diff --git a/res/css/_components.pcss b/res/css/_components.pcss index dadd9aadb3..a0241fb8b5 100644 --- a/res/css/_components.pcss +++ b/res/css/_components.pcss @@ -12,7 +12,6 @@ @import "./components/views/beacon/_LeftPanelLiveShareWarning.pcss"; @import "./components/views/beacon/_LiveTimeRemaining.pcss"; @import "./components/views/beacon/_OwnBeaconStatus.pcss"; -@import "./components/views/beacon/_RoomLiveShareWarning.pcss"; @import "./components/views/beacon/_ShareLatestLocation.pcss"; @import "./components/views/beacon/_StyledLiveBeaconIcon.pcss"; @import "./components/views/context_menus/_KebabContextMenu.pcss"; diff --git a/res/css/components/views/beacon/_RoomLiveShareWarning.pcss b/res/css/components/views/beacon/_RoomLiveShareWarning.pcss deleted file mode 100644 index 60d23ed49a..0000000000 --- a/res/css/components/views/beacon/_RoomLiveShareWarning.pcss +++ /dev/null @@ -1,51 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -.mx_RoomLiveShareWarning { - width: 100%; - - display: flex; - flex-direction: row; - align-items: center; - - box-sizing: border-box; - padding: $spacing-12 $spacing-16; - - color: $primary-content; - background-color: $system; - cursor: pointer; -} - -.mx_RoomLiveShareWarning_icon { - height: 32px; - width: 32px; - margin-right: $spacing-8; -} - -.mx_RoomLiveShareWarning_label { - flex: 1; - font-size: $font-15px; -} - -.mx_RoomLiveShareWarning_spinner { - margin-right: $spacing-16; -} - -.mx_RoomLiveShareWarning_closeButton { - @mixin ButtonResetDefault; - margin-left: $spacing-16; -} - -.mx_RoomLiveShareWarning_stopButton { - margin-left: $spacing-16; -} - -.mx_RoomLiveShareWarning_closeButtonIcon { - height: $font-18px; - padding: $spacing-4; -} diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index b56172be42..ba30a3445d 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -65,7 +65,6 @@ import RoomPreviewBar from "../views/rooms/RoomPreviewBar"; import RoomPreviewCard from "../views/rooms/RoomPreviewCard"; import RoomUpgradeWarningBar from "../views/rooms/RoomUpgradeWarningBar"; import AuxPanel from "../views/rooms/AuxPanel"; -import LegacyRoomHeader from "../views/rooms/LegacyRoomHeader"; import RoomHeader from "../views/rooms/RoomHeader"; import { IOOBData, IThreepidInvite } from "../../stores/ThreepidInviteStore"; import EffectsOverlay from "../views/elements/EffectsOverlay"; @@ -313,26 +312,7 @@ function LocalRoomView(props: LocalRoomViewProps): ReactElement { return (
- {SettingsStore.getValue("feature_new_room_decoration_ui") ? ( - - ) : ( - - )} +
@@ -366,26 +346,7 @@ function LocalRoomCreateLoader(props: ILocalRoomCreateLoaderProps): ReactElement return (
- {SettingsStore.getValue("feature_new_room_decoration_ui") ? ( - - ) : ( - - )} +
@@ -1753,13 +1714,6 @@ export class RoomView extends React.Component { }); }; - private onAppsClick = (): void => { - dis.dispatch({ - action: "appsDrawer", - show: !this.state.showApps, - }); - }; - private onForgetClick = (): void => { dis.dispatch({ action: "forget_room", @@ -1836,10 +1790,6 @@ export class RoomView extends React.Component { dis.fire(Action.ViewRoomDirectory); }; - private onSearchClick = (): void => { - dis.fire(Action.FocusMessageSearch); - }; - private onSearchChange = debounce((e: ChangeEvent): void => { const term = (e.target as HTMLInputElement).value; this.onSearch(term); @@ -2121,15 +2071,13 @@ export class RoomView extends React.Component { } } - const roomHeaderType = SettingsStore.getValue("feature_new_room_decoration_ui") ? "new" : "legacy"; - if (!this.state.room) { const loading = !this.state.matrixClientIsReady || this.state.roomLoading || this.state.peekLoading; if (loading) { // Assume preview loading if we don't have a ready client or a room ID (still resolving the alias) const previewLoading = !this.state.matrixClientIsReady || !this.state.roomId || this.state.peekLoading; return ( -
+
{ // We've got to this room by following a link, possibly a third party invite. const roomAlias = this.state.roomAlias; return ( -
+
{ // We have a regular invite for this room. return ( -
+
{ ([KnownMembership.Knock, KnownMembership.Leave] as Array).includes(myMembership) ) { return ( -
+
{ /> ); if (!this.state.canPeek && !this.state.room?.isSpaceRoom()) { - return ( -
- {previewBar} -
- ); + return
{previewBar}
; } } else if (hiddenHighlightCount > 0) { aux = ( @@ -2587,46 +2531,9 @@ export class RoomView extends React.Component { } const mainSplitContentClasses = classNames("mx_RoomView_body", mainSplitContentClassName); - let excludedRightPanelPhaseButtons = [RightPanelPhases.Timeline]; - let onAppsClick: (() => void) | null = this.onAppsClick; - let onForgetClick: (() => void) | null = this.onForgetClick; - let onSearchClick: (() => void) | null = this.onSearchClick; - let onInviteClick: (() => void) | null = null; - let viewingCall = false; - - // Simplify the header for other main split types - switch (mainSplitContentType) { - case MainSplitContentType.MaximisedWidget: - excludedRightPanelPhaseButtons = []; - onAppsClick = null; - onForgetClick = null; - onSearchClick = null; - break; - case MainSplitContentType.Call: - excludedRightPanelPhaseButtons = []; - onAppsClick = null; - onForgetClick = null; - onSearchClick = null; - if (this.state.room.canInvite(this.context.client.getSafeUserId())) { - onInviteClick = this.onInviteClick; - } - viewingCall = true; - } - - const myMember = this.state.room!.getMember(this.context.client!.getSafeUserId()); - const showForgetButton = - !this.context.client.isGuest() && - (([KnownMembership.Leave, KnownMembership.Ban] as Array).includes(myMembership) || - myMember?.isKicked()); - return ( -
+
{showChatEffects && this.roomView.current && ( )} @@ -2644,31 +2551,10 @@ export class RoomView extends React.Component { ref={this.roomViewBody} data-layout={this.state.layout} > - {SettingsStore.getValue("feature_new_room_decoration_ui") ? ( - - ) : ( - - )} + {mainSplitBody}
diff --git a/src/components/structures/WaitingForThirdPartyRoomView.tsx b/src/components/structures/WaitingForThirdPartyRoomView.tsx index fd21ebeb0e..fd9afc50f2 100644 --- a/src/components/structures/WaitingForThirdPartyRoomView.tsx +++ b/src/components/structures/WaitingForThirdPartyRoomView.tsx @@ -11,9 +11,7 @@ import { MatrixEvent } from "matrix-js-sdk/src/matrix"; import { useRoomContext } from "../../contexts/RoomContext"; import ResizeNotifier from "../../utils/ResizeNotifier"; -import { E2EStatus } from "../../utils/ShieldUtils"; import ErrorBoundary from "../views/elements/ErrorBoundary"; -import LegacyRoomHeader from "../views/rooms/LegacyRoomHeader"; import RoomHeader from "../views/rooms/RoomHeader"; import ScrollPanel from "./ScrollPanel"; import EventTileBubble from "../views/messages/EventTileBubble"; @@ -21,7 +19,6 @@ import NewRoomIntro from "../views/rooms/NewRoomIntro"; import { UnwrappedEventTile } from "../views/rooms/EventTile"; import { _t } from "../../languageHandler"; import SdkConfig from "../../SdkConfig"; -import SettingsStore from "../../settings/SettingsStore"; interface Props { roomView: RefObject; @@ -41,24 +38,7 @@ export const WaitingForThirdPartyRoomView: React.FC = ({ roomView, resize return (
- {SettingsStore.getValue("feature_new_room_decoration_ui") ? ( - - ) : ( - - )} +
diff --git a/src/components/views/beacon/RoomLiveShareWarning.tsx b/src/components/views/beacon/RoomLiveShareWarning.tsx deleted file mode 100644 index 5d37ef075e..0000000000 --- a/src/components/views/beacon/RoomLiveShareWarning.tsx +++ /dev/null @@ -1,148 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import React from "react"; -import { Room } from "matrix-js-sdk/src/matrix"; -import CloseIcon from "@vector-im/compound-design-tokens/assets/web/icons/close"; - -import { _t } from "../../../languageHandler"; -import { useEventEmitterState } from "../../../hooks/useEventEmitter"; -import { OwnBeaconStore, OwnBeaconStoreEvent } from "../../../stores/OwnBeaconStore"; -import { useOwnLiveBeacons } from "../../../utils/beacon"; -import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton"; -import Spinner from "../elements/Spinner"; -import StyledLiveBeaconIcon from "./StyledLiveBeaconIcon"; -import LiveTimeRemaining from "./LiveTimeRemaining"; -import dispatcher from "../../../dispatcher/dispatcher"; -import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload"; -import { Action } from "../../../dispatcher/actions"; - -const getLabel = (hasLocationPublishError: boolean, hasStopSharingError: boolean): string => { - if (hasLocationPublishError) { - return _t("location_sharing|error_sharing_live_location_try_again"); - } - if (hasStopSharingError) { - return _t("location_sharing|error_stopping_live_location_try_again"); - } - return _t("location_sharing|live_location_active"); -}; - -interface RoomLiveShareWarningInnerProps { - liveBeaconIds: string[]; - roomId: Room["roomId"]; -} -const RoomLiveShareWarningInner: React.FC = ({ liveBeaconIds, roomId }) => { - const { - onStopSharing, - onResetLocationPublishError, - beacon, - stoppingInProgress, - hasStopSharingError, - hasLocationPublishError, - } = useOwnLiveBeacons(liveBeaconIds); - - if (!beacon) { - return null; - } - - const hasError = hasStopSharingError || hasLocationPublishError; - - // eat events from buttons so navigate to tile - // is not triggered - const stopPropagationWrapper = - (callback: () => void) => - (e?: ButtonEvent): void => { - e?.stopPropagation(); - callback(); - }; - - const onButtonClick = (): void => { - if (hasLocationPublishError) { - onResetLocationPublishError(); - } else { - onStopSharing(); - } - }; - - const onClick = (): void => { - dispatcher.dispatch({ - action: Action.ViewRoom, - room_id: beacon.roomId, - metricsTrigger: undefined, - event_id: beacon.beaconInfoId, - scroll_into_view: true, - highlighted: true, - }); - }; - - return ( -
- - - - {getLabel(hasLocationPublishError, hasStopSharingError)} - - - {stoppingInProgress && ( - - - - )} - {!stoppingInProgress && !hasError && } - - - {hasError ? _t("action|retry") : _t("action|stop")} - - {hasLocationPublishError && ( - - - - )} -
- ); -}; - -interface Props { - roomId: Room["roomId"]; -} -const RoomLiveShareWarning: React.FC = ({ roomId }) => { - // do we have an active geolocation.watchPosition - const isMonitoringLiveLocation = useEventEmitterState( - OwnBeaconStore.instance, - OwnBeaconStoreEvent.MonitoringLivePosition, - () => OwnBeaconStore.instance.isMonitoringLiveLocation, - ); - - const liveBeaconIds = useEventEmitterState(OwnBeaconStore.instance, OwnBeaconStoreEvent.LivenessChange, () => - OwnBeaconStore.instance.getLiveBeaconIds(roomId), - ); - - if (!isMonitoringLiveLocation || !liveBeaconIds.length) { - // This logic is entangled with the RoomCallBanner-test's. The tests need updating if this logic changes. - return null; - } - - // split into outer/inner to avoid watching various parts of live beacon state - // when there are none - return ; -}; - -export default RoomLiveShareWarning; diff --git a/src/components/views/context_menus/RoomContextMenu.tsx b/src/components/views/context_menus/RoomContextMenu.tsx deleted file mode 100644 index f431218b06..0000000000 --- a/src/components/views/context_menus/RoomContextMenu.tsx +++ /dev/null @@ -1,389 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2021-2023 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import React, { useContext } from "react"; -import { Room, RoomMemberEvent } from "matrix-js-sdk/src/matrix"; -import { KnownMembership } from "matrix-js-sdk/src/types"; - -import { IProps as IContextMenuProps } from "../../structures/ContextMenu"; -import IconizedContextMenu, { - IconizedContextMenuCheckbox, - IconizedContextMenuOption, - IconizedContextMenuOptionList, -} from "./IconizedContextMenu"; -import { _t } from "../../../languageHandler"; -import MatrixClientContext from "../../../contexts/MatrixClientContext"; -import { ButtonEvent } from "../elements/AccessibleButton"; -import { DefaultTagID, TagID } from "../../../stores/room-list/models"; -import RoomListStore, { LISTS_UPDATE_EVENT } from "../../../stores/room-list/RoomListStore"; -import dis from "../../../dispatcher/dispatcher"; -import { EchoChamber } from "../../../stores/local-echo/EchoChamber"; -import { RoomNotifState } from "../../../RoomNotifs"; -import Modal from "../../../Modal"; -import ExportDialog from "../dialogs/ExportDialog"; -import { RightPanelPhases } from "../../../stores/right-panel/RightPanelStorePhases"; -import { RoomSettingsTab } from "../dialogs/RoomSettingsDialog"; -import { useEventEmitterState } from "../../../hooks/useEventEmitter"; -import RightPanelStore from "../../../stores/right-panel/RightPanelStore"; -import DMRoomMap from "../../../utils/DMRoomMap"; -import { Action } from "../../../dispatcher/actions"; -import PosthogTrackers from "../../../PosthogTrackers"; -import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload"; -import { getKeyBindingsManager } from "../../../KeyBindingsManager"; -import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts"; -import SettingsStore from "../../../settings/SettingsStore"; -import { SdkContextClass } from "../../../contexts/SDKContext"; -import { shouldShowComponent } from "../../../customisations/helpers/UIComponents"; -import { UIComponent } from "../../../settings/UIFeature"; -import { DeveloperToolsOption } from "./DeveloperToolsOption"; -import { tagRoom } from "../../../utils/room/tagRoom"; -import { isVideoRoom as calcIsVideoRoom } from "../../../utils/video-rooms"; -import { usePinnedEvents } from "../../../hooks/usePinnedEvents"; - -interface IProps extends IContextMenuProps { - room: Room; -} - -/** - * Room context menu accessible via the room header. - * @deprecated will be removed as part of `feature_new_room_decoration_ui` - */ -const RoomContextMenu: React.FC = ({ room, onFinished, ...props }) => { - const cli = useContext(MatrixClientContext); - const roomTags = useEventEmitterState(RoomListStore.instance, LISTS_UPDATE_EVENT, () => - RoomListStore.instance.getTagsForRoom(room), - ); - - let leaveOption: JSX.Element | undefined; - if (roomTags.includes(DefaultTagID.Archived)) { - const onForgetRoomClick = (ev: ButtonEvent): void => { - ev.preventDefault(); - ev.stopPropagation(); - - dis.dispatch({ - action: "forget_room", - room_id: room.roomId, - }); - onFinished(); - }; - - leaveOption = ( - - ); - } else { - const onLeaveRoomClick = (ev: ButtonEvent): void => { - ev.preventDefault(); - ev.stopPropagation(); - - dis.dispatch({ - action: "leave_room", - room_id: room.roomId, - }); - onFinished(); - - PosthogTrackers.trackInteraction("WebRoomHeaderContextMenuLeaveItem", ev); - }; - - leaveOption = ( - - ); - } - - const isDm = DMRoomMap.shared().getUserIdForRoomId(room.roomId); - const isVideoRoom = calcIsVideoRoom(room); - const canInvite = useEventEmitterState(cli, RoomMemberEvent.PowerLevel, () => room.canInvite(cli.getUserId()!)); - let inviteOption: JSX.Element | undefined; - if (canInvite && !isDm && shouldShowComponent(UIComponent.InviteUsers)) { - const onInviteClick = (ev: ButtonEvent): void => { - ev.preventDefault(); - ev.stopPropagation(); - - dis.dispatch({ - action: "view_invite", - roomId: room.roomId, - }); - onFinished(); - - PosthogTrackers.trackInteraction("WebRoomHeaderContextMenuInviteItem", ev); - }; - - inviteOption = ( - - ); - } - - let favouriteOption: JSX.Element | undefined; - let lowPriorityOption: JSX.Element | undefined; - let notificationOption: JSX.Element | undefined; - if (room.getMyMembership() === KnownMembership.Join) { - const isFavorite = roomTags.includes(DefaultTagID.Favourite); - favouriteOption = ( - { - onTagRoom(e, DefaultTagID.Favourite); - PosthogTrackers.trackInteraction("WebRoomHeaderContextMenuFavouriteToggle", e); - }} - active={isFavorite} - label={isFavorite ? _t("room|context_menu|unfavourite") : _t("room|context_menu|favourite")} - iconClassName="mx_RoomTile_iconStar" - /> - ); - - const isLowPriority = roomTags.includes(DefaultTagID.LowPriority); - lowPriorityOption = ( - onTagRoom(e, DefaultTagID.LowPriority)} - active={isLowPriority} - label={_t("common|low_priority")} - iconClassName="mx_RoomTile_iconArrowDown" - /> - ); - - const echoChamber = EchoChamber.forRoom(room); - let notificationLabel: string | undefined; - let iconClassName: string | undefined; - switch (echoChamber.notificationVolume) { - case RoomNotifState.AllMessages: - notificationLabel = _t("notifications|default"); - iconClassName = "mx_RoomTile_iconNotificationsDefault"; - break; - case RoomNotifState.AllMessagesLoud: - notificationLabel = _t("notifications|all_messages"); - iconClassName = "mx_RoomTile_iconNotificationsAllMessages"; - break; - case RoomNotifState.MentionsOnly: - notificationLabel = _t("room|context_menu|mentions_only"); - iconClassName = "mx_RoomTile_iconNotificationsMentionsKeywords"; - break; - case RoomNotifState.Mute: - notificationLabel = _t("common|mute"); - iconClassName = "mx_RoomTile_iconNotificationsNone"; - break; - } - - notificationOption = ( - { - ev.preventDefault(); - ev.stopPropagation(); - - dis.dispatch({ - action: "open_room_settings", - room_id: room.roomId, - initial_tab_id: RoomSettingsTab.Notifications, - }); - onFinished(); - - PosthogTrackers.trackInteraction("WebRoomHeaderContextMenuNotificationsItem", ev); - }} - label={_t("notifications|enable_prompt_toast_title")} - iconClassName={iconClassName} - > - {notificationLabel} - - ); - } - - let peopleOption: JSX.Element | undefined; - let copyLinkOption: JSX.Element | undefined; - if (!isDm) { - peopleOption = ( - { - ev.preventDefault(); - ev.stopPropagation(); - - ensureViewingRoom(ev); - RightPanelStore.instance.pushCard({ phase: RightPanelPhases.RoomMemberList }, false); - onFinished(); - PosthogTrackers.trackInteraction("WebRoomHeaderContextMenuPeopleItem", ev); - }} - label={_t("common|people")} - iconClassName="mx_RoomTile_iconPeople" - > - {room.getJoinedMemberCount()} - - ); - - copyLinkOption = ( - { - ev.preventDefault(); - ev.stopPropagation(); - - dis.dispatch({ - action: "copy_room", - room_id: room.roomId, - }); - onFinished(); - }} - label={_t("room|context_menu|copy_link")} - iconClassName="mx_RoomTile_iconCopyLink" - /> - ); - } - - let filesOption: JSX.Element | undefined; - if (!isVideoRoom) { - filesOption = ( - { - ev.preventDefault(); - ev.stopPropagation(); - - ensureViewingRoom(ev); - RightPanelStore.instance.pushCard({ phase: RightPanelPhases.FilePanel }, false); - onFinished(); - }} - label={_t("right_panel|files_button")} - iconClassName="mx_RoomTile_iconFiles" - /> - ); - } - - const pinCount = usePinnedEvents(room).length; - - let pinsOption: JSX.Element | undefined; - if (!isVideoRoom) { - pinsOption = ( - { - ev.preventDefault(); - ev.stopPropagation(); - - ensureViewingRoom(ev); - RightPanelStore.instance.pushCard({ phase: RightPanelPhases.PinnedMessages }, false); - onFinished(); - }} - label={_t("right_panel|pinned_messages_button")} - iconClassName="mx_RoomTile_iconPins" - > - {pinCount > 0 && {pinCount}} - - ); - } - - let widgetsOption: JSX.Element | undefined; - if (!isVideoRoom) { - widgetsOption = ( - { - ev.preventDefault(); - ev.stopPropagation(); - - ensureViewingRoom(ev); - RightPanelStore.instance.setCard({ phase: RightPanelPhases.RoomSummary }, false); - onFinished(); - }} - label={_t("right_panel|widgets_section")} - iconClassName="mx_RoomTile_iconWidgets" - /> - ); - } - - let exportChatOption: JSX.Element | undefined; - if (!isVideoRoom) { - exportChatOption = ( - { - ev.preventDefault(); - ev.stopPropagation(); - - Modal.createDialog(ExportDialog, { room }); - onFinished(); - }} - label={_t("right_panel|export_chat_button")} - iconClassName="mx_RoomTile_iconExport" - /> - ); - } - - const onTagRoom = (ev: ButtonEvent, tagId: TagID): void => { - ev.preventDefault(); - ev.stopPropagation(); - - tagRoom(room, tagId); - - const action = getKeyBindingsManager().getAccessibilityAction(ev as React.KeyboardEvent); - switch (action) { - case KeyBindingAction.Enter: - // Implements https://www.w3.org/TR/wai-aria-practices/#keyboard-interaction-12 - onFinished(); - break; - } - }; - - const ensureViewingRoom = (ev: ButtonEvent): void => { - if (SdkContextClass.instance.roomViewStore.getRoomId() === room.roomId) return; - dis.dispatch( - { - action: Action.ViewRoom, - room_id: room.roomId, - metricsTrigger: "RoomList", - metricsViaKeyboard: ev.type !== "click", - }, - true, - ); - }; - - return ( - - - {inviteOption} - {notificationOption} - {favouriteOption} - {peopleOption} - {filesOption} - {pinsOption} - {widgetsOption} - {lowPriorityOption} - {copyLinkOption} - - { - ev.preventDefault(); - ev.stopPropagation(); - - dis.dispatch({ - action: "open_room_settings", - room_id: room.roomId, - }); - onFinished(); - PosthogTrackers.trackInteraction("WebRoomHeaderContextMenuSettingsItem", ev); - }} - label={_t("common|settings")} - iconClassName="mx_RoomTile_iconSettings" - /> - - {exportChatOption} - - {SettingsStore.getValue("developerMode") && ( - - )} - - {leaveOption} - - - ); -}; - -export default RoomContextMenu; diff --git a/src/components/views/right_panel/LegacyRoomHeaderButtons.tsx b/src/components/views/right_panel/LegacyRoomHeaderButtons.tsx deleted file mode 100644 index 85b173c40c..0000000000 --- a/src/components/views/right_panel/LegacyRoomHeaderButtons.tsx +++ /dev/null @@ -1,318 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2019-2023 The Matrix.org Foundation C.I.C. -Copyright 2018 New Vector Ltd -Copyright 2017 Vector Creations Ltd -Copyright 2017 New Vector Ltd -Copyright 2015, 2016 OpenMarket Ltd - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import React from "react"; -import classNames from "classnames"; -import { NotificationCountType, Room, RoomEvent, ThreadEvent } from "matrix-js-sdk/src/matrix"; - -import { _t } from "../../../languageHandler"; -import HeaderButton from "./HeaderButton"; -import HeaderButtons, { HeaderKind } from "./HeaderButtons"; -import { RightPanelPhases } from "../../../stores/right-panel/RightPanelStorePhases"; -import { ActionPayload } from "../../../dispatcher/payloads"; -import RightPanelStore from "../../../stores/right-panel/RightPanelStore"; -import { showThreadPanel } from "../../../dispatcher/dispatch-actions/threads"; -import { - RoomNotificationStateStore, - UPDATE_STATUS_INDICATOR, -} from "../../../stores/notifications/RoomNotificationStateStore"; -import { NotificationLevel } from "../../../stores/notifications/NotificationLevel"; -import { SummarizedNotificationState } from "../../../stores/notifications/SummarizedNotificationState"; -import PosthogTrackers from "../../../PosthogTrackers"; -import { ButtonEvent } from "../elements/AccessibleButton"; -import { doesRoomOrThreadHaveUnreadMessages } from "../../../Unread"; -import { usePinnedEvents, useReadPinnedEvents } from "../../../hooks/usePinnedEvents"; - -const ROOM_INFO_PHASES = [ - RightPanelPhases.RoomSummary, - RightPanelPhases.Widget, - RightPanelPhases.FilePanel, - RightPanelPhases.RoomMemberList, - RightPanelPhases.RoomMemberInfo, - RightPanelPhases.EncryptionPanel, - RightPanelPhases.Room3pidMemberInfo, -]; - -interface IUnreadIndicatorProps { - color?: NotificationLevel; -} - -const UnreadIndicator: React.FC = ({ color }) => { - if (color === NotificationLevel.None) { - return null; - } - - const classes = classNames({ - mx_Indicator: true, - mx_LegacyRoomHeader_button_unreadIndicator: true, - mx_Indicator_activity: color === NotificationLevel.Activity, - mx_Indicator_notification: color === NotificationLevel.Notification, - mx_Indicator_highlight: color === NotificationLevel.Highlight, - }); - return ( - <> -
-
- - ); -}; - -interface IHeaderButtonProps { - room: Room; - isHighlighted: boolean; - onClick: () => void; -} - -const PinnedMessagesHeaderButton: React.FC = ({ room, isHighlighted, onClick }) => { - const pinnedEvents = usePinnedEvents(room); - const readPinnedEvents = useReadPinnedEvents(room); - if (!pinnedEvents?.length) return null; - - let unreadIndicator; - if (pinnedEvents.some((id) => !readPinnedEvents.has(id))) { - unreadIndicator = ; - } - - return ( - - {unreadIndicator} - - ); -}; - -const TimelineCardHeaderButton: React.FC = ({ room, isHighlighted, onClick }) => { - let unreadIndicator; - const color = RoomNotificationStateStore.instance.getRoomState(room).level; - switch (color) { - case NotificationLevel.Activity: - case NotificationLevel.Notification: - case NotificationLevel.Highlight: - unreadIndicator = ; - } - return ( - - {unreadIndicator} - - ); -}; - -interface IProps { - room?: Room; - excludedRightPanelPhaseButtons?: Array; -} - -/** - * @deprecated will be removed as part of 'feature_new_room_decoration_ui' - */ -export default class LegacyRoomHeaderButtons extends HeaderButtons { - private static readonly THREAD_PHASES = [RightPanelPhases.ThreadPanel, RightPanelPhases.ThreadView]; - private globalNotificationState: SummarizedNotificationState; - - public constructor(props: IProps) { - super(props, HeaderKind.Room); - this.globalNotificationState = RoomNotificationStateStore.instance.globalState; - } - - public componentDidMount(): void { - super.componentDidMount(); - // Notification badge may change if the notification counts from the - // server change, if a new thread is created or updated, or if a - // receipt is sent in the thread. - this.props.room?.on(RoomEvent.UnreadNotifications, this.onNotificationUpdate); - this.props.room?.on(RoomEvent.Receipt, this.onNotificationUpdate); - this.props.room?.on(RoomEvent.Timeline, this.onNotificationUpdate); - this.props.room?.on(RoomEvent.Redaction, this.onNotificationUpdate); - this.props.room?.on(RoomEvent.LocalEchoUpdated, this.onNotificationUpdate); - this.props.room?.on(RoomEvent.MyMembership, this.onNotificationUpdate); - this.props.room?.on(ThreadEvent.New, this.onNotificationUpdate); - this.props.room?.on(ThreadEvent.Update, this.onNotificationUpdate); - this.onNotificationUpdate(); - RoomNotificationStateStore.instance.on(UPDATE_STATUS_INDICATOR, this.onUpdateStatus); - } - - public componentWillUnmount(): void { - super.componentWillUnmount(); - this.props.room?.off(RoomEvent.UnreadNotifications, this.onNotificationUpdate); - this.props.room?.off(RoomEvent.Receipt, this.onNotificationUpdate); - this.props.room?.off(RoomEvent.Timeline, this.onNotificationUpdate); - this.props.room?.off(RoomEvent.Redaction, this.onNotificationUpdate); - this.props.room?.off(RoomEvent.LocalEchoUpdated, this.onNotificationUpdate); - this.props.room?.off(RoomEvent.MyMembership, this.onNotificationUpdate); - this.props.room?.off(ThreadEvent.New, this.onNotificationUpdate); - this.props.room?.off(ThreadEvent.Update, this.onNotificationUpdate); - RoomNotificationStateStore.instance.off(UPDATE_STATUS_INDICATOR, this.onUpdateStatus); - } - - private onNotificationUpdate = (): void => { - // console.log - // XXX: why don't we read from this.state.threadNotificationLevel in the render methods? - this.setState({ - threadNotificationLevel: this.notificationLevel, - }); - }; - - private get notificationLevel(): NotificationLevel { - switch (this.props.room?.threadsAggregateNotificationType) { - case NotificationCountType.Highlight: - return NotificationLevel.Highlight; - case NotificationCountType.Total: - return NotificationLevel.Notification; - } - // We don't have any notified messages, but we might have unread messages. Let's - // find out. - for (const thread of this.props.room!.getThreads()) { - // If the current thread has unread messages, we're done. - if (doesRoomOrThreadHaveUnreadMessages(thread)) { - return NotificationLevel.Activity; - } - } - // Otherwise, no notification color. - return NotificationLevel.None; - } - - private onUpdateStatus = (notificationState: SummarizedNotificationState): void => { - // XXX: why don't we read from this.state.globalNotificationCount in the render methods? - this.globalNotificationState = notificationState; - this.setState({ - globalNotificationLevel: notificationState.level, - }); - }; - - protected onAction(payload: ActionPayload): void {} - - private onRoomSummaryClicked = (): void => { - // use roomPanelPhase rather than this.state.phase as it remembers the latest one if we close - const currentPhase = RightPanelStore.instance.currentCard.phase; - if (currentPhase && ROOM_INFO_PHASES.includes(currentPhase)) { - if (this.state.phase === currentPhase) { - RightPanelStore.instance.showOrHidePhase(currentPhase); - } else { - RightPanelStore.instance.showOrHidePhase(currentPhase, RightPanelStore.instance.currentCard.state); - } - } else { - // This toggles for us, if needed - RightPanelStore.instance.showOrHidePhase(RightPanelPhases.RoomSummary); - } - }; - - private onNotificationsClicked = (): void => { - // This toggles for us, if needed - RightPanelStore.instance.showOrHidePhase(RightPanelPhases.NotificationPanel); - }; - - private onPinnedMessagesClicked = (): void => { - // This toggles for us, if needed - RightPanelStore.instance.showOrHidePhase(RightPanelPhases.PinnedMessages); - }; - private onTimelineCardClicked = (): void => { - RightPanelStore.instance.showOrHidePhase(RightPanelPhases.Timeline); - }; - - private onThreadsPanelClicked = (ev: ButtonEvent): void => { - if (this.state.phase && LegacyRoomHeaderButtons.THREAD_PHASES.includes(this.state.phase)) { - RightPanelStore.instance.togglePanel(this.props.room?.roomId ?? null); - } else { - showThreadPanel(); - PosthogTrackers.trackInteraction("WebRoomHeaderButtonsThreadsButton", ev); - } - }; - - public renderButtons(): JSX.Element { - if (!this.props.room) { - return <>; - } - - const rightPanelPhaseButtons: Map = new Map(); - - rightPanelPhaseButtons.set( - RightPanelPhases.PinnedMessages, - , - ); - - rightPanelPhaseButtons.set( - RightPanelPhases.Timeline, - , - ); - rightPanelPhaseButtons.set( - RightPanelPhases.ThreadPanel, - NotificationLevel.None} - > - - , - ); - if (this.state.notificationsEnabled) { - rightPanelPhaseButtons.set( - RightPanelPhases.NotificationPanel, - - {this.globalNotificationState.level === NotificationLevel.Highlight ? ( - - ) : null} - , - ); - } - rightPanelPhaseButtons.set( - RightPanelPhases.RoomSummary, - , - ); - - return ( - <> - {Array.from(rightPanelPhaseButtons.keys()).map((phase) => - this.props.excludedRightPanelPhaseButtons?.includes(phase) - ? null - : rightPanelPhaseButtons.get(phase), - )} - - ); - } -} diff --git a/src/components/views/rooms/LegacyRoomHeader.tsx b/src/components/views/rooms/LegacyRoomHeader.tsx deleted file mode 100644 index 0e4cd28aa8..0000000000 --- a/src/components/views/rooms/LegacyRoomHeader.tsx +++ /dev/null @@ -1,818 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2019-2021 The Matrix.org Foundation C.I.C. -Copyright 2015, 2016 OpenMarket Ltd - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import React, { FC, useState, useMemo, useCallback } from "react"; -import classNames from "classnames"; -import { throttle } from "lodash"; -import { RoomStateEvent } from "matrix-js-sdk/src/matrix"; -import { CallType } from "matrix-js-sdk/src/webrtc/call"; -import { IconButton, Tooltip } from "@vector-im/compound-web"; -import { ViewRoomOpts } from "@matrix-org/react-sdk-module-api/lib/lifecycles/RoomViewLifecycle"; - -import type { MatrixEvent, Room } from "matrix-js-sdk/src/matrix"; -import { _t } from "../../../languageHandler"; -import defaultDispatcher from "../../../dispatcher/dispatcher"; -import { Action } from "../../../dispatcher/actions"; -import { UserTab } from "../dialogs/UserTab"; -import SettingsStore from "../../../settings/SettingsStore"; -import RoomHeaderButtons from "../right_panel/LegacyRoomHeaderButtons"; -import E2EIcon from "./E2EIcon"; -import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar"; -import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton"; -import RoomTopic from "../elements/RoomTopic"; -import RoomName from "../elements/RoomName"; -import { E2EStatus } from "../../../utils/ShieldUtils"; -import { IOOBData } from "../../../stores/ThreepidInviteStore"; -import { RoomKnocksBar } from "./RoomKnocksBar"; -import { aboveLeftOf, ContextMenuTooltipButton, useContextMenu } from "../../structures/ContextMenu"; -import RoomContextMenu from "../context_menus/RoomContextMenu"; -import { contextMenuBelow } from "./RoomTile"; -import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore"; -import { RightPanelPhases } from "../../../stores/right-panel/RightPanelStorePhases"; -import { NotificationStateEvents } from "../../../stores/notifications/NotificationState"; -import RoomContext from "../../../contexts/RoomContext"; -import RoomLiveShareWarning from "../beacon/RoomLiveShareWarning"; -import { BetaPill } from "../beta/BetaCard"; -import RightPanelStore from "../../../stores/right-panel/RightPanelStore"; -import { UPDATE_EVENT } from "../../../stores/AsyncStore"; -import { isVideoRoom as calcIsVideoRoom } from "../../../utils/video-rooms"; -import LegacyCallHandler, { LegacyCallHandlerEvent } from "../../../LegacyCallHandler"; -import { useFeatureEnabled, useSettingValue } from "../../../hooks/useSettings"; -import SdkConfig from "../../../SdkConfig"; -import { useEventEmitterState, useTypedEventEmitterState } from "../../../hooks/useEventEmitter"; -import { useWidgets } from "../../../utils/WidgetUtils"; -import { WidgetType } from "../../../widgets/WidgetType"; -import { useCall, useLayout } from "../../../hooks/useCall"; -import { getJoinedNonFunctionalMembers } from "../../../utils/room/getJoinedNonFunctionalMembers"; -import { Call, ElementCall, Layout } from "../../../models/Call"; -import IconizedContextMenu, { - IconizedContextMenuOption, - IconizedContextMenuOptionList, - IconizedContextMenuRadio, -} from "../context_menus/IconizedContextMenu"; -import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload"; -import { SessionDuration } from "../voip/CallDuration"; -import RoomCallBanner from "../beacon/RoomCallBanner"; -import { shouldShowComponent } from "../../../customisations/helpers/UIComponents"; -import { UIComponent } from "../../../settings/UIFeature"; -import { SearchInfo } from "../../../Searching"; - -class DisabledWithReason { - public constructor(public readonly reason: string) {} -} - -interface VoiceCallButtonProps { - room: Room; - busy: boolean; - setBusy: (value: boolean) => void; - behavior: DisabledWithReason | "legacy_or_jitsi"; -} - -/** - * Button for starting voice calls, supporting only legacy 1:1 calls and Jitsi - * widgets. - */ -const VoiceCallButton: FC = ({ room, busy, setBusy, behavior }) => { - const { onClick, tooltip, disabled } = useMemo(() => { - if (behavior instanceof DisabledWithReason) { - return { - onClick: () => {}, - tooltip: behavior.reason, - disabled: true, - }; - } else { - // behavior === "legacy_or_jitsi" - return { - onClick: async (ev: ButtonEvent): Promise => { - ev.preventDefault(); - setBusy(true); - await LegacyCallHandler.instance.placeCall(room.roomId, CallType.Voice); - setBusy(false); - }, - disabled: false, - }; - } - }, [behavior, room, setBusy]); - - return ( - - ); -}; - -interface VideoCallButtonProps { - room: Room; - busy: boolean; - setBusy: (value: boolean) => void; - behavior: DisabledWithReason | "legacy_or_jitsi" | "element" | "jitsi_or_element" | "legacy_or_element"; -} - -/** - * Button for starting video calls, supporting both legacy 1:1 calls, Jitsi - * widgets, and native group calls. If multiple calling options are available, - * this shows a menu to pick between them. - */ -const VideoCallButton: FC = ({ room, busy, setBusy, behavior }) => { - const [menuOpen, buttonRef, openMenu, closeMenu] = useContextMenu(); - - const startLegacyCall = useCallback(async (): Promise => { - setBusy(true); - await LegacyCallHandler.instance.placeCall(room.roomId, CallType.Video); - setBusy(false); - }, [setBusy, room]); - - const startElementCall = useCallback( - (skipLobby: boolean) => { - setBusy(true); - defaultDispatcher.dispatch({ - action: Action.ViewRoom, - room_id: room.roomId, - view_call: true, - skipLobby: skipLobby, - metricsTrigger: undefined, - }); - setBusy(false); - }, - [setBusy, room], - ); - - const { onClick, tooltip, disabled } = useMemo(() => { - if (behavior instanceof DisabledWithReason) { - return { - onClick: () => {}, - tooltip: behavior.reason, - disabled: true, - }; - } else if (behavior === "legacy_or_jitsi") { - return { - onClick: async (ev: ButtonEvent): Promise => { - ev.preventDefault(); - await startLegacyCall(); - }, - disabled: false, - }; - } else if (behavior === "element") { - return { - onClick: async (ev: ButtonEvent): Promise => { - ev.preventDefault(); - startElementCall("shiftKey" in ev ? ev.shiftKey : false); - }, - disabled: false, - }; - } else { - // behavior === "jitsi_or_element" | "legacy_or_element" - return { - onClick: async (ev: ButtonEvent): Promise => { - ev.preventDefault(); - openMenu(); - }, - disabled: false, - }; - } - }, [behavior, startLegacyCall, startElementCall, openMenu]); - - const onJitsiClick = useCallback( - async (ev: ButtonEvent): Promise => { - ev.preventDefault(); - closeMenu(); - await startLegacyCall(); - }, - [closeMenu, startLegacyCall], - ); - - const onElementClick = useCallback( - (ev: ButtonEvent) => { - ev.preventDefault(); - closeMenu(); - startElementCall("shiftKey" in ev ? ev.shiftKey : false); - }, - [closeMenu, startElementCall], - ); - - let menu: JSX.Element | null = null; - if (menuOpen) { - const buttonRect = buttonRef.current!.getBoundingClientRect(); - const brand = SdkConfig.get("element_call").brand; - menu = ( - - - - - - - ); - } - - return ( - <> - - {menu} - - ); -}; - -interface CallButtonsProps { - room: Room; -} - -// The header buttons for placing calls have become stupidly complex, so here -// they are as a separate component -const CallButtons: FC = ({ room }) => { - const [busy, setBusy] = useState(false); - const showButtons = useSettingValue("showCallButtonsInComposer"); - const groupCallsEnabled = useFeatureEnabled("feature_group_calls"); - const isVideoRoom = useMemo(() => calcIsVideoRoom(room), [room]); - const useElementCallExclusively = useMemo(() => { - return SdkConfig.get("element_call").use_exclusively; - }, []); - - const hasLegacyCall = useEventEmitterState( - LegacyCallHandler.instance, - LegacyCallHandlerEvent.CallsChanged, - useCallback(() => LegacyCallHandler.instance.getCallForRoom(room.roomId) !== null, [room]), - ); - - const widgets = useWidgets(room); - const hasJitsiWidget = useMemo(() => widgets.some((widget) => WidgetType.JITSI.matches(widget.type)), [widgets]); - - const hasGroupCall = useCall(room.roomId) !== null; - - const [functionalMembers, mayEditWidgets, mayCreateElementCalls] = useTypedEventEmitterState( - room, - RoomStateEvent.Update, - useCallback( - () => [ - getJoinedNonFunctionalMembers(room), - room.currentState.mayClientSendStateEvent("im.vector.modular.widgets", room.client), - room.currentState.mayClientSendStateEvent(ElementCall.CALL_EVENT_TYPE.name, room.client), - ], - [room], - ), - ); - - const makeVoiceCallButton = (behavior: VoiceCallButtonProps["behavior"]): JSX.Element => ( - - ); - const makeVideoCallButton = (behavior: VideoCallButtonProps["behavior"]): JSX.Element => ( - - ); - - if (isVideoRoom || !showButtons) { - return null; - } else if (groupCallsEnabled && useElementCallExclusively) { - if (hasGroupCall) { - return makeVideoCallButton(new DisabledWithReason(_t("voip|disabled_ongoing_call"))); - } else if (mayCreateElementCalls) { - return makeVideoCallButton("element"); - } else { - return makeVideoCallButton(new DisabledWithReason(_t("voip|disabled_no_perms_start_video_call"))); - } - } else if (hasLegacyCall || hasJitsiWidget) { - return ( - <> - {makeVoiceCallButton(new DisabledWithReason(_t("voip|disabled_ongoing_call")))} - {makeVideoCallButton(new DisabledWithReason(_t("voip|disabled_ongoing_call")))} - - ); - } else if (functionalMembers.length <= 1) { - return ( - <> - {makeVoiceCallButton(new DisabledWithReason(_t("voip|disabled_no_one_here")))} - {makeVideoCallButton(new DisabledWithReason(_t("voip|disabled_no_one_here")))} - - ); - } else if (functionalMembers.length === 2) { - return ( - <> - {makeVoiceCallButton("legacy_or_jitsi")} - {makeVideoCallButton(groupCallsEnabled ? "legacy_or_element" : "legacy_or_jitsi")} - - ); - } else if (mayEditWidgets) { - return ( - <> - {makeVoiceCallButton("legacy_or_jitsi")} - {makeVideoCallButton( - groupCallsEnabled && mayCreateElementCalls ? "jitsi_or_element" : "legacy_or_jitsi", - )} - - ); - } else { - const videoCallBehavior = - groupCallsEnabled && mayCreateElementCalls - ? "element" - : new DisabledWithReason(_t("voip|disabled_no_perms_start_video_call")); - return ( - <> - {makeVoiceCallButton(new DisabledWithReason(_t("voip|disabled_no_perms_start_voice_call")))} - {makeVideoCallButton(videoCallBehavior)} - - ); - } -}; - -interface CallLayoutSelectorProps { - call: ElementCall; -} - -const CallLayoutSelector: FC = ({ call }) => { - const layout = useLayout(call); - const [menuOpen, buttonRef, openMenu, closeMenu] = useContextMenu(); - - const onClick = useCallback( - (ev: ButtonEvent) => { - ev.preventDefault(); - openMenu(); - }, - [openMenu], - ); - - const onFreedomClick = useCallback( - (ev: ButtonEvent) => { - ev.preventDefault(); - closeMenu(); - call.setLayout(Layout.Tile); - }, - [closeMenu, call], - ); - - const onSpotlightClick = useCallback( - (ev: ButtonEvent) => { - ev.preventDefault(); - closeMenu(); - call.setLayout(Layout.Spotlight); - }, - [closeMenu, call], - ); - - let menu: JSX.Element | null = null; - if (menuOpen) { - const buttonRect = buttonRef.current!.getBoundingClientRect(); - menu = ( - - - - - - - ); - } - - return ( - <> - - {menu} - - ); -}; - -export interface IProps { - room: Room; - oobData?: IOOBData; - inRoom: boolean; - onSearchClick: (() => void) | null; - onInviteClick: (() => void) | null; - onForgetClick: (() => void) | null; - onAppsClick: (() => void) | null; - e2eStatus: E2EStatus; - appsShown: boolean; - searchInfo?: SearchInfo; - excludedRightPanelPhaseButtons?: Array; - showButtons?: boolean; - enableRoomOptionsMenu?: boolean; - viewingCall: boolean; - activeCall: Call | null; - additionalButtons?: ViewRoomOpts["buttons"]; -} - -interface IState { - contextMenuPosition?: DOMRect; - rightPanelOpen: boolean; - featureAskToJoin: boolean; -} - -/** - * @deprecated use `src/components/views/rooms/RoomHeader.tsx` instead - */ -export default class RoomHeader extends React.Component { - public static defaultProps: Partial = { - inRoom: false, - excludedRightPanelPhaseButtons: [], - showButtons: true, - enableRoomOptionsMenu: true, - }; - - public static contextType = RoomContext; - public declare context: React.ContextType; - private readonly client = this.props.room.client; - private readonly featureAskToJoinWatcher: string; - - public constructor(props: IProps, context: React.ContextType) { - super(props, context); - const notiStore = RoomNotificationStateStore.instance.getRoomState(props.room); - notiStore.on(NotificationStateEvents.Update, this.onNotificationUpdate); - this.state = { - rightPanelOpen: RightPanelStore.instance.isOpen, - featureAskToJoin: SettingsStore.getValue("feature_ask_to_join"), - }; - this.featureAskToJoinWatcher = SettingsStore.watchSetting( - "feature_ask_to_join", - null, - (_settingName, _roomId, _atLevel, _newValAtLevel, featureAskToJoin) => { - this.setState({ featureAskToJoin }); - }, - ); - } - - public componentDidMount(): void { - this.client.on(RoomStateEvent.Events, this.onRoomStateEvents); - RightPanelStore.instance.on(UPDATE_EVENT, this.onRightPanelStoreUpdate); - } - - public componentWillUnmount(): void { - this.client.removeListener(RoomStateEvent.Events, this.onRoomStateEvents); - const notiStore = RoomNotificationStateStore.instance.getRoomState(this.props.room); - notiStore.removeListener(NotificationStateEvents.Update, this.onNotificationUpdate); - RightPanelStore.instance.off(UPDATE_EVENT, this.onRightPanelStoreUpdate); - SettingsStore.unwatchSetting(this.featureAskToJoinWatcher); - } - - private onRightPanelStoreUpdate = (): void => { - this.setState({ rightPanelOpen: RightPanelStore.instance.isOpen }); - }; - - private onRoomStateEvents = (event: MatrixEvent): void => { - if (!this.props.room || event.getRoomId() !== this.props.room.roomId) { - return; - } - - // redisplay the room name, topic, etc. - this.rateLimitedUpdate(); - }; - - private onNotificationUpdate = (): void => { - this.forceUpdate(); - }; - - private rateLimitedUpdate = throttle( - () => { - this.forceUpdate(); - }, - 500, - { leading: true, trailing: true }, - ); - - private onContextMenuOpenClick = (ev: ButtonEvent): void => { - ev.preventDefault(); - ev.stopPropagation(); - const target = ev.target as HTMLButtonElement; - this.setState({ contextMenuPosition: target.getBoundingClientRect() }); - }; - - private onContextMenuCloseClick = (): void => { - this.setState({ contextMenuPosition: undefined }); - }; - - private onHideCallClick = (ev: ButtonEvent): void => { - ev.preventDefault(); - defaultDispatcher.dispatch({ - action: Action.ViewRoom, - room_id: this.props.room.roomId, - view_call: false, - metricsTrigger: undefined, - }); - }; - - private renderButtons(isVideoRoom: boolean): React.ReactNode { - const startButtons: JSX.Element[] = []; - - if (!this.props.viewingCall && this.props.inRoom && !this.context.tombstone) { - startButtons.push(); - } - - if (this.props.viewingCall && this.props.activeCall instanceof ElementCall) { - startButtons.push(); - } - - if (!this.props.viewingCall && this.props.onForgetClick) { - startButtons.push( - , - ); - } - - if (!this.props.viewingCall && this.props.onAppsClick) { - startButtons.push( - , - ); - } - - if (!this.props.viewingCall && this.props.onSearchClick && this.props.inRoom) { - startButtons.push( - , - ); - } - - if (this.props.onInviteClick && (!this.props.viewingCall || isVideoRoom) && this.props.inRoom) { - startButtons.push( - , - ); - } - - const endButtons: JSX.Element[] = []; - - if (this.props.viewingCall && !isVideoRoom) { - if (this.props.activeCall === null) { - endButtons.push( - , - ); - } else { - endButtons.push( - , - ); - } - } - - return ( - <> - {this.props.additionalButtons?.map((props) => { - const label = props.label(); - - return ( - - { - props.onClick(); - this.forceUpdate(); - }} - title={label} - > - {typeof props.icon === "function" ? props.icon() : props.icon} - - - ); - })} - {startButtons} - - {endButtons} - - ); - } - - private renderName(oobName: string): JSX.Element { - let contextMenu: JSX.Element | null = null; - if (this.state.contextMenuPosition && this.props.room) { - contextMenu = ( - - ); - } - - // XXX: this is a bit inefficient - we could just compare room.name for 'Empty room'... - let settingsHint = false; - const members = this.props.room ? this.props.room.getJoinedMembers() : undefined; - if (members) { - if (members.length === 1 && members[0].userId === this.client.credentials.userId) { - const nameEvent = this.props.room.currentState.getStateEvents("m.room.name", ""); - if (!nameEvent || !nameEvent.getContent().name) { - settingsHint = true; - } - } - } - - const textClasses = classNames("mx_LegacyRoomHeader_nametext", { - mx_LegacyRoomHeader_settingsHint: settingsHint, - }); - const roomName = ( - - {(name) => { - const roomName = name || oobName; - return ( -
- {roomName} -
- ); - }} -
- ); - - if (this.props.enableRoomOptionsMenu && shouldShowComponent(UIComponent.RoomOptionsMenu)) { - return ( - - {roomName} - {this.props.room &&
} - {contextMenu} - - ); - } - - return
{roomName}
; - } - - public render(): React.ReactNode { - const isVideoRoom = calcIsVideoRoom(this.props.room); - - let roomAvatar: JSX.Element | null = null; - if (this.props.room) { - roomAvatar = ( - - ); - } - - const icon = this.props.viewingCall ? ( -
- ) : this.props.e2eStatus ? ( - - ) : // If we're expecting an E2EE status to come in, but it hasn't - // yet been loaded, insert a blank div to reserve space - this.client.isRoomEncrypted(this.props.room.roomId) && this.client.isCryptoEnabled() ? ( -
- ) : null; - - const buttons = this.props.showButtons ? this.renderButtons(isVideoRoom) : null; - - let oobName = _t("common|unnamed_room"); - if (this.props.oobData && this.props.oobData.name) { - oobName = this.props.oobData.name; - } - - const name = this.renderName(oobName); - - if (this.props.viewingCall && !isVideoRoom) { - return ( -
-
-
{roomAvatar}
- {icon} - {name} - {this.props.activeCall instanceof ElementCall && ( - - )} - {/* Empty topic element to fill out space */} -
- {buttons} -
-
- ); - } - - let searchStatus: JSX.Element | null = null; - - // don't display the search count until the search completes and - // gives us a valid (possibly zero) searchCount. - if (typeof this.props.searchInfo?.count === "number") { - searchStatus = ( -
-   - {_t("room|search|result_count", { count: this.props.searchInfo.count })} -
- ); - } - - const topicElement = ; - - const viewLabs = (): void => - defaultDispatcher.dispatch({ - action: Action.ViewUserSettings, - initialTabId: UserTab.Labs, - }); - const betaPill = isVideoRoom ? ( - - ) : null; - - return ( -
-
-
{roomAvatar}
- {icon} - {name} - {searchStatus} - {topicElement} - {betaPill} - {buttons} -
- {!isVideoRoom && } - - {this.state.featureAskToJoin && } -
- ); - } -} diff --git a/src/hooks/useCall.ts b/src/hooks/useCall.ts index 1fbe4d65e6..5a913f84ad 100644 --- a/src/hooks/useCall.ts +++ b/src/hooks/useCall.ts @@ -9,7 +9,7 @@ Please see LICENSE files in the repository root for full details. import { useState, useCallback, useMemo } from "react"; import type { RoomMember } from "matrix-js-sdk/src/matrix"; -import { Call, ConnectionState, ElementCall, Layout, CallEvent } from "../models/Call"; +import { Call, ConnectionState, CallEvent } from "../models/Call"; import { useTypedEventEmitterState, useEventEmitter } from "./useEventEmitter"; import { CallStore, CallStoreEvent } from "../stores/CallStore"; import SdkConfig, { DEFAULTS } from "../SdkConfig"; @@ -81,10 +81,3 @@ export const useJoinCallButtonDisabledTooltip = (call: Call | null): string | nu if (isFull) return _t("voip|join_button_tooltip_call_full"); return null; }; - -export const useLayout = (call: ElementCall): Layout => - useTypedEventEmitterState( - call, - CallEvent.Layout, - useCallback((state) => state ?? call.layout, [call]), - ); diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 9b608c9ab3..dc7bf30bbd 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1461,9 +1461,6 @@ "location_share_live_description": "Temporary implementation. Locations persist in room history.", "mjolnir": "New ways to ignore people", "msc3531_hide_messages_pending_moderation": "Let moderators hide messages pending moderation.", - "new_room_decoration_ui": "New room header", - "new_room_decoration_ui_beta_caption": "A new look for your rooms with a simpler, cleaner and more accessible room header.", - "new_room_decoration_ui_beta_title": "Room header", "notification_settings": "New Notification Settings", "notification_settings_beta_caption": "Introducing a simpler way to change your notification settings. Customize your %(brand)s, just the way you like.", "notification_settings_beta_title": "Notification Settings", @@ -1558,9 +1555,7 @@ "error_send_description": "%(brand)s could not send your location. Please try again later.", "error_send_title": "We couldn't send your location", "error_sharing_live_location": "An error occurred whilst sharing your live location", - "error_sharing_live_location_try_again": "An error occurred whilst sharing your live location, please try again", "error_stopping_live_location": "An error occurred while stopping your live location", - "error_stopping_live_location_try_again": "An error occurred while stopping your live location, please try again", "expand_map": "Expand map", "failed_generic": "Failed to fetch your location. Please try again later.", "failed_load_map": "Unable to load map", @@ -1590,7 +1585,6 @@ "share_type_own": "My current location", "share_type_pin": "Drop a Pin", "share_type_prompt": "What location type do you want to share?", - "stop_and_close": "Stop and close", "toggle_attribution": "Toggle attribution" }, "member_list": { @@ -1838,7 +1832,6 @@ "right_panel": { "add_integrations": "Add extensions", "add_topic": "Add topic", - "export_chat_button": "Export chat", "extensions_empty_description": "Select “%(addIntegrations)s” to browse and add extensions to this room", "extensions_empty_title": "Boost productivity with more tools, widgets and bots", "files_button": "Files", @@ -1861,7 +1854,6 @@ "title": "All new pinned messages" }, "reply_thread": "Reply to a thread message", - "title": "Pinned messages", "unpin_all": { "button": "Unpin all messages", "content": "Make sure that you really want to remove all pinned messages. This action can’t be undone.", @@ -1903,8 +1895,7 @@ }, "video_room_chat": { "title": "Chat" - }, - "widgets_section": "Widgets" + } }, "room": { "3pid_invite_email_not_found_account": "This invite was sent to %(email)s which is not associated with your account", @@ -1925,7 +1916,6 @@ "low_priority": "Low Priority", "mark_read": "Mark as read", "mark_unread": "Mark as unread", - "mentions_only": "Mentions only", "notifications_default": "Match default setting", "notifications_mute": "Mute room", "title": "Room options", @@ -1968,22 +1958,11 @@ "forget_room": "Forget this room", "forget_space": "Forget this space", "header": { - "close_call_button": "Close call", - "forget_room_button": "Forget room", - "hide_widgets_button": "Hide Widgets", "n_people_asking_to_join": { "one": "Asking to join", "other": "%(count)s people asking to join" }, - "room_is_public": "This room is public", - "show_widgets_button": "Show Widgets", - "video_call_button_ec": "Video call (%(brand)s)", - "video_call_button_jitsi": "Video call (Jitsi)", - "video_call_button_legacy": "Legacy video call", - "video_call_ec_change_layout": "Change layout", - "video_call_ec_layout_freedom": "Freedom", - "video_call_ec_layout_spotlight": "Spotlight", - "video_room_view_chat_button": "View chat timeline" + "room_is_public": "This room is public" }, "header_avatar_open_settings_label": "Open room settings", "header_face_pile_tooltip": "People", @@ -2077,10 +2056,6 @@ "search": { "all_rooms_button": "Search all rooms", "placeholder": "Search messages…", - "result_count": { - "one": "(~%(count)s result)", - "other": "(~%(count)s results)" - }, "summary": { "one": "1 result found for “”", "other": "%(count)s results found for “”" diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index 76bb109cac..298102332e 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -578,18 +578,6 @@ export const SETTINGS: { [setting: string]: ISetting } = { supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG_PRIORITISED, supportedLevelsAreOrdered: true, }, - "feature_new_room_decoration_ui": { - isFeature: true, - labsGroup: LabGroup.Rooms, - displayName: _td("labs|new_room_decoration_ui"), - supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG, - default: true, - controller: new ReloadOnChangeController(), - betaInfo: { - title: _td("labs|new_room_decoration_ui_beta_title"), - caption: () =>

{_t("labs|new_room_decoration_ui_beta_caption")}

, - }, - }, "feature_notifications": { isFeature: true, labsGroup: LabGroup.Messaging, diff --git a/test/components/structures/__snapshots__/RoomView-test.tsx.snap b/test/components/structures/__snapshots__/RoomView-test.tsx.snap index 5ee31e2207..25740fbd0b 100644 --- a/test/components/structures/__snapshots__/RoomView-test.tsx.snap +++ b/test/components/structures/__snapshots__/RoomView-test.tsx.snap @@ -1100,7 +1100,6 @@ exports[`RoomView should show error view if failed to look up room alias 1`] = `
", () => { - const aliceId = "@alice:server.org"; - const room1Id = "$room1:server.org"; - const room2Id = "$room2:server.org"; - const room3Id = "$room3:server.org"; - const mockClient = getMockClientWithEventEmitter({ - getVisibleRooms: jest.fn().mockReturnValue([]), - getUserId: jest.fn().mockReturnValue(aliceId), - getSafeUserId: jest.fn().mockReturnValue(aliceId), - unstable_setLiveBeacon: jest.fn().mockResolvedValue({ event_id: "1" }), - sendEvent: jest.fn(), - isGuest: jest.fn().mockReturnValue(false), - }); - - // 14.03.2022 16:15 - const now = 1647270879403; - const MINUTE_MS = 60000; - const HOUR_MS = 3600000; - // mock the date so events are stable for snapshots etc - jest.spyOn(global.Date, "now").mockReturnValue(now); - const room1Beacon1 = makeBeaconInfoEvent( - aliceId, - room1Id, - { - isLive: true, - timeout: HOUR_MS, - }, - "$0", - ); - const room2Beacon1 = makeBeaconInfoEvent(aliceId, room2Id, { isLive: true, timeout: HOUR_MS }, "$1"); - const room2Beacon2 = makeBeaconInfoEvent(aliceId, room2Id, { isLive: true, timeout: HOUR_MS * 12 }, "$2"); - const room3Beacon1 = makeBeaconInfoEvent(aliceId, room3Id, { isLive: true, timeout: HOUR_MS }, "$3"); - - // make fresh rooms every time - // as we update room state - const makeRoomsWithStateEvents = (stateEvents: MatrixEvent[] = []): [Room, Room] => { - const room1 = new Room(room1Id, mockClient, aliceId); - const room2 = new Room(room2Id, mockClient, aliceId); - - room1.currentState.setStateEvents(stateEvents); - room2.currentState.setStateEvents(stateEvents); - mockClient.getVisibleRooms.mockReturnValue([room1, room2]); - - return [room1, room2]; - }; - - const makeOwnBeaconStore = async () => { - const store = OwnBeaconStore.instance; - - await setupAsyncStoreWithClient(store, mockClient); - return store; - }; - - const defaultProps = { - roomId: room1Id, - }; - const getComponent = (props = {}) => { - return render(); - }; - - const localStorageSpy = jest.spyOn(localStorage.__proto__, "getItem").mockReturnValue(undefined); - - beforeEach(() => { - mockGeolocation(); - jest.spyOn(global.Date, "now").mockReturnValue(now); - mockClient.unstable_setLiveBeacon.mockReset().mockResolvedValue({ event_id: "1" }); - - // assume all beacons were created on this device - localStorageSpy.mockReturnValue( - JSON.stringify([room1Beacon1.getId(), room2Beacon1.getId(), room2Beacon2.getId(), room3Beacon1.getId()]), - ); - }); - - afterEach(async () => { - jest.spyOn(OwnBeaconStore.instance, "beaconHasLocationPublishError").mockRestore(); - await resetAsyncStoreWithClient(OwnBeaconStore.instance); - }); - - afterAll(() => { - jest.spyOn(global.Date, "now").mockRestore(); - localStorageSpy.mockRestore(); - jest.spyOn(defaultDispatcher, "dispatch").mockRestore(); - }); - - it("renders nothing when user has no live beacons at all", async () => { - await makeOwnBeaconStore(); - const { asFragment } = getComponent(); - expect(asFragment()).toMatchInlineSnapshot(``); - }); - - it("renders nothing when user has no live beacons in room", async () => { - await act(async () => { - await makeRoomsWithStateEvents([room2Beacon1]); - await makeOwnBeaconStore(); - }); - const { asFragment } = getComponent({ roomId: room1Id }); - expect(asFragment()).toMatchInlineSnapshot(``); - }); - - it("does not render when geolocation is not working", async () => { - jest.spyOn(logger, "error").mockImplementation(() => {}); - // @ts-ignore - navigator.geolocation = undefined; - await act(async () => { - await makeRoomsWithStateEvents([room1Beacon1, room2Beacon1, room2Beacon2]); - await makeOwnBeaconStore(); - }); - const { asFragment } = getComponent({ roomId: room1Id }); - - expect(asFragment()).toMatchInlineSnapshot(``); - }); - describe("when user has live beacons and geolocation is available", () => { - beforeEach(async () => { - await act(async () => { - await makeRoomsWithStateEvents([room1Beacon1, room2Beacon1, room2Beacon2]); - await makeOwnBeaconStore(); - }); - }); - - it("renders correctly with one live beacon in room", () => { - const { asFragment } = getComponent({ roomId: room1Id }); - // beacons have generated ids that break snapshots - // assert on html - expect(asFragment()).toMatchSnapshot(); - }); - - it("renders correctly with two live beacons in room", () => { - const { asFragment, container } = getComponent({ roomId: room2Id }); - // beacons have generated ids that break snapshots - // assert on html - expect(asFragment()).toMatchSnapshot(); - // later expiry displayed - expect(container).toHaveTextContent("12h left"); - }); - - it("removes itself when user stops having live beacons", async () => { - const { container } = getComponent({ roomId: room1Id }); - // started out rendered - expect(container.firstChild).toBeTruthy(); - - // time travel until room1Beacon1 is expired - act(() => { - advanceDateAndTime(HOUR_MS + 1); - }); - act(() => { - mockClient.emit(BeaconEvent.LivenessChange, false, new Beacon(room1Beacon1)); - }); - - await waitFor(() => expect(container.firstChild).toBeFalsy()); - }); - - it("removes itself when user stops monitoring live position", async () => { - const { container } = getComponent({ roomId: room1Id }); - // started out rendered - expect(container.firstChild).toBeTruthy(); - - act(() => { - // cheat to clear this - // @ts-ignore - OwnBeaconStore.instance.clearPositionWatch = undefined; - OwnBeaconStore.instance.emit(OwnBeaconStoreEvent.MonitoringLivePosition); - }); - - await waitFor(() => expect(container.firstChild).toBeFalsy()); - }); - - it("renders when user adds a live beacon", async () => { - const { container } = getComponent({ roomId: room3Id }); - // started out not rendered - expect(container.firstChild).toBeFalsy(); - act(() => { - mockClient.emit(BeaconEvent.New, room3Beacon1, new Beacon(room3Beacon1)); - }); - - await waitFor(() => expect(container.firstChild).toBeTruthy()); - }); - - it("updates beacon time left periodically", () => { - const { container } = getComponent({ roomId: room1Id }); - expect(container).toHaveTextContent("1h left"); - - act(() => { - advanceDateAndTime(MINUTE_MS * 25); - }); - - expect(container).toHaveTextContent("35m left"); - }); - - it("updates beacon time left when beacon updates", () => { - const { container } = getComponent({ roomId: room1Id }); - expect(container).toHaveTextContent("1h left"); - - act(() => { - const beacon = OwnBeaconStore.instance.getBeaconById(getBeaconInfoIdentifier(room1Beacon1)); - const room1Beacon1Update = makeBeaconInfoEvent( - aliceId, - room1Id, - { - isLive: true, - timeout: 3 * HOUR_MS, - }, - "$0", - ); - beacon?.update(room1Beacon1Update); - }); - - // update to expiry of new beacon - expect(container).toHaveTextContent("3h left"); - }); - - it("clears expiry time interval on unmount", () => { - const clearIntervalSpy = jest.spyOn(global, "clearInterval"); - const { container, unmount } = getComponent({ roomId: room1Id }); - expect(container).toHaveTextContent("1h left"); - - unmount(); - - expect(clearIntervalSpy).toHaveBeenCalled(); - }); - - it("navigates to beacon tile on click", () => { - const dispatcherSpy = jest.spyOn(defaultDispatcher, "dispatch"); - const { container } = getComponent({ roomId: room1Id }); - - act(() => { - fireEvent.click(container.firstChild! as Node); - }); - - expect(dispatcherSpy).toHaveBeenCalledWith({ - action: Action.ViewRoom, - event_id: room1Beacon1.getId(), - room_id: room1Id, - highlighted: true, - scroll_into_view: true, - metricsTrigger: undefined, - }); - }); - - describe("stopping beacons", () => { - it("stops beacon on stop sharing click", async () => { - const { container } = getComponent({ roomId: room2Id }); - - const btn = getByTestId(container, "room-live-share-primary-button"); - - fireEvent.click(btn); - - expect(mockClient.unstable_setLiveBeacon).toHaveBeenCalled(); - - await waitFor(() => expect(screen.queryByTestId("spinner")).toBeInTheDocument()); - - expect(btn.hasAttribute("disabled")).toBe(true); - }); - - it("displays error when stop sharing fails", async () => { - const { container, asFragment } = getComponent({ roomId: room1Id }); - const btn = getByTestId(container, "room-live-share-primary-button"); - - // fail first time - mockClient.unstable_setLiveBeacon - .mockRejectedValueOnce(new Error("oups")) - .mockResolvedValue({ event_id: "1" }); - - await act(async () => { - fireEvent.click(btn); - await flushPromisesWithFakeTimers(); - }); - - expect(asFragment()).toMatchSnapshot(); - - act(() => { - fireEvent.click(btn); - }); - - expect(mockClient.unstable_setLiveBeacon).toHaveBeenCalledTimes(2); - }); - - it("displays again with correct state after stopping a beacon", () => { - // make sure the loading state is reset correctly after removing a beacon - const { container } = getComponent({ roomId: room1Id }); - const btn = getByTestId(container, "room-live-share-primary-button"); - - // stop the beacon - act(() => { - fireEvent.click(btn); - }); - // time travel until room1Beacon1 is expired - act(() => { - advanceDateAndTime(HOUR_MS + 1); - }); - act(() => { - mockClient.emit(BeaconEvent.LivenessChange, false, new Beacon(room1Beacon1)); - }); - - const newLiveBeacon = makeBeaconInfoEvent(aliceId, room1Id, { isLive: true }); - act(() => { - mockClient.emit(BeaconEvent.New, newLiveBeacon, new Beacon(newLiveBeacon)); - }); - - // button not disabled and expiry time shown - expect(btn.hasAttribute("disabled")).toBe(true); - }); - }); - - describe("with location publish errors", () => { - it("displays location publish error when mounted with location publish errors", async () => { - const locationPublishErrorSpy = jest - .spyOn(OwnBeaconStore.instance, "beaconHasLocationPublishError") - .mockReturnValue(true); - const { asFragment } = getComponent({ roomId: room2Id }); - - expect(asFragment()).toMatchSnapshot(); - expect(locationPublishErrorSpy).toHaveBeenCalledWith(getBeaconInfoIdentifier(room2Beacon1), 0, [ - getBeaconInfoIdentifier(room2Beacon1), - ]); - }); - - it( - "displays location publish error when locationPublishError event is emitted" + - " and beacons have errors", - async () => { - const locationPublishErrorSpy = jest - .spyOn(OwnBeaconStore.instance, "beaconHasLocationPublishError") - .mockReturnValue(false); - const { container } = getComponent({ roomId: room2Id }); - - // update mock and emit event - act(() => { - locationPublishErrorSpy.mockReturnValue(true); - OwnBeaconStore.instance.emit( - OwnBeaconStoreEvent.LocationPublishError, - getBeaconInfoIdentifier(room2Beacon1), - ); - }); - - // renders wire error ui - expect(container).toHaveTextContent( - "An error occurred whilst sharing your live location, please try again", - ); - - expect(screen.queryByTestId("room-live-share-wire-error-close-button")).toBeInTheDocument(); - }, - ); - - it("stops displaying wire error when errors are cleared", async () => { - const locationPublishErrorSpy = jest - .spyOn(OwnBeaconStore.instance, "beaconHasLocationPublishError") - .mockReturnValue(true); - const { container } = getComponent({ roomId: room2Id }); - - // update mock and emit event - act(() => { - locationPublishErrorSpy.mockReturnValue(false); - OwnBeaconStore.instance.emit( - OwnBeaconStoreEvent.LocationPublishError, - getBeaconInfoIdentifier(room2Beacon1), - ); - }); - - // renders error-free ui - expect(container).toHaveTextContent("You are sharing your live location"); - expect(screen.queryByTestId("room-live-share-wire-error-close-button")).not.toBeInTheDocument(); - }); - - it("clicking retry button resets location publish errors", async () => { - jest.spyOn(OwnBeaconStore.instance, "beaconHasLocationPublishError").mockReturnValue(true); - const resetErrorSpy = jest.spyOn(OwnBeaconStore.instance, "resetLocationPublishError"); - - const { container } = getComponent({ roomId: room2Id }); - const btn = getByTestId(container, "room-live-share-primary-button"); - - act(() => { - fireEvent.click(btn); - }); - - expect(resetErrorSpy).toHaveBeenCalledWith(getBeaconInfoIdentifier(room2Beacon1)); - }); - - it("clicking close button stops beacons", async () => { - jest.spyOn(OwnBeaconStore.instance, "beaconHasLocationPublishError").mockReturnValue(true); - const stopBeaconSpy = jest.spyOn(OwnBeaconStore.instance, "stopBeacon"); - - const { container } = getComponent({ roomId: room2Id }); - const btn = getByTestId(container, "room-live-share-wire-error-close-button"); - act(() => { - fireEvent.click(btn); - }); - - expect(stopBeaconSpy).toHaveBeenCalledWith(getBeaconInfoIdentifier(room2Beacon1)); - }); - }); - }); -}); diff --git a/test/components/views/beacon/__snapshots__/RoomLiveShareWarning-test.tsx.snap b/test/components/views/beacon/__snapshots__/RoomLiveShareWarning-test.tsx.snap deleted file mode 100644 index c2bb48e33c..0000000000 --- a/test/components/views/beacon/__snapshots__/RoomLiveShareWarning-test.tsx.snap +++ /dev/null @@ -1,133 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[` when user has live beacons and geolocation is available renders correctly with one live beacon in room 1`] = ` - -
-
- - You are sharing your live location - - - 1h left - - -
- -`; - -exports[` when user has live beacons and geolocation is available renders correctly with two live beacons in room 1`] = ` - -
-
- - You are sharing your live location - - - 12h left - - -
- -`; - -exports[` when user has live beacons and geolocation is available stopping beacons displays error when stop sharing fails 1`] = ` - -
-
- - An error occurred while stopping your live location, please try again - - -
- -`; - -exports[` when user has live beacons and geolocation is available with location publish errors displays location publish error when mounted with location publish errors 1`] = ` - -
-
- - An error occurred whilst sharing your live location, please try again - - - -
- -`; diff --git a/test/components/views/context_menus/RoomContextMenu-test.tsx b/test/components/views/context_menus/RoomContextMenu-test.tsx deleted file mode 100644 index 3644d2c325..0000000000 --- a/test/components/views/context_menus/RoomContextMenu-test.tsx +++ /dev/null @@ -1,109 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2023 Mikhail Aheichyk -Copyright 2023 Nordeck IT + Consulting GmbH. -Copyright 2023 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { render, screen } from "@testing-library/react"; -import React, { ComponentProps } from "react"; -import { mocked } from "jest-mock"; -import { MatrixClient, PendingEventOrdering, Room } from "matrix-js-sdk/src/matrix"; -import { KnownMembership } from "matrix-js-sdk/src/types"; - -import MatrixClientContext from "../../../../src/contexts/MatrixClientContext"; -import RoomContextMenu from "../../../../src/components/views/context_menus/RoomContextMenu"; -import { shouldShowComponent } from "../../../../src/customisations/helpers/UIComponents"; -import { stubClient } from "../../../test-utils"; -import { MatrixClientPeg } from "../../../../src/MatrixClientPeg"; -import DMRoomMap from "../../../../src/utils/DMRoomMap"; -import SettingsStore from "../../../../src/settings/SettingsStore"; -import { EchoChamber } from "../../../../src/stores/local-echo/EchoChamber"; -import { RoomNotifState } from "../../../../src/RoomNotifs"; - -jest.mock("../../../../src/customisations/helpers/UIComponents", () => ({ - shouldShowComponent: jest.fn(), -})); - -describe("RoomContextMenu", () => { - const ROOM_ID = "!123:matrix.org"; - - let room: Room; - let mockClient: MatrixClient; - - let onFinished: () => void; - - beforeEach(() => { - jest.clearAllMocks(); - - stubClient(); - mockClient = mocked(MatrixClientPeg.safeGet()); - - room = new Room(ROOM_ID, mockClient, mockClient.getUserId() ?? "", { - pendingEventOrdering: PendingEventOrdering.Detached, - }); - - const dmRoomMap = { - getUserIdForRoomId: jest.fn(), - } as unknown as DMRoomMap; - DMRoomMap.setShared(dmRoomMap); - - onFinished = jest.fn(); - }); - - function renderComponent(props: Partial> = {}) { - render( - - - , - ); - } - - it("does not render invite menu item when UIComponent customisations disable invite", () => { - jest.spyOn(room, "canInvite").mockReturnValue(true); - mocked(shouldShowComponent).mockReturnValue(false); - - renderComponent(); - - expect(screen.queryByRole("menuitem", { name: "Invite" })).not.toBeInTheDocument(); - }); - - it("renders invite menu item when UIComponent customisations enable invite", () => { - jest.spyOn(room, "canInvite").mockReturnValue(true); - mocked(shouldShowComponent).mockReturnValue(true); - - renderComponent(); - - expect(screen.getByRole("menuitem", { name: "Invite" })).toBeInTheDocument(); - }); - - it("when developer mode is disabled, it should not render the developer tools option", () => { - renderComponent(); - expect(screen.queryByText("Developer tools")).not.toBeInTheDocument(); - }); - - describe("when developer mode is enabled", () => { - beforeEach(() => { - jest.spyOn(SettingsStore, "getValue").mockImplementation((setting) => setting === "developerMode"); - }); - - it("should render the developer tools option", () => { - renderComponent(); - expect(screen.getByText("Developer tools")).toBeInTheDocument(); - }); - }); - - it("should render notification option for joined rooms", () => { - const chamber = EchoChamber.forRoom(room); - chamber.notificationVolume = RoomNotifState.Mute; - jest.spyOn(room, "getMyMembership").mockReturnValue(KnownMembership.Join); - renderComponent(); - - expect( - screen.getByRole("menuitem", { name: "Notifications" }).querySelector(".mx_IconizedContextMenu_sublabel"), - ).toHaveTextContent("Mute"); - }); -}); diff --git a/test/components/views/right_panel/LegacyRoomHeaderButtons-test.tsx b/test/components/views/right_panel/LegacyRoomHeaderButtons-test.tsx deleted file mode 100644 index 89e9c330d0..0000000000 --- a/test/components/views/right_panel/LegacyRoomHeaderButtons-test.tsx +++ /dev/null @@ -1,187 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022, 2023 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import { render, waitFor } from "@testing-library/react"; -import { - MatrixEvent, - MsgType, - RelationType, - NotificationCountType, - Room, - MatrixClient, - PendingEventOrdering, - ReceiptType, -} from "matrix-js-sdk/src/matrix"; -import React from "react"; - -import LegacyRoomHeaderButtons from "../../../../src/components/views/right_panel/LegacyRoomHeaderButtons"; -import { MatrixClientPeg } from "../../../../src/MatrixClientPeg"; -import { mkEvent, stubClient } from "../../../test-utils"; -import { mkThread } from "../../../test-utils/threads"; - -describe("LegacyRoomHeaderButtons-test.tsx", function () { - const ROOM_ID = "!roomId:example.org"; - let room: Room; - let client: MatrixClient; - - beforeEach(() => { - jest.clearAllMocks(); - - stubClient(); - client = MatrixClientPeg.safeGet(); - client.supportsThreads = () => true; - room = new Room(ROOM_ID, client, client.getUserId() ?? "", { - pendingEventOrdering: PendingEventOrdering.Detached, - }); - }); - - function getComponent(room?: Room) { - return render(); - } - - function getThreadButton(container: HTMLElement) { - return container.querySelector(".mx_RightPanel_threadsButton"); - } - - function isIndicatorOfType(container: HTMLElement, type: "highlight" | "notification" | "activity") { - return container.querySelector(".mx_RightPanel_threadsButton .mx_Indicator")!.className.includes(type); - } - - it("should render", () => { - const { asFragment } = getComponent(room); - expect(asFragment()).toMatchSnapshot(); - }); - - it("shows the thread button", () => { - const { container } = getComponent(room); - expect(getThreadButton(container)).not.toBeNull(); - }); - - it("room wide notification does not change the thread button", () => { - room.setUnreadNotificationCount(NotificationCountType.Highlight, 1); - room.setUnreadNotificationCount(NotificationCountType.Total, 1); - - const { container } = getComponent(room); - - expect(container.querySelector(".mx_RightPanel_threadsButton .mx_Indicator")).toBeNull(); - }); - - it("thread notification does change the thread button", async () => { - const { container } = getComponent(room); - expect(getThreadButton(container)!.className.includes("mx_LegacyRoomHeader_button--unread")).toBeFalsy(); - - room.setThreadUnreadNotificationCount("$123", NotificationCountType.Total, 1); - await waitFor(() => { - expect(getThreadButton(container)!.className.includes("mx_LegacyRoomHeader_button--unread")).toBeTruthy(); - expect(isIndicatorOfType(container, "notification")).toBe(true); - }); - - room.setThreadUnreadNotificationCount("$123", NotificationCountType.Highlight, 1); - await waitFor(() => expect(isIndicatorOfType(container, "highlight")).toBe(true)); - - room.setThreadUnreadNotificationCount("$123", NotificationCountType.Total, 0); - room.setThreadUnreadNotificationCount("$123", NotificationCountType.Highlight, 0); - - await waitFor(() => expect(container.querySelector(".mx_RightPanel_threadsButton .mx_Indicator")).toBeNull()); - }); - - it("thread activity does change the thread button", async () => { - const { container } = getComponent(room); - - // Thread activity should appear on the icon. - const { rootEvent, events } = mkThread({ - room, - client, - authorId: client.getUserId()!, - participantUserIds: ["@alice:example.org"], - length: 5, - }); - // We need some receipt, otherwise we treat this thread as - // "older than all threaded receipts" and consider it read. - let receipt = new MatrixEvent({ - type: "m.receipt", - room_id: room.roomId, - content: { - [events[1].getId()!]: { - // Receipt for the first event in the thread - [ReceiptType.Read]: { - [client.getUserId()!]: { ts: 1, thread_id: rootEvent.getId() }, - }, - }, - }, - }); - room.addReceipt(receipt); - await waitFor(() => expect(isIndicatorOfType(container, "activity")).toBe(true)); - - // Sending the last event should clear the notification. - let event = mkEvent({ - event: true, - type: "m.room.message", - user: client.getUserId()!, - room: room.roomId, - content: { - "msgtype": MsgType.Text, - "body": "Test", - "m.relates_to": { - event_id: rootEvent.getId(), - rel_type: RelationType.Thread, - }, - }, - }); - room.addLiveEvents([event]); - await waitFor(() => expect(container.querySelector(".mx_RightPanel_threadsButton .mx_Indicator")).toBeNull()); - - // Mark it as unread again. - event = mkEvent({ - event: true, - type: "m.room.message", - user: "@alice:example.org", - room: room.roomId, - content: { - "msgtype": MsgType.Text, - "body": "Test", - "m.relates_to": { - event_id: rootEvent.getId(), - rel_type: RelationType.Thread, - }, - }, - }); - room.addLiveEvents([event]); - await waitFor(() => expect(isIndicatorOfType(container, "activity")).toBe(true)); - - // Sending a read receipt on an earlier event shouldn't do anything. - receipt = new MatrixEvent({ - type: "m.receipt", - room_id: room.roomId, - content: { - [events.at(-1)!.getId()!]: { - [ReceiptType.Read]: { - [client.getUserId()!]: { ts: 1, thread_id: rootEvent.getId() }, - }, - }, - }, - }); - room.addReceipt(receipt); - await waitFor(() => expect(isIndicatorOfType(container, "activity")).toBe(true)); - - // Sending a receipt on the latest event should clear the notification. - receipt = new MatrixEvent({ - type: "m.receipt", - room_id: room.roomId, - content: { - [event.getId()!]: { - [ReceiptType.Read]: { - [client.getUserId()!]: { ts: 1, thread_id: rootEvent.getId() }, - }, - }, - }, - }); - room.addReceipt(receipt); - await waitFor(() => expect(container.querySelector(".mx_RightPanel_threadsButton .mx_Indicator")).toBeNull()); - }); -}); diff --git a/test/components/views/right_panel/__snapshots__/LegacyRoomHeaderButtons-test.tsx.snap b/test/components/views/right_panel/__snapshots__/LegacyRoomHeaderButtons-test.tsx.snap deleted file mode 100644 index 6dbe9ed2e3..0000000000 --- a/test/components/views/right_panel/__snapshots__/LegacyRoomHeaderButtons-test.tsx.snap +++ /dev/null @@ -1,28 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`LegacyRoomHeaderButtons-test.tsx should render 1`] = ` - -
-
-
- -`; diff --git a/test/components/views/rooms/LegacyRoomHeader-test.tsx b/test/components/views/rooms/LegacyRoomHeader-test.tsx deleted file mode 100644 index 16dbefbab0..0000000000 --- a/test/components/views/rooms/LegacyRoomHeader-test.tsx +++ /dev/null @@ -1,917 +0,0 @@ -/* -Copyright 2024 New Vector Ltd. -Copyright 2022 The Matrix.org Foundation C.I.C. - -SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only -Please see LICENSE files in the repository root for full details. -*/ - -import React from "react"; -import { render, screen, act, fireEvent, waitFor, getByRole, RenderResult } from "@testing-library/react"; -import { mocked, Mocked } from "jest-mock"; -import { - EventType, - RoomType, - Room, - RoomStateEvent, - PendingEventOrdering, - ISearchResults, - IContent, -} from "matrix-js-sdk/src/matrix"; -import { KnownMembership } from "matrix-js-sdk/src/types"; -import { CallType } from "matrix-js-sdk/src/webrtc/call"; -import { ClientWidgetApi, Widget } from "matrix-widget-api"; -import EventEmitter from "events"; -import { setupJestCanvasMock } from "jest-canvas-mock"; -import { ViewRoomOpts } from "@matrix-org/react-sdk-module-api/lib/lifecycles/RoomViewLifecycle"; -import { MatrixRTCSession, MatrixRTCSessionManagerEvents } from "matrix-js-sdk/src/matrixrtc"; - -import type { MatrixClient, MatrixEvent, RoomMember } from "matrix-js-sdk/src/matrix"; -import type { MatrixCall } from "matrix-js-sdk/src/webrtc/call"; -import { - stubClient, - mkRoomMember, - setupAsyncStoreWithClient, - resetAsyncStoreWithClient, - mockPlatformPeg, - mkEvent, - filterConsole, -} from "../../../test-utils"; -import { MatrixClientPeg } from "../../../../src/MatrixClientPeg"; -import DMRoomMap from "../../../../src/utils/DMRoomMap"; -import RoomHeader, { IProps as RoomHeaderProps } from "../../../../src/components/views/rooms/LegacyRoomHeader"; -import { E2EStatus } from "../../../../src/utils/ShieldUtils"; -import { IRoomState } from "../../../../src/components/structures/RoomView"; -import RoomContext from "../../../../src/contexts/RoomContext"; -import SdkConfig from "../../../../src/SdkConfig"; -import SettingsStore from "../../../../src/settings/SettingsStore"; -import { ElementCall, JitsiCall } from "../../../../src/models/Call"; -import { CallStore } from "../../../../src/stores/CallStore"; -import LegacyCallHandler from "../../../../src/LegacyCallHandler"; -import defaultDispatcher from "../../../../src/dispatcher/dispatcher"; -import { Action } from "../../../../src/dispatcher/actions"; -import WidgetStore from "../../../../src/stores/WidgetStore"; -import { WidgetMessagingStore } from "../../../../src/stores/widgets/WidgetMessagingStore"; -import MediaDeviceHandler, { MediaDeviceKindEnum } from "../../../../src/MediaDeviceHandler"; -import { shouldShowComponent } from "../../../../src/customisations/helpers/UIComponents"; -import { UIComponent } from "../../../../src/settings/UIFeature"; -import WidgetUtils from "../../../../src/utils/WidgetUtils"; -import { ElementWidgetActions } from "../../../../src/stores/widgets/ElementWidgetActions"; -import { SearchScope } from "../../../../src/Searching"; - -jest.mock("../../../../src/customisations/helpers/UIComponents", () => ({ - shouldShowComponent: jest.fn(), -})); - -describe("LegacyRoomHeader", () => { - let client: Mocked; - let room: Room; - let alice: RoomMember; - let bob: RoomMember; - let carol: RoomMember; - - filterConsole( - "Age for event was not available, using `now - origin_server_ts` as a fallback. If the device clock is not correct issues might occur.", - ); - - beforeEach(async () => { - // some of our tests rely on the jest canvas mock, and `afterEach` will have reset the mock, so we need to - // restore it. - setupJestCanvasMock(); - - mockPlatformPeg({ supportsJitsiScreensharing: () => true }); - - stubClient(); - client = mocked(MatrixClientPeg.safeGet()); - client.getUserId.mockReturnValue("@alice:example.org"); - - room = new Room("!1:example.org", client, "@alice:example.org", { - pendingEventOrdering: PendingEventOrdering.Detached, - }); - room.currentState.setStateEvents([mkCreationEvent(room.roomId, "@alice:example.org")]); - - client.getRoom.mockImplementation((roomId) => (roomId === room.roomId ? room : null)); - client.getRooms.mockReturnValue([room]); - client.reEmitter.reEmit(room, [RoomStateEvent.Events]); - client.sendStateEvent.mockImplementation(async (roomId, eventType, content, stateKey = "") => { - if (roomId !== room.roomId) throw new Error("Unknown room"); - const event = mkEvent({ - event: true, - type: eventType, - room: roomId, - user: alice.userId, - skey: stateKey, - content: content as IContent, - }); - room.addLiveEvents([event]); - return { event_id: event.getId()! }; - }); - - alice = mkRoomMember(room.roomId, "@alice:example.org"); - bob = mkRoomMember(room.roomId, "@bob:example.org"); - carol = mkRoomMember(room.roomId, "@carol:example.org"); - - client.getRoom.mockImplementation((roomId) => (roomId === room.roomId ? room : null)); - client.getRooms.mockReturnValue([room]); - client.reEmitter.reEmit(room, [RoomStateEvent.Events]); - - await Promise.all( - [CallStore.instance, WidgetStore.instance].map((store) => setupAsyncStoreWithClient(store, client)), - ); - - jest.spyOn(MediaDeviceHandler, "getDevices").mockResolvedValue({ - [MediaDeviceKindEnum.AudioInput]: [], - [MediaDeviceKindEnum.VideoInput]: [], - [MediaDeviceKindEnum.AudioOutput]: [], - }); - - DMRoomMap.makeShared(client); - jest.spyOn(DMRoomMap.shared(), "getUserIdForRoomId").mockReturnValue(carol.userId); - }); - - afterEach(async () => { - await Promise.all([CallStore.instance, WidgetStore.instance].map(resetAsyncStoreWithClient)); - client.reEmitter.stopReEmitting(room, [RoomStateEvent.Events]); - jest.restoreAllMocks(); - SdkConfig.reset(); - }); - - const mockRoomType = (type: string) => { - jest.spyOn(room, "getType").mockReturnValue(type); - }; - const mockRoomMembers = (members: RoomMember[]) => { - jest.spyOn(room, "getJoinedMembers").mockReturnValue(members); - jest.spyOn(room, "getMember").mockImplementation( - (userId) => members.find((member) => member.userId === userId) ?? null, - ); - }; - const mockEnabledSettings = (settings: string[]) => { - jest.spyOn(SettingsStore, "getValue").mockImplementation((settingName) => settings.includes(settingName)); - }; - const mockEventPowerLevels = (events: { [eventType: string]: number }) => { - room.currentState.setStateEvents([ - mkEvent({ - event: true, - type: EventType.RoomPowerLevels, - room: room.roomId, - user: alice.userId, - skey: "", - content: { events, state_default: 0 }, - }), - ]); - }; - const mockLegacyCall = () => { - jest.spyOn(LegacyCallHandler.instance, "getCallForRoom").mockReturnValue({} as unknown as MatrixCall); - }; - const withCall = async (fn: (call: ElementCall) => void | Promise): Promise => { - await ElementCall.create(room); - const call = CallStore.instance.getCall(room.roomId); - if (!(call instanceof ElementCall)) throw new Error("Failed to create call"); - - const widget = new Widget(call.widget); - - const eventEmitter = new EventEmitter(); - const messaging = { - on: eventEmitter.on.bind(eventEmitter), - off: eventEmitter.off.bind(eventEmitter), - once: eventEmitter.once.bind(eventEmitter), - emit: eventEmitter.emit.bind(eventEmitter), - stop: jest.fn(), - transport: { - send: jest.fn(), - reply: jest.fn(), - }, - } as unknown as Mocked; - WidgetMessagingStore.instance.storeMessaging(widget, call.roomId, messaging); - - await fn(call); - - call.destroy(); - WidgetMessagingStore.instance.stopMessaging(widget, call.roomId); - }; - - const renderHeader = (props: Partial = {}, roomContext: Partial = {}) => { - render( - - {}} - onInviteClick={null} - onForgetClick={() => {}} - onAppsClick={() => {}} - e2eStatus={E2EStatus.Normal} - appsShown={true} - searchInfo={{ - searchId: Math.random(), - promise: new Promise(() => {}), - term: "", - scope: SearchScope.Room, - count: 0, - }} - viewingCall={false} - activeCall={null} - {...props} - /> - , - ); - }; - - it("hides call buttons in video rooms", () => { - mockRoomType(RoomType.UnstableCall); - mockEnabledSettings(["showCallButtonsInComposer", "feature_video_rooms", "feature_element_call_video_rooms"]); - - renderHeader(); - expect(screen.queryByRole("button", { name: /call/i })).toBeNull(); - }); - - it("hides call buttons if showCallButtonsInComposer is disabled", () => { - mockEnabledSettings([]); - - renderHeader(); - expect(screen.queryByRole("button", { name: /call/i })).toBeNull(); - }); - - it( - "hides the voice call button and disables the video call button if configured to use Element Call exclusively " + - "and there's an ongoing call", - async () => { - mockEnabledSettings(["showCallButtonsInComposer", "feature_group_calls"]); - SdkConfig.put({ - element_call: { url: "https://call.element.io", use_exclusively: true, brand: "Element Call" }, - }); - await ElementCall.create(room); - - renderHeader(); - expect(screen.queryByRole("button", { name: "Voice call" })).toBeNull(); - expect(screen.getByRole("button", { name: "Video call" })).toHaveAttribute("aria-disabled", "true"); - }, - ); - - it( - "hides the voice call button and starts an Element call when the video call button is pressed if configured to " + - "use Element Call exclusively", - async () => { - mockEnabledSettings(["showCallButtonsInComposer", "feature_group_calls"]); - SdkConfig.put({ - element_call: { url: "https://call.element.io", use_exclusively: true, brand: "Element Call" }, - }); - - renderHeader(); - expect(screen.queryByRole("button", { name: "Voice call" })).toBeNull(); - - const dispatcherSpy = jest.fn(); - const dispatcherRef = defaultDispatcher.register(dispatcherSpy); - fireEvent.click(screen.getByRole("button", { name: "Video call" })); - await waitFor(() => - expect(dispatcherSpy).toHaveBeenCalledWith({ - action: Action.ViewRoom, - room_id: room.roomId, - skipLobby: false, - view_call: true, - }), - ); - defaultDispatcher.unregister(dispatcherRef); - }, - ); - - it( - "hides the voice call button and disables the video call button if configured to use Element Call exclusively " + - "and the user lacks permission", - () => { - mockEnabledSettings(["showCallButtonsInComposer", "feature_group_calls"]); - SdkConfig.put({ - element_call: { url: "https://call.element.io", use_exclusively: true, brand: "Element Call" }, - }); - mockEventPowerLevels({ [ElementCall.CALL_EVENT_TYPE.name]: 100 }); - - renderHeader(); - expect(screen.queryByRole("button", { name: "Voice call" })).toBeNull(); - expect(screen.getByRole("button", { name: "Video call" })).toHaveAttribute("aria-disabled", "true"); - }, - ); - - it("disables call buttons in the new group call experience if there's an ongoing Element call", async () => { - mockEnabledSettings(["showCallButtonsInComposer", "feature_group_calls"]); - await ElementCall.create(room); - - renderHeader(); - expect(screen.getByRole("button", { name: "Voice call" })).toHaveAttribute("aria-disabled", "true"); - expect(screen.getByRole("button", { name: "Video call" })).toHaveAttribute("aria-disabled", "true"); - }); - - it("disables call buttons in the new group call experience if there's an ongoing legacy 1:1 call", () => { - mockEnabledSettings(["showCallButtonsInComposer", "feature_group_calls"]); - mockLegacyCall(); - - renderHeader(); - expect(screen.getByRole("button", { name: "Voice call" })).toHaveAttribute("aria-disabled", "true"); - expect(screen.getByRole("button", { name: "Video call" })).toHaveAttribute("aria-disabled", "true"); - }); - - it("disables call buttons in the new group call experience if there's an existing Jitsi widget", async () => { - mockEnabledSettings(["showCallButtonsInComposer", "feature_group_calls"]); - await JitsiCall.create(room); - - renderHeader(); - expect(screen.getByRole("button", { name: "Voice call" })).toHaveAttribute("aria-disabled", "true"); - expect(screen.getByRole("button", { name: "Video call" })).toHaveAttribute("aria-disabled", "true"); - }); - - it("disables call buttons in the new group call experience if there's no other members", () => { - mockEnabledSettings(["showCallButtonsInComposer", "feature_group_calls"]); - - renderHeader(); - expect(screen.getByRole("button", { name: "Voice call" })).toHaveAttribute("aria-disabled", "true"); - expect(screen.getByRole("button", { name: "Video call" })).toHaveAttribute("aria-disabled", "true"); - }); - - it( - "starts a legacy 1:1 call when call buttons are pressed in the new group call experience if there's 1 other " + - "member", - async () => { - mockEnabledSettings(["showCallButtonsInComposer", "feature_group_calls"]); - mockRoomMembers([alice, bob]); - - renderHeader(); - - const placeCallSpy = jest.spyOn(LegacyCallHandler.instance, "placeCall").mockResolvedValue(undefined); - fireEvent.click(screen.getByRole("button", { name: "Voice call" })); - await act(() => Promise.resolve()); // Allow effects to settle - expect(placeCallSpy).toHaveBeenCalledWith(room.roomId, CallType.Voice); - - placeCallSpy.mockClear(); - fireEvent.click(screen.getByRole("button", { name: "Video call" })); - await act(() => Promise.resolve()); // Allow effects to settle - fireEvent.click(screen.getByRole("menuitem", { name: "Legacy video call" })); - await act(() => Promise.resolve()); // Allow effects to settle - expect(placeCallSpy).toHaveBeenCalledWith(room.roomId, CallType.Video); - }, - ); - - it( - "creates a Jitsi widget when call buttons are pressed in the new group call experience if the user lacks " + - "permission to start Element calls", - async () => { - mockEnabledSettings(["showCallButtonsInComposer", "feature_group_calls"]); - mockRoomMembers([alice, bob, carol]); - mockEventPowerLevels({ [ElementCall.CALL_EVENT_TYPE.name]: 100 }); - - renderHeader(); - - const placeCallSpy = jest.spyOn(LegacyCallHandler.instance, "placeCall").mockResolvedValue(undefined); - fireEvent.click(screen.getByRole("button", { name: "Voice call" })); - await act(() => Promise.resolve()); // Allow effects to settle - expect(placeCallSpy).toHaveBeenCalledWith(room.roomId, CallType.Voice); - - placeCallSpy.mockClear(); - fireEvent.click(screen.getByRole("button", { name: "Video call" })); - await act(() => Promise.resolve()); // Allow effects to settle - expect(placeCallSpy).toHaveBeenCalledWith(room.roomId, CallType.Video); - }, - ); - - it( - "creates a Jitsi widget when the voice call button is pressed and shows a menu when the video call button is " + - "pressed in the new group call experience", - async () => { - mockEnabledSettings(["showCallButtonsInComposer", "feature_group_calls"]); - mockRoomMembers([alice, bob, carol]); - - renderHeader(); - - const placeCallSpy = jest.spyOn(LegacyCallHandler.instance, "placeCall").mockResolvedValue(undefined); - fireEvent.click(screen.getByRole("button", { name: "Voice call" })); - await act(() => Promise.resolve()); // Allow effects to settle - expect(placeCallSpy).toHaveBeenCalledWith(room.roomId, CallType.Voice); - - // First try creating a Jitsi widget from the menu - placeCallSpy.mockClear(); - fireEvent.click(screen.getByRole("button", { name: "Video call" })); - fireEvent.click(getByRole(screen.getByRole("menu"), "menuitem", { name: /jitsi/i })); - await act(() => Promise.resolve()); // Allow effects to settle - expect(placeCallSpy).toHaveBeenCalledWith(room.roomId, CallType.Video); - - // Then try starting an Element call from the menu - const dispatcherSpy = jest.fn(); - const dispatcherRef = defaultDispatcher.register(dispatcherSpy); - fireEvent.click(screen.getByRole("button", { name: "Video call" })); - fireEvent.click(getByRole(screen.getByRole("menu"), "menuitem", { name: /element/i })); - await waitFor(() => - expect(dispatcherSpy).toHaveBeenCalledWith({ - action: Action.ViewRoom, - room_id: room.roomId, - skipLobby: false, - view_call: true, - }), - ); - defaultDispatcher.unregister(dispatcherRef); - }, - ); - - it( - "disables the voice call button and starts an Element call when the video call button is pressed in the new " + - "group call experience if the user lacks permission to edit widgets", - async () => { - mockEnabledSettings(["showCallButtonsInComposer", "feature_group_calls"]); - mockRoomMembers([alice, bob, carol]); - mockEventPowerLevels({ "im.vector.modular.widgets": 100 }); - - renderHeader(); - expect(screen.getByRole("button", { name: "Voice call" })).toHaveAttribute("aria-disabled", "true"); - - const dispatcherSpy = jest.fn(); - const dispatcherRef = defaultDispatcher.register(dispatcherSpy); - fireEvent.click(screen.getByRole("button", { name: "Video call" })); - await waitFor(() => - expect(dispatcherSpy).toHaveBeenCalledWith({ - action: Action.ViewRoom, - room_id: room.roomId, - skipLobby: false, - view_call: true, - }), - ); - defaultDispatcher.unregister(dispatcherRef); - }, - ); - - it("disables call buttons in the new group call experience if the user lacks permission", () => { - mockEnabledSettings(["showCallButtonsInComposer", "feature_group_calls"]); - mockRoomMembers([alice, bob, carol]); - mockEventPowerLevels({ [ElementCall.CALL_EVENT_TYPE.name]: 100, "im.vector.modular.widgets": 100 }); - - renderHeader(); - expect(screen.getByRole("button", { name: "Voice call" })).toHaveAttribute("aria-disabled", "true"); - expect(screen.getByRole("button", { name: "Video call" })).toHaveAttribute("aria-disabled", "true"); - }); - - it("disables call buttons if there's an ongoing legacy 1:1 call", () => { - mockEnabledSettings(["showCallButtonsInComposer"]); - mockLegacyCall(); - - renderHeader(); - expect(screen.getByRole("button", { name: "Voice call" })).toHaveAttribute("aria-disabled", "true"); - expect(screen.getByRole("button", { name: "Video call" })).toHaveAttribute("aria-disabled", "true"); - }); - - it("disables call buttons if there's an existing Jitsi widget", async () => { - mockEnabledSettings(["showCallButtonsInComposer"]); - await JitsiCall.create(room); - - renderHeader(); - expect(screen.getByRole("button", { name: "Voice call" })).toHaveAttribute("aria-disabled", "true"); - expect(screen.getByRole("button", { name: "Video call" })).toHaveAttribute("aria-disabled", "true"); - }); - - it("disables call buttons if there's no other members", () => { - mockEnabledSettings(["showCallButtonsInComposer"]); - - renderHeader(); - expect(screen.getByRole("button", { name: "Voice call" })).toHaveAttribute("aria-disabled", "true"); - expect(screen.getByRole("button", { name: "Video call" })).toHaveAttribute("aria-disabled", "true"); - }); - - it("starts a legacy 1:1 call when call buttons are pressed if there's 1 other member", async () => { - mockEnabledSettings(["showCallButtonsInComposer"]); - mockRoomMembers([alice, bob]); - mockEventPowerLevels({ "im.vector.modular.widgets": 100 }); // Just to verify that it doesn't try to use Jitsi - - renderHeader(); - - const placeCallSpy = jest.spyOn(LegacyCallHandler.instance, "placeCall").mockResolvedValue(undefined); - fireEvent.click(screen.getByRole("button", { name: "Voice call" })); - await act(() => Promise.resolve()); // Allow effects to settle - expect(placeCallSpy).toHaveBeenCalledWith(room.roomId, CallType.Voice); - - placeCallSpy.mockClear(); - fireEvent.click(screen.getByRole("button", { name: "Video call" })); - await act(() => Promise.resolve()); // Allow effects to settle - expect(placeCallSpy).toHaveBeenCalledWith(room.roomId, CallType.Video); - }); - - it("creates a Jitsi widget when call buttons are pressed", async () => { - mockEnabledSettings(["showCallButtonsInComposer"]); - mockRoomMembers([alice, bob, carol]); - - renderHeader(); - - const placeCallSpy = jest.spyOn(LegacyCallHandler.instance, "placeCall").mockResolvedValue(undefined); - fireEvent.click(screen.getByRole("button", { name: "Voice call" })); - await act(() => Promise.resolve()); // Allow effects to settle - expect(placeCallSpy).toHaveBeenCalledWith(room.roomId, CallType.Voice); - - placeCallSpy.mockClear(); - fireEvent.click(screen.getByRole("button", { name: "Video call" })); - await act(() => Promise.resolve()); // Allow effects to settle - expect(placeCallSpy).toHaveBeenCalledWith(room.roomId, CallType.Video); - }); - - it("disables call buttons if the user lacks permission", () => { - mockEnabledSettings(["showCallButtonsInComposer"]); - mockRoomMembers([alice, bob, carol]); - mockEventPowerLevels({ "im.vector.modular.widgets": 100 }); - - renderHeader(); - expect(screen.getByRole("button", { name: "Voice call" })).toHaveAttribute("aria-disabled", "true"); - expect(screen.getByRole("button", { name: "Video call" })).toHaveAttribute("aria-disabled", "true"); - }); - - it("shows a close button when viewing a call lobby that returns to the timeline when pressed", async () => { - mockEnabledSettings(["feature_group_calls"]); - - renderHeader({ viewingCall: true }); - - const dispatcherSpy = jest.fn(); - const dispatcherRef = defaultDispatcher.register(dispatcherSpy); - fireEvent.click(screen.getByRole("button", { name: /close/i })); - await waitFor(() => - expect(dispatcherSpy).toHaveBeenCalledWith({ - action: Action.ViewRoom, - room_id: room.roomId, - view_call: false, - }), - ); - defaultDispatcher.unregister(dispatcherRef); - }); - - it("shows a reduce button when viewing a call that returns to the timeline when pressed", async () => { - mockEnabledSettings(["feature_group_calls"]); - - await withCall(async (call) => { - renderHeader({ viewingCall: true, activeCall: call }); - - const dispatcherSpy = jest.fn(); - const dispatcherRef = defaultDispatcher.register(dispatcherSpy); - fireEvent.click(screen.getByRole("button", { name: /timeline/i })); - await waitFor(() => - expect(dispatcherSpy).toHaveBeenCalledWith({ - action: Action.ViewRoom, - room_id: room.roomId, - view_call: false, - }), - ); - defaultDispatcher.unregister(dispatcherRef); - }); - }); - - it("shows a layout button when viewing a call that shows a menu when pressed", async () => { - mockEnabledSettings(["feature_group_calls"]); - - await withCall(async (call) => { - // We set the call to skip lobby because otherwise the connection will wait until - // the user clicks the "join" button, inside the widget lobby which is hard to mock. - call.widget.data = { ...call.widget.data, skipLobby: true }; - // The connect method will wait until the session actually connected. Otherwise it will timeout. - // Emitting SessionStarted will trigger the connect method to resolve. - setTimeout( - () => - client.matrixRTC.emit(MatrixRTCSessionManagerEvents.SessionStarted, room.roomId, { - room, - } as MatrixRTCSession), - 100, - ); - await call.start(); - - const messaging = WidgetMessagingStore.instance.getMessagingForUid(WidgetUtils.getWidgetUid(call.widget))!; - renderHeader({ viewingCall: true, activeCall: call }); - - // Should start with Freedom selected - fireEvent.click(screen.getByRole("button", { name: /layout/i })); - screen.getByRole("menuitemradio", { name: "Freedom", checked: true }); - - // Clicking Spotlight should tell the widget to switch and close the menu - fireEvent.click(screen.getByRole("menuitemradio", { name: "Spotlight" })); - expect(mocked(messaging.transport).send).toHaveBeenCalledWith(ElementWidgetActions.SpotlightLayout, {}); - expect(screen.queryByRole("menu")).toBeNull(); - - // When the widget responds and the user reopens the menu, they should see Spotlight selected - act(() => { - messaging.emit( - `action:${ElementWidgetActions.SpotlightLayout}`, - new CustomEvent("widgetapirequest", { detail: { data: {} } }), - ); - }); - fireEvent.click(screen.getByRole("button", { name: /layout/i })); - screen.getByRole("menuitemradio", { name: "Spotlight", checked: true }); - - // Now try switching back to Freedom - fireEvent.click(screen.getByRole("menuitemradio", { name: "Freedom" })); - expect(mocked(messaging.transport).send).toHaveBeenCalledWith(ElementWidgetActions.TileLayout, {}); - expect(screen.queryByRole("menu")).toBeNull(); - - // When the widget responds and the user reopens the menu, they should see Freedom selected - act(() => { - messaging.emit( - `action:${ElementWidgetActions.TileLayout}`, - new CustomEvent("widgetapirequest", { detail: { data: {} } }), - ); - }); - fireEvent.click(screen.getByRole("button", { name: /layout/i })); - screen.getByRole("menuitemradio", { name: "Freedom", checked: true }); - }); - }); - - it("shows an invite button in video rooms", () => { - mockEnabledSettings(["feature_video_rooms", "feature_element_call_video_rooms"]); - mockRoomType(RoomType.UnstableCall); - - const onInviteClick = jest.fn(); - renderHeader({ onInviteClick, viewingCall: true }); - - fireEvent.click(screen.getByRole("button", { name: /invite/i })); - expect(onInviteClick).toHaveBeenCalled(); - }); - - it("hides the invite button in non-video rooms when viewing a call", () => { - renderHeader({ onInviteClick: () => {}, viewingCall: true }); - - expect(screen.queryByRole("button", { name: /invite/i })).toBeNull(); - }); - - it("shows the room avatar in a room with only ourselves", () => { - // When we render a non-DM room with 1 person in it - const room = createRoom({ name: "X Room", isDm: false, userIds: [] }); - const rendered = mountHeader(room); - - // Then the room's avatar is the initial of its name - const initial = rendered.container.querySelector(".mx_BaseAvatar"); - expect(initial).toHaveTextContent("X"); - }); - - it("shows the room avatar in a room with 2 people", () => { - // When we render a non-DM room with 2 people in it - const room = createRoom({ name: "Y Room", isDm: false, userIds: ["other"] }); - const rendered = mountHeader(room); - - // Then the room's avatar is the initial of its name - const initial = rendered.container.querySelector(".mx_BaseAvatar"); - expect(initial).toHaveTextContent("Y"); - }); - - it("shows the room avatar in a room with >2 people", () => { - // When we render a non-DM room with 3 people in it - const room = createRoom({ name: "Z Room", isDm: false, userIds: ["other1", "other2"] }); - const rendered = mountHeader(room); - - // Then the room's avatar is the initial of its name - const initial = rendered.container.querySelector(".mx_BaseAvatar"); - expect(initial).toHaveTextContent("Z"); - }); - - it("shows the room avatar in a DM with only ourselves", () => { - // When we render a non-DM room with 1 person in it - const room = createRoom({ name: "Z Room", isDm: true, userIds: [] }); - const rendered = mountHeader(room); - - // Then the room's avatar is the initial of its name - const initial = rendered.container.querySelector(".mx_BaseAvatar"); - expect(initial).toHaveTextContent("Z"); - }); - - it("shows the user avatar in a DM with 2 people", () => { - // Note: this is the interesting case - this is the ONLY - // time we should use the user's avatar. - - // When we render a DM room with only 2 people in it - const room = createRoom({ name: "Y Room", isDm: true, userIds: ["other"] }); - const rendered = mountHeader(room); - - // Then we use the other user's avatar as our room's image avatar - const image = rendered.container.querySelector(".mx_BaseAvatar img"); - expect(image).toHaveAttribute("src", "http://this.is.a.url/example.org/other"); - }); - - it("shows the room avatar in a DM with >2 people", () => { - // When we render a DM room with 3 people in it - const room = createRoom({ - name: "Z Room", - isDm: true, - userIds: ["other1", "other2"], - }); - const rendered = mountHeader(room); - - // Then the room's avatar is the initial of its name - const initial = rendered.container.querySelector(".mx_BaseAvatar"); - expect(initial).toHaveTextContent("Z"); - }); - - it("renders call buttons normally", () => { - const room = createRoom({ name: "Room", isDm: false, userIds: ["other"] }); - const wrapper = mountHeader(room); - - expect(wrapper.container.querySelector('[aria-label="Voice call"]')).toBeDefined(); - expect(wrapper.container.querySelector('[aria-label="Video call"]')).toBeDefined(); - }); - - it("hides call buttons when the room is tombstoned", () => { - const room = createRoom({ name: "Room", isDm: false, userIds: [] }); - const wrapper = mountHeader( - room, - {}, - { - tombstone: mkEvent({ - event: true, - type: "m.room.tombstone", - room: room.roomId, - user: "@user1:server", - skey: "", - content: {}, - ts: Date.now(), - }), - }, - ); - - expect(wrapper.container.querySelector('[aria-label="Voice call"]')).toBeFalsy(); - expect(wrapper.container.querySelector('[aria-label="Video call"]')).toBeFalsy(); - }); - - it("should render buttons if not passing showButtons (default true)", () => { - const room = createRoom({ name: "Room", isDm: false, userIds: [] }); - const wrapper = mountHeader(room); - expect(wrapper.container.querySelector(".mx_LegacyRoomHeader_button")).toBeDefined(); - }); - - it("should not render buttons if passing showButtons = false", () => { - const room = createRoom({ name: "Room", isDm: false, userIds: [] }); - const wrapper = mountHeader(room, { showButtons: false }); - expect(wrapper.container.querySelector(".mx_LegacyRoomHeader_button")).toBeFalsy(); - }); - - it("should render the room options context menu if not passing enableRoomOptionsMenu (default true) and UIComponent customisations room options enabled", () => { - mocked(shouldShowComponent).mockReturnValue(true); - const room = createRoom({ name: "Room", isDm: false, userIds: [] }); - const wrapper = mountHeader(room); - expect(shouldShowComponent).toHaveBeenCalledWith(UIComponent.RoomOptionsMenu); - expect(wrapper.container.querySelector(".mx_LegacyRoomHeader_name.mx_AccessibleButton")).toBeDefined(); - }); - - it.each([ - [false, true], - [true, false], - ])( - "should not render the room options context menu if passing enableRoomOptionsMenu = %s and UIComponent customisations room options enable = %s", - (enableRoomOptionsMenu, showRoomOptionsMenu) => { - mocked(shouldShowComponent).mockReturnValue(showRoomOptionsMenu); - const room = createRoom({ name: "Room", isDm: false, userIds: [] }); - const wrapper = mountHeader(room, { enableRoomOptionsMenu }); - expect(wrapper.container.querySelector(".mx_LegacyRoomHeader_name.mx_AccessibleButton")).toBeFalsy(); - }, - ); - - it("renders additionalButtons", async () => { - const additionalButtons: ViewRoomOpts["buttons"] = [ - { - icon: () => <>test-icon, - id: "test-id", - label: () => "test-label", - onClick: () => {}, - }, - ]; - renderHeader({ additionalButtons }); - expect(screen.getByRole("button", { name: "test-icon" })).toBeInTheDocument(); - }); - - it("calls onClick-callback on additionalButtons", () => { - const callback = jest.fn(); - const additionalButtons: ViewRoomOpts["buttons"] = [ - { - icon: () => <>test-icon, - id: "test-id", - label: () => "test-label", - onClick: callback, - }, - ]; - renderHeader({ additionalButtons }); - fireEvent.click(screen.getByRole("button", { name: "test-icon" })); - expect(callback).toHaveBeenCalled(); - }); -}); - -interface IRoomCreationInfo { - name: string; - isDm: boolean; - userIds: string[]; -} - -function createRoom(info: IRoomCreationInfo) { - stubClient(); - const client: MatrixClient = MatrixClientPeg.safeGet(); - - const roomId = "!1234567890:domain"; - const userId = client.getUserId()!; - if (info.isDm) { - client.getAccountData = (eventType) => { - if (eventType === "m.direct") { - return mkDirectEvent(roomId, userId, info.userIds); - } else { - return undefined; - } - }; - } - - DMRoomMap.makeShared(client).start(); - - const room = new Room(roomId, client, userId, { - pendingEventOrdering: PendingEventOrdering.Detached, - }); - - const otherJoinEvents: MatrixEvent[] = []; - for (const otherUserId of info.userIds) { - otherJoinEvents.push(mkJoinEvent(roomId, otherUserId)); - } - - room.currentState.setStateEvents([ - mkCreationEvent(roomId, userId), - mkNameEvent(roomId, userId, info.name), - mkJoinEvent(roomId, userId), - ...otherJoinEvents, - ]); - room.recalculate(); - - return room; -} - -function mountHeader(room: Room, propsOverride = {}, roomContext?: Partial): RenderResult { - const props: RoomHeaderProps = { - room, - inRoom: true, - onSearchClick: () => {}, - onInviteClick: null, - onForgetClick: () => {}, - onAppsClick: () => {}, - e2eStatus: E2EStatus.Normal, - appsShown: true, - searchInfo: { - searchId: Math.random(), - promise: new Promise(() => {}), - term: "", - scope: SearchScope.Room, - count: 0, - }, - viewingCall: false, - activeCall: null, - ...propsOverride, - }; - - return render( - - - , - ); -} - -function mkCreationEvent(roomId: string, userId: string): MatrixEvent { - return mkEvent({ - event: true, - type: "m.room.create", - room: roomId, - user: userId, - content: { - creator: userId, - room_version: "5", - predecessor: { - room_id: "!prevroom", - event_id: "$someevent", - }, - }, - }); -} - -function mkNameEvent(roomId: string, userId: string, name: string): MatrixEvent { - return mkEvent({ - event: true, - type: "m.room.name", - room: roomId, - user: userId, - content: { name }, - }); -} - -function mkJoinEvent(roomId: string, userId: string) { - const ret = mkEvent({ - event: true, - type: "m.room.member", - room: roomId, - user: userId, - content: { - membership: KnownMembership.Join, - avatar_url: "mxc://example.org/" + userId, - }, - }); - ret.event.state_key = userId; - return ret; -} - -function mkDirectEvent(roomId: string, userId: string, otherUsers: string[]): MatrixEvent { - const content: Record = {}; - for (const otherUserId of otherUsers) { - content[otherUserId] = [roomId]; - } - return mkEvent({ - event: true, - type: "m.direct", - room: roomId, - user: userId, - content, - }); -} diff --git a/test/components/views/settings/tabs/user/__snapshots__/LabsUserSettingsTab-test.tsx.snap b/test/components/views/settings/tabs/user/__snapshots__/LabsUserSettingsTab-test.tsx.snap index 51ab465503..4e65f7b61f 100644 --- a/test/components/views/settings/tabs/user/__snapshots__/LabsUserSettingsTab-test.tsx.snap +++ b/test/components/views/settings/tabs/user/__snapshots__/LabsUserSettingsTab-test.tsx.snap @@ -129,61 +129,6 @@ exports[` renders settings marked as beta as beta cards 1
-
-
-
-

- - Room header - - - Beta - -

-
-

- A new look for your rooms with a simpler, cleaner and more accessible room header. -

-
-
-
- Join the beta -
-
-
- Joining the beta will reload BrandedClient. -
-
-
- -
-
-
`;