Commit Graph
184 Commits
Author SHA1 Message Date
Jannis Braun 6fb38d391c feat(mobile): chat polish — floating composer + iOS keyboard handling + bottom-sheet drag-close + file-chip overflow
Multi-pass mobile chat polish landed across this session.

- MessageInput is now a floating glass-bubble (`position: absolute`) on both desktop and mobile — last messages scroll *behind* the translucent bubble. MobileChatScreen wraps MessageList + MessageInput in a `relative` parent so absolute positioning resolves. Removed the prior in-flow mobile branch that clipped message-list bottom against an invisible barrier.
- iOS PWA keyboard handling: new `useVisualViewportInset` hook subscribes to `visualViewport.resize/scroll` AND polls `vv.height` for ~600ms after focusin (iOS PWA standalone often fails to dispatch resize for keyboard transitions). MobileShell sizes its container to `vv.height` when keyboard is open — `bottom: 0` on the composer naturally lands flush with the keyboard top, regardless of how reliably resize events fire. Composer uses 6px gap above home indicator (keyboard closed) and 0px gap above keyboard (keyboard open). Added `interactive-widget=resizes-content` viewport meta as the cleaner native equivalent for Chrome/Android.
- MessageList bottom padding is dynamic via `--composer-clearance` CSS variable. MessageInput writes `composerHeight + bottomOffset + 12px` to its parent via ResizeObserver — re-fires on textarea autosize, reply banner, attachment tile growth, parent resize. Last message always has 12px breathing room above the bubble regardless of composer state.
- AttachmentRenderer generic file chip: `max-w-full sm:max-w-[400px]` on outer + `min-w-0` + `flex-shrink-0` on icon + `flex-1` on text + `flex-wrap` on badge row. Long filenames now ellipsize cleanly on narrow viewports instead of pushing the chip off-screen.
- New `useDragToClose` hook: shared bottom-sheet drag-down-to-dismiss gesture. Spread on handle/header only (body scrolling unaffected). 6px deadzone, 100px or 0.5px/ms velocity threshold, 200ms `cubic-bezier(0.22, 1, 0.36, 1)` close-out animation, rAF-staged transform for a stable from-value. `hasInteracted` latch prevents the open keyframe from re-firing mid-close (the bounce-up-then-vanish bug). Wired into InputPopover (emoji/GIF), MobileVoiceJoinSheet, MobileFolderSheet.

Specs: docs/systems/mobile-ui.md (Floating Composer + Drag-to-Close sections), docs/systems/message-list.md (--composer-clearance), docs/systems/design-system.md (glass-bubble row references).
2026-05-08 10:23:32 +02:00
Jannis Braun fb9fe326c4 feat(mobile): Wave 4 — RegisterPage responsive + TransferIndicator on settings + MessageInput sheets
- RegisterPage: scroll envelope, iOS-zoom-safe inputs, ≥44px tap targets, mobile-friendly invite chip + swatch row
- TransferIndicator: mounted via MobileScreenHeader.rightActions across every settings/instance screen and inline in MobileChatScreen; touch-close listener; viewport-safe panel width
- MessageInput popovers (emoji/GIF/mention) now route through new InputPopover wrapper — desktop popover, mobile bottom-sheet
- EmojiPicker mobile branch: dynamicWidth + scoped CSS (.emoji-picker-wrapper--mobile) so the <em-emoji-picker> custom element fills the sheet; bigger touch targets (perLine 8, button 40, emoji 28); desktop unchanged
- MessageInput trigger row: mobile-first sizing with md: desktop overrides (40×40 buttons + gap on mobile, 34×34 unchanged on desktop)
- Spec updates: docs/systems/auth.md (RegisterPage responsive contract), docs/systems/uploads.md (TransferIndicator mobile surfaces)
2026-05-07 22:58:57 +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 fc9a07523f fix(mobile): SpaceInviteCard Join lands on Spaces tab; skip auto-channel-redirect
AppLayout's auto-channel-redirect (turning /channels/<spaceId> into
/channels/<spaceId>/<firstChannelId>) is desktop-correct but on mobile
catapulted users past the channel sidebar straight into a chat — Join
from a SpaceInviteCard or Spaces-tab tap both inherited this. Guard
the effect with isMobile so /channels/<spaceId> settles at the channel
sidebar overview on mobile.

