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/images/toolbar/icon_firefox.svg b/pkg/extension/src/images/toolbar/icon_firefox.svg
index eba1dfd71..1917916ce 100644
--- a/pkg/extension/src/images/toolbar/icon_firefox.svg
+++ b/pkg/extension/src/images/toolbar/icon_firefox.svg
@@ -1,4 +1,4 @@
diff --git a/pkg/extension/src/images/toolbar/icon_firefox_dark.svg b/pkg/extension/src/images/toolbar/icon_firefox_dark.svg
index e61a624e2..49dcf5e8b 100644
--- a/pkg/extension/src/images/toolbar/icon_firefox_dark.svg
+++ b/pkg/extension/src/images/toolbar/icon_firefox_dark.svg
@@ -1,4 +1,4 @@
diff --git a/pkg/extension/src/images/toolbar/icon_firefox_inactive.svg b/pkg/extension/src/images/toolbar/icon_firefox_inactive.svg
index e739d8b47..49dcf5e8b 100644
--- a/pkg/extension/src/images/toolbar/icon_firefox_inactive.svg
+++ b/pkg/extension/src/images/toolbar/icon_firefox_inactive.svg
@@ -1,4 +1,4 @@
diff --git a/pkg/extension/src/images/toolbar/icon_firefox_inactive_dark.svg b/pkg/extension/src/images/toolbar/icon_firefox_inactive_dark.svg
index a66b17c2a..49dcf5e8b 100644
--- a/pkg/extension/src/images/toolbar/icon_firefox_inactive_dark.svg
+++ b/pkg/extension/src/images/toolbar/icon_firefox_inactive_dark.svg
@@ -1,4 +1,4 @@
diff --git a/pkg/extension/src/manifest.json b/pkg/extension/src/manifest.json
index 7fd7219b8..b9a2f845a 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.2.0",
"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..2bab74be8 100644
--- a/pkg/extension/src/scripts/api.js
+++ b/pkg/extension/src/scripts/api.js
@@ -1,6 +1,7 @@
function gqlRequest(apiUrl, query) {
return getStorageItem('apiKey')
.then((apiKey) => {
+ const auth = apiKey ? { Authorization: apiKey } : {}
return fetch(apiUrl, {
method: 'POST',
redirect: 'follow',
@@ -9,7 +10,7 @@ function gqlRequest(apiUrl, query) {
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
- Authorization: apiKey ? apiKey : undefined,
+ ...auth,
},
body: query,
})
@@ -130,3 +131,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..b065f546f 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
}
}
@@ -532,69 +591,8 @@ function onExtensionClick(tabId) {
})
}
-/* After installing extension, if user hasn’t logged into Omnivore, then we show the splash popup */
function checkAuthOnFirstClickPostInstall(tabId) {
- return getStorageItem('postInstallClickComplete').then(
- async (postInstallClickComplete) => {
- return true
- if (postInstallClickComplete) return true
-
- if (
- typeof browser !== 'undefined' &&
- browser.runtime &&
- browser.runtime.sendNativeMessage
- ) {
- const response = await browser.runtime.sendNativeMessage('omnivore', {
- message: ACTIONS.GetAuthToken,
- })
- if (response.authToken) {
- authToken = response.authToken
- }
- }
-
- return new Promise((resolve) => {
- const xhr = new XMLHttpRequest()
- xhr.onreadystatechange = function () {
- if (xhr.readyState === 4 && xhr.status === 200) {
- const { data } = JSON.parse(xhr.response)
- if (!data.me) {
- browserApi.tabs.sendMessage(tabId, {
- action: ACTIONS.ShowMessage,
- payload: {
- type: 'loading',
- text: 'Loading...',
- },
- })
- browserApi.tabs.sendMessage(tabId, {
- action: ACTIONS.ShowMessage,
- payload: {
- text: '',
- type: 'error',
- errorCode: 401,
- url: omnivoreURL,
- },
- })
- resolve(null)
- } else {
- setStorage({
- postInstallClickComplete: true,
- })
- resolve(true)
- }
- }
- }
-
- const query = '{me{id}}'
- const data = JSON.stringify({
- query,
- })
- xhr.open('POST', omnivoreGraphqlURL + 'graphql', true)
- setupConnection(xhr)
-
- xhr.send(data)
- })
- }
- )
+ return Promise.resolve(true)
}
function handleActionClick() {
@@ -764,6 +762,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..cb807f674 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,52 @@
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) => {
+ label.style.display = 'none'
+ })
+ const container = root.shadowRoot.querySelector(
+ '#omnivore-toast-container'
+ )
+ container.style.width = '280px'
+ container.style.top = 'unset'
+ container.style.bottom = '20px'
+ }
+ }
+
+ 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 +482,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 +636,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..34a5c43d5 100644
--- a/pkg/extension/src/views/toast.html
+++ b/pkg/extension/src/views/toast.html
@@ -2,7 +2,7 @@
#omnivore-toast-container {
position: fixed;
top: 20px;
- right: 30px;
+ right: 20px;
display: flex;
flex-direction: row;
@@ -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,15 @@
padding-bottom: 10px;
}
+ #omnivore-toast-container #omnivore-extra-buttons-row {
+ padding-left: 5px;
+ }
+
+ #omnivore-toast-container #omnivore-extra-buttons-row button {
+ padding-left: 10px;
+ width: 80%;
+ }
+
#omnivore-toast-container #omnivore-logged-out-row {
flex-direction: column;
align-items: center;
@@ -119,6 +128,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 +192,6 @@
}
#omnivore-toast-container #omnivore-extra-buttons-row button {
- width: 80%;
align-self: flex-start;
padding: 10px;
margin: 0px;
@@ -246,11 +276,11 @@
-
@@ -267,10 +297,10 @@
- Read Now
+ Read Now
-
+