Skip to content

Commit a07db41

Browse files
erancybersecclaude
andcommitted
fix: persistent unread badges + group name corruption
Unread badge persists after hard refresh: - Track markedReadAt[jid] = lastMsgTimestamp when user reads a chat - Persist markedReadAt to localStorage so it survives page reloads - chatFetchConversations zeroes unreadCount for any jid where no newer message has arrived since it was marked read (normalised to ms for mixed ISO/Unix timestamp safety) - Badge correctly reappears when a genuinely new message arrives Group names showing individual person names instead of group subject: - Root cause: findChats pushName = last sender's name, not group subject - Fix fallback order: verifiedName → c.subject → c.name (pushName excluded) - Persist _verifiedGroupNames to localStorage so correct names load instantly on hard refresh without waiting for async API call - Remove one-time _groupSubjectsFetched gate; now retries only unverified groups each load so transient 404s are recovered on next visit - All 14 groups now resolve to correct subject names immediately Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent e973489 commit a07db41

File tree

1 file changed

+51
-29
lines changed

1 file changed

+51
-29
lines changed

index.html

Lines changed: 51 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -3936,10 +3936,11 @@ <h3 class="text-sm font-semibold text-gray-800">Privacy Settings</h3>
39363936
activeFilter: 'all', // all | unread | favorites | groups
39373937
typingTimer: null,
39383938
initialized: false,
3939-
_verifiedGroupNames: {}, // in-memory only; populated by chatFetchGroupSubjects
3939+
_verifiedGroupNames: (() => { try { return JSON.parse(localStorage.getItem('chat_verifiedGroupNames') || '{}'); } catch(e) { return {}; } })(), // persisted in localStorage
39403940
_failedPicUrls: new Set(), // URLs that returned 403/error — skip on future renders
39413941
selectMode: false,
39423942
selectedMsgs: new Set(),
3943+
markedReadAt: (() => { try { return JSON.parse(localStorage.getItem('chat_markedReadAt') || '{}'); } catch(e) { return {}; } })(), // jid → lastMsgTimestamp; persisted in localStorage
39433944
};
39443945

39453946
// ── Avatar colors (deterministic by jid) ─────────────────────────────
@@ -4117,20 +4118,17 @@ <h3 class="text-sm font-semibold text-gray-800">Privacy Settings</h3>
41174118
const lastMsg = c.lastMessage || c.lastMsgContent || {};
41184119
const contact = chatState.contacts[jid];
41194120