SpaceInviteCard's join handler now switches to the Spaces tab and seeds
spaceStore.currentSpaceId on mobile before navigating — clears the chat
stack the invite was tapped from and lands the user at the joined space's
channel sidebar instead of stuck behind the originating DM.

MobileSpacesScreen syncs its local selectedSpaceId from currentSpaceId
when external code (the Join handler) seeds the store — covers any future
programmatic space switch too.
2026-05-05 23:23:46 +02:00
Jannis Braun d793ab8f78 fix(message-list): sticky pagination skeleton on channel switch
Two cooperating bugs let the pagination skeleton stick at the top of the
chat across channel switches: (1) the browser's post-clamp scroll event
on channel switch fired the load-more block on the new channel, and
(2) useDelayedLoading's threshold timer refreshed displayStart on every
fire, extending the minDisplay window unboundedly when isLoading cycled.

- useDelayedLoading: stamp displayStart only on the false→true show
  transition (functional setShow form to avoid stale closures).
- MessageList: suppressNextLoadMoreRef armed in Effect 3, consumed by
  the load-more block on the next scroll event, with a 250 ms fallback
  disarm so legitimate user scrolls aren't silently dropped. Suppression
  scoped to the load-more block only.
- Regression test for the displayStart refresh bug.
- Updated docs/systems/message-list.md history.
2026-05-05 19:56:26 +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 c05c04181d fix(chat): keep scroll container mounted during initial-load skeleton
The initial-load skeleton was rendered as an early return that replaced
the JSX containing `containerRef` / `contentRef`. On slow loads, the
200ms threshold flipped the skeleton on before messages arrived, so when
messages did arrive every scroll-affecting effect (Effect A, the
ResizeObserver, the load handler, scrollend) re-fired exactly once
against null refs and bailed — and never re-attached because no dep
changed when the skeleton finally cleared. Net: chat opened scrolled to
the top instead of the bottom; saved-anchor restore was equally broken.

Render the skeleton as an absolutely-positioned overlay alongside the
(always-mounted) scroll container so all refs stay live across the
loading transition. Encode the constraint in the spec as the
"ContainerRef invariant" so future loading/empty/error UI doesn't
reintroduce the early-return pattern.
2026-05-05 00:19:00 +02:00
Jannis Braun 33cfc66ac4 fix(web): boot rehydrate normalizes transfers; paused has its own visual 2026-05-02 19:10:29 +02:00
Jannis Braun 61be9d013f fix(web): hide Retry when blob unavailable; failed-state surfaces actionable error
Pending bubbles that survive a reload (or post-redeploy refresh) without a
FileSystemFileHandle had no way to recover the original File bytes, so the
Retry button silently no-op'd: resumeUpload set state back to 'paused' with
no surfaced error, leaving the user stuck.

- transferStore: add reactive hasInMemoryFile Set mirroring liveUploadFiles.
  setInMemoryRef helper keeps both in sync at every set/delete site
  (startUpload, resumeUpload, onSuccess, remove). Persisted shape is unchanged.
- resumeUpload: when no blob is reachable (no in-memory File and no FS handle),
  call setError with an actionable "File no longer available — discard and
  re-upload" message instead of silently flipping back to 'paused'.
- Message.tsx: compute canRetry reactively from transfersForRow + hasInMemoryFile;
  hide the Retry button when retry is infeasible. Discard remains.
- AttachmentProgress: optional error prop surfaces transfer.error.message via
  title= on the failed-state ring for hover context.
- Tests: existing 3 resumeUpload tests now assert state==='failed' with the
  actionable message; +2 new tests for hasInMemoryFile lifecycle (start/remove,
  abort retains).
