Commit Graph
1189 Commits
Author SHA1 Message Date
ancsemi 8fd4b07bcb Server-stored nicknames are the record; stop re-pushing cached ones on connect (#5560) 2026-09-07 13:01:04 -04:00
ancsemi 41549d1487 Count invite redemptions on the link so a deleted account cannot hand one back (#5562) 2026-09-07 13:01:04 -04:00
ancsemi 9a09be303c Invite links saved before 5583 keep granting every public channel (#5583) 2026-09-07 13:01:03 -04:00
ancsemi abb9848a2e Send is_forum with the channel-created stub so a new forum shows its icon at once 2026-09-07 13:01:03 -04:00
birdcrazy 787e402d22 Image Display groups the animated image settings with the display mode (#5584)
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.
2026-09-07 12:55:32 -04:00
birdcrazy 5a5f2130fd Channel Scrolling setting: separate or combined sidebar scrollbars (#5582)
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.
2026-09-07 12:55:23 -04:00
birdcrazy d191e2d8cf Invite links grant only the channels that were ticked (#5583)
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.
2026-09-07 12:54:57 -04:00
birdcrazy cb339975d5 Thumbnails show the whole image instead of cropping wide ones (#5581)
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.
2026-09-07 12:54:48 -04:00
Matt 114b154f0c Timestamp picker: /time with no argument opens a date and time chooser (#5580)
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.
2026-09-07 12:54:45 -04:00
ancsemi e96b8205fc Add timestamps that render in each reader's own timezone 2026-09-05 20:40:15 -04:00
ancsemi 605f4f430c Release 4.4.1 v4.4.1 2026-09-05 17:57:27 -04:00
ancsemi 4ec25bffd3 Stop sending HTTPS-only headers when Haven serves plain HTTP 2026-09-05 17:47:06 -04:00
ancsemi 4fd0264090 Release 4.4.0 v4.4.0 2026-09-05 17:10:18 -04:00
ancsemi 5844fae1f3 Fold the cloudflared changelog lines into the existing Fixed block 2026-09-05 16:57:07 -04:00
Amnibro 4faad9fe86 Fetch cloudflared when it is missing, and say why Windows refused a port (#5575)
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.
2026-09-05 16:56:53 -04:00
ancsemi 7d7bac453c Changelog for native screen share signaling and account-side promo dismissals 2026-09-05 16:36:29 -04:00
ancsemi 226c13af56 Carry old promo and recovery-notice dismissals over to the account (#5570) 2026-09-05 16:36:29 -04:00
Matt 2b9dc381a4 Promo dismissals live on the account, and login no longer re-asks for consent (#5570)
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.
2026-09-05 16:35:08 -04:00
Bernardo Costa 0c31d06598 Negotiated native screen sharing transport, off by default (#5555)
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.
2026-09-05 16:34:57 -04:00
ancsemi c848021852 Changelog for the reply banner, soundboard, shortcut, invite card and sound preview changes 2026-09-05 00:53:07 -04:00
ancsemi 49a15ce3ef Preview the Messages sound even while the Notifications toggle is off 2026-09-05 00:53:07 -04:00
ancsemi 28f3cb431a Markdown shortcuts skip AltGr and Shift combos and fire input after wrapping (#5571) 2026-09-05 00:53:06 -04:00
ancsemi af2b1a965d Keep flags and keycap emoji in soundboard names (#5567) 2026-09-05 00:53:06 -04:00
delenda-delenda bcfb7eb374 Soundboard names can include emoji (#5567)
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.
2026-09-05 00:51:09 -04:00
birdcrazy 45f9066ae1 Copy an invite as an HTML email card (#5559)
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.
2026-09-05 00:49:42 -04:00
birdcrazy ffd48d77bb Markdown keyboard shortcuts in every composer (#5571)
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.
2026-09-05 00:49:39 -04:00
delenda-delenda 0e04e47f29 Reply banners name the bot instead of [Deleted User] (#5566)
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.
2026-09-05 00:49:36 -04:00
ancsemi a1243f7afc Keep the Moderation section for moderators after the settings access table (#5558)
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.
2026-09-03 21:11:23 -04:00
birdcrazy 03454f54cd Drive admin settings visibility from one access table (#5558)
* 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.
2026-09-03 21:10:13 -04:00
ancsemi 6c475dec10 Point the desktop download links at 1.4.30 2026-09-03 21:09:45 -04:00
Amnibro 1fd5af3c8c Ferry: relay a stream bot's link and thumbnail, not just its ping line (#5557)
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.
2026-09-03 21:09:43 -04:00
ancsemi b34181495e Release 4.3.0 v4.3.0 2026-09-03 00:42:34 -04:00
ancsemi 5eaacdd079 Trim trailing whitespace left by the markdown link paste change 2026-09-03 00:39:08 -04:00
ancsemi 39566120b7 Give forum channels an icon that is not the organize button
The sidebar was using the same clipboard icon for forum channels and for the organize buttons next to the Channels and DMs headers.
2026-09-03 00:38:20 -04:00
ancsemi 4614e8da1b Add forum channels (#144)
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.
2026-09-03 00:33:30 -04:00
ancsemi 0db4c232bf Keep the hover profile card and gear buttons alongside the unified user menu
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.
2026-09-03 00:00:50 -04:00
Mattandbo0sted 0b651e9b31 Unified user context menu and profile card redesign (#5554)
* 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>
2026-09-02 23:51:31 -04:00
Bernardo Costa 14116a4540 Add the Compact Layout plugin and shared layout ownership for structural plugins (#5552)
* Add reversible Compact Layout plugin

* Keep structural layouts stable through Mod Mode
2026-09-02 23:30:35 -04:00
birdcrazy a1f47321ca Paste a link over selected text to make a markdown link (#5553)
* 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()
2026-09-02 23:19:08 -04:00
ancsemi 56282feed7 Point the desktop download links at 1.4.29 2026-09-01 22:01:49 -04:00
ancsemi b2ac4e93e0 Make start-dm match only two-person channels (#5499)
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.
2026-09-01 22:01:48 -04:00
ancsemi 36a71f34ba Keep English as the base locale in the i18n tests (#5551)
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.
2026-09-01 22:01:48 -04:00
AmnibroandCursor daefc658f2 Group DM E2E: crypto core, key distribution, and tests (#5499)
* 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>
2026-09-01 22:00:38 -04:00
Amnibroandancsemi c931320a3c Braid: three spacing levels, and dark ink on the Android badge (#5494)
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>
2026-09-01 22:00:08 -04:00
b3cd0b4f0b GIF search: GIPHY is the supported provider again, not Tenor (#5546)
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>
2026-09-01 21:59:49 -04:00
Bernardo Costa 5f4c8ccd44 Internationalize the full client interface (#5551) 2026-09-01 21:55:56 -04:00
AmnibroandCursor 6b7535a4e3 Show Ferry and bot images on desktop that Haven Mobile already showed (#5550)
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>
2026-09-01 21:52:53 -04:00
ancsemi 8429e14cda Listening webhook follow-up: check the token before the upload, settle the live card (#5523)
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.
2026-09-01 20:54:15 -04:00
ancsemi 94f980aefd Let Full image mode use the whole message width
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.
2026-09-01 20:52:43 -04:00
Matt 52c68bac2a Add a generic listening-presence webhook and a live profile card (#5523)
* 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
2026-09-01 20:50:37 -04:00