{
- 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.tsx b/src/components/structures/AutoHideScrollbar.tsx
index 66f998b616..e8a9872b48 100644
--- a/src/components/structures/AutoHideScrollbar.tsx
+++ b/src/components/structures/AutoHideScrollbar.tsx
@@ -15,12 +15,12 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import React from "react";
+import React, { HTMLAttributes, WheelEvent } from "react";
-interface IProps {
+interface IProps extends Omit, "onScroll"> {
className?: string;
- onScroll?: () => void;
- onWheel?: () => void;
+ onScroll?: (event: Event) => void;
+ onWheel?: (event: WheelEvent) => void;
style?: React.CSSProperties
tabIndex?: number,
wrappedRef?: (ref: HTMLDivElement) => void;
@@ -52,14 +52,18 @@ export default class AutoHideScrollbar extends React.Component {
}
public render() {
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ const { className, onScroll, onWheel, style, tabIndex, wrappedRef, children, ...otherProps } = this.props;
+
return (
- { this.props.children }
+ { children }
);
}
}
diff --git a/src/components/structures/ContextMenu.tsx b/src/components/structures/ContextMenu.tsx
index 9d8665c176..407dc6f04c 100644
--- a/src/components/structures/ContextMenu.tsx
+++ b/src/components/structures/ContextMenu.tsx
@@ -16,13 +16,13 @@ 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
@@ -371,7 +371,7 @@ export class ContextMenu extends React.PureComponent {
return (
@@ -399,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,
@@ -498,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 3a2c611cc9..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 {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
@@ -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 }),
},
);
});
@@ -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 }) }
,
];
}
@@ -775,11 +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 => (
+ { a: sub => (
{ sub }
- )},
+ ) },
);
const title = this.state.selectedCommunityId
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 7916951280..82a44de7a5 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,8 +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';
import MainSplit from './MainSplit';
@@ -59,7 +58,7 @@ import ScrollPanel from "./ScrollPanel";
import TimelinePanel from "./TimelinePanel";
import ErrorBoundary from "../views/elements/ErrorBoundary";
import RoomPreviewBar from "../views/rooms/RoomPreviewBar";
-import SearchBar from "../views/rooms/SearchBar";
+import SearchBar, { SearchScope } from "../views/rooms/SearchBar";
import RoomUpgradeWarningBar from "../views/rooms/RoomUpgradeWarningBar";
import AuxPanel from "../views/rooms/AuxPanel";
import RoomHeader from "../views/rooms/RoomHeader";
@@ -80,8 +79,9 @@ import { objectHasDiff } from "../../utils/objects";
import SpaceRoomView from "./SpaceRoomView";
import { IOpts } from "../../createRoom";
import { replaceableComponent } from "../../utils/replaceableComponent";
-import { omit } from 'lodash';
import UIStore from "../../stores/UIStore";
+import EditorStateTransfer from "../../utils/EditorStateTransfer";
+import { throttle } from "lodash";
const DEBUG = false;
let debuglog = function(msg: string) {};
@@ -139,11 +139,11 @@ export interface IState {
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[];
@@ -172,11 +172,7 @@ export interface IState {
// 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;
@@ -196,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")
@@ -289,7 +286,7 @@ export default class RoomView extends React.Component {
if (this.state.room) {
this.checkWidgets(this.state.room);
}
- }
+ };
private checkWidgets = (room) => {
this.setState({
@@ -532,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 });
}
}
}
@@ -572,16 +569,12 @@ export default class RoomView extends React.Component {
shouldComponentUpdate(nextProps, nextState) {
const hasPropsDiff = objectHasDiff(this.props, nextProps);
- // React only shallow comparison and we only want to trigger
- // a component re-render if a room requires an upgrade
- const newUpgradeRecommendation = nextState.upgradeRecommendation || {}
-
- const state = omit(this.state, ['upgradeRecommendation']);
- const newState = omit(nextState, ['upgradeRecommendation'])
+ const { upgradeRecommendation, ...state } = this.state;
+ const { upgradeRecommendation: newUpgradeRecommendation, ...newState } = nextState;
const hasStateDiff =
- objectHasDiff(state, newState) ||
- (newUpgradeRecommendation.needsUpgrade === true)
+ newUpgradeRecommendation?.needsUpgrade !== upgradeRecommendation?.needsUpgrade ||
+ objectHasDiff(state, newState);
return hasPropsDiff || hasStateDiff;
}
@@ -682,12 +675,8 @@ export default class RoomView extends React.Component {
);
}
- // 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
+ // cancel any pending calls to the throttled updated
+ this.updateRoomMembers.cancel();
for (const watcher of this.settingWatchers) {
SettingsStore.unwatchSetting(watcher);
@@ -701,14 +690,9 @@ export default class RoomView extends React.Component {
room_id: this.state.room.roomId,
event_id: this.state.initialEventId,
highlighted: false,
+ replyingToEvent: this.state.replyToEvent,
});
}
- }
-
- private onLayoutChange = () => {
- this.setState({
- layout: SettingsStore.getValue("layout"),
- });
};
private onRightPanelStoreUpdate = () => {
@@ -828,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;
}
};
@@ -863,7 +877,7 @@ export default class RoomView extends React.Component {
// no change
} else if (!shouldHideEvent(ev, this.state)) {
this.setState((state, props) => {
- return {numUnreadMessages: state.numUnreadMessages + 1};
+ return { numUnreadMessages: state.numUnreadMessages + 1 };
});
}
}
@@ -888,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}` });
}
});
};
@@ -941,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.` +
@@ -969,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({
@@ -1040,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) {
@@ -1060,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);
}
@@ -1092,7 +1092,7 @@ export default class RoomView extends React.Component {
return;
}
- this.updateRoomMembers(member);
+ this.updateRoomMembers();
};
private onMyMembership = (room: Room, membership: string, oldMembership: string) => {
@@ -1109,15 +1109,15 @@ 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 });
}
}
// rate limited because a power level change will emit an event for every member in the room.
- private updateRoomMembers = rateLimitedFunc(() => {
+ private updateRoomMembers = throttle(() => {
this.updateDMState();
this.updateE2EStatus(this.state.room);
- }, 500);
+ }, 500, { leading: true, trailing: true });
private checkDesktopNotifications() {
const memberCount = this.state.room.getJoinedMemberCount() + this.state.room.getInvitedMemberCount();
@@ -1138,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);
}
@@ -1173,7 +1173,7 @@ 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;
@@ -1208,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 });
}
};
@@ -1263,7 +1263,7 @@ export default class RoomView extends React.Component {
private injectSticker(url: string, info: object, text: string) {
if (this.context.isGuest()) {
- dis.dispatch({action: 'require_registration'});
+ dis.dispatch({ action: 'require_registration' });
return;
}
@@ -1276,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,
@@ -1297,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;
@@ -1317,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
@@ -1349,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,
@@ -1590,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 });
}
};
@@ -1644,29 +1645,27 @@ export default class RoomView extends React.Component {
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 });
};
/**
@@ -1701,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() {
@@ -1723,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() {
@@ -1931,7 +1926,7 @@ export default class RoomView extends React.Component {
>
{_t(
"You have %(count)s unread notifications in a prior version of this room.",
- {count: hiddenHighlightCount},
+ { count: hiddenHighlightCount },
)}
);
@@ -2054,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;
@@ -2069,7 +2065,7 @@ 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}
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 f6e1530537..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,97 +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,
-
- /* onUserScroll: callback which is called when the user interacts with the room timeline
- */
- onUserScroll: PropTypes.func,
-
- /* className: classnames to add to the top-level div
- */
- className: PropTypes.string,
-
- /* style: styles to add to the top-level div
- */
- style: PropTypes.object,
-
- /* resizeNotifier: ResizeNotifier to know when middle column has changed size
- */
- resizeNotifier: PropTypes.object,
-
- /* fixedChildren: allows for children to be passed which are rendered outside
- * of the wrapper
- */
- fixedChildren: PropTypes.node,
- };
-
+export default class ScrollPanel extends React.Component {
static defaultProps = {
stickyBottom: true,
startAtBottom: true,
- onFillRequest: function(backwards) { return Promise.resolve(false); },
- onUnfillRequest: function(backwards, scrollToken) {},
+ onFillRequest: function(backwards: boolean) { return Promise.resolve(false); },
+ onUnfillRequest: function(backwards: boolean, scrollToken: string) {},
onScroll: function() {},
};
- constructor(props) {
- super(props);
+ private readonly pendingFillRequests: Record<"b" | "f", boolean> = {
+ b: null,
+ f: null,
+ };
+ private readonly itemlist = createRef();
+ private unmounted = false;
+ private scrollTimeout: Timer;
+ private isFilling: boolean;
+ private fillRequestWhileRunning: boolean;
+ private scrollState: IScrollState;
+ private preventShrinkingState: IPreventShrinkingState;
+ private unfillDebouncer: NodeJS.Timeout;
+ private bottomGrowth: number;
+ private pages: number;
+ private heightUpdateInProgress: boolean;
+ private divScroll: HTMLDivElement;
- this._pendingFillRequests = {b: null, f: null};
+ constructor(props, context) {
+ super(props, context);
if (this.props.resizeNotifier) {
this.props.resizeNotifier.on("middlePanelResizedNoisy", this.onResize);
}
this.resetScrollState();
-
- this._itemlist = createRef();
}
componentDidMount() {
@@ -203,18 +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
@@ -225,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();
};
@@ -238,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.
@@ -278,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;
@@ -293,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.
@@ -330,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 = [];
@@ -348,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) {
@@ -365,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;
@@ -413,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);
@@ -425,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;
}
@@ -436,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
@@ -445,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) {
@@ -477,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.
*
@@ -491,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();
};
/**
@@ -527,18 +553,18 @@ export default class ScrollPanel extends React.Component {
*
* @param {number} mult: -1 to page up, +1 to page down
*/
- scrollRelative = mult => {
- const scrollNode = this._getScrollNode();
+ public scrollRelative = (mult: number): void => {
+ const scrollNode = this.getScrollNode();
const delta = mult * scrollNode.clientHeight * 0.9;
scrollNode.scrollBy(0, delta);
- this._saveScrollState();
+ this.saveScrollState();
};
/**
* Scroll up/down in response to a scroll key
* @param {object} ev the keyboard event
*/
- handleScrollKey = ev => {
+ public handleScrollKey = (ev: KeyboardEvent) => {
let isScrolling = false;
const roomAction = getKeyBindingsManager().getRoomAction(ev);
switch (roomAction) {
@@ -575,17 +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
@@ -593,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;
}
@@ -634,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,
@@ -644,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");
@@ -680,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.");
}
@@ -694,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) {
@@ -713,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
@@ -730,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 &&
@@ -768,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;
};
/**
@@ -814,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;
@@ -841,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;
@@ -857,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
@@ -898,13 +924,15 @@ export default class ScrollPanel extends React.Component {
// list-style-type: none; is no longer a list
return (
+ className={`mx_ScrollPanel ${this.props.className}`}
+ style={this.props.style}
+ >
{ this.props.fixedChildren }
-
+
{ this.props.children }
diff --git a/src/components/structures/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 (
-
+
= ({
ev.preventDefault();
ev.stopPropagation();
onViewRoomClick(false);
- }
+ };
const onJoinClick = (ev: ButtonEvent) => {
ev.preventDefault();
ev.stopPropagation();
onViewRoomClick(true);
- }
+ };
let button;
if (joinedRoom) {
@@ -137,7 +137,7 @@ const Tile: React.FC = ({
} else {
checkbox = { ev.stopPropagation() }}
+ onClick={ev => { ev.stopPropagation(); }}
>
;
@@ -286,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;
@@ -340,7 +340,7 @@ export const HierarchyLevel = ({
))
}
-
+ ;
};
// mutate argument refreshToken to force a reload
@@ -635,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 74%
rename from src/components/structures/TimelinePanel.js
rename to src/components/structures/TimelinePanel.tsx
index bb62745d98..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,18 @@ 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";
@@ -34,11 +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 {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,90 +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 user interacts with the room timeline
- onUserScroll: PropTypes.func,
-
- // callback which is called when the read-up-to mark is updated.
- onReadMarkerUpdated: PropTypes.func,
-
- // callback which is called when we wish to paginate the timeline
- // window.
- onPaginationRequest: PropTypes.func,
-
- // maximum number of events to show in a timeline
- timelineCap: PropTypes.number,
-
- // classname to use for the messagepanel
- className: PropTypes.string,
-
- // shape property to be passed to EventTiles
- tileShape: PropTypes.string,
-
- // placeholder to use if the timeline is empty
- empty: PropTypes.node,
-
- // whether to show reactions for an event
- showReactions: PropTypes.bool,
-
- // which layout to use
- layout: LayoutPropType,
-
- // whether to always show timestamps for an event
- alwaysShowTimestamps: PropTypes.bool,
- }
-
+class TimelinePanel extends React.Component {
static contextType = RoomContext;
// a map from room id to read marker event timestamp
- static roomReadMarkerTsMap = {};
+ static roomReadMarkerTsMap: Record = {};
static defaultProps = {
// By default, disable the timelineCap in favour of unpaginating based on
@@ -139,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;
@@ -157,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
@@ -245,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
@@ -271,7 +312,7 @@ class TimelinePanel extends React.Component {
if (differentEventId || differentHighlightedEventId) {
console.log("TimelinePanel switching to eventId " + newProps.eventId +
" (was " + this.props.eventId + ")");
- return this._initTimeline(newProps);
+ return this.initTimeline(newProps);
}
}
@@ -281,13 +322,13 @@ class TimelinePanel extends React.Component {
//
// (We could use isMounted, but facebook have deprecated that.)
this.unmounted = true;
- if (this._readReceiptActivityTimer) {
- this._readReceiptActivityTimer.abort();
- this._readReceiptActivityTimer = null;
+ if (this.readReceiptActivityTimer) {
+ this.readReceiptActivityTimer.abort();
+ this.readReceiptActivityTimer = null;
}
- if (this._readMarkerActivityTimer) {
- this._readMarkerActivityTimer.abort();
- this._readMarkerActivityTimer = null;
+ if (this.readMarkerActivityTimer) {
+ this.readMarkerActivityTimer.abort();
+ this.readMarkerActivityTimer = null;
}
dis.unregister(this.dispatcherRef);
@@ -307,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);
@@ -326,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 {
@@ -349,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';
@@ -361,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);
}
@@ -373,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,
@@ -394,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;
}
@@ -405,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));
@@ -416,7 +466,7 @@ class TimelinePanel extends React.Component {
});
};
- onMessageListScroll = e => {
+ private onMessageListScroll = e => {
if (this.props.onScroll) {
this.props.onScroll(e);
}
@@ -427,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;
@@ -465,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;
}
@@ -484,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,
@@ -515,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();
}
@@ -531,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
@@ -552,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
@@ -563,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
@@ -572,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
@@ -597,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;
@@ -612,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
@@ -662,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.
//
@@ -678,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) {
@@ -756,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,
@@ -766,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,
});
@@ -774,7 +822,7 @@ class TimelinePanel extends React.Component {
return;
}
const lastDisplayedEvent = this.state.events[lastDisplayedIndex];
- this._setReadMarker(
+ this.setReadMarker(
lastDisplayedEvent.getId(),
lastDisplayedEvent.getTs(),
);
@@ -791,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;
@@ -824,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;
}
@@ -870,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);
@@ -890,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:
@@ -920,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;
}
@@ -943,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
@@ -958,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;
@@ -981,7 +1028,7 @@ class TimelinePanel extends React.Component {
offsetBase = 0.5;
}
- return this._loadTimeline(initialEvent, pixelOffset, offsetBase);
+ return this.loadTimeline(initialEvent, pixelOffset, offsetBase);
}
/**
@@ -997,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
@@ -1034,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) {
@@ -1099,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: [],
@@ -1117,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
@@ -1139,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());
}
@@ -1167,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 ||
@@ -1231,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;
@@ -1240,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;
@@ -1325,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) {
@@ -1336,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
@@ -1361,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.
@@ -1372,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
@@ -1392,7 +1433,7 @@ class TimelinePanel extends React.Component {
if (this.state.timelineLoading) {
return (
-
+
);
}
@@ -1413,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.
@@ -1426,7 +1467,7 @@ class TimelinePanel extends React.Component {
: this.state.events;
return (
[];
@@ -58,7 +58,7 @@ export default class ToastContainer extends React.Component<{}, IState> {
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,
diff --git a/src/components/structures/UploadBar.tsx b/src/components/structures/UploadBar.tsx
index 269c615698..c8e90a1c0a 100644
--- a/src/components/structures/UploadBar.tsx
+++ b/src/components/structures/UploadBar.tsx
@@ -25,7 +25,7 @@ import { Action } from "../../dispatcher/actions";
import ProgressBar from "../views/elements/ProgressBar";
import AccessibleButton from "../views/elements/AccessibleButton";
import { IUpload } from "../../models/IUpload";
-import {replaceableComponent} from "../../utils/replaceableComponent";
+import { replaceableComponent } from "../../utils/replaceableComponent";
import MatrixClientContext from "../../contexts/MatrixClientContext";
interface IProps {
@@ -50,7 +50,7 @@ export default class UploadBar extends React.Component {
// Set initial state to any available upload in this room - we might be mounting
// earlier than the first progress event, so should show something relevant.
const uploadsHere = this.getUploadsInRoom();
- this.state = {currentUpload: uploadsHere[0], uploadsHere};
+ this.state = { currentUpload: uploadsHere[0], uploadsHere };
}
componentDidMount() {
@@ -77,7 +77,7 @@ export default class UploadBar extends React.Component {
case Action.UploadFailed: {
if (!this.mounted) return;
const uploadsHere = this.getUploadsInRoom();
- this.setState({currentUpload: uploadsHere[0], uploadsHere});
+ this.setState({ currentUpload: uploadsHere[0], uploadsHere });
break;
}
}
diff --git a/src/components/structures/UserMenu.tsx b/src/components/structures/UserMenu.tsx
index 6a449cf1a2..d85817486b 100644
--- a/src/components/structures/UserMenu.tsx
+++ b/src/components/structures/UserMenu.tsx
@@ -26,14 +26,14 @@ import { ActionPayload } from "../../dispatcher/payloads";
import { Action } from "../../dispatcher/actions";
import { _t } from "../../languageHandler";
import { ContextMenuButton } from "./ContextMenu";
-import { USER_NOTIFICATIONS_TAB, USER_SECURITY_TAB } from "../views/dialogs/UserSettingsDialog";
+import { UserTab } from "../views/dialogs/UserSettingsDialog";
import { OpenToTabPayload } from "../../dispatcher/payloads/OpenToTabPayload";
import FeedbackDialog from "../views/dialogs/FeedbackDialog";
import Modal from "../../Modal";
import LogoutDialog from "../views/dialogs/LogoutDialog";
import SettingsStore from "../../settings/SettingsStore";
-import {getCustomTheme} from "../../theme";
-import AccessibleButton, {ButtonEvent} from "../views/elements/AccessibleButton";
+import { getCustomTheme } from "../../theme";
+import AccessibleButton, { ButtonEvent } from "../views/elements/AccessibleButton";
import SdkConfig from "../../SdkConfig";
import { getHomePageUrl } from "../../utils/pages";
import { OwnProfileStore } from "../../stores/OwnProfileStore";
@@ -56,7 +56,7 @@ import HostSignupAction from "./HostSignupAction";
import { IHostSignupConfig } from "../views/dialogs/HostSignupDialogTypes";
import SpaceStore, { UPDATE_SELECTED_SPACE } from "../../stores/SpaceStore";
import RoomName from "../views/elements/RoomName";
-import {replaceableComponent} from "../../utils/replaceableComponent";
+import { replaceableComponent } from "../../utils/replaceableComponent";
import InlineSpinner from "../views/elements/InlineSpinner";
import TooltipButton from "../views/elements/TooltipButton";
interface IProps {
@@ -123,7 +123,7 @@ export default class UserMenu extends React.Component {
private onRoom = (room: Room): void => {
this.removePendingJoinRoom(room.roomId);
- }
+ };
private onTagStoreUpdate = () => {
this.forceUpdate(); // we don't have anything useful in state to update
@@ -152,14 +152,14 @@ export default class UserMenu extends React.Component {
};
private onThemeChanged = () => {
- this.setState({isDarkTheme: this.isUserOnDarkTheme()});
+ this.setState({ isDarkTheme: this.isUserOnDarkTheme() });
};
private onAction = (ev: ActionPayload) => {
switch (ev.action) {
case Action.ToggleUserMenu:
if (this.state.contextMenuPosition) {
- this.setState({contextMenuPosition: null});
+ this.setState({ contextMenuPosition: null });
} else {
if (this.buttonRef.current) this.buttonRef.current.click();
}
@@ -185,7 +185,7 @@ export default class UserMenu extends React.Component {
if (this.state.pendingRoomJoin.delete(roomId)) {
this.setState({
pendingRoomJoin: new Set(this.state.pendingRoomJoin),
- })
+ });
}
}
@@ -193,7 +193,7 @@ export default class UserMenu extends React.Component {
ev.preventDefault();
ev.stopPropagation();
const target = ev.target as HTMLButtonElement;
- this.setState({contextMenuPosition: target.getBoundingClientRect()});
+ this.setState({ contextMenuPosition: target.getBoundingClientRect() });
};
private onContextMenu = (ev: React.MouseEvent) => {
@@ -210,7 +210,7 @@ export default class UserMenu extends React.Component {
};
private onCloseMenu = () => {
- this.setState({contextMenuPosition: null});
+ this.setState({ contextMenuPosition: null });
};
private onSwitchThemeClick = (ev: React.MouseEvent) => {
@@ -228,9 +228,9 @@ export default class UserMenu extends React.Component {
ev.preventDefault();
ev.stopPropagation();
- const payload: OpenToTabPayload = {action: Action.ViewUserSettings, initialTabId: tabId};
+ const payload: OpenToTabPayload = { action: Action.ViewUserSettings, initialTabId: tabId };
defaultDispatcher.dispatch(payload);
- this.setState({contextMenuPosition: null}); // also close the menu
+ this.setState({ contextMenuPosition: null }); // also close the menu
};
private onShowArchived = (ev: ButtonEvent) => {
@@ -247,7 +247,7 @@ export default class UserMenu extends React.Component {
ev.stopPropagation();
Modal.createTrackedDialog('Feedback Dialog', '', FeedbackDialog);
- this.setState({contextMenuPosition: null}); // also close the menu
+ this.setState({ contextMenuPosition: null }); // also close the menu
};
private onSignOutClick = async (ev: ButtonEvent) => {
@@ -257,30 +257,30 @@ export default class UserMenu extends React.Component {
const cli = MatrixClientPeg.get();
if (!cli || !cli.isCryptoEnabled() || !(await cli.exportRoomKeys())?.length) {
// log out without user prompt if they have no local megolm sessions
- dis.dispatch({action: 'logout'});
+ dis.dispatch({ action: 'logout' });
} else {
Modal.createTrackedDialog('Logout from LeftPanel', '', LogoutDialog);
}
- this.setState({contextMenuPosition: null}); // also close the menu
+ this.setState({ contextMenuPosition: null }); // also close the menu
};
private onSignInClick = () => {
dis.dispatch({ action: 'start_login' });
- this.setState({contextMenuPosition: null}); // also close the menu
+ this.setState({ contextMenuPosition: null }); // also close the menu
};
private onRegisterClick = () => {
dis.dispatch({ action: 'start_registration' });
- this.setState({contextMenuPosition: null}); // also close the menu
+ this.setState({ contextMenuPosition: null }); // also close the menu
};
private onHomeClick = (ev: ButtonEvent) => {
ev.preventDefault();
ev.stopPropagation();
- defaultDispatcher.dispatch({action: 'view_home_page'});
- this.setState({contextMenuPosition: null}); // also close the menu
+ defaultDispatcher.dispatch({ action: 'view_home_page' });
+ this.setState({ contextMenuPosition: null }); // also close the menu
};
private onCommunitySettingsClick = (ev: ButtonEvent) => {
@@ -290,7 +290,7 @@ export default class UserMenu extends React.Component {
Modal.createTrackedDialog('Edit Community', '', EditCommunityPrototypeDialog, {
communityId: CommunityPrototypeStore.instance.getSelectedCommunityId(),
});
- this.setState({contextMenuPosition: null}); // also close the menu
+ this.setState({ contextMenuPosition: null }); // also close the menu
};
private onCommunityMembersClick = (ev: ButtonEvent) => {
@@ -307,7 +307,7 @@ export default class UserMenu extends React.Component {
action: 'view_room',
room_id: chat.roomId,
}, true);
- dis.dispatch({action: Action.SetRightPanelPhase, phase: RightPanelPhases.RoomMemberList});
+ dis.dispatch({ action: Action.SetRightPanelPhase, phase: RightPanelPhases.RoomMemberList });
} else {
// "This should never happen" clauses go here for the prototype.
Modal.createTrackedDialog('Failed to find general chat', '', ErrorDialog, {
@@ -315,7 +315,7 @@ export default class UserMenu extends React.Component {
description: _t("Failed to find the general chat for this community"),
});
}
- this.setState({contextMenuPosition: null}); // also close the menu
+ this.setState({ contextMenuPosition: null }); // also close the menu
};
private onCommunityInviteClick = (ev: ButtonEvent) => {
@@ -323,7 +323,7 @@ export default class UserMenu extends React.Component {
ev.stopPropagation();
showCommunityInviteDialog(CommunityPrototypeStore.instance.getSelectedCommunityId());
- this.setState({contextMenuPosition: null}); // also close the menu
+ this.setState({ contextMenuPosition: null }); // also close the menu
};
private onDndToggle = (ev) => {
@@ -357,7 +357,7 @@ export default class UserMenu extends React.Component {
),
})}
- )
+ );
} else if (hostSignupConfig) {
if (hostSignupConfig && hostSignupConfig.url) {
// If hostSignup.domains is set to a non-empty array, only show
@@ -408,12 +408,12 @@ export default class UserMenu extends React.Component {
this.onSettingsOpen(e, USER_NOTIFICATIONS_TAB)}
+ onClick={(e) => this.onSettingsOpen(e, UserTab.Notifications)}
/>
this.onSettingsOpen(e, USER_SECURITY_TAB)}
+ onClick={(e) => this.onSettingsOpen(e, UserTab.Security)}
/>
{
/>
- )
+ );
} else if (MatrixClientPeg.get().isGuest()) {
primaryOptionList = (
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 d34582b0c3..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.
@@ -166,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 {
@@ -174,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({
@@ -201,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) {
@@ -252,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 },
)}
@@ -363,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
@@ -501,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 6feb1e34f7..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;
@@ -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,
@@ -230,7 +230,7 @@ export default class Registration extends React.Component {
// 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
@@ -267,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?
@@ -432,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();
@@ -487,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