2026-05-02 18:58:05 +02:00
Jannis Braun 4e5a440176 fix(web): server-side DELETE for failed/discarded uploads (no janitor wait) 2026-05-02 18:22:45 +02:00
Jannis Braun dd91f5f349 fix(web): retry-after-abort starts fresh tus session; preserve File past abort 2026-05-02 18:18:07 +02:00
Jannis Braun 61dea725a3 fix(web): polish failed-upload row — pill-shaped, content-width, proper button affordance 2026-05-02 18:11:31 +02:00
Jannis Braun a58b7459fb fix(web): in-session pause/resume preserves File ref; aborted transfers flip bubble to failed 2026-05-02 18:03:12 +02:00
Jannis Braun 0be69808e4 feat(web): right-click Save Video / Save Audio via transferStore 2026-05-02 17:07:39 +02:00
Jannis Braun aaa15cef88 feat(web): file-card download routes through transferStore 2026-05-02 17:06:26 +02:00
Jannis Braun e534644e57 feat(web): Message renders pending-bubble overlay + retry/discard chrome 2026-05-02 16:31:53 +02:00
Jannis Braun 7b6586110d feat(web): MessageList interleaves pending bubbles by createdAtLocal 2026-05-02 16:28:21 +02:00
Jannis Braun b97326ef18 feat(web): MessageInput uses composerStore + transferStore (eager upload, FS handles) 2026-05-02 16:17:49 +02:00
Jannis Braun 65d8a19c87 feat(web): AttachmentProgress overlay component 2026-05-02 16:02:47 +02:00
Jannis Braun 85987a891c fix(web): SpaceInviteCard navigates to /channels/:spaceId (not the non-existent /spaces/:id) 2026-04-29 22:44:35 +02:00
Jannis Braun d8b08d8990 fix(web): SpaceInviteCard navigates to space on already-member instead of showing error 2026-04-29 22:41:49 +02:00
Jannis Braun 1859f2963c feat(web): SystemMessage dispatches space_invite to SpaceInviteCard 2026-04-29 21:55:06 +02:00
Jannis Braun e351d53dd5 fix(web): SpaceInviteCard — real embed idiom and correct getApiForOrigin import path 2026-04-29 21:52:20 +02:00
Jannis Braun 82533c1434 feat(web): SpaceInviteCard component with snapshot/live/revoked render states
Renders space invite system messages in DMs as embed-style cards on the chat
surface. Three render states: snapshot-only on mount (Join enabled, loading
dot), live-confirmed (memberCount refreshed from preview), revoked (gray-out
+ glass-pill indicator). Join targets the space's home origin via
joinByCode(code, spaceInstanceOrigin || undefined) — the three-way
federation correctness rule. Re-exports getApiForOrigin from api/client to
expose the cross-store resolver under a natural import surface.
2026-04-29 21:50:01 +02:00
Jannis Braun 8ba644fa44 fix(message-list): pagination flag and scroll restore leak across channel switches
Two bugs in handleScroll's loadMoreMessages flow surfaced after the
smooth-scroll race fix.

(1) isLoadingMore stuck across channels. setIsLoadingMore(true) → await
loadMoreMessages → setIsLoadingMore(false) was unguarded. If the user
switched channels during the await, the new channel inherited the flag (same
component instance, same useState slot) and rendered the pagination skeleton
even with no load in flight. Cleared only when the original await resolved
or the component remounted (e.g., navigating to Friends and back).

(2) Wrong-channel scroll restore. The post-await rAF set
container.scrollTop = container.scrollHeight - prevScrollHeight against the
new channel's container with the old channel's prevScrollHeight, yanking
the new channel to a wrong position.

Fix:
- try/finally around the await so setIsLoadingMore(false) always runs.
- currentChannelIdRef tracks the live channelId; capture requestChannelId at
  load start and compare both before scheduling the rAF and inside the rAF
  callback (the 16ms frame gap is enough for a switch).
- Belt-and-suspenders: setIsLoadingMore(false) in the channel-switch effect
  covers the case where the await never resolves (network hang). Without it,
  a stuck await would leave the new channel inheriting the flag indefinitely.

No request cancellation — out of scope; AbortController plumbing through
chatStore is a bigger refactor and the channelId guard already silently
drops stale results.

Spec updated. Smooth-scroll fix from the previous commit untouched.
2026-04-27 18:11:31 +02:00
Jannis Braun b6b830568c fix(message-list): close smooth-scroll-to-bottom race against late-loading media
Smooth scrolls toward the bottom (new-message arrival in Effect A and the
Jump-to-Present click) animate scrollTop over many frames. Each intermediate
handleScroll measurement saw a large distanceFromBottom and flipped
isAtBottomRef to false, closing the Effect B/C gates. Lazy media (avatars,
embeds, Spotify thumbs) finishing mid-animation grew scrollHeight while the
gate was closed, so the smooth scroll landed at its originally-computed
target — leaving the user above the new bottom by ~the height of what loaded.

