{
- const {command, range} = this.getCurrentCommand(query, selection);
+ const { command, range } = this.getCurrentCommand(query, selection);
if (!query || !command) {
return [];
}
diff --git a/src/autocomplete/EmojiProvider.tsx b/src/autocomplete/EmojiProvider.tsx
index b7c4a5120a..2fc77e9a17 100644
--- a/src/autocomplete/EmojiProvider.tsx
+++ b/src/autocomplete/EmojiProvider.tsx
@@ -21,9 +21,9 @@ import React from 'react';
import { _t } from '../languageHandler';
import AutocompleteProvider from './AutocompleteProvider';
import QueryMatcher from './QueryMatcher';
-import {PillCompletion} from './Components';
-import {ICompletion, ISelectionRange} from './Autocompleter';
-import {uniq, sortBy} from 'lodash';
+import { PillCompletion } from './Components';
+import { ICompletion, ISelectionRange } from './Autocompleter';
+import { uniq, sortBy } from 'lodash';
import SettingsStore from "../settings/SettingsStore";
import { shortcodeToUnicode } from '../HtmlUtils';
import { EMOJI, IEmoji } from '../emoji';
@@ -95,7 +95,7 @@ export default class EmojiProvider extends AutocompleteProvider {
}
let completions = [];
- const {command, range} = this.getCurrentCommand(query, selection);
+ const { command, range } = this.getCurrentCommand(query, selection);
if (command) {
const matchedString = command[0];
completions = this.matcher.match(matchedString, limit);
@@ -121,7 +121,7 @@ export default class EmojiProvider extends AutocompleteProvider {
sorters.push((c) => c._orderBy);
completions = sortBy(uniq(completions), sorters);
- completions = completions.map(({shortname}) => {
+ completions = completions.map(({ shortname }) => {
const unicode = shortcodeToUnicode(shortname);
return {
completion: unicode,
diff --git a/src/autocomplete/NotifProvider.tsx b/src/autocomplete/NotifProvider.tsx
index 0bc7ead097..1d42915ec9 100644
--- a/src/autocomplete/NotifProvider.tsx
+++ b/src/autocomplete/NotifProvider.tsx
@@ -15,13 +15,14 @@ limitations under the License.
*/
import React from 'react';
-import Room from "matrix-js-sdk/src/models/room";
+import { Room } from "matrix-js-sdk/src/models/room";
+
import AutocompleteProvider from './AutocompleteProvider';
import { _t } from '../languageHandler';
-import {MatrixClientPeg} from '../MatrixClientPeg';
-import {PillCompletion} from './Components';
+import { MatrixClientPeg } from '../MatrixClientPeg';
+import { PillCompletion } from './Components';
import * as sdk from '../index';
-import {ICompletion, ISelectionRange} from "./Autocompleter";
+import { ICompletion, ISelectionRange } from "./Autocompleter";
const AT_ROOM_REGEX = /@\S*/g;
@@ -45,7 +46,7 @@ export default class NotifProvider extends AutocompleteProvider {
if (!this.room.currentState.mayTriggerNotifOfType('room', client.credentials.userId)) return [];
- const {command, range} = this.getCurrentCommand(query, selection, force);
+ const { command, range } = this.getCurrentCommand(query, selection, force);
if (command && command[0] && '@room'.startsWith(command[0]) && command[0].length > 1) {
return [{
completion: '@room',
diff --git a/src/autocomplete/QueryMatcher.ts b/src/autocomplete/QueryMatcher.ts
index 73bb37ff0f..3948be301c 100644
--- a/src/autocomplete/QueryMatcher.ts
+++ b/src/autocomplete/QueryMatcher.ts
@@ -16,8 +16,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import {at, uniq} from 'lodash';
-import {removeHiddenChars} from "matrix-js-sdk/src/utils";
+import { at, uniq } from 'lodash';
+import { removeHiddenChars } from "matrix-js-sdk/src/utils";
interface IOptions {
keys: Array;
@@ -112,7 +112,7 @@ export default class QueryMatcher {
const index = resultKey.indexOf(query);
if (index !== -1) {
matches.push(
- ...candidates.map((candidate) => ({index, ...candidate})),
+ ...candidates.map((candidate) => ({ index, ...candidate })),
);
}
}
diff --git a/src/autocomplete/RoomProvider.tsx b/src/autocomplete/RoomProvider.tsx
index ad55b19101..7865a76daa 100644
--- a/src/autocomplete/RoomProvider.tsx
+++ b/src/autocomplete/RoomProvider.tsx
@@ -17,28 +17,24 @@ limitations under the License.
*/
import React from "react";
-import {uniqBy, sortBy} from "lodash";
-import Room from "matrix-js-sdk/src/models/room";
+import { uniqBy, sortBy } from "lodash";
+import { Room } from "matrix-js-sdk/src/models/room";
import { _t } from '../languageHandler';
import AutocompleteProvider from './AutocompleteProvider';
-import {MatrixClientPeg} from '../MatrixClientPeg';
+import { MatrixClientPeg } from '../MatrixClientPeg';
import QueryMatcher from './QueryMatcher';
-import {PillCompletion} from './Components';
-import {makeRoomPermalink} from "../utils/permalinks/Permalinks";
-import {ICompletion, ISelectionRange} from "./Autocompleter";
+import { PillCompletion } from './Components';
+import { makeRoomPermalink } from "../utils/permalinks/Permalinks";
+import { ICompletion, ISelectionRange } from "./Autocompleter";
import RoomAvatar from '../components/views/avatars/RoomAvatar';
import SettingsStore from "../settings/SettingsStore";
const ROOM_REGEX = /\B#\S*/g;
-function score(query: string, space: string) {
- const index = space.indexOf(query);
- if (index === -1) {
- return Infinity;
- } else {
- return index;
- }
+// Prefer canonical aliases over non-canonical ones
+function canonicalScore(displayedAlias: string, room: Room): number {
+ return displayedAlias === room.getCanonicalAlias() ? 0 : 1;
}
function matcherObject(room: Room, displayedAlias: string, matchName = "") {
@@ -77,7 +73,7 @@ export default class RoomProvider extends AutocompleteProvider {
limit = -1,
): Promise {
let completions = [];
- const {command, range} = this.getCurrentCommand(query, selection, force);
+ const { command, range } = this.getCurrentCommand(query, selection, force);
if (command) {
// the only reason we need to do this is because Fuse only matches on properties
let matcherObjects = this.getRooms().reduce((aliases, room) => {
@@ -106,7 +102,7 @@ export default class RoomProvider extends AutocompleteProvider {
const matchedString = command[0];
completions = this.matcher.match(matchedString, limit);
completions = sortBy(completions, [
- (c) => score(matchedString, c.displayedAlias),
+ (c) => canonicalScore(c.displayedAlias, c.room),
(c) => c.displayedAlias.length,
]);
completions = uniqBy(completions, (match) => match.room);
diff --git a/src/autocomplete/SpaceProvider.tsx b/src/autocomplete/SpaceProvider.tsx
index 0361a2c91e..1c99aee5ac 100644
--- a/src/autocomplete/SpaceProvider.tsx
+++ b/src/autocomplete/SpaceProvider.tsx
@@ -17,7 +17,7 @@ limitations under the License.
import React from "react";
import { _t } from '../languageHandler';
-import {MatrixClientPeg} from '../MatrixClientPeg';
+import { MatrixClientPeg } from '../MatrixClientPeg';
import RoomProvider from "./RoomProvider";
export default class SpaceProvider extends RoomProvider {
diff --git a/src/autocomplete/UserProvider.tsx b/src/autocomplete/UserProvider.tsx
index 3cf43d0b84..470e018e22 100644
--- a/src/autocomplete/UserProvider.tsx
+++ b/src/autocomplete/UserProvider.tsx
@@ -20,19 +20,19 @@ limitations under the License.
import React from 'react';
import { _t } from '../languageHandler';
import AutocompleteProvider from './AutocompleteProvider';
-import {PillCompletion} from './Components';
+import { PillCompletion } from './Components';
import * as sdk from '../index';
import QueryMatcher from './QueryMatcher';
-import {sortBy} from 'lodash';
-import {MatrixClientPeg} from '../MatrixClientPeg';
+import { sortBy } from 'lodash';
+import { MatrixClientPeg } from '../MatrixClientPeg';
-import MatrixEvent from "matrix-js-sdk/src/models/event";
-import Room from "matrix-js-sdk/src/models/room";
-import RoomMember from "matrix-js-sdk/src/models/room-member";
-import RoomState from "matrix-js-sdk/src/models/room-state";
-import EventTimeline from "matrix-js-sdk/src/models/event-timeline";
-import {makeUserPermalink} from "../utils/permalinks/Permalinks";
-import {ICompletion, ISelectionRange} from "./Autocompleter";
+import { MatrixEvent } from "matrix-js-sdk/src/models/event";
+import { Room } from "matrix-js-sdk/src/models/room";
+import { RoomMember } from "matrix-js-sdk/src/models/room-member";
+import { RoomState } from "matrix-js-sdk/src/models/room-state";
+import { EventTimeline } from "matrix-js-sdk/src/models/event-timeline";
+import { makeUserPermalink } from "../utils/permalinks/Permalinks";
+import { ICompletion, ISelectionRange } from "./Autocompleter";
const USER_REGEX = /\B@\S*/g;
@@ -114,7 +114,7 @@ export default class UserProvider extends AutocompleteProvider {
if (!this.users) this._makeUsers();
let completions = [];
- const {command, range} = this.getCurrentCommand(rawQuery, selection, force);
+ const { command, range } = this.getCurrentCommand(rawQuery, selection, force);
if (!command) return completions;
@@ -158,7 +158,7 @@ export default class UserProvider extends AutocompleteProvider {
}
const currentUserId = MatrixClientPeg.get().credentials.userId;
- this.users = this.room.getJoinedMembers().filter(({userId}) => userId !== currentUserId);
+ this.users = this.room.getJoinedMembers().filter(({ userId }) => userId !== currentUserId);
this.users = this.users.concat(this.room.getMembersWithMembership("invite"));
this.users = sortBy(this.users, (member) => 1E20 - lastSpoken[member.userId] || 1E20);
diff --git a/src/components/structures/AutoHideScrollbar.js b/src/components/structures/AutoHideScrollbar.js
deleted file mode 100644
index 14f7c9ca83..0000000000
--- a/src/components/structures/AutoHideScrollbar.js
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
-Copyright 2018 New Vector Ltd
-Copyright 2020 The Matrix.org Foundation C.I.C.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-import React from "react";
-
-export default class AutoHideScrollbar extends React.Component {
- constructor(props) {
- super(props);
- this._collectContainerRef = this._collectContainerRef.bind(this);
- }
-
- _collectContainerRef(ref) {
- if (ref && !this.containerRef) {
- this.containerRef = ref;
- }
- if (this.props.wrappedRef) {
- this.props.wrappedRef(ref);
- }
- }
-
- getScrollTop() {
- return this.containerRef.scrollTop;
- }
-
- render() {
- return (
- { this.props.children }
-
);
- }
-}
diff --git a/src/components/structures/AutoHideScrollbar.tsx b/src/components/structures/AutoHideScrollbar.tsx
new file mode 100644
index 0000000000..e8a9872b48
--- /dev/null
+++ b/src/components/structures/AutoHideScrollbar.tsx
@@ -0,0 +1,69 @@
+/*
+Copyright 2018 New Vector Ltd
+Copyright 2020 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import React, { HTMLAttributes, WheelEvent } from "react";
+
+interface IProps extends Omit, "onScroll"> {
+ className?: string;
+ onScroll?: (event: Event) => void;
+ onWheel?: (event: WheelEvent) => void;
+ style?: React.CSSProperties
+ tabIndex?: number,
+ wrappedRef?: (ref: HTMLDivElement) => void;
+}
+
+export default class AutoHideScrollbar extends React.Component {
+ private containerRef: React.RefObject = React.createRef();
+
+ public componentDidMount() {
+ if (this.containerRef.current && this.props.onScroll) {
+ // Using the passive option to not block the main thread
+ // https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#improving_scrolling_performance_with_passive_listeners
+ this.containerRef.current.addEventListener("scroll", this.props.onScroll, { passive: true });
+ }
+
+ if (this.props.wrappedRef) {
+ this.props.wrappedRef(this.containerRef.current);
+ }
+ }
+
+ public componentWillUnmount() {
+ if (this.containerRef.current && this.props.onScroll) {
+ this.containerRef.current.removeEventListener("scroll", this.props.onScroll);
+ }
+ }
+
+ public getScrollTop(): number {
+ return this.containerRef.current.scrollTop;
+ }
+
+ public render() {
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ const { className, onScroll, onWheel, style, tabIndex, wrappedRef, children, ...otherProps } = this.props;
+
+ return (
+ { children }
+
);
+ }
+}
diff --git a/src/components/structures/ContextMenu.tsx b/src/components/structures/ContextMenu.tsx
index ad0f75e162..407dc6f04c 100644
--- a/src/components/structures/ContextMenu.tsx
+++ b/src/components/structures/ContextMenu.tsx
@@ -16,13 +16,14 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import React, {CSSProperties, RefObject, useRef, useState} from "react";
+import React, { CSSProperties, RefObject, useRef, useState } from "react";
import ReactDOM from "react-dom";
import classNames from "classnames";
-import {Key} from "../../Keyboard";
-import {Writeable} from "../../@types/common";
-import {replaceableComponent} from "../../utils/replaceableComponent";
+import { Key } from "../../Keyboard";
+import { Writeable } from "../../@types/common";
+import { replaceableComponent } from "../../utils/replaceableComponent";
+import UIStore from "../../stores/UIStore";
// Shamelessly ripped off Modal.js. There's probably a better way
// of doing reusable widgets like dialog boxes & menus where we go and
@@ -370,7 +371,7 @@ export class ContextMenu extends React.PureComponent {
return (
@@ -398,7 +399,7 @@ export const toRightOf = (elementRect: Pick
const left = elementRect.right + window.pageXOffset + 3;
let top = elementRect.top + (elementRect.height / 2) + window.pageYOffset;
top -= chevronOffset + 8; // where 8 is half the height of the chevron
- return {left, top, chevronOffset};
+ return { left, top, chevronOffset };
};
// Placement method for to position context menu right-aligned and flowing to the left of elementRect,
@@ -410,12 +411,12 @@ export const aboveLeftOf = (elementRect: DOMRect, chevronFace = ChevronFace.None
const buttonBottom = elementRect.bottom + window.pageYOffset;
const buttonTop = elementRect.top + window.pageYOffset;
// Align the right edge of the menu to the right edge of the button
- menuOptions.right = window.innerWidth - buttonRight;
+ menuOptions.right = UIStore.instance.windowWidth - buttonRight;
// Align the menu vertically on whichever side of the button has more space available.
- if (buttonBottom < window.innerHeight / 2) {
+ if (buttonBottom < UIStore.instance.windowHeight / 2) {
menuOptions.top = buttonBottom + vPadding;
} else {
- menuOptions.bottom = (window.innerHeight - buttonTop) + vPadding;
+ menuOptions.bottom = (UIStore.instance.windowHeight - buttonTop) + vPadding;
}
return menuOptions;
@@ -430,12 +431,12 @@ export const alwaysAboveLeftOf = (elementRect: DOMRect, chevronFace = ChevronFac
const buttonBottom = elementRect.bottom + window.pageYOffset;
const buttonTop = elementRect.top + window.pageYOffset;
// Align the right edge of the menu to the right edge of the button
- menuOptions.right = window.innerWidth - buttonRight;
+ menuOptions.right = UIStore.instance.windowWidth - buttonRight;
// Align the menu vertically on whichever side of the button has more space available.
- if (buttonBottom < window.innerHeight / 2) {
+ if (buttonBottom < UIStore.instance.windowHeight / 2) {
menuOptions.top = buttonBottom + vPadding;
} else {
- menuOptions.bottom = (window.innerHeight - buttonTop) + vPadding;
+ menuOptions.bottom = (UIStore.instance.windowHeight - buttonTop) + vPadding;
}
return menuOptions;
@@ -451,7 +452,7 @@ export const alwaysAboveRightOf = (elementRect: DOMRect, chevronFace = ChevronFa
// Align the left edge of the menu to the left edge of the button
menuOptions.left = buttonLeft;
// Align the menu vertically above the menu
- menuOptions.bottom = (window.innerHeight - buttonTop) + vPadding;
+ menuOptions.bottom = (UIStore.instance.windowHeight - buttonTop) + vPadding;
return menuOptions;
};
@@ -497,15 +498,15 @@ export function createMenu(ElementClass, props) {
ReactDOM.render(menu, getOrCreateContainer());
- return {close: onFinished};
+ return { close: onFinished };
}
// re-export the semantic helper components for simplicity
-export {ContextMenuButton} from "../../accessibility/context_menu/ContextMenuButton";
-export {ContextMenuTooltipButton} from "../../accessibility/context_menu/ContextMenuTooltipButton";
-export {MenuGroup} from "../../accessibility/context_menu/MenuGroup";
-export {MenuItem} from "../../accessibility/context_menu/MenuItem";
-export {MenuItemCheckbox} from "../../accessibility/context_menu/MenuItemCheckbox";
-export {MenuItemRadio} from "../../accessibility/context_menu/MenuItemRadio";
-export {StyledMenuItemCheckbox} from "../../accessibility/context_menu/StyledMenuItemCheckbox";
-export {StyledMenuItemRadio} from "../../accessibility/context_menu/StyledMenuItemRadio";
+export { ContextMenuButton } from "../../accessibility/context_menu/ContextMenuButton";
+export { ContextMenuTooltipButton } from "../../accessibility/context_menu/ContextMenuTooltipButton";
+export { MenuGroup } from "../../accessibility/context_menu/MenuGroup";
+export { MenuItem } from "../../accessibility/context_menu/MenuItem";
+export { MenuItemCheckbox } from "../../accessibility/context_menu/MenuItemCheckbox";
+export { MenuItemRadio } from "../../accessibility/context_menu/MenuItemRadio";
+export { StyledMenuItemCheckbox } from "../../accessibility/context_menu/StyledMenuItemCheckbox";
+export { StyledMenuItemRadio } from "../../accessibility/context_menu/StyledMenuItemRadio";
diff --git a/src/components/structures/CustomRoomTagPanel.js b/src/components/structures/CustomRoomTagPanel.js
index 73359f17a5..037d7c251c 100644
--- a/src/components/structures/CustomRoomTagPanel.js
+++ b/src/components/structures/CustomRoomTagPanel.js
@@ -21,7 +21,7 @@ import * as sdk from '../../index';
import dis from '../../dispatcher/dispatcher';
import classNames from 'classnames';
import * as FormattingUtils from '../../utils/FormattingUtils';
-import {replaceableComponent} from "../../utils/replaceableComponent";
+import { replaceableComponent } from "../../utils/replaceableComponent";
@replaceableComponent("structures.CustomRoomTagPanel")
class CustomRoomTagPanel extends React.Component {
@@ -34,7 +34,7 @@ class CustomRoomTagPanel extends React.Component {
componentDidMount() {
this._tagStoreToken = CustomRoomTagStore.addListener(() => {
- this.setState({tags: CustomRoomTagStore.getSortedTags()});
+ this.setState({ tags: CustomRoomTagStore.getSortedTags() });
});
}
@@ -64,7 +64,7 @@ class CustomRoomTagPanel extends React.Component {
class CustomRoomTagTile extends React.Component {
onClick = () => {
- dis.dispatch({action: 'select_custom_room_tag', tag: this.props.tag.name});
+ dis.dispatch({ action: 'select_custom_room_tag', tag: this.props.tag.name });
};
render() {
diff --git a/src/components/structures/EmbeddedPage.js b/src/components/structures/EmbeddedPage.js
index c37ab3df48..628c16f322 100644
--- a/src/components/structures/EmbeddedPage.js
+++ b/src/components/structures/EmbeddedPage.js
@@ -22,7 +22,7 @@ import request from 'browser-request';
import { _t } from '../../languageHandler';
import sanitizeHtml from 'sanitize-html';
import dis from '../../dispatcher/dispatcher';
-import {MatrixClientPeg} from '../../MatrixClientPeg';
+import { MatrixClientPeg } from '../../MatrixClientPeg';
import classnames from 'classnames';
import MatrixClientContext from "../../contexts/MatrixClientContext";
import AutoHideScrollbar from "./AutoHideScrollbar";
diff --git a/src/components/structures/FilePanel.js b/src/components/structures/FilePanel.tsx
similarity index 85%
rename from src/components/structures/FilePanel.js
rename to src/components/structures/FilePanel.tsx
index bb7c1f9642..21ef0c4f31 100644
--- a/src/components/structures/FilePanel.js
+++ b/src/components/structures/FilePanel.tsx
@@ -16,37 +16,49 @@ limitations under the License.
*/
import React from 'react';
-import PropTypes from 'prop-types';
-import {Filter} from 'matrix-js-sdk/src/filter';
+import { Filter } from 'matrix-js-sdk/src/filter';
+import { EventTimelineSet } from "matrix-js-sdk/src/models/event-timeline-set";
+import { MatrixEvent } from "matrix-js-sdk/src/models/event";
+import { Room } from 'matrix-js-sdk/src/models/room';
+import { TimelineWindow } from 'matrix-js-sdk/src/timeline-window';
+
import * as sdk from '../../index';
-import {MatrixClientPeg} from '../../MatrixClientPeg';
+import { MatrixClientPeg } from '../../MatrixClientPeg';
import EventIndexPeg from "../../indexing/EventIndexPeg";
import { _t } from '../../languageHandler';
import BaseCard from "../views/right_panel/BaseCard";
-import {RightPanelPhases} from "../../stores/RightPanelStorePhases";
-import DesktopBuildsNotice, {WarningKind} from "../views/elements/DesktopBuildsNotice";
-import {replaceableComponent} from "../../utils/replaceableComponent";
+import { RightPanelPhases } from "../../stores/RightPanelStorePhases";
+import DesktopBuildsNotice, { WarningKind } from "../views/elements/DesktopBuildsNotice";
+import { replaceableComponent } from "../../utils/replaceableComponent";
+
+import ResizeNotifier from '../../utils/ResizeNotifier';
+
+interface IProps {
+ roomId: string;
+ onClose: () => void;
+ resizeNotifier: ResizeNotifier
+}
+
+interface IState {
+ timelineSet: EventTimelineSet;
+}
/*
* Component which shows the filtered file using a TimelinePanel
*/
@replaceableComponent("structures.FilePanel")
-class FilePanel extends React.Component {
- static propTypes = {
- roomId: PropTypes.string.isRequired,
- onClose: PropTypes.func.isRequired,
- };
-
+class FilePanel extends React.Component {
// This is used to track if a decrypted event was a live event and should be
// added to the timeline.
- decryptingEvents = new Set();
+ private decryptingEvents = new Set();
+ public noRoom: boolean;
state = {
timelineSet: null,
};
- onRoomTimeline = (ev, room, toStartOfTimeline, removed, data) => {
+ private onRoomTimeline = (ev: MatrixEvent, room: Room, toStartOfTimeline: true, removed: true, data: any): void => {
if (room?.roomId !== this.props?.roomId) return;
if (toStartOfTimeline || !data || !data.liveEvent || ev.isRedacted()) return;
@@ -60,7 +72,7 @@ class FilePanel extends React.Component {
}
};
- onEventDecrypted = (ev, err) => {
+ private onEventDecrypted = (ev: MatrixEvent, err?: any): void => {
if (ev.getRoomId() !== this.props.roomId) return;
const eventId = ev.getId();
@@ -70,7 +82,7 @@ class FilePanel extends React.Component {
this.addEncryptedLiveEvent(ev);
};
- addEncryptedLiveEvent(ev, toStartOfTimeline) {
+ public addEncryptedLiveEvent(ev: MatrixEvent): void {
if (!this.state.timelineSet) return;
const timeline = this.state.timelineSet.getLiveTimeline();
@@ -84,7 +96,7 @@ class FilePanel extends React.Component {
}
}
- async componentDidMount() {
+ public async componentDidMount(): Promise {
const client = MatrixClientPeg.get();
await this.updateTimelineSet(this.props.roomId);
@@ -105,7 +117,7 @@ class FilePanel extends React.Component {
}
}
- componentWillUnmount() {
+ public componentWillUnmount(): void {
const client = MatrixClientPeg.get();
if (client === null) return;
@@ -117,7 +129,7 @@ class FilePanel extends React.Component {
}
}
- async fetchFileEventsServer(room) {
+ public async fetchFileEventsServer(room: Room): Promise {
const client = MatrixClientPeg.get();
const filter = new Filter(client.credentials.userId);
@@ -141,7 +153,7 @@ class FilePanel extends React.Component {
return timelineSet;
}
- onPaginationRequest = (timelineWindow, direction, limit) => {
+ private onPaginationRequest = (timelineWindow: TimelineWindow, direction: string, limit: number): void => {
const client = MatrixClientPeg.get();
const eventIndex = EventIndexPeg.get();
const roomId = this.props.roomId;
@@ -159,7 +171,7 @@ class FilePanel extends React.Component {
}
};
- async updateTimelineSet(roomId: string) {
+ public async updateTimelineSet(roomId: string): Promise {
const client = MatrixClientPeg.get();
const room = client.getRoom(roomId);
const eventIndex = EventIndexPeg.get();
@@ -195,7 +207,7 @@ class FilePanel extends React.Component {
}
}
- render() {
+ public render() {
if (MatrixClientPeg.get().isGuest()) {
return {
+ onClick = e => {
// only dispatch if its not a no-op
if (this.state.selectedTags.length > 0) {
- dis.dispatch({action: 'deselect_tags'});
+ dis.dispatch({ action: 'deselect_tags' });
}
};
onClearFilterClick = ev => {
- dis.dispatch({action: 'deselect_tags'});
+ dis.dispatch({ action: 'deselect_tags' });
};
renderGlobalIcon() {
@@ -151,28 +150,15 @@ class GroupFilterPanel extends React.Component {
return
-
- { (provided, snapshot) => (
-
- { this.renderGlobalIcon() }
- { tags }
-
- {createButton}
-
- { provided.placeholder }
-
- ) }
-
+
+ { this.renderGlobalIcon() }
+ { tags }
+
+ { createButton }
+
+
;
}
diff --git a/src/components/structures/GroupView.js b/src/components/structures/GroupView.js
index 3ab009d7b8..93c44c4e50 100644
--- a/src/components/structures/GroupView.js
+++ b/src/components/structures/GroupView.js
@@ -18,7 +18,7 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
-import {MatrixClientPeg} from '../../MatrixClientPeg';
+import { MatrixClientPeg } from '../../MatrixClientPeg';
import * as sdk from '../../index';
import dis from '../../dispatcher/dispatcher';
import { getHostingLink } from '../../utils/HostingLink';
@@ -34,13 +34,13 @@ import classnames from 'classnames';
import GroupStore from '../../stores/GroupStore';
import FlairStore from '../../stores/FlairStore';
import { showGroupAddRoomDialog } from '../../GroupAddressPicker';
-import {makeGroupPermalink, makeUserPermalink} from "../../utils/permalinks/Permalinks";
-import {Group} from "matrix-js-sdk/src/models/group";
-import {allSettled, sleep} from "../../utils/promise";
+import { makeGroupPermalink, makeUserPermalink } from "../../utils/permalinks/Permalinks";
+import { Group } from "matrix-js-sdk/src/models/group";
+import { sleep } from "../../utils/promise";
import RightPanelStore from "../../stores/RightPanelStore";
import AutoHideScrollbar from "./AutoHideScrollbar";
-import {mediaFromMxc} from "../../customisations/Media";
-import {replaceableComponent} from "../../utils/replaceableComponent";
+import { mediaFromMxc } from "../../customisations/Media";
+import { replaceableComponent } from "../../utils/replaceableComponent";
const LONG_DESC_PLACEHOLDER = _td(
`
HTML for your community's page
@@ -99,7 +99,7 @@ class CategoryRoomList extends React.Component {
onFinished: (success, addrs) => {
if (!success) return;
const errorList = [];
- allSettled(addrs.map((addr) => {
+ Promise.allSettled(addrs.map((addr) => {
return GroupStore
.addRoomToGroupSummary(this.props.groupId, addr.address)
.catch(() => { errorList.push(addr.address); });
@@ -115,7 +115,7 @@ class CategoryRoomList extends React.Component {
{
title: _t(
"Failed to add the following rooms to the summary of %(groupId)s:",
- {groupId: this.props.groupId},
+ { groupId: this.props.groupId },
),
description: errorList.join(", "),
},
@@ -126,12 +126,11 @@ class CategoryRoomList extends React.Component {
};
render() {
- const TintableSvg = sdk.getComponent("elements.TintableSvg");
const addButton = this.props.editing ?
(
-
+
{ _t('Add a Room') }
@@ -195,9 +194,9 @@ class FeaturedRoom extends React.Component {
{
title: _t(
"Failed to remove the room from the summary of %(groupId)s",
- {groupId: this.props.groupId},
+ { groupId: this.props.groupId },
),
- description: _t("The room '%(roomName)s' could not be removed from the summary.", {roomName}),
+ description: _t("The room '%(roomName)s' could not be removed from the summary.", { roomName }),
},
);
});
@@ -274,7 +273,7 @@ class RoleUserList extends React.Component {
onFinished: (success, addrs) => {
if (!success) return;
const errorList = [];
- allSettled(addrs.map((addr) => {
+ Promise.allSettled(addrs.map((addr) => {
return GroupStore
.addUserToGroupSummary(addr.address)
.catch(() => { errorList.push(addr.address); });
@@ -289,7 +288,7 @@ class RoleUserList extends React.Component {
{
title: _t(
"Failed to add the following users to the summary of %(groupId)s:",
- {groupId: this.props.groupId},
+ { groupId: this.props.groupId },
),
description: errorList.join(", "),
},
@@ -300,10 +299,9 @@ class RoleUserList extends React.Component {
};
render() {
- const TintableSvg = sdk.getComponent("elements.TintableSvg");
const addButton = this.props.editing ?
(
-
+
{ _t('Add a User') }
@@ -361,9 +359,12 @@ class FeaturedUser extends React.Component {
{
title: _t(
"Failed to remove a user from the summary of %(groupId)s",
- {groupId: this.props.groupId},
+ { groupId: this.props.groupId },
+ ),
+ description: _t(
+ "The user '%(displayName)s' could not be removed from the summary.",
+ { displayName },
),
- description: _t("The user '%(displayName)s' could not be removed from the summary.", {displayName}),
},
);
});
@@ -470,7 +471,7 @@ export default class GroupView extends React.Component {
// Leave settings - the user might have clicked the "Leave" button
this._closeSettings();
}
- this.setState({membershipBusy: false});
+ this.setState({ membershipBusy: false });
};
_initGroupStore(groupId, firstInit) {
@@ -491,7 +492,7 @@ export default class GroupView extends React.Component {
group_id: groupId,
},
});
- dis.dispatch({action: 'require_registration', screen_after: {screen: `group/${groupId}`}});
+ dis.dispatch({ action: 'require_registration', screen_after: { screen: `group/${groupId}` } });
willDoOnboarding = true;
}
if (stateKey === GroupStore.STATE_KEY.Summary) {
@@ -592,7 +593,7 @@ export default class GroupView extends React.Component {
};
_closeSettings = () => {
- dis.dispatch({action: 'close_settings'});
+ dis.dispatch({ action: 'close_settings' });
};
_onNameChange = (value) => {
@@ -620,7 +621,7 @@ export default class GroupView extends React.Component {
const file = ev.target.files[0];
if (!file) return;
- this.setState({uploadingAvatar: true});
+ this.setState({ uploadingAvatar: true });
this._matrixClient.uploadContent(file).then((url) => {
const newProfileForm = Object.assign(this.state.profileForm, { avatar_url: url });
this.setState({
@@ -632,7 +633,7 @@ export default class GroupView extends React.Component {
avatarChanged: true,
});
}).catch((e) => {
- this.setState({uploadingAvatar: false});
+ this.setState({ uploadingAvatar: false });
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
console.error("Failed to upload avatar image", e);
Modal.createTrackedDialog('Failed to upload image', '', ErrorDialog, {
@@ -649,7 +650,7 @@ export default class GroupView extends React.Component {
};
_onSaveClick = () => {
- this.setState({saving: true});
+ this.setState({ saving: true });
const savePromise = this.state.isUserPrivileged ? this._saveGroup() : Promise.resolve();
savePromise.then((result) => {
this.setState({
@@ -688,7 +689,7 @@ export default class GroupView extends React.Component {
}
_onAcceptInviteClick = async () => {
- this.setState({membershipBusy: true});
+ this.setState({ membershipBusy: true });
// Wait 500ms to prevent flashing. Do this before sending a request otherwise we risk the
// spinner disappearing after we have fetched new group data.
@@ -697,7 +698,7 @@ export default class GroupView extends React.Component {
GroupStore.acceptGroupInvite(this.props.groupId).then(() => {
// don't reset membershipBusy here: wait for the membership change to come down the sync
}).catch((e) => {
- this.setState({membershipBusy: false});
+ this.setState({ membershipBusy: false });
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Error accepting invite', '', ErrorDialog, {
title: _t("Error"),
@@ -707,7 +708,7 @@ export default class GroupView extends React.Component {
};
_onRejectInviteClick = async () => {
- this.setState({membershipBusy: true});
+ this.setState({ membershipBusy: true });
// Wait 500ms to prevent flashing. Do this before sending a request otherwise we risk the
// spinner disappearing after we have fetched new group data.
@@ -716,7 +717,7 @@ export default class GroupView extends React.Component {
GroupStore.leaveGroup(this.props.groupId).then(() => {
// don't reset membershipBusy here: wait for the membership change to come down the sync
}).catch((e) => {
- this.setState({membershipBusy: false});
+ this.setState({ membershipBusy: false });
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Error rejecting invite', '', ErrorDialog, {
title: _t("Error"),
@@ -727,11 +728,11 @@ export default class GroupView extends React.Component {
_onJoinClick = async () => {
if (this._matrixClient.isGuest()) {
- dis.dispatch({action: 'require_registration', screen_after: {screen: `group/${this.props.groupId}`}});
+ dis.dispatch({ action: 'require_registration', screen_after: { screen: `group/${this.props.groupId}` } });
return;
}
- this.setState({membershipBusy: true});
+ this.setState({ membershipBusy: true });
// Wait 500ms to prevent flashing. Do this before sending a request otherwise we risk the
// spinner disappearing after we have fetched new group data.
@@ -740,7 +741,7 @@ export default class GroupView extends React.Component {
GroupStore.joinGroup(this.props.groupId).then(() => {
// don't reset membershipBusy here: wait for the membership change to come down the sync
}).catch((e) => {
- this.setState({membershipBusy: false});
+ this.setState({ membershipBusy: false });
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Error joining room', '', ErrorDialog, {
title: _t("Error"),
@@ -773,7 +774,7 @@ export default class GroupView extends React.Component {
title: _t("Leave Community"),
description: (
- { _t("Leave %(groupName)s?", {groupName: this.props.groupId}) }
+ { _t("Leave %(groupName)s?", { groupName: this.props.groupId }) }
{ warnings }
),
@@ -782,7 +783,7 @@ export default class GroupView extends React.Component {
onFinished: async (confirmed) => {
if (!confirmed) return;
- this.setState({membershipBusy: true});
+ this.setState({ membershipBusy: true });
// Wait 500ms to prevent flashing. Do this before sending a request otherwise we risk the
// spinner disappearing after we have fetched new group data.
@@ -791,7 +792,7 @@ export default class GroupView extends React.Component {
GroupStore.leaveGroup(this.props.groupId).then(() => {
// don't reset membershipBusy here: wait for the membership change to come down the sync
}).catch((e) => {
- this.setState({membershipBusy: false});
+ this.setState({ membershipBusy: false });
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Error leaving community', '', ErrorDialog, {
title: _t("Error"),
@@ -855,7 +856,6 @@ export default class GroupView extends React.Component {
_getRoomsNode() {
const RoomDetailList = sdk.getComponent('rooms.RoomDetailList');
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
- const TintableSvg = sdk.getComponent('elements.TintableSvg');
const Spinner = sdk.getComponent('elements.Spinner');
const TooltipButton = sdk.getComponent('elements.TooltipButton');
@@ -871,7 +871,7 @@ export default class GroupView extends React.Component {
onClick={this._onAddRoomsClick}
>
-
+
{ _t('Add rooms to this community') }
@@ -1336,7 +1336,7 @@ export default class GroupView extends React.Component {
if (this.state.error.httpStatus === 404) {
return (
- { _t('Community %(groupId)s not found', {groupId: this.props.groupId}) }
+ { _t('Community %(groupId)s not found', { groupId: this.props.groupId }) }
diff --git a/src/components/structures/MainSplit.js b/src/components/structures/MainSplit.js
index 5818d303fc..69d3bd0b51 100644
--- a/src/components/structures/MainSplit.js
+++ b/src/components/structures/MainSplit.js
@@ -17,7 +17,7 @@ limitations under the License.
import React from 'react';
import { Resizable } from 're-resizable';
-import {replaceableComponent} from "../../utils/replaceableComponent";
+import { replaceableComponent } from "../../utils/replaceableComponent";
@replaceableComponent("structures.MainSplit")
export default class MainSplit extends React.Component {
@@ -73,7 +73,7 @@ export default class MainSplit extends React.Component {
onResize={this._onResize}
onResizeStop={this._onResizeStop}
className="mx_RightPanel_ResizeWrapper"
- handleClasses={{left: "mx_RightPanel_ResizeHandle"}}
+ handleClasses={{ left: "mx_RightPanel_ResizeHandle" }}
>
{ panelView }
;
diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx
index 4c7fca2fec..c1e0b8d7cb 100644
--- a/src/components/structures/MatrixChat.tsx
+++ b/src/components/structures/MatrixChat.tsx
@@ -34,7 +34,6 @@ import dis from "../../dispatcher/dispatcher";
import Notifier from '../../Notifier';
import Modal from "../../Modal";
-import Tinter from "../../Tinter";
import * as sdk from '../../index';
import { showRoomInviteDialog, showStartChatInviteDialog } from '../../RoomInvite';
import * as Rooms from '../../Rooms';
@@ -44,11 +43,11 @@ import * as Lifecycle from '../../Lifecycle';
import '../../stores/LifecycleStore';
import PageTypes from '../../PageTypes';
-import createRoom, {IOpts} from "../../createRoom";
-import {_t, _td, getCurrentLanguage} from '../../languageHandler';
+import createRoom, { IOpts } from "../../createRoom";
+import { _t, _td, getCurrentLanguage } from '../../languageHandler';
import SettingsStore from "../../settings/SettingsStore";
import ThemeController from "../../settings/controllers/ThemeController";
-import { startAnyRegistrationFlow } from "../../Registration.js";
+import { startAnyRegistrationFlow } from "../../Registration";
import { messageForSyncError } from '../../utils/ErrorUtils';
import ResizeNotifier from "../../utils/ResizeNotifier";
import AutoDiscoveryUtils, { ValidatedServerConfig } from "../../utils/AutoDiscoveryUtils";
@@ -66,7 +65,7 @@ import {
showToast as showAnalyticsToast,
hideToast as hideAnalyticsToast,
} from "../../toasts/AnalyticsToast";
-import {showToast as showNotificationsToast} from "../../toasts/DesktopNotificationsToast";
+import { showToast as showNotificationsToast } from "../../toasts/DesktopNotificationsToast";
import { OpenToTabPayload } from "../../dispatcher/payloads/OpenToTabPayload";
import ErrorDialog from "../views/dialogs/ErrorDialog";
import { RoomNotificationStateStore } from "../../stores/notifications/RoomNotificationStateStore";
@@ -74,19 +73,20 @@ import { SettingLevel } from "../../settings/SettingLevel";
import { leaveRoomBehaviour } from "../../utils/membership";
import CreateCommunityPrototypeDialog from "../views/dialogs/CreateCommunityPrototypeDialog";
import ThreepidInviteStore, { IThreepidInvite, IThreepidInviteWireFormat } from "../../stores/ThreepidInviteStore";
-import {UIFeature} from "../../settings/UIFeature";
+import { UIFeature } from "../../settings/UIFeature";
import { CommunityPrototypeStore } from "../../stores/CommunityPrototypeStore";
import DialPadModal from "../views/voip/DialPadModal";
import { showToast as showMobileGuideToast } from '../../toasts/MobileGuideToast';
import { shouldUseLoginForWelcome } from "../../utils/pages";
import SpaceStore from "../../stores/SpaceStore";
-import {replaceableComponent} from "../../utils/replaceableComponent";
+import { replaceableComponent } from "../../utils/replaceableComponent";
import RoomListStore from "../../stores/room-list/RoomListStore";
-import {RoomUpdateCause} from "../../stores/room-list/models";
+import { RoomUpdateCause } from "../../stores/room-list/models";
import defaultDispatcher from "../../dispatcher/dispatcher";
import SecurityCustomisations from "../../customisations/Security";
import PerformanceMonitor, { PerformanceEntryNames } from "../../performance";
+import UIStore, { UI_EVENTS } from "../../stores/UIStore";
/** constants for MatrixChat.state.view */
export enum Views {
@@ -225,13 +225,13 @@ export default class MatrixChat extends React.PureComponent {
firstSyncPromise: IDeferred;
private screenAfterLogin?: IScreen;
- private windowWidth: number;
private pageChanging: boolean;
private tokenLogin?: boolean;
private accountPassword?: string;
private accountPasswordTimer?: NodeJS.Timeout;
private focusComposer: boolean;
private subTitleStatus: string;
+ private prevWindowWidth: number;
private readonly loggedInView: React.RefObject;
private readonly dispatcherRef: any;
@@ -277,17 +277,11 @@ export default class MatrixChat extends React.PureComponent {
}
}
- this.windowWidth = 10000;
- this.handleResize();
- window.addEventListener('resize', this.handleResize);
+ this.prevWindowWidth = UIStore.instance.windowWidth || 1000;
+ UIStore.instance.on(UI_EVENTS.Resize, this.handleResize);
this.pageChanging = false;
- // check we have the right tint applied for this theme.
- // N.B. we don't call the whole of setTheme() here as we may be
- // racing with the theme CSS download finishing from index.js
- Tinter.tint();
-
// For PersistentElement
this.state.resizeNotifier.on("middlePanelResized", this.dispatchTimelineResize);
@@ -378,7 +372,7 @@ export default class MatrixChat extends React.PureComponent {
this.onLoggedIn();
}
- const promisesList = [this.firstSyncPromise.promise];
+ const promisesList: Promise[] = [this.firstSyncPromise.promise];
if (cryptoEnabled) {
// wait for the client to finish downloading cross-signing keys for us so we
// know whether or not we have keys set up on this account
@@ -401,7 +395,7 @@ export default class MatrixChat extends React.PureComponent {
if (SecurityCustomisations.SHOW_ENCRYPTION_SETUP_UI === false) {
this.onLoggedIn();
} else {
- this.setStateForNewView({view: Views.COMPLETE_SECURITY});
+ this.setStateForNewView({ view: Views.COMPLETE_SECURITY });
}
} else if (await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing")) {
this.setStateForNewView({ view: Views.E2E_SETUP });
@@ -436,7 +430,7 @@ export default class MatrixChat extends React.PureComponent {
dis.unregister(this.dispatcherRef);
this.themeWatcher.stop();
this.fontWatcher.stop();
- window.removeEventListener('resize', this.handleResize);
+ UIStore.destroy();
this.state.resizeNotifier.removeListener("middlePanelResized", this.dispatchTimelineResize);
if (this.accountPasswordTimer !== null) clearTimeout(this.accountPasswordTimer);
@@ -454,7 +448,7 @@ export default class MatrixChat extends React.PureComponent {
let props = this.state.serverConfig;
if (!props) props = this.props.serverConfig; // for unit tests
if (!props) props = SdkConfig.get()["validated_server_config"];
- return {serverConfig: props};
+ return { serverConfig: props };
}
private loadSession() {
@@ -472,9 +466,9 @@ export default class MatrixChat extends React.PureComponent {
if (!loadedSession) {
// fall back to showing the welcome screen... unless we have a 3pid invite pending
if (ThreepidInviteStore.instance.pickBestInvite()) {
- dis.dispatch({action: 'start_registration'});
+ dis.dispatch({ action: 'start_registration' });
} else {
- dis.dispatch({action: "view_welcome_page"});
+ dis.dispatch({ action: "view_welcome_page" });
}
} else if (SettingsStore.getValue("analyticsOptIn")) {
CountlyAnalytics.instance.enable(/* anonymous = */ false);
@@ -538,7 +532,7 @@ export default class MatrixChat extends React.PureComponent {
action: 'do_after_sync_prepared',
deferred_action: payload,
});
- dis.dispatch({action: 'require_registration'});
+ dis.dispatch({ action: 'require_registration' });
return;
}
@@ -563,11 +557,11 @@ export default class MatrixChat extends React.PureComponent {
}
// redispatch the change with a more specific action
- dis.dispatch({action: 'id_server_changed'});
+ dis.dispatch({ action: 'id_server_changed' });
}
break;
case 'logout':
- dis.dispatch({action: "hangup_all"});
+ dis.dispatch({ action: "hangup_all" });
Lifecycle.logout();
break;
case 'require_registration':
@@ -624,7 +618,7 @@ export default class MatrixChat extends React.PureComponent {
MatrixClientPeg.get().leave(payload.room_id).then(() => {
modal.close();
if (this.state.currentRoomId === payload.room_id) {
- dis.dispatch({action: 'view_home_page'});
+ dis.dispatch({ action: 'view_home_page' });
}
}, (err) => {
modal.close();
@@ -657,7 +651,7 @@ export default class MatrixChat extends React.PureComponent {
const tabPayload = payload as OpenToTabPayload;
const UserSettingsDialog = sdk.getComponent("dialogs.UserSettingsDialog");
Modal.createTrackedDialog('User settings', '', UserSettingsDialog,
- {initialTabId: tabPayload.initialTabId},
+ { initialTabId: tabPayload.initialTabId },
/*className=*/null, /*isPriority=*/false, /*isStatic=*/true);
// View the welcome or home page if we need something to look at
@@ -665,10 +659,10 @@ export default class MatrixChat extends React.PureComponent {
break;
}
case 'view_create_room':
- this.createRoom(payload.public);
+ this.createRoom(payload.public, payload.defaultName);
break;
case 'view_create_group': {
- let CreateGroupDialog = sdk.getComponent("dialogs.CreateGroupDialog")
+ let CreateGroupDialog = sdk.getComponent("dialogs.CreateGroupDialog");
if (SettingsStore.getValue("feature_communities_v2_prototypes")) {
CreateGroupDialog = CreateCommunityPrototypeDialog;
}
@@ -727,9 +721,9 @@ export default class MatrixChat extends React.PureComponent {
// We just dispatch the page change rather than have to worry about
// what the logic is for each of these branches.
if (this.state.page_type === PageTypes.MyGroups) {
- dis.dispatch({action: 'view_last_screen'});
+ dis.dispatch({ action: 'view_last_screen' });
} else {
- dis.dispatch({action: 'view_my_groups'});
+ dis.dispatch({ action: 'view_my_groups' });
}
break;
case 'hide_left_panel':
@@ -770,7 +764,7 @@ export default class MatrixChat extends React.PureComponent {
this.onLoggedOut();
break;
case 'will_start_client':
- this.setState({ready: false}, () => {
+ this.setState({ ready: false }, () => {
// if the client is about to start, we are, by definition, not ready.
// Set ready to false now, then it'll be set to true when the sync
// listener we set below fires.
@@ -1006,12 +1000,12 @@ export default class MatrixChat extends React.PureComponent {
return;
}
this.notifyNewScreen('user/' + userId);
- this.setState({currentUserId: userId});
+ this.setState({ currentUserId: userId });
this.setPage(PageTypes.UserView);
});
}
- private async createRoom(defaultPublic = false) {
+ private async createRoom(defaultPublic = false, defaultName?: string) {
const communityId = CommunityPrototypeStore.instance.getSelectedCommunityId();
if (communityId) {
// double check the user will have permission to associate this room with the community
@@ -1025,7 +1019,10 @@ export default class MatrixChat extends React.PureComponent {
}
const CreateRoomDialog = sdk.getComponent('dialogs.CreateRoomDialog');
- const modal = Modal.createTrackedDialog('Create Room', '', CreateRoomDialog, { defaultPublic });
+ const modal = Modal.createTrackedDialog('Create Room', '', CreateRoomDialog, {
+ defaultPublic,
+ defaultName,
+ });
const [shouldCreate, opts] = await modal.finished;
if (shouldCreate) {
@@ -1128,8 +1125,14 @@ export default class MatrixChat extends React.PureComponent {
description: (
{ isSpace
- ? _t("Are you sure you want to leave the space '%(spaceName)s'?", {spaceName: roomToLeave.name})
- : _t("Are you sure you want to leave the room '%(roomName)s'?", {roomName: roomToLeave.name}) }
+ ? _t(
+ "Are you sure you want to leave the space '%(spaceName)s'?",
+ { spaceName: roomToLeave.name },
+ )
+ : _t(
+ "Are you sure you want to leave the room '%(roomName)s'?",
+ { roomName: roomToLeave.name },
+ )}
{ warnings }
),
@@ -1167,7 +1170,7 @@ export default class MatrixChat extends React.PureComponent {
}).catch((err) => {
const errCode = err.errcode || _td("unknown error code");
Modal.createTrackedDialog("Failed to forget room", '', ErrorDialog, {
- title: _t("Failed to forget room %(errCode)s", {errCode}),
+ title: _t("Failed to forget room %(errCode)s", { errCode }),
description: ((err && err.message) ? err.message : _t("Operation failed")),
});
});
@@ -1251,7 +1254,7 @@ export default class MatrixChat extends React.PureComponent {
if (welcomeUserRoom === null) {
// We didn't redirect to the welcome user room, so show
// the homepage.
- dis.dispatch({action: 'view_home_page', justRegistered: true});
+ dis.dispatch({ action: 'view_home_page', justRegistered: true });
}
} else if (ThreepidInviteStore.instance.pickBestInvite()) {
// The user has a 3pid invite pending - show them that
@@ -1260,11 +1263,11 @@ export default class MatrixChat extends React.PureComponent {
// HACK: This is a pretty brutal way of threading the invite back through
// our systems, but it's the safest we have for now.
const params = ThreepidInviteStore.instance.translateToWireFormat(threepidInvite);
- this.showScreen(`room/${threepidInvite.roomId}`, params)
+ this.showScreen(`room/${threepidInvite.roomId}`, params);
} else {
// The user has just logged in after registering,
// so show the homepage.
- dis.dispatch({action: 'view_home_page', justRegistered: true});
+ dis.dispatch({ action: 'view_home_page', justRegistered: true });
}
} else {
this.showScreenAfterLogin();
@@ -1300,9 +1303,9 @@ export default class MatrixChat extends React.PureComponent {
this.viewLastRoom();
} else {
if (MatrixClientPeg.get().isGuest()) {
- dis.dispatch({action: 'view_welcome_page'});
+ dis.dispatch({ action: 'view_welcome_page' });
} else {
- dis.dispatch({action: 'view_home_page'});
+ dis.dispatch({ action: 'view_home_page' });
}
}
}
@@ -1382,15 +1385,15 @@ export default class MatrixChat extends React.PureComponent {
// So dispatch directly from here. Ideally we'd use a SyncStateStore that
// would do this dispatch and expose the sync state itself (by listening to
// its own dispatch).
- dis.dispatch({action: 'sync_state', prevState, state});
+ dis.dispatch({ action: 'sync_state', prevState, state });
if (state === "ERROR" || state === "RECONNECTING") {
if (data.error instanceof InvalidStoreError) {
Lifecycle.handleInvalidStoreError(data.error);
}
- this.setState({syncError: data.error || true});
+ this.setState({ syncError: data.error || true });
} else if (this.state.syncError) {
- this.setState({syncError: null});
+ this.setState({ syncError: null });
}
this.updateStatusIndicator(state, prevState);
@@ -1458,7 +1461,7 @@ export default class MatrixChat extends React.PureComponent {
});
const dft = new DecryptionFailureTracker((total, errorCode) => {
- Analytics.trackEvent('E2E', 'Decryption failure', errorCode, total);
+ Analytics.trackEvent('E2E', 'Decryption failure', errorCode, String(total));
CountlyAnalytics.instance.track("decryption_failure", { errorCode }, null, { sum: total });
}, (errorCode) => {
// Map JS-SDK error codes to tracker codes for aggregation
@@ -1564,16 +1567,12 @@ export default class MatrixChat extends React.PureComponent {
key: 'verifreq_' + request.channel.transactionId,
title: _t("Verification requested"),
icon: "verification",
- props: {request},
+ props: { request },
component: sdk.getComponent("toasts.VerificationRequestToast"),
priority: 90,
});
}
});
- // Fire the tinter right on startup to ensure the default theme is applied
- // A later sync can/will correct the tint to be the right value for the user
- const colorScheme = SettingsStore.getValue("roomColor");
- Tinter.tint(colorScheme.primary_color, colorScheme.secondary_color);
}
/**
@@ -1665,7 +1664,7 @@ export default class MatrixChat extends React.PureComponent {
// TODO if logged in, skip SSO
let cli = MatrixClientPeg.get();
if (!cli) {
- const {hsUrl, isUrl} = this.props.serverConfig;
+ const { hsUrl, isUrl } = this.props.serverConfig;
cli = createClient({
baseUrl: hsUrl,
idBaseUrl: isUrl,
@@ -1789,7 +1788,7 @@ export default class MatrixChat extends React.PureComponent {
onAliasClick(event: MouseEvent, alias: string) {
event.preventDefault();
- dis.dispatch({action: 'view_room', room_alias: alias});
+ dis.dispatch({ action: 'view_room', room_alias: alias });
}
onUserClick(event: MouseEvent, userId: string) {
@@ -1805,7 +1804,7 @@ export default class MatrixChat extends React.PureComponent {
onGroupClick(event: MouseEvent, groupId: string) {
event.preventDefault();
- dis.dispatch({action: 'view_group', group_id: groupId});
+ dis.dispatch({ action: 'view_group', group_id: groupId });
}
onLogoutClick(event: React.MouseEvent) {
@@ -1817,18 +1816,19 @@ export default class MatrixChat extends React.PureComponent {
}
handleResize = () => {
- const hideLhsThreshold = 1000;
- const showLhsThreshold = 1000;
+ const LHS_THRESHOLD = 1000;
+ const width = UIStore.instance.windowWidth;
- if (this.windowWidth > hideLhsThreshold && window.innerWidth <= hideLhsThreshold) {
- dis.dispatch({ action: 'hide_left_panel' });
- }
- if (this.windowWidth <= showLhsThreshold && window.innerWidth > showLhsThreshold) {
+ if (this.prevWindowWidth < LHS_THRESHOLD && width >= LHS_THRESHOLD) {
dis.dispatch({ action: 'show_left_panel' });
}
+ if (this.prevWindowWidth >= LHS_THRESHOLD && width < LHS_THRESHOLD) {
+ dis.dispatch({ action: 'hide_left_panel' });
+ }
+
+ this.prevWindowWidth = width;
this.state.resizeNotifier.notifyWindowResized();
- this.windowWidth = window.innerWidth;
};
private dispatchTimelineResize() {
@@ -1866,14 +1866,14 @@ export default class MatrixChat extends React.PureComponent {
onSendEvent(roomId: string, event: MatrixEvent) {
const cli = MatrixClientPeg.get();
if (!cli) {
- dis.dispatch({action: 'message_send_failed'});
+ dis.dispatch({ action: 'message_send_failed' });
return;
}
cli.sendEvent(roomId, event.getType(), event.getContent()).then(() => {
- dis.dispatch({action: 'message_sent'});
+ dis.dispatch({ action: 'message_sent' });
}, (err) => {
- dis.dispatch({action: 'message_send_failed'});
+ dis.dispatch({ action: 'message_send_failed' });
});
}
@@ -1920,7 +1920,7 @@ export default class MatrixChat extends React.PureComponent {
}
onServerConfigChange = (serverConfig: ValidatedServerConfig) => {
- this.setState({serverConfig});
+ this.setState({ serverConfig });
};
private makeRegistrationUrl = (params: {[key: string]: string}) => {
@@ -1949,6 +1949,7 @@ export default class MatrixChat extends React.PureComponent {
// Create and start the client
await Lifecycle.setLoggedIn(credentials);
await this.postLoginSetup();
+
PerformanceMonitor.instance.stop(PerformanceEntryNames.LOGIN);
PerformanceMonitor.instance.stop(PerformanceEntryNames.REGISTER);
};
@@ -2087,6 +2088,7 @@ export default class MatrixChat extends React.PureComponent {
onForgotPasswordClick={showPasswordReset ? this.onForgotPasswordClick : undefined}
onServerConfigChange={this.onServerConfigChange}
fragmentAfterLogin={fragmentAfterLogin}
+ defaultUsername={this.props.startingFragmentQueryParams.defaultUsername}
{...this.getServerProperties()}
/>
);
diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.tsx
similarity index 60%
rename from src/components/structures/MessagePanel.js
rename to src/components/structures/MessagePanel.tsx
index d1071a9e19..a0a1ac9b10 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,47 @@ 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 classNames from 'classnames';
-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 {Layout, LayoutPropType} from "../../settings/Layout";
-import {_t} from "../../languageHandler";
-import {haveTileForEvent} from "../views/rooms/EventTile";
-import {textForEvent} from "../../TextForEvent";
+import RoomContext from "../../contexts/RoomContext";
+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];
+const membershipTypes = [EventType.RoomMember, EventType.RoomThirdPartyInvite, EventType.RoomServerAcl];
// 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 +65,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,91 +79,157 @@ function shouldFormContinuation(prevEvent, mxEvent) {
return true;
}
-const isMembershipChange = (e) => e.getType() === 'm.room.member' || e.getType() === 'm.room.third_party_invite';
+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,
+export default class MessagePanel extends React.Component {
+ static contextType = RoomContext;
- // true to show a spinner at the top of the timeline to indicate
- // back-pagination in progress
- backPaginating: PropTypes.bool,
+ // opaque readreceipt info for each userId; used by ReadReceiptMarker
+ // to manage its animations
+ private readonly readReceiptMap: Record = {};
- // true to show a spinner at the end of the timeline to indicate
- // forward-pagination in progress
- forwardPaginating: PropTypes.bool,
+ // 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 = {};
- // the list of MatrixEvents to display
- events: PropTypes.array.isRequired,
+ // 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 = {};
- // ID of an event to highlight. If undefined, no event will be highlighted.
- highlightedEventId: PropTypes.string,
+ private readonly showHiddenEventsInTimeline: boolean;
+ private isMounted = false;
- // The room these events are all in together, if any.
- // (The notification panel won't have a room here, for example.)
- room: PropTypes.object,
+ private readMarkerNode = createRef();
+ private whoIsTyping = createRef();
+ private scrollPanel = createRef();
- // Should we show URL Previews
- showUrlPreview: PropTypes.bool,
+ private readonly showTypingNotificationsWatcherRef: string;
+ private eventNodes: Record;
- // 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 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,
- };
-
- constructor(props) {
- super(props);
+ constructor(props, context) {
+ super(props, context);
this.state = {
// previous positions the read marker has been in, so we can
@@ -159,65 +238,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) {
@@ -230,14 +265,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;
}
@@ -247,8 +282,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
@@ -256,8 +291,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:
@@ -266,15 +301,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
@@ -290,17 +325,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();
}
}
@@ -309,9 +344,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);
}
}
@@ -320,9 +355,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);
}
}
@@ -336,38 +371,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;
}
@@ -378,10 +416,10 @@ export default class MessagePanel extends React.Component {
// Always show highlighted event
if (this.props.highlightedEventId === mxEv.getId()) return true;
- return !shouldHideEvent(mxEv);
+ 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) {
@@ -394,13 +432,13 @@ export default class MessagePanel extends React.Component {
// confused.
if (visible) {
hr = ;
}
return (
@@ -419,8 +457,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 = ;
@@ -440,7 +478,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(() => {
@@ -450,15 +488,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;
@@ -467,16 +505,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;
@@ -492,7 +530,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;
}
@@ -516,18 +554,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)) {
@@ -548,26 +586,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),
});
}
@@ -578,10 +615,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 &&
@@ -596,7 +637,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);
@@ -604,7 +645,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?
@@ -613,16 +654,12 @@ export default class MessagePanel extends React.Component {
const eventId = mxEv.getId();
const highlight = (eventId === this.props.highlightedEventId);
- // we can't use local echoes as scroll tokens, because their event IDs change.
- // Local echos have a send "status".
- const scrollToken = mxEv.status ? undefined : eventId;
-
- 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())) {
@@ -645,45 +682,42 @@ export default class MessagePanel extends React.Component {
// use txnId as key if available so that we don't remount during sending
ret.push(
-
-
-
-
-
,
+
+
+ ,
);
return ret;
}
- _wantsDateSeparator(prevEvent, nextEventDate) {
+ public wantsDateSeparator(prevEvent: MatrixEvent, nextEventDate: Date): boolean {
if (prevEvent == null) {
// first event in the panel: depends if we could back-paginate from
// here.
@@ -694,7 +728,7 @@ export default class MessagePanel extends React.Component {
// Get a list of read receipts that should be shown next to this event
// Receipts are objects which have a 'userId', 'roomMember' and 'ts'.
- _getReadReceiptsForEvent(event) {
+ private getReadReceiptsForEvent(event: MatrixEvent): IReadReceiptProps[] {
const myUserId = MatrixClientPeg.get().credentials.userId;
// get list of read receipts, sorted most recent first
@@ -702,7 +736,7 @@ export default class MessagePanel extends React.Component {
if (!room) {
return null;
}
- const receipts = [];
+ const receipts: IReadReceiptProps[] = [];
room.getReceiptsForEvent(event).forEach((r) => {
if (!r.userId || r.type !== "m.read" || r.userId === myUserId) {
return; // ignore non-read receipts and receipts from self.
@@ -723,13 +757,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) {
@@ -737,7 +771,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
@@ -756,16 +790,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.
@@ -778,21 +812,21 @@ export default class MessagePanel extends React.Component {
return receiptsByEvent;
}
- _collectEventNode = (eventId, node) => {
- this.eventNodes[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) {
@@ -800,8 +834,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
@@ -813,12 +847,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,
@@ -830,18 +864,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) {
@@ -853,20 +883,13 @@ export default class MessagePanel extends React.Component {
const style = this.props.hidden ? { display: 'none' } : {};
- const className = classNames(
- this.props.className,
- {
- "mx_MessagePanel_alwaysShowTimestamps": this.props.alwaysShowTimestamps,
- },
- );
-
let whoIsTyping;
if (this.props.room && !this.props.tileShape && this.state.showTypingNotifications) {
whoIsTyping = (
+ onShown={this.onTypingShown}
+ onHidden={this.onTypingHidden}
+ ref={this.whoIsTyping} />
);
}
@@ -882,10 +905,10 @@ export default class MessagePanel extends React.Component {
return (
{ topSpinner }
- { this._getEventTiles() }
+ { this.getEventTiles() }
{ whoIsTyping }
{ bottomSpinner }
@@ -903,6 +926,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
@@ -918,36 +966,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;
}
@@ -957,37 +990,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(
,
@@ -995,13 +1026,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,
));
}
@@ -1011,7 +1042,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];
@@ -1031,7 +1062,7 @@ class CreationGrouper {
@@ -1046,62 +1077,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(
,
@@ -1112,11 +1140,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), []);
@@ -1129,7 +1157,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 })}
>
@@ -1144,64 +1172,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) && membershipTypes.includes(ev.getType() as EventType);
+ };
+
+ 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];
}
- 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;
- }
-
- 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);
+ return membershipTypes.includes(ev.getType() as EventType);
}
- add(ev) {
- if (ev.getType() === 'm.room.member') {
- // We'll just double check that it's worth our time to do so, through an
- // ugly hack. If textForEvent returns something, we should group it for
- // rendering but if it doesn't then we'll exclude it.
- const renderText = textForEvent(ev);
- if (!renderText || renderText.trim().length === 0) return; // quietly ignore
+ 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(
,
@@ -1229,7 +1251,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) {
@@ -1237,9 +1259,10 @@ class MemberGrouper {
}
ret.push(
-
{ eventTiles }
@@ -1253,7 +1276,7 @@ class MemberGrouper {
return ret;
}
- getNewPrevEvent() {
+ public getNewPrevEvent(): MatrixEvent {
return this.events[0];
}
}
diff --git a/src/components/structures/MyGroups.js b/src/components/structures/MyGroups.js
index 1fab6c4348..87447b6aba 100644
--- a/src/components/structures/MyGroups.js
+++ b/src/components/structures/MyGroups.js
@@ -24,7 +24,7 @@ import dis from '../../dispatcher/dispatcher';
import AccessibleButton from '../views/elements/AccessibleButton';
import MatrixClientContext from "../../contexts/MatrixClientContext";
import AutoHideScrollbar from "./AutoHideScrollbar";
-import {replaceableComponent} from "../../utils/replaceableComponent";
+import { replaceableComponent } from "../../utils/replaceableComponent";
import BetaCard from "../views/beta/BetaCard";
@replaceableComponent("structures.MyGroups")
@@ -41,19 +41,19 @@ export default class MyGroups extends React.Component {
}
_onCreateGroupClick = () => {
- dis.dispatch({action: 'view_create_group'});
+ dis.dispatch({ action: 'view_create_group' });
};
_fetch() {
this.context.getJoinedGroups().then((result) => {
- this.setState({groups: result.groups, error: null});
+ this.setState({ groups: result.groups, error: null });
}, (err) => {
if (err.errcode === 'M_GUEST_ACCESS_FORBIDDEN') {
// Indicate that the guest isn't in any groups (which should be true)
- this.setState({groups: [], error: null});
+ this.setState({ groups: [], error: null });
return;
}
- this.setState({groups: null, error: err});
+ this.setState({ groups: null, error: err });
});
}
@@ -82,8 +82,7 @@ export default class MyGroups extends React.Component {
{ _t(
- "To set up a filter, drag a community avatar over to the filter panel on " +
- "the far left hand side of the screen. You can click on an avatar in the " +
+ "You can click on an avatar in the " +
"filter panel at any time to see only the rooms and people associated " +
"with that community.",
) }
@@ -124,7 +123,7 @@ export default class MyGroups extends React.Component {
{/*
-
+
diff --git a/src/components/structures/NonUrgentToastContainer.tsx b/src/components/structures/NonUrgentToastContainer.tsx
index 7c193ec9d7..b1424a2974 100644
--- a/src/components/structures/NonUrgentToastContainer.tsx
+++ b/src/components/structures/NonUrgentToastContainer.tsx
@@ -18,7 +18,7 @@ import * as React from "react";
import { ComponentClass } from "../../@types/common";
import NonUrgentToastStore from "../../stores/NonUrgentToastStore";
import { UPDATE_EVENT } from "../../stores/AsyncStore";
-import {replaceableComponent} from "../../utils/replaceableComponent";
+import { replaceableComponent } from "../../utils/replaceableComponent";
interface IProps {
}
@@ -44,7 +44,7 @@ export default class NonUrgentToastContainer extends React.PureComponent {
- this.setState({toasts: NonUrgentToastStore.instance.components});
+ this.setState({ toasts: NonUrgentToastStore.instance.components });
};
public render() {
diff --git a/src/components/structures/NotificationPanel.js b/src/components/structures/NotificationPanel.tsx
similarity index 68%
rename from src/components/structures/NotificationPanel.js
rename to src/components/structures/NotificationPanel.tsx
index 41aafc8b13..8c8fab7ece 100644
--- a/src/components/structures/NotificationPanel.js
+++ b/src/components/structures/NotificationPanel.tsx
@@ -1,7 +1,5 @@
/*
-Copyright 2016 OpenMarket Ltd
-Copyright 2019 New Vector Ltd
-Copyright 2019 The Matrix.org Foundation C.I.C.
+Copyright 2016, 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.
@@ -16,29 +14,26 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import React from 'react';
-import PropTypes from "prop-types";
+import React from "react";
import { _t } from '../../languageHandler';
-import {MatrixClientPeg} from "../../MatrixClientPeg";
-import * as sdk from "../../index";
+import { MatrixClientPeg } from "../../MatrixClientPeg";
import BaseCard from "../views/right_panel/BaseCard";
-import {replaceableComponent} from "../../utils/replaceableComponent";
+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;
+}
/*
* Component which shows the global notification list using a TimelinePanel
*/
@replaceableComponent("structures.NotificationPanel")
-class NotificationPanel extends React.Component {
- static propTypes = {
- onClose: PropTypes.func.isRequired,
- };
-
+export default class NotificationPanel extends React.PureComponent {
render() {
- // wrap a TimelinePanel with the jump-to-event bits turned off.
- const TimelinePanel = sdk.getComponent("structures.TimelinePanel");
- const Loader = sdk.getComponent("elements.Spinner");
-
const emptyState = (
{_t('You’re all caught up')}
{_t('You have no visible notifications.')}
@@ -47,19 +42,21 @@ class NotificationPanel extends React.Component {
let content;
const timelineSet = MatrixClientPeg.get().getNotifTimelineSet();
if (timelineSet) {
+ // wrap a TimelinePanel with the jump-to-event bits turned off.
content = (
);
} else {
console.error("No notifTimelineSet available!");
- content = ;
+ content = ;
}
return
@@ -67,5 +64,3 @@ class NotificationPanel extends React.Component {
;
}
}
-
-export default NotificationPanel;
diff --git a/src/components/structures/RightPanel.js b/src/components/structures/RightPanel.tsx
similarity index 76%
rename from src/components/structures/RightPanel.js
rename to src/components/structures/RightPanel.tsx
index d8c763eabd..c608f0eee9 100644
--- a/src/components/structures/RightPanel.js
+++ b/src/components/structures/RightPanel.tsx
@@ -1,6 +1,6 @@
/*
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
-Copyright 2015 - 2020 The Matrix.org Foundation C.I.C.
+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,73 +16,95 @@ limitations under the License.
*/
import React from 'react';
-import PropTypes from 'prop-types';
-import {Room} from "matrix-js-sdk/src/models/room";
+import { Room } from "matrix-js-sdk/src/models/room";
+import { User } from "matrix-js-sdk/src/models/user";
+import { RoomMember } from "matrix-js-sdk/src/models/room-member";
+import { MatrixEvent } from "matrix-js-sdk/src/models/event";
+import { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
-import * as sdk from '../../index';
import dis from '../../dispatcher/dispatcher';
import RateLimitedFunc from '../../ratelimitedfunc';
-import { showGroupInviteDialog, showGroupAddRoomDialog } from '../../GroupAddressPicker';
import GroupStore from '../../stores/GroupStore';
import {
- RightPanelPhases,
RIGHT_PANEL_PHASES_NO_ARGS,
RIGHT_PANEL_SPACE_PHASES,
+ RightPanelPhases,
} from "../../stores/RightPanelStorePhases";
import RightPanelStore from "../../stores/RightPanelStore";
import MatrixClientContext from "../../contexts/MatrixClientContext";
-import {Action} from "../../dispatcher/actions";
+import { Action } from "../../dispatcher/actions";
import RoomSummaryCard from "../views/right_panel/RoomSummaryCard";
import WidgetCard from "../views/right_panel/WidgetCard";
-import {replaceableComponent} from "../../utils/replaceableComponent";
+import { replaceableComponent } from "../../utils/replaceableComponent";
import SettingsStore from "../../settings/SettingsStore";
+import { ActionPayload } from "../../dispatcher/payloads";
+import MemberList from "../views/rooms/MemberList";
+import GroupMemberList from "../views/groups/GroupMemberList";
+import GroupRoomList from "../views/groups/GroupRoomList";
+import GroupRoomInfo from "../views/groups/GroupRoomInfo";
+import UserInfo from "../views/right_panel/UserInfo";
+import ThirdPartyMemberInfo from "../views/rooms/ThirdPartyMemberInfo";
+import FilePanel from "./FilePanel";
+import NotificationPanel from "./NotificationPanel";
+import ResizeNotifier from "../../utils/ResizeNotifier";
+import PinnedMessagesCard from "../views/right_panel/PinnedMessagesCard";
+
+interface IProps {
+ room?: Room; // if showing panels for a given room, this is set
+ groupId?: string; // if showing panels for a given group, this is set
+ user?: User; // used if we know the user ahead of opening the panel
+ resizeNotifier: ResizeNotifier;
+}
+
+interface IState {
+ phase: RightPanelPhases;
+ isUserPrivilegedInGroup?: boolean;
+ member?: RoomMember;
+ verificationRequest?: VerificationRequest;
+ verificationRequestPromise?: Promise;
+ space?: Room;
+ widgetId?: string;
+ groupRoomId?: string;
+ groupId?: string;
+ event: MatrixEvent;
+}
@replaceableComponent("structures.RightPanel")
-export default class RightPanel extends React.Component {
- static get propTypes() {
- return {
- room: PropTypes.instanceOf(Room), // if showing panels for a given room, this is set
- groupId: PropTypes.string, // if showing panels for a given group, this is set
- user: PropTypes.object, // used if we know the user ahead of opening the panel
- };
- }
-
+export default class RightPanel extends React.Component {
static contextType = MatrixClientContext;
+ private readonly delayedUpdate: RateLimitedFunc;
+ private dispatcherRef: string;
+
constructor(props, context) {
super(props, context);
this.state = {
...RightPanelStore.getSharedInstance().roomPanelPhaseParams,
- phase: this._getPhaseFromProps(),
+ phase: this.getPhaseFromProps(),
isUserPrivilegedInGroup: null,
- member: this._getUserForPanel(),
+ member: this.getUserForPanel(),
};
- this.onAction = this.onAction.bind(this);
- this.onRoomStateMember = this.onRoomStateMember.bind(this);
- this.onGroupStoreUpdated = this.onGroupStoreUpdated.bind(this);
- this.onInviteToGroupButtonClick = this.onInviteToGroupButtonClick.bind(this);
- this.onAddRoomToGroupButtonClick = this.onAddRoomToGroupButtonClick.bind(this);
- this._delayedUpdate = new RateLimitedFunc(() => {
+ this.delayedUpdate = new RateLimitedFunc(() => {
this.forceUpdate();
}, 500);
}
- // Helper function to split out the logic for _getPhaseFromProps() and the constructor
+ // Helper function to split out the logic for getPhaseFromProps() and the constructor
// as both are called at the same time in the constructor.
- _getUserForPanel() {
+ private getUserForPanel() {
if (this.state && this.state.member) return this.state.member;
const lastParams = RightPanelStore.getSharedInstance().roomPanelPhaseParams;
return this.props.user || lastParams['member'];
}
// gets the current phase from the props and also maybe the store
- _getPhaseFromProps() {
+ private getPhaseFromProps() {
const rps = RightPanelStore.getSharedInstance();
- const userForPanel = this._getUserForPanel();
+ const userForPanel = this.getUserForPanel();
if (this.props.groupId) {
if (!RIGHT_PANEL_PHASES_NO_ARGS.includes(rps.groupPanelPhase)) {
- dis.dispatch({action: Action.SetRightPanelPhase, phase: RightPanelPhases.GroupMemberList});
+ dis.dispatch({ action: Action.SetRightPanelPhase, phase: RightPanelPhases.GroupMemberList });
return RightPanelPhases.GroupMemberList;
}
return rps.groupPanelPhase;
@@ -118,7 +140,7 @@ export default class RightPanel extends React.Component {
this.dispatcherRef = dis.register(this.onAction);
const cli = this.context;
cli.on("RoomState.members", this.onRoomStateMember);
- this._initGroupStore(this.props.groupId);
+ this.initGroupStore(this.props.groupId);
}
componentWillUnmount() {
@@ -126,61 +148,47 @@ export default class RightPanel extends React.Component {
if (this.context) {
this.context.removeListener("RoomState.members", this.onRoomStateMember);
}
- this._unregisterGroupStore(this.props.groupId);
+ this.unregisterGroupStore();
}
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
UNSAFE_componentWillReceiveProps(newProps) { // eslint-disable-line camelcase
if (newProps.groupId !== this.props.groupId) {
- this._unregisterGroupStore(this.props.groupId);
- this._initGroupStore(newProps.groupId);
+ this.unregisterGroupStore();
+ this.initGroupStore(newProps.groupId);
}
}
- _initGroupStore(groupId) {
+ private initGroupStore(groupId: string) {
if (!groupId) return;
GroupStore.registerListener(groupId, this.onGroupStoreUpdated);
}
- _unregisterGroupStore() {
+ private unregisterGroupStore() {
GroupStore.unregisterListener(this.onGroupStoreUpdated);
}
- onGroupStoreUpdated() {
+ private onGroupStoreUpdated = () => {
this.setState({
isUserPrivilegedInGroup: GroupStore.isUserPrivileged(this.props.groupId),
});
- }
+ };
- onInviteToGroupButtonClick() {
- showGroupInviteDialog(this.props.groupId).then(() => {
- this.setState({
- phase: RightPanelPhases.GroupMemberList,
- });
- });
- }
-
- onAddRoomToGroupButtonClick() {
- showGroupAddRoomDialog(this.props.groupId).then(() => {
- this.forceUpdate();
- });
- }
-
- onRoomStateMember(ev, state, member) {
+ private onRoomStateMember = (ev: MatrixEvent, _, member: RoomMember) => {
if (!this.props.room || member.roomId !== this.props.room.roomId) {
return;
}
// redraw the badge on the membership list
if (this.state.phase === RightPanelPhases.RoomMemberList && member.roomId === this.props.room.roomId) {
- this._delayedUpdate();
+ this.delayedUpdate();
} else if (this.state.phase === RightPanelPhases.RoomMemberInfo && member.roomId === this.props.room.roomId &&
member.userId === this.state.member.userId) {
// refresh the member info (e.g. new power level)
- this._delayedUpdate();
+ this.delayedUpdate();
}
- }
+ };
- onAction(payload) {
+ private onAction = (payload: ActionPayload) => {
if (payload.action === Action.AfterRightPanelPhaseChange) {
this.setState({
phase: payload.phase,
@@ -194,9 +202,9 @@ export default class RightPanel extends React.Component {
space: payload.space,
});
}
- }
+ };
- onClose = () => {
+ private onClose = () => {
// XXX: There are three different ways of 'closing' this panel depending on what state
// things are in... this knows far more than it should do about the state of the rest
// of the app and is generally a bit silly.
@@ -224,16 +232,6 @@ export default class RightPanel extends React.Component {
};
render() {
- const MemberList = sdk.getComponent('rooms.MemberList');
- const UserInfo = sdk.getComponent('right_panel.UserInfo');
- const ThirdPartyMemberInfo = sdk.getComponent('rooms.ThirdPartyMemberInfo');
- const NotificationPanel = sdk.getComponent('structures.NotificationPanel');
- const FilePanel = sdk.getComponent('structures.FilePanel');
-
- const GroupMemberList = sdk.getComponent('groups.GroupMemberList');
- const GroupRoomList = sdk.getComponent('groups.GroupRoomList');
- const GroupRoomInfo = sdk.getComponent('groups.GroupRoomInfo');
-
let panel = ;
const roomId = this.props.room ? this.props.room.roomId : undefined;
@@ -285,6 +283,7 @@ export default class RightPanel extends React.Component {
user={this.state.member}
groupId={this.props.groupId}
key={this.state.member.userId}
+ phase={this.state.phase}
onClose={this.onClose} />;
break;
@@ -299,6 +298,12 @@ export default class RightPanel extends React.Component {
panel = ;
break;
+ case RightPanelPhases.PinnedMessages:
+ if (SettingsStore.getValue("feature_pinning")) {
+ panel = ;
+ }
+ break;
+
case RightPanelPhases.FilePanel:
panel = ;
break;
diff --git a/src/components/structures/RoomDirectory.js b/src/components/structures/RoomDirectory.tsx
similarity index 65%
rename from src/components/structures/RoomDirectory.js
rename to src/components/structures/RoomDirectory.tsx
index 3613261da6..2ac990436f 100644
--- a/src/components/structures/RoomDirectory.js
+++ b/src/components/structures/RoomDirectory.tsx
@@ -1,7 +1,6 @@
/*
-Copyright 2015, 2016 OpenMarket Ltd
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
-Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
+Copyright 2015, 2016, 2019, 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,39 +15,89 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import React from 'react';
-import {MatrixClientPeg} from "../../MatrixClientPeg";
-import * as sdk from "../../index";
+import React from "react";
+
+import { MatrixClientPeg } from "../../MatrixClientPeg";
import dis from "../../dispatcher/dispatcher";
import Modal from "../../Modal";
import { linkifyAndSanitizeHtml } from '../../HtmlUtils';
-import PropTypes from 'prop-types';
import { _t } from '../../languageHandler';
import SdkConfig from '../../SdkConfig';
import { instanceForInstanceId, protocolNameForInstanceId } from '../../utils/DirectoryUtils';
import Analytics from '../../Analytics';
-import {ALL_ROOMS} from "../views/directory/NetworkDropdown";
+import { ALL_ROOMS, IFieldType, IInstance, IProtocol, Protocols } from "../views/directory/NetworkDropdown";
import SettingsStore from "../../settings/SettingsStore";
import GroupFilterOrderStore from "../../stores/GroupFilterOrderStore";
import GroupStore from "../../stores/GroupStore";
import FlairStore from "../../stores/FlairStore";
import CountlyAnalytics from "../../CountlyAnalytics";
-import {replaceableComponent} from "../../utils/replaceableComponent";
-import {mediaFromMxc} from "../../customisations/Media";
+import { replaceableComponent } from "../../utils/replaceableComponent";
+import { mediaFromMxc } from "../../customisations/Media";
+import { IDialogProps } from "../views/dialogs/IDialogProps";
+import AccessibleButton, { ButtonEvent } from "../views/elements/AccessibleButton";
+import BaseAvatar from "../views/avatars/BaseAvatar";
+import ErrorDialog from "../views/dialogs/ErrorDialog";
+import QuestionDialog from "../views/dialogs/QuestionDialog";
+import BaseDialog from "../views/dialogs/BaseDialog";
+import DirectorySearchBox from "../views/elements/DirectorySearchBox";
+import NetworkDropdown from "../views/directory/NetworkDropdown";
+import ScrollPanel from "./ScrollPanel";
+import Spinner from "../views/elements/Spinner";
+import { ActionPayload } from "../../dispatcher/payloads";
const MAX_NAME_LENGTH = 80;
const MAX_TOPIC_LENGTH = 800;
-function track(action) {
+function track(action: string) {
Analytics.trackEvent('RoomDirectory', action);
}
+interface IProps extends IDialogProps {
+ initialText?: string;
+}
+
+interface IState {
+ publicRooms: IRoom[];
+ loading: boolean;
+ protocolsLoading: boolean;
+ error?: string;
+ instanceId: string | symbol;
+ roomServer: string;
+ filterString: string;
+ selectedCommunityId?: string;
+ communityName?: string;
+}
+
+/* eslint-disable camelcase */
+interface IRoom {
+ room_id: string;
+ name?: string;
+ avatar_url?: string;
+ topic?: string;
+ canonical_alias?: string;
+ aliases?: string[];
+ world_readable: boolean;
+ guest_can_join: boolean;
+ num_joined_members: number;
+}
+
+interface IPublicRoomsRequest {
+ limit?: number;
+ since?: string;
+ server?: string;
+ filter?: object;
+ include_all_networks?: boolean;
+ third_party_instance_id?: string;
+}
+/* eslint-enable camelcase */
+
@replaceableComponent("structures.RoomDirectory")
-export default class RoomDirectory extends React.Component {
- static propTypes = {
- initialText: PropTypes.string,
- onFinished: PropTypes.func.isRequired,
- };
+export default class RoomDirectory extends React.Component {
+ private readonly startTime: number;
+ private unmounted = false;
+ private nextBatch: string = null;
+ private filterTimeout: NodeJS.Timeout;
+ private protocols: Protocols;
constructor(props) {
super(props);
@@ -56,41 +105,21 @@ export default class RoomDirectory extends React.Component {
CountlyAnalytics.instance.trackRoomDirectoryBegin();
this.startTime = CountlyAnalytics.getTimestamp();
- const selectedCommunityId = GroupFilterOrderStore.getSelectedTags()[0];
- this.state = {
- publicRooms: [],
- loading: true,
- protocolsLoading: true,
- error: null,
- instanceId: undefined,
- roomServer: MatrixClientPeg.getHomeserverName(),
- filterString: this.props.initialText || "",
- selectedCommunityId: SettingsStore.getValue("feature_communities_v2_prototypes")
- ? selectedCommunityId
- : null,
- communityName: null,
- };
+ const selectedCommunityId = SettingsStore.getValue("feature_communities_v2_prototypes")
+ ? GroupFilterOrderStore.getSelectedTags()[0]
+ : null;
- this._unmounted = false;
- this.nextBatch = null;
- this.filterTimeout = null;
- this.scrollPanel = null;
- this.protocols = null;
-
- this.state.protocolsLoading = true;
+ let protocolsLoading = true;
if (!MatrixClientPeg.get()) {
// We may not have a client yet when invoked from welcome page
- this.state.protocolsLoading = false;
- return;
- }
-
- if (!this.state.selectedCommunityId) {
+ protocolsLoading = false;
+ } else if (!selectedCommunityId) {
MatrixClientPeg.get().getThirdpartyProtocols().then((response) => {
this.protocols = response;
- this.setState({protocolsLoading: false});
+ this.setState({ protocolsLoading: false });
}, (err) => {
console.warn(`error loading third party protocols: ${err}`);
- this.setState({protocolsLoading: false});
+ this.setState({ protocolsLoading: false });
if (MatrixClientPeg.get().isGuest()) {
// Guests currently aren't allowed to use this API, so
// ignore this as otherwise this error is literally the
@@ -103,19 +132,31 @@ export default class RoomDirectory extends React.Component {
error: _t(
'%(brand)s failed to get the protocol list from the homeserver. ' +
'The homeserver may be too old to support third party networks.',
- {brand},
+ { brand },
),
});
});
} else {
// We don't use the protocols in the communities v2 prototype experience
- this.state.protocolsLoading = false;
+ protocolsLoading = false;
// Grab the profile info async
FlairStore.getGroupProfileCached(MatrixClientPeg.get(), this.state.selectedCommunityId).then(profile => {
- this.setState({communityName: profile.name});
+ this.setState({ communityName: profile.name });
});
}
+
+ this.state = {
+ publicRooms: [],
+ loading: true,
+ error: null,
+ instanceId: undefined,
+ roomServer: MatrixClientPeg.getHomeserverName(),
+ filterString: this.props.initialText || "",
+ selectedCommunityId,
+ communityName: null,
+ protocolsLoading,
+ };
}
componentDidMount() {
@@ -126,10 +167,10 @@ export default class RoomDirectory extends React.Component {
if (this.filterTimeout) {
clearTimeout(this.filterTimeout);
}
- this._unmounted = true;
+ this.unmounted = true;
}
- refreshRoomList = () => {
+ private refreshRoomList = () => {
if (this.state.selectedCommunityId) {
this.setState({
publicRooms: GroupStore.getGroupRooms(this.state.selectedCommunityId).map(r => {
@@ -165,44 +206,44 @@ export default class RoomDirectory extends React.Component {
this.getMoreRooms();
};
- 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,
});
- const my_filter_string = this.state.filterString;
- const my_server = this.state.roomServer;
+ const filterString = this.state.filterString;
+ const roomServer = this.state.roomServer;
// remember the next batch token when we sent the request
// too. If it's changed, appending to the list will corrupt it.
- const my_next_batch = this.nextBatch;
- const opts = {limit: 20};
- if (my_server != MatrixClientPeg.getHomeserverName()) {
- opts.server = my_server;
+ const nextBatch = this.nextBatch;
+ const opts: IPublicRoomsRequest = { limit: 20 };
+ if (roomServer != MatrixClientPeg.getHomeserverName()) {
+ opts.server = roomServer;
}
if (this.state.instanceId === ALL_ROOMS) {
opts.include_all_networks = true;
} else if (this.state.instanceId) {
- opts.third_party_instance_id = this.state.instanceId;
+ opts.third_party_instance_id = this.state.instanceId as string;
}
if (this.nextBatch) opts.since = this.nextBatch;
- if (my_filter_string) opts.filter = { generic_search_term: my_filter_string };
+ if (filterString) opts.filter = { generic_search_term: filterString };
return MatrixClientPeg.get().publicRooms(opts).then((data) => {
if (
- my_filter_string != this.state.filterString ||
- my_server != this.state.roomServer ||
- my_next_batch != this.nextBatch) {
+ filterString != this.state.filterString ||
+ roomServer != this.state.roomServer ||
+ nextBatch != this.nextBatch) {
// 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 (this.unmounted) {
// if we've been unmounted, we don't care either.
- return;
+ return false;
}
if (this.state.filterString) {
@@ -211,25 +252,24 @@ export default class RoomDirectory extends React.Component {
}
this.nextBatch = data.next_batch;
- this.setState((s) => {
- s.publicRooms.push(...(data.chunk || []));
- s.loading = false;
- return s;
- });
+ this.setState((s) => ({
+ ...s,
+ publicRooms: [...s.publicRooms, ...(data.chunk || [])],
+ loading: false,
+ }));
return Boolean(data.next_batch);
}, (err) => {
if (
- my_filter_string != this.state.filterString ||
- my_server != this.state.roomServer ||
- my_next_batch != this.nextBatch) {
- // as above: we don't care about errors for old
- // requests either
- return;
+ filterString != this.state.filterString ||
+ roomServer != this.state.roomServer ||
+ nextBatch != this.nextBatch) {
+ // as above: we don't care about errors for old requests either
+ return false;
}
- if (this._unmounted) {
+ 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));
@@ -252,29 +292,25 @@ export default class RoomDirectory extends React.Component {
* HS admins to do this through the RoomSettings interface, but
* this needs SPEC-417.
*/
- removeFromDirectory(room) {
- const alias = get_display_alias_for_room(room);
+ private removeFromDirectory(room: IRoom) {
+ const alias = getDisplayAliasForRoom(room);
const name = room.name || alias || _t('Unnamed room');
- const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
- const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
-
let desc;
if (alias) {
- desc = _t('Delete the room address %(alias)s and remove %(name)s from the directory?', {alias, name});
+ desc = _t('Delete the room address %(alias)s and remove %(name)s from the directory?', { alias, name });
} else {
- desc = _t('Remove %(name)s from the directory?', {name: name});
+ desc = _t('Remove %(name)s from the directory?', { name: name });
}
Modal.createTrackedDialog('Remove from Directory', '', QuestionDialog, {
title: _t('Remove from Directory'),
description: desc,
- onFinished: (should_delete) => {
- if (!should_delete) return;
+ onFinished: (shouldDelete: boolean) => {
+ if (!shouldDelete) return;
- const Loader = sdk.getComponent("elements.Spinner");
- const modal = Modal.createDialog(Loader);
- let step = _t('remove %(name)s from the directory.', {name: name});
+ const modal = Modal.createDialog(Spinner);
+ let step = _t('remove %(name)s from the directory.', { name: name });
MatrixClientPeg.get().setRoomDirectoryVisibility(room.room_id, 'private').then(() => {
if (!alias) return;
@@ -289,23 +325,24 @@ export default class RoomDirectory extends React.Component {
console.error("Failed to " + step + ": " + err);
Modal.createTrackedDialog('Remove from Directory Error', '', ErrorDialog, {
title: _t('Error'),
- description: ((err && err.message) ? err.message : _t('The server may be unavailable or overloaded')),
+ description: (err && err.message)
+ ? err.message
+ : _t('The server may be unavailable or overloaded'),
});
});
},
});
}
- onRoomClicked = (room, ev) => {
+ private onRoomClicked = (room: IRoom, ev: ButtonEvent) => {
+ // If room was shift-clicked, remove it from the room directory
if (ev.shiftKey && !this.state.selectedCommunityId) {
ev.preventDefault();
this.removeFromDirectory(room);
- } else {
- this.showRoom(room);
}
};
- onOptionChange = (server, instanceId) => {
+ private onOptionChange = (server: string, instanceId?: string | symbol) => {
// clear next batch so we don't try to load more rooms
this.nextBatch = null;
this.setState({
@@ -325,13 +362,13 @@ export default class RoomDirectory extends React.Component {
// Easiest to just blow away the state & re-fetch.
};
- onFillRequest = (backwards) => {
+ private onFillRequest = (backwards: boolean) => {
if (backwards || !this.nextBatch) return Promise.resolve(false);
return this.getMoreRooms();
};
- onFilterChange = (alias) => {
+ private onFilterChange = (alias: string) => {
this.setState({
filterString: alias || null,
});
@@ -349,7 +386,7 @@ export default class RoomDirectory extends React.Component {
}, 700);
};
- onFilterClear = () => {
+ private onFilterClear = () => {
// update immediately
this.setState({
filterString: null,
@@ -360,7 +397,7 @@ export default class RoomDirectory extends React.Component {
}
};
- onJoinFromSearchClick = (alias) => {
+ private onJoinFromSearchClick = (alias: string) => {
// If we don't have a particular instance id selected, just show that rooms alias
if (!this.state.instanceId || this.state.instanceId === ALL_ROOMS) {
// If the user specified an alias without a domain, add on whichever server is selected
@@ -373,9 +410,10 @@ export default class RoomDirectory extends React.Component {
// This is a 3rd party protocol. Let's see if we can join it
const protocolName = protocolNameForInstanceId(this.protocols, this.state.instanceId);
const instance = instanceForInstanceId(this.protocols, this.state.instanceId);
- const fields = protocolName ? this._getFieldsForThirdPartyLocation(alias, this.protocols[protocolName], instance) : null;
+ const fields = protocolName
+ ? this.getFieldsForThirdPartyLocation(alias, this.protocols[protocolName], instance)
+ : null;
if (!fields) {
- const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
const brand = SdkConfig.get().brand;
Modal.createTrackedDialog('Unable to join network', '', ErrorDialog, {
title: _t('Unable to join network'),
@@ -387,14 +425,12 @@ export default class RoomDirectory extends React.Component {
if (resp.length > 0 && resp[0].alias) {
this.showRoomAlias(resp[0].alias, true);
} else {
- const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Room not found', '', ErrorDialog, {
title: _t('Room not found'),
description: _t('Couldn\'t find a matching Matrix room'),
});
}
}, (e) => {
- const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Fetching third party location failed', '', ErrorDialog, {
title: _t('Fetching third party location failed'),
description: _t('Unable to look up room ID from server'),
@@ -403,36 +439,37 @@ export default class RoomDirectory extends React.Component {
}
};
- onPreviewClick = (ev, room) => {
+ private onPreviewClick = (ev: ButtonEvent, room: IRoom) => {
this.showRoom(room, null, false, true);
ev.stopPropagation();
};
- onViewClick = (ev, room) => {
+ private onViewClick = (ev: ButtonEvent, room: IRoom) => {
this.showRoom(room);
ev.stopPropagation();
};
- onJoinClick = (ev, room) => {
+ private onJoinClick = (ev: ButtonEvent, room: IRoom) => {
this.showRoom(room, null, true);
ev.stopPropagation();
};
- onCreateRoomClick = room => {
+ private onCreateRoomClick = () => {
this.onFinished();
dis.dispatch({
action: 'view_create_room',
public: true,
+ defaultName: this.state.filterString.trim(),
});
};
- showRoomAlias(alias, autoJoin=false) {
+ private showRoomAlias(alias: string, autoJoin = false) {
this.showRoom(null, alias, autoJoin);
}
- showRoom(room, room_alias, autoJoin = false, shouldPeek = false) {
+ private showRoom(room: IRoom, roomAlias?: string, autoJoin = false, shouldPeek = false) {
this.onFinished();
- const payload = {
+ const payload: ActionPayload = {
action: 'view_room',
auto_join: autoJoin,
should_peek: shouldPeek,
@@ -444,20 +481,20 @@ export default class RoomDirectory extends React.Component {
// to the directory.
if (MatrixClientPeg.get().isGuest()) {
if (!room.world_readable && !room.guest_can_join) {
- dis.dispatch({action: 'require_registration'});
+ dis.dispatch({ action: 'require_registration' });
return;
}
}
- if (!room_alias) {
- room_alias = get_display_alias_for_room(room);
+ if (!roomAlias) {
+ roomAlias = getDisplayAliasForRoom(room);
}
payload.oob_data = {
avatarUrl: room.avatar_url,
// XXX: This logic is duplicated from the JS SDK which
// would normally decide what the name is.
- name: room.name || room_alias || _t('Unnamed room'),
+ name: room.name || roomAlias || _t('Unnamed room'),
};
if (this.state.roomServer) {
@@ -471,21 +508,19 @@ export default class RoomDirectory extends React.Component {
// which servers to start querying. However, there's no other way to join rooms in
// this list without aliases at present, so if roomAlias isn't set here we have no
// choice but to supply the ID.
- if (room_alias) {
- payload.room_alias = room_alias;
+ if (roomAlias) {
+ payload.room_alias = roomAlias;
} else {
payload.room_id = room.room_id;
}
dis.dispatch(payload);
}
- createRoomCells(room) {
+ private createRoomCells(room: IRoom) {
const client = MatrixClientPeg.get();
const clientRoom = client.getRoom(room.room_id);
const hasJoinedRoom = clientRoom && clientRoom.getMyMembership() === "join";
const isGuest = client.isGuest();
- const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
- const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
let previewButton;
let joinOrViewButton;
@@ -495,20 +530,26 @@ export default class RoomDirectory extends React.Component {
// it is readable, the preview appears as normal.
if (!hasJoinedRoom && (room.world_readable || isGuest)) {
previewButton = (
- this.onPreviewClick(ev, room)}>{_t("Preview")}
+ this.onPreviewClick(ev, room)}>
+ { _t("Preview") }
+
);
}
if (hasJoinedRoom) {
joinOrViewButton = (
- this.onViewClick(ev, room)}>{_t("View")}
+ this.onViewClick(ev, room)}>
+ { _t("View") }
+
);
} else if (!isGuest) {
joinOrViewButton = (
- this.onJoinClick(ev, room)}>{_t("Join")}
+ this.onJoinClick(ev, room)}>
+ { _t("Join") }
+
);
}
- let name = room.name || get_display_alias_for_room(room) || _t('Unnamed room');
+ let name = room.name || getDisplayAliasForRoom(room) || _t('Unnamed room');
if (name.length > MAX_NAME_LENGTH) {
name = `${name.substring(0, MAX_NAME_LENGTH)}...`;
}
@@ -524,72 +565,80 @@ export default class RoomDirectory extends React.Component {
let avatarUrl = null;
if (room.avatar_url) avatarUrl = mediaFromMxc(room.avatar_url).getSquareThumbnailHttp(32);
+ // We use onMouseDown instead of onClick, so that we can avoid text getting selected
return [
-
,
];
}
- collectScrollPanel = (element) => {
- this.scrollPanel = element;
- };
-
- _stringLooksLikeId(s, field_type) {
+ private stringLooksLikeId(s: string, fieldType: IFieldType) {
let pat = /^#[^\s]+:[^\s]/;
- if (field_type && field_type.regexp) {
- pat = new RegExp(field_type.regexp);
+ if (fieldType && fieldType.regexp) {
+ pat = new RegExp(fieldType.regexp);
}
return pat.test(s);
}
- _getFieldsForThirdPartyLocation(userInput, protocol, instance) {
+ private getFieldsForThirdPartyLocation(userInput: string, protocol: IProtocol, instance: IInstance) {
// make an object with the fields specified by that protocol. We
// require that the values of all but the last field come from the
// instance. The last is the user input.
@@ -605,71 +654,73 @@ export default class RoomDirectory extends React.Component {
return fields;
}
- /**
- * called by the parent component when PageUp/Down/etc is pressed.
- *
- * We pass it down to the scroll panel.
- */
- handleScrollKey = ev => {
- if (this.scrollPanel) {
- this.scrollPanel.handleScrollKey(ev);
- }
- };
-
- onFinished = () => {
+ private onFinished = () => {
CountlyAnalytics.instance.trackRoomDirectory(this.startTime);
- this.props.onFinished();
+ this.props.onFinished(false);
};
render() {
- const Loader = sdk.getComponent("elements.Spinner");
- const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
- const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
-
let content;
if (this.state.error) {
content = this.state.error;
} else if (this.state.protocolsLoading) {
- content = ;
+ content = ;
} else {
const cells = (this.state.publicRooms || [])
- .reduce((cells, room) => cells.concat(this.createRoomCells(room)), [],);
+ .reduce((cells, room) => cells.concat(this.createRoomCells(room)), []);
// we still show the scrollpanel, at least for now, because
// otherwise we don't fetch more because we don't get a fill
// request from the scrollpanel because there isn't one
let spinner;
if (this.state.loading) {
- spinner = ;
+ spinner = ;
}
- let scrollpanel_content;
+ const createNewButton = <>
+
+
+ { _t("Create new room") }
+
+ >;
+
+ let scrollPanelContent;
+ let footer;
if (cells.length === 0 && !this.state.loading) {
- scrollpanel_content = { _t('No rooms to show') };
+ footer = <>
+
{ _t('No results for "%(query)s"', { query: this.state.filterString.trim() }) }
+
+ { _t("Try different words or check for typos. " +
+ "Some results may not be visible as they're private and you need an invite to join them.") }
+
}
;
}
let listHeader;
if (!this.state.protocolsLoading) {
- const NetworkDropdown = sdk.getComponent('directory.NetworkDropdown');
- const DirectorySearchBox = sdk.getComponent('elements.DirectorySearchBox');
-
const protocolName = protocolNameForInstanceId(this.protocols, this.state.instanceId);
- let instance_expected_field_type;
+ let instanceExpectedFieldType;
if (
protocolName &&
this.protocols &&
@@ -677,21 +728,27 @@ export default class RoomDirectory extends React.Component {
this.protocols[protocolName].location_fields.length > 0 &&
this.protocols[protocolName].field_types
) {
- const last_field = this.protocols[protocolName].location_fields.slice(-1)[0];
- instance_expected_field_type = this.protocols[protocolName].field_types[last_field];
+ const lastField = this.protocols[protocolName].location_fields.slice(-1)[0];
+ instanceExpectedFieldType = this.protocols[protocolName].field_types[lastField];
}
let placeholder = _t('Find a room…');
if (!this.state.instanceId || this.state.instanceId === ALL_ROOMS) {
- placeholder = _t("Find a room… (e.g. %(exampleRoom)s)", {exampleRoom: "#example:" + this.state.roomServer});
- } else if (instance_expected_field_type) {
- placeholder = instance_expected_field_type.placeholder;
+ placeholder = _t("Find a room… (e.g. %(exampleRoom)s)", {
+ exampleRoom: "#example:" + this.state.roomServer,
+ });
+ } else if (instanceExpectedFieldType) {
+ placeholder = instanceExpectedFieldType.placeholder;
}
- let showJoinButton = this._stringLooksLikeId(this.state.filterString, instance_expected_field_type);
+ let showJoinButton = this.stringLooksLikeId(this.state.filterString, instanceExpectedFieldType);
if (protocolName) {
const instance = instanceForInstanceId(this.protocols, this.state.instanceId);
- if (this._getFieldsForThirdPartyLocation(this.state.filterString, this.protocols[protocolName], instance) === null) {
+ if (this.getFieldsForThirdPartyLocation(
+ this.state.filterString,
+ this.protocols[protocolName],
+ instance,
+ ) === null) {
showJoinButton = false;
}
}
@@ -723,12 +780,11 @@ export default class RoomDirectory extends React.Component {
}
const explanation =
_t("If you can't find the room you're looking for, ask for an invite or Create a new room.", null,
- {a: sub => {
- return ({sub});
- }},
+ { a: sub => (
+
+ { sub }
+
+ ) },
);
const title = this.state.selectedCommunityId
@@ -756,6 +812,6 @@ export default class RoomDirectory extends React.Component {
// Similar to matrix-react-sdk's MatrixTools.getDisplayAliasForRoom
// but works with the objects we get from the public room list
-function get_display_alias_for_room(room) {
- return room.canonical_alias || (room.aliases ? room.aliases[0] : "");
+function getDisplayAliasForRoom(room: IRoom) {
+ return room.canonical_alias || room.aliases?.[0] || "";
}
diff --git a/src/components/structures/RoomSearch.tsx b/src/components/structures/RoomSearch.tsx
index bda46aef07..9cdd1efe7e 100644
--- a/src/components/structures/RoomSearch.tsx
+++ b/src/components/structures/RoomSearch.tsx
@@ -108,22 +108,22 @@ export default class RoomSearch extends React.PureComponent {
};
private openSearch = () => {
- defaultDispatcher.dispatch({action: "show_left_panel"});
- defaultDispatcher.dispatch({action: "focus_room_filter"});
+ defaultDispatcher.dispatch({ action: "show_left_panel" });
+ defaultDispatcher.dispatch({ action: "focus_room_filter" });
};
private onChange = () => {
if (!this.inputRef.current) return;
- this.setState({query: this.inputRef.current.value});
+ this.setState({ query: this.inputRef.current.value });
};
private onFocus = (ev: React.FocusEvent) => {
- this.setState({focused: true});
+ this.setState({ focused: true });
ev.target.select();
};
private onBlur = (ev: React.FocusEvent) => {
- this.setState({focused: false});
+ this.setState({ focused: false });
};
private onKeyDown = (ev: React.KeyboardEvent) => {
diff --git a/src/components/structures/RoomStatusBar.js b/src/components/structures/RoomStatusBar.js
index b2f0c70bd7..f6e42a4f9c 100644
--- a/src/components/structures/RoomStatusBar.js
+++ b/src/components/structures/RoomStatusBar.js
@@ -17,15 +17,15 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import { _t, _td } from '../../languageHandler';
-import {MatrixClientPeg} from '../../MatrixClientPeg';
+import { MatrixClientPeg } from '../../MatrixClientPeg';
import Resend from '../../Resend';
import dis from '../../dispatcher/dispatcher';
-import {messageForResourceLimitError} from '../../utils/ErrorUtils';
-import {Action} from "../../dispatcher/actions";
-import {replaceableComponent} from "../../utils/replaceableComponent";
-import {EventStatus} from "matrix-js-sdk/src/models/event";
+import { messageForResourceLimitError } from '../../utils/ErrorUtils';
+import { Action } from "../../dispatcher/actions";
+import { replaceableComponent } from "../../utils/replaceableComponent";
+import { EventStatus } from "matrix-js-sdk/src/models/event";
import NotificationBadge from "../views/rooms/NotificationBadge";
-import {StaticNotificationState} from "../../stores/notifications/StaticNotificationState";
+import { StaticNotificationState } from "../../stores/notifications/StaticNotificationState";
import AccessibleButton from "../views/elements/AccessibleButton";
import InlineSpinner from "../views/elements/InlineSpinner";
@@ -41,7 +41,7 @@ export function getUnsentMessages(room) {
}
@replaceableComponent("structures.RoomStatusBar")
-export default class RoomStatusBar extends React.Component {
+export default class RoomStatusBar extends React.PureComponent {
static propTypes = {
// the room this statusbar is representing.
room: PropTypes.object.isRequired,
@@ -115,9 +115,9 @@ export default class RoomStatusBar extends React.Component {
_onResendAllClick = () => {
Resend.resendUnsentEvents(this.props.room).then(() => {
- this.setState({isResending: false});
+ this.setState({ isResending: false });
});
- this.setState({isResending: true});
+ this.setState({ isResending: true });
dis.fire(Action.FocusComposer);
};
diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx
index d822b6a839..81000a87a6 100644
--- a/src/components/structures/RoomView.tsx
+++ b/src/components/structures/RoomView.tsx
@@ -23,8 +23,9 @@ limitations under the License.
import React, { createRef } from 'react';
import classNames from 'classnames';
-import { Room } from "matrix-js-sdk/src/models/room";
+import { IRecommendedVersion, NotificationCountType, Room } from "matrix-js-sdk/src/models/room";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
+import { SearchResult } from "matrix-js-sdk/src/models/search-result";
import { EventSubscription } from "fbemitter";
import shouldHideEvent from '../../shouldHideEvent';
@@ -36,7 +37,6 @@ import Modal from '../../Modal';
import * as sdk from '../../index';
import CallHandler, { PlaceCallType } from '../../CallHandler';
import dis from '../../dispatcher/dispatcher';
-import Tinter from '../../Tinter';
import rateLimitedFunc from '../../ratelimitedfunc';
import * as Rooms from '../../Rooms';
import eventSearch, { searchPagination } from '../../Searching';
@@ -46,7 +46,7 @@ import RoomViewStore from '../../stores/RoomViewStore';
import RoomScrollStateStore from '../../stores/RoomScrollStateStore';
import WidgetEchoStore from '../../stores/WidgetEchoStore';
import SettingsStore from "../../settings/SettingsStore";
-import {Layout} from "../../settings/Layout";
+import { Layout } from "../../settings/Layout";
import AccessibleButton from "../views/elements/AccessibleButton";
import RightPanelStore from "../../stores/RightPanelStore";
import { haveTileForEvent } from "../views/rooms/EventTile";
@@ -54,16 +54,13 @@ import RoomContext from "../../contexts/RoomContext";
import MatrixClientContext from "../../contexts/MatrixClientContext";
import { E2EStatus, shieldStatusForRoom } from '../../utils/ShieldUtils';
import { Action } from "../../dispatcher/actions";
-import { SettingLevel } from "../../settings/SettingLevel";
import { IMatrixClientCreds } from "../../MatrixClientPeg";
import ScrollPanel from "./ScrollPanel";
import TimelinePanel from "./TimelinePanel";
import ErrorBoundary from "../views/elements/ErrorBoundary";
import RoomPreviewBar from "../views/rooms/RoomPreviewBar";
-import ForwardMessage from "../views/rooms/ForwardMessage";
-import SearchBar from "../views/rooms/SearchBar";
+import SearchBar, { SearchScope } from "../views/rooms/SearchBar";
import RoomUpgradeWarningBar from "../views/rooms/RoomUpgradeWarningBar";
-import PinnedEventsPanel from "../views/rooms/PinnedEventsPanel";
import AuxPanel from "../views/rooms/AuxPanel";
import RoomHeader from "../views/rooms/RoomHeader";
import { XOR } from "../../@types/common";
@@ -82,7 +79,9 @@ import { getKeyBindingsManager, RoomAction } from '../../KeyBindingsManager';
import { objectHasDiff } from "../../utils/objects";
import SpaceRoomView from "./SpaceRoomView";
import { IOpts } from "../../createRoom";
-import {replaceableComponent} from "../../utils/replaceableComponent";
+import { replaceableComponent } from "../../utils/replaceableComponent";
+import UIStore from "../../stores/UIStore";
+import EditorStateTransfer from "../../utils/EditorStateTransfer";
const DEBUG = false;
let debuglog = function(msg: string) {};
@@ -136,16 +135,15 @@ export interface IState {
// Whether to highlight the event scrolled to
isInitialEventHighlighted?: boolean;
replyToEvent?: MatrixEvent;
- forwardingEvent?: MatrixEvent;
numUnreadMessages: number;
draggingFile: boolean;
searching: boolean;
searchTerm?: string;
- searchScope?: "All" | "Room";
+ searchScope?: SearchScope;
searchResults?: XOR<{}, {
count: number;
highlights: string[];
- results: MatrixEvent[];
+ results: SearchResult[];
next_batch: string; // eslint-disable-line camelcase
}>;
searchHighlights?: string[];
@@ -155,8 +153,6 @@ export interface IState {
canPeek: boolean;
showApps: boolean;
isPeeking: boolean;
- showingPinned: boolean;
- showReadReceipts: boolean;
showRightPanel: boolean;
// error object, as from the matrix client/server API
// If we failed to load information about the room,
@@ -175,14 +171,17 @@ export interface IState {
statusBarVisible: boolean;
// We load this later by asking the js-sdk to suggest a version for us.
// This object is the result of Room#getRecommendedVersion()
- upgradeRecommendation?: {
- version: string;
- needsUpgrade: boolean;
- urgent: boolean;
- };
+
+ upgradeRecommendation?: IRecommendedVersion;
canReact: boolean;
canReply: boolean;
layout: Layout;
+ lowBandwidth: boolean;
+ showReadReceipts: boolean;
+ showRedactions: boolean;
+ showJoinLeaves: boolean;
+ showAvatarChanges: boolean;
+ showDisplaynameChanges: boolean;
matrixClientIsReady: boolean;
showUrlPreview?: boolean;
e2eStatus?: E2EStatus;
@@ -193,6 +192,7 @@ export interface IState {
// whether or not a spaces context switch brought us here,
// if it did we don't want the room to be marked as read as soon as it is loaded.
wasContextSwitch?: boolean;
+ editState?: EditorStateTransfer;
}
@replaceableComponent("structures.RoomView")
@@ -200,8 +200,7 @@ export default class RoomView extends React.Component {
private readonly dispatcherRef: string;
private readonly roomStoreToken: EventSubscription;
private readonly rightPanelStoreToken: EventSubscription;
- private readonly showReadReceiptsWatchRef: string;
- private readonly layoutWatcherRef: string;
+ private settingWatchers: string[];
private unmounted = false;
private permalinkCreators: Record = {};
@@ -232,8 +231,6 @@ export default class RoomView extends React.Component {
canPeek: false,
showApps: false,
isPeeking: false,
- showingPinned: false,
- showReadReceipts: true,
showRightPanel: RightPanelStore.getSharedInstance().isOpenForRoom,
joining: false,
atEndOfLiveTimeline: true,
@@ -243,6 +240,12 @@ export default class RoomView extends React.Component {
canReact: false,
canReply: false,
layout: SettingsStore.getValue("layout"),
+ lowBandwidth: SettingsStore.getValue("lowBandwidth"),
+ showReadReceipts: true,
+ showRedactions: true,
+ showJoinLeaves: true,
+ showAvatarChanges: true,
+ showDisplaynameChanges: true,
matrixClientIsReady: this.context && this.context.isInitialSyncComplete(),
dragCounter: 0,
};
@@ -269,16 +272,21 @@ export default class RoomView extends React.Component {
WidgetEchoStore.on(UPDATE_EVENT, this.onWidgetEchoStoreUpdate);
WidgetStore.instance.on(UPDATE_EVENT, this.onWidgetStoreUpdate);
- this.showReadReceiptsWatchRef = SettingsStore.watchSetting("showReadReceipts", null,
- this.onReadReceiptsChange);
- this.layoutWatcherRef = SettingsStore.watchSetting("layout", null, this.onLayoutChange);
+ this.settingWatchers = [
+ SettingsStore.watchSetting("layout", null, () =>
+ this.setState({ layout: SettingsStore.getValue("layout") }),
+ ),
+ SettingsStore.watchSetting("lowBandwidth", null, () =>
+ this.setState({ lowBandwidth: SettingsStore.getValue("lowBandwidth") }),
+ ),
+ ];
}
private onWidgetStoreUpdate = () => {
if (this.state.room) {
this.checkWidgets(this.state.room);
}
- }
+ };
private checkWidgets = (room) => {
this.setState({
@@ -324,14 +332,45 @@ export default class RoomView extends React.Component {
initialEventId: RoomViewStore.getInitialEventId(),
isInitialEventHighlighted: RoomViewStore.isInitialEventHighlighted(),
replyToEvent: RoomViewStore.getQuotingEvent(),
- forwardingEvent: RoomViewStore.getForwardingEvent(),
// we should only peek once we have a ready client
shouldPeek: this.state.matrixClientIsReady && RoomViewStore.shouldPeek(),
- showingPinned: SettingsStore.getValue("PinnedEvents.isOpen", roomId),
showReadReceipts: SettingsStore.getValue("showReadReceipts", roomId),
+ showRedactions: SettingsStore.getValue("showRedactions", roomId),
+ showJoinLeaves: SettingsStore.getValue("showJoinLeaves", roomId),
+ showAvatarChanges: SettingsStore.getValue("showAvatarChanges", roomId),
+ showDisplaynameChanges: SettingsStore.getValue("showDisplaynameChanges", roomId),
wasContextSwitch: RoomViewStore.getWasContextSwitch(),
};
+ // Add watchers for each of the settings we just looked up
+ this.settingWatchers = this.settingWatchers.concat([
+ SettingsStore.watchSetting("showReadReceipts", null, () =>
+ this.setState({
+ showReadReceipts: SettingsStore.getValue("showReadReceipts", roomId),
+ }),
+ ),
+ SettingsStore.watchSetting("showRedactions", null, () =>
+ this.setState({
+ showRedactions: SettingsStore.getValue("showRedactions", roomId),
+ }),
+ ),
+ SettingsStore.watchSetting("showJoinLeaves", null, () =>
+ this.setState({
+ showJoinLeaves: SettingsStore.getValue("showJoinLeaves", roomId),
+ }),
+ ),
+ SettingsStore.watchSetting("showAvatarChanges", null, () =>
+ this.setState({
+ showAvatarChanges: SettingsStore.getValue("showAvatarChanges", roomId),
+ }),
+ ),
+ SettingsStore.watchSetting("showDisplaynameChanges", null, () =>
+ this.setState({
+ showDisplaynameChanges: SettingsStore.getValue("showDisplaynameChanges", roomId),
+ }),
+ ),
+ ]);
+
if (!initial && this.state.shouldPeek && !newState.shouldPeek) {
// Stop peeking because we have joined this room now
this.context.stopPeeking();
@@ -490,7 +529,7 @@ export default class RoomView extends React.Component {
} else if (room) {
// Stop peeking because we have joined this room previously
this.context.stopPeeking();
- this.setState({isPeeking: false});
+ this.setState({ isPeeking: false });
}
}
}
@@ -528,7 +567,16 @@ export default class RoomView extends React.Component {
}
shouldComponentUpdate(nextProps, nextState) {
- return (objectHasDiff(this.props, nextProps) || objectHasDiff(this.state, nextState));
+ const hasPropsDiff = objectHasDiff(this.props, nextProps);
+
+ const { upgradeRecommendation, ...state } = this.state;
+ const { upgradeRecommendation: newUpgradeRecommendation, ...newState } = nextState;
+
+ const hasStateDiff =
+ newUpgradeRecommendation?.needsUpgrade !== upgradeRecommendation?.needsUpgrade ||
+ objectHasDiff(state, newState);
+
+ return hasPropsDiff || hasStateDiff;
}
componentDidUpdate() {
@@ -627,24 +675,24 @@ export default class RoomView extends React.Component {
);
}
- if (this.showReadReceiptsWatchRef) {
- SettingsStore.unwatchSetting(this.showReadReceiptsWatchRef);
- }
-
// cancel any pending calls to the rate_limited_funcs
this.updateRoomMembers.cancelPendingCall();
- // no need to do this as Dir & Settings are now overlays. It just burnt CPU.
- // console.log("Tinter.tint from RoomView.unmount");
- // Tinter.tint(); // reset colourscheme
-
- SettingsStore.unwatchSetting(this.layoutWatcherRef);
+ for (const watcher of this.settingWatchers) {
+ SettingsStore.unwatchSetting(watcher);
+ }
}
- private onLayoutChange = () => {
- this.setState({
- layout: SettingsStore.getValue("layout"),
- });
+ private onUserScroll = () => {
+ if (this.state.initialEventId && this.state.isInitialEventHighlighted) {
+ dis.dispatch({
+ action: 'view_room',
+ room_id: this.state.room.roomId,
+ event_id: this.state.initialEventId,
+ highlighted: false,
+ replyingToEvent: this.state.replyToEvent,
+ });
+ }
};
private onRightPanelStoreUpdate = () => {
@@ -764,6 +812,36 @@ export default class RoomView extends React.Component {
case 'focus_search':
this.onSearchClick();
break;
+
+ case "edit_event": {
+ const editState = payload.event ? new EditorStateTransfer(payload.event) : null;
+ this.setState({ editState }, () => {
+ if (payload.event) {
+ this.messagePanel?.scrollToEventIfNeeded(payload.event.getId());
+ }
+ });
+ break;
+ }
+
+ case Action.ComposerInsert: {
+ // re-dispatch to the correct composer
+ if (this.state.editState) {
+ dis.dispatch({
+ ...payload,
+ action: "edit_composer_insert",
+ });
+ } else {
+ dis.dispatch({
+ ...payload,
+ action: "send_composer_insert",
+ });
+ }
+ break;
+ }
+
+ case "scroll_to_bottom":
+ this.messagePanel?.jumpToLiveTimeline();
+ break;
}
};
@@ -797,9 +875,9 @@ export default class RoomView extends React.Component {
// update unread count when scrolled up
if (!this.state.searchResults && this.state.atEndOfLiveTimeline) {
// no change
- } else if (!shouldHideEvent(ev)) {
+ } else if (!shouldHideEvent(ev, this.state)) {
this.setState((state, props) => {
- return {numUnreadMessages: state.numUnreadMessages + 1};
+ return { numUnreadMessages: state.numUnreadMessages + 1 };
});
}
}
@@ -811,7 +889,7 @@ export default class RoomView extends React.Component {
};
private onEvent = (ev) => {
- if (ev.isBeingDecrypted() || ev.isDecryptionFailure() || ev.shouldAttemptDecryption()) return;
+ if (ev.isBeingDecrypted() || ev.isDecryptionFailure()) return;
this.handleEffects(ev);
};
@@ -824,7 +902,7 @@ export default class RoomView extends React.Component {
CHAT_EFFECTS.forEach(effect => {
if (containsEmoji(ev.getContent(), effect.emojis) || ev.getContent().msgtype === effect.msgType) {
- dis.dispatch({action: `effects.${effect.command}`});
+ dis.dispatch({ action: `effects.${effect.command}` });
}
});
};
@@ -877,7 +955,7 @@ export default class RoomView extends React.Component {
try {
await room.loadMembersIfNeeded();
if (!this.unmounted) {
- this.setState({membersLoaded: true});
+ this.setState({ membersLoaded: true });
}
} catch (err) {
const errorMessage = `Fetching room members for ${room.roomId} failed.` +
@@ -905,7 +983,7 @@ export default class RoomView extends React.Component {
}
}
- private updatePreviewUrlVisibility({roomId}: Room) {
+ private updatePreviewUrlVisibility({ roomId }: Room) {
// URL Previews in E2EE rooms can be a privacy leak so use a different setting which is per-room explicit
const key = this.context.isRoomEncrypted(roomId) ? 'urlPreviewsEnabled_e2ee' : 'urlPreviewsEnabled';
this.setState({
@@ -976,15 +1054,6 @@ export default class RoomView extends React.Component {
});
}
- private updateTint() {
- const room = this.state.room;
- if (!room) return;
-
- console.log("Tinter.tint from updateTint");
- const colorScheme = SettingsStore.getValue("roomColor", room.roomId);
- Tinter.tint(colorScheme.primary_color, colorScheme.secondary_color);
- }
-
private onAccountData = (event: MatrixEvent) => {
const type = event.getType();
if ((type === "org.matrix.preview_urls" || type === "im.vector.web.settings") && this.state.room) {
@@ -996,12 +1065,7 @@ export default class RoomView extends React.Component {
private onRoomAccountData = (event: MatrixEvent, room: Room) => {
if (room.roomId == this.state.roomId) {
const type = event.getType();
- if (type === "org.matrix.room.color_scheme") {
- const colorScheme = event.getContent();
- // XXX: we should validate the event
- console.log("Tinter.tint from onRoomAccountData");
- Tinter.tint(colorScheme.primary_color, colorScheme.secondary_color);
- } else if (type === "org.matrix.room.preview_urls" || type === "im.vector.web.settings") {
+ if (type === "org.matrix.room.preview_urls" || type === "im.vector.web.settings") {
// non-e2ee url previews are stored in legacy event type `org.matrix.room.preview_urls`
this.updatePreviewUrlVisibility(room);
}
@@ -1045,7 +1109,7 @@ export default class RoomView extends React.Component {
const canReact = room.getMyMembership() === "join" && room.currentState.maySendEvent("m.reaction", me);
const canReply = room.maySendMessage();
- this.setState({canReact, canReply});
+ this.setState({ canReact, canReply });
}
}
@@ -1074,7 +1138,7 @@ export default class RoomView extends React.Component {
}
}
- private onSearchResultsFillRequest = (backwards: boolean) => {
+ private onSearchResultsFillRequest = (backwards: boolean): Promise => {
if (!backwards) {
return Promise.resolve(false);
}
@@ -1109,12 +1173,13 @@ export default class RoomView extends React.Component {
room_id: this.getRoomId(),
},
});
- dis.dispatch({action: 'require_registration'});
+ dis.dispatch({ action: 'require_registration' });
} else {
Promise.resolve().then(() => {
const signUrl = this.props.threepidInvite?.signUrl;
dis.dispatch({
- action: 'join_room',
+ action: Action.JoinRoom,
+ roomId: this.getRoomId(),
opts: { inviteSignUrl: signUrl },
_type: "unknown", // TODO: instrumentation
});
@@ -1143,13 +1208,13 @@ export default class RoomView extends React.Component {
// We always increment the counter no matter the types, because dragging is
// still happening. If we didn't, the drag counter would get out of sync.
- this.setState({dragCounter: this.state.dragCounter + 1});
+ this.setState({ dragCounter: this.state.dragCounter + 1 });
// See:
// https://docs.w3cub.com/dom/datatransfer/types
// https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API/Recommended_drag_types#file
if (ev.dataTransfer.types.includes("Files") || ev.dataTransfer.types.includes("application/x-moz-file")) {
- this.setState({draggingFile: true});
+ this.setState({ draggingFile: true });
}
};
@@ -1198,7 +1263,7 @@ export default class RoomView extends React.Component {
private injectSticker(url, info, text) {
if (this.context.isGuest()) {
- dis.dispatch({action: 'require_registration'});
+ dis.dispatch({ action: 'require_registration' });
return;
}
@@ -1211,7 +1276,7 @@ export default class RoomView extends React.Component {
});
}
- private onSearch = (term: string, scope) => {
+ private onSearch = (term: string, scope: SearchScope) => {
this.setState({
searchTerm: term,
searchScope: scope,
@@ -1232,14 +1297,14 @@ export default class RoomView extends React.Component {
this.searchId = new Date().getTime();
let roomId;
- if (scope === "Room") roomId = this.state.room.roomId;
+ if (scope === SearchScope.Room) roomId = this.state.room.roomId;
debuglog("sending search request");
const searchPromise = eventSearch(term, roomId);
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;
@@ -1252,7 +1317,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
@@ -1284,6 +1349,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,
@@ -1375,13 +1441,6 @@ export default class RoomView extends React.Component {
return ret;
}
- private onPinnedClick = () => {
- const nowShowingPinned = !this.state.showingPinned;
- const roomId = this.state.room.roomId;
- this.setState({showingPinned: nowShowingPinned, searching: false});
- SettingsStore.setValue("PinnedEvents.isOpen", roomId, SettingLevel.ROOM_DEVICE, nowShowingPinned);
- };
-
private onCallPlaced = (type: PlaceCallType) => {
dis.dispatch({
action: 'place_call',
@@ -1394,18 +1453,6 @@ export default class RoomView extends React.Component {
dis.dispatch({ action: "open_room_settings" });
};
- private onCancelClick = () => {
- console.log("updateTint from onCancelClick");
- this.updateTint();
- if (this.state.forwardingEvent) {
- dis.dispatch({
- action: 'forward_event',
- event: null,
- });
- }
- dis.fire(Action.FocusComposer);
- };
-
private onAppsClick = () => {
dis.dispatch({
action: "appsDrawer",
@@ -1498,7 +1545,6 @@ export default class RoomView extends React.Component {
private onSearchClick = () => {
this.setState({
searching: !this.state.searching,
- showingPinned: false,
});
};
@@ -1511,8 +1557,19 @@ export default class RoomView extends React.Component {
// jump down to the bottom of this room, where new events are arriving
private jumpToLiveTimeline = () => {
- this.messagePanel.jumpToLiveTimeline();
- dis.fire(Action.FocusComposer);
+ if (this.state.initialEventId && this.state.isInitialEventHighlighted) {
+ // If we were viewing a highlighted event, firing view_room without
+ // an event will take care of both clearing the URL fragment and
+ // jumping to the bottom
+ dis.dispatch({
+ action: 'view_room',
+ room_id: this.state.room.roomId,
+ });
+ } else {
+ // Otherwise we have to jump manually
+ this.messagePanel.jumpToLiveTimeline();
+ dis.fire(Action.FocusComposer);
+ }
};
// jump up to wherever our read marker is
@@ -1534,7 +1591,7 @@ export default class RoomView extends React.Component {
const showBar = this.messagePanel.canJumpToReadMarker();
if (this.state.showTopUnreadMessagesBar != showBar) {
- this.setState({showTopUnreadMessagesBar: showBar});
+ this.setState({ showTopUnreadMessagesBar: showBar });
}
};
@@ -1585,32 +1642,48 @@ export default class RoomView extends React.Component {
// a maxHeight on the underlying remote video tag.
// header + footer + status + give us at least 120px of scrollback at all times.
- let auxPanelMaxHeight = window.innerHeight -
+ let auxPanelMaxHeight = UIStore.instance.windowHeight -
(54 + // height of RoomHeader
36 + // height of the status area
- 51 + // minimum height of the message compmoser
+ 51 + // minimum height of the message composer
120); // amount of desired scrollback
// XXX: this is a bit of a hack and might possibly cause the video to push out the page anyway
// but it's better than the video going missing entirely
if (auxPanelMaxHeight < 50) auxPanelMaxHeight = 50;
- this.setState({auxPanelMaxHeight: auxPanelMaxHeight});
+ if (this.state.auxPanelMaxHeight !== auxPanelMaxHeight) {
+ this.setState({ auxPanelMaxHeight });
+ }
};
private onStatusBarVisible = () => {
- if (this.unmounted) return;
- this.setState({
- statusBarVisible: true,
- });
+ if (this.unmounted || this.state.statusBarVisible) return;
+ this.setState({ statusBarVisible: true });
};
private onStatusBarHidden = () => {
// This is currently not desired as it is annoying if it keeps expanding and collapsing
- if (this.unmounted) return;
- this.setState({
- statusBarVisible: false,
- });
+ if (this.unmounted || !this.state.statusBarVisible) return;
+ this.setState({ statusBarVisible: false });
+ };
+
+ /**
+ * called by the parent component when PageUp/Down/etc is pressed.
+ *
+ * We pass it down to the scroll panel.
+ */
+ private handleScrollKey = ev => {
+ let panel;
+ if (this.searchResultsPanel.current) {
+ panel = this.searchResultsPanel.current;
+ } else if (this.messagePanel) {
+ panel = this.messagePanel;
+ }
+
+ if (panel) {
+ panel.handleScrollKey(ev);
+ }
};
/**
@@ -1627,10 +1700,6 @@ export default class RoomView extends React.Component {
// otherwise react calls it with null on each update.
private gatherTimelinePanelRef = r => {
this.messagePanel = r;
- if (r) {
- console.log("updateTint from RoomView.gatherTimelinePanelRef");
- this.updateTint();
- }
};
private getOldRoom() {
@@ -1649,7 +1718,7 @@ export default class RoomView extends React.Component {
onHiddenHighlightsClick = () => {
const oldRoom = this.getOldRoom();
if (!oldRoom) return;
- dis.dispatch({action: "view_room", room_id: oldRoom.roomId});
+ dis.dispatch({ action: "view_room", room_id: oldRoom.roomId });
};
render() {
@@ -1811,11 +1880,7 @@ export default class RoomView extends React.Component {
let aux = null;
let previewBar;
- let hideCancel = false;
- if (this.state.forwardingEvent) {
- aux = ;
- } else if (this.state.searching) {
- hideCancel = true; // has own cancel
+ if (this.state.searching) {
aux = {
/>;
} else if (showRoomUpgradeBar) {
aux = ;
- hideCancel = true;
- } else if (this.state.showingPinned) {
- hideCancel = true; // has own cancel
- aux = ;
} else if (myMembership !== "join") {
// We do have a room object for this room, but we're not currently in it.
// We may have a 3rd party invite to it.
@@ -1836,7 +1897,6 @@ export default class RoomView extends React.Component {
inviterName = this.props.oobData.inviterName;
}
const invitedEmail = this.props.threepidInvite?.toEmail;
- hideCancel = true;
previewBar = (
{
>
{_t(
"You have %(count)s unread notifications in a prior version of this room.",
- {count: hiddenHighlightCount},
+ { count: hiddenHighlightCount },
)}
);
@@ -1954,11 +2014,8 @@ export default class RoomView extends React.Component {
hideMessagePanel = true;
}
- const shouldHighlight = this.state.isInitialEventHighlighted;
let highlightedEventId = null;
- if (this.state.forwardingEvent) {
- highlightedEventId = this.state.forwardingEvent.getId();
- } else if (shouldHighlight) {
+ if (this.state.isInitialEventHighlighted) {
highlightedEventId = this.state.initialEventId;
}
@@ -1983,6 +2040,7 @@ export default class RoomView extends React.Component {
eventId={this.state.initialEventId}
eventPixelOffset={this.state.initialEventPixelOffset}
onScroll={this.onMessageListScroll}
+ onUserScroll={this.onUserScroll}
onReadMarkerUpdated={this.updateTopUnreadMessagesBar}
showUrlPreview = {this.state.showUrlPreview}
className={messagePanelClassNames}
@@ -1991,6 +2049,7 @@ export default class RoomView extends React.Component {
resizeNotifier={this.props.resizeNotifier}
showReactions={true}
layout={this.state.layout}
+ editState={this.state.editState}
/>);
let topUnreadMessagesBar = null;
@@ -2006,9 +2065,10 @@ export default class RoomView extends React.Component {
if (!this.state.atEndOfLiveTimeline && !this.state.searchResults) {
const JumpToBottomButton = sdk.getComponent('rooms.JumpToBottomButton');
jumpToBottom = ( 0}
+ highlight={this.state.room.getUnreadNotificationCount(NotificationCountType.Highlight) > 0}
numUnreadMessages={this.state.numUnreadMessages}
onScrollToBottomClick={this.jumpToLiveTimeline}
+ roomId={this.state.roomId}
/>);
}
@@ -2045,8 +2105,6 @@ export default class RoomView extends React.Component {
inRoom={myMembership === 'join'}
onSearchClick={this.onSearchClick}
onSettingsClick={this.onSettingsClick}
- onPinnedClick={this.onPinnedClick}
- onCancelClick={(aux && !hideCancel) ? this.onCancelClick : null}
onForgetClick={(myMembership === "leave") ? this.onForgetClick : null}
onLeaveClick={(myMembership === "join") ? this.onLeaveClick : null}
e2eStatus={this.state.e2eStatus}
diff --git a/src/components/structures/ScrollPanel.js b/src/components/structures/ScrollPanel.tsx
similarity index 72%
rename from src/components/structures/ScrollPanel.js
rename to src/components/structures/ScrollPanel.tsx
index 5c5062633d..df885575df 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,17 @@ 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 +43,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,93 +153,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,
-
- /* 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() {
@@ -199,18 +229,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
@@ -221,11 +251,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();
};
@@ -234,8 +264,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.
@@ -274,10 +304,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;
@@ -289,13 +319,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.
@@ -326,17 +356,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 = [];
@@ -344,13 +374,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) {
@@ -361,26 +391,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;
@@ -409,11 +439,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);
@@ -421,9 +451,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;
}
@@ -432,7 +462,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
@@ -441,13 +471,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) {
@@ -473,7 +503,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.
*
@@ -487,35 +517,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();
};
/**
@@ -523,33 +553,41 @@ 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) {
case RoomAction.ScrollUp:
this.scrollRelative(-1);
+ isScrolling = true;
break;
case RoomAction.RoomScrollDown:
this.scrollRelative(1);
+ isScrolling = true;
break;
case RoomAction.JumpToFirstMessage:
this.scrollToTop();
+ isScrolling = true;
break;
case RoomAction.JumpToLatestMessage:
this.scrollToBottom();
+ isScrolling = true;
break;
}
+ if (isScrolling && this.props.onUserScroll) {
+ this.props.onUserScroll(ev);
+ }
};
/* Scroll the panel to bring the DOM node with the scroll token
@@ -563,17 +601,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
@@ -581,36 +619,36 @@ export default class ScrollPanel extends React.Component {
// This because when setting the scrollTop only 10 or so events might be loaded,
// not giving enough content below the trackedNode to scroll downwards
// enough so it ends up in the top of the viewport.
- debuglog("scrollToken: setting scrollTop", {offsetBase, pixelOffset, offsetTop: trackedNode.offsetTop});
+ 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;
}
@@ -622,7 +660,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,
@@ -632,35 +670,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");
@@ -668,11 +706,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.");
}
@@ -682,14 +720,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) {
@@ -701,7 +739,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
@@ -718,22 +756,22 @@ export default class ScrollPanel extends React.Component {
// yield out of date values and cause a jump
// when setting it
sn.scrollBy(0, topDiff);
- debuglog("updateHeight to", {newHeight, topDiff});
+ debuglog("updateHeight to", { newHeight, topDiff });
}
}
}
- _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 &&
@@ -756,45 +794,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;
};
/**
@@ -802,15 +840,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;
@@ -829,8 +867,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;
@@ -845,12 +883,12 @@ 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 {offsetNode, offsetFromBottom} = this.preventShrinkingState;
+ 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;
// if the offsetNode got unmounted, clear
@@ -886,14 +924,15 @@ export default class ScrollPanel extends React.Component {
// list-style-type: none; is no longer a list
return (
{ this.props.fixedChildren }
-
+
{ this.props.children }
diff --git a/src/components/structures/SearchBox.js b/src/components/structures/SearchBox.js
index abeb858274..5c966d2d3a 100644
--- a/src/components/structures/SearchBox.js
+++ b/src/components/structures/SearchBox.js
@@ -15,14 +15,14 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import React, {createRef} from 'react';
+import React, { createRef } from 'react';
import PropTypes from 'prop-types';
import { Key } from '../../Keyboard';
import dis from '../../dispatcher/dispatcher';
-import {throttle} from 'lodash';
+import { throttle } from 'lodash';
import AccessibleButton from '../../components/views/elements/AccessibleButton';
import classNames from 'classnames';
-import {replaceableComponent} from "../../utils/replaceableComponent";
+import { replaceableComponent } from "../../utils/replaceableComponent";
@replaceableComponent("structures.SearchBox")
export default class SearchBox extends React.Component {
@@ -89,7 +89,7 @@ export default class SearchBox extends React.Component {
onSearch = throttle(() => {
this.props.onSearch(this._search.current.value);
- }, 200, {trailing: true, leading: true});
+ }, 200, { trailing: true, leading: true });
_onKeyDown = ev => {
switch (ev.key) {
@@ -101,7 +101,7 @@ export default class SearchBox extends React.Component {
};
_onFocus = ev => {
- this.setState({blurred: false});
+ this.setState({ blurred: false });
ev.target.select();
if (this.props.onFocus) {
this.props.onFocus(ev);
@@ -109,7 +109,7 @@ export default class SearchBox extends React.Component {
};
_onBlur = ev => {
- this.setState({blurred: true});
+ this.setState({ blurred: true });
if (this.props.onBlur) {
this.props.onBlur(ev);
}
@@ -147,7 +147,7 @@ export default class SearchBox extends React.Component {
this.props.placeholder;
const className = this.props.className || "";
return (
-
{ name }
{ suggestedSection }
@@ -278,7 +286,7 @@ export const HierarchyLevel = ({
const children = Array.from(relations.get(spaceId)?.values() || []);
const sortedChildren = sortBy(children, ev => {
// XXX: Space Summary API doesn't give the child origin_server_ts but once it does we should use it for sorting
- return getOrder(ev.content.order, null, ev.state_key);
+ return getChildOrder(ev.content.order, null, ev.state_key);
});
const [subspaces, childRooms] = sortedChildren.reduce((result, ev: ISpaceSummaryEvent) => {
const roomId = ev.state_key;
@@ -311,7 +319,7 @@ export const HierarchyLevel = ({
key={roomId}
room={rooms.get(roomId)}
numChildRooms={Array.from(relations.get(roomId)?.values() || [])
- .filter(ev => rooms.get(ev.state_key)?.room_type !== RoomType.Space).length}
+ .filter(ev => rooms.has(ev.state_key) && !rooms.get(ev.state_key).room_type).length}
suggested={relations.get(spaceId)?.get(roomId)?.content.suggested}
selected={selectedMap?.get(spaceId)?.has(roomId)}
onViewRoomClick={(autoJoin) => {
@@ -332,7 +340,7 @@ export const HierarchyLevel = ({
))
}
-
+ ;
};
// mutate argument refreshToken to force a reload
@@ -429,7 +437,7 @@ export const SpaceHierarchy: React.FC = ({
let content;
if (roomsMap) {
- const numRooms = Array.from(roomsMap.values()).filter(r => r.room_type !== RoomType.Space).length;
+ const numRooms = Array.from(roomsMap.values()).filter(r => !r.room_type).length;
const numSpaces = roomsMap.size - numRooms - 1; // -1 at the end to exclude the space we are looking at
let countsStr;
@@ -512,6 +520,7 @@ export const SpaceHierarchy: React.FC = ({
setError("Failed to update some suggestions. Try again later");
}
setSaving(false);
+ setSelected(new Map());
}}
kind="primary_outline"
disabled={disabled}
@@ -626,9 +635,9 @@ const SpaceRoomDirectory: React.FC = ({ space, onFinished, initialText }
{ _t("If you can't find the room you're looking for, ask for an invite or create a new room.",
null,
- {a: sub => {
+ { a: sub => {
return {sub};
- }},
+ } },
) }
{
) : null}
}
-
diff --git a/src/components/structures/TabbedView.tsx b/src/components/structures/TabbedView.tsx
index 0097d55cf5..3d77eaeac1 100644
--- a/src/components/structures/TabbedView.tsx
+++ b/src/components/structures/TabbedView.tsx
@@ -17,10 +17,10 @@ limitations under the License.
*/
import * as React from "react";
-import {_t} from '../../languageHandler';
+import { _t } from '../../languageHandler';
import * as sdk from "../../index";
import AutoHideScrollbar from './AutoHideScrollbar';
-import {replaceableComponent} from "../../utils/replaceableComponent";
+import { replaceableComponent } from "../../utils/replaceableComponent";
/**
* Represents a tab for the TabbedView.
@@ -75,7 +75,7 @@ export default class TabbedView extends React.Component {
private _setActiveTab(tab: Tab) {
const idx = this.props.tabs.indexOf(tab);
if (idx !== -1) {
- this.setState({activeTabIndex: idx});
+ this.setState({ activeTabIndex: idx });
} else {
console.error("Could not find tab " + tab.label + " in tabs");
}
diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.tsx
similarity index 73%
rename from src/components/structures/TimelinePanel.js
rename to src/components/structures/TimelinePanel.tsx
index af20c31cb2..e4c7d15596 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,15 +14,19 @@ 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 {EventTimeline} from "matrix-js-sdk/src/models/event-timeline";
-import {TimelineWindow} from "matrix-js-sdk/src/timeline-window";
+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 { MatrixClientPeg } from "../../MatrixClientPeg";
+import RoomContext from "../../contexts/RoomContext";
import UserActivity from "../../UserActivity";
import Modal from "../../Modal";
import dis from "../../dispatcher/dispatcher";
@@ -33,12 +34,19 @@ import * as sdk from "../../index";
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 {UIFeature} from "../../settings/UIFeature";
-import {objectHasDiff} from "../../utils/objects";
-import {replaceableComponent} from "../../utils/replaceableComponent";
+import { haveTileForEvent, TileShape } from "../views/rooms/EventTile";
+import { UIFeature } from "../../settings/UIFeature";
+import { replaceableComponent } from "../../utils/replaceableComponent";
import { arrayFastClone } from "../../utils/arrays";
+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";
+import EditorStateTransfer from '../../utils/EditorStateTransfer';
const PAGINATE_SIZE = 20;
const INITIAL_SIZE = 20;
@@ -46,82 +54,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 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,
- }
+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
@@ -131,16 +216,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;
@@ -149,82 +239,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
@@ -237,7 +286,7 @@ class TimelinePanel extends React.Component {
this.updateReadMarkerOnUserActivity();
}
- this._initTimeline(this.props);
+ this.initTimeline(this.props);
}
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
@@ -258,50 +307,28 @@ class TimelinePanel extends React.Component {
console.warn("Replacing timelineSet on a TimelinePanel - confusion may ensue");
}
- if (newProps.eventId != this.props.eventId) {
+ const differentEventId = newProps.eventId != this.props.eventId;
+ const differentHighlightedEventId = newProps.highlightedEventId != this.props.highlightedEventId;
+ if (differentEventId || differentHighlightedEventId) {
console.log("TimelinePanel switching to eventId " + newProps.eventId +
" (was " + this.props.eventId + ")");
- return this._initTimeline(newProps);
+ return this.initTimeline(newProps);
}
}
- shouldComponentUpdate(nextProps, nextState) {
- if (objectHasDiff(this.props, nextProps)) {
- if (DEBUG) {
- console.group("Timeline.shouldComponentUpdate: props change");
- console.log("props before:", this.props);
- console.log("props after:", nextProps);
- console.groupEnd();
- }
- return true;
- }
-
- if (objectHasDiff(this.state, nextState)) {
- if (DEBUG) {
- console.group("Timeline.shouldComponentUpdate: state change");
- console.log("state before:", this.state);
- console.log("state after:", nextState);
- console.groupEnd();
- }
- return true;
- }
-
- return false;
- }
-
componentWillUnmount() {
// set a boolean to say we've been unmounted, which any pending
// promises can use to throw away their results.
//
// (We could use isMounted, but facebook have deprecated that.)
this.unmounted = true;
- 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);
@@ -321,7 +348,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);
@@ -340,21 +367,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 {
@@ -363,8 +399,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';
@@ -375,9 +411,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);
}
@@ -387,15 +423,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,
@@ -408,7 +444,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;
}
@@ -419,9 +455,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));
@@ -430,7 +466,7 @@ class TimelinePanel extends React.Component {
});
};
- onMessageListScroll = e => {
+ private onMessageListScroll = e => {
if (this.props.onScroll) {
this.props.onScroll(e);
}
@@ -441,37 +477,35 @@ 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 => {
- if (payload.action === 'ignore_state_changed') {
- this.forceUpdate();
- }
- if (payload.action === "edit_event") {
- const editState = payload.event ? new EditorStateTransfer(payload.event) : null;
- this.setState({editState}, () => {
- if (payload.event && this._messagePanel.current) {
- this._messagePanel.current.scrollToEventIfNeeded(
- payload.event.getId(),
- );
- }
- });
- }
- if (payload.action === "scroll_to_bottom") {
- this.jumpToLiveTimeline();
+ private onAction = (payload: ActionPayload): void => {
+ switch (payload.action) {
+ case "ignore_state_changed":
+ this.forceUpdate();
+ break;
}
};
- 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;
@@ -479,13 +513,13 @@ 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.
- this.setState({canForwardPaginate: true});
+ this.setState({ canForwardPaginate: true });
return;
}
@@ -498,13 +532,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,
@@ -529,15 +563,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();
}
@@ -545,17 +579,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
@@ -566,7 +600,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
@@ -577,7 +611,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
@@ -586,22 +620,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
@@ -611,7 +645,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;
@@ -626,46 +660,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
@@ -676,8 +710,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.
//
@@ -692,11 +726,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) {
@@ -770,7 +804,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,
@@ -780,7 +814,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,
});
@@ -788,7 +822,7 @@ class TimelinePanel extends React.Component {
return;
}
const lastDisplayedEvent = this.state.events[lastDisplayedIndex];
- this._setReadMarker(
+ this.setReadMarker(
lastDisplayedEvent.getId(),
lastDisplayedEvent.getTs(),
);
@@ -805,15 +839,14 @@ class TimelinePanel extends React.Component {
this.sendReadReceipt();
};
-
// 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;
@@ -838,45 +871,47 @@ 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();
- }
+ this.messagePanel.current?.scrollToBottom();
}
};
+ public scrollToEventIfNeeded = (eventId: string): void => {
+ this.messagePanel.current?.scrollToEventIfNeeded(eventId);
+ };
+
/* 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;
}
@@ -884,15 +919,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);
@@ -904,28 +939,26 @@ 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?.isAtBottom()
+ && this.timelineWindow
+ && !this.timelineWindow.canPaginate(EventTimeline.FORWARDS);
+ };
/* get the current scroll state. See ScrollPanel.getScrollState for
* details.
*
* 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:
@@ -934,11 +967,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;
}
@@ -957,7 +990,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
@@ -972,19 +1005,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;
@@ -995,7 +1028,7 @@ class TimelinePanel extends React.Component {
offsetBase = 0.5;
}
- return this._loadTimeline(initialEvent, pixelOffset, offsetBase);
+ return this.loadTimeline(initialEvent, pixelOffset, offsetBase);
}
/**
@@ -1011,34 +1044,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});
+ { 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
@@ -1048,10 +1079,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) {
@@ -1113,10 +1144,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: [],
@@ -1131,17 +1162,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
@@ -1153,14 +1184,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());
}
@@ -1181,7 +1212,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 ||
@@ -1245,7 +1276,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;
@@ -1254,15 +1285,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;
@@ -1305,7 +1335,7 @@ class TimelinePanel extends React.Component {
const shouldIgnore = !!ev.status || // local echo
(ignoreOwn && ev.sender && ev.sender.userId == myUserId); // own message
- const isWithoutTile = !haveTileForEvent(ev) || shouldHideEvent(ev);
+ const isWithoutTile = !haveTileForEvent(ev) || shouldHideEvent(ev, this.context);
if (isWithoutTile || !node) {
// don't start counting if the event should be ignored,
@@ -1339,7 +1369,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) {
@@ -1350,7 +1380,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
@@ -1375,7 +1405,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.
@@ -1386,12 +1416,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
@@ -1406,7 +1433,7 @@ class TimelinePanel extends React.Component {
if (this.state.timelineLoading) {
return (
-
+
);
}
@@ -1427,7 +1454,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.
@@ -1440,7 +1467,7 @@ class TimelinePanel extends React.Component {
: this.state.events;
return (
[];
@@ -55,9 +55,10 @@ export default class ToastContainer extends React.Component<{}, IState> {
const totalCount = this.state.toasts.length;
const isStacked = totalCount > 1;
let toast;
+ let containerClasses;
if (totalCount !== 0) {
const topToast = this.state.toasts[0];
- const {title, icon, key, component, className, props} = topToast;
+ const { title, icon, key, component, className, props } = topToast;
const toastClasses = classNames("mx_Toast_toast", {
"mx_Toast_hasIcon": icon,
[`mx_Toast_icon_${icon}`]: icon,
@@ -79,16 +80,17 @@ export default class ToastContainer extends React.Component<{}, IState> {
diff --git a/src/components/structures/UserView.js b/src/components/structures/UserView.js
index 6b472783bb..eb839be7be 100644
--- a/src/components/structures/UserView.js
+++ b/src/components/structures/UserView.js
@@ -17,14 +17,14 @@ limitations under the License.
import React from "react";
import PropTypes from "prop-types";
-import {MatrixClientPeg} from "../../MatrixClientPeg";
+import { MatrixClientPeg } from "../../MatrixClientPeg";
import * as sdk from "../../index";
import Modal from '../../Modal';
import { _t } from '../../languageHandler';
import HomePage from "./HomePage";
-import {replaceableComponent} from "../../utils/replaceableComponent";
-import {MatrixEvent} from "matrix-js-sdk/src/models/event";
-import {RoomMember} from "matrix-js-sdk/src/models/room-member";
+import { replaceableComponent } from "../../utils/replaceableComponent";
+import { MatrixEvent } from "matrix-js-sdk/src/models/event";
+import { RoomMember } from "matrix-js-sdk/src/models/room-member";
@replaceableComponent("structures.UserView")
export default class UserView extends React.Component {
@@ -56,7 +56,7 @@ export default class UserView extends React.Component {
async _loadProfileInfo() {
const cli = MatrixClientPeg.get();
- this.setState({loading: true});
+ this.setState({ loading: true });
let profileInfo;
try {
profileInfo = await cli.getProfileInfo(this.props.userId);
@@ -66,13 +66,13 @@ export default class UserView extends React.Component {
title: _t('Could not load user profile'),
description: ((err && err.message) ? err.message : _t("Operation failed")),
});
- this.setState({loading: false});
+ this.setState({ loading: false });
return;
}
- const fakeEvent = new MatrixEvent({type: "m.room.member", content: profileInfo});
+ const fakeEvent = new MatrixEvent({ type: "m.room.member", content: profileInfo });
const member = new RoomMember(null, this.props.userId);
member.setMembershipEvent(fakeEvent);
- this.setState({member, loading: false});
+ this.setState({ member, loading: false });
}
render() {
diff --git a/src/components/structures/ViewSource.js b/src/components/structures/ViewSource.js
index 6fe99dd464..b69a92dd61 100644
--- a/src/components/structures/ViewSource.js
+++ b/src/components/structures/ViewSource.js
@@ -55,7 +55,7 @@ export default class ViewSource extends React.Component {
viewSourceContent() {
const mxEvent = this.props.mxEvent.replacingEvent() || this.props.mxEvent; // show the replacing event, not the original, if it is an edit
const isEncrypted = mxEvent.isEncrypted();
- const decryptedEventSource = mxEvent._clearEvent; // FIXME: _clearEvent is private
+ const decryptedEventSource = mxEvent.clearEvent; // FIXME: clearEvent is private
const originalEventSource = mxEvent.event;
if (isEncrypted) {
diff --git a/src/components/structures/auth/CompleteSecurity.js b/src/components/structures/auth/CompleteSecurity.js
index 49fcf20415..d691f6034b 100644
--- a/src/components/structures/auth/CompleteSecurity.js
+++ b/src/components/structures/auth/CompleteSecurity.js
@@ -18,16 +18,9 @@ import React from 'react';
import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler';
import * as sdk from '../../../index';
-import {
- SetupEncryptionStore,
- PHASE_LOADING,
- PHASE_INTRO,
- PHASE_BUSY,
- PHASE_DONE,
- PHASE_CONFIRM_SKIP,
-} from '../../../stores/SetupEncryptionStore';
+import { SetupEncryptionStore, Phase } from '../../../stores/SetupEncryptionStore';
import SetupEncryptionBody from "./SetupEncryptionBody";
-import {replaceableComponent} from "../../../utils/replaceableComponent";
+import { replaceableComponent } from "../../../utils/replaceableComponent";
@replaceableComponent("structures.auth.CompleteSecurity")
export default class CompleteSecurity extends React.Component {
@@ -40,12 +33,12 @@ export default class CompleteSecurity extends React.Component {
const store = SetupEncryptionStore.sharedInstance();
store.on("update", this._onStoreUpdate);
store.start();
- this.state = {phase: store.phase};
+ this.state = { phase: store.phase };
}
_onStoreUpdate = () => {
const store = SetupEncryptionStore.sharedInstance();
- this.setState({phase: store.phase});
+ this.setState({ phase: store.phase });
};
componentWillUnmount() {
@@ -57,22 +50,22 @@ export default class CompleteSecurity extends React.Component {
render() {
const AuthPage = sdk.getComponent("auth.AuthPage");
const CompleteSecurityBody = sdk.getComponent("auth.CompleteSecurityBody");
- const {phase} = this.state;
+ const { phase } = this.state;
let icon;
let title;
- if (phase === PHASE_LOADING) {
+ if (phase === Phase.Loading) {
return null;
- } else if (phase === PHASE_INTRO) {
+ } else if (phase === Phase.Intro) {
icon = ;
title = _t("Verify this login");
- } else if (phase === PHASE_DONE) {
+ } else if (phase === Phase.Done) {
icon = ;
title = _t("Session verified");
- } else if (phase === PHASE_CONFIRM_SKIP) {
+ } else if (phase === Phase.ConfirmSkip) {
icon = ;
title = _t("Are you sure?");
- } else if (phase === PHASE_BUSY) {
+ } else if (phase === Phase.Busy) {
icon = ;
title = _t("Verify this login");
} else {
diff --git a/src/components/structures/auth/E2eSetup.js b/src/components/structures/auth/E2eSetup.js
index 4e51ae828c..9b627449bc 100644
--- a/src/components/structures/auth/E2eSetup.js
+++ b/src/components/structures/auth/E2eSetup.js
@@ -19,7 +19,7 @@ import PropTypes from 'prop-types';
import AuthPage from '../../views/auth/AuthPage';
import CompleteSecurityBody from '../../views/auth/CompleteSecurityBody';
import CreateCrossSigningDialog from '../../views/dialogs/security/CreateCrossSigningDialog';
-import {replaceableComponent} from "../../../utils/replaceableComponent";
+import { replaceableComponent } from "../../../utils/replaceableComponent";
@replaceableComponent("structures.auth.E2eSetup")
export default class E2eSetup extends React.Component {
diff --git a/src/components/structures/auth/ForgotPassword.js b/src/components/structures/auth/ForgotPassword.js
index 6188fdb5e4..9f2ac9deed 100644
--- a/src/components/structures/auth/ForgotPassword.js
+++ b/src/components/structures/auth/ForgotPassword.js
@@ -22,13 +22,13 @@ import { _t, _td } from '../../../languageHandler';
import * as sdk from '../../../index';
import Modal from "../../../Modal";
import PasswordReset from "../../../PasswordReset";
-import AutoDiscoveryUtils, {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
+import AutoDiscoveryUtils, { ValidatedServerConfig } from "../../../utils/AutoDiscoveryUtils";
import classNames from 'classnames';
import AuthPage from "../../views/auth/AuthPage";
import CountlyAnalytics from "../../../CountlyAnalytics";
import ServerPicker from "../../views/elements/ServerPicker";
import PassphraseField from '../../views/auth/PassphraseField';
-import {replaceableComponent} from "../../../utils/replaceableComponent";
+import { replaceableComponent } from "../../../utils/replaceableComponent";
import { PASSWORD_MIN_SCORE } from '../../views/auth/RegistrationForm';
// Phases
diff --git a/src/components/structures/auth/Login.tsx b/src/components/structures/auth/Login.tsx
index 34a5410928..61d3759dee 100644
--- a/src/components/structures/auth/Login.tsx
+++ b/src/components/structures/auth/Login.tsx
@@ -14,28 +14,28 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import React, {ReactNode} from 'react';
-import {MatrixError} from "matrix-js-sdk/src/http-api";
+import React, { ReactNode } from 'react';
+import { MatrixError } from "matrix-js-sdk/src/http-api";
-import {_t, _td} from '../../../languageHandler';
+import { _t, _td } from '../../../languageHandler';
import * as sdk from '../../../index';
-import Login, {ISSOFlow, LoginFlow} from '../../../Login';
+import Login, { ISSOFlow, LoginFlow } from '../../../Login';
import SdkConfig from '../../../SdkConfig';
import { messageForResourceLimitError } from '../../../utils/ErrorUtils';
-import AutoDiscoveryUtils, {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
+import AutoDiscoveryUtils, { ValidatedServerConfig } from "../../../utils/AutoDiscoveryUtils";
import classNames from "classnames";
import AuthPage from "../../views/auth/AuthPage";
import PlatformPeg from '../../../PlatformPeg';
import SettingsStore from "../../../settings/SettingsStore";
-import {UIFeature} from "../../../settings/UIFeature";
+import { UIFeature } from "../../../settings/UIFeature";
import CountlyAnalytics from "../../../CountlyAnalytics";
-import {IMatrixClientCreds} from "../../../MatrixClientPeg";
+import { IMatrixClientCreds } from "../../../MatrixClientPeg";
import PasswordLogin from "../../views/auth/PasswordLogin";
import InlineSpinner from "../../views/elements/InlineSpinner";
import Spinner from "../../views/elements/Spinner";
import SSOButtons from "../../views/elements/SSOButtons";
import ServerPicker from "../../views/elements/ServerPicker";
-import {replaceableComponent} from "../../../utils/replaceableComponent";
+import { replaceableComponent } from "../../../utils/replaceableComponent";
// These are used in several places, and come from the js-sdk's autodiscovery
// stuff. We define them here so that they'll be picked up by i18n.
@@ -59,6 +59,7 @@ interface IProps {
fallbackHsUrl?: string;
defaultDeviceDisplayName?: string;
fragmentAfterLogin?: string;
+ defaultUsername?: string;
// Called when the user has logged in. Params:
// - The object returned by the login API
@@ -119,7 +120,7 @@ export default class LoginComponent extends React.PureComponent
flows: null,
- username: "",
+ username: props.defaultUsername? props.defaultUsername: '',
phoneCountry: null,
phoneNumber: "",
@@ -165,7 +166,7 @@ export default class LoginComponent extends React.PureComponent
onPasswordLogin = async (username, phoneCountry, phoneNumber, password) => {
if (!this.state.serverIsAlive) {
- this.setState({busy: true});
+ this.setState({ busy: true });
// Do a quick liveliness check on the URLs
let aliveAgain = true;
try {
@@ -173,7 +174,7 @@ export default class LoginComponent extends React.PureComponent
this.props.serverConfig.hsUrl,
this.props.serverConfig.isUrl,
);
- this.setState({serverIsAlive: true, errorText: ""});
+ this.setState({ serverIsAlive: true, errorText: "" });
} catch (e) {
const componentState = AutoDiscoveryUtils.authComponentStateForError(e);
this.setState({
@@ -200,7 +201,7 @@ export default class LoginComponent extends React.PureComponent
this.loginLogic.loginViaPassword(
username, phoneCountry, phoneNumber, password,
).then((data) => {
- this.setState({serverIsAlive: true}); // it must be, we logged in.
+ this.setState({ serverIsAlive: true }); // it must be, we logged in.
this.props.onLoggedIn(data, password);
}, (error) => {
if (this.unmounted) {
@@ -251,7 +252,7 @@ export default class LoginComponent extends React.PureComponent
{_t(
'Please note you are logging into the %(hs)s server, not matrix.org.',
- {hs: this.props.serverConfig.hsName},
+ { hs: this.props.serverConfig.hsName },
)}
@@ -362,7 +363,7 @@ export default class LoginComponent extends React.PureComponent
}
};
- private async initLoginLogic({hsUrl, isUrl}: ValidatedServerConfig) {
+ private async initLoginLogic({ hsUrl, isUrl }: ValidatedServerConfig) {
let isDefaultServer = false;
if (this.props.serverConfig.isDefault
&& hsUrl === this.props.serverConfig.hsUrl
@@ -500,9 +501,9 @@ export default class LoginComponent extends React.PureComponent
return
{ flows.map(flow => {
const stepRenderer = this.stepRendererMap[flow.type];
- return { stepRenderer() }
+ return { stepRenderer() };
}) }
-
+ ;
}
private renderPasswordStep = () => {
diff --git a/src/components/structures/auth/Registration.tsx b/src/components/structures/auth/Registration.tsx
index 96fb9bdc82..f27bed2cc3 100644
--- a/src/components/structures/auth/Registration.tsx
+++ b/src/components/structures/auth/Registration.tsx
@@ -14,23 +14,23 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import {createClient} from 'matrix-js-sdk/src/matrix';
-import React, {ReactNode} from 'react';
-import {MatrixClient} from "matrix-js-sdk/src/client";
+import { createClient } from 'matrix-js-sdk/src/matrix';
+import React, { ReactNode } from 'react';
+import { MatrixClient } from "matrix-js-sdk/src/client";
import * as sdk from '../../../index';
import { _t, _td } from '../../../languageHandler';
import { messageForResourceLimitError } from '../../../utils/ErrorUtils';
-import AutoDiscoveryUtils, {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
+import AutoDiscoveryUtils, { ValidatedServerConfig } from "../../../utils/AutoDiscoveryUtils";
import classNames from "classnames";
import * as Lifecycle from '../../../Lifecycle';
-import {MatrixClientPeg} from "../../../MatrixClientPeg";
+import { MatrixClientPeg } from "../../../MatrixClientPeg";
import AuthPage from "../../views/auth/AuthPage";
-import Login, {ISSOFlow} from "../../../Login";
+import Login, { ISSOFlow } from "../../../Login";
import dis from "../../../dispatcher/dispatcher";
import SSOButtons from "../../views/elements/SSOButtons";
import ServerPicker from '../../views/elements/ServerPicker';
-import {replaceableComponent} from "../../../utils/replaceableComponent";
+import { replaceableComponent } from "../../../utils/replaceableComponent";
interface IProps {
serverConfig: ValidatedServerConfig;
@@ -61,7 +61,7 @@ interface IProps {
is_url?: string;
session_id: string;
/* eslint-enable camelcase */
- }): void;
+ }): string;
// registration shouldn't know or care how login is done.
onLoginClick(): void;
onServerConfigChange(config: ValidatedServerConfig): void;
@@ -131,7 +131,7 @@ export default class Registration extends React.Component {
serverDeadError: "",
};
- const {hsUrl, isUrl} = this.props.serverConfig;
+ const { hsUrl, isUrl } = this.props.serverConfig;
this.loginLogic = new Login(hsUrl, isUrl, null, {
defaultDeviceDisplayName: "Element login check", // We shouldn't ever be used
});
@@ -180,7 +180,7 @@ export default class Registration extends React.Component {
}
}
- const {hsUrl, isUrl} = serverConfig;
+ const { hsUrl, isUrl } = serverConfig;
const cli = createClient({
baseUrl: hsUrl,
idBaseUrl: isUrl,
@@ -223,13 +223,14 @@ export default class Registration extends React.Component {
this.setState({
flows: e.data.flows,
});
- } else if (e.httpStatus === 403 && e.errcode === "M_UNKNOWN") {
+ } else if (e.httpStatus === 403 || e.errcode === "M_FORBIDDEN") {
+ // Check for 403 or M_FORBIDDEN, Synapse used to send 403 M_UNKNOWN but now sends 403 M_FORBIDDEN.
// At this point registration is pretty much disabled, but before we do that let's
// quickly check to see if the server supports SSO instead. If it does, we'll send
// the user off to the login page to figure their account out.
if (ssoFlow) {
// Redirect to login page - server probably expects SSO only
- dis.dispatch({action: 'start_login'});
+ dis.dispatch({ action: 'start_login' });
} else {
this.setState({
serverErrorIsFatal: true, // fatal because user cannot continue on this server
@@ -266,9 +267,9 @@ export default class Registration extends React.Component {
session_id: sessionId,
}),
);
- }
+ };
- private onUIAuthFinished = async (success, response, extra) => {
+ private onUIAuthFinished = async (success: boolean, response: any) => {
if (!success) {
let msg = response.message || response.toString();
// can we give a better error message?
@@ -431,7 +432,7 @@ export default class Registration extends React.Component {
private onLoginClickWithCheck = async ev => {
ev.preventDefault();
- const sessionLoaded = await Lifecycle.loadSession({ignoreGuest: true});
+ const sessionLoaded = await Lifecycle.loadSession({ ignoreGuest: true });
if (!sessionLoaded) {
// ok fine, there's still no session: really go to the login page
this.props.onLoginClick();
@@ -467,7 +468,7 @@ export default class Registration extends React.Component {
let ssoSection;
if (this.state.ssoFlow) {
let continueWithSection;
- const providers = this.state.ssoFlow["org.matrix.msc2858.identity_providers"] || [];
+ const providers = this.state.ssoFlow.identity_providers || [];
// when there is only a single (or 0) providers we show a wide button with `Continue with X` text
if (providers.length > 1) {
// i18n: ssoButtons is a placeholder to help translators understand context
@@ -486,7 +487,13 @@ export default class Registration extends React.Component {
fragmentAfterLogin={this.props.fragmentAfterLogin}
/>
+
+ {/* easiest way to introduce a gap between the components */}
+ { this.renderFileSize() }
+
+
+
+
+
+
+
+
;
+ }
+}
diff --git a/src/components/views/voice_messages/Clock.tsx b/src/components/views/audio_messages/Clock.tsx
similarity index 90%
rename from src/components/views/voice_messages/Clock.tsx
rename to src/components/views/audio_messages/Clock.tsx
index 23e6762c52..7f387715f8 100644
--- a/src/components/views/voice_messages/Clock.tsx
+++ b/src/components/views/audio_messages/Clock.tsx
@@ -15,9 +15,9 @@ limitations under the License.
*/
import React from "react";
-import {replaceableComponent} from "../../../utils/replaceableComponent";
+import { replaceableComponent } from "../../../utils/replaceableComponent";
-interface IProps {
+export interface IProps {
seconds: number;
}
@@ -28,7 +28,7 @@ interface IState {
* Simply converts seconds into minutes and seconds. Note that hours will not be
* displayed, making it possible to see "82:29".
*/
-@replaceableComponent("views.voice_messages.Clock")
+@replaceableComponent("views.audio_messages.Clock")
export default class Clock extends React.Component {
public constructor(props) {
super(props);
diff --git a/src/components/views/audio_messages/DurationClock.tsx b/src/components/views/audio_messages/DurationClock.tsx
new file mode 100644
index 0000000000..81852b5944
--- /dev/null
+++ b/src/components/views/audio_messages/DurationClock.tsx
@@ -0,0 +1,55 @@
+/*
+Copyright 2021 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import React from "react";
+import { replaceableComponent } from "../../../utils/replaceableComponent";
+import Clock from "./Clock";
+import { Playback } from "../../../voice/Playback";
+
+interface IProps {
+ playback: Playback;
+}
+
+interface IState {
+ durationSeconds: number;
+}
+
+/**
+ * A clock which shows a clip's maximum duration.
+ */
+@replaceableComponent("views.audio_messages.DurationClock")
+export default class DurationClock extends React.PureComponent {
+ public constructor(props) {
+ super(props);
+
+ this.state = {
+ // we track the duration on state because we won't really know what the clip duration
+ // is until the first time update, and as a PureComponent we are trying to dedupe state
+ // updates as much as possible. This is just the easiest way to avoid a forceUpdate() or
+ // member property to track "did we get a duration".
+ durationSeconds: this.props.playback.clockInfo.durationSeconds,
+ };
+ this.props.playback.clockInfo.liveData.onUpdate(this.onTimeUpdate);
+ }
+
+ private onTimeUpdate = (time: number[]) => {
+ this.setState({ durationSeconds: time[1] });
+ };
+
+ public render() {
+ return ;
+ }
+}
diff --git a/src/components/views/voice_messages/LiveRecordingClock.tsx b/src/components/views/audio_messages/LiveRecordingClock.tsx
similarity index 52%
rename from src/components/views/voice_messages/LiveRecordingClock.tsx
rename to src/components/views/audio_messages/LiveRecordingClock.tsx
index b82539eb16..a9dbd3c52f 100644
--- a/src/components/views/voice_messages/LiveRecordingClock.tsx
+++ b/src/components/views/audio_messages/LiveRecordingClock.tsx
@@ -15,9 +15,10 @@ limitations under the License.
*/
import React from "react";
-import {IRecordingUpdate, VoiceRecording} from "../../../voice/VoiceRecording";
-import {replaceableComponent} from "../../../utils/replaceableComponent";
+import { IRecordingUpdate, VoiceRecording } from "../../../voice/VoiceRecording";
+import { replaceableComponent } from "../../../utils/replaceableComponent";
import Clock from "./Clock";
+import { MarkedExecution } from "../../../utils/MarkedExecution";
interface IProps {
recorder: VoiceRecording;
@@ -30,18 +31,33 @@ interface IState {
/**
* A clock for a live recording.
*/
-@replaceableComponent("views.voice_messages.LiveRecordingClock")
+@replaceableComponent("views.audio_messages.LiveRecordingClock")
export default class LiveRecordingClock extends React.PureComponent {
- public constructor(props) {
- super(props);
+ private seconds = 0;
+ private scheduledUpdate = new MarkedExecution(
+ () => this.updateClock(),
+ () => requestAnimationFrame(() => this.scheduledUpdate.trigger()),
+ );
- this.state = {seconds: 0};
- this.props.recorder.liveData.onUpdate(this.onRecordingUpdate);
+ constructor(props) {
+ super(props);
+ this.state = {
+ seconds: 0,
+ };
}
- private onRecordingUpdate = (update: IRecordingUpdate) => {
- this.setState({seconds: update.timeSeconds});
- };
+ componentDidMount() {
+ this.props.recorder.liveData.onUpdate((update: IRecordingUpdate) => {
+ this.seconds = update.timeSeconds;
+ this.scheduledUpdate.mark();
+ });
+ }
+
+ private updateClock() {
+ this.setState({
+ seconds: this.seconds,
+ });
+ }
public render() {
return ;
diff --git a/src/components/views/audio_messages/LiveRecordingWaveform.tsx b/src/components/views/audio_messages/LiveRecordingWaveform.tsx
new file mode 100644
index 0000000000..b9c5f80f05
--- /dev/null
+++ b/src/components/views/audio_messages/LiveRecordingWaveform.tsx
@@ -0,0 +1,74 @@
+/*
+Copyright 2021 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import React from "react";
+import { IRecordingUpdate, RECORDING_PLAYBACK_SAMPLES, VoiceRecording } from "../../../voice/VoiceRecording";
+import { replaceableComponent } from "../../../utils/replaceableComponent";
+import { arrayFastResample } from "../../../utils/arrays";
+import { percentageOf } from "../../../utils/numbers";
+import Waveform from "./Waveform";
+import { MarkedExecution } from "../../../utils/MarkedExecution";
+
+interface IProps {
+ recorder: VoiceRecording;
+}
+
+interface IState {
+ waveform: number[];
+}
+
+/**
+ * A waveform which shows the waveform of a live recording
+ */
+@replaceableComponent("views.audio_messages.LiveRecordingWaveform")
+export default class LiveRecordingWaveform extends React.PureComponent {
+ public static defaultProps = {
+ progress: 1,
+ };
+
+ private waveform: number[] = [];
+ private scheduledUpdate = new MarkedExecution(
+ () => this.updateWaveform(),
+ () => requestAnimationFrame(() => this.scheduledUpdate.trigger()),
+ );
+
+ constructor(props) {
+ super(props);
+ this.state = {
+ waveform: [],
+ };
+ }
+
+ componentDidMount() {
+ this.props.recorder.liveData.onUpdate((update: IRecordingUpdate) => {
+ const bars = arrayFastResample(Array.from(update.waveform), RECORDING_PLAYBACK_SAMPLES);
+ // The incoming data is between zero and one, but typically even screaming into a
+ // microphone won't send you over 0.6, so we artificially adjust the gain for the
+ // waveform. This results in a slightly more cinematic/animated waveform for the
+ // user.
+ this.waveform = bars.map(b => percentageOf(b, 0, 0.50));
+ this.scheduledUpdate.mark();
+ });
+ }
+
+ private updateWaveform() {
+ this.setState({ waveform: this.waveform });
+ }
+
+ public render() {
+ return ;
+ }
+}
diff --git a/src/components/views/voice_messages/PlayPauseButton.tsx b/src/components/views/audio_messages/PlayPauseButton.tsx
similarity index 67%
rename from src/components/views/voice_messages/PlayPauseButton.tsx
rename to src/components/views/audio_messages/PlayPauseButton.tsx
index 1f87eb012d..a4f1e770f2 100644
--- a/src/components/views/voice_messages/PlayPauseButton.tsx
+++ b/src/components/views/audio_messages/PlayPauseButton.tsx
@@ -14,14 +14,15 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import React, {ReactNode} from "react";
-import {replaceableComponent} from "../../../utils/replaceableComponent";
+import React, { ReactNode } from "react";
+import { replaceableComponent } from "../../../utils/replaceableComponent";
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
-import {_t} from "../../../languageHandler";
-import {Playback, PlaybackState} from "../../../voice/Playback";
+import { _t } from "../../../languageHandler";
+import { Playback, PlaybackState } from "../../../voice/Playback";
import classNames from "classnames";
-interface IProps {
+// omitted props are handled by render function
+interface IProps extends Omit, "title" | "onClick" | "disabled"> {
// Playback instance to manipulate. Cannot change during the component lifecycle.
playback: Playback;
@@ -33,19 +34,25 @@ interface IProps {
* Displays a play/pause button (activating the play/pause function of the recorder)
* to be displayed in reference to a recording.
*/
-@replaceableComponent("views.voice_messages.PlayPauseButton")
+@replaceableComponent("views.audio_messages.PlayPauseButton")
export default class PlayPauseButton extends React.PureComponent {
public constructor(props) {
super(props);
}
- private onClick = async () => {
- await this.props.playback.toggle();
+ private onClick = () => {
+ // noinspection JSIgnoredPromiseFromCall
+ this.toggleState();
};
+ public async toggleState() {
+ await this.props.playback.toggle();
+ }
+
public render(): ReactNode {
- const isPlaying = this.props.playback.isPlaying;
- const isDisabled = this.props.playbackPhase === PlaybackState.Decoding;
+ const { playback, playbackPhase, ...restProps } = this.props;
+ const isPlaying = playback.isPlaying;
+ const isDisabled = playbackPhase === PlaybackState.Decoding;
const classes = classNames('mx_PlayPauseButton', {
'mx_PlayPauseButton_play': !isPlaying,
'mx_PlayPauseButton_pause': isPlaying,
@@ -56,6 +63,7 @@ export default class PlayPauseButton extends React.PureComponent {
title={isPlaying ? _t("Pause") : _t("Play")}
onClick={this.onClick}
disabled={isDisabled}
+ {...restProps}
/>;
}
}
diff --git a/src/components/views/voice_messages/PlaybackClock.tsx b/src/components/views/audio_messages/PlaybackClock.tsx
similarity index 73%
rename from src/components/views/voice_messages/PlaybackClock.tsx
rename to src/components/views/audio_messages/PlaybackClock.tsx
index 2e8ec9a3e7..374d47c31d 100644
--- a/src/components/views/voice_messages/PlaybackClock.tsx
+++ b/src/components/views/audio_messages/PlaybackClock.tsx
@@ -15,13 +15,18 @@ limitations under the License.
*/
import React from "react";
-import {replaceableComponent} from "../../../utils/replaceableComponent";
+import { replaceableComponent } from "../../../utils/replaceableComponent";
import Clock from "./Clock";
-import {Playback, PlaybackState} from "../../../voice/Playback";
-import {UPDATE_EVENT} from "../../../stores/AsyncStore";
+import { Playback, PlaybackState } from "../../../voice/Playback";
+import { UPDATE_EVENT } from "../../../stores/AsyncStore";
interface IProps {
playback: Playback;
+
+ // The default number of seconds to show when the playback has completed or
+ // has not started. Not used during playback, even when paused. Defaults to
+ // clip duration length.
+ defaultDisplaySeconds?: number;
}
interface IState {
@@ -33,7 +38,7 @@ interface IState {
/**
* A clock for a playback of a recording.
*/
-@replaceableComponent("views.voice_messages.PlaybackClock")
+@replaceableComponent("views.audio_messages.PlaybackClock")
export default class PlaybackClock extends React.PureComponent {
public constructor(props) {
super(props);
@@ -54,17 +59,21 @@ export default class PlaybackClock extends React.PureComponent {
private onPlaybackUpdate = (ev: PlaybackState) => {
// Convert Decoding -> Stopped because we don't care about the distinction here
if (ev === PlaybackState.Decoding) ev = PlaybackState.Stopped;
- this.setState({playbackPhase: ev});
+ this.setState({ playbackPhase: ev });
};
private onTimeUpdate = (time: number[]) => {
- this.setState({seconds: time[0], durationSeconds: time[1]});
+ this.setState({ seconds: time[0], durationSeconds: time[1] });
};
public render() {
let seconds = this.state.seconds;
if (this.state.playbackPhase === PlaybackState.Stopped) {
- seconds = this.state.durationSeconds;
+ if (Number.isFinite(this.props.defaultDisplaySeconds)) {
+ seconds = this.props.defaultDisplaySeconds;
+ } else {
+ seconds = this.state.durationSeconds;
+ }
}
return ;
}
diff --git a/src/components/views/voice_messages/PlaybackWaveform.tsx b/src/components/views/audio_messages/PlaybackWaveform.tsx
similarity index 81%
rename from src/components/views/voice_messages/PlaybackWaveform.tsx
rename to src/components/views/audio_messages/PlaybackWaveform.tsx
index 2e9f163f5e..ea1b846c01 100644
--- a/src/components/views/voice_messages/PlaybackWaveform.tsx
+++ b/src/components/views/audio_messages/PlaybackWaveform.tsx
@@ -15,11 +15,11 @@ limitations under the License.
*/
import React from "react";
-import {replaceableComponent} from "../../../utils/replaceableComponent";
-import {arraySeed, arrayTrimFill} from "../../../utils/arrays";
+import { replaceableComponent } from "../../../utils/replaceableComponent";
+import { arraySeed, arrayTrimFill } from "../../../utils/arrays";
import Waveform from "./Waveform";
-import {Playback, PLAYBACK_WAVEFORM_SAMPLES} from "../../../voice/Playback";
-import {percentageOf} from "../../../utils/numbers";
+import { Playback, PLAYBACK_WAVEFORM_SAMPLES } from "../../../voice/Playback";
+import { percentageOf } from "../../../utils/numbers";
interface IProps {
playback: Playback;
@@ -33,7 +33,7 @@ interface IState {
/**
* A waveform which shows the waveform of a previously recorded recording
*/
-@replaceableComponent("views.voice_messages.PlaybackWaveform")
+@replaceableComponent("views.audio_messages.PlaybackWaveform")
export default class PlaybackWaveform extends React.PureComponent {
public constructor(props) {
super(props);
@@ -53,13 +53,13 @@ export default class PlaybackWaveform extends React.PureComponent {
- this.setState({heights: this.toHeights(waveform)});
+ this.setState({ heights: this.toHeights(waveform) });
};
private onTimeUpdate = (time: number[]) => {
// Track percentages to a general precision to avoid over-waking the component.
const progress = Number(percentageOf(time[0], 0, time[1]).toFixed(3));
- this.setState({progress});
+ this.setState({ progress });
};
public render() {
diff --git a/src/components/views/voice_messages/RecordingPlayback.tsx b/src/components/views/audio_messages/RecordingPlayback.tsx
similarity index 81%
rename from src/components/views/voice_messages/RecordingPlayback.tsx
rename to src/components/views/audio_messages/RecordingPlayback.tsx
index 776997cec2..a0dea1c6db 100644
--- a/src/components/views/voice_messages/RecordingPlayback.tsx
+++ b/src/components/views/audio_messages/RecordingPlayback.tsx
@@ -14,12 +14,13 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import {Playback, PlaybackState} from "../../../voice/Playback";
-import React, {ReactNode} from "react";
-import {UPDATE_EVENT} from "../../../stores/AsyncStore";
+import { Playback, PlaybackState } from "../../../voice/Playback";
+import React, { ReactNode } from "react";
+import { UPDATE_EVENT } from "../../../stores/AsyncStore";
import PlaybackWaveform from "./PlaybackWaveform";
import PlayPauseButton from "./PlayPauseButton";
import PlaybackClock from "./PlaybackClock";
+import { replaceableComponent } from "../../../utils/replaceableComponent";
interface IProps {
// Playback instance to render. Cannot change during component lifecycle: create
@@ -31,6 +32,7 @@ interface IState {
playbackPhase: PlaybackState;
}
+@replaceableComponent("views.audio_messages.RecordingPlayback")
export default class RecordingPlayback extends React.PureComponent {
constructor(props: IProps) {
super(props);
@@ -49,14 +51,14 @@ export default class RecordingPlayback extends React.PureComponent {
- this.setState({playbackPhase: ev});
+ this.setState({ playbackPhase: ev });
};
public render(): ReactNode {
- return
+ return
-
+
;
}
}
diff --git a/src/components/views/audio_messages/SeekBar.tsx b/src/components/views/audio_messages/SeekBar.tsx
new file mode 100644
index 0000000000..5231a2fb79
--- /dev/null
+++ b/src/components/views/audio_messages/SeekBar.tsx
@@ -0,0 +1,112 @@
+/*
+Copyright 2021 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import { Playback, PlaybackState } from "../../../voice/Playback";
+import React, { ChangeEvent, CSSProperties, ReactNode } from "react";
+import { replaceableComponent } from "../../../utils/replaceableComponent";
+import { MarkedExecution } from "../../../utils/MarkedExecution";
+import { percentageOf } from "../../../utils/numbers";
+
+interface IProps {
+ // Playback instance to render. Cannot change during component lifecycle: create
+ // an all-new component instead.
+ playback: Playback;
+
+ // Tab index for the underlying component. Useful if the seek bar is in a managed state.
+ // Defaults to zero.
+ tabIndex?: number;
+
+ playbackPhase: PlaybackState;
+}
+
+interface IState {
+ percentage: number;
+}
+
+interface ISeekCSS extends CSSProperties {
+ '--fillTo': number;
+}
+
+const ARROW_SKIP_SECONDS = 5; // arbitrary
+
+@replaceableComponent("views.audio_messages.SeekBar")
+export default class SeekBar extends React.PureComponent {
+ // We use an animation frame request to avoid overly spamming prop updates, even if we aren't
+ // really using anything demanding on the CSS front.
+
+ private animationFrameFn = new MarkedExecution(
+ () => this.doUpdate(),
+ () => requestAnimationFrame(() => this.animationFrameFn.trigger()));
+
+ public static defaultProps = {
+ tabIndex: 0,
+ };
+
+ constructor(props: IProps) {
+ super(props);
+
+ this.state = {
+ percentage: 0,
+ };
+
+ // We don't need to de-register: the class handles this for us internally
+ this.props.playback.clockInfo.liveData.onUpdate(() => this.animationFrameFn.mark());
+ }
+
+ private doUpdate() {
+ this.setState({
+ percentage: percentageOf(
+ this.props.playback.clockInfo.timeSeconds,
+ 0,
+ this.props.playback.clockInfo.durationSeconds),
+ });
+ }
+
+ public left() {
+ // noinspection JSIgnoredPromiseFromCall
+ this.props.playback.skipTo(this.props.playback.clockInfo.timeSeconds - ARROW_SKIP_SECONDS);
+ }
+
+ public right() {
+ // noinspection JSIgnoredPromiseFromCall
+ this.props.playback.skipTo(this.props.playback.clockInfo.timeSeconds + ARROW_SKIP_SECONDS);
+ }
+
+ private onChange = (ev: ChangeEvent) => {
+ // Thankfully, onChange is only called when the user changes the value, not when we
+ // change the value on the component. We can use this as a reliable "skip to X" function.
+ //
+ // noinspection JSIgnoredPromiseFromCall
+ this.props.playback.skipTo(Number(ev.target.value) * this.props.playback.clockInfo.durationSeconds);
+ };
+
+ public render(): ReactNode {
+ // We use a range input to avoid having to re-invent accessibility handling on
+ // a custom set of divs.
+ return ;
+ }
+}
diff --git a/src/components/views/voice_messages/Waveform.tsx b/src/components/views/audio_messages/Waveform.tsx
similarity index 81%
rename from src/components/views/voice_messages/Waveform.tsx
rename to src/components/views/audio_messages/Waveform.tsx
index 840a5a12b3..3b7a881754 100644
--- a/src/components/views/voice_messages/Waveform.tsx
+++ b/src/components/views/audio_messages/Waveform.tsx
@@ -15,8 +15,13 @@ limitations under the License.
*/
import React from "react";
-import {replaceableComponent} from "../../../utils/replaceableComponent";
+import { replaceableComponent } from "../../../utils/replaceableComponent";
import classNames from "classnames";
+import { CSSProperties } from "react";
+
+interface WaveformCSSProperties extends CSSProperties {
+ '--barHeight': number;
+}
interface IProps {
relHeights: number[]; // relative heights (0-1)
@@ -34,16 +39,12 @@ interface IState {
* For CSS purposes, a mx_Waveform_bar_100pct class is added when the bar should be
* "filled", as a demonstration of the progress property.
*/
-@replaceableComponent("views.voice_messages.Waveform")
+@replaceableComponent("views.audio_messages.Waveform")
export default class Waveform extends React.PureComponent {
public static defaultProps = {
progress: 1,
};
- public constructor(props) {
- super(props);
- }
-
public render() {
return