Commit Graph
61 Commits
Author SHA1 Message Date
Jannis Braun 1629f8fbe1 fix(dm): enforce read-only guard on DM reactions (WS) + harden purge/ownership tests
Finding 1: handleReactionAdd/Remove now drop reactions on a dead 1-on-1
(isDeadOneOnOne) — previously a survivor could react on a Deleted-User
thread and the relay fanned out to all peers via undefined target-origins.
Client Message.tsx withdraws add/toggle reaction affordances for dead DMs
(existing reactions still display read-only).

Finding 2: dmMembership purge test now runs with foreign_keys=ON (matches
prod) and asserts dm_members/dm_messages cascade cleanup on channel purge.

Finding 3: tombstone group-DM ownership transfer filters isDeleted=0 so
ownership can never move to a tombstoned member; covered by a new test.
2026-07-02 16:45:44 +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 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 e534644e57 feat(web): Message renders pending-bubble overlay + retry/discard chrome 2026-05-02 16:31:53 +02:00
Jannis Braun a1363a1ab3 fix: match GIF-only message size to standard media constraints
GIFs from Klipy were capped at 250×250px while all other visual media
(attachments, image embeds) used 400×300px. Unified to 400×300px.
2026-03-25 14:42:27 +01:00
Jannis Braun e0d45da97c refactor: skip imageEmbedSourceUrl computation when isGifOnly is true 2026-03-25 05:05:49 +01:00
Jannis Braun 4fd72cd29b fix: remove redundant !isEditing guard in image-embed-only branch 2026-03-25 05:02:38 +01:00
Jannis Braun 001d45da2b feat: suppress URL text for image-only messages, add sourceUrl to context menu 2026-03-25 04:59:53 +01:00
Jannis Braun dae6f2d518 fix: remove forced 4/3 aspect ratio and dark background from image containers
Images now size naturally within max constraints instead of being
letterboxed into a 4/3 bg-surface-input container. Fixes dark bars
on GIFs and images whose aspect ratio doesn't match 4/3.
2026-03-25 03:37:18 +01:00
Jannis Braun aeb4ea6e1d fix: exclude embed thumbnails from image context menu actions
Add data-embed-thumbnail marker to VideoEmbed, GenericEmbed, and
RichEmbed thumbnail containers. Update detection logic to skip
these alongside avatars. Also remove redundant filename derivation
in Save Image menu item.
2026-03-25 03:23:23 +01:00
Jannis Braun 97f0ebc915 feat: add image actions to message context menu
Detect right-clicked images in handleContextMenu and prepend
Save Image, Copy Image, and Open Original to the custom menu.
Avatars are excluded via the data-avatar semantic marker.
2026-03-25 03:13:58 +01:00
Jannis Braun 995c71367e fix(web): add aspect-ratio fallback for bare GIF URL messages 2026-03-25 01:56:08 +01:00
Jannis Braun acb79c9de7 fix: cap GIF-only messages to 250x250 2026-03-24 05:15:10 +01:00
Jannis Braun 9f8a4ffa90 fix: prevent native text selection on mobile long-press
iOS triggered both the app context menu and native text selection overlay
on long-press. Three-layer fix: CSS user-select:none on buttons/[role=button]/
[data-context-menu] elements; non-passive touchstart listener with conditional
preventDefault on context-menu targets; removeAllRanges() at timer fire to
clear any residual selection. Added data-context-menu to all mobile surfaces
that use onContextMenu handlers.
2026-03-23 21:12:06 +01:00
Jannis Braun ce33d8f7ea feat: global long-press context menus for all mobile touch targets
Add useGlobalLongPress to ContextMenuRenderer that detects 500ms hold
with <10px movement and dispatches a synthetic contextmenu event on the
touched element. All existing onContextMenu handlers (spaces, channels,
voice users, messages, etc.) now work on touch devices automatically.

