Enable users to join calls from multiple devices
This commit is contained in:
parent
46e429c37b
commit
13def24f7e
11 changed files with 199 additions and 216 deletions
|
@ -45,7 +45,7 @@
|
||||||
"i18next": "^21.10.0",
|
"i18next": "^21.10.0",
|
||||||
"i18next-browser-languagedetector": "^6.1.8",
|
"i18next-browser-languagedetector": "^6.1.8",
|
||||||
"i18next-http-backend": "^1.4.4",
|
"i18next-http-backend": "^1.4.4",
|
||||||
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#3f1c3392d45b0fc054c3788cc6c043cd5b4fb730",
|
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#f46ecf970c658ae34b8d5fc3e73369c31ac79e90",
|
||||||
"matrix-widget-api": "^1.0.0",
|
"matrix-widget-api": "^1.0.0",
|
||||||
"mermaid": "^8.13.8",
|
"mermaid": "^8.13.8",
|
||||||
"normalize.css": "^8.0.1",
|
"normalize.css": "^8.0.1",
|
||||||
|
|
|
@ -25,8 +25,7 @@ import React, {
|
||||||
useRef,
|
useRef,
|
||||||
} from "react";
|
} from "react";
|
||||||
import { useHistory } from "react-router-dom";
|
import { useHistory } from "react-router-dom";
|
||||||
import { MatrixClient, ClientEvent } from "matrix-js-sdk/src/client";
|
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
@ -40,6 +39,7 @@ import {
|
||||||
import { widget } from "./widget";
|
import { widget } from "./widget";
|
||||||
import { PosthogAnalytics, RegistrationType } from "./PosthogAnalytics";
|
import { PosthogAnalytics, RegistrationType } from "./PosthogAnalytics";
|
||||||
import { translatedError } from "./TranslatedError";
|
import { translatedError } from "./TranslatedError";
|
||||||
|
import { useEventTarget } from "./useEvents";
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
|
@ -55,6 +55,8 @@ export interface Session {
|
||||||
tempPassword?: string;
|
tempPassword?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const loadChannel = new BroadcastChannel("load");
|
||||||
|
|
||||||
const loadSession = (): Session => {
|
const loadSession = (): Session => {
|
||||||
const data = localStorage.getItem("matrix-auth-store");
|
const data = localStorage.getItem("matrix-auth-store");
|
||||||
if (data) return JSON.parse(data);
|
if (data) return JSON.parse(data);
|
||||||
|
@ -292,47 +294,29 @@ export const ClientProvider: FC<Props> = ({ children }) => {
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
// To protect against multiple sessions writing to the same storage
|
||||||
|
// simultaneously, we send a broadcast message that shuts down all other
|
||||||
|
// running instances of the app. This isn't necessary if the app is running in
|
||||||
|
// a widget though, since then it'll be mostly stateless.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// To protect against multiple sessions writing to the same storage
|
if (!widget) loadChannel.postMessage({});
|
||||||
// simultaneously, we send a to-device message that shuts down all other
|
}, []);
|
||||||
// running instances of the app. This isn't necessary if the app is running
|
|
||||||
// in a widget though, since then it'll be mostly stateless.
|
|
||||||
if (!widget && client) {
|
|
||||||
const loadTime = Date.now();
|
|
||||||
|
|
||||||
const onToDeviceEvent = (event: MatrixEvent) => {
|
useEventTarget(
|
||||||
if (event.getType() !== "org.matrix.call_duplicate_session") return;
|
loadChannel,
|
||||||
|
"message",
|
||||||
|
useCallback(() => {
|
||||||
|
client?.stopClient();
|
||||||
|
|
||||||
const content = event.getContent();
|
setState((prev) => ({
|
||||||
|
...prev,
|
||||||
if (content.session_id === client.getSessionId()) return;
|
error: translatedError(
|
||||||
|
"This application has been opened in another tab.",
|
||||||
if (content.timestamp > loadTime) {
|
t
|
||||||
client?.stopClient();
|
),
|
||||||
|
}));
|
||||||
setState((prev) => ({
|
}, [client, setState, t])
|
||||||
...prev,
|
);
|
||||||
error: translatedError(
|
|
||||||
"This application has been opened in another tab.",
|
|
||||||
t
|
|
||||||
),
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
client.on(ClientEvent.ToDeviceEvent, onToDeviceEvent);
|
|
||||||
|
|
||||||
client.sendToDevice("org.matrix.call_duplicate_session", {
|
|
||||||
[client.getUserId()]: {
|
|
||||||
"*": { session_id: client.getSessionId(), timestamp: loadTime },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
client?.removeListener(ClientEvent.ToDeviceEvent, onToDeviceEvent);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}, [client, t]);
|
|
||||||
|
|
||||||
const context = useMemo<ClientState>(
|
const context = useMemo<ClientState>(
|
||||||
() => ({
|
() => ({
|
||||||
|
|
|
@ -32,7 +32,7 @@ const overlapMap: Partial<Record<Size, number>> = {
|
||||||
interface Props extends HTMLAttributes<HTMLDivElement> {
|
interface Props extends HTMLAttributes<HTMLDivElement> {
|
||||||
className: string;
|
className: string;
|
||||||
client: MatrixClient;
|
client: MatrixClient;
|
||||||
participants: RoomMember[];
|
members: RoomMember[];
|
||||||
max?: number;
|
max?: number;
|
||||||
size?: Size;
|
size?: Size;
|
||||||
}
|
}
|
||||||
|
@ -40,7 +40,7 @@ interface Props extends HTMLAttributes<HTMLDivElement> {
|
||||||
export function Facepile({
|
export function Facepile({
|
||||||
className,
|
className,
|
||||||
client,
|
client,
|
||||||
participants,
|
members,
|
||||||
max = 3,
|
max = 3,
|
||||||
size = Size.XS,
|
size = Size.XS,
|
||||||
...rest
|
...rest
|
||||||
|
@ -51,14 +51,14 @@ export function Facepile({
|
||||||
const _overlap = overlapMap[size];
|
const _overlap = overlapMap[size];
|
||||||
|
|
||||||
const title = useMemo(() => {
|
const title = useMemo(() => {
|
||||||
return participants.reduce<string | null>(
|
return members.reduce<string | null>(
|
||||||
(prev, curr) =>
|
(prev, curr) =>
|
||||||
prev === null
|
prev === null
|
||||||
? curr.name
|
? curr.name
|
||||||
: t("{{names}}, {{name}}", { names: prev, name: curr.name }),
|
: t("{{names}}, {{name}}", { names: prev, name: curr.name }),
|
||||||
null
|
null
|
||||||
) as string;
|
) as string;
|
||||||
}, [participants, t]);
|
}, [members, t]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
@ -66,12 +66,11 @@ export function Facepile({
|
||||||
title={title}
|
title={title}
|
||||||
style={{
|
style={{
|
||||||
width:
|
width:
|
||||||
Math.min(participants.length, max + 1) * (_size - _overlap) +
|
Math.min(members.length, max + 1) * (_size - _overlap) + _overlap,
|
||||||
_overlap,
|
|
||||||
}}
|
}}
|
||||||
{...rest}
|
{...rest}
|
||||||
>
|
>
|
||||||
{participants.slice(0, max).map((member, i) => {
|
{members.slice(0, max).map((member, i) => {
|
||||||
const avatarUrl = member.getMxcAvatarUrl();
|
const avatarUrl = member.getMxcAvatarUrl();
|
||||||
return (
|
return (
|
||||||
<Avatar
|
<Avatar
|
||||||
|
@ -84,11 +83,11 @@ export function Facepile({
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{participants.length > max && (
|
{members.length > max && (
|
||||||
<Avatar
|
<Avatar
|
||||||
key="additional"
|
key="additional"
|
||||||
size={size}
|
size={size}
|
||||||
fallback={`+${participants.length - max}`}
|
fallback={`+${members.length - max}`}
|
||||||
className={styles.avatar}
|
className={styles.avatar}
|
||||||
style={{ left: max * (_size - _overlap) }}
|
style={{ left: max * (_size - _overlap) }}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -92,7 +92,7 @@ function CallTile({
|
||||||
<Facepile
|
<Facepile
|
||||||
className={styles.facePile}
|
className={styles.facePile}
|
||||||
client={client}
|
client={client}
|
||||||
participants={participants}
|
members={participants}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -79,7 +79,6 @@ export function GroupCallView({
|
||||||
isScreensharing,
|
isScreensharing,
|
||||||
screenshareFeeds,
|
screenshareFeeds,
|
||||||
participants,
|
participants,
|
||||||
calls,
|
|
||||||
unencryptedEventsFromUsers,
|
unencryptedEventsFromUsers,
|
||||||
} = useGroupCall(groupCall);
|
} = useGroupCall(groupCall);
|
||||||
|
|
||||||
|
@ -173,9 +172,14 @@ export function GroupCallView({
|
||||||
const onLeave = useCallback(() => {
|
const onLeave = useCallback(() => {
|
||||||
setLeft(true);
|
setLeft(true);
|
||||||
|
|
||||||
|
let participantCount = 0;
|
||||||
|
for (const deviceMap of groupCall.participants.values()) {
|
||||||
|
participantCount += deviceMap.size;
|
||||||
|
}
|
||||||
|
|
||||||
PosthogAnalytics.instance.eventCallEnded.track(
|
PosthogAnalytics.instance.eventCallEnded.track(
|
||||||
groupCall.room.name,
|
groupCall.room.name,
|
||||||
groupCall.participants.length
|
participantCount
|
||||||
);
|
);
|
||||||
|
|
||||||
leave();
|
leave();
|
||||||
|
@ -187,14 +191,7 @@ export function GroupCallView({
|
||||||
if (!isPasswordlessUser && !isEmbedded) {
|
if (!isPasswordlessUser && !isEmbedded) {
|
||||||
history.push("/");
|
history.push("/");
|
||||||
}
|
}
|
||||||
}, [
|
}, [groupCall, leave, isPasswordlessUser, isEmbedded, history]);
|
||||||
groupCall.room.name,
|
|
||||||
groupCall.participants.length,
|
|
||||||
leave,
|
|
||||||
isPasswordlessUser,
|
|
||||||
isEmbedded,
|
|
||||||
history,
|
|
||||||
]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (widget && state === GroupCallState.Entered) {
|
if (widget && state === GroupCallState.Entered) {
|
||||||
|
@ -236,7 +233,6 @@ export function GroupCallView({
|
||||||
roomName={groupCall.room.name}
|
roomName={groupCall.room.name}
|
||||||
avatarUrl={avatarUrl}
|
avatarUrl={avatarUrl}
|
||||||
participants={participants}
|
participants={participants}
|
||||||
calls={calls}
|
|
||||||
microphoneMuted={microphoneMuted}
|
microphoneMuted={microphoneMuted}
|
||||||
localVideoMuted={localVideoMuted}
|
localVideoMuted={localVideoMuted}
|
||||||
toggleLocalVideoMuted={toggleLocalVideoMuted}
|
toggleLocalVideoMuted={toggleLocalVideoMuted}
|
||||||
|
@ -253,12 +249,6 @@ export function GroupCallView({
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else if (state === GroupCallState.Entering) {
|
|
||||||
return (
|
|
||||||
<FullScreenView>
|
|
||||||
<h1>{t("Entering room…")}</h1>
|
|
||||||
</FullScreenView>
|
|
||||||
);
|
|
||||||
} else if (left) {
|
} else if (left) {
|
||||||
if (isPasswordlessUser) {
|
if (isPasswordlessUser) {
|
||||||
return <CallEndedView client={client} />;
|
return <CallEndedView client={client} />;
|
||||||
|
|
|
@ -14,13 +14,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, {
|
import React, { useEffect, useCallback, useMemo, useRef } from "react";
|
||||||
useEffect,
|
|
||||||
useCallback,
|
|
||||||
useMemo,
|
|
||||||
useRef,
|
|
||||||
useState,
|
|
||||||
} from "react";
|
|
||||||
import { usePreventScroll } from "@react-aria/overlays";
|
import { usePreventScroll } from "@react-aria/overlays";
|
||||||
import useMeasure from "react-use-measure";
|
import useMeasure from "react-use-measure";
|
||||||
import { ResizeObserver } from "@juggle/resize-observer";
|
import { ResizeObserver } from "@juggle/resize-observer";
|
||||||
|
@ -31,11 +25,6 @@ import { CallFeed } from "matrix-js-sdk/src/webrtc/callFeed";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { JoinRule } from "matrix-js-sdk/src/@types/partials";
|
import { JoinRule } from "matrix-js-sdk/src/@types/partials";
|
||||||
import {
|
|
||||||
CallEvent,
|
|
||||||
CallState,
|
|
||||||
MatrixCall,
|
|
||||||
} from "matrix-js-sdk/src/webrtc/call";
|
|
||||||
|
|
||||||
import type { IWidgetApiRequest } from "matrix-widget-api";
|
import type { IWidgetApiRequest } from "matrix-widget-api";
|
||||||
import styles from "./InCallView.module.css";
|
import styles from "./InCallView.module.css";
|
||||||
|
@ -73,6 +62,7 @@ import { widget, ElementWidgetActions } from "../widget";
|
||||||
import { useJoinRule } from "./useJoinRule";
|
import { useJoinRule } from "./useJoinRule";
|
||||||
import { useUrlParams } from "../UrlParams";
|
import { useUrlParams } from "../UrlParams";
|
||||||
import { usePrefersReducedMotion } from "../usePrefersReducedMotion";
|
import { usePrefersReducedMotion } from "../usePrefersReducedMotion";
|
||||||
|
import { ConnectionState, ParticipantInfo } from "./useGroupCall";
|
||||||
|
|
||||||
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
|
||||||
|
@ -83,8 +73,7 @@ const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
|
||||||
interface Props {
|
interface Props {
|
||||||
client: MatrixClient;
|
client: MatrixClient;
|
||||||
groupCall: GroupCall;
|
groupCall: GroupCall;
|
||||||
participants: RoomMember[];
|
participants: Map<RoomMember, Map<string, ParticipantInfo>>;
|
||||||
calls: MatrixCall[];
|
|
||||||
roomName: string;
|
roomName: string;
|
||||||
avatarUrl: string;
|
avatarUrl: string;
|
||||||
microphoneMuted: boolean;
|
microphoneMuted: boolean;
|
||||||
|
@ -93,7 +82,7 @@ interface Props {
|
||||||
toggleMicrophoneMuted: () => void;
|
toggleMicrophoneMuted: () => void;
|
||||||
toggleScreensharing: () => void;
|
toggleScreensharing: () => void;
|
||||||
userMediaFeeds: CallFeed[];
|
userMediaFeeds: CallFeed[];
|
||||||
activeSpeaker: string;
|
activeSpeaker: CallFeed | null;
|
||||||
onLeave: () => void;
|
onLeave: () => void;
|
||||||
isScreensharing: boolean;
|
isScreensharing: boolean;
|
||||||
screenshareFeeds: CallFeed[];
|
screenshareFeeds: CallFeed[];
|
||||||
|
@ -102,12 +91,6 @@ interface Props {
|
||||||
hideHeader: boolean;
|
hideHeader: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ConnectionState {
|
|
||||||
EstablishingCall = "establishing call", // call hasn't been established yet
|
|
||||||
WaitMedia = "wait_media", // call is set up, waiting for ICE to connect
|
|
||||||
Connected = "connected", // media is flowing
|
|
||||||
}
|
|
||||||
|
|
||||||
// Represents something that should get a tile on the layout,
|
// Represents something that should get a tile on the layout,
|
||||||
// ie. a user's video feed or a screen share feed.
|
// ie. a user's video feed or a screen share feed.
|
||||||
export interface TileDescriptor {
|
export interface TileDescriptor {
|
||||||
|
@ -124,7 +107,6 @@ export function InCallView({
|
||||||
client,
|
client,
|
||||||
groupCall,
|
groupCall,
|
||||||
participants,
|
participants,
|
||||||
calls,
|
|
||||||
roomName,
|
roomName,
|
||||||
avatarUrl,
|
avatarUrl,
|
||||||
microphoneMuted,
|
microphoneMuted,
|
||||||
|
@ -174,50 +156,6 @@ export function InCallView({
|
||||||
|
|
||||||
const { hideScreensharing } = useUrlParams();
|
const { hideScreensharing } = useUrlParams();
|
||||||
|
|
||||||
const makeConnectionStatesMap = useCallback(() => {
|
|
||||||
const newConnStates = new Map<string, ConnectionState>();
|
|
||||||
for (const participant of participants) {
|
|
||||||
const userCall = groupCall.getCallByUserId(participant.userId);
|
|
||||||
const feed = userMediaFeeds.find((f) => f.userId === participant.userId);
|
|
||||||
let connectionState = ConnectionState.EstablishingCall;
|
|
||||||
if (feed && feed.isLocal()) {
|
|
||||||
connectionState = ConnectionState.Connected;
|
|
||||||
} else if (userCall) {
|
|
||||||
if (userCall.state === CallState.Connected) {
|
|
||||||
connectionState = ConnectionState.Connected;
|
|
||||||
} else if (userCall.state === CallState.Connecting) {
|
|
||||||
connectionState = ConnectionState.WaitMedia;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
newConnStates.set(participant.userId, connectionState);
|
|
||||||
}
|
|
||||||
return newConnStates;
|
|
||||||
}, [groupCall, participants, userMediaFeeds]);
|
|
||||||
|
|
||||||
const [connStates, setConnStates] = useState(
|
|
||||||
new Map<string, ConnectionState>()
|
|
||||||
);
|
|
||||||
|
|
||||||
const updateConnectionStates = useCallback(() => {
|
|
||||||
setConnStates(makeConnectionStatesMap());
|
|
||||||
}, [setConnStates, makeConnectionStatesMap]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
for (const call of calls) {
|
|
||||||
call.on(CallEvent.State, updateConnectionStates);
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
for (const call of calls) {
|
|
||||||
call.off(CallEvent.State, updateConnectionStates);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, [calls, updateConnectionStates]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
updateConnectionStates();
|
|
||||||
}, [participants, updateConnectionStates]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
widget?.api.transport.send(
|
widget?.api.transport.send(
|
||||||
layout === "freedom"
|
layout === "freedom"
|
||||||
|
@ -256,59 +194,57 @@ export function InCallView({
|
||||||
|
|
||||||
const items = useMemo(() => {
|
const items = useMemo(() => {
|
||||||
const tileDescriptors: TileDescriptor[] = [];
|
const tileDescriptors: TileDescriptor[] = [];
|
||||||
|
const localUserId = client.getUserId()!;
|
||||||
|
const localDeviceId = client.getDeviceId()!;
|
||||||
|
|
||||||
// one tile for each participants, to start with (we want a tile for everyone we
|
// One tile for each participant, to start with (we want a tile for everyone we
|
||||||
// think should be in the call, even if we don't have a media feed for them yet)
|
// think should be in the call, even if we don't have a call feed for them yet)
|
||||||
for (const p of participants) {
|
for (const [member, participantMap] of participants) {
|
||||||
const userMediaFeed = userMediaFeeds.find((f) => f.userId === p.userId);
|
for (const [deviceId, { connectionState, presenter }] of participantMap) {
|
||||||
|
const callFeed = userMediaFeeds.find(
|
||||||
|
(f) => f.userId === member.userId && f.deviceId === deviceId
|
||||||
|
);
|
||||||
|
|
||||||
// NB. this assumes that the same user can't join more than once from multiple
|
tileDescriptors.push({
|
||||||
// devices, but the participants are just RoomMembers, so this assumption is baked
|
id: `${member.userId} ${deviceId}`,
|
||||||
// into GroupCall itself.
|
member,
|
||||||
tileDescriptors.push({
|
callFeed,
|
||||||
id: p.userId,
|
focused: screenshareFeeds.length === 0 && callFeed === activeSpeaker,
|
||||||
member: p,
|
isLocal: member.userId === localUserId && deviceId === localDeviceId,
|
||||||
callFeed: userMediaFeed,
|
presenter,
|
||||||
focused: screenshareFeeds.length === 0 && p.userId === activeSpeaker,
|
connectionState,
|
||||||
isLocal: p.userId === client.getUserId(),
|
});
|
||||||
presenter: false,
|
}
|
||||||
connectionState: connStates.get(p.userId),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
PosthogAnalytics.instance.eventCallEnded.cacheParticipantCountChanged(
|
PosthogAnalytics.instance.eventCallEnded.cacheParticipantCountChanged(
|
||||||
participants.length
|
tileDescriptors.length
|
||||||
);
|
);
|
||||||
// add the screenshares too
|
|
||||||
|
// Add the screenshares too
|
||||||
for (const screenshareFeed of screenshareFeeds) {
|
for (const screenshareFeed of screenshareFeeds) {
|
||||||
const userMediaItem = tileDescriptors.find(
|
const member = screenshareFeed.getMember()!;
|
||||||
(item) => item.member.userId === screenshareFeed.userId
|
const connectionState = participants
|
||||||
);
|
.get(member)
|
||||||
|
?.get(screenshareFeed.deviceId!)?.connectionState;
|
||||||
|
|
||||||
if (userMediaItem) {
|
// If the participant has left, their screenshare feed is stale and we
|
||||||
userMediaItem.presenter = true;
|
// shouldn't bother showing it
|
||||||
|
if (connectionState !== undefined) {
|
||||||
|
tileDescriptors.push({
|
||||||
|
id: screenshareFeed.stream.id,
|
||||||
|
member,
|
||||||
|
callFeed: screenshareFeed,
|
||||||
|
focused: true,
|
||||||
|
isLocal: screenshareFeed.isLocal(),
|
||||||
|
presenter: false,
|
||||||
|
connectionState,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
tileDescriptors.push({
|
|
||||||
id: screenshareFeed.stream.id,
|
|
||||||
member: screenshareFeed.getMember()!,
|
|
||||||
callFeed: screenshareFeed,
|
|
||||||
focused: true,
|
|
||||||
isLocal: screenshareFeed.isLocal(),
|
|
||||||
presenter: false,
|
|
||||||
connectionState: connStates.get(screenshareFeed.userId),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return tileDescriptors;
|
return tileDescriptors;
|
||||||
}, [
|
}, [client, participants, userMediaFeeds, activeSpeaker, screenshareFeeds]);
|
||||||
client,
|
|
||||||
participants,
|
|
||||||
userMediaFeeds,
|
|
||||||
activeSpeaker,
|
|
||||||
screenshareFeeds,
|
|
||||||
connStates,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// The maximised participant: either the participant that the user has
|
// The maximised participant: either the participant that the user has
|
||||||
// manually put in fullscreen, or the focused (active) participant if the
|
// manually put in fullscreen, or the focused (active) participant if the
|
||||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useEffect } from "react";
|
import React, { useEffect, useMemo } from "react";
|
||||||
import useMeasure from "react-use-measure";
|
import useMeasure from "react-use-measure";
|
||||||
import { ResizeObserver } from "@juggle/resize-observer";
|
import { ResizeObserver } from "@juggle/resize-observer";
|
||||||
import i18n from "i18next";
|
import i18n from "i18next";
|
||||||
|
@ -43,6 +43,7 @@ import { PTTClips } from "../sound/PTTClips";
|
||||||
import { GroupCallInspector } from "./GroupCallInspector";
|
import { GroupCallInspector } from "./GroupCallInspector";
|
||||||
import { OverflowMenu } from "./OverflowMenu";
|
import { OverflowMenu } from "./OverflowMenu";
|
||||||
import { Size } from "../Avatar";
|
import { Size } from "../Avatar";
|
||||||
|
import { ParticipantInfo } from "./useGroupCall";
|
||||||
|
|
||||||
function getPromptText(
|
function getPromptText(
|
||||||
networkWaiting: boolean,
|
networkWaiting: boolean,
|
||||||
|
@ -100,7 +101,7 @@ interface Props {
|
||||||
roomName: string;
|
roomName: string;
|
||||||
avatarUrl: string;
|
avatarUrl: string;
|
||||||
groupCall: GroupCall;
|
groupCall: GroupCall;
|
||||||
participants: RoomMember[];
|
participants: Map<RoomMember, Map<string, ParticipantInfo>>;
|
||||||
userMediaFeeds: CallFeed[];
|
userMediaFeeds: CallFeed[];
|
||||||
onLeave: () => void;
|
onLeave: () => void;
|
||||||
isEmbedded: boolean;
|
isEmbedded: boolean;
|
||||||
|
@ -152,6 +153,15 @@ export const PTTCallView: React.FC<Props> = ({
|
||||||
connected,
|
connected,
|
||||||
} = usePTT(client, groupCall, userMediaFeeds, playClip);
|
} = usePTT(client, groupCall, userMediaFeeds, playClip);
|
||||||
|
|
||||||
|
const participatingMembers = useMemo(() => {
|
||||||
|
const members: RoomMember[] = [];
|
||||||
|
for (const [member, deviceMap] of participants) {
|
||||||
|
// Repeat the member for as many devices as they're using
|
||||||
|
for (let i = 0; i < deviceMap.size; i++) members.push(member);
|
||||||
|
}
|
||||||
|
return members;
|
||||||
|
}, [participants]);
|
||||||
|
|
||||||
const [talkingExpected, enqueueTalkingExpected, setTalkingExpected] =
|
const [talkingExpected, enqueueTalkingExpected, setTalkingExpected] =
|
||||||
useDelayedState(false);
|
useDelayedState(false);
|
||||||
const showTalkOverError = pttButtonHeld && transmitBlocked;
|
const showTalkOverError = pttButtonHeld && transmitBlocked;
|
||||||
|
@ -205,7 +215,7 @@ export const PTTCallView: React.FC<Props> = ({
|
||||||
<div className={styles.participants}>
|
<div className={styles.participants}>
|
||||||
<p>
|
<p>
|
||||||
{t("{{count}} people connected", {
|
{t("{{count}} people connected", {
|
||||||
count: participants.length,
|
count: participatingMembers.length,
|
||||||
})}
|
})}
|
||||||
</p>
|
</p>
|
||||||
<Facepile
|
<Facepile
|
||||||
|
@ -213,7 +223,7 @@ export const PTTCallView: React.FC<Props> = ({
|
||||||
max={8}
|
max={8}
|
||||||
className={styles.facepile}
|
className={styles.facepile}
|
||||||
client={client}
|
client={client}
|
||||||
participants={participants}
|
members={participatingMembers}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.footer}>
|
<div className={styles.footer}>
|
||||||
|
|
|
@ -23,7 +23,11 @@ import {
|
||||||
GroupCallUnknownDeviceError,
|
GroupCallUnknownDeviceError,
|
||||||
GroupCallError,
|
GroupCallError,
|
||||||
} from "matrix-js-sdk/src/webrtc/groupCall";
|
} from "matrix-js-sdk/src/webrtc/groupCall";
|
||||||
import { MatrixCall } from "matrix-js-sdk/src/webrtc/call";
|
import {
|
||||||
|
CallState,
|
||||||
|
MatrixCall,
|
||||||
|
CallEvent,
|
||||||
|
} from "matrix-js-sdk/src/webrtc/call";
|
||||||
import { CallFeed } from "matrix-js-sdk/src/webrtc/callFeed";
|
import { CallFeed } from "matrix-js-sdk/src/webrtc/callFeed";
|
||||||
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
@ -36,11 +40,21 @@ import { ElementWidgetActions, ScreenshareStartData, widget } from "../widget";
|
||||||
import { getSetting } from "../settings/useSetting";
|
import { getSetting } from "../settings/useSetting";
|
||||||
import { useEventTarget } from "../useEvents";
|
import { useEventTarget } from "../useEvents";
|
||||||
|
|
||||||
|
export enum ConnectionState {
|
||||||
|
EstablishingCall = "establishing call", // call hasn't been established yet
|
||||||
|
WaitMedia = "wait_media", // call is set up, waiting for ICE to connect
|
||||||
|
Connected = "connected", // media is flowing
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ParticipantInfo {
|
||||||
|
connectionState: ConnectionState;
|
||||||
|
presenter: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export interface UseGroupCallReturnType {
|
export interface UseGroupCallReturnType {
|
||||||
state: GroupCallState;
|
state: GroupCallState;
|
||||||
calls: MatrixCall[];
|
|
||||||
localCallFeed: CallFeed;
|
localCallFeed: CallFeed;
|
||||||
activeSpeaker: string;
|
activeSpeaker: CallFeed | null;
|
||||||
userMediaFeeds: CallFeed[];
|
userMediaFeeds: CallFeed[];
|
||||||
microphoneMuted: boolean;
|
microphoneMuted: boolean;
|
||||||
localVideoMuted: boolean;
|
localVideoMuted: boolean;
|
||||||
|
@ -55,16 +69,15 @@ export interface UseGroupCallReturnType {
|
||||||
isScreensharing: boolean;
|
isScreensharing: boolean;
|
||||||
screenshareFeeds: CallFeed[];
|
screenshareFeeds: CallFeed[];
|
||||||
localDesktopCapturerSourceId: string; // XXX: This looks unused?
|
localDesktopCapturerSourceId: string; // XXX: This looks unused?
|
||||||
participants: RoomMember[];
|
participants: Map<RoomMember, Map<string, ParticipantInfo>>;
|
||||||
hasLocalParticipant: boolean;
|
hasLocalParticipant: boolean;
|
||||||
unencryptedEventsFromUsers: Set<string>;
|
unencryptedEventsFromUsers: Set<string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
state: GroupCallState;
|
state: GroupCallState;
|
||||||
calls: MatrixCall[];
|
|
||||||
localCallFeed: CallFeed;
|
localCallFeed: CallFeed;
|
||||||
activeSpeaker: string;
|
activeSpeaker: CallFeed | null;
|
||||||
userMediaFeeds: CallFeed[];
|
userMediaFeeds: CallFeed[];
|
||||||
error: TranslatedError | null;
|
error: TranslatedError | null;
|
||||||
microphoneMuted: boolean;
|
microphoneMuted: boolean;
|
||||||
|
@ -73,15 +86,51 @@ interface State {
|
||||||
localDesktopCapturerSourceId: string;
|
localDesktopCapturerSourceId: string;
|
||||||
isScreensharing: boolean;
|
isScreensharing: boolean;
|
||||||
requestingScreenshare: boolean;
|
requestingScreenshare: boolean;
|
||||||
participants: RoomMember[];
|
participants: Map<RoomMember, Map<string, ParticipantInfo>>;
|
||||||
hasLocalParticipant: boolean;
|
hasLocalParticipant: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getParticipants(
|
||||||
|
groupCall: GroupCall
|
||||||
|
): Map<RoomMember, Map<string, ParticipantInfo>> {
|
||||||
|
const participants = new Map<RoomMember, Map<string, ParticipantInfo>>();
|
||||||
|
|
||||||
|
for (const [member, participantsStateMap] of groupCall.participants) {
|
||||||
|
const callMap = groupCall.calls.get(member);
|
||||||
|
const participantInfoMap = new Map<string, ParticipantInfo>();
|
||||||
|
participants.set(member, participantInfoMap);
|
||||||
|
|
||||||
|
for (const [deviceId, participant] of participantsStateMap) {
|
||||||
|
const call = callMap?.get(deviceId);
|
||||||
|
const feed = groupCall.userMediaFeeds.find(
|
||||||
|
(f) => f.userId === member.userId && f.deviceId === deviceId
|
||||||
|
);
|
||||||
|
|
||||||
|
let connectionState = ConnectionState.EstablishingCall;
|
||||||
|
if (feed?.isLocal()) {
|
||||||
|
connectionState = ConnectionState.Connected;
|
||||||
|
} else if (call !== undefined) {
|
||||||
|
if (call.state === CallState.Connected) {
|
||||||
|
connectionState = ConnectionState.Connected;
|
||||||
|
} else if (call.state === CallState.Connecting) {
|
||||||
|
connectionState = ConnectionState.WaitMedia;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
participantInfoMap.set(deviceId, {
|
||||||
|
connectionState,
|
||||||
|
presenter: participant.screensharing,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return participants;
|
||||||
|
}
|
||||||
|
|
||||||
export function useGroupCall(groupCall: GroupCall): UseGroupCallReturnType {
|
export function useGroupCall(groupCall: GroupCall): UseGroupCallReturnType {
|
||||||
const [
|
const [
|
||||||
{
|
{
|
||||||
state,
|
state,
|
||||||
calls,
|
|
||||||
localCallFeed,
|
localCallFeed,
|
||||||
activeSpeaker,
|
activeSpeaker,
|
||||||
userMediaFeeds,
|
userMediaFeeds,
|
||||||
|
@ -98,7 +147,6 @@ export function useGroupCall(groupCall: GroupCall): UseGroupCallReturnType {
|
||||||
setState,
|
setState,
|
||||||
] = useState<State>({
|
] = useState<State>({
|
||||||
state: GroupCallState.LocalCallFeedUninitialized,
|
state: GroupCallState.LocalCallFeedUninitialized,
|
||||||
calls: [],
|
|
||||||
localCallFeed: null,
|
localCallFeed: null,
|
||||||
activeSpeaker: null,
|
activeSpeaker: null,
|
||||||
userMediaFeeds: [],
|
userMediaFeeds: [],
|
||||||
|
@ -109,7 +157,7 @@ export function useGroupCall(groupCall: GroupCall): UseGroupCallReturnType {
|
||||||
screenshareFeeds: [],
|
screenshareFeeds: [],
|
||||||
localDesktopCapturerSourceId: null,
|
localDesktopCapturerSourceId: null,
|
||||||
requestingScreenshare: false,
|
requestingScreenshare: false,
|
||||||
participants: [],
|
participants: new Map(),
|
||||||
hasLocalParticipant: false,
|
hasLocalParticipant: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -120,29 +168,30 @@ export function useGroupCall(groupCall: GroupCall): UseGroupCallReturnType {
|
||||||
new Set<string>()
|
new Set<string>()
|
||||||
);
|
);
|
||||||
|
|
||||||
const updateState = (state: Partial<State>) =>
|
const updateState = useCallback(
|
||||||
setState((prevState) => ({ ...prevState, ...state }));
|
(state: Partial<State>) => setState((prev) => ({ ...prev, ...state })),
|
||||||
|
[setState]
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
function onGroupCallStateChanged() {
|
function onGroupCallStateChanged() {
|
||||||
updateState({
|
updateState({
|
||||||
state: groupCall.state,
|
state: groupCall.state,
|
||||||
calls: [...groupCall.calls],
|
|
||||||
localCallFeed: groupCall.localCallFeed,
|
localCallFeed: groupCall.localCallFeed,
|
||||||
activeSpeaker: groupCall.activeSpeaker,
|
activeSpeaker: groupCall.activeSpeaker ?? null,
|
||||||
userMediaFeeds: [...groupCall.userMediaFeeds],
|
userMediaFeeds: [...groupCall.userMediaFeeds],
|
||||||
microphoneMuted: groupCall.isMicrophoneMuted(),
|
microphoneMuted: groupCall.isMicrophoneMuted(),
|
||||||
localVideoMuted: groupCall.isLocalVideoMuted(),
|
localVideoMuted: groupCall.isLocalVideoMuted(),
|
||||||
isScreensharing: groupCall.isScreensharing(),
|
isScreensharing: groupCall.isScreensharing(),
|
||||||
localDesktopCapturerSourceId: groupCall.localDesktopCapturerSourceId,
|
localDesktopCapturerSourceId: groupCall.localDesktopCapturerSourceId,
|
||||||
screenshareFeeds: [...groupCall.screenshareFeeds],
|
screenshareFeeds: [...groupCall.screenshareFeeds],
|
||||||
participants: [...groupCall.participants],
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function onUserMediaFeedsChanged(userMediaFeeds: CallFeed[]): void {
|
function onUserMediaFeedsChanged(userMediaFeeds: CallFeed[]): void {
|
||||||
updateState({
|
updateState({
|
||||||
userMediaFeeds: [...userMediaFeeds],
|
userMediaFeeds: [...userMediaFeeds],
|
||||||
|
participants: getParticipants(groupCall),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -152,9 +201,9 @@ export function useGroupCall(groupCall: GroupCall): UseGroupCallReturnType {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function onActiveSpeakerChanged(activeSpeaker: string): void {
|
function onActiveSpeakerChanged(activeSpeaker: CallFeed | undefined): void {
|
||||||
updateState({
|
updateState({
|
||||||
activeSpeaker: activeSpeaker,
|
activeSpeaker: activeSpeaker ?? null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -179,15 +228,31 @@ export function useGroupCall(groupCall: GroupCall): UseGroupCallReturnType {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function onCallsChanged(calls: MatrixCall[]): void {
|
const prevCalls = new Set<MatrixCall>();
|
||||||
updateState({
|
|
||||||
calls: [...calls],
|
function onCallState(): void {
|
||||||
});
|
updateState({ participants: getParticipants(groupCall) });
|
||||||
}
|
}
|
||||||
|
|
||||||
function onParticipantsChanged(participants: RoomMember[]): void {
|
function onCallsChanged(
|
||||||
|
calls: Map<RoomMember, Map<string, MatrixCall>>
|
||||||
|
): void {
|
||||||
|
for (const call of prevCalls) call.off(CallEvent.State, onCallState);
|
||||||
|
prevCalls.clear();
|
||||||
|
|
||||||
|
for (const deviceMap of calls.values()) {
|
||||||
|
for (const call of deviceMap.values()) {
|
||||||
|
call.on(CallEvent.State, onCallState);
|
||||||
|
prevCalls.add(call);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateState({ participants: getParticipants(groupCall) });
|
||||||
|
}
|
||||||
|
|
||||||
|
function onParticipantsChanged(): void {
|
||||||
updateState({
|
updateState({
|
||||||
participants: [...participants],
|
participants: getParticipants(groupCall),
|
||||||
hasLocalParticipant: groupCall.hasLocalParticipant(),
|
hasLocalParticipant: groupCall.hasLocalParticipant(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -218,16 +283,15 @@ export function useGroupCall(groupCall: GroupCall): UseGroupCallReturnType {
|
||||||
updateState({
|
updateState({
|
||||||
error: null,
|
error: null,
|
||||||
state: groupCall.state,
|
state: groupCall.state,
|
||||||
calls: [...groupCall.calls],
|
|
||||||
localCallFeed: groupCall.localCallFeed,
|
localCallFeed: groupCall.localCallFeed,
|
||||||
activeSpeaker: groupCall.activeSpeaker,
|
activeSpeaker: groupCall.activeSpeaker ?? null,
|
||||||
userMediaFeeds: [...groupCall.userMediaFeeds],
|
userMediaFeeds: [...groupCall.userMediaFeeds],
|
||||||
microphoneMuted: groupCall.isMicrophoneMuted(),
|
microphoneMuted: groupCall.isMicrophoneMuted(),
|
||||||
localVideoMuted: groupCall.isLocalVideoMuted(),
|
localVideoMuted: groupCall.isLocalVideoMuted(),
|
||||||
isScreensharing: groupCall.isScreensharing(),
|
isScreensharing: groupCall.isScreensharing(),
|
||||||
localDesktopCapturerSourceId: groupCall.localDesktopCapturerSourceId,
|
localDesktopCapturerSourceId: groupCall.localDesktopCapturerSourceId,
|
||||||
screenshareFeeds: [...groupCall.screenshareFeeds],
|
screenshareFeeds: [...groupCall.screenshareFeeds],
|
||||||
participants: [...groupCall.participants],
|
participants: getParticipants(groupCall),
|
||||||
hasLocalParticipant: groupCall.hasLocalParticipant(),
|
hasLocalParticipant: groupCall.hasLocalParticipant(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -264,7 +328,7 @@ export function useGroupCall(groupCall: GroupCall): UseGroupCallReturnType {
|
||||||
groupCall.removeListener(GroupCallEvent.Error, onError);
|
groupCall.removeListener(GroupCallEvent.Error, onError);
|
||||||
groupCall.leave();
|
groupCall.leave();
|
||||||
};
|
};
|
||||||
}, [groupCall]);
|
}, [groupCall, updateState]);
|
||||||
|
|
||||||
usePageUnload(() => {
|
usePageUnload(() => {
|
||||||
groupCall.leave();
|
groupCall.leave();
|
||||||
|
@ -290,7 +354,7 @@ export function useGroupCall(groupCall: GroupCall): UseGroupCallReturnType {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
updateState({ error });
|
updateState({ error });
|
||||||
});
|
});
|
||||||
}, [groupCall]);
|
}, [groupCall, updateState]);
|
||||||
|
|
||||||
const leave = useCallback(() => groupCall.leave(), [groupCall]);
|
const leave = useCallback(() => groupCall.leave(), [groupCall]);
|
||||||
|
|
||||||
|
@ -341,7 +405,7 @@ export function useGroupCall(groupCall: GroupCall): UseGroupCallReturnType {
|
||||||
// toggling off
|
// toggling off
|
||||||
groupCall.setScreensharingEnabled(false);
|
groupCall.setScreensharingEnabled(false);
|
||||||
}
|
}
|
||||||
}, [groupCall]);
|
}, [groupCall, updateState]);
|
||||||
|
|
||||||
const onScreenshareStart = useCallback(
|
const onScreenshareStart = useCallback(
|
||||||
async (ev: CustomEvent<IWidgetApiRequest>) => {
|
async (ev: CustomEvent<IWidgetApiRequest>) => {
|
||||||
|
@ -355,7 +419,7 @@ export function useGroupCall(groupCall: GroupCall): UseGroupCallReturnType {
|
||||||
});
|
});
|
||||||
await widget.api.transport.reply(ev.detail, {});
|
await widget.api.transport.reply(ev.detail, {});
|
||||||
},
|
},
|
||||||
[groupCall]
|
[groupCall, updateState]
|
||||||
);
|
);
|
||||||
|
|
||||||
const onScreenshareStop = useCallback(
|
const onScreenshareStop = useCallback(
|
||||||
|
@ -364,7 +428,7 @@ export function useGroupCall(groupCall: GroupCall): UseGroupCallReturnType {
|
||||||
await groupCall.setScreensharingEnabled(false);
|
await groupCall.setScreensharingEnabled(false);
|
||||||
await widget.api.transport.reply(ev.detail, {});
|
await widget.api.transport.reply(ev.detail, {});
|
||||||
},
|
},
|
||||||
[groupCall]
|
[groupCall, updateState]
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -402,7 +466,7 @@ export function useGroupCall(groupCall: GroupCall): UseGroupCallReturnType {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
updateState({ error });
|
updateState({ error });
|
||||||
}
|
}
|
||||||
}, [t]);
|
}, [t, updateState]);
|
||||||
|
|
||||||
const [spacebarHeld, setSpacebarHeld] = useState(false);
|
const [spacebarHeld, setSpacebarHeld] = useState(false);
|
||||||
|
|
||||||
|
@ -468,7 +532,6 @@ export function useGroupCall(groupCall: GroupCall): UseGroupCallReturnType {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
state,
|
state,
|
||||||
calls,
|
|
||||||
localCallFeed,
|
localCallFeed,
|
||||||
activeSpeaker,
|
activeSpeaker,
|
||||||
userMediaFeeds,
|
userMediaFeeds,
|
||||||
|
|
|
@ -21,7 +21,8 @@ import { RoomMember } from "matrix-js-sdk";
|
||||||
import { VideoGrid, useVideoGridLayout } from "./VideoGrid";
|
import { VideoGrid, useVideoGridLayout } from "./VideoGrid";
|
||||||
import { VideoTile } from "./VideoTile";
|
import { VideoTile } from "./VideoTile";
|
||||||
import { Button } from "../button";
|
import { Button } from "../button";
|
||||||
import { ConnectionState, TileDescriptor } from "../room/InCallView";
|
import { TileDescriptor } from "../room/InCallView";
|
||||||
|
import { ConnectionState } from "../room/useGroupCall";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: "VideoGrid",
|
title: "VideoGrid",
|
||||||
|
|
|
@ -23,7 +23,7 @@ import styles from "./VideoTile.module.css";
|
||||||
import { ReactComponent as MicMutedIcon } from "../icons/MicMuted.svg";
|
import { ReactComponent as MicMutedIcon } from "../icons/MicMuted.svg";
|
||||||
import { ReactComponent as VideoMutedIcon } from "../icons/VideoMuted.svg";
|
import { ReactComponent as VideoMutedIcon } from "../icons/VideoMuted.svg";
|
||||||
import { AudioButton, FullscreenButton } from "../button/Button";
|
import { AudioButton, FullscreenButton } from "../button/Button";
|
||||||
import { ConnectionState } from "../room/InCallView";
|
import { ConnectionState } from "../room/useGroupCall";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
name: string;
|
name: string;
|
||||||
|
|
|
@ -10196,9 +10196,9 @@ matrix-events-sdk@0.0.1:
|
||||||
resolved "https://registry.yarnpkg.com/matrix-events-sdk/-/matrix-events-sdk-0.0.1.tgz#c8c38911e2cb29023b0bbac8d6f32e0de2c957dd"
|
resolved "https://registry.yarnpkg.com/matrix-events-sdk/-/matrix-events-sdk-0.0.1.tgz#c8c38911e2cb29023b0bbac8d6f32e0de2c957dd"
|
||||||
integrity sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA==
|
integrity sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA==
|
||||||
|
|
||||||
"matrix-js-sdk@github:matrix-org/matrix-js-sdk#3f1c3392d45b0fc054c3788cc6c043cd5b4fb730":
|
"matrix-js-sdk@github:matrix-org/matrix-js-sdk#f46ecf970c658ae34b8d5fc3e73369c31ac79e90":
|
||||||
version "21.1.0"
|
version "21.1.0"
|
||||||
resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/3f1c3392d45b0fc054c3788cc6c043cd5b4fb730"
|
resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/f46ecf970c658ae34b8d5fc3e73369c31ac79e90"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.12.5"
|
"@babel/runtime" "^7.12.5"
|
||||||
"@types/sdp-transform" "^2.4.5"
|
"@types/sdp-transform" "^2.4.5"
|
||||||
|
|
Loading…
Add table
Reference in a new issue