From c6f5a8bb35404be6c285bb908cc164fcdd436611 Mon Sep 17 00:00:00 2001 From: Srinivas Vaddi <38348871+vaddisrinivas@users.noreply.github.com> Date: Sat, 23 May 2026 03:57:15 -0400 Subject: [PATCH] Add custom GIF mascot avatar override (#2347) --- .../settings/panels/MascotPanel.tsx | 92 ++++++++++++++++++- .../panels/__tests__/MascotPanel.test.tsx | 37 ++++++++ app/src/features/human/HumanPage.test.tsx | 18 +++- app/src/features/human/HumanPage.tsx | 11 ++- .../human/Mascot/CustomGifMascot.test.tsx | 16 ++++ .../features/human/Mascot/CustomGifMascot.tsx | 21 +++++ app/src/features/human/Mascot/index.ts | 2 + app/src/lib/i18n/chunks/ar-5.ts | 5 + app/src/lib/i18n/chunks/bn-5.ts | 5 + app/src/lib/i18n/chunks/en-5.ts | 4 + app/src/lib/i18n/chunks/es-5.ts | 5 + app/src/lib/i18n/chunks/fr-5.ts | 5 + app/src/lib/i18n/chunks/hi-5.ts | 5 + app/src/lib/i18n/chunks/id-5.ts | 5 + app/src/lib/i18n/chunks/it-5.ts | 5 + app/src/lib/i18n/chunks/ko-5.ts | 4 + app/src/lib/i18n/chunks/pt-5.ts | 5 + app/src/lib/i18n/chunks/ru-5.ts | 5 + app/src/lib/i18n/chunks/zh-CN-5.ts | 5 + app/src/lib/i18n/en.ts | 4 + app/src/lib/i18n/ko.ts | 4 + app/src/store/__tests__/mascotSlice.test.ts | 78 ++++++++++++++++ app/src/store/index.ts | 14 +-- app/src/store/mascotSlice.ts | 55 +++++++++++ 24 files changed, 397 insertions(+), 13 deletions(-) create mode 100644 app/src/features/human/Mascot/CustomGifMascot.test.tsx create mode 100644 app/src/features/human/Mascot/CustomGifMascot.tsx diff --git a/app/src/components/settings/panels/MascotPanel.tsx b/app/src/components/settings/panels/MascotPanel.tsx index c9e105f0c..2c72de375 100644 --- a/app/src/components/settings/panels/MascotPanel.tsx +++ b/app/src/components/settings/panels/MascotPanel.tsx @@ -1,5 +1,6 @@ import { useEffect, useRef, useState } from 'react'; +import { CustomGifMascot } from '../../../features/human/Mascot'; import { BackendMascot } from '../../../features/human/Mascot/backend/BackendMascot'; import type { MascotDetail, MascotSummary } from '../../../features/human/Mascot/backend/types'; import { getMascotPalette, type MascotColor } from '../../../features/human/Mascot/mascotPalette'; @@ -9,13 +10,16 @@ import { fetchMascotList, getCachedMascotDetail } from '../../../services/mascot import { useAppDispatch, useAppSelector } from '../../../store/hooks'; import { DEFAULT_MASCOT_COLOR, + isCustomMascotGifUrl, type MascotVoiceGender, + selectCustomMascotGifUrl, selectEffectiveMascotVoiceId, selectMascotColor, selectMascotVoiceGender, selectMascotVoiceId, selectMascotVoiceUseLocaleDefault, selectSelectedMascotId, + setCustomMascotGifUrl, setMascotColor, setMascotVoiceGender, setMascotVoiceId, @@ -52,6 +56,7 @@ const MascotPanel = () => { const dispatch = useAppDispatch(); const storedColor = useAppSelector(selectMascotColor); const selectedMascotId = useAppSelector(selectSelectedMascotId); + const customMascotGifUrl = useAppSelector(selectCustomMascotGifUrl); const storedVoiceId = useAppSelector(selectMascotVoiceId); const voiceGender = useAppSelector(selectMascotVoiceGender); const useLocaleDefault = useAppSelector(selectMascotVoiceUseLocaleDefault); @@ -64,6 +69,8 @@ const MascotPanel = () => { const [backendListError, setBackendListError] = useState(null); const [activeDetail, setActiveDetail] = useState(null); const [detailError, setDetailError] = useState(null); + const [customGifDraft, setCustomGifDraft] = useState(customMascotGifUrl ?? ''); + const [customGifError, setCustomGifError] = useState(null); // Voice picker state — paste-mode is sticky because we can't derive it // from the stored value alone (a curated preset id and "user is @@ -138,6 +145,35 @@ const MascotPanel = () => { const handleSelectBackend = (id: string | null) => { dispatch(setSelectedMascotId(id)); + setCustomGifError(null); + if (id == null) { + setCustomGifDraft(''); + dispatch(setCustomMascotGifUrl(null)); + } else { + setCustomGifDraft(''); + } + }; + + const onSaveCustomGif = () => { + const trimmed = customGifDraft.trim(); + setCustomGifDraft(trimmed); + if (trimmed.length === 0) { + setCustomGifError(null); + dispatch(setCustomMascotGifUrl(null)); + return; + } + if (!isCustomMascotGifUrl(trimmed)) { + setCustomGifError(t('settings.mascot.customGifError')); + return; + } + setCustomGifError(null); + dispatch(setCustomMascotGifUrl(trimmed)); + }; + + const onResetCustomGif = () => { + setCustomGifDraft(''); + setCustomGifError(null); + dispatch(setCustomMascotGifUrl(null)); }; // Filter the menu to colors the asset pipeline currently supports — guards @@ -455,6 +491,56 @@ const MascotPanel = () => {

{t('settings.mascot.characterHeading')}

+
+ + {customGifError && ( +

+ {customGifError} +

+ )} + {customMascotGifUrl && ( +
+
+ +
+
+ )} +
{backendListError && (

@@ -477,14 +563,14 @@ const MascotPanel = () => {