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
12 changes: 10 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 UserAddIcon } from "@vector-im/compound-design-tokens/icons/user-add.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 Down Expand Up @@ -78,6 +79,7 @@ export default function RoomHeader({
videoCallClick,
toggleCallMaximized: toggleCall,
isViewingCall,
generateCallLink,
isConnectedToCall,
hasActiveCallSession,
callOptions,
Expand Down Expand Up @@ -125,7 +127,13 @@ export default function RoomHeader({
</IconButton>
</Tooltip>
);

const createExternalLinkButton = (
<Tooltip label={_t("voip|get_call_link")}>
<IconButton onClick={generateCallLink} aria-label={_t("voip|get_call_link")}>
<UserAddIcon />
toger5 marked this conversation as resolved.
Show resolved Hide resolved
</IconButton>
</Tooltip>
);
const joinCallButton = (
<Tooltip label={videoCallDisabledReason ?? _t("voip|video_call")}>
<Button
Expand Down Expand Up @@ -309,7 +317,7 @@ export default function RoomHeader({
</Tooltip>
);
})}

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

{hasActiveCallSession && !isConnectedToCall && !isViewingCall ? (
Expand Down
32 changes: 28 additions & 4 deletions src/hooks/room/useRoomCall.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,13 @@ 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";
import SdkConfig, { DEFAULTS } from "../../SdkConfig";
import { useEventEmitter, useEventEmitterState } from "../useEventEmitter";
import LegacyCallHandler, { LegacyCallHandlerEvent } from "../../LegacyCallHandler";
import { useWidgets } from "../../components/views/right_panel/RoomSummaryCard";
Expand Down Expand Up @@ -78,10 +79,12 @@ 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;
Expand All @@ -92,13 +95,14 @@ export const useRoomCall = (
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 +111,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 Down Expand Up @@ -258,6 +265,22 @@ export const useRoomCall = (
});
}, [isViewingCall, room.roomId]);

const generateCallLink = useCallback(() => {
const url = new URL(SdkConfig.get("element_call").url ?? DEFAULTS.element_call.url!);
url.pathname = "/room/";
// Set params for the sharable url
url.searchParams.set("roomId", room.roomId);
url.searchParams.set("perParticipantE2EE", "true");
url.searchParams.set("viaServers", "matrix.org");
toger5 marked this conversation as resolved.
Show resolved Hide resolved

// Move params into hash
url.hash = "/" + room.name + url.search;
url.search = "";

logger.log("Generated element call external url:", url);
navigator.clipboard.writeText(url.toString());
toger5 marked this conversation as resolved.
Show resolved Hide resolved
return url;
}, [room.name, room.roomId]);
/**
* We've gone through all the steps
*/
Expand All @@ -268,6 +291,7 @@ export const useRoomCall = (
videoCallClick,
toggleCallMaximized: toggleCallMaximized,
isViewingCall: isViewingCall,
generateCallLink: canJoinWithoutInvite ? generateCallLink : undefined,
isConnectedToCall: isConnectedToCall,
hasActiveCallSession: hasActiveCallSession,
callOptions,
Expand Down
1 change: 1 addition & 0 deletions src/i18n/strings/en_EN.json
Original file line number Diff line number Diff line change
Expand Up @@ -3830,6 +3830,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": "Copy call link",
"hangup": "Hangup",
"hide_sidebar_button": "Hide sidebar",
"input_devices": "Input devices",
Expand Down
35 changes: 35 additions & 0 deletions test/components/views/rooms/RoomHeader-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ import { Call, ElementCall } from "../../../../src/models/Call";
import * as ShieldUtils from "../../../../src/utils/ShieldUtils";
import { Container, WidgetLayoutStore } from "../../../../src/stores/widgets/WidgetLayoutStore";
import MatrixClientContext from "../../../../src/contexts/MatrixClientContext";
import { _t } from "../../../../src/languageHandler";
import * as UseCall from "../../../../src/hooks/useCall";
import { SdkContextClass } from "../../../../src/contexts/SDKContext";
import WidgetStore, { IApp } from "../../../../src/stores/WidgetStore";
Expand Down Expand Up @@ -491,6 +492,40 @@ describe("RoomHeader", () => {
});
});

describe("External conference", () => {
beforeEach(() => {
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 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();
});
});

describe("public room", () => {
it("shows a globe", () => {
const joinRuleEvent = new MatrixEvent({
Expand Down
Loading