Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.

Commit 70365c8

Browse files
toger5dbkr
andauthored
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 <[email protected]> * Allow configuring a spa homeserver url. Signed-off-by: Timo K <[email protected]> * temp Signed-off-by: Timo K <[email protected]> * remove homeserver url Signed-off-by: Timo K <[email protected]> * Add custom title to share dialog. So that we can use it as a "share call" dialog. Signed-off-by: Timo K <[email protected]> * - 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 <[email protected]> * rename to title_link Signed-off-by: Timo K <[email protected]> * add tests for ShareDialog Signed-off-by: Timo K <[email protected]> * add tests for share call button Signed-off-by: Timo K <[email protected]> * review Signed-off-by: Timo K <[email protected]> * remove comment Signed-off-by: Timo K <[email protected]> * Update src/components/views/dialogs/ShareDialog.tsx Co-authored-by: David Baker <[email protected]> --------- Signed-off-by: Timo K <[email protected]> Co-authored-by: David Baker <[email protected]>
1 parent af51897 commit 70365c8

File tree

7 files changed

+323
-12
lines changed

7 files changed

+323
-12
lines changed

src/IConfigOptions.ts

+1
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ export interface IConfigOptions {
119119
};
120120
element_call: {
121121
url?: string;
122+
guest_spa_url?: string;
122123
use_exclusively?: boolean;
123124
participant_limit?: number;
124125
brand?: string;

src/components/views/dialogs/ShareDialog.tsx

+28-6
Original file line numberDiff line numberDiff line change
@@ -62,11 +62,28 @@ const socials = [
6262
];
6363

6464
interface BaseProps {
65+
/**
66+
* A function that is called when the dialog is dismissed
67+
*/
6568
onFinished(): void;
69+
/**
70+
* An optional string to use as the dialog title.
71+
* If not provided, an appropriate title for the target type will be used.
72+
*/
73+
customTitle?: string;
74+
/**
75+
* An optional string to use as the dialog subtitle
76+
*/
77+
subtitle?: string;
6678
}
6779

6880
interface Props extends BaseProps {
69-
target: Room | User | RoomMember;
81+
/**
82+
* The target to link to.
83+
* This can be a Room, User, RoomMember, or MatrixEvent or an already computed URL.
84+
* A <u>matrix.to</u> link will be generated out of it if it's not already a url.
85+
*/
86+
target: Room | User | RoomMember | URL;
7087
permalinkCreator?: RoomPermalinkCreator;
7188
}
7289

@@ -109,7 +126,9 @@ export default class ShareDialog extends React.PureComponent<XOR<Props, EventPro
109126
};
110127