Remove per-message useLongPress integration from Message.tsx since the
global handler covers it.
2026-03-23 15:12:58 +01:00
Jannis Braun 80c32ae1e6 feat: add long-press context menus for mobile messages
Create useLongPress hook with 500ms delay, 10px movement cancellation
(Euclidean distance), and ghost click suppression. Integrate into
Message component to trigger the existing context menu bottom sheet
on touch devices.
2026-03-23 14:22:38 +01:00
Jannis Braun 1473f4f88c fix: emoji reaction picker no longer overflows viewport on bottom messages 2026-03-21 18:21:30 +01:00
Jannis Braun 95762a1974 feat: replace Embed.tsx with AttachmentRenderer + EmbedRenderer in Message
Switches Message.tsx from client-side URL metadata fetching (old Embed.tsx)
to server-driven embed objects via message.embeds[]. Delegates attachment
rendering to AttachmentRenderer. Removes firstUrl extraction and urlRegex.
2026-03-20 23:47:16 +01:00
Jannis Braun 720a3c4553 feat: wire up enhanced message context menu with all actions 2026-03-20 22:19:12 +01:00
Jannis Braun 3aa7e93a4d feat: pass previousMessageId prop from MessageList to Message 2026-03-20 22:16:55 +01:00
Jannis Braun c90ba431e8 fix: resolve mentions in reply previews to display usernames
Reply preview was rendering raw <@userId> tokens as plain text instead
of resolving them to MentionBadge components with proper usernames.
2026-03-20 21:25:45 +01:00
Jannis Braun 3ead68b3e7 refactor: migrate Message context menu to centralized store 2026-03-20 18:32:18 +01:00
Jannis Braun b08f40feb7 feat: inline delete confirmation on message hover bar
Click trash icon to arm (morphs to green checkmark), click again to
confirm. Auto-cancels after 3s or 2s after mouse leaves. Uses CSS
scale+opacity transitions for a smooth icon swap animation.
2026-03-15 02:48:07 +01:00
Jannis Braun 42266ff963 fix: match Klipy CDN domain for inline GIF rendering
The GIF URL regex matched media.klipy.com but Klipy serves from
static.klipy.com, causing GIFs to render as plain links instead
of inline images.
2026-03-15 02:09:01 +01:00
Jannis Braun 3de6e4a668 feat: GIF search (Klipy), stickers, emoji picker, and bug fixes
- Add GIF search powered by Klipy API with correct response mapping
  (file.sm/hd tiers, not flat files structure)
- Add sticker system: packs, upload with auto-downscale, send in messages
- Add tabbed InputPopover with emoji, GIF, and sticker pickers
- Fix GIF API key migration race condition (column-add loop vs rename)
- Fix masked API key corruption on settings save (server + client guards)
- Fix sticker packs 403 (reversed isMember parameter order)
- Fix emoji picker not filling popover width (perLine 8→9, CSS 100%)
- Add error logging for Klipy API failures
2026-03-15 02:04:37 +01:00
Jannis Braun 836f0acef6 feat: standardize input styling with tier system, add depth and admin features
- Define 4 input tier CSS classes (input-standard, input-search, input-embedded, input-danger)
  in globals.css, migrating ~50 inputs across ~28 component files to use them
