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.
ResizeObserver and media load callbacks were using isNearBottomRef
(5000px threshold) to decide whether to auto-scroll to bottom. On
mobile, inertial flick scrolling hadn't moved far enough to clear
that threshold before the observer fired, snapping the user back.
Now these callbacks check actual scroll distance at fire time with
a tight 150px threshold — only auto-scrolls if truly at the bottom.
1. Chat scroll: add flex flex-col to MobileChatScreen messages wrapper
so MessageList's flex-1 actually works (parent must be flex container)
2. Instance General panel: add fetchInstanceSettings/fetchStreamingLimits
to MobileInstancePanel mount (mirrors desktop InstancePanel useEffect)
3. Friends activity: add Activity tab to mobile FriendsPage showing
friends grouped by activity status (active, online, offline)
Previously, voice cleanup only happened in finalizeDisconnect after a
5-second grace period, and only when ALL connections closed. If a user
was logged in on multiple devices and closed the one in voice, the
voice state was never cleaned up — creating a permanent ghost in the
channel sidebar.
Now removeConnection checks if the closing socket is the voice-active
one (tracked via voiceWs map) and immediately cleans up voice state,
broadcasts the leave event, and notifies remaining tabs.
Adds device-switch guardrail to handleVoiceJoin: if the user already
has a voice session on a different WebSocket, that old socket receives
a voice_disconnected/displaced event before the new session takes over.
Also binds voiceWs on the caller socket in handleDmCallStart and on
the acceptor socket in handleDmCallAccept.
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.
Create MobileMembersScreen that renders space members grouped by role
without depending on desktop memberListOpen flag or hidden md:block.
Add members button to voice full screen header for space channels.
Replace inline InstancePanel render with MobileInstancePanel that shows
a navigation list for General, Streaming, Storage, and Users sub-panels.
Each sub-panel pushes onto the mobile screen stack with MobileScreenHeader.
Add overflow-hidden to MobileScreenStack stacked screen wrapper to
establish block formatting context. Add min-h-0 to MobileChatScreen
messages wrapper to override flex min-height: auto default.
Adds uploadWithProgress() using XMLHttpRequest for real-time upload
progress events. MessageInput now shows per-file progress bars with
percentage overlay during upload. Failed uploads show toast warnings
with the specific error instead of silently failing. Upload timeout
raised to 10 minutes for large files.
The 500 MB cap was arbitrary. Self-hosted admins should decide based
on their own disk space. Global multipart ceiling raised to 5 GB,
validation cap to 5120 MB.
Replaces the layout-shifting error banner at the bottom of the panel
with non-intrusive toast notifications. Upload limit save, cleanup
success, and all error feedback now use the existing toast system.
Cleanup preview results stay inline as expected contextual responses.
Reads max_upload_size_bytes from instance_settings per-request and
passes it to request.file() so Fastify kills the stream at the
admin's configured limit. Checks file.truncated to properly reject
files that exceed the limit instead of saving corrupted data.
Adds cleanupOldMedia() to the storage janitor and a new endpoint
POST /api/admin/storage/cleanup-media { maxAgeDays, dryRun }.
Deletes chat attachments older than the specified threshold while
preserving profile images.
Rebuilds the attachments table with ON DELETE CASCADE on dm_message_id.
Dangling records are excluded during copy — their files become standard
disk orphans detectable by the storage janitor. Also updates the raw SQL
in index.ts to include width/height/duration columns for fresh installs.
Adds getDanglingAttachments() to find attachment records whose
messageId/dmMessageId points to a deleted message. Updates stats
to show dangling count and cleanup to delete them.
Replace the give-up-after-3-retries path with a persistent background
scheduler: 10 retries every 30s (5 min), then 12 retries every 5 min
(1 hr). Active timers are keyed by origin so rapid password changes
cancel and replace the previous loop. All timers are cleared on logout
via clearPasswordSyncTimers() called from instanceStore.reset().
Add `pendingSyncOrigins: string[]` to InstanceState so components re-render
when pending password-sync flags change. Hydrate from localStorage on
autoConnectAll, clear on reset, and remove the localStorage-only read path
from `hasPendingPasswordSync`.
When a federated user's local password hash is stale (e.g. they changed
their password on the home instance and sync failed), the login handler
now falls back to verifying credentials against the home instance. If the
home instance accepts the password, the local hash is silently updated
without touching passwordChangedAt, so existing valid JWTs remain valid.
Add installers/ to .gitignore and remove from tracking — binaries
should not be in git.
Also includes:
- Re-push Electron activities after WebSocket reconnect (sleep/wake)
- Merge activity map instead of replacing to preserve local state
- Nori mascot design plan
When the app is focused and the native uiohook hook is active, both the
IPC bridge and the web fallback capture the same keypress, dispatching
the toggle action twice (~0-20ms apart). This double-toggles mute/deafen
back to the original state.
Add a 100ms dedup window in dispatchKeybindAction to suppress the second
dispatch. Safe for human input (key repeat starts at ~500ms).
The web UI records keybinds as djb2 hashes of DOM event.code strings
(e.g. "KeyM" → 2090563626), but uiohook fires native scan codes
(e.g. M → 50). These never matched, so global keybinds only worked
in-focus via the web fallback, never out-of-focus via the native hook.
Add a uiohook-to-DOM-code mapping table and pre-compute the djb2
hashes so pressedKeys uses the same numbering as the keybind store.
Fixes system-wide freeze when Accessibility permissions are enabled.
Two changes to libuiohook/src/darwin/input_hook.c via pnpm patch:
- Switch kCGEventTapOptionDefault to kCGEventTapOptionListenOnly
- Strip mouse move/drag/scroll/left/right-click events from mask
Listen-only taps are passive observers that cannot freeze the system.
Keeps keyboard events and mouse buttons 3+ for PTT side buttons.
Windows and Linux native paths are completely untouched.
- Add .catch handler to scheduleNext to prevent unhandled promise
rejections from killing the scheduling loop
- Replace `as unknown as Animation` cast with Cancellable interface
for type-safe cleanup handles
The svgRef was created but never passed to the sub-component SVG
elements, meaning it would always be null when the animation hook
tries to use it. Now passed via SvgProps interface.
Implement the <Mascot> component that renders Nori in four emotional
states (idle, sleeping, excited, lonely) with distinct SVG geometry,
color palettes, and facial expressions. Includes useMascotAnimation
hook stub for Task 2 and full test coverage (15 tests).
When the Electron main process can't start uiohook (macOS Accessibility
denied, Linux missing input group), the IPC path silently produces no
events. Previously this left zero keybind listeners active. Now the web
capture-phase fallback is always set up as a safety net, ensuring
keybinds work in-app even when the OS-level hook fails.