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

Call guest access link creation to join calls as a non registered user via the EC SPA #12259

Merged
merged 14 commits into from
Mar 7, 2024
1 change: 1 addition & 0 deletions src/IConfigOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ export interface IConfigOptions {
};
element_call: {
url?: string;
guest_spa_url?: string;
use_exclusively?: boolean;
participant_limit?: number;
brand?: string;
Expand Down
19 changes: 13 additions & 6 deletions src/components/views/dialogs/ShareDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,12 @@ const socials = [

interface BaseProps {
onFinished(): void;
customTitle?: string;
subtitle?: string;
toger5 marked this conversation as resolved.
Show resolved Hide resolved
}

interface Props extends BaseProps {
target: Room | User | RoomMember;
target: Room | User | RoomMember | URL;
toger5 marked this conversation as resolved.
Show resolved Hide resolved
permalinkCreator?: RoomPermalinkCreator;
}

Expand Down Expand Up @@ -109,7 +111,9 @@ export default class ShareDialog extends React.PureComponent<XOR<Props, EventPro
};

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) {
const events = this.props.target.getLiveTimeline().getEvents();
return this.state.permalinkCreator!.forEvent(events[events.length - 1].getId()!);
Expand All @@ -129,8 +133,10 @@ export default class ShareDialog extends React.PureComponent<XOR<Props, EventPro
let title: string | undefined;
let checkbox: JSX.Element | undefined;

if (this.props.target instanceof Room) {
title = _t("share|title_room");
if (this.props.target instanceof URL) {
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();
if (events.length > 0) {
Expand All @@ -146,9 +152,9 @@ export default class ShareDialog extends React.PureComponent<XOR<Props, EventPro
);
}
} 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) {
title = _t("share|title_message");
title = this.props.customTitle ?? _t("share|title_message");
checkbox = (
<div>
<StyledCheckbox
Expand Down Expand Up @@ -206,6 +212,7 @@ export default class ShareDialog extends React.PureComponent<XOR<Props, EventPro
contentId="mx_Dialog_content"
onFinished={this.props.onFinished}
>
{this.props.subtitle && <p>{this.props.subtitle}</p>}
<div className="mx_ShareDialog_content">
<CopyableText getTextToCopy={() => matrixToUrl}>
<a title={_t("share|link_title")} href={matrixToUrl} onClick={ShareDialog.onLinkClick}>
Expand Down
28 changes: 26 additions & 2 deletions src/components/views/rooms/RoomHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 { 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 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 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";
Expand All @@ -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 { EventType, JoinRule, type Room } from "matrix-js-sdk/src/matrix";
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 { RightPanelPhases } from "../../../stores/right-panel/RightPanelStorePhases";
Expand Down Expand Up @@ -54,6 +56,8 @@ import { VideoRoomChatButton } from "./RoomHeader/VideoRoomChatButton";
import { RoomKnocksBar } from "./RoomKnocksBar";
import { isVideoRoom } from "../../../utils/video-rooms";
import { notificationLevelToIndicator } from "../../../utils/notifications";
import Modal from "../../../Modal";
import ShareDialog from "../dialogs/ShareDialog";

export default function RoomHeader({
room,
Expand All @@ -78,6 +82,7 @@ export default function RoomHeader({
videoCallClick,
toggleCallMaximized: toggleCall,
isViewingCall,
generateCallLink,
isConnectedToCall,
hasActiveCallSession,
callOptions,
Expand Down Expand Up @@ -118,14 +123,33 @@ export default function RoomHeader({

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

const shareClick = useCallback(() => {
const target = generateCallLink ? generateCallLink() : undefined;
if (target) {
Modal.createDialog(ShareDialog, {
target,
customTitle: _t("share|share_call"),
subtitle: _t("share|share_call_subtitle"),
});
} else {
logger.error("Could not generate call link.");
toger5 marked this conversation as resolved.
Show resolved Hide resolved
}
}, [generateCallLink]);

const toggleCallButton = (
<Tooltip label={isViewingCall ? _t("voip|minimise_call") : _t("voip|maximise_call")}>
<IconButton onClick={toggleCall}>
<VideoCallIcon />
</IconButton>
</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 = (
<Tooltip label={videoCallDisabledReason ?? _t("voip|video_call")}>
<Button
Expand Down Expand Up @@ -309,7 +333,7 @@ export default function RoomHeader({
</Tooltip>
);
})}

{isViewingCall && generateCallLink && createExternalLinkButton}
{((isConnectedToCall && isViewingCall) || isVideoRoom(room)) && <VideoRoomChatButton room={room} />}

{hasActiveCallSession && !isConnectedToCall && !isViewingCall ? (
Expand Down
40 changes: 36 additions & 4 deletions src/hooks/room/useRoomCall.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,10 @@ See the License for the specific language governing permissions and
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 { CallType } from "matrix-js-sdk/src/webrtc/call";
import { logger } from "matrix-js-sdk/src/logger";

import { useFeatureEnabled } from "../useSettings";
import SdkConfig from "../../SdkConfig";
Expand All @@ -39,6 +40,7 @@ import defaultDispatcher from "../../dispatcher/dispatcher";
import { ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload";
import { Action } from "../../dispatcher/actions";
import { CallStore, CallStoreEvent } from "../../stores/CallStore";
import { calculateRoomVia } from "../../utils/permalinks/Permalinks";

export enum PlatformCallType {
ElementCall,
Expand Down Expand Up @@ -78,27 +80,34 @@ export const useRoomCall = (
videoCallClick(evt: React.MouseEvent | undefined, selectedType: PlatformCallType): void;
toggleCallMaximized: () => void;
isViewingCall: boolean;
generateCallLink: (() => URL) | undefined;
isConnectedToCall: boolean;
hasActiveCallSession: boolean;
callOptions: PlatformCallType[];
} => {
// settings
const groupCallsEnabled = useFeatureEnabled("feature_group_calls");
const useElementCallExclusively = useMemo(() => {
return SdkConfig.get("element_call").use_exclusively;
}, []);

const guestSpaUrl = useMemo(() => {
return SdkConfig.get("element_call").guest_spa_url;
}, []);

const hasLegacyCall = useEventEmitterState(
LegacyCallHandler.instance,
LegacyCallHandlerEvent.CallsChanged,
() => LegacyCallHandler.instance.getCallForRoom(room.roomId) !== null,
);

// settings
const widgets = useWidgets(room);
const jitsiWidget = useMemo(() => widgets.find((widget) => WidgetType.JITSI.matches(widget.type)), [widgets]);
const hasJitsiWidget = !!jitsiWidget;
const managedHybridWidget = useMemo(() => widgets.find(isManagedHybridWidget), [widgets]);
const hasManagedHybridWidget = !!managedHybridWidget;

// group call
const groupCall = useCall(room.roomId);
const isConnectedToCall = useConnectionState(groupCall) === ConnectionState.Connected;
const hasGroupCall = groupCall !== null;
Expand All @@ -107,11 +116,14 @@ export const useRoomCall = (
SdkContextClass.instance.roomViewStore.isViewingCall(),
);

// 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(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.
Expand All @@ -131,7 +143,7 @@ export const useRoomCall = (
return [PlatformCallType.ElementCall];
}
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];
}
}
Expand Down Expand Up @@ -258,6 +270,25 @@ export const useRoomCall = (
});
}, [isViewingCall, room.roomId]);

const generateCallLink = useCallback(() => {
// Should never happen, because the hook only passes generateCallLink if externalSpaUrl is set.
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;
}, [guestSpaUrl, room]);
/**
* We've gone through all the steps
*/
Expand All @@ -268,6 +299,7 @@ export const useRoomCall = (
videoCallClick,
toggleCallMaximized: toggleCallMaximized,
isViewingCall: isViewingCall,
generateCallLink: guestSpaUrl && canJoinWithoutInvite ? generateCallLink : undefined,
isConnectedToCall: isConnectedToCall,
hasActiveCallSession: hasActiveCallSession,
callOptions,
Expand Down
4 changes: 4 additions & 0 deletions src/i18n/strings/en_EN.json
Original file line number Diff line number Diff line change
Expand Up @@ -2896,6 +2896,9 @@
"link_title": "Link to room",
"permalink_message": "Link to selected 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_room": "Share Room",
"title_user": "Share User"
Expand Down Expand Up @@ -3828,6 +3831,7 @@
"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_title": "Can’t start a call",
"get_call_link": "Share call link",
"hangup": "Hangup",
"hide_sidebar_button": "Hide sidebar",
"input_devices": "Input devices",
Expand Down
Loading
Loading