Commit Graph
230 Commits
Author SHA1 Message Date
devsyncwrld d7da0ff203 feat(activity): show the current activity on the profile card
The activity pipeline was already complete end to end — Activity type, store,
WS broadcast, server validation, presence relay, and an ActivityCard used by
four list surfaces — but the profile card rendered none of it, which is the
'Listening to Spotify' block the design calls for.

Add ProfileActivity: richer than ActivityCard because the card has room for
artwork, track and artist, so it reads details/state/assets. All optional, so
it degrades to the bare name that today's process-based detector supplies.

Also scheme-check activity image assets server-side. activity.url was already
restricted to http(s) but assets.largeImage/smallImage were only length-checked
— an asymmetry that was harmless while nothing rendered them, and is not once
they become <img src>: a client could point them at a host it controls and
harvest the IP of everyone opening that profile.
2026-08-31 12:05:01 -03:00
devsyncwrld bfe62d7078 feat(voice): mic test with loopback in voice settings
The level meter only measured a stream a call had already opened, so settings
offered no way to check a mic before joining — the panel said as much.

Add startMicTest/stopMicTest on AudioManager: the processed input bus is
routed to the master output through a dedicated gain node, so the loopback can
be disconnected precisely. Settings had deliberately never opened the mic
itself; a mic test cannot honour that, so the test hands the mic back when it
stops.

Releasing needs two independent guards, because the user may join a call
mid-test: AudioManager only stops the exact stream it opened (identity check,
not a flag), and the caller must consent — the UI reads the call state, which
AudioManager cannot, as it does not import stores. Unmounting mid-test tears
the loopback down too.
2026-08-31 11:59:51 -03:00
devsyncwrld 20526e1bc8 feat(gif): outlined GIF glyph, and GIF picker for the profile banner
The composer's GIF button drew a filled rounded rect with the letters knocked
out, which reads as a solid square rather than a picker. Invert it: stroked
outline with filled letters, reusing the original glyph paths scaled to centre.

