From cecf0ce299b7208fc2fe03e54c7c7adbeb06a98d Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Tue, 22 Jun 2021 20:41:26 +0100
Subject: [PATCH] Convert MessagePanel, TimelinePanel, ScrollPanel, and more to
Typescript
---
src/@types/global.d.ts | 32 +-
.../structures/AutoHideScrollbar.tsx | 6 +-
.../{MessagePanel.js => MessagePanel.tsx} | 723 +++++++++---------
.../structures/NotificationPanel.tsx | 3 +-
src/components/structures/RoomDirectory.tsx | 17 +-
src/components/structures/RoomView.tsx | 7 +-
.../{ScrollPanel.js => ScrollPanel.tsx} | 431 ++++++-----
.../{TimelinePanel.js => TimelinePanel.tsx} | 637 ++++++++-------
.../views/dialogs/ForwardDialog.tsx | 33 +-
.../{ErrorBoundary.js => ErrorBoundary.tsx} | 35 +-
.../views/elements/EventListSummary.tsx | 14 +-
.../views/elements/EventTilePreview.tsx | 11 +-
.../views/elements/MemberEventListSummary.tsx | 14 +-
.../{DateSeparator.js => DateSeparator.tsx} | 26 +-
...ErrorBoundary.js => TileErrorBoundary.tsx} | 28 +-
src/components/views/rooms/EventTile.tsx | 18 +-
16 files changed, 1087 insertions(+), 948 deletions(-)
rename src/components/structures/{MessagePanel.js => MessagePanel.tsx} (64%)
rename src/components/structures/{ScrollPanel.js => ScrollPanel.tsx} (73%)
rename src/components/structures/{TimelinePanel.js => TimelinePanel.tsx} (75%)
rename src/components/views/elements/{ErrorBoundary.js => ErrorBoundary.tsx} (80%)
rename src/components/views/messages/{DateSeparator.js => DateSeparator.tsx} (82%)
rename src/components/views/messages/{TileErrorBoundary.js => TileErrorBoundary.tsx} (77%)
diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts
index 7eff341095..f75c17aaf4 100644
--- a/src/@types/global.d.ts
+++ b/src/@types/global.d.ts
@@ -16,6 +16,7 @@ limitations under the License.
import "matrix-js-sdk/src/@types/global"; // load matrix-js-sdk's type extensions first
import * as ModernizrStatic from "modernizr";
+
import ContentMessages from "../ContentMessages";
import { IMatrixClientPeg } from "../MatrixClientPeg";
import ToastStore from "../stores/ToastStore";
@@ -23,25 +24,25 @@ import DeviceListener from "../DeviceListener";
import { RoomListStoreClass } from "../stores/room-list/RoomListStore";
import { PlatformPeg } from "../PlatformPeg";
import RoomListLayoutStore from "../stores/room-list/RoomListLayoutStore";
-import {IntegrationManagers} from "../integrations/IntegrationManagers";
-import {ModalManager} from "../Modal";
+import { IntegrationManagers } from "../integrations/IntegrationManagers";
+import { ModalManager } from "../Modal";
import SettingsStore from "../settings/SettingsStore";
-import {ActiveRoomObserver} from "../ActiveRoomObserver";
-import {Notifier} from "../Notifier";
-import type {Renderer} from "react-dom";
+import { ActiveRoomObserver } from "../ActiveRoomObserver";
+import { Notifier } from "../Notifier";
+import type { Renderer } from "react-dom";
import RightPanelStore from "../stores/RightPanelStore";
import WidgetStore from "../stores/WidgetStore";
import CallHandler from "../CallHandler";
-import {Analytics} from "../Analytics";
+import { Analytics } from "../Analytics";
import CountlyAnalytics from "../CountlyAnalytics";
import UserActivity from "../UserActivity";
-import {ModalWidgetStore} from "../stores/ModalWidgetStore";
+import { ModalWidgetStore } from "../stores/ModalWidgetStore";
import { WidgetLayoutStore } from "../stores/widgets/WidgetLayoutStore";
import VoipUserMapper from "../VoipUserMapper";
-import {SpaceStoreClass} from "../stores/SpaceStore";
+import { SpaceStoreClass } from "../stores/SpaceStore";
import TypingStore from "../stores/TypingStore";
import { EventIndexPeg } from "../indexing/EventIndexPeg";
-import {VoiceRecordingStore} from "../stores/VoiceRecordingStore";
+import { VoiceRecordingStore } from "../stores/VoiceRecordingStore";
import PerformanceMonitor from "../performance";
import UIStore from "../stores/UIStore";
import { SetupEncryptionStore } from "../stores/SetupEncryptionStore";
@@ -127,11 +128,24 @@ declare global {
setSinkId(outputId: string);
}
+ // Add Chrome-specific `instant` ScrollBehaviour
+ type _ScrollBehavior = ScrollBehavior | "instant";
+
+ interface _ScrollOptions {
+ behavior?: _ScrollBehavior;
+ }
+
+ interface _ScrollIntoViewOptions extends _ScrollOptions {
+ block?: ScrollLogicalPosition;
+ inline?: ScrollLogicalPosition;
+ }
+
interface Element {
// Safari & IE11 only have this prefixed: we used prefixed versions
// previously so let's continue to support them for now
webkitRequestFullScreen(options?: FullscreenOptions): Promise;
msRequestFullscreen(options?: FullscreenOptions): Promise;
+ scrollIntoView(arg?: boolean | _ScrollIntoViewOptions): void;
}
interface Error {
diff --git a/src/components/structures/AutoHideScrollbar.tsx b/src/components/structures/AutoHideScrollbar.tsx
index 66f998b616..3b7fee3a08 100644
--- a/src/components/structures/AutoHideScrollbar.tsx
+++ b/src/components/structures/AutoHideScrollbar.tsx
@@ -15,12 +15,12 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import React from "react";
+import React, { WheelEvent } from "react";
interface IProps {
className?: string;
- onScroll?: () => void;
- onWheel?: () => void;
+ onScroll?: (event: Event) => void;
+ onWheel?: (event: WheelEvent) => void;
style?: React.CSSProperties
tabIndex?: number,
wrappedRef?: (ref: HTMLDivElement) => void;
diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.tsx
similarity index 64%
rename from src/components/structures/MessagePanel.js
rename to src/components/structures/MessagePanel.tsx
index eb9611a6fc..492d9d9a53 100644
--- a/src/components/structures/MessagePanel.js
+++ b/src/components/structures/MessagePanel.tsx
@@ -1,7 +1,5 @@
/*
-Copyright 2016 OpenMarket Ltd
-Copyright 2018 New Vector Ltd
-Copyright 2019 The Matrix.org Foundation C.I.C.
+Copyright 2016 - 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -16,32 +14,46 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import React, {createRef} from 'react';
+import React, { createRef, KeyboardEvent, ReactNode, SyntheticEvent, TransitionEvent } from 'react';
import ReactDOM from 'react-dom';
-import PropTypes from 'prop-types';
-import shouldHideEvent from '../../shouldHideEvent';
-import {wantsDateSeparator} from '../../DateUtils';
-import * as sdk from '../../index';
+import { Room } from 'matrix-js-sdk/src/models/room';
+import { EventType } from 'matrix-js-sdk/src/@types/event';
+import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
+import { Relations } from "matrix-js-sdk/src/models/relations";
+import { RoomMember } from 'matrix-js-sdk/src/models/room-member';
-import {MatrixClientPeg} from '../../MatrixClientPeg';
+import shouldHideEvent from '../../shouldHideEvent';
+import { wantsDateSeparator } from '../../DateUtils';
+import { MatrixClientPeg } from '../../MatrixClientPeg';
import SettingsStore from '../../settings/SettingsStore';
import RoomContext from "../../contexts/RoomContext";
-import {Layout, LayoutPropType} from "../../settings/Layout";
-import {_t} from "../../languageHandler";
-import {haveTileForEvent} from "../views/rooms/EventTile";
-import {hasText} from "../../TextForEvent";
+import { Layout } from "../../settings/Layout";
+import { _t } from "../../languageHandler";
+import EventTile, { haveTileForEvent, IReadReceiptProps, TileShape } from "../views/rooms/EventTile";
+import { hasText } from "../../TextForEvent";
import IRCTimelineProfileResizer from "../views/elements/IRCTimelineProfileResizer";
import DMRoomMap from "../../utils/DMRoomMap";
import NewRoomIntro from "../views/rooms/NewRoomIntro";
-import {replaceableComponent} from "../../utils/replaceableComponent";
+import { replaceableComponent } from "../../utils/replaceableComponent";
import defaultDispatcher from '../../dispatcher/dispatcher';
+import WhoIsTypingTile from '../views/rooms/WhoIsTypingTile';
+import ScrollPanel, { IScrollState } from "./ScrollPanel";
+import EventListSummary from '../views/elements/EventListSummary';
+import MemberEventListSummary from '../views/elements/MemberEventListSummary';
+import DateSeparator from '../views/messages/DateSeparator';
+import ErrorBoundary from '../views/elements/ErrorBoundary';
+import ResizeNotifier from "../../utils/ResizeNotifier";
+import Spinner from "../views/elements/Spinner";
+import TileErrorBoundary from '../views/messages/TileErrorBoundary';
+import { RoomPermalinkCreator } from "../../utils/permalinks/Permalinks";
+import EditorStateTransfer from "../../utils/EditorStateTransfer";
const CONTINUATION_MAX_INTERVAL = 5 * 60 * 1000; // 5 minutes
-const continuedTypes = ['m.sticker', 'm.room.message'];
+const continuedTypes = [EventType.Sticker, EventType.RoomMessage];
// check if there is a previous event and it has the same sender as this event
// and the types are the same/is in continuedTypes and the time between them is <= CONTINUATION_MAX_INTERVAL
-function shouldFormContinuation(prevEvent, mxEvent) {
+function shouldFormContinuation(prevEvent: MatrixEvent, mxEvent: MatrixEvent): boolean {
// sanity check inputs
if (!prevEvent || !prevEvent.sender || !mxEvent.sender) return false;
// check if within the max continuation period
@@ -52,8 +64,8 @@ function shouldFormContinuation(prevEvent, mxEvent) {
// Some events should appear as continuations from previous events of different types.
if (mxEvent.getType() !== prevEvent.getType() &&
- (!continuedTypes.includes(mxEvent.getType()) ||
- !continuedTypes.includes(prevEvent.getType()))) return false;
+ (!continuedTypes.includes(mxEvent.getType() as EventType) ||
+ !continuedTypes.includes(prevEvent.getType() as EventType))) return false;
// Check if the sender is the same and hasn't changed their displayname/avatar between these events
if (mxEvent.sender.userId !== prevEvent.sender.userId ||
@@ -66,96 +78,161 @@ function shouldFormContinuation(prevEvent, mxEvent) {
return true;
}
-const isMembershipChange = (e) => e.getType() === 'm.room.member' || e.getType() === 'm.room.third_party_invite';
+const isMembershipChange = (e: MatrixEvent): boolean => {
+ return e.getType() === EventType.RoomMember || e.getType() === EventType.RoomThirdPartyInvite;
+}
+
+interface IProps {
+ // the list of MatrixEvents to display
+ events: MatrixEvent[];
+
+ // true to give the component a 'display: none' style.
+ hidden?: boolean;
+
+ // true to show a spinner at the top of the timeline to indicate
+ // back-pagination in progress
+ backPaginating?: boolean;
+
+ // true to show a spinner at the end of the timeline to indicate
+ // forward-pagination in progress
+ forwardPaginating?: boolean;
+
+ // ID of an event to highlight. If undefined, no event will be highlighted.
+ highlightedEventId?: string;
+
+ // The room these events are all in together, if any.
+ // (The notification panel won't have a room here, for example.)
+ room?: Room;
+
+ // Should we show URL Previews
+ showUrlPreview?: boolean;
+
+ // event after which we should show a read marker
+ readMarkerEventId?: string;
+
+ // whether the read marker should be visible
+ readMarkerVisible?: boolean;
+
+ // the userid of our user. This is used to suppress the read marker
+ // for pending messages.
+ ourUserId?: string;
+
+ // true to suppress the date at the start of the timeline
+ suppressFirstDateSeparator?: boolean;
+
+ // whether to show read receipts
+ showReadReceipts?: boolean;
+
+ // true if updates to the event list should cause the scroll panel to
+ // scroll down when we are at the bottom of the window. See ScrollPanel
+ // for more details.
+ stickyBottom?: boolean;
+
+ // className for the panel
+ className: string;
+
+ // shape parameter to be passed to EventTiles
+ tileShape?: TileShape;
+
+ // show twelve hour timestamps
+ isTwelveHour?: boolean;
+
+ // show timestamps always
+ alwaysShowTimestamps?: boolean;
+
+ // whether to show reactions for an event
+ showReactions?: boolean;
+
+ // which layout to use
+ layout?: Layout;
+
+ // whether or not to show flair at all
+ enableFlair?: boolean;
+
+ resizeNotifier: ResizeNotifier;
+ permalinkCreator?: RoomPermalinkCreator;
+ editState?: EditorStateTransfer;
+
+ // callback which is called when the panel is scrolled.
+ onScroll?(event: Event): void;
+
+ // callback which is called when the user interacts with the room timeline
+ onUserScroll(event: SyntheticEvent): void;
+
+ // callback which is called when more content is needed.
+ onFillRequest?(backwards: boolean): Promise;
+
+ // helper function to access relations for an event
+ onUnfillRequest?(backwards: boolean, scrollToken: string): void;
+
+ getRelationsForEvent?(eventId: string, relationType: string, eventType: string): Relations;
+}
+
+interface IState {
+ ghostReadMarkers: string[];
+ showTypingNotifications: boolean;
+}
+
+interface IReadReceiptForUser {
+ lastShownEventId: string;
+ receipt: IReadReceiptProps;
+}
/* (almost) stateless UI component which builds the event tiles in the room timeline.
*/
@replaceableComponent("structures.MessagePanel")
-export default class MessagePanel extends React.Component {
- static propTypes = {
- // true to give the component a 'display: none' style.
- hidden: PropTypes.bool,
-
- // true to show a spinner at the top of the timeline to indicate
- // back-pagination in progress
- backPaginating: PropTypes.bool,
-
- // true to show a spinner at the end of the timeline to indicate
- // forward-pagination in progress
- forwardPaginating: PropTypes.bool,
-
- // the list of MatrixEvents to display
- events: PropTypes.array.isRequired,
-
- // ID of an event to highlight. If undefined, no event will be highlighted.
- highlightedEventId: PropTypes.string,
-
- // The room these events are all in together, if any.
- // (The notification panel won't have a room here, for example.)
- room: PropTypes.object,
-
- // Should we show URL Previews
- showUrlPreview: PropTypes.bool,
-
- // event after which we should show a read marker
- readMarkerEventId: PropTypes.string,
-
- // whether the read marker should be visible
- readMarkerVisible: PropTypes.bool,
-
- // the userid of our user. This is used to suppress the read marker
- // for pending messages.
- ourUserId: PropTypes.string,
-
- // true to suppress the date at the start of the timeline
- suppressFirstDateSeparator: PropTypes.bool,
-
- // whether to show read receipts
- showReadReceipts: PropTypes.bool,
-
- // true if updates to the event list should cause the scroll panel to
- // scroll down when we are at the bottom of the window. See ScrollPanel
- // for more details.
- stickyBottom: PropTypes.bool,
-
- // callback which is called when the panel is scrolled.
- onScroll: PropTypes.func,
-
- // callback which is called when the user interacts with the room timeline
- onUserScroll: PropTypes.func,
-
- // callback which is called when more content is needed.
- onFillRequest: PropTypes.func,
-
- // className for the panel
- className: PropTypes.string.isRequired,
-
- // shape parameter to be passed to EventTiles
- tileShape: PropTypes.string,
-
- // show twelve hour timestamps
- isTwelveHour: PropTypes.bool,
-
- // show timestamps always
- alwaysShowTimestamps: PropTypes.bool,
-
- // helper function to access relations for an event
- getRelationsForEvent: PropTypes.func,
-
- // whether to show reactions for an event
- showReactions: PropTypes.bool,
-
- // which layout to use
- layout: LayoutPropType,
-
- // whether or not to show flair at all
- enableFlair: PropTypes.bool,
- };
-
+export default class MessagePanel extends React.Component {
static contextType = RoomContext;
- constructor(props) {
- super(props);
+ // opaque readreceipt info for each userId; used by ReadReceiptMarker
+ // to manage its animations
+ private readonly readReceiptMap: Record = {};
+
+ // Track read receipts by event ID. For each _shown_ event ID, we store
+ // the list of read receipts to display:
+ // [
+ // {
+ // userId: string,
+ // member: RoomMember,
+ // ts: number,
+ // },
+ // ]
+ // This is recomputed on each render. It's only stored on the component
+ // for ease of passing the data around since it's computed in one pass
+ // over all events.
+ private readReceiptsByEvent: Record = {};
+
+ // Track read receipts by user ID. For each user ID we've ever shown a
+ // a read receipt for, we store an object:
+ // {
+ // lastShownEventId: string,
+ // receipt: {
+ // userId: string,
+ // member: RoomMember,
+ // ts: number,
+ // },
+ // }
+ // so that we can always keep receipts displayed by reverting back to
+ // the last shown event for that user ID when needed. This may feel like
+ // it duplicates the receipt storage in the room, but at this layer, we
+ // are tracking _shown_ event IDs, which the JS SDK knows nothing about.
+ // This is recomputed on each render, using the data from the previous
+ // render as our fallback for any user IDs we can't match a receipt to a
+ // displayed event in the current render cycle.
+ private readReceiptsByUserId: Record = {};
+
+ private readonly showHiddenEventsInTimeline: boolean;
+ private isMounted = false;
+
+ private readMarkerNode = createRef();
+ private whoIsTyping = createRef();
+ private scrollPanel = createRef();
+
+ private readonly showTypingNotificationsWatcherRef: string;
+ private eventNodes: Record;
+
+ constructor(props, context) {
+ super(props, context);
this.state = {
// previous positions the read marker has been in, so we can
@@ -164,65 +241,21 @@ export default class MessagePanel extends React.Component {
showTypingNotifications: SettingsStore.getValue("showTypingNotifications"),
};
- // opaque readreceipt info for each userId; used by ReadReceiptMarker
- // to manage its animations
- this._readReceiptMap = {};
-
- // Track read receipts by event ID. For each _shown_ event ID, we store
- // the list of read receipts to display:
- // [
- // {
- // userId: string,
- // member: RoomMember,
- // ts: number,
- // },
- // ]
- // This is recomputed on each render. It's only stored on the component
- // for ease of passing the data around since it's computed in one pass
- // over all events.
- this._readReceiptsByEvent = {};
-
- // Track read receipts by user ID. For each user ID we've ever shown a
- // a read receipt for, we store an object:
- // {
- // lastShownEventId: string,
- // receipt: {
- // userId: string,
- // member: RoomMember,
- // ts: number,
- // },
- // }
- // so that we can always keep receipts displayed by reverting back to
- // the last shown event for that user ID when needed. This may feel like
- // it duplicates the receipt storage in the room, but at this layer, we
- // are tracking _shown_ event IDs, which the JS SDK knows nothing about.
- // This is recomputed on each render, using the data from the previous
- // render as our fallback for any user IDs we can't match a receipt to a
- // displayed event in the current render cycle.
- this._readReceiptsByUserId = {};
-
// Cache hidden events setting on mount since Settings is expensive to
// query, and we check this in a hot code path.
- this._showHiddenEventsInTimeline =
- SettingsStore.getValue("showHiddenEventsInTimeline");
+ this.showHiddenEventsInTimeline = SettingsStore.getValue("showHiddenEventsInTimeline");
- this._isMounted = false;
-
- this._readMarkerNode = createRef();
- this._whoIsTyping = createRef();
- this._scrollPanel = createRef();
-
- this._showTypingNotificationsWatcherRef =
+ this.showTypingNotificationsWatcherRef =
SettingsStore.watchSetting("showTypingNotifications", null, this.onShowTypingNotificationsChange);
}
componentDidMount() {
- this._isMounted = true;
+ this.isMounted = true;
}
componentWillUnmount() {
- this._isMounted = false;
- SettingsStore.unwatchSetting(this._showTypingNotificationsWatcherRef);
+ this.isMounted = false;
+ SettingsStore.unwatchSetting(this.showTypingNotificationsWatcherRef);
}
componentDidUpdate(prevProps, prevState) {
@@ -235,14 +268,14 @@ export default class MessagePanel extends React.Component {
}
}
- onShowTypingNotificationsChange = () => {
+ private onShowTypingNotificationsChange = (): void => {
this.setState({
showTypingNotifications: SettingsStore.getValue("showTypingNotifications"),
});
};
/* get the DOM node representing the given event */
- getNodeForEventId(eventId) {
+ public getNodeForEventId(eventId: string): HTMLElement {
if (!this.eventNodes) {
return undefined;
}
@@ -252,8 +285,8 @@ export default class MessagePanel extends React.Component {
/* return true if the content is fully scrolled down right now; else false.
*/
- isAtBottom() {
- return this._scrollPanel.current && this._scrollPanel.current.isAtBottom();
+ public isAtBottom(): boolean {
+ return this.scrollPanel.current?.isAtBottom();
}
/* get the current scroll state. See ScrollPanel.getScrollState for
@@ -261,8 +294,8 @@ export default class MessagePanel extends React.Component {
*
* returns null if we are not mounted.
*/
- getScrollState() {
- return this._scrollPanel.current ? this._scrollPanel.current.getScrollState() : null;
+ public getScrollState(): IScrollState {
+ return this.scrollPanel.current?.getScrollState() ?? null;
}
// returns one of:
@@ -271,15 +304,15 @@ export default class MessagePanel extends React.Component {
// -1: read marker is above the window
// 0: read marker is within the window
// +1: read marker is below the window
- getReadMarkerPosition() {
- const readMarker = this._readMarkerNode.current;
- const messageWrapper = this._scrollPanel.current;
+ public getReadMarkerPosition(): number {
+ const readMarker = this.readMarkerNode.current;
+ const messageWrapper = this.scrollPanel.current;
if (!readMarker || !messageWrapper) {
return null;
}
- const wrapperRect = ReactDOM.findDOMNode(messageWrapper).getBoundingClientRect();
+ const wrapperRect = (ReactDOM.findDOMNode(messageWrapper) as HTMLElement).getBoundingClientRect();
const readMarkerRect = readMarker.getBoundingClientRect();
// the read-marker pretends to have zero height when it is actually
@@ -295,17 +328,17 @@ export default class MessagePanel extends React.Component {
/* jump to the top of the content.
*/
- scrollToTop() {
- if (this._scrollPanel.current) {
- this._scrollPanel.current.scrollToTop();
+ public scrollToTop(): void {
+ if (this.scrollPanel.current) {
+ this.scrollPanel.current.scrollToTop();
}
}
/* jump to the bottom of the content.
*/
- scrollToBottom() {
- if (this._scrollPanel.current) {
- this._scrollPanel.current.scrollToBottom();
+ public scrollToBottom(): void {
+ if (this.scrollPanel.current) {
+ this.scrollPanel.current.scrollToBottom();
}
}
@@ -314,9 +347,9 @@ export default class MessagePanel extends React.Component {
*
* @param {number} mult: -1 to page up, +1 to page down
*/
- scrollRelative(mult) {
- if (this._scrollPanel.current) {
- this._scrollPanel.current.scrollRelative(mult);
+ public scrollRelative(mult: number): void {
+ if (this.scrollPanel.current) {
+ this.scrollPanel.current.scrollRelative(mult);
}
}
@@ -325,9 +358,9 @@ export default class MessagePanel extends React.Component {
*
* @param {KeyboardEvent} ev: the keyboard event to handle
*/
- handleScrollKey(ev) {
- if (this._scrollPanel.current) {
- this._scrollPanel.current.handleScrollKey(ev);
+ public handleScrollKey(ev: KeyboardEvent): void {
+ if (this.scrollPanel.current) {
+ this.scrollPanel.current.handleScrollKey(ev);
}
}
@@ -341,38 +374,41 @@ export default class MessagePanel extends React.Component {
* node (specifically, the bottom of it) will be positioned. If omitted, it
* defaults to 0.
*/
- scrollToEvent(eventId, pixelOffset, offsetBase) {
- if (this._scrollPanel.current) {
- this._scrollPanel.current.scrollToToken(eventId, pixelOffset, offsetBase);
+ public scrollToEvent(eventId: string, pixelOffset: number, offsetBase: number): void {
+ if (this.scrollPanel.current) {
+ this.scrollPanel.current.scrollToToken(eventId, pixelOffset, offsetBase);
}
}
- scrollToEventIfNeeded(eventId) {
+ public scrollToEventIfNeeded(eventId: string): void {
const node = this.eventNodes[eventId];
if (node) {
- node.scrollIntoView({block: "nearest", behavior: "instant"});
+ node.scrollIntoView({
+ block: "nearest",
+ behavior: "instant",
+ });
}
}
/* check the scroll state and send out pagination requests if necessary.
*/
- checkFillState() {
- if (this._scrollPanel.current) {
- this._scrollPanel.current.checkFillState();
+ public checkFillState(): void {
+ if (this.scrollPanel.current) {
+ this.scrollPanel.current.checkFillState();
}
}
- _isUnmounting = () => {
- return !this._isMounted;
+ private isUnmounting = (): boolean => {
+ return !this.isMounted;
};
// TODO: Implement granular (per-room) hide options
- _shouldShowEvent(mxEv) {
+ public shouldShowEvent(mxEv: MatrixEvent): boolean {
if (mxEv.sender && MatrixClientPeg.get().isUserIgnored(mxEv.sender.userId)) {
return false; // ignored = no show (only happens if the ignore happens after an event was received)
}
- if (this._showHiddenEventsInTimeline) {
+ if (this.showHiddenEventsInTimeline) {
return true;
}
@@ -386,7 +422,7 @@ export default class MessagePanel extends React.Component {
return !shouldHideEvent(mxEv, this.context);
}
- _readMarkerForEvent(eventId, isLastEvent) {
+ public readMarkerForEvent(eventId: string, isLastEvent: boolean): ReactNode {
const visible = !isLastEvent && this.props.readMarkerVisible;
if (this.props.readMarkerEventId === eventId) {
@@ -405,7 +441,7 @@ export default class MessagePanel extends React.Component {
return (
@@ -424,8 +460,8 @@ export default class MessagePanel extends React.Component {
// transition (ie. the read markers do but the event tiles do not)
// and TransitionGroup requires that all its children are Transitions.
const hr =
;
@@ -445,7 +481,7 @@ export default class MessagePanel extends React.Component {
return null;
}
- _collectGhostReadMarker = (node) => {
+ private collectGhostReadMarker = (node: HTMLElement): void => {
if (node) {
// now the element has appeared, change the style which will trigger the CSS transition
requestAnimationFrame(() => {
@@ -455,15 +491,15 @@ export default class MessagePanel extends React.Component {
}
};
- _onGhostTransitionEnd = (ev) => {
+ private onGhostTransitionEnd = (ev: TransitionEvent): void => {
// we can now clean up the ghost element
- const finishedEventId = ev.target.dataset.eventid;
+ const finishedEventId = (ev.target as HTMLElement).dataset.eventid;
this.setState({
ghostReadMarkers: this.state.ghostReadMarkers.filter(eid => eid !== finishedEventId),
});
};
- _getNextEventInfo(arr, i) {
+ private getNextEventInfo(arr: MatrixEvent[], i: number): { nextEvent: MatrixEvent, nextTile: MatrixEvent } {
const nextEvent = i < arr.length - 1
? arr[i + 1]
: null;
@@ -472,16 +508,16 @@ export default class MessagePanel extends React.Component {
// when rendering the tile. The shouldShowEvent function is pretty quick at what
// it does, so this should have no significant cost even when a room is used for
// not-chat purposes.
- const nextTile = arr.slice(i + 1).find(e => this._shouldShowEvent(e));
+ const nextTile = arr.slice(i + 1).find(e => this.shouldShowEvent(e));
- return {nextEvent, nextTile};
+ return { nextEvent, nextTile };
}
- get _roomHasPendingEdit() {
+ private get roomHasPendingEdit(): string {
return this.props.room && localStorage.getItem(`mx_edit_room_${this.props.room.roomId}`);
}
- _getEventTiles() {
+ private getEventTiles(): ReactNode[] {
this.eventNodes = {};
let i;
@@ -497,7 +533,7 @@ export default class MessagePanel extends React.Component {
let lastShownNonLocalEchoIndex = -1;
for (i = this.props.events.length-1; i >= 0; i--) {
const mxEv = this.props.events[i];
- if (!this._shouldShowEvent(mxEv)) {
+ if (!this.shouldShowEvent(mxEv)) {
continue;
}
@@ -521,18 +557,18 @@ export default class MessagePanel extends React.Component {
// Note: the EventTile might still render a "sent/sending receipt" independent of
// this information. When not providing read receipt information, the tile is likely
// to assume that sent receipts are to be shown more often.
- this._readReceiptsByEvent = {};
+ this.readReceiptsByEvent = {};
if (this.props.showReadReceipts) {
- this._readReceiptsByEvent = this._getReadReceiptsByShownEvent();
+ this.readReceiptsByEvent = this.getReadReceiptsByShownEvent();
}
- let grouper = null;
+ let grouper: BaseGrouper = null;
for (i = 0; i < this.props.events.length; i++) {
const mxEv = this.props.events[i];
const eventId = mxEv.getId();
const last = (mxEv === lastShownEvent);
- const {nextEvent, nextTile} = this._getNextEventInfo(this.props.events, i);
+ const { nextEvent, nextTile } = this.getNextEventInfo(this.props.events, i);
if (grouper) {
if (grouper.shouldGroup(mxEv)) {
@@ -553,26 +589,25 @@ export default class MessagePanel extends React.Component {
}
}
if (!grouper) {
- const wantTile = this._shouldShowEvent(mxEv);
+ const wantTile = this.shouldShowEvent(mxEv);
const isGrouped = false;
if (wantTile) {
- // make sure we unpack the array returned by _getTilesForEvent,
+ // make sure we unpack the array returned by getTilesForEvent,
// otherwise react will auto-generate keys and we will end up
// replacing all of the DOM elements every time we paginate.
- ret.push(...this._getTilesForEvent(prevEvent, mxEv, last, isGrouped,
- nextEvent, nextTile));
+ ret.push(...this.getTilesForEvent(prevEvent, mxEv, last, isGrouped, nextEvent, nextTile));
prevEvent = mxEv;
}
- const readMarker = this._readMarkerForEvent(eventId, i >= lastShownNonLocalEchoIndex);
+ const readMarker = this.readMarkerForEvent(eventId, i >= lastShownNonLocalEchoIndex);
if (readMarker) ret.push(readMarker);
}
}
- if (!this.props.editState && this._roomHasPendingEdit) {
+ if (!this.props.editState && this.roomHasPendingEdit) {
defaultDispatcher.dispatch({
action: "edit_event",
- event: this.props.room.findEventById(this._roomHasPendingEdit),
+ event: this.props.room.findEventById(this.roomHasPendingEdit),
});
}
@@ -583,10 +618,14 @@ export default class MessagePanel extends React.Component {
return ret;
}
- _getTilesForEvent(prevEvent, mxEv, last, isGrouped=false, nextEvent, nextEventWithTile) {
- const TileErrorBoundary = sdk.getComponent('messages.TileErrorBoundary');
- const EventTile = sdk.getComponent('rooms.EventTile');
- const DateSeparator = sdk.getComponent('messages.DateSeparator');
+ public getTilesForEvent(
+ prevEvent: MatrixEvent,
+ mxEv: MatrixEvent,
+ last = false,
+ isGrouped = false,
+ nextEvent?: MatrixEvent,
+ nextEventWithTile?: MatrixEvent,
+ ): ReactNode[] {
const ret = [];
const isEditing = this.props.editState &&
@@ -601,7 +640,7 @@ export default class MessagePanel extends React.Component {
}
// do we need a date separator since the last event?
- const wantsDateSeparator = this._wantsDateSeparator(prevEvent, eventDate);
+ const wantsDateSeparator = this.wantsDateSeparator(prevEvent, eventDate);
if (wantsDateSeparator && !isGrouped) {
const dateSeparator = ;
ret.push(dateSeparator);
@@ -609,7 +648,7 @@ export default class MessagePanel extends React.Component {
let willWantDateSeparator = false;
if (nextEvent) {
- willWantDateSeparator = this._wantsDateSeparator(mxEv, nextEvent.getDate() || new Date());
+ willWantDateSeparator = this.wantsDateSeparator(mxEv, nextEvent.getDate() || new Date());
}
// is this a continuation of the previous message?
@@ -618,12 +657,12 @@ export default class MessagePanel extends React.Component {
const eventId = mxEv.getId();
const highlight = (eventId === this.props.highlightedEventId);
- const readReceipts = this._readReceiptsByEvent[eventId];
+ const readReceipts = this.readReceiptsByEvent[eventId];
let isLastSuccessful = false;
const isSentState = s => !s || s === 'sent';
const isSent = isSentState(mxEv.getAssociatedStatus());
- const hasNextEvent = nextEvent && this._shouldShowEvent(nextEvent);
+ const hasNextEvent = nextEvent && this.shouldShowEvent(nextEvent);
if (!hasNextEvent && isSent) {
isLastSuccessful = true;
} else if (hasNextEvent && isSent && !isSentState(nextEvent.getAssociatedStatus())) {
@@ -649,18 +688,18 @@ export default class MessagePanel extends React.Component {
{
if (!r.userId || r.type !== "m.read" || r.userId === myUserId) {
return; // ignore non-read receipts and receipts from self.
@@ -721,13 +760,13 @@ export default class MessagePanel extends React.Component {
// Get an object that maps from event ID to a list of read receipts that
// should be shown next to that event. If a hidden event has read receipts,
// they are folded into the receipts of the last shown event.
- _getReadReceiptsByShownEvent() {
+ private getReadReceiptsByShownEvent(): Record {
const receiptsByEvent = {};
const receiptsByUserId = {};
let lastShownEventId;
for (const event of this.props.events) {
- if (this._shouldShowEvent(event)) {
+ if (this.shouldShowEvent(event)) {
lastShownEventId = event.getId();
}
if (!lastShownEventId) {
@@ -735,7 +774,7 @@ export default class MessagePanel extends React.Component {
}
const existingReceipts = receiptsByEvent[lastShownEventId] || [];
- const newReceipts = this._getReadReceiptsForEvent(event);
+ const newReceipts = this.getReadReceiptsForEvent(event);
receiptsByEvent[lastShownEventId] = existingReceipts.concat(newReceipts);
// Record these receipts along with their last shown event ID for
@@ -754,16 +793,16 @@ export default class MessagePanel extends React.Component {
// someone which had one in the last. By looking through our previous
// mapping of receipts by user ID, we can cover recover any receipts
// that would have been lost by using the same event ID from last time.
- for (const userId in this._readReceiptsByUserId) {
+ for (const userId in this.readReceiptsByUserId) {
if (receiptsByUserId[userId]) {
continue;
}
- const { lastShownEventId, receipt } = this._readReceiptsByUserId[userId];
+ const { lastShownEventId, receipt } = this.readReceiptsByUserId[userId];
const existingReceipts = receiptsByEvent[lastShownEventId] || [];
receiptsByEvent[lastShownEventId] = existingReceipts.concat(receipt);
receiptsByUserId[userId] = { lastShownEventId, receipt };
}
- this._readReceiptsByUserId = receiptsByUserId;
+ this.readReceiptsByUserId = receiptsByUserId;
// After grouping receipts by shown events, do another pass to sort each
// receipt list.
@@ -776,21 +815,21 @@ export default class MessagePanel extends React.Component {
return receiptsByEvent;
}
- _collectEventNode = (eventId, node) => {
+ private collectEventNode = (eventId: string, node: EventTile): void => {
this.eventNodes[eventId] = node?.ref?.current;
}
// once dynamic content in the events load, make the scrollPanel check the
// scroll offsets.
- _onHeightChanged = () => {
- const scrollPanel = this._scrollPanel.current;
+ public onHeightChanged = (): void => {
+ const scrollPanel = this.scrollPanel.current;
if (scrollPanel) {
scrollPanel.checkScroll();
}
};
- _onTypingShown = () => {
- const scrollPanel = this._scrollPanel.current;
+ private onTypingShown = (): void => {
+ const scrollPanel = this.scrollPanel.current;
// this will make the timeline grow, so checkScroll
scrollPanel.checkScroll();
if (scrollPanel && scrollPanel.getScrollState().stuckAtBottom) {
@@ -798,8 +837,8 @@ export default class MessagePanel extends React.Component {
}
};
- _onTypingHidden = () => {
- const scrollPanel = this._scrollPanel.current;
+ private onTypingHidden = (): void => {
+ const scrollPanel = this.scrollPanel.current;
if (scrollPanel) {
// as hiding the typing notifications doesn't
// update the scrollPanel, we tell it to apply
@@ -811,12 +850,12 @@ export default class MessagePanel extends React.Component {
}
};
- updateTimelineMinHeight() {
- const scrollPanel = this._scrollPanel.current;
+ public updateTimelineMinHeight(): void {
+ const scrollPanel = this.scrollPanel.current;
if (scrollPanel) {
const isAtBottom = scrollPanel.isAtBottom();
- const whoIsTyping = this._whoIsTyping.current;
+ const whoIsTyping = this.whoIsTyping.current;
const isTypingVisible = whoIsTyping && whoIsTyping.isVisible();
// when messages get added to the timeline,
// but somebody else is still typing,
@@ -828,18 +867,14 @@ export default class MessagePanel extends React.Component {
}
}
- onTimelineReset() {
- const scrollPanel = this._scrollPanel.current;
+ public onTimelineReset(): void {
+ const scrollPanel = this.scrollPanel.current;
if (scrollPanel) {
scrollPanel.clearPreventShrinking();
}
}
render() {
- const ErrorBoundary = sdk.getComponent('elements.ErrorBoundary');
- const ScrollPanel = sdk.getComponent("structures.ScrollPanel");
- const WhoIsTypingTile = sdk.getComponent("rooms.WhoIsTypingTile");
- const Spinner = sdk.getComponent("elements.Spinner");
let topSpinner;
let bottomSpinner;
if (this.props.backPaginating) {
@@ -855,9 +890,9 @@ export default class MessagePanel extends React.Component {
if (this.props.room && !this.props.tileShape && this.state.showTypingNotifications) {
whoIsTyping = (
+ onShown={this.onTypingShown}
+ onHidden={this.onTypingHidden}
+ ref={this.whoIsTyping} />
);
}
@@ -873,11 +908,10 @@ export default class MessagePanel extends React.Component {
return (
{ topSpinner }
- { this._getEventTiles() }
+ { this.getEventTiles() }
{ whoIsTyping }
{ bottomSpinner }
@@ -895,6 +929,31 @@ export default class MessagePanel extends React.Component {
}
}
+abstract class BaseGrouper {
+ static canStartGroup = (panel: MessagePanel, ev: MatrixEvent): boolean => true;
+
+ public events: MatrixEvent[] = [];
+ // events that we include in the group but then eject out and place above the group.
+ public ejectedEvents: MatrixEvent[] = [];
+ public readMarker: ReactNode;
+
+ constructor(
+ public readonly panel: MessagePanel,
+ public readonly event: MatrixEvent,
+ public readonly prevEvent: MatrixEvent,
+ public readonly lastShownEvent: MatrixEvent,
+ public readonly nextEvent?: MatrixEvent,
+ public readonly nextEventTile?: MatrixEvent,
+ ) {
+ this.readMarker = panel.readMarkerForEvent(event.getId(), event === lastShownEvent);
+ }
+
+ public abstract shouldGroup(ev: MatrixEvent): boolean;
+ public abstract add(ev: MatrixEvent): void;
+ public abstract getTiles(): ReactNode[];
+ public abstract getNewPrevEvent(): MatrixEvent;
+}
+
/* Grouper classes determine when events can be grouped together in a summary.
* Groupers should have the following methods:
* - canStartGroup (static): determines if a new group should be started with the
@@ -910,36 +969,21 @@ export default class MessagePanel extends React.Component {
// Wrap initial room creation events into an EventListSummary
// Grouping only events sent by the same user that sent the `m.room.create` and only until
// the first non-state event or membership event which is not regarding the sender of the `m.room.create` event
-class CreationGrouper {
- static canStartGroup = function(panel, ev) {
- return ev.getType() === "m.room.create";
+class CreationGrouper extends BaseGrouper {
+ static canStartGroup = function(panel: MessagePanel, ev: MatrixEvent): boolean {
+ return ev.getType() === EventType.RoomCreate;
};
- constructor(panel, createEvent, prevEvent, lastShownEvent) {
- this.panel = panel;
- this.createEvent = createEvent;
- this.prevEvent = prevEvent;
- this.lastShownEvent = lastShownEvent;
- this.events = [];
- // events that we include in the group but then eject out and place
- // above the group.
- this.ejectedEvents = [];
- this.readMarker = panel._readMarkerForEvent(
- createEvent.getId(),
- createEvent === lastShownEvent,
- );
- }
-
- shouldGroup(ev) {
+ public shouldGroup(ev: MatrixEvent): boolean {
const panel = this.panel;
- const createEvent = this.createEvent;
- if (!panel._shouldShowEvent(ev)) {
+ const createEvent = this.event;
+ if (!panel.shouldShowEvent(ev)) {
return true;
}
- if (panel._wantsDateSeparator(this.createEvent, ev.getDate())) {
+ if (panel.wantsDateSeparator(this.event, ev.getDate())) {
return false;
}
- if (ev.getType() === "m.room.member"
+ if (ev.getType() === EventType.RoomMember
&& (ev.getStateKey() !== createEvent.getSender() || ev.getContent()["membership"] !== "join")) {
return false;
}
@@ -949,37 +993,35 @@ class CreationGrouper {
return false;
}
- add(ev) {
+ public add(ev: MatrixEvent): void {
const panel = this.panel;
- this.readMarker = this.readMarker || panel._readMarkerForEvent(
+ this.readMarker = this.readMarker || panel.readMarkerForEvent(
ev.getId(),
ev === this.lastShownEvent,
);
- if (!panel._shouldShowEvent(ev)) {
+ if (!panel.shouldShowEvent(ev)) {
return;
}
- if (ev.getType() === "m.room.encryption") {
+ if (ev.getType() === EventType.RoomEncryption) {
this.ejectedEvents.push(ev);
} else {
this.events.push(ev);
}
}
- getTiles() {
+ public getTiles(): ReactNode[] {
// If we don't have any events to group, don't even try to group them. The logic
// below assumes that we have a group of events to deal with, but we might not if
// the events we were supposed to group were redacted.
if (!this.events || !this.events.length) return [];
- const DateSeparator = sdk.getComponent('messages.DateSeparator');
- const EventListSummary = sdk.getComponent('views.elements.EventListSummary');
const panel = this.panel;
const ret = [];
const isGrouped = true;
- const createEvent = this.createEvent;
+ const createEvent = this.event;
const lastShownEvent = this.lastShownEvent;
- if (panel._wantsDateSeparator(this.prevEvent, createEvent.getDate())) {
+ if (panel.wantsDateSeparator(this.prevEvent, createEvent.getDate())) {
const ts = createEvent.getTs();
ret.push(
,
@@ -987,13 +1029,13 @@ class CreationGrouper {
}
// If this m.room.create event should be shown (room upgrade) then show it before the summary
- if (panel._shouldShowEvent(createEvent)) {
+ if (panel.shouldShowEvent(createEvent)) {
// pass in the createEvent as prevEvent as well so no extra DateSeparator is rendered
- ret.push(...panel._getTilesForEvent(createEvent, createEvent));
+ ret.push(...panel.getTilesForEvent(createEvent, createEvent));
}
for (const ejected of this.ejectedEvents) {
- ret.push(...panel._getTilesForEvent(
+ ret.push(...panel.getTilesForEvent(
createEvent, ejected, createEvent === lastShownEvent, isGrouped,
));
}
@@ -1003,7 +1045,7 @@ class CreationGrouper {
// of EventListSummary, render each member event as if the previous
// one was itself. This way, the timestamp of the previous event === the
// timestamp of the current event, and no DateSeparator is inserted.
- return panel._getTilesForEvent(e, e, e === lastShownEvent, isGrouped);
+ return panel.getTilesForEvent(e, e, e === lastShownEvent, isGrouped);
}).reduce((a, b) => a.concat(b), []);
// Get sender profile from the latest event in the summary as the m.room.create doesn't contain one
const ev = this.events[this.events.length - 1];
@@ -1023,7 +1065,7 @@ class CreationGrouper {
@@ -1038,62 +1080,59 @@ class CreationGrouper {
return ret;
}
- getNewPrevEvent() {
- return this.createEvent;
+ public getNewPrevEvent(): MatrixEvent {
+ return this.event;
}
}
-class RedactionGrouper {
- static canStartGroup = function(panel, ev) {
- return panel._shouldShowEvent(ev) && ev.isRedacted();
+class RedactionGrouper extends BaseGrouper {
+ static canStartGroup = function(panel: MessagePanel, ev: MatrixEvent): boolean {
+ return panel.shouldShowEvent(ev) && ev.isRedacted();
}
- constructor(panel, ev, prevEvent, lastShownEvent, nextEvent, nextEventTile) {
- this.panel = panel;
- this.readMarker = panel._readMarkerForEvent(
- ev.getId(),
- ev === lastShownEvent,
- );
+ constructor(
+ panel: MessagePanel,
+ ev: MatrixEvent,
+ prevEvent: MatrixEvent,
+ lastShownEvent: MatrixEvent,
+ nextEvent: MatrixEvent,
+ nextEventTile: MatrixEvent,
+ ) {
+ super(panel, ev, prevEvent, lastShownEvent, nextEvent, nextEventTile);
this.events = [ev];
- this.prevEvent = prevEvent;
- this.lastShownEvent = lastShownEvent;
- this.nextEvent = nextEvent;
- this.nextEventTile = nextEventTile;
}
- shouldGroup(ev) {
+ public shouldGroup(ev: MatrixEvent): boolean {
// absorb hidden events so that they do not break up streams of messages & redaction events being grouped
- if (!this.panel._shouldShowEvent(ev)) {
+ if (!this.panel.shouldShowEvent(ev)) {
return true;
}
- if (this.panel._wantsDateSeparator(this.events[0], ev.getDate())) {
+ if (this.panel.wantsDateSeparator(this.events[0], ev.getDate())) {
return false;
}
return ev.isRedacted();
}
- add(ev) {
- this.readMarker = this.readMarker || this.panel._readMarkerForEvent(
+ public add(ev: MatrixEvent): void {
+ this.readMarker = this.readMarker || this.panel.readMarkerForEvent(
ev.getId(),
ev === this.lastShownEvent,
);
- if (!this.panel._shouldShowEvent(ev)) {
+ if (!this.panel.shouldShowEvent(ev)) {
return;
}
this.events.push(ev);
}
- getTiles() {
+ public getTiles(): ReactNode[] {
if (!this.events || !this.events.length) return [];
- const DateSeparator = sdk.getComponent('messages.DateSeparator');
- const EventListSummary = sdk.getComponent('views.elements.EventListSummary');
const isGrouped = true;
const panel = this.panel;
const ret = [];
const lastShownEvent = this.lastShownEvent;
- if (panel._wantsDateSeparator(this.prevEvent, this.events[0].getDate())) {
+ if (panel.wantsDateSeparator(this.prevEvent, this.events[0].getDate())) {
const ts = this.events[0].getTs();
ret.push(
,
@@ -1104,11 +1143,11 @@ class RedactionGrouper {
this.prevEvent ? this.events[0].getId() : "initial"
);
- const senders = new Set();
+ const senders = new Set();
let eventTiles = this.events.map((e, i) => {
senders.add(e.sender);
const prevEvent = i === 0 ? this.prevEvent : this.events[i - 1];
- return panel._getTilesForEvent(
+ return panel.getTilesForEvent(
prevEvent, e, e === lastShownEvent, isGrouped, this.nextEvent, this.nextEventTile);
}).reduce((a, b) => a.concat(b), []);
@@ -1121,7 +1160,7 @@ class RedactionGrouper {
key={key}
threshold={2}
events={this.events}
- onToggle={panel._onHeightChanged} // Update scroll state
+ onToggle={panel.onHeightChanged} // Update scroll state
summaryMembers={Array.from(senders)}
summaryText={_t("%(count)s messages deleted.", { count: eventTiles.length })}
>
@@ -1136,61 +1175,58 @@ class RedactionGrouper {
return ret;
}
- getNewPrevEvent() {
+ public getNewPrevEvent(): MatrixEvent {
return this.events[this.events.length - 1];
}
}
// Wrap consecutive member events in a ListSummary, ignore if redacted
-class MemberGrouper {
- static canStartGroup = function(panel, ev) {
- return panel._shouldShowEvent(ev) && isMembershipChange(ev);
+class MemberGrouper extends BaseGrouper {
+ static canStartGroup = function(panel: MessagePanel, ev: MatrixEvent): boolean {
+ return panel.shouldShowEvent(ev) && isMembershipChange(ev);
}
- constructor(panel, ev, prevEvent, lastShownEvent) {
- this.panel = panel;
- this.readMarker = panel._readMarkerForEvent(
- ev.getId(),
- ev === lastShownEvent,
- );
- this.events = [ev];
- this.prevEvent = prevEvent;
- this.lastShownEvent = lastShownEvent;
+ constructor(
+ public readonly panel: MessagePanel,
+ public readonly event: MatrixEvent,
+ public readonly prevEvent: MatrixEvent,
+ public readonly lastShownEvent: MatrixEvent,
+ ) {
+ super(panel, event, prevEvent, lastShownEvent);
+ this.events = [event];
}
- shouldGroup(ev) {
- if (this.panel._wantsDateSeparator(this.events[0], ev.getDate())) {
+ public shouldGroup(ev: MatrixEvent): boolean {
+ if (this.panel.wantsDateSeparator(this.events[0], ev.getDate())) {
return false;
}
return isMembershipChange(ev);
}
- add(ev) {
- if (ev.getType() === 'm.room.member') {
+ public add(ev: MatrixEvent): void {
+ if (ev.getType() === EventType.RoomMember) {
// We can ignore any events that don't actually have a message to display
if (!hasText(ev)) return;
}
- this.readMarker = this.readMarker || this.panel._readMarkerForEvent(
+ this.readMarker = this.readMarker || this.panel.readMarkerForEvent(
ev.getId(),
ev === this.lastShownEvent,
);
this.events.push(ev);
}
- getTiles() {
+ public getTiles(): ReactNode[] {
// If we don't have any events to group, don't even try to group them. The logic
// below assumes that we have a group of events to deal with, but we might not if
// the events we were supposed to group were redacted.
if (!this.events || !this.events.length) return [];
- const DateSeparator = sdk.getComponent('messages.DateSeparator');
- const MemberEventListSummary = sdk.getComponent('views.elements.MemberEventListSummary');
const isGrouped = true;
const panel = this.panel;
const lastShownEvent = this.lastShownEvent;
const ret = [];
- if (panel._wantsDateSeparator(this.prevEvent, this.events[0].getDate())) {
+ if (panel.wantsDateSeparator(this.prevEvent, this.events[0].getDate())) {
const ts = this.events[0].getTs();
ret.push(
,
@@ -1218,7 +1254,7 @@ class MemberGrouper {
// of MemberEventListSummary, render each member event as if the previous
// one was itself. This way, the timestamp of the previous event === the
// timestamp of the current event, and no DateSeparator is inserted.
- return panel._getTilesForEvent(e, e, e === lastShownEvent, isGrouped);
+ return panel.getTilesForEvent(e, e, e === lastShownEvent, isGrouped);
}).reduce((a, b) => a.concat(b), []);
if (eventTiles.length === 0) {
@@ -1226,9 +1262,10 @@ class MemberGrouper {
}
ret.push(
-
{ eventTiles }
@@ -1242,7 +1279,7 @@ class MemberGrouper {
return ret;
}
- getNewPrevEvent() {
+ public getNewPrevEvent(): MatrixEvent {
return this.events[0];
}
}
diff --git a/src/components/structures/NotificationPanel.tsx b/src/components/structures/NotificationPanel.tsx
index 6c22835447..8c8fab7ece 100644
--- a/src/components/structures/NotificationPanel.tsx
+++ b/src/components/structures/NotificationPanel.tsx
@@ -22,6 +22,7 @@ import BaseCard from "../views/right_panel/BaseCard";
import { replaceableComponent } from "../../utils/replaceableComponent";
import TimelinePanel from "./TimelinePanel";
import Spinner from "../views/elements/Spinner";
+import { TileShape } from "../views/rooms/EventTile";
interface IProps {
onClose(): void;
@@ -48,7 +49,7 @@ export default class NotificationPanel extends React.PureComponent {
manageReadMarkers={false}
timelineSet={timelineSet}
showUrlPreview={false}
- tileShape="notif"
+ tileShape={TileShape.Notif}
empty={emptyState}
alwaysShowTimestamps={true}
/>
diff --git a/src/components/structures/RoomDirectory.tsx b/src/components/structures/RoomDirectory.tsx
index 1e0605f263..7770b32f04 100644
--- a/src/components/structures/RoomDirectory.tsx
+++ b/src/components/structures/RoomDirectory.tsx
@@ -207,9 +207,9 @@ export default class RoomDirectory extends React.Component {
this.getMoreRooms();
};
- private getMoreRooms() {
- if (this.state.selectedCommunityId) return Promise.resolve(); // no more rooms
- if (!MatrixClientPeg.get()) return Promise.resolve();
+ private getMoreRooms(): Promise {
+ if (this.state.selectedCommunityId) return Promise.resolve(false); // no more rooms
+ if (!MatrixClientPeg.get()) return Promise.resolve(false);
this.setState({
loading: true,
@@ -239,12 +239,12 @@ export default class RoomDirectory extends React.Component {
// if the filter or server has changed since this request was sent,
// throw away the result (don't even clear the busy flag
// since we must still have a request in flight)
- return;
+ return false;
}
if (this.unmounted) {
// if we've been unmounted, we don't care either.
- return;
+ return false;
}
if (this.state.filterString) {
@@ -264,14 +264,13 @@ export default class RoomDirectory extends React.Component {
filterString != this.state.filterString ||
roomServer != this.state.roomServer ||
nextBatch != this.nextBatch) {
- // as above: we don't care about errors for old
- // requests either
- return;
+ // as above: we don't care about errors for old requests either
+ return false;
}
if (this.unmounted) {
// if we've been unmounted, we don't care either.
- return;
+ return false;
}
console.error("Failed to get publicRooms: %s", JSON.stringify(err));
diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx
index 885851e8e6..338da29875 100644
--- a/src/components/structures/RoomView.tsx
+++ b/src/components/structures/RoomView.tsx
@@ -1125,7 +1125,7 @@ export default class RoomView extends React.Component {
}
}
- private onSearchResultsFillRequest = (backwards: boolean) => {
+ private onSearchResultsFillRequest = (backwards: boolean): Promise => {
if (!backwards) {
return Promise.resolve(false);
}
@@ -1291,7 +1291,7 @@ export default class RoomView extends React.Component {
this.handleSearchResult(searchPromise);
};
- private handleSearchResult(searchPromise: Promise) {
+ private handleSearchResult(searchPromise: Promise): Promise {
// keep a record of the current search id, so that if the search terms
// change before we get a response, we can ignore the results.
const localSearchId = this.searchId;
@@ -1304,7 +1304,7 @@ export default class RoomView extends React.Component {
debuglog("search complete");
if (this.unmounted || !this.state.searching || this.searchId != localSearchId) {
console.error("Discarding stale search results");
- return;
+ return false;
}
// postgres on synapse returns us precise details of the strings
@@ -1336,6 +1336,7 @@ export default class RoomView extends React.Component {
description: ((error && error.message) ? error.message :
_t("Server may be unavailable, overloaded, or search timed out :(")),
});
+ return false;
}).finally(() => {
this.setState({
searchInProgress: false,
diff --git a/src/components/structures/ScrollPanel.js b/src/components/structures/ScrollPanel.tsx
similarity index 73%
rename from src/components/structures/ScrollPanel.js
rename to src/components/structures/ScrollPanel.tsx
index f6e1530537..b8e0cdbc34 100644
--- a/src/components/structures/ScrollPanel.js
+++ b/src/components/structures/ScrollPanel.tsx
@@ -1,5 +1,5 @@
/*
-Copyright 2015, 2016 OpenMarket Ltd
+Copyright 2015 - 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -14,17 +14,18 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import React, {createRef} from "react";
-import PropTypes from 'prop-types';
+import React, { createRef, CSSProperties, ReactNode, SyntheticEvent, KeyboardEvent } from "react";
+
import Timer from '../../utils/Timer';
import AutoHideScrollbar from "./AutoHideScrollbar";
-import {replaceableComponent} from "../../utils/replaceableComponent";
-import {getKeyBindingsManager, RoomAction} from "../../KeyBindingsManager";
+import { replaceableComponent } from "../../utils/replaceableComponent";
+import { getKeyBindingsManager, RoomAction } from "../../KeyBindingsManager";
+import ResizeNotifier from "../../utils/ResizeNotifier";
const DEBUG_SCROLL = false;
// The amount of extra scroll distance to allow prior to unfilling.
-// See _getExcessHeight.
+// See getExcessHeight.
const UNPAGINATION_PADDING = 6000;
// The number of milliseconds to debounce calls to onUnfillRequest, to prevent
// many scroll events causing many unfilling requests.
@@ -43,6 +44,75 @@ if (DEBUG_SCROLL) {
debuglog = function() {};
}
+interface IProps {
+ /* stickyBottom: if set to true, then once the user hits the bottom of
+ * the list, any new children added to the list will cause the list to
+ * scroll down to show the new element, rather than preserving the
+ * existing view.
+ */
+ stickyBottom?: boolean;
+
+ /* startAtBottom: if set to true, the view is assumed to start
+ * scrolled to the bottom.
+ * XXX: It's likely this is unnecessary and can be derived from
+ * stickyBottom, but I'm adding an extra parameter to ensure
+ * behaviour stays the same for other uses of ScrollPanel.
+ * If so, let's remove this parameter down the line.
+ */
+ startAtBottom?: boolean;
+
+ /* className: classnames to add to the top-level div
+ */
+ className?: string;
+
+ /* style: styles to add to the top-level div
+ */
+ style?: CSSProperties;
+
+ /* resizeNotifier: ResizeNotifier to know when middle column has changed size
+ */
+ resizeNotifier?: ResizeNotifier;
+
+ /* fixedChildren: allows for children to be passed which are rendered outside
+ * of the wrapper
+ */
+ fixedChildren?: ReactNode;
+
+ /* onFillRequest(backwards): a callback which is called on scroll when
+ * the user nears the start (backwards = true) or end (backwards =
+ * false) of the list.
+ *
+ * This should return a promise; no more calls will be made until the
+ * promise completes.
+ *
+ * The promise should resolve to true if there is more data to be
+ * retrieved in this direction (in which case onFillRequest may be
+ * called again immediately), or false if there is no more data in this
+ * directon (at this time) - which will stop the pagination cycle until
+ * the user scrolls again.
+ */
+ onFillRequest?(backwards: boolean): Promise;
+
+ /* onUnfillRequest(backwards): a callback which is called on scroll when
+ * there are children elements that are far out of view and could be removed
+ * without causing pagination to occur.
+ *
+ * This function should accept a boolean, which is true to indicate the back/top
+ * of the panel and false otherwise, and a scroll token, which refers to the
+ * first element to remove if removing from the front/bottom, and last element
+ * to remove if removing from the back/top.
+ */
+ onUnfillRequest?(backwards: boolean, scrollToken: string): void;
+
+ /* onScroll: a callback which is called whenever any scroll happens.
+ */
+ onScroll?(event: Event): void;
+
+ /* onUserScroll: callback which is called when the user interacts with the room timeline
+ */
+ onUserScroll?(event: SyntheticEvent): void;
+}
+
/* This component implements an intelligent scrolling list.
*
* It wraps a list of children; when items are added to the start or end
@@ -84,97 +154,54 @@ if (DEBUG_SCROLL) {
* offset as normal.
*/
+export interface IScrollState {
+ stuckAtBottom: boolean;
+ trackedNode?: HTMLElement;
+ trackedScrollToken?: string;
+ bottomOffset?: number;
+ pixelOffset?: number;
+}
+
+interface IPreventShrinkingState {
+ offsetFromBottom: number;
+ offsetNode: HTMLElement;
+}
+
@replaceableComponent("structures.ScrollPanel")
-export default class ScrollPanel extends React.Component {
- static propTypes = {
- /* stickyBottom: if set to true, then once the user hits the bottom of
- * the list, any new children added to the list will cause the list to
- * scroll down to show the new element, rather than preserving the
- * existing view.
- */
- stickyBottom: PropTypes.bool,
-
- /* startAtBottom: if set to true, the view is assumed to start
- * scrolled to the bottom.
- * XXX: It's likely this is unnecessary and can be derived from
- * stickyBottom, but I'm adding an extra parameter to ensure
- * behaviour stays the same for other uses of ScrollPanel.
- * If so, let's remove this parameter down the line.
- */
- startAtBottom: PropTypes.bool,
-
- /* onFillRequest(backwards): a callback which is called on scroll when
- * the user nears the start (backwards = true) or end (backwards =
- * false) of the list.
- *
- * This should return a promise; no more calls will be made until the
- * promise completes.
- *
- * The promise should resolve to true if there is more data to be
- * retrieved in this direction (in which case onFillRequest may be
- * called again immediately), or false if there is no more data in this
- * directon (at this time) - which will stop the pagination cycle until
- * the user scrolls again.
- */
- onFillRequest: PropTypes.func,
-
- /* onUnfillRequest(backwards): a callback which is called on scroll when
- * there are children elements that are far out of view and could be removed
- * without causing pagination to occur.
- *
- * This function should accept a boolean, which is true to indicate the back/top
- * of the panel and false otherwise, and a scroll token, which refers to the
- * first element to remove if removing from the front/bottom, and last element
- * to remove if removing from the back/top.
- */
- onUnfillRequest: PropTypes.func,
-
- /* onScroll: a callback which is called whenever any scroll happens.
- */
- onScroll: PropTypes.func,
-
- /* onUserScroll: callback which is called when the user interacts with the room timeline
- */
- onUserScroll: PropTypes.func,
-
- /* className: classnames to add to the top-level div
- */
- className: PropTypes.string,
-
- /* style: styles to add to the top-level div
- */
- style: PropTypes.object,
-
- /* resizeNotifier: ResizeNotifier to know when middle column has changed size
- */
- resizeNotifier: PropTypes.object,
-
- /* fixedChildren: allows for children to be passed which are rendered outside
- * of the wrapper
- */
- fixedChildren: PropTypes.node,
- };
-
+export default class ScrollPanel extends React.Component {
static defaultProps = {
stickyBottom: true,
startAtBottom: true,
- onFillRequest: function(backwards) { return Promise.resolve(false); },
- onUnfillRequest: function(backwards, scrollToken) {},
+ onFillRequest: function(backwards: boolean) { return Promise.resolve(false); },
+ onUnfillRequest: function(backwards: boolean, scrollToken: string) {},
onScroll: function() {},
};
- constructor(props) {
- super(props);
+ private readonly pendingFillRequests: Record<"b" | "f", boolean> = {
+ b: null,
+ f: null,
+ };
+ private readonly itemlist = createRef();
+ private unmounted = false;
+ private scrollTimeout: Timer;
+ private isFilling: boolean;
+ private fillRequestWhileRunning: boolean;
+ private scrollState: IScrollState;
+ private preventShrinkingState: IPreventShrinkingState;
+ private unfillDebouncer: NodeJS.Timeout;
+ private bottomGrowth: number;
+ private pages: number;
+ private heightUpdateInProgress: boolean;
+ private divScroll: HTMLDivElement;
- this._pendingFillRequests = {b: null, f: null};
+ constructor(props, context) {
+ super(props, context);
if (this.props.resizeNotifier) {
this.props.resizeNotifier.on("middlePanelResizedNoisy", this.onResize);
}
this.resetScrollState();
-
- this._itemlist = createRef();
}
componentDidMount() {
@@ -203,18 +230,18 @@ export default class ScrollPanel extends React.Component {
}
}
- onScroll = ev => {
+ private onScroll = ev => {
// skip scroll events caused by resizing
if (this.props.resizeNotifier && this.props.resizeNotifier.isResizing) return;
- debuglog("onScroll", this._getScrollNode().scrollTop);
- this._scrollTimeout.restart();
- this._saveScrollState();
+ debuglog("onScroll", this.getScrollNode().scrollTop);
+ this.scrollTimeout.restart();
+ this.saveScrollState();
this.updatePreventShrinking();
this.props.onScroll(ev);
this.checkFillState();
};
- onResize = () => {
+ private onResize = () => {
debuglog("onResize");
this.checkScroll();
// update preventShrinkingState if present
@@ -225,11 +252,11 @@ export default class ScrollPanel extends React.Component {
// after an update to the contents of the panel, check that the scroll is
// where it ought to be, and set off pagination requests if necessary.
- checkScroll = () => {
+ public checkScroll = () => {
if (this.unmounted) {
return;
}
- this._restoreSavedScrollState();
+ this.restoreSavedScrollState();
this.checkFillState();
};
@@ -238,8 +265,8 @@ export default class ScrollPanel extends React.Component {
// note that this is independent of the 'stuckAtBottom' state - it is simply
// about whether the content is scrolled down right now, irrespective of
// whether it will stay that way when the children update.
- isAtBottom = () => {
- const sn = this._getScrollNode();
+ public isAtBottom = () => {
+ const sn = this.getScrollNode();
// fractional values (both too big and too small)
// for scrollTop happen on certain browsers/platforms
// when scrolled all the way down. E.g. Chrome 72 on debian.
@@ -278,10 +305,10 @@ export default class ScrollPanel extends React.Component {
// |#########| - |
// |#########| |
// `---------' -
- _getExcessHeight(backwards) {
- const sn = this._getScrollNode();
- const contentHeight = this._getMessagesHeight();
- const listHeight = this._getListHeight();
+ private getExcessHeight(backwards: boolean): number {
+ const sn = this.getScrollNode();
+ const contentHeight = this.getMessagesHeight();
+ const listHeight = this.getListHeight();
const clippedHeight = contentHeight - listHeight;
const unclippedScrollTop = sn.scrollTop + clippedHeight;
@@ -293,13 +320,13 @@ export default class ScrollPanel extends React.Component {
}
// check the scroll state and send out backfill requests if necessary.
- checkFillState = async (depth=0) => {
+ public checkFillState = async (depth = 0): Promise => {
if (this.unmounted) {
return;
}
const isFirstCall = depth === 0;
- const sn = this._getScrollNode();
+ const sn = this.getScrollNode();
// if there is less than a screenful of messages above or below the
// viewport, try to get some more messages.
@@ -330,17 +357,17 @@ export default class ScrollPanel extends React.Component {
// do make a note when a new request comes in while already running one,
// so we can trigger a new chain of calls once done.
if (isFirstCall) {
- if (this._isFilling) {
- debuglog("_isFilling: not entering while request is ongoing, marking for a subsequent request");
- this._fillRequestWhileRunning = true;
+ if (this.isFilling) {
+ debuglog("isFilling: not entering while request is ongoing, marking for a subsequent request");
+ this.fillRequestWhileRunning = true;
return;
}
- debuglog("_isFilling: setting");
- this._isFilling = true;
+ debuglog("isFilling: setting");
+ this.isFilling = true;
}
- const itemlist = this._itemlist.current;
- const firstTile = itemlist && itemlist.firstElementChild;
+ const itemlist = this.itemlist.current;
+ const firstTile = itemlist && itemlist.firstElementChild as HTMLElement;
const contentTop = firstTile && firstTile.offsetTop;
const fillPromises = [];
@@ -348,13 +375,13 @@ export default class ScrollPanel extends React.Component {
// try backward filling
if (!firstTile || (sn.scrollTop - contentTop) < sn.clientHeight) {
// need to back-fill
- fillPromises.push(this._maybeFill(depth, true));
+ fillPromises.push(this.maybeFill(depth, true));
}
// if scrollTop gets to 2 screens from the end (so 1 screen below viewport),
// try forward filling
if ((sn.scrollHeight - sn.scrollTop) < sn.clientHeight * 2) {
// need to forward-fill
- fillPromises.push(this._maybeFill(depth, false));
+ fillPromises.push(this.maybeFill(depth, false));
}
if (fillPromises.length) {
@@ -365,26 +392,26 @@ export default class ScrollPanel extends React.Component {
}
}
if (isFirstCall) {
- debuglog("_isFilling: clearing");
- this._isFilling = false;
+ debuglog("isFilling: clearing");
+ this.isFilling = false;
}
- if (this._fillRequestWhileRunning) {
- this._fillRequestWhileRunning = false;
+ if (this.fillRequestWhileRunning) {
+ this.fillRequestWhileRunning = false;
this.checkFillState();
}
};
// check if unfilling is possible and send an unfill request if necessary
- _checkUnfillState(backwards) {
- let excessHeight = this._getExcessHeight(backwards);
+ private checkUnfillState(backwards: boolean): void {
+ let excessHeight = this.getExcessHeight(backwards);
if (excessHeight <= 0) {
return;
}
const origExcessHeight = excessHeight;
- const tiles = this._itemlist.current.children;
+ const tiles = this.itemlist.current.children;
// The scroll token of the first/last tile to be unpaginated
let markerScrollToken = null;
@@ -413,11 +440,11 @@ export default class ScrollPanel extends React.Component {
if (markerScrollToken) {
// Use a debouncer to prevent multiple unfill calls in quick succession
// This is to make the unfilling process less aggressive
- if (this._unfillDebouncer) {
- clearTimeout(this._unfillDebouncer);
+ if (this.unfillDebouncer) {
+ clearTimeout(this.unfillDebouncer);
}
- this._unfillDebouncer = setTimeout(() => {
- this._unfillDebouncer = null;
+ this.unfillDebouncer = setTimeout(() => {
+ this.unfillDebouncer = null;
debuglog("unfilling now", backwards, origExcessHeight);
this.props.onUnfillRequest(backwards, markerScrollToken);
}, UNFILL_REQUEST_DEBOUNCE_MS);
@@ -425,9 +452,9 @@ export default class ScrollPanel extends React.Component {
}
// check if there is already a pending fill request. If not, set one off.
- _maybeFill(depth, backwards) {
+ private maybeFill(depth: number, backwards: boolean): Promise {
const dir = backwards ? 'b' : 'f';
- if (this._pendingFillRequests[dir]) {
+ if (this.pendingFillRequests[dir]) {
debuglog("Already a "+dir+" fill in progress - not starting another");
return;
}
@@ -436,7 +463,7 @@ export default class ScrollPanel extends React.Component {
// onFillRequest can end up calling us recursively (via onScroll
// events) so make sure we set this before firing off the call.
- this._pendingFillRequests[dir] = true;
+ this.pendingFillRequests[dir] = true;
// wait 1ms before paginating, because otherwise
// this will block the scroll event handler for +700ms
@@ -445,13 +472,13 @@ export default class ScrollPanel extends React.Component {
return new Promise(resolve => setTimeout(resolve, 1)).then(() => {
return this.props.onFillRequest(backwards);
}).finally(() => {
- this._pendingFillRequests[dir] = false;
+ this.pendingFillRequests[dir] = false;
}).then((hasMoreResults) => {
if (this.unmounted) {
return;
}
// Unpaginate once filling is complete
- this._checkUnfillState(!backwards);
+ this.checkUnfillState(!backwards);
debuglog(""+dir+" fill complete; hasMoreResults:"+hasMoreResults);
if (hasMoreResults) {
@@ -477,7 +504,7 @@ export default class ScrollPanel extends React.Component {
* the number of pixels the bottom of the tracked child is above the
* bottom of the scroll panel.
*/
- getScrollState = () => this.scrollState;
+ public getScrollState = (): IScrollState => this.scrollState;
/* reset the saved scroll state.
*
@@ -491,35 +518,35 @@ export default class ScrollPanel extends React.Component {
* no use if no children exist yet, or if you are about to replace the
* child list.)
*/
- resetScrollState = () => {
+ public resetScrollState = (): void => {
this.scrollState = {
stuckAtBottom: this.props.startAtBottom,
};
- this._bottomGrowth = 0;
- this._pages = 0;
- this._scrollTimeout = new Timer(100);
- this._heightUpdateInProgress = false;
+ this.bottomGrowth = 0;
+ this.pages = 0;
+ this.scrollTimeout = new Timer(100);
+ this.heightUpdateInProgress = false;
};
/**
* jump to the top of the content.
*/
- scrollToTop = () => {
- this._getScrollNode().scrollTop = 0;
- this._saveScrollState();
+ public scrollToTop = (): void => {
+ this.getScrollNode().scrollTop = 0;
+ this.saveScrollState();
};
/**
* jump to the bottom of the content.
*/
- scrollToBottom = () => {
+ public scrollToBottom = (): void => {
// the easiest way to make sure that the scroll state is correctly
// saved is to do the scroll, then save the updated state. (Calculating
// it ourselves is hard, and we can't rely on an onScroll callback
// happening, since there may be no user-visible change here).
- const sn = this._getScrollNode();
+ const sn = this.getScrollNode();
sn.scrollTop = sn.scrollHeight;
- this._saveScrollState();
+ this.saveScrollState();
};
/**
@@ -527,18 +554,18 @@ export default class ScrollPanel extends React.Component {
*
* @param {number} mult: -1 to page up, +1 to page down
*/
- scrollRelative = mult => {
- const scrollNode = this._getScrollNode();
+ public scrollRelative = (mult: number): void => {
+ const scrollNode = this.getScrollNode();
const delta = mult * scrollNode.clientHeight * 0.9;
scrollNode.scrollBy(0, delta);
- this._saveScrollState();
+ this.saveScrollState();
};
/**
* Scroll up/down in response to a scroll key
* @param {object} ev the keyboard event
*/
- handleScrollKey = ev => {
+ public handleScrollKey = (ev: KeyboardEvent) => {
let isScrolling = false;
const roomAction = getKeyBindingsManager().getRoomAction(ev);
switch (roomAction) {
@@ -575,17 +602,17 @@ export default class ScrollPanel extends React.Component {
* node (specifically, the bottom of it) will be positioned. If omitted, it
* defaults to 0.
*/
- scrollToToken = (scrollToken, pixelOffset, offsetBase) => {
+ public scrollToToken = (scrollToken: string, pixelOffset: number, offsetBase: number): void => {
pixelOffset = pixelOffset || 0;
offsetBase = offsetBase || 0;
- // set the trackedScrollToken so we can get the node through _getTrackedNode
+ // set the trackedScrollToken so we can get the node through getTrackedNode
this.scrollState = {
stuckAtBottom: false,
trackedScrollToken: scrollToken,
};
- const trackedNode = this._getTrackedNode();
- const scrollNode = this._getScrollNode();
+ const trackedNode = this.getTrackedNode();
+ const scrollNode = this.getScrollNode();
if (trackedNode) {
// set the scrollTop to the position we want.
// note though, that this might not succeed if the combination of offsetBase and pixelOffset
@@ -595,34 +622,34 @@ export default class ScrollPanel extends React.Component {
// enough so it ends up in the top of the viewport.
debuglog("scrollToken: setting scrollTop", {offsetBase, pixelOffset, offsetTop: trackedNode.offsetTop});
scrollNode.scrollTop = (trackedNode.offsetTop - (scrollNode.clientHeight * offsetBase)) + pixelOffset;
- this._saveScrollState();
+ this.saveScrollState();
}
};
- _saveScrollState() {
+ private saveScrollState(): void {
if (this.props.stickyBottom && this.isAtBottom()) {
this.scrollState = { stuckAtBottom: true };
debuglog("saved stuckAtBottom state");
return;
}
- const scrollNode = this._getScrollNode();
+ const scrollNode = this.getScrollNode();
const viewportBottom = scrollNode.scrollHeight - (scrollNode.scrollTop + scrollNode.clientHeight);
- const itemlist = this._itemlist.current;
+ const itemlist = this.itemlist.current;
const messages = itemlist.children;
let node = null;
// TODO: do a binary search here, as items are sorted by offsetTop
// loop backwards, from bottom-most message (as that is the most common case)
- for (let i = messages.length-1; i >= 0; --i) {
- if (!messages[i].dataset.scrollTokens) {
+ for (let i = messages.length - 1; i >= 0; --i) {
+ if (!(messages[i] as HTMLElement).dataset.scrollTokens) {
continue;
}
node = messages[i];
// break at the first message (coming from the bottom)
// that has it's offsetTop above the bottom of the viewport.
- if (this._topFromBottom(node) > viewportBottom) {
+ if (this.topFromBottom(node) > viewportBottom) {
// Use this node as the scrollToken
break;
}
@@ -634,7 +661,7 @@ export default class ScrollPanel extends React.Component {
}
const scrollToken = node.dataset.scrollTokens.split(',')[0];
debuglog("saving anchored scroll state to message", node && node.innerText, scrollToken);
- const bottomOffset = this._topFromBottom(node);
+ const bottomOffset = this.topFromBottom(node);
this.scrollState = {
stuckAtBottom: false,
trackedNode: node,
@@ -644,35 +671,35 @@ export default class ScrollPanel extends React.Component {
};
}
- async _restoreSavedScrollState() {
+ private async restoreSavedScrollState(): Promise {
const scrollState = this.scrollState;
if (scrollState.stuckAtBottom) {
- const sn = this._getScrollNode();
+ const sn = this.getScrollNode();
if (sn.scrollTop !== sn.scrollHeight) {
sn.scrollTop = sn.scrollHeight;
}
} else if (scrollState.trackedScrollToken) {
- const itemlist = this._itemlist.current;
- const trackedNode = this._getTrackedNode();
+ const itemlist = this.itemlist.current;
+ const trackedNode = this.getTrackedNode();
if (trackedNode) {
- const newBottomOffset = this._topFromBottom(trackedNode);
+ const newBottomOffset = this.topFromBottom(trackedNode);
const bottomDiff = newBottomOffset - scrollState.bottomOffset;
- this._bottomGrowth += bottomDiff;
+ this.bottomGrowth += bottomDiff;
scrollState.bottomOffset = newBottomOffset;
- const newHeight = `${this._getListHeight()}px`;
+ const newHeight = `${this.getListHeight()}px`;
if (itemlist.style.height !== newHeight) {
itemlist.style.height = newHeight;
}
debuglog("balancing height because messages below viewport grew by", bottomDiff);
}
}
- if (!this._heightUpdateInProgress) {
- this._heightUpdateInProgress = true;
+ if (!this.heightUpdateInProgress) {
+ this.heightUpdateInProgress = true;
try {
- await this._updateHeight();
+ await this.updateHeight();
} finally {
- this._heightUpdateInProgress = false;
+ this.heightUpdateInProgress = false;
}
} else {
debuglog("not updating height because request already in progress");
@@ -680,11 +707,11 @@ export default class ScrollPanel extends React.Component {
}
// need a better name that also indicates this will change scrollTop? Rebalance height? Reveal content?
- async _updateHeight() {
+ private async updateHeight(): Promise {
// wait until user has stopped scrolling
- if (this._scrollTimeout.isRunning()) {
+ if (this.scrollTimeout.isRunning()) {
debuglog("updateHeight waiting for scrolling to end ... ");
- await this._scrollTimeout.finished();
+ await this.scrollTimeout.finished();
} else {
debuglog("updateHeight getting straight to business, no scrolling going on.");
}
@@ -694,14 +721,14 @@ export default class ScrollPanel extends React.Component {
return;
}
- const sn = this._getScrollNode();
- const itemlist = this._itemlist.current;
- const contentHeight = this._getMessagesHeight();
+ const sn = this.getScrollNode();
+ const itemlist = this.itemlist.current;
+ const contentHeight = this.getMessagesHeight();
const minHeight = sn.clientHeight;
const height = Math.max(minHeight, contentHeight);
- this._pages = Math.ceil(height / PAGE_SIZE);
- this._bottomGrowth = 0;
- const newHeight = `${this._getListHeight()}px`;
+ this.pages = Math.ceil(height / PAGE_SIZE);
+ this.bottomGrowth = 0;
+ const newHeight = `${this.getListHeight()}px`;
const scrollState = this.scrollState;
if (scrollState.stuckAtBottom) {
@@ -713,7 +740,7 @@ export default class ScrollPanel extends React.Component {
}
debuglog("updateHeight to", newHeight);
} else if (scrollState.trackedScrollToken) {
- const trackedNode = this._getTrackedNode();
+ const trackedNode = this.getTrackedNode();
// if the timeline has been reloaded
// this can be called before scrollToBottom or whatever has been called
// so don't do anything if the node has disappeared from
@@ -735,17 +762,17 @@ export default class ScrollPanel extends React.Component {
}
}
- _getTrackedNode() {
+ private getTrackedNode(): HTMLElement {
const scrollState = this.scrollState;
const trackedNode = scrollState.trackedNode;
if (!trackedNode || !trackedNode.parentElement) {
let node;
- const messages = this._itemlist.current.children;
+ const messages = this.itemlist.current.children;
const scrollToken = scrollState.trackedScrollToken;
for (let i = messages.length-1; i >= 0; --i) {
- const m = messages[i];
+ const m = messages[i] as HTMLElement;
// 'data-scroll-tokens' is a DOMString of comma-separated scroll tokens
// There might only be one scroll token
if (m.dataset.scrollTokens &&
@@ -768,45 +795,45 @@ export default class ScrollPanel extends React.Component {
return scrollState.trackedNode;
}
- _getListHeight() {
- return this._bottomGrowth + (this._pages * PAGE_SIZE);
+ private getListHeight(): number {
+ return this.bottomGrowth + (this.pages * PAGE_SIZE);
}
- _getMessagesHeight() {
- const itemlist = this._itemlist.current;
- const lastNode = itemlist.lastElementChild;
+ private getMessagesHeight(): number {
+ const itemlist = this.itemlist.current;
+ const lastNode = itemlist.lastElementChild as HTMLElement;
const lastNodeBottom = lastNode ? lastNode.offsetTop + lastNode.clientHeight : 0;
- const firstNodeTop = itemlist.firstElementChild ? itemlist.firstElementChild.offsetTop : 0;
+ const firstNodeTop = itemlist.firstElementChild ? (itemlist.firstElementChild as HTMLElement).offsetTop : 0;
// 18 is itemlist padding
return lastNodeBottom - firstNodeTop + (18 * 2);
}
- _topFromBottom(node) {
+ private topFromBottom(node: HTMLElement): number {
// current capped height - distance from top = distance from bottom of container to top of tracked element
- return this._itemlist.current.clientHeight - node.offsetTop;
+ return this.itemlist.current.clientHeight - node.offsetTop;
}
/* get the DOM node which has the scrollTop property we care about for our
* message panel.
*/
- _getScrollNode() {
+ private getScrollNode(): HTMLDivElement {
if (this.unmounted) {
// this shouldn't happen, but when it does, turn the NPE into
// something more meaningful.
- throw new Error("ScrollPanel._getScrollNode called when unmounted");
+ throw new Error("ScrollPanel.getScrollNode called when unmounted");
}
- if (!this._divScroll) {
+ if (!this.divScroll) {
// Likewise, we should have the ref by this point, but if not
// turn the NPE into something meaningful.
- throw new Error("ScrollPanel._getScrollNode called before AutoHideScrollbar ref collected");
+ throw new Error("ScrollPanel.getScrollNode called before AutoHideScrollbar ref collected");
}
- return this._divScroll;
+ return this.divScroll;
}
- _collectScroll = divScroll => {
- this._divScroll = divScroll;
+ private collectScroll = (divScroll: HTMLDivElement) => {
+ this.divScroll = divScroll;
};
/**
@@ -814,15 +841,15 @@ export default class ScrollPanel extends React.Component {
anything below it changes, by calling updatePreventShrinking, to keep
the same minimum bottom offset, effectively preventing the timeline to shrink.
*/
- preventShrinking = () => {
- const messageList = this._itemlist.current;
+ public preventShrinking = (): void => {
+ const messageList = this.itemlist.current;
const tiles = messageList && messageList.children;
if (!messageList) {
return;
}
let lastTileNode;
for (let i = tiles.length - 1; i >= 0; i--) {
- const node = tiles[i];
+ const node = tiles[i] as HTMLElement;
if (node.dataset.scrollTokens) {
lastTileNode = node;
break;
@@ -841,8 +868,8 @@ export default class ScrollPanel extends React.Component {
};
/** Clear shrinking prevention. Used internally, and when the timeline is reloaded. */
- clearPreventShrinking = () => {
- const messageList = this._itemlist.current;
+ public clearPreventShrinking = (): void => {
+ const messageList = this.itemlist.current;
const balanceElement = messageList && messageList.parentElement;
if (balanceElement) balanceElement.style.paddingBottom = null;
this.preventShrinkingState = null;
@@ -857,11 +884,11 @@ export default class ScrollPanel extends React.Component {
from the bottom of the marked tile grows larger than
what it was when marking.
*/
- updatePreventShrinking = () => {
+ public updatePreventShrinking = (): void => {
if (this.preventShrinkingState) {
- const sn = this._getScrollNode();
+ const sn = this.getScrollNode();
const scrollState = this.scrollState;
- const messageList = this._itemlist.current;
+ const messageList = this.itemlist.current;
const {offsetNode, offsetFromBottom} = this.preventShrinkingState;
// element used to set paddingBottom to balance the typing notifs disappearing
const balanceElement = messageList.parentElement;
@@ -898,13 +925,15 @@ export default class ScrollPanel extends React.Component {
// list-style-type: none; is no longer a list
return (
+ className={`mx_ScrollPanel ${this.props.className}`}
+ style={this.props.style}
+ >
{ this.props.fixedChildren }
-
+
{ this.props.children }
diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.tsx
similarity index 75%
rename from src/components/structures/TimelinePanel.js
rename to src/components/structures/TimelinePanel.tsx
index 03d0b5c6d7..c2e7a6f346 100644
--- a/src/components/structures/TimelinePanel.js
+++ b/src/components/structures/TimelinePanel.tsx
@@ -1,8 +1,5 @@
/*
-Copyright 2016 OpenMarket Ltd
-Copyright 2017 Vector Creations Ltd
-Copyright 2019 New Vector Ltd
-Copyright 2019-2020 The Matrix.org Foundation C.I.C.
+Copyright 2016 - 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -17,13 +14,16 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import SettingsStore from "../../settings/SettingsStore";
-import { LayoutPropType } from "../../settings/Layout";
-import React, { createRef } from 'react';
+import React, { createRef, ReactNode, SyntheticEvent } from 'react';
import ReactDOM from "react-dom";
-import PropTypes from 'prop-types';
+import { Room } from "matrix-js-sdk/src/models/room";
+import { MatrixEvent } from "matrix-js-sdk/src/models/event";
+import { TimelineSet } from "matrix-js-sdk/src/models/event-timeline-set";
import { EventTimeline } from "matrix-js-sdk/src/models/event-timeline";
import { TimelineWindow } from "matrix-js-sdk/src/timeline-window";
+
+import SettingsStore from "../../settings/SettingsStore";
+import { Layout } from "../../settings/Layout";
import { _t } from '../../languageHandler';
import { MatrixClientPeg } from "../../MatrixClientPeg";
import RoomContext from "../../contexts/RoomContext";
@@ -35,11 +35,19 @@ import { Key } from '../../Keyboard';
import Timer from '../../utils/Timer';
import shouldHideEvent from '../../shouldHideEvent';
import EditorStateTransfer from '../../utils/EditorStateTransfer';
-import { haveTileForEvent } from "../views/rooms/EventTile";
+import { haveTileForEvent, TileShape } from "../views/rooms/EventTile";
import { UIFeature } from "../../settings/UIFeature";
import { replaceableComponent } from "../../utils/replaceableComponent";
import { arrayFastClone } from "../../utils/arrays";
import { Action } from "../../dispatcher/actions";
+import MessagePanel from "./MessagePanel";
+import { SyncState } from 'matrix-js-sdk/src/sync.api';
+import { IScrollState } from "./ScrollPanel";
+import { ActionPayload } from "../../dispatcher/payloads";
+import { EventType } from 'matrix-js-sdk/src/@types/event';
+import ResizeNotifier from "../../utils/ResizeNotifier";
+import { RoomPermalinkCreator } from "../../utils/permalinks/Permalinks";
+import Spinner from "../views/elements/Spinner";
const PAGINATE_SIZE = 20;
const INITIAL_SIZE = 20;
@@ -47,90 +55,159 @@ const READ_RECEIPT_INTERVAL_MS = 500;
const DEBUG = false;
-let debuglog = function() {};
+let debuglog = function(...s: any[]) {};
if (DEBUG) {
// using bind means that we get to keep useful line numbers in the console
debuglog = console.log.bind(console);
}
+interface IProps {
+ // The js-sdk EventTimelineSet object for the timeline sequence we are
+ // representing. This may or may not have a room, depending on what it's
+ // a timeline representing. If it has a room, we maintain RRs etc for
+ // that room.
+ timelineSet: TimelineSet;
+ showReadReceipts?: boolean;
+ // Enable managing RRs and RMs. These require the timelineSet to have a room.
+ manageReadReceipts?: boolean;
+ sendReadReceiptOnLoad?: boolean;
+ manageReadMarkers?: boolean;
+
+ // true to give the component a 'display: none' style.
+ hidden?: boolean;
+
+ // ID of an event to highlight. If undefined, no event will be highlighted.
+ // typically this will be either 'eventId' or undefined.
+ highlightedEventId?: string;
+
+ // id of an event to jump to. If not given, will go to the end of the live timeline.
+ eventId?: string;
+
+ // where to position the event given by eventId, in pixels from the bottom of the viewport.
+ // If not given, will try to put the event half way down the viewport.
+ eventPixelOffset?: number;
+
+ // Should we show URL Previews
+ showUrlPreview?: boolean;
+
+ // maximum number of events to show in a timeline
+ timelineCap?: number;
+
+ // classname to use for the messagepanel
+ className?: string;
+
+ // shape property to be passed to EventTiles
+ tileShape?: TileShape;
+
+ // placeholder to use if the timeline is empty
+ empty?: ReactNode;
+
+ // whether to show reactions for an event
+ showReactions?: boolean;
+
+ // which layout to use
+ layout?: Layout;
+
+ // whether to always show timestamps for an event
+ alwaysShowTimestamps?: boolean;
+
+ resizeNotifier?: ResizeNotifier;
+ editState?: EditorStateTransfer;
+ permalinkCreator?: RoomPermalinkCreator;
+ membersLoaded?: boolean;
+
+ // callback which is called when the panel is scrolled.
+ onScroll?(event: Event): void;
+
+ // callback which is called when the user interacts with the room timeline
+ onUserScroll?(event: SyntheticEvent): void;
+
+ // callback which is called when the read-up-to mark is updated.
+ onReadMarkerUpdated?(): void;
+
+ // callback which is called when we wish to paginate the timeline window.
+ onPaginationRequest?(timelineWindow: TimelineWindow, direction: string, size: number): Promise,
+}
+
+interface IState {
+ events: MatrixEvent[];
+ liveEvents: MatrixEvent[];
+ // track whether our room timeline is loading
+ timelineLoading: boolean;
+
+ // the index of the first event that is to be shown
+ firstVisibleEventIndex: number;
+
+ // canBackPaginate == false may mean:
+ //
+ // * we haven't (successfully) loaded the timeline yet, or:
+ //
+ // * we have got to the point where the room was created, or:
+ //
+ // * the server indicated that there were no more visible events
+ // (normally implying we got to the start of the room), or:
+ //
+ // * we gave up asking the server for more events
+ canBackPaginate: boolean;
+
+ // canForwardPaginate == false may mean:
+ //
+ // * we haven't (successfully) loaded the timeline yet
+ //
+ // * we have got to the end of time and are now tracking the live
+ // timeline, or:
+ //
+ // * the server indicated that there were no more visible events
+ // (not sure if this ever happens when we're not at the live
+ // timeline), or:
+ //
+ // * we are looking at some historical point, but gave up asking
+ // the server for more events
+ canForwardPaginate: boolean;
+
+ // start with the read-marker visible, so that we see its animated
+ // disappearance when switching into the room.
+ readMarkerVisible: boolean;
+
+ readMarkerEventId: string;
+
+ backPaginating: boolean;
+ forwardPaginating: boolean;
+
+ // cache of matrixClient.getSyncState() (but from the 'sync' event)
+ clientSyncState: SyncState;
+
+ // should the event tiles have twelve hour times
+ isTwelveHour: boolean;
+
+ // always show timestamps on event tiles?
+ alwaysShowTimestamps: boolean;
+
+ // how long to show the RM for when it's visible in the window
+ readMarkerInViewThresholdMs: number;
+
+ // how long to show the RM for when it's scrolled off-screen
+ readMarkerOutOfViewThresholdMs: number;
+
+ editState?: EditorStateTransfer;
+}
+
+interface IEventIndexOpts {
+ ignoreOwn?: boolean;
+ allowPartial?: boolean;
+}
+
/*
* Component which shows the event timeline in a room view.
*
* Also responsible for handling and sending read receipts.
*/
@replaceableComponent("structures.TimelinePanel")
-class TimelinePanel extends React.Component {
- static propTypes = {
- // The js-sdk EventTimelineSet object for the timeline sequence we are
- // representing. This may or may not have a room, depending on what it's
- // a timeline representing. If it has a room, we maintain RRs etc for
- // that room.
- timelineSet: PropTypes.object.isRequired,
-
- showReadReceipts: PropTypes.bool,
- // Enable managing RRs and RMs. These require the timelineSet to have a room.
- manageReadReceipts: PropTypes.bool,
- sendReadReceiptOnLoad: PropTypes.bool,
- manageReadMarkers: PropTypes.bool,
-
- // true to give the component a 'display: none' style.
- hidden: PropTypes.bool,
-
- // ID of an event to highlight. If undefined, no event will be highlighted.
- // typically this will be either 'eventId' or undefined.
- highlightedEventId: PropTypes.string,
-
- // id of an event to jump to. If not given, will go to the end of the
- // live timeline.
- eventId: PropTypes.string,
-
- // where to position the event given by eventId, in pixels from the
- // bottom of the viewport. If not given, will try to put the event
- // half way down the viewport.
- eventPixelOffset: PropTypes.number,
-
- // Should we show URL Previews
- showUrlPreview: PropTypes.bool,
-
- // callback which is called when the panel is scrolled.
- onScroll: PropTypes.func,
-
- // callback which is called when the user interacts with the room timeline
- onUserScroll: PropTypes.func,
-
- // callback which is called when the read-up-to mark is updated.
- onReadMarkerUpdated: PropTypes.func,
-
- // callback which is called when we wish to paginate the timeline
- // window.
- onPaginationRequest: PropTypes.func,
-
- // maximum number of events to show in a timeline
- timelineCap: PropTypes.number,
-
- // classname to use for the messagepanel
- className: PropTypes.string,
-
- // shape property to be passed to EventTiles
- tileShape: PropTypes.string,
-
- // placeholder to use if the timeline is empty
- empty: PropTypes.node,
-
- // whether to show reactions for an event
- showReactions: PropTypes.bool,
-
- // which layout to use
- layout: LayoutPropType,
-
- // whether to always show timestamps for an event
- alwaysShowTimestamps: PropTypes.bool,
- }
-
+class TimelinePanel extends React.Component {
static contextType = RoomContext;
// a map from room id to read marker event timestamp
- static roomReadMarkerTsMap = {};
+ static roomReadMarkerTsMap: Record = {};
static defaultProps = {
// By default, disable the timelineCap in favour of unpaginating based on
@@ -140,16 +217,21 @@ class TimelinePanel extends React.Component {
sendReadReceiptOnLoad: true,
};
- constructor(props) {
- super(props);
+ private lastRRSentEventId: string = undefined;
+ private lastRMSentEventId: string = undefined;
+
+ private readonly messagePanel = createRef();
+ private readonly dispatcherRef: string;
+ private timelineWindow?: TimelineWindow;
+ private unmounted = false;
+ private readReceiptActivityTimer: Timer;
+ private readMarkerActivityTimer: Timer;
+
+ constructor(props, context) {
+ super(props, context);
debuglog("TimelinePanel: mounting");
- this.lastRRSentEventId = undefined;
- this.lastRMSentEventId = undefined;
-
- this._messagePanel = createRef();
-
// XXX: we could track RM per TimelineSet rather than per Room.
// but for now we just do it per room for simplicity.
let initialReadMarker = null;
@@ -158,82 +240,41 @@ class TimelinePanel extends React.Component {
if (readmarker) {
initialReadMarker = readmarker.getContent().event_id;
} else {
- initialReadMarker = this._getCurrentReadReceipt();
+ initialReadMarker = this.getCurrentReadReceipt();
}
}
this.state = {
events: [],
liveEvents: [],
- timelineLoading: true, // track whether our room timeline is loading
-
- // the index of the first event that is to be shown
+ timelineLoading: true,
firstVisibleEventIndex: 0,
-
- // canBackPaginate == false may mean:
- //
- // * we haven't (successfully) loaded the timeline yet, or:
- //
- // * we have got to the point where the room was created, or:
- //
- // * the server indicated that there were no more visible events
- // (normally implying we got to the start of the room), or:
- //
- // * we gave up asking the server for more events
canBackPaginate: false,
-
- // canForwardPaginate == false may mean:
- //
- // * we haven't (successfully) loaded the timeline yet
- //
- // * we have got to the end of time and are now tracking the live
- // timeline, or:
- //
- // * the server indicated that there were no more visible events
- // (not sure if this ever happens when we're not at the live
- // timeline), or:
- //
- // * we are looking at some historical point, but gave up asking
- // the server for more events
canForwardPaginate: false,
-
- // start with the read-marker visible, so that we see its animated
- // disappearance when switching into the room.
readMarkerVisible: true,
-
readMarkerEventId: initialReadMarker,
-
backPaginating: false,
forwardPaginating: false,
-
- // cache of matrixClient.getSyncState() (but from the 'sync' event)
clientSyncState: MatrixClientPeg.get().getSyncState(),
-
- // should the event tiles have twelve hour times
isTwelveHour: SettingsStore.getValue("showTwelveHourTimestamps"),
-
- // always show timestamps on event tiles?
alwaysShowTimestamps: SettingsStore.getValue("alwaysShowTimestamps"),
-
- // how long to show the RM for when it's visible in the window
readMarkerInViewThresholdMs: SettingsStore.getValue("readMarkerInViewThresholdMs"),
-
- // how long to show the RM for when it's scrolled off-screen
readMarkerOutOfViewThresholdMs: SettingsStore.getValue("readMarkerOutOfViewThresholdMs"),
};
this.dispatcherRef = dis.register(this.onAction);
- MatrixClientPeg.get().on("Room.timeline", this.onRoomTimeline);
- MatrixClientPeg.get().on("Room.timelineReset", this.onRoomTimelineReset);
- MatrixClientPeg.get().on("Room.redaction", this.onRoomRedaction);
+ const cli = MatrixClientPeg.get();
+ cli.on("Room.timeline", this.onRoomTimeline);
+ cli.on("Room.timelineReset", this.onRoomTimelineReset);
+ cli.on("Room.redaction", this.onRoomRedaction);
// same event handler as Room.redaction as for both we just do forceUpdate
- MatrixClientPeg.get().on("Room.redactionCancelled", this.onRoomRedaction);
- MatrixClientPeg.get().on("Room.receipt", this.onRoomReceipt);
- MatrixClientPeg.get().on("Room.localEchoUpdated", this.onLocalEchoUpdated);
- MatrixClientPeg.get().on("Room.accountData", this.onAccountData);
- MatrixClientPeg.get().on("Event.decrypted", this.onEventDecrypted);
- MatrixClientPeg.get().on("Event.replaced", this.onEventReplaced);
- MatrixClientPeg.get().on("sync", this.onSync);
+ cli.on("Room.redactionCancelled", this.onRoomRedaction);
+ cli.on("Room.receipt", this.onRoomReceipt);
+ cli.on("Room.localEchoUpdated", this.onLocalEchoUpdated);
+ cli.on("Room.accountData", this.onAccountData);
+ cli.on("Event.decrypted", this.onEventDecrypted);
+ cli.on("Event.replaced", this.onEventReplaced);
+ cli.on("sync", this.onSync);
}
// TODO: [REACT-WARNING] Move into constructor
@@ -246,7 +287,7 @@ class TimelinePanel extends React.Component {
this.updateReadMarkerOnUserActivity();
}
- this._initTimeline(this.props);
+ this.initTimeline(this.props);
}
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
@@ -272,7 +313,7 @@ class TimelinePanel extends React.Component {
if (differentEventId || differentHighlightedEventId) {
console.log("TimelinePanel switching to eventId " + newProps.eventId +
" (was " + this.props.eventId + ")");
- return this._initTimeline(newProps);
+ return this.initTimeline(newProps);
}
}
@@ -282,13 +323,13 @@ class TimelinePanel extends React.Component {
//
// (We could use isMounted, but facebook have deprecated that.)
this.unmounted = true;
- if (this._readReceiptActivityTimer) {
- this._readReceiptActivityTimer.abort();
- this._readReceiptActivityTimer = null;
+ if (this.readReceiptActivityTimer) {
+ this.readReceiptActivityTimer.abort();
+ this.readReceiptActivityTimer = null;
}
- if (this._readMarkerActivityTimer) {
- this._readMarkerActivityTimer.abort();
- this._readMarkerActivityTimer = null;
+ if (this.readMarkerActivityTimer) {
+ this.readMarkerActivityTimer.abort();
+ this.readMarkerActivityTimer = null;
}
dis.unregister(this.dispatcherRef);
@@ -308,7 +349,7 @@ class TimelinePanel extends React.Component {
}
}
- onMessageListUnfillRequest = (backwards, scrollToken) => {
+ private onMessageListUnfillRequest = (backwards: boolean, scrollToken: string): void => {
// If backwards, unpaginate from the back (i.e. the start of the timeline)
const dir = backwards ? EventTimeline.BACKWARDS : EventTimeline.FORWARDS;
debuglog("TimelinePanel: unpaginating events in direction", dir);
@@ -327,21 +368,30 @@ class TimelinePanel extends React.Component {
if (count > 0) {
debuglog("TimelinePanel: Unpaginating", count, "in direction", dir);
- this._timelineWindow.unpaginate(count, backwards);
+ this.timelineWindow.unpaginate(count, backwards);
- // We can now paginate in the unpaginated direction
- const canPaginateKey = (backwards) ? 'canBackPaginate' : 'canForwardPaginate';
- const { events, liveEvents, firstVisibleEventIndex } = this._getEvents();
- this.setState({
- [canPaginateKey]: true,
+ const { events, liveEvents, firstVisibleEventIndex } = this.getEvents();
+ const newState: Partial = {
events,
liveEvents,
firstVisibleEventIndex,
- });
+ }
+
+ // We can now paginate in the unpaginated direction
+ if (backwards) {
+ newState.canBackPaginate = true;
+ } else {
+ newState.canForwardPaginate = true;
+ }
+ this.setState(newState);
}
};
- onPaginationRequest = (timelineWindow, direction, size) => {
+ private onPaginationRequest = (
+ timelineWindow: TimelineWindow,
+ direction: string,
+ size: number,
+ ): Promise => {
if (this.props.onPaginationRequest) {
return this.props.onPaginationRequest(timelineWindow, direction, size);
} else {
@@ -350,8 +400,8 @@ class TimelinePanel extends React.Component {
};
// set off a pagination request.
- onMessageListFillRequest = backwards => {
- if (!this._shouldPaginate()) return Promise.resolve(false);
+ private onMessageListFillRequest = (backwards: boolean): Promise => {
+ if (!this.shouldPaginate()) return Promise.resolve(false);
const dir = backwards ? EventTimeline.BACKWARDS : EventTimeline.FORWARDS;
const canPaginateKey = backwards ? 'canBackPaginate' : 'canForwardPaginate';
@@ -362,9 +412,9 @@ class TimelinePanel extends React.Component {
return Promise.resolve(false);
}
- if (!this._timelineWindow.canPaginate(dir)) {
+ if (!this.timelineWindow.canPaginate(dir)) {
debuglog("TimelinePanel: can't", dir, "paginate any further");
- this.setState({[canPaginateKey]: false});
+ this.setState({ [canPaginateKey]: false });
return Promise.resolve(false);
}
@@ -374,15 +424,15 @@ class TimelinePanel extends React.Component {
}
debuglog("TimelinePanel: Initiating paginate; backwards:"+backwards);
- this.setState({[paginatingKey]: true});
+ this.setState({ [paginatingKey]: true });
- return this.onPaginationRequest(this._timelineWindow, dir, PAGINATE_SIZE).then((r) => {
+ return this.onPaginationRequest(this.timelineWindow, dir, PAGINATE_SIZE).then((r) => {
if (this.unmounted) { return; }
debuglog("TimelinePanel: paginate complete backwards:"+backwards+"; success:"+r);
- const { events, liveEvents, firstVisibleEventIndex } = this._getEvents();
- const newState = {
+ const { events, liveEvents, firstVisibleEventIndex } = this.getEvents();
+ const newState: Partial = {
[paginatingKey]: false,
[canPaginateKey]: r,
events,
@@ -395,7 +445,7 @@ class TimelinePanel extends React.Component {
const otherDirection = backwards ? EventTimeline.FORWARDS : EventTimeline.BACKWARDS;
const canPaginateOtherWayKey = backwards ? 'canForwardPaginate' : 'canBackPaginate';
if (!this.state[canPaginateOtherWayKey] &&
- this._timelineWindow.canPaginate(otherDirection)) {
+ this.timelineWindow.canPaginate(otherDirection)) {
debuglog('TimelinePanel: can now', otherDirection, 'paginate again');
newState[canPaginateOtherWayKey] = true;
}
@@ -406,9 +456,9 @@ class TimelinePanel extends React.Component {
// has in memory because we never gave the component a chance to scroll
// itself into the right place
return new Promise((resolve) => {
- this.setState(newState, () => {
+ this.setState(newState, () => {
// we can continue paginating in the given direction if:
- // - _timelineWindow.paginate says we can
+ // - timelineWindow.paginate says we can
// - we're paginating forwards, or we won't be trying to
// paginate backwards past the first visible event
resolve(r && (!backwards || firstVisibleEventIndex === 0));
@@ -417,7 +467,7 @@ class TimelinePanel extends React.Component {
});
};
- onMessageListScroll = e => {
+ private onMessageListScroll = e => {
if (this.props.onScroll) {
this.props.onScroll(e);
}
@@ -428,18 +478,18 @@ class TimelinePanel extends React.Component {
// it goes back off the top of the screen (presumably because the user
// clicks on the 'jump to bottom' button), we need to re-enable it.
if (rmPosition < 0) {
- this.setState({readMarkerVisible: true});
+ this.setState({ readMarkerVisible: true });
}
// if read marker position goes between 0 and -1/1,
// (and user is active), switch timeout
- const timeout = this._readMarkerTimeout(rmPosition);
+ const timeout = this.readMarkerTimeout(rmPosition);
// NO-OP when timeout already has set to the given value
- this._readMarkerActivityTimer.changeTimeout(timeout);
+ this.readMarkerActivityTimer.changeTimeout(timeout);
}
};
- onAction = payload => {
+ private onAction = (payload: ActionPayload): void => {
switch (payload.action) {
case "ignore_state_changed":
this.forceUpdate();
@@ -447,9 +497,9 @@ class TimelinePanel extends React.Component {
case "edit_event": {
const editState = payload.event ? new EditorStateTransfer(payload.event) : null;
- this.setState({editState}, () => {
- if (payload.event && this._messagePanel.current) {
- this._messagePanel.current.scrollToEventIfNeeded(
+ this.setState({ editState }, () => {
+ if (payload.event && this.messagePanel.current) {
+ this.messagePanel.current.scrollToEventIfNeeded(
payload.event.getId(),
);
}
@@ -479,7 +529,16 @@ class TimelinePanel extends React.Component {
}
};
- onRoomTimeline = (ev, room, toStartOfTimeline, removed, data) => {
+ private onRoomTimeline = (
+ ev: MatrixEvent,
+ room: Room,
+ toStartOfTimeline: boolean,
+ removed: boolean,
+ data: {
+ timeline: EventTimeline;
+ liveEvent?: boolean;
+ },
+ ): void => {
// ignore events for other timeline sets
if (data.timeline.getTimelineSet() !== this.props.timelineSet) return;
@@ -487,9 +546,9 @@ class TimelinePanel extends React.Component {
// updates from pagination will happen when the paginate completes.
if (toStartOfTimeline || !data || !data.liveEvent) return;
- if (!this._messagePanel.current) return;
+ if (!this.messagePanel.current) return;
- if (!this._messagePanel.current.getScrollState().stuckAtBottom) {
+ if (!this.messagePanel.current.getScrollState().stuckAtBottom) {
// we won't load this event now, because we don't want to push any
// events off the other end of the timeline. But we need to note
// that we can now paginate.
@@ -506,13 +565,13 @@ class TimelinePanel extends React.Component {
// timeline window.
//
// see https://github.com/vector-im/vector-web/issues/1035
- this._timelineWindow.paginate(EventTimeline.FORWARDS, 1, false).then(() => {
+ this.timelineWindow.paginate(EventTimeline.FORWARDS, 1, false).then(() => {
if (this.unmounted) { return; }
- const { events, liveEvents, firstVisibleEventIndex } = this._getEvents();
+ const { events, liveEvents, firstVisibleEventIndex } = this.getEvents();
const lastLiveEvent = liveEvents[liveEvents.length - 1];
- const updatedState = {
+ const updatedState: Partial = {
events,
liveEvents,
firstVisibleEventIndex,
@@ -537,15 +596,15 @@ class TimelinePanel extends React.Component {
// we know we're stuckAtBottom, so we can advance the RM
// immediately, to save a later render cycle
- this._setReadMarker(lastLiveEvent.getId(), lastLiveEvent.getTs(), true);
+ this.setReadMarker(lastLiveEvent.getId(), lastLiveEvent.getTs(), true);
updatedState.readMarkerVisible = false;
updatedState.readMarkerEventId = lastLiveEvent.getId();
callRMUpdated = true;
}
}
- this.setState(updatedState, () => {
- this._messagePanel.current.updateTimelineMinHeight();
+ this.setState(updatedState, () => {
+ this.messagePanel.current.updateTimelineMinHeight();
if (callRMUpdated) {
this.props.onReadMarkerUpdated();
}
@@ -553,17 +612,17 @@ class TimelinePanel extends React.Component {
});
};
- onRoomTimelineReset = (room, timelineSet) => {
+ private onRoomTimelineReset = (room: Room, timelineSet: TimelineSet): void => {
if (timelineSet !== this.props.timelineSet) return;
- if (this._messagePanel.current && this._messagePanel.current.isAtBottom()) {
- this._loadTimeline();
+ if (this.messagePanel.current && this.messagePanel.current.isAtBottom()) {
+ this.loadTimeline();
}
};
- canResetTimeline = () => this._messagePanel.current && this._messagePanel.current.isAtBottom();
+ public canResetTimeline = () => this.messagePanel?.current.isAtBottom();
- onRoomRedaction = (ev, room) => {
+ private onRoomRedaction = (ev: MatrixEvent, room: Room): void => {
if (this.unmounted) return;
// ignore events for other rooms
@@ -574,7 +633,7 @@ class TimelinePanel extends React.Component {
this.forceUpdate();
};
- onEventReplaced = (replacedEvent, room) => {
+ private onEventReplaced = (replacedEvent: MatrixEvent, room: Room): void => {
if (this.unmounted) return;
// ignore events for other rooms
@@ -585,7 +644,7 @@ class TimelinePanel extends React.Component {
this.forceUpdate();
};
- onRoomReceipt = (ev, room) => {
+ private onRoomReceipt = (ev: MatrixEvent, room: Room): void => {
if (this.unmounted) return;
// ignore events for other rooms
@@ -594,22 +653,22 @@ class TimelinePanel extends React.Component {
this.forceUpdate();
};
- onLocalEchoUpdated = (ev, room, oldEventId) => {
+ private onLocalEchoUpdated = (ev: MatrixEvent, room: Room, oldEventId: string): void => {
if (this.unmounted) return;
// ignore events for other rooms
if (room !== this.props.timelineSet.room) return;
- this._reloadEvents();
+ this.reloadEvents();
};
- onAccountData = (ev, room) => {
+ private onAccountData = (ev: MatrixEvent, room: Room): void => {
if (this.unmounted) return;
// ignore events for other rooms
if (room !== this.props.timelineSet.room) return;
- if (ev.getType() !== "m.fully_read") return;
+ if (ev.getType() !== EventType.FullyRead) return;
// XXX: roomReadMarkerTsMap not updated here so it is now inconsistent. Replace
// this mechanism of determining where the RM is relative to the view-port with
@@ -619,7 +678,7 @@ class TimelinePanel extends React.Component {
}, this.props.onReadMarkerUpdated);
};
- onEventDecrypted = ev => {
+ private onEventDecrypted = (ev: MatrixEvent): void => {
// Can be null for the notification timeline, etc.
if (!this.props.timelineSet.room) return;
@@ -634,46 +693,46 @@ class TimelinePanel extends React.Component {
}
};
- onSync = (state, prevState, data) => {
- this.setState({clientSyncState: state});
+ private onSync = (clientSyncState: SyncState, prevState: SyncState, data: object): void => {
+ this.setState({ clientSyncState });
};
- _readMarkerTimeout(readMarkerPosition) {
+ private readMarkerTimeout(readMarkerPosition: number): number {
return readMarkerPosition === 0 ?
this.state.readMarkerInViewThresholdMs :
this.state.readMarkerOutOfViewThresholdMs;
}
- async updateReadMarkerOnUserActivity() {
- const initialTimeout = this._readMarkerTimeout(this.getReadMarkerPosition());
- this._readMarkerActivityTimer = new Timer(initialTimeout);
+ private async updateReadMarkerOnUserActivity(): Promise {
+ const initialTimeout = this.readMarkerTimeout(this.getReadMarkerPosition());
+ this.readMarkerActivityTimer = new Timer(initialTimeout);
- while (this._readMarkerActivityTimer) { //unset on unmount
- UserActivity.sharedInstance().timeWhileActiveRecently(this._readMarkerActivityTimer);
+ while (this.readMarkerActivityTimer) { //unset on unmount
+ UserActivity.sharedInstance().timeWhileActiveRecently(this.readMarkerActivityTimer);
try {
- await this._readMarkerActivityTimer.finished();
+ await this.readMarkerActivityTimer.finished();
} catch (e) { continue; /* aborted */ }
// outside of try/catch to not swallow errors
this.updateReadMarker();
}
}
- async updateReadReceiptOnUserActivity() {
- this._readReceiptActivityTimer = new Timer(READ_RECEIPT_INTERVAL_MS);
- while (this._readReceiptActivityTimer) { //unset on unmount
- UserActivity.sharedInstance().timeWhileActiveNow(this._readReceiptActivityTimer);
+ private async updateReadReceiptOnUserActivity(): Promise {
+ this.readReceiptActivityTimer = new Timer(READ_RECEIPT_INTERVAL_MS);
+ while (this.readReceiptActivityTimer) { //unset on unmount
+ UserActivity.sharedInstance().timeWhileActiveNow(this.readReceiptActivityTimer);
try {
- await this._readReceiptActivityTimer.finished();
+ await this.readReceiptActivityTimer.finished();
} catch (e) { continue; /* aborted */ }
// outside of try/catch to not swallow errors
this.sendReadReceipt();
}
}
- sendReadReceipt = () => {
+ private sendReadReceipt = (): void => {
if (SettingsStore.getValue("lowBandwidth")) return;
- if (!this._messagePanel.current) return;
+ if (!this.messagePanel.current) return;
if (!this.props.manageReadReceipts) return;
// This happens on user_activity_end which is delayed, and it's
// very possible have logged out within that timeframe, so check
@@ -684,8 +743,8 @@ class TimelinePanel extends React.Component {
let shouldSendRR = true;
- const currentRREventId = this._getCurrentReadReceipt(true);
- const currentRREventIndex = this._indexForEventId(currentRREventId);
+ const currentRREventId = this.getCurrentReadReceipt(true);
+ const currentRREventIndex = this.indexForEventId(currentRREventId);
// We want to avoid sending out read receipts when we are looking at
// events in the past which are before the latest RR.
//
@@ -700,11 +759,11 @@ class TimelinePanel extends React.Component {
// the user eventually hits the live timeline.
//
if (currentRREventId && currentRREventIndex === null &&
- this._timelineWindow.canPaginate(EventTimeline.FORWARDS)) {
+ this.timelineWindow.canPaginate(EventTimeline.FORWARDS)) {
shouldSendRR = false;
}
- const lastReadEventIndex = this._getLastDisplayedEventIndex({
+ const lastReadEventIndex = this.getLastDisplayedEventIndex({
ignoreOwn: true,
});
if (lastReadEventIndex === null) {
@@ -778,7 +837,7 @@ class TimelinePanel extends React.Component {
// if the read marker is on the screen, we can now assume we've caught up to the end
// of the screen, so move the marker down to the bottom of the screen.
- updateReadMarker = () => {
+ private updateReadMarker = (): void => {
if (!this.props.manageReadMarkers) return;
if (this.getReadMarkerPosition() === 1) {
// the read marker is at an event below the viewport,
@@ -788,7 +847,7 @@ class TimelinePanel extends React.Component {
// move the RM to *after* the message at the bottom of the screen. This
// avoids a problem whereby we never advance the RM if there is a huge
// message which doesn't fit on the screen.
- const lastDisplayedIndex = this._getLastDisplayedEventIndex({
+ const lastDisplayedIndex = this.getLastDisplayedEventIndex({
allowPartial: true,
});
@@ -796,7 +855,7 @@ class TimelinePanel extends React.Component {
return;
}
const lastDisplayedEvent = this.state.events[lastDisplayedIndex];
- this._setReadMarker(
+ this.setReadMarker(
lastDisplayedEvent.getId(),
lastDisplayedEvent.getTs(),
);
@@ -815,13 +874,13 @@ class TimelinePanel extends React.Component {
// advance the read marker past any events we sent ourselves.
- _advanceReadMarkerPastMyEvents() {
+ private advanceReadMarkerPastMyEvents(): void {
if (!this.props.manageReadMarkers) return;
- // we call `_timelineWindow.getEvents()` rather than using
+ // we call `timelineWindow.getEvents()` rather than using
// `this.state.liveEvents`, because React batches the update to the
// latter, so it may not have been updated yet.
- const events = this._timelineWindow.getEvents();
+ const events = this.timelineWindow.getEvents();
// first find where the current RM is
let i;
@@ -846,22 +905,22 @@ class TimelinePanel extends React.Component {
i--;
const ev = events[i];
- this._setReadMarker(ev.getId(), ev.getTs());
+ this.setReadMarker(ev.getId(), ev.getTs());
}
/* jump down to the bottom of this room, where new events are arriving
*/
- jumpToLiveTimeline = () => {
+ public jumpToLiveTimeline = (): void => {
// if we can't forward-paginate the existing timeline, then there
// is no point reloading it - just jump straight to the bottom.
//
// Otherwise, reload the timeline rather than trying to paginate
// through all of space-time.
- if (this._timelineWindow.canPaginate(EventTimeline.FORWARDS)) {
- this._loadTimeline();
+ if (this.timelineWindow.canPaginate(EventTimeline.FORWARDS)) {
+ this.loadTimeline();
} else {
- if (this._messagePanel.current) {
- this._messagePanel.current.scrollToBottom();
+ if (this.messagePanel.current) {
+ this.messagePanel.current.scrollToBottom();
}
}
};
@@ -869,22 +928,22 @@ class TimelinePanel extends React.Component {
/* scroll to show the read-up-to marker. We put it 1/3 of the way down
* the container.
*/
- jumpToReadMarker = () => {
+ public jumpToReadMarker = (): void => {
if (!this.props.manageReadMarkers) return;
- if (!this._messagePanel.current) return;
+ if (!this.messagePanel.current) return;
if (!this.state.readMarkerEventId) return;
// we may not have loaded the event corresponding to the read-marker
- // into the _timelineWindow. In that case, attempts to scroll to it
+ // into the timelineWindow. In that case, attempts to scroll to it
// will fail.
//
// a quick way to figure out if we've loaded the relevant event is
// simply to check if the messagepanel knows where the read-marker is.
- const ret = this._messagePanel.current.getReadMarkerPosition();
+ const ret = this.messagePanel.current.getReadMarkerPosition();
if (ret !== null) {
// The messagepanel knows where the RM is, so we must have loaded
// the relevant event.
- this._messagePanel.current.scrollToEvent(this.state.readMarkerEventId,
+ this.messagePanel.current.scrollToEvent(this.state.readMarkerEventId,
0, 1/3);
return;
}
@@ -892,15 +951,15 @@ class TimelinePanel extends React.Component {
// Looks like we haven't loaded the event corresponding to the read-marker.
// As with jumpToLiveTimeline, we want to reload the timeline around the
// read-marker.
- this._loadTimeline(this.state.readMarkerEventId, 0, 1/3);
+ this.loadTimeline(this.state.readMarkerEventId, 0, 1/3);
};
/* update the read-up-to marker to match the read receipt
*/
- forgetReadMarker = () => {
+ public forgetReadMarker = (): void => {
if (!this.props.manageReadMarkers) return;
- const rmId = this._getCurrentReadReceipt();
+ const rmId = this.getCurrentReadReceipt();
// see if we know the timestamp for the rr event
const tl = this.props.timelineSet.getTimelineForEvent(rmId);
@@ -912,17 +971,17 @@ class TimelinePanel extends React.Component {
}
}
- this._setReadMarker(rmId, rmTs);
+ this.setReadMarker(rmId, rmTs);
};
/* return true if the content is fully scrolled down and we are
* at the end of the live timeline.
*/
- isAtEndOfLiveTimeline = () => {
- return this._messagePanel.current
- && this._messagePanel.current.isAtBottom()
- && this._timelineWindow
- && !this._timelineWindow.canPaginate(EventTimeline.FORWARDS);
+ public isAtEndOfLiveTimeline = (): boolean => {
+ return this.messagePanel.current
+ && this.messagePanel.current.isAtBottom()
+ && this.timelineWindow
+ && !this.timelineWindow.canPaginate(EventTimeline.FORWARDS);
}
@@ -931,9 +990,9 @@ class TimelinePanel extends React.Component {
*
* returns null if we are not mounted.
*/
- getScrollState = () => {
- if (!this._messagePanel.current) { return null; }
- return this._messagePanel.current.getScrollState();
+ public getScrollState = (): IScrollState => {
+ if (!this.messagePanel.current) { return null; }
+ return this.messagePanel.current.getScrollState();
};
// returns one of:
@@ -942,11 +1001,11 @@ class TimelinePanel extends React.Component {
// -1: read marker is above the window
// 0: read marker is visible
// +1: read marker is below the window
- getReadMarkerPosition = () => {
+ public getReadMarkerPosition = (): number => {
if (!this.props.manageReadMarkers) return null;
- if (!this._messagePanel.current) return null;
+ if (!this.messagePanel.current) return null;
- const ret = this._messagePanel.current.getReadMarkerPosition();
+ const ret = this.messagePanel.current.getReadMarkerPosition();
if (ret !== null) {
return ret;
}
@@ -965,7 +1024,7 @@ class TimelinePanel extends React.Component {
return null;
};
- canJumpToReadMarker = () => {
+ public canJumpToReadMarker = (): boolean => {
// 1. Do not show jump bar if neither the RM nor the RR are set.
// 3. We want to show the bar if the read-marker is off the top of the screen.
// 4. Also, if pos === null, the event might not be paginated - show the unread bar
@@ -980,19 +1039,19 @@ class TimelinePanel extends React.Component {
*
* We pass it down to the scroll panel.
*/
- handleScrollKey = ev => {
- if (!this._messagePanel.current) { return; }
+ public handleScrollKey = ev => {
+ if (!this.messagePanel.current) { return; }
// jump to the live timeline on ctrl-end, rather than the end of the
// timeline window.
if (ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey && ev.key === Key.END) {
this.jumpToLiveTimeline();
} else {
- this._messagePanel.current.handleScrollKey(ev);
+ this.messagePanel.current.handleScrollKey(ev);
}
};
- _initTimeline(props) {
+ private initTimeline(props: IProps): void {
const initialEvent = props.eventId;
const pixelOffset = props.eventPixelOffset;
@@ -1003,7 +1062,7 @@ class TimelinePanel extends React.Component {
offsetBase = 0.5;
}
- return this._loadTimeline(initialEvent, pixelOffset, offsetBase);
+ return this.loadTimeline(initialEvent, pixelOffset, offsetBase);
}
/**
@@ -1019,34 +1078,32 @@ class TimelinePanel extends React.Component {
* @param {number?} offsetBase the reference point for the pixelOffset. 0
* means the top of the container, 1 means the bottom, and fractional
* values mean somewhere in the middle. If omitted, it defaults to 0.
- *
- * returns a promise which will resolve when the load completes.
*/
- _loadTimeline(eventId, pixelOffset, offsetBase) {
- this._timelineWindow = new TimelineWindow(
+ private loadTimeline(eventId?: string, pixelOffset?: number, offsetBase?: number): void {
+ this.timelineWindow = new TimelineWindow(
MatrixClientPeg.get(), this.props.timelineSet,
{windowLimit: this.props.timelineCap});
const onLoaded = () => {
// clear the timeline min-height when
// (re)loading the timeline
- if (this._messagePanel.current) {
- this._messagePanel.current.onTimelineReset();
+ if (this.messagePanel.current) {
+ this.messagePanel.current.onTimelineReset();
}
- this._reloadEvents();
+ this.reloadEvents();
// If we switched away from the room while there were pending
// outgoing events, the read-marker will be before those events.
// We need to skip over any which have subsequently been sent.
- this._advanceReadMarkerPastMyEvents();
+ this.advanceReadMarkerPastMyEvents();
this.setState({
- canBackPaginate: this._timelineWindow.canPaginate(EventTimeline.BACKWARDS),
- canForwardPaginate: this._timelineWindow.canPaginate(EventTimeline.FORWARDS),
+ canBackPaginate: this.timelineWindow.canPaginate(EventTimeline.BACKWARDS),
+ canForwardPaginate: this.timelineWindow.canPaginate(EventTimeline.FORWARDS),
timelineLoading: false,
}, () => {
// initialise the scroll state of the message panel
- if (!this._messagePanel.current) {
+ if (!this.messagePanel.current) {
// this shouldn't happen - we know we're mounted because
// we're in a setState callback, and we know
// timelineLoading is now false, so render() should have
@@ -1056,10 +1113,10 @@ class TimelinePanel extends React.Component {
return;
}
if (eventId) {
- this._messagePanel.current.scrollToEvent(eventId, pixelOffset,
+ this.messagePanel.current.scrollToEvent(eventId, pixelOffset,
offsetBase);
} else {
- this._messagePanel.current.scrollToBottom();
+ this.messagePanel.current.scrollToBottom();
}
if (this.props.sendReadReceiptOnLoad) {
@@ -1121,10 +1178,10 @@ class TimelinePanel extends React.Component {
if (timeline) {
// This is a hot-path optimization by skipping a promise tick
// by repeating a no-op sync branch in TimelineSet.getTimelineForEvent & MatrixClient.getEventTimeline
- this._timelineWindow.load(eventId, INITIAL_SIZE); // in this branch this method will happen in sync time
+ this.timelineWindow.load(eventId, INITIAL_SIZE); // in this branch this method will happen in sync time
onLoaded();
} else {
- const prom = this._timelineWindow.load(eventId, INITIAL_SIZE);
+ const prom = this.timelineWindow.load(eventId, INITIAL_SIZE);
this.setState({
events: [],
liveEvents: [],
@@ -1139,17 +1196,17 @@ class TimelinePanel extends React.Component {
// handle the completion of a timeline load or localEchoUpdate, by
// reloading the events from the timelinewindow and pending event list into
// the state.
- _reloadEvents() {
+ private reloadEvents(): void {
// we might have switched rooms since the load started - just bin
// the results if so.
if (this.unmounted) return;
- this.setState(this._getEvents());
+ this.setState(this.getEvents());
}
// get the list of events from the timeline window and the pending event list
- _getEvents() {
- const events = this._timelineWindow.getEvents();
+ private getEvents(): Pick {
+ const events: MatrixEvent[] = this.timelineWindow.getEvents();
// `arrayFastClone` performs a shallow copy of the array
// we want the last event to be decrypted first but displayed last
@@ -1161,14 +1218,14 @@ class TimelinePanel extends React.Component {
client.decryptEventIfNeeded(event);
});
- const firstVisibleEventIndex = this._checkForPreJoinUISI(events);
+ const firstVisibleEventIndex = this.checkForPreJoinUISI(events);
// Hold onto the live events separately. The read receipt and read marker
// should use this list, so that they don't advance into pending events.
const liveEvents = [...events];
// if we're at the end of the live timeline, append the pending events
- if (!this._timelineWindow.canPaginate(EventTimeline.FORWARDS)) {
+ if (!this.timelineWindow.canPaginate(EventTimeline.FORWARDS)) {
events.push(...this.props.timelineSet.getPendingEvents());
}
@@ -1189,7 +1246,7 @@ class TimelinePanel extends React.Component {
* undecryptable event that was sent while the user was not in the room. If no
* such events were found, then it returns 0.
*/
- _checkForPreJoinUISI(events) {
+ private checkForPreJoinUISI(events: MatrixEvent[]): number {
const room = this.props.timelineSet.room;
if (events.length === 0 || !room ||
@@ -1253,7 +1310,7 @@ class TimelinePanel extends React.Component {
return 0;
}
- _indexForEventId(evId) {
+ private indexForEventId(evId: string): number | null {
for (let i = 0; i < this.state.events.length; ++i) {
if (evId == this.state.events[i].getId()) {
return i;
@@ -1262,15 +1319,14 @@ class TimelinePanel extends React.Component {
return null;
}
- _getLastDisplayedEventIndex(opts) {
- opts = opts || {};
+ private getLastDisplayedEventIndex(opts: IEventIndexOpts = {}): number | null {
const ignoreOwn = opts.ignoreOwn || false;
const allowPartial = opts.allowPartial || false;
- const messagePanel = this._messagePanel.current;
+ const messagePanel = this.messagePanel.current;
if (!messagePanel) return null;
- const messagePanelNode = ReactDOM.findDOMNode(messagePanel);
+ const messagePanelNode = ReactDOM.findDOMNode(messagePanel) as HTMLElement;
if (!messagePanelNode) return null; // sometimes this happens for fresh rooms/post-sync
const wrapperRect = messagePanelNode.getBoundingClientRect();
const myUserId = MatrixClientPeg.get().credentials.userId;
@@ -1347,7 +1403,7 @@ class TimelinePanel extends React.Component {
* SDK.
* @return {String} the event ID
*/
- _getCurrentReadReceipt(ignoreSynthesized) {
+ private getCurrentReadReceipt(ignoreSynthesized = false): string {
const client = MatrixClientPeg.get();
// the client can be null on logout
if (client == null) {
@@ -1358,7 +1414,7 @@ class TimelinePanel extends React.Component {
return this.props.timelineSet.room.getEventReadUpTo(myUserId, ignoreSynthesized);
}
- _setReadMarker(eventId, eventTs, inhibitSetState) {
+ private setReadMarker(eventId: string, eventTs: number, inhibitSetState = false): void {
const roomId = this.props.timelineSet.room.roomId;
// don't update the state (and cause a re-render) if there is
@@ -1383,7 +1439,7 @@ class TimelinePanel extends React.Component {
}, this.props.onReadMarkerUpdated);
}
- _shouldPaginate() {
+ private shouldPaginate(): boolean {
// don't try to paginate while events in the timeline are
// still being decrypted. We don't render events while they're
// being decrypted, so they don't take up space in the timeline.
@@ -1394,12 +1450,9 @@ class TimelinePanel extends React.Component {
});
}
- getRelationsForEvent = (...args) => this.props.timelineSet.getRelationsForEvent(...args);
+ private getRelationsForEvent = (...args) => this.props.timelineSet.getRelationsForEvent(...args);
render() {
- const MessagePanel = sdk.getComponent("structures.MessagePanel");
- const Loader = sdk.getComponent("elements.Spinner");
-
// just show a spinner while the timeline loads.
//
// put it in a div of the right class (mx_RoomView_messagePanel) so
@@ -1414,7 +1467,7 @@ class TimelinePanel extends React.Component {
if (this.state.timelineLoading) {
return (
-
+
);
}
@@ -1435,7 +1488,7 @@ class TimelinePanel extends React.Component {
// forwards, otherwise if somebody hits the bottom of the loaded
// events when viewing historical messages, we get stuck in a loop
// of paginating our way through the entire history of the room.
- const stickyBottom = !this._timelineWindow.canPaginate(EventTimeline.FORWARDS);
+ const stickyBottom = !this.timelineWindow.canPaginate(EventTimeline.FORWARDS);
// If the state is PREPARED or CATCHUP, we're still waiting for the js-sdk to sync with
// the HS and fetch the latest events, so we are effectively forward paginating.
@@ -1448,7 +1501,7 @@ class TimelinePanel extends React.Component {
: this.state.events;
return (
= ({ matrixClient: cli, event, permalinkCr
userId,
getAvatarUrl: (..._) => {
return avatarUrlForUser(
- { avatarUrl: profileInfo.avatar_url },
+ {avatarUrl: profileInfo.avatar_url},
AVATAR_SIZE, AVATAR_SIZE, "crop",
);
},
getMxcAvatarUrl: () => profileInfo.avatar_url,
- };
+ } as RoomMember;
const [query, setQuery] = useState("");
const lcQuery = query.toLowerCase();
diff --git a/src/components/views/elements/ErrorBoundary.js b/src/components/views/elements/ErrorBoundary.tsx
similarity index 80%
rename from src/components/views/elements/ErrorBoundary.js
rename to src/components/views/elements/ErrorBoundary.tsx
index 9037287f49..f967b8c594 100644
--- a/src/components/views/elements/ErrorBoundary.js
+++ b/src/components/views/elements/ErrorBoundary.tsx
@@ -1,5 +1,5 @@
/*
-Copyright 2019 The Matrix.org Foundation C.I.C.
+Copyright 2019 - 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -14,21 +14,27 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import React from 'react';
-import * as sdk from '../../../index';
+import React, { ErrorInfo } from 'react';
+
import { _t } from '../../../languageHandler';
-import {MatrixClientPeg} from '../../../MatrixClientPeg';
+import { MatrixClientPeg } from '../../../MatrixClientPeg';
import PlatformPeg from '../../../PlatformPeg';
import Modal from '../../../Modal';
import SdkConfig from "../../../SdkConfig";
-import {replaceableComponent} from "../../../utils/replaceableComponent";
+import { replaceableComponent } from "../../../utils/replaceableComponent";
+import BugReportDialog from '../dialogs/BugReportDialog';
+import AccessibleButton from './AccessibleButton';
+
+interface IState {
+ error: Error;
+}
/**
* This error boundary component can be used to wrap large content areas and
* catch exceptions during rendering in the component tree below them.
*/
@replaceableComponent("views.elements.ErrorBoundary")
-export default class ErrorBoundary extends React.PureComponent {
+export default class ErrorBoundary extends React.PureComponent<{}, IState> {
constructor(props) {
super(props);
@@ -37,13 +43,13 @@ export default class ErrorBoundary extends React.PureComponent {
};
}
- static getDerivedStateFromError(error) {
+ static getDerivedStateFromError(error: Error): Partial {
// Side effects are not permitted here, so we only update the state so
// that the next render shows an error message.
return { error };
}
- componentDidCatch(error, { componentStack }) {
+ componentDidCatch(error: Error, { componentStack }: ErrorInfo): void {
// Browser consoles are better at formatting output when native errors are passed
// in their own `console.error` invocation.
console.error(error);
@@ -53,7 +59,7 @@ export default class ErrorBoundary extends React.PureComponent {
);
}
- _onClearCacheAndReload = () => {
+ private onClearCacheAndReload = (): void => {
if (!PlatformPeg.get()) return;
MatrixClientPeg.get().stopClient();
@@ -62,11 +68,7 @@ export default class ErrorBoundary extends React.PureComponent {
});
};
- _onBugReport = () => {
- const BugReportDialog = sdk.getComponent("dialogs.BugReportDialog");
- if (!BugReportDialog) {
- return;
- }
+ private onBugReport = (): void => {
Modal.createTrackedDialog('Bug Report Dialog', '', BugReportDialog, {
label: 'react-soft-crash',
});
@@ -74,7 +76,6 @@ export default class ErrorBoundary extends React.PureComponent {
render() {
if (this.state.error) {
- const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const newIssueUrl = "https://github.com/vector-im/element-web/issues/new";
let bugReportSection;
@@ -95,7 +96,7 @@ export default class ErrorBoundary extends React.PureComponent {
"the rooms or groups you have visited and the usernames of " +
"other users. They do not contain messages.",
)}
-
+
{_t("Submit debug logs")}
;
@@ -105,7 +106,7 @@ export default class ErrorBoundary extends React.PureComponent {
{_t("Something went wrong!")}
{ bugReportSection }
-
+
{_t("Clear cache and reload")}
diff --git a/src/components/views/elements/EventListSummary.tsx b/src/components/views/elements/EventListSummary.tsx
index 86d3e082ad..ab647db9ed 100644
--- a/src/components/views/elements/EventListSummary.tsx
+++ b/src/components/views/elements/EventListSummary.tsx
@@ -14,13 +14,13 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import React, {ReactChildren, useEffect} from 'react';
-import {MatrixEvent} from "matrix-js-sdk/src/models/event";
-import {RoomMember} from "matrix-js-sdk/src/models/room-member";
+import React, { ReactNode, useEffect } from 'react';
+import { MatrixEvent } from "matrix-js-sdk/src/models/event";
+import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import MemberAvatar from '../avatars/MemberAvatar';
import { _t } from '../../../languageHandler';
-import {useStateToggle} from "../../../hooks/useStateToggle";
+import { useStateToggle } from "../../../hooks/useStateToggle";
import AccessibleButton from "./AccessibleButton";
interface IProps {
@@ -31,11 +31,11 @@ interface IProps {
// Whether or not to begin with state.expanded=true
startExpanded?: boolean,
// The list of room members for which to show avatars next to the summary
- summaryMembers?: RoomMember[],
+ summaryMembers?: RoomMember[];
// The text to show as the summary of this event list
- summaryText?: string,
+ summaryText?: string;
// An array of EventTiles to render when expanded
- children: ReactChildren,
+ children: ReactNode[];
// Called when the event list expansion is toggled
onToggle?(): void;
}
diff --git a/src/components/views/elements/EventTilePreview.tsx b/src/components/views/elements/EventTilePreview.tsx
index cf3b7a6e61..8e73b3d9ca 100644
--- a/src/components/views/elements/EventTilePreview.tsx
+++ b/src/components/views/elements/EventTilePreview.tsx
@@ -17,13 +17,14 @@ limitations under the License.
import React from 'react';
import classnames from 'classnames';
import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
+import { RoomMember } from 'matrix-js-sdk/src/models/room-member';
import * as Avatar from '../../../Avatar';
import EventTile from '../rooms/EventTile';
import SettingsStore from "../../../settings/SettingsStore";
-import {Layout} from "../../../settings/Layout";
-import {UIFeature} from "../../../settings/UIFeature";
-import {replaceableComponent} from "../../../utils/replaceableComponent";
+import { Layout } from "../../../settings/Layout";
+import { UIFeature } from "../../../settings/UIFeature";
+import { replaceableComponent } from "../../../utils/replaceableComponent";
interface IProps {
/**
@@ -105,12 +106,12 @@ export default class EventTilePreview extends React.Component {
userId: this.props.userId,
getAvatarUrl: (..._) => {
return Avatar.avatarUrlForUser(
- { avatarUrl: this.props.avatarUrl },
+ {avatarUrl: this.props.avatarUrl},
AVATAR_SIZE, AVATAR_SIZE, "crop",
);
},
getMxcAvatarUrl: () => this.props.avatarUrl,
- };
+ } as RoomMember;
return event;
}
diff --git a/src/components/views/elements/MemberEventListSummary.tsx b/src/components/views/elements/MemberEventListSummary.tsx
index f10884ce9d..8d411c5f6c 100644
--- a/src/components/views/elements/MemberEventListSummary.tsx
+++ b/src/components/views/elements/MemberEventListSummary.tsx
@@ -16,7 +16,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import React, { ReactChildren } from 'react';
+import React, { ComponentProps } from 'react';
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
@@ -26,21 +26,11 @@ import { isValid3pidInvite } from "../../../RoomInvite";
import EventListSummary from "./EventListSummary";
import { replaceableComponent } from "../../../utils/replaceableComponent";
-interface IProps {
- // An array of member events to summarise
- events: MatrixEvent[];
+interface IProps extends Omit, "summaryText" | "summaryMembers"> {
// The maximum number of names to show in either each summary e.g. 2 would result "A, B and 234 others left"
summaryLength?: number;
// The maximum number of avatars to display in the summary
avatarsMaxLength?: number;
- // The minimum number of events needed to trigger summarisation
- threshold?: number,
- // Whether or not to begin with state.expanded=true
- startExpanded?: boolean,
- // An array of EventTiles to render when expanded
- children: ReactChildren;
- // Called when the MELS expansion is toggled
- onToggle?(): void,
}
interface IUserEvents {
diff --git a/src/components/views/messages/DateSeparator.js b/src/components/views/messages/DateSeparator.tsx
similarity index 82%
rename from src/components/views/messages/DateSeparator.js
rename to src/components/views/messages/DateSeparator.tsx
index 82ce8dc4ae..5d43e2182d 100644
--- a/src/components/views/messages/DateSeparator.js
+++ b/src/components/views/messages/DateSeparator.tsx
@@ -1,6 +1,6 @@
/*
-Copyright 2015, 2016 OpenMarket Ltd
Copyright 2018 Michael Telatynski <7t3chguy@gmail.com>
+Copyright 2015 - 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -16,12 +16,12 @@ limitations under the License.
*/
import React from 'react';
-import PropTypes from 'prop-types';
-import { _t } from '../../../languageHandler';
-import {formatFullDateNoTime} from '../../../DateUtils';
-import {replaceableComponent} from "../../../utils/replaceableComponent";
-function getdaysArray() {
+import { _t } from '../../../languageHandler';
+import { formatFullDateNoTime } from '../../../DateUtils';
+import { replaceableComponent } from "../../../utils/replaceableComponent";
+
+function getDaysArray(): string[] {
return [
_t('Sunday'),
_t('Monday'),
@@ -33,17 +33,17 @@ function getdaysArray() {
];
}
-@replaceableComponent("views.messages.DateSeparator")
-export default class DateSeparator extends React.Component {
- static propTypes = {
- ts: PropTypes.number.isRequired,
- };
+interface IProps {
+ ts: number;
+}
- getLabel() {
+@replaceableComponent("views.messages.DateSeparator")
+export default class DateSeparator extends React.Component {
+ private getLabel() {
const date = new Date(this.props.ts);
const today = new Date();
const yesterday = new Date();
- const days = getdaysArray();
+ const days = getDaysArray();
yesterday.setDate(today.getDate() - 1);
if (date.toDateString() === today.toDateString()) {
diff --git a/src/components/views/messages/TileErrorBoundary.js b/src/components/views/messages/TileErrorBoundary.tsx
similarity index 77%
rename from src/components/views/messages/TileErrorBoundary.js
rename to src/components/views/messages/TileErrorBoundary.tsx
index 0e9a7b6128..967127d275 100644
--- a/src/components/views/messages/TileErrorBoundary.js
+++ b/src/components/views/messages/TileErrorBoundary.tsx
@@ -1,5 +1,5 @@
/*
-Copyright 2020 The Matrix.org Foundation C.I.C.
+Copyright 2020 - 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -16,14 +16,24 @@ limitations under the License.
import React from 'react';
import classNames from 'classnames';
+import { MatrixEvent } from "matrix-js-sdk/src/models/event";
+
import { _t } from '../../../languageHandler';
-import * as sdk from '../../../index';
import Modal from '../../../Modal';
import SdkConfig from "../../../SdkConfig";
-import {replaceableComponent} from "../../../utils/replaceableComponent";
+import { replaceableComponent } from "../../../utils/replaceableComponent";
+import BugReportDialog from '../dialogs/BugReportDialog';
+
+interface IProps {
+ mxEvent: MatrixEvent;
+}
+
+interface IState {
+ error: Error;
+}
@replaceableComponent("views.messages.TileErrorBoundary")
-export default class TileErrorBoundary extends React.Component {
+export default class TileErrorBoundary extends React.Component {
constructor(props) {
super(props);
@@ -32,17 +42,13 @@ export default class TileErrorBoundary extends React.Component {
};
}
- static getDerivedStateFromError(error) {
+ static getDerivedStateFromError(error: Error): Partial {
// Side effects are not permitted here, so we only update the state so
// that the next render shows an error message.
return { error };
}
- _onBugReport = () => {
- const BugReportDialog = sdk.getComponent("dialogs.BugReportDialog");
- if (!BugReportDialog) {
- return;
- }
+ private onBugReport = (): void => {
Modal.createTrackedDialog('Bug Report Dialog', '', BugReportDialog, {
label: 'react-soft-crash-tile',
});
@@ -60,7 +66,7 @@ export default class TileErrorBoundary extends React.Component {
let submitLogsButton;
if (SdkConfig.get().bug_report_endpoint_url) {
- submitLogsButton =
+ submitLogsButton =
{_t("Submit logs")}
;
}
diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx
index 3d674efe04..6c306904f5 100644
--- a/src/components/views/rooms/EventTile.tsx
+++ b/src/components/views/rooms/EventTile.tsx
@@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import React from 'react';
+import React, { createRef } from 'react';
import classNames from "classnames";
import { EventType } from "matrix-js-sdk/src/@types/event";
import { EventStatus, MatrixEvent } from "matrix-js-sdk/src/models/event";
@@ -176,12 +176,19 @@ const MAX_READ_AVATARS = 5;
// | '--------------------------------------' |
// '----------------------------------------------------------'
-interface IReadReceiptProps {
+export interface IReadReceiptProps {
userId: string;
roomMember: RoomMember;
ts: number;
}
+export enum TileShape {
+ Notif = "notif",
+ FileGrid = "file_grid",
+ Reply = "reply",
+ ReplyPreview = "reply_preview",
+}
+
interface IProps {
// the MatrixEvent to show
mxEvent: MatrixEvent;
@@ -248,7 +255,7 @@ interface IProps {
// It could also be done by subclassing EventTile, but that'd be quite
// boiilerplatey. So just make the necessary render decisions conditional
// for now.
- tileShape?: 'notif' | 'file_grid' | 'reply' | 'reply_preview';
+ tileShape?: TileShape;
// show twelve hour timestamps
isTwelveHour?: boolean;
@@ -306,10 +313,11 @@ interface IState {
export default class EventTile extends React.Component {
private suppressReadReceiptAnimation: boolean;
private isListeningForReceipts: boolean;
- private ref: React.RefObject;
private tile = React.createRef();
private replyThread = React.createRef();
+ public readonly ref = createRef();
+
static defaultProps = {
// no-op function because onHeightChanged is optional yet some sub-components assume its existence
onHeightChanged: function() {},
@@ -345,8 +353,6 @@ export default class EventTile extends React.Component {
// to determine if we've already subscribed and use a combination of other flags to find
// out if we should even be subscribed at all.
this.isListeningForReceipts = false;
-
- this.ref = React.createRef();
}
/**