Skip to content
Merged
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
113 changes: 112 additions & 1 deletion src/injected.ts
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,7 @@ const start = async () => {
const setupFlags = {
urlResolver: false,
badgeUpdates: false,
unreadChangedEvent: false,
faviconUpdates: false,
jitsiIntegration: false,
backgroundSettings: false,
Expand All @@ -394,6 +395,15 @@ const start = async () => {
userPresence: false,
};

// Per-subscription unread state, accumulated from the
// `unread-changed-by-subscription` global event so the badge can reproduce
// the server's alert-only "•" indicator. Lives outside setupReactiveFeatures
// so it survives the periodic re-invocation below.
const unreadSubscriptions = new Map<
string,
{ unread: number; alert?: boolean; unreadAlert?: string }
>();

// Setup reactive features that depend on modules (with polling)
// eslint-disable-next-line complexity
const setupReactiveFeatures = () => {
Expand All @@ -402,14 +412,115 @@ const start = async () => {
setupFlags.urlResolver = true;
}

if (Tracker && Session && !setupFlags.badgeUpdates) {
// Pre-7.8.0 only: those servers still publish the badge through the Meteor
// Session reactive dict. On 7.8.0+ this key is gone (Session.get('unread')
// resolves to undefined) and the global-event path below owns the badge, so
// running this autorun there would clear the badge with setBadge(undefined).
if (
!versionIsGreaterOrEqualsTo(serverInfo.version, '7.8.0') &&
Tracker &&
Session &&
!setupFlags.badgeUpdates
) {
Tracker.autorun(() => {
const unread = Session.get('unread');
window.RocketChatDesktop.setBadge(unread);
});
setupFlags.badgeUpdates = true;
}

// Servers >= 7.x removed `unread` from the Meteor Session reactive dict
// (RocketChat/Rocket.Chat#36001), which is why the Session autorun above is
// gated to pre-7.8.0. Those servers no longer expose the badge through
// Meteor at all, but they still broadcast it through two global
// CustomEvents on window, mirroring the server's own `useUnread` hook:
// - `unread-changed-by-subscription`: fires per subscription whenever its
// unread-relevant fields change, carrying { rid, unread, alert,
// unreadAlert }. We accumulate these into `unreadSubscriptions` to
// rebuild the alert-only "•" indicator that the numeric count alone
// cannot represent.
// - `unread-changed`: fires on every recompute with the aggregate count.
// We use it as the recompute trigger and the source of truth for the
// numeric total.
// The badge value is resolved exactly as the server's `useUnread` does:
// a positive count wins, otherwise an alert-only "•", otherwise no badge.
// Registered independently of the Meteor modules so it works even when they
// never load.
if (!setupFlags.unreadChangedEvent) {
Comment thread
coderabbitai[bot] marked this conversation as resolved.
// `aggregateCount`, when provided, is the authoritative total carried by
// the `unread-changed` event. The per-subscription map is incremental and
// only sees rooms that changed after the listener attached, so it can
// undercount; we prefer the aggregate for the numeric total and use the
// map solely to rebuild the alert-only "•" indicator.
const resolveBadge = (aggregateCount?: number): void => {
let unreadCount = 0;
let alertIndicator: '•' | undefined;

// The user's `unreadAlert` preference only influences rooms left on the
// default. No global event carries it, so we read it from the Meteor
// user document when available and fall back to the server's shipped
// default of `true`.
const unreadAlertEnabled =
Meteor?.user?.()?.settings?.preferences?.unreadAlert ?? true;

for (const subscription of unreadSubscriptions.values()) {
const { unread, alert, unreadAlert } = subscription;
if (alert || unread > 0) {
if (
alert === true &&
unreadAlert !== 'nothing' &&
(unreadAlert === 'all' || unreadAlertEnabled !== false)
) {
alertIndicator = '•';
}
unreadCount += unread;
}
}

const total =
aggregateCount !== undefined && aggregateCount > unreadCount
? aggregateCount
: unreadCount;

if (total > 0) {
window.RocketChatDesktop.setBadge(total);
return;
}
window.RocketChatDesktop.setBadge(alertIndicator ?? 0);
};

window.addEventListener('unread-changed-by-subscription', (event) => {
const subscription = (
event as CustomEvent<{
rid?: string;
unread?: number;
alert?: boolean;
unreadAlert?: string;
}>
).detail;
if (!subscription?.rid) {
return;
}
unreadSubscriptions.set(subscription.rid, {
unread: subscription.unread ?? 0,
alert: subscription.alert,
unreadAlert: subscription.unreadAlert,
});
resolveBadge();
});

window.addEventListener('unread-changed', (event) => {
const { detail } = event as CustomEvent<number | undefined>;
const aggregateCount =
typeof detail === 'number' && Number.isFinite(detail)
? detail
: undefined;
resolveBadge(aggregateCount);
});

setupFlags.unreadChangedEvent = true;
}

if (Tracker && settings && !setupFlags.faviconUpdates) {
Tracker.autorun(() => {
const { url, defaultUrl } = settings.get('Assets_favicon') || {};
Expand Down
Loading