Add support for using CallViewModel for reactions sounds.

This commit is contained in:
Half-Shot
2024-12-09 14:36:25 +00:00
parent a8a95c3f00
commit 865187d58e
4 changed files with 64 additions and 31 deletions

View File

@@ -41,6 +41,7 @@ import { InviteModal } from "./InviteModal";
import { useUrlParams } from "../UrlParams"; import { useUrlParams } from "../UrlParams";
import { E2eeType } from "../e2ee/e2eeType"; import { E2eeType } from "../e2ee/e2eeType";
import { Link } from "../button/Link"; import { Link } from "../button/Link";
import { ReactionsProvider } from "../useReactions";
declare global { declare global {
interface Window { interface Window {
@@ -328,18 +329,20 @@ export const GroupCallView: FC<Props> = ({
return ( return (
<> <>
{shareModal} {shareModal}
<ActiveCall <ReactionsProvider rtcSession={rtcSession}>
client={client} <ActiveCall
matrixInfo={matrixInfo} client={client}
rtcSession={rtcSession as unknown as MatrixRTCSession} matrixInfo={matrixInfo}
participantCount={participantCount} rtcSession={rtcSession as MatrixRTCSession}
onLeave={onLeave} participantCount={participantCount}
hideHeader={hideHeader} onLeave={onLeave}
muteStates={muteStates} hideHeader={hideHeader}
e2eeSystem={e2eeSystem} muteStates={muteStates}
//otelGroupCallMembership={otelGroupCallMembership} e2eeSystem={e2eeSystem}
onShareClick={onShareClick} //otelGroupCallMembership={otelGroupCallMembership}
/> onShareClick={onShareClick}
/>
</ReactionsProvider>
</> </>
); );
} else if (left && widget === null) { } else if (left && widget === null) {

View File

@@ -107,6 +107,7 @@ export const ActiveCall: FC<ActiveCallProps> = (props) => {
[connState], [connState],
); );
const [vm, setVm] = useState<CallViewModel | null>(null); const [vm, setVm] = useState<CallViewModel | null>(null);
const reactions = useReactions();
useEffect(() => { useEffect(() => {
return (): void => { return (): void => {
@@ -117,6 +118,10 @@ export const ActiveCall: FC<ActiveCallProps> = (props) => {
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, []);
useEffect(() => {
vm?.updateReactions(reactions);
}, [vm, reactions]);
useEffect(() => { useEffect(() => {
if (livekitRoom !== undefined) { if (livekitRoom !== undefined) {
const vm = new CallViewModel( const vm = new CallViewModel(
@@ -638,7 +643,7 @@ export const InCallView: FC<InCallViewProps> = ({
{renderContent()} {renderContent()}
<CallEventAudioRenderer vm={vm} /> <CallEventAudioRenderer vm={vm} />
<ReactionsAudioRenderer /> <ReactionsAudioRenderer />
<ReactionsOverlay /> <ReactionsOverlay vm={vm} />
{footer} {footer}
{layout.type !== "pip" && ( {layout.type !== "pip" && (
<> <>

View File

@@ -5,33 +5,26 @@ SPDX-License-Identifier: AGPL-3.0-only
Please see LICENSE in the repository root for full details. Please see LICENSE in the repository root for full details.
*/ */
import { ReactNode, useMemo } from "react"; import { ReactNode } from "react";
import { useReactions } from "../useReactions";
import { import {
showReactions as showReactionsSetting, showReactions as showReactionsSetting,
useSetting, useSetting,
} from "../settings/settings"; } from "../settings/settings";
import styles from "./ReactionsOverlay.module.css"; import styles from "./ReactionsOverlay.module.css";
import { CallViewModel } from "../state/CallViewModel";
import { useObservableState } from "observable-hooks";
export function ReactionsOverlay(): ReactNode { export function ReactionsOverlay({ vm }: { vm: CallViewModel }): ReactNode {
const { reactions } = useReactions();
const [showReactions] = useSetting(showReactionsSetting); const [showReactions] = useSetting(showReactionsSetting);
const reactionsIcons = useMemo( const reactionsIcons = useObservableState(vm.visibleReactions);
() =>
showReactions if (!showReactions) {
? Object.entries(reactions).map(([sender, { emoji }]) => ({ return;
sender, }
emoji,
startX: Math.ceil(Math.random() * 80) + 10,
}))
: [],
[showReactions, reactions],
);
return ( return (
<div className={styles.container}> <div className={styles.container}>
{reactionsIcons.map(({ sender, emoji, startX }) => ( {reactionsIcons?.map(({ sender, emoji, startX }) => (
<span <span
// Reactions effects are considered presentation elements. The reaction // Reactions effects are considered presentation elements. The reaction
// is also present on the sender's tile, which assistive technology can // is also present on the sender's tile, which assistive technology can

View File

@@ -66,7 +66,7 @@ import {
} from "./MediaViewModel"; } from "./MediaViewModel";
import { accumulate, finalizeValue } from "../utils/observable"; import { accumulate, finalizeValue } from "../utils/observable";
import { ObservableScope } from "./ObservableScope"; import { ObservableScope } from "./ObservableScope";
import { duplicateTiles } from "../settings/settings"; import { duplicateTiles, showReactions } from "../settings/settings";
import { isFirefox } from "../Platform"; import { isFirefox } from "../Platform";
import { setPipEnabled } from "../controls"; import { setPipEnabled } from "../controls";
import { GridTileViewModel, SpotlightTileViewModel } from "./TileViewModel"; import { GridTileViewModel, SpotlightTileViewModel } from "./TileViewModel";
@@ -77,6 +77,8 @@ import { oneOnOneLayout } from "./OneOnOneLayout";
import { pipLayout } from "./PipLayout"; import { pipLayout } from "./PipLayout";
import { EncryptionSystem } from "../e2ee/sharedKeyManagement"; import { EncryptionSystem } from "../e2ee/sharedKeyManagement";
import { observeSpeaker } from "./observeSpeaker"; import { observeSpeaker } from "./observeSpeaker";
import { useReactions } from "../useReactions";
import { ReactionOption } from "../reactions";
// How long we wait after a focus switch before showing the real participant // How long we wait after a focus switch before showing the real participant
// list again // list again
@@ -1091,6 +1093,36 @@ export class CallViewModel extends ViewModel {
this.scope.state(), this.scope.state(),
); );
public readonly handsRaised = new Subject<Record<string, Date>>();
public readonly reactions = new Subject<Record<string, ReactionOption>>();
public updateReactions(data: ReturnType<typeof useReactions>) {
this.handsRaised.next(data.raisedHands);
this.reactions.next(data.reactions);
}
public readonly visibleReactions = combineLatest([
this.reactions,
showReactions.value,
])
.pipe(
map(([reactions, setting]) => (setting ? reactions : {})),
scan<
Record<string, ReactionOption>,
{ sender: string; emoji: string; startX: number }[]
>((acc, latest) => {
const newSet: { sender: string; emoji: string; startX: number }[] = [];
for (const [sender, reaction] of Object.entries(latest)) {
const startX =
acc.find((v) => v.sender === sender && v.emoji)?.startX ??
Math.ceil(Math.random() * 80) + 10;
newSet.push({ sender, emoji: reaction.emoji, startX });
}
return newSet;
}, []),
)
.pipe(this.scope.state());
public constructor( public constructor(
// A call is permanently tied to a single Matrix room and LiveKit room // A call is permanently tied to a single Matrix room and LiveKit room
private readonly matrixRTCSession: MatrixRTCSession, private readonly matrixRTCSession: MatrixRTCSession,