Call guest access link creation to join calls as a non registered user via the EC SPA (#12259)
* Add externall call link button if in public call room Signed-off-by: Timo K <toger5@hotmail.de> * Allow configuring a spa homeserver url. Signed-off-by: Timo K <toger5@hotmail.de> * temp Signed-off-by: Timo K <toger5@hotmail.de> * remove homeserver url Signed-off-by: Timo K <toger5@hotmail.de> * Add custom title to share dialog. So that we can use it as a "share call" dialog. Signed-off-by: Timo K <toger5@hotmail.de> * - rename config options - only show link button if a guest url is provided - share dialog custom Title - rename call share labels Signed-off-by: Timo K <toger5@hotmail.de> * rename to title_link Signed-off-by: Timo K <toger5@hotmail.de> * add tests for ShareDialog Signed-off-by: Timo K <toger5@hotmail.de> * add tests for share call button Signed-off-by: Timo K <toger5@hotmail.de> * review Signed-off-by: Timo K <toger5@hotmail.de> * remove comment Signed-off-by: Timo K <toger5@hotmail.de> * Update src/components/views/dialogs/ShareDialog.tsx Co-authored-by: David Baker <dbkr@users.noreply.github.com> --------- Signed-off-by: Timo K <toger5@hotmail.de> Co-authored-by: David Baker <dbkr@users.noreply.github.com>
This commit is contained in:
parent
af51897889
commit
70365c891b
7 changed files with 323 additions and 12 deletions
|
@ -119,6 +119,7 @@ export interface IConfigOptions {
|
||||||
};
|
};
|
||||||
element_call: {
|
element_call: {
|
||||||
url?: string;
|
url?: string;
|
||||||
|
guest_spa_url?: string;
|
||||||
use_exclusively?: boolean;
|
use_exclusively?: boolean;
|
||||||
participant_limit?: number;
|
participant_limit?: number;
|
||||||
brand?: string;
|
brand?: string;
|
||||||
|
|
|
@ -62,11 +62,28 @@ const socials = [
|
||||||
];
|
];
|
||||||
|
|
||||||
interface BaseProps {
|
interface BaseProps {
|
||||||
|
/**
|
||||||
|
* A function that is called when the dialog is dismissed
|
||||||
|
*/
|
||||||
onFinished(): void;
|
onFinished(): void;
|
||||||
|
/**
|
||||||
|
* An optional string to use as the dialog title.
|
||||||
|
* If not provided, an appropriate title for the target type will be used.
|
||||||
|
*/
|
||||||
|
customTitle?: string;
|
||||||
|
/**
|
||||||
|
* An optional string to use as the dialog subtitle
|
||||||
|
*/
|
||||||
|
subtitle?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Props extends BaseProps {
|
interface Props extends BaseProps {
|
||||||
target: Room | User | RoomMember;
|
/**
|
||||||
|
* The target to link to.
|
||||||
|
* This can be a Room, User, RoomMember, or MatrixEvent or an already computed URL.
|
||||||
|
* A <u>matrix.to</u> link will be generated out of it if it's not already a url.
|
||||||
|
*/
|
||||||
|
target: Room | User | RoomMember | URL;
|
||||||
permalinkCreator?: RoomPermalinkCreator;
|
permalinkCreator?: RoomPermalinkCreator;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -109,7 +126,9 @@ export default class ShareDialog extends React.PureComponent<XOR<Props, EventPro
|
||||||
};
|
};
|
||||||
|
|
||||||
private getUrl(): string {
|
private getUrl(): string {
|
||||||
if (this.props.target instanceof Room) {
|
if (this.props.target instanceof URL) {
|
||||||
|
return this.props.target.toString();
|
||||||
|
} else if (this.props.target instanceof Room) {
|
||||||
if (this.state.linkSpecificEvent) {
|
if (this.state.linkSpecificEvent) {
|
||||||
const events = this.props.target.getLiveTimeline().getEvents();
|
const events = this.props.target.getLiveTimeline().getEvents();
|
||||||
return this.state.permalinkCreator!.forEvent(events[events.length - 1].getId()!);
|
return this.state.permalinkCreator!.forEvent(events[events.length - 1].getId()!);
|
||||||
|
@ -129,8 +148,10 @@ export default class ShareDialog extends React.PureComponent<XOR<Props, EventPro
|
||||||
let title: string | undefined;
|
let title: string | undefined;
|
||||||
let checkbox: JSX.Element | undefined;
|
let checkbox: JSX.Element | undefined;
|
||||||
|
|
||||||
if (this.props.target instanceof Room) {
|
if (this.props.target instanceof URL) {
|
||||||
title = _t("share|title_room");
|
title = this.props.customTitle ?? _t("share|title_link");
|
||||||
|
} else if (this.props.target instanceof Room) {
|
||||||
|
title = this.props.customTitle ?? _t("share|title_room");
|
||||||
|
|
||||||
const events = this.props.target.getLiveTimeline().getEvents();
|
const events = this.props.target.getLiveTimeline().getEvents();
|
||||||
if (events.length > 0) {
|
if (events.length > 0) {
|
||||||
|
@ -146,9 +167,9 @@ export default class ShareDialog extends React.PureComponent<XOR<Props, EventPro
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else if (this.props.target instanceof User || this.props.target instanceof RoomMember) {
|
} else if (this.props.target instanceof User || this.props.target instanceof RoomMember) {
|
||||||
title = _t("share|title_user");
|
title = this.props.customTitle ?? _t("share|title_user");
|
||||||
} else if (this.props.target instanceof MatrixEvent) {
|
} else if (this.props.target instanceof MatrixEvent) {
|
||||||
title = _t("share|title_message");
|
title = this.props.customTitle ?? _t("share|title_message");
|
||||||
checkbox = (
|
checkbox = (
|
||||||
<div>
|
<div>
|
||||||
<StyledCheckbox
|
<StyledCheckbox
|
||||||
|
@ -206,6 +227,7 @@ export default class ShareDialog extends React.PureComponent<XOR<Props, EventPro
|
||||||
contentId="mx_Dialog_content"
|
contentId="mx_Dialog_content"
|
||||||
onFinished={this.props.onFinished}
|
onFinished={this.props.onFinished}
|
||||||
>
|
>
|
||||||
|
{this.props.subtitle && <p>{this.props.subtitle}</p>}
|
||||||
<div className="mx_ShareDialog_content">
|
<div className="mx_ShareDialog_content">
|
||||||
<CopyableText getTextToCopy={() => matrixToUrl}>
|
<CopyableText getTextToCopy={() => matrixToUrl}>
|
||||||
<a title={_t("share|link_title")} href={matrixToUrl} onClick={ShareDialog.onLinkClick}>
|
<a title={_t("share|link_title")} href={matrixToUrl} onClick={ShareDialog.onLinkClick}>
|
||||||
|
|
|
@ -18,6 +18,7 @@ import React, { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import { Body as BodyText, Button, IconButton, Menu, MenuItem, Tooltip } from "@vector-im/compound-web";
|
import { Body as BodyText, Button, IconButton, Menu, MenuItem, Tooltip } from "@vector-im/compound-web";
|
||||||
import { Icon as VideoCallIcon } from "@vector-im/compound-design-tokens/icons/video-call-solid.svg";
|
import { Icon as VideoCallIcon } from "@vector-im/compound-design-tokens/icons/video-call-solid.svg";
|
||||||
import { Icon as VoiceCallIcon } from "@vector-im/compound-design-tokens/icons/voice-call.svg";
|
import { Icon as VoiceCallIcon } from "@vector-im/compound-design-tokens/icons/voice-call.svg";
|
||||||
|
import { Icon as ExternalLinkIcon } from "@vector-im/compound-design-tokens/icons/link.svg";
|
||||||
import { Icon as CloseCallIcon } from "@vector-im/compound-design-tokens/icons/close.svg";
|
import { Icon as CloseCallIcon } from "@vector-im/compound-design-tokens/icons/close.svg";
|
||||||
import { Icon as ThreadsIcon } from "@vector-im/compound-design-tokens/icons/threads-solid.svg";
|
import { Icon as ThreadsIcon } from "@vector-im/compound-design-tokens/icons/threads-solid.svg";
|
||||||
import { Icon as NotificationsIcon } from "@vector-im/compound-design-tokens/icons/notifications-solid.svg";
|
import { Icon as NotificationsIcon } from "@vector-im/compound-design-tokens/icons/notifications-solid.svg";
|
||||||
|
@ -26,6 +27,7 @@ import { Icon as ErrorIcon } from "@vector-im/compound-design-tokens/icons/error
|
||||||
import { Icon as PublicIcon } from "@vector-im/compound-design-tokens/icons/public.svg";
|
import { Icon as PublicIcon } from "@vector-im/compound-design-tokens/icons/public.svg";
|
||||||
import { EventType, JoinRule, type Room } from "matrix-js-sdk/src/matrix";
|
import { EventType, JoinRule, type Room } from "matrix-js-sdk/src/matrix";
|
||||||
import { ViewRoomOpts } from "@matrix-org/react-sdk-module-api/lib/lifecycles/RoomViewLifecycle";
|
import { ViewRoomOpts } from "@matrix-org/react-sdk-module-api/lib/lifecycles/RoomViewLifecycle";
|
||||||
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
|
|
||||||
import { useRoomName } from "../../../hooks/useRoomName";
|
import { useRoomName } from "../../../hooks/useRoomName";
|
||||||
import { RightPanelPhases } from "../../../stores/right-panel/RightPanelStorePhases";
|
import { RightPanelPhases } from "../../../stores/right-panel/RightPanelStorePhases";
|
||||||
|
@ -54,6 +56,8 @@ import { VideoRoomChatButton } from "./RoomHeader/VideoRoomChatButton";
|
||||||
import { RoomKnocksBar } from "./RoomKnocksBar";
|
import { RoomKnocksBar } from "./RoomKnocksBar";
|
||||||
import { isVideoRoom } from "../../../utils/video-rooms";
|
import { isVideoRoom } from "../../../utils/video-rooms";
|
||||||
import { notificationLevelToIndicator } from "../../../utils/notifications";
|
import { notificationLevelToIndicator } from "../../../utils/notifications";
|
||||||
|
import Modal from "../../../Modal";
|
||||||
|
import ShareDialog from "../dialogs/ShareDialog";
|
||||||
|
|
||||||
export default function RoomHeader({
|
export default function RoomHeader({
|
||||||
room,
|
room,
|
||||||
|
@ -78,6 +82,8 @@ export default function RoomHeader({
|
||||||
videoCallClick,
|
videoCallClick,
|
||||||
toggleCallMaximized: toggleCall,
|
toggleCallMaximized: toggleCall,
|
||||||
isViewingCall,
|
isViewingCall,
|
||||||
|
generateCallLink,
|
||||||
|
canGenerateCallLink,
|
||||||
isConnectedToCall,
|
isConnectedToCall,
|
||||||
hasActiveCallSession,
|
hasActiveCallSession,
|
||||||
callOptions,
|
callOptions,
|
||||||
|
@ -118,6 +124,20 @@ export default function RoomHeader({
|
||||||
|
|
||||||
const videoClick = useCallback((ev) => videoCallClick(ev, callOptions[0]), [callOptions, videoCallClick]);
|
const videoClick = useCallback((ev) => videoCallClick(ev, callOptions[0]), [callOptions, videoCallClick]);
|
||||||
|
|
||||||
|
const shareClick = useCallback(() => {
|
||||||
|
try {
|
||||||
|
// generateCallLink throws if the permissions are not met
|
||||||
|
const target = generateCallLink();
|
||||||
|
Modal.createDialog(ShareDialog, {
|
||||||
|
target,
|
||||||
|
customTitle: _t("share|share_call"),
|
||||||
|
subtitle: _t("share|share_call_subtitle"),
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
logger.error("Could not generate call link.", e);
|
||||||
|
}
|
||||||
|
}, [generateCallLink]);
|
||||||
|
|
||||||
const toggleCallButton = (
|
const toggleCallButton = (
|
||||||
<Tooltip label={isViewingCall ? _t("voip|minimise_call") : _t("voip|maximise_call")}>
|
<Tooltip label={isViewingCall ? _t("voip|minimise_call") : _t("voip|maximise_call")}>
|
||||||
<IconButton onClick={toggleCall}>
|
<IconButton onClick={toggleCall}>
|
||||||
|
@ -125,7 +145,13 @@ export default function RoomHeader({
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
|
const createExternalLinkButton = (
|
||||||
|
<Tooltip label={_t("voip|get_call_link")}>
|
||||||
|
<IconButton onClick={shareClick} aria-label={_t("voip|get_call_link")}>
|
||||||
|
<ExternalLinkIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
const joinCallButton = (
|
const joinCallButton = (
|
||||||
<Tooltip label={videoCallDisabledReason ?? _t("voip|video_call")}>
|
<Tooltip label={videoCallDisabledReason ?? _t("voip|video_call")}>
|
||||||
<Button
|
<Button
|
||||||
|
@ -309,7 +335,7 @@ export default function RoomHeader({
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
{isViewingCall && canGenerateCallLink && createExternalLinkButton}
|
||||||
{((isConnectedToCall && isViewingCall) || isVideoRoom(room)) && <VideoRoomChatButton room={room} />}
|
{((isConnectedToCall && isViewingCall) || isVideoRoom(room)) && <VideoRoomChatButton room={room} />}
|
||||||
|
|
||||||
{hasActiveCallSession && !isConnectedToCall && !isViewingCall ? (
|
{hasActiveCallSession && !isConnectedToCall && !isViewingCall ? (
|
||||||
|
|
|
@ -14,9 +14,10 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Room } from "matrix-js-sdk/src/matrix";
|
import { JoinRule, Room } from "matrix-js-sdk/src/matrix";
|
||||||
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import { CallType } from "matrix-js-sdk/src/webrtc/call";
|
import { CallType } from "matrix-js-sdk/src/webrtc/call";
|
||||||
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
|
|
||||||
import { useFeatureEnabled } from "../useSettings";
|
import { useFeatureEnabled } from "../useSettings";
|
||||||
import SdkConfig from "../../SdkConfig";
|
import SdkConfig from "../../SdkConfig";
|
||||||
|
@ -39,6 +40,7 @@ import defaultDispatcher from "../../dispatcher/dispatcher";
|
||||||
import { ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload";
|
import { ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload";
|
||||||
import { Action } from "../../dispatcher/actions";
|
import { Action } from "../../dispatcher/actions";
|
||||||
import { CallStore, CallStoreEvent } from "../../stores/CallStore";
|
import { CallStore, CallStoreEvent } from "../../stores/CallStore";
|
||||||
|
import { calculateRoomVia } from "../../utils/permalinks/Permalinks";
|
||||||
|
|
||||||
export enum PlatformCallType {
|
export enum PlatformCallType {
|
||||||
ElementCall,
|
ElementCall,
|
||||||
|
@ -78,27 +80,35 @@ export const useRoomCall = (
|
||||||
videoCallClick(evt: React.MouseEvent | undefined, selectedType: PlatformCallType): void;
|
videoCallClick(evt: React.MouseEvent | undefined, selectedType: PlatformCallType): void;
|
||||||
toggleCallMaximized: () => void;
|
toggleCallMaximized: () => void;
|
||||||
isViewingCall: boolean;
|
isViewingCall: boolean;
|
||||||
|
generateCallLink: () => URL;
|
||||||
|
canGenerateCallLink: boolean;
|
||||||
isConnectedToCall: boolean;
|
isConnectedToCall: boolean;
|
||||||
hasActiveCallSession: boolean;
|
hasActiveCallSession: boolean;
|
||||||
callOptions: PlatformCallType[];
|
callOptions: PlatformCallType[];
|
||||||
} => {
|
} => {
|
||||||
|
// settings
|
||||||
const groupCallsEnabled = useFeatureEnabled("feature_group_calls");
|
const groupCallsEnabled = useFeatureEnabled("feature_group_calls");
|
||||||
const useElementCallExclusively = useMemo(() => {
|
const useElementCallExclusively = useMemo(() => {
|
||||||
return SdkConfig.get("element_call").use_exclusively;
|
return SdkConfig.get("element_call").use_exclusively;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const guestSpaUrl = useMemo(() => {
|
||||||
|
return SdkConfig.get("element_call").guest_spa_url;
|
||||||
|
}, []);
|
||||||
|
|
||||||
const hasLegacyCall = useEventEmitterState(
|
const hasLegacyCall = useEventEmitterState(
|
||||||
LegacyCallHandler.instance,
|
LegacyCallHandler.instance,
|
||||||
LegacyCallHandlerEvent.CallsChanged,
|
LegacyCallHandlerEvent.CallsChanged,
|
||||||
() => LegacyCallHandler.instance.getCallForRoom(room.roomId) !== null,
|
() => LegacyCallHandler.instance.getCallForRoom(room.roomId) !== null,
|
||||||
);
|
);
|
||||||
|
// settings
|
||||||
const widgets = useWidgets(room);
|
const widgets = useWidgets(room);
|
||||||
const jitsiWidget = useMemo(() => widgets.find((widget) => WidgetType.JITSI.matches(widget.type)), [widgets]);
|
const jitsiWidget = useMemo(() => widgets.find((widget) => WidgetType.JITSI.matches(widget.type)), [widgets]);
|
||||||
const hasJitsiWidget = !!jitsiWidget;
|
const hasJitsiWidget = !!jitsiWidget;
|
||||||
const managedHybridWidget = useMemo(() => widgets.find(isManagedHybridWidget), [widgets]);
|
const managedHybridWidget = useMemo(() => widgets.find(isManagedHybridWidget), [widgets]);
|
||||||
const hasManagedHybridWidget = !!managedHybridWidget;
|
const hasManagedHybridWidget = !!managedHybridWidget;
|
||||||
|
|
||||||
|
// group call
|
||||||
const groupCall = useCall(room.roomId);
|
const groupCall = useCall(room.roomId);
|
||||||
const isConnectedToCall = useConnectionState(groupCall) === ConnectionState.Connected;
|
const isConnectedToCall = useConnectionState(groupCall) === ConnectionState.Connected;
|
||||||
const hasGroupCall = groupCall !== null;
|
const hasGroupCall = groupCall !== null;
|
||||||
|
@ -107,11 +117,14 @@ export const useRoomCall = (
|
||||||
SdkContextClass.instance.roomViewStore.isViewingCall(),
|
SdkContextClass.instance.roomViewStore.isViewingCall(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// room
|
||||||
const memberCount = useRoomMemberCount(room);
|
const memberCount = useRoomMemberCount(room);
|
||||||
|
|
||||||
const [mayEditWidgets, mayCreateElementCalls] = useRoomState(room, () => [
|
const [mayEditWidgets, mayCreateElementCalls, canJoinWithoutInvite] = useRoomState(room, () => [
|
||||||
room.currentState.mayClientSendStateEvent("im.vector.modular.widgets", room.client),
|
room.currentState.mayClientSendStateEvent("im.vector.modular.widgets", room.client),
|
||||||
room.currentState.mayClientSendStateEvent(ElementCall.MEMBER_EVENT_TYPE.name, room.client),
|
room.currentState.mayClientSendStateEvent(ElementCall.MEMBER_EVENT_TYPE.name, room.client),
|
||||||
|
room.getJoinRule() === "public" || room.getJoinRule() === JoinRule.Knock,
|
||||||
|
/*|| room.getJoinRule() === JoinRule.Restricted <- rule for joining via token?*/
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// The options provided to the RoomHeader.
|
// The options provided to the RoomHeader.
|
||||||
|
@ -131,7 +144,7 @@ export const useRoomCall = (
|
||||||
return [PlatformCallType.ElementCall];
|
return [PlatformCallType.ElementCall];
|
||||||
}
|
}
|
||||||
if (hasGroupCall && WidgetType.CALL.matches(groupCall.widget.type)) {
|
if (hasGroupCall && WidgetType.CALL.matches(groupCall.widget.type)) {
|
||||||
// only allow joining joining the ongoing Element call if there is one.
|
// only allow joining the ongoing Element call if there is one.
|
||||||
return [PlatformCallType.ElementCall];
|
return [PlatformCallType.ElementCall];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -258,6 +271,26 @@ export const useRoomCall = (
|
||||||
});
|
});
|
||||||
}, [isViewingCall, room.roomId]);
|
}, [isViewingCall, room.roomId]);
|
||||||
|
|
||||||
|
const generateCallLink = useCallback(() => {
|
||||||
|
if (!canJoinWithoutInvite)
|
||||||
|
throw new Error("Cannot create link for room that users can not join without invite.");
|
||||||
|
if (!guestSpaUrl) throw new Error("No guest SPA url for external links provided.");
|
||||||
|
const url = new URL(guestSpaUrl);
|
||||||
|
url.pathname = "/room/";
|
||||||
|
// Set params for the sharable url
|
||||||
|
url.searchParams.set("roomId", room.roomId);
|
||||||
|
url.searchParams.set("perParticipantE2EE", "true");
|
||||||
|
for (const server of calculateRoomVia(room)) {
|
||||||
|
url.searchParams.set("viaServers", server);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move params into hash
|
||||||
|
url.hash = "/" + room.name + url.search;
|
||||||
|
url.search = "";
|
||||||
|
|
||||||
|
logger.info("Generated element call external url:", url);
|
||||||
|
return url;
|
||||||
|
}, [canJoinWithoutInvite, guestSpaUrl, room]);
|
||||||
/**
|
/**
|
||||||
* We've gone through all the steps
|
* We've gone through all the steps
|
||||||
*/
|
*/
|
||||||
|
@ -268,6 +301,8 @@ export const useRoomCall = (
|
||||||
videoCallClick,
|
videoCallClick,
|
||||||
toggleCallMaximized: toggleCallMaximized,
|
toggleCallMaximized: toggleCallMaximized,
|
||||||
isViewingCall: isViewingCall,
|
isViewingCall: isViewingCall,
|
||||||
|
generateCallLink,
|
||||||
|
canGenerateCallLink: guestSpaUrl !== undefined && canJoinWithoutInvite,
|
||||||
isConnectedToCall: isConnectedToCall,
|
isConnectedToCall: isConnectedToCall,
|
||||||
hasActiveCallSession: hasActiveCallSession,
|
hasActiveCallSession: hasActiveCallSession,
|
||||||
callOptions,
|
callOptions,
|
||||||
|
|
|
@ -2896,6 +2896,9 @@
|
||||||
"link_title": "Link to room",
|
"link_title": "Link to room",
|
||||||
"permalink_message": "Link to selected message",
|
"permalink_message": "Link to selected message",
|
||||||
"permalink_most_recent": "Link to most recent message",
|
"permalink_most_recent": "Link to most recent message",
|
||||||
|
"share_call": "Conference invite link",
|
||||||
|
"share_call_subtitle": "Link for external users to join the call without a matrix account:",
|
||||||
|
"title_link": "Share Link",
|
||||||
"title_message": "Share Room Message",
|
"title_message": "Share Room Message",
|
||||||
"title_room": "Share Room",
|
"title_room": "Share Room",
|
||||||
"title_user": "Share User"
|
"title_user": "Share User"
|
||||||
|
@ -3828,6 +3831,7 @@
|
||||||
"expand": "Return to call",
|
"expand": "Return to call",
|
||||||
"failed_call_live_broadcast_description": "You can’t start a call as you are currently recording a live broadcast. Please end your live broadcast in order to start a call.",
|
"failed_call_live_broadcast_description": "You can’t start a call as you are currently recording a live broadcast. Please end your live broadcast in order to start a call.",
|
||||||
"failed_call_live_broadcast_title": "Can’t start a call",
|
"failed_call_live_broadcast_title": "Can’t start a call",
|
||||||
|
"get_call_link": "Share call link",
|
||||||
"hangup": "Hangup",
|
"hangup": "Hangup",
|
||||||
"hide_sidebar_button": "Hide sidebar",
|
"hide_sidebar_button": "Hide sidebar",
|
||||||
"input_devices": "Input devices",
|
"input_devices": "Input devices",
|
||||||
|
|
130
test/components/views/dialogs/ShareDialog-test.tsx
Normal file
130
test/components/views/dialogs/ShareDialog-test.tsx
Normal file
|
@ -0,0 +1,130 @@
|
||||||
|
/*
|
||||||
|
Copyright 2024 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import { EventTimeline, MatrixEvent, Room, RoomMember } from "matrix-js-sdk/src/matrix";
|
||||||
|
import { render, RenderOptions } from "@testing-library/react";
|
||||||
|
import { TooltipProvider } from "@vector-im/compound-web";
|
||||||
|
|
||||||
|
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
|
||||||
|
import SettingsStore from "../../../../src/settings/SettingsStore";
|
||||||
|
import MatrixClientContext from "../../../../src/contexts/MatrixClientContext";
|
||||||
|
import { _t } from "../../../../src/languageHandler";
|
||||||
|
import ShareDialog from "../../../../src/components/views/dialogs/ShareDialog";
|
||||||
|
import { UIFeature } from "../../../../src/settings/UIFeature";
|
||||||
|
import { stubClient } from "../../../test-utils";
|
||||||
|
jest.mock("../../../../src/utils/ShieldUtils");
|
||||||
|
|
||||||
|
function getWrapper(): RenderOptions {
|
||||||
|
return {
|
||||||
|
wrapper: ({ children }) => (
|
||||||
|
<TooltipProvider>
|
||||||
|
<MatrixClientContext.Provider value={MatrixClientPeg.safeGet()}>
|
||||||
|
{children}
|
||||||
|
</MatrixClientContext.Provider>
|
||||||
|
</TooltipProvider>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("ShareDialog", () => {
|
||||||
|
let room: Room;
|
||||||
|
|
||||||
|
const ROOM_ID = "!1:example.org";
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
stubClient();
|
||||||
|
room = new Room(ROOM_ID, MatrixClientPeg.get()!, "@alice:example.org");
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders room share dialog", () => {
|
||||||
|
const { container: withoutEvents } = render(<ShareDialog target={room} onFinished={jest.fn()} />, getWrapper());
|
||||||
|
expect(withoutEvents).toHaveTextContent(_t("share|title_room"));
|
||||||
|
|
||||||
|
jest.spyOn(room, "getLiveTimeline").mockReturnValue({ getEvents: () => [{} as MatrixEvent] } as EventTimeline);
|
||||||
|
const { container: withEvents } = render(<ShareDialog target={room} onFinished={jest.fn()} />, getWrapper());
|
||||||
|
expect(withEvents).toHaveTextContent(_t("share|permalink_most_recent"));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders user share dialog", () => {
|
||||||
|
mockRoomMembers(room, 1);
|
||||||
|
const { container } = render(
|
||||||
|
<ShareDialog target={room.getJoinedMembers()[0]} onFinished={jest.fn()} />,
|
||||||
|
getWrapper(),
|
||||||
|
);
|
||||||
|
expect(container).toHaveTextContent(_t("share|title_user"));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders link share dialog", () => {
|
||||||
|
mockRoomMembers(room, 1);
|
||||||
|
const { container } = render(
|
||||||
|
<ShareDialog target={new URL("https://matrix.org")} onFinished={jest.fn()} />,
|
||||||
|
getWrapper(),
|
||||||
|
);
|
||||||
|
expect(container).toHaveTextContent(_t("share|title_link"));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders the QR code if configured", () => {
|
||||||
|
const originalGetValue = SettingsStore.getValue;
|
||||||
|
jest.spyOn(SettingsStore, "getValue").mockImplementation((feature) => {
|
||||||
|
if (feature === UIFeature.ShareQRCode) return true;
|
||||||
|
return originalGetValue(feature);
|
||||||
|
});
|
||||||
|
const { container } = render(<ShareDialog target={room} onFinished={jest.fn()} />, getWrapper());
|
||||||
|
const qrCodesVisible = container.getElementsByClassName("mx_ShareDialog_qrcode_container").length > 0;
|
||||||
|
expect(qrCodesVisible).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders the social button if configured", () => {
|
||||||
|
const originalGetValue = SettingsStore.getValue;
|
||||||
|
jest.spyOn(SettingsStore, "getValue").mockImplementation((feature) => {
|
||||||
|
if (feature === UIFeature.ShareSocial) return true;
|
||||||
|
return originalGetValue(feature);
|
||||||
|
});
|
||||||
|
const { container } = render(<ShareDialog target={room} onFinished={jest.fn()} />, getWrapper());
|
||||||
|
const qrCodesVisible = container.getElementsByClassName("mx_ShareDialog_social_container").length > 0;
|
||||||
|
expect(qrCodesVisible).toBe(true);
|
||||||
|
});
|
||||||
|
it("renders custom title and subtitle", () => {
|
||||||
|
const { container } = render(
|
||||||
|
<ShareDialog
|
||||||
|
target={room}
|
||||||
|
customTitle="test_title_123"
|
||||||
|
subtitle="custom_subtitle_1234"
|
||||||
|
onFinished={jest.fn()}
|
||||||
|
/>,
|
||||||
|
getWrapper(),
|
||||||
|
);
|
||||||
|
expect(container).toHaveTextContent("test_title_123");
|
||||||
|
expect(container).toHaveTextContent("custom_subtitle_1234");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param count the number of users to create
|
||||||
|
*/
|
||||||
|
function mockRoomMembers(room: Room, count: number) {
|
||||||
|
const members = Array(count)
|
||||||
|
.fill(0)
|
||||||
|
.map((_, index) => new RoomMember(room.roomId, "@alice:example.org"));
|
||||||
|
|
||||||
|
room.currentState.setJoinedMemberCount(members.length);
|
||||||
|
room.getJoinedMembers = jest.fn().mockReturnValue(members);
|
||||||
|
}
|
|
@ -55,9 +55,12 @@ import { Call, ElementCall } from "../../../../src/models/Call";
|
||||||
import * as ShieldUtils from "../../../../src/utils/ShieldUtils";
|
import * as ShieldUtils from "../../../../src/utils/ShieldUtils";
|
||||||
import { Container, WidgetLayoutStore } from "../../../../src/stores/widgets/WidgetLayoutStore";
|
import { Container, WidgetLayoutStore } from "../../../../src/stores/widgets/WidgetLayoutStore";
|
||||||
import MatrixClientContext from "../../../../src/contexts/MatrixClientContext";
|
import MatrixClientContext from "../../../../src/contexts/MatrixClientContext";
|
||||||
|
import { _t } from "../../../../src/languageHandler";
|
||||||
import * as UseCall from "../../../../src/hooks/useCall";
|
import * as UseCall from "../../../../src/hooks/useCall";
|
||||||
import { SdkContextClass } from "../../../../src/contexts/SDKContext";
|
import { SdkContextClass } from "../../../../src/contexts/SDKContext";
|
||||||
import WidgetStore, { IApp } from "../../../../src/stores/WidgetStore";
|
import WidgetStore, { IApp } from "../../../../src/stores/WidgetStore";
|
||||||
|
import ShareDialog from "../../../../src/components/views/dialogs/ShareDialog";
|
||||||
|
import Modal from "../../../../src/Modal";
|
||||||
jest.mock("../../../../src/utils/ShieldUtils");
|
jest.mock("../../../../src/utils/ShieldUtils");
|
||||||
|
|
||||||
function getWrapper(): RenderOptions {
|
function getWrapper(): RenderOptions {
|
||||||
|
@ -491,6 +494,96 @@ describe("RoomHeader", () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("External conference", () => {
|
||||||
|
const oldGet = SdkConfig.get;
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.spyOn(SdkConfig, "get").mockImplementation((key) => {
|
||||||
|
if (key === "element_call") {
|
||||||
|
return { guest_spa_url: "https://guest_spa_url.com", url: "https://spa_url.com" };
|
||||||
|
}
|
||||||
|
return oldGet(key);
|
||||||
|
});
|
||||||
|
mockRoomMembers(room, 3);
|
||||||
|
jest.spyOn(SdkContextClass.instance.roomViewStore, "isViewingCall").mockReturnValue(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows the external conference if the room has public join rules", () => {
|
||||||
|
jest.spyOn(room, "getJoinRule").mockReturnValue(JoinRule.Public);
|
||||||
|
|
||||||
|
const { container } = render(<RoomHeader room={room} />, getWrapper());
|
||||||
|
expect(getByLabelText(container, _t("voip|get_call_link"))).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows the external conference if the room has Knock join rules", () => {
|
||||||
|
jest.spyOn(room, "getJoinRule").mockReturnValue(JoinRule.Knock);
|
||||||
|
|
||||||
|
const { container } = render(<RoomHeader room={room} />, getWrapper());
|
||||||
|
expect(getByLabelText(container, _t("voip|get_call_link"))).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("don't show external conference button if the call is not shown", () => {
|
||||||
|
jest.spyOn(room, "getJoinRule").mockReturnValue(JoinRule.Public);
|
||||||
|
jest.spyOn(SdkContextClass.instance.roomViewStore, "isViewingCall").mockReturnValue(false);
|
||||||
|
|
||||||
|
let { container } = render(<RoomHeader room={room} />, getWrapper());
|
||||||
|
expect(screen.queryByLabelText(_t("voip|get_call_link"))).not.toBeInTheDocument();
|
||||||
|
|
||||||
|
jest.spyOn(SdkContextClass.instance.roomViewStore, "isViewingCall").mockReturnValue(true);
|
||||||
|
|
||||||
|
container = render(<RoomHeader room={room} />, getWrapper()).container;
|
||||||
|
|
||||||
|
expect(getByLabelText(container, _t("voip|get_call_link"))).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("don't show external conference button if now guest spa link is configured", () => {
|
||||||
|
jest.spyOn(room, "getJoinRule").mockReturnValue(JoinRule.Public);
|
||||||
|
jest.spyOn(SdkContextClass.instance.roomViewStore, "isViewingCall").mockReturnValue(true);
|
||||||
|
|
||||||
|
jest.spyOn(SdkConfig, "get").mockImplementation((key) => {
|
||||||
|
if (key === "element_call") {
|
||||||
|
return { url: "https://example2.com" };
|
||||||
|
}
|
||||||
|
return oldGet(key);
|
||||||
|
});
|
||||||
|
|
||||||
|
render(<RoomHeader room={room} />, getWrapper());
|
||||||
|
|
||||||
|
// We only change the SdkConfig and show that this everything else is
|
||||||
|
// configured so that the call link button is shown.
|
||||||
|
|
||||||
|
jest.spyOn(SdkConfig, "get").mockImplementation((key) => {
|
||||||
|
if (key === "element_call") {
|
||||||
|
return { guest_spa_url: "https://guest_spa_url.com", url: "https://example2.com" };
|
||||||
|
}
|
||||||
|
return oldGet(key);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(screen.queryByLabelText(_t("voip|get_call_link"))).not.toBeInTheDocument();
|
||||||
|
const { container } = render(<RoomHeader room={room} />, getWrapper());
|
||||||
|
expect(getByLabelText(container, _t("voip|get_call_link"))).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
it("opens the share dialog with the correct share link", () => {
|
||||||
|
jest.spyOn(room, "getJoinRule").mockReturnValue(JoinRule.Public);
|
||||||
|
jest.spyOn(SdkContextClass.instance.roomViewStore, "isViewingCall").mockReturnValue(true);
|
||||||
|
|
||||||
|
const { container } = render(<RoomHeader room={room} />, getWrapper());
|
||||||
|
const modalSpy = jest.spyOn(Modal, "createDialog");
|
||||||
|
fireEvent.click(getByLabelText(container, _t("voip|get_call_link")));
|
||||||
|
const target =
|
||||||
|
"https://guest_spa_url.com/room/#/!1:example.org?roomId=%211%3Aexample.org&perParticipantE2EE=true&viaServers=example.org";
|
||||||
|
expect(modalSpy).toHaveBeenCalled();
|
||||||
|
const arg0 = modalSpy.mock.calls[0][0];
|
||||||
|
const arg1 = modalSpy.mock.calls[0][1] as any;
|
||||||
|
expect(arg0).toEqual(ShareDialog);
|
||||||
|
const { customTitle, subtitle } = arg1;
|
||||||
|
expect({ customTitle, subtitle }).toEqual({
|
||||||
|
customTitle: "Conference invite link",
|
||||||
|
subtitle: _t("share|share_call_subtitle"),
|
||||||
|
});
|
||||||
|
expect(arg1.target.toString()).toEqual(target);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("public room", () => {
|
describe("public room", () => {
|
||||||
it("shows a globe", () => {
|
it("shows a globe", () => {
|
||||||
const joinRuleEvent = new MatrixEvent({
|
const joinRuleEvent = new MatrixEvent({
|
||||||
|
|
Loading…
Reference in a new issue