Skip to content
Draft
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
92 changes: 68 additions & 24 deletions src/libs/UnreadIndicatorUpdater/updateUnread/index.ts
Original file line number Diff line number Diff line change
@@ -1,54 +1,98 @@
/**
* Web browsers have a tab title and favicon which can be updated to show there are unread comments
*
* FIX for #86679: Document title flickers when navigating
*
* Three changes from original:
* 1. Gate document.title = '' behind popstate flag — only clear on back/forward nav
* (the Chrome bug this works around). Normal navigation skips the blank intermediate.
* 2. Coalesce rapid calls via requestAnimationFrame — multiple triggers during a single
* navigation transition produce one frame-level write instead of N competing setTimeout(0)s.
* 3. Skip no-op writes — if the computed title matches current document.title, don't touch it.
*/
import CONFIG from '@src/CONFIG';
import type UpdateUnread from './types';

let unreadTotalCount = 0;
let currentPageTitle = '';

// rAF coalescing state
let pendingRAF: number | null = null;

// Only set true by popstate (actual back/forward navigation),
// which is the only case the Chrome title-reversion workaround is needed for.
let isPopstateNavigation = false;

/**
* Set the current page-specific title (called by useDocumentTitle hook)
* @param title - The page-specific title
* Compute the target title from current state
*/
function setPageTitle(title: string) {
currentPageTitle = title;
// Immediately update the document title when page title changes
updateDocumentTitle();
function getTargetTitle(): string {
const hasUnread = unreadTotalCount !== 0;
const baseTitle = currentPageTitle || CONFIG.SITE_TITLE;
return hasUnread ? `(${unreadTotalCount}) ${baseTitle}` : baseTitle;
}

/**
* Update the actual document title and favicon
* Commit title + favicon in a single rAF callback.
* Multiple scheduleCommit() calls within the same frame produce one write.
*/
function updateDocumentTitle() {
const hasUnread = unreadTotalCount !== 0;
// This setTimeout is required because due to how react rendering messes with the DOM, the document title can't be modified synchronously, and we must wait until all JS is done
// running before setting the title.
setTimeout(() => {
// There is a Chrome browser bug that causes the title to revert back to the previous when we are navigating back. Setting the title to an empty string
// seems to improve this issue.
function commitTitleAndFavicon() {
pendingRAF = null;
const target = getTargetTitle();

// Only apply the Chrome back-nav workaround when we actually came from popstate
if (isPopstateNavigation) {
document.title = '';
isPopstateNavigation = false;
}

// Skip no-op writes — prevents unnecessary paint cycles
if (target !== document.title) {
document.title = target;
}

// Update favicon
const hasUnread = unreadTotalCount !== 0;
const favicon = document.getElementById('favicon');
if (favicon instanceof HTMLLinkElement) {
favicon.href = hasUnread ? CONFIG.FAVICON.UNREAD : CONFIG.FAVICON.DEFAULT;
}
}

// Use page-specific title if available, otherwise use the default SITE_TITLE
const baseTitle = currentPageTitle || CONFIG.SITE_TITLE;
document.title = hasUnread ? `(${unreadTotalCount}) ${baseTitle}` : baseTitle;
/**
* Schedule a single rAF-coalesced title commit.
* If already scheduled, the pending one is cancelled — only the latest state wins.
*/
function scheduleCommit() {
if (pendingRAF !== null) {
cancelAnimationFrame(pendingRAF);
}
pendingRAF = requestAnimationFrame(commitTitleAndFavicon);
}

const favicon = document.getElementById('favicon');
if (favicon instanceof HTMLLinkElement) {
favicon.href = hasUnread ? CONFIG.FAVICON.UNREAD : CONFIG.FAVICON.DEFAULT;
}
}, 0);
/**
* Set the current page-specific title (called by useDocumentTitle hook)
* @param title - The page-specific title
*/
function setPageTitle(title: string) {
currentPageTitle = title;
scheduleCommit();
}

/**
* Set the page title on web
* Update unread count and schedule a title commit
*/
const updateUnread: UpdateUnread = (totalCount) => {
unreadTotalCount = totalCount;
updateDocumentTitle();
scheduleCommit();
};

/**
* Popstate handler — only this path triggers the Chrome workaround.
* Non-route-changing popstates are harmless (no-op if title unchanged).
*/
window.addEventListener('popstate', () => {
isPopstateNavigation = true;
updateUnread(unreadTotalCount);
});

Expand Down