diff --git a/packages/web/lib/networking/queries/useGetLabelsQuery.tsx b/packages/web/lib/networking/queries/useGetLabelsQuery.tsx index a4af02c2c..a4f3cbb67 100644 --- a/packages/web/lib/networking/queries/useGetLabelsQuery.tsx +++ b/packages/web/lib/networking/queries/useGetLabelsQuery.tsx @@ -31,7 +31,13 @@ export function useGetLabelsQuery(): LabelsQueryResponse { } } } - ${labelFragment} + fragment LabelFields on Label { + id + name + color + description + createdAt + } ` const { data, mutate, isValidating } = useSWR(query, publicGqlFetcher) diff --git a/pkg/extension/src/manifest.json b/pkg/extension/src/manifest.json index 25b2d5a32..213f4ce8c 100644 --- a/pkg/extension/src/manifest.json +++ b/pkg/extension/src/manifest.json @@ -21,14 +21,17 @@ "128": "/images/extension/icon-128.png", "256": "/images/extension/icon-256.png" }, - - "permissions": ["activeTab", "storage", "contextMenus", "https://*/**", "http://*/**"], - + "permissions": [ + "activeTab", + "storage", + "contextMenus", + "https://*/**", + "http://*/**" + ], "background": { "page": "/views/background.html", "persistent": true }, - "minimum_chrome_version": "21", "minimum_opera_version": "15", "applications": { @@ -41,25 +44,31 @@ "id": "save-extension@omnivore.app" } }, - "content_scripts": [ { - "matches": ["https://*/**", "http://*/**"], + "matches": [ + "https://*/**", + "http://*/**" + ], "js": [ - "/scripts/constants.js", + "/scripts/common.js", + "/scripts/content/toast.js", "/scripts/content/page-info.js", "/scripts/content/prepare-content.js", - "/scripts/content/toast.js", "/scripts/content/content-listener-script.js" ] }, { - "matches": ["https://*/**", "http://*/**"], - "js": ["/scripts/content/grab-iframe-content.js"], + "matches": [ + "https://*/**", + "http://*/**" + ], + "js": [ + "/scripts/content/grab-iframe-content.js" + ], "all_frames": true } ], - "browser_action": { "default_icon": { "16": "/images/toolbar/icon-16.png", @@ -71,15 +80,16 @@ }, "default_title": "Omnivore Save Article" }, - "commands": { "_execute_browser_action": { - "suggested_key": { - "default": "Alt + O" - }, - "description": "Save the current tab to Omnivore" + "suggested_key": { + "default": "Alt + O" + }, + "description": "Save the current tab to Omnivore" } }, - - "web_accessible_resources": ["views/cta-popup.html"] -} + "web_accessible_resources": [ + "views/toast.html", + "views/cta-popup.html" + ] +} \ No newline at end of file diff --git a/pkg/extension/src/scripts/background.js b/pkg/extension/src/scripts/background.js index 914399bdc..7a839a3d9 100644 --- a/pkg/extension/src/scripts/background.js +++ b/pkg/extension/src/scripts/background.js @@ -11,152 +11,130 @@ XMLHttpRequest */ -'use strict'; +'use strict' -import { v4 as uuidv4 } from 'uuid'; +import { v4 as uuidv4 } from 'uuid' -let authToken = undefined; -const omnivoreURL = process.env.OMNIVORE_URL; -const omnivoreGraphqlURL = process.env.OMNIVORE_GRAPHQL_URL; +let authToken = undefined +const omnivoreURL = process.env.OMNIVORE_URL +const omnivoreGraphqlURL = process.env.OMNIVORE_GRAPHQL_URL -/* storage helper functions */ -function getStorage (keyOrKeys) { +function getCurrentTab() { return new Promise((resolve) => { - browserApi.storage.local.get(keyOrKeys || null, (result) => { - resolve(result || {}); - }); - }); -} - -function getStorageItem (singleKey) { - return new Promise((resolve) => { - browserApi.storage.local.get(singleKey, (result) => { - const finalResult = (result && result[singleKey]) || null; - resolve(finalResult); - }); - }); -} - -function setStorage (itemsToSet) { - return new Promise((resolve) => { - browserApi.storage.local.set(itemsToSet, resolve); - }); -} - -function removeStorage (itemsToRemove) { - return new Promise((resolve) => { - browserApi.storage.local.remove(itemsToRemove, resolve); - }); -} - -function getCurrentTab () { - return new Promise((resolve) => { - browserApi.tabs.query({ - active: true, - currentWindow: true - }, function (tabs) { - resolve(tabs[0] || null); - }); - }); + browserApi.tabs.query( + { + active: true, + currentWindow: true, + }, + function (tabs) { + resolve(tabs[0] || null) + } + ) + }) } function setupConnection(xhr) { - xhr.setRequestHeader('Content-Type', 'application/json'); + xhr.setRequestHeader('Content-Type', 'application/json') if (authToken) { - xhr.setRequestHeader('Authorization', authToken); + xhr.setRequestHeader('Authorization', authToken) } } /* other code */ -function uploadFile ({ id, uploadSignedUrl }, contentType, contentObjUrl) { +function uploadFile({ id, uploadSignedUrl }, contentType, contentObjUrl) { return fetch(contentObjUrl) .then((r) => r.blob()) .then((blob) => { return new Promise((resolve) => { - const xhr = new XMLHttpRequest(); - xhr.open('PUT', uploadSignedUrl, true); - xhr.setRequestHeader('Content-Type', contentType); + const xhr = new XMLHttpRequest() + xhr.open('PUT', uploadSignedUrl, true) + xhr.setRequestHeader('Content-Type', contentType) xhr.onerror = () => { - resolve(undefined); - }; + resolve(undefined) + } xhr.onload = () => { // Uploaded. - resolve({ id }); - }; - xhr.send(blob); - }); + resolve({ id }) + } + xhr.send(blob) + }) }) .catch((err) => { console.error('error uploading file', err) return undefined - }); + }) } function savePdfFile(tab, url, contentType, contentObjUrl) { - return new Promise(resolve => { - const xhr = new XMLHttpRequest(); + return new Promise((resolve) => { + const xhr = new XMLHttpRequest() xhr.onreadystatechange = async function () { if (xhr.readyState === 4) { if (xhr.status === 200) { - const { data } = JSON.parse(xhr.response); + const { data } = JSON.parse(xhr.response) if ('errorCodes' in data.uploadFileRequest) { if (data.uploadFileRequest.errorCodes[0] === 'UNAUTHORIZED') { - clearClickCompleteState(); + clearClickCompleteState() browserApi.tabs.sendMessage(tab.id, { action: ACTIONS.ShowMessage, payload: { text: 'Unable to save page', type: 'error', errorCode: 401, - url: omnivoreURL - } - }); + url: omnivoreURL, + }, + }) } } - if (!data.uploadFileRequest || !data.uploadFileRequest.id || !data.uploadFileRequest.createdPageId || 'errorCodes' in data.uploadFileRequest) { + if ( + !data.uploadFileRequest || + !data.uploadFileRequest.id || + !data.uploadFileRequest.createdPageId || + 'errorCodes' in data.uploadFileRequest + ) { browserApi.tabs.sendMessage(tab.id, { action: ACTIONS.ShowMessage, payload: { text: 'Unable to save page', - type: 'error' - } - }); + type: 'error', + }, + }) } else { - const result = await uploadFile(data.uploadFileRequest, contentType, contentObjUrl); - URL.revokeObjectURL(contentObjUrl); + const result = await uploadFile( + data.uploadFileRequest, + contentType, + contentObjUrl + ) + URL.revokeObjectURL(contentObjUrl) if (!result) { return undefined } const createdPageId = data.uploadFileRequest.createdPageId - const url = omnivoreURL + '/article/sr/' + createdPageId - browserApi.tabs.sendMessage(tab.id, { - action: ACTIONS.ShowMessage, + action: ACTIONS.UpdateStatus, payload: { - text: 'Saved to Omnivore', - link: url, - linkText: 'Read Now', - type: 'success' - } + status: 'success', + requestId: createdPageId, + }, }) - return resolve(data.uploadFileRequest); + return resolve(data.uploadFileRequest) } } else if (xhr.status === 400) { browserApi.tabs.sendMessage(tab.id, { action: ACTIONS.ShowMessage, payload: { text: 'Unable to save page', - type: 'error' - } - }); + type: 'error', + }, + }) } - resolve(false); + resolve(false) } - }; + } const data = JSON.stringify({ query: `mutation UploadFileRequest($input: UploadFileRequestInput!) { @@ -176,29 +154,32 @@ function savePdfFile(tab, url, contentType, contentObjUrl) { url, contentType, createPageEntry: true, - } - } - }); + }, + }, + }) - xhr.open('POST', omnivoreGraphqlURL + 'graphql', true); - setupConnection(xhr); + xhr.open('POST', omnivoreGraphqlURL + 'graphql', true) + setupConnection(xhr) - xhr.send(data); - }); + xhr.send(data) + }) } -function clearClickCompleteState () { - getStorageItem('postInstallClickComplete').then((postInstallClickComplete) => { - if (postInstallClickComplete) { - removeStorage('postInstallClickComplete'); +function clearClickCompleteState() { + getStorageItem('postInstallClickComplete').then( + (postInstallClickComplete) => { + if (postInstallClickComplete) { + removeStorage('postInstallClickComplete') + } } - }); + ) } function handleSaveResponse(tab, field, xhr) { if (xhr.readyState === 4) { if (xhr.status === 200) { - const { data } = JSON.parse(xhr.response); + const { data } = JSON.parse(xhr.response) + console.log('response data: ', data) const item = data[field] if (!item) { return undefined @@ -206,8 +187,10 @@ function handleSaveResponse(tab, field, xhr) { if ('errorCodes' in item) { const messagePayload = { - text: descriptions[data.createArticle.errorCodes[0]] || 'Unable to save page', - type: 'error' + text: + descriptions[data.createArticle.errorCodes[0]] || + 'Unable to save page', + type: 'error', } if (item.errorCodes[0] === 'UNAUTHORIZED') { @@ -218,7 +201,7 @@ function handleSaveResponse(tab, field, xhr) { browserApi.tabs.sendMessage(tab.id, { action: ACTIONS.ShowMessage, - payload: messagePayload + payload: messagePayload, }) return undefined @@ -226,13 +209,11 @@ function handleSaveResponse(tab, field, xhr) { const url = item['url'] browserApi.tabs.sendMessage(tab.id, { - action: ACTIONS.ShowMessage, + action: ACTIONS.UpdateStatus, payload: { - text: 'Saved to Omnivore', + status: 'success', link: url ?? omnivoreURL + '/home', - linkText: 'Read Now', - type: 'success' - } + }, }) return item @@ -241,8 +222,8 @@ function handleSaveResponse(tab, field, xhr) { action: ACTIONS.ShowMessage, payload: { text: 'Unable to save page', - type: 'error' - } + type: 'error', + }, }) return undefined } @@ -260,19 +241,21 @@ async function saveUrl(currentTab, url) { async function saveApiRequest(currentTab, query, field, input) { browserApi.tabs.sendMessage(currentTab.id, { - action: ACTIONS.ShowMessage, + action: ACTIONS.ShowToolbar, payload: { type: 'loading', - text: 'Saving...' - } - }); + requestId: input.clientRequestId, + }, + }) return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest() xhr.open('POST', omnivoreGraphqlURL + 'graphql', true) setupConnection(xhr) - xhr.onerror = (err) => { reject(err) } + xhr.onerror = (err) => { + reject(err) + } xhr.onload = () => { try { @@ -280,7 +263,7 @@ async function saveApiRequest(currentTab, query, field, input) { if (!res) { return reject() } - resolve(res); + resolve(res) } catch (err) { reject(err) } @@ -289,146 +272,162 @@ async function saveApiRequest(currentTab, query, field, input) { const data = JSON.stringify({ query, variables: { - input - } + input, + }, }) - xhr.send(data); - }) - .catch((err) => { + xhr.send(data) + }).catch((err) => { console.log('error saving page', err) browserApi.tabs.sendMessage(currentTab.id, { action: ACTIONS.ShowMessage, payload: { text: 'Unable to save page', type: 'error', - } - }); + }, + }) return undefined - }); + }) } -async function saveArticle (tab) { - browserApi.tabs.sendMessage(tab.id, { - action: ACTIONS.GetContent - }, async (response) => { - if (!response || typeof response !== 'object') { - // In the case of an invalid response, we attempt - // to just save the URL. This can happen in Firefox - // with PDF URLs - await saveUrl(tab, tab.url) - return; - } - - const requestId = uuidv4() - var { type } = response; - const { pageInfo, doc, uploadContentObjUrl } = response; - - if (type == 'html' && handleBackendUrl(tab.url)) { - type = 'url' - } - - switch(type) { - case 'html': { - await saveApiRequest(tab, SAVE_PAGE_QUERY, 'savePage', { - source: 'extension', - clientRequestId: requestId, - originalContent: doc, - title: pageInfo.title, - url: encodeURI(tab.url), - }) - break +async function saveArticle(tab) { + browserApi.tabs.sendMessage( + tab.id, + { + action: ACTIONS.GetContent, + }, + async (response) => { + if (!response || typeof response !== 'object') { + // In the case of an invalid response, we attempt + // to just save the URL. This can happen in Firefox + // with PDF URLs + await saveUrl(tab, tab.url) + return } - case 'url': { - await saveApiRequest(tab, SAVE_URL_QUERY, 'saveUrl', { - source: 'extension', - clientRequestId: requestId, - url: encodeURI(tab.url), - }) - break + + const requestId = uuidv4() + var { type } = response + const { pageInfo, doc, uploadContentObjUrl } = response + + if (type == 'html' && handleBackendUrl(tab.url)) { + type = 'url' } - case 'pdf': { - const uploadResult = await savePdfFile(tab, encodeURI(tab.url), pageInfo.contentType, uploadContentObjUrl); - if (!uploadResult || !uploadResult.id) { - // If the upload failed for any reason, try to save the PDF URL instead + + switch (type) { + case 'html': { + await saveApiRequest(tab, SAVE_PAGE_QUERY, 'savePage', { + source: 'extension', + clientRequestId: requestId, + originalContent: doc, + title: pageInfo.title, + url: encodeURI(tab.url), + }) + break + } + case 'url': { await saveApiRequest(tab, SAVE_URL_QUERY, 'saveUrl', { source: 'extension', clientRequestId: requestId, url: encodeURI(tab.url), }) - return; + break + } + case 'pdf': { + const uploadResult = await savePdfFile( + tab, + encodeURI(tab.url), + pageInfo.contentType, + uploadContentObjUrl + ) + if (!uploadResult || !uploadResult.id) { + // If the upload failed for any reason, try to save the PDF URL instead + await saveApiRequest(tab, SAVE_URL_QUERY, 'saveUrl', { + source: 'extension', + clientRequestId: requestId, + url: encodeURI(tab.url), + }) + return + } + break } - break } } - }); + ) } // credit: https://stackoverflow.com/questions/21535233/injecting-multiple-scripts-through-executescript-in-google-chrome -function executeScripts (tabId, scriptsArray, onCompleteCallback) { - function createCallback (tabId, injectDetails, innerCallback) { +function executeScripts(tabId, scriptsArray, onCompleteCallback) { + function createCallback(tabId, injectDetails, innerCallback) { return function () { - browserScriptingApi.executeScript(tabId, injectDetails, innerCallback); - }; + browserScriptingApi.executeScript(tabId, injectDetails, innerCallback) + } } // any callback passed by caller is called upon all scripts execute completion - let callback = onCompleteCallback; + let callback = onCompleteCallback for (let i = scriptsArray.length - 1; i >= 0; --i) { - callback = createCallback(tabId, { file: scriptsArray[i] }, callback); + callback = createCallback(tabId, { file: scriptsArray[i] }, callback) } if (callback !== null) { - callback(); // execute outermost function + callback() // execute outermost function } } -function cleanupTabState (tabId) { +function cleanupTabState(tabId) { getStorage().then(function (result) { - const itemsToRemove = []; - const keys = Object.keys(result); - const keyPrefix = tabId + '_saveInProgress'; + const itemsToRemove = [] + const keys = Object.keys(result) + const keyPrefix = tabId + '_saveInProgress' for (let i = 0; i < keys.length; i++) { - const key = keys[i]; + const key = keys[i] if (key.startsWith(keyPrefix)) { - itemsToRemove.push(key); + itemsToRemove.push(key) } } - if (itemsToRemove.length === 0) return; - removeStorage(itemsToRemove); - }); + if (itemsToRemove.length === 0) return + removeStorage(itemsToRemove) + }) } /* setup an interval timer and a timeout timer (failsave) to clear interval timer after a timeout */ -function setupTimedInterval (timerCallback, timeoutCallback, callback, delay = 1000, timeout = 10500) { - const intervalId = setInterval(timerCallback, delay); +function setupTimedInterval( + timerCallback, + timeoutCallback, + callback, + delay = 1000, + timeout = 10500 +) { + const intervalId = setInterval(timerCallback, delay) const timeoutId = setTimeout(() => { - clearInterval(intervalId); - timeoutCallback(); - }, timeout); + clearInterval(intervalId) + timeoutCallback() + }, timeout) if (callback && typeof callback === 'function') { - callback(intervalId, timeoutId); + callback(intervalId, timeoutId) } } -async function clearPreviousIntervalTimer (tabId) { - const prevIntervalId = await getStorageItem(tabId + '_saveInProgress'); - if (!prevIntervalId) return; +async function clearPreviousIntervalTimer(tabId) { + const prevIntervalId = await getStorageItem(tabId + '_saveInProgress') + if (!prevIntervalId) return - clearInterval(prevIntervalId); + clearInterval(prevIntervalId) - const intervalTimeoutId = await getStorageItem(tabId + '_saveInProgressTimeoutId_' + prevIntervalId); - if (!intervalTimeoutId) return; + const intervalTimeoutId = await getStorageItem( + tabId + '_saveInProgressTimeoutId_' + prevIntervalId + ) + if (!intervalTimeoutId) return - clearTimeout(intervalTimeoutId); + clearTimeout(intervalTimeoutId) } -function onExtensionClick (tabId) { +function onExtensionClick(tabId) { /* clear any previous timers on each click */ - clearPreviousIntervalTimer(tabId); + clearPreviousIntervalTimer(tabId) /* Method to check tab loading state prior to save */ - function checkTabLoadingState (onSuccess, onPending) { + function checkTabLoadingState(onSuccess, onPending) { browserApi.tabs.get(tabId, async (tab) => { if (tab.status !== 'complete') { // show message to user on page yet to complete load @@ -436,271 +435,365 @@ function onExtensionClick (tabId) { action: ACTIONS.ShowMessage, payload: { type: 'loading', - text: 'Page loading...' - } - }); + text: 'Page loading...', + }, + }) if (onPending && typeof onPending === 'function') { - onPending(); + onPending() } } else { if (onSuccess && typeof onSuccess === 'function') { - onSuccess(); + onSuccess() + } + await saveArticle(tab) + try { + await updateLabelsCache(omnivoreGraphqlURL + 'graphql', tab) + browserApi.tabs.sendMessage(tab.id, { + action: ACTIONS.LabelCacheUpdated, + payload: {}, + }) + } catch (err) { + console.error('error fetching labels', err, omnivoreGraphqlURL) + return undefined } - await saveArticle(tab); } - }); + }) } /* call above method immediately, and if tab in loading state then setup timer */ checkTabLoadingState(null, () => { - setupTimedInterval(() => { - /* interval timer to check tab/page loading state and save */ - checkTabLoadingState(() => { - clearPreviousIntervalTimer(tabId); - }); - }, () => { - /* timeout handling, clear timer and show timeout msg */ - clearPreviousIntervalTimer(tabId); + setupTimedInterval( + () => { + /* interval timer to check tab/page loading state and save */ + checkTabLoadingState(() => { + clearPreviousIntervalTimer(tabId) + }) + }, + () => { + /* timeout handling, clear timer and show timeout msg */ + clearPreviousIntervalTimer(tabId) - browserApi.tabs.get(tabId, async (tab) => { - /* - * post timeout, we proceed to save as some sites (people.com) take a - * long time to reach complete state and remain in interactive state. - */ - await saveArticle(tab); - }); - }, (intervalId, timeoutId) => { - /* Track interval timer and timeout timer in browser storage keyed by tabId */ - const itemsToSet = {}; - itemsToSet[tabId + '_saveInProgress'] = intervalId; - itemsToSet[tabId + '_saveInProgressTimeoutId_' + intervalId] = timeoutId; - setStorage(itemsToSet); - }); - }); + browserApi.tabs.get(tabId, async (tab) => { + /* + * post timeout, we proceed to save as some sites (people.com) take a + * long time to reach complete state and remain in interactive state. + */ + await saveArticle(tab) + }) + }, + (intervalId, timeoutId) => { + /* Track interval timer and timeout timer in browser storage keyed by tabId */ + const itemsToSet = {} + itemsToSet[tabId + '_saveInProgress'] = intervalId + itemsToSet[tabId + '_saveInProgressTimeoutId_' + intervalId] = timeoutId + setStorage(itemsToSet) + } + ) + }) } /* 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) => { - if (postInstallClickComplete) return true; +function checkAuthOnFirstClickPostInstall(tabId) { + return getStorageItem('postInstallClickComplete').then( + async (postInstallClickComplete) => { + 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; + 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); + 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); + const query = '{me{id}}' + const data = JSON.stringify({ + query, + }) + xhr.open('POST', omnivoreGraphqlURL + 'graphql', true) + setupConnection(xhr) - xhr.send(data); - }); - }); + xhr.send(data) + }) + } + ) } -function handleActionClick () { +function handleActionClick() { executeAction(function (currentTab) { - onExtensionClick(currentTab.id); + onExtensionClick(currentTab.id) }) } function executeAction(action) { getCurrentTab().then((currentTab) => { - browserApi.tabs.sendMessage(currentTab.id, { - action: ACTIONS.Ping - }, async function (response) { - if (response && response.pong) { - // Content script ready - const isSignedUp = await checkAuthOnFirstClickPostInstall(currentTab.id); - if (isSignedUp) { - action(currentTab); - } - } else { - const extensionManifest = browserApi.runtime.getManifest(); - const contentScripts = extensionManifest.content_scripts; - - // No listener on the other end, inject content scripts - const scriptFiles = [ - ...contentScripts[0].js, - ...contentScripts[1].js - ]; - executeScripts(currentTab.id, scriptFiles, async function () { - const isSignedUp = await checkAuthOnFirstClickPostInstall(currentTab.id); + browserApi.tabs.sendMessage( + currentTab.id, + { + action: ACTIONS.Ping, + }, + async function (response) { + if (response && response.pong) { + // Content script ready + const isSignedUp = await checkAuthOnFirstClickPostInstall( + currentTab.id + ) if (isSignedUp) { - action(currentTab); + action(currentTab) } - }); + } else { + const extensionManifest = browserApi.runtime.getManifest() + const contentScripts = extensionManifest.content_scripts + + // No listener on the other end, inject content scripts + const scriptFiles = [...contentScripts[0].js, ...contentScripts[1].js] + executeScripts(currentTab.id, scriptFiles, async function () { + const isSignedUp = await checkAuthOnFirstClickPostInstall( + currentTab.id + ) + if (isSignedUp) { + action(currentTab) + } + }) + } } - }); - }); + ) + }) } -function getIconPath (active, dark) { - let iconPath = '/images/toolbar/icon'; +function getIconPath(active, dark) { + let iconPath = '/images/toolbar/icon' if (ENV_IS_FIREFOX) { - iconPath += '_firefox'; + iconPath += '_firefox' } else if (ENV_IS_EDGE) { - iconPath += '_edge'; + iconPath += '_edge' } if (!active) { - iconPath += '_inactive'; + iconPath += '_inactive' } /* we have to evaluate this every time as the onchange is not * fired inside background pages, due to https://crbug.com/968651 */ - const useDarkIcon = typeof dark === 'boolean' - ? dark - : window.matchMedia('(prefers-color-scheme: dark)').matches; + const useDarkIcon = + typeof dark === 'boolean' + ? dark + : window.matchMedia('(prefers-color-scheme: dark)').matches if (useDarkIcon) { - iconPath += '_dark'; + iconPath += '_dark' } if (ENV_IS_FIREFOX) { - return iconPath + '.svg'; + return iconPath + '.svg' } - const iconSizes = ['16', '24', '32', '48']; + const iconSizes = ['16', '24', '32', '48'] if (!ENV_IS_EDGE) { - iconSizes.push('19', '38'); + iconSizes.push('19', '38') } - const iconPaths = {}; + const iconPaths = {} for (let i = 0; i < iconSizes.length; i++) { - const iconSize = iconSizes[i]; - iconPaths[iconSize] = iconPath + '-' + iconSize + '.png'; + const iconSize = iconSizes[i] + iconPaths[iconSize] = iconPath + '-' + iconSize + '.png' } - return iconPaths; + return iconPaths } -function updateActionIcon (tabId, active, dark) { +function updateActionIcon(tabId, active, dark) { browserActionApi.setIcon({ path: getIconPath(active, dark), - tabId: tabId - }); + tabId: tabId, + }) } -function getActionableState (tab) { - if (tab.status !== 'complete') return false; +function getActionableState(tab) { + if (tab.status !== 'complete') return false - const tabUrl = tab.pendingUrl || tab.url; - if (!tabUrl) return false; + const tabUrl = tab.pendingUrl || tab.url + if (!tabUrl) return false - if ( - !tabUrl.startsWith('https://') && - !tabUrl.startsWith('http://') - ) return false; + if (!tabUrl.startsWith('https://') && !tabUrl.startsWith('http://')) + return false if ( tabUrl.startsWith('https://omnivore.app/') && tabUrl.startsWith('https://dev.omnivore.app/') - ) return false; + ) + return false - return true; + return true } -function reflectIconState (tab) { - const tabId = tab && tab.id; - if (!tabId) return; +function reflectIconState(tab) { + const tabId = tab && tab.id + if (!tabId) return - const active = getActionableState(tab); + const active = getActionableState(tab) - updateActionIcon(tabId, active); + updateActionIcon(tabId, active) } -function init () { +// function updateLabelsCache(tab) { +// const query = JSON.stringify({ +// query: `query GetLabels { +// labels { +// ... on LabelsSuccess { +// labels { +// ...LabelFields +// } +// } +// ... on LabelsError { +// errorCodes +// } +// } +// } +// fragment LabelFields on Label { +// id +// name +// color +// description +// createdAt +// } +// `, +// }) +// return fetch(omnivoreGraphqlURL + 'graphql', { +// method: 'POST', +// redirect: 'follow', +// credentials: 'include', +// mode: 'cors', +// headers: { +// Accept: 'application/json', +// 'Content-Type': 'application/json', +// }, +// body: query, +// }) +// .then((response) => response.json()) +// .then((data) => { +// const result = data.data.labels.labels +// return result +// }) +// .then((labels) => { +// setStorage({ +// labels: labels, +// labelsLastUpdated: new Date().toISOString(), +// }) +// return labels +// }) +// .then((labels) => { +// browserApi.tabs.sendMessage(tab.id, { +// action: ACTIONS.LabelCacheUpdated, +// payload: {}, +// }) +// }) +// .catch((err) => { +// console.error('error fetching labels', err, omnivoreGraphqlURL) +// return undefined +// }) +// } + +function init() { /* Extension icon switcher on page/tab load status */ browserApi.tabs.onActivated.addListener(({ tabId }) => { // Due to a chrome bug, chrome.tabs.* may run into an error because onActivated is triggered too fast. - function checkCurrentTab () { + function checkCurrentTab() { browserApi.tabs.get(tabId, function (tab) { if (browserApi.runtime.lastError) { - setTimeout(checkCurrentTab, 150); + setTimeout(checkCurrentTab, 150) } - reflectIconState(tab); - }); + reflectIconState(tab) + }) } - checkCurrentTab(); - }); + checkCurrentTab() + }) /* Extension icon switcher on page/tab load status */ browserApi.tabs.onUpdated.addListener((tabId, changeInfo, tab) => { /* Not an update while this tab is active so we skip updating icon */ - if (!changeInfo.status || !tab || !tab.active) return; + if (!changeInfo.status || !tab || !tab.active) return - reflectIconState(tab); - }); + reflectIconState(tab) + }) browserApi.tabs.onRemoved.addListener((tabId) => { /* cleanup any previous saveInProgress state for the tab */ - cleanupTabState(tabId); - }); + cleanupTabState(tabId) + }) - browserActionApi.onClicked.addListener(handleActionClick); + browserActionApi.onClicked.addListener(handleActionClick) // forward messages from grab-iframe-content.js script to tabs browserApi.runtime.onMessage.addListener((request, sender, sendResponse) => { if (request.forwardToTab) { - delete request.forwardToTab; - browserApi.tabs.sendRequest(sender.tab.id, request); - return; + delete request.forwardToTab + browserApi.tabs.sendRequest(sender.tab.id, request) + return } if (request.action === ACTIONS.RefreshDarkMode) { - updateActionIcon(sender.tab.id, request.payload.value); + updateActionIcon(sender.tab.id, request.payload.value) } - }); + + if (request.action === ACTIONS.EditTitle) { + console.log('EDITING TITLE REQUEST: ') + updatePageTitle( + omnivoreGraphqlURL + 'graphql', + request.payload.pageId, + request.payload.title + ) + } + }) // set initial extension icon browserActionApi.setIcon({ - path: getIconPath(true) - }); + path: getIconPath(true), + }) browserApi.contextMenus.create({ - id: "save-selection", - title: "Save to Omnivore", - contexts: ["link"], - onclick: async function(obj) { + id: 'save-selection', + title: 'Save to Omnivore', + contexts: ['link'], + onclick: async function (obj) { executeAction(async function (currentTab) { await saveUrl(currentTab, obj.linkUrl) }) }, - }); + }) } -init(); +init() diff --git a/pkg/extension/src/scripts/constants.js b/pkg/extension/src/scripts/constants.js deleted file mode 100644 index d99b15948..000000000 --- a/pkg/extension/src/scripts/constants.js +++ /dev/null @@ -1,114 +0,0 @@ -'use strict'; - -window.browserApi = (typeof chrome === 'object' && chrome && chrome.runtime && chrome) || (typeof browser === 'object' && browser) || {}; // eslint-disable-line no-undef -window.browserActionApi = browserApi.action || browserApi.browserAction || browserApi.pageAction; -window.browserScriptingApi = browserApi.scripting || browserApi.tabs; - -window.ENV_EXTENSION_ORIGIN = browserApi.runtime.getURL('PATH/').replace('/PATH/', ''); -window.ENV_IS_FIREFOX = ENV_EXTENSION_ORIGIN.startsWith('moz-extension://'); -window.ENV_IS_EDGE = navigator.userAgent.toLowerCase().indexOf('edg') > -1; -window.ENV_DOES_NOT_SUPPORT_BLOB_URL_ACCESS = /^((?!chrome|android).)*safari/i.test(navigator.userAgent); - -window.SELECTORS = { - CANONICAL_URL: ["head > link[rel='canonical']"], - TITLE: ["head > meta[property='og:title']"] -}; - -window.ACTIONS = { - ShowMessage: 'SHOW_MESSAGE', - GetContent: 'GET_CONTENT', - GetPageInfo: 'GET_PAGE_INFO', - Ping: 'PING', - AddIframeContent: 'ADD_IFRAME_CONTENT', - RefreshDarkMode: 'REFRESH_DARK_MODE', - GetAuthToken: 'GET_AUTH_TOKEN', -}; - -window.DONT_REMOVE_ELEMENTS = [ - 'meta', - 'script', - 'title' -]; - -window.SAVE_URL_QUERY = `mutation SaveUrl ($input: SaveUrlInput!) { - saveUrl(input:$input){ - ... on SaveSuccess { - url - } - ... on SaveError { - errorCodes - } - } -}` - -window.SAVE_FILE_QUERY = `mutation SaveFile ($input: SaveFileInput!) { - saveFile(input:$input){ - ... on SaveSuccess { - url - } - ... on SaveError { - errorCodes - } - } -}` - -window.SAVE_PAGE_QUERY = `mutation SavePage ($input: SavePageInput!) { - savePage(input:$input){ - ... on SaveSuccess { - url - } - ... on SaveError { - errorCodes - } - } -}` - -window.CREATE_ARTICLE_QUERY = `mutation CreateArticle ($input: CreateArticleInput!){ - createArticle(input:$input){ - ... on CreateArticleSuccess{ - createdArticle{ - id - title - slug - hasContent - } - user { - id - profile { - id - username - } - } -} - ... on CreateArticleError{ - errorCodes - } -} -}`; - -window.CREATE_ARTICLE_SAVING_REQUEST_QUERY = `mutation CreateArticleSavingRequest ($input: CreateArticleSavingRequestInput!){ - createArticleSavingRequest(input:$input){ - ... on CreateArticleSavingRequestSuccess{ - articleSavingRequest{ - id - } - } - ... on CreateArticleSavingRequestError{ - errorCodes - } - } -}`; - -function handleBackendUrl(url) { - try { - const FORCE_CONTENT_FETCH_URLS = [ - // twitter status url regex - /twitter\.com\/(?:#!\/)?(\w+)\/status(?:es)?\/(\d+)(?:\/.*)?/, - /^((?:https?:)?\/\/)?((?:www|m)\.)?((?:youtube\.com|youtu.be))(\/(?:[\w-]+\?v=|embed\/|v\/)?)([\w-]+)(\S+)?$/, - ] - return FORCE_CONTENT_FETCH_URLS.some((regex) => regex.test(url)) - } catch (error) { - console.log('error checking url', url) - } - return false -} diff --git a/pkg/extension/src/scripts/content/content-listener-script.js b/pkg/extension/src/scripts/content/content-listener-script.js index 8fc3f0b73..5fbdfd6bd 100644 --- a/pkg/extension/src/scripts/content/content-listener-script.js +++ b/pkg/extension/src/scripts/content/content-listener-script.js @@ -1,53 +1,55 @@ /* global browserApi - showMessage + showToolbar prepareContent getPageInfo ACTIONS */ -'use strict'; - -(function () { - const darkModeQuery = window.matchMedia('(prefers-color-scheme: dark)'); +'use strict' +;(function () { + const darkModeQuery = window.matchMedia('(prefers-color-scheme: dark)') if (darkModeQuery) { darkModeQuery.onchange = function (ev) { browserApi.runtime.sendMessage({ action: ACTIONS.RefreshDarkMode, payload: { - value: ev.matches - } - }); - }; + value: ev.matches, + }, + }) + } } - browserApi.runtime.onMessage.addListener(({ action, payload }, sender, sendResponse) => { - if (action === ACTIONS.GetContent) { - prepareContent().then((pageContent) => { - sendResponse({ - type: pageContent.type, - doc: pageContent.content || '', - uploadContentObjUrl: pageContent.uploadContentObjUrl, - pageInfo: getPageInfo() - }); - }); + browserApi.runtime.onMessage.addListener( + ({ action, payload }, sender, sendResponse) => { + if (action === ACTIONS.GetContent) { + prepareContent().then((pageContent) => { + sendResponse({ + type: pageContent.type, + doc: pageContent.content || '', + uploadContentObjUrl: pageContent.uploadContentObjUrl, + pageInfo: getPageInfo(), + }) + }) - /* return true to signify handlers above can asynchronously invoke the response callback */ - return true; - } + return true + } - /* other actions */ - if (action === ACTIONS.Ping) { - sendResponse({ pong: true }); - } else if (action === ACTIONS.ShowMessage) { - showMessage(payload); - } else if (action === ACTIONS.GetPageInfo) { - const pageInfo = getPageInfo(); - sendResponse(pageInfo); - } else if (action === ACTIONS.AddIframeContent) { - // do nothing, handled by prepare-content.js - } else { - console.warn('Unknown message has been taken'); + handleMessage(action, payload) + + if (action === ACTIONS.Ping) { + sendResponse({ pong: true }) + } else if (action === ACTIONS.ShowToolbar) { + showToolbar(payload) + } else if (action === ACTIONS.UpdateStatus) { + updateStatus(payload) + } else if (action === ACTIONS.LabelCacheUpdated) { + updateLabelsFromCache(payload) + } else if (action === ACTIONS.AddIframeContent) { + // do nothing, handled by prepare-content.js + } else { + console.warn('Unknown message has been taken') + } } - }); -})(); + ) +})() diff --git a/pkg/extension/src/scripts/content/toast.js b/pkg/extension/src/scripts/content/toast.js index 10885147e..dfce411f7 100644 --- a/pkg/extension/src/scripts/content/toast.js +++ b/pkg/extension/src/scripts/content/toast.js @@ -1,216 +1,453 @@ /* global ENV_EXTENSION_ORIGIN */ - -'use strict'; - -(function () { - let currentToastEl; - let currentIconEl; - let currentTextEl; - let hideToastTimeout; +;('use strict') +;(function () { + let currentToastEl + let hideToastTimeout + let labels = [] + let requestId + let finalURL const systemIcons = { - spinner: '', - success: '', - failed: '', - close: '' - }; - - function createToastContainer () { - const toastEl = document.createElement('div'); - toastEl.className = 'webext-omnivore-toast'; - toastEl.style.cssText = `all: initial !important; - position: fixed !important; - top: 20px !important; - right: 45px !important; - z-index: 9999999 !important; - display: flex !important; - align-items: center !important; - justify-content: center !important; - overflow: hidden !important; - width: 240px !important; - height: 80px !important; - border-radius: 10px !important; - background: #fff !important; - color: #3d3d3d !important; - fill: currentColor !important; - font: 700 13px Inter, sans-serif !important; - box-shadow: 0 1px 89px rgba(57, 59, 67, 0.25) !important; - user-select: none !important; - transition: all 300ms ease !important; - `; - return toastEl; + spinner: ` + + + + + + + + + + `, + success: ` + + + + `, + failed: + '', + close: + '', + animatedLoader: ` + +
+ `, } - function createToastCloseButton () { - const buttonEl = document.createElement('button'); - buttonEl.style.cssText = `all: initial !important; - position: absolute !important; - top: 8px !important; - right: 8px !important; - border: none !important; - background: none !important; - color: inherit !important; - fill: inherit !important; - outline: none !important; - cursor: pointer !important; - `; + async function createToastContainer() { + const file = await fetch(browserApi.runtime.getURL('views/toast.html')) + const html = await file.text() - const iconEl = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); - iconEl.setAttribute('viewBox', '0 0 16 16'); - iconEl.setAttribute('xmlns', 'http://www.w3.org/2000/svg'); - iconEl.style.cssText = `all: initial !important; - width: 16px !important; - height: 16px !important; - color: inherit !important; - fill: inherit !important; - `; - iconEl.addEventListener('click', function () { - currentToastEl.remove(); - }); - iconEl.innerHTML = systemIcons.close; - buttonEl.appendChild(iconEl); + const toastEl = document.createElement('div') + toastEl.id = '#omnivore-toast' + toastEl.innerHTML = html - return buttonEl; + return toastEl } - function createCtaModal (url) { - const fragment = document.createDocumentFragment(); + function createCtaModal(url) { + const fragment = document.createDocumentFragment() - const closeButtonEl = createToastCloseButton(); - fragment.appendChild(closeButtonEl); - - const iframeEl = document.createElement('iframe'); - const iframePath = '/views/cta-popup.html?url=' + encodeURIComponent(url); - const iframeUrl = ENV_EXTENSION_ORIGIN + iframePath; - iframeEl.src = iframeUrl; + const iframeEl = document.createElement('iframe') + const iframePath = '/views/cta-popup.html?url=' + encodeURIComponent(url) + const iframeUrl = ENV_EXTENSION_ORIGIN + iframePath + iframeEl.src = iframeUrl iframeEl.style.cssText = `all: initial !important; width: 310px !important; height: 360px !important; - `; - fragment.appendChild(iframeEl); - return fragment; + ` + fragment.appendChild(iframeEl) + return fragment } - function updateToastText (payload) { - if (!currentTextEl) return; - - if (!payload) { - currentTextEl.textContent = ''; - return; - } - - currentTextEl.textContent = payload.text || ''; - - const potentialLink = payload.link; - if (!potentialLink) return; - - const linkEl = document.createElement('a'); - if (potentialLink.startsWith('http')) { - linkEl.href = potentialLink; - } - linkEl.target = '_blank'; - linkEl.rel = 'external nofollow noopener noreferrer'; - linkEl.textContent = payload.linkText || 'link'; - linkEl.style.cssText = `all: initial !important; - margin-left: 1rem !important; - color: #0645ad !important; - font: inherit !important; - cursor: pointer !important; - `; - currentTextEl.appendChild(linkEl); - } - - function loadInitialToastContent () { - currentToastEl.textContent = ''; - - const iconEl = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); - iconEl.setAttribute('viewBox', '0 0 16 16'); - iconEl.setAttribute('xmlns', 'http://www.w3.org/2000/svg'); - iconEl.style.cssText = `all: initial !important; - height: 20px !important; - width: 20px !important; - margin-left: 24px !important; - fill: inherit !important; - color: inherit !important; - `; - currentIconEl = iconEl; - currentToastEl.appendChild(iconEl); - - const textEl = document.createElement('div'); - textEl.style.cssText = `all: initial !important; - flex: 1 !important; - padding: 0 24px !important; - color: inherit !important; - font: inherit !important; - `; - currentTextEl = textEl; - currentToastEl.appendChild(textEl); - } - - function hideToastAfter (timeInMs) { - if (hideToastTimeout) clearTimeout(hideToastTimeout); + function hideToastAfter(timeInMs) { + console.log('hiding toast in: ', timeInMs) + if (hideToastTimeout) clearTimeout(hideToastTimeout) hideToastTimeout = setTimeout(function () { - currentToastEl.remove(); - }, timeInMs); + console.log('clearing currentToastEl:', currentToastEl) + currentToastEl.remove() + }, timeInMs) } - function showMessageToast (payload) { - const bodyEl = document.body; - if (!bodyEl) return; + function cancelAutoDismiss() { + console.log('canceling auto dismiss') + if (hideToastTimeout) clearTimeout(hideToastTimeout) + } - let duration = 5e3; + function updateStatus(payload) { + if (!currentToastEl) { + console.log('no statusBox to update') + return + } + + const statusBox = currentToastEl.querySelector('.omnivore-toast-statusBox') + switch (payload.status) { + case 'loading': + statusBox.innerHTML = systemIcons.animatedLoader + break + case 'success': + statusBox.innerHTML = systemIcons.success + break + } + } + + // Called for all messages + function handleMessage(action, payload) { + if (payload && 'requestId' in payload && payload.requestId) { + requestId = payload.requestId ?? requestId + } + if (payload && 'link' in payload && payload.link) { + finalURL = payload.link + } + } + + function showToolbar(payload) { + showToolbarAsync(payload).catch((err) => + console.log('error showing toast', err) + ) + } + + function updateLabelsFromCache(payload) { + ;(async () => { + await getStorageItem('labels').then((cachedLabels) => { + labels = cachedLabels + console.log(' == updated labels', cachedLabels) + }) + })() + } + + async function showToolbarAsync(payload) { + const bodyEl = document.body + if (!bodyEl) return + + let duration = 5e3 if (!currentToastEl) { - currentToastEl = createToastContainer(); + currentToastEl = await createToastContainer() } - if (!currentIconEl || !currentTextEl) { - loadInitialToastContent(); - } - - let styleAsError = false; + // let styleAsError = false if (payload.type === 'loading') { - duration = 20e3; - currentIconEl.innerHTML = systemIcons.spinner; - updateToastText(payload); - } else if (payload.type !== 'error') { - currentIconEl.innerHTML = systemIcons.success; - updateToastText(payload); - } else if (payload.errorCode && payload.errorCode === 401) { - currentToastEl.textContent = ''; - currentToastEl.style.setProperty('width', '310px', 'important'); - currentToastEl.style.setProperty('height', 'auto', 'important'); - currentIconEl = null; - currentTextEl = null; - const ctaModalEl = createCtaModal(payload.url); - currentToastEl.appendChild(ctaModalEl); - duration = 8e3; - } else { - styleAsError = true; - currentIconEl.innerHTML = systemIcons.failed; - updateToastText(payload); + duration = 5000 + const image = currentToastEl.querySelector('.omnivore-toast-statusBox') + image.innerHTML = systemIcons.animatedLoader } - - const newBackgroundColor = styleAsError ? '#808080' : '#fff'; - const newTextColor = styleAsError ? '#fff' : '#3d3d3d'; - currentToastEl.style.setProperty('background', newBackgroundColor, 'important'); - currentToastEl.style.setProperty('color', newTextColor, 'important'); + // } else if (payload.type !== 'error') { + // currentIconEl.innerHTML = systemIcons.success + // updateToastText(payload) + // } else if (payload.errorCode && payload.errorCode === 401) { + // currentToastEl.textContent = '' + // currentIconEl = null + // currentTextEl = null + // const ctaModalEl = createCtaModal(payload.url) + // currentToastEl.appendChild(ctaModalEl) + // duration = 8e3 + // } else { + // styleAsError = true + // currentIconEl.innerHTML = systemIcons.failed + // updateToastText(payload) + // } if (currentToastEl.parentNode !== bodyEl) { - bodyEl.appendChild(currentToastEl); + bodyEl.appendChild(currentToastEl) + connectButtons() } - hideToastAfter(duration); + hideToastAfter(duration) // remove any existing toasts not created by current content script - const presentToastsCol = document.querySelectorAll('.webext-omnivore-toast'); - for (let i = 0; i < presentToastsCol.length; i++) { - const toastEl = presentToastsCol[i]; + document.querySelectorAll('#omnivore-toast').forEach((toastEl) => { if (toastEl !== currentToastEl) { - toastEl.remove(); + console.log('removing current toast el: ', currentToastEl) + toastEl.remove() + } + }) + } + + function toggleRow(rowId) { + const container = document.getElementById(rowId) + const initialState = container?.getAttribute('data-state') + const rows = document.querySelectorAll('.omnivore-toast-func-row') + + rows.forEach((r) => { + r.setAttribute('data-state', 'closed') + }) + + if (container && initialState) { + const newState = initialState === 'open' ? 'closed' : 'open' + container.setAttribute('data-state', newState) + } + } + + function connectButtons() { + const btns = [ + { 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-button', func: closeToast }, + ] + + for (const btnInfo of btns) { + const btn = document.getElementById(btnInfo.id) + if (btn) { + btn.addEventListener('click', btnInfo.func) } } } - window.showMessage = showMessageToast; -})(); + function createLabelRow(label, idx) { + console.log('createLabelRow, ', label, idx) + const element = document.createElement('button') + const dot = document.createElement('span') + dot.style = 'width:10px;height:10px;border-radius:1000px;' + dot.style.setProperty('background-color', label.color) + const title = document.createElement('span') + title.style = 'margin-left: 10px;pointer-events: none;' + title.innerText = label.name + + const check = document.createElement('span') + check.style = 'margin-left: auto;pointer-events: none;' + check.className = 'checkbox' + check.innerHTML = ` + + + + ` + + element.appendChild(dot) + element.appendChild(title) + element.appendChild(check) + + element.onclick = labelClick + element.onkeydown = labelKeyDown + element.setAttribute('data-label-idx', idx) + element.setAttribute('data-label-selected', 'off') + element.setAttribute('tabIndex', '-1') + + return element + } + + function labelClick(event) { + event.preventDefault() + + const labelSelected = event.target.getAttribute('data-label-selected') + if (!labelSelected) { + return + } + event.target.setAttribute( + 'data-label-selected', + labelSelected == 'on' ? 'off' : 'on' + ) + } + + function labelKeyDown(event) { + console.log('event.key.toLowerCase(): ', event.key.toLowerCase()) + switch (event.key.toLowerCase()) { + case 'arrowup': { + if ( + event.target == + event.target.form.querySelector('#omnivore-edit-label-text') + ) { + return + } + + const idx = event.target.getAttribute('data-label-idx') + let prevIdx = idx && Number(idx) != NaN ? Number(idx) - 1 : 0 + console.log( + ' prevIdx: ', + prevIdx, + 'is save', + event.target == + event.target.form.querySelector('#omnivore-save-button') + ) + if ( + event.target == + event.target.form.querySelector('#omnivore-save-button') + ) { + // Focus the last label index + const maxItemIdx = Math.max( + ...Array.from( + document.querySelectorAll(`button[data-label-idx]`) + ).map((b) => Number(b.getAttribute('data-label-idx'))) + ) + if (maxItemIdx != NaN) { + prevIdx = maxItemIdx + } + } + + const prev = document.querySelector( + `button[data-label-idx='${prevIdx}']` + ) + if (prev) { + prev.focus() + } else { + // Focus the text area + event.target.form.querySelector('#omnivore-edit-label-text')?.focus() + } + event.preventDefault() + break + } + case 'arrowdown': { + const idx = event.target.getAttribute('data-label-idx') + const nextIdx = idx && Number(idx) != NaN ? Number(idx) + 1 : 0 + const next = document.querySelector( + `button[data-label-idx='${nextIdx}']` + ) + if (next) { + next.focus() + } else { + // Focus the save button + event.target.form.querySelector('.omnivore-save-button')?.focus() + } + event.preventDefault() + break + } + case 'enter': { + const labelSelected = event.target.getAttribute('data-label-selected') + if (!labelSelected) { + return + } + event.target.setAttribute( + 'data-label-selected', + labelSelected == 'on' ? 'off' : 'on' + ) + console.log( + 'data-label-selected:', + event.target.getAttribute('data-label-selected') + ) + event.preventDefault() + break + } + } + } + + function editTitle() { + cancelAutoDismiss() + toggleRow('omnivore-edit-title-row') + document.getElementById('omnivore-edit-title-textarea')?.focus() + + document.getElementById('omnivore-edit-title-form').onsubmit = (event) => { + event.preventDefault() + browserApi.runtime.sendMessage({ + action: ACTIONS.EditTitle, + payload: { + pageId: requestId, + title: document.getElementById('omnivore-edit-title-textarea').value, + }, + }) + } + } + + async function editLabels() { + cancelAutoDismiss() + + await getStorageItem('labels').then((cachedLabels) => { + labels = cachedLabels + }) + + toggleRow('omnivore-edit-labels-row') + document.getElementById('omnivore-edit-label-text')?.focus() + const list = document.getElementById('omnivore-edit-labels-list') + document + .getElementById('omnivore-edit-label-text') + .addEventListener('input', function () { + updateLabels(this.value) + }) + + document.getElementById('omnivore-edit-label-text').onkeydown = labelKeyDown + + if (list) { + list.innerHTML = '' + labels.forEach(function (label, idx) { + const rowHtml = createLabelRow(label, idx) + list.appendChild(rowHtml) + }) + } + + document.getElementById('omnivore-edit-labels-form').onsubmit = (event) => { + event.preventDefault() + browserApi.runtime.sendMessage({ + action: ACTIONS.EditTitle, + payload: { + labels: [], + }, + }) + } + } + + async function updateLabels(filterValue) { + const list = document.getElementById('omnivore-edit-labels-list') + if (list) { + list.innerHTML = '' + if (filterValue) { + labels + .filter( + (l) => l.name.toLowerCase().indexOf(filterValue.toLowerCase()) > -1 + ) + .forEach(function (label, idx) { + const rowHtml = createLabelRow(label, idx) + list.appendChild(rowHtml) + }) + } else { + labels.forEach(function (label, idx) { + const rowHtml = createLabelRow(label, idx) + list.appendChild(rowHtml) + }) + } + } + } + + function readNow() { + cancelAutoDismiss() + const container = document.getElementById('omnivore-toast-container') + container.setAttribute('data-state', 'open') + + if (finalURL) { + window.open(finalURL) + } else { + window.open(`https://demo.omnivore.app/article/sr/${requestId}`) + } + + setTimeout(() => { + closeToast() + }, 1000) + } + + function openMenu() { + cancelAutoDismiss() + toggleRow('omnivore-extra-buttons-row') + } + + function closeToast() { + console.log('closing toast') + currentToastEl.remove() + } + + window.showToolbar = showToolbar + window.updateStatus = updateStatus + window.handleMessage = handleMessage + window.updateLabelsFromCache = updateLabelsFromCache +})() diff --git a/pkg/extension/src/views/background.html b/pkg/extension/src/views/background.html index 07a7a0029..2045c3080 100644 --- a/pkg/extension/src/views/background.html +++ b/pkg/extension/src/views/background.html @@ -6,7 +6,8 @@ Omnivore background - + +