Call Guest Access, give user the option to change the acces level so they can generate a call link. (#12401)
* Ask the user to change the room access settings if they click the create link button. Signed-off-by: Timo K <toger5@hotmail.de> * disable call button if appropriate. Signed-off-by: Timo K <toger5@hotmail.de> * Add tests Refactor tests to be in CallGuestLinkButton-test instead of the RoomHeader Signed-off-by: Timo K <toger5@hotmail.de> * add test for: no button if cannot change join rule and room not public nor knock Signed-off-by: Timo K <toger5@hotmail.de> * fix tests Signed-off-by: Timo K <toger5@hotmail.de> * add JoinRuleDialog tests Signed-off-by: Timo K <toger5@hotmail.de> * move spy into before each Signed-off-by: Timo K <toger5@hotmail.de> * Update src/i18n/strings/en_EN.json Co-authored-by: Robin <robin@robin.town> * remove inline css and update modal style Signed-off-by: Timo K <toger5@hotmail.de> * Update src/i18n/strings/en_EN.json Co-authored-by: Robin <robin@robin.town> * Update src/i18n/strings/en_EN.json Co-authored-by: Robin <robin@robin.town> * Invite state was not reactive. Changing power level did not update the ui. Signed-off-by: Timo K <toger5@hotmail.de> * linter Signed-off-by: Timo K <toger5@hotmail.de> * make useGuestAccessInformation use useRoomState Signed-off-by: Timo K <toger5@hotmail.de> * fix tests and simplify logic * fix tests * review Signed-off-by: Timo K <toger5@hotmail.de> --------- Signed-off-by: Timo K <toger5@hotmail.de> Co-authored-by: Robin <robin@robin.town>
This commit is contained in:
parent
59395abb6b
commit
d35fce198c
11 changed files with 588 additions and 175 deletions
|
@ -15,7 +15,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React, { useContext } from "react";
|
||||
import { Room } from "matrix-js-sdk/src/matrix";
|
||||
import { Room, RoomMemberEvent } from "matrix-js-sdk/src/matrix";
|
||||
import { KnownMembership } from "matrix-js-sdk/src/types";
|
||||
|
||||
import { IProps as IContextMenuProps } from "../../structures/ContextMenu";
|
||||
|
@ -117,9 +117,9 @@ const RoomContextMenu: React.FC<IProps> = ({ room, onFinished, ...props }) => {
|
|||
const elementCallVideoRoomsEnabled = useFeatureEnabled("feature_element_call_video_rooms");
|
||||
const isVideoRoom =
|
||||
videoRoomsEnabled && (room.isElementVideoRoom() || (elementCallVideoRoomsEnabled && room.isCallRoom()));
|
||||
|
||||
const canInvite = useEventEmitterState(cli, RoomMemberEvent.PowerLevel, () => room.canInvite(cli.getUserId()!));
|
||||
let inviteOption: JSX.Element | undefined;
|
||||
if (room.canInvite(cli.getUserId()!) && !isDm && shouldShowComponent(UIComponent.InviteUsers)) {
|
||||
if (canInvite && !isDm && shouldShowComponent(UIComponent.InviteUsers)) {
|
||||
const onInviteClick = (ev: ButtonEvent): void => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
|
|
@ -32,7 +32,7 @@ import { Icon as LockIcon } from "@vector-im/compound-design-tokens/icons/lock-s
|
|||
import { Icon as LockOffIcon } from "@vector-im/compound-design-tokens/icons/lock-off.svg";
|
||||
import { Icon as PublicIcon } from "@vector-im/compound-design-tokens/icons/public.svg";
|
||||
import { Icon as ErrorIcon } from "@vector-im/compound-design-tokens/icons/error.svg";
|
||||
import { EventType, JoinRule, Room } from "matrix-js-sdk/src/matrix";
|
||||
import { EventType, JoinRule, Room, RoomStateEvent } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import { useIsEncrypted } from "../../../hooks/useIsEncrypted";
|
||||
|
@ -393,6 +393,7 @@ const RoomSummaryCard: React.FC<IProps> = ({ room, permalinkCreator, onClose, on
|
|||
const roomTags = useEventEmitterState(RoomListStore.instance, LISTS_UPDATE_EVENT, () =>
|
||||
RoomListStore.instance.getTagsForRoom(room),
|
||||
);
|
||||
const canInviteToState = useEventEmitterState(room, RoomStateEvent.Update, () => canInviteTo(room));
|
||||
const isFavorite = roomTags.includes(DefaultTagID.Favourite);
|
||||
|
||||
return (
|
||||
|
@ -439,7 +440,7 @@ const RoomSummaryCard: React.FC<IProps> = ({ room, permalinkCreator, onClose, on
|
|||
<MenuItem
|
||||
Icon={UserAddIcon}
|
||||
label={_t("action|invite")}
|
||||
disabled={!canInviteTo(room)}
|
||||
disabled={!canInviteToState}
|
||||
onSelect={() => inviteToRoom(room)}
|
||||
/>
|
||||
<MenuItem Icon={LinkIcon} label={_t("action|copy_link")} onSelect={onShareRoomClick} />
|
||||
|
|
|
@ -18,7 +18,6 @@ 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";
|
||||
|
@ -27,7 +26,6 @@ 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";
|
||||
|
@ -56,8 +54,7 @@ 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";
|
||||
import { CallGuestLinkButton } from "./RoomHeader/CallGuestLinkButton";
|
||||
|
||||
export default function RoomHeader({
|
||||
room,
|
||||
|
@ -82,8 +79,6 @@ export default function RoomHeader({
|
|||
videoCallClick,
|
||||
toggleCallMaximized: toggleCall,
|
||||
isViewingCall,
|
||||
generateCallLink,
|
||||
canGenerateCallLink,
|
||||
isConnectedToCall,
|
||||
hasActiveCallSession,
|
||||
callOptions,
|
||||
|
@ -124,20 +119,6 @@ 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}>
|
||||
|
@ -145,13 +126,7 @@ export default function RoomHeader({
|
|||
</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
|
||||
|
@ -227,7 +202,10 @@ export default function RoomHeader({
|
|||
const voiceCallButton = (
|
||||
<Tooltip label={voiceCallDisabledReason ?? _t("voip|voice_call")}>
|
||||
<IconButton
|
||||
disabled={!!voiceCallDisabledReason}
|
||||
// We need both: isViewingCall and isConnectedToCall
|
||||
// - in the Lobby we are viewing a call but are not connected to it.
|
||||
// - in pip view we are connected to the call but not viewing it.
|
||||
disabled={!!voiceCallDisabledReason || isViewingCall || isConnectedToCall}
|
||||
aria-label={voiceCallDisabledReason ?? _t("voip|voice_call")}
|
||||
onClick={(ev) => voiceCallClick(ev, callOptions[0])}
|
||||
>
|
||||
|
@ -335,7 +313,8 @@ export default function RoomHeader({
|
|||
</Tooltip>
|
||||
);
|
||||
})}
|
||||
{isViewingCall && canGenerateCallLink && createExternalLinkButton}
|
||||
|
||||
{isViewingCall && <CallGuestLinkButton room={room} />}
|
||||
{((isConnectedToCall && isViewingCall) || isVideoRoom(room)) && <VideoRoomChatButton room={room} />}
|
||||
|
||||
{hasActiveCallSession && !isConnectedToCall && !isViewingCall ? (
|
||||
|
|
167
src/components/views/rooms/RoomHeader/CallGuestLinkButton.tsx
Normal file
167
src/components/views/rooms/RoomHeader/CallGuestLinkButton.tsx
Normal file
|
@ -0,0 +1,167 @@
|
|||
/*
|
||||
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 { Icon as ExternalLinkIcon } from "@vector-im/compound-design-tokens/icons/link.svg";
|
||||
import { Button, IconButton, Tooltip } from "@vector-im/compound-web";
|
||||
import React, { useCallback } from "react";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { EventType, IJoinRuleEventContent, JoinRule, Room } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import Modal from "../../../../Modal";
|
||||
import ShareDialog from "../../dialogs/ShareDialog";
|
||||
import { _t } from "../../../../languageHandler";
|
||||
import SettingsStore from "../../../../settings/SettingsStore";
|
||||
import { calculateRoomVia } from "../../../../utils/permalinks/Permalinks";
|
||||
import BaseDialog from "../../dialogs/BaseDialog";
|
||||
import { useGuestAccessInformation } from "../../../../hooks/room/useGuestAccessInformation";
|
||||
|
||||
/**
|
||||
* Display a button to open a dialog to share a link to the call using a element call guest spa url (`element_call:guest_spa_url` in the EW config).
|
||||
* @param room
|
||||
* @returns Nothing if there is not the option to share a link (No guest_spa_url is set) or a button to open a dialog to share the link.
|
||||
*/
|
||||
export const CallGuestLinkButton: React.FC<{ room: Room }> = ({ room }) => {
|
||||
const { canInviteGuests, guestSpaUrl, isRoomJoinable, canInvite } = useGuestAccessInformation(room);
|
||||
|
||||
const generateCallLink = useCallback(() => {
|
||||
if (!isRoomJoinable()) 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);
|
||||
if (room.hasEncryptionStateEvent()) 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, isRoomJoinable, room]);
|
||||
|
||||
const showLinkModal = useCallback(() => {
|
||||
try {
|
||||
// generateCallLink throws if the invite rules 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 shareClick = useCallback(() => {
|
||||
if (isRoomJoinable()) {
|
||||
showLinkModal();
|
||||
} else {
|
||||
// the room needs to be set to public or knock to generate a link
|
||||
Modal.createDialog(JoinRuleDialog, {
|
||||
room,
|
||||
// If the user cannot invite the Knocking is not given as an option.
|
||||
canInvite,
|
||||
}).finished.then(() => {
|
||||
// we need to use the function here because the callback got called before the state was updated.
|
||||
if (isRoomJoinable()) showLinkModal();
|
||||
});
|
||||
}
|
||||
}, [isRoomJoinable, showLinkModal, room, canInvite]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{canInviteGuests && (
|
||||
<Tooltip label={_t("voip|get_call_link")}>
|
||||
<IconButton onClick={shareClick} aria-label={_t("voip|get_call_link")}>
|
||||
<ExternalLinkIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* A dialog to change the join rule of a room to public or knock.
|
||||
* @param room The room to change the join rule of.
|
||||
* @param onFinished Callback that is getting called if the dialog wants to close.
|
||||
*/
|
||||
export const JoinRuleDialog: React.FC<{
|
||||
onFinished(): void;
|
||||
room: Room;
|
||||
canInvite: boolean;
|
||||
}> = ({ onFinished, room, canInvite }) => {
|
||||
const askToJoinEnabled = SettingsStore.getValue("feature_ask_to_join");
|
||||
const [isUpdating, setIsUpdating] = React.useState<undefined | JoinRule>(undefined);
|
||||
const changeJoinRule = useCallback(
|
||||
async (newRule: JoinRule) => {
|
||||
if (isUpdating !== undefined) return;
|
||||
setIsUpdating(newRule);
|
||||
await room.client.sendStateEvent(
|
||||
room.roomId,
|
||||
EventType.RoomJoinRules,
|
||||
{
|
||||
join_rule: newRule,
|
||||
} as IJoinRuleEventContent,
|
||||
"",
|
||||
);
|
||||
// Show the dialog for a bit to give the user feedback
|
||||
setTimeout(() => onFinished(), 500);
|
||||
},
|
||||
[isUpdating, onFinished, room.client, room.roomId],
|
||||
);
|
||||
return (
|
||||
<BaseDialog title={_t("update_room_access_modal|title")} onFinished={onFinished} className="mx_JoinRuleDialog">
|
||||
<p>{_t("update_room_access_modal|description")}</p>
|
||||
<div className="mx_JoinRuleDialogButtons">
|
||||
{askToJoinEnabled && canInvite && (
|
||||
<Button
|
||||
kind="secondary"
|
||||
className="mx_Dialog_nonDialogButton"
|
||||
disabled={isUpdating === JoinRule.Knock}
|
||||
onClick={() => changeJoinRule(JoinRule.Knock)}
|
||||
>
|
||||
{_t("action|ask_to_join")}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
className="mx_Dialog_nonDialogButton"
|
||||
kind="destructive"
|
||||
disabled={isUpdating === JoinRule.Public}
|
||||
onClick={() => changeJoinRule(JoinRule.Public)}
|
||||
>
|
||||
{_t("common|public")}
|
||||
</Button>
|
||||
</div>
|
||||
<p>{_t("update_room_access_modal|dont_change_description")}</p>
|
||||
<div className="mx_JoinRuleDialogButtons">
|
||||
<Button
|
||||
kind="tertiary"
|
||||
className="mx_Dialog_nonDialogButton"
|
||||
onClick={() => {
|
||||
if (isUpdating === undefined) onFinished();
|
||||
}}
|
||||
>
|
||||
{_t("update_room_access_modal|no_change")}
|
||||
</Button>
|
||||
</div>
|
||||
</BaseDialog>
|
||||
);
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue