Check for reaction & redaction capabilities in widget mode

Signed-off-by: Milton Moura <miltonmoura@gmail.com>
This commit is contained in:
Milton Moura
2024-09-19 11:33:48 +01:00
parent 7ac5642245
commit 69a50fb2a8
6 changed files with 133 additions and 83 deletions

View File

@@ -28,6 +28,7 @@ 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);
@@ -82,27 +83,29 @@ export const App: FC<AppProps> = ({ history }) => {
<TooltipProvider> <TooltipProvider>
{loaded ? ( {loaded ? (
<Suspense fallback={null}> <Suspense fallback={null}>
<ClientProvider> <ReactionsProvider>
<MediaDevicesProvider> <ClientProvider>
<Sentry.ErrorBoundary fallback={errorPage}> <MediaDevicesProvider>
<DisconnectedBanner /> <Sentry.ErrorBoundary fallback={errorPage}>
<Switch> <DisconnectedBanner />
<SentryRoute exact path="/"> <Switch>
<HomePage /> <SentryRoute exact path="/">
</SentryRoute> <HomePage />
<SentryRoute exact path="/login"> </SentryRoute>
<LoginPage /> <SentryRoute exact path="/login">
</SentryRoute> <LoginPage />
<SentryRoute exact path="/register"> </SentryRoute>
<RegisterPage /> <SentryRoute exact path="/register">
</SentryRoute> <RegisterPage />
<SentryRoute path="*"> </SentryRoute>
<RoomPage /> <SentryRoute path="*">
</SentryRoute> <RoomPage />
</Switch> </SentryRoute>
</Sentry.ErrorBoundary> </Switch>
</MediaDevicesProvider> </Sentry.ErrorBoundary>
</ClientProvider> </MediaDevicesProvider>
</ClientProvider>
</ReactionsProvider>
</Suspense> </Suspense>
) : ( ) : (
<LoadingView /> <LoadingView />

View File

@@ -25,6 +25,7 @@ import { logger } from "matrix-js-sdk/src/logger";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { ISyncStateData, SyncState } from "matrix-js-sdk/src/sync"; import { ISyncStateData, SyncState } from "matrix-js-sdk/src/sync";
import { MatrixError } from "matrix-js-sdk/src/matrix"; import { MatrixError } from "matrix-js-sdk/src/matrix";
import { WidgetApi } from "matrix-widget-api";
import { ErrorView } from "./FullScreenView"; import { ErrorView } from "./FullScreenView";
import { fallbackICEServerAllowed, initClient } from "./utils/matrix"; import { fallbackICEServerAllowed, initClient } from "./utils/matrix";
@@ -36,6 +37,7 @@ 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 {
@@ -144,6 +146,7 @@ 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
@@ -188,11 +191,11 @@ export const ClientProvider: FC<Props> = ({ children }) => {
saveSession({ ...session, passwordlessUser: false }); saveSession({ ...session, passwordlessUser: false });
setInitClientState({ setInitClientState({
client: initClientState.client, ...initClientState,
passwordlessUser: false, passwordlessUser: false,
}); });
}, },
[initClientState?.client], [initClientState],
); );
const setClient = useCallback( const setClient = useCallback(
@@ -206,6 +209,7 @@ export const ClientProvider: FC<Props> = ({ children }) => {
if (clientParams) { if (clientParams) {
saveSession(clientParams.session); saveSession(clientParams.session);
setInitClientState({ setInitClientState({
widgetApi: null,
client: clientParams.client, client: clientParams.client,
passwordlessUser: clientParams.session.passwordlessUser, passwordlessUser: clientParams.session.passwordlessUser,
}); });
@@ -309,12 +313,40 @@ export const ClientProvider: FC<Props> = ({ children }) => {
initClientState.client.on(ClientEvent.Sync, onSync); initClientState.client.on(ClientEvent.Sync, onSync);
} }
if (initClientState.widgetApi) {
let supportsReactions = true;
const reactSend = initClientState.widgetApi.hasCapability(
"org.matrix.msc2762.send.event:m.reaction",
);
const redactSend = initClientState.widgetApi.hasCapability(
"org.matrix.msc2762.send.event:m.room.redaction",
);
const reactRcv = initClientState.widgetApi.hasCapability(
"org.matrix.msc2762.receive.event:m.reaction",
);
const redactRcv = initClientState.widgetApi.hasCapability(
"org.matrix.msc2762.receive.event:m.room.redaction",
);
if (!reactSend || !reactRcv || !redactSend || !redactRcv) {
supportsReactions = false;
}
setSupportsReactions(supportsReactions);
if (!supportsReactions) {
logger.warn("Widget does not support reactions");
} else {
logger.warn("Widget does support reactions");
}
}
return (): void => { return (): void => {
if (initClientState.client) { if (initClientState.client) {
initClientState.client.removeListener(ClientEvent.Sync, onSync); initClientState.client.removeListener(ClientEvent.Sync, onSync);
} }
}; };
}, [initClientState, onSync]); }, [initClientState, onSync, setSupportsReactions]);
if (alreadyOpenedErr) { if (alreadyOpenedErr) {
return <ErrorView error={alreadyOpenedErr} />; return <ErrorView error={alreadyOpenedErr} />;
@@ -326,6 +358,7 @@ export const ClientProvider: FC<Props> = ({ children }) => {
}; };
type InitResult = { type InitResult = {
widgetApi: WidgetApi | null;
client: MatrixClient; client: MatrixClient;
passwordlessUser: boolean; passwordlessUser: boolean;
}; };
@@ -336,6 +369,7 @@ async function loadClient(): Promise<InitResult | null> {
logger.log("Using a matryoshka client"); logger.log("Using a matryoshka client");
const client = await widget.client; const client = await widget.client;
return { return {
widgetApi: widget.api,
client, client,
passwordlessUser: false, passwordlessUser: false,
}; };
@@ -364,6 +398,7 @@ async function loadClient(): Promise<InitResult | null> {
try { try {
const client = await initClient(initClientParams, true); const client = await initClient(initClientParams, true);
return { return {
widgetApi: null,
client, client,
passwordlessUser, passwordlessUser,
}; };

View File

@@ -85,8 +85,8 @@ 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 { RaisedHandsProvider, useRaisedHands } from "./useRaisedHands";
import { useMatrixRTCSessionMemberships } from "../useMatrixRTCSessionMemberships"; import { useMatrixRTCSessionMemberships } from "../useMatrixRTCSessionMemberships";
import { useReactions } from "../useReactions";
const canScreenshare = "getDisplayMedia" in (navigator.mediaDevices ?? {}); const canScreenshare = "getDisplayMedia" in (navigator.mediaDevices ?? {});
@@ -139,14 +139,12 @@ export const ActiveCall: FC<ActiveCallProps> = (props) => {
return ( return (
<RoomContext.Provider value={livekitRoom}> <RoomContext.Provider value={livekitRoom}>
<RaisedHandsProvider> <InCallView
<InCallView {...props}
{...props} vm={vm}
vm={vm} livekitRoom={livekitRoom}
livekitRoom={livekitRoom} connState={connState}
connState={connState} />
/>
</RaisedHandsProvider>
</RoomContext.Provider> </RoomContext.Provider>
); );
}; };
@@ -179,6 +177,8 @@ export const InCallView: FC<InCallViewProps> = ({
connState, connState,
onShareClick, onShareClick,
}) => { }) => {
const { supportsReactions } = useReactions();
useWakeLock(); useWakeLock();
useEffect(() => { useEffect(() => {
@@ -310,10 +310,9 @@ export const InCallView: FC<InCallViewProps> = ({
); );
const memberships = useMatrixRTCSessionMemberships(rtcSession); const memberships = useMatrixRTCSessionMemberships(rtcSession);
const { raisedHands, setRaisedHands } = useRaisedHands(); const { raisedHands, setRaisedHands } = useReactions();
const [reactionId, setReactionId] = useState<string | null>(null); const [reactionId, setReactionId] = useState<string | null>(null);
const [username, localpart] = localParticipant.identity.split(":"); const userId = client.getUserId()!;
const userId = `${username}:${localpart}`;
const isHandRaised = raisedHands.includes(userId); const isHandRaised = raisedHands.includes(userId);
useEffect(() => { useEffect(() => {
@@ -638,13 +637,15 @@ export const InCallView: FC<InCallViewProps> = ({
/>, />,
); );
} }
buttons.push( if (supportsReactions) {
<RaiseHandButton buttons.push(
key="4" <RaiseHandButton
onClick={toggleRaisedHand} key="4"
raised={isHandRaised} onClick={toggleRaisedHand}
/>, raised={isHandRaised}
); />,
);
}
buttons.push(<SettingsButton key="5" onClick={openSettings} />); buttons.push(<SettingsButton key="5" onClick={openSettings} />);
} }

View File

@@ -1,38 +0,0 @@
/*
Copyright 2024 Milton Moura <miltonmoura@gmail.com>
SPDX-License-Identifier: AGPL-3.0-only
Please see LICENSE in the repository root for full details.
*/
import React, { createContext, useContext, useState, ReactNode } from "react";
interface RaisedHandsContextType {
raisedHands: string[];
setRaisedHands: React.Dispatch<React.SetStateAction<string[]>>;
}
const RaisedHandsContext = createContext<RaisedHandsContextType | undefined>(
undefined,
);
export const useRaisedHands = (): RaisedHandsContextType => {
const context = useContext(RaisedHandsContext);
if (!context) {
throw new Error("useRaisedHands must be used within a RaisedHandsProvider");
}
return context;
};
export const RaisedHandsProvider = ({
children,
}: {
children: ReactNode;
}): JSX.Element => {
const [raisedHands, setRaisedHands] = useState<string[]>([]);
return (
<RaisedHandsContext.Provider value={{ raisedHands, setRaisedHands }}>
{children}
</RaisedHandsContext.Provider>
);
};

View File

@@ -44,7 +44,7 @@ import {
import { Slider } from "../Slider"; import { Slider } from "../Slider";
import { MediaView } from "./MediaView"; import { MediaView } from "./MediaView";
import { useLatest } from "../useLatest"; import { useLatest } from "../useLatest";
import { useRaisedHands } from "../room/useRaisedHands"; import { useReactions } from "../useReactions";
interface TileProps { interface TileProps {
className?: string; className?: string;
@@ -91,7 +91,7 @@ const UserMediaTile = forwardRef<HTMLDivElement, UserMediaTileProps>(
}, },
[vm], [vm],
); );
const { raisedHands } = useRaisedHands(); const { raisedHands } = useReactions();
const raisedHand = raisedHands.includes(vm.member?.userId ?? ""); const raisedHand = raisedHands.includes(vm.member?.userId ?? "");
const MicIcon = audioEnabled ? MicOnSolidIcon : MicOffSolidIcon; const MicIcon = audioEnabled ? MicOnSolidIcon : MicOffSolidIcon;

49
src/useReactions.tsx Normal file
View File

@@ -0,0 +1,49 @@
/*
Copyright 2024 Milton Moura <miltonmoura@gmail.com>
SPDX-License-Identifier: AGPL-3.0-only
Please see LICENSE in the repository root for full details.
*/
import React, { createContext, useContext, useState, ReactNode } from "react";
interface ReactionsContextType {
raisedHands: string[];
setRaisedHands: React.Dispatch<React.SetStateAction<string[]>>;
supportsReactions: boolean;
setSupportsReactions: React.Dispatch<React.SetStateAction<boolean>>;
}
const ReactionsContext = createContext<ReactionsContextType | undefined>(
undefined,
);
export const useReactions = (): ReactionsContextType => {
const context = useContext(ReactionsContext);
if (!context) {
throw new Error("useReactions must be used within a ReactionsProvider");
}
return context;
};
export const ReactionsProvider = ({
children,
}: {
children: ReactNode;
}): JSX.Element => {
const [raisedHands, setRaisedHands] = useState<string[]>([]);
const [supportsReactions, setSupportsReactions] = useState<boolean>(true);
return (
<ReactionsContext.Provider
value={{
raisedHands,
setRaisedHands,
supportsReactions,
setSupportsReactions,
}}
>
{children}
</ReactionsContext.Provider>
);
};