- Add tauri-plugin-updater and tauri-plugin-process for in-app updates
- Rust commands (check_client_update, download_and_install_update) build
updater with dynamic endpoint at runtime for self-hosted compatibility
- Server endpoint GET /api/v1/client-update/{target}/{version} translates
GitHub Releases into Tauri updater JSON format with .sig content
- UpdateNotifier banner component with install/dismiss controls
- CI workflow produces signed .nsis.zip + .sig updater artifacts
- Self-signed TLS support via dangerousAcceptInvalidCerts config
- Add file download with native save dialog (Tauri dialog + fs plugins)
- Make attachment filename clickable as additional download trigger
- Add download button with hover styling to file attachments
- Exempt /api/v1/uploads from global 1MB body size limit (MaxBodySizeUnless)
- Add native E2E test suite (8 specs) with Playwright CDP fixture
Server:
- Add POST /api/v1/uploads and GET /api/v1/files/{id} endpoints
- Add CreateAttachment DB method for file upload records
- Allow empty message content when attachments are present
- CORS headers on file serving for WebView2 compatibility
Client — File uploads & attachments:
- Clipboard paste (Ctrl+V) and attach button (+) for file uploads
- Preview bar above input with thumbnail, spinner, and remove button
- Images fetched via Tauri HTTP plugin as base64 data URIs (bypasses
self-signed cert rejection in WebView2)
- Three-layer image cache: memory → IndexedDB → network
- In-flight deduplication prevents duplicate concurrent fetches
- Image lightbox with click-to-zoom, scroll wheel zoom, pan, keyboard shortcuts
Client — URL previews & embeds:
- URLs in messages rendered as clickable links
- YouTube embeds with thumbnail, play button, video title via oEmbed API
- Generic link previews with OG metadata (title, description, image)
- Fetched via Tauri HTTP plugin with Facebook crawler User-Agent
- YouTube title cache and OG metadata cache prevent re-fetch on re-render
- Links open in default browser via tauri-plugin-opener
Client — Voice & audio fixes:
- Mute uses replaceTrack(null) for reliable RTP-level muting in WebView2
- Deafen also mutes mic; undeafen/unmute unmutes both
- Muted users show crossed mic icon, deafened show crossed mic + headphone
- Re-apply mute state after input device switch
Client — UX improvements:
- Disable browser context menu globally (only custom menus show)
- Emoji search now matches by keyword names (smile, heart, fire, etc.)
- Emoji picker closes on click outside
- User bar status text moved below username
- Messages sorted chronologically (oldest first, newest at bottom)
- Scroll to bottom on initial load with deferred retries for layout shifts
- Image attachments constrained to 400x350px with click-to-lightbox
- Remove microphone and headphone buttons from UserBar (already in VoiceWidget)
- Disable default browser right-click menu globally so only custom context menus show
Server:
- Enforce category-type validation: text/announcement only under text categories,
voice only under voice categories (400 on mismatch)
- Admin panel category field changed to dropdown with auto-filtered type options
- Default setup creates both Text Channels and Voice Channels categories
Client:
- Add create/edit/delete channel modals (admin/owner only)
- "+" button on category headers to create channels with pre-filled category
- Right-click context menu on channels for edit/delete
- Mouse-based drag-and-drop reordering within categories
- Admin API methods: adminCreateChannel, adminUpdateChannel, adminDeleteChannel
- Immediate local store update on reorder for instant feedback
Tests: 7 server integration tests, 31 client unit tests (create/edit/delete modals)
When switching voice channels, the old PC's OnICEConnectionStateChange(closed)
fires asynchronously after the new PC is set. Both server and client had the
same race: the stale callback saw "voice is active" and called handleVoiceLeave,
closing the new session.
Server: setupICEMonitor now compares the PC reference before acting on events
Client: joinVoice now calls leaveVoice(false) to clean up old session first
- BUG-039: switchOutputDevice continues loop on partial failure instead of early return
- BUG-040: clearOnError() prevents stale callback after MainPage destroy
- BUG-041: voice store tests cover localCamera, localScreenshare, setLocalSpeaking
- BUG-042: auth store updateUser tests and UserBar mute/deafen callback tests
- BUG-043: switchInputDevice guards against no active WebRTC session
- BUG-044: replace synchronous confirm() with double-click-to-delete via toast
- BUG-045: isSafeUrl() blocks javascript: URLs in image attachment src
- BUG-031: Add switchInputDevice/switchOutputDevice to voiceSession;
VoiceAudioTab now applies device changes to active WebRTC session.
- BUG-032: Closed as false positive — channel WS handlers already wired
in dispatcher.ts:173-200.
- BUG-033: Closed as false positive — member WS handlers already wired
in dispatcher.ts:219-229.
- BUG-034: Closed as false positive — InviteManager filter runs inside
.then(), not before promise resolves.
- BUG-035: DmSidebar click handler now toggles .active class on items.
- BUG-036: Add setOnError callback to voiceSession; MainPage wires it
to toast for WebRTC failure feedback.
- BUG-026: Replace placeholder div with <img src=att.url> element in
renderAttachment, with lazy loading and error fallback.
- BUG-030: Delete orphaned MessageActionsBar.ts and ReactionBar.ts
components (never imported) along with their tests.
- BUG-024: Toggle reaction_add/reaction_remove based on me field.
- BUG-028: Add confirm() guard before chat_delete.
- BUG-029: Validate edit content is non-empty and changed.
- BUG-037: Show error toast when reaction rate limited.
- BUG-038: Add success toasts for delete and edit operations.
- BUG-021: Camera toggle reads actual localCamera state from voice store
instead of hardcoded false.
- BUG-022: Screenshare toggle sends voice_screenshare WS message with
localScreenshare state tracking.
- BUG-023: UserBar mute/deafen buttons wired via UserBarOptions callbacks
passed from MainPage, with voice channel guard and rate limiting.
- BUG-027: VAD onSpeakingChange callback wired to setLocalSpeaking in
voice store for local speaking indicator feedback.
- Added localCamera, localScreenshare, setLocalSpeaking to voice store.
- BUG-020: Wire api.changePassword() and api.updateProfile() into
MainPage settings overlay callbacks. Add updateUser() to auth store
for username sync. Add toast feedback for success/error.
- BUG-025: Call setTheme() in AppearanceTab click handler so uiStore
stays in sync with localStorage and applied CSS.
- Update test mock to include setTheme export.
Add actionTimeout, navigationTimeout, local retry, video capture, and
reducedMotion to both Playwright configs. Introduce waitForWsReady(),
navigateToMainPageReady(), and emitWsMessageAndWait() helpers. Fix
voice-widget selectors to match actual DOM structure. Update
E2E-ISSUES.md to reflect 209/209 passing.
Add comprehensive tests for db, storage, updater, and ws packages
covering edge cases, error paths, and voice handler functions.
New test files for attachment queries and WebSocket coverage boost.
- Exclude voiceSession.ts from coverage (browser API dependency, same
pattern as audio.ts/vad.ts/webrtc.ts)
- Remove stale TestHub_Register_CleansUpOldVoiceState test that tested
old duplicate-login behavior removed in b53c729
- Server: handle ICEConnectionStateClosed in setupICEMonitor to clean up
phantom participants when client PC is destroyed
- Server: skip TURN config when turn_secret is empty (suppresses noisy
"password is empty" errors)
- Client: voiceSession.leaveVoice() now sends voice_leave to server by
default, fixing the case where WebRTC failure triggers local cleanup
but server never learns the user left
- Client: explicit leave paths (UI button, logout, beforeunload) pass
sendWs=false to avoid double-sending voice_leave
Rewrites setupOnTrack to create TrackLocalStaticRTP for fan-out,
store tracks on VoiceRoom, add them to other participants' PCs,
and forward RTP while parsing audio levels. Updates handleVoiceJoin
to subscribe new joiners to existing tracks and handleVoiceLeave
to remove departing users' tracks from all subscribers.
Adds Hub.renegotiateParticipant to ws/voice_handlers.go. The function
creates a new SDP offer for a client's PeerConnection and sends it as
voice_offer, implementing the impolite side of Perfect Negotiation by
skipping renegotiation when the PC is in have-remote-offer state.
Add two new server-to-client WebSocket message builders following the
existing buildVoiceAnswer pattern. Expose them via export_test.go
wrappers and cover with ws_test package tests (TDD: RED → GREEN).
Introduces VoiceTrack (Remote/Local track pair + per-subscriber RTPSender map)
and five thread-safe methods on VoiceRoom (SetTrack, RemoveTrack, GetTracks,
TrackUserIDs, GetTrack) needed for SFU audio forwarding in Phase 3.
Close() now also resets the tracks map. Five new tests cover all paths (TDD).
The candidate field was typed as string but the server's handleVoiceICE
parses it as webrtc.ICECandidateInit (an object with candidate, sdpMid,
sdpMLineIndex, usernameFragment fields). Corrected to RTCIceCandidateInit.
PROTOCOL.md voice_ice example updated locally (gitignored vault).
Server now checks IsUserConnected before accepting a new WebSocket and
returns an auth_error with a clear message instead of silently replacing
the old session. Client dispatcher surfaces the error via transient UI
state so the ConnectPage can display it.
- Add cert mismatch modal for TOFU certificate pinning
- Separate voice channels from text in sidebar with user lists
- Implement scrollToMessage and jump-to-pinned-message in overlay
- Add server profiles with credential auto-fill on connect page
- Fix credential auto-fill race condition on rapid profile clicks
- Add channel_focus event for channel-scoped message delivery
- Fix member list case-insensitive role filtering
- Server normalizes role names to lowercase for protocol consistency
- Remove redundant permission-denied log in handleChannelFocus
- Voice store: bulk set states from ready payload, leave cleanup
- WebSocket reconnect and structured logging improvements
- Add tests for cert modal, overlay managers, voice sidebar,
message list scroll, quick switcher, and profile management
- Fix VoiceWidget disconnect not sending voice_leave to server
- Add voice cleanup on logout (send voice_leave before ws.disconnect)
- Add beforeunload handler for best-effort voice_leave on app close
- Broadcast voice_state/voice_leave to ALL clients (not just channel
members) so every sidebar updates when users join/leave voice
- Fix CleanupVoiceForChannel using BroadcastToChannel instead of
BroadcastToAll
- Remove user list from VoiceWidget (users only shown in sidebar)
- Guard VoiceWidget disconnect against double-click
- Add 6 tests for voice disconnect behavior
- #16: Fix golangci-lint issues (unchecked Close(), unused funcs, naming)
- #17: Add KeybindsTab and LogsTab unit tests (19 tests, 81%+ coverage)
- #18: Add rate limiting to chat_edit and chat_delete handlers
- #19: Fix cert mismatch handling via event listener instead of string match
- #20: Validate SHA-256 colon-hex fingerprint format in Rust ws_proxy
- #21: Optimize session+ban check with single JOIN query
- #22: Sort channels by position when redirecting after deletion
- #23: Rename admin test files for clarity
Also fixes ban expiry regression (H-1 from code review) by using
auth.IsEffectivelyBanned() to properly respect temporary ban expiry.
Fix 70+ errcheck violations by adding explicit error discards (`_ =`)
for unchecked return values across test helpers and deferred Close()
calls. Remove unused `senderID` field from broadcastMsg and unused
`defaultCleanupMaxWindow` const. Apply De Morgan's law, remove empty
branch, and simplify redundant type declaration per staticcheck.
- Fix double-close panic in Hub.Stop/GracefulStop using sync.Once (#3)
- Bump golangci-lint action to v9 with v2.11.3 for Go 1.25 support (#4)
- Add input validation guards to SearchMessages (#5)
- Handle promise rejections in InviteManager with error toasts (#6)
- Add missing reply_to and edited_at columns to admin test schema (#7)
- Add ClientCount to HubBroadcaster interface and wire into stats endpoint (#8)
Server:
- Add message search and pinned messages support
- Add admin hub integration and live connection stats
- Update admin test mocks for hub interface
Client:
- Add data-testid attributes to components for E2E testing
- Add window management capabilities (position, size, maximize)
- Add prod E2E test config and script
- Fix CSS imports (use vite bundling instead of HTML link tags)
- Add inline styles to InviteManager overlay for reliability
- Update CHATSERVER.md references from WPF to Tauri
Docs:
- Update quick-start guide
Implement DOM windowing so only visible messages (plus 10-item overscan
buffer) are rendered. Uses estimated heights with measured-height cache,
top/bottom spacer elements, and rAF-debounced scroll updates.
- Extract rendering helpers to components/message-list/renderers.ts
- MessageList uses VirtualItem abstraction for messages + day dividers
- Height cache keyed by message ID for accurate scroll positioning
- Update test assertion for new virtual scroll DOM structure
Resolves TODOS.md #11.