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 {