Merge pull request #574 from robintown/ec-in-ew
Prepare for integration into Element Web
This commit is contained in:
commit
3bffe58549
12 changed files with 434 additions and 144 deletions
|
@ -38,7 +38,7 @@
|
|||
"classnames": "^2.3.1",
|
||||
"color-hash": "^2.0.1",
|
||||
"events": "^3.3.0",
|
||||
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#83c848093fe49652aedee71d963dfe07fd6d73f2",
|
||||
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#36a6117ee284cefe7d16055352c9cefce30ce6b1",
|
||||
"matrix-widget-api": "^1.0.0",
|
||||
"mermaid": "^8.13.8",
|
||||
"normalize.css": "^8.0.1",
|
||||
|
@ -79,4 +79,4 @@
|
|||
"vite-plugin-html-template": "^1.1.0",
|
||||
"vite-plugin-svgr": "^0.4.0"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -31,10 +31,10 @@ import { logger } from "matrix-js-sdk/src/logger";
|
|||
import { ErrorView } from "./FullScreenView";
|
||||
import {
|
||||
initClient,
|
||||
initMatroskaClient,
|
||||
defaultHomeserver,
|
||||
CryptoStoreIntegrityError,
|
||||
} from "./matrix-utils";
|
||||
import { widget } from "./widget";
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
|
@ -100,16 +100,12 @@ export const ClientProvider: FC<Props> = ({ children }) => {
|
|||
const init = async (): Promise<
|
||||
Pick<ClientProviderState, "client" | "isPasswordlessUser">
|
||||
> => {
|
||||
const query = new URLSearchParams(window.location.search);
|
||||
const widgetId = query.get("widgetId");
|
||||
const parentUrl = query.get("parentUrl");
|
||||
|
||||
if (widgetId && parentUrl) {
|
||||
// We're inside a widget, so let's engage *Matroska mode*
|
||||
logger.log("Using a Matroska client");
|
||||
if (widget) {
|
||||
// We're inside a widget, so let's engage *matryoshka mode*
|
||||
logger.log("Using a matryoshka client");
|
||||
|
||||
return {
|
||||
client: await initMatroskaClient(widgetId, parentUrl),
|
||||
client: await widget.client,
|
||||
isPasswordlessUser: false,
|
||||
};
|
||||
} else {
|
||||
|
|
90
src/LazyEventEmitter.ts
Normal file
90
src/LazyEventEmitter.ts
Normal file
|
@ -0,0 +1,90 @@
|
|||
/*
|
||||
Copyright 2022 Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import EventEmitter from "events";
|
||||
|
||||
type NonEmptyArray<T> = [T, ...T[]];
|
||||
|
||||
/**
|
||||
* An event emitter that lets events pile up in a backlog until a listener is
|
||||
* present, at which point any events that were missed are re-emitted.
|
||||
*/
|
||||
export class LazyEventEmitter extends EventEmitter {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
private eventBacklogs = new Map<string | symbol, NonEmptyArray<any[]>>();
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
public emit(type: string | symbol, ...args: any[]): boolean {
|
||||
const hasListeners = super.emit(type, ...args);
|
||||
|
||||
if (!hasListeners) {
|
||||
// The event was missed, so add it to the backlog
|
||||
const backlog = this.eventBacklogs.get(type);
|
||||
if (backlog) {
|
||||
backlog.push(args);
|
||||
} else {
|
||||
// Start a new backlog
|
||||
this.eventBacklogs.set(type, [args]);
|
||||
}
|
||||
}
|
||||
|
||||
return hasListeners;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
public on(type: string | symbol, listener: (...args: any[]) => void): this {
|
||||
super.on(type, listener);
|
||||
|
||||
const backlog = this.eventBacklogs.get(type);
|
||||
if (backlog) {
|
||||
// That was the first listener for this type, so let's send it all the
|
||||
// events that have piled up
|
||||
for (const args of backlog) super.emit(type, ...args);
|
||||
// Backlog is now clear
|
||||
this.eventBacklogs.delete(type);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public addListener(
|
||||
type: string | symbol,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
listener: (...args: any[]) => void
|
||||
): this {
|
||||
return this.on(type, listener);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
public once(type: string | symbol, listener: (...args: any[]) => void): this {
|
||||
super.once(type, listener);
|
||||
|
||||
const backlog = this.eventBacklogs.get(type);
|
||||
if (backlog) {
|
||||
// That was the first listener for this type, so let's send it the first
|
||||
// of the events that have piled up
|
||||
super.emit(type, ...backlog[0]);
|
||||
// Clear the event from the backlog
|
||||
if (backlog.length === 1) {
|
||||
this.eventBacklogs.delete(type);
|
||||
} else {
|
||||
backlog.shift();
|
||||
}
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
}
|
|
@ -5,23 +5,18 @@ import { MemoryStore } from "matrix-js-sdk/src/store/memory";
|
|||
import { IndexedDBCryptoStore } from "matrix-js-sdk/src/crypto/store/indexeddb-crypto-store";
|
||||
import { LocalStorageCryptoStore } from "matrix-js-sdk/src/crypto/store/localStorage-crypto-store";
|
||||
import { MemoryCryptoStore } from "matrix-js-sdk/src/crypto/store/memory-crypto-store";
|
||||
import {
|
||||
createClient,
|
||||
createRoomWidgetClient,
|
||||
MatrixClient,
|
||||
} from "matrix-js-sdk/src/matrix";
|
||||
import { createClient } from "matrix-js-sdk/src/matrix";
|
||||
import { ICreateClientOpts } from "matrix-js-sdk/src/matrix";
|
||||
import { ClientEvent } from "matrix-js-sdk/src/client";
|
||||
import { EventType } from "matrix-js-sdk/src/@types/event";
|
||||
import { Visibility, Preset } from "matrix-js-sdk/src/@types/partials";
|
||||
import { ISyncStateData, SyncState } from "matrix-js-sdk/src/sync";
|
||||
import { WidgetApi } from "matrix-widget-api";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import {
|
||||
GroupCallIntent,
|
||||
GroupCallType,
|
||||
} from "matrix-js-sdk/src/webrtc/groupCall";
|
||||
|
||||
import type { MatrixClient } from "matrix-js-sdk/src/client";
|
||||
import type { Room } from "matrix-js-sdk/src/models/room";
|
||||
import IndexedDBWorker from "./IndexedDBWorker?worker";
|
||||
import { getRoomParams } from "./room/useRoomParams";
|
||||
|
@ -64,73 +59,6 @@ function waitForSync(client: MatrixClient) {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialises and returns a new widget-API-based Matrix Client.
|
||||
* @param widgetId The ID of the widget that the app is running inside.
|
||||
* @param parentUrl The URL of the parent client.
|
||||
* @returns The MatrixClient instance
|
||||
*/
|
||||
export async function initMatroskaClient(
|
||||
widgetId: string,
|
||||
parentUrl: string
|
||||
): Promise<MatrixClient> {
|
||||
// In this mode, we use a special client which routes all requests through
|
||||
// the host application via the widget API
|
||||
|
||||
const { roomId, userId, deviceId } = getRoomParams();
|
||||
if (!roomId) throw new Error("Room ID must be supplied");
|
||||
if (!userId) throw new Error("User ID must be supplied");
|
||||
if (!deviceId) throw new Error("Device ID must be supplied");
|
||||
|
||||
// These are all the event types the app uses
|
||||
const sendState = [
|
||||
{ eventType: EventType.GroupCallPrefix },
|
||||
{ eventType: EventType.GroupCallMemberPrefix, stateKey: userId },
|
||||
];
|
||||
const receiveState = [
|
||||
{ eventType: EventType.RoomMember },
|
||||
{ eventType: EventType.GroupCallPrefix },
|
||||
{ eventType: EventType.GroupCallMemberPrefix },
|
||||
];
|
||||
const sendRecvToDevice = [
|
||||
EventType.CallInvite,
|
||||
EventType.CallCandidates,
|
||||
EventType.CallAnswer,
|
||||
EventType.CallHangup,
|
||||
EventType.CallReject,
|
||||
EventType.CallSelectAnswer,
|
||||
EventType.CallNegotiate,
|
||||
EventType.CallSDPStreamMetadataChanged,
|
||||
EventType.CallSDPStreamMetadataChangedPrefix,
|
||||
EventType.CallReplaces,
|
||||
"org.matrix.call_duplicate_session",
|
||||
];
|
||||
|
||||
// Since all data should be coming from the host application, there's no
|
||||
// need to persist anything, and therefore we can use the default stores
|
||||
// We don't even need to set up crypto
|
||||
const client = createRoomWidgetClient(
|
||||
new WidgetApi(widgetId, new URL(parentUrl).origin),
|
||||
{
|
||||
sendState,
|
||||
receiveState,
|
||||
sendToDevice: sendRecvToDevice,
|
||||
receiveToDevice: sendRecvToDevice,
|
||||
turnServers: true,
|
||||
},
|
||||
roomId,
|
||||
{
|
||||
baseUrl: "",
|
||||
userId,
|
||||
deviceId,
|
||||
timelineSupport: true,
|
||||
}
|
||||
);
|
||||
|
||||
await client.startClient();
|
||||
return client;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialises and returns a new standalone Matrix Client.
|
||||
* If true is passed for the 'restore' parameter, a check will be made
|
||||
|
|
|
@ -19,6 +19,8 @@ import { useHistory } from "react-router-dom";
|
|||
import { GroupCall, GroupCallState } from "matrix-js-sdk/src/webrtc/groupCall";
|
||||
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 { ErrorView, FullScreenView } from "../FullScreenView";
|
||||
import { LobbyView } from "./LobbyView";
|
||||
|
@ -28,22 +30,30 @@ import { CallEndedView } from "./CallEndedView";
|
|||
import { useRoomAvatar } from "./useRoomAvatar";
|
||||
import { useSentryGroupCallHandler } from "./useSentryGroupCallHandler";
|
||||
import { useLocationNavigation } from "../useLocationNavigation";
|
||||
import { useMediaHandler } from "../settings/useMediaHandler";
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
groupCall: GroupCall;
|
||||
groupCall?: GroupCall;
|
||||
}
|
||||
}
|
||||
|
||||
interface Props {
|
||||
client: MatrixClient;
|
||||
isPasswordlessUser: boolean;
|
||||
isEmbedded: boolean;
|
||||
preload: boolean;
|
||||
hideHeader: boolean;
|
||||
roomIdOrAlias: string;
|
||||
groupCall: GroupCall;
|
||||
}
|
||||
|
||||
export function GroupCallView({
|
||||
client,
|
||||
isPasswordlessUser,
|
||||
isEmbedded,
|
||||
preload,
|
||||
hideHeader,
|
||||
roomIdOrAlias,
|
||||
groupCall,
|
||||
}: Props) {
|
||||
|
@ -69,14 +79,50 @@ export function GroupCallView({
|
|||
unencryptedEventsFromUsers,
|
||||
} = useGroupCall(groupCall);
|
||||
|
||||
const { setAudioInput, setVideoInput } = useMediaHandler();
|
||||
|
||||
const avatarUrl = useRoomAvatar(groupCall.room);
|
||||
|
||||
useEffect(() => {
|
||||
window.groupCall = groupCall;
|
||||
return () => {
|
||||
delete window.groupCall;
|
||||
};
|
||||
}, [groupCall]);
|
||||
|
||||
// In embedded mode, bypass the lobby and just enter the call straight away
|
||||
if (isEmbedded) groupCall.enter();
|
||||
}, [groupCall, isEmbedded]);
|
||||
useEffect(() => {
|
||||
if (widget && preload) {
|
||||
// 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);
|
||||
|
||||
|
@ -86,13 +132,31 @@ export function GroupCallView({
|
|||
const history = useHistory();
|
||||
|
||||
const onLeave = useCallback(() => {
|
||||
setLeft(true);
|
||||
leave();
|
||||
if (widget) {
|
||||
widget.api.transport.send(ElementWidgetActions.HangupCall, {});
|
||||
widget.api.setAlwaysOnScreen(false);
|
||||
}
|
||||
|
||||
if (!isPasswordlessUser) {
|
||||
if (isPasswordlessUser) {
|
||||
setLeft(true);
|
||||
} else if (!isEmbedded) {
|
||||
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) {
|
||||
return <ErrorView error={error} />;
|
||||
|
@ -109,6 +173,7 @@ export function GroupCallView({
|
|||
userMediaFeeds={userMediaFeeds}
|
||||
onLeave={onLeave}
|
||||
isEmbedded={isEmbedded}
|
||||
hideHeader={hideHeader}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
|
@ -131,6 +196,7 @@ export function GroupCallView({
|
|||
screenshareFeeds={screenshareFeeds}
|
||||
roomIdOrAlias={roomIdOrAlias}
|
||||
unencryptedEventsFromUsers={unencryptedEventsFromUsers}
|
||||
hideHeader={hideHeader}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -142,32 +208,33 @@ export function GroupCallView({
|
|||
);
|
||||
} else if (left) {
|
||||
return <CallEndedView client={client} />;
|
||||
} else if (preload) {
|
||||
return null;
|
||||
} else if (isEmbedded) {
|
||||
return (
|
||||
<FullScreenView>
|
||||
<h1>Loading room...</h1>
|
||||
</FullScreenView>
|
||||
);
|
||||
} else {
|
||||
if (isEmbedded) {
|
||||
return (
|
||||
<FullScreenView>
|
||||
<h1>Loading room...</h1>
|
||||
</FullScreenView>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<LobbyView
|
||||
client={client}
|
||||
groupCall={groupCall}
|
||||
roomName={groupCall.room.name}
|
||||
avatarUrl={avatarUrl}
|
||||
state={state}
|
||||
onInitLocalCallFeed={initLocalCallFeed}
|
||||
localCallFeed={localCallFeed}
|
||||
onEnter={enter}
|
||||
microphoneMuted={microphoneMuted}
|
||||
localVideoMuted={localVideoMuted}
|
||||
toggleLocalVideoMuted={toggleLocalVideoMuted}
|
||||
toggleMicrophoneMuted={toggleMicrophoneMuted}
|
||||
roomIdOrAlias={roomIdOrAlias}
|
||||
isEmbedded={isEmbedded}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<LobbyView
|
||||
client={client}
|
||||
groupCall={groupCall}
|
||||
roomName={groupCall.room.name}
|
||||
avatarUrl={avatarUrl}
|
||||
state={state}
|
||||
onInitLocalCallFeed={initLocalCallFeed}
|
||||
localCallFeed={localCallFeed}
|
||||
onEnter={enter}
|
||||
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.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useMemo, useRef } from "react";
|
||||
import React, { useEffect, useCallback, useMemo, useRef } from "react";
|
||||
import { usePreventScroll } from "@react-aria/overlays";
|
||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||
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 classNames from "classnames";
|
||||
|
||||
import type { IWidgetApiRequest } from "matrix-widget-api";
|
||||
import styles from "./InCallView.module.css";
|
||||
import {
|
||||
HangupButton,
|
||||
|
@ -52,6 +53,7 @@ import { useAudioContext } from "../video-grid/useMediaStream";
|
|||
import { useFullscreen } from "../video-grid/useFullscreen";
|
||||
import { AudioContainer } from "../video-grid/AudioContainer";
|
||||
import { useAudioOutputDevice } from "../video-grid/useAudioOutputDevice";
|
||||
import { widget, ElementWidgetActions } from "../widget";
|
||||
|
||||
const canScreenshare = "getDisplayMedia" in (navigator.mediaDevices ?? {});
|
||||
// There is currently a bug in Safari our our code with cloning and sending MediaStreams
|
||||
|
@ -77,6 +79,7 @@ interface Props {
|
|||
localScreenshareFeed: CallFeed;
|
||||
roomIdOrAlias: string;
|
||||
unencryptedEventsFromUsers: Set<string>;
|
||||
hideHeader: boolean;
|
||||
}
|
||||
|
||||
export interface Participant {
|
||||
|
@ -105,6 +108,7 @@ export function InCallView({
|
|||
localScreenshareFeed,
|
||||
roomIdOrAlias,
|
||||
unencryptedEventsFromUsers,
|
||||
hideHeader,
|
||||
}: Props) {
|
||||
usePreventScroll();
|
||||
const elementRef = useRef<HTMLDivElement>();
|
||||
|
@ -122,6 +126,42 @@ export function InCallView({
|
|||
|
||||
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 participants: Participant[] = [];
|
||||
|
||||
|
@ -246,7 +286,7 @@ export function InCallView({
|
|||
audioDestination={audioDestination}
|
||||
/>
|
||||
)}
|
||||
{!fullscreenParticipant && (
|
||||
{!hideHeader && !fullscreenParticipant && (
|
||||
<Header>
|
||||
<LeftNav>
|
||||
<RoomHeaderInfo roomName={roomName} avatarUrl={avatarUrl} />
|
||||
|
|
|
@ -47,6 +47,7 @@ interface Props {
|
|||
localVideoMuted: boolean;
|
||||
roomIdOrAlias: string;
|
||||
isEmbedded: boolean;
|
||||
hideHeader: boolean;
|
||||
}
|
||||
export function LobbyView({
|
||||
client,
|
||||
|
@ -63,6 +64,7 @@ export function LobbyView({
|
|||
toggleMicrophoneMuted,
|
||||
roomIdOrAlias,
|
||||
isEmbedded,
|
||||
hideHeader,
|
||||
}: Props) {
|
||||
const { stream } = useCallFeed(localCallFeed);
|
||||
const {
|
||||
|
@ -90,14 +92,16 @@ export function LobbyView({
|
|||
|
||||
return (
|
||||
<div className={styles.room}>
|
||||
<Header>
|
||||
<LeftNav>
|
||||
<RoomHeaderInfo roomName={roomName} avatarUrl={avatarUrl} />
|
||||
</LeftNav>
|
||||
<RightNav>
|
||||
<UserMenuContainer />
|
||||
</RightNav>
|
||||
</Header>
|
||||
{!hideHeader && (
|
||||
<Header>
|
||||
<LeftNav>
|
||||
<RoomHeaderInfo roomName={roomName} avatarUrl={avatarUrl} />
|
||||
</LeftNav>
|
||||
<RightNav>
|
||||
<UserMenuContainer />
|
||||
</RightNav>
|
||||
</Header>
|
||||
)}
|
||||
<div className={styles.joinRoom}>
|
||||
<div className={styles.joinRoomContent}>
|
||||
{groupCall.isPtt ? (
|
||||
|
|
|
@ -97,6 +97,7 @@ interface Props {
|
|||
userMediaFeeds: CallFeed[];
|
||||
onLeave: () => void;
|
||||
isEmbedded: boolean;
|
||||
hideHeader: boolean;
|
||||
}
|
||||
|
||||
export const PTTCallView: React.FC<Props> = ({
|
||||
|
@ -109,6 +110,7 @@ export const PTTCallView: React.FC<Props> = ({
|
|||
userMediaFeeds,
|
||||
onLeave,
|
||||
isEmbedded,
|
||||
hideHeader,
|
||||
}) => {
|
||||
const { modalState: inviteModalState, modalProps: inviteModalProps } =
|
||||
useModalTriggerState();
|
||||
|
@ -176,7 +178,7 @@ export const PTTCallView: React.FC<Props> = ({
|
|||
// https://github.com/vector-im/element-call/issues/328
|
||||
show={false}
|
||||
/>
|
||||
{showControls && (
|
||||
{!hideHeader && showControls && (
|
||||
<Header className={styles.header}>
|
||||
<LeftNav>
|
||||
<RoomSetupHeaderInfo
|
||||
|
|
|
@ -14,8 +14,9 @@ See the License for the specific language governing permissions and
|
|||
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 { ErrorView, LoadingView } from "../FullScreenView";
|
||||
import { RoomAuthView } from "./RoomAuthView";
|
||||
|
@ -29,8 +30,16 @@ export const RoomPage: FC = () => {
|
|||
const { loading, isAuthenticated, error, client, isPasswordlessUser } =
|
||||
useClient();
|
||||
|
||||
const { roomAlias, roomId, viaServers, isEmbedded, isPtt, displayName } =
|
||||
useRoomParams();
|
||||
const {
|
||||
roomAlias,
|
||||
roomId,
|
||||
viaServers,
|
||||
isEmbedded,
|
||||
preload,
|
||||
hideHeader,
|
||||
isPtt,
|
||||
displayName,
|
||||
} = useRoomParams();
|
||||
const roomIdOrAlias = roomId ?? roomAlias;
|
||||
if (!roomIdOrAlias) throw new Error("No room specified");
|
||||
|
||||
|
@ -53,6 +62,21 @@ export const RoomPage: FC = () => {
|
|||
registerPasswordlessUser,
|
||||
]);
|
||||
|
||||
const groupCallView = useCallback(
|
||||
(groupCall: GroupCall) => (
|
||||
<GroupCallView
|
||||
client={client}
|
||||
roomIdOrAlias={roomIdOrAlias}
|
||||
groupCall={groupCall}
|
||||
isPasswordlessUser={isPasswordlessUser}
|
||||
isEmbedded={isEmbedded}
|
||||
preload={preload}
|
||||
hideHeader={hideHeader}
|
||||
/>
|
||||
),
|
||||
[client, roomIdOrAlias, isPasswordlessUser, isEmbedded, preload, hideHeader]
|
||||
);
|
||||
|
||||
if (loading || isRegistering) {
|
||||
return <LoadingView />;
|
||||
}
|
||||
|
@ -73,15 +97,7 @@ export const RoomPage: FC = () => {
|
|||
viaServers={viaServers}
|
||||
createPtt={isPtt}
|
||||
>
|
||||
{(groupCall) => (
|
||||
<GroupCallView
|
||||
client={client}
|
||||
roomIdOrAlias={roomIdOrAlias}
|
||||
groupCall={groupCall}
|
||||
isPasswordlessUser={isPasswordlessUser}
|
||||
isEmbedded={isEmbedded}
|
||||
/>
|
||||
)}
|
||||
{groupCallView}
|
||||
</GroupCallLoader>
|
||||
</MediaHandlerProvider>
|
||||
);
|
||||
|
|
|
@ -24,6 +24,11 @@ export interface RoomParams {
|
|||
// Whether the app is running in embedded mode, and should keep the user
|
||||
// confined to the current room
|
||||
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
|
||||
hideHeader: boolean;
|
||||
// Whether to start a walkie-talkie call instead of a video call
|
||||
isPtt: boolean;
|
||||
// Whether to use end-to-end encryption
|
||||
|
@ -75,6 +80,8 @@ export const getRoomParams = (
|
|||
roomId: getParam("roomId"),
|
||||
viaServers: getAllParams("via"),
|
||||
isEmbedded: hasParam("embed"),
|
||||
preload: hasParam("preload"),
|
||||
hideHeader: hasParam("hideHeader"),
|
||||
isPtt: hasParam("ptt"),
|
||||
e2eEnabled: getParam("enableE2e") !== "false", // Defaults to true
|
||||
userId: getParam("userId"),
|
||||
|
|
140
src/widget.ts
Normal file
140
src/widget.ts
Normal file
|
@ -0,0 +1,140 @@
|
|||
/*
|
||||
Copyright 2022 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { EventType } from "matrix-js-sdk/src/@types/event";
|
||||
import { createRoomWidgetClient } from "matrix-js-sdk/src/matrix";
|
||||
import { WidgetApi, MatrixCapabilities } from "matrix-widget-api";
|
||||
|
||||
import type { MatrixClient } from "matrix-js-sdk/src/client";
|
||||
import type { IWidgetApiRequest } from "matrix-widget-api";
|
||||
import { LazyEventEmitter } from "./LazyEventEmitter";
|
||||
import { getRoomParams } from "./room/useRoomParams";
|
||||
|
||||
// Subset of the actions in matrix-react-sdk
|
||||
export enum ElementWidgetActions {
|
||||
JoinCall = "io.element.join",
|
||||
HangupCall = "im.vector.hangup",
|
||||
TileLayout = "io.element.tile_layout",
|
||||
SpotlightLayout = "io.element.spotlight_layout",
|
||||
}
|
||||
|
||||
export interface JoinCallData {
|
||||
audioInput: string | null;
|
||||
videoInput: string | null;
|
||||
}
|
||||
|
||||
interface WidgetHelpers {
|
||||
api: WidgetApi;
|
||||
lazyActions: LazyEventEmitter;
|
||||
client: Promise<MatrixClient>;
|
||||
}
|
||||
|
||||
/**
|
||||
* A point of access to the widget API, if the app is running as a widget. This
|
||||
* is declared and initialized on the top level because the widget messaging
|
||||
* needs to be set up ASAP on load to ensure it doesn't miss any requests.
|
||||
*/
|
||||
export const widget: WidgetHelpers | null = (() => {
|
||||
try {
|
||||
const query = new URLSearchParams(window.location.search);
|
||||
const widgetId = query.get("widgetId");
|
||||
const parentUrl = query.get("parentUrl");
|
||||
|
||||
if (widgetId && parentUrl) {
|
||||
const parentOrigin = new URL(parentUrl).origin;
|
||||
logger.info("Widget API is available");
|
||||
const api = new WidgetApi(widgetId, parentOrigin);
|
||||
api.requestCapability(MatrixCapabilities.AlwaysOnScreen);
|
||||
|
||||
// Set up the lazy action emitter, but only for select actions that we
|
||||
// intend for the app to handle
|
||||
const lazyActions = new LazyEventEmitter();
|
||||
[
|
||||
ElementWidgetActions.JoinCall,
|
||||
ElementWidgetActions.HangupCall,
|
||||
ElementWidgetActions.TileLayout,
|
||||
ElementWidgetActions.SpotlightLayout,
|
||||
].forEach((action) => {
|
||||
api.on(`action:${action}`, (ev: CustomEvent<IWidgetApiRequest>) => {
|
||||
ev.preventDefault();
|
||||
lazyActions.emit(action, ev);
|
||||
});
|
||||
});
|
||||
|
||||
// Now, initialize the matryoshka MatrixClient (so named because it routes
|
||||
// all requests through the host client via the widget API)
|
||||
// We need to do this now rather than later because it has capabilities to
|
||||
// request, and is responsible for starting the transport (should it be?)
|
||||
|
||||
const { roomId, userId, deviceId } = getRoomParams();
|
||||
if (!roomId) throw new Error("Room ID must be supplied");
|
||||
if (!userId) throw new Error("User ID must be supplied");
|
||||
if (!deviceId) throw new Error("Device ID must be supplied");
|
||||
|
||||
// These are all the event types the app uses
|
||||
const sendState = [
|
||||
{ eventType: EventType.GroupCallPrefix },
|
||||
{ eventType: EventType.GroupCallMemberPrefix, stateKey: userId },
|
||||
];
|
||||
const receiveState = [
|
||||
{ eventType: EventType.RoomMember },
|
||||
{ eventType: EventType.GroupCallPrefix },
|
||||
{ eventType: EventType.GroupCallMemberPrefix },
|
||||
];
|
||||
const sendRecvToDevice = [
|
||||
EventType.CallInvite,
|
||||
EventType.CallCandidates,
|
||||
EventType.CallAnswer,
|
||||
EventType.CallHangup,
|
||||
EventType.CallReject,
|
||||
EventType.CallSelectAnswer,
|
||||
EventType.CallNegotiate,
|
||||
EventType.CallSDPStreamMetadataChanged,
|
||||
EventType.CallSDPStreamMetadataChangedPrefix,
|
||||
EventType.CallReplaces,
|
||||
"org.matrix.call_duplicate_session",
|
||||
];
|
||||
|
||||
const client = createRoomWidgetClient(
|
||||
api,
|
||||
{
|
||||
sendState,
|
||||
receiveState,
|
||||
sendToDevice: sendRecvToDevice,
|
||||
receiveToDevice: sendRecvToDevice,
|
||||
turnServers: true,
|
||||
},
|
||||
roomId,
|
||||
{
|
||||
baseUrl: "",
|
||||
userId,
|
||||
deviceId,
|
||||
timelineSupport: true,
|
||||
}
|
||||
);
|
||||
const clientPromise = client.startClient().then(() => client);
|
||||
|
||||
return { api, lazyActions, client: clientPromise };
|
||||
} else {
|
||||
logger.info("No widget API available");
|
||||
return null;
|
||||
}
|
||||
} catch (e) {
|
||||
logger.warn("Continuing without the widget API", e);
|
||||
return null;
|
||||
}
|
||||
})();
|
|
@ -8390,9 +8390,9 @@ matrix-events-sdk@^0.0.1-beta.7:
|
|||
resolved "https://registry.yarnpkg.com/matrix-events-sdk/-/matrix-events-sdk-0.0.1-beta.7.tgz#5ffe45eba1f67cc8d7c2377736c728b322524934"
|
||||
integrity sha512-9jl4wtWanUFSy2sr2lCjErN/oC8KTAtaeaozJtrgot1JiQcEI4Rda9OLgQ7nLKaqb4Z/QUx/fR3XpDzm5Jy1JA==
|
||||
|
||||
"matrix-js-sdk@github:matrix-org/matrix-js-sdk#83c848093fe49652aedee71d963dfe07fd6d73f2":
|
||||
version "19.3.0"
|
||||
resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/83c848093fe49652aedee71d963dfe07fd6d73f2"
|
||||
"matrix-js-sdk@github:matrix-org/matrix-js-sdk#36a6117ee284cefe7d16055352c9cefce30ce6b1":
|
||||
version "19.4.0"
|
||||
resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/36a6117ee284cefe7d16055352c9cefce30ce6b1"
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.12.5"
|
||||
"@types/sdp-transform" "^2.4.5"
|
||||
|
|
Loading…
Add table
Reference in a new issue