From 44e85ea295723c5d9d2254f33892fd93c23c9bf0 Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Sat, 30 Sep 2023 12:46:23 +0800 Subject: [PATCH] Merge --- android/Omnivore/app/build.gradle | 6 + packages/db/migrate.ts | 44 +-- packages/web/components/elements/Button.tsx | 9 +- .../web/components/elements/FormElements.tsx | 19 +- .../web/components/elements/StyledText.tsx | 1 - .../patterns/LibraryCards/LibraryGridCard.tsx | 6 + .../components/templates/PrimaryDropdown.tsx | 4 + .../components/templates/SettingsLayout.tsx | 8 +- .../queries/useValidateUsernameQuery.tsx | 18 +- pkg/extension/src/manifest.json | 4 +- pkg/extension/src/scripts/api.js | 10 +- pkg/extension/src/scripts/background.js | 244 ++++++++----- .../content/content-listener-script.js | 3 +- .../src/scripts/content/prepare-content.js | 327 ++++++++++++------ pkg/extension/src/scripts/content/toast.js | 105 ++++-- 15 files changed, 536 insertions(+), 272 deletions(-) diff --git a/android/Omnivore/app/build.gradle b/android/Omnivore/app/build.gradle index 681e873b7..a2dca9ae5 100644 --- a/android/Omnivore/app/build.gradle +++ b/android/Omnivore/app/build.gradle @@ -160,3 +160,9 @@ dependencies { apollo { packageName.set 'app.omnivore.omnivore.graphql.generated' } + +task printVersion { + doLast { + println "omnivoreVersion: ${android.defaultConfig.versionName}" + } +} diff --git a/packages/db/migrate.ts b/packages/db/migrate.ts index ef17fe997..401f29f29 100755 --- a/packages/db/migrate.ts +++ b/packages/db/migrate.ts @@ -131,26 +131,26 @@ const postgresMigration = postgrator process.exit(1) }) -// elastic migration -log('Creating elastic index...') -const elasticMigration = esClient.indices - .exists({ index: INDEX_ALIAS }) - .then(({ body: exists }) => { - if (!exists) { - return createIndex().then(() => log('Elastic index created.')) - } else { - log('Elastic index already exists.') - } - }) - .then(() => { - log('Updating elastic index mappings...') - return updateMappings().then(() => { - log('Elastic index mappings updated.') - }) - }) - .catch((error) => { - log(`${chalk.red('Elastic migration failed: ')}${error.message}`, chalk.red) - process.exit(1) - }) +// // elastic migration +// log('Creating elastic index...') +// const elasticMigration = esClient.indices +// .exists({ index: INDEX_ALIAS }) +// .then(({ body: exists }) => { +// if (!exists) { +// return createIndex().then(() => log('Elastic index created.')) +// } else { +// log('Elastic index already exists.') +// } +// }) +// .then(() => { +// log('Updating elastic index mappings...') +// return updateMappings().then(() => { +// log('Elastic index mappings updated.') +// }) +// }) +// .catch((error) => { +// log(`${chalk.red('Elastic migration failed: ')}${error.message}`, chalk.red) +// process.exit(1) +// }) -Promise.all([postgresMigration, elasticMigration]).then(() => log('Exiting...')) +Promise.all([postgresMigration]).then(() => log('Exiting...')) diff --git a/packages/web/components/elements/Button.tsx b/packages/web/components/elements/Button.tsx index 831fc58cc..2939850cf 100644 --- a/packages/web/components/elements/Button.tsx +++ b/packages/web/components/elements/Button.tsx @@ -194,12 +194,13 @@ export const Button = styled('button', { }, }, link: { - color: '$grayText', border: 'none', bg: 'transparent', - '&:hover': { - opacity: 0.8, - }, + fontSize: '14px', + fontWeight: 'regular', + fontFamily: '$display', + color: '$thLibraryMenuUnselected', + cursor: 'pointer', }, circularIcon: { mx: '$1', diff --git a/packages/web/components/elements/FormElements.tsx b/packages/web/components/elements/FormElements.tsx index 995e7f759..1c00bded2 100644 --- a/packages/web/components/elements/FormElements.tsx +++ b/packages/web/components/elements/FormElements.tsx @@ -25,16 +25,21 @@ export interface FormInputProps { } export const FormInput = styled('input', { - border: 'none', + border: '1px solid $textNonessential', width: '100%', bg: 'transparent', fontSize: '16px', fontFamily: 'inter', fontWeight: 'normal', lineHeight: '1.35', + borderRadius: '5px', + textIndent: '8px', + marginBottom: '2px', + height: '38px', color: '$grayTextContrast', '&:focus': { - outline: 'none', + border: '1px solid transparent', + outline: '2px solid $omnivoreCtaYellow', }, }) @@ -63,6 +68,10 @@ export const BorderedFormInput = styled(FormInput, { borderColor: '#d9d9d9', borderRadius: '6px', transition: 'all .2s', + '&:focus': { + border: '1px solid transparent', + outline: '2px solid $omnivoreCtaYellow', + }, }) export function GeneralFormInput(props: FormInputProps): JSX.Element { @@ -170,7 +179,7 @@ export function GeneralFormInput(props: FormInputProps): JSX.Element { required={input.required} css={{ border: '1px solid $textNonessential', - borderRadius: '8px', + borderRadius: '5px', width: '100%', bg: 'transparent', fontSize: '16px', @@ -179,8 +188,8 @@ export function GeneralFormInput(props: FormInputProps): JSX.Element { height: '38px', color: '$grayTextContrast', '&:focus': { - outline: 'none', - boxShadow: '0px 0px 2px 2px rgba(255, 234, 159, 0.56)', + border: '1px solid transparent', + outline: '2px solid $omnivoreCtaYellow', }, }} name={input.name} diff --git a/packages/web/components/elements/StyledText.tsx b/packages/web/components/elements/StyledText.tsx index 0d480266b..0a47e4747 100644 --- a/packages/web/components/elements/StyledText.tsx +++ b/packages/web/components/elements/StyledText.tsx @@ -146,7 +146,6 @@ const textVariants = { }, navLink: { m: 0, - fontSize: '$1', fontWeight: 400, color: '$graySolid', cursor: 'pointer', diff --git a/packages/web/components/patterns/LibraryCards/LibraryGridCard.tsx b/packages/web/components/patterns/LibraryCards/LibraryGridCard.tsx index 0ba2db164..75cf6431b 100644 --- a/packages/web/components/patterns/LibraryCards/LibraryGridCard.tsx +++ b/packages/web/components/patterns/LibraryCards/LibraryGridCard.tsx @@ -183,6 +183,12 @@ const LibraryGridCardContent = (props: LinkedItemCardProps): JSX.Element => { const { isChecked, setIsChecked, item } = props const [menuOpen, setMenuOpen] = useState(false) const originText = siteName(props.item.originalArticleUrl, props.item.url) + console.log( + 'const originText = siteName(props.item.originalArticleUrl, props.item.url)', + siteName(props.item.originalArticleUrl, props.item.url), + props.item.originalArticleUrl, + props.item.url + ) const handleCheckChanged = useCallback(() => { const newValue = !isChecked diff --git a/packages/web/components/templates/PrimaryDropdown.tsx b/packages/web/components/templates/PrimaryDropdown.tsx index 431076b85..5d46f9932 100644 --- a/packages/web/components/templates/PrimaryDropdown.tsx +++ b/packages/web/components/templates/PrimaryDropdown.tsx @@ -106,6 +106,10 @@ export function PrimaryDropdown(props: PrimaryDropdownProps): JSX.Element { cursor: 'pointer', mouseEvents: 'all', }} + onClick={(event) => { + router.push('/settings/account') + event.preventDefault() + }} > - {props.children} + + + {props.children} + {showLogoutConfirmation ? ( diff --git a/packages/web/lib/networking/queries/useValidateUsernameQuery.tsx b/packages/web/lib/networking/queries/useValidateUsernameQuery.tsx index c40dd9fc0..467d737fc 100644 --- a/packages/web/lib/networking/queries/useValidateUsernameQuery.tsx +++ b/packages/web/lib/networking/queries/useValidateUsernameQuery.tsx @@ -7,6 +7,7 @@ type ValidateUsernameInput = { } type ValidateUsernameResponse = { + isLoading: boolean isUsernameValid: boolean usernameErrorMessage?: string } @@ -20,12 +21,19 @@ export function useValidateUsernameQuery({ } ` - const { data } = useSWR([query, username], makePublicGqlFetcher({ username })) + // Don't fetch if username is empty + const { data, error, isValidating } = useSWR( + username ? [query, username] : null, + makePublicGqlFetcher({ username }) + ) // eslint-disable-next-line @typescript-eslint/no-explicit-any const isUsernameValid = (data as any)?.validateUsername ?? false if (isUsernameValid) { - return { isUsernameValid } + return { + isUsernameValid, + isLoading: !data && !error, + } } // Try to figure out why the username is invalid @@ -33,12 +41,14 @@ export function useValidateUsernameQuery({ if (usernameErrorMessage) { return { isUsernameValid: false, + isLoading: !data && !error, usernameErrorMessage, } } return { isUsernameValid: false, + isLoading: !data && !error, usernameErrorMessage: 'This username is not available', } } @@ -48,8 +58,8 @@ function validationErrorMessage(username: string): string | undefined { return undefined } - if (username.length < 3) { - return 'Username should contain at least three characters' + if (username.length < 4) { + return 'Username should contain at least four characters' } if (username.length > 15) { diff --git a/pkg/extension/src/manifest.json b/pkg/extension/src/manifest.json index c82046e48..1739f5f59 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.4.4", + "version": "2.6.1", "description": "Save PDFs and Articles to your Omnivore library", "author": "Omnivore Media, Inc", "default_locale": "en", @@ -11,7 +11,7 @@ "url": "https://omnivore.app/" }, "homepage_url": "https://omnivore.app/", - "content_security_policy": "default-src 'none'; child-src 'none'; manifest-src 'none'; media-src 'none'; object-src 'none'; prefetch-src 'none'; worker-src 'none'; connect-src https://storage.googleapis.com/ process.env.OMNIVORE_GRAPHQL_URL blob:; frame-src 'none'; font-src 'none'; img-src data:; script-src 'self'; script-src-elem 'self'; script-src-attr 'none'; style-src 'self'; style-src-elem 'self'; style-src-attr 'none'; base-uri 'none'; form-action 'none'; block-all-mixed-content; upgrade-insecure-requests; report-uri https://api.jeurissen.co/reports/csp/webext/omnivore/", + "content_security_policy": "default-src 'none'; child-src 'none'; manifest-src 'none'; media-src 'none'; object-src 'none'; worker-src 'none'; connect-src https://storage.googleapis.com/ process.env.OMNIVORE_GRAPHQL_URL blob:; frame-src 'none'; font-src 'none'; img-src data:; script-src 'self'; script-src-elem 'self'; script-src-attr 'none'; style-src 'self'; style-src-elem 'self'; style-src-attr 'none'; base-uri 'none'; form-action 'none'; block-all-mixed-content; upgrade-insecure-requests; report-uri https://api.jeurissen.co/reports/csp/webext/omnivore/", "icons": { "16": "/images/extension/icon-16.png", "24": "/images/extension/icon-24.png", diff --git a/pkg/extension/src/scripts/api.js b/pkg/extension/src/scripts/api.js index 5dc219331..48465ca9d 100644 --- a/pkg/extension/src/scripts/api.js +++ b/pkg/extension/src/scripts/api.js @@ -98,12 +98,7 @@ async function updatePageTitle(apiUrl, pageId, title) { return data.updatePage.updatePage } -async function setLabels(apiUrl, pageId, labelIds, createdLabels) { - console.log( - 'setLabels(apiUrl, pageId, labelIds, createdLabels)', - labelIds, - createdLabels - ) +async function setLabels(apiUrl, pageId, labels) { const mutation = JSON.stringify({ query: `mutation SetLabels($input: SetLabelsInput!) { setLabels(input: $input) { @@ -123,8 +118,7 @@ async function setLabels(apiUrl, pageId, labelIds, createdLabels) { variables: { input: { pageId, - labelIds, - labels: createdLabels, + labels, }, }, }) diff --git a/pkg/extension/src/scripts/background.js b/pkg/extension/src/scripts/background.js index 3bb5fd6fb..0fccd47c8 100644 --- a/pkg/extension/src/scripts/background.js +++ b/pkg/extension/src/scripts/background.js @@ -14,11 +14,51 @@ import { v4 as uuidv4 } from 'uuid' import { nanoid } from 'nanoid' +class TaskQueue { + constructor() { + this.queue = [] + this.isRunning = false + this.isReady = false + } + + enqueue(task) { + this.queue.push(task) + + // Only run the next task if the queue is ready + if (this.isReady) { + this.runNext() + } + } + + async runNext() { + if (this.isRunning || this.queue.length === 0 || !this.isReady) return + + this.isRunning = true + const task = this.queue.shift() + + try { + await task() + } catch (err) { + console.error('Task failed:', err) + } finally { + this.isRunning = false + if (this.isReady) { + this.runNext() + } + } + } + + setReady() { + this.isReady = true + this.runNext() + } +} + let authToken = undefined +const queue = new TaskQueue() const omnivoreURL = process.env.OMNIVORE_URL const omnivoreGraphqlURL = process.env.OMNIVORE_GRAPHQL_URL -let pendingRequests = [] let completedRequests = {} function getCurrentTab() { @@ -135,7 +175,6 @@ async function savePdfFile( contentType, contentObjUrl ) - console.log(' uploadFileResult: ', uploadFileResult) URL.revokeObjectURL(contentObjUrl) if (uploadFileResult && uploadRequestResult.createdPageId) { @@ -255,7 +294,7 @@ async function saveApiRequest(currentTab, query, field, input) { console.log('error saving: ', err) } - processPendingRequests(currentTab.id) + queue.setReady() } function updateClientStatus(tabId, target, status, message) { @@ -312,12 +351,18 @@ async function setLabelsRequest(tabId, request, completedResponse) { return setLabels( omnivoreGraphqlURL + 'graphql', completedResponse.responseId, - request.labelIds + request.labels ) .then(() => { updateClientStatus(tabId, 'labels', 'success', 'Labels updated.') return true }) + .then(() => { + browserApi.tabs.sendMessage(tabId, { + action: ACTIONS.LabelCacheUpdated, + payload: {}, + }) + }) .catch(() => { updateClientStatus(tabId, 'labels', 'failure', 'Error updating labels.') return true @@ -351,48 +396,49 @@ async function deleteRequest(tabId, request, completedResponse) { }) } -async function processPendingRequests(tabId) { - const tabRequests = pendingRequests.filter((pr) => pr.tabId === tabId) - - tabRequests.forEach(async (pr) => { - let handled = false - const completed = completedRequests[pr.clientRequestId] - if (completed) { - switch (pr.type) { - 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 - } - } - - if (handled) { - const idx = pendingRequests.findIndex((opr) => pr.id === opr.id) - if (idx > -1) { - pendingRequests.splice(idx, 1) - } - } - }) - - // TODO: need to handle clearing completedRequests also +async function processEditTitleRequest(tabId, pr) { + const completed = completedRequests[pr.clientRequestId] + handled = await editTitleRequest(tabId, pr, completed) + console.log('processEditTitleRequest: ', handled) + return handled } -async function saveArticle(tab) { +async function processAddNoteRequest(tabId, pr) { + const completed = completedRequests[pr.clientRequestId] + const handled = await addNoteRequest(tabId, pr, completed) + console.log('processAddNoteRequest: ', handled) + return handled +} + +async function processSetLabelsRequest(tabId, pr) { + const completed = completedRequests[pr.clientRequestId] + const handled = await setLabelsRequest(tabId, pr, completed) + console.log('processSetLabelsRequest: ', handled) + return handled +} + +async function processArchiveRequest(tabId, pr) { + const completed = completedRequests[pr.clientRequestId] + const handled = await archiveRequest(tabId, pr, completed) + console.log('processArchiveRequest: ', handled) + return handled +} + +async function processDeleteRequest(tabId, pr) { + const completed = completedRequests[pr.clientRequestId] + const handled = await deleteRequest(tabId, pr, completed) + console.log('processDeleteRequest: ', handled) + return handled +} + +async function saveArticle(tab, createHighlight) { browserApi.tabs.sendMessage( tab.id, { action: ACTIONS.GetContent, + payload: { + createHighlight: createHighlight, + }, }, async (response) => { if (!response || typeof response !== 'object') { @@ -521,7 +567,8 @@ async function clearPreviousIntervalTimer(tabId) { clearTimeout(intervalTimeoutId) } -function onExtensionClick(tabId) { +function extensionSaveCurrentPage(tabId, createHighlight) { + createHighlight = createHighlight ? true : false /* clear any previous timers on each click */ clearPreviousIntervalTimer(tabId) @@ -544,7 +591,7 @@ function onExtensionClick(tabId) { if (onSuccess && typeof onSuccess === 'function') { onSuccess() } - await saveArticle(tab) + await saveArticle(tab, createHighlight) try { await updateLabelsCache(omnivoreGraphqlURL + 'graphql', tab) browserApi.tabs.sendMessage(tab.id, { @@ -577,7 +624,7 @@ function onExtensionClick(tabId) { * 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) + await saveArticle(tab, createHighlight) }) }, (intervalId, timeoutId) => { @@ -597,13 +644,12 @@ function checkAuthOnFirstClickPostInstall(tabId) { function handleActionClick() { executeAction(function (currentTab) { - onExtensionClick(currentTab.id) + extensionSaveCurrentPage(currentTab.id) }) } function executeAction(action) { getCurrentTab().then((currentTab) => { - console.log('currentTab: ', currentTab) browserApi.tabs.sendMessage( currentTab.id, { @@ -685,65 +731,65 @@ function init() { } if (request.action === ACTIONS.EditTitle) { - pendingRequests.push({ - id: uuidv4(), - type: 'EDIT_TITLE', - tabId: sender.tab.id, - title: request.payload.title, - clientRequestId: request.payload.ctx.requestId, - }) - - processPendingRequests(sender.tab.id) + queue.enqueue(() => + processEditTitleRequest(sender.tab.id, { + id: uuidv4(), + type: 'EDIT_TITLE', + tabId: sender.tab.id, + title: request.payload.title, + clientRequestId: request.payload.ctx.requestId, + }) + ) } if (request.action === ACTIONS.Archive) { - pendingRequests.push({ - id: uuidv4(), - type: 'ARCHIVE', - tabId: sender.tab.id, - clientRequestId: request.payload.ctx.requestId, - }) - - processPendingRequests(sender.tab.id) + queue.enqueue(() => + processArchiveRequest(sender.tab.id, { + id: uuidv4(), + type: 'ARCHIVE', + tabId: sender.tab.id, + clientRequestId: request.payload.ctx.requestId, + }) + ) } if (request.action === ACTIONS.Delete) { - pendingRequests.push({ - type: 'DELETE', - tabId: sender.tab.id, - clientRequestId: request.payload.ctx.requestId, - }) - - processPendingRequests(sender.tab.id) + queue.enqueue(() => + processDeleteRequest(sender.tab.id, { + type: 'DELETE', + tabId: sender.tab.id, + clientRequestId: request.payload.ctx.requestId, + }) + ) } 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) + queue.enqueue(() => + processAddNoteRequest(sender.tab.id, { + id: uuidv4(), + type: 'ADD_NOTE', + tabId: sender.tab.id, + note: request.payload.note, + clientRequestId: request.payload.ctx.requestId, + }) + ) } if (request.action === ACTIONS.SetLabels) { - pendingRequests.push({ - id: uuidv4(), - type: 'SET_LABELS', - tabId: sender.tab.id, - labelIds: request.payload.labelIds, - clientRequestId: request.payload.ctx.requestId, - }) - - processPendingRequests(sender.tab.id) + queue.enqueue(() => + processSetLabelsRequest(sender.tab.id, { + id: uuidv4(), + type: 'SET_LABELS', + tabId: sender.tab.id, + labels: request.payload.labels, + clientRequestId: request.payload.ctx.requestId, + }) + ) } }) browserApi.contextMenus.create({ - id: 'save-selection', + id: 'save-link-selection', title: 'Save this link to Omnivore', contexts: ['link'], onclick: async function (obj) { @@ -752,6 +798,28 @@ function init() { }) }, }) + + browserApi.contextMenus.create({ + id: 'save-page-selection', + title: 'Save this page to Omnivore', + contexts: ['page'], + onclick: async function (obj) { + executeAction(function (currentTab) { + extensionSaveCurrentPage(currentTab.id) + }) + }, + }) + + browserApi.contextMenus.create({ + id: 'save-text-selection', + title: 'Create Highlight and Save to Omnivore', + contexts: ['selection'], + onclick: async function (obj) { + executeAction(function (currentTab) { + extensionSaveCurrentPage(currentTab.id, true) + }) + }, + }) } init() diff --git a/pkg/extension/src/scripts/content/content-listener-script.js b/pkg/extension/src/scripts/content/content-listener-script.js index b5c6d9cee..d86bb3ed8 100644 --- a/pkg/extension/src/scripts/content/content-listener-script.js +++ b/pkg/extension/src/scripts/content/content-listener-script.js @@ -23,7 +23,8 @@ browserApi.runtime.onMessage.addListener( ({ action, payload }, sender, sendResponse) => { if (action === ACTIONS.GetContent) { - prepareContent().then((pageContent) => { + const createHighlight = payload && payload.createHighlight + prepareContent(createHighlight).then((pageContent) => { sendResponse({ type: pageContent.type, doc: pageContent.content || '', diff --git a/pkg/extension/src/scripts/content/prepare-content.js b/pkg/extension/src/scripts/content/prepare-content.js index 59208ebed..01dc089fa 100644 --- a/pkg/extension/src/scripts/content/prepare-content.js +++ b/pkg/extension/src/scripts/content/prepare-content.js @@ -5,37 +5,38 @@ ENV_DOES_NOT_SUPPORT_BLOB_URL_ACCESS */ -'use strict'; +'use strict' +;(function () { + const iframes = {} -(function () { - const iframes = {}; + browserApi.runtime.onMessage.addListener( + ({ action, payload }, sender, sendResponse) => { + if (action !== ACTIONS.AddIframeContent) return + const { url, content } = payload + iframes[url] = content + sendResponse({}) + } + ) - browserApi.runtime.onMessage.addListener(({ action, payload }, sender, sendResponse) => { - if (action !== ACTIONS.AddIframeContent) return; - const { url, content } = payload; - iframes[url] = content; - sendResponse({}); - }); - - async function grabPdfContent () { - const fileExtension = window.location.pathname.slice(-4).toLowerCase(); - const hasPdfExtension = fileExtension === '.pdf'; + async function grabPdfContent() { + const fileExtension = window.location.pathname.slice(-4).toLowerCase() + const hasPdfExtension = fileExtension === '.pdf' const pdfContentTypes = [ 'application/acrobat', 'application/pdf', 'application/x-pdf', 'applications/vnd.pdf', 'text/pdf', - 'text/x-pdf' - ]; - const isPdfContent = pdfContentTypes.indexOf(document.contentType) !== -1; + 'text/x-pdf', + ] + const isPdfContent = pdfContentTypes.indexOf(document.contentType) !== -1 if (!hasPdfExtension && !isPdfContent) { - return Promise.resolve(null); + return Promise.resolve(null) } - const embedEl = document.querySelector('embed'); + const embedEl = document.querySelector('embed') if (embedEl && embedEl.type !== 'application/pdf') { - return Promise.resolve(null); + return Promise.resolve(null) } if (ENV_DOES_NOT_SUPPORT_BLOB_URL_ACCESS && embedEl.src) { @@ -43,115 +44,120 @@ } return new Promise((resolve, reject) => { - const xhr = new XMLHttpRequest(); + const xhr = new XMLHttpRequest() // load `document` from `cache` - xhr.open('GET', '', true); - xhr.responseType = 'blob'; + xhr.open('GET', '', true) + xhr.responseType = 'blob' xhr.onload = function (e) { if (this.status === 200) { - resolve({ type: 'pdf', uploadContentObjUrl: URL.createObjectURL(this.response) }) + resolve({ + type: 'pdf', + uploadContentObjUrl: URL.createObjectURL(this.response), + }) } else { - reject(e); + reject(e) } - }; - xhr.send(); - }); + } + xhr.send() + }) } - function prepareContentPostItem (itemEl) { - const lowerTagName = itemEl.tagName.toLowerCase(); + function prepareContentPostItem(itemEl) { + const lowerTagName = itemEl.tagName.toLowerCase() if (lowerTagName === 'iframe') { - const frameHtml = iframes[itemEl.src]; - if (!frameHtml) return; + const frameHtml = iframes[itemEl.src] + if (!frameHtml) return - const containerEl = document.createElement('div'); - containerEl.className = 'omnivore-instagram-embed'; - containerEl.innerHTML = frameHtml; + const containerEl = document.createElement('div') + containerEl.className = 'omnivore-instagram-embed' + containerEl.innerHTML = frameHtml - const parentEl = itemEl.parentNode; - if (!parentEl) return; + const parentEl = itemEl.parentNode + if (!parentEl) return - parentEl.replaceChild(containerEl, itemEl); + parentEl.replaceChild(containerEl, itemEl) - return; + return } if (lowerTagName === 'img' || lowerTagName === 'image') { // Removing blurred images since they are mostly the copies of lazy loaded ones - const style = window.getComputedStyle(itemEl); - const filter = style.getPropertyValue('filter'); - if (filter.indexOf('blur(') === -1) return; - itemEl.remove(); - return; + const style = window.getComputedStyle(itemEl) + const filter = style.getPropertyValue('filter') + if (filter.indexOf('blur(') === -1) return + itemEl.remove() + return } - const style = window.getComputedStyle(itemEl); - const backgroundImage = style.getPropertyValue('background-image'); + const style = window.getComputedStyle(itemEl) + const backgroundImage = style.getPropertyValue('background-image') // convert all nodes with background image to img nodes - const noBackgroundImage = !backgroundImage || backgroundImage === 'none'; - if (!noBackgroundImage) return; + const noBackgroundImage = !backgroundImage || backgroundImage === 'none' + if (!noBackgroundImage) return - const filter = style.getPropertyValue('filter'); + const filter = style.getPropertyValue('filter') // avoiding image nodes with a blur effect creation if (filter && filter.indexOf('blur(') !== -1) { - itemEl.remove(); - return; + itemEl.remove() + return } // Replacing element only of there are no content inside, b/c might remove important div with content. // Article example: http://www.josiahzayner.com/2017/01/genetic-designer-part-i.html // DIV with class "content-inner" has `url("https://resources.blogblog.com/blogblog/data/1kt/travel/bg_container.png")` background image. - if (itemEl.src) return; - if (itemEl.innerHTML.length > 24) return; + if (itemEl.src) return + if (itemEl.innerHTML.length > 24) return - const BI_SRC_REGEXP = /url\("(.+?)"\)/gi; - const matchedSRC = BI_SRC_REGEXP.exec(backgroundImage); + const BI_SRC_REGEXP = /url\("(.+?)"\)/gi + const matchedSRC = BI_SRC_REGEXP.exec(backgroundImage) // Using "g" flag with a regex we have to manually break down lastIndex to zero after every usage // More details here: https://stackoverflow.com/questions/1520800/why-does-a-regexp-with-global-flag-give-wrong-results - BI_SRC_REGEXP.lastIndex = 0; + BI_SRC_REGEXP.lastIndex = 0 - const targetSrc = matchedSRC && matchedSRC[1]; - if (!targetSrc) return; + const targetSrc = matchedSRC && matchedSRC[1] + if (!targetSrc) return - const imgEl = document.createElement('img'); - imgEl.src = targetSrc; - const parentEl = itemEl.parentNode; - if (!parentEl) return; + const imgEl = document.createElement('img') + imgEl.src = targetSrc + const parentEl = itemEl.parentNode + if (!parentEl) return - parentEl.replaceChild(imgEl, itemEl); + parentEl.replaceChild(imgEl, itemEl) } - function prepareContentPostScroll () { - const contentCopyEl = document.createElement('div'); - contentCopyEl.style.position = 'absolute'; - contentCopyEl.style.left = '-2000px'; - contentCopyEl.style.zIndex = '-2000'; - contentCopyEl.innerHTML = document.body.innerHTML; + function prepareContentPostScroll() { + const contentCopyEl = document.createElement('div') + contentCopyEl.style.position = 'absolute' + contentCopyEl.style.left = '-2000px' + contentCopyEl.style.zIndex = '-2000' + contentCopyEl.innerHTML = document.body.innerHTML // Appending copy of the content to the DOM to enable computed styles capturing ability // Without adding that copy to the DOM the `window.getComputedStyle` method will always return undefined. - document.documentElement.appendChild(contentCopyEl); + document.documentElement.appendChild(contentCopyEl) - Array.from(contentCopyEl.getElementsByTagName('*')).forEach(prepareContentPostItem); + Array.from(contentCopyEl.getElementsByTagName('*')).forEach( + prepareContentPostItem + ) /* - * Grab head and body separately as using clone on entire document into a div - * removes the head and body tags while grabbing html in them. Instead we - * capture them separately and concatenate them here with head and body tags - * preserved. - */ - const contentCopyHtml = `${document.head.innerHTML}${contentCopyEl.innerHTML}`; + * Grab head and body separately as using clone on entire document into a div + * removes the head and body tags while grabbing html in them. Instead we + * capture them separately and concatenate them here with head and body tags + * preserved. + */ + const contentCopyHtml = `${document.head.innerHTML}${contentCopyEl.innerHTML}` // Cleaning up the copy element - contentCopyEl.remove(); - return contentCopyHtml; + contentCopyEl.remove() + return contentCopyHtml } - function createBackdrop () { - const backdropEl = document.createElement('div'); - backdropEl.className = 'webext-omnivore-backdrop'; + function createBackdrop() { + const backdropEl = document.createElement('div') + backdropEl.className = 'webext-omnivore-backdrop' backdropEl.style.cssText = `all: initial !important; position: fixed !important; top: 0 !important; @@ -164,74 +170,171 @@ transition: opacity 0.3s !important; -webkit-backdrop-filter: blur(4px) !important; backdrop-filter: blur(4px) !important; - `; - return backdropEl; + ` + return backdropEl } - function clearExistingBackdrops () { - const backdropCol = document.querySelectorAll('.webext-omnivore-backdrop'); + const getQuoteText = (containerNode) => { + const nonParagraphTagsRegEx = + /^(a|b|basefont|bdo|big|em|font|i|s|small|span|strike|strong|su[bp]|tt|u|code|mark)$/i + + let textResult = '' + let newParagraph = false + + const getTextNodes = (node) => { + let isPre = false + const nodeElement = + node instanceof HTMLElement ? node : node.parentElement + if (nodeElement) { + isPre = window + .getComputedStyle(nodeElement) + .whiteSpace.startsWith('pre') + } + + if (node.nodeType == 3) { + const text = isPre ? node.nodeValue : node.nodeValue.replace(/\n/g, '') + textResult += text + } else if (node != containerNode) { + if (!nonParagraphTagsRegEx.test(node.tagName)) { + textResult += '\n\n' + } + } + + const children = node.childNodes + children.forEach(function (child) { + getTextNodes(child) + }) + } + + getTextNodes(containerNode) + + return textResult.trim() + } + + const markHighlightSelection = () => { + // First remove any previous markers, this would only normally happen during debugging + try { + const markers = window.document.querySelectorAll( + `span[data-omnivore-highlight-start="true"], + span[data-omnivore-highlight-end="true"]` + ) + + for (let i = 0; i < markers.length; i++) { + markers[i].remove() + } + } catch (error) { + console.log('remove marker error: ', error) + // This should be OK + } + try { + const sel = window.getSelection() + if (sel.rangeCount) { + const range = sel.getRangeAt(0) + const endMarker = document.createElement('span') + const startMarker = document.createElement('span') + endMarker.setAttribute('data-omnivore-highlight-end', 'true') + startMarker.setAttribute('data-omnivore-highlight-start', 'true') + + var container = document.createElement('div') + for (var i = 0, len = sel.rangeCount; i < len; ++i) { + container.appendChild(sel.getRangeAt(i).cloneContents()) + } + + const endRange = range.cloneRange() + endRange.collapse(false) + endRange.insertNode(endMarker) + + range.insertNode(startMarker) + + return { + highlightHTML: container.innerHTML, + highlightText: getQuoteText(container), + } + } + } catch (error) { + console.log('get text error', error) + } + return null + } + + function clearExistingBackdrops() { + const backdropCol = document.querySelectorAll('.webext-omnivore-backdrop') for (let i = 0; i < backdropCol.length; i++) { - const backdropEl = backdropCol[i]; - backdropEl.style.setProperty('opacity', '0', 'important'); + const backdropEl = backdropCol[i] + backdropEl.style.setProperty('opacity', '0', 'important') } setTimeout(() => { for (let i = 0; i < backdropCol.length; i++) { - backdropCol[i].remove(); + backdropCol[i].remove() } - }, 0.5e3); + }, 0.5e3) } - async function prepareContent () { - const pdfContent = await grabPdfContent(); + async function prepareContent(createHighlight) { + const pdfContent = await grabPdfContent() if (pdfContent) { return pdfContent } - const url = window.location.href; + const url = window.location.href try { - if (handleBackendUrl(url)) { + if (!createHighlight && handleBackendUrl(url)) { return { type: 'url' } } } catch { console.log('error checking url') } - async function scrollPage (url) { - const scrollingEl = (document.scrollingElement || document.body); - const lastScrollPos = scrollingEl.scrollTop; - const currentScrollHeight = scrollingEl.scrollHeight; + console.log('get content: ', createHighlight) + if (createHighlight) { + console.log('creating highlight while saving') + const highlightSelection = markHighlightSelection() + console.log('highlightSelection', highlightSelection) + } + + async function scrollPage(url) { + const scrollingEl = document.scrollingElement || document.body + const lastScrollPos = scrollingEl.scrollTop + const currentScrollHeight = scrollingEl.scrollHeight /* add blurred overlay while scrolling */ - clearExistingBackdrops(); + clearExistingBackdrops() - const backdropEl = createBackdrop(); - document.body.appendChild(backdropEl); + const backdropEl = createBackdrop() + document.body.appendChild(backdropEl) /* * check below compares scrollTop against initial page height to handle * pages with infinite scroll else we shall be infinitely scrolling here. * stop scrolling if the url has changed in the meantime. */ - while (scrollingEl.scrollTop <= (currentScrollHeight - 500) && window.location.href === url) { - const prevScrollTop = scrollingEl.scrollTop; - scrollingEl.scrollTop += 500; + while ( + scrollingEl.scrollTop <= currentScrollHeight - 500 && + window.location.href === url + ) { + const prevScrollTop = scrollingEl.scrollTop + scrollingEl.scrollTop += 500 /* sleep upon scrolling position change for event loop to handle events from scroll */ - await (new Promise((resolve) => { setTimeout(resolve, 10); })); + await new Promise((resolve) => { + setTimeout(resolve, 10) + }) if (scrollingEl.scrollTop === prevScrollTop) { /* break out scroll loop if we are not able to scroll for any reason */ // console.log('breaking out scroll loop', scrollingEl.scrollTop, currentScrollHeight); - break; + break } } - scrollingEl.scrollTop = lastScrollPos; + scrollingEl.scrollTop = lastScrollPos /* sleep upon scrolling position change for event loop to handle events from scroll */ - await (new Promise((resolve) => { setTimeout(resolve, 10); })); + await new Promise((resolve) => { + setTimeout(resolve, 10) + }) } - await scrollPage(url); + await scrollPage(url) - clearExistingBackdrops(); - return { type: 'html', content: prepareContentPostScroll() }; + clearExistingBackdrops() + return { type: 'html', content: prepareContentPostScroll() } } - window.prepareContent = prepareContent; -})(); + window.prepareContent = prepareContent +})() diff --git a/pkg/extension/src/scripts/content/toast.js b/pkg/extension/src/scripts/content/toast.js index 31a519f41..8c1d6f9fd 100644 --- a/pkg/extension/src/scripts/content/toast.js +++ b/pkg/extension/src/scripts/content/toast.js @@ -192,8 +192,18 @@ function updateLabelsFromCache(payload) { ;(async () => { await getStorageItem('labels').then((cachedLabels) => { + if (labels) { + const selectedLabels = labels.filter((l) => l.selected) + selectedLabels.forEach((l) => { + const cached = cachedLabels.find((cached) => cached.name == l.name) + if (cached) { + cached.selected = true + } else { + cachedLabels.push(l) + } + }) + } labels = cachedLabels - console.log(' == updated labels', cachedLabels) }) })() } @@ -279,6 +289,12 @@ } function toggleRow(rowId) { + if (!currentToastEl) { + // its possible this was called after closing the extension + // so just return + return + } + const container = currentToastEl.shadowRoot.querySelector(rowId) const initialState = container?.getAttribute('data-state') const rows = currentToastEl.shadowRoot.querySelectorAll( @@ -538,7 +554,15 @@ } } - function addNote() { + function noteCacheKey() { + return document.location + ? `cached-note-${document.location.href}` + : undefined + } + + async function addNote() { + const cachedNoteKey = noteCacheKey() + cancelAutoDismiss() toggleRow('#omnivore-add-note-row') @@ -547,7 +571,24 @@ ) if (noteArea) { - noteArea.focus() + if (cachedNoteKey) { + const existingNote = await getStorageItem(cachedNoteKey) + noteArea.value = existingNote + } + + if (noteArea.value) { + noteArea.select() + } else { + noteArea.focus() + } + + noteArea.addEventListener('input', (event) => { + ;(async () => { + const note = {} + note[cachedNoteKey] = event.target.value + await setStorage(note) + })() + }) noteArea.onkeydown = (e) => { e.cancelBubble = true @@ -587,7 +628,9 @@ }) event.preventDefault() - event.stopPropogation() + if (event.stopPropogation) { + event.stopPropogation() + } } } @@ -745,27 +788,32 @@ rowElement.setAttribute('data-label-selected', 'off') } - updateLabels() + syncLabelChanges() } function syncLabelChanges() { - console.log('syncLabels') - updateStatusBox( '#omnivore-edit-labels-status', 'loading', 'Updating Labels...', undefined ) - const labelIds = labels.filter((l) => l['selected']).map((l) => l.id) + const setLabels = labels + .filter((l) => l['selected']) + .map((l) => { + return { + name: l.name, + color: l.color, + } + }) - // browserApi.runtime.sendMessage({ - // - action: ACTIONS.SetLabels, - // - payload: { - // - ctx: ctx, - // - labelIds: labelIds, - // - }, - // - }) + browserApi.runtime.sendMessage({ + action: ACTIONS.SetLabels, + payload: { + ctx: ctx, + labels: setLabels, + }, + }) } async function editLabels() { @@ -799,10 +847,14 @@ if (list) { list.innerHTML = '' - labels.forEach(function (label, idx) { - const rowHtml = createLabelRow(label) - list.appendChild(rowHtml) - }) + labels + .sort((a, b) => + a.name.localeCompare(b.name, undefined, { sensitivity: 'base' }) + ) + .forEach(function (label, idx) { + const rowHtml = createLabelRow(label) + list.appendChild(rowHtml) + }) } } @@ -817,15 +869,22 @@ .filter( (l) => l.name.toLowerCase().indexOf(filterValue.toLowerCase()) > -1 ) + .sort((a, b) => + a.name.localeCompare(b.name, undefined, { sensitivity: 'base' }) + ) .forEach(function (label) { const rowHtml = createLabelRow(label) list.appendChild(rowHtml) }) } else { - labels.forEach(function (label) { - const rowHtml = createLabelRow(label) - list.appendChild(rowHtml) - }) + labels + .sort((a, b) => + a.name.localeCompare(b.name, undefined, { sensitivity: 'base' }) + ) + .forEach(function (label) { + const rowHtml = createLabelRow(label) + list.appendChild(rowHtml) + }) } } }