From 006da91444fe1afda16b976982abbe974a9f43ec Mon Sep 17 00:00:00 2001 From: JT Smith Date: Thu, 27 Aug 2026 18:31:09 -0600 Subject: [PATCH] fix(messenger): restore unread badge and support both messenger.com and facebook.com/messages Reverts serviceURL to https://messenger.com to fix login for messenger-only accounts (fixes ferdium/ferdium-app#2397) and restores unread counting that broke after moving to facebook.com/messages (fixes ferdium/ferdium-app#2416). On facebook.com/messages the thread list does not expose unread via the old messenger.com selectors. The new webview: - fixes crash where querySelector(...).ariaLabel threw on null - supports both /t/ and /messages/t/ prefixes (and requests/marketplace) - adds facebook.com/messages detection via [data-testid="mwthreadlist-item"] with unread indicators (.is6700om, .lrazzd5p, .o48pnaf2 etc.) - keeps legacy .bp9cbjyn fallback for messenger.com - adds generic aria-label and document.title fallbacks - handles message-requests via additional selectors - wraps counting in try/catch so loop never breaks Bump version 1.8.6 -> 1.8.7. --- recipes/messenger/package.json | 4 +- recipes/messenger/webview.js | 185 ++++++++++++++++++++++++++------- 2 files changed, 149 insertions(+), 40 deletions(-) diff --git a/recipes/messenger/package.json b/recipes/messenger/package.json index b5c4b6266..75997f860 100644 --- a/recipes/messenger/package.json +++ b/recipes/messenger/package.json @@ -1,10 +1,10 @@ { "id": "messenger", "name": "Messenger", - "version": "1.8.6", + "version": "1.8.7", "license": "MIT", "config": { - "serviceURL": "https://facebook.com/messages", + "serviceURL": "https://messenger.com", "hasNotificationSound": true } } diff --git a/recipes/messenger/webview.js b/recipes/messenger/webview.js index 781436b5a..264c7ede6 100644 --- a/recipes/messenger/webview.js +++ b/recipes/messenger/webview.js @@ -17,52 +17,161 @@ function hideInstallMessage() { module.exports = (Ferdium, settings) => { const getMessages = () => { let count = 0; - let newMessengerUI = false; - - /* - * try the counting with the new UI - */ - for (let href of ['/', '/requests/', '/marketplace/']) { - const elem = document.querySelector( - `a[href^='${href}t/'][role='link'][tabindex='0']`, - ).ariaLabel; - if (elem) { - newMessengerUI = true; - const match = elem.match(/(\d+)/g); - if (match) { - count += Ferdium.safeParseInt(match[0]); + + try { + let newMessengerUI = false; + let newUICount = 0; + + /* + * Try the counting with the new Messenger.com UI via ariaLabel on + * section tabs. Supports both messenger.com (/t/) and + * facebook.com/messages (/messages/t/) paths. + * Original code crashed when selector returned null ( accessing .ariaLabel + * on null). Use optional chaining and support both URL prefixes. + */ + const hrefPrefixes = [ + '/t/', + '/messages/t/', + '/requests/t/', + '/messages/requests/t/', + '/marketplace/t/', + '/messages/marketplace/t/', + ]; + for (const prefix of hrefPrefixes) { + const anchor = document.querySelector( + `a[href^='${prefix}'][role='link'][tabindex='0']`, + ); + const label = + anchor?.ariaLabel || anchor?.getAttribute?.('aria-label') || null; + if (label) { + newMessengerUI = true; + const match = label.match(/(\d+)/g); + if (match) { + newUICount += Ferdium.safeParseInt(match[0]); + } } } - } - /* - * do the old counting if the interface is not the last one - */ - if (!newMessengerUI) { - count = [ - ...document.querySelectorAll( - '.bp9cbjyn.j83agx80.owycx6da:not(.btwxx1t3)', - ), - ] - .map(elem => { - const hasPing = !!elem.querySelector( - '.pq6dq46d.is6700om.qu0x051f.esr5mh6w.e9989ue4.r7d6kgcz.s45kfl79.emlxlaya.bkmhp75w.spb7xbtv.cyypbtt7.fwizqjfa', + // Also try generic query for any tab with aria-label containing a number + // e.g. Facebook's messenger tabs may use different href patterns + if (!newMessengerUI) { + const genericTabs = document.querySelectorAll( + 'a[role="link"][tabindex="0"][aria-label]', + ); + for (const tab of genericTabs) { + const label = tab.ariaLabel || tab.getAttribute('aria-label'); + if (label && /unread/i.test(label) && /(\d+)/.test(label)) { + newMessengerUI = true; + const m = label.match(/(\d+)/g); + if (m) newUICount += Ferdium.safeParseInt(m[0]); + } + } + } + + if (newMessengerUI) { + count = newUICount; + } else { + // Strategy 2: facebook.com/messages thread list uses + // [data-testid="mwthreadlist-item"] for each conversation. + // Unread threads contain a blue dot / bold indicator with classes + // like is6700om / o48pnaf2 / lrazzd5p etc. Check those plus + // aria-label fallback. + const threadItems = document.querySelectorAll( + '[data-testid="mwthreadlist-item"]', + ); + if (threadItems.length > 0) { + let threadCount = 0; + for (const node of threadItems) { + const hasUnread = + !!node.querySelector( + '.lrazzd5p, .is6700om, .o48pnaf2, .pq6dq46d.is6700om.qu0x051f.esr5mh6w.e9989ue4.r7d6kgcz.s45kfl79.emlxlaya.bkmhp75w.spb7xbtv.cyypbtt7.fwizqjfa, [aria-label*="Unread"], [aria-label*="unread"]', + ) || + (node.getAttribute('aria-label') || '') + .toLowerCase() + .includes('unread'); + const isMuted = !!node.querySelector( + '.a8c37x1j.ms05siws.l3qrxjdp.b7h9ocf4.trssfv1o, [aria-label*="Muted"], [aria-label*="muted"]', + ); + if (hasUnread && !isMuted) threadCount += 1; + } + if (threadCount > 0) { + count = threadCount; + } + } + + // Strategy 3: Legacy messenger.com selectors (fallback if data-testid not present) + if (count === 0) { + const legacyItems = document.querySelectorAll( + '.bp9cbjyn.j83agx80.owycx6da:not(.btwxx1t3)', ); - const isMuted = !!elem.querySelector( - '.a8c37x1j.ms05siws.l3qrxjdp.b7h9ocf4.trssfv1o', + if (legacyItems.length > 0) { + count = [...legacyItems] + .map(elem => { + const hasPing = !!elem.querySelector( + '.pq6dq46d.is6700om.qu0x051f.esr5mh6w.e9989ue4.r7d6kgcz.s45kfl79.emlxlaya.bkmhp75w.spb7xbtv.cyypbtt7.fwizqjfa, .is6700om, .lrazzd5p, .o48pnaf2', + ); + const isMuted = !!elem.querySelector( + '.a8c37x1j.ms05siws.l3qrxjdp.b7h9ocf4.trssfv1o', + ); + const ariaUnread = (elem.getAttribute('aria-label') || '') + .toLowerCase() + .includes('unread'); + return (hasPing || ariaUnread) && !isMuted; + }) + .reduce((prev, curr) => prev + curr, 0); + } + } + + // Strategy 4: Generic unread aria-label counting (works on both domains) + if (count === 0) { + const unreadByAria = document.querySelectorAll( + '[aria-label*="Unread"]', ); + // Filter out muted and non-thread items heuristically + if (unreadByAria.length > 0 && unreadByAria.length < 100) { + let ariaCount = 0; + for (const el of unreadByAria) { + const label = el.getAttribute('aria-label') || ''; + // Count only thread-like entries, not buttons + if (/unread/i.test(label)) { + // Avoid counting the same thread twice via nested elements + const isThread = + el.closest('[data-testid="mwthreadlist-item"]') || + el.closest('.bp9cbjyn.j83agx80.owycx6da') || + el.matches('[data-testid="mwthreadlist-item"]'); + if (isThread) ariaCount += 1; + } + } + if (ariaCount > 0) count = ariaCount; + } + } - return hasPing && !isMuted; - }) - .reduce((prev, curr) => prev + curr, 0); + // Strategy 5: Title fallback e.g. "(2) Messenger" or "(1) Facebook" + if (count === 0) { + const titleMatch = document.title.match(/^\((\d+)\)/); + if (titleMatch) { + count = Ferdium.safeParseInt(titleMatch[1]); + } + } - /* - * add count of message requests on top of notification counter - */ - const messageRequestsElement = document.querySelector('._5nxf'); - if (messageRequestsElement) { - count += Ferdium.safeParseInt(messageRequestsElement.textContent); + /* + * add count of message requests on top of notification counter + */ + const messageRequestsElement = document.querySelector( + '._5nxf, [data-testid="message-requests-count"], a[href*="requests"] [aria-label*="request" i]', + ); + if (messageRequestsElement) { + const txt = + messageRequestsElement.textContent || + messageRequestsElement.getAttribute('aria-label') || + ''; + const req = Ferdium.safeParseInt(txt); + if (!Number.isNaN(req) && req > 0) count += req; + } } + } catch (error) { + // Never break badge loop on selector errors + console.error('Messenger getMessages error:', error); } Ferdium.setBadge(count);