mirror of
https://github.com/vector-im/element-call.git
synced 2026-09-04 21:35:19 +00:00
Refactor into own files.
This commit is contained in:
45
src/App.tsx
45
src/App.tsx
@@ -28,7 +28,6 @@ import { Initializer } from "./initializer";
|
|||||||
import { MediaDevicesProvider } from "./livekit/MediaDevicesContext";
|
import { MediaDevicesProvider } from "./livekit/MediaDevicesContext";
|
||||||
import { widget } from "./widget";
|
import { widget } from "./widget";
|
||||||
import { useTheme } from "./useTheme";
|
import { useTheme } from "./useTheme";
|
||||||
import { ReactionsProvider } from "./useReactions";
|
|
||||||
|
|
||||||
const SentryRoute = Sentry.withSentryRouting(Route);
|
const SentryRoute = Sentry.withSentryRouting(Route);
|
||||||
|
|
||||||
@@ -83,29 +82,27 @@ export const App: FC<AppProps> = ({ history }) => {
|
|||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
{loaded ? (
|
{loaded ? (
|
||||||
<Suspense fallback={null}>
|
<Suspense fallback={null}>
|
||||||
<ReactionsProvider>
|
<ClientProvider>
|
||||||
<ClientProvider>
|
<MediaDevicesProvider>
|
||||||
<MediaDevicesProvider>
|
<Sentry.ErrorBoundary fallback={errorPage}>
|
||||||
<Sentry.ErrorBoundary fallback={errorPage}>
|
<DisconnectedBanner />
|
||||||
<DisconnectedBanner />
|
<Switch>
|
||||||
<Switch>
|
<SentryRoute exact path="/">
|
||||||
<SentryRoute exact path="/">
|
<HomePage />
|
||||||
<HomePage />
|
</SentryRoute>
|
||||||
</SentryRoute>
|
<SentryRoute exact path="/login">
|
||||||
<SentryRoute exact path="/login">
|
<LoginPage />
|
||||||
<LoginPage />
|
</SentryRoute>
|
||||||
</SentryRoute>
|
<SentryRoute exact path="/register">
|
||||||
<SentryRoute exact path="/register">
|
<RegisterPage />
|
||||||
<RegisterPage />
|
</SentryRoute>
|
||||||
</SentryRoute>
|
<SentryRoute path="*">
|
||||||
<SentryRoute path="*">
|
<RoomPage />
|
||||||
<RoomPage />
|
</SentryRoute>
|
||||||
</SentryRoute>
|
</Switch>
|
||||||
</Switch>
|
</Sentry.ErrorBoundary>
|
||||||
</Sentry.ErrorBoundary>
|
</MediaDevicesProvider>
|
||||||
</MediaDevicesProvider>
|
</ClientProvider>
|
||||||
</ClientProvider>
|
|
||||||
</ReactionsProvider>
|
|
||||||
</Suspense>
|
</Suspense>
|
||||||
) : (
|
) : (
|
||||||
<LoadingView />
|
<LoadingView />
|
||||||
|
|||||||
@@ -37,7 +37,6 @@ import {
|
|||||||
import { translatedError } from "./TranslatedError";
|
import { translatedError } from "./TranslatedError";
|
||||||
import { useEventTarget } from "./useEvents";
|
import { useEventTarget } from "./useEvents";
|
||||||
import { Config } from "./config/Config";
|
import { Config } from "./config/Config";
|
||||||
import { useReactions } from "./useReactions";
|
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
@@ -54,6 +53,9 @@ export type ValidClientState = {
|
|||||||
// 'Disconnected' rather than 'connected' because it tracks specifically
|
// 'Disconnected' rather than 'connected' because it tracks specifically
|
||||||
// whether the client is supposed to be connected but is not
|
// whether the client is supposed to be connected but is not
|
||||||
disconnected: boolean;
|
disconnected: boolean;
|
||||||
|
supportedFeatures: {
|
||||||
|
reactions: boolean;
|
||||||
|
};
|
||||||
setClient: (params?: SetClientParams) => void;
|
setClient: (params?: SetClientParams) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -146,7 +148,6 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const ClientProvider: FC<Props> = ({ children }) => {
|
export const ClientProvider: FC<Props> = ({ children }) => {
|
||||||
const { setSupportsReactions } = useReactions();
|
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
|
||||||
// null = signed out, undefined = loading
|
// null = signed out, undefined = loading
|
||||||
@@ -258,6 +259,7 @@ export const ClientProvider: FC<Props> = ({ children }) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const [isDisconnected, setIsDisconnected] = useState(false);
|
const [isDisconnected, setIsDisconnected] = useState(false);
|
||||||
|
const [supportsReactions, setSupportsReactions] = useState(false);
|
||||||
|
|
||||||
const state: ClientState | undefined = useMemo(() => {
|
const state: ClientState | undefined = useMemo(() => {
|
||||||
if (alreadyOpenedErr) {
|
if (alreadyOpenedErr) {
|
||||||
@@ -281,6 +283,9 @@ export const ClientProvider: FC<Props> = ({ children }) => {
|
|||||||
authenticated,
|
authenticated,
|
||||||
setClient,
|
setClient,
|
||||||
disconnected: isDisconnected,
|
disconnected: isDisconnected,
|
||||||
|
supportedFeatures: {
|
||||||
|
reactions: supportsReactions,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}, [
|
}, [
|
||||||
alreadyOpenedErr,
|
alreadyOpenedErr,
|
||||||
@@ -289,6 +294,7 @@ export const ClientProvider: FC<Props> = ({ children }) => {
|
|||||||
logout,
|
logout,
|
||||||
setClient,
|
setClient,
|
||||||
isDisconnected,
|
isDisconnected,
|
||||||
|
supportsReactions,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const onSync = useCallback(
|
const onSync = useCallback(
|
||||||
@@ -314,8 +320,6 @@ export const ClientProvider: FC<Props> = ({ children }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (initClientState.widgetApi) {
|
if (initClientState.widgetApi) {
|
||||||
let supportsReactions = true;
|
|
||||||
|
|
||||||
const reactSend = initClientState.widgetApi.hasCapability(
|
const reactSend = initClientState.widgetApi.hasCapability(
|
||||||
"org.matrix.msc2762.send.event:m.reaction",
|
"org.matrix.msc2762.send.event:m.reaction",
|
||||||
);
|
);
|
||||||
@@ -330,15 +334,13 @@ export const ClientProvider: FC<Props> = ({ children }) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (!reactSend || !reactRcv || !redactSend || !redactRcv) {
|
if (!reactSend || !reactRcv || !redactSend || !redactRcv) {
|
||||||
supportsReactions = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
setSupportsReactions(supportsReactions);
|
|
||||||
if (!supportsReactions) {
|
|
||||||
logger.warn("Widget does not support reactions");
|
logger.warn("Widget does not support reactions");
|
||||||
|
setSupportsReactions(false);
|
||||||
} else {
|
} else {
|
||||||
logger.warn("Widget does support reactions");
|
setSupportsReactions(true);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
setSupportsReactions(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (): void => {
|
return (): void => {
|
||||||
@@ -346,7 +348,7 @@ export const ClientProvider: FC<Props> = ({ children }) => {
|
|||||||
initClientState.client.removeListener(ClientEvent.Sync, onSync);
|
initClientState.client.removeListener(ClientEvent.Sync, onSync);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [initClientState, onSync, setSupportsReactions]);
|
}, [initClientState, onSync]);
|
||||||
|
|
||||||
if (alreadyOpenedErr) {
|
if (alreadyOpenedErr) {
|
||||||
return <ErrorView error={alreadyOpenedErr} />;
|
return <ErrorView error={alreadyOpenedErr} />;
|
||||||
|
|||||||
@@ -91,39 +91,6 @@ export const ShareScreenButton: FC<ShareScreenButtonProps> = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
interface RaiseHandButtonProps extends ComponentPropsWithoutRef<"button"> {
|
|
||||||
raised: boolean;
|
|
||||||
}
|
|
||||||
export const RaiseHandButton: FC<RaiseHandButtonProps> = ({
|
|
||||||
raised,
|
|
||||||
...props
|
|
||||||
}) => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Tooltip label={t("common.raise_hand")}>
|
|
||||||
<CpdButton
|
|
||||||
kind={raised ? "primary" : "secondary"}
|
|
||||||
{...props}
|
|
||||||
style={{ paddingLeft: 8, paddingRight: 8 }}
|
|
||||||
>
|
|
||||||
<p
|
|
||||||
role="img"
|
|
||||||
aria-label="raised hand"
|
|
||||||
style={{
|
|
||||||
width: "30px",
|
|
||||||
height: "0px",
|
|
||||||
display: "inline-block",
|
|
||||||
fontSize: "22px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
✋
|
|
||||||
</p>
|
|
||||||
</CpdButton>
|
|
||||||
</Tooltip>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const EndCallButton: FC<ComponentPropsWithoutRef<"button">> = ({
|
export const EndCallButton: FC<ComponentPropsWithoutRef<"button">> = ({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
|
|||||||
143
src/button/RaisedHandToggleButton.tsx
Normal file
143
src/button/RaisedHandToggleButton.tsx
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2024 New Vector Ltd.
|
||||||
|
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
Please see LICENSE in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Button as CpdButton, Tooltip } from "@vector-im/compound-web";
|
||||||
|
import {
|
||||||
|
ComponentPropsWithoutRef,
|
||||||
|
FC,
|
||||||
|
ReactNode,
|
||||||
|
useCallback,
|
||||||
|
useState,
|
||||||
|
} from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
|
import { EventType, RelationType } from "matrix-js-sdk/src/matrix";
|
||||||
|
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||||
|
import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession";
|
||||||
|
|
||||||
|
import { useReactions } from "../useReactions";
|
||||||
|
import { useMatrixRTCSessionMemberships } from "../useMatrixRTCSessionMemberships";
|
||||||
|
|
||||||
|
interface InnerButtonButtonProps extends ComponentPropsWithoutRef<"button"> {
|
||||||
|
raised: boolean;
|
||||||
|
}
|
||||||
|
const InnerButton: FC<InnerButtonButtonProps> = ({ raised, ...props }) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip label={t("common.raise_hand")}>
|
||||||
|
<CpdButton
|
||||||
|
kind={raised ? "primary" : "secondary"}
|
||||||
|
{...props}
|
||||||
|
style={{ paddingLeft: 8, paddingRight: 8 }}
|
||||||
|
>
|
||||||
|
<p
|
||||||
|
role="img"
|
||||||
|
aria-label="raised hand"
|
||||||
|
style={{
|
||||||
|
width: "30px",
|
||||||
|
height: "0px",
|
||||||
|
display: "inline-block",
|
||||||
|
fontSize: "22px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
✋
|
||||||
|
</p>
|
||||||
|
</CpdButton>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface RaisedHandToggleButton {
|
||||||
|
key: string;
|
||||||
|
rtcSession: MatrixRTCSession;
|
||||||
|
client: MatrixClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RaiseHandToggleButton({
|
||||||
|
key,
|
||||||
|
client,
|
||||||
|
rtcSession,
|
||||||
|
}: RaisedHandToggleButton): ReactNode {
|
||||||
|
const {
|
||||||
|
raisedHands,
|
||||||
|
removeRaisedHand,
|
||||||
|
addRaisedHand,
|
||||||
|
myReactionId,
|
||||||
|
setMyReactionId,
|
||||||
|
} = useReactions();
|
||||||
|
const [busy, setBusy] = useState(false);
|
||||||
|
const userId = client.getUserId()!;
|
||||||
|
const isHandRaised = !!raisedHands[userId];
|
||||||
|
const memberships = useMatrixRTCSessionMemberships(rtcSession);
|
||||||
|
|
||||||
|
const toggleRaisedHand = useCallback(() => {
|
||||||
|
if (isHandRaised) {
|
||||||
|
if (myReactionId) {
|
||||||
|
setBusy(true);
|
||||||
|
client
|
||||||
|
.redactEvent(rtcSession.room.roomId, myReactionId)
|
||||||
|
.then(() => {
|
||||||
|
logger.debug("Redacted raise hand event");
|
||||||
|
setMyReactionId(null);
|
||||||
|
removeRaisedHand(userId);
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
logger.error("Failed to redact reaction event", e);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setBusy(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const myMembership = memberships.find((m) => m.sender === userId);
|
||||||
|
if (!myMembership?.eventId) {
|
||||||
|
logger.error("Cannot find own membership event");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setBusy(true);
|
||||||
|
client
|
||||||
|
.sendEvent(rtcSession.room.roomId, EventType.Reaction, {
|
||||||
|
"m.relates_to": {
|
||||||
|
rel_type: RelationType.Annotation,
|
||||||
|
event_id: myMembership.eventId,
|
||||||
|
key: "🖐️",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then((reaction) => {
|
||||||
|
logger.debug("Sent raise hand event", reaction.event_id);
|
||||||
|
setMyReactionId(reaction.event_id);
|
||||||
|
addRaisedHand(userId, new Date());
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
logger.error("Failed to send reaction event", e);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setBusy(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
client,
|
||||||
|
isHandRaised,
|
||||||
|
memberships,
|
||||||
|
myReactionId,
|
||||||
|
rtcSession.room.roomId,
|
||||||
|
addRaisedHand,
|
||||||
|
removeRaisedHand,
|
||||||
|
setMyReactionId,
|
||||||
|
userId,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<InnerButton
|
||||||
|
key={key}
|
||||||
|
disabled={busy}
|
||||||
|
onClick={toggleRaisedHand}
|
||||||
|
raised={isHandRaised}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -7,3 +7,4 @@ Please see LICENSE in the repository root for full details.
|
|||||||
|
|
||||||
export * from "./Button";
|
export * from "./Button";
|
||||||
export * from "./LinkButton";
|
export * from "./LinkButton";
|
||||||
|
export * from "./RaisedHandToggleButton";
|
||||||
|
|||||||
@@ -11,10 +11,6 @@ import {
|
|||||||
useLocalParticipant,
|
useLocalParticipant,
|
||||||
} from "@livekit/components-react";
|
} from "@livekit/components-react";
|
||||||
import { ConnectionState, Room } from "livekit-client";
|
import { ConnectionState, Room } from "livekit-client";
|
||||||
import {
|
|
||||||
MatrixEvent,
|
|
||||||
RoomEvent as MatrixRoomEvent,
|
|
||||||
} from "matrix-js-sdk/src/matrix";
|
|
||||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||||
import {
|
import {
|
||||||
FC,
|
FC,
|
||||||
@@ -34,8 +30,6 @@ import classNames from "classnames";
|
|||||||
import { BehaviorSubject, of } from "rxjs";
|
import { BehaviorSubject, of } from "rxjs";
|
||||||
import { useObservableEagerState } from "observable-hooks";
|
import { useObservableEagerState } from "observable-hooks";
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
import { EventType, RelationType } from "matrix-js-sdk/src/matrix";
|
|
||||||
import { ReactionEventContent } from "matrix-js-sdk/src/types";
|
|
||||||
|
|
||||||
import LogoMark from "../icons/LogoMark.svg?react";
|
import LogoMark from "../icons/LogoMark.svg?react";
|
||||||
import LogoType from "../icons/LogoType.svg?react";
|
import LogoType from "../icons/LogoType.svg?react";
|
||||||
@@ -45,8 +39,8 @@ import {
|
|||||||
MicButton,
|
MicButton,
|
||||||
VideoButton,
|
VideoButton,
|
||||||
ShareScreenButton,
|
ShareScreenButton,
|
||||||
RaiseHandButton,
|
|
||||||
SettingsButton,
|
SettingsButton,
|
||||||
|
RaiseHandToggleButton,
|
||||||
} from "../button";
|
} from "../button";
|
||||||
import { Header, LeftNav, RightNav, RoomHeaderInfo } from "../Header";
|
import { Header, LeftNav, RightNav, RoomHeaderInfo } from "../Header";
|
||||||
import { useUrlParams } from "../UrlParams";
|
import { useUrlParams } from "../UrlParams";
|
||||||
@@ -85,8 +79,7 @@ import { makeOneOnOneLayout } from "../grid/OneOnOneLayout";
|
|||||||
import { makeSpotlightExpandedLayout } from "../grid/SpotlightExpandedLayout";
|
import { makeSpotlightExpandedLayout } from "../grid/SpotlightExpandedLayout";
|
||||||
import { makeSpotlightLandscapeLayout } from "../grid/SpotlightLandscapeLayout";
|
import { makeSpotlightLandscapeLayout } from "../grid/SpotlightLandscapeLayout";
|
||||||
import { makeSpotlightPortraitLayout } from "../grid/SpotlightPortraitLayout";
|
import { makeSpotlightPortraitLayout } from "../grid/SpotlightPortraitLayout";
|
||||||
import { useMatrixRTCSessionMemberships } from "../useMatrixRTCSessionMemberships";
|
import { ReactionsProvider, useReactions } from "../useReactions";
|
||||||
import { useReactions } from "../useReactions";
|
|
||||||
import handSound from "../res/sounds/raise-hand.ogg?url";
|
import handSound from "../res/sounds/raise-hand.ogg?url";
|
||||||
|
|
||||||
const canScreenshare = "getDisplayMedia" in (navigator.mediaDevices ?? {});
|
const canScreenshare = "getDisplayMedia" in (navigator.mediaDevices ?? {});
|
||||||
@@ -140,12 +133,14 @@ export const ActiveCall: FC<ActiveCallProps> = (props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<RoomContext.Provider value={livekitRoom}>
|
<RoomContext.Provider value={livekitRoom}>
|
||||||
<InCallView
|
<ReactionsProvider rtcSession={props.rtcSession}>
|
||||||
{...props}
|
<InCallView
|
||||||
vm={vm}
|
{...props}
|
||||||
livekitRoom={livekitRoom}
|
vm={vm}
|
||||||
connState={connState}
|
livekitRoom={livekitRoom}
|
||||||
/>
|
connState={connState}
|
||||||
|
/>
|
||||||
|
</ReactionsProvider>
|
||||||
</RoomContext.Provider>
|
</RoomContext.Provider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -178,7 +173,9 @@ export const InCallView: FC<InCallViewProps> = ({
|
|||||||
connState,
|
connState,
|
||||||
onShareClick,
|
onShareClick,
|
||||||
}) => {
|
}) => {
|
||||||
const { supportsReactions } = useReactions();
|
const { supportsReactions, raisedHandCount } = useReactions();
|
||||||
|
const [previousRaisedHandCount, setPreviousRaisedHandCount] =
|
||||||
|
useState(raisedHandCount);
|
||||||
|
|
||||||
useWakeLock();
|
useWakeLock();
|
||||||
|
|
||||||
@@ -310,87 +307,19 @@ export const InCallView: FC<InCallViewProps> = ({
|
|||||||
[vm],
|
[vm],
|
||||||
);
|
);
|
||||||
|
|
||||||
const memberships = useMatrixRTCSessionMemberships(rtcSession);
|
// Play a sound when the raised hand count increases.
|
||||||
const { raisedHands, addRaisedHand, removeRaisedHand } = useReactions();
|
|
||||||
const [reactionId, setReactionId] = useState<string | null>(null);
|
|
||||||
const userId = client.getUserId()!;
|
|
||||||
|
|
||||||
const handRaisePlayer = useRef<HTMLAudioElement>(null);
|
const handRaisePlayer = useRef<HTMLAudioElement>(null);
|
||||||
|
|
||||||
const isHandRaised = !!raisedHands[userId];
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const getLastReactionEvent = async (
|
if (!handRaisePlayer.current) {
|
||||||
eventId: string,
|
return;
|
||||||
): Promise<MatrixEvent | undefined> => {
|
}
|
||||||
return client
|
if (previousRaisedHandCount < raisedHandCount) {
|
||||||
.relations(
|
handRaisePlayer.current.play().catch((ex) => {
|
||||||
rtcSession.room.roomId,
|
logger.warn("Failed to play raise hand sound", ex);
|
||||||
eventId,
|
});
|
||||||
RelationType.Annotation,
|
}
|
||||||
EventType.Reaction,
|
setPreviousRaisedHandCount(raisedHandCount);
|
||||||
{
|
}, [raisedHandCount, handRaisePlayer, previousRaisedHandCount]);
|
||||||
limit: 1,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.then((rels) => {
|
|
||||||
return rels.events.length > 0 ? rels.events[0] : undefined;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const fetchReactions = async (): Promise<void> => {
|
|
||||||
for (const m of memberships) {
|
|
||||||
if (!m.sender) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const reaction = await getLastReactionEvent(m.eventId!);
|
|
||||||
if (reaction && reaction.getType() === EventType.Reaction) {
|
|
||||||
const content = reaction.getContent() as ReactionEventContent;
|
|
||||||
if (content?.["m.relates_to"].key === "🖐️") {
|
|
||||||
addRaisedHand(m.sender, new Date(m.createdTs()));
|
|
||||||
if (m.sender === userId) {
|
|
||||||
setReactionId(m.eventId!);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
void fetchReactions();
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const handleReactionEvent = (event: MatrixEvent): void => {
|
|
||||||
const sender = event.getSender();
|
|
||||||
if (!sender) {
|
|
||||||
// Weird, skip.
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (event.getType() === EventType.Reaction) {
|
|
||||||
// TODO: check if target of reaction is a call membership event
|
|
||||||
const content = event.getContent() as ReactionEventContent;
|
|
||||||
if (content?.["m.relates_to"].key === "🖐️") {
|
|
||||||
addRaisedHand(sender, new Date(event.localTimestamp));
|
|
||||||
handRaisePlayer.current?.play().catch((ex) => {
|
|
||||||
logger.warn("Failed to play hand raise sound", ex);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (event.getType() === EventType.RoomRedaction && event.getSender()) {
|
|
||||||
// TODO: check target of redaction event
|
|
||||||
removeRaisedHand(sender);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
client.on(MatrixRoomEvent.Timeline, handleReactionEvent);
|
|
||||||
client.on(MatrixRoomEvent.Redaction, handleReactionEvent);
|
|
||||||
|
|
||||||
return (): void => {
|
|
||||||
client.on(MatrixRoomEvent.Timeline, handleReactionEvent);
|
|
||||||
client.off(MatrixRoomEvent.Redaction, handleReactionEvent);
|
|
||||||
};
|
|
||||||
}, [client, raisedHands, addRaisedHand, removeRaisedHand]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
widget?.api.transport
|
widget?.api.transport
|
||||||
@@ -573,52 +502,6 @@ export const InCallView: FC<InCallViewProps> = ({
|
|||||||
.catch(logger.error);
|
.catch(logger.error);
|
||||||
}, [localParticipant, isScreenShareEnabled]);
|
}, [localParticipant, isScreenShareEnabled]);
|
||||||
|
|
||||||
const toggleRaisedHand = useCallback(() => {
|
|
||||||
if (isHandRaised) {
|
|
||||||
if (reactionId) {
|
|
||||||
client
|
|
||||||
.redactEvent(rtcSession.room.roomId, reactionId)
|
|
||||||
.then(() => {
|
|
||||||
setReactionId(null);
|
|
||||||
removeRaisedHand(userId);
|
|
||||||
logger.debug("Redacted reaction event");
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
logger.error("Failed to redact reaction event", e);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const m = memberships.filter((m) => m.sender === userId);
|
|
||||||
const eventId = m[0].eventId!;
|
|
||||||
client
|
|
||||||
.sendEvent(rtcSession.room.roomId, EventType.Reaction, {
|
|
||||||
"m.relates_to": {
|
|
||||||
rel_type: RelationType.Annotation,
|
|
||||||
event_id: eventId,
|
|
||||||
key: "🖐️",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.then((reaction) => {
|
|
||||||
setReactionId(reaction.event_id);
|
|
||||||
addRaisedHand(userId, new Date());
|
|
||||||
logger.debug("Sent reaction event", reaction.event_id);
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
logger.error("Failed to send reaction event", e);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [
|
|
||||||
client,
|
|
||||||
isHandRaised,
|
|
||||||
memberships,
|
|
||||||
reactionId,
|
|
||||||
rtcSession.room.roomId,
|
|
||||||
addRaisedHand,
|
|
||||||
removeRaisedHand,
|
|
||||||
setReactionId,
|
|
||||||
userId,
|
|
||||||
]);
|
|
||||||
|
|
||||||
let footer: JSX.Element | null;
|
let footer: JSX.Element | null;
|
||||||
|
|
||||||
if (noControls) {
|
if (noControls) {
|
||||||
@@ -655,10 +538,10 @@ export const InCallView: FC<InCallViewProps> = ({
|
|||||||
}
|
}
|
||||||
if (supportsReactions) {
|
if (supportsReactions) {
|
||||||
buttons.push(
|
buttons.push(
|
||||||
<RaiseHandButton
|
<RaiseHandToggleButton
|
||||||
|
client={client}
|
||||||
|
rtcSession={rtcSession}
|
||||||
key="4"
|
key="4"
|
||||||
onClick={toggleRaisedHand}
|
|
||||||
raised={isHandRaised}
|
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { RemoteTrackPublication } from "livekit-client";
|
|||||||
import { test, expect } from "vitest";
|
import { test, expect } from "vitest";
|
||||||
import { render, screen } from "@testing-library/react";
|
import { render, screen } from "@testing-library/react";
|
||||||
import { axe } from "vitest-axe";
|
import { axe } from "vitest-axe";
|
||||||
|
import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession";
|
||||||
|
|
||||||
import { GridTile } from "./GridTile";
|
import { GridTile } from "./GridTile";
|
||||||
import { withRemoteMedia } from "../utils/test";
|
import { withRemoteMedia } from "../utils/test";
|
||||||
@@ -26,8 +27,17 @@ test("GridTile is accessible", async () => {
|
|||||||
({}) as Partial<RemoteTrackPublication> as RemoteTrackPublication,
|
({}) as Partial<RemoteTrackPublication> as RemoteTrackPublication,
|
||||||
},
|
},
|
||||||
async (vm) => {
|
async (vm) => {
|
||||||
|
const fakeRtcSession = {
|
||||||
|
on: () => {},
|
||||||
|
off: () => {},
|
||||||
|
room: {
|
||||||
|
on: () => {},
|
||||||
|
off: () => {},
|
||||||
|
},
|
||||||
|
memberships: [],
|
||||||
|
} as unknown as MatrixRTCSession;
|
||||||
const { container } = render(
|
const { container } = render(
|
||||||
<ReactionsProvider>
|
<ReactionsProvider rtcSession={fakeRtcSession}>
|
||||||
<GridTile
|
<GridTile
|
||||||
vm={vm}
|
vm={vm}
|
||||||
onOpenProfile={() => {}}
|
onOpenProfile={() => {}}
|
||||||
|
|||||||
@@ -5,20 +5,34 @@ 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 React, {
|
import {
|
||||||
|
EventType,
|
||||||
|
MatrixEvent,
|
||||||
|
RelationType,
|
||||||
|
RoomEvent as MatrixRoomEvent,
|
||||||
|
} from "matrix-js-sdk/src/matrix";
|
||||||
|
import { ReactionEventContent } from "matrix-js-sdk/src/types";
|
||||||
|
import {
|
||||||
createContext,
|
createContext,
|
||||||
useContext,
|
useContext,
|
||||||
useState,
|
useState,
|
||||||
ReactNode,
|
ReactNode,
|
||||||
useCallback,
|
useCallback,
|
||||||
|
useEffect,
|
||||||
} from "react";
|
} from "react";
|
||||||
|
import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession";
|
||||||
|
|
||||||
|
import { useMatrixRTCSessionMemberships } from "./useMatrixRTCSessionMemberships";
|
||||||
|
import { useClientState } from "./ClientContext";
|
||||||
|
|
||||||
interface ReactionsContextType {
|
interface ReactionsContextType {
|
||||||
raisedHands: Record<string, Date>;
|
raisedHands: Record<string, Date>;
|
||||||
|
raisedHandCount: number;
|
||||||
addRaisedHand: (userId: string, date: Date) => void;
|
addRaisedHand: (userId: string, date: Date) => void;
|
||||||
removeRaisedHand: (userId: string) => void;
|
removeRaisedHand: (userId: string) => void;
|
||||||
supportsReactions: boolean;
|
supportsReactions: boolean;
|
||||||
setSupportsReactions: React.Dispatch<React.SetStateAction<boolean>>;
|
myReactionId: string | null;
|
||||||
|
setMyReactionId: (id: string | null) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ReactionsContext = createContext<ReactionsContextType | undefined>(
|
const ReactionsContext = createContext<ReactionsContextType | undefined>(
|
||||||
@@ -35,11 +49,19 @@ export const useReactions = (): ReactionsContextType => {
|
|||||||
|
|
||||||
export const ReactionsProvider = ({
|
export const ReactionsProvider = ({
|
||||||
children,
|
children,
|
||||||
|
rtcSession,
|
||||||
}: {
|
}: {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
|
rtcSession: MatrixRTCSession;
|
||||||
}): JSX.Element => {
|
}): JSX.Element => {
|
||||||
const [raisedHands, setRaisedHands] = useState<Record<string, Date>>({});
|
const [raisedHands, setRaisedHands] = useState<Record<string, Date>>({});
|
||||||
const [supportsReactions, setSupportsReactions] = useState<boolean>(true);
|
const [myReactionId, setMyReactionId] = useState<string | null>(null);
|
||||||
|
const [raisedHandCount, setRaisedHandCount] = useState(0);
|
||||||
|
const memberships = useMatrixRTCSessionMemberships(rtcSession);
|
||||||
|
const clientState = useClientState();
|
||||||
|
const supportsReactions =
|
||||||
|
clientState?.state === "valid" && clientState.supportedFeatures.reactions;
|
||||||
|
const room = rtcSession.room;
|
||||||
|
|
||||||
const addRaisedHand = useCallback(
|
const addRaisedHand = useCallback(
|
||||||
(userId: string, time: Date) => {
|
(userId: string, time: Date) => {
|
||||||
@@ -47,6 +69,7 @@ export const ReactionsProvider = ({
|
|||||||
...raisedHands,
|
...raisedHands,
|
||||||
[userId]: time,
|
[userId]: time,
|
||||||
});
|
});
|
||||||
|
setRaisedHandCount(Object.keys(raisedHands).length + 1);
|
||||||
},
|
},
|
||||||
[raisedHands],
|
[raisedHands],
|
||||||
);
|
);
|
||||||
@@ -55,18 +78,83 @@ export const ReactionsProvider = ({
|
|||||||
(userId: string) => {
|
(userId: string) => {
|
||||||
delete raisedHands[userId];
|
delete raisedHands[userId];
|
||||||
setRaisedHands(raisedHands);
|
setRaisedHands(raisedHands);
|
||||||
|
setRaisedHandCount(Object.keys(raisedHands).length);
|
||||||
},
|
},
|
||||||
[raisedHands],
|
[raisedHands],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Load any existing reactions.
|
||||||
|
useEffect(() => {
|
||||||
|
const getLastReactionEvent = (eventId: string): MatrixEvent | undefined => {
|
||||||
|
const relations = room.relations.getChildEventsForEvent(
|
||||||
|
eventId,
|
||||||
|
RelationType.Annotation,
|
||||||
|
EventType.Reaction,
|
||||||
|
);
|
||||||
|
const allEvents = relations?.getRelations() ?? [];
|
||||||
|
return allEvents.length > 0 ? allEvents[0] : undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchReactions = (): void => {
|
||||||
|
for (const m of memberships) {
|
||||||
|
if (!m.sender) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const reaction = getLastReactionEvent(m.eventId!);
|
||||||
|
if (reaction && reaction.getType() === EventType.Reaction) {
|
||||||
|
const content = reaction.getContent() as ReactionEventContent;
|
||||||
|
if (content?.["m.relates_to"].key === "🖐️") {
|
||||||
|
addRaisedHand(m.sender, new Date(m.createdTs()));
|
||||||
|
if (m.sender === room.client.getUserId()) {
|
||||||
|
setMyReactionId(m.eventId!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
void fetchReactions();
|
||||||
|
}, [room, addRaisedHand, memberships]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleReactionEvent = (event: MatrixEvent): void => {
|
||||||
|
const sender = event.getSender();
|
||||||
|
if (!sender) {
|
||||||
|
// Skip any event without a sender.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (event.getType() === EventType.Reaction) {
|
||||||
|
// TODO: check if target of reaction is a call membership event
|
||||||
|
const content = event.getContent() as ReactionEventContent;
|
||||||
|
if (content?.["m.relates_to"].key === "🖐️") {
|
||||||
|
addRaisedHand(sender, new Date(event.localTimestamp));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (event.getType() === EventType.RoomRedaction && event.getSender()) {
|
||||||
|
// TODO: check target of redaction event
|
||||||
|
removeRaisedHand(sender);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
room.on(MatrixRoomEvent.Timeline, handleReactionEvent);
|
||||||
|
room.on(MatrixRoomEvent.Redaction, handleReactionEvent);
|
||||||
|
|
||||||
|
return (): void => {
|
||||||
|
room.off(MatrixRoomEvent.Timeline, handleReactionEvent);
|
||||||
|
room.off(MatrixRoomEvent.Redaction, handleReactionEvent);
|
||||||
|
};
|
||||||
|
}, [room, raisedHands, addRaisedHand, removeRaisedHand]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ReactionsContext.Provider
|
<ReactionsContext.Provider
|
||||||
value={{
|
value={{
|
||||||
raisedHands,
|
raisedHands,
|
||||||
|
raisedHandCount,
|
||||||
addRaisedHand,
|
addRaisedHand,
|
||||||
removeRaisedHand,
|
removeRaisedHand,
|
||||||
supportsReactions,
|
supportsReactions,
|
||||||
setSupportsReactions,
|
myReactionId,
|
||||||
|
setMyReactionId,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
Reference in New Issue
Block a user