Remove legacy room header and promote beta room header (#105)

* Remove legacy room header and promote beta room header

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Tidy up

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Remove unused component

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Prune i18n

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
Michael Telatynski 2024-10-02 13:10:58 +01:00 committed by GitHub
parent e60d3bd1ee
commit 8a263ac1b0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 16 additions and 3769 deletions

View file

@ -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 (
<div className="mx_RoomView mx_RoomView--local">
<ErrorBoundary>
{SettingsStore.getValue("feature_new_room_decoration_ui") ? (
<RoomHeader room={room} />
) : (
<LegacyRoomHeader
room={context.room}
searchInfo={undefined}
inRoom={true}
onSearchClick={null}
onInviteClick={null}
onForgetClick={null}
e2eStatus={room.encrypted ? E2EStatus.Normal : undefined}
onAppsClick={null}
appsShown={false}
excludedRightPanelPhaseButtons={[]}
showButtons={false}
enableRoomOptionsMenu={false}
viewingCall={false}
activeCall={null}
/>
)}
<RoomHeader room={room} />
<main className="mx_RoomView_body" ref={props.roomView}>
<FileDropTarget parent={props.roomView.current} onFileDrop={props.onFileDrop} />
<div className="mx_RoomView_timeline">
@ -366,26 +346,7 @@ function LocalRoomCreateLoader(props: ILocalRoomCreateLoaderProps): ReactElement
return (
<div className="mx_RoomView mx_RoomView--local">
<ErrorBoundary>
{SettingsStore.getValue("feature_new_room_decoration_ui") ? (
<RoomHeader room={props.localRoom} />
) : (
<LegacyRoomHeader
room={props.localRoom}
searchInfo={undefined}
inRoom={true}
onSearchClick={null}
onInviteClick={null}
onForgetClick={null}
e2eStatus={props.localRoom.encrypted ? E2EStatus.Normal : undefined}
onAppsClick={null}
appsShown={false}
excludedRightPanelPhaseButtons={[]}
showButtons={false}
enableRoomOptionsMenu={false}
viewingCall={false}
activeCall={null}
/>
)}
<RoomHeader room={props.localRoom} />
<div className="mx_RoomView_body">
<LargeLoader text={text} />
</div>
@ -1753,13 +1714,6 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
});
};
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<IRoomProps, IRoomState> {
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<IRoomProps, IRoomState> {
}
}
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 (
<div className="mx_RoomView" data-room-header={roomHeaderType}>
<div className="mx_RoomView">
<ErrorBoundary>
<RoomPreviewBar
canPreview={false}
@ -2154,7 +2102,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
// We've got to this room by following a link, possibly a third party invite.
const roomAlias = this.state.roomAlias;
return (
<div className="mx_RoomView" data-room-header={roomHeaderType}>
<div className="mx_RoomView">
<ErrorBoundary>
<RoomPreviewBar
onJoinClick={this.onJoinButtonClicked}
@ -2224,7 +2172,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
// We have a regular invite for this room.
return (
<div className="mx_RoomView" data-room-header={roomHeaderType}>
<div className="mx_RoomView">
<ErrorBoundary>
<RoomPreviewBar
onJoinClick={this.onJoinButtonClicked}
@ -2248,7 +2196,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
([KnownMembership.Knock, KnownMembership.Leave] as Array<string>).includes(myMembership)
) {
return (
<div className="mx_RoomView" data-room-header={roomHeaderType}>
<div className="mx_RoomView">
<ErrorBoundary>
<RoomPreviewBar
onJoinClick={this.onJoinButtonClicked}
@ -2354,11 +2302,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
/>
);
if (!this.state.canPeek && !this.state.room?.isSpaceRoom()) {
return (
<div className="mx_RoomView" data-room-header={roomHeaderType}>
{previewBar}
</div>
);
return <div className="mx_RoomView">{previewBar}</div>;
}
} else if (hiddenHighlightCount > 0) {
aux = (
@ -2587,46 +2531,9 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
}
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<string>).includes(myMembership) ||
myMember?.isKicked());
return (
<RoomContext.Provider value={this.state}>
<div
className={mainClasses}
ref={this.roomView}
onKeyDown={this.onReactKeyDown}
data-room-header={roomHeaderType}
>
<div className={mainClasses} ref={this.roomView} onKeyDown={this.onReactKeyDown}>
{showChatEffects && this.roomView.current && (
<EffectsOverlay roomWidth={this.roomView.current.offsetWidth} />
)}
@ -2644,31 +2551,10 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
ref={this.roomViewBody}
data-layout={this.state.layout}
>
{SettingsStore.getValue("feature_new_room_decoration_ui") ? (
<RoomHeader
room={this.state.room}
additionalButtons={this.state.viewRoomOpts.buttons}
/>
) : (
<LegacyRoomHeader
room={this.state.room}
searchInfo={this.state.search}
oobData={this.props.oobData}
inRoom={myMembership === KnownMembership.Join}
onSearchClick={onSearchClick}
onInviteClick={onInviteClick}
onForgetClick={showForgetButton ? onForgetClick : null}
e2eStatus={this.state.e2eStatus}
onAppsClick={this.state.hasPinnedWidgets ? onAppsClick : null}
appsShown={this.state.showApps}
excludedRightPanelPhaseButtons={excludedRightPanelPhaseButtons}
showButtons={!this.viewsLocalRoom}
enableRoomOptionsMenu={!this.viewsLocalRoom}
viewingCall={viewingCall}
activeCall={this.state.activeCall}
additionalButtons={this.state.viewRoomOpts.buttons}
/>
)}
<RoomHeader
room={this.state.room}
additionalButtons={this.state.viewRoomOpts.buttons}
/>
{mainSplitBody}
</div>
</MainSplit>

View file

@ -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<HTMLElement>;
@ -41,24 +38,7 @@ export const WaitingForThirdPartyRoomView: React.FC<Props> = ({ roomView, resize
return (
<div className="mx_RoomView mx_RoomView--local">
<ErrorBoundary>
{SettingsStore.getValue("feature_new_room_decoration_ui") ? (
<RoomHeader room={context.room!} />
) : (
<LegacyRoomHeader
room={context.room}
inRoom={true}
onInviteClick={null}
onForgetClick={null}
e2eStatus={E2EStatus.Normal}
onAppsClick={null}
appsShown={false}
excludedRightPanelPhaseButtons={[]}
showButtons={false}
enableRoomOptionsMenu={false}
viewingCall={false}
activeCall={null}
/>
)}
<RoomHeader room={context.room!} />
<main className="mx_RoomView_body" ref={roomView}>
<div className="mx_RoomView_timeline">
<ScrollPanel className="mx_RoomView_messagePanel" resizeNotifier={resizeNotifier}>

View file

@ -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<RoomLiveShareWarningInnerProps> = ({ 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<ViewRoomPayload>({
action: Action.ViewRoom,
room_id: beacon.roomId,
metricsTrigger: undefined,
event_id: beacon.beaconInfoId,
scroll_into_view: true,
highlighted: true,
});
};
return (
<div className="mx_RoomLiveShareWarning" onClick={onClick}>
<StyledLiveBeaconIcon className="mx_RoomLiveShareWarning_icon" withError={hasError} />
<span className="mx_RoomLiveShareWarning_label">
{getLabel(hasLocationPublishError, hasStopSharingError)}
</span>
{stoppingInProgress && (
<span className="mx_RoomLiveShareWarning_spinner">
<Spinner h={16} w={16} />
</span>
)}
{!stoppingInProgress && !hasError && <LiveTimeRemaining beacon={beacon} />}
<AccessibleButton
className="mx_RoomLiveShareWarning_stopButton"
data-testid="room-live-share-primary-button"
onClick={stopPropagationWrapper(onButtonClick)}
kind="danger"
element="button"
disabled={stoppingInProgress}
>
{hasError ? _t("action|retry") : _t("action|stop")}
</AccessibleButton>
{hasLocationPublishError && (
<AccessibleButton
data-testid="room-live-share-wire-error-close-button"
title={_t("location_sharing|stop_and_close")}
element="button"
className="mx_RoomLiveShareWarning_closeButton"
onClick={stopPropagationWrapper(onStopSharing)}
>
<CloseIcon className="mx_RoomLiveShareWarning_closeButtonIcon" />
</AccessibleButton>
)}
</div>
);
};
interface Props {
roomId: Room["roomId"];
}
const RoomLiveShareWarning: React.FC<Props> = ({ 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 <RoomLiveShareWarningInner liveBeaconIds={liveBeaconIds} roomId={roomId} />;
};
export default RoomLiveShareWarning;

View file

@ -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<IProps> = ({ 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 = (
<IconizedContextMenuOption
iconClassName="mx_RoomTile_iconSignOut"
label={_t("room|context_menu|forget")}
className="mx_IconizedContextMenu_option_red"
onClick={onForgetRoomClick}
/>
);
} 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 = (
<IconizedContextMenuOption
onClick={onLeaveRoomClick}
label={_t("action|leave")}
className="mx_IconizedContextMenu_option_red"
iconClassName="mx_RoomTile_iconSignOut"
/>
);
}
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 = (
<IconizedContextMenuOption
onClick={onInviteClick}
label={_t("action|invite")}
iconClassName="mx_RoomTile_iconInvite"
/>
);
}
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 = (
<IconizedContextMenuCheckbox
onClick={(e) => {
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 = (
<IconizedContextMenuCheckbox
onClick={(e) => 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 = (
<IconizedContextMenuOption
onClick={(ev: ButtonEvent) => {
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}
>
<span className="mx_IconizedContextMenu_sublabel">{notificationLabel}</span>
</IconizedContextMenuOption>
);
}
let peopleOption: JSX.Element | undefined;
let copyLinkOption: JSX.Element | undefined;
if (!isDm) {
peopleOption = (
<IconizedContextMenuOption
onClick={(ev: ButtonEvent) => {
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"
>
<span className="mx_IconizedContextMenu_sublabel">{room.getJoinedMemberCount()}</span>
</IconizedContextMenuOption>
);
copyLinkOption = (
<IconizedContextMenuOption
onClick={(ev: ButtonEvent) => {
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 = (
<IconizedContextMenuOption
onClick={(ev: ButtonEvent) => {
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 = (
<IconizedContextMenuOption
onClick={(ev: ButtonEvent) => {
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 && <span className="mx_IconizedContextMenu_sublabel">{pinCount}</span>}
</IconizedContextMenuOption>
);
}
let widgetsOption: JSX.Element | undefined;
if (!isVideoRoom) {
widgetsOption = (
<IconizedContextMenuOption
onClick={(ev: ButtonEvent) => {
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 = (
<IconizedContextMenuOption
onClick={(ev: ButtonEvent) => {
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<ViewRoomPayload>(
{
action: Action.ViewRoom,
room_id: room.roomId,
metricsTrigger: "RoomList",
metricsViaKeyboard: ev.type !== "click",
},
true,
);
};
return (
<IconizedContextMenu {...props} onFinished={onFinished} className="mx_RoomTile_contextMenu" compact>
<IconizedContextMenuOptionList>
{inviteOption}
{notificationOption}
{favouriteOption}
{peopleOption}
{filesOption}
{pinsOption}
{widgetsOption}
{lowPriorityOption}
{copyLinkOption}
<IconizedContextMenuOption
onClick={(ev: ButtonEvent) => {
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") && (
<DeveloperToolsOption onFinished={onFinished} roomId={room.roomId} />
)}
{leaveOption}
</IconizedContextMenuOptionList>
</IconizedContextMenu>
);
};
export default RoomContextMenu;

View file

@ -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<IUnreadIndicatorProps> = ({ 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 (
<>
<div className="mx_LegacyRoomHeader_button_unreadIndicator_bg" />
<div className={classes} />
</>
);
};
interface IHeaderButtonProps {
room: Room;
isHighlighted: boolean;
onClick: () => void;
}
const PinnedMessagesHeaderButton: React.FC<IHeaderButtonProps> = ({ 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 = <UnreadIndicator />;
}
return (
<HeaderButton
name="pinnedMessagesButton"
title={_t("right_panel|pinned_messages|title")}
isHighlighted={isHighlighted}
isUnread={!!unreadIndicator}
onClick={onClick}
>
{unreadIndicator}
</HeaderButton>
);
};
const TimelineCardHeaderButton: React.FC<IHeaderButtonProps> = ({ room, isHighlighted, onClick }) => {
let unreadIndicator;
const color = RoomNotificationStateStore.instance.getRoomState(room).level;
switch (color) {
case NotificationLevel.Activity:
case NotificationLevel.Notification:
case NotificationLevel.Highlight:
unreadIndicator = <UnreadIndicator color={color} />;
}
return (
<HeaderButton
name="timelineCardButton"
title={_t("right_panel|video_room_chat|title")}
isHighlighted={isHighlighted}
onClick={onClick}
>
{unreadIndicator}
</HeaderButton>
);
};
interface IProps {
room?: Room;
excludedRightPanelPhaseButtons?: Array<RightPanelPhases>;
}
/**
* @deprecated will be removed as part of 'feature_new_room_decoration_ui'
*/
export default class LegacyRoomHeaderButtons extends HeaderButtons<IProps> {
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<RightPanelPhases, any> = new Map();
rightPanelPhaseButtons.set(
RightPanelPhases.PinnedMessages,
<PinnedMessagesHeaderButton
key="pinnedMessagesButton"
room={this.props.room}
isHighlighted={this.isPhase(RightPanelPhases.PinnedMessages)}
onClick={this.onPinnedMessagesClicked}
/>,
);
rightPanelPhaseButtons.set(
RightPanelPhases.Timeline,
<TimelineCardHeaderButton
key="timelineButton"
room={this.props.room}
isHighlighted={this.isPhase(RightPanelPhases.Timeline)}
onClick={this.onTimelineCardClicked}
/>,
);
rightPanelPhaseButtons.set(
RightPanelPhases.ThreadPanel,
<HeaderButton
key={RightPanelPhases.ThreadPanel}
name="threadsButton"
data-testid="threadsButton"
title={_t("common|threads")}
onClick={this.onThreadsPanelClicked}
isHighlighted={this.isPhase(LegacyRoomHeaderButtons.THREAD_PHASES)}
isUnread={this.state.threadNotificationLevel > NotificationLevel.None}
>
<UnreadIndicator color={this.state.threadNotificationLevel} />
</HeaderButton>,
);
if (this.state.notificationsEnabled) {
rightPanelPhaseButtons.set(
RightPanelPhases.NotificationPanel,
<HeaderButton
key="notifsButton"
name="notifsButton"
title={_t("notifications|enable_prompt_toast_title")}
isHighlighted={this.isPhase(RightPanelPhases.NotificationPanel)}
onClick={this.onNotificationsClicked}
isUnread={this.globalNotificationState.level === NotificationLevel.Highlight}
>
{this.globalNotificationState.level === NotificationLevel.Highlight ? (
<UnreadIndicator color={this.globalNotificationState.level} />
) : null}
</HeaderButton>,
);
}
rightPanelPhaseButtons.set(
RightPanelPhases.RoomSummary,
<HeaderButton
key="roomSummaryButton"
name="roomSummaryButton"
title={_t("right_panel|room_summary_card|title")}
isHighlighted={this.isPhase(ROOM_INFO_PHASES)}
onClick={this.onRoomSummaryClicked}
/>,
);
return (
<>
{Array.from(rightPanelPhaseButtons.keys()).map((phase) =>
this.props.excludedRightPanelPhaseButtons?.includes(phase)
? null
: rightPanelPhaseButtons.get(phase),
)}
</>
);
}
}

View file

@ -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<VoiceCallButtonProps> = ({ 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<void> => {
ev.preventDefault();
setBusy(true);
await LegacyCallHandler.instance.placeCall(room.roomId, CallType.Voice);
setBusy(false);
},
disabled: false,
};
}
}, [behavior, room, setBusy]);
return (
<AccessibleButton
className="mx_LegacyRoomHeader_button mx_LegacyRoomHeader_voiceCallButton"
onClick={onClick}
aria-label={_t("voip|voice_call")}
title={tooltip ?? _t("voip|voice_call")}
placement="bottom"
disabled={disabled || busy}
/>
);
};
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<VideoCallButtonProps> = ({ room, busy, setBusy, behavior }) => {
const [menuOpen, buttonRef, openMenu, closeMenu] = useContextMenu();
const startLegacyCall = useCallback(async (): Promise<void> => {
setBusy(true);
await LegacyCallHandler.instance.placeCall(room.roomId, CallType.Video);
setBusy(false);
}, [setBusy, room]);
const startElementCall = useCallback(
(skipLobby: boolean) => {
setBusy(true);
defaultDispatcher.dispatch<ViewRoomPayload>({
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<void> => {
ev.preventDefault();
await startLegacyCall();
},
disabled: false,
};
} else if (behavior === "element") {
return {
onClick: async (ev: ButtonEvent): Promise<void> => {
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<void> => {
ev.preventDefault();
openMenu();
},
disabled: false,
};
}
}, [behavior, startLegacyCall, startElementCall, openMenu]);
const onJitsiClick = useCallback(
async (ev: ButtonEvent): Promise<void> => {
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 = (
<IconizedContextMenu {...aboveLeftOf(buttonRect)} onFinished={closeMenu}>
<IconizedContextMenuOptionList>
<IconizedContextMenuOption
label={
behavior == "legacy_or_element"
? _t("room|header|video_call_button_legacy")
: _t("room|header|video_call_button_jitsi")
}
onClick={onJitsiClick}
/>
<IconizedContextMenuOption
label={_t("room|header|video_call_button_ec", { brand })}
onClick={onElementClick}
/>
</IconizedContextMenuOptionList>
</IconizedContextMenu>
);
}
return (
<>
<AccessibleButton
ref={buttonRef}
className="mx_LegacyRoomHeader_button mx_LegacyRoomHeader_videoCallButton"
onClick={onClick}
aria-label={_t("voip|video_call")}
title={tooltip ?? _t("voip|video_call")}
placement="bottom"
disabled={disabled || busy}
/>
{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<CallButtonsProps> = ({ room }) => {
const [busy, setBusy] = useState(false);
const showButtons = useSettingValue<boolean>("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 => (
<VoiceCallButton room={room} busy={busy} setBusy={setBusy} behavior={behavior} />
);
const makeVideoCallButton = (behavior: VideoCallButtonProps["behavior"]): JSX.Element => (
<VideoCallButton room={room} busy={busy} setBusy={setBusy} behavior={behavior} />
);
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<CallLayoutSelectorProps> = ({ 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 = (
<IconizedContextMenu
className="mx_LegacyRoomHeader_layoutMenu"
{...aboveLeftOf(buttonRect)}
onFinished={closeMenu}
>
<IconizedContextMenuOptionList>
<IconizedContextMenuRadio
iconClassName="mx_LegacyRoomHeader_freedomIcon"
label={_t("room|header|video_call_ec_layout_freedom")}
active={layout === Layout.Tile}
onClick={onFreedomClick}
/>
<IconizedContextMenuRadio
iconClassName="mx_LegacyRoomHeader_spotlightIcon"
label={_t("room|header|video_call_ec_layout_spotlight")}
active={layout === Layout.Spotlight}
onClick={onSpotlightClick}
/>
</IconizedContextMenuOptionList>
</IconizedContextMenu>
);
}
return (
<>
<AccessibleButton
ref={buttonRef}
className={classNames("mx_LegacyRoomHeader_button", {
"mx_LegacyRoomHeader_layoutButton--freedom": layout === Layout.Tile,
"mx_LegacyRoomHeader_layoutButton--spotlight": layout === Layout.Spotlight,
})}
onClick={onClick}
title={_t("room|header|video_call_ec_change_layout")}
placement="bottom"
key="layout"
/>
{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<RightPanelPhases>;
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<IProps, IState> {
public static defaultProps: Partial<IProps> = {
inRoom: false,
excludedRightPanelPhaseButtons: [],
showButtons: true,
enableRoomOptionsMenu: true,
};
public static contextType = RoomContext;
public declare context: React.ContextType<typeof RoomContext>;
private readonly client = this.props.room.client;
private readonly featureAskToJoinWatcher: string;
public constructor(props: IProps, context: React.ContextType<typeof RoomContext>) {
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<ViewRoomPayload>({
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(<CallButtons key="calls" room={this.props.room} />);
}
if (this.props.viewingCall && this.props.activeCall instanceof ElementCall) {
startButtons.push(<CallLayoutSelector key="layout" call={this.props.activeCall} />);
}
if (!this.props.viewingCall && this.props.onForgetClick) {
startButtons.push(
<AccessibleButton
className="mx_LegacyRoomHeader_button mx_LegacyRoomHeader_forgetButton"
onClick={this.props.onForgetClick}
title={_t("room|header|forget_room_button")}
placement="bottom"
key="forget"
/>,
);
}
if (!this.props.viewingCall && this.props.onAppsClick) {
startButtons.push(
<AccessibleButton
className={classNames("mx_LegacyRoomHeader_button mx_LegacyRoomHeader_appsButton", {
mx_LegacyRoomHeader_appsButton_highlight: this.props.appsShown,
})}
onClick={this.props.onAppsClick}
title={
this.props.appsShown
? _t("room|header|hide_widgets_button")
: _t("room|header|show_widgets_button")
}
aria-checked={this.props.appsShown}
placement="bottom"
key="apps"
/>,
);
}
if (!this.props.viewingCall && this.props.onSearchClick && this.props.inRoom) {
startButtons.push(
<AccessibleButton
className="mx_LegacyRoomHeader_button mx_LegacyRoomHeader_searchButton"
onClick={this.props.onSearchClick}
title={_t("action|search")}
placement="bottom"
key="search"
/>,
);
}
if (this.props.onInviteClick && (!this.props.viewingCall || isVideoRoom) && this.props.inRoom) {
startButtons.push(
<AccessibleButton
className="mx_LegacyRoomHeader_button mx_LegacyRoomHeader_inviteButton"
onClick={this.props.onInviteClick}
title={_t("action|invite")}
placement="bottom"
key="invite"
/>,
);
}
const endButtons: JSX.Element[] = [];
if (this.props.viewingCall && !isVideoRoom) {
if (this.props.activeCall === null) {
endButtons.push(
<AccessibleButton
className="mx_LegacyRoomHeader_button mx_LegacyRoomHeader_closeButton"
onClick={this.onHideCallClick}
title={_t("room|header|close_call_button")}
key="close"
/>,
);
} else {
endButtons.push(
<AccessibleButton
className="mx_LegacyRoomHeader_button mx_LegacyRoomHeader_minimiseButton"
onClick={this.onHideCallClick}
title={_t("room|header|video_room_view_chat_button")}
placement="bottom"
key="minimise"
/>,
);
}
}
return (
<>
{this.props.additionalButtons?.map((props) => {
const label = props.label();
return (
<Tooltip label={label} key={props.id}>
<IconButton
onClick={() => {
props.onClick();
this.forceUpdate();
}}
title={label}
>
{typeof props.icon === "function" ? props.icon() : props.icon}
</IconButton>
</Tooltip>
);
})}
{startButtons}
<RoomHeaderButtons
room={this.props.room}
excludedRightPanelPhaseButtons={this.props.excludedRightPanelPhaseButtons}
/>
{endButtons}
</>
);
}
private renderName(oobName: string): JSX.Element {
let contextMenu: JSX.Element | null = null;
if (this.state.contextMenuPosition && this.props.room) {
contextMenu = (
<RoomContextMenu
{...contextMenuBelow(this.state.contextMenuPosition)}
room={this.props.room}
onFinished={this.onContextMenuCloseClick}
/>
);
}
// 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 = (
<RoomName room={this.props.room}>
{(name) => {
const roomName = name || oobName;
return (
<div dir="auto" className={textClasses} title={roomName} role="heading" aria-level={1}>
{roomName}
</div>
);
}}
</RoomName>
);
if (this.props.enableRoomOptionsMenu && shouldShowComponent(UIComponent.RoomOptionsMenu)) {
return (
<ContextMenuTooltipButton
className="mx_LegacyRoomHeader_name"
onClick={this.onContextMenuOpenClick}
isExpanded={!!this.state.contextMenuPosition}
title={_t("room|context_menu|title")}
placement="bottom"
>
{roomName}
{this.props.room && <div className="mx_LegacyRoomHeader_chevron" />}
{contextMenu}
</ContextMenuTooltipButton>
);
}
return <div className="mx_LegacyRoomHeader_name mx_LegacyRoomHeader_name--textonly">{roomName}</div>;
}
public render(): React.ReactNode {
const isVideoRoom = calcIsVideoRoom(this.props.room);
let roomAvatar: JSX.Element | null = null;
if (this.props.room) {
roomAvatar = (
<DecoratedRoomAvatar
room={this.props.room}
size="24px"
oobData={this.props.oobData}
viewAvatarOnClick={true}
/>
);
}
const icon = this.props.viewingCall ? (
<div className="mx_LegacyRoomHeader_icon mx_LegacyRoomHeader_icon_video" />
) : this.props.e2eStatus ? (
<E2EIcon className="mx_LegacyRoomHeader_icon" status={this.props.e2eStatus} tooltipPlacement="bottom" />
) : // 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() ? (
<div className="mx_LegacyRoomHeader_icon" />
) : 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 (
<header className="mx_LegacyRoomHeader light-panel">
<div
className="mx_LegacyRoomHeader_wrapper"
aria-owns={this.state.rightPanelOpen ? "mx_RightPanel" : undefined}
>
<div className="mx_LegacyRoomHeader_avatar">{roomAvatar}</div>
{icon}
{name}
{this.props.activeCall instanceof ElementCall && (
<SessionDuration session={this.props.activeCall?.session} />
)}
{/* Empty topic element to fill out space */}
<div className="mx_LegacyRoomHeader_topic" />
{buttons}
</div>
</header>
);
}
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 = (
<div className="mx_LegacyRoomHeader_searchStatus">
&nbsp;
{_t("room|search|result_count", { count: this.props.searchInfo.count })}
</div>
);
}
const topicElement = <RoomTopic room={this.props.room} className="mx_LegacyRoomHeader_topic" />;
const viewLabs = (): void =>
defaultDispatcher.dispatch({
action: Action.ViewUserSettings,
initialTabId: UserTab.Labs,
});
const betaPill = isVideoRoom ? (
<BetaPill onClick={viewLabs} tooltipTitle={_t("labs|video_rooms_beta")} />
) : null;
return (
<header className="mx_LegacyRoomHeader light-panel">
<div
className="mx_LegacyRoomHeader_wrapper"
aria-owns={this.state.rightPanelOpen ? "mx_RightPanel" : undefined}
>
<div className="mx_LegacyRoomHeader_avatar">{roomAvatar}</div>
{icon}
{name}
{searchStatus}
{topicElement}
{betaPill}
{buttons}
</div>
{!isVideoRoom && <RoomCallBanner roomId={this.props.room.roomId} />}
<RoomLiveShareWarning roomId={this.props.room.roomId} />
{this.state.featureAskToJoin && <RoomKnocksBar room={this.props.room} />}
</header>
);
}
}

View file

@ -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]),
);

View file

@ -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 <link>thread message</link>",
"title": "Pinned messages",
"unpin_all": {
"button": "Unpin all messages",
"content": "Make sure that you really want to remove all pinned messages. This action cant 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 “<query/>”",
"other": "%(count)s results found for “<query/>”"

View file

@ -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: () => <p>{_t("labs|new_room_decoration_ui_beta_caption")}</p>,
},
},
"feature_notifications": {
isFeature: true,
labsGroup: LabGroup.Messaging,