Image Display Mode, Animated Profile Pictures and Animated Images in Chat sit together under one Image Display section instead of three scattered ones. Thanks to @birdcrazy.
Layout settings gain a Channel Scrolling choice: separate scrollbars for channels and DMs, or one for the whole sidebar. Short windows fall back to the combined layout so the channel controls stay reachable. The Layout section is split into Density and Channel Scrolling subsections, and the picker setup code is shared. Thanks to @birdcrazy.
An invite created by someone who cannot see every public channel no longer drops the invitee into channels the inviter never saw, and unticking every box means no channels rather than all of them. Fixes#5569. Thanks to @birdcrazy.
Chat image thumbnails use object-fit contain, so a wide image is letterboxed inside the thumbnail box rather than having its sides cut off. Thanks to @birdcrazy.
A clock button in the input bar and a bare /time open a picker with date and time fields, a 24-hour or AM/PM toggle that follows the reader's own clock, and all seven styles previewed live with their own Insert button. Tokens land at the caret. The render pipeline is reused unchanged. Thanks to @Bo0sted.
The tunnel module looks for cloudflared on PATH, then in the data directory's bin folder, and downloads the official release binary there on first start when neither has it. The installer pre-fetches the same file when Cloudflare is chosen. A refused port on Windows now points at the Hyper-V/WSL reserved range check. Thanks to @Amnibro.
The desktop and mobile promo popups and the recovery-codes notice remember Don't show again on the account instead of in localStorage, so hardened browsers that wipe storage stop re-showing them. The 18+ and Terms checkboxes now appear only when creating an account; login carries a note that signing in accepts the current Terms. Thanks to @Bo0sted.
Server-side signaling and codec negotiation for a native Desktop screen share path. Off unless the user opts in from Settings, and the toggle only appears inside Haven Desktop builds that carry the native helper. Browser and per-app audio capture are unchanged. Thanks to @bernardokcosta.
Custom sound names keep letters in any script, numbers, spaces and emoji instead of being stripped to ASCII on upload and rename, so the name you typed is the name you see. Fixes#5565. Thanks to @delenda-delenda.
Each invite link gets an envelope button that copies a formatted invitation card, using the active theme, language and server branding, ready to paste into an email. Thanks to @birdcrazy.
Ctrl/Cmd+I, Ctrl/Cmd+B, Ctrl/Cmd+Shift+C, Ctrl/Cmd+Shift+X and Ctrl/Cmd+Shift+P wrap the selected text in the message box, thread box, PiP DM box and the message editor. Pasting a link over selected text in the editor makes a markdown link too. Thanks to @birdcrazy.
Bot and webhook messages keep their name in the reply banner. Reply enrichment in channel history, live sends, threads and the webhook HTTP path now shares one helper that formats authors the same way message rendering does. Fixes#5564. Thanks to @delenda-delenda.
The new access table listed section-moderation as admin only, which would have
taken the idle-online oversight page away from moderators who hold ban, kick,
audit log or view-all-members rights, the same people the server lets act on it.
The table now lists those permissions, and the Admin tab gate in app.js carries
the same three so the tab can be opened by everyone who can see a section.
Also restores the flush class on the invite-links divider for invite_users-only
users, and clears the changelog's Unreleased section of entries 4.3.0 shipped.
* hide admin settings except invite section for invite only users
* Also hide the max ivite links uses setting
* Refactor _syncSettingsNav() for maintainability
It now uses a dictionary, defining the admin-settings access, to control visibility of admin settings sections and nav-items.
Also added a few missing categories to the nav-items list.
* also use list to effect admin settings tab visibilty.
Promotion bots (CouchBot and friends) type the ping line and put the stream link, title, and thumbnail in a rich embed. Ferry only read the embed when the Discord message had no text, so Haven got "X is live" with nothing to click or look at. Rich embeds are now read alongside the typed text, the promoted link is policy-checked on its own so an allowlist server drops the link and keeps the announcement, and Discord's own unfurls of typed links stay ignored.
A forum is a regular channel where every top-level message is a topic.
Replies go in the message's thread, the topic shows its reply count, and a
reply bumps the topic to the newest end of the channel, so old topics
resurface instead of sinking. Toggle it in Channel Functions or tick Forum
when creating a channel.
The history query orders topics by their latest activity, with cursors
resolved to activity stamps so paging keeps working, and topics never
compact into each other so a bump can move the node in place. Covered by an
integration test that boots a scratch server.
PR #5554 folded the moderation actions into the right-click menu, which stays.
This restores the hover preview card behind a new Settings > Chat toggle (on by
default) and brings back the gear buttons on member rows and the profile card.
Both gears now open that same unified menu anchored to the button, so touch and
keyboard users keep a visible way into the moderation actions.
* Remove hover profile card, copy its styling to the click card
* Anchor the profile card to the right of the clicked name or icon
* Toggle the profile card when the same user is clicked
* Unify the user menus into one right-click context menu
Fold the mod-only gear menu into the member-list context menu so
right-clicking a user (member list or chat author) opens one menu with
everything: profile actions plus role/channel/moderation, gated as
before. Remove all gear-icon entry points (profile card and sidebar),
the old gear menu, and the profile card's Set Nickname button, since the
context menu now covers them. Add a "Role Management" label across locales.
* Open the full-res avatar in the lightbox from the profile card
Clicking a real avatar (not the letter fallback) on a profile card opens
the image in the existing lightbox, for other users and yourself. The
card stays open behind it: the lightbox now sits above the card, and the
card's outside-click handler ignores clicks on the lightbox so dismissing
the image keeps the card open.
---------
Co-authored-by: bo0sted <mc526+github@proton.me>
* Create markdown link when pasting a link over highlighted text in msgInput
* Also paste markdown links over selected text in threads and dm PiPs
* Use exisiting paste handlers instead of creating new ones.
* remove link markdown pasting function from setupImageUpload()
The lookup for an existing DM checked that both people were members of an
is_dm channel and nothing else. Now that group DM channels exist with the
same flag, a 1:1 DM between two people who also share a group could resolve
to the group instead. The lookup now requires exactly two members (one for a
notes-to-self DM), so a direct message always lands in a direct message.
The new parity tests required pt.json to carry every key English has, and
failed if any page referenced a key Portuguese lacked. Haven's other locales
are partial by design and fall back to English per key, and a contributor who
adds an English string usually cannot write the Portuguese for it, so that
check would have failed every future change that touches a string.
What stays enforced: every referenced key exists in English, pt.json never
carries a key English has dropped, shared keys keep matching placeholders, and
the HTML-bearing strings keep their markup in every locale that has them.
* Group DM E2E: crypto core, key distribution, and tests
Implements the design in docs/group-dm-e2e-plan.md far enough to prove the
properties hold. No UI yet — this is the layer everything else sits on.
public/js/e2e-group.js holds the primitives, deliberately free of storage,
sockets and DOM so the same file runs in the browser and under node --test.
Confidentiality comes from an AES-256 epoch key wrapped per member over the
pairwise ECDH secret that already exists; authenticity from an ECDSA P-256
signature over a digest binding channel, epoch, sender and the previous
message. Verification happens before decryption and a rejected message
returns null plaintext, so a forgery cannot be rendered by mistake.
The signature is what makes one shared group key safe: every member can
decrypt, only a key holder can author.
src/socketHandlers/groupE2E.js distributes wrapped blobs the server cannot
open, while enforcing what no client can check for itself — only a member
may publish an epoch, epochs are strictly sequential, a member reads only
their own blob, and an epoch must cover exactly the current membership.
That last rule stops a member publishing an epoch that silently omits
someone, locking them out of a conversation the UI still shows them in.
Two suites, 28 tests, runnable with npm test. The crypto suite is written
as attacks rather than happy paths: forging as another member, re-attributing
a genuine message, tampering, stripping the signature, replaying into another
channel or epoch, reading an epoch you were not given, and unwrapping a key
not wrapped for you. It also asserts scrollback still decrypts across several
rotations, which is the regression a per-message ratchet would have caused,
and that concurrent sends sharing a prev do not false-alarm.
Confirmed the suite is load-bearing by disabling the signature check: exactly
the five attack tests fail and nothing else.
* Group DMs invite instead of auto-joining, and rewraps require a request.
Addresses review on #5499: drop leftover setup.js, consent plus roster
dedup on start-group-dm, and pin rewraps to a prior request plus the
recipient's TOFU public key.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Braid shipped one spacing. Settings → Braid spacing (and Menu → Braid
spacing, which cycles without closing the menu) now offers Compact, Cozy
and Spacious. Cozy is exactly what shipped, so the two new levels go in
both directions: the same eight messages measure 417 / 513 / 617 px.
The scale is tokens on html[data-braid-density], read by both the form
and layout sheets, so message runs and channel rows tighten together
along with the sidebar width. Corner radii are deliberately left out of
the scale — this is a density control, not a shape one, and the cards
measure an identical 14px (12px for channel rows) at all three levels.
--braid-gutter is now derived as inset + avatar + row gap rather than
hardcoded. A continuation indents by exactly its leader's gutter, so a
level that shrinks the avatar can no longer drift a run out of line with
its own first message. Measured 0px drift at all three levels.
The Android App badge kept a hardcoded white label. It cannot follow
--accent-text because it paints itself a fixed Android-green gradient in
every theme, so white sat at 1.78:1 at the light end of that gradient.
It and its dismiss button now use an ink that measures 9.65:1 there and
6.46:1 at the dark end.
Co-authored-by: ancsemi <ancsemi@gmail.com>
5472 preferred Tenor because GIPHY looked dead. That was backwards: GIPHY still works, and Tenor is no longer offered for new setup. Existing Tenor keys still serve if no GIPHY key is set.
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: ancsemi <ancsemi@gmail.com>
The web client HTML-escaped CDN query strings before the media proxy fetched them, so signed Discord URLs 404ed and the bubble was blank. Decode first, align the /uploads/ image regex with mobile, and stop 403ing Discord with a bot User-Agent.
Co-authored-by: Cursor <cursoragent@cursor.com>
The webhook ran the multipart parser before looking the token up, so a post
to a made-up token still cost up to 512KB of buffering. The token is now
resolved first and the body is only read for a real one.
The open profile card re-rendered its status and activity slots on every
online-users broadcast, which fires for every join, leave and poll, so a card
showing cover art recreated its image and restarted the progress timer each
time. It now compares against what it last rendered and only writes on a
change.
The API reference moves from the repo root to docs/listening-api.md next to
the other guides, and the links in Settings follow it.
The Full setting capped inline images at 25rem wide and 18.75rem tall, which
on a normal desktop window is well under half the message area, so the
"large" choice never read as large. Images in Full mode can now span the full
width, with a height cap that scales with the window so a wide image can
still fill the row without a tall one taking over the screen. Thumbnail mode
and the mobile sizes are unchanged.
* Add Navidrome rich presence integration
Shows what a user is playing in Navidrome on their Haven profile, through the
Havidrome companion plugin posting to a reserved per-user webhook.
Users generate a revocable webhook token in Settings > Activity > Navidrome.
The webhook takes strict multipart: title and duration are required, and cover
bytes are magic byte checked and held in Haven's memory. The Haven server hands
those bytes to viewers' browsers from its own /api/activity/navidrome-cover
path, so Navidrome is never contacted and its address never leaves the plugin.
Presence runs through the existing activity slot so it stays online gated,
clears on disconnect, and auto expires at track end. The profile card gains a
live progress bar; pause freezes it, stop clears it.
* Update the open profile card live from presence
The profile card was a snapshot, so a track pausing, resuming, changing or
ending did nothing until it was reopened. It now re-renders its activity from
the online-users broadcasts the client already receives, so the bar freezes on
pause, resumes on play, swaps on a new track and clears on stop while the card
stays open. It rides the existing presence stream, no new traffic.
* Make the whole profile card live and stop it clipping off-screen
The open profile card only re-rendered its music activity from presence;
status (online/away/dnd/offline) and custom status text stayed frozen at
whatever they were when it opened. It now refreshes all three from the
online-users broadcasts the client already receives, so a user going away,
setting a status or dropping offline updates the card in place while it
stays open. Shared _statusDotClass/_statusLabel/_profileStatusTextHtml
helpers keep the initial render and the live refresh in sync, and the
custom-status line gets a stable slot so it can appear and clear. Rides
the existing presence stream, no new traffic.
Positioning measured the card with a hardcoded 320x400 guess, but its
width is a rem value and its height varies with content, so once font-size
or zoom differed from 16px the real card was larger than the guess and the
viewport clamp let it spill off the right edge (desktop app and web). It
now measures the real offsetWidth/offsetHeight before clamping.
* Expire a paused Navidrome track after 5 minutes idle
A paused track cleared its play-to-end timer and set no replacement, so
the slot lingered indefinitely with no further posts. Since presence is
gated on being online, the stale track stayed hidden while offline but
reappeared on reconnect. Arm a 5-minute idle timeout while paused instead;
every update resets it, so an actively-paused client stays shown.
* Rate-limit the Navidrome cover route
The cover-serving GET had no limiter while the webhook it pairs with did.
It's viewer-facing (every open profile card fetches it), so add a separate,
looser cap (120/min vs the webhook's 60/min) rather than reusing the
webhook limiter, keeping the two routes consistent without throttling
legitimate viewing.
* Make the music presence webhook generic, not Navidrome-only
The rich presence webhook was tied to Navidrome and a single companion
plugin, so Haven had a settings page that only worked with one source and
would point at nothing if that plugin went away.
It is now a source-agnostic Listening webhook. Any music player that can
run a plugin or a script posts a title, artist and duration (plus optional
album, position, cover art and a source label) to a personal URL. The
contract is documented in listening_api.md.
- Route is /api/webhooks/listening/:token, cover is
/api/activity/listening-cover/:userId. Table navidrome_tokens is renamed
to listening_tokens with a migration that keeps existing tokens.
- A track is identified by a server-side hash of title, artist and
duration rather than a poster-supplied id, so nothing untrusted is used
as an identifier.
- Expiry is a capped lease instead of trusting the reported duration: a
playing track is held until it would end, up to 60 minutes, and long
media stays visible by sending a heartbeat. A fake duration can no
longer leave a status stuck.
Anyone still using the old Navidrome webhook needs to point their plugin
at the new /api/webhooks/listening URL.
* Scroll long track titles and artists instead of clipping them
A long title or artist was cut off with an ellipsis at whatever width
the viewer's profile card happened to be, so different people saw
different amounts of the same track. Past 30 characters the text now
scrolls in a seamless loop at a constant speed, gated on character
count rather than pixel width so it reads the same for everyone.
Shorter text is unchanged, and the scroll respects reduced-motion.
* Trim the redundant intro in the listening API doc