94f5da4013a9cc8e22ca225d149b3e05acf7f0e1
19
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
94f5da4013 |
feat(i18n): translate five more systems, and unify the permission labels
CI / Build & test (Node 20) (push) Canceled after 0s
CI / Build & test (Node 24) (push) Canceled after 0s
CI / Build & test (push) Canceled after 0s
CodeQL / Analyze (javascript-typescript) (push) Canceled after 0s
Security / Secret scan (gitleaks) (push) Canceled after 0s
Security / Dependency scan (OSV-Scanner) (push) Canceled after 0s
Security / IaC/config scan (Trivy) (push) Canceled after 0s
Security / License compliance scan (Trivy) (push) Canceled after 0s
OpenSSF Scorecard / Scorecard analysis (push) Canceled after 0s
- Space settings > Overview — identity, icon, banner, transfer, delete - Space settings > Members and Bans - Space settings > Roles, including the whole permission table - Category settings — rename, privacy, overrides, delete - Instance settings > General — name, discovery, GIF search The permission labels were duplicated: the roles panel carried one English list and the channel/category override editors carried another. Both now read a single `perm.*` set, so renaming a permission lands in all three at once instead of one and a half. This removes the `channel.perm.*` keys added two commits ago rather than adding a third copy beside them. Same pattern as before for anything chosen outside the render: the roles panel's permission groups and the category's override definitions hold keys and resolve them where they render. The category delete dialog emphasises the category name, so its sentence is split around the placeholder rather than interpolated. Dictionary is at 723 keys, en and pt-BR at exact parity. Verified: web build (tsc + vite), 490 web tests. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
4fd6ac890c |
feat(i18n): translate five more systems
CI / Build & test (Node 20) (push) Canceled after 0s
CI / Build & test (Node 24) (push) Canceled after 0s
CI / Build & test (push) Canceled after 0s
CodeQL / Analyze (javascript-typescript) (push) Canceled after 0s
Security / Secret scan (gitleaks) (push) Canceled after 0s
Security / Dependency scan (OSV-Scanner) (push) Canceled after 0s
Security / IaC/config scan (Trivy) (push) Canceled after 0s
Security / License compliance scan (Trivy) (push) Canceled after 0s
OpenSSF Scorecard / Scorecard analysis (push) Canceled after 0s
- Settings > Voice: video — preview, permission states, camera picker.
This closes the Voice & Video tab; input, output and video are all done.
- Create channel / category / space
- Group DMs — new DM, add friends, group settings, leave
- Join a space — discovery, invite code, and both federated auth phases
- Delete account, transfer ownership, and the top-level error screen
Three patterns worth noting. Strings that are chosen in a helper or held in
state (the camera's hard-blocked copy, its preview errors, the space
visibility options, the invite failure reasons) now travel as translation
keys and are resolved where they render, so switching language re-labels
what is already on screen. Sentences that wrap an emphasised literal — the
host name when joining, the space and member names when transferring — are
split around their placeholder instead of interpolated, leaving word order
to the translation. And `CreateChannel` mapped over `(t) =>` for the channel
type, which shadowed the translator; the loop variable is now `channelType`.
The error boundary in main.tsx is a class component, so it reads the locale
once per render via `translate` rather than the hook — correct here anyway,
since an error screen has no reason to re-render on a language switch.
Two GroupDmSettings tests located the name input by its aria-label, which
this change translates. They now use the `data-group-dm-name-input`
attribute the input already carries, matching how the rest of that file
locates elements — and no longer break when copy changes.
Dictionary is at 595 keys, en and pt-BR at exact parity.
Verified: web build (tsc + vite), 490 web tests.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
115c81cba3 |
feat(i18n): translate five more systems, and check for updates more often
CI / Build & test (Node 20) (push) Canceled after 0s
CI / Build & test (Node 24) (push) Canceled after 0s
CI / Build & test (push) Canceled after 0s
CodeQL / Analyze (javascript-typescript) (push) Canceled after 0s
Security / Secret scan (gitleaks) (push) Canceled after 0s
Security / Dependency scan (OSV-Scanner) (push) Canceled after 0s
Security / IaC/config scan (Trivy) (push) Canceled after 0s
Security / License compliance scan (Trivy) (push) Canceled after 0s
OpenSSF Scorecard / Scorecard analysis (push) Canceled after 0s
Update cadence: the desktop app checked once ten seconds after launch and
then every four hours. A release published while the app sits open went
unnoticed until the next poll — which is exactly what happened with 1.4.0.
It now polls every 30 minutes and also checks when the window regains focus,
throttled to one check per five minutes so alt-tabbing does not hammer the
feed. `lastCheckAt` is stamped from the `checking-for-update` event rather
than by the callers, so a manual check from the settings panel counts too.
The focus listener is on `app`, not on mainWindow, because the window is
destroyed and recreated when closed to tray.
Translations, five whole systems:
- Settings > Desktop (autostart, updates, instance)
- Settings > Voice: output device, which completes that tab
- Settings > Keybinds, including the action names and conflict prompt
- Settings > Account, the largest — profile, password, detached-instance
notice, danger zone
- Explore spaces
Two sentences wrap a literal that must not be translated (the `input` group
name, the `user@instance` example). Those are split around a placeholder
rather than interpolated as text, so a translation keeps control of the word
order on both sides of it. Keybind action labels gained a `labelKey`
alongside the English `label`, which stays for non-React callers.
Verified: web build (tsc + vite), desktop typecheck, 490 web tests.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
860f02b2c1 |
feat(channels): rename channels, and quieter defaults for sound and streams
CI / Build & test (Node 20) (push) Canceled after 0s
CI / Build & test (Node 24) (push) Canceled after 0s
CI / Build & test (push) Canceled after 0s
CodeQL / Analyze (javascript-typescript) (push) Canceled after 0s
Security / Secret scan (gitleaks) (push) Canceled after 0s
Security / Dependency scan (OSV-Scanner) (push) Canceled after 0s
Security / IaC/config scan (Trivy) (push) Canceled after 0s
Security / License compliance scan (Trivy) (push) Canceled after 0s
OpenSSF Scorecard / Scorecard analysis (push) Canceled after 0s
Four owner requests in one pass. Rename: the PATCH /api/channels/:id route already accepted a name and a topic, and the API client already exposed it — the only thing missing was a way to reach it. The channel settings Overview tab now edits both, gated on MANAGE_CHANNELS like the server is, with Save/Discard appearing only when the form is dirty. The server's lowercase-and-hyphenate rule moved to @backspace/shared so the field can show what a name will actually become instead of letting it change shape on save; one copy of the rule, used by both sides. Stream volume: someone else's screen share now starts at 50 instead of 100 (DEFAULT_STREAM_VOLUME). The publisher boosts system audio before sending, so the old default arrived far too loud for a viewer who never touched the slider. Only the default moves — the slider still covers the full range and a value the user set is still persisted. Sound effects: SFX_BASE_VOLUME 0.8 -> 0.4, halving every cue. The .ogg files were left alone; they already peak below full scale, so the harshness was level, not recorded distortion. Every SFX playback path goes through getSfxVolume(), so this is the single knob. Translations: channel settings (including the override editor's permission labels), the friends page end to end — tabs, lists, requests, the add tab and discovery cards — the invite modal including its per-invite failure reasons, and the group-DM member actions. Failure reasons are carried as translation keys rather than finished text, so results already on screen re-label when the language changes. Verified: web build (tsc + vite), server typecheck, 490 web tests, 804 server tests. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
1fb61377b9 |
feat(notify): in-app notifications with the app's own sound
CI / Build & test (Node 20) (push) Canceled after 0s
CI / Build & test (Node 24) (push) Canceled after 0s
CI / Build & test (push) Canceled after 0s
CodeQL / Analyze (javascript-typescript) (push) Canceled after 0s
Security / Secret scan (gitleaks) (push) Canceled after 0s
Security / Dependency scan (OSV-Scanner) (push) Canceled after 0s
Security / IaC/config scan (Trivy) (push) Canceled after 0s
Security / License compliance scan (Trivy) (push) Canceled after 0s
OpenSSF Scorecard / Scorecard analysis (push) Canceled after 0s
The system balloon carries the OS notification sound, which does not belong to this app, and it only fired while the window was out of focus — with the app focused nothing appeared at all. Notifications now surface inside the window, carry the same synthesised timbre as the rest of the app's sounds, and clicking one opens the channel. The native balloon is kept for when the window is not visible, since an in-app card nobody can see is no notification, but it is now silent: the app plays its own effect instead. A focused window is notified only about other channels — announcing the conversation someone is already reading is noise. Also fixes the Gitea publish cleanup, which silently deleted nothing: it interpolated an Actions expression inside a bash , and when the pattern did not match, the loop passed over every asset. The release ended with two latest.yml files and the updater served the older one, reporting 1.1.0 as current — an update that exists but is never offered, with no error anywhere. The filter is plain bash now, logs what it found, and the job fails if more than one latest.yml survives. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
71d2109b2f |
feat(desktop): in-app prompt to restart for an update
CI / Build & test (Node 20) (push) Canceled after 0s
CI / Build & test (Node 24) (push) Canceled after 0s
CI / Build & test (push) Canceled after 0s
CodeQL / Analyze (javascript-typescript) (push) Canceled after 0s
Security / Secret scan (gitleaks) (push) Canceled after 0s
Security / Dependency scan (OSV-Scanner) (push) Canceled after 0s
Security / IaC/config scan (Trivy) (push) Canceled after 0s
Security / License compliance scan (Trivy) (push) Canceled after 0s
OpenSSF Scorecard / Scorecard analysis (push) Canceled after 0s
The main process already showed a system notification when an update finished downloading, and nothing in the renderer listened — so the only cue vanished on its own and was missed by anyone not watching the desktop at that moment. A prompt now stays in the window until it is acted on or dismissed, with the restart wired to the install-update channel the preload already exposed. Dismissing is 'not now', not 'never': a later version reopens the prompt, since the version it refers to has changed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
7f08384372 |
feat(expressions): pickers and upload for emojis and stickers
Completes the feature: the tables existed but nothing could be put in them. Space settings gain an Emojis & Stickers panel behind MANAGE_SPACE, with a 512KB ceiling — both are fetched on every message that uses them, so weight matters more than fidelity. The suggested name is pre-normalised so the common case needs no typing, and a name collision reports itself distinctly from an upload failure: the corrective action is different. Custom emojis join the emoji picker as their own category. They have no native character, so selecting one inserts :name: — the same text the renderer resolves back to an image, which also means copying a message yields something that still reads. Stickers get a picker tab that only appears inside a space, since that is where they exist, and send immediately on click: a sticker is the whole message, so parking it in the composer to await Enter would make no sense. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
50a8f12c77 |
feat(search): inline filters, and translate the search UI
The filters themselves already existed end to end — the server takes q, from, has, before and after, the API client passes them, and the popover has fields for each. What it lacked was discovery: the panel sits behind a button, so the capability was invisible. Typing 'de:fulano' or 'has:image' straight into the search box now applies the same filters. Keys are accepted in both languages, since the app is bilingual, and an unrecognised token falls back to being search text — otherwise a message containing a URL or 'algo:coisa' would become unfindable. Inline filters win over the panel's: whoever just typed one is expressing the more recent intent. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
4ceb5cd66a |
feat(pins): pin messages to a channel
Pinned state lives on the message rather than a join table: a message is pinned in exactly one channel, its own, so a separate table would add a join to every lookup and buy nothing. Every message now carries its pin state, so the timeline can mark a pin without a second request and the panel and the timeline cannot disagree. Toggling is deliberately not optimistic — the server refuses past the channel's limit, and showing it pinned before confirmation would lie in exactly that case. The pins list reuses the same assembly the channel history uses, extracted into one helper, so the two cannot drift apart in what they include. The migration also adds the (channel, user, created) index the filtered search will need, since both touch the same table and one migration is cheaper than two. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
61c68761df |
i18n: translate the channel and member sidebars
CI / Build & test (Node 20) (push) Canceled after 0s
CI / Build & test (Node 24) (push) Canceled after 0s
CI / Build & test (push) Canceled after 0s
CodeQL / Analyze (javascript-typescript) (push) Canceled after 0s
Security / Secret scan (gitleaks) (push) Canceled after 0s
Security / Dependency scan (OSV-Scanner) (push) Canceled after 0s
Security / IaC/config scan (Trivy) (push) Canceled after 0s
Security / License compliance scan (Trivy) (push) Canceled after 0s
OpenSSF Scorecard / Scorecard analysis (push) Canceled after 0s
Clears the two remaining rounds owed under the fork's per-update translation rule. Covers channels, direct messages, the context menus, the inline voice settings and the member list's loading and grouping labels. The member grouping needed care: one function returns either a fixed label or a role's own name, and role names are user data that must never be translated. It now marks only the fixed cases and the component resolves them at render, leaving role names untouched. The function is not a component, so it cannot call the hook itself. |
||
|
|
c04b8b47eb |
i18n: translate the chat composer and messages
CI / Build & test (Node 20) (push) Canceled after 0s
CI / Build & test (Node 24) (push) Canceled after 0s
CI / Build & test (push) Canceled after 0s
CodeQL / Analyze (javascript-typescript) (push) Canceled after 0s
Security / Secret scan (gitleaks) (push) Canceled after 0s
Security / Dependency scan (OSV-Scanner) (push) Canceled after 0s
Security / IaC/config scan (Trivy) (push) Canceled after 0s
Security / License compliance scan (Trivy) (push) Canceled after 0s
OpenSSF Scorecard / Scorecard analysis (push) Canceled after 0s
Catching up on the fork's rule that every update also translates one existing system — three updates went by without one. The composer's placeholder takes the channel as a parameter rather than being concatenated, since Portuguese puts the channel elsewhere in the sentence than English does. The translation hook is bound to `tr` in these two files, not `t`: both already use `t` for transfer records in inner scopes, which shadowed the function and made it uncallable exactly where it was needed. |
||
|
|
c899253e52 |
fix: soundboard upload in Electron, and Spotify sync/disappearing/progress
Soundboard: naming a clip used window.prompt, which Electron does not implement — it returned nothing, the flow aborted in silence, and adding a sound worked in the browser while doing nothing at all in the desktop app. Replaced with a two-step field inside the popover, identical in both. Spotify, three separate defects behind the two symptoms reported: Out of sync — a 20s poll stacked on the activity store's 5s debounce left everyone else on the previous track for up to 25s. The next poll is now scheduled just past the current track's end instead of on a fixed interval, and a track change bypasses the debounce (it happens once every few minutes; the debounce exists for chatty producers). Vanishing — a paused track, and the silent gap Spotify reports between two songs, both cleared the activity outright. Pausing is now carried as state rather than absence, and an empty answer is tolerated for 25s before the block comes down. Progress bar — timestamps are computed with the server's clock and were drawn against the viewer's, so any drift displaced the bar; and it kept advancing after a pause until the next poll. The ready payload now carries server time so each client can correct its own offset, and the bar freezes when paused. Tray, native notifications and system audio in screen share were all found already implemented and wired end to end; recorded in the roadmap rather than built again. |
||
|
|
e89966435a |
fix(soundboard): read MANAGE_SPACE from the space bitfield
CI / Build & test (Node 20) (push) Canceled after 0s
CI / Build & test (Node 24) (push) Canceled after 0s
CI / Build & test (push) Canceled after 0s
CodeQL / Analyze (javascript-typescript) (push) Canceled after 0s
Security / Secret scan (gitleaks) (push) Canceled after 0s
Security / Dependency scan (OSV-Scanner) (push) Canceled after 0s
Security / IaC/config scan (Trivy) (push) Canceled after 0s
Security / License compliance scan (Trivy) (push) Canceled after 0s
OpenSSF Scorecard / Scorecard analysis (push) Canceled after 0s
The add-sound control was gated on channelPermissions, which carries the per-channel bitfield; MANAGE_SPACE lives in the space-level one. The check silently evaluated false for everybody, including owners, so the button never rendered and there was no way to add a sound at all. Also raise the clip cap to 2 MB — a 30-second clip at a high bitrate cleared 1 MB — and stop reporting every upload failure as 'too large', which sent people to shrink a file that was not the problem. |
||
|
|
f5451e1b14 |
feat: soundboard, account menu, and call timer
CI / Build & test (Node 20) (push) Canceled after 0s
CI / Build & test (Node 24) (push) Canceled after 0s
CI / Build & test (push) Canceled after 0s
CodeQL / Analyze (javascript-typescript) (push) Canceled after 0s
Security / Secret scan (gitleaks) (push) Canceled after 0s
Security / Dependency scan (OSV-Scanner) (push) Canceled after 0s
Security / IaC/config scan (Trivy) (push) Canceled after 0s
Security / License compliance scan (Trivy) (push) Canceled after 0s
OpenSSF Scorecard / Scorecard analysis (push) Canceled after 0s
Soundboard: the trigger travels over the WebSocket and every client in the call plays the clip locally, instead of mixing it into the presser's microphone or publishing a LiveKit track. No upstream bandwidth, no media stack changes, and the clip is not degraded by voice processing. Fan-out uses a new sendToRoomParticipants rather than sendToRoom: the latter broadcasts a space room to the whole space, which is right for the presence the sidebar shows and wrong for anything audible. The cooldown is enforced server-side — a client-side one only slows down people not trying to abuse it, and a soundboard is the easiest thing here to turn into a weapon. Playing is open to anyone in the call; deciding what the buttons are needs MANAGE_SPACE. Account menu: the name in the user bar had cursor-pointer and no handler, so the interface was already promising a click that did nothing. Offers profile, status and copy-id — not the Clips or account switching the reference design shows, which would be dead UI here. Call timer: startedAt comes from the server, so a late joiner sees the call's age rather than their own arrival. Empty space rooms are destroyed already, which is what makes the next call start from zero — no reset logic needed. |
||
|
|
1830051732 |
feat(stats): voice-time and message leaderboards per space
CI / Build & test (Node 20) (push) Canceled after 0s
CI / Build & test (Node 24) (push) Canceled after 0s
CI / Build & test (push) Canceled after 0s
CodeQL / Analyze (javascript-typescript) (push) Canceled after 0s
Security / Secret scan (gitleaks) (push) Canceled after 0s
Security / Dependency scan (OSV-Scanner) (push) Canceled after 0s
Security / IaC/config scan (Trivy) (push) Canceled after 0s
Security / License compliance scan (Trivy) (push) Canceled after 0s
OpenSSF Scorecard / Scorecard analysis (push) Canceled after 0s
Voice stays get their own table rather than joining the audit log: that table records points in time, a call is an interval, and pairing join/leave point events would leave every query guessing at joins whose leave never arrived. Sessions are opened and closed inside joinRoom/leaveCurrentRoom rather than at the seven call sites that reach them, so no path can be missed, and destroyRoom closes them too — it bypasses leaveCurrentRoom and would otherwise leak open rows. A restart leaves sessions open with an unknowable end time. They are closed at startedAt, discarding that time rather than inventing it: crediting the gap would hand someone hours they never spent, and the numbers are the point. Mirrors the existing users.status sweep on boot. Only closed sessions count, so a figure does not move on every refresh. Bars scale to the leader, not the total — with five people every share of a total looks identical. Statistics are readable by any member, since they are the group's own numbers; the audit log, which names who did what, stays on MANAGE_SPACE. |
||
|
|
bbb190cbda |
feat(audit): append-only audit log for spaces
CI / Build & test (Node 20) (push) Canceled after 0s
CI / Build & test (Node 24) (push) Canceled after 0s
CI / Build & test (push) Canceled after 0s
CodeQL / Analyze (javascript-typescript) (push) Canceled after 0s
Security / Secret scan (gitleaks) (push) Canceled after 0s
Security / Dependency scan (OSV-Scanner) (push) Canceled after 0s
Security / IaC/config scan (Trivy) (push) Canceled after 0s
Security / License compliance scan (Trivy) (push) Canceled after 0s
OpenSSF Scorecard / Scorecard analysis (push) Canceled after 0s
Records who changed what, and is the mechanism statistics will read — one event table rather than two logs that drift apart. The table is deliberately generic (action + target + JSON metadata) so a new action needs no migration. Writes never throw: a kick must not fail because its log entry could not be written, since the kick already happened. Leaving is recorded as a different action from being removed. The same route serves both, and a log that conflates them misleads exactly when it matters. Actor is nullable with ON DELETE SET NULL: the event outlives the account, and a log that vanished with its actor would be worthless. Reads are gated on MANAGE_SPACE rather than a new permission bit, which would default to nobody until every role was re-edited. Paging uses the snowflake id, stable even for two events in the same millisecond, and an action this build does not know still renders a row. |
||
|
|
fb662bfe12 |
feat(gif): favourites and category shortcuts
CI / Build & test (Node 20) (push) Canceled after 0s
CI / Build & test (Node 24) (push) Canceled after 0s
CI / Build & test (push) Canceled after 0s
CodeQL / Analyze (javascript-typescript) (push) Canceled after 0s
Security / Secret scan (gitleaks) (push) Canceled after 0s
Security / Dependency scan (OSV-Scanner) (push) Canceled after 0s
Security / IaC/config scan (Trivy) (push) Canceled after 0s
Security / License compliance scan (Trivy) (push) Canceled after 0s
OpenSSF Scorecard / Scorecard analysis (push) Canceled after 0s
Favourites are stored server-side per user, so one made on the phone is there on the desktop — the point of favouriting. The whole result is stored rather than an id: the provider offers no lookup by id, so an id-only favourite could not be rendered without re-finding it through search. Category chips translate their label but not their query, which goes to a provider that indexes in English. The star sits beside the tile button rather than inside it: a button within a button is invalid and swallows the click. Toggling is optimistic and reverts on failure, and favourites skip both the loading skeleton and the infinite scroll, which belong to provider-backed browsing only. Server caps favourites per user and rejects non-http(s) URLs, which become <img src> in everyone's picker. |
||
|
|
75316b0882 |
feat(spotify): show the current track as an activity
CI / Build & test (Node 20) (push) Canceled after 0s
CI / Build & test (Node 24) (push) Canceled after 0s
CI / Build & test (push) Canceled after 0s
CodeQL / Analyze (javascript-typescript) (push) Canceled after 0s
Security / Secret scan (gitleaks) (push) Canceled after 0s
Security / Dependency scan (OSV-Scanner) (push) Canceled after 0s
Security / IaC/config scan (Trivy) (push) Canceled after 0s
Security / License compliance scan (Trivy) (push) Canceled after 0s
OpenSSF Scorecard / Scorecard analysis (push) Canceled after 0s
OAuth Authorization Code flow, with tokens kept server-side: refreshing needs the client secret, so the browser never holds a Spotify token — it asks this instance what is playing and this instance calls Spotify. The callback arrives as a plain browser redirect with no Authorization header, so the OAuth state carries the user id signed with the instance secret and is compared in constant time; without that, anyone could bind their Spotify account to another user. Activities are now tracked per producer. pushActivities replaced the whole list, so the desktop game detector and Spotify would erase each other — losing exactly the case this is for, a game and Spotify at once. Polling backs off when the tab is hidden and keeps the last known track on a network error rather than reporting 'stopped listening'. A rejected refresh token (access revoked on Spotify's side) drops the row so the UI stops claiming a live connection. Scope is read-only: user-read-currently-playing and user-read-playback-state. Per the fork's language rule, the new UI ships in en and pt-BR, and this round also translates the privacy panel. |
||
|
|
688a1335cb |
feat(i18n): language foundation with en and pt-BR
Nothing in the project was translatable — every string sat inline in English. en.ts is the source dictionary and its type is derived from it, so a typo or a missing key fails typecheck instead of rendering the raw key at runtime. pt-BR.ts is deliberately Partial: translation proceeds one system per update and anything absent falls back to English, so a half-migrated interface is never broken, only partly English. Locale is persisted, guessed from the browser on first run, and kept in sync with <html lang> through a subscription — persisted state rehydrates after first paint, so a one-off assignment would miss it. Translates the voice input panel (including the mic test shipped earlier today) and the profile card as this round's system. Language options are labelled in the active language, so a wrong pick can always be undone. |