4120-
// Name resolution priority:
4121-
// 1. Contact saved name (phone address book)
41224121
// Name resolution:
4123-
// Groups: API pushName (group subject) → contact savedName → fallback
4124-
// 1:1: contact savedName/displayName → API pushName → last incoming msg pushName → fallback
4122+
// Groups: verifiedGroupName (group metadata API) → c.subject (findChats subject field) → c.name → fallback
4123+
// 1:1: contact savedName/displayName → c.pushName → c.name → fallback
4124+
// NOTE: c.pushName in findChats for groups = LAST SENDER's name, NOT the group subject — never use it for groups
41254125
const chatPushName = c.pushName || c.name || c.subject || '';
41264126
let name;
41274127
if (chatIsGroup(jid)) {
4128-
// For groups, the ONLY reliable name source is chatFetchGroupSubjects (group API).
4129-
// The findChats pushName is the LAST SENDER's name, NOT the group subject.
4130-
// contact.displayName in IndexedDB may also be contaminated from previous bad writes.
4131-
// So we use a dedicated in-memory cache that can ONLY be written by the group API.
4132-
const verifiedName = chatState._verifiedGroupNames && chatState._verifiedGroupNames[jid];
4133-
name = verifiedName || chatPushName || chatExtractNumber(jid);
4128+
// Priority: verified from group metadata API → subject field from findChats → name field → number fallback
4129+
// c.pushName is intentionally excluded: it holds the last sender's name for groups, not the group subject.
4130+
const verifiedName = chatState._verifiedGroupNames?.[jid];
4131+
name = verifiedName || c.subject || c.name || chatExtractNumber(jid);
41344132
// Do NOT write to contact.displayName for groups here — only chatFetchGroupSubjects may do that.
41354133
} else {
41364134
const contactName = (contact && contact.savedName) || (contact && contact.displayName && !/^\d+$/.test(contact.displayName) ? contact.displayName : '');
@@ -4189,11 +4187,10 @@ <h3 class="text-sm font-semibold text-gray-800">Privacy Settings</h3>
41894187

41904188
async function chatFetchGroupSubjects(convs) {
41914189
const cfg = getCfg();
4192-
const groups = convs.filter(c => chatIsGroup(c.id));
4190+
// Only fetch groups that don't have a verified name yet — skip ones already cached
4191+
const groups = convs.filter(c => chatIsGroup(c.id) && !chatState._verifiedGroupNames[c.id]);
41934192
if (!groups.length) return;
4194-
// Initialize the verified names cache (in-memory only — never contaminated by IndexedDB)
4195-
if (!chatState._verifiedGroupNames) chatState._verifiedGroupNames = {};
4196-
// Fetch group metadata in parallel
4193+
// Fetch group metadata in parallel for unverified groups only
41974194
const results = await Promise.allSettled(
41984195
groups.map(g =>
41994196
fetch(`${cfg.base}/group/findGroupInfos/${cfg.instance}?groupJid=${encodeURIComponent(g.id)}`, {
@@ -4221,6 +4218,9 @@ <h3 class="text-sm font-semibold text-gray-800">Privacy Settings</h3>
42214218
okCount++;
42224219
});
42234220
console.log(`[Chat] Group subjects: ${okCount} OK, ${failCount} failed, total ${groups.length}`);
4221+
if (okCount > 0) {
4222+
try { localStorage.setItem('chat_verifiedGroupNames', JSON.stringify(chatState._verifiedGroupNames)); } catch(e) {}
4223+
}
42244224
}
42254225

42264226
async function chatFetchConversations() {
@@ -4430,22 +4430,36 @@ <h3 class="text-sm font-semibold text-gray-800">Privacy Settings</h3>
44304430

44314431
// Fetch authoritative group subjects from the group metadata API
44324432
// (findChats pushName is unreliable for groups — returns last sender's name).
4433-
// Only run on first load — verified names are then cached in chatState.contacts
4434-
// and protected in _chatParseConv, so repeating every 5s poll is unnecessary
4435-
// and causes name flickering when the API call is slow.
4436-
if (!chatState._groupSubjectsFetched) {
4437-
chatState._groupSubjectsFetched = true;
4438-
const groupCount = convs.filter(c => chatIsGroup(c.id)).length;
4439-
if (groupCount) chatSetLoadStatus(`Updating ${groupCount} group names...`);
4433+
// Fetch group subjects for any groups not yet verified.
4434+
// chatFetchGroupSubjects skips already-cached groups, so this is cheap on repeat calls.
4435+
// Only show the status bar if there are actually unverified groups to fetch.
4436+
const unverifiedGroupCount = convs.filter(c => chatIsGroup(c.id) && !chatState._verifiedGroupNames[c.id]).length;
4437+
if (unverifiedGroupCount) {
4438+
chatSetLoadStatus(`Updating ${unverifiedGroupCount} group names...`);
44404439
await chatFetchGroupSubjects(convs);
44414440
}
44424441

4443-
// Preserve unreadCount=0 for any conversation the user currently has open
4444-
// (the API may lag behind our mark-as-read call, especially for groups)
4445-
if (chatState.activeJid) {
4446-
const fresh = convs.find(c => c.id === chatState.activeJid);
4447-
if (fresh) fresh.unreadCount = 0;
4448-
}
4442+
// Preserve unreadCount=0 for the active chat and any recently-read chats.
4443+
// The Evolution API (especially for groups) can lag behind our mark-as-read
4444+
// call and keep returning a stale unreadCount for several poll cycles.
4445+
// We suppress that stale count until a genuinely newer message arrives.
4446+
convs.forEach(c => {
4447+
if (c.id === chatState.activeJid) {
4448+
c.unreadCount = 0;
4449+
} else if (chatState.markedReadAt[c.id] !== undefined) {
4450+
// Normalize both to ms numbers — API may return ISO strings or Unix ms
4451+
const lastTs = +new Date(c.lastMsgTimestamp);
4452+
const markedTs = +new Date(chatState.markedReadAt[c.id]);
4453+
if (lastTs <= markedTs) {
4454+
// No new messages since we marked it read — keep badge at 0
4455+
c.unreadCount = 0;
4456+
} else {
4457+
// A genuinely new message arrived — show it as unread and stop suppressing
4458+
delete chatState.markedReadAt[c.id];
4459+
try { localStorage.setItem('chat_markedReadAt', JSON.stringify(chatState.markedReadAt)); } catch(e) {}
4460+
}
4461+
}
4462+
});
44494463
// Always assign to state first — UI must work even if DB write fails
44504464
chatState.conversations = convs;
44514465
const archivedCount = convs.filter(c => c.archived).length;
@@ -5276,7 +5290,15 @@ <h3 class="text-sm font-semibold text-gray-800">Privacy Settings</h3>
52765290
try {
52775291
// Always clear local unread count immediately when viewing a chat
52785292
const conv = chatState.conversations.find(c => c.id === jid);
5279-
if (conv && conv.unreadCount > 0) { conv.unreadCount = 0; chatRenderList(); chatUpdateUnreadBadge(); }
5293+
if (conv && conv.unreadCount > 0) {
5294+
conv.unreadCount = 0;
5295+
// Record the last message timestamp at the moment we marked this read.
5296+
// chatFetchConversations will keep unreadCount=0 for this jid until a
5297+
// message arrives with a newer timestamp (i.e. genuinely new activity).
5298+
chatState.markedReadAt[jid] = conv.lastMsgTimestamp || Date.now();
5299+
try { localStorage.setItem('chat_markedReadAt', JSON.stringify(chatState.markedReadAt)); } catch(e) {}
5300+
chatRenderList(); chatUpdateUnreadBadge();
5301+
}
52805302
// Only send API mark-as-read if this conversation is currently active and its
52815303
// messages are loaded — otherwise chatState.messages belongs to a different chat
52825304
if (chatState.activeJid !== jid) return;

0 commit comments

Comments
 (0)