mirror of
https://github.com/vector-im/element-call.git
synced 2026-07-18 18:59:23 +00:00
Check for reaction & redaction capabilities in widget mode
Signed-off-by: Milton Moura <miltonmoura@gmail.com>
This commit is contained in:
45
src/App.tsx
45
src/App.tsx
@@ -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 />
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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} />);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -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
49
src/useReactions.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user