diff --git a/src/frontend/src/features/rooms/livekit/components/ReactionPortal.tsx b/src/frontend/src/features/rooms/livekit/components/ReactionPortal.tsx index 40f5a51d..30dcefe5 100644 --- a/src/frontend/src/features/rooms/livekit/components/ReactionPortal.tsx +++ b/src/frontend/src/features/rooms/livekit/components/ReactionPortal.tsx @@ -6,6 +6,8 @@ import { Participant } from 'livekit-client' import { useTranslation } from 'react-i18next' import { Reaction } from '@/features/rooms/livekit/components/controls/ReactionsToggle' import { getEmojiLabel } from '@/features/rooms/livekit/utils/reactionUtils' +import { accessibilityStore } from '@/stores/accessibility' +import { useSnapshot } from 'valtio' export const ANIMATION_DURATION = 3000 export const ANIMATION_DISTANCE = 300 @@ -155,6 +157,7 @@ export function ReactionPortal({ export const ReactionPortals = ({ reactions }: { reactions: Reaction[] }) => { const { t } = useTranslation('rooms', { keyPrefix: 'controls.reactions' }) + const { announceReactions } = useSnapshot(accessibilityStore) const [announcement, setAnnouncement] = useState(null) const [lastAnnouncedId, setLastAnnouncedId] = useState(null) @@ -162,6 +165,10 @@ export const ReactionPortals = ({ reactions }: { reactions: Reaction[] }) => { reactions.length > 0 ? reactions[reactions.length - 1] : undefined useEffect(() => { + if (!announceReactions) { + setAnnouncement(null) + return + } if (!latestReaction) return const isNewReaction = latestReaction.id !== lastAnnouncedId if (!isNewReaction) return @@ -175,7 +182,7 @@ export const ReactionPortals = ({ reactions }: { reactions: Reaction[] }) => { const timer = setTimeout(() => setAnnouncement(null), 1200) return () => clearTimeout(timer) - }, [latestReaction, lastAnnouncedId, t]) + }, [latestReaction, lastAnnouncedId, announceReactions, t]) return ( <> diff --git a/src/frontend/src/stores/accessibility.ts b/src/frontend/src/stores/accessibility.ts new file mode 100644 index 00000000..39265fa6 --- /dev/null +++ b/src/frontend/src/stores/accessibility.ts @@ -0,0 +1,76 @@ +import { proxy, subscribe } from 'valtio' +import { STORAGE_KEYS } from '@/utils/storageKeys' +import { deserializeToProxyMap } from '@/utils/valtio' + +type AccessibilityState = { + announceReactions: boolean +} + +const DEFAULT_STATE: AccessibilityState = { + announceReactions: false, +} + +function getAccessibilityState(): AccessibilityState { + try { + const stored = localStorage.getItem(STORAGE_KEYS.ACCESSIBILITY) + if (stored) { + const parsed = JSON.parse(stored) + return { + ...DEFAULT_STATE, + ...parsed, + announceReactions: + typeof parsed.announceReactions === 'boolean' + ? parsed.announceReactions + : DEFAULT_STATE.announceReactions, + } + } + + // Legacy migration: if the setting was previously stored in notifications + const legacy = localStorage.getItem(STORAGE_KEYS.NOTIFICATIONS) + if (legacy) { + try { + const parsedLegacy = JSON.parse(legacy, deserializeToProxyMap) + if (typeof parsedLegacy?.announceReactions === 'boolean') { + const migratedState: AccessibilityState = { + ...DEFAULT_STATE, + ...parsedLegacy, + announceReactions: parsedLegacy.announceReactions, + } + + try { + localStorage.setItem( + STORAGE_KEYS.ACCESSIBILITY, + JSON.stringify(migratedState) + ) + localStorage.removeItem(STORAGE_KEYS.NOTIFICATIONS) + } catch { + // ignore persistence issues during migration + } + + return migratedState + } + } catch { + // ignore legacy parsing issues + } + } + + return DEFAULT_STATE + } catch (error: unknown) { + console.error( + '[AccessibilityStore] Failed to parse stored settings:', + error + ) + return DEFAULT_STATE + } +} + +export const accessibilityStore = proxy( + getAccessibilityState() +) + +subscribe(accessibilityStore, () => { + localStorage.setItem( + STORAGE_KEYS.ACCESSIBILITY, + JSON.stringify(accessibilityStore) + ) +})