From 081221731e21a224c9070491e1bdc577eb9b32ff Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Mon, 9 Dec 2024 14:58:51 +0000 Subject: [PATCH] Convert reaction sounds to call view model / rxjs --- src/room/InCallView.tsx | 2 +- src/room/ReactionAudioRenderer.tsx | 42 ++++++++++++++---------------- src/state/CallViewModel.ts | 40 +++++++++++++++++++++++----- 3 files changed, 54 insertions(+), 30 deletions(-) diff --git a/src/room/InCallView.tsx b/src/room/InCallView.tsx index b1382047..7dbc6aec 100644 --- a/src/room/InCallView.tsx +++ b/src/room/InCallView.tsx @@ -642,7 +642,7 @@ export const InCallView: FC = ({ {renderContent()} - + {footer} {layout.type !== "pip" && ( diff --git a/src/room/ReactionAudioRenderer.tsx b/src/room/ReactionAudioRenderer.tsx index 15bfc90f..1d9f8daa 100644 --- a/src/room/ReactionAudioRenderer.tsx +++ b/src/room/ReactionAudioRenderer.tsx @@ -13,6 +13,7 @@ import { GenericReaction, ReactionSet } from "../reactions"; import { useAudioContext } from "../useAudioContext"; import { prefetchSounds } from "../soundUtils"; import { useLatest } from "../useLatest"; +import { CallViewModel } from "../state/CallViewModel"; const soundMap = Object.fromEntries([ ...ReactionSet.filter((v) => v.sound !== undefined).map((v) => [ @@ -22,8 +23,11 @@ const soundMap = Object.fromEntries([ [GenericReaction.name, GenericReaction.sound], ]); -export function ReactionsAudioRenderer(): ReactNode { - const { reactions } = useReactions(); +export function ReactionsAudioRenderer({ + vm, +}: { + vm: CallViewModel; +}): ReactNode { const [shouldPlay] = useSetting(playReactionsSound); const [soundCache, setSoundCache] = useState { if (!shouldPlay || soundCache) { @@ -46,26 +49,19 @@ export function ReactionsAudioRenderer(): ReactNode { }, [soundCache, shouldPlay]); useEffect(() => { - if (!shouldPlay || !audioEngineRef.current) { - return; - } - const oldReactionSet = new Set( - Object.values(oldReactions).map((r) => r.name), - ); - for (const reactionName of new Set( - Object.values(reactions).map((r) => r.name), - )) { - if (oldReactionSet.has(reactionName)) { - // Don't replay old reactions - return; + const sub = vm.audibleReactions.subscribe((newReactions) => { + for (const reactionName of newReactions) { + if (soundMap[reactionName]) { + audioEngineRef.current?.playSound(reactionName); + } else { + // Fallback sounds. + audioEngineRef.current?.playSound("generic"); + } } - if (soundMap[reactionName]) { - audioEngineRef.current.playSound(reactionName); - } else { - // Fallback sounds. - audioEngineRef.current.playSound("generic"); - } - } - }, [audioEngineRef, shouldPlay, oldReactions, reactions]); + }); + return (): void => { + sub.unsubscribe(); + }; + }, [vm, audioEngineRef]); return null; } diff --git a/src/state/CallViewModel.ts b/src/state/CallViewModel.ts index c695170a..d9f12348 100644 --- a/src/state/CallViewModel.ts +++ b/src/state/CallViewModel.ts @@ -26,6 +26,7 @@ import { Subject, combineLatest, concat, + distinct, distinctUntilChanged, filter, forkJoin, @@ -66,7 +67,11 @@ import { } from "./MediaViewModel"; import { accumulate, finalizeValue } from "../utils/observable"; import { ObservableScope } from "./ObservableScope"; -import { duplicateTiles, showReactions } from "../settings/settings"; +import { + duplicateTiles, + playReactionsSound, + showReactions, +} from "../settings/settings"; import { isFirefox } from "../Platform"; import { setPipEnabled } from "../controls"; import { GridTileViewModel, SpotlightTileViewModel } from "./TileViewModel"; @@ -1101,12 +1106,9 @@ export class CallViewModel extends ViewModel { this.reactions.next(data.reactions); } - public readonly visibleReactions = combineLatest([ - this.reactions, - showReactions.value, - ]) + public readonly visibleReactions = showReactions.value + .pipe(switchMap((show) => (show ? this.reactions : of({})))) .pipe( - map(([reactions, setting]) => (setting ? reactions : {})), scan< Record, { sender: string; emoji: string; startX: number }[] @@ -1123,6 +1125,32 @@ export class CallViewModel extends ViewModel { ) .pipe(this.scope.state()); + public readonly audibleReactions = playReactionsSound.value + .pipe( + switchMap((show) => + show ? this.reactions : of>({}), + ), + ) + .pipe( + map((reactions) => Object.values(reactions).map((v) => v.name)), + scan( + (acc, latest) => { + return { + playing: latest.filter( + (v) => acc.playing.includes(v) || acc.newSounds.includes(v), + ), + newSounds: latest.filter( + (v) => !acc.playing.includes(v) && !acc.newSounds.includes(v), + ), + }; + }, + { playing: [], newSounds: [] }, + ), + map((v) => v.newSounds), + distinct(), + ) + .pipe(this.scope.state()); + public constructor( // A call is permanently tied to a single Matrix room and LiveKit room private readonly matrixRTCSession: MatrixRTCSession,