Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 8 additions & 6 deletions recipes/alibaba-chat/webview.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@ function _interopRequireDefault(obj) {
const _path = _interopRequireDefault(require('path'));

module.exports = Ferdium => {
// TODO: If your SNAME service has unread messages, uncomment these lines to implement the logic for updating the badges
const getMessages = () => {
// TODO: Insert your notification-finding code here
const count = document.querySelector(
'#im-list > div > div.im-conversation-list-container > div.im-next-tabs.im-next-tabs-pure.im-next-tabs-scrollable.im-next-medium.list-tab > div.im-next-tabs-bar > div > div > div > ul > li:nth-child(2) > div > div > span.red-num',
);
Ferdium.setBadge(count, 0);
const unreadText = document.querySelector(
'.inbox-list-container .panel-content .option-item:last-child .item-unread-num',
)?.textContent?.trim() ?? '';

const unreadCount =
Number.parseInt(unreadText.replace(/[^\d]/g, ''), 10) || 0;

Ferdium.setBadge(unreadCount, 0);
};
Ferdium.loop(getMessages);

Expand Down
8 changes: 3 additions & 5 deletions recipes/clickup/webview.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
function _interopRequireDefault(obj) {
return obj && obj.__esModule ? obj : { default: obj };
}

const _path = _interopRequireDefault(require('path'));

module.exports = Ferdium => {
const getMessages = () => {
const unread = document.querySelector('.cu-notification-alert__dot');
Ferdium.setBadge(unread ? 1 : 0);
const counter = document.querySelector('[data-test=simple-bar-item-counter-chat]');
const unread = counter ? parseInt(counter.innerText, 10) || 0 : 0;
Ferdium.setBadge(unread);
};

Ferdium.loop(getMessages);

Ferdium.injectCSS(_path.default.join(__dirname, 'service.css'));
};
130 changes: 121 additions & 9 deletions recipes/hangouts/webview.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,133 @@
function _interopRequireDefault(obj) {
return obj && obj.__esModule ? obj : { default: obj };
}
const _path = require('path');

const _path = _interopRequireDefault(require('path'));
// Distinctive substring of Google Chat's "unread" aria-label, per locale.
// Lowercased, matched case-insensitively, chosen to survive singular/plural.
const UNREAD_TOKENS = [
'unread', // English
'sin leer', // Spanish
'não lid', // Portuguese
'non lu', // French
'ungelesene', // German
'non lett', // Italian
'непрочитанн', // Russian
'未読', // Japanese
'읽지 않은', // Korean
'未读', // Chinese (Simplified)
'未讀', // Chinese (Traditional)
'غير مقروء', // Arabic
'अपठित', // Hindi
'belum dibaca', // Indonesian
'ongelezen', // Dutch
'okunmamış', // Turkish
'nieprzeczytan', // Polish
];

module.exports = Ferdium => {
let updateScheduled = false;
let observer = null;

const isUnreadBadge = label => {
const normalizedLabel = label.toLowerCase();

return UNREAD_TOKENS.some(token =>
normalizedLabel.includes(token),
);
};

const getMessages = () => {
// get unread messages
let count = 0;
for (const span of document.querySelectorAll('span[jsname=DW2nlb]'))
count += Ferdium.safeParseInt(span.textContent);

// set Ferdium badge
for (const el of document.querySelectorAll('[aria-label]')) {
// Only roster section headers (Direct messages, Spaces, Apps...).
// Skip "Shortcuts" (type 10), since its badge aggregates everything
// and would double-count the individual section totals.
const section = el.closest('[data-section-type]');

if (
!section ||
section.getAttribute('data-section-type') === '10'
) {
continue;
}

const label = el.getAttribute('aria-label') || '';

if (!isUnreadBadge(label)) {
continue;
}

const match = label.match(/\d+/);

if (match) {
count += Ferdium.safeParseInt(match[0]);
}
}

Ferdium.setBadge(count);
};

// Google Chat may trigger many DOM mutations for a single update.
// Batch them into one badge recalculation per animation frame.
const scheduleUpdate = () => {
if (updateScheduled) {
return;
}

updateScheduled = true;

requestAnimationFrame(() => {
updateScheduled = false;
getMessages();
});
};

const startObserver = () => {
if (!document.body) {
setTimeout(startObserver, 250);
return;
}

observer = new MutationObserver(mutations => {
const hasRelevantChange = mutations.some(mutation => {
if (mutation.type === 'attributes') {
return true;
}

const target =
mutation.target.nodeType === Node.ELEMENT_NODE
? mutation.target
: mutation.target.parentElement;

return Boolean(
target?.closest?.('[data-section-type]') ||
target?.querySelector?.('[data-section-type]'),
);
});

if (hasRelevantChange) {
scheduleUpdate();
}
});

observer.observe(document.body, {
subtree: true,
childList: true,
characterData: true,
attributes: true,
attributeFilter: [
'aria-label',
'data-section-type',
],
});

scheduleUpdate();
};

startObserver();

// Periodic fallback in case Google Chat changes something that does not
// generate a relevant observable mutation.
Ferdium.loop(getMessages);

Ferdium.injectCSS(_path.default.join(__dirname, 'service.css'));
Ferdium.injectCSS(_path.join(__dirname, 'service.css'));
};