Fix: typed smoothScrollIntentRef ('bottom' | 'message' | null) with an 800ms
deadline. handleScroll suppresses the at-bottom flip while intent is 'bottom'
and the user hasn't wheeled past the 5000px nearBottom threshold. Effect D
fires a final defensive instant pin via native scrollend (Chrome 114+,
Safari 18+) or a setTimeout(800) fallback. 'message' intent (jump-to-message
from search) does NOT suppress — the gate flips honestly so the user is left
at the targeted message.

Verified live on nova.ddns.net Orbit → general: Jump-to-Present
lands flush at bottom; new Spotify-link messages stay at bottom as embeds
arrive via WS. docs/systems/message-list.md updated.
2026-04-27 17:51:55 +02:00
Jannis Braun cebbd5c859 feat(web): user-facing pending peering subscriptions and outcome notifications
Two new inline sections in the user-facing federation/connections settings
panel: 'Recent peering outcomes' (terminal-state notifications with
Retry-for-approved + Dismiss) and 'Pending peering approvals' (active
subscriber rows the user is waiting on, with Cancel). New WS handlers for
peering_subscription_changed and peering_notification_received refresh the
lists in real-time and surface a transient toast for online users. Retry
deep-link for friend_add prefills the friend-add input with the original
target handle (other reasons get Dismiss only — the gate doesn't wire
those paths yet).
2026-04-26 22:42:53 +02:00
Jannis Braun fe969a7d96 fix(web): friend-add error toasts surface raw codes — read err.message, not err.body
The T19/T20 catch blocks looked for an `.body` property on thrown errors
to extract the structured error code. The shared API client (api/client.ts:298)
actually throws `new Error(body.error)` — the code lives in `err.message`,
and there's no `.body` attached.

Live E2E (T22 scenario 2) caught this: typing alice@orbit against an
awaiting_approval peer surfaced the raw code 'peer_pending_approval' as
the toast text instead of the human-readable mapServerErrorToMessage
output. Same defect would have hit every server-error toast on both
FriendsPage (AddFriend + UserDiscoverCard) and UserProfileModal.

Catch blocks now use err.message as both the code and the fallback text;
the inline comment points at the API client throw site so the contract
is documented at the consumer.
2026-04-25 23:17:19 +02:00
Jannis Braun 7309f44de5 refactor(web): FriendsPage — toast on server errors, drop ConnectInstanceModal triggers
Removes try/catch on the deleted InstanceNotConnectedError/Disconnected
classes (T17). Server now returns structured error codes; client maps
them to human-readable toasts via the new mapServerErrorToMessage helper.

The friend-add flow no longer triggers ConnectInstanceModal — the server
handles all routing/peering/lookup. The modal itself stays for Connections
settings and space-join flows.
2026-04-25 22:33:26 +02:00
Jannis Braun 92a8a6a727 chore(friends): tighten Direct-Add gate consistency
Per code-review: directAddDisplay now reads directAt === -1
instead of re-deriving includes('@'); add a one-line comment on
showDirectAdd so the predicate's intent is obvious at first read.
2026-04-25 19:34:29 +02:00
Jannis Braun da98d78489 feat(friends): always-visible Direct-Add row with resolved-form display
The Send-Friend-Request action row in the Add Friend tab previously
appeared only when the typed query contained a non-edge @, leaving
no way to fire a blind request for a bare local handle. Widen the
gate to allow non-empty bare handles, keep the malformed @ shapes
(@, @bob, bob@) hidden. When the typed query has no @, display the
resolved form <query>@<window.location.host> so the user sees which
instance the request will hit. Submission string is unchanged.

Updates FriendsPage.test.tsx: inverts the now-stale 'does not show
Direct Add row for plain usernames' test into the new positive
assertion, and adds a separate test for the malformed @ shapes.
2026-04-25 19:32:03 +02:00
Jannis Braun d659637930 docs(message-list): note smooth-scroll exclusion; point sentinel comment at subsystem doc
Reviewer caught two small gaps after Task 3:
- Effect A's smooth-scroll path on new messages is intentionally NOT
  instrumented with the sentinel (the animation lands asynchronously
  across frames; no intermediate scrollTop is worth pinning to). The
  doc now records this so the reader's intuition matches the code.