111128
private getUrl(): string {
112-
if (this.props.target instanceof Room) {
129+
if (this.props.target instanceof URL) {
130+
return this.props.target.toString();
131+
} else if (this.props.target instanceof Room) {
113132
if (this.state.linkSpecificEvent) {
114133
const events = this.props.target.getLiveTimeline().getEvents();
115134
return this.state.permalinkCreator!.forEvent(events[events.length - 1].getId()!);
@@ -129,8 +148,10 @@ export default class ShareDialog extends React.PureComponent<XOR<Props, EventPro
129148
let title: string | undefined;
130149
let checkbox: JSX.Element | undefined;
131150

132-
if (this.props.target instanceof Room) {
133-
title = _t("share|title_room");
151+
if (this.props.target instanceof URL) {
152+
title = this.props.customTitle ?? _t("share|title_link");
153+
} else if (this.props.target instanceof Room) {
154+
title = this.props.customTitle ?? _t("share|title_room");
134155

135156
const events = this.props.target.getLiveTimeline().getEvents();
136157
if (events.length > 0) {
@@ -146,9 +167,9 @@ export default class ShareDialog extends React.PureComponent<XOR<Props, EventPro
146167
);
147168
}
148169
} else if (this.props.target instanceof User || this.props.target instanceof RoomMember) {
149-
title = _t("share|title_user");
170+
title = this.props.customTitle ?? _t("share|title_user");
150171
} else if (this.props.target instanceof MatrixEvent) {
151-
title = _t("share|title_message");
172+
title = this.props.customTitle ?? _t("share|title_message");
152173
checkbox = (
153174
<div>
154175
<StyledCheckbox
@@ -206,6 +227,7 @@ export default class ShareDialog extends React.PureComponent<XOR<Props, EventPro
206227
contentId="mx_Dialog_content"
207228
onFinished={this.props.onFinished}
208229
>
230+
{this.props.subtitle && <p>{this.props.subtitle}</p>}
209231
<div className="mx_ShareDialog_content">
210232
<CopyableText getTextToCopy={() => matrixToUrl}>
211233
<a title={_t("share|link_title")} href={matrixToUrl} onClick={ShareDialog.onLinkClick}>

src/components/views/rooms/RoomHeader.tsx

+28-2
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import React, { useCallback, useEffect, useMemo, useState } from "react";
1818
import { Body as BodyText, Button, IconButton, Menu, MenuItem, Tooltip } from "@vector-im/compound-web";
1919
import { Icon as VideoCallIcon } from "@vector-im/compound-design-tokens/icons/video-call-solid.svg";
2020
import { Icon as VoiceCallIcon } from "@vector-im/compound-design-tokens/icons/voice-call.svg";
21+
import { Icon as ExternalLinkIcon } from "@vector-im/compound-design-tokens/icons/link.svg";
2122
import { Icon as CloseCallIcon } from "@vector-im/compound-design-tokens/icons/close.svg";
2223
import { Icon as ThreadsIcon } from "@vector-im/compound-design-tokens/icons/threads-solid.svg";
2324
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
2627
import { Icon as PublicIcon } from "@vector-im/compound-design-tokens/icons/public.svg";
2728
import { EventType, JoinRule, type Room } from "matrix-js-sdk/src/matrix";
2829
import { ViewRoomOpts } from "@matrix-org/react-sdk-module-api/lib/lifecycles/RoomViewLifecycle";
30+
import { logger } from "matrix-js-sdk/src/logger";
2931

3032
import { useRoomName } from "../../../hooks/useRoomName";
3133
import { RightPanelPhases } from "../../../stores/right-panel/RightPanelStorePhases";
@@ -54,6 +56,8 @@ import { VideoRoomChatButton } from "./RoomHeader/VideoRoomChatButton";
5456
import { RoomKnocksBar } from "./RoomKnocksBar";
5557
import { isVideoRoom } from "../../../utils/video-rooms";
5658
import { notificationLevelToIndicator } from "../../../utils/notifications";
59+
import Modal from "../../../Modal";
60+
import ShareDialog from "../dialogs/ShareDialog";
5761

5862
export default function RoomHeader({
5963
room,
@@ -78,6 +82,8 @@ export default function RoomHeader({
7882
videoCallClick,
7983
toggleCallMaximized: toggleCall,
8084
isViewingCall,
85+
generateCallLink,
86+
canGenerateCallLink,
8187
isConnectedToCall,
8288
hasActiveCallSession,
8389
callOptions,
@@ -118,14 +124,34 @@ export default function RoomHeader({
118124

119125
const videoClick = useCallback((ev) => videoCallClick(ev, callOptions[0]), [callOptions, videoCallClick]);
120126

127+
const shareClick = useCallback(() => {
128+
try {
129+
// generateCallLink throws if the permissions are not met
130+
const target = generateCallLink();
131+
Modal.createDialog(ShareDialog, {
132+
target,
133+
customTitle: _t("share|share_call"),
134+
subtitle: _t("share|share_call_subtitle"),
135+
});
136+
} catch (e) {
137+
logger.error("Could not generate call link.", e);
138+
}
139+
}, [generateCallLink]);
140+
121141
const toggleCallButton = (
122142
<Tooltip label={isViewingCall ? _t("voip|minimise_call") : _t("voip|maximise_call")}>
123143
<IconButton onClick={toggleCall}>
124144
<VideoCallIcon />
125145
</IconButton>
126146
</Tooltip>
127147
);
128-
148+
const createExternalLinkButton = (
149+
<Tooltip label={_t("voip|get_call_link")}>
150+
<IconButton onClick={shareClick} aria-label={_t("voip|get_call_link")}>
151+
<ExternalLinkIcon />
152+
</IconButton>
153+
</Tooltip>
154+
);
129155
const joinCallButton = (
130156
<Tooltip label={videoCallDisabledReason ?? _t("voip|video_call")}>
131157
<Button
@@ -309,7 +335,7 @@ export default function RoomHeader({
309335
</Tooltip>
310336
);
311337
})}
312-
338+
{isViewingCall && canGenerateCallLink && createExternalLinkButton}
313339
{((isConnectedToCall && isViewingCall) || isVideoRoom(room)) && <VideoRoomChatButton room={room} />}
314340

315341
{hasActiveCallSession && !isConnectedToCall && !isViewingCall ? (

src/hooks/room/useRoomCall.ts

+39-4
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,10 @@ See the License for the specific language governing permissions and
1414
limitations under the License.
1515
*/
1616

17-
import { Room } from "matrix-js-sdk/src/matrix";
17+
import { JoinRule, Room } from "matrix-js-sdk/src/matrix";
1818
import React, { useCallback, useEffect, useMemo, useState } from "react";
1919
import { CallType } from "matrix-js-sdk/src/webrtc/call";
20+
import { logger } from "matrix-js-sdk/src/logger";
2021

2122
import { useFeatureEnabled } from "../useSettings";
2223
import SdkConfig from "../../SdkConfig";
@@ -39,6 +40,7 @@ import defaultDispatcher from "../../dispatcher/dispatcher";
3940
import { ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload";
4041
import { Action } from "../../dispatcher/actions";
4142
import { CallStore, CallStoreEvent } from "../../stores/CallStore";
43+
import { calculateRoomVia } from "../../utils/permalinks/Permalinks";
4244

4345
export enum PlatformCallType {
4446
ElementCall,
@@ -78,27 +80,35 @@ export const useRoomCall = (
7880
videoCallClick(evt: React.MouseEvent | undefined, selectedType: PlatformCallType): void;
7981
toggleCallMaximized: () => void;
8082
isViewingCall: boolean;
83+
generateCallLink: () => URL;
84+
canGenerateCallLink: boolean;
8185
isConnectedToCall: boolean;
8286
hasActiveCallSession: boolean;
8387
callOptions: PlatformCallType[];
8488
} => {
89+
// settings
8590
const groupCallsEnabled = useFeatureEnabled("feature_group_calls");
8691
const useElementCallExclusively = useMemo(() => {
8792
return SdkConfig.get("element_call").use_exclusively;
8893
}, []);
8994

95+
const guestSpaUrl = useMemo(() => {
96+
return SdkConfig.get("element_call").guest_spa_url;
97+
}, []);
98+
9099
const hasLegacyCall = useEventEmitterState(
91100
LegacyCallHandler.instance,
92101
LegacyCallHandlerEvent.CallsChanged,
93102
() => LegacyCallHandler.instance.getCallForRoom(room.roomId) !== null,
94103
);
95-
104+
// settings
96105
const widgets = useWidgets(room);
97106
const jitsiWidget = useMemo(() => widgets.find((widget) => WidgetType.JITSI.matches(widget.type)), [widgets]);
98107
const hasJitsiWidget = !!jitsiWidget;
99108
const managedHybridWidget = useMemo(() => widgets.find(isManagedHybridWidget), [widgets]);
100109
const hasManagedHybridWidget = !!managedHybridWidget;
101110

111+
// group call
102112
const groupCall = useCall(room.roomId);
103113
const isConnectedToCall = useConnectionState(groupCall) === ConnectionState.Connected;
104114
const hasGroupCall = groupCall !== null;
@@ -107,11 +117,14 @@ export const useRoomCall = (
107117
SdkContextClass.instance.roomViewStore.isViewingCall(),
108118
);
109119

120+
// room
110121
const memberCount = useRoomMemberCount(room);
111122

112-
const [mayEditWidgets, mayCreateElementCalls] = useRoomState(room, () => [
123+
const [mayEditWidgets, mayCreateElementCalls, canJoinWithoutInvite] = useRoomState(room, () => [
113124
room.currentState.mayClientSendStateEvent("im.vector.modular.widgets", room.client),
114125
room.currentState.mayClientSendStateEvent(ElementCall.MEMBER_EVENT_TYPE.name, room.client),
126+
room.getJoinRule() === "public" || room.getJoinRule() === JoinRule.Knock,
127+
/*|| room.getJoinRule() === JoinRule.Restricted <- rule for joining via token?*/
115128
]);
116129

117130
// The options provided to the RoomHeader.
@@ -131,7 +144,7 @@ export const useRoomCall = (
131144
return [PlatformCallType.ElementCall];
132145
}
133146
if (hasGroupCall && WidgetType.CALL.matches(groupCall.widget.type)) {
134-
// only allow joining joining the ongoing Element call if there is one.
147+
// only allow joining the ongoing Element call if there is one.
135148
return [PlatformCallType.ElementCall];
136149
}
137150
}
@@ -258,6 +271,26 @@ export const useRoomCall = (
258271
});
259272
}, [isViewingCall, room.roomId]);
260273

274+
const generateCallLink = useCallback(() => {
275+
if (!canJoinWithoutInvite)
276+
throw new Error("Cannot create link for room that users can not join without invite.");
277+
if (!guestSpaUrl) throw new Error("No guest SPA url for external links provided.");
278+
const url = new URL(guestSpaUrl);
279+
url.pathname = "/room/";
280+
// Set params for the sharable url
281+
url.searchParams.set("roomId", room.roomId);
282+
url.searchParams.set("perParticipantE2EE", "true");
283+
for (const server of calculateRoomVia(room)) {
284+
url.searchParams.set("viaServers", server);
285+
}
286+
287+
// Move params into hash
288+
url.hash = "/" + room.name + url.search;
289+
url.search = "";
290+
291+
logger.info("Generated element call external url:", url);
292+
return url;
293+
}, [canJoinWithoutInvite, guestSpaUrl, room]);
261294
/**
262295
* We've gone through all the steps
263296
*/
@@ -268,6 +301,8 @@ export const useRoomCall = (
268301
videoCallClick,
269302
toggleCallMaximized: toggleCallMaximized,
270303
isViewingCall: isViewingCall,
304+
generateCallLink,
305+
canGenerateCallLink: guestSpaUrl !== undefined && canJoinWithoutInvite,
271306
isConnectedToCall: isConnectedToCall,
272307
hasActiveCallSession: hasActiveCallSession,
273308
callOptions,

src/i18n/strings/en_EN.json

+4
Original file line numberDiff line numberDiff line change
@@ -2896,6 +2896,9 @@
28962896
"link_title": "Link to room",
28972897
"permalink_message": "Link to selected message",
28982898
"permalink_most_recent": "Link to most recent message",
2899+
"share_call": "Conference invite link",
2900+
"share_call_subtitle": "Link for external users to join the call without a matrix account:",
2901+
"title_link": "Share Link",
28992902
"title_message": "Share Room Message",
29002903
"title_room": "Share Room",
29012904
"title_user": "Share User"
@@ -3828,6 +3831,7 @@
38283831
"expand": "Return to call",
38293832
"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.",
38303833
"failed_call_live_broadcast_title": "Can’t start a call",
3834+
"get_call_link": "Share call link",
38313835
"hangup": "Hangup",
38323836
"hide_sidebar_button": "Hide sidebar",
38333837
"input_devices": "Input devices",

0 commit comments

Comments
 (0)