feat: Matcha! social integration — friends, chat, DMs, avatars, presence

Add full Matcha social panel (butter.lat API) as a right-side slide-out:

Backend (matchaService.js):
- HTTP client for auth, friends, messages, unread, avatar, heartbeat APIs
- WebSocket with auto-reconnect (exponential backoff, no hard cap)
- Token management via config, presence heartbeat every 30s
- WS error message type handling, game running presence

Renderer (matcha.js):
- State machine UI: intro → login/register → app (friends/chat/DMs/profile)
- Two-phase registration with master key display and verification
- Friends list with presence dots, collapsible requests, 12s polling
- Global chat + DM with optimistic rendering, cursor pagination
- Scroll position preserved on load-more, separate loading flags
- Clickable URLs in messages (linkify with proper escaping)
- User profile popup with avatar upload/delete
- Unread badges (messages + friend requests) on nav icon
- Escape key closes panel/overlay, try/catch on auth flows

IPC bridge (preload.js + main.js):
- 21 IPC invoke methods + 8 WS event listeners
- Avatar upload via file picker dialog in main process
- Game launch sets in_game heartbeat state

CSS (style.css):
- ~1500 lines: panel, auth screens, friends, chat, profile, toast
- Responsive panel width, improved contrast, no overflow clipping
- Loading states, disabled states, pulse animations

Credits: Powered by Butter Launcher & Matcha! (butterlauncher.tech)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
sanasol
2026-03-02 00:49:37 +01:00
parent 57056e5b7a
commit a40465cac9
9 changed files with 4219 additions and 5 deletions

View File

@@ -153,5 +153,39 @@ contextBridge.exposeInMainWorld('electronAPI', {
},
onUpdateError: (callback) => {
ipcRenderer.on('update-error', (event, data) => callback(data));
},
// Matcha Social API
matcha: {
log: (level, ...args) => ipcRenderer.invoke('matcha:log', level, ...args),
register: (username, password, password2) => ipcRenderer.invoke('matcha:register', username, password, password2),
confirmRegister: (pendingId, proofId) => ipcRenderer.invoke('matcha:confirm-register', pendingId, proofId),
login: (handle, password) => ipcRenderer.invoke('matcha:login', handle, password),
logout: () => ipcRenderer.invoke('matcha:logout'),
getAuthState: () => ipcRenderer.invoke('matcha:get-auth-state'),
getMe: () => ipcRenderer.invoke('matcha:get-me'),
getUser: (userId) => ipcRenderer.invoke('matcha:get-user', userId),
getFriends: () => ipcRenderer.invoke('matcha:get-friends'),
friendRequest: (handle) => ipcRenderer.invoke('matcha:friend-request', handle),
friendAccept: (requestId) => ipcRenderer.invoke('matcha:friend-accept', requestId),
friendReject: (requestId) => ipcRenderer.invoke('matcha:friend-reject', requestId),
friendCancel: (requestId) => ipcRenderer.invoke('matcha:friend-cancel', requestId),
friendRemove: (friendId) => ipcRenderer.invoke('matcha:friend-remove', friendId),
getMessages: (withTarget, cursor, after) => ipcRenderer.invoke('matcha:get-messages', withTarget, cursor, after),
sendMessage: (to, body, replyTo) => ipcRenderer.invoke('matcha:send-message', to, body, replyTo),
deleteMessage: (messageId) => ipcRenderer.invoke('matcha:delete-message', messageId),
getUnread: () => ipcRenderer.invoke('matcha:get-unread'),
clearUnread: (withTarget) => ipcRenderer.invoke('matcha:clear-unread', withTarget),
uploadAvatar: (mode) => ipcRenderer.invoke('matcha:upload-avatar', mode),
deleteAvatar: () => ipcRenderer.invoke('matcha:delete-avatar'),
reconnect: () => ipcRenderer.invoke('matcha:reconnect'),
onWsMessage: (callback) => ipcRenderer.on('matcha:ws:message', (event, data) => callback(data)),
onWsConnected: (callback) => ipcRenderer.on('matcha:ws:connected', (event, data) => callback(data)),
onWsDisconnected: (callback) => ipcRenderer.on('matcha:ws:disconnected', () => callback()),
onMessageDeleted: (callback) => ipcRenderer.on('matcha:ws:message-deleted', (event, data) => callback(data)),
onAvatarUpdated: (callback) => ipcRenderer.on('matcha:ws:avatar-updated', (event, data) => callback(data)),
onBanned: (callback) => ipcRenderer.on('matcha:ws:banned', (event, data) => callback(data)),
onAnnouncement: (callback) => ipcRenderer.on('matcha:ws:announcement', (event, data) => callback(data)),
onMaxRetries: (callback) => ipcRenderer.on('matcha:ws:max-retries', () => callback())
}
});