- The sentinel-branch comment in MessageList.tsx pointed at "spec §2",
  which is the planning doc rather than the durable subsystem spec.
  Pointed at docs/systems/message-list.md instead.
2026-04-25 13:11:05 +02:00
Jannis Braun d18845acb5 fix(web): close handleScroll race that disabled auto-bottom on cold-cache image load
Tracks the post-clamp scrollTop of every programmatic scroll-to-bottom in
lastProgrammaticBottomScrollRef. handleScroll skips the at-bottom flip and
re-pins when the event's scrollTop matches the sentinel — i.e., the event
was queued by our own command and layout grew underneath. User scrolls
break the match (scrollTop changes) and flow through the normal path.

This complements the 2026-03-25 race-fix (which gated auxiliary effects on
isAtBottomRef) by also preventing handleScroll from flipping that ref to
false based on a post-growth distance measurement of our own scroll.
2026-04-25 13:02:01 +02:00
Jannis Braun 84ec8e04b7 fix(web): restore known-dimension reservation in ImageEmbed without letterbox fallback
Restores the dimension reservation reverted in dae6f2d, scoped to
the embed.width && embed.height case only. No fallback aspect-ratio
when dims are null — that was the source of the dark letterbox bars
on Tenor/Klipy GIFs and OG-less images that triggered the revert.