- Add subtle border and inset shadow to solid input tiers for resting-state visibility
- Fix focus ring clipping in settings panel scroll container
- Fix phantom Tailwind tokens (border-border-primary, placeholder-txt-muted)
- Add admin user management panel, storage management, and account deletion utilities
2026-03-14 13:49:32 +01:00
Jannis Braun 3e97c2b0f5 feat: image optimization — client-side resize + server-side thumbnails
Avatars/banners now resize to max 512px/1920px and convert to WebP before
upload (zero server cost). Chat image uploads generate an 800px-wide WebP
thumbnail via Sharp; the feed shows the thumbnail, click opens the full-res
original. Adds lazy loading to avatars. Federation-compatible: remote
instances without this feature fall back gracefully.
2026-03-13 16:44:14 +01:00
Jannis Braun cea18b19b5 fix: compact timestamp vertical misalignment — remove legacy mt-[3px] nudge 2026-03-13 01:27:50 +01:00
Jannis Braun b9781a8755 fix: message gutter layout — use explicit flex gap matching prototype
Gutter column was 56px (w-14) with the 16px gap baked into the width.
This worked for avatars but left only 4px between compact hover
timestamps and message text. Switch to w-10 (40px) gutter + gap-4
(16px flex gap), preserving the same 76px content offset while giving
timestamps proper breathing room.
2026-03-13 01:22:41 +01:00
Jannis Braun 5300e78d9d feat: implement message search and clean up header buttons
Remove placeholder buttons (Threads, Inbox, Help) from channel and DM
headers. Add full-text message search with backend endpoints for both
space channels and DMs, supporting filters (from, has, before, after)
and pagination. Search popover with debounced input, highlighted matches,
and jump-to-message that scrolls with a highlight animation. Includes
messages/around endpoints for loading context when jumping to uncached
messages.
2026-03-11 00:22:00 +01:00
Jannis Braun e2c18ad2b0 feat: bans system, voice moderation, and federated space settings fixes
Add ban/unban functionality with BansPanel in space settings, voice
moderation context menu (mute/deafen/disconnect), and fix federated
space settings panels to use origin-aware API client. Show domain
indicators for federated members in MembersPanel.
2026-03-09 15:56:46 +01:00
Jannis Braun fc06e25731 refactor: rename Server → Space across entire codebase
Renames all domain terminology from "Server" to "Space" throughout the
application — database schema, API routes, shared types, stores, components,
and UI strings. Files renamed: ServerSidebar → SpaceSidebar, CreateServer →
CreateSpace, JoinServer → JoinSpace, ServerSettings → SpaceSettings,
serverStore → spaceStore, routes/servers → routes/spaces.
2026-03-08 20:08:24 +01:00
Jannis Braun f7809cf457 fix: federation reaction identity + remote server reload race condition
- Include user object in reaction_added WS broadcasts for isSelf() resolution
- Use isSelf() instead of userId comparison for reaction ownership checks
- Load remote server detail after ready event to prevent empty channel list
2026-03-04 03:53:07 +01:00
Jannis Braun dca9c4dc83 feat: stateless federated identity resolver + federation UX improvements
Add identity.ts with isSelf() and resolveDisplayIdentity() — pure
stateless functions that detect replicated-self using the immutable
(username, homeInstance) composite key. No store lookups, no data
mutation. Fixes wrong avatar gradient and missing edit/delete on
own messages in remote channels.

Also includes: optimistic message dedup fix for cross-instance
messages (content-only matching), federation toast notifications,
Username component with @domain display, invite parser, and
deploy script simplification.
2026-03-04 03:37:49 +01:00
Jannis Braun 758c6a7b5b feat: normalize remote asset URLs and route UI actions by instance origin
Phase 5 of multi-instance federation. Adds a two-layer fix:

Layer 1 — Data ingestion normalization: Remote instance user avatars,
server icons, and attachment filenames are rewritten to absolute URLs
when entering the app (via WebSocket events or API responses), so all
downstream components render them correctly without changes.

Layer 2 — Outbound action routing: wsSend calls (voice join/leave/status,
typing) and file uploads in UI components now route through the correct
instance based on the active channel's origin.
2026-03-03 00:28:24 +01:00
Jannis Braun 72e07c1cc1 fix: persistent random Snowflake worker ID + clean reaction API
Two fixes addressing architectural review feedback:

1. Snowflake ID collisions: Replace process.pid-based worker ID with a
   cryptographically random value (0-1023) generated once at first boot
   and persisted to instance_settings.worker_id. Eliminates deterministic
   ID collisions between Docker instances that all run as PID 1.

2. Reaction API leak: Revert addReaction/removeReaction signatures to
   (messageId, emoji) — the store now resolves the channel internally by
   scanning its message cache, keeping routing logic out of the UI layer.
2026-03-03 00:03:29 +01:00
Jannis Braun 5194dbef25 feat: multi-WebSocket connection manager for instance federation
Refactor the WebSocket layer from a singleton connection to a connection
map supporting N concurrent instances. Each connection has its own
heartbeat worker, reconnect state, and token. Stores are now
instance-aware: serverStore merges servers by origin, chatStore routes
API calls through the correct client, and instanceStore triggers WS
connect/disconnect on add/remove. DM, social, and voice events remain
home-only.
2026-03-02 23:53:30 +01:00
Jannis Braun 37dc73231d refactor: convert color system to RGB channels for native Tailwind opacity
Root cause fix for all broken CSS variable opacity modifiers. Converted
every hex CSS variable to space-separated RGB channels (e.g. #fda4af →
253 164 175) and updated Tailwind config to use rgb(var(...) / <alpha-value>).
This makes bg-accent-rose/10, border-border-hard/50, etc. work natively
everywhere — no more currentColor fallback. Reverted all previous explicit
rgba() workarounds back to clean Tailwind syntax. Also fixed the voice
connection wifi icon SVG (was bottom-left aligned, now centered).
2026-03-02 17:46:21 +01:00
Jannis Braun 0e235fdf1c fix: DM reactions + eradicate broken CSS variable opacity modifiers
Server-side: handleReactionAdd/Remove now check dm_messages table when
message isn't found in server messages, enabling DM emoji reactions
via the same unified client event path.

