mirror of
https://github.com/vector-im/element-call.git
synced 2026-08-29 21:15:19 +00:00
Add support for using CallViewModel for reactions sounds.
This commit is contained in:
@@ -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) {
|
||||||
|
|||||||
@@ -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" && (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user