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

Commit

Permalink
Call guest access link creation to join calls as a non registered use…
Browse files Browse the repository at this point in the history
…r 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]>
  • Loading branch information
toger5 and dbkr authored Mar 7, 2024
1 parent af51897 commit 70365c8
Show file tree
Hide file tree
Showing 7 changed files with 323 additions and 12 deletions.
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
34 changes: 28 additions & 6 deletions src/components/views/dialogs/ShareDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,28 @@ const socials = [
];

interface BaseProps {
/**
* A function that is called when the dialog is dismissed
*/
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 {
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;
}

Expand Down Expand Up @@ -109,7 +126,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 +148,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 +167,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 +227,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
30 changes: 28 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,8 @@ export default function RoomHeader({
videoCallClick,
toggleCallMaximized: toggleCall,
isViewingCall,
generateCallLink,
canGenerateCallLink,
isConnectedToCall,
hasActiveCallSession,
callOptions,
Expand Down Expand Up @@ -118,14 +124,34 @@ export default function RoomHeader({

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 = (
<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 +335,7 @@ export default function RoomHeader({
</Tooltip>
);
})}

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

{hasActiveCallSession && !isConnectedToCall && !isViewingCall ? (
Expand Down
43 changes: 39 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,35 @@ export const useRoomCall = (
videoCallClick(evt: React.MouseEvent | undefined, selectedType: PlatformCallType): void;
toggleCallMaximized: () => void;
isViewingCall: boolean;
generateCallLink: () => URL;
canGenerateCallLink: boolean;
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 +117,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 +144,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 +271,26 @@ export const useRoomCall = (
});
}, [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
*/
Expand All @@ -268,6 +301,8 @@ export const useRoomCall = (
videoCallClick,
toggleCallMaximized: toggleCallMaximized,
isViewingCall: isViewingCall,
generateCallLink,
canGenerateCallLink: guestSpaUrl !== undefined && canJoinWithoutInvite,
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

0 comments on commit 70365c8

Please sign in to comment.