You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Server-side per-channel mute (#5399 followup). Channel mute state has only ever lived in browser localStorage, which meant the server's push helper had no idea who'd muted what and pushed every message to every member regardless. Mobile users in particular reported getting FCM pings for channels they'd muted in the web client weeks earlier; on Android there was no way to silence a noisy channel short of disabling Haven notifications system-wide. A new user_channel_prefs table now mirrors the mute set on the server, with GET /api/user/channel-prefs, POST /api/user/channel-prefs/mute (single-toggle), and PUT /api/user/channel-prefs/muted (transactional bulk replace, capped at 500 entries) backing it. sendPushNotifications filters muted recipients out of both the web-push subscription loop and the FCM inactive-members list. Existing clients converge automatically on first connect — the renderer unions localStorage with the server set and pushes the merged list back up, so nobody loses their existing mutes.
Fixed
Blank login page / blank app shell after updating directly from v3.18.0 (#5399). Two SyntaxErrors had been quietly sitting in main since the v3.19.0 Guest mode merge (b6b95bd): a duplicate const loginForm redeclaration in auth.js, and an orphan _setupServerBar() { opener with no body in app-ui.js (a half-merged method definition that was never closed). Anyone already running v3.19.x kept working because their cached/loaded modules survived the crash on initial parse, but users updating directly from v3.18.0 — the prior version many self-hosters were sitting on — hit both errors on first load and got a blank login page followed by a blank app. Mistakenly attributed to the v3.20.1 STUN refresh at first; that work is unaffected.
/api/ice-servers was still returning dead STUN URLs (#5399 followup). The server-side default in /api/ice-servers was still handing out stun.stunprotocol.org + stun.nextcloud.com — the same pair the v3.20.1 client fix had to route around. Any Haven server using the server-side STUN defaults was giving its clients dead endpoints and only working at all because voice.js had been updated to ignore them. Mirrored the same Cloudflare/Metered/Twilio/Google fallback list on the server so the two sides stay in sync.
Right-click → Copy image silently failed for almost everyone, on both static images and GIFs. Two compounding causes: the previous implementation awaited an Image() load + canvas.toBlob before calling navigator.clipboard.write, by which point the user-gesture token had been dropped and Chromium silently rejected the write with NotAllowedError; on top of that, Electron's renderer added enough latency around fetch+decode that even a corrected promise-based path was unreliable. Rewrote with three strategies tried in order: under Haven Desktop, hand the PNG bytes to the main process via a new clipboard:write-image IPC (Electron's clipboard.writeImage has no gesture restrictions); otherwise call navigator.clipboard.write with a promise-based ClipboardItem so the gesture token is preserved; last-ditch, copy the image URL as text so the user has something to paste. Failure toasts now include the underlying error message so diagnosing future regressions doesn't require devtools. (Requires Haven Desktop 1.4.22 for the IPC path.)
Image and member context menus appeared offscreen on top of the image lightbox or PiP DM. Both .image-context-menu and .user-context-menu sat at z-index: 10001, which left them buried under the image lightbox (100010) and the PiP DM panel (99999). Right-clicking an enlarged image or a member while a PiP DM was open made the menu look like it vanished. Bumped both to 100020.
Password eye-icon toggle sat on top of the typed characters. The .haven-pw-wrap wrapper was inline-block with no width hint, so the inner input collapsed to its intrinsic width and the absolutely-positioned eye sat directly over the right edge of the password text instead of in its own gutter. The wrap is now a full-width block and the input fills it with reserved right-padding for the toggle.