fix: presence field name (f.state not f.presence) and WS message routing via data.convo

Bug 1: Butter API returns friend presence in `state` field, not `presence`.
All 7 occurrences (friends list sort, count, display, DM header) now use f.state.

Bug 2: WS messages use `data.convo` for conversation routing ("global" or convo ID).
Message object may not have toId/to fields, causing all messages to fall into DM branch.
Now checks data.convo first with msg field fallbacks.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
sanasol
2026-03-02 11:06:13 +01:00
parent e610072fa6
commit c5e5ddbcc4

View File

@@ -684,19 +684,19 @@ function renderFriendsList() {
// Sort: online first, then alphabetical // Sort: online first, then alphabetical
const sorted = [...friends].sort((a, b) => { const sorted = [...friends].sort((a, b) => {
const aOnline = a.presence !== 'offline' ? 1 : 0; const aOnline = a.state !== 'offline' ? 1 : 0;
const bOnline = b.presence !== 'offline' ? 1 : 0; const bOnline = b.state !== 'offline' ? 1 : 0;
if (aOnline !== bOnline) return bOnline - aOnline; if (aOnline !== bOnline) return bOnline - aOnline;
return (a.handle || '').localeCompare(b.handle || ''); return (a.handle || '').localeCompare(b.handle || '');
}); });
const onlineCount = sorted.filter(f => f.presence !== 'offline').length; const onlineCount = sorted.filter(f => f.state !== 'offline').length;
let html = `<div class="matcha-section-header">Friends \u2014 ${onlineCount} connected (${friends.length} total)</div>`; let html = `<div class="matcha-section-header">Friends \u2014 ${onlineCount} connected (${friends.length} total)</div>`;
sorted.forEach(f => { sorted.forEach(f => {
const presenceClass = f.presence === 'offline' ? 'offline' : (f.presence === 'in_game' ? 'ingame' : 'online'); const presenceClass = f.state === 'offline' ? 'offline' : (f.state === 'in_game' ? 'ingame' : 'online');
const presenceText = f.presence === 'offline' ? 'Offline' : (f.presence === 'in_game' ? 'In Game' : 'Online'); const presenceText = f.state === 'offline' ? 'Offline' : (f.state === 'in_game' ? 'In Game' : 'Online');
const unread = matchaState.unreadDms[f.id] || 0; const unread = matchaState.unreadDms[f.id] || 0;
const unreadBadge = unread > 0 ? `<span class="matcha-unread-dot">${unread}</span>` : ''; const unreadBadge = unread > 0 ? `<span class="matcha-unread-dot">${unread}</span>` : '';
const avUrl = avatarUrl(f.id); const avUrl = avatarUrl(f.id);
@@ -925,8 +925,8 @@ function renderDmChat(body, header) {
const target = matchaState.dmTarget; const target = matchaState.dmTarget;
// Find friend for presence/avatar info // Find friend for presence/avatar info
const friend = matchaState.friends.find(f => f.id === target.id); const friend = matchaState.friends.find(f => f.id === target.id);
const presenceClass = friend?.presence === 'offline' ? 'offline' : (friend?.presence === 'in_game' ? 'ingame' : 'online'); const presenceClass = friend?.state === 'offline' ? 'offline' : (friend?.state === 'in_game' ? 'ingame' : 'online');
const presenceText = friend?.presence === 'offline' ? 'Offline' : (friend?.presence === 'in_game' ? 'In Game' : 'Online'); const presenceText = friend?.state === 'offline' ? 'Offline' : (friend?.state === 'in_game' ? 'In Game' : 'Online');
const avUrl = avatarUrl(friend?.id); const avUrl = avatarUrl(friend?.id);
header.innerHTML = ` header.innerHTML = `
@@ -1598,12 +1598,13 @@ function setupWsListeners() {
}); });
matcha.onWsMessage((data) => { matcha.onWsMessage((data) => {
// The WS data has type:"message" + message fields at top level // WS data has: type:"message", convo:"global"|<convoId>, message:{...}
const msg = data.message || data; const msg = data.message || data;
const isOwnMessage = !!msg.fromId && String(msg.fromId) === String(matchaState.user?.id || ''); const isOwnMessage = !!msg.fromId && String(msg.fromId) === String(matchaState.user?.id || '');
const isGlobal = msg.toId === 'global' || msg.to === 'global'; // Use data.convo (Butter's WS format) as primary routing, fallback to msg fields
const isGlobal = data.convo === 'global' || msg.toId === 'global' || msg.to === 'global';
mlog.log('WS msg:', msg.fromHandle, '->', msg.toId || msg.to, '| own:', isOwnMessage, '| fromId:', msg.fromId, '| userId:', matchaState.user?.id); mlog.log('WS msg:', msg.fromHandle, '-> convo:', data.convo, 'toId:', msg.toId, '| own:', isOwnMessage, '| fromId:', msg.fromId, '| userId:', matchaState.user?.id);
if (isGlobal) { if (isGlobal) {
// Skip if own message (already rendered optimistically) // Skip if own message (already rendered optimistically)
@@ -1632,8 +1633,8 @@ function setupWsListeners() {
updateUnreadBadges(); updateUnreadBadges();
} }
} else { } else {
// DM // DM — use data.convo as conversation identifier, fallback to msg fields
const otherId = isOwnMessage ? msg.toId : msg.fromId; const otherId = data.convo || (isOwnMessage ? msg.toId : msg.fromId);
// Skip own messages (already rendered optimistically) // Skip own messages (already rendered optimistically)
if (isOwnMessage) { if (isOwnMessage) {