From 62e37b776663d600f4bbec15e227de341117c6d6 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Mon, 22 May 2023 14:51:53 +0800 Subject: [PATCH 1/7] Add note and menu to the extension --- pkg/extension/package.json | 1 + pkg/extension/src/manifest.json | 2 +- pkg/extension/src/scripts/api.js | 187 +++++++++++++++++++++ pkg/extension/src/scripts/background.js | 92 ++++++++++ pkg/extension/src/scripts/common.js | 4 + pkg/extension/src/scripts/content/toast.js | 164 +++++++++++++++--- pkg/extension/src/views/toast.html | 70 ++++++-- pkg/extension/yarn.lock | 5 + 8 files changed, 483 insertions(+), 42 deletions(-) diff --git a/pkg/extension/package.json b/pkg/extension/package.json index b15b4c790..117dccb31 100644 --- a/pkg/extension/package.json +++ b/pkg/extension/package.json @@ -24,6 +24,7 @@ "webpack-merge": "^5.7.3" }, "dependencies": { + "nanoid": "^4.0.2", "uuid": "^8.3.2" } } diff --git a/pkg/extension/src/manifest.json b/pkg/extension/src/manifest.json index 7fd7219b8..9eccfcce5 100644 --- a/pkg/extension/src/manifest.json +++ b/pkg/extension/src/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 2, "name": "process.env.EXTENSION_NAME", "short_name": "process.env.EXTENSION_NAME", - "version": "2.0.2", + "version": "2.0.8", "description": "Save PDFs and Articles to your Omnivore library", "author": "Omnivore Media, Inc", "default_locale": "en", diff --git a/pkg/extension/src/scripts/api.js b/pkg/extension/src/scripts/api.js index 4f50120e1..2ef615d36 100644 --- a/pkg/extension/src/scripts/api.js +++ b/pkg/extension/src/scripts/api.js @@ -130,3 +130,190 @@ async function setLabels(apiUrl, pageId, labelIds) { } return data.setLabels.labels } + +async function addNote(apiUrl, pageId, noteId, shortId, note) { + const query = JSON.stringify({ + query: `query GetArticle( + $username: String! + $slug: String! + $includeFriendsHighlights: Boolean + ) { + article(username: $username, slug: $slug) { + ... on ArticleSuccess { + article { + highlights(input: { includeFriends: $includeFriendsHighlights }) { + ...HighlightFields + } + } + } + ... on ArticleError { + errorCodes + } + } + } + fragment HighlightFields on Highlight { + id + type + annotation + } + `, + variables: { + username: 'me', + slug: pageId, + includeFriendsHighlights: false, + }, + }) + + const data = await gqlRequest(apiUrl, query) + if (!data.article || data.article['errorCodes'] || !data.article['article']) { + console.log('GQL Error getting existing highlights:', data) + return + } + + const existingNote = data.article.article.highlights.find( + (h) => h.type == 'NOTE' + ) + + if (existingNote) { + const mutation = JSON.stringify({ + query: ` + mutation UpdateHighlight($input: UpdateHighlightInput!) { + updateHighlight(input: $input) { + ... on UpdateHighlightSuccess { + highlight { + id + } + } + ... on UpdateHighlightError { + errorCodes + } + } + } + `, + variables: { + input: { + highlightId: existingNote.id, + annotation: existingNote.annotation + ? existingNote.annotation + '\n\n' + note + : note, + }, + }, + }) + const result = await gqlRequest(apiUrl, mutation) + if ( + !result.updateHighlight || + result.updateHighlight['errorCodes'] || + !result.updateHighlight.highlight + ) { + console.log('GQL Error updating note:', result) + return + } + return result.updateHighlight.highlight.id + } else { + const mutation = JSON.stringify({ + query: ` + mutation CreateHighlight($input: CreateHighlightInput!) { + createHighlight(input: $input) { + ... on CreateHighlightSuccess { + highlight { + id + } + } + ... on CreateHighlightError { + errorCodes + } + } + } + `, + variables: { + input: { + id: noteId, + shortId: shortId, + type: 'NOTE', + articleId: pageId, + annotation: note, + }, + }, + }) + const result = await gqlRequest(apiUrl, mutation) + if ( + !result.createHighlight || + result.createHighlight['errorCodes'] || + !result.createHighlight.highlight + ) { + console.log('GQL Error setting note:', result) + return + } + return result.createHighlight.highlight.id + } +} + +async function archive(apiUrl, pageId) { + const mutation = JSON.stringify({ + query: `mutation SetLinkArchived($input: ArchiveLinkInput!) { + setLinkArchived(input: $input) { + ... on ArchiveLinkSuccess { + linkId + message + } + ... on ArchiveLinkError { + message + errorCodes + } + } + } + `, + variables: { + input: { + linkId: pageId, + archived: true, + }, + }, + }) + + const data = await gqlRequest(apiUrl, mutation) + if ( + !data.setLinkArchived || + data.setLinkArchived['errorCodes'] || + !data.setLinkArchived.linkId + ) { + console.log('GQL Error archiving:', data) + throw new Error('Error archiving.') + } + return data.setLinkArchived.linkId +} + +async function deleteItem(apiUrl, pageId) { + const mutation = JSON.stringify({ + query: `mutation SetBookmarkArticle($input: SetBookmarkArticleInput!) { + setBookmarkArticle(input: $input) { + ... on SetBookmarkArticleSuccess { + bookmarkedArticle { + id + } + } + ... on SetBookmarkArticleError { + errorCodes + } + } + } + `, + variables: { + input: { + articleID: pageId, + bookmark: false, + }, + }, + }) + + const data = await gqlRequest(apiUrl, mutation) + if ( + !data.setBookmarkArticle || + data.setBookmarkArticle['errorCodes'] || + !data.setBookmarkArticle.bookmarkedArticle + ) { + console.log('GQL Error deleting:', data) + throw new Error('Error deleting.') + } + return data.setBookmarkArticle.bookmarkedArticle +} diff --git a/pkg/extension/src/scripts/background.js b/pkg/extension/src/scripts/background.js index efa8ecb2f..2ff4ff892 100644 --- a/pkg/extension/src/scripts/background.js +++ b/pkg/extension/src/scripts/background.js @@ -12,6 +12,7 @@ 'use strict' import { v4 as uuidv4 } from 'uuid' +import { nanoid } from 'nanoid' let authToken = undefined const omnivoreURL = process.env.OMNIVORE_URL @@ -285,6 +286,28 @@ async function editTitleRequest(tabId, request, completedResponse) { }) } +async function addNoteRequest(tabId, request, completedResponse) { + const noteId = uuidv4() + const shortId = nanoid(8) + + return addNote( + omnivoreGraphqlURL + 'graphql', + completedResponse.responseId, + noteId, + shortId, + request.note + ) + .then(() => { + updateClientStatus(tabId, 'note', 'success', 'Note updated.') + return true + }) + .catch((err) => { + console.log('caught error updating title: ', err) + updateClientStatus(tabId, 'note', 'failure', 'Error adding note.') + return true + }) +} + async function setLabelsRequest(tabId, request, completedResponse) { return setLabels( omnivoreGraphqlURL + 'graphql', @@ -301,6 +324,33 @@ async function setLabelsRequest(tabId, request, completedResponse) { }) } +async function archiveRequest(tabId, request, completedResponse) { + return archive(omnivoreGraphqlURL + 'graphql', completedResponse.responseId) + .then(() => { + updateClientStatus(tabId, 'extra', 'success', 'Archived') + return true + }) + .catch(() => { + updateClientStatus(tabId, 'extra', 'failure', 'Error archiving') + return true + }) +} + +async function deleteRequest(tabId, request, completedResponse) { + return deleteItem( + omnivoreGraphqlURL + 'graphql', + completedResponse.responseId + ) + .then(() => { + updateClientStatus(tabId, 'extra', 'success', 'Deleted') + return true + }) + .catch(() => { + updateClientStatus(tabId, 'extra', 'failure', 'Error deleting') + return true + }) +} + async function processPendingRequests(tabId) { const tabRequests = pendingRequests.filter((pr) => pr.tabId === tabId) @@ -312,9 +362,18 @@ async function processPendingRequests(tabId) { case 'EDIT_TITLE': handled = await editTitleRequest(tabId, pr, completed) break + case 'ADD_NOTE': + handled = await addNoteRequest(tabId, pr, completed) + break case 'SET_LABELS': handled = await setLabelsRequest(tabId, pr, completed) break + case 'ARCHIVE': + handled = await archiveRequest(tabId, pr, completed) + break + case 'DELETE': + handled = await deleteRequest(tabId, pr, completed) + break } } @@ -764,6 +823,39 @@ function init() { processPendingRequests(sender.tab.id) } + if (request.action === ACTIONS.Archive) { + pendingRequests.push({ + id: uuidv4(), + type: 'ARCHIVE', + tabId: sender.tab.id, + clientRequestId: request.payload.ctx.requestId, + }) + + processPendingRequests(sender.tab.id) + } + + if (request.action === ACTIONS.Delete) { + pendingRequests.push({ + type: 'DELETE', + tabId: sender.tab.id, + clientRequestId: request.payload.ctx.requestId, + }) + + processPendingRequests(sender.tab.id) + } + + if (request.action === ACTIONS.AddNote) { + pendingRequests.push({ + id: uuidv4(), + type: 'ADD_NOTE', + tabId: sender.tab.id, + note: request.payload.note, + clientRequestId: request.payload.ctx.requestId, + }) + + processPendingRequests(sender.tab.id) + } + if (request.action === ACTIONS.SetLabels) { pendingRequests.push({ id: uuidv4(), diff --git a/pkg/extension/src/scripts/common.js b/pkg/extension/src/scripts/common.js index 763163a13..1f964c49b 100644 --- a/pkg/extension/src/scripts/common.js +++ b/pkg/extension/src/scripts/common.js @@ -35,8 +35,12 @@ window.ACTIONS = { ShowToolbar: 'SHOW_TOOLBAR', UpdateStatus: 'UPDATE_STATUS', + AddNote: 'ADD_NOTE', EditTitle: 'EDIT_TITLE', SetLabels: 'SET_LABELS', + + Archive: 'ARCHIVE', + Delete: 'DELETE', } window.SAVE_URL_QUERY = `mutation SaveUrl ($input: SaveUrlInput!) { diff --git a/pkg/extension/src/scripts/content/toast.js b/pkg/extension/src/scripts/content/toast.js index 370a46724..761a2ac60 100644 --- a/pkg/extension/src/scripts/content/toast.js +++ b/pkg/extension/src/scripts/content/toast.js @@ -76,32 +76,7 @@ document.body.appendChild(root) connectButtons(root) - - return root - } - - async function createCtaModal(url) { - if (currentToastEl) { - currentToastEl.remove() - currentToastEl = undefined - } - - const file = await fetch(browserApi.runtime.getURL('/views/cta-popup.html')) - const html = await file.text() - - const root = document.createElement('div') - root.attachShadow({ mode: 'open' }) - if (root.shadowRoot) { - root.shadowRoot.innerHTML = `` - } - - const toastEl = document.createElement('div') - toastEl.id = '#omnivore-toast' - toastEl.innerHTML = html - root.shadowRoot.appendChild(toastEl) - - document.body.appendChild(root) - connectButtons(root) + // connectKeyboard(root) return root } @@ -123,6 +98,8 @@ '#omnivore-toast-edit-title-btn', '#omnivore-toast-edit-labels-btn', '#omnivore-toast-read-now-btn', + '#omnivore-toast-add-note-btn', + '#omnivore-open-menu-btn', ] actionButtons.forEach((btnId) => { const btn = currentToastEl.shadowRoot.querySelector(btnId) @@ -152,6 +129,19 @@ case 'page': updatePageStatus(payload.status) break + case 'note': + updateStatusBox( + '#omnivore-add-note-status', + payload.status, + payload.message, + payload.status == 'success' ? 2500 : undefined + ) + if (payload.status == 'success') { + setTimeout(() => { + toggleRow('#omnivore-add-note-status') + }, 3000) + } + break case 'title': updateStatusBox( '#omnivore-edit-title-status', @@ -159,6 +149,11 @@ payload.message, payload.status == 'success' ? 2500 : undefined ) + if (payload.status == 'success') { + setTimeout(() => { + toggleRow('#omnivore-edit-title-status') + }, 3000) + } break case 'labels': updateStatusBox( @@ -168,6 +163,19 @@ payload.status == 'success' ? 2500 : undefined ) break + case 'extra': + updateStatusBox( + '#omnivore-extra-status', + payload.status, + payload.message, + payload.status == 'success' ? 2500 : undefined + ) + if (payload.status == 'success') { + setTimeout(() => { + currentToastEl.remove() + }, 3000) + } + break } } @@ -259,12 +267,13 @@ } if (dismissAfter) { setTimeout(() => { - statusBox.innerHTML = null + statusBox.innerHTML = '' }, dismissAfter) } } function toggleRow(rowId) { + console.log('currentToastEl: ', currentToastEl) const container = currentToastEl.shadowRoot.querySelector(rowId) const initialState = container?.getAttribute('data-state') const rows = currentToastEl.shadowRoot.querySelectorAll( @@ -283,12 +292,15 @@ function connectButtons(root) { const btns = [ + { id: '#omnivore-toast-add-note-btn', func: addNote }, { id: '#omnivore-toast-edit-title-btn', func: editTitle }, { id: '#omnivore-toast-edit-labels-btn', func: editLabels }, { id: '#omnivore-toast-read-now-btn', func: readNow }, { id: '#omnivore-open-menu-btn', func: openMenu }, { id: '#omnivore-toast-close-btn', func: closeToast }, { id: '#omnivore-toast-login-btn', func: login }, + { id: '#omnivore-toast-archive-btn', func: archive }, + { id: '#omnivore-toast-delete-btn', func: deleteItem }, ] for (const btnInfo of btns) { @@ -297,6 +309,53 @@ btn.addEventListener('click', btnInfo.func) } } + + var x = window.matchMedia('(max-width: 500px)') + if (x.matches) { + const labels = root.shadowRoot.querySelectorAll( + '.omnivore-top-button-label' + ) + labels.forEach((label) => { + console.log(' - hiding label', label) + label.style.display = 'none' + }) + const container = root.shadowRoot.querySelector( + '#omnivore-toast-container' + ) + container.style.width = '280px' + } + + root.shadowRoot.focus() + } + + function connectKeyboard(root) { + console.log('connecting keyboard') + root.addEventListener('keydown', (e) => { + console.log( + 'root.addEventListener document code: ', + e.key, + 'activeElement:', + document.activeElement + ) + + switch (e.key) { + case 'r': + readNow() + break + case 'l': + editLabels() + break + case 'm': + openMenu() + break + case 'i': + editTitle() + break + case 't': + addNote() + break + } + }) } function createLabelRow(label, idx) { @@ -424,7 +483,34 @@ } } + function addNote() { + cancelAutoDismiss() + toggleRow('#omnivore-add-note-row') + currentToastEl.shadowRoot + .querySelector('#omnivore-add-note-textarea') + ?.focus() + + currentToastEl.shadowRoot.querySelector( + '#omnivore-add-note-form' + ).onsubmit = (event) => { + console.log('submitting form: ', event) + updateStatusBox('#omnivore-add-note-status', 'loading', 'Adding note...') + + browserApi.runtime.sendMessage({ + action: ACTIONS.AddNote, + payload: { + ctx: ctx, + note: event.target.elements.title.value, + }, + }) + + event.preventDefault() + } + } + function editTitle() { + console.log('editing title') + cancelAutoDismiss() toggleRow('#omnivore-edit-title-row') currentToastEl.shadowRoot @@ -551,9 +637,31 @@ }, 1000) } + function archive(event) { + browserApi.runtime.sendMessage({ + action: ACTIONS.Archive, + payload: { + ctx: ctx, + }, + }) + + event.preventDefault() + } + + function deleteItem(event) { + browserApi.runtime.sendMessage({ + action: ACTIONS.Delete, + payload: { + ctx: ctx, + }, + }) + + event.preventDefault() + } + function openMenu() { cancelAutoDismiss() - toggleRow('omnivore-extra-buttons-row') + toggleRow('#omnivore-extra-buttons-row') } function closeToast() { diff --git a/pkg/extension/src/views/toast.html b/pkg/extension/src/views/toast.html index d8664247c..d406e34bb 100644 --- a/pkg/extension/src/views/toast.html +++ b/pkg/extension/src/views/toast.html @@ -18,7 +18,7 @@ box-shadow: 0px 5px 20px rgba(32, 31, 29, 0.12); transition: all 300ms ease; z-index: 9999999; - width: 455px; + width: 480px; } #omnivore-toast-container .omnivore-toast-func-row { @@ -30,6 +30,14 @@ padding-bottom: 10px; } + #omnivore-toast-container #omnivore-extra-buttons-row { + padding-left: 5px; + } + + #omnivore-toast-container #omnivore-extra-buttons-row button { + padding-left: 10px; + } + #omnivore-toast-container #omnivore-logged-out-row { flex-direction: column; align-items: center; @@ -119,6 +127,28 @@ #omnivore-edit-title-row textarea { width: 100%; height: 100px; + padding: 5px; + padding-top: 10px; + padding-bottom: 10px; + resize: none; + border: 1px solid #8E8E93; + border-radius: 4px; + box-sizing: border-box; + background-color: transparent; + } + + #omnivore-add-note-row { + flex-direction: column; + visibility: unset; + padding-top: 20px; + height: 100%; + gap: 10px; + } + + #omnivore-add-note-row textarea { + width: 100%; + height: 100px; + padding: 5px; padding-top: 10px; padding-bottom: 10px; resize: none; @@ -161,7 +191,6 @@ } #omnivore-toast-container #omnivore-extra-buttons-row button { - width: 80%; align-self: flex-start; padding: 10px; margin: 0px; @@ -246,11 +275,11 @@ - @@ -259,7 +288,7 @@ - Set Labels + Set Labels @@ -267,10 +296,10 @@ - Read Now + Read Now - + + +
@@ -310,7 +346,15 @@
- + + -