Merge pull request #761 from robintown/update-js-sdk
Enable users to join calls from multiple devices
This commit is contained in:
commit
aa828fe9f5
13 changed files with 292 additions and 230 deletions
|
@ -45,7 +45,7 @@
|
|||
"i18next": "^21.10.0",
|
||||
"i18next-browser-languagedetector": "^6.1.8",
|
||||
"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#9d3ac66cf847fe8cb0359d64e1f1d67902b982a1",
|
||||
"matrix-widget-api": "^1.0.0",
|
||||
"mermaid": "^8.13.8",
|
||||
"normalize.css": "^8.0.1",
|
||||
|
@ -86,7 +86,7 @@
|
|||
"i18next-parser": "^6.6.0",
|
||||
"identity-obj-proxy": "^3.0.0",
|
||||
"jest": "^29.2.2",
|
||||
"jest-environment-jsdom": "^29.2.2",
|
||||
"jest-environment-jsdom": "^29.3.1",
|
||||
"prettier": "^2.6.2",
|
||||
"sass": "^1.42.1",
|
||||
"storybook-builder-vite": "^0.1.12",
|
||||
|
|
|
@ -43,8 +43,6 @@
|
|||
"Display name": "Display name",
|
||||
"Download debug logs": "Download debug logs",
|
||||
"Element Call Home": "Element Call Home",
|
||||
"Single-key keyboard shortcuts": "Single-key keyboard shortcuts",
|
||||
"Entering room…": "Entering room…",
|
||||
"Exit full screen": "Exit full screen",
|
||||
"Fetching group call timed out.": "Fetching group call timed out.",
|
||||
"Freedom": "Freedom",
|
||||
|
@ -105,6 +103,7 @@
|
|||
"Show call inspector": "Show call inspector",
|
||||
"Sign in": "Sign in",
|
||||
"Sign out": "Sign out",
|
||||
"Single-key keyboard shortcuts": "Single-key keyboard shortcuts",
|
||||
"Spatial audio": "Spatial audio",
|
||||
"Speaker": "Speaker",
|
||||
"Speaker {{n}}": "Speaker {{n}}",
|
||||
|
|
|
@ -25,8 +25,7 @@ import React, {
|
|||
useRef,
|
||||
} from "react";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import { MatrixClient, ClientEvent } from "matrix-js-sdk/src/client";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
|
@ -40,6 +39,7 @@ import {
|
|||
import { widget } from "./widget";
|
||||
import { PosthogAnalytics, RegistrationType } from "./PosthogAnalytics";
|
||||
import { translatedError } from "./TranslatedError";
|
||||
import { useEventTarget } from "./useEvents";
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
|
@ -55,6 +55,9 @@ export interface Session {
|
|||
tempPassword?: string;
|
||||
}
|
||||
|
||||
const loadChannel =
|
||||
"BroadcastChannel" in window ? new BroadcastChannel("load") : null;
|
||||
|
||||
const loadSession = (): Session => {
|
||||
const data = localStorage.getItem("matrix-auth-store");
|
||||
if (data) return JSON.parse(data);
|
||||
|
@ -292,47 +295,29 @@ export const ClientProvider: FC<Props> = ({ children }) => {
|
|||
|
||||
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(() => {
|
||||
// To protect against multiple sessions writing to the same storage
|
||||
// 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();
|
||||
if (!widget) loadChannel?.postMessage({});
|
||||
}, []);
|
||||
|
||||
const onToDeviceEvent = (event: MatrixEvent) => {
|
||||
if (event.getType() !== "org.matrix.call_duplicate_session") return;
|
||||
useEventTarget(
|
||||
loadChannel,
|
||||
"message",
|
||||
useCallback(() => {
|
||||
client?.stopClient();
|
||||
|
||||
const content = event.getContent();
|
||||
|
||||
if (content.session_id === client.getSessionId()) return;
|
||||
|
||||
if (content.timestamp > loadTime) {
|
||||
client?.stopClient();
|
||||
|
||||
setState((prev) => ({
|
||||
...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]);
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
error: translatedError(
|
||||
"This application has been opened in another tab.",
|
||||
t
|
||||
),
|
||||
}));
|
||||
}, [client, setState, t])
|
||||
);
|
||||
|
||||
const context = useMemo<ClientState>(
|
||||
() => ({
|
||||
|
|
|
@ -32,7 +32,7 @@ const overlapMap: Partial<Record<Size, number>> = {
|
|||
interface Props extends HTMLAttributes<HTMLDivElement> {
|
||||
className: string;
|
||||
client: MatrixClient;
|
||||
participants: RoomMember[];
|
||||
members: RoomMember[];
|
||||
max?: number;
|
||||
size?: Size;
|
||||
}
|
||||
|
@ -40,7 +40,7 @@ interface Props extends HTMLAttributes<HTMLDivElement> {
|
|||
export function Facepile({
|
||||
className,
|
||||
client,
|
||||
participants,
|
||||
members,
|
||||
max = 3,
|
||||
size = Size.XS,
|
||||
...rest
|
||||
|
@ -51,14 +51,14 @@ export function Facepile({
|
|||
const _overlap = overlapMap[size];
|
||||
|
||||
const title = useMemo(() => {
|
||||
return participants.reduce<string | null>(
|
||||
return members.reduce<string | null>(
|
||||
(prev, curr) =>
|
||||
prev === null
|
||||
? curr.name
|
||||
: t("{{names}}, {{name}}", { names: prev, name: curr.name }),
|
||||
null
|
||||
) as string;
|
||||
}, [participants, t]);
|
||||
}, [members, t]);
|
||||
|
||||
return (
|
||||
<div
|
||||
|
@ -66,12 +66,11 @@ export function Facepile({
|
|||
title={title}
|
||||
style={{
|
||||
width:
|
||||
Math.min(participants.length, max + 1) * (_size - _overlap) +
|
||||
_overlap,
|
||||
Math.min(members.length, max + 1) * (_size - _overlap) + _overlap,
|
||||
}}
|
||||
{...rest}
|
||||
>
|
||||
{participants.slice(0, max).map((member, i) => {
|
||||
{members.slice(0, max).map((member, i) => {
|
||||
const avatarUrl = member.getMxcAvatarUrl();
|
||||
return (
|
||||
<Avatar
|
||||
|
@ -84,11 +83,11 @@ export function Facepile({
|
|||
/>
|
||||
);
|
||||
})}
|
||||
{participants.length > max && (
|
||||
{members.length > max && (
|
||||
<Avatar
|
||||
key="additional"
|
||||
size={size}
|
||||
fallback={`+${participants.length - max}`}
|
||||
fallback={`+${members.length - max}`}
|
||||
className={styles.avatar}
|
||||
style={{ left: max * (_size - _overlap) }}
|
||||
/>
|
||||
|
|
|
@ -92,7 +92,7 @@ function CallTile({
|
|||
<Facepile
|
||||
className={styles.facePile}
|
||||
client={client}
|
||||
participants={participants}
|
||||
members={participants}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
@ -79,7 +79,6 @@ export function GroupCallView({
|
|||
isScreensharing,
|
||||
screenshareFeeds,
|
||||
participants,
|
||||
calls,
|
||||
unencryptedEventsFromUsers,
|
||||
} = useGroupCall(groupCall);
|
||||
|
||||
|
@ -173,9 +172,14 @@ export function GroupCallView({
|
|||
const onLeave = useCallback(() => {
|
||||
setLeft(true);
|
||||
|
||||
let participantCount = 0;
|
||||
for (const deviceMap of groupCall.participants.values()) {
|
||||
participantCount += deviceMap.size;
|
||||
}
|
||||
|
||||
PosthogAnalytics.instance.eventCallEnded.track(
|
||||
groupCall.room.name,
|
||||
groupCall.participants.length
|
||||
participantCount
|
||||
);
|
||||
|
||||
leave();
|
||||
|
@ -187,14 +191,7 @@ export function GroupCallView({
|
|||
if (!isPasswordlessUser && !isEmbedded) {
|
||||
history.push("/");
|
||||
}
|
||||
}, [
|
||||
groupCall.room.name,
|
||||
groupCall.participants.length,
|
||||
leave,
|
||||
isPasswordlessUser,
|
||||
isEmbedded,
|
||||
history,
|
||||
]);
|
||||
}, [groupCall, leave, isPasswordlessUser, isEmbedded, history]);
|
||||
|
||||
useEffect(() => {
|
||||
if (widget && state === GroupCallState.Entered) {
|
||||
|
@ -236,7 +233,6 @@ export function GroupCallView({
|
|||
roomName={groupCall.room.name}
|
||||
avatarUrl={avatarUrl}
|
||||
participants={participants}
|
||||
calls={calls}
|
||||
microphoneMuted={microphoneMuted}
|
||||
localVideoMuted={localVideoMuted}
|
||||
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) {
|
||||
if (isPasswordlessUser) {
|
||||
return <CallEndedView client={client} />;
|
||||
|
|
|
@ -14,13 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, {
|
||||
useEffect,
|
||||
useCallback,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import React, { useEffect, useCallback, useMemo, useRef } from "react";
|
||||
import { usePreventScroll } from "@react-aria/overlays";
|
||||
import useMeasure from "react-use-measure";
|
||||
import { ResizeObserver } from "@juggle/resize-observer";
|
||||
|
@ -31,11 +25,6 @@ import { CallFeed } from "matrix-js-sdk/src/webrtc/callFeed";
|
|||
import classNames from "classnames";
|
||||
import { useTranslation } from "react-i18next";
|
||||
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 styles from "./InCallView.module.css";
|
||||
|
@ -73,6 +62,7 @@ import { widget, ElementWidgetActions } from "../widget";
|
|||
import { useJoinRule } from "./useJoinRule";
|
||||
import { useUrlParams } from "../UrlParams";
|
||||
import { usePrefersReducedMotion } from "../usePrefersReducedMotion";
|
||||
import { ConnectionState, ParticipantInfo } from "./useGroupCall";
|
||||
|
||||
const canScreenshare = "getDisplayMedia" in (navigator.mediaDevices ?? {});
|
||||
// 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 {
|
||||
client: MatrixClient;
|
||||
groupCall: GroupCall;
|
||||
participants: RoomMember[];
|
||||
calls: MatrixCall[];
|
||||
participants: Map<RoomMember, Map<string, ParticipantInfo>>;
|
||||
roomName: string;
|
||||
avatarUrl: string;
|
||||
microphoneMuted: boolean;
|
||||
|
@ -93,7 +82,7 @@ interface Props {
|
|||
toggleMicrophoneMuted: () => void;
|
||||
toggleScreensharing: () => void;
|
||||
userMediaFeeds: CallFeed[];
|
||||
activeSpeaker: string;
|
||||
activeSpeaker: CallFeed | null;
|
||||
onLeave: () => void;
|
||||
isScreensharing: boolean;
|
||||
screenshareFeeds: CallFeed[];
|
||||
|
@ -102,12 +91,6 @@ interface Props {
|
|||
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,
|
||||
// ie. a user's video feed or a screen share feed.
|
||||
export interface TileDescriptor {
|
||||
|
@ -124,7 +107,6 @@ export function InCallView({
|
|||
client,
|
||||
groupCall,
|
||||
participants,
|
||||
calls,
|
||||
roomName,
|
||||
avatarUrl,
|
||||
microphoneMuted,
|
||||
|
@ -174,50 +156,6 @@ export function InCallView({
|
|||
|
||||
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(() => {
|
||||
widget?.api.transport.send(
|
||||
layout === "freedom"
|
||||
|
@ -256,59 +194,57 @@ export function InCallView({
|
|||
|
||||
const items = useMemo(() => {
|
||||
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
|
||||
// think should be in the call, even if we don't have a media feed for them yet)
|
||||
for (const p of participants) {
|
||||
const userMediaFeed = userMediaFeeds.find((f) => f.userId === p.userId);
|
||||
// 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 call feed for them yet)
|
||||
for (const [member, participantMap] of participants) {
|
||||
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
|
||||
// devices, but the participants are just RoomMembers, so this assumption is baked
|
||||
// into GroupCall itself.
|
||||
tileDescriptors.push({
|
||||
id: p.userId,
|
||||
member: p,
|
||||
callFeed: userMediaFeed,
|
||||
focused: screenshareFeeds.length === 0 && p.userId === activeSpeaker,
|
||||
isLocal: p.userId === client.getUserId(),
|
||||
presenter: false,
|
||||
connectionState: connStates.get(p.userId),
|
||||
});
|
||||
tileDescriptors.push({
|
||||
id: `${member.userId} ${deviceId}`,
|
||||
member,
|
||||
callFeed,
|
||||
focused: screenshareFeeds.length === 0 && callFeed === activeSpeaker,
|
||||
isLocal: member.userId === localUserId && deviceId === localDeviceId,
|
||||
presenter,
|
||||
connectionState,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
PosthogAnalytics.instance.eventCallEnded.cacheParticipantCountChanged(
|
||||
participants.length
|
||||
tileDescriptors.length
|
||||
);
|
||||
// add the screenshares too
|
||||
|
||||
// Add the screenshares too
|
||||
for (const screenshareFeed of screenshareFeeds) {
|
||||
const userMediaItem = tileDescriptors.find(
|
||||
(item) => item.member.userId === screenshareFeed.userId
|
||||
);
|
||||
const member = screenshareFeed.getMember()!;
|
||||
const connectionState = participants
|
||||
.get(member)
|
||||
?.get(screenshareFeed.deviceId!)?.connectionState;
|
||||
|
||||
if (userMediaItem) {
|
||||
userMediaItem.presenter = true;
|
||||
// If the participant has left, their screenshare feed is stale and we
|
||||
// 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;
|
||||
}, [
|
||||
client,
|
||||
participants,
|
||||
userMediaFeeds,
|
||||
activeSpeaker,
|
||||
screenshareFeeds,
|
||||
connStates,
|
||||
]);
|
||||
}, [client, participants, userMediaFeeds, activeSpeaker, screenshareFeeds]);
|
||||
|
||||
// The maximised participant: either the participant that the user has
|
||||
// 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.
|
||||
*/
|
||||
|
||||
import React, { useEffect } from "react";
|
||||
import React, { useEffect, useMemo } from "react";
|
||||
import useMeasure from "react-use-measure";
|
||||
import { ResizeObserver } from "@juggle/resize-observer";
|
||||
import i18n from "i18next";
|
||||
|
@ -43,6 +43,7 @@ import { PTTClips } from "../sound/PTTClips";
|
|||
import { GroupCallInspector } from "./GroupCallInspector";
|
||||
import { OverflowMenu } from "./OverflowMenu";
|
||||
import { Size } from "../Avatar";
|
||||
import { ParticipantInfo } from "./useGroupCall";
|
||||
|
||||
function getPromptText(
|
||||
networkWaiting: boolean,
|
||||
|
@ -100,7 +101,7 @@ interface Props {
|
|||
roomName: string;
|
||||
avatarUrl: string;
|
||||
groupCall: GroupCall;
|
||||
participants: RoomMember[];
|
||||
participants: Map<RoomMember, Map<string, ParticipantInfo>>;
|
||||
userMediaFeeds: CallFeed[];
|
||||
onLeave: () => void;
|
||||
isEmbedded: boolean;
|
||||
|
@ -152,6 +153,15 @@ export const PTTCallView: React.FC<Props> = ({
|
|||
connected,
|
||||
} = 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] =
|
||||
useDelayedState(false);
|
||||
const showTalkOverError = pttButtonHeld && transmitBlocked;
|
||||
|
@ -205,7 +215,7 @@ export const PTTCallView: React.FC<Props> = ({
|
|||
<div className={styles.participants}>
|
||||
<p>
|
||||
{t("{{count}} people connected", {
|
||||
count: participants.length,
|
||||
count: participatingMembers.length,
|
||||
})}
|
||||
</p>
|
||||
<Facepile
|
||||
|
@ -213,7 +223,7 @@ export const PTTCallView: React.FC<Props> = ({
|
|||
max={8}
|
||||
className={styles.facepile}
|
||||
client={client}
|
||||
participants={participants}
|
||||
members={participatingMembers}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.footer}>
|
||||
|
|
|
@ -23,7 +23,11 @@ import {
|
|||
GroupCallUnknownDeviceError,
|
||||
GroupCallError,
|
||||
} 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 { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
@ -36,11 +40,21 @@ import { ElementWidgetActions, ScreenshareStartData, widget } from "../widget";
|
|||
import { getSetting } from "../settings/useSetting";
|
||||
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 {
|
||||
state: GroupCallState;
|
||||
calls: MatrixCall[];
|
||||
localCallFeed: CallFeed;
|
||||
activeSpeaker: string;
|
||||
activeSpeaker: CallFeed | null;
|
||||
userMediaFeeds: CallFeed[];
|
||||
microphoneMuted: boolean;
|
||||
localVideoMuted: boolean;
|
||||
|
@ -55,16 +69,15 @@ export interface UseGroupCallReturnType {
|
|||
isScreensharing: boolean;
|
||||
screenshareFeeds: CallFeed[];
|
||||
localDesktopCapturerSourceId: string; // XXX: This looks unused?
|
||||
participants: RoomMember[];
|
||||
participants: Map<RoomMember, Map<string, ParticipantInfo>>;
|
||||
hasLocalParticipant: boolean;
|
||||
unencryptedEventsFromUsers: Set<string>;
|
||||
}
|
||||
|
||||
interface State {
|
||||
state: GroupCallState;
|
||||
calls: MatrixCall[];
|
||||
localCallFeed: CallFeed;
|
||||
activeSpeaker: string;
|
||||
activeSpeaker: CallFeed | null;
|
||||
userMediaFeeds: CallFeed[];
|
||||
error: TranslatedError | null;
|
||||
microphoneMuted: boolean;
|
||||
|
@ -73,15 +86,51 @@ interface State {
|
|||
localDesktopCapturerSourceId: string;
|
||||
isScreensharing: boolean;
|
||||
requestingScreenshare: boolean;
|
||||
participants: RoomMember[];
|
||||
participants: Map<RoomMember, Map<string, ParticipantInfo>>;
|
||||
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 {
|
||||
const [
|
||||
{
|
||||
state,
|
||||
calls,
|
||||
localCallFeed,
|
||||
activeSpeaker,
|
||||
userMediaFeeds,
|
||||
|
@ -98,7 +147,6 @@ export function useGroupCall(groupCall: GroupCall): UseGroupCallReturnType {
|
|||
setState,
|
||||
] = useState<State>({
|
||||
state: GroupCallState.LocalCallFeedUninitialized,
|
||||
calls: [],
|
||||
localCallFeed: null,
|
||||
activeSpeaker: null,
|
||||
userMediaFeeds: [],
|
||||
|
@ -109,7 +157,7 @@ export function useGroupCall(groupCall: GroupCall): UseGroupCallReturnType {
|
|||
screenshareFeeds: [],
|
||||
localDesktopCapturerSourceId: null,
|
||||
requestingScreenshare: false,
|
||||
participants: [],
|
||||
participants: new Map(),
|
||||
hasLocalParticipant: false,
|
||||
});
|
||||
|
||||
|
@ -120,29 +168,30 @@ export function useGroupCall(groupCall: GroupCall): UseGroupCallReturnType {
|
|||
new Set<string>()
|
||||
);
|
||||
|
||||
const updateState = (state: Partial<State>) =>
|
||||
setState((prevState) => ({ ...prevState, ...state }));
|
||||
const updateState = useCallback(
|
||||
(state: Partial<State>) => setState((prev) => ({ ...prev, ...state })),
|
||||
[setState]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
function onGroupCallStateChanged() {
|
||||
updateState({
|
||||
state: groupCall.state,
|
||||
calls: [...groupCall.calls],
|
||||
localCallFeed: groupCall.localCallFeed,
|
||||
activeSpeaker: groupCall.activeSpeaker,
|
||||
activeSpeaker: groupCall.activeSpeaker ?? null,
|
||||
userMediaFeeds: [...groupCall.userMediaFeeds],
|
||||
microphoneMuted: groupCall.isMicrophoneMuted(),
|
||||
localVideoMuted: groupCall.isLocalVideoMuted(),
|
||||
isScreensharing: groupCall.isScreensharing(),
|
||||
localDesktopCapturerSourceId: groupCall.localDesktopCapturerSourceId,
|
||||
screenshareFeeds: [...groupCall.screenshareFeeds],
|
||||
participants: [...groupCall.participants],
|
||||
});
|
||||
}
|
||||
|
||||
function onUserMediaFeedsChanged(userMediaFeeds: CallFeed[]): void {
|
||||
updateState({
|
||||
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({
|
||||
activeSpeaker: activeSpeaker,
|
||||
activeSpeaker: activeSpeaker ?? null,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -179,15 +228,31 @@ export function useGroupCall(groupCall: GroupCall): UseGroupCallReturnType {
|
|||
});
|
||||
}
|
||||
|
||||
function onCallsChanged(calls: MatrixCall[]): void {
|
||||
updateState({
|
||||
calls: [...calls],
|
||||
});
|
||||
const prevCalls = new Set<MatrixCall>();
|
||||
|
||||
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({
|
||||
participants: [...participants],
|
||||
participants: getParticipants(groupCall),
|
||||
hasLocalParticipant: groupCall.hasLocalParticipant(),
|
||||
});
|
||||
}
|
||||
|
@ -218,16 +283,15 @@ export function useGroupCall(groupCall: GroupCall): UseGroupCallReturnType {
|
|||
updateState({
|
||||
error: null,
|
||||
state: groupCall.state,
|
||||
calls: [...groupCall.calls],
|
||||
localCallFeed: groupCall.localCallFeed,
|
||||
activeSpeaker: groupCall.activeSpeaker,
|
||||
activeSpeaker: groupCall.activeSpeaker ?? null,
|
||||
userMediaFeeds: [...groupCall.userMediaFeeds],
|
||||
microphoneMuted: groupCall.isMicrophoneMuted(),
|
||||
localVideoMuted: groupCall.isLocalVideoMuted(),
|
||||
isScreensharing: groupCall.isScreensharing(),
|
||||
localDesktopCapturerSourceId: groupCall.localDesktopCapturerSourceId,
|
||||
screenshareFeeds: [...groupCall.screenshareFeeds],
|
||||
participants: [...groupCall.participants],
|
||||
participants: getParticipants(groupCall),
|
||||
hasLocalParticipant: groupCall.hasLocalParticipant(),
|
||||
});
|
||||
|
||||
|
@ -264,7 +328,7 @@ export function useGroupCall(groupCall: GroupCall): UseGroupCallReturnType {
|
|||
groupCall.removeListener(GroupCallEvent.Error, onError);
|
||||
groupCall.leave();
|
||||
};
|
||||
}, [groupCall]);
|
||||
}, [groupCall, updateState]);
|
||||
|
||||
usePageUnload(() => {
|
||||
groupCall.leave();
|
||||
|
@ -290,7 +354,7 @@ export function useGroupCall(groupCall: GroupCall): UseGroupCallReturnType {
|
|||
console.error(error);
|
||||
updateState({ error });
|
||||
});
|
||||
}, [groupCall]);
|
||||
}, [groupCall, updateState]);
|
||||
|
||||
const leave = useCallback(() => groupCall.leave(), [groupCall]);
|
||||
|
||||
|
@ -341,7 +405,7 @@ export function useGroupCall(groupCall: GroupCall): UseGroupCallReturnType {
|
|||
// toggling off
|
||||
groupCall.setScreensharingEnabled(false);
|
||||
}
|
||||
}, [groupCall]);
|
||||
}, [groupCall, updateState]);
|
||||
|
||||
const onScreenshareStart = useCallback(
|
||||
async (ev: CustomEvent<IWidgetApiRequest>) => {
|
||||
|
@ -355,7 +419,7 @@ export function useGroupCall(groupCall: GroupCall): UseGroupCallReturnType {
|
|||
});
|
||||
await widget.api.transport.reply(ev.detail, {});
|
||||
},
|
||||
[groupCall]
|
||||
[groupCall, updateState]
|
||||
);
|
||||
|
||||
const onScreenshareStop = useCallback(
|
||||
|
@ -364,7 +428,7 @@ export function useGroupCall(groupCall: GroupCall): UseGroupCallReturnType {
|
|||
await groupCall.setScreensharingEnabled(false);
|
||||
await widget.api.transport.reply(ev.detail, {});
|
||||
},
|
||||
[groupCall]
|
||||
[groupCall, updateState]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -402,7 +466,7 @@ export function useGroupCall(groupCall: GroupCall): UseGroupCallReturnType {
|
|||
console.error(error);
|
||||
updateState({ error });
|
||||
}
|
||||
}, [t]);
|
||||
}, [t, updateState]);
|
||||
|
||||
const [spacebarHeld, setSpacebarHeld] = useState(false);
|
||||
|
||||
|
@ -468,7 +532,6 @@ export function useGroupCall(groupCall: GroupCall): UseGroupCallReturnType {
|
|||
|
||||
return {
|
||||
state,
|
||||
calls,
|
||||
localCallFeed,
|
||||
activeSpeaker,
|
||||
userMediaFeeds,
|
||||
|
|
|
@ -24,7 +24,7 @@ import type {
|
|||
|
||||
// Shortcut for registering a listener on an EventTarget
|
||||
export const useEventTarget = <T extends Event>(
|
||||
target: EventTarget,
|
||||
target: EventTarget | null | undefined,
|
||||
eventType: string,
|
||||
listener: (event: T) => void,
|
||||
options?: AddEventListenerOptions
|
||||
|
|
|
@ -21,7 +21,8 @@ import { RoomMember } from "matrix-js-sdk";
|
|||
import { VideoGrid, useVideoGridLayout } from "./VideoGrid";
|
||||
import { VideoTile } from "./VideoTile";
|
||||
import { Button } from "../button";
|
||||
import { ConnectionState, TileDescriptor } from "../room/InCallView";
|
||||
import { TileDescriptor } from "../room/InCallView";
|
||||
import { ConnectionState } from "../room/useGroupCall";
|
||||
|
||||
export default {
|
||||
title: "VideoGrid",
|
||||
|
|
|
@ -23,7 +23,7 @@ import styles from "./VideoTile.module.css";
|
|||
import { ReactComponent as MicMutedIcon } from "../icons/MicMuted.svg";
|
||||
import { ReactComponent as VideoMutedIcon } from "../icons/VideoMuted.svg";
|
||||
import { AudioButton, FullscreenButton } from "../button/Button";
|
||||
import { ConnectionState } from "../room/InCallView";
|
||||
import { ConnectionState } from "../room/useGroupCall";
|
||||
|
||||
interface Props {
|
||||
name: string;
|
||||
|
|
103
yarn.lock
103
yarn.lock
|
@ -1581,6 +1581,16 @@
|
|||
"@types/node" "*"
|
||||
jest-mock "^29.2.2"
|
||||
|
||||
"@jest/environment@^29.3.1":
|
||||
version "29.3.1"
|
||||
resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-29.3.1.tgz#eb039f726d5fcd14698acd072ac6576d41cfcaa6"
|
||||
integrity sha512-pMmvfOPmoa1c1QpfFW0nXYtNLpofqo4BrCIk6f2kW4JFeNlHV2t3vd+3iDLf31e2ot2Mec0uqZfmI+U0K2CFag==
|
||||
dependencies:
|
||||
"@jest/fake-timers" "^29.3.1"
|
||||
"@jest/types" "^29.3.1"
|
||||
"@types/node" "*"
|
||||
jest-mock "^29.3.1"
|
||||
|
||||
"@jest/expect-utils@^29.2.2":
|
||||
version "29.2.2"
|
||||
resolved "https://registry.yarnpkg.com/@jest/expect-utils/-/expect-utils-29.2.2.tgz#460a5b5a3caf84d4feb2668677393dd66ff98665"
|
||||
|
@ -1608,6 +1618,18 @@
|
|||
jest-mock "^29.2.2"
|
||||
jest-util "^29.2.1"
|
||||
|
||||
"@jest/fake-timers@^29.3.1":
|
||||
version "29.3.1"
|
||||
resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-29.3.1.tgz#b140625095b60a44de820876d4c14da1aa963f67"
|
||||
integrity sha512-iHTL/XpnDlFki9Tq0Q1GGuVeQ8BHZGIYsvCO5eN/O/oJaRzofG9Xndd9HuSDBI/0ZS79pg0iwn07OMTQ7ngF2A==
|
||||
dependencies:
|
||||
"@jest/types" "^29.3.1"
|
||||
"@sinonjs/fake-timers" "^9.1.2"
|
||||
"@types/node" "*"
|
||||
jest-message-util "^29.3.1"
|
||||
jest-mock "^29.3.1"
|
||||
jest-util "^29.3.1"
|
||||
|
||||
"@jest/globals@^29.2.2":
|
||||
version "29.2.2"
|
||||
resolved "https://registry.yarnpkg.com/@jest/globals/-/globals-29.2.2.tgz#205ff1e795aa774301c2c0ba0be182558471b845"
|
||||
|
@ -1717,6 +1739,18 @@
|
|||
"@types/yargs" "^17.0.8"
|
||||
chalk "^4.0.0"
|
||||
|
||||
"@jest/types@^29.3.1":
|
||||
version "29.3.1"
|
||||
resolved "https://registry.yarnpkg.com/@jest/types/-/types-29.3.1.tgz#7c5a80777cb13e703aeec6788d044150341147e3"
|
||||
integrity sha512-d0S0jmmTpjnhCmNpApgX3jrUZgZ22ivKJRvL2lli5hpCRoNnp1f85r2/wpKfXuYu8E7Jjh1hGfhPyup1NM5AmA==
|
||||
dependencies:
|
||||
"@jest/schemas" "^29.0.0"
|
||||
"@types/istanbul-lib-coverage" "^2.0.0"
|
||||
"@types/istanbul-reports" "^3.0.0"
|
||||
"@types/node" "*"
|
||||
"@types/yargs" "^17.0.8"
|
||||
chalk "^4.0.0"
|
||||
|
||||
"@joshwooding/vite-plugin-react-docgen-typescript@0.0.2":
|
||||
version "0.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@joshwooding/vite-plugin-react-docgen-typescript/-/vite-plugin-react-docgen-typescript-0.0.2.tgz#e0ae8c94f468da3a273a7b0acf23ba3565f86cbc"
|
||||
|
@ -9457,18 +9491,18 @@ jest-each@^29.2.1:
|
|||
jest-util "^29.2.1"
|
||||
pretty-format "^29.2.1"
|
||||
|
||||
jest-environment-jsdom@^29.2.2:
|
||||
version "29.2.2"
|
||||
resolved "https://registry.yarnpkg.com/jest-environment-jsdom/-/jest-environment-jsdom-29.2.2.tgz#1e2d9f1f017fbaa7362a83e670b569158b4b8527"
|
||||
integrity sha512-5mNtTcky1+RYv9kxkwMwt7fkzyX4EJUarV7iI+NQLigpV4Hz4sgfOdP4kOpCHXbkRWErV7tgXoXLm2CKtucr+A==
|
||||
jest-environment-jsdom@^29.3.1:
|
||||
version "29.3.1"
|
||||
resolved "https://registry.yarnpkg.com/jest-environment-jsdom/-/jest-environment-jsdom-29.3.1.tgz#14ca63c3e0ef5c63c5bcb46033e50bc649e3b639"
|
||||
integrity sha512-G46nKgiez2Gy4zvYNhayfMEAFlVHhWfncqvqS6yCd0i+a4NsSUD2WtrKSaYQrYiLQaupHXxCRi8xxVL2M9PbhA==
|
||||
dependencies:
|
||||
"@jest/environment" "^29.2.2"
|
||||
"@jest/fake-timers" "^29.2.2"
|
||||
"@jest/types" "^29.2.1"
|
||||
"@jest/environment" "^29.3.1"
|
||||
"@jest/fake-timers" "^29.3.1"
|
||||
"@jest/types" "^29.3.1"
|
||||
"@types/jsdom" "^20.0.0"
|
||||
"@types/node" "*"
|
||||
jest-mock "^29.2.2"
|
||||
jest-util "^29.2.1"
|
||||
jest-mock "^29.3.1"
|
||||
jest-util "^29.3.1"
|
||||
jsdom "^20.0.0"
|
||||
|
||||
jest-environment-node@^29.2.2:
|
||||
|
@ -9540,6 +9574,21 @@ jest-message-util@^29.2.1:
|
|||
slash "^3.0.0"
|
||||
stack-utils "^2.0.3"
|
||||
|
||||
jest-message-util@^29.3.1:
|
||||
version "29.3.1"
|
||||
resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-29.3.1.tgz#37bc5c468dfe5120712053dd03faf0f053bd6adb"
|
||||
integrity sha512-lMJTbgNcDm5z+6KDxWtqOFWlGQxD6XaYwBqHR8kmpkP+WWWG90I35kdtQHY67Ay5CSuydkTBbJG+tH9JShFCyA==
|
||||
dependencies:
|
||||
"@babel/code-frame" "^7.12.13"
|
||||
"@jest/types" "^29.3.1"
|
||||
"@types/stack-utils" "^2.0.0"
|
||||
chalk "^4.0.0"
|
||||
graceful-fs "^4.2.9"
|
||||
micromatch "^4.0.4"
|
||||
pretty-format "^29.3.1"
|
||||
slash "^3.0.0"
|
||||
stack-utils "^2.0.3"
|
||||
|
||||
jest-mock@^29.2.2:
|
||||
version "29.2.2"
|
||||
resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-29.2.2.tgz#9045618b3f9d27074bbcf2d55bdca6a5e2e8bca7"
|
||||
|
@ -9549,6 +9598,15 @@ jest-mock@^29.2.2:
|
|||
"@types/node" "*"
|
||||
jest-util "^29.2.1"
|
||||
|
||||
jest-mock@^29.3.1:
|
||||
version "29.3.1"
|
||||
resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-29.3.1.tgz#60287d92e5010979d01f218c6b215b688e0f313e"
|
||||
integrity sha512-H8/qFDtDVMFvFP4X8NuOT3XRDzOUTz+FeACjufHzsOIBAxivLqkB1PoLCaJx9iPPQ8dZThHPp/G3WRWyMgA3JA==
|
||||
dependencies:
|
||||
"@jest/types" "^29.3.1"
|
||||
"@types/node" "*"
|
||||
jest-util "^29.3.1"
|
||||
|
||||
jest-pnp-resolver@^1.2.2:
|
||||
version "1.2.2"
|
||||
resolved "https://registry.yarnpkg.com/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz#b704ac0ae028a89108a4d040b3f919dfddc8e33c"
|
||||
|
@ -9679,6 +9737,18 @@ jest-util@^29.2.1:
|
|||
graceful-fs "^4.2.9"
|
||||
picomatch "^2.2.3"
|
||||
|
||||
jest-util@^29.3.1:
|
||||
version "29.3.1"
|
||||
resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-29.3.1.tgz#1dda51e378bbcb7e3bc9d8ab651445591ed373e1"
|
||||
integrity sha512-7YOVZaiX7RJLv76ZfHt4nbNEzzTRiMW/IiOG7ZOKmTXmoGBxUDefgMAxQubu6WPVqP5zSzAdZG0FfLcC7HOIFQ==
|
||||
dependencies:
|
||||
"@jest/types" "^29.3.1"
|
||||
"@types/node" "*"
|
||||
chalk "^4.0.0"
|
||||
ci-info "^3.2.0"
|
||||
graceful-fs "^4.2.9"
|
||||
picomatch "^2.2.3"
|
||||
|
||||
jest-validate@^29.2.2:
|
||||
version "29.2.2"
|
||||
resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-29.2.2.tgz#e43ce1931292dfc052562a11bc681af3805eadce"
|
||||
|
@ -10196,9 +10266,9 @@ matrix-events-sdk@0.0.1:
|
|||
resolved "https://registry.yarnpkg.com/matrix-events-sdk/-/matrix-events-sdk-0.0.1.tgz#c8c38911e2cb29023b0bbac8d6f32e0de2c957dd"
|
||||
integrity sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA==
|
||||
|
||||
"matrix-js-sdk@github:matrix-org/matrix-js-sdk#3f1c3392d45b0fc054c3788cc6c043cd5b4fb730":
|
||||
version "21.1.0"
|
||||
resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/3f1c3392d45b0fc054c3788cc6c043cd5b4fb730"
|
||||
"matrix-js-sdk@github:matrix-org/matrix-js-sdk#9d3ac66cf847fe8cb0359d64e1f1d67902b982a1":
|
||||
version "21.2.0"
|
||||
resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/9d3ac66cf847fe8cb0359d64e1f1d67902b982a1"
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.12.5"
|
||||
"@types/sdp-transform" "^2.4.5"
|
||||
|
@ -11745,6 +11815,15 @@ pretty-format@^29.0.0, pretty-format@^29.2.1:
|
|||
ansi-styles "^5.0.0"
|
||||
react-is "^18.0.0"
|
||||
|
||||
pretty-format@^29.3.1:
|
||||
version "29.3.1"
|
||||
resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-29.3.1.tgz#1841cac822b02b4da8971dacb03e8a871b4722da"
|
||||
integrity sha512-FyLnmb1cYJV8biEIiRyzRFvs2lry7PPIvOqKVe1GCUEYg4YGmlx1qG9EJNMxArYm7piII4qb8UV1Pncq5dxmcg==
|
||||
dependencies:
|
||||
"@jest/schemas" "^29.0.0"
|
||||
ansi-styles "^5.0.0"
|
||||
react-is "^18.0.0"
|
||||
|
||||
pretty-hrtime@^1.0.3:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz#b7e3ea42435a4c9b2759d99e0f201eb195802ee1"
|
||||
|
|
Loading…
Reference in a new issue