clipper: improve connection reliability

this adds support for working with multiple Notesnook tabs
even though it is not currently recommended that you
open multiple Notesnook tabs
This commit is contained in:
Abdullah Atta
2023-05-13 17:49:10 +05:00
committed by Abdullah Atta
parent 1d5ac37222
commit 4e1862d653
7 changed files with 198 additions and 160 deletions

View File

@@ -45,11 +45,8 @@ import { isTesting } from "./utils/platform";
import { updateStatus, removeStatus, getStatus } from "./hooks/use-status";
import { showToast } from "./utils/toast";
import { interruptedOnboarding } from "./components/dialogs/onboarding-dialog";
import { WebExtensionRelay } from "./utils/web-extension-relay";
import { hashNavigate } from "./navigation";
const relay = new WebExtensionRelay();
export default function AppEffects({ setShow }) {
const refreshNavItems = useStore((store) => store.refreshNavItems);
const updateLastSynced = useStore((store) => store.updateLastSynced);
@@ -103,7 +100,6 @@ export default function AppEffects({ setShow }) {
await showOnboardingDialog(interruptedOnboarding());
await showFeatureDialog("highlights");
await scheduleBackups();
relay.connect();
})();
return () => {

View File

@@ -35,6 +35,9 @@ import StatusBar from "./components/status-bar";
import { EditorLoader } from "./components/loaders/editor-loader";
import { FlexScrollContainer } from "./components/scroll-container";
import CachedRouter from "./components/cached-router";
import { WebExtensionRelay } from "./utils/web-extension-relay";
new WebExtensionRelay();
const GlobalMenuWrapper = React.lazy(() =>
import("./components/global-menu-wrapper")

View File

@@ -19,22 +19,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { expose, Remote, wrap } from "comlink";
import { updateStatus } from "../hooks/use-status";
import { db } from "../common/db";
import {
Gateway,
ItemReference,
NotebookReference,
Server,
Clip,
WEB_EXTENSION_CHANNEL_EVENTS
} from "@notesnook/web-clipper/dist/common/bridge";
import { isUserPremium } from "../hooks/use-is-user-premium";
import { store as themestore } from "../stores/theme-store";
import { store as appstore } from "../stores/app-store";
import { formatDate } from "@notesnook/core/utils/date";
import { h } from "./html";
import { sanitizeFilename } from "./filename";
import { attachFile } from "../components/editor/picker";
export class WebExtensionRelay {
private gateway?: Remote<Gateway>;
@@ -52,6 +40,8 @@ export class WebExtensionRelay {
async connect(): Promise<boolean> {
if (this.gateway) return true;
const { WebExtensionServer } = await import("./web-extension-server");
const channel = new MessageChannel();
channel.port1.start();
channel.port2.start();
@@ -76,96 +66,3 @@ export class WebExtensionRelay {
return true;
}
}
class WebExtensionServer implements Server {
async login() {
const user = await db.user?.getUser();
if (!user) return null;
const { theme, accent } = themestore.get();
return { email: user.email, pro: isUserPremium(user), accent, theme };
}
async getNotes(): Promise<ItemReference[] | undefined> {
await db.notes?.init();
return db.notes?.all
.filter((n) => !n.locked)
.map((note) => ({ id: note.id, title: note.title }));
}
async getNotebooks(): Promise<NotebookReference[] | undefined> {
return db.notebooks?.all.map((nb) => ({
id: nb.id,
title: nb.title,
topics: nb.topics.map((topic: ItemReference) => ({
id: topic.id,
title: topic.title
}))
}));
}
async getTags(): Promise<ItemReference[] | undefined> {
return db.tags?.all.map((tag) => ({
id: tag.id,
title: tag.title
}));
}
async saveClip(clip: Clip) {
let clipContent = "";
if (clip.mode === "simplified" || clip.mode === "screenshot") {
clipContent += clip.data;
} else {
const clippedFile = new File(
[new TextEncoder().encode(clip.data).buffer],
`${sanitizeFilename(clip.title)}.clip`,
{
type: "application/vnd.notesnook.web-clip"
}
);
const attachment = await attachFile(clippedFile);
if (!attachment) return;
clipContent += h("iframe", [], {
"data-hash": attachment.hash,
"data-mime": attachment.type,
src: clip.url,
title: clip.pageTitle || clip.title,
width: clip.width ? `${clip.width}` : undefined,
height: clip.height ? `${clip.height}` : undefined,
class: "web-clip"
}).outerHTML;
}
const note =
(clip.note?.id && db.notes?.note(clip.note?.id)) ||
db.notes?.note(await db.notes?.add({ title: clip.title }));
let content = (await note?.content()) || "";
content += clipContent;
content += h("div", [
h("hr"),
h("p", ["Clipped from ", h("a", [clip.title], { href: clip.url })]),
h("p", [`Date clipped: ${formatDate(Date.now())}`])
]).innerHTML;
await db.notes?.add({
id: note?.id,
content: { type: "tiptap", data: content }
});
if (clip.notebook && note) {
await db.notes?.addToNotebook(
{ id: clip.notebook.id, topic: clip.notebook.topic.id },
note.id
);
}
if (clip.tags && note) {
for (const tag of clip.tags) {
await db.tags?.add(tag, note.id);
}
}
await appstore.refresh();
}
}

View File

@@ -0,0 +1,126 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { db } from "../common/db";
import {
ItemReference,
NotebookReference,
Server,
Clip
} from "@notesnook/web-clipper/dist/common/bridge";
import { isUserPremium } from "../hooks/use-is-user-premium";
import { store as themestore } from "../stores/theme-store";
import { store as appstore } from "../stores/app-store";
import { formatDate } from "@notesnook/core/utils/date";
import { h } from "./html";
import { sanitizeFilename } from "./filename";
import { attachFile } from "../components/editor/picker";
export class WebExtensionServer implements Server {
async login() {
const user = await db.user?.getUser();
const { theme, accent } = themestore.get();
if (!user) return { pro: false, theme, accent };
return { email: user.email, pro: isUserPremium(user), theme, accent };
}
async getNotes(): Promise<ItemReference[] | undefined> {
await db.notes?.init();
return db.notes?.all
.filter((n) => !n.locked)
.map((note) => ({ id: note.id, title: note.title }));
}
async getNotebooks(): Promise<NotebookReference[] | undefined> {
return db.notebooks?.all.map((nb) => ({
id: nb.id,
title: nb.title,
topics: nb.topics.map((topic: ItemReference) => ({
id: topic.id,
title: topic.title
}))
}));
}
async getTags(): Promise<ItemReference[] | undefined> {
return db.tags?.all.map((tag) => ({
id: tag.id,
title: tag.title
}));
}
async saveClip(clip: Clip) {
let clipContent = "";
if (clip.mode === "simplified" || clip.mode === "screenshot") {
clipContent += clip.data;
} else {
const clippedFile = new File(
[new TextEncoder().encode(clip.data).buffer],
`${sanitizeFilename(clip.title)}.clip`,
{
type: "application/vnd.notesnook.web-clip"
}
);
const attachment = await attachFile(clippedFile);
if (!attachment) return;
clipContent += h("iframe", [], {
"data-hash": attachment.hash,
"data-mime": attachment.type,
src: clip.url,
title: clip.pageTitle || clip.title,
width: clip.width ? `${clip.width}` : undefined,
height: clip.height ? `${clip.height}` : undefined,
class: "web-clip"
}).outerHTML;
}
const note =
(clip.note?.id && db.notes?.note(clip.note?.id)) ||
db.notes?.note(await db.notes?.add({ title: clip.title }));
let content = (await note?.content()) || "";
content += clipContent;
content += h("div", [
h("hr"),
h("p", ["Clipped from ", h("a", [clip.title], { href: clip.url })]),
h("p", [`Date clipped: ${formatDate(Date.now())}`])
]).innerHTML;
await db.notes?.add({
id: note?.id,
content: { type: "tiptap", data: content }
});
if (clip.notebook && note) {
await db.notes?.addToNotebook(
{ id: clip.notebook.id, topic: clip.notebook.topic.id },
note.id
);
}
if (clip.tags && note) {
for (const tag of clip.tags) {
await db.tags?.add(tag, note.id);
}
}
await appstore.refresh();
}
}

View File

@@ -16,7 +16,7 @@ GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { browser, Runtime } from "webextension-polyfill-ts";
import { browser, Runtime, Tabs } from "webextension-polyfill-ts";
import { Remote, wrap } from "comlink";
import { createEndpoint } from "./utils/comlink-extension";
import { Server } from "./common/bridge";
@@ -28,14 +28,32 @@ let api: Remote<Server> | undefined;
export async function connectApi(openNew = false, onDisconnect?: () => void) {
if (api) return api;
const tab = await getTab(openNew);
if (!tab) return false;
const tabs = await findNotesnookTabs(openNew);
for (const tab of tabs) {
try {
const api = await Promise.race([
connectToTab(tab, onDisconnect),
timeout(5000)
]);
if (!api) continue;
return api as Remote<Server>;
} catch (e) {
console.error(e);
}
}
return await new Promise<Remote<Server> | false>(function connect(resolve) {
return false;
}
function timeout(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms, false));
}
function connectToTab(tab: Tabs.Tab, onDisconnect?: () => void) {
return new Promise<Remote<Server> | false>(function connect(resolve) {
if (!tab.id) return resolve(false);
const port = browser.tabs.connect(tab.id);
port.onDisconnect.addListener(() => {
api = undefined;
onDisconnect?.();
@@ -58,23 +76,26 @@ export async function connectApi(openNew = false, onDisconnect?: () => void) {
});
}
async function getTab(openNew = false) {
export async function findNotesnookTabs(openNew = false) {
const tabs = await browser.tabs.query({
url: APP_URL_FILTER
url: APP_URL_FILTER,
discarded: false,
status: "complete"
});
if (tabs.length) return tabs[0];
if (tabs.length) return tabs;
if (openNew) {
const [tab] = await Promise.all([
browser.tabs.create({ url: APP_URL, active: false }),
new Promise((resolve) => {
browser.runtime.onMessage.addListener((message) => {
if (message.type === "start_connection") resolve(true);
});
const tab = await browser.tabs.create({ url: APP_URL, active: false });
await new Promise<void>((resolve) =>
browser.tabs.onUpdated.addListener(function onUpdated(id, info) {
if (id === tab.id && info.status === "complete") {
browser.tabs.onUpdated.removeListener(onUpdated);
resolve();
}
})
]);
return tab;
);
return [tab];
}
return undefined;
return [];
}

View File

@@ -22,7 +22,7 @@ export type ClipArea = "full-page" | "visible" | "selection" | "article";
export type ClipMode = "simplified" | "screenshot" | "complete";
export type User = {
email: string;
email?: string;
pro: boolean;
accent: string;
theme: "dark" | "light";

View File

@@ -26,43 +26,38 @@ import {
WEB_EXTENSION_CHANNEL_EVENTS
} from "../common/bridge";
let mainPort: MessagePort | undefined;
browser.runtime.onConnect.addListener((port) => {
if (mainPort) {
const server: Remote<Server> = wrap<Server>(mainPort);
expose(
{
login: () => server.login(),
getNotes: () => server.getNotes(),
getNotebooks: () => server.getNotebooks(),
getTags: () => server.getTags(),
saveClip: (clip: Clip) => server.saveClip(clip)
},
createEndpoint(port)
);
port.postMessage({ success: true });
} else {
port.postMessage({ success: false });
}
});
window.addEventListener("message", (ev) => {
const { type } = ev.data;
switch (type) {
case WEB_EXTENSION_CHANNEL_EVENTS.ON_CREATED:
if (ev.ports.length) {
const mainPort = ev.ports.at(0);
if (mainPort) {
expose(new BackgroundGateway(), mainPort);
const server: Remote<Server> = wrap<Server>(mainPort);
expose(
{
login: () => server.login(),
getNotes: () => server.getNotes(),
getNotebooks: () => server.getNotebooks(),
getTags: () => server.getTags(),
saveClip: (clip: Clip) => server.saveClip(clip)
},
createEndpoint(port)
);
port.postMessage({ success: true });
} else {
port.postMessage({ success: false });
}
}
break;
}
});
window.addEventListener("message", (ev) => {
const { type } = ev.data;
switch (type) {
case WEB_EXTENSION_CHANNEL_EVENTS.ON_CREATED:
if (ev.ports.length) {
const [port] = ev.ports;
mainPort = port;
expose(new BackgroundGateway(), port);
browser.runtime.sendMessage(undefined, { type: "start_connection" });
}
break;
}
window.postMessage({ type: WEB_EXTENSION_CHANNEL_EVENTS.ON_READY }, "*");
});
window.postMessage({ type: WEB_EXTENSION_CHANNEL_EVENTS.ON_READY }, "*");
class BackgroundGateway implements Gateway {
connect() {
return {