diff --git a/src/app/components/GifPicker.tsx b/src/app/components/GifPicker.tsx index 57ad613cd..d9576e997 100644 --- a/src/app/components/GifPicker.tsx +++ b/src/app/components/GifPicker.tsx @@ -209,6 +209,13 @@ type GifPickerProps = { export function GifPicker({ apiKey, onSelect, requestClose }: GifPickerProps) { const [lotusTerminal] = useSetting(settingsAtom, 'lotusTerminal'); + const [gifPickerEnabled] = useSetting(settingsAtom, 'gifPickerEnabled'); + + // Searches (and every keystroke) go straight to Giphy's API, so the picker + // is opt-in (Settings → Messages) and must not render or fetch until then. + if (!gifPickerEnabled) { + return null; + } const containerStyle = lotusTerminal ? { diff --git a/src/app/features/room/RoomInput.tsx b/src/app/features/room/RoomInput.tsx index cd0ceb3ad..6dcdd03ec 100644 --- a/src/app/features/room/RoomInput.tsx +++ b/src/app/features/room/RoomInput.tsx @@ -241,7 +241,10 @@ export const RoomInput = forwardRef( const showFormat = composerToolbarButtons?.showFormat ?? true; const showEmoji = composerToolbarButtons?.showEmoji ?? true; const showSticker = composerToolbarButtons?.showSticker ?? true; - const showGif = composerToolbarButtons?.showGif ?? true; + // [Gitea #68] The GIF picker is opt-in (searches go to Giphy); hide the + // toolbar button entirely when it's off so it never opens an empty popover. + const [gifPickerEnabled] = useSetting(settingsAtom, 'gifPickerEnabled'); + const showGif = (composerToolbarButtons?.showGif ?? true) && gifPickerEnabled; const showLocation = composerToolbarButtons?.showLocation ?? true; const showPoll = composerToolbarButtons?.showPoll ?? true; const showVoice = composerToolbarButtons?.showVoice ?? true; diff --git a/src/app/features/settings/general/General.tsx b/src/app/features/settings/general/General.tsx index 67c50852a..32b439332 100644 --- a/src/app/features/settings/general/General.tsx +++ b/src/app/features/settings/general/General.tsx @@ -1,5 +1,6 @@ import React, { ChangeEventHandler, + FocusEventHandler, FormEventHandler, KeyboardEventHandler, MouseEventHandler, @@ -395,6 +396,16 @@ function PageZoomInput() { setCurrentZoom(evt.target.value); }; + // Shared by Enter and blur so a typed-but-unconfirmed value is never + // silently dropped when the input loses focus. + const commitZoom = (value: string) => { + const newZoom = parseInt(value, 10); + if (Number.isNaN(newZoom)) return; + const safeZoom = Math.max(Math.min(newZoom, 150), 75); + setPageZoom(safeZoom); + setCurrentZoom(safeZoom.toString()); + }; + const handleZoomEnter: KeyboardEventHandler = (evt) => { if (isKeyHotkey('escape', evt)) { evt.stopPropagation(); @@ -405,14 +416,14 @@ function PageZoomInput() { 'value' in evt.target && typeof evt.target.value === 'string' ) { - const newZoom = parseInt(evt.target.value, 10); - if (Number.isNaN(newZoom)) return; - const safeZoom = Math.max(Math.min(newZoom, 150), 75); - setPageZoom(safeZoom); - setCurrentZoom(safeZoom.toString()); + commitZoom(evt.target.value); } }; + const handleZoomBlur: FocusEventHandler = (evt) => { + commitZoom(evt.target.value); + }; + return ( %} outlined /> @@ -2344,6 +2356,7 @@ function Messages() { 'translateTargetLang', ); const [autoTranslate, setAutoTranslate] = useSetting(settingsAtom, 'autoTranslate'); + const [gifPickerEnabled, setGifPickerEnabled] = useSetting(settingsAtom, 'gifPickerEnabled'); const translationSupported = chromeTranslationEngine.isSupported(); const selectedTargetLang = isSupportedTargetLang(translateTargetLang) ? normalizeLang(translateTargetLang) @@ -2446,6 +2459,15 @@ function Messages() { } /> + + + } + /> + { + setStored(null); + const defaults = getSettings(); + assert.equal(defaults.seasonalThemeOverride, 'auto'); + + setStored(JSON.stringify({ seasonalThemeOverride: 'retired-theme' })); + assert.equal(getSettings().seasonalThemeOverride, 'auto'); + + setStored(JSON.stringify({ seasonalThemeOverride: 'earthday' })); + assert.equal(getSettings().seasonalThemeOverride, 'earthday'); +}); + test('merges stored values over defaults', () => { setStored(JSON.stringify({ callNoiseSuppression: 'off', someUnknownKey: 1 })); const s = getSettings(); diff --git a/src/app/state/settings.ts b/src/app/state/settings.ts index 1845e1956..75a0ad696 100644 --- a/src/app/state/settings.ts +++ b/src/app/state/settings.ts @@ -25,6 +25,38 @@ export type DenoiseModelId = 'rnnoise' | 'speex' | 'dtln' | 'deepfilternet'; // 'soft' / 'retro' are synthesized in-browser (see utils/ringtones.ts); // 'none' is silent (visual-only incoming-call UI). export type RingtoneId = 'classic' | 'chime' | 'soft' | 'retro' | 'none'; + +export type SeasonalThemeOverride = + | 'auto' + | 'off' + | 'halloween' + | 'christmas' + | 'newyear' + | 'autumn' + | 'aprilfools' + | 'lunar' + | 'valentines' + | 'stpatricks' + | 'earthday' + | 'deepspace' + | 'arcade'; + +// Allow-list used to validate persisted values below. +const SEASONAL_THEME_OVERRIDES: SeasonalThemeOverride[] = [ + 'auto', + 'off', + 'halloween', + 'christmas', + 'newyear', + 'autumn', + 'aprilfools', + 'lunar', + 'valentines', + 'stpatricks', + 'earthday', + 'deepspace', + 'arcade', +]; // [P5-31] Granular call quality caps. 'auto' = don't cap (the EC fork keeps its // default encoding). Numbers are kbps (audio/screenshare bitrate) or fps // (screenshare framerate); converted to the fork's bits/sec + fps payload in @@ -254,24 +286,15 @@ export interface Settings { soundboardEnabled: boolean; soundboardVolume: number; // 0–100 - seasonalThemeOverride: - | 'auto' - | 'off' - | 'halloween' - | 'christmas' - | 'newyear' - | 'autumn' - | 'aprilfools' - | 'lunar' - | 'valentines' - | 'stpatricks' - | 'earthday' - | 'deepspace' - | 'arcade'; + seasonalThemeOverride: SeasonalThemeOverride; // On-device message translation translateTargetLang: string; // BCP-47 base code, default 'en' autoTranslate: boolean; // auto-translate incoming foreign messages (opt-in) + + // GIF picker sends every search term (and the user's IP) directly to Giphy, + // so it's opt-in and off by default. + gifPickerEnabled: boolean; } const defaultSettings: Settings = { @@ -374,6 +397,8 @@ const defaultSettings: Settings = { translateTargetLang: 'en', autoTranslate: false, + + gifPickerEnabled: false, }; export const getSettings = (): Settings => { @@ -422,6 +447,12 @@ export const getSettings = (): Settings => { typeof saved.autoTranslate === 'boolean' ? saved.autoTranslate : defaultSettings.autoTranslate, + // Coerce any unknown/retired persisted seasonal theme id back to 'auto'. + seasonalThemeOverride: SEASONAL_THEME_OVERRIDES.includes( + saved.seasonalThemeOverride as SeasonalThemeOverride, + ) + ? (saved.seasonalThemeOverride as SeasonalThemeOverride) + : defaultSettings.seasonalThemeOverride, composerToolbarButtons: { ...DEFAULT_COMPOSER_TOOLBAR, ...(saved.composerToolbarButtons ?? {}),