feat: backup/restore, user profile popover, image compress, video blur, wake lock

- backup/restore dialog + user profile popover components
- image compression, video blur, wake lock utilities
- message cache + conversation messages hook refinements
- call context, active speakers, screen share dialog tweaks
- audio + screen share settings persistence
- refreshed app icons (smaller sizes) across all platforms
This commit is contained in:
2026-04-21 12:11:09 +02:00
parent 48ac9d2922
commit 1303c8e26f
71 changed files with 1077 additions and 114 deletions
+117 -8
View File
@@ -9,6 +9,7 @@ import { useTranslation } from 'react-i18next';
import { Avatar } from '../components/Avatar';
import { BackupExportDialog } from '../components/BackupExportDialog';
import { BackupRestoreDialog } from '../components/BackupRestoreDialog';
import { RingtoneSettings } from '../components/RingtoneSettings';
import { SoundboardSettings } from '../components/SoundboardSettings';
import { LockIcon } from '../components/icons';
@@ -347,10 +348,99 @@ function AudioQualityControls() {
'Mono 48 kbps mit Noise-Suppression, Echo-Cancellation und Auto-Gain. Optimiert für Sprache im Raum.',
})}
</p>
<SettingRow
label={t('app:settings.noise_suppression', { defaultValue: 'Noise Suppression' })}
>
<InlineToggle
checked={cfg.noiseSuppression}
onChange={(v) => updateAudioSettings({ noiseSuppression: v })}
/>
</SettingRow>
<p className="text-xs text-fg-muted">
{t('app:settings.noise_suppression_hint', {
defaultValue:
'Unterdrückt Hintergrundgeräusche (Tastatur, Lüfter, Café-Lärm). Ausschalten nur bei Musik/Instrumenten.',
})}
</p>
<SettingRow
label={t('app:settings.video_blur', {
defaultValue: 'Video-Hintergrund unscharf',
})}
>
<InlineToggle
checked={cfg.videoBackgroundBlur}
onChange={(v) => updateAudioSettings({ videoBackgroundBlur: v })}
/>
</SettingRow>
<p className="text-xs text-fg-muted">
{t('app:settings.video_blur_hint', {
defaultValue:
'Blendet den Hintergrund hinter dir aus. Braucht etwas GPU-Leistung und lädt beim ersten Aktivieren ~1,5 MB Modell nach.',
})}
</p>
<SettingRow
label={t('app:settings.voice_threshold', {
defaultValue: 'Sprach-Erkennungs-Schwelle',
})}
>
<div className="flex w-48 items-center gap-2">
<input
type="range"
min={0.005}
max={0.1}
step={0.005}
value={cfg.voiceThreshold}
onChange={(e) =>
updateAudioSettings({ voiceThreshold: Number(e.target.value) })
}
className="flex-1 accent-accent"
/>
<span className="w-10 tabular-nums text-right text-[11px] text-fg-muted">
{(cfg.voiceThreshold * 100).toFixed(1)}
</span>
</div>
</SettingRow>
<p className="text-xs text-fg-muted">
{t('app:settings.voice_threshold_hint', {
defaultValue:
'Wann der grüne Sprech-Ring aufleuchtet. Niedriger = empfindlicher (leise Stimme erfassen), höher = tolerant gegen Raumlärm.',
})}
</p>
</>
);
}
function InlineToggle({
checked,
onChange,
}: {
checked: boolean;
onChange: (next: boolean) => void;
}) {
return (
<button
type="button"
role="switch"
aria-checked={checked}
onClick={() => onChange(!checked)}
className={
'relative inline-flex h-6 w-11 shrink-0 cursor-pointer rounded-full transition focus:outline-none focus-visible:ring-2 focus-visible:ring-accent/40 ' +
(checked ? 'bg-accent' : 'bg-surface')
}
>
<span
className={
'absolute left-0.5 top-0.5 h-5 w-5 rounded-full bg-white shadow transition ' +
(checked ? 'translate-x-5' : '')
}
/>
</button>
);
}
interface AvatarControlsProps {
patchProfile: (patch: Parameters<typeof updateOwnProfile>[1]) => Promise<void>;
busy: boolean;
@@ -464,6 +554,7 @@ function DeviceKeyBackupControls() {
const { t } = useTranslation(['app']);
const { profile, device } = useAuth();
const [open, setOpen] = useState(false);
const [restoreOpen, setRestoreOpen] = useState(false);
const [privateKey, setPrivateKey] = useState<Uint8Array | null>(null);
const [err, setErr] = useState<string | null>(null);
const canRun = !!profile?.userId && !!device?.id;
@@ -502,14 +593,24 @@ function DeviceKeyBackupControls() {
'Backup deines Geräteschlüssels. Ohne Backup kein Zugriff auf alte Nachrichten wenn Gerät oder Browser-Storage verloren geht. Wiederherstellung passiert im Gerät-Einrichtungsbildschirm.',
})}
</p>
<button
type="button"
disabled={!canRun}
onClick={() => void handleOpen()}
className="mt-3 inline-flex cursor-pointer items-center gap-1.5 rounded-lg bg-accent px-3 py-2 text-sm font-semibold text-accent-fg transition hover:brightness-110 disabled:cursor-not-allowed disabled:opacity-50"
>
{t('app:backup.export_open', { defaultValue: 'Backup erstellen' })}
</button>
<div className="mt-3 flex flex-wrap gap-2">
<button
type="button"
disabled={!canRun}
onClick={() => void handleOpen()}
className="inline-flex cursor-pointer items-center gap-1.5 rounded-lg bg-accent px-3 py-2 text-sm font-semibold text-accent-fg transition hover:brightness-110 disabled:cursor-not-allowed disabled:opacity-50"
>
{t('app:backup.export_open', { defaultValue: 'Backup erstellen' })}
</button>
<button
type="button"
disabled={!profile?.userId}
onClick={() => setRestoreOpen(true)}
className="inline-flex cursor-pointer items-center gap-1.5 rounded-lg border border-line bg-surface-2 px-3 py-2 text-sm font-semibold text-fg transition hover:bg-surface-3 disabled:cursor-not-allowed disabled:opacity-50"
>
{t('app:backup.restore_open', { defaultValue: 'Backup wiederherstellen' })}
</button>
</div>
{err && (
<p className="mt-2 text-xs text-rose-600 dark:text-rose-300">{err}</p>
)}
@@ -523,6 +624,14 @@ function DeviceKeyBackupControls() {
onClose={handleClose}
/>
)}
{profile && (
<BackupRestoreDialog
open={restoreOpen}
userId={profile.userId}
onClose={() => setRestoreOpen(false)}
/>
)}
</div>
);
}