Compare commits

..

170 Commits

Author SHA1 Message Date
hobokenchicken 065f036807 fix(web): navigate to newly created DM in NewConversationModal
Release Desktop Apps / build-linux (push) Failing after 11m33s
Release Desktop Apps / build-windows (push) Failing after 11m37s
Release Desktop Apps / release (push) Has been skipped
2026-07-27 08:50:14 -04:00
hobokenchicken f53cd49803 fix(web): robust date parsing and merge strategy in conversation store 2026-07-27 08:49:57 -04:00
hobokenchicken 4e48815b91 fix(web): use keyed Fragment in DMChat message mapping 2026-07-27 08:49:41 -04:00
hobokenchicken d9b3162f1c fix(web): move hasMore selector below id declaration to avoid TDZ crash in DMChat 2026-07-22 08:36:12 -04:00
hobokenchicken 6384588122 chore: bump to 0.2.10 (versionCode 2010) 2026-07-21 10:09:20 -04:00
hobokenchicken 978e94da90 fix(android): bump safe-top to 2rem, enlarge toolbar icons, fix login spacing
- --safe-top: 1.5rem → 2rem for extra status bar clearance
- Formatting toolbar: w-3→w-4, p-1→p-1.5 for better touch targets
- Login form: increased vertical spacing between buttons, text-xs on passkey
2026-07-21 09:58:02 -04:00
hobokenchicken 34c18c13ae fix(android): increase login form button spacing, shrink passkey text
- forgot password link: mt-3 → mt-4
- passkey button: mt-3 → mt-4, added text-xs to prevent overflow
- create account: mt-4 → mt-5
2026-07-21 09:45:40 -04:00
hobokenchicken cca6ea0e37 fix: stop infinite scroll feedback when no more messages
Scroll handler now checks !hasMore to avoid calling fetchOlderMessages
when all messages are loaded. Previously the handler would fire on every
scroll event (since scrollTop stayed < 100), creating a .then() callback
loop that adjusted scroll position repeatedly, causing 'stuck' scrolling.
2026-07-21 09:05:39 -04:00
hobokenchicken 738b9b17ff fix: add unsafe-inline to Tauri script-src CSP
Release Desktop Apps / build-linux (push) Successful in 4m50s
Release Desktop Apps / build-windows (push) Successful in 16m59s
Release Desktop Apps / release (push) Successful in 11s
2026-07-20 13:42:21 -04:00
hobokenchicken 44370f41e5 ops: .dockerignore, non-root user, network isolation, deploy rollback
- .dockerignore excludes .git, node_modules, certs, *.zip
- Dockerfile runs as non-root appuser
- compose.yml isolates frontend (Caddy only) and backend (DB/cache/media) networks
- deploy.sh snapshots binary before pull and rolls back on failed health check
2026-07-20 13:15:19 -04:00
hobokenchicken 1d6c9bdfe6 perf: rate limiter cleanup goroutine, DB pool constraints
- ipLimiter evicts stale entries after 10 min of inactivity via 5 min sweep
- DB pool capped at 25 max open, 5 idle, 15 min lifetime
2026-07-20 13:13:58 -04:00
hobokenchicken 57aec2c6b3 sec: hash tokens in DB, set Tauri CSP, escape quotes in XSS guard
- Session and email verification/reset tokens stored as SHA-256 hash in DB
  (raw token stays client-side in cookie/email link)
- Tauri CSP set: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline';
  img-src 'self' data: https:; connect-src 'self' wss: https:
- escapeHtml now handles double and single quotes to prevent attribute-based XSS
2026-07-20 13:13:09 -04:00
hobokenchicken d4ca5f576e sec: point coturn at live LE certs, remove old ./certs/ mount 2026-07-20 13:10:27 -04:00
hobokenchicken 5f2b0aea8b sec: remove certs/cookie from tracking, parameterize LiveKit secrets, add .env.example 2026-07-20 13:03:42 -04:00
hobokenchicken 6f6d1329c6 fix(linux): guard WebSocket.prototype assignment — read-only on WebKitGTK 2026-07-20 12:26:14 -04:00
hobokenchicken 156ee4197c fix: remove env() from CSS, set safe-area via JS only 2026-07-20 11:50:07 -04:00
hobokenchicken a5882f32c4 chore: bump to v0.2.9 (version code 2009) 2026-07-20 11:37:26 -04:00
hobokenchicken 516168d290 fix(android): replace env() with JS-set CSS vars for safe areas 2026-07-20 11:30:28 -04:00
hobokenchicken c2be53e53f fix: append messages locally on send, don't rely solely on WS 2026-07-20 11:07:57 -04:00
hobokenchicken 953a1e26e6 fix: add Tauri origins to CSRF allowlist 2026-07-20 11:02:34 -04:00
hobokenchicken 5d9b629dca fix: correct debug symbols structure for Play Store 2026-07-20 10:58:21 -04:00
hobokenchicken 4cc6c6f1ed chore: bump to v0.2.8 (version code 2008) 2026-07-20 10:25:47 -04:00
hobokenchicken 82f329808e fix(android): replace default icons with dumpster fire logo 2026-07-20 10:12:38 -04:00
hobokenchicken 0ab08a9374 chore: bump to v0.2.7 (version code 2007) 2026-07-20 10:01:32 -04:00
hobokenchicken c9884d1cbd chore: add native debug symbols for Play Store 2026-07-20 09:52:24 -04:00
hobokenchicken 974eca4bb2 fix(android): edge-to-edge alignment, keyboard handling, toolbar spacing 2026-07-20 09:49:38 -04:00
hobokenchicken 8437ddcb7f chore: remove leftover hashgen temp file 2026-07-17 15:48:46 -04:00
hobokenchicken aa0af5db4f feat: account deletion in settings + privacy policy with deletion link 2026-07-17 15:47:29 -04:00
hobokenchicken e79505d8b4 fix(tauri): disable WebKitGTK sandbox inside AppImage on Linux and bump version to 0.2.6 2026-07-16 15:28:36 -04:00
hobokenchicken b0d820cd92 fix(tauri): set WEBKIT_DISABLE_DMABUF_RENDERER=1 on Linux to prevent white screen 2026-07-16 15:03:10 -04:00
hobokenchicken a8cb5c5933 fix: use SameSite=Lax for session cookie
SameSite=None requires Secure=true or modern browsers silently reject
the Set-Cookie header. Since the site is served over HTTPS via Caddy,
this was causing login to succeed (200) but the session cookie to be
dropped, making the subsequent /auth/me call fail with 401.

SameSite=Lax is the correct setting for same-origin session cookies.
2026-07-16 14:52:13 -04:00
hobokenchicken bda4c9d73d fix: handle 401 gracefully on web; add Bearer token auth for Tauri
- fetchMe() no longer surfaces 401 as a user-facing error (it just
  means 'no session', not a failure)
- API client auto-clears auth state on 401 mid-session so the user
  gets redirected to login instead of seeing 'ERR: Request failed: 401'
- Session middleware now accepts Authorization: Bearer <token> header
  as fallback when no cookie is present (for Tauri/native clients)
- Login, register, and WebAuthn endpoints expose X-Session-Token header
  so non-browser clients can capture the token
2026-07-16 14:46:17 -04:00
hobokenchicken d4fff01e35 fix(desktop): implement bearer token auth for windows webview2 2026-07-16 14:30:28 -04:00
hobokenchicken 77313671b6 fix(auth): set SameSite=None for session cookies to fix cross-origin session loss in Tauri apps 2026-07-16 14:26:18 -04:00
hobokenchicken e76f755abb fix(api): add CORS headers to allow cross-origin requests from Tauri desktop app 2026-07-16 14:21:15 -04:00
hobokenchicken 3bc8f9721e added firebase json 2026-07-16 14:07:29 -04:00
hobokenchicken a66b777712 fix(tauri): unregister and prevent service worker to fix 404 cache routing issues 2026-07-16 13:58:06 -04:00
hobokenchicken 19386c2b33 fix(tauri): redirect relative fetch and websocket urls to production backend for desktop app 2026-07-16 13:27:33 -04:00
hobokenchicken e7deb84660 fix(ci): manually configure caching using actions/cache@v3 for gitea compatibility 2026-07-16 13:03:56 -04:00
hobokenchicken 76230bb9d6 fix(ci): recursively glob artifact files to prevent empty releases 2026-07-16 12:47:45 -04:00
hobokenchicken 908b87eb5d fix(ci): correct rustup download URL for windows 2026-07-16 12:22:18 -04:00
hobokenchicken e62f551e2f fix(ci): use valid release action (softprops/action-gh-release) 2026-07-16 12:17:30 -04:00
hobokenchicken 83409cef0d chore(ci): add rust and npm caching to speed up builds 2026-07-16 11:02:23 -04:00
hobokenchicken a99714eec6 fix(ci): downgrade artifact actions to v3 for gitea compatibility 2026-07-16 10:46:54 -04:00
hobokenchicken 6a24b6f335 fix(ci): use curl and CI=true to prevent Windows runner hang 2026-07-16 10:33:46 -04:00
hobokenchicken 87a49118b5 added rpm to apt get list 2026-07-16 14:08:08 +00:00
hobokenchicken 300aeb219f fix: message area scroll — add min-h-0 to flex column 2026-07-16 09:23:08 -04:00
hobokenchicken 0f19b9089d feat(ui): Discord-style roles/channel perms + IDE themes
Split-pane role editor with tri-state channel overrides (roles/members).
CSS-var themes (Gruvbox default + 9 IDE palettes) in top bar and settings.
2026-07-15 21:46:07 -04:00
hobokenchicken 3c7b8278ce fix: client perms, @everyone/@channel, docs, unit tests
- usePermissions ORs current user roles + @everyone only (not all server roles)
- cache myRolesByServer; load on active server; refresh after self role edit
- gate/notify @everyone and @channel; plain @username push; special mention UI
- refresh FEATURE_PARITY (DMs exist; drop stale critical gaps)
- README production deploy notes dumpster.service
- unit tests for permission bits and broadcast mention tokens
2026-07-15 20:56:53 -04:00
hobokenchicken a277c78e2c feat(ui): BOTS section in member list
Members API appends server bots (is_bot). Sidebar groups ONLINE / OFFLINE / BOTS.
Bots get green BOT badge, no kick menu or profile. Mentions and DMs skip bots.
2026-07-15 20:37:05 -04:00
hobokenchicken 038ac1fe8e fix(bots): intercept /confess so original never hits chat
Root cause of "not anonymous":
1. Confess deleted via raw SQL with no MESSAGE_DELETE broadcast
2. Frontend extractIds only accepted message_id, but deletes send id
   so live clients never removed deleted messages without refresh

