Merge remote-tracking branch 'upstream/livekit' into SimonBrandner/feat/friendly-url
Signed-off-by: Šimon Brandner <simon.bra.ag@gmail.com>
This commit is contained in:
@@ -38,6 +38,15 @@ export function GridLayoutMenu({ layout, setLayout }: Props) {
|
||||
const { t } = useTranslation();
|
||||
const tooltip = useCallback(() => t("Change layout"), [t]);
|
||||
|
||||
const onAction = useCallback(
|
||||
(key: React.Key) => {
|
||||
setLayout(key.toString() as Layout);
|
||||
},
|
||||
[setLayout]
|
||||
);
|
||||
|
||||
const onClose = useCallback(() => {}, []);
|
||||
|
||||
return (
|
||||
<PopoverMenuTrigger placement="bottom right">
|
||||
<TooltipTrigger tooltip={tooltip}>
|
||||
@@ -46,7 +55,12 @@ export function GridLayoutMenu({ layout, setLayout }: Props) {
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
{(props: JSX.IntrinsicAttributes) => (
|
||||
<Menu {...props} label={t("Grid layout menu")} onAction={setLayout}>
|
||||
<Menu
|
||||
{...props}
|
||||
label={t("Grid layout menu")}
|
||||
onAction={onAction}
|
||||
onClose={onClose}
|
||||
>
|
||||
<Item key="freedom" textValue={t("Freedom")}>
|
||||
<FreedomIcon />
|
||||
<span>Freedom</span>
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-nocheck
|
||||
|
||||
/*
|
||||
Copyright 2022 New Vector Ltd
|
||||
|
||||
@@ -70,7 +73,7 @@ const defaultCollapsedFields = [
|
||||
];
|
||||
|
||||
function shouldCollapse({ name }: CollapsedFieldProps) {
|
||||
return defaultCollapsedFields.includes(name);
|
||||
return name ? defaultCollapsedFields.includes(name) : false;
|
||||
}
|
||||
|
||||
function getUserName(userId: string) {
|
||||
@@ -196,7 +199,7 @@ export function SequenceDiagramViewer({
|
||||
onSelectUserId,
|
||||
events,
|
||||
}: SequenceDiagramViewerProps) {
|
||||
const mermaidElRef = useRef<HTMLDivElement>();
|
||||
const mermaidElRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
mermaid.initialize({
|
||||
@@ -217,6 +220,7 @@ export function SequenceDiagramViewer({
|
||||
`;
|
||||
|
||||
mermaid.mermaidAPI.render("mermaid", graphDefinition, (svgCode: string) => {
|
||||
if (!mermaidElRef.current) return;
|
||||
mermaidElRef.current.innerHTML = svgCode;
|
||||
});
|
||||
}, [events, localUserId, selectedUserId]);
|
||||
@@ -228,7 +232,7 @@ export function SequenceDiagramViewer({
|
||||
className={styles.selectInput}
|
||||
label="Remote User"
|
||||
selectedKey={selectedUserId}
|
||||
onSelectionChange={onSelectUserId}
|
||||
onSelectionChange={(key) => onSelectUserId(key.toString())}
|
||||
>
|
||||
{remoteUserIds.map((userId) => (
|
||||
<Item key={userId}>{userId}</Item>
|
||||
@@ -498,7 +502,7 @@ export function GroupCallInspector({
|
||||
return (
|
||||
<Resizable
|
||||
enable={{ top: true }}
|
||||
defaultSize={{ height: 200, width: undefined }}
|
||||
defaultSize={{ height: 200, width: 0 }}
|
||||
className={styles.inspector}
|
||||
>
|
||||
<div className={styles.toolbar}>
|
||||
@@ -507,15 +511,19 @@ export function GroupCallInspector({
|
||||
</button>
|
||||
<button onClick={() => setCurrentTab("inspector")}>Inspector</button>
|
||||
</div>
|
||||
{currentTab === "sequence-diagrams" && (
|
||||
<SequenceDiagramViewer
|
||||
localUserId={state.localUserId}
|
||||
selectedUserId={selectedUserId}
|
||||
onSelectUserId={setSelectedUserId}
|
||||
remoteUserIds={state.remoteUserIds}
|
||||
events={state.eventsByUserId[selectedUserId]}
|
||||
/>
|
||||
)}
|
||||
{currentTab === "sequence-diagrams" &&
|
||||
state.localUserId &&
|
||||
selectedUserId &&
|
||||
state.eventsByUserId &&
|
||||
state.remoteUserIds && (
|
||||
<SequenceDiagramViewer
|
||||
localUserId={state.localUserId}
|
||||
selectedUserId={selectedUserId}
|
||||
onSelectUserId={setSelectedUserId}
|
||||
remoteUserIds={state.remoteUserIds}
|
||||
events={state.eventsByUserId[selectedUserId]}
|
||||
/>
|
||||
)}
|
||||
{currentTab === "inspector" && (
|
||||
<ReactJson
|
||||
theme="monokai"
|
||||
|
||||
@@ -21,7 +21,6 @@ import { useTranslation } from "react-i18next";
|
||||
|
||||
import { useLoadGroupCall } from "./useLoadGroupCall";
|
||||
import { ErrorView, FullScreenView } from "../FullScreenView";
|
||||
import { usePageTitle } from "../usePageTitle";
|
||||
|
||||
interface Props {
|
||||
client: MatrixClient;
|
||||
@@ -39,26 +38,23 @@ export function GroupCallLoader({
|
||||
createPtt,
|
||||
}: Props): JSX.Element {
|
||||
const { t } = useTranslation();
|
||||
const { loading, error, groupCall } = useLoadGroupCall(
|
||||
const groupCallState = useLoadGroupCall(
|
||||
client,
|
||||
roomIdOrAlias,
|
||||
viaServers,
|
||||
createPtt
|
||||
);
|
||||
|
||||
usePageTitle(groupCall ? groupCall.room.name : t("Loading…"));
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<FullScreenView>
|
||||
<h1>{t("Loading…")}</h1>
|
||||
</FullScreenView>
|
||||
);
|
||||
switch (groupCallState.kind) {
|
||||
case "loading":
|
||||
return (
|
||||
<FullScreenView>
|
||||
<h1>{t("Loading…")}</h1>
|
||||
</FullScreenView>
|
||||
);
|
||||
case "loaded":
|
||||
return <>{children(groupCallState.groupCall)}</>;
|
||||
case "failed":
|
||||
return <ErrorView error={groupCallState.error} />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <ErrorView error={error} />;
|
||||
}
|
||||
|
||||
return <>{children(groupCall)}</>;
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import { GroupCall, GroupCallState } from "matrix-js-sdk/src/webrtc/groupCall";
|
||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||
@@ -49,7 +49,6 @@ interface Props {
|
||||
isEmbedded: boolean;
|
||||
preload: boolean;
|
||||
hideHeader: boolean;
|
||||
roomIdOrAlias: string;
|
||||
groupCall: GroupCall;
|
||||
}
|
||||
|
||||
@@ -59,7 +58,6 @@ export function GroupCallView({
|
||||
isEmbedded,
|
||||
preload,
|
||||
hideHeader,
|
||||
roomIdOrAlias,
|
||||
groupCall,
|
||||
}: Props) {
|
||||
const {
|
||||
@@ -82,13 +80,14 @@ export function GroupCallView({
|
||||
}, [groupCall]);
|
||||
|
||||
const { displayName, avatarUrl } = useProfile(client);
|
||||
|
||||
const matrixInfo: MatrixInfo = {
|
||||
displayName,
|
||||
avatarUrl,
|
||||
roomName: groupCall.room.name,
|
||||
roomIdOrAlias,
|
||||
};
|
||||
const matrixInfo = useMemo((): MatrixInfo => {
|
||||
return {
|
||||
displayName: displayName!,
|
||||
avatarUrl: avatarUrl!,
|
||||
roomId: groupCall.room.roomId,
|
||||
roomName: groupCall.room.name,
|
||||
};
|
||||
}, [displayName, avatarUrl, groupCall]);
|
||||
|
||||
useEffect(() => {
|
||||
if (widget && preload) {
|
||||
@@ -139,14 +138,14 @@ export function GroupCallView({
|
||||
PosthogAnalytics.instance.eventCallStarted.track(groupCall.groupCallId);
|
||||
|
||||
await Promise.all([
|
||||
widget.api.setAlwaysOnScreen(true),
|
||||
widget.api.transport.reply(ev.detail, {}),
|
||||
widget!.api.setAlwaysOnScreen(true),
|
||||
widget!.api.transport.reply(ev.detail, {}),
|
||||
]);
|
||||
};
|
||||
|
||||
widget.lazyActions.on(ElementWidgetActions.JoinCall, onJoin);
|
||||
return () => {
|
||||
widget.lazyActions.off(ElementWidgetActions.JoinCall, onJoin);
|
||||
widget!.lazyActions.off(ElementWidgetActions.JoinCall, onJoin);
|
||||
};
|
||||
}
|
||||
}, [groupCall, preload, enter]);
|
||||
@@ -205,12 +204,12 @@ export function GroupCallView({
|
||||
if (widget && state === GroupCallState.Entered) {
|
||||
const onHangup = async (ev: CustomEvent<IWidgetApiRequest>) => {
|
||||
leave();
|
||||
await widget.api.transport.reply(ev.detail, {});
|
||||
widget.api.setAlwaysOnScreen(false);
|
||||
await widget!.api.transport.reply(ev.detail, {});
|
||||
widget!.api.setAlwaysOnScreen(false);
|
||||
};
|
||||
widget.lazyActions.once(ElementWidgetActions.HangupCall, onHangup);
|
||||
return () => {
|
||||
widget.lazyActions.off(ElementWidgetActions.HangupCall, onHangup);
|
||||
widget!.lazyActions.off(ElementWidgetActions.HangupCall, onHangup);
|
||||
};
|
||||
}
|
||||
}, [groupCall, state, leave]);
|
||||
@@ -219,26 +218,14 @@ export function GroupCallView({
|
||||
undefined
|
||||
);
|
||||
|
||||
const [livekitServiceURL, setLivekitServiceURL] = useState<
|
||||
string | undefined
|
||||
>(groupCall.foci[0]?.livekitServiceUrl);
|
||||
|
||||
useEffect(() => {
|
||||
setLivekitServiceURL(groupCall.foci[0]?.livekitServiceUrl);
|
||||
}, [setLivekitServiceURL, groupCall]);
|
||||
|
||||
if (!livekitServiceURL) {
|
||||
return <ErrorView error={new Error("No livekit_service_url defined")} />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <ErrorView error={error} />;
|
||||
} else if (state === GroupCallState.Entered && userChoices) {
|
||||
return (
|
||||
<OpenIDLoader
|
||||
client={client}
|
||||
livekitServiceURL={livekitServiceURL}
|
||||
roomName={matrixInfo.roomName}
|
||||
groupCall={groupCall}
|
||||
roomName={`${groupCall.room.roomId}-${groupCall.groupCallId}`}
|
||||
>
|
||||
<ActiveCall
|
||||
client={client}
|
||||
@@ -247,7 +234,6 @@ export function GroupCallView({
|
||||
onLeave={onLeave}
|
||||
unencryptedEventsFromUsers={unencryptedEventsFromUsers}
|
||||
hideHeader={hideHeader}
|
||||
matrixInfo={matrixInfo}
|
||||
userChoices={userChoices}
|
||||
otelGroupCallMembership={otelGroupCallMembership}
|
||||
/>
|
||||
|
||||
@@ -68,22 +68,21 @@ import { ElementWidgetActions, widget } from "../widget";
|
||||
import { GridLayoutMenu } from "./GridLayoutMenu";
|
||||
import { GroupCallInspector } from "./GroupCallInspector";
|
||||
import styles from "./InCallView.module.css";
|
||||
import { MatrixInfo } from "./VideoPreview";
|
||||
import { useJoinRule } from "./useJoinRule";
|
||||
import { ParticipantInfo } from "./useGroupCall";
|
||||
import { ItemData, TileContent } from "../video-grid/VideoTile";
|
||||
import { ItemData, TileContent, VideoTile } from "../video-grid/VideoTile";
|
||||
import { NewVideoGrid } from "../video-grid/NewVideoGrid";
|
||||
import { OTelGroupCallMembership } from "../otel/OTelGroupCallMembership";
|
||||
import { SettingsModal } from "../settings/SettingsModal";
|
||||
import { InviteModal } from "./InviteModal";
|
||||
import { useRageshakeRequestModal } from "../settings/submit-rageshake";
|
||||
import { RageshakeRequestModal } from "./RageshakeRequestModal";
|
||||
import { VideoTile } from "../video-grid/VideoTile";
|
||||
import { UserChoices, useLiveKit } from "../livekit/useLiveKit";
|
||||
import { useMediaDevices } from "../livekit/useMediaDevices";
|
||||
import { useMediaDevicesSwitcher } from "../livekit/useMediaDevicesSwitcher";
|
||||
import { useFullscreen } from "./useFullscreen";
|
||||
import { useLayoutStates } from "../video-grid/Layout";
|
||||
import { useSFUConfig } from "../livekit/OpenIDLoader";
|
||||
import { E2EELock } from "../E2EELock";
|
||||
|
||||
const canScreenshare = "getDisplayMedia" in (navigator.mediaDevices ?? {});
|
||||
// There is currently a bug in Safari our our code with cloning and sending MediaStreams
|
||||
@@ -99,12 +98,14 @@ export function ActiveCall(props: ActiveCallProps) {
|
||||
const sfuConfig = useSFUConfig();
|
||||
const livekitRoom = useLiveKit(props.userChoices, sfuConfig);
|
||||
|
||||
if (!livekitRoom) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
livekitRoom && (
|
||||
<RoomContext.Provider value={livekitRoom}>
|
||||
<InCallView {...props} livekitRoom={livekitRoom} />
|
||||
</RoomContext.Provider>
|
||||
)
|
||||
<RoomContext.Provider value={livekitRoom}>
|
||||
<InCallView {...props} livekitRoom={livekitRoom} />
|
||||
</RoomContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -116,8 +117,7 @@ export interface InCallViewProps {
|
||||
onLeave: () => void;
|
||||
unencryptedEventsFromUsers: Set<string>;
|
||||
hideHeader: boolean;
|
||||
matrixInfo: MatrixInfo;
|
||||
otelGroupCallMembership: OTelGroupCallMembership;
|
||||
otelGroupCallMembership?: OTelGroupCallMembership;
|
||||
}
|
||||
|
||||
export function InCallView({
|
||||
@@ -128,7 +128,6 @@ export function InCallView({
|
||||
onLeave,
|
||||
unencryptedEventsFromUsers,
|
||||
hideHeader,
|
||||
matrixInfo,
|
||||
otelGroupCallMembership,
|
||||
}: InCallViewProps) {
|
||||
const { t } = useTranslation();
|
||||
@@ -147,7 +146,7 @@ export function InCallView({
|
||||
);
|
||||
|
||||
// Managed media devices state coupled with an active room.
|
||||
const roomMediaDevices = useMediaDevices(livekitRoom);
|
||||
const roomMediaSwitcher = useMediaDevicesSwitcher(livekitRoom);
|
||||
|
||||
const screenSharingTracks = useTracks(
|
||||
[{ source: Track.Source.ScreenShare, withPlaceholder: false }],
|
||||
@@ -202,11 +201,11 @@ export function InCallView({
|
||||
if (widget) {
|
||||
const onTileLayout = async (ev: CustomEvent<IWidgetApiRequest>) => {
|
||||
setLayout("freedom");
|
||||
await widget.api.transport.reply(ev.detail, {});
|
||||
await widget!.api.transport.reply(ev.detail, {});
|
||||
};
|
||||
const onSpotlightLayout = async (ev: CustomEvent<IWidgetApiRequest>) => {
|
||||
setLayout("spotlight");
|
||||
await widget.api.transport.reply(ev.detail, {});
|
||||
await widget!.api.transport.reply(ev.detail, {});
|
||||
};
|
||||
|
||||
widget.lazyActions.on(ElementWidgetActions.TileLayout, onTileLayout);
|
||||
@@ -216,8 +215,8 @@ export function InCallView({
|
||||
);
|
||||
|
||||
return () => {
|
||||
widget.lazyActions.off(ElementWidgetActions.TileLayout, onTileLayout);
|
||||
widget.lazyActions.off(
|
||||
widget!.lazyActions.off(ElementWidgetActions.TileLayout, onTileLayout);
|
||||
widget!.lazyActions.off(
|
||||
ElementWidgetActions.SpotlightLayout,
|
||||
onSpotlightLayout
|
||||
);
|
||||
@@ -340,7 +339,12 @@ export function InCallView({
|
||||
|
||||
const toggleScreensharing = useCallback(async () => {
|
||||
exitFullscreen();
|
||||
await localParticipant.setScreenShareEnabled(!isScreenShareEnabled);
|
||||
await localParticipant.setScreenShareEnabled(!isScreenShareEnabled, {
|
||||
audio: true,
|
||||
selfBrowserSurface: "include",
|
||||
surfaceSwitching: "include",
|
||||
systemAudio: "include",
|
||||
});
|
||||
}, [localParticipant, isScreenShareEnabled, exitFullscreen]);
|
||||
|
||||
let footer: JSX.Element | null;
|
||||
@@ -390,11 +394,12 @@ export function InCallView({
|
||||
{!hideHeader && maximisedParticipant === null && (
|
||||
<Header>
|
||||
<LeftNav>
|
||||
<RoomHeaderInfo roomName={matrixInfo.roomName} />
|
||||
<RoomHeaderInfo roomName={groupCall.room.name} />
|
||||
<VersionMismatchWarning
|
||||
users={unencryptedEventsFromUsers}
|
||||
room={groupCall.room}
|
||||
/>
|
||||
<E2EELock />
|
||||
</LeftNav>
|
||||
<RightNav>
|
||||
<GridLayoutMenu layout={layout} setLayout={setLayout} />
|
||||
@@ -409,31 +414,30 @@ export function InCallView({
|
||||
{renderContent()}
|
||||
{footer}
|
||||
</div>
|
||||
<GroupCallInspector
|
||||
client={client}
|
||||
groupCall={groupCall}
|
||||
otelGroupCallMembership={otelGroupCallMembership}
|
||||
show={showInspector}
|
||||
/>
|
||||
{otelGroupCallMembership && (
|
||||
<GroupCallInspector
|
||||
client={client}
|
||||
groupCall={groupCall}
|
||||
otelGroupCallMembership={otelGroupCallMembership}
|
||||
show={showInspector}
|
||||
/>
|
||||
)}
|
||||
{rageshakeRequestModalState.isOpen && !noControls && (
|
||||
<RageshakeRequestModal
|
||||
{...rageshakeRequestModalProps}
|
||||
roomIdOrAlias={matrixInfo.roomIdOrAlias}
|
||||
roomId={groupCall.room.roomId}
|
||||
/>
|
||||
)}
|
||||
{settingsModalState.isOpen && (
|
||||
<SettingsModal
|
||||
client={client}
|
||||
roomId={groupCall.room.roomId}
|
||||
mediaDevices={roomMediaDevices}
|
||||
mediaDevicesSwitcher={roomMediaSwitcher}
|
||||
{...settingsModalProps}
|
||||
/>
|
||||
)}
|
||||
{inviteModalState.isOpen && (
|
||||
<InviteModal
|
||||
roomIdOrAlias={matrixInfo.roomIdOrAlias}
|
||||
{...inviteModalProps}
|
||||
/>
|
||||
<InviteModal roomId={groupCall.room.roomId} {...inviteModalProps} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -23,10 +23,10 @@ import { getRoomUrl } from "../matrix-utils";
|
||||
import styles from "./InviteModal.module.css";
|
||||
|
||||
interface Props extends Omit<ModalProps, "title" | "children"> {
|
||||
roomIdOrAlias: string;
|
||||
roomId: string;
|
||||
}
|
||||
|
||||
export const InviteModal: FC<Props> = ({ roomIdOrAlias, ...rest }) => {
|
||||
export const InviteModal: FC<Props> = ({ roomId, ...rest }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
@@ -40,7 +40,7 @@ export const InviteModal: FC<Props> = ({ roomIdOrAlias, ...rest }) => {
|
||||
<p>{t("Copy and share this call link")}</p>
|
||||
<CopyButton
|
||||
className={styles.copyButton}
|
||||
value={getRoomUrl(roomIdOrAlias)}
|
||||
value={getRoomUrl(roomId)}
|
||||
data-testid="modal_inviteLink"
|
||||
/>
|
||||
</ModalContent>
|
||||
|
||||
@@ -39,7 +39,7 @@ export function LobbyView(props: Props) {
|
||||
const { t } = useTranslation();
|
||||
useLocationNavigation();
|
||||
|
||||
const joinCallButtonRef = useRef<HTMLButtonElement>();
|
||||
const joinCallButtonRef = useRef<HTMLButtonElement>(null);
|
||||
useEffect(() => {
|
||||
if (joinCallButtonRef.current) {
|
||||
joinCallButtonRef.current.focus();
|
||||
@@ -81,7 +81,7 @@ export function LobbyView(props: Props) {
|
||||
<Body>Or</Body>
|
||||
<CopyButton
|
||||
variant="secondaryCopy"
|
||||
value={getRoomUrl(props.matrixInfo.roomName)}
|
||||
value={getRoomUrl(props.matrixInfo.roomId)}
|
||||
className={styles.copyButton}
|
||||
copiedMessage={t("Call link copied")}
|
||||
data-testid="lobby_inviteLink"
|
||||
|
||||
@@ -25,13 +25,13 @@ import { Body } from "../typography/Typography";
|
||||
|
||||
interface Props extends Omit<ModalProps, "title" | "children"> {
|
||||
rageshakeRequestId: string;
|
||||
roomIdOrAlias: string;
|
||||
roomId: string;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const RageshakeRequestModal: FC<Props> = ({
|
||||
rageshakeRequestId,
|
||||
roomIdOrAlias,
|
||||
roomId,
|
||||
...rest
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
@@ -57,7 +57,7 @@ export const RageshakeRequestModal: FC<Props> = ({
|
||||
submitRageshake({
|
||||
sendLogs: true,
|
||||
rageshakeRequestId,
|
||||
roomId: roomIdOrAlias, // Possibly not a room ID, but oh well
|
||||
roomId,
|
||||
})
|
||||
}
|
||||
disabled={sending}
|
||||
|
||||
@@ -26,15 +26,18 @@ import { FieldRow, InputField, ErrorMessage } from "../input/Input";
|
||||
import { Form } from "../form/Form";
|
||||
import { UserMenuContainer } from "../UserMenuContainer";
|
||||
import { useRegisterPasswordlessUser } from "../auth/useRegisterPasswordlessUser";
|
||||
import { Config } from "../config/Config";
|
||||
|
||||
export function RoomAuthView() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<Error>();
|
||||
|
||||
const { registerPasswordlessUser, recaptchaId, privacyPolicyUrl } =
|
||||
const { registerPasswordlessUser, recaptchaId } =
|
||||
useRegisterPasswordlessUser();
|
||||
|
||||
const onSubmit = useCallback(
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
(e) => {
|
||||
e.preventDefault();
|
||||
const data = new FormData(e.target);
|
||||
@@ -83,7 +86,7 @@ export function RoomAuthView() {
|
||||
<Caption>
|
||||
<Trans>
|
||||
By clicking "Join call now", you agree to our{" "}
|
||||
<Link href={privacyPolicyUrl}>
|
||||
<Link href={Config.get().eula}>
|
||||
End User Licensing Agreement (EULA)
|
||||
</Link>
|
||||
</Trans>
|
||||
|
||||
@@ -18,7 +18,7 @@ import { FC, useEffect, useState, useCallback } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import type { GroupCall } from "matrix-js-sdk/src/webrtc/groupCall";
|
||||
import { useClient } from "../ClientContext";
|
||||
import { useClientLegacy } from "../ClientContext";
|
||||
import { ErrorView, LoadingView } from "../FullScreenView";
|
||||
import { RoomAuthView } from "./RoomAuthView";
|
||||
import { GroupCallLoader } from "./GroupCallLoader";
|
||||
@@ -30,8 +30,6 @@ import { useOptInAnalytics } from "../settings/useSetting";
|
||||
|
||||
export const RoomPage: FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const { loading, isAuthenticated, error, client, isPasswordlessUser } =
|
||||
useClient();
|
||||
|
||||
const {
|
||||
roomAlias,
|
||||
@@ -52,39 +50,41 @@ export const RoomPage: FC = () => {
|
||||
|
||||
useEffect(() => {
|
||||
// During the beta, opt into analytics by default
|
||||
if (optInAnalytics === null) setOptInAnalytics(true);
|
||||
if (optInAnalytics === null && setOptInAnalytics) setOptInAnalytics(true);
|
||||
}, [optInAnalytics, setOptInAnalytics]);
|
||||
|
||||
const { loading, authenticated, client, error, passwordlessUser } =
|
||||
useClientLegacy();
|
||||
|
||||
useEffect(() => {
|
||||
// If we've finished loading, are not already authed and we've been given a display name as
|
||||
// a URL param, automatically register a passwordless user
|
||||
if (!loading && !isAuthenticated && displayName) {
|
||||
if (!loading && !authenticated && displayName) {
|
||||
setIsRegistering(true);
|
||||
registerPasswordlessUser(displayName).finally(() => {
|
||||
setIsRegistering(false);
|
||||
});
|
||||
}
|
||||
}, [
|
||||
isAuthenticated,
|
||||
loading,
|
||||
authenticated,
|
||||
displayName,
|
||||
setIsRegistering,
|
||||
registerPasswordlessUser,
|
||||
loading,
|
||||
]);
|
||||
|
||||
const groupCallView = useCallback(
|
||||
(groupCall: GroupCall) => (
|
||||
<GroupCallView
|
||||
client={client}
|
||||
roomIdOrAlias={roomIdOrAlias}
|
||||
client={client!}
|
||||
groupCall={groupCall}
|
||||
isPasswordlessUser={isPasswordlessUser}
|
||||
isPasswordlessUser={passwordlessUser}
|
||||
isEmbedded={isEmbedded}
|
||||
preload={preload}
|
||||
hideHeader={hideHeader}
|
||||
/>
|
||||
),
|
||||
[client, roomIdOrAlias, isPasswordlessUser, isEmbedded, preload, hideHeader]
|
||||
[client, passwordlessUser, isEmbedded, preload, hideHeader]
|
||||
);
|
||||
|
||||
if (loading || isRegistering) {
|
||||
@@ -95,7 +95,7 @@ export const RoomPage: FC = () => {
|
||||
return <ErrorView error={error} />;
|
||||
}
|
||||
|
||||
if (!isAuthenticated) {
|
||||
if (!client) {
|
||||
return <RoomAuthView />;
|
||||
}
|
||||
|
||||
|
||||
@@ -14,11 +14,12 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useRef, useCallback } from "react";
|
||||
import React, { useState, useEffect, useCallback, useRef } from "react";
|
||||
import useMeasure from "react-use-measure";
|
||||
import { ResizeObserver } from "@juggle/resize-observer";
|
||||
import { OverlayTriggerState } from "@react-stately/overlays";
|
||||
import { usePreviewDevice } from "@livekit/components-react";
|
||||
import { usePreviewTracks } from "@livekit/components-react";
|
||||
import { LocalAudioTrack, LocalVideoTrack, Track } from "livekit-client";
|
||||
|
||||
import { MicButton, SettingsButton, VideoButton } from "../button";
|
||||
import { Avatar } from "../Avatar";
|
||||
@@ -26,15 +27,15 @@ import styles from "./VideoPreview.module.css";
|
||||
import { useModalTriggerState } from "../Modal";
|
||||
import { SettingsModal } from "../settings/SettingsModal";
|
||||
import { useClient } from "../ClientContext";
|
||||
import { useMediaDevices } from "../livekit/useMediaDevices";
|
||||
import { DeviceChoices, UserChoices } from "../livekit/useLiveKit";
|
||||
import { useMediaDevicesSwitcher } from "../livekit/useMediaDevicesSwitcher";
|
||||
import { UserChoices } from "../livekit/useLiveKit";
|
||||
import { useDefaultDevices } from "../settings/useSetting";
|
||||
|
||||
export type MatrixInfo = {
|
||||
displayName: string;
|
||||
avatarUrl: string;
|
||||
roomId: string;
|
||||
roomName: string;
|
||||
roomIdOrAlias: string;
|
||||
};
|
||||
|
||||
interface Props {
|
||||
@@ -61,85 +62,111 @@ export function VideoPreview({ matrixInfo, onUserChoicesChanged }: Props) {
|
||||
settingsModalState.open();
|
||||
}, [settingsModalState]);
|
||||
|
||||
// Fetch user media devices.
|
||||
const mediaDevices = useMediaDevices();
|
||||
|
||||
// Create local media tracks.
|
||||
const [videoEnabled, setVideoEnabled] = useState<boolean>(true);
|
||||
const [audioEnabled, setAudioEnabled] = useState<boolean>(true);
|
||||
const [videoId, audioId] = [
|
||||
mediaDevices.videoIn.selectedId,
|
||||
mediaDevices.audioIn.selectedId,
|
||||
];
|
||||
const [defaultDevices] = useDefaultDevices();
|
||||
const video = usePreviewDevice(
|
||||
videoEnabled,
|
||||
videoId != "" ? videoId : defaultDevices.videoinput,
|
||||
"videoinput"
|
||||
|
||||
// The settings are updated as soon as the device changes. We wrap the settings value in a ref to store their initial value.
|
||||
// Not changing the device options prohibits the usePreviewTracks hook to recreate the tracks.
|
||||
const initialDefaultDevices = useRef(useDefaultDevices()[0]);
|
||||
const tracks = usePreviewTracks(
|
||||
{
|
||||
audio: { deviceId: initialDefaultDevices.current.audioinput },
|
||||
video: { deviceId: initialDefaultDevices.current.videoinput },
|
||||
},
|
||||
(error) => {
|
||||
console.error("Error while creating preview Tracks:", error);
|
||||
}
|
||||
);
|
||||
const audio = usePreviewDevice(
|
||||
audioEnabled,
|
||||
audioId != "" ? audioId : defaultDevices.audioinput,
|
||||
"audioinput"
|
||||
const videoTrack = React.useMemo(
|
||||
() =>
|
||||
tracks?.filter((t) => t.kind === Track.Kind.Video)[0] as LocalVideoTrack,
|
||||
[tracks]
|
||||
);
|
||||
const audioTrack = React.useMemo(
|
||||
() =>
|
||||
tracks?.filter((t) => t.kind === Track.Kind.Audio)[0] as LocalAudioTrack,
|
||||
[tracks]
|
||||
);
|
||||
|
||||
const activeVideoId = video?.selectedDevice?.deviceId;
|
||||
const activeAudioId = audio?.selectedDevice?.deviceId;
|
||||
// Only let the MediaDeviceSwitcher request permissions if a video track is already available.
|
||||
// Otherwise we would end up asking for permissions in usePreviewTracks and in useMediaDevicesSwitcher.
|
||||
const requestPermissions = !!audioTrack && !!videoTrack;
|
||||
const mediaSwitcher = useMediaDevicesSwitcher(
|
||||
undefined,
|
||||
{ videoTrack, audioTrack },
|
||||
requestPermissions
|
||||
);
|
||||
const { videoIn, audioIn } = mediaSwitcher;
|
||||
|
||||
const videoEl = React.useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
const createChoices = (
|
||||
enabled: boolean,
|
||||
deviceId?: string
|
||||
): DeviceChoices | undefined => {
|
||||
if (deviceId === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
selectedId: deviceId,
|
||||
enabled,
|
||||
};
|
||||
};
|
||||
|
||||
// Effect to update the settings
|
||||
onUserChoicesChanged({
|
||||
video: createChoices(videoEnabled, activeVideoId),
|
||||
audio: createChoices(audioEnabled, activeAudioId),
|
||||
video: {
|
||||
selectedId: videoIn.selectedId,
|
||||
enabled: videoEnabled && !!videoTrack,
|
||||
},
|
||||
audio: {
|
||||
selectedId: audioIn.selectedId,
|
||||
enabled: audioEnabled && !!audioTrack,
|
||||
},
|
||||
});
|
||||
}, [
|
||||
onUserChoicesChanged,
|
||||
activeVideoId,
|
||||
videoIn.selectedId,
|
||||
videoEnabled,
|
||||
activeAudioId,
|
||||
audioIn.selectedId,
|
||||
audioEnabled,
|
||||
videoTrack,
|
||||
audioTrack,
|
||||
]);
|
||||
|
||||
const [selectVideo, selectAudio] = [
|
||||
mediaDevices.videoIn.setSelected,
|
||||
mediaDevices.audioIn.setSelected,
|
||||
];
|
||||
useEffect(() => {
|
||||
if (activeVideoId && activeVideoId !== "") {
|
||||
selectVideo(activeVideoId);
|
||||
// Effect to update the initial device selection for the ui elements based on the current preview track.
|
||||
if (!videoIn.selectedId || videoIn.selectedId == "") {
|
||||
videoTrack?.getDeviceId().then((videoId) => {
|
||||
videoIn.setSelected(videoId ?? "default");
|
||||
});
|
||||
}
|
||||
if (activeAudioId && activeAudioId !== "") {
|
||||
selectAudio(activeAudioId);
|
||||
if (!audioIn.selectedId || audioIn.selectedId == "") {
|
||||
audioTrack?.getDeviceId().then((audioId) => {
|
||||
// getDeviceId() can return undefined for audio devices. This happens if
|
||||
// the devices list uses "default" as the device id for the current
|
||||
// device and the device set on the track also uses the deviceId
|
||||
// "default". Check `normalizeDeviceId` in `getDeviceId` for more
|
||||
// details.
|
||||
audioIn.setSelected(audioId ?? "default");
|
||||
});
|
||||
}
|
||||
}, [selectVideo, selectAudio, activeVideoId, activeAudioId]);
|
||||
}, [videoIn, audioIn, videoTrack, audioTrack]);
|
||||
|
||||
const mediaElement = useRef(null);
|
||||
useEffect(() => {
|
||||
if (mediaElement.current) {
|
||||
video?.localTrack?.attach(mediaElement.current);
|
||||
// Effect to connect the videoTrack with the video element.
|
||||
if (videoEl.current) {
|
||||
videoTrack?.unmute();
|
||||
videoTrack?.attach(videoEl.current);
|
||||
}
|
||||
return () => {
|
||||
video?.localTrack?.detach();
|
||||
videoTrack?.detach();
|
||||
};
|
||||
}, [video?.localTrack, mediaElement]);
|
||||
}, [videoTrack]);
|
||||
|
||||
useEffect(() => {
|
||||
// Effect to mute/unmute video track. (This has to be done, so that the hardware camera indicator does not confuse the user)
|
||||
if (videoTrack && videoEnabled) {
|
||||
videoTrack?.unmute();
|
||||
} else if (videoTrack) {
|
||||
videoTrack?.mute();
|
||||
}
|
||||
}, [videoEnabled, videoTrack]);
|
||||
|
||||
return (
|
||||
<div className={styles.preview} ref={previewRef}>
|
||||
<video ref={mediaElement} muted playsInline disablePictureInPicture />
|
||||
<video ref={videoEl} muted playsInline disablePictureInPicture />
|
||||
<>
|
||||
{(video ? !videoEnabled : true) && (
|
||||
{(videoTrack ? !videoEnabled : true) && (
|
||||
<div className={styles.avatarContainer}>
|
||||
<Avatar
|
||||
size={(previewBounds.height - 66) / 2}
|
||||
@@ -149,25 +176,23 @@ export function VideoPreview({ matrixInfo, onUserChoicesChanged }: Props) {
|
||||
</div>
|
||||
)}
|
||||
<div className={styles.previewButtons}>
|
||||
{audio.localTrack && (
|
||||
<MicButton
|
||||
muted={!audioEnabled}
|
||||
onPress={() => setAudioEnabled(!audioEnabled)}
|
||||
/>
|
||||
)}
|
||||
{video.localTrack && (
|
||||
<VideoButton
|
||||
muted={!videoEnabled}
|
||||
onPress={() => setVideoEnabled(!videoEnabled)}
|
||||
/>
|
||||
)}
|
||||
<MicButton
|
||||
muted={!audioEnabled}
|
||||
onPress={() => setAudioEnabled(!audioEnabled)}
|
||||
disabled={!audioTrack}
|
||||
/>
|
||||
<VideoButton
|
||||
muted={!videoEnabled}
|
||||
onPress={() => setVideoEnabled(!videoEnabled)}
|
||||
disabled={!videoTrack}
|
||||
/>
|
||||
<SettingsButton onPress={openSettings} />
|
||||
</div>
|
||||
</>
|
||||
{settingsModalState.isOpen && (
|
||||
{settingsModalState.isOpen && client && (
|
||||
<SettingsModal
|
||||
client={client}
|
||||
mediaDevices={mediaDevices}
|
||||
mediaDevicesSwitcher={mediaSwitcher}
|
||||
{...settingsModalProps}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -58,12 +58,12 @@ export interface ParticipantInfo {
|
||||
|
||||
interface UseGroupCallReturnType {
|
||||
state: GroupCallState;
|
||||
localCallFeed: CallFeed;
|
||||
activeSpeaker: CallFeed | null;
|
||||
localCallFeed?: CallFeed;
|
||||
activeSpeaker?: CallFeed;
|
||||
userMediaFeeds: CallFeed[];
|
||||
microphoneMuted: boolean;
|
||||
localVideoMuted: boolean;
|
||||
error: TranslatedError | null;
|
||||
error?: TranslatedError;
|
||||
initLocalCallFeed: () => void;
|
||||
enter: () => Promise<void>;
|
||||
leave: () => void;
|
||||
@@ -74,23 +74,21 @@ interface UseGroupCallReturnType {
|
||||
requestingScreenshare: boolean;
|
||||
isScreensharing: boolean;
|
||||
screenshareFeeds: CallFeed[];
|
||||
localDesktopCapturerSourceId: string; // XXX: This looks unused?
|
||||
participants: Map<RoomMember, Map<string, ParticipantInfo>>;
|
||||
hasLocalParticipant: boolean;
|
||||
unencryptedEventsFromUsers: Set<string>;
|
||||
otelGroupCallMembership: OTelGroupCallMembership;
|
||||
otelGroupCallMembership?: OTelGroupCallMembership;
|
||||
}
|
||||
|
||||
interface State {
|
||||
state: GroupCallState;
|
||||
localCallFeed: CallFeed;
|
||||
activeSpeaker: CallFeed | null;
|
||||
localCallFeed?: CallFeed;
|
||||
activeSpeaker?: CallFeed;
|
||||
userMediaFeeds: CallFeed[];
|
||||
error: TranslatedError | null;
|
||||
error?: TranslatedError;
|
||||
microphoneMuted: boolean;
|
||||
localVideoMuted: boolean;
|
||||
screenshareFeeds: CallFeed[];
|
||||
localDesktopCapturerSourceId: string;
|
||||
isScreensharing: boolean;
|
||||
requestingScreenshare: boolean;
|
||||
participants: Map<RoomMember, Map<string, ParticipantInfo>>;
|
||||
@@ -101,7 +99,7 @@ interface State {
|
||||
// level so that it doesn't pop in & out of existence as react mounts & unmounts
|
||||
// components. The right solution is probably for this to live in the js-sdk and have
|
||||
// the same lifetime as groupcalls themselves.
|
||||
let groupCallOTelMembership: OTelGroupCallMembership;
|
||||
let groupCallOTelMembership: OTelGroupCallMembership | undefined;
|
||||
let groupCallOTelMembershipGroupCallId: string;
|
||||
|
||||
function getParticipants(
|
||||
@@ -159,7 +157,6 @@ export function useGroupCall(
|
||||
localVideoMuted,
|
||||
isScreensharing,
|
||||
screenshareFeeds,
|
||||
localDesktopCapturerSourceId,
|
||||
participants,
|
||||
hasLocalParticipant,
|
||||
requestingScreenshare,
|
||||
@@ -167,15 +164,11 @@ export function useGroupCall(
|
||||
setState,
|
||||
] = useState<State>({
|
||||
state: GroupCallState.LocalCallFeedUninitialized,
|
||||
localCallFeed: null,
|
||||
activeSpeaker: null,
|
||||
userMediaFeeds: [],
|
||||
error: null,
|
||||
microphoneMuted: false,
|
||||
localVideoMuted: false,
|
||||
isScreensharing: false,
|
||||
screenshareFeeds: [],
|
||||
localDesktopCapturerSourceId: null,
|
||||
requestingScreenshare: false,
|
||||
participants: new Map(),
|
||||
hasLocalParticipant: false,
|
||||
@@ -248,12 +241,11 @@ export function useGroupCall(
|
||||
updateState({
|
||||
state: groupCall.state,
|
||||
localCallFeed: groupCall.localCallFeed,
|
||||
activeSpeaker: groupCall.activeSpeaker ?? null,
|
||||
activeSpeaker: groupCall.activeSpeaker,
|
||||
userMediaFeeds: [...groupCall.userMediaFeeds],
|
||||
microphoneMuted: groupCall.isMicrophoneMuted(),
|
||||
localVideoMuted: groupCall.isLocalVideoMuted(),
|
||||
isScreensharing: groupCall.isScreensharing(),
|
||||
localDesktopCapturerSourceId: groupCall.localDesktopCapturerSourceId,
|
||||
screenshareFeeds: [...groupCall.screenshareFeeds],
|
||||
});
|
||||
}
|
||||
@@ -303,7 +295,7 @@ export function useGroupCall(
|
||||
|
||||
function onActiveSpeakerChanged(activeSpeaker: CallFeed | undefined): void {
|
||||
updateState({
|
||||
activeSpeaker: activeSpeaker ?? null,
|
||||
activeSpeaker: activeSpeaker,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -319,12 +311,11 @@ export function useGroupCall(
|
||||
|
||||
function onLocalScreenshareStateChanged(
|
||||
isScreensharing: boolean,
|
||||
_localScreenshareFeed: CallFeed,
|
||||
localDesktopCapturerSourceId: string
|
||||
_localScreenshareFeed?: CallFeed,
|
||||
localDesktopCapturerSourceId?: string
|
||||
): void {
|
||||
updateState({
|
||||
isScreensharing,
|
||||
localDesktopCapturerSourceId,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -405,15 +396,14 @@ export function useGroupCall(
|
||||
);
|
||||
|
||||
updateState({
|
||||
error: null,
|
||||
error: undefined,
|
||||
state: groupCall.state,
|
||||
localCallFeed: groupCall.localCallFeed,
|
||||
activeSpeaker: groupCall.activeSpeaker ?? null,
|
||||
activeSpeaker: groupCall.activeSpeaker,
|
||||
userMediaFeeds: [...groupCall.userMediaFeeds],
|
||||
microphoneMuted: groupCall.isMicrophoneMuted(),
|
||||
localVideoMuted: groupCall.isLocalVideoMuted(),
|
||||
isScreensharing: groupCall.isScreensharing(),
|
||||
localDesktopCapturerSourceId: groupCall.localDesktopCapturerSourceId,
|
||||
screenshareFeeds: [...groupCall.screenshareFeeds],
|
||||
participants: getParticipants(groupCall),
|
||||
hasLocalParticipant: groupCall.hasLocalParticipant(),
|
||||
@@ -516,7 +506,7 @@ export function useGroupCall(
|
||||
}, [groupCall]);
|
||||
|
||||
const setMicrophoneMuted = useCallback(
|
||||
(setMuted) => {
|
||||
(setMuted: boolean) => {
|
||||
groupCall.setMicrophoneMuted(setMuted);
|
||||
groupCallOTelMembership?.onSetMicrophoneMuted(setMuted);
|
||||
PosthogAnalytics.instance.eventMuteMicrophone.track(
|
||||
@@ -575,7 +565,7 @@ export function useGroupCall(
|
||||
desktopCapturerSourceId: data.desktopCapturerSourceId as string,
|
||||
audio: !data.desktopCapturerSourceId,
|
||||
});
|
||||
await widget.api.transport.reply(ev.detail, {});
|
||||
await widget?.api.transport.reply(ev.detail, {});
|
||||
},
|
||||
[groupCall, updateState]
|
||||
);
|
||||
@@ -584,7 +574,7 @@ export function useGroupCall(
|
||||
async (ev: CustomEvent<IWidgetApiRequest>) => {
|
||||
updateState({ requestingScreenshare: false });
|
||||
await groupCall.setScreensharingEnabled(false);
|
||||
await widget.api.transport.reply(ev.detail, {});
|
||||
await widget?.api.transport.reply(ev.detail, {});
|
||||
},
|
||||
[groupCall, updateState]
|
||||
);
|
||||
@@ -601,11 +591,11 @@ export function useGroupCall(
|
||||
);
|
||||
|
||||
return () => {
|
||||
widget.lazyActions.off(
|
||||
widget?.lazyActions.off(
|
||||
ElementWidgetActions.ScreenshareStart,
|
||||
onScreenshareStart
|
||||
);
|
||||
widget.lazyActions.off(
|
||||
widget?.lazyActions.off(
|
||||
ElementWidgetActions.ScreenshareStop,
|
||||
onScreenshareStop
|
||||
);
|
||||
@@ -644,7 +634,6 @@ export function useGroupCall(
|
||||
requestingScreenshare,
|
||||
isScreensharing,
|
||||
screenshareFeeds,
|
||||
localDesktopCapturerSourceId,
|
||||
participants,
|
||||
hasLocalParticipant,
|
||||
unencryptedEventsFromUsers,
|
||||
|
||||
@@ -34,8 +34,26 @@ import { widget } from "../widget";
|
||||
|
||||
const STATS_COLLECT_INTERVAL_TIME_MS = 10000;
|
||||
|
||||
export type GroupCallLoaded = {
|
||||
kind: "loaded";
|
||||
groupCall: GroupCall;
|
||||
};
|
||||
|
||||
export type GroupCallLoadFailed = {
|
||||
kind: "failed";
|
||||
error: Error;
|
||||
};
|
||||
|
||||
export type GroupCallLoading = {
|
||||
kind: "loading";
|
||||
};
|
||||
|
||||
export type GroupCallStatus =
|
||||
| GroupCallLoaded
|
||||
| GroupCallLoadFailed
|
||||
| GroupCallLoading;
|
||||
|
||||
export interface GroupCallLoadState {
|
||||
loading: boolean;
|
||||
error?: Error;
|
||||
groupCall?: GroupCall;
|
||||
}
|
||||
@@ -45,13 +63,11 @@ export const useLoadGroupCall = (
|
||||
roomIdOrAlias: string,
|
||||
viaServers: string[],
|
||||
createPtt: boolean
|
||||
): GroupCallLoadState => {
|
||||
): GroupCallStatus => {
|
||||
const { t } = useTranslation();
|
||||
const [state, setState] = useState<GroupCallLoadState>({ loading: true });
|
||||
const [state, setState] = useState<GroupCallStatus>({ kind: "loading" });
|
||||
|
||||
useEffect(() => {
|
||||
setState({ loading: true });
|
||||
|
||||
const fetchOrCreateRoom = async (): Promise<Room> => {
|
||||
try {
|
||||
// We lowercase the localpart when we create the room, so we must lowercase
|
||||
@@ -74,8 +90,14 @@ export const useLoadGroupCall = (
|
||||
} catch (error) {
|
||||
if (
|
||||
isLocalRoomId(roomIdOrAlias, client) &&
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
(error.errcode === "M_NOT_FOUND" ||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
(error.message &&
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
error.message.indexOf("Failed to fetch alias") !== -1))
|
||||
) {
|
||||
// The room doesn't exist, but we can create it
|
||||
@@ -86,7 +108,7 @@ export const useLoadGroupCall = (
|
||||
);
|
||||
// likewise, wait for the room
|
||||
await client.waitUntilRoomReadyForGroupCalls(roomId);
|
||||
return client.getRoom(roomId);
|
||||
return client.getRoom(roomId)!;
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
@@ -170,12 +192,8 @@ export const useLoadGroupCall = (
|
||||
|
||||
waitForClientSyncing()
|
||||
.then(fetchOrCreateGroupCall)
|
||||
.then((groupCall) =>
|
||||
setState((prevState) => ({ ...prevState, loading: false, groupCall }))
|
||||
)
|
||||
.catch((error) =>
|
||||
setState((prevState) => ({ ...prevState, loading: false, error }))
|
||||
);
|
||||
.then((groupCall) => setState({ kind: "loaded", groupCall }))
|
||||
.catch((error) => setState({ kind: "failed", error }));
|
||||
}, [client, roomIdOrAlias, viaServers, createPtt, t]);
|
||||
|
||||
return state;
|
||||
|
||||
@@ -55,14 +55,22 @@ export function usePageUnload(callback: () => void) {
|
||||
// iOS doesn't fire beforeunload event, so leave the call when you hide the page.
|
||||
if (isIOS()) {
|
||||
window.addEventListener("pagehide", onBeforeUnload);
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
document.addEventListener("visibilitychange", onBeforeUnload);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
window.addEventListener("beforeunload", onBeforeUnload);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("pagehide", onBeforeUnload);
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
document.removeEventListener("visibilitychange", onBeforeUnload);
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
window.removeEventListener("beforeunload", onBeforeUnload);
|
||||
clearTimeout(pageVisibilityTimeout);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user