Client-side: replaced all remaining broken opacity modifiers on CSS
variables (border-border-soft/50, bg-accent-rose/10, bg-surface-*/N,
etc.) with explicit rgba values. Tailwind can't decompose var() for
opacity, causing fallback to currentColor. Fixes 16 files across
error boxes, table borders, surface tints, and input separators.
2026-03-02 17:36:33 +01:00
Jannis Braun 907728ab24 fix: replace broken border-hard/50 opacity with subtle white borders
Tailwind can't decompose CSS variables for opacity modifiers, so
border-border-hard/50 fell back to white currentColor. Replaced with
border-white/[0.06] on image attachments, code blocks, and action
toolbar. Also applied frosted glass material to message action toolbar.
2026-03-02 17:07:33 +01:00
Jannis Braun 7608afccc4 fix: panel separator borders and reaction spacing to match prototype
Replace @apply border-none with @apply border-0 on global * selector.
border-none sets border-style: none, which silently kills all Tailwind
border utilities (they only set width/color, relying on preflight's
border-style: solid). border-0 sets border-width: 0 instead, preserving
the solid style so border-r/border-l/border-b utilities render correctly.

- Add border-r border-border-hard to ChannelSidebar (both DM and server views)
- Add border-l border-border-hard to MemberSidebar and ActivityPanel
- Replace shadow-header with border-b border-border-hard on sidebar headers
- Remove duplicate mt-1 on reaction container (parent gap-1 already provides 4px)
2026-03-02 16:51:17 +01:00
Jannis Braun b8ce05ad6c fix: precise chat layout alignment to match Aether Drift prototype
Message rows: 20px horizontal padding, 3px vertical, 56px avatar column,
subtle 2.5% hover. Channel headers: border instead of shadow, text # hash,
10px gap, 6px button radius. Date dividers: 8px margin, 11px text, border-hard
lines. Input bubble: 34px buttons with 18px SVGs, 10px textarea padding.
Inline code: lavender on #111115 with border-soft border.
2026-03-02 16:17:42 +01:00
Jannis Braun 9491edb3c9 fix: reaction pill frosted glass + compact sizing to match prototype
Replace inline JS hover handlers with .glass-pill CSS classes for proper
Aether Drift frosted glass depth (blur, inset highlight, box-shadow).
Constrain emoji size and use exact prototype padding (2px 8px) for compact pills.
2026-03-02 15:55:42 +01:00
Jannis Braun cdee450cf5 fix: channel sidebar and reaction bubbles to match Aether Drift prototype
Channel sidebar: subtle bg-surface-elevated highlight with rounded-[6px],
active channel left pill, rose unread dot on right, smaller #/voice icons
(18px), 10px horizontal padding, 24px voice user avatars at 36px indent.

Reaction bubbles: inline styles for background/border colors to bypass
Tailwind opacity modifier failures on hex CSS variables. Both mine (mint
tint) and non-mine states now render visible pill containers reliably.
2026-03-02 14:45:47 +01:00
Jannis Braun ddeb72101e feat: dynamic PiP collision avoidance, reaction pill redesign, channel dedup fix
PiP positioning:
- Replace hardcoded layout constants with dynamic DOM measurement system
- Obstacle elements declare themselves via data-pip-obstacle="left|bottom"
- getPipBounds() queries actual element rects at runtime via getBoundingClientRect
- MutationObserver + ResizeObserver re-clamp PiP when obstacles appear/resize
- PiP reappears after close when navigating away from voice channel

Reactions:
- Align reaction pills with Aether Drift prototype
- Own-reaction accent: purple → mint (bg, border, count color)
- Default pills: subtle white overlay bg + border-soft border
- Count text: 12px/semibold/txt-secondary per prototype spec

Bug fix:
- Deduplicate channel insertion in createChannel store action
- Prevents double-add race between REST response and WS broadcast
2026-03-02 02:09:52 +01:00