Fix:
- Intercept /confess at message create: never store or broadcast the
  original; post only the anonymous bot message
- Accept both id and message_id on MESSAGE_DELETE in the WS store
- Include both fields on delete broadcasts
2026-07-15 20:28:47 -04:00
hobokenchicken 07c72b041d fix(bots): auto-join server from channel_id so built-ins can post
Root cause: makeSender requires bot_servers membership, but create
flow never auto-added bots when users only picked a channel.

- Start() resolves config.channel_id → server and upserts bot_servers
- Confess cursor uses (created_at,id) so deletes don't stall polling
2026-07-15 20:17:23 -04:00
hobokenchicken f6322fb779 feat(bots): anonConfess + shitpostLeaderboard built-in bots
- ConfessBot: polls for /confess messages, deletes original, reposts anonymous
- LeaderboardBot: daily top-10 message count recap from DB
- BotFunc extended with *sql.DB param for DB-reading bots
- Both types registered in runner + BotManager UI
2026-07-15 19:46:09 -04:00
hobokenchicken 49ea7c4e3b fix(pwa): replace hamburger overlays with proper mobile bottom nav
- MobileBottomNav: [SERVERS] [CHAT] [MEMBERS] tab bar, always visible
- Servers tab opens sidebar overlay, chat/members switch views
- Removed hamburger + members toggle from mobile top bar
- Top bar compact on mobile (no redundant buttons)
- safe-area-inset-bottom on nav, clean inset on frame
- Desktop status bar hidden on mobile, preserved on desktop
- Dead MobileNav/MobileDrawer left in place (unused, can prune later)
2026-07-15 17:21:10 -04:00
hobokenchicken 95cbcf79a4 docs: update README for bot store, built-in runner, steamfree 2026-07-15 15:40:22 -04:00
hobokenchicken 3fd0b0e9e2 fix(bots): channel picker dropdown instead of ID text input
Server selector + channel dropdown for built-in bot config.
No more asking users to paste UUIDs.
2026-07-15 15:02:39 -04:00
hobokenchicken 97c5d81d70 chore: add steamfree binary to gitignore 2026-07-15 14:21:51 -04:00
hobokenchicken f9af9ae15d chore: remove committed binary 2026-07-15 14:16:57 -04:00
hobokenchicken 5127144709 feat(bots): built-in bot runner + steamfree from UI
- BotRunner: server-side goroutine manager for built-in bot types
- steamfree bot embedded in server (polls Steam API, posts free games)
- bot_type + config JSONB columns on bots table
- Create/Update/Delete handlers manage runner lifecycle
- GET /bots/types returns registered bot types
- BotManager: type selector dropdown + config fields on create
- No SSH needed: create a 'Steam Free Games' bot from /bots/manage
2026-07-15 14:14:02 -04:00
hobokenchicken 3d31054626 feat(bot): SteamFree bot — posts free Steam games
Polls Steam featured categories API every 30m, filters for 100%
discounts on games that had a real price, posts new finds to the
configured channel. Env: BOT_TOKEN, DUMPSTER_HOST, CHANNEL_ID,
POLL_MINUTES.
2026-07-15 13:05:28 -04:00
hobokenchicken 7c70082f37 feat(bots): bot framework polish + store
- /ws/bot endpoint: bot token auth via query param, SHA-256 lookup
- Bot WS actions: SEND_MESSAGE + DELETE_MESSAGE handled in gateway
- Bot messages: bot_id on messages table, bot badge in chat (green + BOT tag)
- Bot store: /bots lists all bots with server count + add-to-server
- Bot manager moved to /bots/manage
- Fix: command routes were double-nested under /bots/{botID}/commands
- Fix: fetchServerCommands route corrected to /bots/servers/...
2026-07-15 12:45:44 -04:00
hobokenchicken 2674c254a1 fix: register forum/thread routes at top level + notification dots in ServerBar
Moved thread and forum-tag routes from nested /servers/{sid}/channels/ to
top-level /channels/{id}/... to match frontend API calls. Forum posts were
getting SPA HTML fallback instead of JSON.

Added orange notification dots to DM and server buttons in ServerBar.
2026-07-10 09:45:37 -04:00
hobokenchicken 64df016f3f feat(ui): character counter in message compose box 2026-07-09 16:00:28 -04:00
hobokenchicken 2f55521187 fix(ui): textarea auto-expands instead of scrolling 2026-07-09 15:55:00 -04:00
hobokenchicken b23e9ffc6f feat(calendar): weekly list view with grid toggle, defaults to list 2026-07-09 15:13:40 -04:00
hobokenchicken fb9e3e39a8 fix(calendar): use r.Route subrouter to avoid chi path ambiguity with /channels/events/{eventID} 2026-07-09 15:02:40 -04:00
hobokenchicken c55ab2a8bb fix(voice): add reconnect/disconnect event handlers + connection quality logging 2026-07-09 13:15:21 -04:00
hobokenchicken 01dd273bd5 fix(voice): AudioRenderers listens to room events directly, not zustand 2026-07-09 13:08:02 -04:00
hobokenchicken e1c20044ac fix(voice): remove unused import 2026-07-09 13:03:07 -04:00
hobokenchicken 3783b37df8 fix(voice): handle browser autoplay block — flush pending audio on interaction 2026-07-09 13:02:40 -04:00
hobokenchicken 9a5594da4f fix(voice): simplify RemoteAudioTrack — remove stale listener race 2026-07-09 13:01:49 -04:00
hobokenchicken b325d5185b fix(voice): AudioRenderers re-renders on participant join 2026-07-09 12:55:36 -04:00
hobokenchicken 57615f5117 fix(voice): broadcast VOICE_JOIN/LEAVE via ws, show participants in sidebar for all users 2026-07-09 12:47:42 -04:00
hobokenchicken d20c0da498 feat(voice): persist A/V device preferences in localStorage 2026-07-09 12:36:43 -04:00
hobokenchicken 19a3ea489b fix: use powershell -Command for download, keep shell:cmd for execution 2026-07-07 19:46:27 +00:00
hobokenchicken a086a575be fix: Windows build — use cmd shell to avoid PowerShell execution policy 2026-07-07 19:19:55 +00:00
hobokenchicken 83eb6ea2f7 fix: use powershell instead of pwsh for Windows runner 2026-07-07 19:18:33 +00:00
hobokenchicken c83319ba37 fix: windows build — use pwsh instead of bash for Rust install 2026-07-07 19:17:42 +00:00
hobokenchicken 1ab5cfd721 fix: release workflow for Gitea Actions (runner labels + gitea-release-action) 2026-07-07 14:19:22 -04:00
hobokenchicken 62b8097746 bump: desktop app v0.2.0, build all Linux bundles (appimage, deb, rpm) 2026-07-07 10:20:27 -04:00
hobokenchicken 2025d763f9 fix: DMs now show full date+time like channels [DD.MM.YYYY @ HH:MM] 2026-07-07 14:01:27 +00:00
hobokenchicken c8151c6e38 change upload limit from 25MB to 1GB 2026-07-07 13:43:41 +00:00
hobokenchicken 6d08016451 feat: read indicator — NEW divider line between read and unread messages 2026-07-06 19:41:19 +00:00
hobokenchicken ca64608e12 ci: Gitea Actions release workflow for Linux + Windows desktop builds
Triggers on v* tags. Builds AppImage/deb/rpm on Linux runner,
MSI/NSIS on Windows runner, then creates a release with all assets.
Requires self-hosted runners registered in Gitea Actions.
2026-07-06 14:48:59 -04:00
hobokenchicken a91720d045 build: add build-tauri target (NO_STRIP=true for Arch) 2026-07-06 14:32:16 -04:00
hobokenchicken a3064b94ab feat: Tauri desktop app scaffold
- src-tauri/ with Rust backend, Cargo.toml, main.rs, lib.rs
- tauri.conf.json: Linux deb bundle with webkit2gtk deps
- package.json: added tauri script
- Build on a machine with Rust: cd web && npm run tauri build
2026-07-06 18:07:34 +00:00
hobokenchicken d033a3e088 docs: add formatting toolbar, DM kaomoji/GIFs, last-channel restore to features list 2026-07-06 14:04:48 -04:00
hobokenchicken 4e429ca920 feat: unread markers for channels and DMs
- Read states store: smarter hasUnread compares against latest message
- ConversationList: orange dot + bold name for unread DMs
- DMChat: auto-mark-read when viewing conversation
- WS handler: mark DM read on new message while active+focused
2026-07-06 17:56:19 +00:00
hobokenchicken c9a213ab8a fix: screenshare rendering, voice panel layout, SW cache bust with git SHA
- VideoGrid: add screen share track display with proper sizing
- participantToVoice: detect ScreenShare + ScreenShareAudio tracks
- VoicePanel: flex layout, no scroll, tiles dynamically fill space
- VoiceChannel: show screen/camera icons in participant list
- vite.config: inject git SHA into SW CACHE_VERSION on build
2026-07-06 17:49:17 +00:00
hobokenchicken cb4df31f43 fix: DM message ordering, consolidate input toolbar, add rich text/WYSIWYG, file upload with drag-drop
- Fix DM backend ListMessages to use DESC + reverse (match channel handler)
- Remove spurious .reverse() from frontend message/conversation stores
- Create shared MessageInput component with Slack-style single toolbar row
- Add file upload via + button with progress bar and drag-and-drop
- Add markdown/rich text toggle with full WYSIWYG block formatting
  (lists, blockquotes, links, headings, code blocks)
