Add feature flag 'feature_new_room_decoration_ui' and segrate legacy UI component (#11345)
* Move RoomHeader to LegacyRoomHeader * Create new RoomHeader component
This commit is contained in:
parent
89a92c6351
commit
6ae7c033d5
30 changed files with 2309 additions and 2103 deletions
|
@ -63,7 +63,8 @@ import RoomPreviewCard from "../views/rooms/RoomPreviewCard";
|
|||
import SearchBar, { SearchScope } from "../views/rooms/SearchBar";
|
||||
import RoomUpgradeWarningBar from "../views/rooms/RoomUpgradeWarningBar";
|
||||
import AuxPanel from "../views/rooms/AuxPanel";
|
||||
import RoomHeader, { ISearchInfo } from "../views/rooms/RoomHeader";
|
||||
import LegacyRoomHeader, { ISearchInfo } from "../views/rooms/LegacyRoomHeader";
|
||||
import RoomHeader from "../views/rooms/RoomHeader";
|
||||
import { IOOBData, IThreepidInvite } from "../../stores/ThreepidInviteStore";
|
||||
import EffectsOverlay from "../views/elements/EffectsOverlay";
|
||||
import { containsEmoji } from "../../effects/utils";
|
||||
|
@ -295,22 +296,26 @@ function LocalRoomView(props: LocalRoomViewProps): ReactElement {
|
|||
return (
|
||||
<div className="mx_RoomView mx_RoomView--local">
|
||||
<ErrorBoundary>
|
||||
<RoomHeader
|
||||
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}
|
||||
/>
|
||||
{SettingsStore.getValue("feature_new_room_decoration_ui") ? (
|
||||
<RoomHeader room={context.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}
|
||||
/>
|
||||
)}
|
||||
<main className="mx_RoomView_body" ref={props.roomView}>
|
||||
<FileDropTarget parent={props.roomView.current} onFileDrop={props.onFileDrop} />
|
||||
<div className="mx_RoomView_timeline">
|
||||
|
@ -345,22 +350,26 @@ function LocalRoomCreateLoader(props: ILocalRoomCreateLoaderProps): ReactElement
|
|||
return (
|
||||
<div className="mx_RoomView mx_RoomView--local">
|
||||
<ErrorBoundary>
|
||||
<RoomHeader
|
||||
room={context.room}
|
||||
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}
|
||||
/>
|
||||
{SettingsStore.getValue("feature_new_room_decoration_ui") ? (
|
||||
<RoomHeader room={context.room} />
|
||||
) : (
|
||||
<LegacyRoomHeader
|
||||
room={context.room}
|
||||
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}
|
||||
/>
|
||||
)}
|
||||
<div className="mx_RoomView_body">
|
||||
<LargeLoader text={text} />
|
||||
</div>
|
||||
|
@ -2460,23 +2469,27 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
<EffectsOverlay roomWidth={this.roomView.current.offsetWidth} />
|
||||
)}
|
||||
<ErrorBoundary>
|
||||
<RoomHeader
|
||||
room={this.state.room}
|
||||
searchInfo={this.state.search}
|
||||
oobData={this.props.oobData}
|
||||
inRoom={myMembership === "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}
|
||||
/>
|
||||
{SettingsStore.getValue("feature_new_room_decoration_ui") ? (
|
||||
<RoomHeader room={this.state.room} />
|
||||
) : (
|
||||
<LegacyRoomHeader
|
||||
room={this.state.room}
|
||||
searchInfo={this.state.search}
|
||||
oobData={this.props.oobData}
|
||||
inRoom={myMembership === "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}
|
||||
/>
|
||||
)}
|
||||
<MainSplit
|
||||
panel={rightPanel}
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
|
|
|
@ -22,6 +22,7 @@ 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";
|
||||
|
@ -29,6 +30,7 @@ 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>;
|
||||
|
@ -48,21 +50,25 @@ export const WaitingForThirdPartyRoomView: React.FC<Props> = ({ roomView, resize
|
|||
return (
|
||||
<div className="mx_RoomView mx_RoomView--local">
|
||||
<ErrorBoundary>
|
||||
<RoomHeader
|
||||
room={context.room}
|
||||
inRoom={true}
|
||||
onSearchClick={null}
|
||||
onInviteClick={null}
|
||||
onForgetClick={null}
|
||||
e2eStatus={E2EStatus.Normal}
|
||||
onAppsClick={null}
|
||||
appsShown={false}
|
||||
excludedRightPanelPhaseButtons={[]}
|
||||
showButtons={false}
|
||||
enableRoomOptionsMenu={false}
|
||||
viewingCall={false}
|
||||
activeCall={null}
|
||||
/>
|
||||
{SettingsStore.getValue("feature_new_room_decoration_ui") ? (
|
||||
<RoomHeader room={context.room} />
|
||||
) : (
|
||||
<LegacyRoomHeader
|
||||
room={context.room}
|
||||
inRoom={true}
|
||||
onSearchClick={null}
|
||||
onInviteClick={null}
|
||||
onForgetClick={null}
|
||||
e2eStatus={E2EStatus.Normal}
|
||||
onAppsClick={null}
|
||||
appsShown={false}
|
||||
excludedRightPanelPhaseButtons={[]}
|
||||
showButtons={false}
|
||||
enableRoomOptionsMenu={false}
|
||||
viewingCall={false}
|
||||
activeCall={null}
|
||||
/>
|
||||
)}
|
||||
<main className="mx_RoomView_body" ref={roomView}>
|
||||
<div className="mx_RoomView_timeline">
|
||||
<ScrollPanel className="mx_RoomView_messagePanel" resizeNotifier={resizeNotifier}>
|
||||
|
|
|
@ -59,6 +59,7 @@ interface IProps extends IContextMenuProps {
|
|||
|
||||
/**
|
||||
* 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);
|
||||
|
|
|
@ -45,9 +45,9 @@ export default class HeaderButton extends React.Component<IProps> {
|
|||
const { isHighlighted, isUnread = false, onClick, name, title, ...props } = this.props;
|
||||
|
||||
const classes = classNames({
|
||||
"mx_RoomHeader_button": true,
|
||||
"mx_RoomHeader_button--highlight": isHighlighted,
|
||||
"mx_RoomHeader_button--unread": isUnread,
|
||||
"mx_LegacyRoomHeader_button": true,
|
||||
"mx_LegacyRoomHeader_button--highlight": isHighlighted,
|
||||
"mx_LegacyRoomHeader_button--unread": isUnread,
|
||||
[`mx_RightPanel_${name}`]: true,
|
||||
});
|
||||
|
||||
|
|
|
@ -64,14 +64,14 @@ const UnreadIndicator: React.FC<IUnreadIndicatorProps> = ({ color }) => {
|
|||
|
||||
const classes = classNames({
|
||||
mx_Indicator: true,
|
||||
mx_RoomHeader_button_unreadIndicator: true,
|
||||
mx_LegacyRoomHeader_button_unreadIndicator: true,
|
||||
mx_Indicator_bold: color === NotificationColor.Bold,
|
||||
mx_Indicator_gray: color === NotificationColor.Grey,
|
||||
mx_Indicator_red: color === NotificationColor.Red,
|
||||
});
|
||||
return (
|
||||
<>
|
||||
<div className="mx_RoomHeader_button_unreadIndicator_bg" />
|
||||
<div className="mx_LegacyRoomHeader_button_unreadIndicator_bg" />
|
||||
<div className={classes} />
|
||||
</>
|
||||
);
|
||||
|
@ -127,7 +127,10 @@ interface IProps {
|
|||
excludedRightPanelPhaseButtons?: Array<RightPanelPhases>;
|
||||
}
|
||||
|
||||
export default class RoomHeaderButtons extends HeaderButtons<IProps> {
|
||||
/**
|
||||
* @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;
|
||||
|
||||
|
@ -257,7 +260,7 @@ export default class RoomHeaderButtons extends HeaderButtons<IProps> {
|
|||
};
|
||||
|
||||
private onThreadsPanelClicked = (ev: ButtonEvent): void => {
|
||||
if (this.state.phase && RoomHeaderButtons.THREAD_PHASES.includes(this.state.phase)) {
|
||||
if (this.state.phase && LegacyRoomHeaderButtons.THREAD_PHASES.includes(this.state.phase)) {
|
||||
RightPanelStore.instance.togglePanel(this.props.room?.roomId ?? null);
|
||||
} else {
|
||||
showThreadPanel();
|
||||
|
@ -300,7 +303,7 @@ export default class RoomHeaderButtons extends HeaderButtons<IProps> {
|
|||
data-testid="threadsButton"
|
||||
title={_t("Threads")}
|
||||
onClick={this.onThreadsPanelClicked}
|
||||
isHighlighted={this.isPhase(RoomHeaderButtons.THREAD_PHASES)}
|
||||
isHighlighted={this.isPhase(LegacyRoomHeaderButtons.THREAD_PHASES)}
|
||||
isUnread={this.state.threadNotificationColor > NotificationColor.None}
|
||||
>
|
||||
<UnreadIndicator color={this.state.threadNotificationColor} />
|
825
src/components/views/rooms/LegacyRoomHeader.tsx
Normal file
825
src/components/views/rooms/LegacyRoomHeader.tsx
Normal file
|
@ -0,0 +1,825 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2019, 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { FC, useState, useMemo, useCallback } from "react";
|
||||
import classNames from "classnames";
|
||||
import { throttle } from "lodash";
|
||||
import { RoomStateEvent } from "matrix-js-sdk/src/models/room-state";
|
||||
import { CallType } from "matrix-js-sdk/src/webrtc/call";
|
||||
import { ISearchResults } from "matrix-js-sdk/src/@types/search";
|
||||
|
||||
import type { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import type { Room } from "matrix-js-sdk/src/models/room";
|
||||
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 AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||
import RoomTopic from "../elements/RoomTopic";
|
||||
import RoomName from "../elements/RoomName";
|
||||
import { E2EStatus } from "../../../utils/ShieldUtils";
|
||||
import { IOOBData } from "../../../stores/ThreepidInviteStore";
|
||||
import { SearchScope } from "./SearchBar";
|
||||
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 "../right_panel/RoomSummaryCard";
|
||||
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 { GroupCallDuration } from "../voip/CallDuration";
|
||||
import { Alignment } from "../elements/Tooltip";
|
||||
import RoomCallBanner from "../beacon/RoomCallBanner";
|
||||
import { shouldShowComponent } from "../../../customisations/helpers/UIComponents";
|
||||
import { UIComponent } from "../../../settings/UIFeature";
|
||||
|
||||
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 (
|
||||
<AccessibleTooltipButton
|
||||
className="mx_LegacyRoomHeader_button mx_LegacyRoomHeader_voiceCallButton"
|
||||
onClick={onClick}
|
||||
title={_t("Voice call")}
|
||||
tooltip={tooltip ?? _t("Voice call")}
|
||||
alignment={Alignment.Bottom}
|
||||
disabled={disabled || busy}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
interface VideoCallButtonProps {
|
||||
room: Room;
|
||||
busy: boolean;
|
||||
setBusy: (value: boolean) => void;
|
||||
behavior: DisabledWithReason | "legacy_or_jitsi" | "element" | "jitsi_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(() => {
|
||||
setBusy(true);
|
||||
defaultDispatcher.dispatch<ViewRoomPayload>({
|
||||
action: Action.ViewRoom,
|
||||
room_id: room.roomId,
|
||||
view_call: true,
|
||||
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();
|
||||
},
|
||||
disabled: false,
|
||||
};
|
||||
} else {
|
||||
// behavior === "jitsi_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();
|
||||
},
|
||||
[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={_t("Video call (Jitsi)")} onClick={onJitsiClick} />
|
||||
<IconizedContextMenuOption
|
||||
label={_t("Video call (%(brand)s)", { brand })}
|
||||
onClick={onElementClick}
|
||||
/>
|
||||
</IconizedContextMenuOptionList>
|
||||
</IconizedContextMenu>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<AccessibleTooltipButton
|
||||
inputRef={buttonRef}
|
||||
className="mx_LegacyRoomHeader_button mx_LegacyRoomHeader_videoCallButton"
|
||||
onClick={onClick}
|
||||
title={_t("Video call")}
|
||||
tooltip={tooltip ?? _t("Video call")}
|
||||
alignment={Alignment.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 videoRoomsEnabled = useFeatureEnabled("feature_video_rooms");
|
||||
const isVideoRoom = useMemo(() => videoRoomsEnabled && calcIsVideoRoom(room), [videoRoomsEnabled, 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) {
|
||||
if (useElementCallExclusively) {
|
||||
if (hasGroupCall) {
|
||||
return makeVideoCallButton(new DisabledWithReason(_t("Ongoing call")));
|
||||
} else if (mayCreateElementCalls) {
|
||||
return makeVideoCallButton("element");
|
||||
} else {
|
||||
return makeVideoCallButton(
|
||||
new DisabledWithReason(_t("You do not have permission to start video calls")),
|
||||
);
|
||||
}
|
||||
} else if (hasLegacyCall || hasJitsiWidget || hasGroupCall) {
|
||||
return (
|
||||
<>
|
||||
{makeVoiceCallButton(new DisabledWithReason(_t("Ongoing call")))}
|
||||
{makeVideoCallButton(new DisabledWithReason(_t("Ongoing call")))}
|
||||
</>
|
||||
);
|
||||
} else if (functionalMembers.length <= 1) {
|
||||
return (
|
||||
<>
|
||||
{makeVoiceCallButton(new DisabledWithReason(_t("There's no one here to call")))}
|
||||
{makeVideoCallButton(new DisabledWithReason(_t("There's no one here to call")))}
|
||||
</>
|
||||
);
|
||||
} else if (functionalMembers.length === 2) {
|
||||
return (
|
||||
<>
|
||||
{makeVoiceCallButton("legacy_or_jitsi")}
|
||||
{makeVideoCallButton("legacy_or_jitsi")}
|
||||
</>
|
||||
);
|
||||
} else if (mayEditWidgets) {
|
||||
return (
|
||||
<>
|
||||
{makeVoiceCallButton("legacy_or_jitsi")}
|
||||
{makeVideoCallButton(mayCreateElementCalls ? "jitsi_or_element" : "legacy_or_jitsi")}
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
const videoCallBehavior = mayCreateElementCalls
|
||||
? "element"
|
||||
: new DisabledWithReason(_t("You do not have permission to start video calls"));
|
||||
return (
|
||||
<>
|
||||
{makeVoiceCallButton(new DisabledWithReason(_t("You do not have permission to start voice calls")))}
|
||||
{makeVideoCallButton(videoCallBehavior)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
} else if (hasLegacyCall || hasJitsiWidget) {
|
||||
return (
|
||||
<>
|
||||
{makeVoiceCallButton(new DisabledWithReason(_t("Ongoing call")))}
|
||||
{makeVideoCallButton(new DisabledWithReason(_t("Ongoing call")))}
|
||||
</>
|
||||
);
|
||||
} else if (functionalMembers.length <= 1) {
|
||||
return (
|
||||
<>
|
||||
{makeVoiceCallButton(new DisabledWithReason(_t("There's no one here to call")))}
|
||||
{makeVideoCallButton(new DisabledWithReason(_t("There's no one here to call")))}
|
||||
</>
|
||||
);
|
||||
} else if (functionalMembers.length === 2 || mayEditWidgets) {
|
||||
return (
|
||||
<>
|
||||
{makeVoiceCallButton("legacy_or_jitsi")}
|
||||
{makeVideoCallButton("legacy_or_jitsi")}
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<>
|
||||
{makeVoiceCallButton(new DisabledWithReason(_t("You do not have permission to start voice calls")))}
|
||||
{makeVideoCallButton(new DisabledWithReason(_t("You do not have permission to start video calls")))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
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("Freedom")}
|
||||
active={layout === Layout.Tile}
|
||||
onClick={onFreedomClick}
|
||||
/>
|
||||
<IconizedContextMenuRadio
|
||||
iconClassName="mx_LegacyRoomHeader_spotlightIcon"
|
||||
label={_t("Spotlight")}
|
||||
active={layout === Layout.Spotlight}
|
||||
onClick={onSpotlightClick}
|
||||
/>
|
||||
</IconizedContextMenuOptionList>
|
||||
</IconizedContextMenu>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<AccessibleTooltipButton
|
||||
inputRef={buttonRef}
|
||||
className={classNames("mx_LegacyRoomHeader_button", {
|
||||
"mx_LegacyRoomHeader_layoutButton--freedom": layout === Layout.Tile,
|
||||
"mx_LegacyRoomHeader_layoutButton--spotlight": layout === Layout.Spotlight,
|
||||
})}
|
||||
onClick={onClick}
|
||||
title={_t("Change layout")}
|
||||
alignment={Alignment.Bottom}
|
||||
key="layout"
|
||||
/>
|
||||
{menu}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export interface ISearchInfo {
|
||||
searchId: number;
|
||||
roomId?: string;
|
||||
term: string;
|
||||
scope: SearchScope;
|
||||
promise: Promise<ISearchResults>;
|
||||
abortController?: AbortController;
|
||||
|
||||
inProgress?: boolean;
|
||||
count?: number;
|
||||
}
|
||||
|
||||
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?: ISearchInfo;
|
||||
excludedRightPanelPhaseButtons?: Array<RightPanelPhases>;
|
||||
showButtons?: boolean;
|
||||
enableRoomOptionsMenu?: boolean;
|
||||
viewingCall: boolean;
|
||||
activeCall: Call | null;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
contextMenuPosition?: DOMRect;
|
||||
rightPanelOpen: boolean;
|
||||
}
|
||||
|
||||
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 context!: React.ContextType<typeof RoomContext>;
|
||||
private readonly client = this.props.room.client;
|
||||
|
||||
public constructor(props: IProps, context: IState) {
|
||||
super(props, context);
|
||||
const notiStore = RoomNotificationStateStore.instance.getRoomState(props.room);
|
||||
notiStore.on(NotificationStateEvents.Update, this.onNotificationUpdate);
|
||||
this.state = {
|
||||
rightPanelOpen: RightPanelStore.instance.isOpen,
|
||||
};
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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(
|
||||
<AccessibleTooltipButton
|
||||
className="mx_LegacyRoomHeader_button mx_LegacyRoomHeader_forgetButton"
|
||||
onClick={this.props.onForgetClick}
|
||||
title={_t("Forget room")}
|
||||
alignment={Alignment.Bottom}
|
||||
key="forget"
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
||||
if (!this.props.viewingCall && this.props.onAppsClick) {
|
||||
startButtons.push(
|
||||
<AccessibleTooltipButton
|
||||
className={classNames("mx_LegacyRoomHeader_button mx_LegacyRoomHeader_appsButton", {
|
||||
mx_LegacyRoomHeader_appsButton_highlight: this.props.appsShown,
|
||||
})}
|
||||
onClick={this.props.onAppsClick}
|
||||
title={this.props.appsShown ? _t("Hide Widgets") : _t("Show Widgets")}
|
||||
aria-checked={this.props.appsShown}
|
||||
alignment={Alignment.Bottom}
|
||||
key="apps"
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
||||
if (!this.props.viewingCall && this.props.onSearchClick && this.props.inRoom) {
|
||||
startButtons.push(
|
||||
<AccessibleTooltipButton
|
||||
className="mx_LegacyRoomHeader_button mx_LegacyRoomHeader_searchButton"
|
||||
onClick={this.props.onSearchClick}
|
||||
title={_t("Search")}
|
||||
alignment={Alignment.Bottom}
|
||||
key="search"
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
||||
if (this.props.onInviteClick && (!this.props.viewingCall || isVideoRoom) && this.props.inRoom) {
|
||||
startButtons.push(
|
||||
<AccessibleTooltipButton
|
||||
className="mx_LegacyRoomHeader_button mx_LegacyRoomHeader_inviteButton"
|
||||
onClick={this.props.onInviteClick}
|
||||
title={_t("Invite")}
|
||||
alignment={Alignment.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("Close call")}
|
||||
key="close"
|
||||
/>,
|
||||
);
|
||||
} else {
|
||||
endButtons.push(
|
||||
<AccessibleTooltipButton
|
||||
className="mx_LegacyRoomHeader_button mx_LegacyRoomHeader_minimiseButton"
|
||||
onClick={this.onHideCallClick}
|
||||
title={_t("View chat timeline")}
|
||||
alignment={Alignment.Bottom}
|
||||
key="minimise"
|
||||
/>,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{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 options")}
|
||||
alignment={Alignment.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 = SettingsStore.getValue("feature_video_rooms") && calcIsVideoRoom(this.props.room);
|
||||
|
||||
let roomAvatar: JSX.Element | null = null;
|
||||
if (this.props.room) {
|
||||
roomAvatar = (
|
||||
<DecoratedRoomAvatar
|
||||
room={this.props.room}
|
||||
avatarSize={24}
|
||||
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}
|
||||
tooltipAlignment={Alignment.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("Join 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 && (
|
||||
<GroupCallDuration groupCall={this.props.activeCall.groupCall} />
|
||||
)}
|
||||
{/* 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">
|
||||
|
||||
{_t("(~%(count)s results)", { 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("Video rooms are a beta feature")} />
|
||||
) : 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} />
|
||||
</header>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,6 +1,5 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2019, 2021 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -15,805 +14,41 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { FC, useState, useMemo, useCallback } from "react";
|
||||
import classNames from "classnames";
|
||||
import { throttle } from "lodash";
|
||||
import { RoomStateEvent } from "matrix-js-sdk/src/models/room-state";
|
||||
import { CallType } from "matrix-js-sdk/src/webrtc/call";
|
||||
import { ISearchResults } from "matrix-js-sdk/src/@types/search";
|
||||
import React from "react";
|
||||
|
||||
import type { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import type { Room } from "matrix-js-sdk/src/models/room";
|
||||
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/RoomHeaderButtons";
|
||||
import E2EIcon from "./E2EIcon";
|
||||
import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar";
|
||||
import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton";
|
||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||
import RoomTopic from "../elements/RoomTopic";
|
||||
import RoomName from "../elements/RoomName";
|
||||
import { E2EStatus } from "../../../utils/ShieldUtils";
|
||||
import { IOOBData } from "../../../stores/ThreepidInviteStore";
|
||||
import { SearchScope } from "./SearchBar";
|
||||
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 "../right_panel/RoomSummaryCard";
|
||||
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 { GroupCallDuration } from "../voip/CallDuration";
|
||||
import { Alignment } from "../elements/Tooltip";
|
||||
import RoomCallBanner from "../beacon/RoomCallBanner";
|
||||
import { shouldShowComponent } from "../../../customisations/helpers/UIComponents";
|
||||
import { UIComponent } from "../../../settings/UIFeature";
|
||||
|
||||
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 (
|
||||
<AccessibleTooltipButton
|
||||
className="mx_RoomHeader_button mx_RoomHeader_voiceCallButton"
|
||||
onClick={onClick}
|
||||
title={_t("Voice call")}
|
||||
tooltip={tooltip ?? _t("Voice call")}
|
||||
alignment={Alignment.Bottom}
|
||||
disabled={disabled || busy}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
interface VideoCallButtonProps {
|
||||
room: Room;
|
||||
busy: boolean;
|
||||
setBusy: (value: boolean) => void;
|
||||
behavior: DisabledWithReason | "legacy_or_jitsi" | "element" | "jitsi_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(() => {
|
||||
setBusy(true);
|
||||
defaultDispatcher.dispatch<ViewRoomPayload>({
|
||||
action: Action.ViewRoom,
|
||||
room_id: room.roomId,
|
||||
view_call: true,
|
||||
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();
|
||||
},
|
||||
disabled: false,
|
||||
};
|
||||
} else {
|
||||
// behavior === "jitsi_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();
|
||||
},
|
||||
[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={_t("Video call (Jitsi)")} onClick={onJitsiClick} />
|
||||
<IconizedContextMenuOption
|
||||
label={_t("Video call (%(brand)s)", { brand })}
|
||||
onClick={onElementClick}
|
||||
/>
|
||||
</IconizedContextMenuOptionList>
|
||||
</IconizedContextMenu>
|
||||
);
|
||||
export default function RoomHeader({ room, oobData }: { room?: Room; oobData?: IOOBData }): JSX.Element {
|
||||
let oobName = _t("Join Room");
|
||||
if (oobData && oobData.name) {
|
||||
oobName = oobData.name;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<AccessibleTooltipButton
|
||||
inputRef={buttonRef}
|
||||
className="mx_RoomHeader_button mx_RoomHeader_videoCallButton"
|
||||
onClick={onClick}
|
||||
title={_t("Video call")}
|
||||
tooltip={tooltip ?? _t("Video call")}
|
||||
alignment={Alignment.Bottom}
|
||||
disabled={disabled || busy}
|
||||
/>
|
||||
{menu}
|
||||
</>
|
||||
<header className="mx_LegacyRoomHeader light-panel">
|
||||
<div className="mx_LegacyRoomHeader_wrapper">
|
||||
{room && (
|
||||
<RoomName room={room}>
|
||||
{(name) => {
|
||||
const roomName = name || oobName;
|
||||
return (
|
||||
<div
|
||||
className="mx_LegacyRoomHeader_name"
|
||||
dir="auto"
|
||||
title={roomName}
|
||||
role="heading"
|
||||
aria-level={1}
|
||||
>
|
||||
{roomName}
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</RoomName>
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
};
|
||||
|
||||
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 videoRoomsEnabled = useFeatureEnabled("feature_video_rooms");
|
||||
const isVideoRoom = useMemo(() => videoRoomsEnabled && calcIsVideoRoom(room), [videoRoomsEnabled, 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) {
|
||||
if (useElementCallExclusively) {
|
||||
if (hasGroupCall) {
|
||||
return makeVideoCallButton(new DisabledWithReason(_t("Ongoing call")));
|
||||
} else if (mayCreateElementCalls) {
|
||||
return makeVideoCallButton("element");
|
||||
} else {
|
||||
return makeVideoCallButton(
|
||||
new DisabledWithReason(_t("You do not have permission to start video calls")),
|
||||
);
|
||||
}
|
||||
} else if (hasLegacyCall || hasJitsiWidget || hasGroupCall) {
|
||||
return (
|
||||
<>
|
||||
{makeVoiceCallButton(new DisabledWithReason(_t("Ongoing call")))}
|
||||
{makeVideoCallButton(new DisabledWithReason(_t("Ongoing call")))}
|
||||
</>
|
||||
);
|
||||
} else if (functionalMembers.length <= 1) {
|
||||
return (
|
||||
<>
|
||||
{makeVoiceCallButton(new DisabledWithReason(_t("There's no one here to call")))}
|
||||
{makeVideoCallButton(new DisabledWithReason(_t("There's no one here to call")))}
|
||||
</>
|
||||
);
|
||||
} else if (functionalMembers.length === 2) {
|
||||
return (
|
||||
<>
|
||||
{makeVoiceCallButton("legacy_or_jitsi")}
|
||||
{makeVideoCallButton("legacy_or_jitsi")}
|
||||
</>
|
||||
);
|
||||
} else if (mayEditWidgets) {
|
||||
return (
|
||||
<>
|
||||
{makeVoiceCallButton("legacy_or_jitsi")}
|
||||
{makeVideoCallButton(mayCreateElementCalls ? "jitsi_or_element" : "legacy_or_jitsi")}
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
const videoCallBehavior = mayCreateElementCalls
|
||||
? "element"
|
||||
: new DisabledWithReason(_t("You do not have permission to start video calls"));
|
||||
return (
|
||||
<>
|
||||
{makeVoiceCallButton(new DisabledWithReason(_t("You do not have permission to start voice calls")))}
|
||||
{makeVideoCallButton(videoCallBehavior)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
} else if (hasLegacyCall || hasJitsiWidget) {
|
||||
return (
|
||||
<>
|
||||
{makeVoiceCallButton(new DisabledWithReason(_t("Ongoing call")))}
|
||||
{makeVideoCallButton(new DisabledWithReason(_t("Ongoing call")))}
|
||||
</>
|
||||
);
|
||||
} else if (functionalMembers.length <= 1) {
|
||||
return (
|
||||
<>
|
||||
{makeVoiceCallButton(new DisabledWithReason(_t("There's no one here to call")))}
|
||||
{makeVideoCallButton(new DisabledWithReason(_t("There's no one here to call")))}
|
||||
</>
|
||||
);
|
||||
} else if (functionalMembers.length === 2 || mayEditWidgets) {
|
||||
return (
|
||||
<>
|
||||
{makeVoiceCallButton("legacy_or_jitsi")}
|
||||
{makeVideoCallButton("legacy_or_jitsi")}
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<>
|
||||
{makeVoiceCallButton(new DisabledWithReason(_t("You do not have permission to start voice calls")))}
|
||||
{makeVideoCallButton(new DisabledWithReason(_t("You do not have permission to start video calls")))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
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_RoomHeader_layoutMenu"
|
||||
{...aboveLeftOf(buttonRect)}
|
||||
onFinished={closeMenu}
|
||||
>
|
||||
<IconizedContextMenuOptionList>
|
||||
<IconizedContextMenuRadio
|
||||
iconClassName="mx_RoomHeader_freedomIcon"
|
||||
label={_t("Freedom")}
|
||||
active={layout === Layout.Tile}
|
||||
onClick={onFreedomClick}
|
||||
/>
|
||||
<IconizedContextMenuRadio
|
||||
iconClassName="mx_RoomHeader_spotlightIcon"
|
||||
label={_t("Spotlight")}
|
||||
active={layout === Layout.Spotlight}
|
||||
onClick={onSpotlightClick}
|
||||
/>
|
||||
</IconizedContextMenuOptionList>
|
||||
</IconizedContextMenu>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<AccessibleTooltipButton
|
||||
inputRef={buttonRef}
|
||||
className={classNames("mx_RoomHeader_button", {
|
||||
"mx_RoomHeader_layoutButton--freedom": layout === Layout.Tile,
|
||||
"mx_RoomHeader_layoutButton--spotlight": layout === Layout.Spotlight,
|
||||
})}
|
||||
onClick={onClick}
|
||||
title={_t("Change layout")}
|
||||
alignment={Alignment.Bottom}
|
||||
key="layout"
|
||||
/>
|
||||
{menu}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export interface ISearchInfo {
|
||||
searchId: number;
|
||||
roomId?: string;
|
||||
term: string;
|
||||
scope: SearchScope;
|
||||
promise: Promise<ISearchResults>;
|
||||
abortController?: AbortController;
|
||||
|
||||
inProgress?: boolean;
|
||||
count?: number;
|
||||
}
|
||||
|
||||
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?: ISearchInfo;
|
||||
excludedRightPanelPhaseButtons?: Array<RightPanelPhases>;
|
||||
showButtons?: boolean;
|
||||
enableRoomOptionsMenu?: boolean;
|
||||
viewingCall: boolean;
|
||||
activeCall: Call | null;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
contextMenuPosition?: DOMRect;
|
||||
rightPanelOpen: boolean;
|
||||
}
|
||||
|
||||
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 context!: React.ContextType<typeof RoomContext>;
|
||||
private readonly client = this.props.room.client;
|
||||
|
||||
public constructor(props: IProps, context: IState) {
|
||||
super(props, context);
|
||||
const notiStore = RoomNotificationStateStore.instance.getRoomState(props.room);
|
||||
notiStore.on(NotificationStateEvents.Update, this.onNotificationUpdate);
|
||||
this.state = {
|
||||
rightPanelOpen: RightPanelStore.instance.isOpen,
|
||||
};
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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(
|
||||
<AccessibleTooltipButton
|
||||
className="mx_RoomHeader_button mx_RoomHeader_forgetButton"
|
||||
onClick={this.props.onForgetClick}
|
||||
title={_t("Forget room")}
|
||||
alignment={Alignment.Bottom}
|
||||
key="forget"
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
||||
if (!this.props.viewingCall && this.props.onAppsClick) {
|
||||
startButtons.push(
|
||||
<AccessibleTooltipButton
|
||||
className={classNames("mx_RoomHeader_button mx_RoomHeader_appsButton", {
|
||||
mx_RoomHeader_appsButton_highlight: this.props.appsShown,
|
||||
})}
|
||||
onClick={this.props.onAppsClick}
|
||||
title={this.props.appsShown ? _t("Hide Widgets") : _t("Show Widgets")}
|
||||
aria-checked={this.props.appsShown}
|
||||
alignment={Alignment.Bottom}
|
||||
key="apps"
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
||||
if (!this.props.viewingCall && this.props.onSearchClick && this.props.inRoom) {
|
||||
startButtons.push(
|
||||
<AccessibleTooltipButton
|
||||
className="mx_RoomHeader_button mx_RoomHeader_searchButton"
|
||||
onClick={this.props.onSearchClick}
|
||||
title={_t("Search")}
|
||||
alignment={Alignment.Bottom}
|
||||
key="search"
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
||||
if (this.props.onInviteClick && (!this.props.viewingCall || isVideoRoom) && this.props.inRoom) {
|
||||
startButtons.push(
|
||||
<AccessibleTooltipButton
|
||||
className="mx_RoomHeader_button mx_RoomHeader_inviteButton"
|
||||
onClick={this.props.onInviteClick}
|
||||
title={_t("Invite")}
|
||||
alignment={Alignment.Bottom}
|
||||
key="invite"
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
||||
const endButtons: JSX.Element[] = [];
|
||||
|
||||
if (this.props.viewingCall && !isVideoRoom) {
|
||||
if (this.props.activeCall === null) {
|
||||
endButtons.push(
|
||||
<AccessibleButton
|
||||
className="mx_RoomHeader_button mx_RoomHeader_closeButton"
|
||||
onClick={this.onHideCallClick}
|
||||
title={_t("Close call")}
|
||||
key="close"
|
||||
/>,
|
||||
);
|
||||
} else {
|
||||
endButtons.push(
|
||||
<AccessibleTooltipButton
|
||||
className="mx_RoomHeader_button mx_RoomHeader_minimiseButton"
|
||||
onClick={this.onHideCallClick}
|
||||
title={_t("View chat timeline")}
|
||||
alignment={Alignment.Bottom}
|
||||
key="minimise"
|
||||
/>,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{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_RoomHeader_nametext", { mx_RoomHeader_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_RoomHeader_name"
|
||||
onClick={this.onContextMenuOpenClick}
|
||||
isExpanded={!!this.state.contextMenuPosition}
|
||||
title={_t("Room options")}
|
||||
alignment={Alignment.Bottom}
|
||||
>
|
||||
{roomName}
|
||||
{this.props.room && <div className="mx_RoomHeader_chevron" />}
|
||||
{contextMenu}
|
||||
</ContextMenuTooltipButton>
|
||||
);
|
||||
}
|
||||
|
||||
return <div className="mx_RoomHeader_name mx_RoomHeader_name--textonly">{roomName}</div>;
|
||||
}
|
||||
|
||||
public render(): React.ReactNode {
|
||||
const isVideoRoom = SettingsStore.getValue("feature_video_rooms") && calcIsVideoRoom(this.props.room);
|
||||
|
||||
let roomAvatar: JSX.Element | null = null;
|
||||
if (this.props.room) {
|
||||
roomAvatar = (
|
||||
<DecoratedRoomAvatar
|
||||
room={this.props.room}
|
||||
avatarSize={24}
|
||||
oobData={this.props.oobData}
|
||||
viewAvatarOnClick={true}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const icon = this.props.viewingCall ? (
|
||||
<div className="mx_RoomHeader_icon mx_RoomHeader_icon_video" />
|
||||
) : this.props.e2eStatus ? (
|
||||
<E2EIcon className="mx_RoomHeader_icon" status={this.props.e2eStatus} tooltipAlignment={Alignment.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_RoomHeader_icon" />
|
||||
) : null;
|
||||
|
||||
const buttons = this.props.showButtons ? this.renderButtons(isVideoRoom) : null;
|
||||
|
||||
let oobName = _t("Join 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_RoomHeader light-panel">
|
||||
<div
|
||||
className="mx_RoomHeader_wrapper"
|
||||
aria-owns={this.state.rightPanelOpen ? "mx_RightPanel" : undefined}
|
||||
>
|
||||
<div className="mx_RoomHeader_avatar">{roomAvatar}</div>
|
||||
{icon}
|
||||
{name}
|
||||
{this.props.activeCall instanceof ElementCall && (
|
||||
<GroupCallDuration groupCall={this.props.activeCall.groupCall} />
|
||||
)}
|
||||
{/* Empty topic element to fill out space */}
|
||||
<div className="mx_RoomHeader_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_RoomHeader_searchStatus">
|
||||
|
||||
{_t("(~%(count)s results)", { count: this.props.searchInfo.count })}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const topicElement = <RoomTopic room={this.props.room} className="mx_RoomHeader_topic" />;
|
||||
|
||||
const viewLabs = (): void =>
|
||||
defaultDispatcher.dispatch({
|
||||
action: Action.ViewUserSettings,
|
||||
initialTabId: UserTab.Labs,
|
||||
});
|
||||
const betaPill = isVideoRoom ? (
|
||||
<BetaPill onClick={viewLabs} tooltipTitle={_t("Video rooms are a beta feature")} />
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<header className="mx_RoomHeader light-panel">
|
||||
<div
|
||||
className="mx_RoomHeader_wrapper"
|
||||
aria-owns={this.state.rightPanelOpen ? "mx_RightPanel" : undefined}
|
||||
>
|
||||
<div className="mx_RoomHeader_avatar">{roomAvatar}</div>
|
||||
{icon}
|
||||
{name}
|
||||
{searchStatus}
|
||||
{topicElement}
|
||||
{betaPill}
|
||||
{buttons}
|
||||
</div>
|
||||
{!isVideoRoom && <RoomCallBanner roomId={this.props.room.roomId} />}
|
||||
<RoomLiveShareWarning roomId={this.props.room.roomId} />
|
||||
</header>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1009,6 +1009,7 @@
|
|||
"Hide notification dot (only display counters badges)": "Hide notification dot (only display counters badges)",
|
||||
"Enable intentional mentions": "Enable intentional mentions",
|
||||
"Enable ask to join": "Enable ask to join",
|
||||
"Under active development, new room header & details interface": "Under active development, new room header & details interface",
|
||||
"Use a more compact 'Modern' layout": "Use a more compact 'Modern' layout",
|
||||
"Show a placeholder for removed messages": "Show a placeholder for removed messages",
|
||||
"Show join/leave messages (invites/removes/bans unaffected)": "Show join/leave messages (invites/removes/bans unaffected)",
|
||||
|
@ -1941,6 +1942,26 @@
|
|||
"Encrypted messages before this point are unavailable.": "Encrypted messages before this point are unavailable.",
|
||||
"You can't see earlier messages": "You can't see earlier messages",
|
||||
"Scroll to most recent messages": "Scroll to most recent messages",
|
||||
"Video call (Jitsi)": "Video call (Jitsi)",
|
||||
"Video call (%(brand)s)": "Video call (%(brand)s)",
|
||||
"Ongoing call": "Ongoing call",
|
||||
"You do not have permission to start video calls": "You do not have permission to start video calls",
|
||||
"There's no one here to call": "There's no one here to call",
|
||||
"You do not have permission to start voice calls": "You do not have permission to start voice calls",
|
||||
"Freedom": "Freedom",
|
||||
"Spotlight": "Spotlight",
|
||||
"Change layout": "Change layout",
|
||||
"Forget room": "Forget room",
|
||||
"Hide Widgets": "Hide Widgets",
|
||||
"Show Widgets": "Show Widgets",
|
||||
"Search": "Search",
|
||||
"Close call": "Close call",
|
||||
"View chat timeline": "View chat timeline",
|
||||
"Room options": "Room options",
|
||||
"Join Room": "Join Room",
|
||||
"(~%(count)s results)|other": "(~%(count)s results)",
|
||||
"(~%(count)s results)|one": "(~%(count)s result)",
|
||||
"Video rooms are a beta feature": "Video rooms are a beta feature",
|
||||
"Show %(count)s other previews|other": "Show %(count)s other previews",
|
||||
"Show %(count)s other previews|one": "Show %(count)s other preview",
|
||||
"Close preview": "Close preview",
|
||||
|
@ -2018,26 +2039,6 @@
|
|||
"Room %(name)s": "Room %(name)s",
|
||||
"Recently visited rooms": "Recently visited rooms",
|
||||
"No recently visited rooms": "No recently visited rooms",
|
||||
"Video call (Jitsi)": "Video call (Jitsi)",
|
||||
"Video call (%(brand)s)": "Video call (%(brand)s)",
|
||||
"Ongoing call": "Ongoing call",
|
||||
"You do not have permission to start video calls": "You do not have permission to start video calls",
|
||||
"There's no one here to call": "There's no one here to call",
|
||||
"You do not have permission to start voice calls": "You do not have permission to start voice calls",
|
||||
"Freedom": "Freedom",
|
||||
"Spotlight": "Spotlight",
|
||||
"Change layout": "Change layout",
|
||||
"Forget room": "Forget room",
|
||||
"Hide Widgets": "Hide Widgets",
|
||||
"Show Widgets": "Show Widgets",
|
||||
"Search": "Search",
|
||||
"Close call": "Close call",
|
||||
"View chat timeline": "View chat timeline",
|
||||
"Room options": "Room options",
|
||||
"Join Room": "Join Room",
|
||||
"(~%(count)s results)|other": "(~%(count)s results)",
|
||||
"(~%(count)s results)|one": "(~%(count)s result)",
|
||||
"Video rooms are a beta feature": "Video rooms are a beta feature",
|
||||
"Video room": "Video room",
|
||||
"Public space": "Public space",
|
||||
"Public room": "Public room",
|
||||
|
@ -2242,11 +2243,11 @@
|
|||
"Yours, or the other users' session": "Yours, or the other users' session",
|
||||
"Error starting verification": "Error starting verification",
|
||||
"We were unable to start a chat with the other user.": "We were unable to start a chat with the other user.",
|
||||
"Nothing pinned, yet": "Nothing pinned, yet",
|
||||
"If you have permissions, open the menu on any message and select <b>Pin</b> to stick them here.": "If you have permissions, open the menu on any message and select <b>Pin</b> to stick them here.",
|
||||
"Pinned messages": "Pinned messages",
|
||||
"Chat": "Chat",
|
||||
"Room info": "Room info",
|
||||
"Nothing pinned, yet": "Nothing pinned, yet",
|
||||
"If you have permissions, open the menu on any message and select <b>Pin</b> to stick them here.": "If you have permissions, open the menu on any message and select <b>Pin</b> to stick them here.",
|
||||
"You can only pin up to %(count)s widgets|other": "You can only pin up to %(count)s widgets",
|
||||
"Maximise": "Maximise",
|
||||
"Unpin this widget to view it in this panel": "Unpin this widget to view it in this panel",
|
||||
|
|
|
@ -566,6 +566,14 @@ export const SETTINGS: { [setting: string]: ISetting } = {
|
|||
labsGroup: LabGroup.Rooms,
|
||||
supportedLevels: LEVELS_FEATURE,
|
||||
},
|
||||
"feature_new_room_decoration_ui": {
|
||||
isFeature: true,
|
||||
labsGroup: LabGroup.Rooms,
|
||||
displayName: _td("Under active development, new room header & details interface"),
|
||||
supportedLevels: LEVELS_FEATURE,
|
||||
default: false,
|
||||
controller: new ReloadOnChangeController(),
|
||||
},
|
||||
"useCompactLayout": {
|
||||
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS,
|
||||
displayName: _td("Use a more compact 'Modern' layout"),
|
||||
|
|
|
@ -181,23 +181,23 @@ export default class HTMLExporter extends Exporter {
|
|||
<div class="mx_MatrixChat_wrapper" aria-hidden="false">
|
||||
<div class="mx_MatrixChat">
|
||||
<main class="mx_RoomView">
|
||||
<div class="mx_RoomHeader light-panel">
|
||||
<div class="mx_RoomHeader_wrapper" aria-owns="mx_RightPanel">
|
||||
<div class="mx_RoomHeader_avatar">
|
||||
<div class="mx_LegacyRoomHeader light-panel">
|
||||
<div class="mx_LegacyRoomHeader_wrapper" aria-owns="mx_RightPanel">
|
||||
<div class="mx_LegacyRoomHeader_avatar">
|
||||
<div class="mx_DecoratedRoomAvatar">
|
||||
${roomAvatar}
|
||||
</div>
|
||||
</div>
|
||||
<div class="mx_RoomHeader_name">
|
||||
<div class="mx_LegacyRoomHeader_name">
|
||||
<div
|
||||
dir="auto"
|
||||
class="mx_RoomHeader_nametext"
|
||||
class="mx_LegacyRoomHeader_nametext"
|
||||
title="${safeRoomName}"
|
||||
>
|
||||
${safeRoomName}
|
||||
</div>
|
||||
</div>
|
||||
<div class="mx_RoomHeader_topic" dir="auto"> ${safeTopic} </div>
|
||||
<div class="mx_LegacyRoomHeader_topic" dir="auto"> ${safeTopic} </div>
|
||||
</div>
|
||||
</div>
|
||||
${previousMessagesLink}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue