Compare commits

..

134 Commits

Author SHA1 Message Date
hobokenchicken 7cdee73542 fix(tauri): unregister and prevent service worker to fix 404 cache routing issues
Release Desktop Apps / build-linux (push) Successful in 3m17s
Release Desktop Apps / build-windows (push) Successful in 3h17m8s
Release Desktop Apps / release (push) Successful in 13s
2026-07-16 13:58:06 -04:00
hobokenchicken e8ba8ffdba fix(tauri): redirect relative fetch and websocket urls to production backend for desktop app
Release Desktop Apps / build-linux (push) Successful in 3m2s
Release Desktop Apps / build-windows (push) Successful in 3h24m0s
Release Desktop Apps / release (push) Successful in 10s
2026-07-16 13:27:33 -04:00
hobokenchicken 52298d1d46 fix(ci): manually configure caching using actions/cache@v3 for gitea compatibility
Release Desktop Apps / build-linux (push) Successful in 10m35s
Release Desktop Apps / build-windows (push) Successful in 3h16m21s
Release Desktop Apps / release (push) Successful in 11s
2026-07-16 13:03:56 -04:00
hobokenchicken 7fc5d66b8c fix(ci): recursively glob artifact files to prevent empty releases
Release Desktop Apps / build-linux (push) Failing after 11m16s
Release Desktop Apps / release (push) Has been cancelled
Release Desktop Apps / build-windows (push) Has been cancelled
2026-07-16 12:47:45 -04:00
hobokenchicken 7a6b4f961a fix(ci): correct rustup download URL for windows
Release Desktop Apps / build-linux (push) Successful in 2m45s
Release Desktop Apps / build-windows (push) Successful in 3h21m29s
Release Desktop Apps / release (push) Successful in 10s
2026-07-16 12:22:18 -04:00
hobokenchicken 71ee9c59c4 fix(ci): use valid release action (softprops/action-gh-release)
Release Desktop Apps / build-windows (push) Failing after 3h0m44s
Release Desktop Apps / build-linux (push) Successful in 2m42s
Release Desktop Apps / release (push) Has been skipped
2026-07-16 12:17:30 -04:00
hobokenchicken 3e343a9c9b chore(ci): add rust and npm caching to speed up builds
Release Desktop Apps / build-linux (push) Successful in 5m29s
Release Desktop Apps / build-windows (push) Successful in 3h31m6s
Release Desktop Apps / release (push) Failing after 3s
2026-07-16 11:02:23 -04:00
hobokenchicken 9371616508 fix(ci): downgrade artifact actions to v3 for gitea compatibility 2026-07-16 10:46:54 -04:00
hobokenchicken 5951c91102 fix(ci): use curl and CI=true to prevent Windows runner hang 2026-07-16 10:33:46 -04:00
hobokenchicken ff431d7f81 added rpm to apt get list 2026-07-16 14:08:08 +00:00
hobokenchicken aa5fdbe8c4 fix: message area scroll — add min-h-0 to flex column 2026-07-16 09:23:08 -04:00
hobokenchicken 4da08d91bc 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 11b1089126 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 fd7fa4a147 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 7bf1eaf845 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 13bd4478f6 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 53530ce6dd 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 f215f000b8 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 191fe2a89f docs: update README for bot store, built-in runner, steamfree 2026-07-15 15:40:22 -04:00
hobokenchicken eb5f38de1c 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 51eb2ed310 chore: add steamfree binary to gitignore 2026-07-15 14:21:51 -04:00
hobokenchicken bd73d79b56 chore: remove committed binary 2026-07-15 14:16:57 -04:00
hobokenchicken 4b32655e67 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 c839e67c47 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 5bdb758d23 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 56af584ede 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 87d7345155 feat(ui): character counter in message compose box 2026-07-09 16:00:28 -04:00
hobokenchicken a7646481a4 fix(ui): textarea auto-expands instead of scrolling 2026-07-09 15:55:00 -04:00
hobokenchicken 5d37fb899d feat(calendar): weekly list view with grid toggle, defaults to list 2026-07-09 15:13:40 -04:00
hobokenchicken af913b9923 fix(calendar): use r.Route subrouter to avoid chi path ambiguity with /channels/events/{eventID} 2026-07-09 15:02:40 -04:00
hobokenchicken 9a6f15662b fix(voice): add reconnect/disconnect event handlers + connection quality logging 2026-07-09 13:15:21 -04:00
hobokenchicken 7d1bd02e31 fix(voice): AudioRenderers listens to room events directly, not zustand 2026-07-09 13:08:02 -04:00
hobokenchicken 8542243745 fix(voice): remove unused import 2026-07-09 13:03:07 -04:00
hobokenchicken 90bddc65d4 fix(voice): handle browser autoplay block — flush pending audio on interaction 2026-07-09 13:02:40 -04:00
hobokenchicken 67a5a54244 fix(voice): simplify RemoteAudioTrack — remove stale listener race 2026-07-09 13:01:49 -04:00
hobokenchicken 33c9dc4f15 fix(voice): AudioRenderers re-renders on participant join 2026-07-09 12:55:36 -04:00
hobokenchicken e6dfe43926 fix(voice): broadcast VOICE_JOIN/LEAVE via ws, show participants in sidebar for all users 2026-07-09 12:47:42 -04:00
hobokenchicken 62e8354d03 feat(voice): persist A/V device preferences in localStorage 2026-07-09 12:36:43 -04:00
hobokenchicken 1139e90fc3 fix: use powershell -Command for download, keep shell:cmd for execution
Release Desktop Apps / build-linux (push) Failing after 3m29s
Release Desktop Apps / release (push) Has been cancelled
Release Desktop Apps / build-windows (push) Has been cancelled
2026-07-07 19:46:27 +00:00
hobokenchicken 60edc0b5b4 fix: Windows build — use cmd shell to avoid PowerShell execution policy
Release Desktop Apps / build-windows (push) Failing after 3h22m14s
Release Desktop Apps / build-linux (push) Has been cancelled
Release Desktop Apps / release (push) Has been cancelled
2026-07-07 19:19:55 +00:00
hobokenchicken 836bcd8b8d fix: use powershell instead of pwsh for Windows runner
Release Desktop Apps / build-windows (push) Failing after 3h0m34s
Release Desktop Apps / build-linux (push) Has been cancelled
Release Desktop Apps / release (push) Has been cancelled
2026-07-07 19:18:33 +00:00
hobokenchicken 8fd649356c fix: windows build — use pwsh instead of bash for Rust install 2026-07-07 19:17:42 +00:00
hobokenchicken 4a02d054cc fix: release workflow for Gitea Actions (runner labels + gitea-release-action)
Release Desktop Apps / build-windows (push) Failing after 3h0m44s
Release Desktop Apps / build-linux (push) Has been cancelled
Release Desktop Apps / release (push) Has been cancelled
2026-07-07 14:19:22 -04:00
hobokenchicken dbd4de8e61 bump: desktop app v0.2.0, build all Linux bundles (appimage, deb, rpm) 2026-07-07 10:20:27 -04:00
hobokenchicken a0bb229173 fix: DMs now show full date+time like channels [DD.MM.YYYY @ HH:MM]
Release Desktop Apps / build-windows (push) Failing after 3h0m23s
Release Desktop Apps / build-linux (push) Has been cancelled
Release Desktop Apps / release (push) Has been cancelled
2026-07-07 14:01:27 +00:00
hobokenchicken c86c602294 change upload limit from 25MB to 1GB 2026-07-07 13:43:41 +00:00
hobokenchicken aa39b6a5be feat: read indicator — NEW divider line between read and unread messages 2026-07-06 19:41:19 +00:00
hobokenchicken 8cf1e91b2b 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 f068e15398 build: add build-tauri target (NO_STRIP=true for Arch) 2026-07-06 14:32:16 -04:00
hobokenchicken 559d17d1f2 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 ea72050202 docs: add formatting toolbar, DM kaomoji/GIFs, last-channel restore to features list 2026-07-06 14:04:48 -04:00
hobokenchicken 88194b17c4 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 2b45b11ea8 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 b3b5ff495d 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 9512dedec1 feat(chat): add native emoji pickers for chat input and reactions 2026-07-06 13:59:20 +00:00
hobokenchicken 8bbe2a0ff0 fix(dm): remove local append on send to prevent double messages 2026-07-06 13:53:11 +00:00
hobokenchicken c2fb3165f4 fix(dm): add image paste support to DM chat and fix realtime DM updates 2026-07-06 13:44:41 +00:00
hobokenchicken 0c26e2402b feat(chat): support pasting images directly into the chat box 2026-07-06 13:38:02 +00:00
hobokenchicken 88e7faaafb fix(webrtc): fix typescript errors for participant list migration 2026-07-06 13:32:10 +00:00
hobokenchicken 1d37284c25 fix(permissions): wire up MUTE_MEMBERS bitmask and fix member permissions in frontend 2026-07-06 13:27:38 +00:00
hobokenchicken b1b41ca5e1 fix(webrtc): add error handling and ui alerts for background processor initialization 2026-07-06 13:16:01 +00:00
hobokenchicken e10cb22811 fix(webrtc): update CSP to allow background processor wasm and blob execution 2026-07-06 13:14:27 +00:00
hobokenchicken 2631e6ce4a fix(webrtc): fix invalid hook call in device settings modal for bgMode 2026-07-06 13:13:05 +00:00
hobokenchicken 239640975f feat(webrtc): add background blur and virtual background options 2026-07-06 13:08:59 +00:00
hobokenchicken ed408c4044 feat(webrtc): add admin option to mute other participants 2026-07-06 13:04:39 +00:00
hobokenchicken 9aeedb1a53 feat(webrtc): add UI to select audio/video input and output devices 2026-07-06 12:56:27 +00:00
hobokenchicken 59846fc1e4 chore(build): automatically embed git commit hash in version string 2026-07-06 12:52:20 +00:00
hobokenchicken 944ae4074e fix(webrtc): sync participants state when local user publishes tracks 2026-07-06 12:49:14 +00:00
hobokenchicken 549bf842d0 feat(ui): tabulate voice and chat panels to prevent overlapping 2026-07-06 12:47:23 +00:00
hobokenchicken 83c05731ed fix(webrtc): correctly subscribe to remote camera tracks 2026-07-06 12:40:26 +00:00
hobokenchicken 5d8f420882 fix(webrtc): fix VideoGrid reactivity and layout 2026-07-06 12:37:35 +00:00
hobokenchicken 074742ddb4 feat(webrtc): render remote audio tracks and allow camera access 2026-07-06 12:25:40 +00:00
hobokenchicken f9b1e16e3a fix(webrtc): configure external TURN server and update CSP to allow inline scripts 2026-07-06 12:20:53 +00:00
hobokenchicken f9edd8c069 fix(docker): update livekit schema, coturn ports, and proxy keys 2026-07-05 23:59:32 +00:00
hobokenchicken 7e1c0b822b 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 f61b33ff81 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 ed50815902 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 b4e2f97dba 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 53f8c6b2ef 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 d1022d7c2a 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 15f9f6ae07 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 c803991cda 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 a502cd8fd4 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 a593f04d7f 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 6b7dc2addd chore: remove committed keygen binary, gitignore 2026-07-02 14:24:30 -04:00
hobokenchicken 8336ca5d87 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 85f9c21f4c 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 92af2e21b1 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 1e22daead3 fix: remove unused activeChannelId variable 2026-07-02 14:00:12 -04:00
hobokenchicken 9e70884a16 fix: use ChannelStore for activeChannelId subscription
activeChannelId lives on ChannelStore, not ServerStore.
2026-07-02 13:59:27 -04:00
hobokenchicken b6d4614881 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 0b645fe765 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 f6f63ecd2b 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 f30ddb1323 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 5ca5e018ca 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 3002626ac4 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 4ed2e5bfd0 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 aa80e354c9 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 cf2f1c96a3 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 40f8d193ff 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 d8b4defaff 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 722eab8e94 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 19ac68d058 feat: Ctrl+E keyboard shortcut for kaomoji picker in message input 2026-07-02 12:13:00 -04:00
hobokenchicken 3a04eff42b 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 59f6a40882 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 5a7d4dc57e 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 89f8381e2d added features and fixes 2026-07-02 15:34:00 +00:00
hobokenchicken eb5b7d55a4 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 7d67d6f360 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 08dfc4e400 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 cbfbcb2627 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 ca7a9e30f1 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 10a702f732 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 af961d805e 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 7f2a58599d 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 67ce6bbfd9 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 c0686d4ca3 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 74a1db8392 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 5cc9d76394 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 3aa1ca3ead 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 3ed66f3d43 debug: add error surfacing for notes-to-self creation 2026-07-02 08:40:07 -04:00
hobokenchicken a5c3b25cc0 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 17c09edc4f 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 d0da35e816 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 31cb295a24 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 ecb26cb731 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 cb8aeddde6 fix(pwa): generate icons, network-first HTML, bust stale cache 2026-07-01 16:14:29 -04:00
hobokenchicken 33dc63b94c fix: stop double-posting messages (WS + HTTP both appended) 2026-07-01 15:59:31 -04:00
hobokenchicken 48a99d58ee 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 c80a36bacb fix: move DUMPSTER banner back inside login card, centered 2026-06-30 15:56:45 -04:00
hobokenchicken 65de4efb0d fix: span DUMPSTER banner full-width above login card 2026-06-30 15:55:14 -04:00
hobokenchicken b8ae43e250 fix: center DUMPSTER and LOGIN headings on login screen 2026-06-30 15:53:04 -04:00
hobokenchicken 82ddf914e4 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 f4437c5e1d feat: add SMTP email support and fix profile/permissions bugs 2026-06-30 19:29:08 +00:00
94 changed files with 163 additions and 1820 deletions
-8
View File
@@ -1,8 +0,0 @@
.git
node_modules
web/dist
certs
*.zip
dumpster-server
.env
.env.local
+24 -22
View File
@@ -1,23 +1,25 @@
# 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
# App
DUMPSTER_HOST=localhost
DUMPSTER_PORT=8080
DUMPSTER_SECRET=*** Database
POSTGRES_HOST=localhost
POSTGRES_PORT=5432
POSTGRES_USER=dumpster
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=
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
+6 -3
View File
@@ -61,10 +61,13 @@ jobs:
rustc --version
shell: cmd
- name: Cache Rust target and registry
uses: Swatinem/rust-cache@v2
uses: actions/cache@v3
with:
workspaces: |
web/src-tauri
path: |
~/.cargo/registry
~/.cargo/git
web/src-tauri/target
key: ${{ runner.os }}-cargo-${{ hashFiles('web/src-tauri/Cargo.lock') }}
- name: Build frontend
run: cd web && npm ci && npm run build
shell: cmd
-8
View File
@@ -42,11 +42,3 @@ minio_data/
/dumpster-server
keygen
steamfree
# Tauri Android keystore (contains signing passwords)
web/src-tauri/gen/android/keystore.properties
# Secrets and certificates
/certs/
cookie.txt
-10
View File
@@ -18,16 +18,6 @@ build-tui:
build-tauri:
cd web && NODE_ENV=development NO_STRIP=true npx tauri build --bundles appimage,deb,rpm
# Build signed Android APK
build-android:
cd web && ANDROID_HOME=$(HOME)/Android/Sdk NDK_HOME=$(HOME)/Android/Sdk/ndk/27.2.12479018 JAVA_HOME=/usr/lib/jvm/java-17-openjdk npx tauri android build --apk
@echo "APK: web/src-tauri/gen/android/app/build/outputs/apk/universal/release/app-universal-release.apk"
# Build signed Android AAB (Google Play bundle)
build-android-aab:
cd web && ANDROID_HOME=$(HOME)/Android/Sdk NDK_HOME=$(HOME)/Android/Sdk/ndk/27.2.12479018 JAVA_HOME=/usr/lib/jvm/java-17-openjdk npx tauri android build --aab
@echo "AAB: web/src-tauri/gen/android/app/build/outputs/bundle/universalRelease/app-universal-release.aab"
# Generate Swagger docs
docs:
~/go/bin/swag init -g cmd/server/main.go -o docs
+81
View File
@@ -0,0 +1,81 @@
-----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
@@ -0,0 +1,5 @@
-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQggnJOfQM8flEJFULr
8vvHwWRlXlJ1a0sI5Jw/Y6KRQeGhRANCAATB70bxIRjXyxdwR/3k56AkiFRKwApx
q/h7xNXK5/I5xZ+5+HCLMHjEmFOIUtUj87DpcPHsrAfJZcH19gQIt7Xl
-----END PRIVATE KEY-----
+2 -124
View File
@@ -37,7 +37,6 @@ import (
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/webhook"
"github.com/go-chi/chi/v5"
chimw "github.com/go-chi/chi/v5/middleware"
"github.com/go-chi/cors"
httpSwagger "github.com/swaggo/http-swagger"
)
@@ -71,7 +70,7 @@ func main() {
sessionStore := auth.NewSessionStore(database.DB, cfg)
// WebSocket origin allowlist
wsOrigins := []string{"https://" + cfg.Host, "http://tauri.localhost", "https://tauri.localhost", "tauri://localhost"}
wsOrigins := []string{"https://" + cfg.Host}
if cfg.Host == "localhost" {
wsOrigins = append(wsOrigins, "http://localhost:"+cfg.Port)
}
@@ -120,16 +119,6 @@ func main() {
memberHandler := server.NewMemberHandler(database.DB)
r := chi.NewRouter()
r.Use(cors.Handler(cors.Options{
AllowedOrigins: []string{"https://" + cfg.Host, "http://localhost:" + cfg.Port, "http://tauri.localhost", "https://tauri.localhost", "tauri://localhost"},
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"},
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"},
ExposedHeaders: []string{"Link", "X-Session-Token"},
AllowCredentials: true,
MaxAge: 300,
}))
r.Use(chimw.Logger)
r.Use(chimw.Recoverer)
r.Use(chimw.RequestID)
@@ -163,10 +152,7 @@ func main() {
r.Group(func(r chi.Router) {
r.Use(middleware.Session(sessionStore, cfg))
r.Use(middleware.RequireAuth)
r.Use(middleware.CSRFProtect(cfg.Host, cfg.Port, append(
strings.Split(os.Getenv("DUMPSTER_CSRF_ORIGINS"), ","),
"tauri://localhost", "http://tauri.localhost", "https://tauri.localhost",
)))
r.Use(middleware.CSRFProtect(cfg.Host, cfg.Port, strings.Split(os.Getenv("DUMPSTER_CSRF_ORIGINS"), ",")))
// Auth (protected: me, update profile)
authHandler.RegisterProtectedRoutes(r)
@@ -431,13 +417,6 @@ func main() {
))
})
// Privacy policy page (served before SPA catch-all)
r.HandleFunc("/privacy", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Header().Set("Cache-Control", "public, max-age=3600")
w.Write([]byte(privacyPage))
})
// Static file serving for production (SPA)
staticDir := "web/dist"
if _, err := os.Stat(staticDir); err == nil {
@@ -460,104 +439,3 @@ func main() {
os.Exit(1)
}
}
const privacyPage = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Privacy Policy — dumpsterChat</title>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
background: #1d2021; color: #ebdbb2; line-height: 1.7; padding: 2rem 1rem;
}
main { max-width: 720px; margin: 0 auto; }
h1 { font-size: 1.8rem; margin-bottom: 0.25rem; color: #fabd2f; }
.subtitle { color: #a89984; font-size: 0.85rem; margin-bottom: 2rem; }
h2 { font-size: 1.15rem; margin: 1.5rem 0 0.5rem; color: #83a598; }
p, li { margin-bottom: 0.6rem; }
ul { padding-left: 1.25rem; }
li { margin-bottom: 0.3rem; }
a { color: #8ec07c; }
.footer { margin-top: 2.5rem; padding-top: 1rem; border-top: 1px solid #3c3836; font-size: 0.8rem; color: #928374; }
.update { color: #928374; font-size: 0.8rem; margin-top: 1.5rem; }
</style>
</head>
<body>
<main>
<h1>Privacy Policy</h1>
<p class="subtitle"><strong>dumpsterChat</strong> — Last updated: July 17, 2026</p>
<h2>Overview</h2>
<p>dumpsterChat is a self-hosted messaging platform. This privacy policy describes how your data is handled when you use the app. Because dumpsterChat is <strong>self-hosted</strong>, your data is stored on the server instance you connect to, which is operated by the server owner — not by us.</p>
<h2>Data We Collect</h2>
<p>When you use dumpsterChat, the following data is stored on the server:</p>
<ul>
<li><strong>Account information:</strong> username, email address, avatar, and password hash (Argon2id, not reversible).</li>
<li><strong>Messages and content:</strong> text messages, reactions, uploaded files, voice activity metadata, and poll votes.</li>
<li><strong>Session data:</strong> login sessions stored in encrypted cookies.</li>
</ul>
<h2>How We Use Your Data</h2>
<p>Your data is used solely to operate the chat platform:</p>
<ul>
<li>Deliver messages and notifications to the intended recipients.</li>
<li>Sync read states and presence (online/offline) across your devices.</li>
<li>Provide moderation tools (kicks, bans, mutes) per server rules.</li>
</ul>
<h2>No Third-Party Analytics</h2>
<p>dumpsterChat does <strong>not</strong> include any analytics SDKs, tracking pixels, or telemetry. No usage data is sent to us or to any third party for advertising, profiling, or analytics purposes.</p>
<h2>Third-Party Integrations</h2>
<p>If enabled by the server owner, optional integrations may be used:</p>
<ul>
<li><strong>Giphy:</strong> GIF search queries are proxied through the server to Giphy's API. No user data is shared with Giphy.</li>
<li><strong>LiveKit:</strong> Voice and video calls use a self-hosted LiveKit server. Media streams are processed in real time and are not recorded or stored by default.</li>
</ul>
<p>These integrations are optional and controlled entirely by the server owner.</p>
<h2>Data Retention</h2>
<p>Data is retained for as long as the server owner maintains the database. You can delete your messages or account at any time through the app. Server owners may also set message retention limits. Uploaded files persist until explicitly removed.</p>
<h2>Your Rights</h2>
<p>Depending on your jurisdiction, you may have the right to:</p>
<ul>
<li>Request a copy of your stored data.</li>
<li>Delete your account and associated data.</li>
<li>Correct inaccurate personal information.</li>
</ul>
<p>To exercise these rights, contact the operator of the dumpsterChat instance you use, or use the account management tools available within the app.</p>
<h2>Account Deletion Requests</h2>
<p>To request deletion of your account and all associated data:</p>
<ul>
<li>Use the <strong>Delete Account</strong> option in your account settings within the app.</li>
<li>Or email <a href="mailto:account-deletion@dustin.coffee">account-deletion@dustin.coffee</a> from the email address associated with your account.</li>
</ul>
<p>We will process your request within 30 days. Deletion removes your account, messages, uploaded files, and all personal data from the server.</p>
<h2>Security</h2>
<p>We take reasonable measures to protect your data:</p>
<ul>
<li>Passwords are hashed with Argon2id.</li>
<li>Session tokens use httpOnly cookies.</li>
<li>All communications are encrypted over HTTPS and WSS where available.</li>
</ul>
<h2>Children's Privacy</h2>
<p>dumpsterChat is not directed at children under 13. We do not knowingly collect personal information from children.</p>
<h2>Changes to This Policy</h2>
<p>We may update this privacy policy from time to time. Changes will be posted at this URL. Continued use of the app after changes constitutes acceptance of the updated policy.</p>
<h2>Contact</h2>
<p>If you have questions about this privacy policy, contact the operator of the dumpsterChat instance you use, or open an issue on our project repository.</p>
<div class="update">This privacy policy applies to the dumpsterChat mobile app and web app. The specific data practices of each instance may vary based on the server operator's configuration.</div>
</main>
</body>
</html>`
+5
View File
@@ -0,0 +1,5 @@
# 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
Binary file not shown.
Binary file not shown.
+1 -24
View File
@@ -8,31 +8,8 @@ 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
# 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
echo "Deploy complete — $(date)"
+1 -3
View File
@@ -16,10 +16,8 @@ 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"]
+3 -22
View File
@@ -1,9 +1,5 @@
version: '3.8'
networks:
frontend:
backend:
services:
app:
build:
@@ -36,9 +32,6 @@ services:
condition: service_healthy
valkey:
condition: service_healthy
networks:
- frontend
- backend
postgres:
image: postgres:16
@@ -56,8 +49,6 @@ services:
interval: 5s
timeout: 5s
retries: 5
networks:
- backend
valkey:
image: valkey/valkey:8
@@ -71,8 +62,6 @@ services:
interval: 5s
timeout: 3s
retries: 5
networks:
- backend
minio:
image: minio/minio:latest
@@ -86,8 +75,6 @@ services:
- "9001:9001"
volumes:
- minio_data:/data
networks:
- backend
livekit:
image: livekit/livekit-server:latest
@@ -102,8 +89,6 @@ 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
@@ -114,8 +99,8 @@ services:
- "--max-port=50200"
- "--realm=${DUMPSTER_HOST:-localhost}"
- "--user=${LIVEKIT_API_KEY:-devkey}:${LIVEKIT_API_SECRET:-secret}"
- "--cert=/etc/letsencrypt/live/turn.dustin.coffee/fullchain.pem"
- "--pkey=/etc/letsencrypt/live/turn.dustin.coffee/privkey.pem"
- "--cert=/etc/coturn/certs/turn.crt"
- "--pkey=/etc/coturn/certs/turn.key"
- "--tls-listening-port=5349"
ports:
- "3478:3478/tcp"
@@ -124,9 +109,7 @@ services:
- "5349:5349/udp"
- "50101-50200:50101-50200/udp"
volumes:
- /etc/letsencrypt:/etc/letsencrypt:ro
networks:
- backend
- ./certs:/etc/coturn/certs:ro
caddy:
image: caddy:2
@@ -141,8 +124,6 @@ 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: "${LIVEKIT_API_KEY}"
credential: "${LIVEKIT_API_SECRET}"
username: V8S6EWOU1Lqp
credential: jQRJztgWbqDn9vjMUhHZg1ooxuhiBjhk
turn:
enabled: false
keys:
"${LIVEKIT_API_KEY}": "${LIVEKIT_API_SECRET}"
V8S6EWOU1Lqp: jQRJztgWbqDn9vjMUhHZg1ooxuhiBjhk
-369
View File
@@ -1,369 +0,0 @@
# Capacitor Android App — Implementation Plan
> **For Hermes:** Use subagent-driven-development skill to implement this plan task-by-task.
**Goal:** Wrap the existing dumpsterChat Vite/React PWA in a Capacitor shell and publish to Google Play.
**Architecture:** Capacitor loads the Vite build output as local assets in an Android WebView. `@capacitor/core` bridges native APIs (push, status bar, etc.). No UI rewrite — the web app IS the app.
**Tech Stack:** Vite, React 18, Capacitor 6, FCM (push), Gradle (Android build)
---
### Phase 1: Capacitor Init
#### Task 1: Add Capacitor dependencies
**Objective:** Install Capacitor core + CLI in the web project.
**Files:**
- Modify: `web/package.json`
**Steps:**
```bash
cd web
npm install @capacitor/core @capacitor/cli @capacitor/android
```
Then init Capacitor:
```bash
npx cap init "Dumpster Chat" "coffee.dustin.dumpster" --web-dir dist
```
This creates `capacitor.config.ts` at the web root.
**Verify:** `cat capacitor.config.ts` shows appId `coffee.dustin.dumpster`, webDir `dist`.
---
#### Task 2: Configure capacitor.config.ts
**Objective:** Set server URL for dev, configure Android-specific settings.
**Files:**
- Modify: `web/capacitor.config.ts`
**Content:**
```ts
import type { CapacitorConfig } from '@capacitor/cli';
const config: CapacitorConfig = {
appId: 'coffee.dustin.dumpster',
appName: 'Dumpster Chat',
webDir: 'dist',
server: {
// ponytail: no server.url — serve local assets. API calls go to absolute URL from api.ts.
androidScheme: 'https', // cookies work over https scheme in WebView
},
plugins: {
PushNotifications: {
presentationOptions: ['badge', 'sound', 'alert'],
},
},
};
export default config;
```
**Key decisions:**
- `androidScheme: 'https'` makes `credentials: 'include'` cookies work in the WebView (http scheme blocks them).
- No `server.url` — local assets load from the APK, not from the web. Faster, works offline.
---
#### Task 3: Add Android platform
**Objective:** Generate the native Android project.
**Files:**
- Create: `web/android/` (generated by Capacitor)
**Steps:**
```bash
cd web
npx cap add android
```
**Verify:** `ls web/android/app/src/main/AndroidManifest.xml` exists.
---
### Phase 2: API Client Fix
#### Task 4: Update API base URL for native
**Objective:** When running in Capacitor, API calls need an absolute URL (no origin in a WebView). Keep relative paths for web/PWA.
**Files:**
- Modify: `web/src/lib/api.ts`
**Changes:**
```ts
import { Capacitor } from '@capacitor/core';
// ponytail: single switch. native = absolute URL, web = relative (Caddy same-origin).
const API_BASE = Capacitor.isNativePlatform()
? 'https://dumpster.dustin.coffee/api/v1'
: '/api/v1';
```
The rest of the file stays unchanged. `Capacitor.isNativePlatform()` returns `false` in browsers and `true` in the Android WebView.
**Skipped:** `@capacitor/http` plugin. Not needed — `androidScheme: 'https'` + absolute URL + `credentials: 'include'` works. Add the HTTP plugin only if cookies break.
---
### Phase 3: Push Notifications (FCM)
This is the only non-trivial part. VAPID web push does not work in Android WebViews. Need FCM.
#### Task 5: Create Firebase project
**Objective:** Set up FCM credentials for native push.
**Steps (manual, one-time):**
1. Go to https://console.firebase.google.com
2. Create project (or use existing) named `dumpster-chat`
3. Add Android app with package name `coffee.dustin.dumpster`
4. Download `google-services.json` → place in `web/android/app/google-services.json`
5. In Firebase Console → Project Settings → Cloud Messaging → note the **Server Key** (legacy) or set up **Firebase Admin SDK** service account
**Verify:** `google-services.json` exists in `web/android/app/`.
---
#### Task 6: Add Capacitor Push Notifications plugin
**Objective:** Register for FCM token on Android, send it to the server.
**Files:**
- Modify: `web/package.json` (install plugin)
- Modify: `web/src/stores/push.ts` (add native branch)
**Install:**
```bash
cd web
npm install @capacitor/push-notifications
```
**Modify `push.ts`** — add a native registration path alongside the existing web push:
```ts
import { Capacitor } from '@capacitor/core';
// Existing web push subscribe stays as-is for PWA.
// Add native branch:
async function subscribeNative() {
const { PushNotifications } = await import('@capacitor/push-notifications');
const permStatus = await PushNotifications.requestPermissions();
if (permStatus.receive !== 'granted') return;
await PushNotifications.register();
// Server sends us the FCM token via this event
PushNotifications.addListener('registration', async (token) => {
await api.post('/push/subscribe', {
endpoint: 'fcm:' + token.value, // ponytail: prefix to distinguish from web push endpoints
keys: { p256dh: '', auth: '' }, // not used for FCM, but server expects the shape
});
});
PushNotifications.addListener('pushNotificationReceived', (notification) => {
// Foreground notification — show in-app toast or badge
// ponytail: handled by existing in-app notification system
});
}
```
Then in the existing `subscribe()` function, branch:
```ts
if (Capacitor.isNativePlatform()) {
return subscribeNative();
}
// ... existing web push logic
```
---
#### Task 7: Server-side FCM send support
**Objective:** When a push subscription's endpoint starts with `fcm:`, send via FCM HTTP v1 API instead of VAPID.
**Files:**
- Modify: `internal/push/handlers.go`
**Changes:**
1. In `Subscribe()`: detect `fcm:` prefix on endpoint, store differently (or store as-is, the prefix distinguishes it).
2. In the send functions (`Send`, `SendToUser`): check if subscription endpoint starts with `fcm:` → use Firebase Admin SDK to send.
**Install Go Firebase Admin:**
```bash
go get firebase.google.com/go/v4
```
**Pattern:**
```go
// ponytail: one if/else in the send loop. endpoint prefix = routing key.
if strings.HasPrefix(sub.Endpoint, "fcm:") {
token := strings.TrimPrefix(sub.Endpoint, "fcm:")
msg := &messaging.Message{
Token: token,
Notification: &messaging.Notification{
Title: title,
Body: body,
},
Data: map[string]string{"url": url},
}
_, err = fcmClient.Send(ctx, msg)
} else {
// existing VAPID webpush send
}
```
**Config:** Add `FIREBASE_CREDENTIALS_FILE` env var (path to service account JSON) to the systemd unit / Docker compose.
**Skipped:** Topic-based broadcast. Per-device tokens is fine for now. Add topics when channel count grows.
---
### Phase 4: Gradle / Build Config
#### Task 8: Configure Android build
**Objective:** Set minimum SDK, app icon, theme.
**Files:**
- Modify: `web/android/app/build.gradle`
- Modify: `web/android/app/src/main/res/values/strings.xml`
**Changes in `build.gradle`:**
```gradle
minSdkVersion = 24 // ponytail: Android 7+ covers 99% of Play Store. lower = more compat bugs.
```
**App name in `strings.xml`:**
```xml
<string name="app_name">Dumpster Chat</string>
```
**App icon:** Copy existing PWA icons into Android mipmap directories:
```bash
# Capacitor can sync icons automatically if placed at web/public/icon.png (1024x1024)
# or manually: web/android/app/src/main/res/mipmap-*/
npx cap assets generate # if a 1024x1024 source icon exists
```
---
### Phase 5: Build & Publish
#### Task 9: Sync and build debug APK
**Objective:** Verify the app runs on a real device or emulator.
**Steps:**
```bash
cd web
npm run build # builds Vite → dist/
npx cap sync android # copies dist/ into android/assets, syncs plugins
cd android
./gradlew assembleDebug
```
**Output:** `web/android/app/build/outputs/apk/debug/app-debug.apk`
**Verify:** Install on Android device:
```bash
adb install app-debug.apk
```
---
#### Task 10: Build signed release AAB for Play Store
**Objective:** Create a signed Android App Bundle (.aab) for Google Play upload.
**Steps:**
1. Generate keystore (one-time):
```bash
keytool -genkey -v -keystore dumpster-release.jks -keyalg RSA -keysize 2048 -validity 10000 -alias dumpster
```
Store `dumpster-release.jks` securely. Back it up. Lose it = can't update the app.
2. Add signing config to `web/android/app/build.gradle`:
```gradle
android {
signingConfigs {
release {
storeFile file('dumpster-release.jks')
storePassword System.getenv('KEYSTORE_PASSWORD')
keyAlias 'dumpster'
keyPassword System.getenv('KEY_PASSWORD')
}
}
buildTypes {
release {
signingConfig signingConfigs.release
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}
```
3. Build AAB:
```bash
cd web/android
KEYSTORE_PASSWORD=xxx KEY_PASSWORD=xxx ./gradlew bundleRelease
```
**Output:** `web/android/app/build/outputs/bundle/release/app-release.aab`
4. Upload to Google Play Console → your new developer account → Create app → Upload AAB.
---
#### Task 11: Clean up Tauri dependencies
**Objective:** Remove unused Tauri packages (pivoted away from Tauri).
**Files:**
- Modify: `web/package.json`
**Steps:**
```bash
cd web
npm uninstall @tauri-apps/api @tauri-apps/cli
```
---
### Summary
| Phase | What | Time estimate |
|-------|------|---------------|
| 1 | Capacitor init + Android platform | 10 min |
| 2 | API base URL native branch | 5 min |
| 3 | FCM push (plugin + server) | 1-2 hrs (incl. Firebase setup) |
| 4 | Gradle config / icons | 15 min |
| 5 | Build, test, publish | 30 min |
**Total:** ~2-3 hours end-to-end. Phase 3 is the only real work.
**Dependencies between tasks:**
- Tasks 1-3 sequential (Capacitor init)
- Task 4 independent of 5-7
- Tasks 5-7 sequential (FCM chain)
- Task 8 depends on 1-3
- Task 9 depends on all above
- Task 10 depends on 9
- Task 11 independent, do anytime
**After this plan:** Update the Makefile `build` target to include `npm run build && npx cap sync android` so deploys sync native assets too.
-1
View File
@@ -49,7 +49,6 @@ require (
github.com/fsnotify/fsnotify v1.10.1 // indirect
github.com/fxamacker/cbor/v2 v2.9.2 // indirect
github.com/gammazero/deque v1.2.1 // indirect
github.com/go-chi/cors v1.2.2 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/jsonpointer v0.19.5 // indirect
-2
View File
@@ -87,8 +87,6 @@ github.com/gammazero/deque v1.2.1 h1:9fnQVFCCZ9/NOc7ccTNqzoKd1tCWOqeI05/lPqFPMGQ
github.com/gammazero/deque v1.2.1/go.mod h1:5nSFkzVm+afG9+gy0VIowlqVAW4N8zNcMne+CMQVD2g=
github.com/go-chi/chi/v5 v5.3.0 h1:halUjDxhshgXHMrao5bB8eNBXo/rnzwr8m5m36glehM=
github.com/go-chi/chi/v5 v5.3.0/go.mod h1:R+tYY2hNuVUUjxoPtqUdgBqevM9s9njzkTLutVsOCto=
github.com/go-chi/cors v1.2.2 h1:Jmey33TE+b+rB7fT8MUy1u0I4L+NARQlK6LhzKPSyQE=
github.com/go-chi/cors v1.2.2/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
-1
View File
@@ -21,4 +21,3 @@ func SetSessionCookie(w http.ResponseWriter, cookieName, token string, duration
MaxAge: int(duration.Seconds()),
})
}
+4 -6
View File
@@ -30,11 +30,10 @@ 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, tokenHash)
`, userID, token)
if err != nil {
http.Error(w, `{"error":"failed to generate token"}`, http.StatusInternalServerError)
return
@@ -64,7 +63,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
`, hashToken(req.Token)).Scan(&userID)
`, req.Token).Scan(&userID)
if err != nil {
http.Error(w, `{"error":"invalid or expired token"}`, http.StatusBadRequest)
return
@@ -101,11 +100,10 @@ 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, tokenHash)
`, userID, token)
if err != nil {
http.Error(w, `{"error":"failed to generate token"}`, http.StatusInternalServerError)
return
@@ -136,7 +134,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
`, hashToken(req.Token)).Scan(&userID)
`, req.Token).Scan(&userID)
if err != nil {
http.Error(w, `{"error":"invalid or expired token"}`, http.StatusBadRequest)
return
-32
View File
@@ -50,7 +50,6 @@ func (h *Handler) RegisterProtectedRoutes(r chi.Router) {
r.Get("/auth/me", h.Me)
r.Patch("/auth/me", h.UpdateProfile)
r.Put("/auth/me/password", h.ChangePassword)
r.Delete("/auth/me", h.DeleteAccount)
r.Get("/users/{userID}/profile", h.GetPublicProfile)
r.Get("/users/me/blocks", h.ListBlocks)
r.Post("/users/me/blocks", h.BlockUser)
@@ -332,7 +331,6 @@ func (h *Handler) Register(w http.ResponseWriter, r *http.Request) {
}
h.setSessionCookie(w, token)
w.Header().Set("X-Session-Token", token)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"id": userID})
}
@@ -385,7 +383,6 @@ func (h *Handler) Login(w http.ResponseWriter, r *http.Request) {
}
h.setSessionCookie(w, token)
w.Header().Set("X-Session-Token", token)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"id": userID})
}
@@ -663,32 +660,3 @@ func (h *Handler) ChangePassword(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"message": "password updated"})
}
// DeleteAccount deletes the authenticated user and all associated data.
// ponytail: cascading FK handles all DB cleanup — push subscriptions cleaned manually.
func (h *Handler) DeleteAccount(w http.ResponseWriter, r *http.Request) {
userID, ok := middleware.UserIDFromContext(r.Context())
if !ok {
http.Error(w, `{"error":"unauthorized"}`, http.StatusUnauthorized)
return
}
// Clean up push subscriptions
h.db.ExecContext(r.Context(), `DELETE FROM push_subscriptions WHERE user_id = $1`, userID)
// ON DELETE CASCADE handles everything else: sessions, tokens, messages,
// reactions, memberships, invites, bots, etc.
result, err := h.db.ExecContext(r.Context(), `DELETE FROM users WHERE id = $1`, userID)
if err != nil {
http.Error(w, `{"error":"failed to delete account"}`, http.StatusInternalServerError)
return
}
rows, _ := result.RowsAffected()
if rows == 0 {
http.Error(w, `{"error":"user not found"}`, http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"message": "account deleted"})
}
+3 -12
View File
@@ -3,7 +3,6 @@ package auth
import (
"context"
"crypto/rand"
"crypto/sha256"
"database/sql"
"encoding/hex"
"fmt"
@@ -21,25 +20,17 @@ 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, tokenHash, time.Now().Add(s.cfg.Session.Duration))
`, userID, token, time.Now().Add(s.cfg.Session.Duration))
if err != nil {
return "", fmt.Errorf("insert session: %w", err)
}
@@ -52,7 +43,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()
`, hashToken(token)).Scan(&userID)
`, token).Scan(&userID)
if err != nil {
return "", err
}
@@ -60,6 +51,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`, hashToken(token))
_, err := s.db.ExecContext(ctx, `DELETE FROM sessions WHERE token = $1`, token)
return err
}
+2 -5
View File
@@ -234,8 +234,7 @@ func (h *WebAuthnHandler) LoginBegin(w http.ResponseWriter, r *http.Request) {
Path: "/",
HttpOnly: true,
MaxAge: 300, // 5 minutes
SameSite: http.SameSiteNoneMode,
Secure: true,
SameSite: http.SameSiteStrictMode,
})
w.Header().Set("Content-Type", "application/json")
@@ -342,11 +341,9 @@ func (h *WebAuthnHandler) LoginFinish(w http.ResponseWriter, r *http.Request) {
Path: "/",
HttpOnly: true,
MaxAge: -1,
SameSite: http.SameSiteNoneMode,
Secure: true,
SameSite: http.SameSiteStrictMode,
})
w.Header().Set("X-Session-Token", token)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"status": "authenticated"})
}
-6
View File
@@ -4,7 +4,6 @@ import (
"context"
"database/sql"
"fmt"
"time"
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/config"
@@ -25,11 +24,6 @@ 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
}
+1 -25
View File
@@ -4,7 +4,6 @@ import (
"net/http"
"strings"
"sync"
"time"
"golang.org/x/time/rate"
)
@@ -13,21 +12,16 @@ 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 {
l := &ipLimiter{
return &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 {
@@ -39,27 +33,9 @@ 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 -12
View File
@@ -18,23 +18,13 @@ type SessionStore interface {
func Session(store SessionStore, cfg *config.Config) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var token string
cookie, err := r.Cookie(cfg.Session.CookieName)
if err == nil {
token = cookie.Value
} else {
authHeader := r.Header.Get("Authorization")
if len(authHeader) > 7 && authHeader[:7] == "Bearer " {
token = authHeader[7:]
}
}
if token == "" {
if err != nil {
next.ServeHTTP(w, r)
return
}
userID, err := store.GetUserIDByToken(r.Context(), token)
userID, err := store.GetUserIDByToken(r.Context(), cookie.Value)
if err != nil {
next.ServeHTTP(w, r)
return
Binary file not shown.
Binary file not shown.
-29
View File
@@ -1,29 +0,0 @@
{
"project_info": {
"project_number": "629643353973",
"project_id": "dumpster-chat",
"storage_bucket": "dumpster-chat.firebasestorage.app"
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:629643353973:android:29dcc703959dd306c0fd3c",
"android_client_info": {
"package_name": "coffee.dustin.dumpster"
}
},
"oauth_client": [],
"api_key": [
{
"current_key": "AIzaSyDt3h4G-imzD7IedRqYOUBIv8CtZQT2YyA"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": []
}
}
}
],
"configuration_version": "1"
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 502 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

-12
View File
@@ -1,12 +0,0 @@
# EditorConfig is awesome: https://EditorConfig.org
# top-most EditorConfig file
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = false
insert_final_newline = false
-20
View File
@@ -1,20 +0,0 @@
*.iml
.gradle
/local.properties
/.idea/caches
/.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
.DS_Store
build
/captures
.externalNativeBuild
.cxx
local.properties
key.properties
keystore.properties
/.tauri
/tauri.settings.gradle
-6
View File
@@ -1,6 +0,0 @@
/src/main/**/generated
/src/main/jniLibs/**/*.so
/src/main/assets/tauri.conf.json
/tauri.build.gradle.kts
/proguard-tauri.pro
/tauri.properties
@@ -1,90 +0,0 @@
import java.util.Properties
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
id("rust")
}
val tauriProperties = Properties().apply {
val propFile = file("tauri.properties")
if (propFile.exists()) {
propFile.inputStream().use { load(it) }
}
}
val keystorePropertiesFile = rootProject.file("keystore.properties")
val keystoreProperties = Properties()
if (keystorePropertiesFile.exists()) {
keystoreProperties.load(keystorePropertiesFile.inputStream())
}
android {
compileSdk = 36
namespace = "coffee.dustin.dumpster"
defaultConfig {
manifestPlaceholders["usesCleartextTraffic"] = "false"
applicationId = "coffee.dustin.dumpster"
minSdk = 24
targetSdk = 36
versionCode = tauriProperties.getProperty("tauri.android.versionCode", "1").toInt()
versionName = tauriProperties.getProperty("tauri.android.versionName", "1.0")
}
signingConfigs {
if (keystorePropertiesFile.exists()) {
create("release") {
storeFile = file(keystoreProperties["storeFile"] as String)
storePassword = keystoreProperties["storePassword"] as String
keyAlias = keystoreProperties["keyAlias"] as String
keyPassword = keystoreProperties["keyPassword"] as String
}
}
}
buildTypes {
getByName("debug") {
manifestPlaceholders["usesCleartextTraffic"] = "true"
isDebuggable = true
isJniDebuggable = true
isMinifyEnabled = false
packaging { jniLibs.keepDebugSymbols.add("*/arm64-v8a/*.so")
jniLibs.keepDebugSymbols.add("*/armeabi-v7a/*.so")
jniLibs.keepDebugSymbols.add("*/x86/*.so")
jniLibs.keepDebugSymbols.add("*/x86_64/*.so")
}
}
getByName("release") {
if (keystorePropertiesFile.exists()) {
signingConfig = signingConfigs.getByName("release")
}
isMinifyEnabled = true
proguardFiles(
*fileTree(".") { include("**/*.pro") }
.plus(getDefaultProguardFile("proguard-android-optimize.txt"))
.toList().toTypedArray()
)
}
}
kotlinOptions {
jvmTarget = "1.8"
}
buildFeatures {
buildConfig = true
}
}
rust {
rootDirRel = "../../../"
}
dependencies {
implementation("androidx.webkit:webkit:1.14.0")
implementation("androidx.appcompat:appcompat:1.7.1")
implementation("androidx.activity:activity-ktx:1.10.1")
implementation("com.google.android.material:material:1.12.0")
implementation("androidx.lifecycle:lifecycle-process:2.10.0")
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.4")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.0")
}
apply(from = "tauri.build.gradle.kts")
-21
View File
@@ -1,21 +0,0 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
@@ -1,38 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<!-- AndroidTV support -->
<uses-feature android:name="android.software.leanback" android:required="false" />
<application
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:theme="@style/Theme.app"
android:usesCleartextTraffic="${usesCleartextTraffic}">
<activity
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode"
android:launchMode="singleTask"
android:label="@string/main_activity_title"
android:name=".MainActivity"
android:exported="true"
android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
<!-- AndroidTV support -->
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter>
</activity>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
</application>
</manifest>
@@ -1,11 +0,0 @@
package coffee.dustin.dumpster
import android.os.Bundle
import androidx.activity.enableEdgeToEdge
class MainActivity : TauriActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
super.onCreate(savedInstanceState)
}
}
@@ -1,30 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>
@@ -1,170 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector>
@@ -1,18 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

@@ -1,10 +0,0 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. Forces dark mode since our CSS themes are always dark. -->
<style name="Theme.app" parent="Theme.MaterialComponents.NoActionBar">
<item name="android:statusBarColor">@android:color/transparent</item>
<item name="android:navigationBarColor">@android:color/transparent</item>
<!-- Force light status bar icons (white) against our dark background -->
<item name="android:windowLightStatusBar">false</item>
<item name="android:windowLightNavigationBar">false</item>
</style>
</resources>
@@ -1,10 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="purple_200">#FFBB86FC</color>
<color name="purple_500">#FF6200EE</color>
<color name="purple_700">#FF3700B3</color>
<color name="teal_200">#FF03DAC5</color>
<color name="teal_700">#FF018786</color>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
</resources>
@@ -1,4 +0,0 @@
<resources>
<string name="app_name">"dumpsterChat"</string>
<string name="main_activity_title">"dumpsterChat"</string>
</resources>
@@ -1,10 +0,0 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. Forces dark mode since our CSS themes are always dark. -->
<style name="Theme.app" parent="Theme.MaterialComponents.NoActionBar">
<item name="android:statusBarColor">@android:color/transparent</item>
<item name="android:navigationBarColor">@android:color/transparent</item>
<!-- Force light status bar icons (white) against our dark background -->
<item name="android:windowLightStatusBar">false</item>
<item name="android:windowLightNavigationBar">false</item>
</style>
</resources>
@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-path name="my_images" path="." />
<cache-path name="my_cache_images" path="." />
</paths>
@@ -1,22 +0,0 @@
buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
classpath("com.android.tools.build:gradle:8.11.0")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.25")
}
}
allprojects {
repositories {
google()
mavenCentral()
}
}
tasks.register("clean").configure {
delete("build")
}
@@ -1,23 +0,0 @@
plugins {
`kotlin-dsl`
}
gradlePlugin {
plugins {
create("pluginsForCoolKids") {
id = "rust"
implementationClass = "RustPlugin"
}
}
}
repositories {
google()
mavenCentral()
}
dependencies {
compileOnly(gradleApi())
implementation("com.android.tools.build:gradle:8.11.0")
}
@@ -1,68 +0,0 @@
import java.io.File
import org.apache.tools.ant.taskdefs.condition.Os
import org.gradle.api.DefaultTask
import org.gradle.api.GradleException
import org.gradle.api.logging.LogLevel
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.TaskAction
open class BuildTask : DefaultTask() {
@Input
var rootDirRel: String? = null
@Input
var target: String? = null
@Input
var release: Boolean? = null
@TaskAction
fun assemble() {
val executable = """npm""";
try {
runTauriCli(executable)
} catch (e: Exception) {
if (Os.isFamily(Os.FAMILY_WINDOWS)) {
// Try different Windows-specific extensions
val fallbacks = listOf(
"$executable.exe",
"$executable.cmd",
"$executable.bat",
)
var lastException: Exception = e
for (fallback in fallbacks) {
try {
runTauriCli(fallback)
return
} catch (fallbackException: Exception) {
lastException = fallbackException
}
}
throw lastException
} else {
throw e;
}
}
}
fun runTauriCli(executable: String) {
val rootDirRel = rootDirRel ?: throw GradleException("rootDirRel cannot be null")
val target = target ?: throw GradleException("target cannot be null")
val release = release ?: throw GradleException("release cannot be null")
val args = listOf("run", "--", "tauri", "android", "android-studio-script");
project.exec {
workingDir(File(project.projectDir, rootDirRel))
executable(executable)
args(args)
if (project.logger.isEnabled(LogLevel.DEBUG)) {
args("-vv")
} else if (project.logger.isEnabled(LogLevel.INFO)) {
args("-v")
}
if (release) {
args("--release")
}
args(listOf("--target", target))
}.assertNormalExitValue()
}
}
@@ -1,85 +0,0 @@
import com.android.build.api.dsl.ApplicationExtension
import org.gradle.api.DefaultTask
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.configure
import org.gradle.kotlin.dsl.get
const val TASK_GROUP = "rust"
open class Config {
lateinit var rootDirRel: String
}
open class RustPlugin : Plugin<Project> {
private lateinit var config: Config
override fun apply(project: Project) = with(project) {
config = extensions.create("rust", Config::class.java)
val defaultAbiList = listOf("arm64-v8a", "armeabi-v7a", "x86", "x86_64");
val abiList = (findProperty("abiList") as? String)?.split(',') ?: defaultAbiList
val defaultArchList = listOf("arm64", "arm", "x86", "x86_64");
val archList = (findProperty("archList") as? String)?.split(',') ?: defaultArchList
val targetsList = (findProperty("targetList") as? String)?.split(',') ?: listOf("aarch64", "armv7", "i686", "x86_64")
extensions.configure<ApplicationExtension> {
@Suppress("UnstableApiUsage")
flavorDimensions.add("abi")
productFlavors {
create("universal") {
dimension = "abi"
ndk {
abiFilters += abiList
}
}
defaultArchList.forEachIndexed { index, arch ->
create(arch) {
dimension = "abi"
ndk {
abiFilters.add(defaultAbiList[index])
}
}
}
}
}
afterEvaluate {
for (profile in listOf("debug", "release")) {
val profileCapitalized = profile.replaceFirstChar { it.uppercase() }
val buildTask = tasks.maybeCreate(
"rustBuildUniversal$profileCapitalized",
DefaultTask::class.java
).apply {
group = TASK_GROUP
description = "Build dynamic library in $profile mode for all targets"
}
tasks["mergeUniversal${profileCapitalized}JniLibFolders"].dependsOn(buildTask)
for (targetPair in targetsList.withIndex()) {
val targetName = targetPair.value
val targetArch = archList[targetPair.index]
val targetArchCapitalized = targetArch.replaceFirstChar { it.uppercase() }
val targetBuildTask = project.tasks.maybeCreate(
"rustBuild$targetArchCapitalized$profileCapitalized",
BuildTask::class.java
).apply {
group = TASK_GROUP
description = "Build dynamic library in $profile mode for $targetArch"
rootDirRel = config.rootDirRel
target = targetName
release = profile == "release"
}
buildTask.dependsOn(targetBuildTask)
tasks["merge$targetArchCapitalized${profileCapitalized}JniLibFolders"].dependsOn(
targetBuildTask
)
}
}
}
}
}
@@ -1,24 +0,0 @@
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app"s APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
# Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official
# Enables namespacing of each library's R class so that its R class includes only the
# resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library
android.nonTransitiveRClass=true
android.nonFinalResIds=false
Binary file not shown.
@@ -1,6 +0,0 @@
#Tue May 10 19:22:52 CST 2022
distributionBase=GRADLE_USER_HOME
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
distributionPath=wrapper/dists
zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
-185
View File
@@ -1,185 +0,0 @@
#!/usr/bin/env sh
#
# Copyright 2015 the original author or authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=`expr $i + 1`
done
case $i in
0) set -- ;;
1) set -- "$args0" ;;
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;;
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=`save "$@"`
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
exec "$JAVACMD" "$@"
-89
View File
@@ -1,89 +0,0 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
Binary file not shown.
@@ -1,3 +0,0 @@
include ':app'
apply from: 'tauri.settings.gradle'
-8
View File
@@ -2,13 +2,5 @@
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
fn main() {
#[cfg(target_os = "linux")]
{
std::env::set_var("WEBKIT_DISABLE_DMABUF_RENDERER", "1");
std::env::set_var("WEBKIT_DISABLE_SANDBOX_THIS_IS_DANGEROUS", "1");
}
app_lib::run();
}
+3 -3
View File
@@ -1,8 +1,8 @@
{
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
"productName": "dumpsterChat",
"version": "0.2.9",
"identifier": "coffee.dustin.dumpster",
"version": "0.2.0",
"identifier": "coffee.dustin.dumpsterchat",
"build": {
"frontendDist": "../dist",
"devUrl": "http://localhost:5173",
@@ -23,7 +23,7 @@
}
],
"security": {
"csp": "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self' wss: https:;"
"csp": null
}
},
"bundle": {
+2 -2
View File
@@ -104,7 +104,7 @@ export function Layout() {
<div className="h-full w-full bg-gb-bg text-gb-fg font-mono flex flex-col">
{/* Outer terminal frame with safe area */}
<div className="flex-1 terminal-border bg-gb-bg-h flex flex-col min-h-0"
style={{ padding: 'var(--safe-top, 0.25rem) var(--safe-right, 0) var(--safe-bottom, 0) var(--safe-left, 0)' }}>
style={{ padding: 'max(env(safe-area-inset-top), 0.25rem) max(env(safe-area-inset-right), 0) max(env(safe-area-inset-bottom), 0) max(env(safe-area-inset-left), 0)' }}>
{/* Top bar — minimal on mobile */}
<div className="flex items-center justify-between px-2 md:px-3 py-1 border-b border-gb-bg-t bg-gb-bg-s gap-1">
@@ -237,7 +237,7 @@ function MobileBottomNav({
return (
<div className="md:hidden flex items-center bg-gb-bg-h border-t border-gb-bg-t shrink-0"
style={{ paddingBottom: 'var(--safe-bottom)' }}>
style={{ paddingBottom: 'env(safe-area-inset-bottom)' }}>
<NavTab active={mobileView === 'sidebar'} onClick={() => onViewChange('sidebar')}>
[SERVERS]
</NavTab>
+2 -2
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;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
}
function inlineMarkdownToHtml(text: string): string {
@@ -598,7 +598,7 @@ export const MessageInput = forwardRef<HTMLTextAreaElement, MessageInputProps>(f
)}
{/* toolbar row */}
<div className="flex items-center gap-x-1 md:gap-x-0.5 pt-0.5 border-t border-gb-bg-t">
<div className="flex items-center gap-0.5 pt-0.5 border-t border-gb-bg-t">
{/* + file upload */}
<input
ref={fileInputRef}
+1 -1
View File
@@ -47,7 +47,7 @@ export function MobileNav({ activeTab, onTabChange, onToggleDrawer }: MobileNavP
<ThemeToggle />
</div>
{/* Safe area for phones with bottom notch */}
<div className="h-[var(--safe-bottom)]" />
<div className="h-[env(safe-area-inset-bottom)]" />
</div>
);
}
+1 -26
View File
@@ -5,7 +5,7 @@ import { usePushStore } from '../stores/push.ts';
import { ThemeToggle } from './ThemeToggle.tsx';
export function UserSettings() {
const { user, updateProfile, changePassword, deleteAccount, isLoading, error, clearError } = useAuthStore();
const { user, updateProfile, changePassword, isLoading, error, clearError } = useAuthStore();
const { isSupported, isSubscribed, subscribe, unsubscribe, checkSubscription } = usePushStore();
const navigate = useNavigate();
@@ -440,31 +440,6 @@ export function UserSettings() {
</div>
</form>
{/* Account Deletion */}
<div className="mt-6 pt-4 border-t border-gb-bg-t">
<details className="group">
<summary className="text-gb-red font-mono text-xs cursor-pointer hover:text-gb-bright-red select-none">
[DELETE ACCOUNT]
</summary>
<div className="mt-3 p-3 border border-gb-red/40 bg-gb-bg-t/30">
<p className="text-gb-fg-f text-xs font-mono mb-3">
This action is permanent and cannot be undone. All your messages, servers you own, and uploaded files will be deleted.
</p>
<button
type="button"
onClick={async () => {
if (!confirm('Are you sure you want to permanently delete your account? This cannot be undone.')) return;
await deleteAccount();
navigate('/login');
}}
className="terminal-button text-xs border-gb-red text-gb-red hover:bg-gb-red/20"
>
[CONFIRM DELETE ACCOUNT]
</button>
</div>
</details>
</div>
{/* Footer info */}
<div className="mt-6 pt-4 border-t border-gb-bg-t">
<p className="text-gb-fg-f text-xs font-mono">
-13
View File
@@ -40,19 +40,6 @@ async function request<T>(method: string, path: string, body?: unknown): Promise
const ra = response.headers.get('Retry-After');
err.retryAfter = retryAfter ?? (ra ? parseInt(ra, 10) : undefined);
}
// Auto-logout on 401 for any non-login endpoint so expired sessions
// redirect to the login page instead of showing cryptic errors.
if (response.status === 401 && !path.startsWith('/auth/login') && !path.startsWith('/auth/register')) {
// Dynamically import to avoid circular deps
import('../stores/auth.ts').then(({ useAuthStore }) => {
const state = useAuthStore.getState();
if (state.isAuthenticated) {
useAuthStore.setState({ user: null, isAuthenticated: false, error: null });
}
});
}
throw err;
}
+5 -56
View File
@@ -5,54 +5,15 @@ import App from './App.tsx';
const isTauri = 'window' in globalThis && '__TAURI_INTERNALS__' in window;
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-bottom', '0.75rem');
document.documentElement.style.setProperty('--safe-left', '0px');
document.documentElement.style.setProperty('--safe-right', '0px');
} else {
// Linux / macOS Tauri — no notch, but body has default padding already
document.documentElement.style.setProperty('--safe-top', '0px');
document.documentElement.style.setProperty('--safe-bottom', '0px');
document.documentElement.style.setProperty('--safe-left', '0px');
document.documentElement.style.setProperty('--safe-right', '0px');
}
const TARGET = 'https://dumpster.dustin.coffee';
const WS_TARGET = 'wss://dumpster.dustin.coffee';
let sessionToken = localStorage.getItem('dumpster_session_token') || '';
const originalFetch = window.fetch;
window.fetch = async (input, init) => {
let url = typeof input === 'string' ? input : input.toString();
if (url.startsWith('/')) {
url = TARGET + url;
if (typeof input === 'string' && input.startsWith('/')) {
input = TARGET + input;
}
let newInit = init ? { ...init } : {};
if (sessionToken) {
newInit.headers = {
...newInit.headers,
'Authorization': 'Bearer ' + sessionToken
};
}
const response = await originalFetch(url, newInit);
const token = response.headers.get('X-Session-Token');
if (token) {
sessionToken = token;
localStorage.setItem('dumpster_session_token', token);
}
if (url.endsWith('/auth/logout') && response.ok) {
sessionToken = '';
localStorage.removeItem('dumpster_session_token');
}
return response;
return originalFetch(input, init);
};
const OriginalWebSocket = window.WebSocket;
@@ -66,21 +27,9 @@ if (isTauri) {
} else if (urlStr.startsWith('/')) {
urlStr = WS_TARGET + urlStr;
}
const ws = new OriginalWebSocket(urlStr, protocols);
if (sessionToken && urlStr.includes('/ws') && !urlStr.includes('/ws/bot')) {
const originalOnOpen = ws.onopen;
ws.onopen = function(ev) {
ws.send(JSON.stringify({ token: sessionToken }));
if (originalOnOpen) originalOnOpen.call(ws, ev);
};
}
return ws;
return new OriginalWebSocket(urlStr, protocols);
} as unknown as typeof WebSocket;
// ponytail: WebKitGTK may have read-only prototype — guard against throw
try { window.WebSocket.prototype = OriginalWebSocket.prototype; } catch { /* readonly, fine */ }
window.WebSocket.prototype = OriginalWebSocket.prototype;
}
createRoot(document.getElementById('root')!).render(
+2 -14
View File
@@ -69,7 +69,6 @@ interface AuthState {
blockUser: (userId: string) => Promise<void>;
unblockUser: (userId: string) => Promise<void>;
clearError: () => void;
deleteAccount: () => Promise<void>;
}
// ponytail: auto-subscribe to push notifications on auth
@@ -152,19 +151,12 @@ export const useAuthStore = create<AuthState>((set) => ({
set({ user, isAuthenticated: true, isLoading: false });
autoSubscribePush();
} catch (error) {
// 401 from /auth/me simply means "no active session" — not a
// user-facing error. Only surface non-auth failures.
const isAuthError =
error instanceof Error && (error as import('../lib/api.ts').ApiError).status === 401;
set({
user: null,
isAuthenticated: false,
isLoading: false,
error: isAuthError
? null
: error instanceof Error
? error.message
: "Failed to fetch user",
error:
error instanceof Error ? error.message : "Failed to fetch user",
});
}
},
@@ -250,8 +242,4 @@ export const useAuthStore = create<AuthState>((set) => ({
unblockUser: async (userId) => {
await api.delete(`/users/me/blocks/${userId}`);
},
deleteAccount: async () => {
await api.delete("/auth/me");
},
}));
+2 -4
View File
@@ -135,14 +135,12 @@ export const useConversationStore = create<ConversationState>((set, get) => ({
},
sendMessage: async (conversationId, content) => {
// Add locally so the message appears immediately even if WS lags.
// addMessage dedupes, so a late WS event won't double it.
// don't append here — WS MESSAGE_CREATE broadcast is the single source of truth.
// Appending in both places caused double messages when the WS event arrived before the HTTP response.
const message = await api.post<ConversationMessage>(
`/conversations/${conversationId}/messages`,
{ content },
);
// ponytail: local append as fallback for WS MESSAGE_CREATE; remove if WS reliability improves
get().addMessage(message);
return message;
},
+2 -4
View File
@@ -169,14 +169,12 @@ export const useMessageStore = create<MessageState>((set, get) => ({
sendMessage: async (channelId, content, replyTo) => {
const body: { content: string; reply_to?: string } = { content };
if (replyTo) body.reply_to = replyTo;
// Add locally so the message appears immediately even if WS lags.
// addMessage dedupes, so a late WS event won't double it.
// ponytail: don't append here — WS MESSAGE_CREATE broadcast is the single source of truth.
// Appending in both places caused double messages when the WS event arrived before the HTTP response.
const message = await api.post<Message>(
`/channels/${channelId}/messages`,
body,
);
// ponytail: local append as fallback for WS MESSAGE_CREATE; remove if WS reliability improves
get().addMessage(message);
return message;
},