From ed9db10ba147aea9dc5731c7c8e70e719c478770 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Guillot?= Date: Sat, 2 Aug 2025 17:16:40 -0700 Subject: [PATCH 1/5] refactor(js): combine `handleShare()` and `triggerWebShare()` functions --- internal/ui/static/js/app.js | 42 ++++++++++++++---------------------- 1 file changed, 16 insertions(+), 26 deletions(-) diff --git a/internal/ui/static/js/app.js b/internal/ui/static/js/app.js index e53fdbf85d9..525f1785730 100644 --- a/internal/ui/static/js/app.js +++ b/internal/ui/static/js/app.js @@ -323,35 +323,25 @@ function goToListItem(offset) { * If the share status is "share", it will send an Ajax request to fetch the share URL and then trigger the Web Share API. * If the Web Share API is not supported, it will redirect to the entry URL. */ -async function handleShare() { +async function handleEntryShareAction() { const link = document.querySelector(':is(a, button)[data-share-status]'); if (link.dataset.shareStatus === "shared") { const title = document.querySelector(".entry-header > h1 > a"); - await triggerWebShare(title, link.href); - } -} + const url = link.href; -/** - * Trigger the Web Share API to share the entry. - * - * If the Web Share API is not supported, it will redirect to the entry URL. - * - * @param {Element} title - The title element of the entry. - * @param {string} url - The URL of the entry to share. - */ -async function triggerWebShare(title, url) { - if (!navigator.canShare) { - console.error("Your browser doesn't support the Web Share API."); - window.location = url; - return; - } - try { - await navigator.share({ - title: title ? title.textContent : url, - url: url - }); - } catch (err) { - console.error(err); + if (!navigator.canShare) { + console.error("Your browser doesn't support the Web Share API."); + window.location = url; + return; + } + try { + await navigator.share({ + title: title ? title.textContent : url, + url: url + }); + } catch (err) { + console.error(err); + } } } @@ -1189,7 +1179,7 @@ function initializeClickHandlers() { onClick(":is(a, button)[data-toggle-bookmark]", (event) => handleBookmark(event.target)); onClick(":is(a, button)[data-toggle-status]", (event) => handleEntryStatus("next", event.target)); onClick(":is(a, button)[data-fetch-content-entry]", handleFetchOriginalContent); - onClick(":is(a, button)[data-share-status]", handleShare); + onClick(":is(a, button)[data-share-status]", handleEntryShareAction); // Page actions with confirmation onClick(":is(a, button)[data-action=markPageAsRead]", (event) => From 85000567a0ebc821df5abfb53f59c802e1c38fe9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Guillot?= Date: Sat, 2 Aug 2025 17:23:18 -0700 Subject: [PATCH 2/5] refactor(js): split `openOriginalLink()` into smaller functions --- internal/ui/static/js/app.js | 80 ++++++++++++++++++++++-------------- 1 file changed, 49 insertions(+), 31 deletions(-) diff --git a/internal/ui/static/js/app.js b/internal/ui/static/js/app.js index 525f1785730..11e2581d4e9 100644 --- a/internal/ui/static/js/app.js +++ b/internal/ui/static/js/app.js @@ -556,21 +556,6 @@ function toggleEntryStatus(element, toasting) { }); } -/** - * Mark the entry as read if it is currently unread. - * - * @param {Element} element The entry element to mark as read. - */ -function markEntryAsRead(element) { - if (element.classList.contains("item-status-unread")) { - element.classList.remove("item-status-unread"); - element.classList.add("item-status-read"); - - const entryID = parseInt(element.dataset.id, 10); - updateEntriesStatus([entryID], "read"); - } -} - /** * Handle the refresh of all feeds. * @@ -725,26 +710,59 @@ function handleFetchOriginalContent() { * @returns {void} */ function openOriginalLink(openLinkInCurrentTab) { + if (isEntryView()) { + openOriginalLinkFromEntryView(openLinkInCurrentTab); + } else if (isListView()) { + openOriginalLinkFromListView(); + } +} + +/** + * Open the original link from entry view. + * + * @param {boolean} openLinkInCurrentTab - Whether to open the link in the current tab. + * @returns {void} + */ +function openOriginalLinkFromEntryView(openLinkInCurrentTab) { const entryLink = document.querySelector(".entry h1 a"); - if (entryLink) { - if (openLinkInCurrentTab) { - window.location.href = entryLink.getAttribute("href"); - } else { - openNewTab(entryLink.getAttribute("href")); - } - return; + if (!entryLink) return; + + const url = entryLink.getAttribute("href"); + if (openLinkInCurrentTab) { + window.location.href = url; + } else { + openNewTab(url); } +} + +/** + * Open the original link from list view. + * + * @returns {void} + */ +function openOriginalLinkFromListView() { + const currentItem = document.querySelector(".current-item"); + const originalLink = currentItem?.querySelector(":is(a, button)[data-original-link]"); - const currentItemOriginalLink = document.querySelector(".current-item :is(a, button)[data-original-link]"); - if (currentItemOriginalLink) { - openNewTab(currentItemOriginalLink.getAttribute("href")); + if (!currentItem || !originalLink) return; - const currentItem = document.querySelector(".current-item"); - // If we are not on the list of starred items, move to the next item - if (document.location.href !== document.querySelector(':is(a, button)[data-page=starred]').href) { - goToListItem(1); - } - markEntryAsRead(currentItem); + // Open the link + openNewTab(originalLink.getAttribute("href")); + + // Don't navigate or mark as read on starred page + const isStarredPage = document.location.href === document.querySelector(':is(a, button)[data-page=starred]').href; + if (isStarredPage) return; + + // Navigate to next item + goToListItem(1); + + // Mark as read if currently unread + if (currentItem.classList.contains("item-status-unread")) { + currentItem.classList.remove("item-status-unread"); + currentItem.classList.add("item-status-read"); + + const entryID = parseInt(currentItem.dataset.id, 10); + updateEntriesStatus([entryID], "read"); } } From 76304d688df2b6a3a0ce9179a83084550ef22bbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Guillot?= Date: Sat, 2 Aug 2025 17:46:24 -0700 Subject: [PATCH 3/5] refactor(js): combine `handleSaveEntry()` and `saveEntry()` functions --- internal/ui/static/js/app.js | 57 ++++++++++++++---------------------- 1 file changed, 22 insertions(+), 35 deletions(-) diff --git a/internal/ui/static/js/app.js b/internal/ui/static/js/app.js index 11e2581d4e9..782beb1e486 100644 --- a/internal/ui/static/js/app.js +++ b/internal/ui/static/js/app.js @@ -148,12 +148,17 @@ function findEntry(element) { * * @param {Element} parentElement The parent element to insert the icon label into. * @param {string} iconLabelText The text to display in the icon label. + * @param {boolean} clearParentTextcontent If true, clear the parent's text content before appending the icon label. * @returns {void} */ -function insertIconLabelElement(parentElement, iconLabelText) { +function insertIconLabelElement(parentElement, iconLabelText, clearParentTextcontent = true) { const span = document.createElement('span'); span.classList.add('icon-label'); span.textContent = iconLabelText; + + if (clearParentTextcontent) { + parentElement.textContent = ''; + } parentElement.appendChild(span); } @@ -542,7 +547,7 @@ function toggleEntryStatus(element, toasting) { } link.replaceChildren(iconElement.content.cloneNode(true)); - insertIconLabelElement(link, label); + insertIconLabelElement(link, label, false); link.dataset.value = newStatus; if (element.classList.contains("item-status-" + currentStatus)) { @@ -561,7 +566,7 @@ function toggleEntryStatus(element, toasting) { * * This function redirects the user to the URL specified in the data-refresh-all-feeds-url attribute of the body element. */ -function handleRefreshAllFeeds() { +function handleRefreshAllFeedsAction() { const refreshAllFeedsUrl = document.body.dataset.refreshAllFeedsUrl; if (refreshAllFeedsUrl) { window.location.href = refreshAllFeedsUrl; @@ -589,38 +594,22 @@ function updateEntriesStatus(entryIDs, status, callback) { /** * Handle save entry from list view and entry view. * - * @param {Element} element + * @param {Element|null} element - The element that triggered the save action (optional). */ -function handleSaveEntry(element) { - const toasting = !element; +function handleSaveEntryAction(element = null) { const currentEntry = findEntry(element); - if (currentEntry) { - saveEntry(currentEntry.querySelector(":is(a, button)[data-save-entry]"), toasting); - } -} + if (!currentEntry) return; -/** - * Save the entry by sending an Ajax request to the server. - * - * @param {Element} element The element that triggered the save action. - * @param {boolean} toasting If true, show a toast notification after saving the entry. - * @return {void} - */ -function saveEntry(element, toasting) { - if (!element || element.dataset.completed) { - return; - } + const buttonElement = currentEntry.querySelector(":is(a, button)[data-save-entry]"); + if (!buttonElement || buttonElement.dataset.completed) return; - element.textContent = ""; - insertIconLabelElement(element, element.dataset.labelLoading); + insertIconLabelElement(buttonElement, buttonElement.dataset.labelLoading); - sendPOSTRequest(element.dataset.saveUrl).then(() => { - element.textContent = ""; - insertIconLabelElement(element, element.dataset.labelDone); - element.dataset.completed = "true"; - if (toasting) { - const iconElement = document.querySelector("template#icon-save"); - showToast(element.dataset.toastDone, iconElement); + sendPOSTRequest(buttonElement.dataset.saveUrl).then(() => { + insertIconLabelElement(buttonElement, buttonElement.dataset.labelDone); + buttonElement.dataset.completed = "true"; + if (!element) { + showToast(buttonElement.dataset.toastDone, document.querySelector("template#icon-save")); } }); } @@ -649,7 +638,6 @@ function toggleBookmark(parentElement, toasting) { const buttonElement = parentElement.querySelector(":is(a, button)[data-toggle-bookmark]"); if (!buttonElement) return; - buttonElement.textContent = ""; insertIconLabelElement(buttonElement, buttonElement.dataset.labelLoading); sendPOSTRequest(buttonElement.dataset.bookmarkUrl).then(() => { @@ -684,7 +672,6 @@ function handleFetchOriginalContent() { const previousElement = buttonElement.cloneNode(true); - buttonElement.textContent = ""; insertIconLabelElement(buttonElement, buttonElement.dataset.labelLoading); sendPOSTRequest(buttonElement.dataset.fetchContentUrl).then((response) => { @@ -1157,13 +1144,13 @@ function initializeKeyboardShortcuts() { keyboardHandler.on("m", () => handleEntryStatus("next")); keyboardHandler.on("M", () => handleEntryStatus("previous")); keyboardHandler.on("A", markPageAsRead); - keyboardHandler.on("s", () => handleSaveEntry()); + keyboardHandler.on("s", () => handleSaveEntryAction()); keyboardHandler.on("d", handleFetchOriginalContent); keyboardHandler.on("f", () => handleBookmark()); // Feed actions keyboardHandler.on("F", goToFeedPage); - keyboardHandler.on("R", handleRefreshAllFeeds); + keyboardHandler.on("R", handleRefreshAllFeedsAction); keyboardHandler.on("+", goToAddSubscriptionPage); keyboardHandler.on("#", unsubscribeFromFeed); @@ -1193,7 +1180,7 @@ function initializeTouchHandler() { */ function initializeClickHandlers() { // Entry actions - onClick(":is(a, button)[data-save-entry]", (event) => handleSaveEntry(event.target)); + onClick(":is(a, button)[data-save-entry]", (event) => handleSaveEntryAction(event.target)); onClick(":is(a, button)[data-toggle-bookmark]", (event) => handleBookmark(event.target)); onClick(":is(a, button)[data-toggle-status]", (event) => handleEntryStatus("next", event.target)); onClick(":is(a, button)[data-fetch-content-entry]", handleFetchOriginalContent); From 7b70666c34ab2b31d70b8887bf67300c5ec452d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Guillot?= Date: Sat, 2 Aug 2025 17:56:23 -0700 Subject: [PATCH 4/5] refactor(js): combine `handleBookmark` and `toggleBookmark` functions --- internal/ui/static/js/app.js | 32 ++++++++++---------------------- 1 file changed, 10 insertions(+), 22 deletions(-) diff --git a/internal/ui/static/js/app.js b/internal/ui/static/js/app.js index 782beb1e486..8ce166ff38f 100644 --- a/internal/ui/static/js/app.js +++ b/internal/ui/static/js/app.js @@ -619,23 +619,11 @@ function handleSaveEntryAction(element = null) { * * @param {Element} element - The element that triggered the bookmark action. */ -function handleBookmark(element) { - const toasting = !element; +function handleBookmarkAction(element) { const currentEntry = findEntry(element); - if (currentEntry) { - toggleBookmark(currentEntry, toasting); - } -} + if (!currentEntry) return; -/** - * Toggle the bookmark status of an entry. - * - * @param {Element} parentElement - The parent element containing the bookmark button. - * @param {boolean} toasting - Whether to show a toast notification. - * @returns {void} - */ -function toggleBookmark(parentElement, toasting) { - const buttonElement = parentElement.querySelector(":is(a, button)[data-toggle-bookmark]"); + const buttonElement = currentEntry.querySelector(":is(a, button)[data-toggle-bookmark]"); if (!buttonElement) return; insertIconLabelElement(buttonElement, buttonElement.dataset.labelLoading); @@ -648,14 +636,14 @@ function toggleBookmark(parentElement, toasting) { const iconElement = document.querySelector(isStarred ? "template#icon-star" : "template#icon-unstar"); const label = isStarred ? buttonElement.dataset.labelStar : buttonElement.dataset.labelUnstar; - if (toasting) { + buttonElement.replaceChildren(iconElement.content.cloneNode(true)); + insertIconLabelElement(buttonElement, label, false); + buttonElement.dataset.value = newStarStatus; + + if (!element) { const toastKey = isStarred ? "toastUnstar" : "toastStar"; showToast(buttonElement.dataset[toastKey], iconElement); } - - buttonElement.replaceChildren(iconElement.content.cloneNode(true)); - insertIconLabelElement(buttonElement, label); - buttonElement.dataset.value = newStarStatus; }); } @@ -1146,7 +1134,7 @@ function initializeKeyboardShortcuts() { keyboardHandler.on("A", markPageAsRead); keyboardHandler.on("s", () => handleSaveEntryAction()); keyboardHandler.on("d", handleFetchOriginalContent); - keyboardHandler.on("f", () => handleBookmark()); + keyboardHandler.on("f", () => handleBookmarkAction()); // Feed actions keyboardHandler.on("F", goToFeedPage); @@ -1181,7 +1169,7 @@ function initializeTouchHandler() { function initializeClickHandlers() { // Entry actions onClick(":is(a, button)[data-save-entry]", (event) => handleSaveEntryAction(event.target)); - onClick(":is(a, button)[data-toggle-bookmark]", (event) => handleBookmark(event.target)); + onClick(":is(a, button)[data-toggle-bookmark]", (event) => handleBookmarkAction(event.target)); onClick(":is(a, button)[data-toggle-status]", (event) => handleEntryStatus("next", event.target)); onClick(":is(a, button)[data-fetch-content-entry]", handleFetchOriginalContent); onClick(":is(a, button)[data-share-status]", handleEntryShareAction); From 4f389cb33a0439408ea451bbaeb14a7689bfe4e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Guillot?= Date: Sat, 2 Aug 2025 18:04:40 -0700 Subject: [PATCH 5/5] refactor(js): rename functions to include action suffix --- internal/ui/static/js/app.js | 43 ++++++++++++++++++------------------ 1 file changed, 21 insertions(+), 22 deletions(-) diff --git a/internal/ui/static/js/app.js b/internal/ui/static/js/app.js index 8ce166ff38f..910b6ea42dd 100644 --- a/internal/ui/static/js/app.js +++ b/internal/ui/static/js/app.js @@ -453,7 +453,7 @@ function initializeFormHandlers() { /** * Show the keyboard shortcuts modal. */ -function showKeyboardShortcuts() { +function showKeyboardShortcutsAction() { const template = document.getElementById("keyboard-shortcuts"); ModalHandler.open(template.content, "dialog-title"); } @@ -461,7 +461,7 @@ function showKeyboardShortcuts() { /** * Mark all visible entries on the current page as read. */ -function markPageAsRead() { +function markPageAsReadAction() { const items = getVisibleEntries(); if (items.length === 0) return; @@ -652,7 +652,7 @@ function handleBookmarkAction(element) { * * @returns {void} */ -function handleFetchOriginalContent() { +function handleFetchOriginalContentAction() { if (isListView()) return; const buttonElement = document.querySelector(":is(a, button)[data-fetch-content-entry]"); @@ -684,7 +684,7 @@ function handleFetchOriginalContent() { * @param {boolean} openLinkInCurrentTab - Whether to open the link in the current tab. * @returns {void} */ -function openOriginalLink(openLinkInCurrentTab) { +function openOriginalLinkAction(openLinkInCurrentTab) { if (isEntryView()) { openOriginalLinkFromEntryView(openLinkInCurrentTab); } else if (isListView()) { @@ -747,7 +747,7 @@ function openOriginalLinkFromListView() { * @param {boolean} openLinkInCurrentTab - Whether to open the link in the current tab. * @returns {void} */ -function openCommentLink(openLinkInCurrentTab) { +function openCommentLinkAction(openLinkInCurrentTab) { const entryLink = document.querySelector(isListView() ? ".current-item :is(a, button)[data-comments-link]" : ":is(a, button)[data-comments-link]"); if (entryLink) { @@ -765,7 +765,7 @@ function openCommentLink(openLinkInCurrentTab) { * If the current view is a list view, it will navigate to the link of the currently selected item. * If the current view is an entry view, it will navigate to the link of the entry. */ -function openSelectedItem() { +function openSelectedItemAction() { const currentItemLink = document.querySelector(".current-item .item-title a"); if (currentItemLink) { window.location.href = currentItemLink.getAttribute("href"); @@ -775,7 +775,7 @@ function openSelectedItem() { /** * Unsubscribe from the feed of the currently selected item. */ -function unsubscribeFromFeed() { +function handleRemoveFeedAction() { const unsubscribeLink = document.querySelector("[data-action=remove-feed]"); if (unsubscribeLink) { sendPOSTRequest(unsubscribeLink.dataset.url).then(() => { @@ -787,7 +787,7 @@ function unsubscribeFromFeed() { /** * Scroll the page to the currently selected item. */ -function scrollToCurrentItem() { +function scrollToCurrentItemAction() { const currentItem = document.querySelector(".current-item"); if (currentItem) { scrollPageTo(currentItem, true); @@ -1118,32 +1118,32 @@ function initializeKeyboardShortcuts() { keyboardHandler.on("n", goToNextPage); keyboardHandler.on("h", () => goToPage("previous")); keyboardHandler.on("l", () => goToPage("next")); - keyboardHandler.on("z t", scrollToCurrentItem); + keyboardHandler.on("z t", scrollToCurrentItemAction); // Item actions - keyboardHandler.on("o", openSelectedItem); - keyboardHandler.on("Enter", () => openSelectedItem()); - keyboardHandler.on("v", () => openOriginalLink(false)); - keyboardHandler.on("V", () => openOriginalLink(true)); - keyboardHandler.on("c", () => openCommentLink(false)); - keyboardHandler.on("C", () => openCommentLink(true)); + keyboardHandler.on("o", openSelectedItemAction); + keyboardHandler.on("Enter", () => openSelectedItemAction()); + keyboardHandler.on("v", () => openOriginalLinkAction(false)); + keyboardHandler.on("V", () => openOriginalLinkAction(true)); + keyboardHandler.on("c", () => openCommentLinkAction(false)); + keyboardHandler.on("C", () => openCommentLinkAction(true)); // Entry management keyboardHandler.on("m", () => handleEntryStatus("next")); keyboardHandler.on("M", () => handleEntryStatus("previous")); - keyboardHandler.on("A", markPageAsRead); + keyboardHandler.on("A", markPageAsReadAction); keyboardHandler.on("s", () => handleSaveEntryAction()); - keyboardHandler.on("d", handleFetchOriginalContent); + keyboardHandler.on("d", handleFetchOriginalContentAction); keyboardHandler.on("f", () => handleBookmarkAction()); // Feed actions keyboardHandler.on("F", goToFeedPage); keyboardHandler.on("R", handleRefreshAllFeedsAction); keyboardHandler.on("+", goToAddSubscriptionPage); - keyboardHandler.on("#", unsubscribeFromFeed); + keyboardHandler.on("#", handleRemoveFeedAction); // UI actions - keyboardHandler.on("?", showKeyboardShortcuts); + keyboardHandler.on("?", showKeyboardShortcutsAction); keyboardHandler.on("Escape", () => ModalHandler.close()); keyboardHandler.on("a", () => { const enclosureElement = document.querySelector('.entry-enclosures'); @@ -1171,12 +1171,11 @@ function initializeClickHandlers() { onClick(":is(a, button)[data-save-entry]", (event) => handleSaveEntryAction(event.target)); onClick(":is(a, button)[data-toggle-bookmark]", (event) => handleBookmarkAction(event.target)); onClick(":is(a, button)[data-toggle-status]", (event) => handleEntryStatus("next", event.target)); - onClick(":is(a, button)[data-fetch-content-entry]", handleFetchOriginalContent); + onClick(":is(a, button)[data-fetch-content-entry]", handleFetchOriginalContentAction); onClick(":is(a, button)[data-share-status]", handleEntryShareAction); // Page actions with confirmation - onClick(":is(a, button)[data-action=markPageAsRead]", (event) => - handleConfirmationMessage(event.target, markPageAsRead)); + onClick(":is(a, button)[data-action=markPageAsRead]", (event) => handleConfirmationMessage(event.target, markPageAsReadAction)); // Generic confirmation handler onClick(":is(a, button)[data-confirm]", (event) => {