Add widget actions for joining and leaving calls and switching layouts
These actions are processed lazily to ensure that even if the app takes a while to start up, they won't be missed.
This commit is contained in:
parent
f0045c9406
commit
b7be3011da
4 changed files with 141 additions and 36 deletions
|
@ -19,6 +19,8 @@ import { useHistory } from "react-router-dom";
|
||||||
import { GroupCall, GroupCallState } from "matrix-js-sdk/src/webrtc/groupCall";
|
import { GroupCall, GroupCallState } from "matrix-js-sdk/src/webrtc/groupCall";
|
||||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||||
|
|
||||||
|
import type { IWidgetApiRequest } from "matrix-widget-api";
|
||||||
|
import { widget, ElementWidgetActions, JoinCallData } from "../widget";
|
||||||
import { useGroupCall } from "./useGroupCall";
|
import { useGroupCall } from "./useGroupCall";
|
||||||
import { ErrorView, FullScreenView } from "../FullScreenView";
|
import { ErrorView, FullScreenView } from "../FullScreenView";
|
||||||
import { LobbyView } from "./LobbyView";
|
import { LobbyView } from "./LobbyView";
|
||||||
|
@ -28,6 +30,8 @@ import { CallEndedView } from "./CallEndedView";
|
||||||
import { useRoomAvatar } from "./useRoomAvatar";
|
import { useRoomAvatar } from "./useRoomAvatar";
|
||||||
import { useSentryGroupCallHandler } from "./useSentryGroupCallHandler";
|
import { useSentryGroupCallHandler } from "./useSentryGroupCallHandler";
|
||||||
import { useLocationNavigation } from "../useLocationNavigation";
|
import { useLocationNavigation } from "../useLocationNavigation";
|
||||||
|
import { useMediaHandler } from "../settings/useMediaHandler";
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
groupCall?: GroupCall;
|
groupCall?: GroupCall;
|
||||||
|
@ -38,6 +42,7 @@ interface Props {
|
||||||
client: MatrixClient;
|
client: MatrixClient;
|
||||||
isPasswordlessUser: boolean;
|
isPasswordlessUser: boolean;
|
||||||
isEmbedded: boolean;
|
isEmbedded: boolean;
|
||||||
|
preload: boolean;
|
||||||
hideHeader: boolean;
|
hideHeader: boolean;
|
||||||
roomIdOrAlias: string;
|
roomIdOrAlias: string;
|
||||||
groupCall: GroupCall;
|
groupCall: GroupCall;
|
||||||
|
@ -47,6 +52,7 @@ export function GroupCallView({
|
||||||
client,
|
client,
|
||||||
isPasswordlessUser,
|
isPasswordlessUser,
|
||||||
isEmbedded,
|
isEmbedded,
|
||||||
|
preload,
|
||||||
hideHeader,
|
hideHeader,
|
||||||
roomIdOrAlias,
|
roomIdOrAlias,
|
||||||
groupCall,
|
groupCall,
|
||||||
|
@ -73,14 +79,50 @@ export function GroupCallView({
|
||||||
unencryptedEventsFromUsers,
|
unencryptedEventsFromUsers,
|
||||||
} = useGroupCall(groupCall);
|
} = useGroupCall(groupCall);
|
||||||
|
|
||||||
|
const { setAudioInput, setVideoInput } = useMediaHandler();
|
||||||
|
|
||||||
const avatarUrl = useRoomAvatar(groupCall.room);
|
const avatarUrl = useRoomAvatar(groupCall.room);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
window.groupCall = groupCall;
|
window.groupCall = groupCall;
|
||||||
|
return () => {
|
||||||
|
delete window.groupCall;
|
||||||
|
};
|
||||||
|
}, [groupCall]);
|
||||||
|
|
||||||
// In embedded mode, bypass the lobby and just enter the call straight away
|
useEffect(() => {
|
||||||
if (isEmbedded) groupCall.enter();
|
if (widget && preload) {
|
||||||
}, [groupCall, isEmbedded]);
|
// In preload mode, wait for a join action before entering
|
||||||
|
const onJoin = async (ev: CustomEvent<IWidgetApiRequest>) => {
|
||||||
|
const { audioInput, videoInput } = ev.detail
|
||||||
|
.data as unknown as JoinCallData;
|
||||||
|
if (audioInput !== null) setAudioInput(audioInput);
|
||||||
|
if (videoInput !== null) setVideoInput(videoInput);
|
||||||
|
await Promise.all([
|
||||||
|
groupCall.setMicrophoneMuted(audioInput === null),
|
||||||
|
groupCall.setLocalVideoMuted(videoInput === null),
|
||||||
|
]);
|
||||||
|
|
||||||
|
await groupCall.enter();
|
||||||
|
await Promise.all([
|
||||||
|
widget.api.setAlwaysOnScreen(true),
|
||||||
|
widget.api.transport.reply(ev.detail, {}),
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
widget.lazyActions.on(ElementWidgetActions.JoinCall, onJoin);
|
||||||
|
return () => {
|
||||||
|
widget.lazyActions.off(ElementWidgetActions.JoinCall, onJoin);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}, [groupCall, preload, setAudioInput, setVideoInput]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isEmbedded && !preload) {
|
||||||
|
// In embedded mode, bypass the lobby and just enter the call straight away
|
||||||
|
groupCall.enter();
|
||||||
|
}
|
||||||
|
}, [groupCall, isEmbedded, preload]);
|
||||||
|
|
||||||
useSentryGroupCallHandler(groupCall);
|
useSentryGroupCallHandler(groupCall);
|
||||||
|
|
||||||
|
@ -90,13 +132,31 @@ export function GroupCallView({
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
|
||||||
const onLeave = useCallback(() => {
|
const onLeave = useCallback(() => {
|
||||||
setLeft(true);
|
|
||||||
leave();
|
leave();
|
||||||
|
if (widget) {
|
||||||
|
widget.api.transport.send(ElementWidgetActions.HangupCall, {});
|
||||||
|
widget.api.setAlwaysOnScreen(false);
|
||||||
|
}
|
||||||
|
|
||||||
if (!isPasswordlessUser) {
|
if (isPasswordlessUser) {
|
||||||
|
setLeft(true);
|
||||||
|
} else if (!isEmbedded) {
|
||||||
history.push("/");
|
history.push("/");
|
||||||
}
|
}
|
||||||
}, [leave, isPasswordlessUser, history]);
|
}, [leave, isPasswordlessUser, isEmbedded, history]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (widget && state === GroupCallState.Entered) {
|
||||||
|
const onHangup = async (ev: CustomEvent<IWidgetApiRequest>) => {
|
||||||
|
leave();
|
||||||
|
await widget.api.transport.reply(ev.detail, {});
|
||||||
|
};
|
||||||
|
widget.lazyActions.once(ElementWidgetActions.HangupCall, onHangup);
|
||||||
|
return () => {
|
||||||
|
widget.lazyActions.off(ElementWidgetActions.HangupCall, onHangup);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}, [groupCall, state, leave]);
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return <ErrorView error={error} />;
|
return <ErrorView error={error} />;
|
||||||
|
@ -148,33 +208,33 @@ export function GroupCallView({
|
||||||
);
|
);
|
||||||
} else if (left) {
|
} else if (left) {
|
||||||
return <CallEndedView client={client} />;
|
return <CallEndedView client={client} />;
|
||||||
|
} else if (preload) {
|
||||||
|
return null;
|
||||||
|
} else if (isEmbedded) {
|
||||||
|
return (
|
||||||
|
<FullScreenView>
|
||||||
|
<h1>Loading room...</h1>
|
||||||
|
</FullScreenView>
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
if (isEmbedded) {
|
return (
|
||||||
return (
|
<LobbyView
|
||||||
<FullScreenView>
|
client={client}
|
||||||
<h1>Loading room...</h1>
|
groupCall={groupCall}
|
||||||
</FullScreenView>
|
roomName={groupCall.room.name}
|
||||||
);
|
avatarUrl={avatarUrl}
|
||||||
} else {
|
state={state}
|
||||||
return (
|
onInitLocalCallFeed={initLocalCallFeed}
|
||||||
<LobbyView
|
localCallFeed={localCallFeed}
|
||||||
client={client}
|
onEnter={enter}
|
||||||
groupCall={groupCall}
|
microphoneMuted={microphoneMuted}
|
||||||
roomName={groupCall.room.name}
|
localVideoMuted={localVideoMuted}
|
||||||
avatarUrl={avatarUrl}
|
toggleLocalVideoMuted={toggleLocalVideoMuted}
|
||||||
state={state}
|
toggleMicrophoneMuted={toggleMicrophoneMuted}
|
||||||
onInitLocalCallFeed={initLocalCallFeed}
|
roomIdOrAlias={roomIdOrAlias}
|
||||||
localCallFeed={localCallFeed}
|
isEmbedded={isEmbedded}
|
||||||
onEnter={enter}
|
hideHeader={hideHeader}
|
||||||
microphoneMuted={microphoneMuted}
|
/>
|
||||||
localVideoMuted={localVideoMuted}
|
);
|
||||||
toggleLocalVideoMuted={toggleLocalVideoMuted}
|
|
||||||
toggleMicrophoneMuted={toggleMicrophoneMuted}
|
|
||||||
roomIdOrAlias={roomIdOrAlias}
|
|
||||||
isEmbedded={isEmbedded}
|
|
||||||
hideHeader={hideHeader}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useCallback, useMemo, useRef } from "react";
|
import React, { useEffect, useCallback, useMemo, useRef } from "react";
|
||||||
import { usePreventScroll } from "@react-aria/overlays";
|
import { usePreventScroll } from "@react-aria/overlays";
|
||||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||||
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||||
|
@ -22,6 +22,7 @@ import { GroupCall } from "matrix-js-sdk/src/webrtc/groupCall";
|
||||||
import { CallFeed } from "matrix-js-sdk/src/webrtc/callFeed";
|
import { CallFeed } from "matrix-js-sdk/src/webrtc/callFeed";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
|
|
||||||
|
import type { IWidgetApiRequest } from "matrix-widget-api";
|
||||||
import styles from "./InCallView.module.css";
|
import styles from "./InCallView.module.css";
|
||||||
import {
|
import {
|
||||||
HangupButton,
|
HangupButton,
|
||||||
|
@ -52,6 +53,7 @@ import { useAudioContext } from "../video-grid/useMediaStream";
|
||||||
import { useFullscreen } from "../video-grid/useFullscreen";
|
import { useFullscreen } from "../video-grid/useFullscreen";
|
||||||
import { AudioContainer } from "../video-grid/AudioContainer";
|
import { AudioContainer } from "../video-grid/AudioContainer";
|
||||||
import { useAudioOutputDevice } from "../video-grid/useAudioOutputDevice";
|
import { useAudioOutputDevice } from "../video-grid/useAudioOutputDevice";
|
||||||
|
import { widget, ElementWidgetActions } from "../widget";
|
||||||
|
|
||||||
const canScreenshare = "getDisplayMedia" in (navigator.mediaDevices ?? {});
|
const canScreenshare = "getDisplayMedia" in (navigator.mediaDevices ?? {});
|
||||||
// There is currently a bug in Safari our our code with cloning and sending MediaStreams
|
// There is currently a bug in Safari our our code with cloning and sending MediaStreams
|
||||||
|
@ -124,6 +126,42 @@ export function InCallView({
|
||||||
|
|
||||||
useAudioOutputDevice(audioRef, audioOutput);
|
useAudioOutputDevice(audioRef, audioOutput);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
widget?.api.transport.send(
|
||||||
|
layout === "freedom"
|
||||||
|
? ElementWidgetActions.TileLayout
|
||||||
|
: ElementWidgetActions.SpotlightLayout,
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
}, [layout]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (widget) {
|
||||||
|
const onTileLayout = async (ev: CustomEvent<IWidgetApiRequest>) => {
|
||||||
|
setLayout("freedom");
|
||||||
|
await widget.api.transport.reply(ev.detail, {});
|
||||||
|
};
|
||||||
|
const onSpotlightLayout = async (ev: CustomEvent<IWidgetApiRequest>) => {
|
||||||
|
setLayout("spotlight");
|
||||||
|
await widget.api.transport.reply(ev.detail, {});
|
||||||
|
};
|
||||||
|
|
||||||
|
widget.lazyActions.on(ElementWidgetActions.TileLayout, onTileLayout);
|
||||||
|
widget.lazyActions.on(
|
||||||
|
ElementWidgetActions.SpotlightLayout,
|
||||||
|
onSpotlightLayout
|
||||||
|
);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
widget.lazyActions.off(ElementWidgetActions.TileLayout, onTileLayout);
|
||||||
|
widget.lazyActions.off(
|
||||||
|
ElementWidgetActions.SpotlightLayout,
|
||||||
|
onSpotlightLayout
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}, [setLayout]);
|
||||||
|
|
||||||
const items = useMemo(() => {
|
const items = useMemo(() => {
|
||||||
const participants: Participant[] = [];
|
const participants: Participant[] = [];
|
||||||
|
|
||||||
|
|
|
@ -14,8 +14,9 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { FC, useEffect, useState } from "react";
|
import React, { FC, useEffect, useState, useCallback } from "react";
|
||||||
|
|
||||||
|
import type { GroupCall } from "matrix-js-sdk/src/webrtc/groupCall";
|
||||||
import { useClient } from "../ClientContext";
|
import { useClient } from "../ClientContext";
|
||||||
import { ErrorView, LoadingView } from "../FullScreenView";
|
import { ErrorView, LoadingView } from "../FullScreenView";
|
||||||
import { RoomAuthView } from "./RoomAuthView";
|
import { RoomAuthView } from "./RoomAuthView";
|
||||||
|
@ -34,6 +35,7 @@ export const RoomPage: FC = () => {
|
||||||
roomId,
|
roomId,
|
||||||
viaServers,
|
viaServers,
|
||||||
isEmbedded,
|
isEmbedded,
|
||||||
|
preload,
|
||||||
hideHeader,
|
hideHeader,
|
||||||
isPtt,
|
isPtt,
|
||||||
displayName,
|
displayName,
|
||||||
|
@ -68,10 +70,11 @@ export const RoomPage: FC = () => {
|
||||||
groupCall={groupCall}
|
groupCall={groupCall}
|
||||||
isPasswordlessUser={isPasswordlessUser}
|
isPasswordlessUser={isPasswordlessUser}
|
||||||
isEmbedded={isEmbedded}
|
isEmbedded={isEmbedded}
|
||||||
|
preload={preload}
|
||||||
hideHeader={hideHeader}
|
hideHeader={hideHeader}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
[client, roomIdOrAlias, isPasswordlessUser, isEmbedded, hideHeader]
|
[client, roomIdOrAlias, isPasswordlessUser, isEmbedded, preload, hideHeader]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (loading || isRegistering) {
|
if (loading || isRegistering) {
|
||||||
|
|
|
@ -24,6 +24,9 @@ export interface RoomParams {
|
||||||
// Whether the app is running in embedded mode, and should keep the user
|
// Whether the app is running in embedded mode, and should keep the user
|
||||||
// confined to the current room
|
// confined to the current room
|
||||||
isEmbedded: boolean;
|
isEmbedded: boolean;
|
||||||
|
// Whether the app should pause before joining the call until it sees an
|
||||||
|
// io.element.join widget action, allowing it to be preloaded
|
||||||
|
preload: boolean;
|
||||||
// Whether to hide the room header when in a call
|
// Whether to hide the room header when in a call
|
||||||
hideHeader: boolean;
|
hideHeader: boolean;
|
||||||
// Whether to start a walkie-talkie call instead of a video call
|
// Whether to start a walkie-talkie call instead of a video call
|
||||||
|
@ -77,6 +80,7 @@ export const getRoomParams = (
|
||||||
roomId: getParam("roomId"),
|
roomId: getParam("roomId"),
|
||||||
viaServers: getAllParams("via"),
|
viaServers: getAllParams("via"),
|
||||||
isEmbedded: hasParam("embed"),
|
isEmbedded: hasParam("embed"),
|
||||||
|
preload: hasParam("preload"),
|
||||||
hideHeader: hasParam("hideHeader"),
|
hideHeader: hasParam("hideHeader"),
|
||||||
isPtt: hasParam("ptt"),
|
isPtt: hasParam("ptt"),
|
||||||
e2eEnabled: getParam("enableE2e") !== "false", // Defaults to true
|
e2eEnabled: getParam("enableE2e") !== "false", // Defaults to true
|
||||||
|
|
Loading…
Reference in a new issue