From c5e5ddbcc422143c0744574478732495d32d0de0 Mon Sep 17 00:00:00 2001 From: sanasol Date: Mon, 2 Mar 2026 11:06:13 +0100 Subject: [PATCH] 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 --- GUI/js/matcha.js | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/GUI/js/matcha.js b/GUI/js/matcha.js index 0d72b9b..3c1e6fc 100644 --- a/GUI/js/matcha.js +++ b/GUI/js/matcha.js @@ -684,19 +684,19 @@ function renderFriendsList() { // Sort: online first, then alphabetical const sorted = [...friends].sort((a, b) => { - const aOnline = a.presence !== 'offline' ? 1 : 0; - const bOnline = b.presence !== 'offline' ? 1 : 0; + const aOnline = a.state !== 'offline' ? 1 : 0; + const bOnline = b.state !== 'offline' ? 1 : 0; if (aOnline !== bOnline) return bOnline - aOnline; 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 = `
Friends \u2014 ${onlineCount} connected (${friends.length} total)
`; sorted.forEach(f => { - const presenceClass = f.presence === 'offline' ? 'offline' : (f.presence === 'in_game' ? 'ingame' : 'online'); - const presenceText = f.presence === 'offline' ? 'Offline' : (f.presence === 'in_game' ? 'In Game' : 'Online'); + const presenceClass = f.state === 'offline' ? 'offline' : (f.state === 'in_game' ? 'ingame' : 'online'); + const presenceText = f.state === 'offline' ? 'Offline' : (f.state === 'in_game' ? 'In Game' : 'Online'); const unread = matchaState.unreadDms[f.id] || 0; const unreadBadge = unread > 0 ? `${unread}` : ''; const avUrl = avatarUrl(f.id); @@ -925,8 +925,8 @@ function renderDmChat(body, header) { const target = matchaState.dmTarget; // Find friend for presence/avatar info const friend = matchaState.friends.find(f => f.id === target.id); - const presenceClass = friend?.presence === 'offline' ? 'offline' : (friend?.presence === 'in_game' ? 'ingame' : 'online'); - const presenceText = friend?.presence === 'offline' ? 'Offline' : (friend?.presence === 'in_game' ? 'In Game' : 'Online'); + const presenceClass = friend?.state === 'offline' ? 'offline' : (friend?.state === 'in_game' ? 'ingame' : 'online'); + const presenceText = friend?.state === 'offline' ? 'Offline' : (friend?.state === 'in_game' ? 'In Game' : 'Online'); const avUrl = avatarUrl(friend?.id); header.innerHTML = ` @@ -1598,12 +1598,13 @@ function setupWsListeners() { }); matcha.onWsMessage((data) => { - // The WS data has type:"message" + message fields at top level + // WS data has: type:"message", convo:"global"|, message:{...} const msg = data.message || data; 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) { // Skip if own message (already rendered optimistically) @@ -1632,8 +1633,8 @@ function setupWsListeners() { updateUnreadBadges(); } } else { - // DM - const otherId = isOwnMessage ? msg.toId : msg.fromId; + // DM — use data.convo as conversation identifier, fallback to msg fields + const otherId = data.convo || (isOwnMessage ? msg.toId : msg.fromId); // Skip own messages (already rendered optimistically) if (isOwnMessage) {