diff --git a/src/background.ts b/src/background.ts index 4abfe7f1..6f3d8b4c 100644 --- a/src/background.ts +++ b/src/background.ts @@ -1,6 +1,9 @@ import setId from "background/init/set-id"; import runMigrations from "background/init/migrations/run-migrations"; -import { showNewVersionNotification } from "background/init/notifications"; +import { + showNewVersionNotification, + attachNotificationsOnClicked, +} from "background/init/notifications"; import { openMyNotesOnIconClick, @@ -41,6 +44,9 @@ saveTextOnRemoteTransfer(); // when you use "Save to remotely open My Notes" fro // Permissions handleChangedPermissions(); // react to granted/removed optional permissions: "identity" +// Notifications +attachNotificationsOnClicked(); + // Run when Installed or Updated chrome.runtime.onInstalled.addListener((details) => { setId(); // Set unique My Notes ID, if not set before diff --git a/src/background/init/context-menu/__tests__/get-text-to-save.test.ts b/src/background/init/context-menu/__tests__/get-text-to-save.test.ts new file mode 100644 index 00000000..37578f5a --- /dev/null +++ b/src/background/init/context-menu/__tests__/get-text-to-save.test.ts @@ -0,0 +1,41 @@ +import getTextToSave from "../get-text-to-save"; + +const irrelevant: Pick = { + menuItemId: "", + editable: true, +}; + +describe("getTextToSave()", () => { + describe("page context", () => { + it("returns url to save", () => { + expect(getTextToSave("page", { + ...irrelevant, + pageUrl: "https://github.com/penge/my-notes", + })).toBe('https://github.com/penge/my-notes' + + "

"); + }); + }); + + describe("image context", () => { + it("returns img to save", () => { + expect(getTextToSave("image", { + ...irrelevant, + pageUrl: "https://domain.com", + srcUrl: "https://domain.com/image.png", + })).toBe('' + + "

"); + }); + }); + + describe("selection context", () => { + it("returns selection to save", () => { + expect(getTextToSave("selection", { + ...irrelevant, + pageUrl: "https://articles.com/good-article", + selectionText: "Text selected from the article...", + })).toBe("Text selected from the article...
" + + '(https://articles.com/good-article)' + + "

"); + }); + }); +}); diff --git a/src/background/init/context-menu/__tests__/is-note-locked.test.ts b/src/background/init/context-menu/__tests__/is-note-locked.test.ts new file mode 100644 index 00000000..ae75fd37 --- /dev/null +++ b/src/background/init/context-menu/__tests__/is-note-locked.test.ts @@ -0,0 +1,20 @@ +import { NotesObject, Note } from "shared/storage/schema"; +import isNoteLocked from "../is-note-locked"; + +const dummyNote: Note = { + content: "", + createdTime: "", + modifiedTime: "", +}; + +const notes: NotesObject = { + "@clipboard": { ...dummyNote, locked: false }, + "@images": { ...dummyNote, locked: true }, + Todo: { ...dummyNote, locked: undefined }, +}; + +test("isNoteLocked()", () => { + expect(isNoteLocked(notes, "@clipboard")).toBe(false); + expect(isNoteLocked(notes, "@images")).toBe(true); + expect(isNoteLocked(notes, "Todo")).toBe(false); +}); diff --git a/src/background/init/context-menu/__tests__/split-id.test.ts b/src/background/init/context-menu/__tests__/split-id.test.ts new file mode 100644 index 00000000..03050940 --- /dev/null +++ b/src/background/init/context-menu/__tests__/split-id.test.ts @@ -0,0 +1,41 @@ +import splitId, { SplitIdResult } from "../split-id"; + +type T = SplitIdResult; + +test("splitId()", () => { + expect(splitId("page-note-@clipboard")).toEqual({ + context: "page", + destination: "note", + noteName: "@clipboard", + }); + + expect(splitId("page-note-Todo")).toEqual({ + context: "page", + destination: "note", + noteName: "Todo", + }); + + expect(splitId("page-note-Tables and Chairs")).toEqual({ + context: "page", + destination: "note", + noteName: "Tables and Chairs", + }); + + expect(splitId("page-note-npm-packages")).toEqual({ + context: "page", + destination: "note", + noteName: "npm-packages", + }); + + expect(splitId("image-note-@images")).toEqual({ + context: "image", + destination: "note", + noteName: "@images", + }); + + expect(splitId("selection-remote")).toEqual({ + context: "selection", + destination: "remote", + noteName: "", + }); +}); diff --git a/src/background/init/context-menu/get-text-to-save.ts b/src/background/init/context-menu/get-text-to-save.ts index 5e3fe3b7..d675f45a 100644 --- a/src/background/init/context-menu/get-text-to-save.ts +++ b/src/background/init/context-menu/get-text-to-save.ts @@ -9,6 +9,12 @@ const getUrlToSave: Handler = (info) => { return toSave; }; +const getImageToSave: Handler = (info) => { + const { srcUrl } = info; + const toSave = `

`; + return toSave; +}; + const getSelectionToSave: Handler = (info) => { const { pageUrl, selectionText } = info; const pageUrlHtml = getPageUrlHtml(pageUrl); @@ -18,6 +24,7 @@ const getSelectionToSave: Handler = (info) => { const handlers: Partial> = { page: getUrlToSave, + image: getImageToSave, selection: getSelectionToSave, }; diff --git a/src/background/init/context-menu/index.ts b/src/background/init/context-menu/index.ts index 8aee64bc..56d807d2 100644 --- a/src/background/init/context-menu/index.ts +++ b/src/background/init/context-menu/index.ts @@ -1,22 +1,17 @@ import { NotesObject } from "shared/storage/schema"; import { tString } from "i18n"; +import isNoteLocked from "./is-note-locked"; +import splitId from "./split-id"; import getTextToSave from "./get-text-to-save"; import { - CLIPBOARD_NOTE_NAME, saveTextToLocalMyNotes, saveTextToRemotelyOpenMyNotes, } from "../saving"; import { notify } from "../notifications"; +import { CLIPBOARD_NOTE_NAME, IMAGES_NOTE_NAME } from "../reserved-note-names"; const ID = "my-notes"; - -const PAGE_NOTE_PREFIX = "page-note-"; -const PAGE_REMOTE_MY_NOTES = "page-remote-my-notes"; - -const SELECTION_NOTE_PREFIX = "selection-note-"; -const SELECTION_REMOTE_MY_NOTES = "selection-remote-my-notes"; - -const isLocked = (notes: NotesObject, noteName: string): boolean => !!(notes[noteName]?.locked); +const contexts: chrome.contextMenus.ContextType[] = ["page", "image", "selection"]; /** * Creates My Notes Context menu @@ -29,135 +24,82 @@ const isLocked = (notes: NotesObject, noteName: string): boolean => !!(notes[not const createContextMenu = (notes: NotesObject): string | number => chrome.contextMenus.create({ id: ID, title: "My Notes", - contexts: ["page", "selection"], -}, () => { - const forEveryNoteExceptClipboard = (callback: (noteName: string) => void) => { - Object.keys(notes).filter((noteName) => noteName !== CLIPBOARD_NOTE_NAME).sort().forEach(callback); - }; - - /* -------------< page >------------- */ - - const pageCommonProperties: chrome.contextMenus.CreateProperties = { + contexts, +}, () => contexts.forEach((context) => { + const reservedNoteNames = [ + CLIPBOARD_NOTE_NAME, + context === "image" ? IMAGES_NOTE_NAME : "", + ].filter(Boolean); + + const nonReservedNoteNames = Object.keys(notes) + .filter((noteName) => !reservedNoteNames.includes(noteName)) + .sort(); + + const commonProperties: chrome.contextMenus.CreateProperties = { parentId: ID, - contexts: ["page"], + contexts: [context], }; - chrome.contextMenus.create({ - ...pageCommonProperties, - id: [PAGE_NOTE_PREFIX, CLIPBOARD_NOTE_NAME].join(""), - title: tString("Context Menu.menus.Save URL to", { note: CLIPBOARD_NOTE_NAME }), - enabled: !isLocked(notes, CLIPBOARD_NOTE_NAME), + const noteProperties = (noteName: string): chrome.contextMenus.CreateProperties => ({ + ...commonProperties, + id: `${context}-note-${noteName}`, + title: tString(`Context Menu.${context}-note.title`, { note: noteName }), + enabled: !isNoteLocked(notes, noteName), }); - chrome.contextMenus.create({ - ...pageCommonProperties, + const separatorProperties = (suffix: string): chrome.contextMenus.CreateProperties => ({ + ...commonProperties, type: "separator", - id: "page-separator-one", + id: `${context}-separator-${suffix}`, }); - forEveryNoteExceptClipboard((noteName) => { - chrome.contextMenus.create({ - ...pageCommonProperties, - id: [PAGE_NOTE_PREFIX, noteName].join(""), - title: tString("Context Menu.menus.Save URL to", { note: noteName }), - enabled: !isLocked(notes, noteName), - }); + reservedNoteNames.forEach((noteName) => { + chrome.contextMenus.create(noteProperties(noteName)); }); - chrome.contextMenus.create({ - ...pageCommonProperties, - type: "separator", - id: "page-separator-two", - }); + chrome.contextMenus.create(separatorProperties("first")); - chrome.contextMenus.create({ - ...pageCommonProperties, - id: PAGE_REMOTE_MY_NOTES, - title: tString("Context Menu.menus.Save URL to remotely open My Notes"), + nonReservedNoteNames.forEach((noteName) => { + chrome.contextMenus.create(noteProperties(noteName)); }); - /* -------------< selection >------------- */ - - const selectionCommonProperties: chrome.contextMenus.CreateProperties = { - parentId: ID, - contexts: ["selection"], - }; - - chrome.contextMenus.create({ - ...selectionCommonProperties, - id: [SELECTION_NOTE_PREFIX, CLIPBOARD_NOTE_NAME].join(""), - title: tString("Context Menu.menus.Save to", { note: CLIPBOARD_NOTE_NAME }), - enabled: !isLocked(notes, CLIPBOARD_NOTE_NAME), - }); + chrome.contextMenus.create(separatorProperties("second")); chrome.contextMenus.create({ - ...selectionCommonProperties, - type: "separator", - id: "selection-separator-one", - }); - - forEveryNoteExceptClipboard((noteName) => { - chrome.contextMenus.create({ - ...selectionCommonProperties, - id: [SELECTION_NOTE_PREFIX, noteName].join(""), - title: tString("Context Menu.menus.Save to", { note: noteName }), - enabled: !isLocked(notes, noteName), - }); + ...commonProperties, + id: `${context}-remote`, + title: tString(`Context Menu.${context}-remote.title`), }); - - chrome.contextMenus.create({ - ...selectionCommonProperties, - type: "separator", - id: "selection-separator-two", - }); - - chrome.contextMenus.create({ - ...selectionCommonProperties, - id: SELECTION_REMOTE_MY_NOTES, - title: tString("Context Menu.menus.Save to remotely open My Notes"), - }); -}); - -let currentNotesString: string; +})); export const attachContextMenuOnClicked = (): void => chrome.contextMenus.onClicked.addListener((info) => { - const menuId: string = info.menuItemId.toString(); - const context = menuId.split("-")[0] as chrome.contextMenus.ContextType; + const { context, destination, noteName } = splitId(info.menuItemId.toString()); + const tKey = `Context Menu.${context}-${destination}.notification`; const textToSave = getTextToSave(context, info); if (!textToSave) { return; } - /* -------------< page >------------- */ - - if (menuId.startsWith(PAGE_NOTE_PREFIX)) { - const noteName = menuId.replace(PAGE_NOTE_PREFIX, ""); + if (destination === "note" && noteName) { saveTextToLocalMyNotes(textToSave, noteName); - notify(tString("Context Menu.notifications.Saved URL to", { note: noteName })); - return; - } - - if (menuId === PAGE_REMOTE_MY_NOTES) { - saveTextToRemotelyOpenMyNotes(textToSave); - notify(tString("Context Menu.notifications.Sent URL to remotely open My Notes")); - return; - } - - /* -------------< selection >------------- */ - - if (menuId.startsWith(SELECTION_NOTE_PREFIX)) { - const noteName = menuId.replace(SELECTION_NOTE_PREFIX, ""); - saveTextToLocalMyNotes(textToSave, noteName); - notify(tString("Context Menu.notifications.Saved text to", { note: noteName })); + notify({ + notificationId: `note-${new Date().getTime()}-${noteName}`, + message: tString(tKey, { note: noteName }), + }); return; } - if (menuId === SELECTION_REMOTE_MY_NOTES) { + if (destination === "remote") { saveTextToRemotelyOpenMyNotes(textToSave); - notify(tString("Context Menu.notifications.Sent text to remotely open My Notes")); + notify({ + notificationId: `remote-${new Date().getTime()}`, + message: tString(tKey), + }); } }); +let currentNotesString: string; + const recreateContextMenuFromNotes = (notes: NotesObject | undefined): void => { if (!notes) { currentNotesString = ""; diff --git a/src/background/init/context-menu/is-note-locked.ts b/src/background/init/context-menu/is-note-locked.ts new file mode 100644 index 00000000..ea2cc735 --- /dev/null +++ b/src/background/init/context-menu/is-note-locked.ts @@ -0,0 +1,3 @@ +import { NotesObject } from "shared/storage/schema"; + +export default (notes: NotesObject, noteName: string): boolean => !!(notes[noteName]?.locked); diff --git a/src/background/init/context-menu/split-id.ts b/src/background/init/context-menu/split-id.ts new file mode 100644 index 00000000..6f1879e2 --- /dev/null +++ b/src/background/init/context-menu/split-id.ts @@ -0,0 +1,14 @@ +export type SplitIdResult = { + context: C + destination: D + noteName: string +}; + +export default (id: string): SplitIdResult => { + const [context, destination, ...noteNameParts] = id.split("-") as [C, D, string[]]; + return { + context, + destination, + noteName: noteNameParts.join("-"), + }; +}; diff --git a/src/background/init/notifications.ts b/src/background/init/notifications.ts index 7fdf3d4b..ab0f8561 100644 --- a/src/background/init/notifications.ts +++ b/src/background/init/notifications.ts @@ -1,4 +1,5 @@ import { Notification, NotificationType } from "shared/storage/schema"; +import splitId from "./context-menu/split-id"; // Shows a notification when a new version of My Notes is installed export const showNewVersionNotification = (details: chrome.runtime.InstalledDetails): void => { @@ -15,9 +16,30 @@ export const showNewVersionNotification = (details: chrome.runtime.InstalledDeta }; // Shows a Chrome notification in the top-right corner -export const notify = (message: string) => chrome.notifications.create({ +export const notify = ({ + notificationId, + message, +}: { + notificationId: string, + message: string, +}) => chrome.notifications.create(notificationId, { type: "basic", title: "My Notes", message, iconUrl: "images/icon128.png", }); + +export const attachNotificationsOnClicked = () => { + chrome.notifications.onClicked.addListener((notificationId) => { + const { context, noteName } = splitId(notificationId); + if (context === "note" && noteName) { + chrome.storage.local.get(["notes"], (local) => { + if (noteName in local.notes) { + chrome.tabs.create({ + url: `notes.html?note=${noteName}`, + }); + } + }); + } + }); +}; diff --git a/src/background/init/reserved-note-names.ts b/src/background/init/reserved-note-names.ts new file mode 100644 index 00000000..b803dbc6 --- /dev/null +++ b/src/background/init/reserved-note-names.ts @@ -0,0 +1,3 @@ +export const CLIPBOARD_NOTE_NAME = "@clipboard"; +export const IMAGES_NOTE_NAME = "@images"; +export const RECEIVED_NOTE_NAME = "@received"; diff --git a/src/background/init/saving.ts b/src/background/init/saving.ts index f45f69a3..66bfc275 100644 --- a/src/background/init/saving.ts +++ b/src/background/init/saving.ts @@ -4,9 +4,7 @@ import { MessageType, Message, } from "shared/storage/schema"; - -export const CLIPBOARD_NOTE_NAME = "@clipboard"; -const RECEIVED_NOTE_NAME = "@received"; +import { RECEIVED_NOTE_NAME } from "./reserved-note-names"; export const saveTextToLocalMyNotes = (textToSave: string, noteName: string): void => { chrome.storage.local.get(["notes"], (local) => { diff --git a/src/i18n/en.json b/src/i18n/en.json index 98b94781..4eecf561 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -216,17 +216,29 @@ "Created by": "Created by", "Buy me a coffee": "Buy me a coffee", "Context Menu": { - "menus": { - "Save URL to": "Save URL to {{note}}", - "Save URL to remotely open My Notes": "Save URL to remotely open My Notes", - "Save to": "Save to {{note}}", - "Save to remotely open My Notes": "Save to remotely open My Notes" + "page-note": { + "title": "Save URL to {{note}}", + "notification": "Saved URL to {{note}}" }, - "notifications": { - "Saved URL to": "Saved URL to {{note}}", - "Sent URL to remotely open My Notes": "Sent URL to remotely open My Notes", - "Saved text to": "Saved text to {{note}}", - "Sent text to remotely open My Notes": "Sent text to remotely open My Notes" + "page-remote": { + "title": "Save URL to remotely open My Notes", + "notification": "Sent URL to remotely open My Notes" + }, + "image-note": { + "title": "Save image to {{note}}", + "notification": "Saved image to {{note}}" + }, + "image-remote": { + "title": "Save image to remotely open My Notes", + "notification": "Sent image to remotely open My Notes" + }, + "selection-note": { + "title": "Save to {{note}}", + "notification": "Saved text to {{note}}" + }, + "selection-remote": { + "title": "Save to remotely open My Notes", + "notification": "Sent text to remotely open My Notes" } }, "Notifications": {