Files
ThreadNet-Web/src/components/structures/RoomView.tsx
T

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

2689 lines
111 KiB
TypeScript
Raw Normal View History

/*
2024-09-09 14:57:16 +01:00
Copyright 2024 New Vector Ltd.
Copyright 2019-2023 The Matrix.org Foundation C.I.C.
Copyright 2018, 2019 New Vector Ltd
2024-09-09 14:57:16 +01:00
Copyright 2017 Vector Creations Ltd
Copyright 2015, 2016 OpenMarket Ltd
2024-09-09 14:57:16 +01:00
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
2024-07-08 10:57:41 +01:00
import React, { ChangeEvent, createRef, ReactElement, ReactNode, RefObject, useContext } from "react";
2019-04-02 10:50:17 +01:00
import classNames from "classnames";
import {
IRecommendedVersion,
NotificationCountType,
Room,
RoomEvent,
RoomState,
RoomStateEvent,
MatrixEvent,
MatrixEventEvent,
EventTimeline,
IRoomTimelineData,
EventType,
HistoryVisibility,
JoinRule,
ClientEvent,
MatrixError,
ISearchResults,
THREAD_RELATION_TYPE,
} from "matrix-js-sdk/src/matrix";
import { KnownMembership } from "matrix-js-sdk/src/types";
import { logger } from "matrix-js-sdk/src/logger";
import { CallState, MatrixCall } from "matrix-js-sdk/src/webrtc/call";
2024-07-08 10:57:41 +01:00
import { debounce, throttle } from "lodash";
import { CryptoEvent } from "matrix-js-sdk/src/crypto";
import { ViewRoomOpts } from "@matrix-org/react-sdk-module-api/lib/lifecycles/RoomViewLifecycle";
2020-09-07 17:32:03 +01:00
import shouldHideEvent from "../../shouldHideEvent";
2021-01-22 16:58:47 -07:00
import { _t } from "../../languageHandler";
2024-09-02 11:07:07 +02:00
import * as TimezoneHandler from "../../TimezoneHandler";
2021-01-22 16:58:47 -07:00
import { RoomPermalinkCreator } from "../../utils/permalinks/Permalinks";
2020-09-07 17:32:03 +01:00
import ResizeNotifier from "../../utils/ResizeNotifier";
import ContentMessages from "../../ContentMessages";
2019-04-02 10:50:17 +01:00
import Modal from "../../Modal";
import { LegacyCallHandlerEvent } from "../../LegacyCallHandler";
2022-08-04 08:19:52 +02:00
import dis, { defaultDispatcher } from "../../dispatcher/dispatcher";
2019-05-09 16:17:54 +02:00
import * as Rooms from "../../Rooms";
import MainSplit from "./MainSplit";
import RightPanel from "./RightPanel";
2021-07-03 12:17:38 +01:00
import RoomScrollStateStore, { ScrollState } from "../../stores/RoomScrollStateStore";
2018-07-03 11:16:44 +01:00
import WidgetEchoStore from "../../stores/WidgetEchoStore";
import SettingsStore from "../../settings/SettingsStore";
import { Layout } from "../../settings/enums/Layout";
2023-02-13 11:39:16 +00:00
import AccessibleButton, { ButtonEvent } from "../views/elements/AccessibleButton";
import RoomContext, { TimelineRenderingType } from "../../contexts/RoomContext";
2021-01-22 16:58:47 -07:00
import { E2EStatus, shieldStatusForRoom } from "../../utils/ShieldUtils";
import { Action } from "../../dispatcher/actions";
import { IMatrixClientCreds } from "../../MatrixClientPeg";
2020-09-07 17:32:03 +01:00
import ScrollPanel from "./ScrollPanel";
import TimelinePanel from "./TimelinePanel";
import ErrorBoundary from "../views/elements/ErrorBoundary";
import RoomPreviewBar from "../views/rooms/RoomPreviewBar";
2022-05-04 17:02:06 -04:00
import RoomPreviewCard from "../views/rooms/RoomPreviewCard";
2020-09-07 17:32:03 +01:00
import RoomUpgradeWarningBar from "../views/rooms/RoomUpgradeWarningBar";
import AuxPanel from "../views/rooms/AuxPanel";
import LegacyRoomHeader from "../views/rooms/LegacyRoomHeader";
import RoomHeader from "../views/rooms/RoomHeader";
2021-07-01 19:54:05 +01:00
import { IOOBData, IThreepidInvite } from "../../stores/ThreepidInviteStore";
import EffectsOverlay from "../views/elements/EffectsOverlay";
2021-01-22 16:58:47 -07:00
import { containsEmoji } from "../../effects/utils";
import { CHAT_EFFECTS } from "../../effects";
import { CallView } from "../views/voip/CallView";
2021-01-22 16:58:47 -07:00
import { UPDATE_EVENT } from "../../stores/AsyncStore";
2020-11-02 17:25:48 +00:00
import Notifier from "../../Notifier";
2021-01-22 16:58:47 -07:00
import { showToast as showNotificationsToast } from "../../toasts/DesktopNotificationsToast";
import { Container, WidgetLayoutStore } from "../../stores/widgets/WidgetLayoutStore";
import { getKeyBindingsManager } from "../../KeyBindingsManager";
2021-02-19 00:00:10 +00:00
import { objectHasDiff } from "../../utils/objects";
import SpaceRoomView from "./SpaceRoomView";
import { IOpts } from "../../createRoom";
import EditorStateTransfer from "../../utils/EditorStateTransfer";
2021-07-03 11:51:23 +02:00
import ErrorDialog from "../views/dialogs/ErrorDialog";
import UploadBar from "./UploadBar";
import RoomStatusBar from "./RoomStatusBar";
import MessageComposer from "../views/rooms/MessageComposer";
import JumpToBottomButton from "../views/rooms/JumpToBottomButton";
import TopUnreadMessagesBar from "../views/rooms/TopUnreadMessagesBar";
import { fetchInitialEvent } from "../../utils/EventUtils";
import { ComposerInsertPayload, ComposerType } from "../../dispatcher/payloads/ComposerInsertPayload";
2021-11-16 15:43:18 +01:00
import AppsDrawer from "../views/rooms/AppsDrawer";
2022-01-05 16:14:44 +01:00
import { RightPanelPhases } from "../../stores/right-panel/RightPanelStorePhases";
import { ActionPayload } from "../../dispatcher/payloads";
import { KeyBindingAction } from "../../accessibility/KeyboardShortcuts";
import { ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload";
import { JoinRoomPayload } from "../../dispatcher/payloads/JoinRoomPayload";
2022-02-22 11:04:27 +01:00
import { DoAfterSyncPreparedPayload } from "../../dispatcher/payloads/DoAfterSyncPreparedPayload";
import FileDropTarget from "./FileDropTarget";
import Measured from "../views/elements/Measured";
import { FocusComposerPayload } from "../../dispatcher/payloads/FocusComposerPayload";
2022-08-04 08:19:52 +02:00
import { LocalRoom, LocalRoomState } from "../../models/LocalRoom";
import { createRoomFromLocalRoom } from "../../utils/direct-messages";
import NewRoomIntro from "../views/rooms/NewRoomIntro";
import EncryptionEvent from "../views/messages/EncryptionEvent";
import { StaticNotificationState } from "../../stores/notifications/StaticNotificationState";
import { isLocalRoom } from "../../utils/localRoom/isLocalRoom";
import { ShowThreadPayload } from "../../dispatcher/payloads/ShowThreadPayload";
2022-08-04 08:19:52 +02:00
import { RoomStatusBarUnsentMessages } from "./RoomStatusBarUnsentMessages";
import { LargeLoader } from "./LargeLoader";
2022-09-16 11:12:27 -04:00
import { isVideoRoom } from "../../utils/video-rooms";
import { SDKContext } from "../../contexts/SDKContext";
import { CallStore, CallStoreEvent } from "../../stores/CallStore";
import { Call } from "../../models/Call";
import { RoomSearchView } from "./RoomSearchView";
import eventSearch, { SearchInfo, SearchScope } from "../../Searching";
import VoipUserMapper from "../../VoipUserMapper";
import { isCallEvent } from "./LegacyCallEventGrouper";
import { WidgetType } from "../../widgets/WidgetType";
import WidgetUtils from "../../utils/WidgetUtils";
import { shouldEncryptRoomWithSingle3rdPartyInvite } from "../../utils/room/shouldEncryptRoomWithSingle3rdPartyInvite";
import { WaitingForThirdPartyRoomView } from "./WaitingForThirdPartyRoomView";
2023-06-30 13:06:03 +01:00
import { isNotUndefined } from "../../Typeguards";
2023-08-07 08:27:09 +02:00
import { CancelAskToJoinPayload } from "../../dispatcher/payloads/CancelAskToJoinPayload";
import { SubmitAskToJoinPayload } from "../../dispatcher/payloads/SubmitAskToJoinPayload";
2023-08-31 07:46:09 +01:00
import RightPanelStore from "../../stores/right-panel/RightPanelStore";
import { onView3pidInvite } from "../../stores/right-panel/action-handlers";
2024-07-08 10:57:41 +01:00
import RoomSearchAuxPanel from "../views/rooms/RoomSearchAuxPanel";
import { PinnedMessageBanner } from "../views/rooms/PinnedMessageBanner";
2017-10-11 17:56:17 +01:00
const DEBUG = false;
const PREVENT_MULTIPLE_JITSI_WITHIN = 30_000;
let debuglog = function (msg: string): void {};
2017-07-12 10:21:43 +01:00
const BROWSER_SUPPORTS_SANDBOX = "sandbox" in document.createElement("iframe");
/* istanbul ignore next */
2016-02-10 18:27:40 +00:00
if (DEBUG) {
2016-01-04 16:28:32 +00:00
// using bind means that we get to keep useful line numbers in the console
debuglog = logger.log.bind(console);
2016-01-04 16:28:32 +00:00
}
interface IRoomProps {
threepidInvite?: IThreepidInvite;
2021-07-01 19:54:05 +01:00
oobData?: IOOBData;
2016-06-14 12:56:37 +01:00
2020-09-07 17:32:03 +01:00
resizeNotifier: ResizeNotifier;
justCreatedOpts?: IOpts;
forceTimeline?: boolean; // should we force access to the timeline, overriding (for eg) spaces
2020-09-07 17:32:03 +01:00
// Called with the credentials of a registered user (if they were a ROU that transitioned to PWLU)
onRegistered?(credentials: IMatrixClientCreds): void;
}
2021-11-16 15:43:18 +01:00
// This defines the content of the mainSplit.
// If the mainSplit does not contain the Timeline, the chat is shown in the right panel.
export enum MainSplitContentType {
2021-11-16 15:43:18 +01:00
Timeline,
MaximisedWidget,
Call,
2021-11-16 15:43:18 +01:00
}
export interface IRoomState {
2020-09-07 17:32:03 +01:00
room?: Room;
virtualRoom?: Room;
2020-09-07 17:32:03 +01:00
roomId?: string;
roomAlias?: string;
roomLoading: boolean;
peekLoading: boolean;
shouldPeek: boolean;
// used to trigger a rerender in TimelinePanel once the members are loaded,
// so RR are rendered again (now with the members available), ...
membersLoaded: boolean;
// The event to be scrolled to initially
initialEventId?: string;
// The offset in pixels from the event with which to scroll vertically
initialEventPixelOffset?: number;
// Whether to highlight the event scrolled to
isInitialEventHighlighted?: boolean;
// Whether to scroll the event into view
initialEventScrollIntoView?: boolean;
replyToEvent?: MatrixEvent;
2020-09-07 17:32:03 +01:00
numUnreadMessages: number;
/**
* The state of an ongoing search if there is one.
*/
search?: SearchInfo;
2020-10-09 18:56:07 +01:00
callState?: CallState;
activeCall: Call | null;
2020-09-07 17:32:03 +01:00
canPeek: boolean;
canSelfRedact: boolean;
2020-09-07 17:32:03 +01:00
showApps: boolean;
isPeeking: boolean;
showRightPanel: boolean;
/**
* Whether the right panel shown is either of ThreadPanel or ThreadView.
* Always false when `showRightPanel` is false.
*/
threadRightPanel: boolean;
2020-09-07 17:32:03 +01:00
// error object, as from the matrix client/server API
// If we failed to load information about the room,
// store the error here.
2021-12-14 15:34:54 +00:00
roomLoadError?: MatrixError;
2020-09-07 17:32:03 +01:00
// Have we sent a request to join the room that we're waiting to complete?
joining: boolean;
// this is true if we are fully scrolled-down, and are looking at
// the end of the live timeline. It has the effect of hiding the
// 'scroll to bottom' knob, among a couple of other things.
atEndOfLiveTimeline?: boolean;
2020-09-07 17:32:03 +01:00
showTopUnreadMessagesBar: boolean;
statusBarVisible: boolean;
// We load this later by asking the js-sdk to suggest a version for us.
// This object is the result of Room#getRecommendedVersion()
2021-05-19 14:32:49 +01:00
2021-06-18 15:31:12 +01:00
upgradeRecommendation?: IRecommendedVersion;
2020-09-07 17:32:03 +01:00
canReact: boolean;
canSendMessages: boolean;
tombstone?: MatrixEvent;
2022-03-22 18:14:11 -04:00
resizing: boolean;
2021-01-22 13:44:45 +01:00
layout: Layout;
lowBandwidth: boolean;
alwaysShowTimestamps: boolean;
showTwelveHourTimestamps: boolean;
2024-09-02 11:07:07 +02:00
userTimezone: string | undefined;
readMarkerInViewThresholdMs: number;
readMarkerOutOfViewThresholdMs: number;
showHiddenEvents: boolean;
showReadReceipts: boolean;
showRedactions: boolean;
showJoinLeaves: boolean;
showAvatarChanges: boolean;
showDisplaynameChanges: boolean;
2020-09-07 17:32:03 +01:00
matrixClientIsReady: boolean;
showUrlPreview?: boolean;
e2eStatus?: E2EStatus;
rejecting?: boolean;
2020-10-09 08:52:34 +01:00
hasPinnedWidgets?: boolean;
2021-11-16 15:43:18 +01:00
mainSplitContentType?: MainSplitContentType;
// whether or not a spaces context switch brought us here,
// if it did we don't want the room to be marked as read as soon as it is loaded.
wasContextSwitch?: boolean;
editState?: EditorStateTransfer;
timelineRenderingType: TimelineRenderingType;
liveTimeline?: EventTimeline;
narrow: boolean;
msc3946ProcessDynamicPredecessor: boolean;
2023-08-07 08:27:09 +02:00
canAskToJoin: boolean;
promptAskToJoin: boolean;
viewRoomOpts: ViewRoomOpts;
2020-09-07 17:32:03 +01:00
}
2022-08-04 08:19:52 +02:00
interface LocalRoomViewProps {
localRoom: LocalRoom;
2022-08-04 08:19:52 +02:00
resizeNotifier: ResizeNotifier;
permalinkCreator: RoomPermalinkCreator;
roomView: RefObject<HTMLElement>;
onFileDrop: (dataTransfer: DataTransfer) => Promise<void>;
}
/**
* Local room view. Uses only the bits necessary to display a local room view like room header or composer.
*
* @param {LocalRoomViewProps} props Room view props
* @returns {ReactElement}
*/
function LocalRoomView(props: LocalRoomViewProps): ReactElement {
const context = useContext(RoomContext);
const room = context.room as LocalRoom;
const encryptionEvent = props.localRoom.currentState.getStateEvents(EventType.RoomEncryption)[0];
2022-08-04 08:19:52 +02:00
let encryptionTile: ReactNode;
if (encryptionEvent) {
encryptionTile = <EncryptionEvent mxEvent={encryptionEvent} />;
}
const onRetryClicked = (): void => {
2022-08-04 08:19:52 +02:00
room.state = LocalRoomState.NEW;
defaultDispatcher.dispatch({
action: "local_room_event",
roomId: room.roomId,
});
};
let statusBar: ReactElement | null = null;
let composer: ReactElement | null = null;
2022-08-04 08:19:52 +02:00
if (room.isError) {
const buttons = (
<AccessibleButton onClick={onRetryClicked} className="mx_RoomStatusBar_unsentRetry">
{_t("action|retry")}
2022-08-04 08:19:52 +02:00
</AccessibleButton>
);
statusBar = (
<RoomStatusBarUnsentMessages
title={_t("room|status_bar|some_messages_not_sent")}
2022-08-04 08:19:52 +02:00
notificationState={StaticNotificationState.RED_EXCLAMATION}
buttons={buttons}
/>
);
} else {
composer = (
<MessageComposer
room={props.localRoom}
2022-08-04 08:19:52 +02:00
resizeNotifier={props.resizeNotifier}
permalinkCreator={props.permalinkCreator}
/>
);
}
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}
/>
)}
2022-08-04 08:19:52 +02:00
<main className="mx_RoomView_body" ref={props.roomView}>
<FileDropTarget parent={props.roomView.current} onFileDrop={props.onFileDrop} />
<div className="mx_RoomView_timeline">
<ScrollPanel className="mx_RoomView_messagePanel" resizeNotifier={props.resizeNotifier}>
{encryptionTile}
<NewRoomIntro />
</ScrollPanel>
</div>
{statusBar}
{composer}
</main>
</ErrorBoundary>
</div>
);
}
interface ILocalRoomCreateLoaderProps {
localRoom: LocalRoom;
2022-08-04 08:19:52 +02:00
names: string;
resizeNotifier: ResizeNotifier;
}
/**
* Room create loader view displaying a message and a spinner.
*
* @param {ILocalRoomCreateLoaderProps} props Room view props
* @return {ReactElement}
*/
function LocalRoomCreateLoader(props: ILocalRoomCreateLoaderProps): ReactElement {
const text = _t("room|creating_room_text", { names: props.names });
2022-08-04 08:19:52 +02:00
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}
/>
)}
2022-08-04 08:19:52 +02:00
<div className="mx_RoomView_body">
<LargeLoader text={text} />
</div>
</ErrorBoundary>
</div>
);
}
export class RoomView extends React.Component<IRoomProps, IRoomState> {
// We cache the latest computed e2eStatus per room to show as soon as we switch rooms otherwise defaulting to
// unencrypted causes a flicker which can yield confusion/concern in a larger room.
private static e2eStatusCache = new Map<string, E2EStatus>();
2023-08-07 08:27:09 +02:00
private readonly askToJoinEnabled: boolean;
2020-09-07 17:32:03 +01:00
private readonly dispatcherRef: string;
private settingWatchers: string[];
2020-09-07 17:32:03 +01:00
private unmounted = false;
private permalinkCreators: Record<string, RoomPermalinkCreator> = {};
private roomView = createRef<HTMLDivElement>();
2020-09-07 17:32:03 +01:00
private searchResultsPanel = createRef<ScrollPanel>();
private messagePanel: TimelinePanel | null = null;
private roomViewBody = createRef<HTMLDivElement>();
2020-09-07 17:32:03 +01:00
public static contextType = SDKContext;
2024-08-01 17:14:28 +01:00
public declare context: React.ContextType<typeof SDKContext>;
2020-08-29 12:14:16 +01:00
public constructor(props: IRoomProps, context: React.ContextType<typeof SDKContext>) {
2020-08-29 12:51:37 +01:00
super(props, context);
2023-08-07 08:27:09 +02:00
this.askToJoinEnabled = SettingsStore.getValue("feature_ask_to_join");
if (!context.client) {
throw new Error("Unable to create RoomView without MatrixClient");
}
const llMembers = context.client.hasLazyLoadMembersEnabled();
2020-08-29 12:14:16 +01:00
this.state = {
roomId: undefined,
2016-06-14 14:38:45 +01:00
roomLoading: true,
peekLoading: false,
2017-06-16 18:24:07 +01:00
shouldPeek: true,
membersLoaded: !llMembers,
numUnreadMessages: 0,
callState: undefined,
activeCall: null,
canPeek: false,
canSelfRedact: false,
showApps: false,
2017-10-11 18:43:24 -06:00
isPeeking: false,
2022-02-16 11:19:28 +00:00
showRightPanel: false,
threadRightPanel: false,
2017-09-15 15:07:09 +01:00
joining: false,
showTopUnreadMessagesBar: false,
statusBarVisible: false,
canReact: false,
canSendMessages: false,
2022-03-22 18:14:11 -04:00
resizing: false,
2021-01-22 13:44:45 +01:00
layout: SettingsStore.getValue("layout"),
lowBandwidth: SettingsStore.getValue("lowBandwidth"),
alwaysShowTimestamps: SettingsStore.getValue("alwaysShowTimestamps"),
showTwelveHourTimestamps: SettingsStore.getValue("showTwelveHourTimestamps"),
2024-09-02 11:07:07 +02:00
userTimezone: TimezoneHandler.getUserTimezone(),
readMarkerInViewThresholdMs: SettingsStore.getValue("readMarkerInViewThresholdMs"),
readMarkerOutOfViewThresholdMs: SettingsStore.getValue("readMarkerOutOfViewThresholdMs"),
showHiddenEvents: SettingsStore.getValue("showHiddenEventsInTimeline"),
showReadReceipts: true,
showRedactions: true,
showJoinLeaves: true,
showAvatarChanges: true,
showDisplaynameChanges: true,
matrixClientIsReady: context.client?.isInitialSyncComplete(),
2021-11-16 15:43:18 +01:00
mainSplitContentType: MainSplitContentType.Timeline,
timelineRenderingType: TimelineRenderingType.Room,
liveTimeline: undefined,
narrow: false,
msc3946ProcessDynamicPredecessor: SettingsStore.getValue("feature_dynamic_room_predecessors"),
2023-08-07 08:27:09 +02:00
canAskToJoin: this.askToJoinEnabled,
promptAskToJoin: false,
viewRoomOpts: { buttons: [] },
};
this.dispatcherRef = dis.register(this.onAction);
context.client.on(ClientEvent.Room, this.onRoom);
context.client.on(RoomEvent.Timeline, this.onRoomTimeline);
context.client.on(RoomEvent.TimelineReset, this.onRoomTimelineReset);
context.client.on(RoomEvent.Name, this.onRoomName);
context.client.on(RoomStateEvent.Events, this.onRoomStateEvents);
context.client.on(RoomStateEvent.Update, this.onRoomStateUpdate);
context.client.on(RoomEvent.MyMembership, this.onMyMembership);
context.client.on(CryptoEvent.KeyBackupStatus, this.onKeyBackupStatus);
context.client.on(CryptoEvent.DeviceVerificationChanged, this.onDeviceVerificationChanged);
context.client.on(CryptoEvent.UserTrustStatusChanged, this.onUserVerificationChanged);
context.client.on(CryptoEvent.KeysChanged, this.onCrossSigningKeysChanged);
context.client.on(MatrixEventEvent.Decrypted, this.onEventDecrypted);
2017-05-24 16:56:13 +01:00
// Start listening for RoomViewStore updates
context.roomViewStore.on(UPDATE_EVENT, this.onRoomViewStoreUpdate);
2022-01-05 16:14:44 +01:00
context.rightPanelStore.on(UPDATE_EVENT, this.onRightPanelStoreUpdate);
2018-07-03 11:16:44 +01:00
2020-10-09 08:42:21 +01:00
WidgetEchoStore.on(UPDATE_EVENT, this.onWidgetEchoStoreUpdate);
context.widgetStore.on(UPDATE_EVENT, this.onWidgetStoreUpdate);
2020-10-09 08:42:21 +01:00
CallStore.instance.on(CallStoreEvent.ConnectedCalls, this.onConnectedCalls);
2022-03-22 18:14:11 -04:00
this.props.resizeNotifier.on("isResizing", this.onIsResizing);
this.settingWatchers = [
SettingsStore.watchSetting("layout", null, (...[, , , value]) =>
this.setState({ layout: value as Layout }),
),
SettingsStore.watchSetting("lowBandwidth", null, (...[, , , value]) =>
this.setState({ lowBandwidth: value as boolean }),
),
SettingsStore.watchSetting("alwaysShowTimestamps", null, (...[, , , value]) =>
this.setState({ alwaysShowTimestamps: value as boolean }),
),
SettingsStore.watchSetting("showTwelveHourTimestamps", null, (...[, , , value]) =>
this.setState({ showTwelveHourTimestamps: value as boolean }),
),
2024-09-02 11:07:07 +02:00
SettingsStore.watchSetting(TimezoneHandler.USER_TIMEZONE_KEY, null, (...[, , , value]) =>
this.setState({ userTimezone: value as string }),
),
SettingsStore.watchSetting("readMarkerInViewThresholdMs", null, (...[, , , value]) =>
this.setState({ readMarkerInViewThresholdMs: value as number }),
),
SettingsStore.watchSetting("readMarkerOutOfViewThresholdMs", null, (...[, , , value]) =>
this.setState({ readMarkerOutOfViewThresholdMs: value as number }),
),
SettingsStore.watchSetting("showHiddenEventsInTimeline", null, (...[, , , value]) =>
this.setState({ showHiddenEvents: value as boolean }),
),
SettingsStore.watchSetting("urlPreviewsEnabled", null, this.onUrlPreviewsEnabledChange),
SettingsStore.watchSetting("urlPreviewsEnabled_e2ee", null, this.onUrlPreviewsEnabledChange),
SettingsStore.watchSetting("feature_dynamic_room_predecessors", null, (...[, , , value]) =>
this.setState({ msc3946ProcessDynamicPredecessor: value as boolean }),
),
];
2020-08-29 12:14:16 +01:00
}
2019-10-03 23:20:00 -06:00
private onIsResizing = (resizing: boolean): void => {
2022-03-22 18:14:11 -04:00
this.setState({ resizing });
};
private onWidgetStoreUpdate = (): void => {
2021-11-16 15:43:18 +01:00
if (!this.state.room) return;
this.checkWidgets(this.state.room);
this.doMaybeRemoveOwnJitsiWidget();
2021-11-16 15:43:18 +01:00
};
private onWidgetEchoStoreUpdate = (): void => {
2021-11-16 15:43:18 +01:00
if (!this.state.room) return;
this.checkWidgets(this.state.room);
};
private onWidgetLayoutChange = (): void => {
2021-11-16 15:43:18 +01:00
if (!this.state.room) return;
dis.dispatch({
action: "appsDrawer",
show: true,
});
if (this.context.widgetLayoutStore.hasMaximisedWidget(this.state.room)) {
2021-11-29 17:06:15 +01:00
// Show chat in right panel when a widget is maximised
this.context.rightPanelStore.setCard({ phase: RightPanelPhases.Timeline });
2021-11-29 17:06:15 +01:00
}
2021-11-16 15:43:18 +01:00
this.checkWidgets(this.state.room);
2021-06-29 13:11:58 +01:00
};
2020-10-09 08:42:21 +01:00
/**
* Removes the Jitsi widget from the current user if
* - Multiple Jitsi widgets have been added within {@link PREVENT_MULTIPLE_JITSI_WITHIN}
* - The last (server timestamp) of these widgets is from the current user
* This solves the issue if some people decide to start a conference and click the call button at the same time.
*/
private doMaybeRemoveOwnJitsiWidget(): void {
if (!this.state.roomId || !this.state.room || !this.context.client) return;
const apps = this.context.widgetStore.getApps(this.state.roomId);
const jitsiApps = apps.filter((app) => app.eventId && WidgetType.JITSI.matches(app.type));
// less than two Jitsi widgets → nothing to do
if (jitsiApps.length < 2) return;
const currentUserId = this.context.client.getSafeUserId();
const createdByCurrentUser = jitsiApps.find((apps) => apps.creatorUserId === currentUserId);
// no Jitsi widget from current user → nothing to do
if (!createdByCurrentUser) return;
const createdByCurrentUserEvent = this.state.room.findEventById(createdByCurrentUser.eventId!);
// widget event not found → nothing can be done
if (!createdByCurrentUserEvent) return;
const createdByCurrentUserTs = createdByCurrentUserEvent.getTs();
// widget timestamp is empty → nothing can be done
if (!createdByCurrentUserTs) return;
const lastCreatedByOtherTs = jitsiApps.reduce((maxByNow: number, app) => {
if (app.eventId === createdByCurrentUser.eventId) return maxByNow;
const appCreateTs = this.state.room!.findEventById(app.eventId!)?.getTs() || 0;
return Math.max(maxByNow, appCreateTs);
}, 0);
// last widget timestamp from other is empty → nothing can be done
if (!lastCreatedByOtherTs) return;
if (
createdByCurrentUserTs > lastCreatedByOtherTs &&
createdByCurrentUserTs - lastCreatedByOtherTs < PREVENT_MULTIPLE_JITSI_WITHIN
) {
// more than one Jitsi widget with the last one from the current user → remove it
WidgetUtils.setRoomWidget(this.context.client, this.state.roomId, createdByCurrentUser.id);
}
}
private checkWidgets = (room: Room): void => {
2020-10-09 08:42:21 +01:00
this.setState({
hasPinnedWidgets: this.context.widgetLayoutStore.hasPinnedWidgets(room),
mainSplitContentType: this.getMainSplitContentType(room),
showApps: this.shouldShowApps(room),
});
2020-10-09 08:42:21 +01:00
};
private getMainSplitContentType = (room: Room): MainSplitContentType => {
if (
(SettingsStore.getValue("feature_group_calls") && this.context.roomViewStore.isViewingCall()) ||
isVideoRoom(room)
) {
return MainSplitContentType.Call;
2022-03-22 18:14:11 -04:00
}
if (this.context.widgetLayoutStore.hasMaximisedWidget(room)) {
2022-03-22 18:14:11 -04:00
return MainSplitContentType.MaximisedWidget;
}
return MainSplitContentType.Timeline;
2021-11-16 15:43:18 +01:00
};
private onRoomViewStoreUpdate = async (initial?: boolean): Promise<void> => {
2017-05-24 16:56:13 +01:00
if (this.unmounted) {
return;
2016-06-14 11:37:04 +01:00
}
const roomLoadError = this.context.roomViewStore.getRoomLoadError() ?? undefined;
if (!initial && !roomLoadError && this.state.roomId !== this.context.roomViewStore.getRoomId()) {
// RoomView explicitly does not support changing what room
// is being viewed: instead it should just be re-mounted when
// switching rooms. Therefore, if the room ID changes, we
// ignore this. We either need to do this or add code to handle
// saving the scroll position (otherwise we end up saving the
2017-09-11 18:39:30 +01:00
// scroll position against the wrong room).
// Given that doing the setState here would cause a bunch of
// unnecessary work, we just ignore the change since we know
// that if the current room ID has changed from what we thought
// it was, it means we're about to be unmounted.
return;
}
const roomId = this.context.roomViewStore.getRoomId() ?? null;
const room = this.context.client?.getRoom(roomId ?? undefined) ?? undefined;
const newState: Partial<IRoomState> = {
roomId: roomId ?? undefined,
roomAlias: this.context.roomViewStore.getRoomAlias() ?? undefined,
roomLoading: this.context.roomViewStore.isRoomLoading(),
roomLoadError,
joining: this.context.roomViewStore.isJoining(),
replyToEvent: this.context.roomViewStore.getQuotingEvent() ?? undefined,
// we should only peek once we have a ready client
shouldPeek: this.state.matrixClientIsReady && this.context.roomViewStore.shouldPeek(),
showReadReceipts: SettingsStore.getValue("showReadReceipts", roomId),
showRedactions: SettingsStore.getValue("showRedactions", roomId),
showJoinLeaves: SettingsStore.getValue("showJoinLeaves", roomId),
showAvatarChanges: SettingsStore.getValue("showAvatarChanges", roomId),
showDisplaynameChanges: SettingsStore.getValue("showDisplaynameChanges", roomId),
wasContextSwitch: this.context.roomViewStore.getWasContextSwitch(),
mainSplitContentType: room ? this.getMainSplitContentType(room) : undefined,
initialEventId: undefined, // default to clearing this, will get set later in the method if needed
showRightPanel: roomId ? this.context.rightPanelStore.isOpenForRoom(roomId) : false,
threadRightPanel: roomId
? [RightPanelPhases.ThreadView, RightPanelPhases.ThreadPanel].includes(
this.context.rightPanelStore.currentCardForRoom(roomId).phase!,
)
: false,
activeCall: roomId ? CallStore.instance.getActiveCall(roomId) : null,
2023-08-07 08:27:09 +02:00
promptAskToJoin: this.context.roomViewStore.promptAskToJoin(),
viewRoomOpts: this.context.roomViewStore.getViewRoomOpts(),
};
if (
this.state.mainSplitContentType !== MainSplitContentType.Timeline &&
newState.mainSplitContentType === MainSplitContentType.Timeline &&
this.context.rightPanelStore.isOpen &&
this.context.rightPanelStore.currentCard.phase === RightPanelPhases.Timeline &&
this.context.rightPanelStore.roomPhaseHistory.some((card) => card.phase === RightPanelPhases.Timeline)
) {
// We're returning to the main timeline, so hide the right panel timeline
this.context.rightPanelStore.setCard({ phase: RightPanelPhases.RoomSummary });
this.context.rightPanelStore.togglePanel(this.state.roomId ?? null);
newState.showRightPanel = false;
}
2024-02-27 13:02:16 +01:00
const initialEventId = this.context.roomViewStore.getInitialEventId() ?? this.state.initialEventId;
if (initialEventId) {
let initialEvent = room?.findEventById(initialEventId);
// The event does not exist in the current sync data
// We need to fetch it to know whether to route this request
// to the main timeline or to a threaded one
// In the current state, if a thread does not exist in the sync data
// We will only display the event targeted by the `matrix.to` link
// and the root event.
// The rest will be lost for now, until the aggregation API on the server
// becomes available to fetch a whole thread
if (!initialEvent && this.context.client && roomId) {
initialEvent = (await fetchInitialEvent(this.context.client, roomId, initialEventId)) ?? undefined;
}
// If we have an initial event, we want to reset the event pixel offset to ensure it ends up visible
newState.initialEventPixelOffset = undefined;
const thread = initialEvent?.getThread();
2024-04-12 11:07:57 +02:00
// Handle the use case of a link to a thread message
// ie: #/room/roomId/eventId (eventId of a thread message)
if (thread?.rootEvent && !initialEvent?.isThreadRoot) {
dis.dispatch<ShowThreadPayload>({
action: Action.ShowThread,
rootEvent: thread.rootEvent,
initialEvent,
highlighted: this.context.roomViewStore.isInitialEventHighlighted(),
scroll_into_view: this.context.roomViewStore.initialEventScrollIntoView(),
});
} else {
newState.initialEventId = initialEventId;
newState.isInitialEventHighlighted = this.context.roomViewStore.isInitialEventHighlighted();
newState.initialEventScrollIntoView = this.context.roomViewStore.initialEventScrollIntoView();
}
}
// Add watchers for each of the settings we just looked up
this.settingWatchers = this.settingWatchers.concat([
SettingsStore.watchSetting("showReadReceipts", roomId, (...[, , , value]) =>
this.setState({ showReadReceipts: value as boolean }),
),
SettingsStore.watchSetting("showRedactions", roomId, (...[, , , value]) =>
this.setState({ showRedactions: value as boolean }),
),
SettingsStore.watchSetting("showJoinLeaves", roomId, (...[, , , value]) =>
this.setState({ showJoinLeaves: value as boolean }),
),
SettingsStore.watchSetting("showAvatarChanges", roomId, (...[, , , value]) =>
this.setState({ showAvatarChanges: value as boolean }),
),
SettingsStore.watchSetting("showDisplaynameChanges", roomId, (...[, , , value]) =>
this.setState({ showDisplaynameChanges: value as boolean }),
),
]);
if (!initial && this.state.shouldPeek && !newState.shouldPeek) {
// Stop peeking because we have joined this room now
this.context.client?.stopPeeking();
}
// Temporary logging to diagnose https://github.com/vector-im/element-web/issues/4307
logger.log(
2017-06-14 16:53:21 +01:00
"RVS update:",
newState.roomId,
newState.roomAlias,
"loading?",
newState.roomLoading,
"joining?",
newState.joining,
2017-06-16 18:24:07 +01:00
"initial?",
initial,
"shouldPeek?",
newState.shouldPeek,
2017-06-14 16:53:21 +01:00
);
// NB: This does assume that the roomID will not change for the lifetime of
// the RoomView instance
if (initial) {
newState.room = this.context.client!.getRoom(newState.roomId) || undefined;
2017-09-08 18:11:13 +01:00
if (newState.room) {
2020-09-07 17:32:03 +01:00
newState.showApps = this.shouldShowApps(newState.room);
this.onRoomLoaded(newState.room);
2017-09-08 18:11:13 +01:00
}
}
if (this.state.roomId === undefined && newState.roomId !== undefined) {
2017-09-08 13:39:22 +01:00
// Get the scroll state for the new room
2017-09-07 17:08:36 +01:00
// If an event ID wasn't specified, default to the one saved for this room
2017-09-07 17:16:32 +01:00
// in the scroll state store. Assume initialEventPixelOffset should be set.
if (!newState.initialEventId && newState.roomId) {
const roomScrollState = RoomScrollStateStore.getScrollState(newState.roomId);
2017-09-07 17:08:36 +01:00
if (roomScrollState) {
newState.initialEventId = roomScrollState.focussedEvent;
newState.initialEventPixelOffset = roomScrollState.pixelOffset;
}
}
}
// Clear the search results when clicking a search result (which changes the
// currently scrolled to event, this.state.initialEventId).
if (
this.state.timelineRenderingType === TimelineRenderingType.Search &&
this.state.initialEventId !== newState.initialEventId
) {
newState.timelineRenderingType = TimelineRenderingType.Room;
this.state.search?.abortController?.abort();
newState.search = undefined;
}
this.setState(newState as IRoomState);
2017-09-08 17:06:46 +01:00
// At this point, newState.roomId could be null (e.g. the alias might not
// have been resolved yet) so anything called here must handle this case.
2017-09-08 17:39:10 +01:00
2017-09-08 17:06:46 +01:00
// We pass the new state into this function for it to read: it needs to
// observe the new state but we don't want to put it in the setState
// callback because this would prevent the setStates from being batched,
// ie. cause it to render RoomView twice rather than the once that is necessary.
if (initial) {
this.setupRoom(newState.room, newState.roomId, !!newState.joining, !!newState.shouldPeek);
2017-09-08 17:06:46 +01:00
}
2020-08-29 12:14:16 +01:00
};
2016-06-14 11:37:04 +01:00
private onConnectedCalls = (): void => {
if (this.state.roomId === undefined) return;
const activeCall = CallStore.instance.getActiveCall(this.state.roomId);
if (activeCall === null) {
// We disconnected from the call, so stop viewing it
dis.dispatch<ViewRoomPayload>(
{
action: Action.ViewRoom,
room_id: this.state.roomId,
view_call: false,
metricsTrigger: undefined,
},
true,
); // Synchronous so that CallView disappears immediately
}
this.setState({ activeCall });
};
private getRoomId = (): string | undefined => {
2020-09-07 17:32:03 +01:00
// According to `onRoomViewStoreUpdate`, `state.roomId` can be null
2019-02-12 08:59:38 +00:00
// if we have a room alias we haven't resolved yet. To work around this,
// first we'll try the room object if it's there, and then fallback to
// the bare room ID. (We may want to update `state.roomId` after
// resolving aliases, so we could always trust it.)
return this.state.room?.roomId ?? this.state.roomId;
2020-09-07 17:32:03 +01:00
};
2019-02-12 08:59:38 +00:00
2023-06-30 13:06:03 +01:00
private getPermalinkCreatorForRoom(): RoomPermalinkCreator {
const { room, roomId } = this.state;
// If room is undefined, attempt to use the roomId to create and store a permalinkCreator.
// Throw an error if we can not find a roomId in state.
if (room === undefined) {
if (isNotUndefined(roomId)) {
const permalinkCreator = new RoomPermalinkCreator(null, roomId);
this.permalinkCreators[roomId] = permalinkCreator;
return permalinkCreator;
} else {
throw new Error("Cannot get a permalink creator without a roomId");
}
}
2020-09-07 17:32:03 +01:00
if (this.permalinkCreators[room.roomId]) return this.permalinkCreators[room.roomId];
2020-09-07 17:32:03 +01:00
this.permalinkCreators[room.roomId] = new RoomPermalinkCreator(room);
2019-04-12 10:38:39 -06:00
if (this.state.room && room.roomId === this.state.room.roomId) {
// We want to watch for changes in the creator for the primary room in the view, but
// don't need to do so for search results.
2020-09-07 17:32:03 +01:00
this.permalinkCreators[room.roomId].start();
2019-04-12 10:38:39 -06:00
} else {
2020-09-07 17:32:03 +01:00
this.permalinkCreators[room.roomId].load();
2019-04-12 10:38:39 -06:00
}
2020-09-07 17:32:03 +01:00
return this.permalinkCreators[room.roomId];
2020-08-29 12:14:16 +01:00
}
private stopAllPermalinkCreators(): void {
2020-09-07 17:32:03 +01:00
if (!this.permalinkCreators) return;
for (const roomId of Object.keys(this.permalinkCreators)) {
this.permalinkCreators[roomId].stop();
}
2020-08-29 12:14:16 +01:00
}
private setupRoom(room: Room | undefined, roomId: string | undefined, joining: boolean, shouldPeek: boolean): void {
// if this is an unknown room then we're in one of three states:
// - This is a room we can peek into (search engine) (we can /peek)
// - This is a room we can publicly join or were invited to. (we can /join)
// - This is a room we cannot join at all. (no action can help us)
// We can't try to /join because this may implicitly accept invites (!)
// We can /peek though. If it fails then we present the join UI. If it
// succeeds then great, show the preview (but we still may be able to /join!).
2016-06-14 11:37:04 +01:00
// Note that peeking works by room ID and room ID only, as opposed to joining
// which must be by alias or invite wherever possible (peeking currently does
// not work over federation).
2016-01-27 07:54:15 +00:00
// NB. We peek if we have never seen the room before (i.e. js-sdk does not know
// about it). We don't peek in the historical case where we were joined but are
// now not joined because the js-sdk peeking API will clobber our historical room,
// making it impossible to indicate a newly joined room.
if (!joining && roomId) {
if (!room && shouldPeek) {
logger.info(`Attempting to peek into room ${roomId}`);
this.setState({
peekLoading: true,
2017-10-11 18:43:24 -06:00
isPeeking: true, // this will change to false if peeking fails
});
this.context.client
?.peekInRoom(roomId)
.then((room) => {
if (this.unmounted) {
return;
}
this.setState({
room: room,
peekLoading: false,
2023-08-07 08:27:09 +02:00
canAskToJoin: this.askToJoinEnabled && room.getJoinRule() === JoinRule.Knock,
});
2020-09-07 17:32:03 +01:00
this.onRoomLoaded(room);
2022-12-12 12:24:14 +01:00
})
.catch((err) => {
if (this.unmounted) {
2022-12-12 12:24:14 +01:00
return;
}
// Stop peeking if anything went wrong
this.setState({
isPeeking: false,
2022-12-12 12:24:14 +01:00
});
// This won't necessarily be a MatrixError, but we duck-type
// here and say if it's got an 'errcode' key with the right value,
// it means we can't peek.
if (err.errcode === "M_GUEST_ACCESS_FORBIDDEN" || err.errcode === "M_FORBIDDEN") {
// This is fine: the room just isn't peekable (we assume).
2021-06-29 13:11:58 +01:00
this.setState({
peekLoading: false,
2022-12-12 12:24:14 +01:00
});
} else {
throw err;
}
});
} else if (room) {
// Stop peeking because we have joined this room previously
this.context.client?.stopPeeking();
2023-08-07 08:27:09 +02:00
this.setState({
isPeeking: false,
canAskToJoin: this.askToJoinEnabled && room.getJoinRule() === JoinRule.Knock,
});
}
2016-02-10 18:27:40 +00:00
}
2020-08-29 12:14:16 +01:00
}
private shouldShowApps(room: Room): boolean {
if (!BROWSER_SUPPORTS_SANDBOX || !room) return false;
2017-07-12 10:21:43 +01:00
// Check if user has previously chosen to hide the app drawer for this
// room. If so, do not show apps
const hideWidgetKey = room.roomId + "_hide_widget_drawer";
const hideWidgetDrawer = localStorage.getItem(hideWidgetKey);
// If unset show the Tray
// Otherwise (in case the user set hideWidgetDrawer by clicking the button) follow the parameter.
const isManuallyShown = hideWidgetDrawer ? hideWidgetDrawer === "false" : true;
const widgets = this.context.widgetLayoutStore.getContainerWidgets(room, Container.Top);
return isManuallyShown && widgets.length > 0;
2020-08-29 12:14:16 +01:00
}
public componentDidMount(): void {
2021-01-31 16:09:11 +00:00
this.onRoomViewStoreUpdate(true);
2020-09-07 17:32:03 +01:00
const call = this.getCallForRoom();
const callState = call?.state;
this.setState({
callState,
});
this.context.legacyCallHandler.on(LegacyCallHandlerEvent.CallState, this.onCallState);
2017-04-29 06:25:30 +01:00
window.addEventListener("beforeunload", this.onPageUnload);
2020-08-29 12:14:16 +01:00
}
2023-02-13 11:39:16 +00:00
public shouldComponentUpdate(nextProps: IRoomProps, nextState: IRoomState): boolean {
2021-05-19 14:32:49 +01:00
const hasPropsDiff = objectHasDiff(this.props, nextProps);
const { upgradeRecommendation, ...state } = this.state;
const { upgradeRecommendation: newUpgradeRecommendation, ...newState } = nextState;
2021-05-19 14:32:49 +01:00
const hasStateDiff =
newUpgradeRecommendation?.needsUpgrade !== upgradeRecommendation?.needsUpgrade ||
objectHasDiff(state, newState);
2021-05-19 14:32:49 +01:00
return hasPropsDiff || hasStateDiff;
2020-08-29 12:14:16 +01:00
}
public componentDidUpdate(): void {
// Note: We check the ref here with a flag because componentDidMount, despite
// documentation, does not define our messagePanel ref. It looks like our spinner
// in render() prevents the ref from being set on first mount, so we try and
// catch the messagePanel when it does mount. Because we only want the ref once,
// we use a boolean flag to avoid duplicate work.
if (this.messagePanel && this.state.atEndOfLiveTimeline === undefined) {
this.setState({
2020-09-07 17:32:03 +01:00
atEndOfLiveTimeline: this.messagePanel.isAtEndOfLiveTimeline(),
});
}
2020-08-29 12:14:16 +01:00
}
public componentWillUnmount(): void {
// set a boolean to say we've been unmounted, which any pending
// promises can use to throw away their results.
//
// (We could use isMounted, but facebook have deprecated that.)
this.unmounted = true;
this.context.legacyCallHandler.removeListener(LegacyCallHandlerEvent.CallState, this.onCallState);
2021-11-30 19:09:13 +01:00
2016-11-04 09:28:35 +00:00
// update the scroll map before we get unmounted
2017-09-08 13:39:22 +01:00
if (this.state.roomId) {
2020-09-07 17:32:03 +01:00
RoomScrollStateStore.setScrollState(this.state.roomId, this.getScrollState());
2017-09-08 13:39:22 +01:00
}
2016-11-04 09:28:35 +00:00
2020-03-31 12:51:03 +01:00
if (this.state.shouldPeek) {
this.context.client?.stopPeeking();
2020-03-31 12:51:03 +01:00
}
// stop tracking room changes to format permalinks
2020-09-07 17:32:03 +01:00
this.stopAllPermalinkCreators();
dis.unregister(this.dispatcherRef);
if (this.context.client) {
this.context.client.removeListener(ClientEvent.Room, this.onRoom);
this.context.client.removeListener(RoomEvent.Timeline, this.onRoomTimeline);
this.context.client.removeListener(RoomEvent.TimelineReset, this.onRoomTimelineReset);
this.context.client.removeListener(RoomEvent.Name, this.onRoomName);
this.context.client.removeListener(RoomStateEvent.Events, this.onRoomStateEvents);
this.context.client.removeListener(RoomEvent.MyMembership, this.onMyMembership);
this.context.client.removeListener(RoomStateEvent.Update, this.onRoomStateUpdate);
this.context.client.removeListener(CryptoEvent.KeyBackupStatus, this.onKeyBackupStatus);
this.context.client.removeListener(CryptoEvent.DeviceVerificationChanged, this.onDeviceVerificationChanged);
this.context.client.removeListener(CryptoEvent.UserTrustStatusChanged, this.onUserVerificationChanged);
this.context.client.removeListener(CryptoEvent.KeysChanged, this.onCrossSigningKeysChanged);
this.context.client.removeListener(MatrixEventEvent.Decrypted, this.onEventDecrypted);
}
2017-04-29 06:25:30 +01:00
window.removeEventListener("beforeunload", this.onPageUnload);
this.context.roomViewStore.off(UPDATE_EVENT, this.onRoomViewStoreUpdate);
2017-06-01 18:01:30 +01:00
this.context.rightPanelStore.off(UPDATE_EVENT, this.onRightPanelStoreUpdate);
2020-10-09 08:42:21 +01:00
WidgetEchoStore.removeListener(UPDATE_EVENT, this.onWidgetEchoStoreUpdate);
this.context.widgetStore.removeListener(UPDATE_EVENT, this.onWidgetStoreUpdate);
2018-07-03 11:16:44 +01:00
2022-03-22 18:14:11 -04:00
this.props.resizeNotifier.off("isResizing", this.onIsResizing);
if (this.state.room) {
this.context.widgetLayoutStore.off(
2021-01-22 17:03:36 -07:00
WidgetLayoutStore.emissionForRoom(this.state.room),
this.onWidgetLayoutChange,
);
}
CallStore.instance.off(CallStoreEvent.ConnectedCalls, this.onConnectedCalls);
this.context.legacyCallHandler.off(LegacyCallHandlerEvent.CallState, this.onCallState);
2021-12-15 18:06:37 +01:00
2021-07-01 18:35:38 +01:00
// cancel any pending calls to the throttled updated
this.updateRoomMembers.cancel();
for (const watcher of this.settingWatchers) {
SettingsStore.unwatchSetting(watcher);
}
2022-08-04 08:19:52 +02:00
if (this.viewsLocalRoom && this.state.room) {
2022-08-04 08:19:52 +02:00
// clean up if this was a local room
this.context.client?.store.removeRoom(this.state.room.roomId);
2022-08-04 08:19:52 +02:00
}
2020-08-29 12:14:16 +01:00
}
2020-05-26 15:56:16 +01:00
private onRightPanelStoreUpdate = (): void => {
const { roomId } = this.state;
this.setState({
showRightPanel: roomId ? this.context.rightPanelStore.isOpenForRoom(roomId) : false,
threadRightPanel: roomId
? [RightPanelPhases.ThreadView, RightPanelPhases.ThreadPanel].includes(
this.context.rightPanelStore.currentCardForRoom(roomId).phase!,
)
: false,
});
2020-08-29 12:14:16 +01:00
};
private onPageUnload = (event: BeforeUnloadEvent): string | undefined => {
if (ContentMessages.sharedInstance().getCurrentUploads().length > 0) {
return (event.returnValue = _t("quit_warning|file_upload_in_progress"));
2020-09-07 17:32:03 +01:00
} else if (this.getCallForRoom() && this.state.callState !== "ended") {
return (event.returnValue = _t("quit_warning|call_in_progress"));
2017-04-29 06:25:30 +01:00
}
2020-08-29 12:14:16 +01:00
};
2017-04-29 06:25:30 +01:00
2023-02-13 11:39:16 +00:00
private onReactKeyDown = (ev: React.KeyboardEvent): void => {
let handled = false;
2021-03-01 21:43:00 +13:00
const action = getKeyBindingsManager().getRoomAction(ev);
switch (action) {
case KeyBindingAction.DismissReadMarker:
this.messagePanel?.forgetReadMarker();
this.jumpToLiveTimeline();
handled = true;
2020-03-24 21:35:17 -05:00
break;
case KeyBindingAction.JumpToOldestUnread:
this.jumpToReadMarker();
handled = true;
break;
case KeyBindingAction.UploadFile: {
dis.dispatch(
{
action: "upload_file",
context: TimelineRenderingType.Room,
},
true,
);
handled = true;
break;
}
}
if (handled) {
ev.stopPropagation();
ev.preventDefault();
}
2020-08-29 12:14:16 +01:00
};
2021-11-30 19:09:13 +01:00
private onCallState = (roomId: string): void => {
// don't filter out payloads for room IDs other than props.room because
// we may be interested in the conf 1:1 room
if (!roomId) return;
const call = this.getCallForRoom();
this.setState({ callState: call?.state });
2021-11-30 19:09:13 +01:00
};
private onAction = async (payload: ActionPayload): Promise<void> => {
if (!this.context.client) return;
switch (payload.action) {
case "message_sent":
2020-11-05 15:39:22 +00:00
this.checkDesktopNotifications();
break;
2018-01-04 18:41:47 +00:00
case "post_sticker_message":
2020-09-07 17:32:03 +01:00
this.injectSticker(
payload.data.content.url,
payload.data.content.info,
payload.data.description || payload.data.name,
payload.data.threadId,
);
2020-09-07 17:32:03 +01:00
break;
2023-06-30 13:06:03 +01:00
case "picture_snapshot": {
const roomId = this.getRoomId();
if (isNotUndefined(roomId)) {
ContentMessages.sharedInstance().sendContentListToRoom(
[payload.file],
roomId,
undefined,
this.context.client,
);
}
2019-09-10 08:38:51 +01:00
break;
2023-06-30 13:06:03 +01:00
}
2019-02-24 17:44:55 -07:00
case "notifier_enabled":
case Action.UploadStarted:
case Action.UploadFinished:
case Action.UploadCanceled:
this.forceUpdate();
break;
case "appsDrawer":
this.setState({
2017-06-28 12:21:05 +01:00
showApps: payload.show,
});
break;
case "reply_to_event":
if (
!this.unmounted &&
this.state.search &&
payload.event?.getRoomId() === this.state.roomId &&
payload.context === TimelineRenderingType.Search
) {
this.onCancelSearchClick();
// we don't need to re-dispatch as RoomViewStore knows to persist with context=Search also
2020-02-28 23:12:10 +00:00
}
break;
case "MatrixActions.sync":
if (!this.state.matrixClientIsReady) {
this.setState(
{
matrixClientIsReady: !!this.context.client?.isInitialSyncComplete(),
},
() => {
// send another "initial" RVS update to trigger peeking if needed
2020-09-07 17:32:03 +01:00
this.onRoomViewStoreUpdate(true);
},
);
}
break;
2022-08-04 08:19:52 +02:00
case "local_room_event":
this.onLocalRoomEvent(payload.roomId);
break;
case Action.EditEvent: {
// Quit early if we're trying to edit events in wrong rendering context
if (payload.timelineRenderingType !== this.state.timelineRenderingType) return;
if (payload.event && payload.event.getRoomId() !== this.state.roomId) {
// If the event is in a different room (e.g. because the event to be edited is being displayed
// in the results of an all-rooms search), we need to view that room first.
dis.dispatch<ViewRoomPayload>({
action: Action.ViewRoom,
room_id: payload.event.getRoomId(),
metricsTrigger: undefined,
deferred_action: payload,
});
return;
}
const editState = payload.event ? new EditorStateTransfer(payload.event) : undefined;
this.setState(
{
editState,
// If a search is active (implying that the "edit" button has been pressed on one of the
// events in the search result), we need to close that search, because RoomSearchView
// doesn't handle editing and won't render the composer.
search: undefined,
},
() => {
if (payload.event) {
this.messagePanel?.scrollToEventIfNeeded(payload.event.getId());
}
},
);
break;
}
case Action.ComposerInsert: {
if (payload.composerType) break;
let timelineRenderingType: TimelineRenderingType = payload.timelineRenderingType;
// ThreadView handles Action.ComposerInsert itself due to it having its own editState
if (timelineRenderingType === TimelineRenderingType.Thread) break;
if (
this.state.timelineRenderingType === TimelineRenderingType.Search &&
payload.timelineRenderingType === TimelineRenderingType.Search
) {
// we don't have the composer rendered in this state, so bring it back first
await this.onCancelSearchClick();
timelineRenderingType = TimelineRenderingType.Room;
}
// re-dispatch to the correct composer
dis.dispatch<ComposerInsertPayload>({
...(payload as ComposerInsertPayload),
timelineRenderingType,
composerType: this.state.editState ? ComposerType.Edit : ComposerType.Send,
2021-07-08 18:31:47 +02:00
});
break;
}
2021-07-08 17:33:49 +02:00
case Action.FocusAComposer: {
dis.dispatch<FocusComposerPayload>({
...(payload as FocusComposerPayload),
// re-dispatch to the correct composer (the send message will still be on screen even when editing a message)
action: this.state.editState ? Action.FocusEditMessageComposer : Action.FocusSendMessageComposer,
});
break;
}
case "scroll_to_bottom":
2021-12-06 15:32:15 +01:00
if (payload.timelineRenderingType === TimelineRenderingType.Room) {
this.messagePanel?.jumpToLiveTimeline();
}
break;
2023-08-31 07:46:09 +01:00
case Action.ViewUser:
if (payload.member) {
if (payload.push) {
RightPanelStore.instance.pushCard({
phase: RightPanelPhases.RoomMemberInfo,
state: { member: payload.member },
});
} else {
RightPanelStore.instance.setCards([
{ phase: RightPanelPhases.RoomSummary },
{ phase: RightPanelPhases.RoomMemberList },
{ phase: RightPanelPhases.RoomMemberInfo, state: { member: payload.member } },
]);
}
} else {
2024-07-09 17:06:50 +05:30
RightPanelStore.instance.showOrHidePhase(RightPanelPhases.RoomMemberList);
2023-08-31 07:46:09 +01:00
}
break;
case Action.View3pidInvite:
onView3pidInvite(payload, RightPanelStore.instance);
2023-08-31 07:46:09 +01:00
break;
}
2020-08-29 12:14:16 +01:00
};
private onLocalRoomEvent(roomId: string): void {
if (!this.context.client || !this.state.room || roomId !== this.state.room.roomId) return;
createRoomFromLocalRoom(this.context.client, this.state.room as LocalRoom);
2022-08-04 08:19:52 +02:00
}
2023-02-13 11:39:16 +00:00
private onRoomTimeline = (
ev: MatrixEvent,
room: Room | undefined,
toStartOfTimeline: boolean | undefined,
2023-02-13 11:39:16 +00:00
removed: boolean,
data: IRoomTimelineData,
2023-02-13 11:39:16 +00:00
): void => {
if (this.unmounted) return;
// ignore events for other rooms or the notification timeline set
if (!room || room.roomId !== this.state.room?.roomId) return;
2016-01-27 07:54:15 +00:00
// ignore events from filtered timelines
if (data.timeline.getTimelineSet() !== room.getUnfilteredTimelineSet()) return;
2016-07-18 10:47:03 +01:00
if (ev.getType() === "org.matrix.room.preview_urls") {
2020-09-07 17:32:03 +01:00
this.updatePreviewUrlVisibility(room);
2016-07-18 01:35:42 +01:00
}
if (ev.getType() === "m.room.encryption") {
2020-09-07 17:32:03 +01:00
this.updateE2EStatus(room);
this.updatePreviewUrlVisibility(room);
}
2016-01-27 07:54:15 +00:00
// ignore anything but real-time updates at the end of the room:
// updates from pagination will happen when the paginate completes.
2023-02-13 11:39:16 +00:00
if (toStartOfTimeline || !data?.liveEvent) return;
// no point handling anything while we're waiting for the join to finish:
// we'll only be showing a spinner.
if (this.state.joining) return;
if (!ev.isBeingDecrypted() && !ev.isDecryptionFailure()) {
this.handleEffects(ev);
}
if (this.context.client && ev.getSender() !== this.context.client.getSafeUserId()) {
// update unread count when scrolled up
if (!this.state.search && this.state.atEndOfLiveTimeline) {
2016-01-27 07:54:15 +00:00
// no change
} else if (!shouldHideEvent(ev, this.state)) {
this.setState((state) => {
2021-06-29 13:11:58 +01:00
return { numUnreadMessages: state.numUnreadMessages + 1 };
2016-01-27 07:54:15 +00:00
});
}
}
2020-08-29 12:14:16 +01:00
};
private onEventDecrypted = (ev: MatrixEvent): void => {
if (!this.state.room || !this.state.matrixClientIsReady) return; // not ready at all
if (ev.getRoomId() !== this.state.room.roomId) return; // not for us
if (ev.isDecryptionFailure()) return;
2020-10-21 16:15:15 +02:00
this.handleEffects(ev);
2020-10-19 23:10:43 +02:00
};
private handleEffects = (ev: MatrixEvent): void => {
if (!this.state.room) return;
const notifState = this.context.roomNotificationStateStore.getRoomState(this.state.room);
2020-12-07 16:54:09 -07:00
if (!notifState.isUnread) return;
CHAT_EFFECTS.forEach((effect) => {
2020-10-26 16:37:45 +01:00
if (containsEmoji(ev.getContent(), effect.emojis) || ev.getContent().msgtype === effect.msgType) {
// For initial threads launch, chat effects are disabled see #19731
if (!ev.isRelation(THREAD_RELATION_TYPE.name)) {
dis.dispatch({ action: `effects.${effect.command}`, event: ev });
}
2020-10-26 16:37:45 +01:00
}
2020-12-07 16:54:09 -07:00
});
2020-10-19 23:10:43 +02:00
};
2016-01-18 19:56:35 +00:00
private onRoomName = (room: Room): void => {
2017-02-17 11:44:56 +00:00
if (this.state.room && room.roomId == this.state.room.roomId) {
this.forceUpdate();
}
2020-08-29 12:14:16 +01:00
};
2017-02-17 11:44:56 +00:00
private onKeyBackupStatus = (): void => {
// Key backup status changes affect whether the in-room recovery
// reminder is displayed.
this.forceUpdate();
2020-08-29 12:14:16 +01:00
};
public canResetTimeline = (): boolean => {
2020-09-07 17:32:03 +01:00
if (!this.messagePanel) {
return true;
}
2020-09-07 17:32:03 +01:00
return this.messagePanel.canResetTimeline();
2020-08-29 12:14:16 +01:00
};
private loadVirtualRoom = async (room?: Room): Promise<void> => {
const virtualRoom = room?.roomId && (await VoipUserMapper.sharedInstance().getVirtualRoomForRoom(room?.roomId));
this.setState({ virtualRoom: virtualRoom || undefined });
};
2016-02-10 18:27:40 +00:00
// called when state.room is first initialised (either at initial load,
// after a successful peek, or after we join the room).
private onRoomLoaded = (room: Room): void => {
if (this.unmounted) return;
// Attach a widget store listener only when we get a room
this.context.widgetLayoutStore.on(WidgetLayoutStore.emissionForRoom(room), this.onWidgetLayoutChange);
2020-09-07 17:32:03 +01:00
this.calculatePeekRules(room);
this.updatePreviewUrlVisibility(room);
this.loadMembersIfJoined(room);
this.calculateRecommendedVersion(room);
this.updateE2EStatus(room);
this.updatePermissions(room);
2020-10-09 08:42:21 +01:00
this.checkWidgets(room);
this.loadVirtualRoom(room);
if (
this.getMainSplitContentType(room) !== MainSplitContentType.Timeline &&
this.context.roomNotificationStateStore.getRoomState(room).isUnread
) {
// Automatically open the chat panel to make unread messages easier to discover
this.context.rightPanelStore.setCard({ phase: RightPanelPhases.Timeline }, true, room.roomId);
}
this.setState({
tombstone: this.getRoomTombstone(room),
liveTimeline: room.getLiveTimeline(),
});
2024-02-27 13:02:16 +01:00
dis.dispatch<ActionPayload>({ action: Action.RoomLoaded });
2020-08-29 12:14:16 +01:00
};
2019-01-28 17:21:33 -07:00
private onRoomTimelineReset = (room?: Room): void => {
if (room && room.roomId === this.state.room?.roomId && room.getLiveTimeline() !== this.state.liveTimeline) {
logger.log(`Live timeline of ${room.roomId} was reset`);
this.setState({ liveTimeline: room.getLiveTimeline() });
}
};
private getRoomTombstone(room = this.state.room): MatrixEvent | undefined {
return room?.currentState.getStateEvents(EventType.RoomTombstone, "") ?? undefined;
}
private async calculateRecommendedVersion(room: Room): Promise<void> {
const upgradeRecommendation = await room.getRecommendedVersion();
if (this.unmounted) return;
this.setState({ upgradeRecommendation });
2020-08-29 12:14:16 +01:00
}
private async loadMembersIfJoined(room: Room): Promise<void> {
// lazy load members if enabled
if (this.context.client?.hasLazyLoadMembersEnabled()) {
if (room && room.getMyMembership() === KnownMembership.Join) {
try {
await room.loadMembersIfNeeded();
if (!this.unmounted) {
2021-06-29 13:11:58 +01:00
this.setState({ membersLoaded: true });
}
2018-09-17 20:01:55 +02:00
} catch (err) {
const errorMessage =
`Fetching room members for ${room.roomId} failed.` + " Room members will appear incomplete.";
2021-10-15 16:30:53 +02:00
logger.error(errorMessage);
logger.error(err);
}
}
}
2020-08-29 12:14:16 +01:00
}
private calculatePeekRules(room: Room): void {
const historyVisibility = room.currentState.getStateEvents(EventType.RoomHistoryVisibility, "");
this.setState({
canPeek: historyVisibility?.getContent().history_visibility === HistoryVisibility.WorldReadable,
});
2020-08-29 12:14:16 +01:00
}
private updatePreviewUrlVisibility({ roomId }: Room): void {
// URL Previews in E2EE rooms can be a privacy leak so use a different setting which is per-room explicit
const key = this.context.client?.isRoomEncrypted(roomId) ? "urlPreviewsEnabled_e2ee" : "urlPreviewsEnabled";
2016-07-18 01:35:42 +01:00
this.setState({
showUrlPreview: SettingsStore.getValue(key, roomId),
2016-07-18 01:35:42 +01:00
});
2020-08-29 12:14:16 +01:00
}
2016-07-18 01:35:42 +01:00
private onRoom = (room: Room): void => {
2017-05-24 16:56:13 +01:00
if (!room || room.roomId !== this.state.roomId) {
return;
}
// Detach the listener if the room is changing for some reason
if (this.state.room) {
this.context.widgetLayoutStore.off(
2021-01-22 17:03:36 -07:00
WidgetLayoutStore.emissionForRoom(this.state.room),
this.onWidgetLayoutChange,
);
}
2017-05-24 16:56:13 +01:00
this.setState(
{
room: room,
},
() => {
2020-09-07 17:32:03 +01:00
this.onRoomLoaded(room);
2017-05-24 16:56:13 +01:00
},
);
2020-08-29 12:14:16 +01:00
};
private onDeviceVerificationChanged = (userId: string): void => {
const room = this.state.room;
if (!room?.currentState.getMember(userId)) {
return;
}
2020-09-07 17:32:03 +01:00
this.updateE2EStatus(room);
2020-08-29 12:14:16 +01:00
};
private onUserVerificationChanged = (userId: string): void => {
const room = this.state.room;
if (!room || !room.currentState.getMember(userId)) {
return;
}
2020-09-07 17:32:03 +01:00
this.updateE2EStatus(room);
2020-08-29 12:14:16 +01:00
};
private onCrossSigningKeysChanged = (): void => {
const room = this.state.room;
if (room) {
2020-09-07 17:32:03 +01:00
this.updateE2EStatus(room);
}
2020-08-29 12:14:16 +01:00
};
private async updateE2EStatus(room: Room): Promise<void> {
if (!this.context.client?.isRoomEncrypted(room.roomId)) return;
// If crypto is not currently enabled, we aren't tracking devices at all,
// so we don't know what the answer is. Let's error on the safe side and show
// a warning for this case.
let e2eStatus = RoomView.e2eStatusCache.get(room.roomId) ?? E2EStatus.Warning;
// set the state immediately then update, so we don't scare the user into thinking the room is unencrypted
this.setState({ e2eStatus });
if (this.context.client.isCryptoEnabled()) {
/* At this point, the user has encryption on and cross-signing on */
e2eStatus = await shieldStatusForRoom(this.context.client, room);
RoomView.e2eStatusCache.set(room.roomId, e2eStatus);
if (this.unmounted) return;
this.setState({ e2eStatus });
}
2020-08-29 12:14:16 +01:00
}
private onUrlPreviewsEnabledChange = (): void => {
if (this.state.room) {
2020-09-07 17:32:03 +01:00
this.updatePreviewUrlVisibility(this.state.room);
}
2020-08-29 12:14:16 +01:00
};
private onRoomStateEvents = (ev: MatrixEvent, state: RoomState): void => {
// ignore if we don't have a room yet
if (!this.state.room || this.state.room.roomId !== state.roomId) return;
switch (ev.getType()) {
case EventType.RoomTombstone:
this.setState({ tombstone: this.getRoomTombstone() });
break;
default:
this.updatePermissions(this.state.room);
}
2020-08-29 12:14:16 +01:00
};
private onRoomStateUpdate = (state: RoomState): void => {
2016-04-12 19:25:07 +01:00
// ignore members in other rooms
if (state.roomId !== this.state.room?.roomId) {
return;
}
2016-04-12 19:25:07 +01:00
2021-07-01 18:35:38 +01:00
this.updateRoomMembers();
2020-08-29 12:14:16 +01:00
};
2016-07-26 18:15:26 +01:00
private onMyMembership = (room: Room): void => {
if (room.roomId === this.state.roomId) {
2017-09-15 15:07:09 +01:00
this.forceUpdate();
2020-09-07 17:32:03 +01:00
this.loadMembersIfJoined(room);
this.updatePermissions(room);
}
2020-08-29 12:14:16 +01:00
};
private updatePermissions(room: Room): void {
if (room && this.context.client) {
const me = this.context.client.getSafeUserId();
const canReact =
room.getMyMembership() === KnownMembership.Join &&
room.currentState.maySendEvent(EventType.Reaction, me);
const canSendMessages = room.maySendMessage();
const canSelfRedact = room.currentState.maySendEvent(EventType.RoomRedaction, me);
2022-09-19 08:42:29 +02:00
this.setState({
canReact,
canSendMessages,
canSelfRedact,
});
2017-02-17 15:50:30 +00:00
}
2020-08-29 12:14:16 +01:00
}
2017-02-17 15:50:30 +00:00
2020-09-07 17:32:03 +01:00
// rate limited because a power level change will emit an event for every member in the room.
2021-07-01 18:35:38 +01:00
private updateRoomMembers = throttle(
() => {
if (!this.state.room) return;
2020-09-07 17:32:03 +01:00
this.updateDMState();
this.updateE2EStatus(this.state.room);
2021-07-01 18:35:38 +01:00
},
500,
{ leading: true, trailing: true },
);
private checkDesktopNotifications(): void {
if (!this.state.room) return;
2020-11-05 15:39:22 +00:00
const memberCount = this.state.room.getJoinedMemberCount() + this.state.room.getInvitedMemberCount();
// if they are not alone prompt the user about notifications so they don't miss replies
if (memberCount > 1 && Notifier.shouldShowPrompt()) {
2020-11-02 17:25:48 +00:00
showNotificationsToast(true);
}
2020-08-29 12:14:16 +01:00
}
private updateDMState(): void {
const room = this.state.room;
if (room?.getMyMembership() != KnownMembership.Join) {
2018-02-20 14:10:34 +00:00
return;
}
const dmInviter = room?.getDMInviter();
if (dmInviter) {
Rooms.setDMRoom(room.client, room.roomId, dmInviter);
2018-02-20 14:10:34 +00:00
}
2020-08-29 12:14:16 +01:00
}
2018-02-20 14:10:34 +00:00
private onInviteClick = (): void => {
// open the room inviter
dis.dispatch({
action: "view_invite",
roomId: this.getRoomId(),
});
2020-08-29 12:14:16 +01:00
};
private onJoinButtonClicked = (): void => {
// If the user is a ROU, allow them to transition to a PWLU
if (this.context.client?.isGuest()) {
2017-05-24 16:56:13 +01:00
// Join this room once the user has registered and logged in
2019-02-11 14:33:19 +00:00
// (If we failed to peek, we may not have a valid room object.)
2022-02-22 11:04:27 +01:00
dis.dispatch<DoAfterSyncPreparedPayload<ViewRoomPayload>>({
action: Action.DoAfterSyncPrepared,
2017-05-24 16:56:13 +01:00
deferred_action: {
2021-11-25 17:49:43 -03:00
action: Action.ViewRoom,
2020-09-07 17:32:03 +01:00
room_id: this.getRoomId(),
2022-02-22 11:04:27 +01:00
metricsTrigger: undefined,
2017-05-24 16:56:13 +01:00
},
});
2021-06-29 13:11:58 +01:00
dis.dispatch({ action: "require_registration" });
} else {
Promise.resolve().then(() => {
const signUrl = this.props.threepidInvite?.signUrl;
2023-06-30 13:06:03 +01:00
const roomId = this.getRoomId();
if (isNotUndefined(roomId)) {
dis.dispatch<JoinRoomPayload>({
action: Action.JoinRoom,
roomId,
opts: { inviteSignUrl: signUrl },
2024-03-11 17:16:53 +00:00
metricsTrigger:
this.state.room?.getMyMembership() === KnownMembership.Invite ? "Invite" : "RoomPreview",
2023-08-07 08:27:09 +02:00
canAskToJoin: this.state.canAskToJoin,
2023-06-30 13:06:03 +01:00
});
}
return Promise.resolve();
});
}
2020-08-29 12:14:16 +01:00
};
private onMessageListScroll = (): void => {
if (this.messagePanel?.isAtEndOfLiveTimeline()) {
2016-02-10 18:27:40 +00:00
this.setState({
numUnreadMessages: 0,
atEndOfLiveTimeline: true,
});
2017-10-11 17:56:17 +01:00
} else {
2016-02-10 18:27:40 +00:00
this.setState({
atEndOfLiveTimeline: false,
});
}
2020-09-07 17:32:03 +01:00
this.updateTopUnreadMessagesBar();
2020-08-29 12:14:16 +01:00
};
private resetJumpToEvent = (eventId?: string): void => {
if (
this.state.initialEventId &&
this.state.initialEventScrollIntoView &&
this.state.initialEventId === eventId
) {
debuglog("Removing scroll_into_view flag from initial event");
dis.dispatch<ViewRoomPayload>({
action: Action.ViewRoom,
room_id: this.getRoomId(),
event_id: this.state.initialEventId,
highlighted: this.state.isInitialEventHighlighted,
scroll_into_view: false,
replyingToEvent: this.state.replyToEvent,
metricsTrigger: undefined, // room doesn't change
});
}
};
private injectSticker(url: string, info: object, text: string, threadId: string | null): void {
const roomId = this.getRoomId();
if (!this.context.client || !roomId) return;
if (this.context.client.isGuest()) {
2021-06-29 13:11:58 +01:00
dis.dispatch({ action: "require_registration" });
2018-01-04 09:53:26 +00:00
return;
}
ContentMessages.sharedInstance()
.sendStickerContentToRoom(url, roomId, threadId, info, text, this.context.client)
2019-11-18 10:03:05 +00:00
.then(undefined, (error) => {
2018-03-29 16:46:22 +01:00
if (error.name === "UnknownDeviceError") {
// Let the staus bar handle this
return;
}
});
2020-08-29 12:14:16 +01:00
}
2018-01-04 09:53:26 +00:00
2024-07-08 10:57:41 +01:00
private onSearch = (term: string, scope = SearchScope.Room): void => {
const roomId = scope === SearchScope.Room ? this.getRoomId() : undefined;
2016-01-05 15:51:16 +00:00
debuglog("sending search request");
const abortController = new AbortController();
const promise = eventSearch(this.context.client!, term, roomId, abortController.signal);
this.setState({
2024-07-08 10:57:41 +01:00
timelineRenderingType: TimelineRenderingType.Search,
search: {
// make sure that we don't end up showing results from
// an aborted search by keeping a unique id.
searchId: new Date().getTime(),
roomId,
term,
scope,
promise,
abortController,
},
});
2020-08-29 12:14:16 +01:00
};
2016-01-04 16:54:27 +00:00
2024-07-08 10:57:41 +01:00
private onSearchScopeChange = (scope: SearchScope): void => {
this.onSearch(this.state.search?.term ?? "", scope);
};
private onSearchUpdate = (inProgress: boolean, searchResults: ISearchResults | null): void => {
2015-12-18 11:11:41 +00:00
this.setState({
search: {
...this.state.search!,
count: searchResults?.count,
inProgress,
},
2015-12-18 11:11:41 +00:00
});
};
2015-12-22 15:18:50 +00:00
private onAppsClick = (): void => {
2020-10-09 08:42:21 +01:00
dis.dispatch({
action: "appsDrawer",
2020-10-09 08:42:21 +01:00
show: !this.state.showApps,
});
};
private onForgetClick = (): void => {
dis.dispatch({
action: "forget_room",
room_id: this.getRoomId(),
});
2020-08-29 12:14:16 +01:00
};
private onRejectButtonClicked = (): void => {
const roomId = this.getRoomId();
if (!roomId) return;
this.setState({
2017-10-11 17:56:17 +01:00
rejecting: true,
});
this.context.client?.leave(roomId).then(
() => {
2022-02-22 11:04:27 +01:00
dis.dispatch({ action: Action.ViewHomePage });
2020-09-07 17:32:03 +01:00
this.setState({
2017-10-11 17:56:17 +01:00
rejecting: false,
});
2020-09-07 17:32:03 +01:00
},
(error) => {
logger.error(`Failed to reject invite: ${error}`);
2017-10-11 17:56:17 +01:00
const msg = error.message ? error.message : JSON.stringify(error);
2022-06-14 17:51:51 +01:00
Modal.createDialog(ErrorDialog, {
title: _t("room|failed_reject_invite"),
description: msg,
});
2020-09-07 17:32:03 +01:00
this.setState({
rejecting: false,
});
},
);
2020-08-29 12:14:16 +01:00
};
private onRejectAndIgnoreClick = async (): Promise<void> => {
this.setState({
rejecting: true,
});
try {
const myMember = this.state.room!.getMember(this.context.client!.getSafeUserId());
const inviteEvent = myMember!.events.member;
const ignoredUsers = this.context.client!.getIgnoredUsers();
ignoredUsers.push(inviteEvent!.getSender()!); // de-duped internally in the js-sdk
await this.context.client!.setIgnoredUsers(ignoredUsers);
await this.context.client!.leave(this.state.roomId!);
2022-02-22 11:04:27 +01:00
dis.dispatch({ action: Action.ViewHomePage });
this.setState({
rejecting: false,
});
} catch (error) {
logger.error(`Failed to reject invite: ${error}`);
const msg = error instanceof Error ? error.message : JSON.stringify(error);
2022-06-14 17:51:51 +01:00
Modal.createDialog(ErrorDialog, {
title: _t("room|failed_reject_invite"),
description: msg,
});
2020-09-07 17:32:03 +01:00
this.setState({
rejecting: false,
});
}
2020-08-29 12:14:16 +01:00
};
private onRejectThreepidInviteButtonClicked = (): void => {
// We can reject 3pid invites in the same way that we accept them,
// using /leave rather than /join. In the short term though, we
// just ignore them.
// https://github.com/vector-im/vector-web/issues/1134
dis.fire(Action.ViewRoomDirectory);
2020-08-29 12:14:16 +01:00
};
private onSearchClick = (): void => {
2024-07-08 10:57:41 +01:00
dis.fire(Action.FocusMessageSearch);
2020-08-29 12:14:16 +01:00
};
2024-07-08 10:57:41 +01:00
private onSearchChange = debounce((e: ChangeEvent): void => {
const term = (e.target as HTMLInputElement).value;
this.onSearch(term);
}, 300);
private onCancelSearchClick = (): Promise<void> => {
return new Promise<void>((resolve) => {
this.setState(
{
timelineRenderingType: TimelineRenderingType.Room,
search: undefined,
},
resolve,
);
2015-12-18 16:42:46 +00:00
});
2020-08-29 12:14:16 +01:00
};
2015-12-18 16:42:46 +00:00
2016-02-03 08:03:10 +00:00
// jump down to the bottom of this room, where new events are arriving
private jumpToLiveTimeline = (): void => {
if (this.state.initialEventId && this.state.isInitialEventHighlighted) {
// If we were viewing a highlighted event, firing view_room without
// an event will take care of both clearing the URL fragment and
// jumping to the bottom
dis.dispatch<ViewRoomPayload>({
2021-11-25 17:49:43 -03:00
action: Action.ViewRoom,
room_id: this.getRoomId(),
metricsTrigger: undefined, // room doesn't change
});
} else {
// Otherwise we have to jump manually
this.messagePanel?.jumpToLiveTimeline();
2021-07-08 17:36:31 +02:00
dis.fire(Action.FocusSendMessageComposer);
}
2020-08-29 12:14:16 +01:00
};
// jump up to wherever our read marker is
private jumpToReadMarker = (): void => {
this.messagePanel?.jumpToReadMarker();
2020-08-29 12:14:16 +01:00
};
// update the read marker to match the read-receipt
2023-02-13 11:39:16 +00:00
private forgetReadMarker = (ev: ButtonEvent): void => {
2016-04-13 12:46:36 +01:00
ev.stopPropagation();
this.messagePanel?.forgetReadMarker();
2020-08-29 12:14:16 +01:00
};
// decide whether or not the top 'unread messages' bar should be shown
private updateTopUnreadMessagesBar = (): void => {
2020-09-07 17:32:03 +01:00
if (!this.messagePanel) {
return;
2017-01-20 14:22:27 +00:00
}
2020-09-07 17:32:03 +01:00
const showBar = this.messagePanel.canJumpToReadMarker();
if (this.state.showTopUnreadMessagesBar != showBar) {
2021-06-29 13:11:58 +01:00
this.setState({ showTopUnreadMessagesBar: showBar });
}
2020-08-29 12:14:16 +01:00
};
2015-12-10 21:34:10 +00:00
// get the current scroll position of the room, so that it can be
2016-02-01 16:31:12 +00:00
// restored when we switch back to it.
//
private getScrollState(): ScrollState | null {
2020-09-07 17:32:03 +01:00
const messagePanel = this.messagePanel;
2015-12-22 15:18:50 +00:00
if (!messagePanel) return null;
// if we're following the live timeline, we want to return null; that
// means that, if we switch back, we will jump to the read-up-to mark.
//
// That should be more intuitive than slavishly preserving the current
// scroll state, in the case where the room advances in the meantime
// (particularly in the case that the user reads some stuff on another
// device).
//
if (this.state.atEndOfLiveTimeline) {
return null;
}
2017-10-11 17:56:17 +01:00
const scrollState = messagePanel.getScrollState();
2015-12-10 16:26:36 +00:00
// getScrollState on TimelinePanel *may* return null, so guard against that
if (!scrollState || scrollState.stuckAtBottom) {
2016-02-01 16:31:12 +00:00
// we don't really expect to be in this state, but it will
2016-02-03 08:03:10 +00:00
// occasionally happen when no scroll state has been set on the
// messagePanel (ie, we didn't have an initial event (so it's
// probably a new room), there has been no user-initiated scroll, and
// no read-receipts have arrived to update the scroll position).
//
// Return null, which will cause us to scroll to last unread on
// reload.
2016-02-01 16:31:12 +00:00
return null;
2015-12-10 16:26:36 +00:00
}
2016-02-01 16:31:12 +00:00
return {
2016-02-03 08:03:10 +00:00
focussedEvent: scrollState.trackedScrollToken,
2016-02-01 16:31:12 +00:00
pixelOffset: scrollState.pixelOffset,
};
2020-08-29 12:14:16 +01:00
}
2015-12-10 16:26:36 +00:00
private onStatusBarVisible = (): void => {
if (this.unmounted || this.state.statusBarVisible) return;
this.setState({ statusBarVisible: true });
2020-08-29 12:14:16 +01:00
};
private onStatusBarHidden = (): void => {
2018-01-05 12:03:46 +00:00
// This is currently not desired as it is annoying if it keeps expanding and collapsing
if (this.unmounted || !this.state.statusBarVisible) return;
this.setState({ statusBarVisible: false });
2020-08-29 12:14:16 +01:00
};
/**
* called by the parent component when PageUp/Down/etc is pressed.
*
* We pass it down to the scroll panel.
*/
2023-02-13 11:39:16 +00:00
public handleScrollKey = (ev: React.KeyboardEvent | KeyboardEvent): void => {
let panel: ScrollPanel | TimelinePanel | undefined;
if (this.searchResultsPanel.current) {
panel = this.searchResultsPanel.current;
} else if (this.messagePanel) {
panel = this.messagePanel;
}
panel?.handleScrollKey(ev);
};
2016-04-12 19:25:07 +01:00
/**
* get any current call for this room
*/
private getCallForRoom(): MatrixCall | null {
2016-04-12 19:25:07 +01:00
if (!this.state.room) {
return null;
}
return this.context.legacyCallHandler.getCallForRoom(this.state.room.roomId);
2020-08-29 12:14:16 +01:00
}
2016-04-12 19:25:07 +01:00
// this has to be a proper method rather than an unnamed function,
// otherwise react calls it with null on each update.
private gatherTimelinePanelRef = (r: TimelinePanel | null): void => {
2020-09-07 17:32:03 +01:00
this.messagePanel = r;
2020-08-29 12:14:16 +01:00
};
private getOldRoom(): Room | null {
const { roomId } = this.state.room?.findPredecessor(this.state.msc3946ProcessDynamicPredecessor) || {};
return this.context.client?.getRoom(roomId) || null;
2020-08-29 12:14:16 +01:00
}
public getHiddenHighlightCount(): number {
2020-09-07 17:32:03 +01:00
const oldRoom = this.getOldRoom();
if (!oldRoom) return 0;
return oldRoom.getUnreadNotificationCount(NotificationCountType.Highlight);
2020-08-29 12:14:16 +01:00
}
public onHiddenHighlightsClick = (): void => {
2020-09-07 17:32:03 +01:00
const oldRoom = this.getOldRoom();
if (!oldRoom) return;
dis.dispatch<ViewRoomPayload>({
action: Action.ViewRoom,
room_id: oldRoom.roomId,
metricsTrigger: "Predecessor",
});
2020-08-29 12:14:16 +01:00
};
private get messagePanelClassNames(): string {
return classNames("mx_RoomView_messagePanel", {
mx_IRCLayout: this.state.layout === Layout.IRC,
});
}
private onFileDrop = async (dataTransfer: DataTransfer): Promise<void> => {
const roomId = this.getRoomId();
if (!roomId || !this.context.client) return;
await ContentMessages.sharedInstance().sendContentListToRoom(
Array.from(dataTransfer.files),
roomId,
undefined,
this.context.client,
TimelineRenderingType.Room,
);
};
private onMeasurement = (narrow: boolean): void => {
this.setState({ narrow });
};
2022-08-04 08:19:52 +02:00
private get viewsLocalRoom(): boolean {
return isLocalRoom(this.state.room);
}
private get permalinkCreator(): RoomPermalinkCreator {
2023-06-30 13:06:03 +01:00
return this.getPermalinkCreatorForRoom();
2022-08-04 08:19:52 +02:00
}
private renderLocalRoomCreateLoader(localRoom: LocalRoom): ReactNode {
if (!this.state.room || !this.context?.client) return null;
const names = this.state.room.getDefaultRoomName(this.context.client.getSafeUserId());
2022-08-04 08:19:52 +02:00
return (
<RoomContext.Provider value={this.state}>
<LocalRoomCreateLoader localRoom={localRoom} names={names} resizeNotifier={this.props.resizeNotifier} />
2022-08-04 08:19:52 +02:00
</RoomContext.Provider>
);
}
private renderLocalRoomView(localRoom: LocalRoom): ReactNode {
2022-08-04 08:19:52 +02:00
return (
<RoomContext.Provider value={this.state}>
<LocalRoomView
localRoom={localRoom}
2022-08-04 08:19:52 +02:00
resizeNotifier={this.props.resizeNotifier}
permalinkCreator={this.permalinkCreator}
roomView={this.roomView}
onFileDrop={this.onFileDrop}
/>
</RoomContext.Provider>
);
}
private renderWaitingForThirdPartyRoomView(inviteEvent: MatrixEvent): ReactNode {
return (
<RoomContext.Provider value={this.state}>
<WaitingForThirdPartyRoomView
resizeNotifier={this.props.resizeNotifier}
roomView={this.roomView}
inviteEvent={inviteEvent}
/>
</RoomContext.Provider>
);
}
2023-08-07 08:27:09 +02:00
/**
* Handles the submission of a request to join a room.
*
* @param {string} reason - An optional reason for the request to join.
* @returns {void}
*/
private onSubmitAskToJoin = (reason?: string): void => {
const roomId = this.getRoomId();
if (isNotUndefined(roomId)) {
dis.dispatch<SubmitAskToJoinPayload>({
action: Action.SubmitAskToJoin,
roomId,
opts: { reason },
});
}
};
/**
* Handles the cancellation of a request to join a room.
*
* @returns {void}
*/
private onCancelAskToJoin = (): void => {
const roomId = this.getRoomId();
if (isNotUndefined(roomId)) {
dis.dispatch<CancelAskToJoinPayload>({
action: Action.CancelAskToJoin,
roomId,
});
}
};
public render(): ReactNode {
if (!this.context.client) return null;
2022-08-04 08:19:52 +02:00
if (this.state.room instanceof LocalRoom) {
if (this.state.room.state === LocalRoomState.CREATING) {
return this.renderLocalRoomCreateLoader(this.state.room);
2022-08-04 08:19:52 +02:00
}
return this.renderLocalRoomView(this.state.room);
}
if (this.state.room) {
const { shouldEncrypt, inviteEvent } = shouldEncryptRoomWithSingle3rdPartyInvite(this.state.room);
if (shouldEncrypt) {
return this.renderWaitingForThirdPartyRoomView(inviteEvent);
}
2022-08-04 08:19:52 +02:00
}
const roomHeaderType = SettingsStore.getValue("feature_new_room_decoration_ui") ? "new" : "legacy";
2016-02-10 18:27:40 +00:00
if (!this.state.room) {
2020-05-22 11:18:14 +01:00
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;
2017-05-30 11:21:33 +01:00
return (
<div className="mx_RoomView" data-room-header={roomHeaderType}>
2019-10-02 17:28:03 +01:00
<ErrorBoundary>
<RoomPreviewBar
canPreview={false}
previewLoading={previewLoading && !this.state.roomLoadError}
2019-10-02 17:28:03 +01:00
error={this.state.roomLoadError}
loading={loading}
joining={this.state.joining}
oobData={this.props.oobData}
roomId={this.state.roomId}
2019-10-02 17:28:03 +01:00
/>
</ErrorBoundary>
2017-05-30 11:21:33 +01:00
</div>
);
} else {
let inviterName: string | undefined;
2017-05-30 11:21:33 +01:00
if (this.props.oobData) {
inviterName = this.props.oobData.inviterName;
}
const invitedEmail = this.props.threepidInvite?.toEmail;
2017-05-30 11:21:33 +01:00
// We have no room object for this room, only the ID.
// We've got to this room by following a link, possibly a third party invite.
const roomAlias = this.state.roomAlias;
2017-05-30 11:21:33 +01:00
return (
<div className="mx_RoomView" data-room-header={roomHeaderType}>
2019-10-02 17:28:03 +01:00
<ErrorBoundary>
<RoomPreviewBar
onJoinClick={this.onJoinButtonClicked}
2019-10-02 17:28:03 +01:00
onForgetClick={this.onForgetClick}
onRejectClick={this.onRejectThreepidInviteButtonClicked}
2021-07-23 10:23:45 +01:00
canPreview={false}
error={this.state.roomLoadError}
2019-10-02 17:28:03 +01:00
roomAlias={roomAlias}
joining={this.state.joining}
inviterName={inviterName}
invitedEmail={invitedEmail}
oobData={this.props.oobData}
signUrl={this.props.threepidInvite?.signUrl}
roomId={this.state.roomId}
2023-08-07 08:27:09 +02:00
promptAskToJoin={this.state.promptAskToJoin}
onSubmitAskToJoin={this.onSubmitAskToJoin}
onCancelAskToJoin={this.onCancelAskToJoin}
2019-10-02 17:28:03 +01:00
/>
</ErrorBoundary>
2017-05-30 11:21:33 +01:00
</div>
);
}
}
const myMembership = this.state.room.getMyMembership();
2022-05-04 17:02:06 -04:00
if (
2022-09-16 11:12:27 -04:00
isVideoRoom(this.state.room) &&
!(SettingsStore.getValue("feature_video_rooms") && myMembership === KnownMembership.Join)
2022-05-04 17:02:06 -04:00
) {
return (
<ErrorBoundary>
<div className="mx_MainSplit">
<RoomPreviewCard
room={this.state.room}
onJoinButtonClicked={this.onJoinButtonClicked}
onRejectButtonClicked={this.onRejectButtonClicked}
/>
</div>
;
</ErrorBoundary>
);
}
// SpaceRoomView handles invites itself
if (myMembership === KnownMembership.Invite && !this.state.room.isSpaceRoom()) {
if (this.state.joining || this.state.rejecting) {
return (
2019-10-02 17:28:03 +01:00
<ErrorBoundary>
<RoomPreviewBar
canPreview={false}
error={this.state.roomLoadError}
joining={this.state.joining}
rejecting={this.state.rejecting}
roomId={this.state.roomId}
/>
2019-10-02 17:28:03 +01:00
</ErrorBoundary>
);
} else {
const myUserId = this.context.client.getSafeUserId();
const myMember = this.state.room.getMember(myUserId);
2020-05-10 11:17:21 +01:00
const inviteEvent = myMember ? myMember.events.member : null;
let inviterName = _t("room|inviter_unknown");
2020-05-10 11:17:21 +01:00
if (inviteEvent) {
inviterName = inviteEvent.sender?.name ?? inviteEvent.getSender()!;
2020-05-10 11:17:21 +01:00
}
// We deliberately don't try to peek into invites, even if we have permission to peek
// as they could be a spam vector.
// XXX: in future we could give the option of a 'Preview' button which lets them view anyway.
2016-01-18 20:15:12 +00:00
// We have a regular invite for this room.
return (
<div className="mx_RoomView" data-room-header={roomHeaderType}>
2019-10-02 17:28:03 +01:00
<ErrorBoundary>
<RoomPreviewBar
onJoinClick={this.onJoinButtonClicked}
2019-10-02 17:28:03 +01:00
onForgetClick={this.onForgetClick}
onRejectClick={this.onRejectButtonClicked}
onRejectAndIgnoreClick={this.onRejectAndIgnoreClick}
2019-10-02 17:28:03 +01:00
inviterName={inviterName}
canPreview={false}
joining={this.state.joining}
room={this.state.room}
roomId={this.state.roomId}
2019-10-02 17:28:03 +01:00
/>
</ErrorBoundary>
</div>
);
}
}
if (
this.state.canAskToJoin &&
([KnownMembership.Knock, KnownMembership.Leave] as Array<string>).includes(myMembership)
) {
2023-08-07 08:27:09 +02:00
return (
<div className="mx_RoomView" data-room-header={roomHeaderType}>
2023-08-07 08:27:09 +02:00
<ErrorBoundary>
<RoomPreviewBar
2023-12-04 13:20:24 +03:00
onJoinClick={this.onJoinButtonClicked}
2023-08-07 08:27:09 +02:00
room={this.state.room}
canAskToJoinAndMembershipIsLeave={myMembership === KnownMembership.Leave}
2023-12-04 13:20:24 +03:00
promptAskToJoin={this.state.promptAskToJoin}
knocked={myMembership === KnownMembership.Knock}
2023-08-07 08:27:09 +02:00
onSubmitAskToJoin={this.onSubmitAskToJoin}
onCancelAskToJoin={this.onCancelAskToJoin}
onForgetClick={this.onForgetClick}
2023-08-07 08:27:09 +02:00
/>
</ErrorBoundary>
</div>
);
}
// We have successfully loaded this room, and are not previewing.
// Display the "normal" room view.
let activeCall: MatrixCall | null = null;
2020-10-09 18:56:07 +01:00
{
2020-10-13 09:55:03 +01:00
// New block because this variable doesn't need to hang around for the rest of the function
2020-10-09 18:56:07 +01:00
const call = this.getCallForRoom();
if (call && this.state.callState !== "ended" && this.state.callState !== "ringing") {
activeCall = call;
}
}
let statusBar: JSX.Element | undefined;
let isStatusAreaExpanded = true;
if (ContentMessages.sharedInstance().getCurrentUploads().length > 0) {
2017-01-20 14:22:27 +00:00
statusBar = <UploadBar room={this.state.room} />;
} else if (!this.state.search) {
isStatusAreaExpanded = this.state.statusBarVisible;
statusBar = (
<RoomStatusBar
room={this.state.room}
isPeeking={myMembership !== KnownMembership.Join}
2022-04-20 11:03:33 -04:00
onInviteClick={this.onInviteClick}
onVisible={this.onStatusBarVisible}
onHidden={this.onStatusBarHidden}
/>
);
}
const statusBarAreaClass = classNames("mx_RoomView_statusArea", {
mx_RoomView_statusArea_expanded: isStatusAreaExpanded,
});
2021-08-08 21:00:15 +03:00
// if statusBar does not exist then statusBarArea is blank and takes up unnecessary space on the screen
// show statusBarArea only if statusBar is present
const statusBarArea = statusBar && (
<div role="region" className={statusBarAreaClass} aria-label={_t("a11y|room_status_bar")}>
<div className="mx_RoomView_statusAreaBox">
<div className="mx_RoomView_statusAreaBox_line" />
2021-07-24 19:00:47 +03:00
{statusBar}
2022-12-12 12:24:14 +01:00
</div>
</div>
2021-07-24 16:10:51 +03:00
);
2019-01-28 17:21:33 -07:00
const roomVersionRecommendation = this.state.upgradeRecommendation;
2018-08-17 14:54:43 +01:00
const showRoomUpgradeBar =
2019-01-28 17:21:33 -07:00
roomVersionRecommendation &&
roomVersionRecommendation.needsUpgrade &&
this.state.room.userMayUpgradeRoom(this.context.client.getSafeUserId());
2018-08-17 14:54:43 +01:00
2020-09-07 17:32:03 +01:00
const hiddenHighlightCount = this.getHiddenHighlightCount();
let aux: JSX.Element | undefined;
2019-04-10 17:49:45 +02:00
let previewBar;
if (this.state.timelineRenderingType === TimelineRenderingType.Search) {
2020-09-07 17:32:03 +01:00
aux = (
2024-07-08 10:57:41 +01:00
<RoomSearchAuxPanel
searchInfo={this.state.search}
2020-09-07 17:32:03 +01:00
onCancelClick={this.onCancelSearchClick}
2024-07-08 10:57:41 +01:00
onSearchScopeChange={this.onSearchScopeChange}
isRoomEncrypted={this.context.client.isRoomEncrypted(this.state.room.roomId)}
2020-09-07 17:32:03 +01:00
/>
);
2018-08-17 14:54:43 +01:00
} else if (showRoomUpgradeBar) {
2021-08-16 09:16:02 +01:00
aux = <RoomUpgradeWarningBar room={this.state.room} />;
} else if (myMembership !== KnownMembership.Join) {
2016-03-18 16:06:36 +00:00
// We do have a room object for this room, but we're not currently in it.
// We may have a 3rd party invite to it.
let inviterName: string | undefined;
if (this.props.oobData) {
inviterName = this.props.oobData.inviterName;
}
const invitedEmail = this.props.threepidInvite?.toEmail;
2019-04-10 17:49:45 +02:00
previewBar = (
2020-09-07 17:32:03 +01:00
<RoomPreviewBar
onJoinClick={this.onJoinButtonClicked}
onForgetClick={this.onForgetClick}
onRejectClick={this.onRejectThreepidInviteButtonClicked}
joining={this.state.joining}
inviterName={inviterName}
invitedEmail={invitedEmail}
oobData={this.props.oobData}
canPreview={this.state.canPeek}
room={this.state.room}
roomId={this.state.roomId}
/>
);
if (!this.state.canPeek && !this.state.room?.isSpaceRoom()) {
return (
<div className="mx_RoomView" data-room-header={roomHeaderType}>
{previewBar}
</div>
);
}
} else if (hiddenHighlightCount > 0) {
aux = (
2020-09-07 17:32:03 +01:00
<AccessibleButton
element="div"
className="mx_RoomView_auxPanel_hiddenHighlights"
onClick={this.onHiddenHighlightsClick}
>
{_t("room|unread_notifications_predecessor", {
2021-06-29 13:11:58 +01:00
count: hiddenHighlightCount,
})}
</AccessibleButton>
);
}
if (this.state.room?.isSpaceRoom() && !this.props.forceTimeline) {
2021-03-01 18:10:17 +00:00
return (
<SpaceRoomView
space={this.state.room}
justCreatedOpts={this.props.justCreatedOpts}
resizeNotifier={this.props.resizeNotifier}
permalinkCreator={this.permalinkCreator}
2021-03-01 18:10:17 +00:00
onJoinButtonClicked={this.onJoinButtonClicked}
onRejectButtonClicked={
this.props.threepidInvite
? this.onRejectThreepidInviteButtonClicked
: this.onRejectButtonClicked
}
/>
);
}
2017-10-11 17:56:17 +01:00
const auxPanel = (
2020-09-02 11:13:00 +01:00
<AuxPanel
room={this.state.room}
userId={this.context.client.getSafeUserId()}
2020-09-02 11:13:00 +01:00
showApps={this.state.showApps}
resizeNotifier={this.props.resizeNotifier}
>
{aux}
</AuxPanel>
);
2024-09-16 16:51:59 +02:00
const pinnedMessageBanner = (
<PinnedMessageBanner room={this.state.room} permalinkCreator={this.permalinkCreator} />
);
let messageComposer;
const showComposer =
// joined and not showing search results
myMembership === KnownMembership.Join && !this.state.search;
if (showComposer) {
messageComposer = (
<MessageComposer
room={this.state.room}
e2eStatus={this.state.e2eStatus}
resizeNotifier={this.props.resizeNotifier}
replyToEvent={this.state.replyToEvent}
2022-08-04 08:19:52 +02:00
permalinkCreator={this.permalinkCreator}
/>
);
}
// if we have search results, we keep the messagepanel (so that it preserves its
// scroll state), but hide it.
2017-10-11 17:56:17 +01:00
let searchResultsPanel;
let hideMessagePanel = false;
if (this.state.search) {
searchResultsPanel = (
<RoomSearchView
key={this.state.search.searchId}
ref={this.searchResultsPanel}
term={this.state.search.term}
scope={this.state.search.scope}
promise={this.state.search.promise}
abortController={this.state.search.abortController}
2024-07-08 10:57:41 +01:00
inProgress={!!this.state.search.inProgress}
resizeNotifier={this.props.resizeNotifier}
className={this.messagePanelClassNames}
onUpdate={this.onSearchUpdate}
/>
);
hideMessagePanel = true;
}
let highlightedEventId: string | undefined;
2021-05-08 20:07:51 -04:00
if (this.state.isInitialEventHighlighted) {
highlightedEventId = this.state.initialEventId;
}
2017-10-11 17:56:17 +01:00
const messagePanel = (
2019-12-08 12:16:17 +00:00
<TimelinePanel
2020-09-07 17:32:03 +01:00
ref={this.gatherTimelinePanelRef}
2017-08-30 13:32:07 +01:00
timelineSet={this.state.room.getUnfilteredTimelineSet()}
overlayTimelineSet={this.state.virtualRoom?.getUnfilteredTimelineSet()}
overlayTimelineSetFilter={isCallEvent}
showReadReceipts={this.state.showReadReceipts}
2017-10-11 18:43:24 -06:00
manageReadReceipts={!this.state.isPeeking}
sendReadReceiptOnLoad={!this.state.wasContextSwitch}
2017-10-11 18:43:24 -06:00
manageReadMarkers={!this.state.isPeeking}
2017-08-30 13:32:07 +01:00
hidden={hideMessagePanel}
highlightedEventId={highlightedEventId}
eventId={this.state.initialEventId}
eventScrollIntoView={this.state.initialEventScrollIntoView}
2017-08-30 13:32:07 +01:00
eventPixelOffset={this.state.initialEventPixelOffset}
2017-10-11 17:56:17 +01:00
onScroll={this.onMessageListScroll}
onEventScrolledIntoView={this.resetJumpToEvent}
2020-09-07 17:32:03 +01:00
onReadMarkerUpdated={this.updateTopUnreadMessagesBar}
showUrlPreview={this.state.showUrlPreview}
className={this.messagePanelClassNames}
membersLoaded={this.state.membersLoaded}
2022-08-04 08:19:52 +02:00
permalinkCreator={this.permalinkCreator}
resizeNotifier={this.props.resizeNotifier}
showReactions={true}
2021-01-22 13:44:45 +01:00
layout={this.state.layout}
2021-06-29 08:16:43 +01:00
editState={this.state.editState}
2016-02-10 18:27:40 +00:00
/>
);
let topUnreadMessagesBar: JSX.Element | undefined;
// Do not show TopUnreadMessagesBar if we have search results showing, it makes no sense
if (this.state.showTopUnreadMessagesBar && !this.state.search) {
2020-09-07 17:32:03 +01:00
topUnreadMessagesBar = (
<TopUnreadMessagesBar onScrollUpClick={this.jumpToReadMarker} onCloseClick={this.forgetReadMarker} />
);
}
2019-01-22 14:50:16 +01:00
let jumpToBottom;
// Do not show JumpToBottomButton if we have search results showing, it makes no sense
if (this.state.atEndOfLiveTimeline === false && !this.state.search) {
2019-01-22 14:50:16 +01:00
jumpToBottom = (
<JumpToBottomButton
2021-06-18 16:18:42 +01:00
highlight={this.state.room.getUnreadNotificationCount(NotificationCountType.Highlight) > 0}
2019-01-22 14:50:16 +01:00
numUnreadMessages={this.state.numUnreadMessages}
onScrollToBottomClick={this.jumpToLiveTimeline}
/>
);
}
2017-10-25 15:15:49 +01:00
2020-10-01 12:30:41 +01:00
const showRightPanel = this.state.room && this.state.showRightPanel;
2021-11-29 17:06:15 +01:00
2019-12-05 17:47:18 -07:00
const rightPanel = showRightPanel ? (
2021-08-17 10:38:09 +01:00
<RightPanel
room={this.state.room}
resizeNotifier={this.props.resizeNotifier}
2022-08-04 08:19:52 +02:00
permalinkCreator={this.permalinkCreator}
2021-09-07 16:02:26 +01:00
e2eStatus={this.state.e2eStatus}
2024-07-08 10:57:41 +01:00
onSearchChange={this.onSearchChange}
onSearchCancel={this.onCancelSearchClick}
2021-09-07 16:02:26 +01:00
/>
) : undefined;
const timelineClasses = classNames("mx_RoomView_timeline", {
mx_RoomView_timeline_rr_enabled: this.state.showReadReceipts,
});
let { mainSplitContentType } = this.state;
if (this.state.search) {
// When in the middle of a search force the main split content type to timeline
mainSplitContentType = MainSplitContentType.Timeline;
}
const mainClasses = classNames("mx_RoomView", {
2020-10-09 18:56:07 +01:00
mx_RoomView_inCall: Boolean(activeCall),
mx_RoomView_immersive: mainSplitContentType !== MainSplitContentType.Timeline,
});
2020-10-19 21:25:01 +02:00
const showChatEffects = SettingsStore.getValue("showChatEffects");
let mainSplitBody: JSX.Element | undefined;
let mainSplitContentClassName: string | undefined;
2021-11-16 15:43:18 +01:00
// Decide what to show in the main split
switch (mainSplitContentType) {
2021-11-16 15:43:18 +01:00
case MainSplitContentType.Timeline:
mainSplitContentClassName = "mx_MainSplit_timeline";
2022-03-22 18:14:11 -04:00
mainSplitBody = (
<>
{this.roomViewBody.current && (
<Measured sensor={this.roomViewBody.current} onMeasurement={this.onMeasurement} />
)}
2022-03-22 18:14:11 -04:00
{auxPanel}
{pinnedMessageBanner}
<main className={timelineClasses}>
2022-03-22 18:14:11 -04:00
<FileDropTarget parent={this.roomView.current} onFileDrop={this.onFileDrop} />
{topUnreadMessagesBar}
{jumpToBottom}
{messagePanel}
{searchResultsPanel}
</main>
2022-03-22 18:14:11 -04:00
{statusBarArea}
{previewBar}
{messageComposer}
</>
);
2021-11-16 15:43:18 +01:00
break;
case MainSplitContentType.MaximisedWidget:
mainSplitContentClassName = "mx_MainSplit_maximisedWidget";
mainSplitBody = (
<>
<AppsDrawer
room={this.state.room}
userId={this.context.client.getSafeUserId()}
resizeNotifier={this.props.resizeNotifier}
showApps={true}
role="main"
/>
{previewBar}
</>
);
2021-11-16 15:43:18 +01:00
break;
case MainSplitContentType.Call: {
mainSplitContentClassName = "mx_MainSplit_call";
2022-04-20 11:03:33 -04:00
mainSplitBody = (
<>
<CallView
room={this.state.room}
resizing={this.state.resizing}
waitForCall={isVideoRoom(this.state.room)}
skipLobby={this.context.roomViewStore.skipCallLobby() ?? false}
role="main"
/>
2022-04-20 11:03:33 -04:00
{previewBar}
</>
);
2022-03-22 18:14:11 -04:00
}
2021-11-16 15:43:18 +01:00
}
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())) {
2022-04-20 11:03:33 -04:00
onInviteClick = this.onInviteClick;
}
viewingCall = true;
}
const myMember = this.state.room!.getMember(this.context.client!.getSafeUserId());
const showForgetButton =
2024-03-11 17:16:53 +00:00
!this.context.client.isGuest() &&
(([KnownMembership.Leave, KnownMembership.Ban] as Array<string>).includes(myMembership) ||
myMember?.isKicked());
return (
2019-12-17 17:26:12 +00:00
<RoomContext.Provider value={this.state}>
<div
className={mainClasses}
ref={this.roomView}
onKeyDown={this.onReactKeyDown}
data-room-header={roomHeaderType}
>
{showChatEffects && this.roomView.current && (
2020-10-19 21:25:01 +02:00
<EffectsOverlay roomWidth={this.roomView.current.offsetWidth} />
2020-10-19 13:54:09 +02:00
)}
2019-12-17 17:26:12 +00:00
<ErrorBoundary>
<MainSplit
panel={rightPanel}
resizeNotifier={this.props.resizeNotifier}
// Override defaults when a thread is being shown to allow persisting a separate
// right panel width for thread panels as they tend to want to be wider.
sizeKey={this.state.threadRightPanel ? "thread" : undefined}
defaultSize={this.state.threadRightPanel ? 500 : undefined}
>
<div
className={mainSplitContentClasses}
ref={this.roomViewBody}
data-layout={this.state.layout}
>
2023-10-20 14:30:37 +01:00
{SettingsStore.getValue("feature_new_room_decoration_ui") ? (
<RoomHeader
room={this.state.room}
additionalButtons={this.state.viewRoomOpts.buttons}
/>
2023-10-20 14:30:37 +01:00
) : (
<LegacyRoomHeader
room={this.state.room}
searchInfo={this.state.search}
oobData={this.props.oobData}
inRoom={myMembership === KnownMembership.Join}
2023-10-20 14:30:37 +01:00
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}
2023-10-20 14:30:37 +01:00
/>
)}
2021-11-16 15:43:18 +01:00
{mainSplitBody}
2019-10-02 17:28:03 +01:00
</div>
2019-12-17 17:26:12 +00:00
</MainSplit>
</ErrorBoundary>
</div>
2019-12-17 17:26:12 +00:00
</RoomContext.Provider>
);
2020-08-29 12:14:16 +01:00
}
}
export default RoomView;