mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-02-24 04:00:59 +01:00
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:
committed by
Abdullah Atta
parent
1d5ac37222
commit
4e1862d653
@@ -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 () => {
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
126
apps/web/src/utils/web-extension-server.ts
Normal file
126
apps/web/src/utils/web-extension-server.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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 [];
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user