- Add frontend+backend security for file uploads (extension + content-type guards)
2026-07-06 17:33:20 +00:00
hobokenchicken d3756b5f47 feat(chat): add native emoji pickers for chat input and reactions 2026-07-06 13:59:20 +00:00
hobokenchicken c731101bc7 fix(dm): remove local append on send to prevent double messages 2026-07-06 13:53:11 +00:00
hobokenchicken b0248571f4 fix(dm): add image paste support to DM chat and fix realtime DM updates 2026-07-06 13:44:41 +00:00
hobokenchicken e6a3eea181 feat(chat): support pasting images directly into the chat box 2026-07-06 13:38:02 +00:00
hobokenchicken c65e93f9ba fix(webrtc): fix typescript errors for participant list migration 2026-07-06 13:32:10 +00:00
hobokenchicken d069707473 fix(permissions): wire up MUTE_MEMBERS bitmask and fix member permissions in frontend 2026-07-06 13:27:38 +00:00
hobokenchicken 0fa0227013 fix(webrtc): add error handling and ui alerts for background processor initialization 2026-07-06 13:16:01 +00:00
hobokenchicken 1ac176eb9f fix(webrtc): update CSP to allow background processor wasm and blob execution 2026-07-06 13:14:27 +00:00
hobokenchicken 90e9c24d81 fix(webrtc): fix invalid hook call in device settings modal for bgMode 2026-07-06 13:13:05 +00:00
hobokenchicken 682bfd9c6a feat(webrtc): add background blur and virtual background options 2026-07-06 13:08:59 +00:00
hobokenchicken b360832128 feat(webrtc): add admin option to mute other participants 2026-07-06 13:04:39 +00:00
hobokenchicken 30b24a5f9a feat(webrtc): add UI to select audio/video input and output devices 2026-07-06 12:56:27 +00:00
hobokenchicken ee7c4c4144 chore(build): automatically embed git commit hash in version string 2026-07-06 12:52:20 +00:00
hobokenchicken a4dbca093e fix(webrtc): sync participants state when local user publishes tracks 2026-07-06 12:49:14 +00:00
hobokenchicken 4adbffab0c feat(ui): tabulate voice and chat panels to prevent overlapping 2026-07-06 12:47:23 +00:00
hobokenchicken eb2ded9614 fix(webrtc): correctly subscribe to remote camera tracks 2026-07-06 12:40:26 +00:00
hobokenchicken 126c0323ee fix(webrtc): fix VideoGrid reactivity and layout 2026-07-06 12:37:35 +00:00
hobokenchicken 0d822803f9 feat(webrtc): render remote audio tracks and allow camera access 2026-07-06 12:25:40 +00:00
hobokenchicken 8222cc9502 fix(webrtc): configure external TURN server and update CSP to allow inline scripts 2026-07-06 12:20:53 +00:00
hobokenchicken 0427b0e624 fix(docker): update livekit schema, coturn ports, and proxy keys 2026-07-05 23:59:32 +00:00
hobokenchicken fde0bfef37 feat: inline markdown formatting toolbar
FormatToolbar component: B I S ` || buttons that wrap selected text
with markdown syntax. Added to both channel and DM input areas.
Keyboard shortcuts: Ctrl+B bold, Ctrl+I italic already work via
browser defaults on the rendered markdown.
2026-07-02 15:58:59 -04:00
hobokenchicken 7277614ae5 feat: GIF picker and kaomoji in DMs
- Added GiphyPicker to DM input area (toggle with [GIF] button)
- Added EmojiPicker/kaomoji to DMs (toggle with [☺] or Ctrl+E)
- GiphyPicker shows above input when toggled
- Kaomoji appends to input text
2026-07-02 15:50:28 -04:00
hobokenchicken 883b775c2f feat: restore last active channel on login
- Channel store saves {serverId, channelId} to localStorage on select
- ServerBar restores last server+channel after fetching servers
- Fetches channels for the saved server, then sets the channel if valid
- Gracefully handles missing/deleted servers or channels
2026-07-02 15:44:55 -04:00
hobokenchicken bca2ad5bcd docs: push notification setup in README, fix VAPID_SUBJECT example
- .env.example: VAPID_SUBJECT without mailto: prefix (library adds it)
- README: VAPID env vars in table, push notification setup section
- README: add dm/ to project structure, keygen instructions
- README: iOS tap-gesture requirement and Android battery optimization notes
2026-07-02 15:40:18 -04:00
hobokenchicken de53da1c76 fix: remove requireInteraction, bump vibrate for Android
requireInteraction can suppress heads-up delivery on some Android
versions when screen is off. Removed it so the OS can auto-dismiss
after showing the notification.
2026-07-02 15:36:51 -04:00
hobokenchicken d416d5d926 fix: remove Topic header, add push response logging
- Remove Topic header (causes issues with Apple push service)
- Log push response status codes for debugging
- Log endpoint prefix on errors for identification
- Auto-cleanup 410 (gone) subscriptions on success path too
2026-07-02 15:16:33 -04:00
hobokenchicken 41ae10ad64 fix: push delivery when app is closed
- TTL 30s -> 86400 (24h): push services were dropping undelivered notifs
- Urgency: high: wakes device from doze/sleep
- Topic: per-user to collapse duplicate pending pushes
- SW push handler: try/catch for malformed payloads, always call waitUntil
- Bump SW cache to v6
2026-07-02 15:11:07 -04:00
hobokenchicken e80334e336 fix: iOS push notification prompt
iOS requires user gesture for Notification.requestPermission().
- Added NotificationPrompt banner that shows on permission='default'
- autoSubscribePush only fires when already 'granted' (re-subscribe on restore)
- Banner uses tap handler for permission request (works on iOS)
- Gruvbox themed, dismissible, bottom-center toast
2026-07-02 15:04:31 -04:00
hobokenchicken 0bd049f52a feat: push notifications for DMs
- DM handler now sends push notifications to other conversation members
- Cleared stale push subscriptions (old broken VAPID keys)
- Auto-subscribe will re-create them on next page load
2026-07-02 14:56:58 -04:00
hobokenchicken c7df642876 feat: auto-subscribe push on login, better push logging
- Auto-subscribe to push on login, register, and session restore
- Guard against nil db in SendChannelNotification
- Log push send count per channel message
- Bump SW cache to v5
2026-07-02 14:34:02 -04:00
hobokenchicken fffa422d16 chore: remove committed keygen binary, gitignore 2026-07-02 14:24:30 -04:00
hobokenchicken d17eab3f11 fix: use webpush-go's GenerateVAPIDKeys
Old implementation just copied random bytes as the public key (not real EC math).
New cmd/keygen calls the library's proper P-256 key generation.
Removed broken local GenerateVAPIDKeys from push/handlers.go.
2026-07-02 14:24:24 -04:00
hobokenchicken 76e430aa22 fix: center and fill PWA icons
Trim background, cover-crop to square, center. No more letterboxing.
2026-07-02 14:16:14 -04:00
hobokenchicken f5e767cdbb feat: safe area insets for Dynamic Island / notch / cutout
viewport-fit=cover already set. Added env(safe-area-inset-*) padding
to outer container so content doesn't hide behind Dynamic Island,
notches, or the home indicator bar.
2026-07-02 14:07:19 -04:00
hobokenchicken 6bf21a81f7 fix: remove unused activeChannelId variable 2026-07-02 14:00:12 -04:00
hobokenchicken 8311ecaf7f fix: use ChannelStore for activeChannelId subscription
activeChannelId lives on ChannelStore, not ServerStore.
2026-07-02 13:59:27 -04:00
hobokenchicken d000b06748 feat: mobile-responsive layout
- Mobile (<md): single-panel view, sidebar/chat/members as overlays
- Hamburger button toggles sidebar overlay (ServerBar + ChannelList)
- Members button toggles MemberList overlay
- ← back button in ChatArea and DMChat headers returns to sidebar
- Auto-switch to chat view when channel or DM is selected
- Tap empty area to dismiss overlays
- Desktop layout unchanged (all panels side by side)
- Status/settings buttons hidden on small screens to save space
2026-07-02 13:58:15 -04:00
hobokenchicken b2726bdc5a feat: PWA overhaul
- Remove debug artifact from login page ('Use Link component: HOME')
- Full icon set: 48/72/96/128/144/152/192/384/512 + maskable variants
- Proper manifest: display_override, shortcuts, categories, scope
- Better service worker: offline fallback page, pre-caching, stale-while-revalidate
- Custom install banner (Android beforeinstallprompt + iOS instructions)
- Connection status indicator (online/offline toast)
- Updated index.html with proper favicon sizes and meta tags
2026-07-02 13:51:42 -04:00
hobokenchicken 2deb4acc0f fix: pass GIT_SHA inline to npm run build
The export-then-chain wasn't propagating into the subshell.
Use Make variable and pass GIT_SHA= directly to the command.
2026-07-02 13:21:17 -04:00
hobokenchicken cca1f1e619 feat: show git commit SHA as version in footer
TERM v1.0 → TERM v5ca5e01 (or whatever SHA is building).
GIT_SHA is injected via vite define from Makefile.
2026-07-02 13:19:43 -04:00
hobokenchicken 0f54657b23 feat: feature requests board with voting
- [FEATURES] tab next to PINNED in channel header
- Create, vote/unvote, filter by status (open/planned/done/rejected)
- Sort by vote count (most voted first)
- Status selector for moderation
- DB: feature_requests + feature_request_votes tables
- Backend: full CRUD + vote endpoints under /servers/{id}/feature-requests
2026-07-02 13:11:52 -04:00
hobokenchicken 6654a9ee6b fix: use window.addEventListener for dropdown keyboard nav
inputRef-based listener never fired (ref null at effect time or
element-level listeners don't catch events in this React setup).
Switched to window.addEventListener matching the pattern every other
keyboard handler in this codebase already uses.
2026-07-02 13:03:41 -04:00
hobokenchicken 174134630e fix: use addEventListener for dropdown keyboard navigation
Moved keydown handler from inline React prop to useEffect with
addEventListener on the input ref. Uses a ref to track latest
dropdown state, avoiding stale closure issues that prevented
arrow keys from working. Added stopPropagation on Enter to
prevent form submission race. Guard in handleSubmit to bail
when a dropdown is open.
2026-07-02 12:58:40 -04:00
hobokenchicken eee4bcaa8d fix: Enter on dropdown executes command immediately
Previously selecting a slash command with Enter just filled the
input, requiring a second Enter to send. Now Enter directly
executes the command transform (or opens /poll modal) and sends.
2026-07-02 12:47:58 -04:00
hobokenchicken a82fd7a473 feat: keyboard navigation for mention and slash command dropdowns
Arrow up/down cycles through items with highlight. Enter selects
the highlighted item. Escape dismisses the dropdown. Index resets
when the query changes.
2026-07-02 12:44:27 -04:00
hobokenchicken a8532fea21 feat: polls with live voting via WebSocket
- /poll command opens creation modal (2-10 options)
- PollDisplay with vote bars, percentages, live WS updates
- Backend: polls/poll_options/poll_votes tables, Create/Get/Vote endpoints
- attachPolls enriches message list responses
- POLL_UPDATE broadcast on vote for real-time sync
2026-07-02 12:35:05 -04:00
hobokenchicken b03dcb05a8 feat: slash commands with autocomplete dropdown
Type / at start of message to see available commands.
Commands: /shrug, /tableflip, /unflip, /lenny, /bear,
/disapprove, /facepalm, /cry, /dance, /hug, /greet,
/me (action text), /spoiler (hidden text).

Autocomplete dropdown with tab-complete. Commands transform
input text client-side before sending. Unknown /commands
pass through as regular messages.
2026-07-02 12:22:43 -04:00
hobokenchicken 73c2f4f01e fix: position kaomoji picker above message input bar
Moved EmojiPicker inside the form element so it anchors to the
input area (form is already position:relative). Picker now pops
up above the input instead of floating in the message scroll area.
2026-07-02 12:16:57 -04:00
hobokenchicken fe8ad61143 feat: Ctrl+E keyboard shortcut for kaomoji picker in message input 2026-07-02 12:13:00 -04:00
hobokenchicken 5d9b29a89b feat: kaomoji picker with full kaomojikuma.com library
34 categories, 3300+ kaomoji from kaomojikuma.com.
Data in web/src/lib/kaomojiData.ts (exported KAOMOJI_CATEGORIES).
EmojiPicker refactored to import from external data file.
Categories: Greetings, Joy, Sad, Mad, Stress, Love, Animals,
Action, Shy, OwO, OMG, Neutral, Kawaii, Wave, Bodily, Table,
Fight, Magic, Dance, Sleep, Run, Music, Hugs, Kiss, Wink,
Heart, Rage, Thumbs, Cheer, Drink, Write, Comfort, Flourishes.
Search by emoji text or tags. Tabbed browsing.
2026-07-02 12:04:22 -04:00
hobokenchicken 6d0a1fd15f fix: server settings groups tab visible to owner
Server interface had 'ownerId' (camelCase) but API returns
'owner_id' (snake_case). isOwner was always false, hiding
groups/roles/audit tabs for the server owner.
2026-07-02 11:47:53 -04:00
hobokenchicken 199be07961 fix: preserve whitespace around mentions
renderContent: mention span now includes trailing space when the
next text segment doesn't start with one (React collapses whitespace
between sibling inline elements).

handleMentionSelect: avoid double space when the text after cursor
already starts with a space.
2026-07-02 11:41:12 -04:00
hobokenchicken 7c6037bd93 added features and fixes 2026-07-02 15:34:00 +00:00
hobokenchicken 64267cbdd0 fix: TYPING_START events now carry user_id and username
The backend was broadcasting TYPING_START with only channel_id.
Frontend required user_id + username to show who is typing.
Added Username to Client struct, fetched on WS connect via JOIN.
Typing events now enriched with sender identity before broadcast.
2026-07-02 09:39:15 -04:00
hobokenchicken 52f949fece feat: typing indicators in channel and DM chats
Wired existing typing store (backend WS broadcast + frontend
_handleTypingEvent with 3s auto-expiry) into ChatArea and DMChat.
Typing events throttled to once per 3s. Shows 'X is typing...' or
'X and N others are typing...' below the messages, above the input.
2026-07-02 09:33:51 -04:00
hobokenchicken aad0eab716 feat: scroll-to-top loads older messages
Both ChatArea and DMChat now detect when you scroll near the top
and fetch the next 50 older messages. Scroll position is preserved
so you don't lose your place. Works for both server channels and
DM conversations.
2026-07-02 09:28:28 -04:00
hobokenchicken 17ee59c9c4 feat: thin themed scrollbar replaces hidden scrollbars
6px scrollbar with bg track, bg-t thumb, fg-f hover. Subtle but
visible. Applies globally to all scrollable areas.
2026-07-02 09:22:08 -04:00
hobokenchicken b28e072e81 fix: message list now returns most recent 50, not oldest 50
Query was ORDER BY ASC LIMIT 50 — returning the 50 oldest messages.
On page refresh users saw ancient messages instead of recent ones.
Changed to ORDER BY DESC LIMIT then reverse in Go so the API still
returns chronological order but from the bottom of the history.
2026-07-02 09:19:31 -04:00
hobokenchicken f4c5fae10a fix: server owner can now update channel settings
Channel Update handler required MANAGE_CHANNELS role permission.
Server owner was getting 'forbidden' because their roles didn't
include that bit. Added owner bypass (same pattern as Delete).
2026-07-02 09:15:18 -04:00
hobokenchicken f6403339fd feat: channel settings modal now has name editing
Added [OVERVIEW] tab to ChannelSettingsModal with an editable
channel name field. The ⚙ button on each channel opens the modal.
Users can rename channels without needing to know about the
right-click context menu.
2026-07-02 09:10:33 -04:00
hobokenchicken c57889e477 fix: invisible notification/settings icons were eating channel clicks
The notification bell and settings gear spans had opacity-0 but
pointer-events were still active. Their stopPropagation handlers
prevented the parent button's onClick from firing when clicking
in the icon area. Added pointer-events-none to the container,
re-enabled on group-hover.
2026-07-02 09:06:04 -04:00
hobokenchicken 9ee7b9e587 refactor: remove redundant far-left [S][@] selector
ServerBar already has [@] at the top and server icons below.
The far-left w-16 div was a duplicate layer. ServerBar now
navigates on server/DM clicks.
2026-07-02 09:02:40 -04:00
hobokenchicken 5462f74d03 fix: nested button inside button broke channel clicks
Invalid HTML: <button> inside <button> causes the browser to close
the outer button prematurely, so clicking the channel name never
fires setActiveChannel. Changed inner elements to <span role=button>.
2026-07-02 08:52:26 -04:00
hobokenchicken 5153ad7210 fix: nav buttons now navigate, not just toggle store state
The [S] button only called setDM(false) without navigating away from
/chat area. Since the notes-to-self feature now calls navigate('/dm/:id'),
the URL stayed at /dm/... and ChatArea never re-mounted.

[S] now navigates to /, [@] navigates to /dm.
2026-07-02 08:49:47 -04:00
hobokenchicken 3fee0e5d43 fix: remove user_ids validation that blocked notes-to-self
The 'at least one user_id is required' check ran before the creator
was added to memberSet, so {user_ids: []} always 400'd. Now empty
user_ids is allowed — the creator is always in memberSet.
2026-07-02 08:45:52 -04:00
hobokenchicken b22d231a18 fix: clicking DM entries actually navigates to conversation
ConversationList was only setting activeConversationId in the store
but never navigating. If the Outlet was still showing ChatArea from
a server channel route, DMChat never mounted. Now every click
navigates to /dm/:conversationId so the router mounts DMChat.
2026-07-02 08:43:57 -04:00
hobokenchicken 6d97d28237 debug: add error surfacing for notes-to-self creation 2026-07-02 08:40:07 -04:00
hobokenchicken e951e4b922 feat: notes to self via self-DM
Backend:
- Removed self-DM restriction in conversation Create
- Added duplicate check for self-DMs (returns existing one)

Frontend:
- ConversationList: [📝] button creates/opens notes to self
- Self-DMs show as 'Notes' with 📝 icon in sidebar
- DMChat header shows 'Notes' for self-DMs
2026-07-02 08:33:57 -04:00
hobokenchicken ac98f73616 fix: channel PATCH was nulling group_id on name-only updates
group_id =  without COALESCE wrote NULL when only name was sent,
orphaning the channel from its group.
2026-07-02 08:30:09 -04:00
hobokenchicken d2c5830466 fix: channel/group rename now works
- Input field no longer nested inside a button/clickable div
- Snapshot state before clearing editing mode to avoid stale closures
- Log errors instead of silently swallowing them
2026-07-02 08:25:11 -04:00
hobokenchicken bf3bf2392c feat: right-click context menus for channels, groups, and servers
ContextMenu: reusable hook-based component that positions a menu at
the cursor and auto-closes on outside click or Escape.

ChannelList:
- Right-click channel -> Edit Name, Permissions, Delete Channel
- Right-click group header -> Edit Name, Create Channel Here, Delete Group
- Inline rename: replacing name text with an input, commits on Enter/blur
- CreateChannelModal now accepts defaultGroupId to pre-select a group

ServerBar:
- Right-click server icon -> Server Settings, Invite People, Leave Server
- Leave calls DELETE /servers/{id}/members/me

Backend:
- Added LeaveServer handler (DELETE /servers/{serverID}/members/me)
- Server owner cannot leave; must transfer or delete
2026-07-02 08:18:12 -04:00
hobokenchicken 37d8b82d5b fix: use gif image URL instead of giphy page URL in messages
gif.url is the giphy.com page link, gif.images.fixed_height.url is
the actual media CDN URL. react-markdown was rendering a dead link
instead of an embedded image.
2026-07-02 08:06:31 -04:00
hobokenchicken 5f95fa6357 fix(pwa): generate icons, network-first HTML, bust stale cache 2026-07-01 16:14:29 -04:00
hobokenchicken 086ed99ae0 fix: stop double-posting messages (WS + HTTP both appended) 2026-07-01 15:59:31 -04:00
hobokenchicken eaf8d0d8fa fix: use APP_URL for email links instead of host:port
DUMPSTER_PORT=8080 (internal) was baked into email reset links,
producing http://dumpster.dustin.coffee:8080/reset-password which
doesn't resolve through Caddy. Added Config.AppURL() that reads
APP_URL env var (set to https://dumpster.dustin.coffee on server),
falls back to http://host:port for dev.
2026-06-30 15:59:55 -04:00
hobokenchicken 0c25e42bc3 fix: move DUMPSTER banner back inside login card, centered 2026-06-30 15:56:45 -04:00
hobokenchicken 876ae5c227 fix: span DUMPSTER banner full-width above login card 2026-06-30 15:55:14 -04:00
hobokenchicken d5f0310218 fix: center DUMPSTER and LOGIN headings on login screen 2026-06-30 15:53:04 -04:00
hobokenchicken 1d81b811bd feat: add forgot password UI flow
- ForgotPasswordPage: email input, sends reset request, shows success
- ResetPasswordPage: reads token from ?token= query param, password
  + confirm inputs, validates min length, shows success with login link
- LoginForm: added [FORGOT PASSWORD?] link below the form
- App.tsx: /forgot-password and /reset-password routes (public, no auth)
- auth store: requestPasswordReset + resetPassword actions
- Backend routes already existed at POST /auth/request-password-reset
  and POST /auth/reset-password
2026-06-30 15:49:23 -04:00
root 413c423ad1 feat: add SMTP email support and fix profile/permissions bugs 2026-06-30 19:29:08 +00:00
22 changed files with 234 additions and 198 deletions
+8
View File
@@ -0,0 +1,8 @@
.git
node_modules
web/dist
certs
*.zip
dumpster-server
.env
.env.local
+22 -24
View File
@@ -1,25 +1,23 @@
# App
DUMPSTER_HOST=localhost
DUMPSTER_PORT=8080
DUMPSTER_SECRET=*** Database
POSTGRES_HOST=localhost
POSTGRES_PORT=5432
# dumpsterChat Environment Configuration
# Copy this file to .env and fill in your values.
# Server
DUMPSTER_HOST=dumpster.dustin.coffee
DUMPSTER_SECRET=generate-a-random-secret-here
# Database
POSTGRES_USER=dumpster
POSTGRES_PASSWORD=dumpster
POSTGRES_SSLMODE=disable
# Valkey
VALKEY_URL=redis://localhost:***@example.com
# MinIO
MINIO_ENDPOINT=minio:9000
MINIO_ACCESS_KEY=<generated>
MINIO_SECRET_KEY=<gener...n
# LiveKit
LIVEKIT_URL=wss://livekit.your.domain
LIVEKIT_API_KEY=<generated>
LIVEKIT_API_SECRET=<gener...n
# Web Push
VAPID_PUBLIC_KEY=<generated>
VAPID_PRIVATE_KEY=<generated>
VAPID_SUBJECT=admin@your.domain
# Integrations
GIPHY_API_KEY=your_giphy_api_key_here
POSTGRES_PASSWORD=generate-a-random-password-here
POSTGRES_DB=dumpster
# MinIO (optional — omit for local filesystem)
MINIO_ENDPOINT=
MINIO_ACCESS_KEY=
MINIO_SECRET_KEY=
# LiveKit (WebRTC / voice)
LIVEKIT_API_KEY=generate-a-random-key-here
LIVEKIT_API_SECRET=generate-a-random-secret-here
# Giphy (optional — for GIF search)
GIPHY_API_KEY=
+4
View File
@@ -46,3 +46,7 @@ steamfree
# Tauri Android keystore (contains signing passwords)
web/src-tauri/gen/android/keystore.properties
# Secrets and certificates
/certs/
cookie.txt
-81
View File
@@ -1,81 +0,0 @@
-----BEGIN CERTIFICATE-----
MIIDkTCCAxegAwIBAgISBQjShZO8+oxeU80iyTOUKHhdMAoGCCqGSM49BAMDMDMx
CzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MQwwCgYDVQQDEwNZ
RTIwHhcNMjYwNzA2MDEzMDM1WhcNMjYxMDA0MDEzMDM0WjAdMRswGQYDVQQDExJ0
dXJuLmR1c3Rpbi5jb2ZmZWUwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATB70bx
IRjXyxdwR/3k56AkiFRKwApxq/h7xNXK5/I5xZ+5+HCLMHjEmFOIUtUj87DpcPHs
rAfJZcH19gQIt7Xlo4ICHzCCAhswDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoG
CCsGAQUFBwMBMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFE8HllwR3SN9Ay0rvVLX
9mOsdJ0UMB8GA1UdIwQYMBaAFLlZ8o7PIvCG0zdI/3YUGLqC2FWHMDMGCCsGAQUF
BwEBBCcwJTAjBggrBgEFBQcwAoYXaHR0cDovL3llMi5pLmxlbmNyLm9yZy8wHQYD
VR0RBBYwFIISdHVybi5kdXN0aW4uY29mZmVlMBMGA1UdIAQMMAowCAYGZ4EMAQIB
MC4GA1UdHwQnMCUwI6AhoB+GHWh0dHA6Ly95ZTIuYy5sZW5jci5vcmcvOTQuY3Js
MIIBCwYKKwYBBAHWeQIEAgSB/ASB+QD3AHUAwjF+V0UZo0XufzjespBB68fCIVoi
v3/Vta12mtkOUs0AAAGfNUH9yAAABAMARjBEAiBUIFA17umU+cAdlBJlkjhivt7h
4f1HF7kdlQtt0hURaAIgRWAiqclKJy3QikfbGF7EFK3sNRyf/PZNBLtjJeieP1IA
fgAm42RuWGkhI7w0P0ckNZs3ks0kWojYFdOTM/2ZGKtHIwAAAZ81QfvIAAgAAAUA
JeWwugQDAEcwRQIhAJ9gzEj26/VOpYO0HkS2ZcItIhoBf7bDfzt9gpIK2RH2AiA2
66DvuRKPYthZ4AFBXbVtSX4rf4aX2IvgQIWTudJMlDAKBggqhkjOPQQDAwNoADBl
AjANu3rbmE/SqrfsMNGEv7rl+qvblv0btupKZ2qHg87079XD4gTh3Sqyl+Z4RnHq
JkkCMQDI3q5ZQ6GVF3geVTbNvLlJ+R35eB+EIcoX0Cbs20S5k9N/SiWS2BlQzlUM
QpPxaok=
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIICjDCCAhGgAwIBAgIQTfOxXdbAeExQfNN7WObxFTAKBggqhkjOPQQDAzAuMQsw
CQYDVQQGEwJVUzENMAsGA1UEChMESVNSRzEQMA4GA1UEAxMHUm9vdCBZRTAeFw0y
NTA5MDMwMDAwMDBaFw0yODA5MDIyMzU5NTlaMDMxCzAJBgNVBAYTAlVTMRYwFAYD
VQQKEw1MZXQncyBFbmNyeXB0MQwwCgYDVQQDEwNZRTIwdjAQBgcqhkjOPQIBBgUr
gQQAIgNiAARxmrQzkdbEEL3MqXt3dJQttYc47axkdDTHud5TPqM2z5uSD5cmk0Wr
HlWXvnlvqBLqiB34kluxIbmMyAiq3/YD6e80/vV259K8XQIdjFXloYOa0mIU71f7
HQ09PvYDlw+jge4wgeswDgYDVR0PAQH/BAQDAgGGMBMGA1UdJQQMMAoGCCsGAQUF
BwMBMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFLlZ8o7PIvCG0zdI/3YU
GLqC2FWHMB8GA1UdIwQYMBaAFKPIJlqOoUzQNWP8myPIOq5W809WMDIGCCsGAQUF
BwEBBCYwJDAiBggrBgEFBQcwAoYWaHR0cDovL3llLmkubGVuY3Iub3JnLzATBgNV
HSAEDDAKMAgGBmeBDAECATAnBgNVHR8EIDAeMBygGqAYhhZodHRwOi8veWUuYy5s
ZW5jci5vcmcvMAoGCCqGSM49BAMDA2kAMGYCMQDIcnw5dcZLN9ffynXnnkLD/itS
JEycJPb3sRkzeqBowup7vOsAwaqoCnNn/jh9wycCMQCJM6CPlaOC4pQYYbJtVPYb
DKrIb2EKk5NpOpE6/XttQYZV/3gilB9l+Cc/DOVwmyg=
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIICpjCCAiugAwIBAgIRAIchZfw0tuX7qK3Vs3BftTowCgYIKoZIzj0EAwMwTzEL
MAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2VhcmNo
IEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDIwHhcNMjYwNTEzMDAwMDAwWhcN
MzIwOTAyMjM1OTU5WjAuMQswCQYDVQQGEwJVUzENMAsGA1UEChMESVNSRzEQMA4G
A1UEAxMHUm9vdCBZRTB2MBAGByqGSM49AgEGBSuBBAAiA2IABDwS/6vhrcVqcbBo
+wgdI3fwn9x7DNJJOY/lTOti0vkwuRN87RhEhTH17E7XyFjWsPYhIPt/wzOqxTd2
b+4ZJNy9ID04YywF9U5zasDVyGSNErVNtz8uSGh5izW87j77GaOB6zCB6DAOBgNV
HQ8BAf8EBAMCAQYwEwYDVR0lBAwwCgYIKwYBBQUHAwEwDwYDVR0TAQH/BAUwAwEB
/zAdBgNVHQ4EFgQUo8gmWo6hTNA1Y/ybI8g6rlbzT1YwHwYDVR0jBBgwFoAUfEKW
rt5LSDv6kviejM9ti6lyN5UwMgYIKwYBBQUHAQEEJjAkMCIGCCsGAQUFBzAChhZo
dHRwOi8veDIuaS5sZW5jci5vcmcvMBMGA1UdIAQMMAowCAYGZ4EMAQIBMCcGA1Ud
HwQgMB4wHKAaoBiGFmh0dHA6Ly94Mi5jLmxlbmNyLm9yZy8wCgYIKoZIzj0EAwMD
aQAwZgIxAMU19WCtmxVND8UHBZRoma49Z7jPs64Dma0eTu1OChVbB/2J7GV3nvYK
Ax54uk1G9QIxAO0miLVJu8PLNiXXXkiE/gsK3CTRTF/aeo4bMX42Zw40csRU6AC2
6hSW1/IWaas6dg==
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIEcDCCAligAwIBAgIQbI8dxyfHEX97r4U6yYD5zTANBgkqhkiG9w0BAQsFADBP
MQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJuZXQgU2VjdXJpdHkgUmVzZWFy
Y2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBYMTAeFw0yNjA1MTMwMDAwMDBa
Fw0zMjA5MDIyMzU5NTlaME8xCzAJBgNVBAYTAlVTMSkwJwYDVQQKEyBJbnRlcm5l
dCBTZWN1cml0eSBSZXNlYXJjaCBHcm91cDEVMBMGA1UEAxMMSVNSRyBSb290IFgy
MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEzZvVn4CDCuwJSvMWSj5cz3es3mcFDR0H
ttwW+1qLFNvicWDEukWVEYmO6gbf9yoWHKS5xcUy4APgHoIYOIvXRdgKam7mAHf7
AlF9ItgKbppbd9/w+kHsOdx1ymgHDB/qo4H1MIHyMA4GA1UdDwEB/wQEAwIBBjAd
BgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwDwYDVR0TAQH/BAUwAwEB/zAd
BgNVHQ4EFgQUfEKWrt5LSDv6kviejM9ti6lyN5UwHwYDVR0jBBgwFoAUebRZ5nu2
5eQBc4AIiMgaWPbpm24wMgYIKwYBBQUHAQEEJjAkMCIGCCsGAQUFBzAChhZodHRw
Oi8veDEuaS5sZW5jci5vcmcvMBMGA1UdIAQMMAowCAYGZ4EMAQIBMCcGA1UdHwQg
MB4wHKAaoBiGFmh0dHA6Ly94MS5jLmxlbmNyLm9yZy8wDQYJKoZIhvcNAQELBQAD
ggIBAD2/e9frmMxNpCV03qUHegg+MV2wz9644YoXdqtH8RyWYcBO7xfjjGEXdU1e
/o0OkEFiynUCOSIk/vLLo7ttz6CPAeNlWfC0XNkoGeWgK6jjXvozBaGuGH5n0Ufo
shMeWTuURqNN5G00sSXDTBrpp2+mgvdZQjb8K11TYMA25QA+YHNfbIEL0BniAhKS
2gsnJjSzrdZLI+EZ7SEyqdR2rkjd1KutLDU+n3TFyxjniZVGur4YlhMP3mY/dV95
IruAkkjOZier6hGBdEgZXXvaCz9u9iVEadsIE75pAGL8oHV5vxdARDiotRpul1IN
/UZwzAbrfUFcw1HkAcYD/mlZfnQ2ieCF2MS7j3Vhv7JPDKp45fmykmzYNSrumRW0
upFFKDBOoF7hsOb7oLyHS+Uft6jOUfOrogj8YUx38hKb2K20r42OgsSdDdxdeYWc
MS3Sb6mwJeSZEYxJ2gaXnDSPaKhhrNkYwljyVQyr4Nq+MEJytXNTnHqaAcrNwZlV
pcJL1KBnMrMjP7eanvUwL3FYj3cF17jtboLt7gLoi4+2rWZFvn+w54jmd/FIuhhZ
cEaU/wvU6BUNMtcVquVGHp7itQeDth5j+XL3j4WJ2SABwzUl6OeYdgpIt/ITZa+p
TT0mQ/r5XyA4MEAiabn7XJjvCERlF2dcn2wqJw+CreTkkQ2R
-----END CERTIFICATE-----
-5
View File
@@ -1,5 +0,0 @@
-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQggnJOfQM8flEJFULr
8vvHwWRlXlJ1a0sI5Jw/Y6KRQeGhRANCAATB70bxIRjXyxdwR/3k56AkiFRKwApx
q/h7xNXK5/I5xZ+5+HCLMHjEmFOIUtUj87DpcPHsrAfJZcH19gQIt7Xl
-----END PRIVATE KEY-----
-5
View File
@@ -1,5 +0,0 @@
# Netscape HTTP Cookie File
# https://curl.se/docs/http-cookies.html
# This file was generated by libcurl! Edit at your own risk.
#HttpOnly_localhost FALSE / TRUE 1785435515 dumpster_session 720a3aad74029e6c8e0c4b93f8db0b48381afc5355e1c1345143a0f5e3d5ae34
+25 -2
View File
@@ -8,8 +8,31 @@ export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
nvm use 22
# Snapshot the current binary before pulling
if [ -f dumpster-server ]; then
cp dumpster-server dumpster-server.bak
echo "Backed up existing binary to dumpster-server.bak"
fi
git pull
make build
systemctl restart dumpster
echo "Deploy complete — $(date)"
# Health check after restart — roll back on failure
systemctl restart dumpster
echo "Waiting for app to become healthy..."
for i in $(seq 1 15); do
if curl -sf http://localhost:8080/ > /dev/null 2>&1; then
echo "Server is listening — deploy complete"
rm -f dumpster-server.bak
echo "Deploy complete — $(date)"
exit 0
fi
sleep 2
done
echo "Health check failed after 30s — rolling back..."
systemctl stop dumpster
cp dumpster-server.bak dumpster-server
systemctl start dumpster
echo "Rolled back to previous binary — $(date)"
exit 1
+3 -1
View File
@@ -16,8 +16,10 @@ RUN npm run build
FROM alpine:latest
RUN apk --no-cache add ca-certificates
RUN adduser -D -g '' appuser
WORKDIR /app
COPY --from=go-builder /bin/dumpster-server /app/dumpster-server
COPY --from=web-builder /app/dist /srv/web
USER appuser
EXPOSE 8080
CMD ["sh", "-c", "if [ -z \"$DUMPSTER_SECRET\" ]; then export DUMPSTER_SECRET=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | head -c 32); fi; /app/dumpster-server"]
CMD sh -c "if [ -z \"$DUMPSTER_SECRET\" ]; then export DUMPSTER_SECRET=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | head -c 32); fi; /app/dumpster-server"
+22 -3
View File
@@ -1,5 +1,9 @@
version: '3.8'
networks:
frontend:
backend:
services:
app:
build:
@@ -32,6 +36,9 @@ services:
condition: service_healthy
valkey:
condition: service_healthy
networks:
- frontend
- backend
postgres:
image: postgres:16
@@ -49,6 +56,8 @@ services:
interval: 5s
timeout: 5s
retries: 5
networks:
- backend
valkey:
image: valkey/valkey:8
@@ -62,6 +71,8 @@ services:
interval: 5s
timeout: 3s
retries: 5
networks:
- backend
minio:
image: minio/minio:latest
@@ -75,6 +86,8 @@ services:
- "9001:9001"
volumes:
- minio_data:/data
networks:
- backend
livekit:
image: livekit/livekit-server:latest
@@ -89,6 +102,8 @@ services:
- ./livekit.yaml:/etc/livekit.yaml:ro
environment:
LIVEKIT_KEYS: "${LIVEKIT_API_KEY:-devkey}: ${LIVEKIT_API_SECRET:-secret}"
networks:
- backend
coturn:
image: coturn/coturn:latest
@@ -99,8 +114,8 @@ services:
- "--max-port=50200"
- "--realm=${DUMPSTER_HOST:-localhost}"
- "--user=${LIVEKIT_API_KEY:-devkey}:${LIVEKIT_API_SECRET:-secret}"
- "--cert=/etc/coturn/certs/turn.crt"
- "--pkey=/etc/coturn/certs/turn.key"
- "--cert=/etc/letsencrypt/live/turn.dustin.coffee/fullchain.pem"
- "--pkey=/etc/letsencrypt/live/turn.dustin.coffee/privkey.pem"
- "--tls-listening-port=5349"
ports:
- "3478:3478/tcp"
@@ -109,7 +124,9 @@ services:
- "5349:5349/udp"
- "50101-50200:50101-50200/udp"
volumes:
- ./certs:/etc/coturn/certs:ro
- /etc/letsencrypt:/etc/letsencrypt:ro
networks:
- backend
caddy:
image: caddy:2
@@ -124,6 +141,8 @@ services:
- caddy_config:/config
depends_on:
- app
networks:
- frontend
volumes:
postgres_data:
+3 -3
View File
@@ -9,9 +9,9 @@ rtc:
- host: turn.dustin.coffee
port: 443
protocol: tls
username: V8S6EWOU1Lqp
credential: jQRJztgWbqDn9vjMUhHZg1ooxuhiBjhk
username: "${LIVEKIT_API_KEY}"
credential: "${LIVEKIT_API_SECRET}"
turn:
enabled: false
keys:
V8S6EWOU1Lqp: jQRJztgWbqDn9vjMUhHZg1ooxuhiBjhk
"${LIVEKIT_API_KEY}": "${LIVEKIT_API_SECRET}"
+6 -4
View File
@@ -30,10 +30,11 @@ func (h *Handler) RequestVerification(w http.ResponseWriter, r *http.Request) {
}
token := generateToken()
tokenHash := hashToken(token)
_, err = h.db.ExecContext(r.Context(), `
INSERT INTO user_tokens (user_id, token, type, expires_at)
VALUES ($1, $2, 'verify_email', NOW() + INTERVAL '24 hours')
`, userID, token)
`, userID, tokenHash)
if err != nil {
http.Error(w, `{"error":"failed to generate token"}`, http.StatusInternalServerError)
return
@@ -63,7 +64,7 @@ func (h *Handler) VerifyEmail(w http.ResponseWriter, r *http.Request) {
DELETE FROM user_tokens
WHERE token = $1 AND type = 'verify_email' AND expires_at > NOW()
RETURNING user_id
`, req.Token).Scan(&userID)
`, hashToken(req.Token)).Scan(&userID)
if err != nil {
http.Error(w, `{"error":"invalid or expired token"}`, http.StatusBadRequest)
return
@@ -100,10 +101,11 @@ func (h *Handler) RequestPasswordReset(w http.ResponseWriter, r *http.Request) {
}
token := generateToken()
tokenHash := hashToken(token)
_, err = h.db.ExecContext(r.Context(), `
INSERT INTO user_tokens (user_id, token, type, expires_at)
VALUES ($1, $2, 'reset_password', NOW() + INTERVAL '1 hour')
`, userID, token)
`, userID, tokenHash)
if err != nil {
http.Error(w, `{"error":"failed to generate token"}`, http.StatusInternalServerError)
return
@@ -134,7 +136,7 @@ func (h *Handler) ResetPassword(w http.ResponseWriter, r *http.Request) {
DELETE FROM user_tokens
WHERE token = $1 AND type = 'reset_password' AND expires_at > NOW()
RETURNING user_id
`, req.Token).Scan(&userID)
`, hashToken(req.Token)).Scan(&userID)
if err != nil {
http.Error(w, `{"error":"invalid or expired token"}`, http.StatusBadRequest)
return
+12 -3
View File
@@ -3,6 +3,7 @@ package auth
import (
"context"
"crypto/rand"
"crypto/sha256"
"database/sql"
"encoding/hex"
"fmt"
@@ -20,17 +21,25 @@ func NewSessionStore(db *sql.DB, cfg *config.Config) *SessionStore {
return &SessionStore{db: db, cfg: cfg}
}
// hashToken computes a SHA-256 hash of a token string.
// Tokens are stored hashed in the DB so a DB leak doesn't expose active sessions.
func hashToken(token string) string {
h := sha256.Sum256([]byte(token))
return hex.EncodeToString(h[:])
}
func (s *SessionStore) Create(ctx context.Context, userID string) (string, error) {
b := make([]byte, 32)
if _, err := rand.Read(b); err != nil {
return "", fmt.Errorf("generate token: %w", err)
}
token := hex.EncodeToString(b)
tokenHash := hashToken(token)
_, err := s.db.ExecContext(ctx, `
INSERT INTO sessions (user_id, token, expires_at)
VALUES ($1, $2, $3)
`, userID, token, time.Now().Add(s.cfg.Session.Duration))
`, userID, tokenHash, time.Now().Add(s.cfg.Session.Duration))
if err != nil {
return "", fmt.Errorf("insert session: %w", err)
}
@@ -43,7 +52,7 @@ func (s *SessionStore) GetUserIDByToken(ctx context.Context, token string) (stri
err := s.db.QueryRowContext(ctx, `
SELECT user_id FROM sessions
WHERE token = $1 AND expires_at > NOW()
`, token).Scan(&userID)
`, hashToken(token)).Scan(&userID)
if err != nil {
return "", err
}
@@ -51,6 +60,6 @@ func (s *SessionStore) GetUserIDByToken(ctx context.Context, token string) (stri
}
func (s *SessionStore) Delete(ctx context.Context, token string) error {
_, err := s.db.ExecContext(ctx, `DELETE FROM sessions WHERE token = $1`, token)
_, err := s.db.ExecContext(ctx, `DELETE FROM sessions WHERE token = $1`, hashToken(token))
return err
}
+6
View File
@@ -4,6 +4,7 @@ import (
"context"
"database/sql"
"fmt"
"time"
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/config"
@@ -24,6 +25,11 @@ func New(cfg *config.Config) (*DB, error) {
return nil, fmt.Errorf("ping database: %w", err)
}
// ponytail: prevent unbounded connection growth under burst load
db.SetMaxOpenConns(25)
db.SetMaxIdleConns(5)
db.SetConnMaxLifetime(15 * time.Minute)
return &DB{db}, nil
}
+25 -1
View File
@@ -4,6 +4,7 @@ import (
"net/http"
"strings"
"sync"
"time"
"golang.org/x/time/rate"
)
@@ -12,16 +13,21 @@ import (
type ipLimiter struct {
mu sync.Mutex
limiters map[string]*rate.Limiter
lastUsed map[string]time.Time
rate rate.Limit
burst int
}
func newIPLimiter(r rate.Limit, burst int) *ipLimiter {
return &ipLimiter{
l := &ipLimiter{
limiters: make(map[string]*rate.Limiter),
lastUsed: make(map[string]time.Time),
rate: r,
burst: burst,
}
// ponytail: evict inactive limiters every 5 minutes to prevent unbounded map growth
go l.cleanup()
return l
}
func (l *ipLimiter) getLimiter(ip string) *rate.Limiter {
@@ -33,9 +39,27 @@ func (l *ipLimiter) getLimiter(ip string) *rate.Limiter {
lim = rate.NewLimiter(l.rate, l.burst)
l.limiters[ip] = lim
}
l.lastUsed[ip] = time.Now()
return lim
}
// cleanup runs in a background goroutine and removes limiters that haven't
// been accessed in 10 minutes.
func (l *ipLimiter) cleanup() {
for {
time.Sleep(5 * time.Minute)
l.mu.Lock()
now := time.Now()
for ip, last := range l.lastUsed {
if now.Sub(last) > 10*time.Minute {
delete(l.limiters, ip)
delete(l.lastUsed, ip)
}
}
l.mu.Unlock()
}
}
// RateLimit returns middleware that limits requests per IP.
// requestsPerSecond is the sustained rate, burst is the max burst size.
func RateLimit(requestsPerSecond float64, burst int) func(http.Handler) http.Handler {
+2 -2
View File
@@ -1,7 +1,7 @@
{
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
"productName": "dumpsterChat",
"version": "0.2.9",
"version": "0.2.10",
"identifier": "coffee.dustin.dumpster",
"build": {
"frontendDist": "../dist",
@@ -23,7 +23,7 @@
}
],
"security": {
"csp": null
"csp": "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self' wss: https:;"
}
},
"bundle": {
+3 -3
View File
@@ -292,6 +292,7 @@ export function ChatArea() {
);
const isLoading = useMessageStore((s) => s.isLoading);
const isLoadingOlder = useMessageStore((s) => s.isLoadingOlder);
const hasMore = useMessageStore((s) => activeChannelId ? s.hasMoreByChannel[activeChannelId] !== false : true);
const fetchMessages = useMessageStore((s) => s.fetchMessages);
const fetchOlderMessages = useMessageStore((s) => s.fetchOlderMessages);
const sendMessage = useMessageStore((s) => s.sendMessage);
@@ -447,17 +448,16 @@ export function ChatArea() {
const handleScroll = useCallback(() => {
const el = scrollContainerRef.current;
if (!el || !activeChannelId || isLoadingOlder) return;
if (!el || !activeChannelId || isLoadingOlder || !hasMore) return;
if (el.scrollTop < 100) {
const prevHeight = el.scrollHeight;
fetchOlderMessages(activeChannelId).then(() => {
// ponytail: maintain scroll position after prepending older messages
requestAnimationFrame(() => {
el.scrollTop = el.scrollHeight - prevHeight;
});
});
}
}, [activeChannelId, isLoadingOlder, fetchOlderMessages]);
}, [activeChannelId, isLoadingOlder, hasMore, fetchOlderMessages]);
useEffect(() => {
bottomRef.current?.scrollIntoView({ behavior: "auto" });
+6 -6
View File
@@ -1,4 +1,4 @@
import { useEffect, useRef, useState, useCallback, memo } from "react";
import { useEffect, useRef, useState, useCallback, memo, Fragment } from "react";
import { useParams } from "react-router-dom";
import { useConversationStore, type ConversationMessage } from "../stores/conversation.ts";
import { useAuthStore } from "../stores/auth.ts";
@@ -175,6 +175,7 @@ export function DMChat() {
const id = conversationId || activeId;
const conversation = conversations.find((c) => c.id === id);
const hasMore = useConversationStore((s) => id ? s.hasMoreByConversation[id] !== false : true);
const handleAddReaction = useCallback(async (messageId: string, emoji: string) => {
if (!id) return;
@@ -189,7 +190,7 @@ export function DMChat() {
const handleScroll = useCallback(() => {
const el = scrollContainerRef.current;
if (!el || !id || isLoadingOlder) return;
if (!el || !id || isLoadingOlder || !hasMore) return;
if (el.scrollTop < 100) {
const prevHeight = el.scrollHeight;
fetchOlderMessages(id).then(() => {
@@ -198,7 +199,7 @@ export function DMChat() {
});
});
}
}, [id, isLoadingOlder, fetchOlderMessages]);
}, [id, isLoadingOlder, hasMore, fetchOlderMessages]);
const messages = id ? messagesByConv[id] || [] : [];
useEffect(() => {
@@ -329,9 +330,8 @@ export function DMChat() {
const lastReadId = id ? convStates[id] : undefined;
const showDivider = lastReadId && msg.id === lastReadId && i < messages.length - 1;
return (
<>
<Fragment key={msg.id}>
<DMMessageItem
key={msg.id}
msg={msg}
onAddReaction={handleAddReaction}
activeReactionMessageId={activeReactionMessageId}
@@ -348,7 +348,7 @@ export function DMChat() {
<span className="flex-1 border-t border-gb-red"></span>
</div>
)}
</>
</Fragment>
);
})}
<div ref={bottomRef} />
+4 -4
View File
@@ -117,7 +117,7 @@ export function LoginForm() {
</button>
</form>
{!isRegister && (
<div className="mt-3 text-center">
<div className="mt-4 text-center">
<Link
to="/forgot-password"
className="text-gb-yellow hover:text-gb-orange text-xs"
@@ -127,7 +127,7 @@ export function LoginForm() {
</div>
)}
{!isRegister && (
<div className="mt-3">
<div className="mt-4">
<button
type="button"
onClick={async () => {
@@ -157,13 +157,13 @@ export function LoginForm() {
console.error("Passkey login failed:", err);
}
}}
className="terminal-button w-full border-gb-aqua text-gb-aqua"
className="terminal-button w-full border-gb-aqua text-gb-aqua text-xs"
>
[SIGN IN WITH PASSKEY]
</button>
</div>
)}
<div className="mt-4 text-center">
<div className="mt-5 text-center">
<button
type="button"
onClick={() => {
+26 -26
View File
@@ -155,7 +155,7 @@ function markdownToHtml(md: string): string {
}
function escapeHtml(s: string): string {
return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
}
function inlineMarkdownToHtml(text: string): string {
@@ -610,8 +610,8 @@ export const MessageInput = forwardRef<HTMLTextAreaElement, MessageInputProps>(f
<button type="button" disabled={disabled || uploading}
onClick={() => fileInputRef.current?.click()}
title="Upload file"
className="text-gb-fg-f hover:text-gb-orange p-1 disabled:opacity-50">
<FontAwesomeIcon icon={faPlus} className={`w-3.5 h-3.5 ${uploading ? "animate-pulse" : ""}`} />
className="text-gb-fg-f hover:text-gb-orange p-1.5 disabled:opacity-50">
<FontAwesomeIcon icon={faPlus} className={`w-4 h-4 ${uploading ? "animate-pulse" : ""}`} />
</button>
<span className="text-gb-bg-t mx-1"></span>
@@ -619,28 +619,28 @@ export const MessageInput = forwardRef<HTMLTextAreaElement, MessageInputProps>(f
{/* block formatting */}
<button type="button" disabled={disabled}
onClick={() => execBlock("ul")} title="Unordered list"
className="text-gb-fg-f hover:text-gb-orange p-1 disabled:opacity-50">
<FontAwesomeIcon icon={faListUl} className="w-3 h-3" />
className="text-gb-fg-f hover:text-gb-orange p-1.5 disabled:opacity-50">
<FontAwesomeIcon icon={faListUl} className="w-4 h-4" />
</button>
<button type="button" disabled={disabled}
onClick={() => execBlock("ol")} title="Ordered list"
className="text-gb-fg-f hover:text-gb-orange p-1 disabled:opacity-50">
<FontAwesomeIcon icon={faListOl} className="w-3 h-3" />
className="text-gb-fg-f hover:text-gb-orange p-1.5 disabled:opacity-50">
<FontAwesomeIcon icon={faListOl} className="w-4 h-4" />
</button>
<button type="button" disabled={disabled}
onClick={() => execBlock("blockquote")} title="Blockquote"
className="text-gb-fg-f hover:text-gb-orange p-1 disabled:opacity-50">
<FontAwesomeIcon icon={faQuoteRight} className="w-3 h-3" />
className="text-gb-fg-f hover:text-gb-orange p-1.5 disabled:opacity-50">
<FontAwesomeIcon icon={faQuoteRight} className="w-4 h-4" />
</button>
<button type="button" disabled={disabled}
onClick={execLink} title="Insert link"
className="text-gb-fg-f hover:text-gb-orange p-1 disabled:opacity-50">
<FontAwesomeIcon icon={faLink} className="w-3 h-3" />
className="text-gb-fg-f hover:text-gb-orange p-1.5 disabled:opacity-50">
<FontAwesomeIcon icon={faLink} className="w-4 h-4" />
</button>
<button type="button" disabled={disabled}
onClick={() => execBlock("h2")} title="Heading"
className="text-gb-fg-f hover:text-gb-orange p-1 disabled:opacity-50">
<FontAwesomeIcon icon={faHeading} className="w-3 h-3" />
className="text-gb-fg-f hover:text-gb-orange p-1.5 disabled:opacity-50">
<FontAwesomeIcon icon={faHeading} className="w-4 h-4" />
</button>
<span className="text-gb-bg-t mx-1"></span>
@@ -648,43 +648,43 @@ export const MessageInput = forwardRef<HTMLTextAreaElement, MessageInputProps>(f
{/* inline formatting */}
<button type="button" disabled={disabled}
onClick={() => execInline("**", "**")} title="Bold"
className="text-gb-fg-f hover:text-gb-orange p-1 disabled:opacity-50">
<FontAwesomeIcon icon={faBold} className="w-3 h-3" />
className="text-gb-fg-f hover:text-gb-orange p-1.5 disabled:opacity-50">
<FontAwesomeIcon icon={faBold} className="w-4 h-4" />
</button>
<button type="button" disabled={disabled}
onClick={() => execInline("*", "*")} title="Italic"
className="text-gb-fg-f hover:text-gb-orange p-1 disabled:opacity-50">
<FontAwesomeIcon icon={faItalic} className="w-3 h-3" />
className="text-gb-fg-f hover:text-gb-orange p-1.5 disabled:opacity-50">
<FontAwesomeIcon icon={faItalic} className="w-4 h-4" />
</button>
<button type="button" disabled={disabled}
onClick={() => execInline("~~", "~~")} title="Strikethrough"
className="text-gb-fg-f hover:text-gb-orange p-1 disabled:opacity-50">
<FontAwesomeIcon icon={faStrikethrough} className="w-3 h-3" />
className="text-gb-fg-f hover:text-gb-orange p-1.5 disabled:opacity-50">
<FontAwesomeIcon icon={faStrikethrough} className="w-4 h-4" />
</button>
<button type="button" disabled={disabled}
onClick={() => execInline("`", "`")} title="Code"
className="text-gb-fg-f hover:text-gb-orange p-1 disabled:opacity-50">
<FontAwesomeIcon icon={faCode} className="w-3 h-3" />
className="text-gb-fg-f hover:text-gb-orange p-1.5 disabled:opacity-50">
<FontAwesomeIcon icon={faCode} className="w-4 h-4" />
</button>
<button type="button" disabled={disabled}
onClick={() => execInline("||", "||")} title="Spoiler"
className="text-gb-fg-f hover:text-gb-orange p-1 disabled:opacity-50">
<FontAwesomeIcon icon={faEyeSlash} className="w-3 h-3" />
className="text-gb-fg-f hover:text-gb-orange p-1.5 disabled:opacity-50">
<FontAwesomeIcon icon={faEyeSlash} className="w-4 h-4" />
</button>
<span className="text-gb-bg-t mx-1"></span>
{/* emoji / kaomoji / gif */}
<button type="button" disabled={disabled} onClick={toggleEmoji} title="Emoji"
className={`p-1 disabled:opacity-50 ${showEmoji ? "text-gb-orange" : "text-gb-fg-f hover:text-gb-orange"}`}>
className={`p-1.5 disabled:opacity-50 ${showEmoji ? "text-gb-orange" : "text-gb-fg-f hover:text-gb-orange"}`}>
<FontAwesomeIcon icon={faSmile} className="w-4 h-4" />
</button>
<button type="button" disabled={disabled} onClick={toggleKaomoji} title="Kaomoji"
className={`p-1 disabled:opacity-50 ${showKaomoji ? "text-gb-orange" : "text-gb-fg-f hover:text-gb-orange"}`}>
className={`p-1.5 disabled:opacity-50 ${showKaomoji ? "text-gb-orange" : "text-gb-fg-f hover:text-gb-orange"}`}>
<FontAwesomeIcon icon={faGrin} className="w-4 h-4" />
</button>
<button type="button" disabled={disabled} onClick={toggleGif} title="GIF"
className={`p-1 disabled:opacity-50 ${showGif ? "text-gb-orange" : "text-gb-fg-f hover:text-gb-orange"}`}>
className={`p-1.5 disabled:opacity-50 ${showGif ? "text-gb-orange" : "text-gb-fg-f hover:text-gb-orange"}`}>
<FontAwesomeIcon icon={faFilm} className="w-4 h-4" />
</button>
+10 -1
View File
@@ -1,4 +1,5 @@
import { useState, useMemo } from "react";
import { useNavigate } from "react-router-dom";
import { useConversationStore } from "../stores/conversation.ts";
import { useMemberStore } from "../stores/member.ts";
import { useServerStore } from "../stores/server.ts";
@@ -11,6 +12,7 @@ interface NewConversationModalProps {
export function NewConversationModal({ onClose }: NewConversationModalProps) {
const [query, setQuery] = useState("");
const [selected, setSelected] = useState<string[]>([]);
const navigate = useNavigate();
const createConversation = useConversationStore((s) => s.createConversation);
const activeServerId = useServerStore((s) => s.activeServerId);
const membersByServer = useMemberStore((s) => s.membersByServer);
@@ -48,7 +50,14 @@ export function NewConversationModal({ onClose }: NewConversationModalProps) {
const handleCreate = async () => {
if (selected.length === 0) return;
await createConversation(selected);
try {
const conv = await createConversation(selected);
if (conv?.id) {
navigate(`/dm/${conv.id}`);
}
} catch (err) {
console.error("Failed to create conversation:", err);
}
onClose();
};
+1 -1
View File
@@ -8,7 +8,7 @@ if (isTauri) {
// WebView safe-area fallbacks: Android doesn't support CSS env(), and
// Linux WebKitGTK chokes on env() inside var(). Set explicit values via JS.
if (navigator.userAgent.includes('Android')) {
document.documentElement.style.setProperty('--safe-top', '1.5rem');
document.documentElement.style.setProperty('--safe-top', '2rem');
document.documentElement.style.setProperty('--safe-bottom', '0.75rem');
document.documentElement.style.setProperty('--safe-left', '0px');
document.documentElement.style.setProperty('--safe-right', '0px');
+46 -23
View File
@@ -2,6 +2,13 @@ import { create } from "zustand";
import { api } from "../lib/api.ts";
import { type Reaction } from "./message.ts";
function parseDate(iso: string): number {
if (!iso) return 0;
const normalized = iso.includes("T") ? iso : iso.replace(" ", "T");
const t = new Date(normalized).getTime();
return isNaN(t) ? 0 : t;
}
export interface ConversationMember {
id: string;
username: string;
@@ -89,17 +96,26 @@ export const useConversationStore = create<ConversationState>((set, get) => ({
`/conversations/${conversationId}/messages${params}`,
);
const list = Array.isArray(messages) ? messages : [];
set((state) => ({
messagesByConversation: {
...state.messagesByConversation,
[conversationId]: list,
},
hasMoreByConversation: {
...state.hasMoreByConversation,
[conversationId]: list.length >= 50,
},
isLoading: false,
}));
set((state) => {
const existing = state.messagesByConversation[conversationId] || [];
const map = new Map<string, ConversationMessage>();
existing.forEach((m) => map.set(m.id, m));
list.forEach((m) => map.set(m.id, m));
const merged = Array.from(map.values()).sort(
(a, b) => parseDate(a.created_at) - parseDate(b.created_at)
);
return {
messagesByConversation: {
...state.messagesByConversation,
[conversationId]: merged,
},
hasMoreByConversation: {
...state.hasMoreByConversation,
[conversationId]: list.length >= 50,
},
isLoading: false,
};
});
} catch (error) {
set({ isLoading: false, error: error instanceof Error ? error.message : "Failed" });
}
@@ -118,17 +134,24 @@ export const useConversationStore = create<ConversationState>((set, get) => ({
`/conversations/${conversationId}/messages?before=${encodeURIComponent(oldestId)}`,
);
const list = Array.isArray(older) ? older : [];
set((state) => ({
messagesByConversation: {
...state.messagesByConversation,
[conversationId]: [...list, ...existing],
},
hasMoreByConversation: {
...state.hasMoreByConversation,
[conversationId]: list.length >= 50,
},
isLoadingOlder: false,
}));
set((state) => {
const map = new Map<string, ConversationMessage>();
[...list, ...existing].forEach((m) => map.set(m.id, m));
const merged = Array.from(map.values()).sort(
(a, b) => parseDate(a.created_at) - parseDate(b.created_at)
);
return {
messagesByConversation: {
...state.messagesByConversation,
[conversationId]: merged,
},
hasMoreByConversation: {
...state.hasMoreByConversation,
[conversationId]: list.length >= 50,
},
isLoadingOlder: false,
};
});
} catch {
set({ isLoadingOlder: false });
}
@@ -156,7 +179,7 @@ export const useConversationStore = create<ConversationState>((set, get) => ({
messagesByConversation: {
...state.messagesByConversation,
[message.conversation_id]: [...existing, message]
.sort((a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime()),
.sort((a, b) => parseDate(a.created_at) - parseDate(b.created_at)),
},
};
});