Banners already accept absolute URLs on both ends (server isValidAssetUrl
allows http(s); the profile render branches on banner.startsWith('http')), so
the picker stores the remote URL directly with no upload path. Previews can now
hold either a blob: or an https: URL, so revoking is guarded — calling
revokeObjectURL on a remote URL is a silent no-op that would hide a mistake.
2026-08-31 11:44:40 -03:00
cnrd 8456b8f976 fix(web): stop the profile card re-anchoring to its own avatar (#39)
Avatar opened the profile popout whenever it received a user prop. Since user is how every avatar gets its gradient, colour and status dot, all 22 call sites became profile triggers by accident — including the picture inside the profile card itself, which re-anchored the card to that picture on every click and walked it across the screen (120px right, 36px down, until it pinned at the viewport clamp).

Avatar is now presentational. A new ProfileAvatar carries the open-the-profile behaviour at the five call sites that actually want it. The card's own picture escalates to the full profile modal instead of reopening the card.

The card also places itself off its measured size via the shared computeFloatingPosition engine, replacing six call sites that each hand-computed coordinates against a guessed 460px card height.

Closes #37
2026-08-25 15:51:04 +02:00
Jannis Braun 85e1975fa5 fix(permissions): deny space permissions to non-members (invite-bypass)
computePermissions() returned the space @everyone role's permissions without
verifying the caller had joined the space. Because CREATE_INVITE is in
DEFAULT_EVERYONE_PERMISSIONS, any authenticated user could mint an invite code
for a request-only space — whose id is listed by /api/spaces/explore — and then
self-join via /api/spaces/:id/join, bypassing the join-request approval flow.
The same gap let non-members read message history and search default channels.

Root cause:
- computePermissions now returns 0n for non-members (space owner and instance
  admin still short-circuit first, so they are unaffected).

Defense in depth (request-only spaces are approval-gated, never invite-joinable):
- both invite-code join endpoints reject visibility='request' (private stays
  invite-joinable — its only entry path; public too).
- POST /api/spaces/:id/invite refuses to hand out a code for request spaces.
- POST /api/dm/space-invite refuses to card a local request space, checked by
  space id against the local table so a spoofed spaceInstanceOrigin can't slip
  past it.
- InviteModal hides the invite affordances for request spaces.

Also removes the unused computeCategoryPermissions(), which duplicated the
resolution algorithm without the membership gate.

Adds unit + route + component tests covering non-member/member/owner/admin
resolution and the request/private/public visibility matrix.

Reported-by: BadAtCaptchas (#2)
2026-07-07 19:45:51 +02:00
Jannis Braun 21c8731f3a feat(web): refetch DM list after re-attach so reconciled conversation replaces the split (reattach-dm-reconcile spec §3.4) 2026-07-03 12:44:00 +02:00
Jannis Braun d3af4f2170 fix(federation): close re-attach final-review findings — client/server domain normalization, merge attachment repoint, empty-domain guard, test hardening 2026-07-03 02:43:45 +02:00
Jannis Braun 521aff6e52 docs(federation/auth/api/db/client): detached-account re-attach flow; reset-cleanup panel mentions re-attach 2026-07-03 02:28:35 +02:00
Jannis Braun 344a429e98 feat(web): automatic re-attach on connect + AccountPanel fallback action (re-attach spec §3.4) 2026-07-03 02:19:51 +02:00
Jannis Braun 13d050c1bb fix(federation): close detached-account gaps from final review — presence/hydrate guards, ack re-detect clear, self-delete password (detach spec §4.3/§4.4/§4.6) 2026-07-02 19:34:32 +02:00
Jannis Braun 9e917d5117 feat(federation): account-settings notice for detached accounts (detach spec §4.7) 2026-07-02 19:10:35 +02:00
Jannis Braun 5ad8aefaff feat(federation): reset-cleanup panel — informational detach copy, real server-side Dismiss, Keep removed (detach spec §4.6) 2026-07-02 19:06:21 +02:00
Jannis Braun ad7c86e89e fix(web): Re-peer surfaces incomplete outcome instead of false success (BUG-2) 2026-07-02 13:03:03 +02:00
Jannis Braun 43d1dad1d7 fix(federation): carry error body on HttpError so Reset-cleanup owns-spaces copy reaches the UI
Also narrow SanitizedPeer.needsAttentionReason to the shared union.
2026-07-02 02:19:13 +02:00
Jannis Braun be7749b83f fix(federation): scope Reset-cleanup Remove owns-spaces detection to ownedSpaces payload 2026-07-02 02:01:50 +02:00
Jannis Braun 946255c4a1 feat(federation): admin Reset cleanup UI (Re-peer + Keep/Remove) 2026-07-02 01:57:30 +02:00
Jannis Braun 807b2f6234 refactor(web): fold final-review polish into join modal feature 2026-07-01 19:06:10 +02:00
Jannis Braun 309abd86e2 feat(web): discovery-first Join a Space modal 2026-07-01 18:56:56 +02:00
Jannis Braun fc8df54cb0 feat(web): compact ExploreSpacePreviewCard for join modal 2026-07-01 18:51:31 +02:00
Jannis Braun f481e1fe9e license: relicense to AGPL-3.0-only with commercial dual-license
- LICENSE -> verbatim GNU AGPL-3.0; add LICENSE-COMMERCIAL.md + SECURITY.md
- CLA -> exclusive-license grant (contributors keep copyright); add README
  anti-rugpull covenant + relicense record
- NOTICE / README / CONTRIBUTING / CLAUDE.md / package.json x5 updated;
  contact routed through GitHub (no email placeholders)
- AGPL section 13 source offer: operator-configurable BACKSPACE_SOURCE_URL +
  build-injected commit; sourceCodeUrl+commit on /api/instance/info;
  SourceCodeLink on login/register/settings/desktop; docs + .env.example updated
2026-07-01 16:38:22 +02:00
Jannis Braun e372a7a571 feat(web): Check now button to manually recover unreachable federation peers 2026-06-26 13:58:38 +02:00
Jannis Braun f04477c812 test(ui): GroupDmSettings — mock ImageCropModal to harden cancel + save-with-icon tests 2026-05-10 20:33:06 +02:00
Jannis Braun 5e739b170e feat(ui): GroupDmSettings modal with Overview + Members tabs 2026-05-10 20:26:25 +02:00
Jannis Braun 8544f83225 refactor: drop unwired ConnectInstanceModal
Commit 7309f44 removed the friend-add trigger because the server now
handles all routing/peering/lookup; the commit message reserved the
modal for 'Connections settings and space-join flows' but neither flow
ever wired it back in. grep across packages/web/src finds only
self-references — pure dead code.

Removing 162 lines of UI plus the stale vi.mock in FriendsPage.test.tsx.
If a per-instance password-re-entry consent UX is ever needed, the
useInstanceConnect hook is the actual abstraction and the modal can be
rebuilt cleanly with current primitives (Modal mobileStyle="fullscreen",
ContextMenuRenderer's bottom-sheet, etc.).
2026-05-05 23:24:19 +02:00
Jannis Braun 438e7fb113 fix(storage): StoragePanel rows wrap on narrow viewports
Upload Limit, Stale Uploads, and Media Retention rows now use
flex-wrap with gap-x-3 gap-y-2; input + unit pairs that must stay
together are wrapped in a nested flex group so they migrate as a unit.
Desktop spacing identical (gap-x-3 ≡ original gap-3 when content fits
on one line).
2026-05-05 23:24:10 +02:00
Jannis Braun 899dc8b601 feat(settings/mobile): Voice & Video adaption + chat header username normalization
MobileChatScreen DM header now applies parseFederatedUsername and
useCanonicalUserView, matching MobileDmsScreen so federated users render
as 'realname' rather than 'realname@domain'.

Mobile Voice & Audio renamed to Voice & Video across MobileSettingsScreen
and MobileYouScreen — parity with desktop's VoicePanel title.

AudioInputSection and VideoSection picker click-outside now listens for
touchstart alongside mousedown so a single tap dismisses on iOS Safari
(which doesn't synthesize mousedown reliably from touch).

AudioOutputSection feature-detects HTMLMediaElement.setSinkId at module
load and returns null on unsupported platforms (notably iOS Safari).
Split into outer gate + inner body to keep Rules of Hooks intact;
VoicePanel's space-y-5 collapses cleanly with no visible hole.

VideoSection 'Stop preview' CTA gets responsive sizing (mobile:
always-visible 44 px tap target; desktop: original hover-reveal pill via
md: prefixes). The preview <video> gains autoPlay so iOS Safari starts
the stream when srcObject is assigned — manual videoEl.play() after an
awaited getUserMedia loses the user-gesture context on iOS and was
silently rejected by .catch(()=>{}), causing the black-preview bug.

voice.md updated for the iOS hide-when-unsupported policy and the
touch-close contract on device pickers.
2026-05-05 23:24:03 +02:00
Jannis Braun e2d938e6c0 feat(mobile): admin reach + post-mount routing + RegistrationPanel modals → Modal
MobileInstancePanel gains Registration and Federation entries (parity with
desktop's six sub-tabs). MobileShell registers the matching screenMap
wrappers; the Federation entry surfaces the live approval-count badge via
a new uiStore slot and a wrapper that forwards FederationPanel's
onApprovalCountChange.

MobileShell's deep-link reconstruction now reacts to post-mount pathname
changes (was [] / mount-only) with an idempotency guard that skips the
push when the topmost stack entry already represents the new URL —
prevents the pushMobileScreen → history.pushState → location-effect
double-push.

RegistrationPanel's four portaled modals (CreateInvite, EditInvite,
ReinstateInvite, Redemptions) now render through the shared <Modal>
with mobileStyle="fullscreen", portaled to document.body to escape the
parent settings dialog's backdrop-filter containing block. Desktop
appearance preserved (same maxWidth, same sticky action bar).
2026-05-05 23:23:37 +02:00
Jannis Braun 49e9047005 feat(client-federation): user-view cache for cross-instance DM render
Fixes a render bug where a federated user (e.g. axel@nova) appeared with
the federation globe icon and a broken avatar when viewed on his own home
instance. Root cause: `populateFromReady` is first-wins by federatedId and
discards the entire skipped DM payload — including its `members` array —
so when a sibling instance's ready arrived first, the home instance's view
of every shared user was dropped on the floor.

Adds a render-only `userViews` cache that mirrors the `dmAlternatives`
philosophy: information from skipped ready payloads is preserved for
rendering. Every wire surface that delivers a User upserts into the cache
regardless of dedup outcome; render sites read through a Zustand selector
hook to surface the home view when one is loaded. The DM channel ingestion
race is left untouched — the existing no-flapping invariant on origin
reconnect is intentional and load-bearing for failover.

Layered changes:

- `identity.ts`: `normalizeOriginToHost`, `canonicalUserKey`,
  `isDeliveryFromHome`, `isFederationGlobeApplicable` — single helpers
  for origin/host normalization and the home/stub tier decision.
- `spaceStore.ts`: `userViews` Map, `UserViewEntry` type, `upsertUserView`
  action with the home-wins preference rule, prune by `deliveredBy` in
  `removeInstanceSpaces` (mirrors `dmAlternatives` cleanup), `reset`
  clears.
- `userViewLookup.ts`: `useCanonicalUserView` (Zustand selector hook for
  React) + `getCanonicalUserView` (sync getter for non-React paths).
  Render reactivity is structural via the selector, not coincidence on
  legacy update paths.
- `populateFromReady` upsert pass runs BEFORE the federatedId dedup so
  members of skipped DMs still reach the cache.
- WS handlers (dm_message_*, message_*, user_updated, member_joined,
  friend_request_*, dm_channel_created, dm_member_added) and REST
  hydrators (socialStore, discoverStore, mutuals) feed the cache with
  their delivering origin.
- Render-site routing through `useCanonicalUserView` at every audited
  user-rendering site (sidebar, header, search, message bubble, reply
  chips, profile popout/modal, group settings, voice tiles, mention
  chips, member lists, friends, invites). Self-rendering sites compose
  alongside via existing `isSelf`/`resolveDisplayIdentity`.
- Globe predicate hoisted to `isFederationGlobeApplicable` and applied
  at three sites, gating on `domain !== window.location.host` so we
  never show the globe for users whose home IS our own.

Tests: 31 new unit tests across `identity`, `userViews` store, and
`userViewLookup`. Full suite 276/276.

Docs: `client-federation.md` §3 gains a "User View Cache" section
parallel to "DM Origin Failover"; `dm-system.md` notes the new store
action and WS handler upserts.

Bug 3 (federation profile-sync gap — orbit's stale profile data on
nova-Axel after a clear/color-change on nova never propagated)
remains open. The user-view cache routes around it for the common case
(home instance is connected), but the underlying S2S relay gap is its
own diagnosis and follows in a separate branch.
2026-05-05 01:59:05 +02:00
Jannis Braun ff8dcc28ea feat(web): wire AudioInput/Output sections into Voice & Video settings; fix EC tooltip 2026-05-03 01:52:20 +02:00
Jannis Braun 5b8af14e12 feat(web): AudioOutputSection — output picker, volume, test tone, lifecycle-aware sinkId check
- Uses shared SectionShell/DropdownItem from ./_shared/SettingsPickerPrimitives
  rather than redefining local copies (parity with AudioInputSection).
- supportsSinkId is reactive to AudioContext lifecycle via the audioCtxGen
  pattern (bumped by AudioManager.onResumed). Defaults to true and only flips
  to false when a real context exists AND lacks setSinkId (Safari < 17), so
  the picker is never preemptively hidden when the user opens Settings before
  joining voice. AudioManager.setOutputDevice + initContext re-apply path
  handles the deferred sinkId binding once the context appears.
2026-05-03 01:47:49 +02:00
Jannis Braun 8e67f1d553 fix(web): mic meter reacts to AudioContext lifecycle; extract picker primitives
- AudioInputSection now subscribes to AudioManager.onResumed and bumps an
  audioCtxGen state on each 'running' transition. Mic-level meter and
  resolved-default hint effects depend on it, so opening Settings before
  joining voice and then joining voice activates the meter without needing
  to remount the panel. Footer copy updated to match the new behavior.
- SectionShell and DropdownItem extracted to
  settingsPanels/_shared/SettingsPickerPrimitives.tsx so Task 6
  (AudioOutputSection) can import them instead of triplicating the markup.
  The _shared/ folder keeps these settings-internal primitives out of the
  broader ui/ namespace.
- Deliberate scope choice: VideoSection.tsx still has its own DropdownItem
  copy. Unifying all three is left to a follow-up; touching VideoSection
  here would expand scope beyond the audio-device-ux branch.
2026-05-03 01:43:01 +02:00
Jannis Braun 3aea8210bd feat(web): AudioInputSection — full input picker with level meter and resolved-default hint 2026-05-03 01:37:37 +02:00
Jannis Braun 2f0940c30b feat(admin): manual cleanup of stale tus upload sessions + visibility
Adds an admin-driven sweep on top of the existing 24h auto-expire so
operators can see and reap abandoned `.tus/` sessions without waiting.

- storageJanitor: extract `walkTusDir(predicate)` helper, add
  `getStaleTusInfo` + `cleanupStaleTusSessions(thresholdMs, dryRun)`;
  refactor `cleanupTusStragglers` to delegate while preserving its
  janitor-tick `{ removed }` contract.
- StorageStats gains `staleTusSessions` + `staleTusSize` (fixed 1h
  display threshold).
- New `POST /api/admin/storage/cleanup-tus` route with
  `maxAgeHours` validation (positive finite number, default 1) and
  `dryRun` support; admin-gated.
- StoragePanel: 6th overview card "Stale Uploads" + new cleanup
  subsection mirroring the media-cleanup pattern (preview-then-clean
  with shared result panel styling).
- Tests: 8 new janitor tests covering empty dir, threshold filtering,
  dry-run vs live, oldest-mtime tracking, subdir skipping, and the
  override path on the existing straggler sweep. New
  `routes/admin.test.ts` covers auth/admin gates, validation (zero,
  negative, NaN), default `maxAgeHours`, dry-run vs live unlink.
- Docs: `uploads.md` §Janitor expanded to the full lifecycle (cancel
  DELETE, discard DELETE, auto-expire, straggler sweep, admin route);
  `admin.md` Storage Management updated with the new endpoint and
  StorageStats fields.
2026-05-02 18:44:19 +02:00
Jannis Braun 2be243336b feat: all profile uploads through transferStore; delete legacy POST /api/uploads
Migrates the remaining 5 profile/space upload sites (CreateSpace, AccountPanel
avatar+banner, OverviewPanel icon+banner) to transferStore.startUpload with
tray:false. Space sites pass _instanceOrigin so uploads route to the space's
home instance.

Removes upload/uploadWithProgress from api.uploads (and their private
uploadFile/uploadFileWithProgress helpers); api.uploads.url is preserved for
GET-path URL building. Deletes the server-side POST /api/uploads handler and
the now-unused @fastify/multipart plugin registration. GET /api/uploads/:filename
remains intact.
2026-05-02 16:46:29 +02:00
Jannis Braun a46d35f5f8 feat(web): InviteModal — friend picker + restyled share-link footer, drop deep link 2026-04-29 22:02:23 +02:00
Jannis Braun d31a9cf579 refactor(web): RegistrationPanel — glass-bubble modal buttons + clearer Revoke copy 2026-04-29 03:01:47 +02:00
Jannis Braun f0ac847d9e refactor(web): RegistrationPanel sibling parity — Edit/Reinstate redesign + Redemptions header 2026-04-29 02:51:44 +02:00
Jannis Braun a52bab4c16 refactor(web): CreateInviteModal — icon-badge header + uppercase section labels + dominant submit 2026-04-29 02:47:44 +02:00
Jannis Braun 526043a3b4 feat(web): RegistrationPanel sort + status filter via FilterDropdown 2026-04-29 02:44:32 +02:00
Jannis Braun b91bc89c58 refactor(web): RegistrationPanel InviteRow → collapsed/expanded model 2026-04-29 02:40:33 +02:00
Jannis Braun 3fdf501620 refactor(web): RegistrationPanel tab strip restyle + count badges 2026-04-29 02:36:52 +02:00
Jannis Braun a7c321a9d4 feat(web): show federated-registration warning banner in Connections add-instance form 2026-04-29 01:49:32 +02:00
Jannis Braun bde4d829ef feat(web): RegistrationPanel Edit/Revoke/Reinstate/Delete + Redemptions 2026-04-29 01:21:14 +02:00
Jannis Braun ba5c84fc6b fix(web): add Custom… expiry preset to CreateInviteModal per spec §4.2 2026-04-29 01:13:22 +02:00
Jannis Braun 234ec8e70d feat(web): RegistrationPanel Create-invite modal 2026-04-29 01:06:48 +02:00
Jannis Braun 42d1be54fa fix(web): guard RegistrationPanel invite-list against stale-fetch race 2026-04-29 00:57:57 +02:00
Jannis Braun dcd3daf390 feat(web): RegistrationPanel invite-list with Active/Archived tabs 2026-04-29 00:53:39 +02:00
Jannis Braun 7dd6bd6263 refactor(web): move registrationOpen toggle out of GeneralPanel 2026-04-29 00:44:35 +02:00
Jannis Braun cad6c50bff feat(web): RegistrationPanel scaffold with both registration toggles 2026-04-29 00:40:04 +02:00
Jannis Braun 76b6621d62 feat(storage): remove upload-size cap, add MB/GB unit toggle
Storage panel's max-upload-size input now accepts any positive integer
(bounded only by JS safe-integer ceiling) and offers an MB/GB unit
toggle. Multipart limit relaxed to MAX_SAFE_INTEGER — actual cap is
enforced per-request from the DB setting, not at the framework layer.
2026-04-28 18:37:16 +02:00