The server-side probe in embedResolver.ts:196 already populates dims
for all image-type embeds; this change makes the client honor them.
2026-04-25 12:53:11 +02:00
Jannis Braun ae5bdaa338 test(friends): drop stale origin arg from addDmChannel assertion
The DM "Message button" test asserted addDmChannel was called with two
arguments — the channel and an empty-string origin — but the assertion
has been stale since commit 7f3ca4e ("route DM creation to home instance
with federated identity", 2026-04-01). That refactor made FriendsPage
always route DM creation through the home api client and dropped the
second argument from the addDmChannel call because the remote friend's
instanceOrigin no longer applies — home-created DMs don't need a
channelOriginMap entry (lookups default to '' for missing keys; remote-
delivered DMs still get their origin tagged by useWebSocket).

The two-arg assertion was introduced on 2026-03-25 (commit 277b69a)
against an intermediate form of the code that was later rewritten. Drop
the trailing '' so the assertion matches the current, intentional
one-arg call.
2026-04-23 02:28:57 +02:00
Jannis Braun 3152c66bd2 fix(web): restore original rounded separator style on friend rows 2026-04-03 01:02:53 +02:00
Jannis Braun 23517447c3 fix(web): flat separators with wrapper divs, first-row border, DM sidebar title case
- FriendItem/RequestItem: add plain wrapper div so divide-y border
  lands on a non-rounded element (fixes curved separator lines)
- Add border-t to divide-y containers so first row also gets top line
- Remove uppercase from "Direct Messages" in DM sidebar
2026-04-03 00:58:03 +02:00
Jannis Braun e16cedded7 feat(web): add confirmation dialog for unfriend action 2026-04-03 00:48:28 +02:00
Jannis Braun fbd69b09cf fix(web): use title case for all Friends page headers 2026-04-03 00:47:19 +02:00
Jannis Braun 33f5edb419 fix(web): use divide-y for flat friend row separators 2026-04-03 00:46:03 +02:00
Jannis Braun c5eddff084 fix(web): friends page header, button visibility, and status casing 2026-04-03 00:44:48 +02:00
Jannis Braun 7f3ca4e6d1 feat(web): route DM creation to home instance with federated identity
All DM creation and add-member call sites now use the home api client
and pass homeUserId/homeInstance instead of routing to the remote instance.
Also updates addMember in the API client to accept AddDmMemberRequest.
2026-04-01 12:45:13 +02:00
Jannis Braun 04c665e0f7 fix: three code review issues — leaveGroup duplicate, as any casts, federation ownership fallback
- Remove redundant `leaveGroup` API method from client.ts (duplicated `leave`); update MessageList.tsx WelcomeHeader to call `api.dm.leave` directly
- Add optional `type` field to shared `Message` interface so `MessageWithUser` carries it; remove `(msg as any).type` casts in `isSameGroup` and the render branch in MessageList.tsx
- Fix `processOwnershipTransferEvent` in federation.ts: replace `channel.ownerId` fallbacks (pre-update, old owner) with `event.ownership.newOwner.homeUserId` in the db update, dm_owner_updated broadcast, and both system message content payloads
2026-03-27 05:50:38 +01:00
Jannis Braun c3191be4e7 feat: system messages, group DM welcome header, and dm_owner_updated event
- Render system messages (member_added, member_removed, owner_changed) inline
  in MessageList with icon + human-readable text; system messages never group
  with adjacent user messages
- Rewrite WelcomeHeader to branch on ownerId: group DMs show overlapping avatars,
  group name, creator attribution, federated privacy note, and a Leave Group button
- Add dm_owner_updated ServerEvent; broadcast from dm.ts leave handler and
  federation processOwnershipTransferEvent so all clients update ownerId in real-time
- Add updateDmOwner action to spaceStore and handle dm_owner_updated in useWebSocket
- Add leaveGroup alias to API client dm namespace
2026-03-27 05:43:30 +01:00
Jannis Braun c5c7ab571a fix(federation): use inline badge for all attachment types
Overlay badges inside overflow-hidden containers (images, video)
were invisible. Use inline badges below the media for all types.
2026-03-27 05:20:58 +01:00
Jannis Braun dd3ddd9cc2 fix(federation): fix badge positioning and tooltip text for file attachments
- Use inline badges for file/audio attachments (next to file size)
  instead of absolute positioning which overflowed the container
- Keep overlay badges for images/video (overflow-hidden containers)
- Match tooltip text to toast notification text for consistency
2026-03-27 05:07:55 +01:00
Jannis Braun ceb0c9812d fix(federation): resolve code review issues in upload size mismatch
- Fix critical: outbox worker now copies file_rejected payload fields
  (attachmentId, sourceFilename, rejectionReason, rejectionLimit,
  affectedUserIds) so the reverse relay actually delivers them
- Fix: add sourceFilename to file_rejected event for reliable
  multi-attachment matching on the sender side
- Fix: change text-accent-warning to text-accent-amber (valid class)
- Add Array.isArray guard on federationMeta parse
2026-03-27 04:51:30 +01:00
Jannis Braun 4686ec6594 feat(federation): add federation status badges to attachment renderer 2026-03-27 04:42:15 +01:00
Jannis Braun 0d6bb9dbd3 fix(federation): auto-create and hydrate user profiles during DM relay
Federated 1-on-1 DMs showed the raw snowflake ID as the display name
and no avatar when the remote user had no pre-existing local record.
processCreateEvent used resolveLocalUser (find-only) instead of
resolveOrCreateReplicatedUser, and relay events carried no profile data
for participants.

- Add profile snapshot (displayName, avatar, avatarColor) to
  FederationRelayParticipant and populate it in getDmParticipants
- Change processCreateEvent to auto-create replicated user stubs and
  hydrate them with profile data from the relay event
- Fix hydrateReplicatedUserProfile URL resolution for homeInstance
  values without protocol prefix
- Fix WelcomeHeader: return null while DM data is loading (eliminates
  "unknown" flash on reload), use displayName for @mention text
2026-03-27 03:43:45 +01:00
Jannis Braun 6df80aa1f0 fix(federation): resolve persistent unread indicator on federated DMs
Federated relay messages can have local snowflake IDs that don't match
chronological (createdAt) order — a message sent earlier on a remote
instance can arrive later and get a higher local ID. This caused a
permanent mismatch between the ready event's lastMessage (MAX id) and
the acked message (last in createdAt display order), making federated
DM channels appear unread after every server restart.

- Server: change ready event DM lastMessage query from MAX(id) to
  ORDER BY created_at DESC (matching the DM REST API)
- Frontend: change ackChannel to ack MAX(id) among loaded messages
  instead of last in display order (consistent with server comparison)
- Fix federated username display fallback in UserDiscoverCard
2026-03-27 03:05:26 +01:00