Skip to content

Releases: ancsemi/Haven

v3.15.9

Choose a tag to compare

@ancsemi ancsemi released this 12 May 15:21

Polish translation update (contributed by dvw1xx, #5356).

Changed

  • Polish (\pl) locale updated. Adds translations for: \�ia_persona,
    ecover_encryption, \�dd_to_channel, all
    oles_*\ entries (multi-role UI), and
    eapply_access_tooltip. Corrects \�oice_active\ to " Trwa rozmowa.

v3.15.8

Choose a tag to compare

@ancsemi ancsemi released this 12 May 04:27

[3.15.8] - 2026-05-12

The fix that was already shipped, but actually shipped this time. The reason all the voice fixes from v3.15.3-v3.15.7 appeared to do nothing for users is that none of the client-side changes since v3.14.14 were ever delivered to a single browser. The <script> cache-bust strings in public/app.html were pinned at ?v=3.15.2, and worse, the ES-module imports inside public/js/app.js (which load app-voice.js, app-socket.js, app-users.js, app-channels.js, etc.) were pinned at ?v=3.14.14. Every browser kept happily serving cached pre-3.14.14 client JS even on a fully-updated 3.15.7 server. So:

  • The screen-share renegotiation work (3.15.5)
  • The sidebar / voice-panel sync (3.15.4)
  • The _softLeave rejoin path (3.15.4)
  • The voice ghost-prune broadcasts (3.15.3)
  • The persona autocomplete + nickname fixes (3.15.6)

...all of it ran on the server but never on the user. The server-only getUserAllRoles fix in 3.15.7 was the only thing that actually took effect, which is why voice presence was still misbehaving even after that release.

Fixed

  • Bumped every ?v= cache-bust string in public/app.html (3.15.2 → 3.15.8) and public/js/app.js (3.14.14 → 3.15.8). Forces every browser to fetch the post-3.14.14 client JS that the previous releases shipped to the server but never to users.

Apologies for the runaround. (#5347)

v3.15.7

Choose a tag to compare

@ancsemi ancsemi released this 10 May 04:02

[3.15.7] - 2026-05-10

Critical hotfix for voice (#5347). The voice presence work in 3.15.4 added a getUserAllRoles call inside broadcastVoiceUsers but the helper was never destructured from createPermissions(db), so every voice user broadcast (join, leave, mute, deafen, ghost-prune) was throwing ReferenceError: getUserAllRoles is not defined. The error was swallowed by the async handler, so the server stayed up but broadcasts never completed. Clients kept showing whoever was last successfully broadcast, leavers stuck around as ghosts, rejoiners didn't appear. This is the actual root cause of the "voice list disagrees with reality" symptom.

Fixed

  • ReferenceError: getUserAllRoles is not defined on every voice broadcast (#5347). Added the missing destructure in setupSocketHandlers and forwarded it on the shared ctx.
  • PUT /api/auth/user-servers returned 500 PayloadTooLargeError once the saved server list exceeded ~50 entries. The global 16kb body parser in server.js was preempting the per-route 96kb limit in auth.js. Bumped the global limit to 128kb.

[3.15.6] - 2026-05-09

Follow-up fixes for personas (#5353).

Fixed

  • Persona :: autocomplete appeared in fullscreen DM view. The PiP input already suppressed it but the main #message-input (used when a DM is opened as a full channel view) still ran _checkPersonaTrigger. Added a guard that bails out when the active channel is a DM.
  • Nickname set for a user overrode the persona name on their messages. Message rendering called _getNickname(msg.user_id, ...) unconditionally, so any local nickname for the real account replaced the persona display name. Persona messages now use msg.username (the persona name) directly.

v3.15.5

Choose a tag to compare

@ancsemi ancsemi released this 09 May 21:38

[3.15.5] - 2026-05-09

Long-standing screen-share reliability fixes. The flow had multiple silent-failure paths that left receivers with audio but no video (or no tile at all).

Fixed

  • Screen share goes live but never appears for some viewers. _renegotiate called RTCPeerConnection.createOffer() without checking signalingState, so any renegotiation that fired while a previous offer/answer was still pending threw and the catch silently swallowed it. The peer ended up with audio (already on the stable m-section) but no screen video, with no retry. The renegotiate now waits up to ~5s for the connection to reach a stable state before issuing the offer.
  • Late-joiner renegotiate skipped adding screen tracks if the sharer also had a webcam on. The sharer's renegotiate-screen handler used a generic "any video sender exists" check to decide whether to add the screen tracks, which was true the moment a webcam track was attached. Result: late joiners got the webcam but never the screen. Now matches by track identity so screen tracks are added if and only if they aren't already on the connection.
  • screen-share-started was emitted after the renegotiation completed. That meant receivers' ontrack for the new screen video could fire before screenSharers.has(sharerId) was true, so the screen-vs-webcam classifier fell through to a default route that misbehaves when stale webcam state is present. The notification is now emitted before the per-peer renegotiation loop starts.
  • No recovery when the renegotiation offer was dropped or stalled. Added a request-screen-renegotiate server event the receiver fires (a) ~3s after screen-share-started if no video receiver appeared on the peer connection, and (b) once during the existing video tile retry loop if videoWidth stays at 0. The server forwards a renegotiate-screen to the sharer, which re-issues an offer for that specific peer.

[3.15.4] - 2026-05-09

Deeper voice presence fix on top of 3.15.3 (#5347).

Fixed

  • Left sidebar and right voice panel could disagree (#5347 follow-up). The two panels were driven by two unrelated client stores. When one event arrived stale or out of order the two views diverged and stayed diverged until a full reload. The right-panel handler now updates the sidebar stores as well.
  • Reconnect after socket drop stopped using voice-rejoin (#5347 follow-up). _softLeave cleared inVoice/currentChannel so the reconnect handler fell back to a delayed setTimeout(1500) auto-rejoin emitting plain voice-join. That path did not broadcast voice-user-left to the rest of the room, so peers held on to dead RTCPeerConnections and audio never re-established. _softLeave now stashes the channel intent in _softLeftChannel, reconnect picks it up immediately, and voice-join's stale-entry branch now mirrors voice-rejoin and broadcasts voice-user-left so peers tear down dead connections cleanly.
  • Connection-time voice snapshot racing voice-rejoin's broadcast. The snapshot now only sends the count update to the connecting socket and lets pruneStaleVoiceUsers handle per-ghost broadcasts on its own.

[3.15.3] - 2026-05-08

Fixed

  • Voice list shows users who already left (#5347 follow-up). The connection-time voice snapshot and get-voice-counts both iterated the voice room map without pruning ghost entries from sockets that had disconnected in ways the cleanup missed. Both paths now prune first, broadcast fresh roster and voice-user-left so peer clients tear down dead RTCPeerConnections.

[3.15.2] - 2026-05-08

Fixed

  • Thread panel hidden behind message bar (#5354). Thread dock z-index raised above the composer bar.
  • Persona autocomplete shown in DMs and threads (#5353). The :: autocomplete no longer appears where personas aren't supported.
  • Persona tooltip shows raw i18n key (#5353). Fixed missing locale key on the "Sent via ..." badge tooltip.
  • Double close button in media gallery (#5352). Auto-inject control group skipped for the gallery modal which already has its own close button.
  • Media gallery shown in DMs (#5352). Gallery toolbar button hidden in DM channels.
  • OpenSSL not recognized when starting Haven on Windows (#5351). Fixed a cmd.exe delayed-expansion bug in the startup bat.

v3.15.1 — Channel Media Gallery + Personas

Choose a tag to compare

@ancsemi ancsemi released this 08 May 15:23

What's new in 3.15.1

This release ships the Channel Media Gallery and Personas features (introduced in 3.15.0) plus a full polish pass fixing every issue found in initial testing.


Channel Media Gallery (#5350)

A new 🖼 button in the channel header opens a near full-screen modal with five tabs: Photos, Videos, Audio, Files, and Links. Photos and videos render as a clickable album-style grid. Audio, files, and links use a list layout with inline players and download links. Every item shows the date it was posted and a jump-to-message button.

3.15.1 fixes:

  • Clicking a photo thumbnail no longer opens it underneath the gallery modal (lightbox z-index raised above modal overlays)
  • Clicking a video tile now opens an inline player overlay with full controls instead of silently closing the gallery
  • Each photo and video tile now has a ↗ jump-to-message button in the top-right corner (appears on hover)
  • Only media from channels you're a member of is shown (server-side channel membership check)

Personas (#86, #5349)

Profile settings now have a 👥 Personas section. Create up to 25 named alter-egos with their own name and avatar. To speak as a persona, type ::Name your message in chat — the prefix is stripped before storing and your real account stays linked for moderation. Messages sent through a persona show a small persona badge (hover to see the real sender).

3.15.1 fixes:

  • Prefix changed from >> to ::>> was treated as a markdown nested blockquote when the persona lookup didn't match, causing messages to appear blockquoted instead of sending normally
  • Persona names with spaces (e.g. Persona 1) now match correctly — the previous regex excluded whitespace so only the first word was captured, failing the DB lookup
  • Typing :: at the start of a message opens an autocomplete dropdown of your own personas; arrow keys / Tab / Enter to insert
  • @PersonaName now resolves as a mention and pings the persona's owner
  • Fixed a grouping bug where consecutive messages sent through different personas all stacked under the first persona's avatar header

Other fixes in this release (since v3.14.16)

  • Burn-after-read channel resolved correctly from PiP. Sending a burn-after-read message from the DM pop-in-picture was using the main channel's code instead of the PiP channel.
  • Top-bar header buttons follow the Colorful Emoji / Monochrome icon setting. The notification bell, pinned, gallery, copy, and other header emoji buttons now go grayscale in Monochrome mode alongside the message action bar.

v3.14.16

Choose a tag to compare

@ancsemi ancsemi released this 08 May 00:55

[3.14.16]

Fixed

  • Browser cache force-refreshed after status-icon patch series. All ?v= cache-busting strings on script and stylesheet tags bumped so browsers fetch updated files instead of serving stale cached copies from the 3.14.11+ patch set.
  • Message status icons (E2E lock, burn flame) completely reworked in the message layout. Icons were rendering in a right-gutter column that misaligned with compact (grouped) messages and collided with message text on narrow layouts. They now occupy a fixed, right-anchored slot: the lock always sits at right: 8px, the flame shifts left in JS when both icons are present. Covers full-mode, compact-mode, and DM PiP renders. DM PiP fixed slot tightened to avoid excess whitespace when only one icon (or neither) is shown.
  • PiP parent message action controls anchored to outer message box. The reply/react/etc. toolbar that appears on hover for parent messages inside the DM PiP was mispositioned after the inline status icon refactor. Controls are now anchored relative to the outer .message container.

[3.14.10]

  • E2E recovery error messaging overhaul. e2e.syncFromServer() now returns { ok, reason } instead of a bare boolean, distinguishing no-backup, bad-password, network, and error cases. The recovery toast now tells the user exactly what failed and never advises Reset for bad-password or network failures (Reset destroys all encrypted DMs).
  • Recovery toast no longer disappears in 3 seconds. Error toasts from the recovery flow now stay 8-10 s and use the standard opaque toast background.
  • Composer no longer hides behind theme particle effects. .message-input-area given an explicit z-index above the #fx-layers overlay.
  • Burn-after-read indicator cleanup. Removed the orange vertical border-left line on burn-pending message bodies. The compact-mode flame icon is now vertically centered.
  • Manage Servers modal now fills its height when resized.

[3.14.6]

Changed

  • DM channel codes are no longer exposed on the client. The server now strips display_code, code_visibility, code_mode, code_rotation_type, and code_rotation_interval from every DM channel object. The Copy DM Link option has been removed from the DM right-click menu.

[3.14.5]

Security

  • #5348: Third parties could join DMs via channel code or message link. The server's join-channel handler now returns a generic Invalid channel code error for any is_dm channel, making DM codes indistinguishable from non-existent ones.

Added

  • Recover Keys from Backup in the E2E dropdown. The Encryption options menu now has a middle option that re-fetches the server-side encrypted keypair and unwraps it with your password — the non-destructive recovery path.

[3.14.4]

Fixed

  • Threads were still reachable inside DMs. Threads are now removed from DMs at every layer: message toolbar, thread preview block, main-pane click handler, PiP click handler, and the server's send-thread-message / get-thread-messages socket handlers.

Added

  • Add to Channel in the user gear menu. Mirrors the context menu Invite to Channel action from the gear menu.

[3.14.3]

Fixed

  • #5325: Burn-after-read DMs never burned, and the sender saw no flame indicator on their own message. _wireBurnMessages was called with the message element as root and querySelectorAll only matches descendants, so the root's own .message-burn-pending class was never picked up. The wiring now treats the root element as a candidate too.
  • #5347 follow-up: voice participant list still didn't refresh until full client reload. voice-users-update only re-rendered when the event's channelCode matched the currently viewed text channel. Now also re-renders whenever the user is actually in voice on that channel.

[3.14.2]

Fixed

  • Role Assignment Center: per-user permission edits silently reverted on reopen. get-role-assignment-data only sent each role's default permissions back to the modal. Each currentRoles entry now carries an effectivePerms array (role defaults +/- the user's overrides).
  • Role Assignment Center: assigning one role wiped every other role at the same scope. assign-role ran a blanket DELETE before re-inserting. The handler now only replaces the row for this (user, role, channel) tuple.
  • Role Assignment Center: channel pane showed every channel for admins even inaccessible ones. The pane now only lists channels the target user is actually in.
  • Role Assignment Center: Transfer Admin listed as a checkbox permission. The transfer_admin permission had no effect anywhere in the codebase. Removed from the RAC permissions grid.
  • assign-role: customPerms was not validated against the caller's privileges. A non-admin user with promote_user could escalate a target above the caller. The server now drops admin-only perms unless the caller is an admin, and drops anything the caller doesn't currently hold.

v3.14.1

Choose a tag to compare

@ancsemi ancsemi released this 07 May 04:43

What's fixed

#5347: Voice call broken after random disconnect

Three separate bugs in the voice rejoin path were combining to produce the reported symptoms: users invisible to each other after a network blip, audio silent in both directions, the voice bar still showing "Voice Connected" while the panel showed "No one in voice", needing two or three leave/rejoin cycles before voice recovered.

Server: voice-rejoin handler silently overwrote the voiceUsers entry
When a socket reconnected and sent voice-rejoin, the server clobbered the user's slot without first telling the rest of the room. Everyone else kept their stale RTCPeerConnection from the previous session, and the rejoiner's fresh offer was applied on top of dead ICE, so audio never recovered. The handler now mirrors voice-join's cleanup: kicks the previous socket cleanly, broadcasts voice-user-left so all peers tear down, then re-adds the user and triggers a fresh offer/answer round. Also preserves isMuted/isDeafened on rejoin, refreshes voiceLastActivity (AFK timer), and includes voiceBitrate in the response (matching voice-join).

Client: voice-offer accepted onto a failed/closed peer connection
Even with the server fix, if a peer connection's connectionState or iceConnectionState was already failed/closed, the client was applying the new offer on top of it instead of rebuilding. It now tears down and recreates the peer in that state.

Client: voice-leave retry race silently kicked users after rejoin
The 2-second ack-retry on voice-leave had no guard against the user having already rejoined. It would fire a stale leave for a channel the user was back in, kicking them out server-side while their client still showed connected. That is the exact symptom in the screenshot: voice bar says "Voice Connected #ANC", panel says "No one in voice". The retry now bails if the user has rejoined any voice channel.

Defensive: _createPeer now closes pre-existing peers before creating
Prevents a stale RTCPeerConnection from leaking a second audio element and analyser for the same userId.

v3.14.0

Choose a tag to compare

@ancsemi ancsemi released this 06 May 23:41

Haven v3.14.0

Added

  • #5344: Registration token gate. New admin-controlled token that sits alongside (or instead of) the username whitelist on the registration page. When enabled in Settings → Admin → Whitelist, anyone signing up must enter the 16-char hex token the admin generated. Generate / Reroll / Copy / Clear buttons. New GET /api/auth/registration-info exposes only a requiresToken boolean so the field can show on demand without ever leaking the token. Whitelist + token can both be active — both checks must pass.
  • #5345: Default channels for invite joiners + private-channel safety fix. When someone joins via the server invite code or vanity link, admins can curate exactly which public channels they auto-land in via a checkbox list under Settings → Admin → Server Invite Code. "Select all" stores no allowlist (every public channel — default behavior). Critically, the auto-join logic was tightened so private parent channels (is_private = 1 or code_visibility = 'private') are now never unlocked by an invite code regardless of the allowlist — previously a private top-level channel was being granted alongside everything else.

Fixed

  • #5280: Burn-after-read DM toggle is now a true on/off toggle. The 🔥 button used to auto-disarm itself after a single message even though the click handler reads as a toggle. Now the armed state persists across messages until the user clicks the button again to disarm (or switches out of the DM channel, which still clears it for safety).
  • Style: fallout/matrix theme button border reduced from 2px to 1px for a slightly less chunky look.

Full changelog: https://github.com/ancsemi/Haven/blob/main/CHANGELOG.md

v3.13.0

Choose a tag to compare

@ancsemi ancsemi released this 06 May 21:05

What's new in v3.13.0

This release ships the full multi-role system, significantly expanded webhook integration, several new UX features, and a handful of bug fixes across chat, voice, and the admin panel.


Added

  • #5341: Multi-role assignments per user. Users can now hold multiple roles in the same channel or category scope simultaneously. The "Assign Role" modal replaces the single dropdown with a checkbox list — it pre-checks every role the user already holds and diffs on confirm, so one click both assigns new roles and revokes removed ones. The Role Assignment Center (RAC) config pane now shows a per-role card list with state indicators (held / pending add / pending remove / edited), a Configure button that opens a level + permissions editor with an optional apply-to-sub-channels toggle, and an add-role dropdown for assigning additional roles to the same scope. Voice users carry a full roles[] array; in chat, the author's primary badge continues to use the highest role with a +N suffix when more roles are held (hover shows the full list). The All Members admin view and profile popup both show the complete role set. A one-time admin notice explains the system on first login.
  • #5340: DM auto-cleanup notice banner. When cleanup_enabled is on with a non-zero cleanup_max_age_days, every DM channel shows a one-line banner under the header: "Messages older than N days are auto-deleted on this server." Reacts live to setting changes and is hidden in regular channels and on the welcome screen.
  • Admin settings — Server Updates moved to top of sidebar. The section was buried below Backup with no sidebar link; it now sits first above Branding.

Fixed

  • Admin settings — action buttons were stretched full-width. "Manage Sounds", "Manage Emojis", "Manage Stickers", "Manage Roles", "View Bans", "View Deleted Users", and "View All Members" buttons had btn-full applied; removed so they size to content and sit beside each other in a row.
  • #5294: Admin-configurable login session duration. New session_duration_days setting (1–365, default 7) replaces the hard-coded expiresIn: '7d' on every JWT signing site. Existing tokens keep their original expiry. Thanks @Amnibro.
  • Webhook integration expansion. Per-event subscriptions (message, reaction-added, member-joined, or *); HMAC signature header upgraded to sha256=<hex> under X-Haven-Signature plus X-Haven-Event; one automatic retry on 5xx/network failures; per-webhook delivery health fields (last_delivery_status, last_delivery_at, last_delivery_error, failure_count); new reaction-added and member-joined event types; test-webhook admin socket event; inbound reply_to support; inbound avatar_url per-message override. Existing webhooks default to * so nothing breaks on upgrade.
  • #5337: Link previews exhausted their rate-limit budget on chat re-renders. Rate limiter was running before the cache lookup, so cached previews still burned a token; budget doubled (30 → 60); client now keeps a 10-minute in-memory cache plus per-URL in-flight Promise dedup so concurrent re-renders share one request.
  • Phantom taskbar overlay badge with nothing visible to clear. Collapsed category labels now render an unread count bubble (previously the short-circuit skipped them, so their unread children counted toward the desktop badge but showed nothing in the sidebar). Muted channels no longer count toward the badge or tab title.
  • #5342: Media bundled with a text message was blocked by slow mode. Text + queued images fired two events — the text consumed the slow-mode tick and the images immediately hit the cooldown. Images sent as part of a combined call now carry bundled=true and skip the gate server-side. Same fix in PiP DM queues.
  • False CRITICAL perf alerts when the window was in the background. Chromium throttles rAF to ~1 FPS when hidden, making the FPS monitor fire CRITICAL every 15 s on healthy instances. Frame counting and the baseline timer now pause while document.hidden is true.
  • #5280 (follow-up): Burn-on-read visual indicator and pagination edge cases. Sender no longer sees the "tap to view" placeholder on their own burn messages. A flame label now appears in the message header of every unstarted burn message (removed once the countdown pill takes over). Forward and backward scroll pagination now correctly wire burn behavior (_wireBurnMessages was missing from both pagination paths).
  • PiP DM and thread text inputs can now be resized vertically. Both inputs had resize: none with a 120 px cap; changed to resize: vertical (200 px max, min-height keeps the handle visible). Send/emoji buttons stay anchored at the bottom; drag handle is center-aligned.
  • Role badges in chat messages were never displayed. this.users was never assigned — the correct stores are this._lastOnlineUsers and this.channelMembers. Role color on author names, role badges, and role icons in chat were silently broken.
  • Various fallout/matrix theme button contrast improvements; gear menu anchoring fix in the profile popup; getUserAllRoles deduplication (same role in two channels no longer renders twice in tooltips/badges).

v3.12.0

Choose a tag to compare

@ancsemi ancsemi released this 04 May 15:44

v3.12.0

This release rolls up everything since v3.10.13 (3.11.0, 3.11.1, 3.11.2, and 3.12.0).

3.12.0

Added

  • Starter sticker pack (#5335). Fresh installs now ship with a small built-in "Starter" pack (8 reaction stickers: 👍, ❤️, 😂, 🔥, 🎉, ✅, ❌, 👀) so the picker isn't empty before any sticker is uploaded. Existing servers keep whatever they already have.
  • :stickername: shortcode in the main composer (#5335). Type a sticker name surrounded by colons (e.g. :fire:) to send that sticker the same way :emoji: works for custom emojis.
  • Dedicated manage_stickers role permission (#5335). Sticker upload/management is now a separate permission from manage_emojis. Existing roles with manage_emojis keep sticker access automatically so nothing breaks on upgrade.

Fixed

  • Emoji picker auto-closed when switching to the Stickers tab and vice versa (#5335). The picker rebuilt the section button DOM on each tab switch, but the global outside-click handler ran after the rebuild and saw the original click target detached from the picker subtree, so picker.contains(e.target) returned false and the picker was dismissed. The section toggle now calls stopPropagation() on the click event and short-circuits if the user re-clicks the active section, so the picker stays open across tab switches.
  • Burn-after-read DM button gave no visible feedback and the placeholder showed a raw i18n key (#5325, #5280). Both _wireBurnMessages and _replaceBurnedMessage used the broken t('key') || 'fallback' pattern, but t() returns the key string itself when a translation is missing, so the || short-circuit never fired and the literal messages.burn_reveal text was rendered. Added the missing keys to en.json, switched every burn-related call site to a key-aware fallback, and added a toast on the 🔥 toggle confirming the armed/disarmed state so the user knows the click registered.

3.11.2

Added

  • Quick links to Bans / Deleted Users from the All Members modal. Mods and admins now see "View Bans" and "View Deleted Users" buttons in the bottom-left of the members list so they can jump between the three lists without going back through Settings → Admin. Buttons are hidden for users without ban_user (View Bans) or admin (View Deleted Users), and the server handlers re-validate permissions on emit, so DOM tampering can't reveal the lists.

Fixed

  • #5307 (follow-up): "Delete DM" and "Delete Channel" confirm dialogs showed settings.admin.delete / messages.delete raw i18n keys instead of "Delete". Both call sites now use t('msg_toolbar.delete'), which exists in every locale.
  • Manage Roles modal opened from the Role Assignment center showed an empty role list. The handler now uses _openRoleModal(), which shows the modal first and then loads, matching how the modal is opened from Settings.
  • Role Management modal didn't fill its window and resized vertical-only. The role modal is now a flex column (sidebar/detail panes grow with the modal, Close button pinned to the bottom-right), and its max-width is raised to 95vw so the resize handle works in both directions.

3.11.1

Fixed

  • #184: Voice audio routed to the system default playback device instead of the user's chosen output. The AudioContext now reads localStorage.haven_output_device at construction time and applies it via the sinkId constructor option (with a setSinkId() fallback for browsers/Electron builds that don't accept the option) so the saved sink takes effect on the very first track-add.

3.11.0

Added

  • Stickers (#5335). Server admins (and anyone with manage_emojis, plus the new manage_stickers permission added in 3.12.0) can upload sticker images grouped into named packs from Settings → Admin → Stickers. Stickers are larger than emojis (default 1 MB max, configurable via the max_sticker_kb server setting) and are sent as standalone images, not inline with text. The emoji picker now has an Emoji / Stickers section toggle; in the Stickers tab, packs appear as horizontal pills and stickers render as a 4-column thumbnail grid.

Fixed

  • #5333: DM PiP popped open over the channel you were already viewing. Added a guard at the top of _openDMPiP so the function bails out when the DM is already the current channel.

Full changelog: https://github.com/ancsemi/Haven/blob/main/CHANGELOG.md