diff --git a/src/app/features/settings/general/General.tsx b/src/app/features/settings/general/General.tsx index 9a03578da..6e75be3ae 100644 --- a/src/app/features/settings/general/General.tsx +++ b/src/app/features/settings/general/General.tsx @@ -1593,7 +1593,19 @@ export function Privacy() { settingsAtom, 'warnOnUnverifiedDevices', ); + const [urlPreview, setUrlPreview] = useSetting(settingsAtom, 'urlPreview'); + const [encUrlPreview, setEncUrlPreview] = useSetting(settingsAtom, 'encUrlPreview'); const [stripTracking, setStripTracking] = useSetting(settingsAtom, 'stripTrackingParams'); + // [Gitea #461] One choice instead of two overlapping switches. Maps onto the + // existing settings, so every old combination keeps its behaviour: + // hideActivity also hides typing and other people's receipts from you. + let activity: 'everyone' | 'private' | 'hidden' = 'everyone'; + if (hideActivity) activity = 'hidden'; + else if (privateReadReceipts) activity = 'private'; + const chooseActivity = (next: typeof activity) => { + setHideActivity(next === 'hidden'); + setPrivateReadReceipts(next !== 'everyone'); + }; const [stripImageMeta, setStripImageMeta] = useSetting(settingsAtom, 'stripImageMetadata'); return ( @@ -1615,24 +1627,87 @@ export function Privacy() { } + title="Link previews" + description="Your homeserver fetches and caches link previews. It will see the URLs of all links you preview." + after={} /> + title="Link previews in encrypted rooms" + description={ + + + Your homeserver fetches link previews on your behalf. It cannot decrypt your + messages, but will see every URL you preview in encrypted rooms, potentially + revealing conversation topics. + + + Privacy risk — enabled by default + + } + after={} /> + + + + {( + [ + ['everyone', 'Everyone', "Others see what you've read and when you're typing."], + [ + 'private', + 'Private read receipts', + "Others don't see what you've read (only you and your server do). Typing still shows.", + ], + [ + 'hidden', + 'Hide my activity', + "No typing indicator and private read receipts. You also won't see other people's read receipts.", + ], + ] as const + ).map(([value, label, help]) => ( + + ))} + + m.id === callDenoiseModel); return ( - - Calls - - } - /> - - - - {/* [Gitea #271] T200 like every other setting description. */} - - Filter background noise from your mic during calls. Browser-native uses the built-in - WebRTC suppressor (Google NSNet2). ML runs a dedicated model for stronger removal. - - - {!mlSupported && ( - - - ML options are not supported in this browser. - - - {ML_DENOISE_REQUIREMENTS.map((req) => ( - - {req} - - ))} - - - )} - - } - after={ - - value={callNoiseSuppression} - onChange={setCallNoiseSuppression} - options={[ - { value: 'off', label: 'Off' }, - { value: 'browser', label: 'Browser-native' }, - { - value: 'ml', - label: 'ML (Advanced)', - disabled: !mlSupported, - }, - ]} - /> - } - /> - - {callNoiseSuppression === 'ml' && ( - - {/* ── Model selection ───────────────────────────────────────── */} - - Model - - value={callDenoiseModel} - onChange={setCallDenoiseModel} - options={DENOISE_MODELS.map((m) => ({ value: m.id, label: m.name }))} - /> - } - /> - {selectedDenoiseModel && ( - - {selectedDenoiseModel.name} - - {selectedDenoiseModel.description} - - - {( - [ - { label: 'CPU', value: selectedDenoiseModel.cpuUsage }, - { label: 'Voice quality', value: selectedDenoiseModel.voiceQuality }, - { label: 'Transients', value: selectedDenoiseModel.transients }, - { label: 'Download', value: selectedDenoiseModel.binarySize }, - ] as const - ).map((stat) => ( - - - {stat.label} - - {stat.value} - - ))} - - - )} - -
- - - Compare all models - - - - - - - Model - - - - - CPU - - - - - Quality - - - - - Transients - - - - {DENOISE_MODELS.map((model) => ( - - - {model.name} - - - {model.cpuUsage} - - - {model.voiceQuality} - - - {model.transients} - - - ))} - -
- - - Note: Applying changes requires rejoining the call. - -
- - {/* ── Enhancement toggles ───────────────────────────────────── */} - - Enhancements - - } - /> - - - } - /> - - {callDenoiseGate && ( - - - Gate Threshold - {callDenoiseGateThreshold} dB - - setCallDenoiseGateThreshold(parseInt(e.target.value, 10))} - style={{ width: '100%', accentColor: color.Primary.Main }} - /> - - )} - - - {/* ── Test & calibrate ──────────────────────────────────────── */} - - Test and calibrate - - Audition the selected model and tune the gate without joining a call. Changes to the - settings above apply the next time you start a monitor or play a clip. - - - -
- )} -
- - - value={callRejoin} - onChange={setCallRejoin} - options={[ - { value: 'ask', label: 'Ask' }, - { value: 'auto', label: 'Rejoin automatically' }, - { value: 'off', label: 'Do nothing' }, - ]} - /> - } - /> - - } - /> - {hapticsSupported() && ( + // [Gitea #462] Fixed groups in every mode (it was one flat list). + + + Joining + } + title="Join with camera on" + description="Enable camera automatically when joining a voice or video call. Camera is off by default." + after={} /> - )} - } - /> - {pttMode && ( + + - {pttBind.listening ? 'Press a key…' : keyLabel(pttKey)} - - } - /> - )} - - - {deafenHotkey && ( - - )} - - } - /> - {isTauriEnv() && ( - - } - /> - )} - - - - } - /> - } - /> - {afkAutoMute && ( - setAfkTimeoutMinutes(Number(v))} + + value={callRejoin} + onChange={setCallRejoin} options={[ - { value: '1', label: '1 minute' }, - { value: '5', label: '5 minutes' }, - { value: '10', label: '10 minutes' }, - { value: '20', label: '20 minutes' }, - { value: '30', label: '30 minutes' }, + { value: 'ask', label: 'Ask' }, + { value: 'auto', label: 'Rejoin automatically' }, + { value: 'off', label: 'Do nothing' }, ]} /> } /> - )} - - - handleRingtoneChange(v as RingtoneId)} - options={RINGTONE_OPTIONS} - /> - } - /> - - - - - setRingtoneVolume(parseInt(e.target.value, 10))} - aria-label="Ringtone volume" - style={{ flex: 1, accentColor: color.Primary.Main }} + + } /> - - {ringtoneVolume}% - - - - - handleJoinLeaveSoundChange(v as 'off' | 'chime' | 'soft' | 'retro')} - options={[ - { value: 'off', label: 'Off' }, - { value: 'chime', label: 'Chime' }, - { value: 'soft', label: 'Soft' }, - { value: 'retro', label: 'Retro' }, - ]} - /> - } - /> - + + + + Microphone and noise + + + {/* [Gitea #271] T200 like every other setting description. */} + + Filter background noise from your mic during calls. Browser-native uses the + built-in WebRTC suppressor (Google NSNet2). ML runs a dedicated model for stronger + removal. + - - - value={callAudioBitrate} - onChange={setCallAudioBitrate} - options={AUDIO_BITRATE_OPTIONS} - /> - } - /> - - - - - value={screenshareBitrate} - onChange={setScreenshareBitrate} - options={SCREENSHARE_BITRATE_OPTIONS} - /> - } - /> - - - - - value={screenshareFramerate} - onChange={setScreenshareFramerate} - options={SCREENSHARE_FRAMERATE_OPTIONS} - /> - } - /> - - - - - } - /> - {soundboardEnabled && ( - <> - - - setSoundboardVolume(parseInt(e.target.value, 10))} - style={{ flexGrow: 1 }} - aria-label="Soundboard volume" + {!mlSupported && ( + + + ML options are not supported in this browser. + + + {ML_DENOISE_REQUIREMENTS.map((req) => ( + + {req} + + ))} + + + )} + + } + after={ + + value={callNoiseSuppression} + onChange={setCallNoiseSuppression} + options={[ + { value: 'off', label: 'Off' }, + { value: 'browser', label: 'Browser-native' }, + { + value: 'ml', + label: 'ML (Advanced)', + disabled: !mlSupported, + }, + ]} /> - - {soundboardVolume}% - + } + /> + + {callNoiseSuppression === 'ml' && ( + + {/* ── Model selection ───────────────────────────────────────── */} + + Model + + value={callDenoiseModel} + onChange={setCallDenoiseModel} + options={DENOISE_MODELS.map((m) => ({ value: m.id, label: m.name }))} + /> + } + /> + {selectedDenoiseModel && ( + + {selectedDenoiseModel.name} + + {selectedDenoiseModel.description} + + + {( + [ + { label: 'CPU', value: selectedDenoiseModel.cpuUsage }, + { label: 'Voice quality', value: selectedDenoiseModel.voiceQuality }, + { label: 'Transients', value: selectedDenoiseModel.transients }, + { label: 'Download', value: selectedDenoiseModel.binarySize }, + ] as const + ).map((stat) => ( + + + {stat.label} + + {stat.value} + + ))} + + + )} + +
+ + + Compare all models + + + + + + + Model + + + + + CPU + + + + + Quality + + + + + Transients + + + + {DENOISE_MODELS.map((model) => ( + + + {model.name} + + + {model.cpuUsage} + + + {model.voiceQuality} + + + {model.transients} + + + ))} + +
+ + + Note: Applying changes requires rejoining the call. + +
+ + {/* ── Enhancement toggles ───────────────────────────────────── */} + + Enhancements + + } + /> + + + } + /> + + {callDenoiseGate && ( + + + Gate Threshold + {callDenoiseGateThreshold} dB + + setCallDenoiseGateThreshold(parseInt(e.target.value, 10))} + style={{ width: '100%', accentColor: color.Primary.Main }} + /> + + )} + + + {/* ── Test & calibrate ──────────────────────────────────────── */} + + Test and calibrate + + Audition the selected model and tune the gate without joining a call. Changes to + the settings above apply the next time you start a monitor or play a clip. + + +
- - )} -
+ )} +
+ + + } + /> + } + /> + {afkAutoMute && ( + setAfkTimeoutMinutes(Number(v))} + options={[ + { value: '1', label: '1 minute' }, + { value: '5', label: '5 minutes' }, + { value: '10', label: '10 minutes' }, + { value: '20', label: '20 minutes' }, + { value: '30', label: '30 minutes' }, + ]} + /> + } + /> + )} + +
+ + Push to talk and hotkeys + + } + /> + {pttMode && ( + + {pttBind.listening ? 'Press a key…' : keyLabel(pttKey)} + + } + /> + )} + + + {deafenHotkey && ( + + )} + + } + /> + {isTauriEnv() && ( + + } + /> + )} + {hapticsSupported() && ( + } + /> + )} + + + + Sounds + + handleRingtoneChange(v as RingtoneId)} + options={RINGTONE_OPTIONS} + /> + } + /> + + + + + setRingtoneVolume(parseInt(e.target.value, 10))} + aria-label="Ringtone volume" + style={{ flex: 1, accentColor: color.Primary.Main }} + /> + + {ringtoneVolume}% + + + + + + handleJoinLeaveSoundChange(v as 'off' | 'chime' | 'soft' | 'retro') + } + options={[ + { value: 'off', label: 'Off' }, + { value: 'chime', label: 'Chime' }, + { value: 'soft', label: 'Soft' }, + { value: 'retro', label: 'Retro' }, + ]} + /> + } + /> + + + + Quality + + + value={callAudioBitrate} + onChange={setCallAudioBitrate} + options={AUDIO_BITRATE_OPTIONS} + /> + } + /> + + + + value={screenshareBitrate} + onChange={setScreenshareBitrate} + options={SCREENSHARE_BITRATE_OPTIONS} + /> + } + /> + + + + value={screenshareFramerate} + onChange={setScreenshareFramerate} + options={SCREENSHARE_FRAMERATE_OPTIONS} + /> + } + /> + + + + Soundboard + + + } + /> + {soundboardEnabled && ( + <> + + + setSoundboardVolume(parseInt(e.target.value, 10))} + style={{ flexGrow: 1 }} + aria-label="Soundboard volume" + /> + + {soundboardVolume}% + + + + )} + + ); } @@ -2736,8 +2848,6 @@ export function Messages() { 'hideNickAvatarEvents', ); const [mediaAutoLoad, setMediaAutoLoad] = useSetting(settingsAtom, 'mediaAutoLoad'); - const [urlPreview, setUrlPreview] = useSetting(settingsAtom, 'urlPreview'); - const [encUrlPreview, setEncUrlPreview] = useSetting(settingsAtom, 'encUrlPreview'); const [inlineMediaEmbeds, setInlineMediaEmbeds] = useSetting(settingsAtom, 'inlineMediaEmbeds'); const [enforceRetentionLocally, setEnforceRetentionLocally] = useSetting( settingsAtom, @@ -2801,37 +2911,6 @@ export function Messages() { } /> - - } - /> - - - - - Your homeserver fetches link previews on your behalf. It cannot decrypt your - messages, but will see every URL you preview in encrypted rooms, potentially - revealing conversation topics. - - - Privacy risk — enabled by default - - - } - after={} - /> -