Hover an image attachment in the composer to reveal a pencil-edit button;
clicking it opens ImageAnnotator and Save replaces the File in attachments[].
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Shrink the StripToggleIcon SVG from 18x18 to 14x14 px so the 2-people
icon sits centred with ~11 px of breathing room inside its 36x36 button,
matching the h-4/w-4 small-icon convention used elsewhere in InCallPanel.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Pass width/height as { ideal: N } constraints (not exact values) so
Chromium preserves the source's aspect ratio instead of cropping
non-matching monitors. Previously a 1920x1200 monitor under a 1080p
preset lost the bottom 120px including the Windows taskbar.
The auto-preset fallback now omits width/height entirely (both set to
undefined) so getDisplayMedia uses the screen's native resolution.
Cast through `unknown` to satisfy LiveKit's VideoResolution type, which
declares width/height as plain number but Chromium accepts the full
MediaTrackConstraints shape at runtime.
Adds useOwnDevices React hook that fetches the user device list via
listOwnDevices and re-fetches on any postgres_changes event on the
devices table, matching the channel/filter convention in AuthContext.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Google closed Tenor v2 to new API clients in Jan 2026, so the only
people who could use the picker were those with a pre-existing Google
Cloud Console key. Swapped to GIPHY's Developer API (still open, free
keys at https://developers.giphy.com/dashboard/).
- Env var renamed VITE_TENOR_API_KEY → VITE_GIPHY_API_KEY
- Endpoint, response mapping, error sentinel updated
- File still named tenor.ts for import-path stability — renaming
later if it bothers anyone
- Public GifResult interface unchanged so the picker UI didn't need
edits beyond the error-message switch
The old code registered every enabled hotkey through Electron globalShortcut
API, which captures system-wide. Setting M as mute meant m could not be
typed in any other app. Now the OS-level registration only happens when
binding.global === true; otherwise the existing window-keydown listener
handles it.
The inner cards (Nachrichten-Ton / Klingelton / Audio-Gerät / Hotkeys /
…) looked like they had a stark white outline. The cause: --color-line
is a semi-transparent white token, and applying the `/60` alpha modifier
in `border-line/60` overrides the original alpha — so the inner border
ended up brighter than the outer Section's normal `border-line`.
Drop the border entirely. Background tint + caps heading is enough
grouping signal in a tab-pattern panel.
The previous build used IntersectionObserver to highlight whichever
section was in view. With nine sections of unequal heights and smooth-
scroll firing observer callbacks mid-scroll, the active highlight
drifted (clicking 'Konto' showed 'Soundboard' as active because the
last section never crossed the observer's 20%-30% band).
Switched to a tab pattern (macOS System Settings / Discord / GitHub
style): the sidebar selects ONE section, only that section renders.
`useState<TabId>` is the single source of truth; no observer, no
scrolling between sections, no anchor links to clash with HashRouter.
Mobile fallback (<lg) gets a `<select>` dropdown above the panel.
Drops the now-unused `id` prop from Section and removes the
IntersectionObserver effect.
App uses HashRouter, so an <a href="#profile"> changes the routing
hash and the router can't find a match — it falls back to /chats.
Replace anchors with buttons that scroll the target section via
scrollIntoView and update the active highlight optimistically.
Two UI fixes:
1. MessageBubble: 'Nachricht nicht lesbar' was rendered with
text-fg-muted on the blue 'mine' bubble — invisible. Now uses
text-accent-fg/80 on mine, text-fg-muted on peer (still
≥4.5:1 contrast in both modes).
2. SettingsPage: redesigned from a long single-column scroll into a
sticky-sidebar + content layout (lg+) with:
- 9 anchor-linked sections with icons in the sidebar
- IntersectionObserver highlights the active section
- Each section has a description subtitle for context
- Voice (the densest section) is now sub-grouped into Audio-Gerät /
Qualität / PTT / Hotkeys / E2EE via SubSection cards
- Notifications consolidates message-sound + ringtone
- Danger-toned account section visually separated
- Mobile fallback is the original single-column scroll
Root cause of "alle Nachrichten verschlüsselt + kann nicht schreiben":
uploadUserKeyBlob (called by setupNewUserIdentity, changePin and
regenerateRecoveryCode) routed through reset_user_key, which DELETES
every conversation_keys row addressed to the user or one of their
devices. So setting a PIN destroyed every legacy bundle BEFORE the
migration could re-wrap them. The user ended up with user_keys set,
zero un-migrated bundles, no decryption, no send.
Fixes shipped:
* supabase/migrations/20260516000001_user_key_rpcs_v2.sql
- upsert_user_key: same UPSERT, NO delete. Used everywhere except
"Identität zurücksetzen" (which keeps reset_user_key on purpose).
- rotate_conv_key: bumps active_key_version atomically and inserts
a fresh batch of bundles (per-user + per-device fallback).
* shared/auth/userKey.ts: uploadUserKeyBlob now calls upsert_user_key.
* shared/chat/convKeys.ts: new rotateConvKey() that wraps the fresh
conv-key for every member's user_keys (preferred) and falls back to
each member's per-device public_key for peers still on 0.17.x.
* shared/chat/convKeys.ts: getOrCreateConvKey auto-triggers rotate
when the user has no recipient_user_id row at the active version
but rows exist (the deadlock case). Existing outbox retries drain
on their own once the rotate completes — no manual button.
* desktop/MessageBubble.tsx: "...cannot decrypt" is now a softer,
German "Nachricht nicht lesbar" so users don't think the app
crashed when historical messages can't be unwrapped.