From 4e1862d653cf946ce335d8b30e78f3a18b73297f Mon Sep 17 00:00:00 2001 From: Abdullah Atta Date: Sat, 13 May 2023 17:49:10 +0500 Subject: [PATCH] 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 --- apps/web/src/app-effects.js | 4 - apps/web/src/app.js | 3 + apps/web/src/utils/web-extension-relay.ts | 107 +-------------- apps/web/src/utils/web-extension-server.ts | 126 ++++++++++++++++++ extensions/web-clipper/src/api.ts | 55 +++++--- extensions/web-clipper/src/common/bridge.ts | 2 +- .../web-clipper/src/content-scripts/nn.ts | 61 ++++----- 7 files changed, 198 insertions(+), 160 deletions(-) create mode 100644 apps/web/src/utils/web-extension-server.ts diff --git a/apps/web/src/app-effects.js b/apps/web/src/app-effects.js index 345fd3a6b..ca02349df 100644 --- a/apps/web/src/app-effects.js +++ b/apps/web/src/app-effects.js @@ -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 () => { diff --git a/apps/web/src/app.js b/apps/web/src/app.js index 4a5a06913..23b0b1c94 100644 --- a/apps/web/src/app.js +++ b/apps/web/src/app.js @@ -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") diff --git a/apps/web/src/utils/web-extension-relay.ts b/apps/web/src/utils/web-extension-relay.ts index f572aacf0..ed8e808a0 100644 --- a/apps/web/src/utils/web-extension-relay.ts +++ b/apps/web/src/utils/web-extension-relay.ts @@ -19,22 +19,10 @@ along with this program. If not, see . 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; @@ -52,6 +40,8 @@ export class WebExtensionRelay { async connect(): Promise { 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 { - await db.notes?.init(); - - return db.notes?.all - .filter((n) => !n.locked) - .map((note) => ({ id: note.id, title: note.title })); - } - - async getNotebooks(): Promise { - 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 { - 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(); - } -} diff --git a/apps/web/src/utils/web-extension-server.ts b/apps/web/src/utils/web-extension-server.ts new file mode 100644 index 000000000..960b64496 --- /dev/null +++ b/apps/web/src/utils/web-extension-server.ts @@ -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 . +*/ + +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 { + await db.notes?.init(); + + return db.notes?.all + .filter((n) => !n.locked) + .map((note) => ({ id: note.id, title: note.title })); + } + + async getNotebooks(): Promise { + 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 { + 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(); + } +} diff --git a/extensions/web-clipper/src/api.ts b/extensions/web-clipper/src/api.ts index 245f2af64..ea6a16b1d 100644 --- a/extensions/web-clipper/src/api.ts +++ b/extensions/web-clipper/src/api.ts @@ -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 . */ -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 | 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; + } catch (e) { + console.error(e); + } + } - return await new Promise | 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 | 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((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 []; } diff --git a/extensions/web-clipper/src/common/bridge.ts b/extensions/web-clipper/src/common/bridge.ts index efae420aa..1ff0c5537 100644 --- a/extensions/web-clipper/src/common/bridge.ts +++ b/extensions/web-clipper/src/common/bridge.ts @@ -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"; diff --git a/extensions/web-clipper/src/content-scripts/nn.ts b/extensions/web-clipper/src/content-scripts/nn.ts index efe0232ac..822fbcc18 100644 --- a/extensions/web-clipper/src/content-scripts/nn.ts +++ b/extensions/web-clipper/src/content-scripts/nn.ts @@ -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 = wrap(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 = wrap(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 {