diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx
index 0af2d3d635..7986da203d 100644
--- a/src/components/structures/MatrixChat.tsx
+++ b/src/components/structures/MatrixChat.tsx
@@ -48,7 +48,7 @@ import createRoom, {IOpts} from "../../createRoom";
import {_t, _td, getCurrentLanguage} from '../../languageHandler';
import SettingsStore from "../../settings/SettingsStore";
import ThemeController from "../../settings/controllers/ThemeController";
-import { startAnyRegistrationFlow } from "../../Registration.js";
+import { startAnyRegistrationFlow } from "../../Registration";
import { messageForSyncError } from '../../utils/ErrorUtils';
import ResizeNotifier from "../../utils/ResizeNotifier";
import AutoDiscoveryUtils, { ValidatedServerConfig } from "../../utils/AutoDiscoveryUtils";
@@ -1461,7 +1461,7 @@ export default class MatrixChat extends React.PureComponent {
});
const dft = new DecryptionFailureTracker((total, errorCode) => {
- Analytics.trackEvent('E2E', 'Decryption failure', errorCode, total);
+ Analytics.trackEvent('E2E', 'Decryption failure', errorCode, String(total));
CountlyAnalytics.instance.track("decryption_failure", { errorCode }, null, { sum: total });
}, (errorCode) => {
// Map JS-SDK error codes to tracker codes for aggregation
diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.tsx
similarity index 64%
rename from src/components/structures/MessagePanel.js
rename to src/components/structures/MessagePanel.tsx
index 16563bd4e9..c7d9944435 100644
--- a/src/components/structures/MessagePanel.js
+++ b/src/components/structures/MessagePanel.tsx
@@ -1,7 +1,5 @@
/*
-Copyright 2016 OpenMarket Ltd
-Copyright 2018 New Vector Ltd
-Copyright 2019 The Matrix.org Foundation C.I.C.
+Copyright 2016 - 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -16,32 +14,50 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import React, {createRef} from 'react';
+import React, { createRef, KeyboardEvent, ReactNode, SyntheticEvent, TransitionEvent } from 'react';
import ReactDOM from 'react-dom';
-import PropTypes from 'prop-types';
-import shouldHideEvent from '../../shouldHideEvent';
-import {wantsDateSeparator} from '../../DateUtils';
-import * as sdk from '../../index';
+import { Room } from 'matrix-js-sdk/src/models/room';
+import { EventType } from 'matrix-js-sdk/src/@types/event';
+import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
+import { Relations } from "matrix-js-sdk/src/models/relations";
+import { RoomMember } from 'matrix-js-sdk/src/models/room-member';
-import {MatrixClientPeg} from '../../MatrixClientPeg';
+import shouldHideEvent from '../../shouldHideEvent';
+import { wantsDateSeparator } from '../../DateUtils';
+import { MatrixClientPeg } from '../../MatrixClientPeg';
import SettingsStore from '../../settings/SettingsStore';
import RoomContext from "../../contexts/RoomContext";
-import {Layout, LayoutPropType} from "../../settings/Layout";
-import {_t} from "../../languageHandler";
-import {haveTileForEvent} from "../views/rooms/EventTile";
-import {hasText} from "../../TextForEvent";
+import { Layout } from "../../settings/Layout";
+import { _t } from "../../languageHandler";
+import EventTile, { haveTileForEvent, IReadReceiptProps, TileShape } from "../views/rooms/EventTile";
+import { hasText } from "../../TextForEvent";
import IRCTimelineProfileResizer from "../views/elements/IRCTimelineProfileResizer";
import DMRoomMap from "../../utils/DMRoomMap";
import NewRoomIntro from "../views/rooms/NewRoomIntro";
-import {replaceableComponent} from "../../utils/replaceableComponent";
+import { replaceableComponent } from "../../utils/replaceableComponent";
import defaultDispatcher from '../../dispatcher/dispatcher';
+import WhoIsTypingTile from '../views/rooms/WhoIsTypingTile';
+import ScrollPanel, { IScrollState } from "./ScrollPanel";
+import EventListSummary from '../views/elements/EventListSummary';
+import MemberEventListSummary from '../views/elements/MemberEventListSummary';
+import DateSeparator from '../views/messages/DateSeparator';
+import ErrorBoundary from '../views/elements/ErrorBoundary';
+import ResizeNotifier from "../../utils/ResizeNotifier";
+import Spinner from "../views/elements/Spinner";
+import TileErrorBoundary from '../views/messages/TileErrorBoundary';
+import { RoomPermalinkCreator } from "../../utils/permalinks/Permalinks";
+import EditorStateTransfer from "../../utils/EditorStateTransfer";
const CONTINUATION_MAX_INTERVAL = 5 * 60 * 1000; // 5 minutes
-const continuedTypes = ['m.sticker', 'm.room.message'];
+const continuedTypes = [EventType.Sticker, EventType.RoomMessage];
// check if there is a previous event and it has the same sender as this event
// and the types are the same/is in continuedTypes and the time between them is <= CONTINUATION_MAX_INTERVAL
-function shouldFormContinuation(prevEvent, mxEvent, showHiddenEvents) {
+function shouldFormContinuation(
+ prevEvent: MatrixEvent,
+ mxEvent: MatrixEvent,
+ showHiddenEvents: boolean
+): boolean {
// sanity check inputs
if (!prevEvent || !prevEvent.sender || !mxEvent.sender) return false;
// check if within the max continuation period
@@ -52,8 +68,8 @@ function shouldFormContinuation(prevEvent, mxEvent, showHiddenEvents) {
// Some events should appear as continuations from previous events of different types.
if (mxEvent.getType() !== prevEvent.getType() &&
- (!continuedTypes.includes(mxEvent.getType()) ||
- !continuedTypes.includes(prevEvent.getType()))) return false;
+ (!continuedTypes.includes(mxEvent.getType() as EventType) ||
+ !continuedTypes.includes(prevEvent.getType() as EventType))) return false;
// Check if the sender is the same and hasn't changed their displayname/avatar between these events
if (mxEvent.sender.userId !== prevEvent.sender.userId ||
@@ -66,96 +82,161 @@ function shouldFormContinuation(prevEvent, mxEvent, showHiddenEvents) {
return true;
}
-const isMembershipChange = (e) => e.getType() === 'm.room.member' || e.getType() === 'm.room.third_party_invite';
+const isMembershipChange = (e: MatrixEvent): boolean => {
+ return e.getType() === EventType.RoomMember || e.getType() === EventType.RoomThirdPartyInvite;
+}
+
+interface IProps {
+ // the list of MatrixEvents to display
+ events: MatrixEvent[];
+
+ // true to give the component a 'display: none' style.
+ hidden?: boolean;
+
+ // true to show a spinner at the top of the timeline to indicate
+ // back-pagination in progress
+ backPaginating?: boolean;
+
+ // true to show a spinner at the end of the timeline to indicate
+ // forward-pagination in progress
+ forwardPaginating?: boolean;
+
+ // ID of an event to highlight. If undefined, no event will be highlighted.
+ highlightedEventId?: string;
+
+ // The room these events are all in together, if any.
+ // (The notification panel won't have a room here, for example.)
+ room?: Room;
+
+ // Should we show URL Previews
+ showUrlPreview?: boolean;
+
+ // event after which we should show a read marker
+ readMarkerEventId?: string;
+
+ // whether the read marker should be visible
+ readMarkerVisible?: boolean;
+
+ // the userid of our user. This is used to suppress the read marker
+ // for pending messages.
+ ourUserId?: string;
+
+ // true to suppress the date at the start of the timeline
+ suppressFirstDateSeparator?: boolean;
+
+ // whether to show read receipts
+ showReadReceipts?: boolean;
+
+ // true if updates to the event list should cause the scroll panel to
+ // scroll down when we are at the bottom of the window. See ScrollPanel
+ // for more details.
+ stickyBottom?: boolean;
+
+ // className for the panel
+ className: string;
+
+ // shape parameter to be passed to EventTiles
+ tileShape?: TileShape;
+
+ // show twelve hour timestamps
+ isTwelveHour?: boolean;
+
+ // show timestamps always
+ alwaysShowTimestamps?: boolean;
+
+ // whether to show reactions for an event
+ showReactions?: boolean;
+
+ // which layout to use
+ layout?: Layout;
+
+ // whether or not to show flair at all
+ enableFlair?: boolean;
+
+ resizeNotifier: ResizeNotifier;
+ permalinkCreator?: RoomPermalinkCreator;
+ editState?: EditorStateTransfer;
+
+ // callback which is called when the panel is scrolled.
+ onScroll?(event: Event): void;
+
+ // callback which is called when the user interacts with the room timeline
+ onUserScroll(event: SyntheticEvent): void;
+
+ // callback which is called when more content is needed.
+ onFillRequest?(backwards: boolean): Promise;
+
+ // helper function to access relations for an event
+ onUnfillRequest?(backwards: boolean, scrollToken: string): void;
+
+ getRelationsForEvent?(eventId: string, relationType: string, eventType: string): Relations;
+}
+
+interface IState {
+ ghostReadMarkers: string[];
+ showTypingNotifications: boolean;
+}
+
+interface IReadReceiptForUser {
+ lastShownEventId: string;
+ receipt: IReadReceiptProps;
+}
/* (almost) stateless UI component which builds the event tiles in the room timeline.
*/
@replaceableComponent("structures.MessagePanel")
-export default class MessagePanel extends React.Component {
- static propTypes = {
- // true to give the component a 'display: none' style.
- hidden: PropTypes.bool,
-
- // true to show a spinner at the top of the timeline to indicate
- // back-pagination in progress
- backPaginating: PropTypes.bool,
-
- // true to show a spinner at the end of the timeline to indicate
- // forward-pagination in progress
- forwardPaginating: PropTypes.bool,
-
- // the list of MatrixEvents to display
- events: PropTypes.array.isRequired,
-
- // ID of an event to highlight. If undefined, no event will be highlighted.
- highlightedEventId: PropTypes.string,
-
- // The room these events are all in together, if any.
- // (The notification panel won't have a room here, for example.)
- room: PropTypes.object,
-
- // Should we show URL Previews
- showUrlPreview: PropTypes.bool,
-
- // event after which we should show a read marker
- readMarkerEventId: PropTypes.string,
-
- // whether the read marker should be visible
- readMarkerVisible: PropTypes.bool,
-
- // the userid of our user. This is used to suppress the read marker
- // for pending messages.
- ourUserId: PropTypes.string,
-
- // true to suppress the date at the start of the timeline
- suppressFirstDateSeparator: PropTypes.bool,
-
- // whether to show read receipts
- showReadReceipts: PropTypes.bool,
-
- // true if updates to the event list should cause the scroll panel to
- // scroll down when we are at the bottom of the window. See ScrollPanel
- // for more details.
- stickyBottom: PropTypes.bool,
-
- // callback which is called when the panel is scrolled.
- onScroll: PropTypes.func,
-
- // callback which is called when the user interacts with the room timeline
- onUserScroll: PropTypes.func,
-
- // callback which is called when more content is needed.
- onFillRequest: PropTypes.func,
-
- // className for the panel
- className: PropTypes.string.isRequired,
-
- // shape parameter to be passed to EventTiles
- tileShape: PropTypes.string,
-
- // show twelve hour timestamps
- isTwelveHour: PropTypes.bool,
-
- // show timestamps always
- alwaysShowTimestamps: PropTypes.bool,
-
- // helper function to access relations for an event
- getRelationsForEvent: PropTypes.func,
-
- // whether to show reactions for an event
- showReactions: PropTypes.bool,
-
- // which layout to use
- layout: LayoutPropType,
-
- // whether or not to show flair at all
- enableFlair: PropTypes.bool,
- };
-
+export default class MessagePanel extends React.Component {
static contextType = RoomContext;
- constructor(props) {
- super(props);
+ // opaque readreceipt info for each userId; used by ReadReceiptMarker
+ // to manage its animations
+ private readonly readReceiptMap: Record = {};
+
+ // Track read receipts by event ID. For each _shown_ event ID, we store
+ // the list of read receipts to display:
+ // [
+ // {
+ // userId: string,
+ // member: RoomMember,
+ // ts: number,
+ // },
+ // ]
+ // This is recomputed on each render. It's only stored on the component
+ // for ease of passing the data around since it's computed in one pass
+ // over all events.
+ private readReceiptsByEvent: Record = {};
+
+ // Track read receipts by user ID. For each user ID we've ever shown a
+ // a read receipt for, we store an object:
+ // {
+ // lastShownEventId: string,
+ // receipt: {
+ // userId: string,
+ // member: RoomMember,
+ // ts: number,
+ // },
+ // }
+ // so that we can always keep receipts displayed by reverting back to
+ // the last shown event for that user ID when needed. This may feel like
+ // it duplicates the receipt storage in the room, but at this layer, we
+ // are tracking _shown_ event IDs, which the JS SDK knows nothing about.
+ // This is recomputed on each render, using the data from the previous
+ // render as our fallback for any user IDs we can't match a receipt to a
+ // displayed event in the current render cycle.
+ private readReceiptsByUserId: Record = {};
+
+ private readonly showHiddenEventsInTimeline: boolean;
+ private isMounted = false;
+
+ private readMarkerNode = createRef();
+ private whoIsTyping = createRef();
+ private scrollPanel = createRef();
+
+ private readonly showTypingNotificationsWatcherRef: string;
+ private eventNodes: Record;
+
+ constructor(props, context) {
+ super(props, context);
this.state = {
// previous positions the read marker has been in, so we can
@@ -164,66 +245,22 @@ export default class MessagePanel extends React.Component {
showTypingNotifications: SettingsStore.getValue("showTypingNotifications"),
};
- // opaque readreceipt info for each userId; used by ReadReceiptMarker
- // to manage its animations
- this._readReceiptMap = {};
-
- // Track read receipts by event ID. For each _shown_ event ID, we store
- // the list of read receipts to display:
- // [
- // {
- // userId: string,
- // member: RoomMember,
- // ts: number,
- // },
- // ]
- // This is recomputed on each render. It's only stored on the component
- // for ease of passing the data around since it's computed in one pass
- // over all events.
- this._readReceiptsByEvent = {};
-
- // Track read receipts by user ID. For each user ID we've ever shown a
- // a read receipt for, we store an object:
- // {
- // lastShownEventId: string,
- // receipt: {
- // userId: string,
- // member: RoomMember,
- // ts: number,
- // },
- // }
- // so that we can always keep receipts displayed by reverting back to
- // the last shown event for that user ID when needed. This may feel like
- // it duplicates the receipt storage in the room, but at this layer, we
- // are tracking _shown_ event IDs, which the JS SDK knows nothing about.
- // This is recomputed on each render, using the data from the previous
- // render as our fallback for any user IDs we can't match a receipt to a
- // displayed event in the current render cycle.
- this._readReceiptsByUserId = {};
-
// Cache hidden events setting on mount since Settings is expensive to
// query, and we check this in a hot code path. This is also cached in
// our RoomContext, however we still need a fallback for roomless MessagePanels.
- this._showHiddenEventsInTimeline =
- SettingsStore.getValue("showHiddenEventsInTimeline");
+ this.showHiddenEventsInTimeline = SettingsStore.getValue("showHiddenEventsInTimeline");
- this._isMounted = false;
-
- this._readMarkerNode = createRef();
- this._whoIsTyping = createRef();
- this._scrollPanel = createRef();
-
- this._showTypingNotificationsWatcherRef =
+ this.showTypingNotificationsWatcherRef =
SettingsStore.watchSetting("showTypingNotifications", null, this.onShowTypingNotificationsChange);
}
componentDidMount() {
- this._isMounted = true;
+ this.isMounted = true;
}
componentWillUnmount() {
- this._isMounted = false;
- SettingsStore.unwatchSetting(this._showTypingNotificationsWatcherRef);
+ this.isMounted = false;
+ SettingsStore.unwatchSetting(this.showTypingNotificationsWatcherRef);
}
componentDidUpdate(prevProps, prevState) {
@@ -236,14 +273,14 @@ export default class MessagePanel extends React.Component {
}
}
- onShowTypingNotificationsChange = () => {
+ private onShowTypingNotificationsChange = (): void => {
this.setState({
showTypingNotifications: SettingsStore.getValue("showTypingNotifications"),
});
};
/* get the DOM node representing the given event */
- getNodeForEventId(eventId) {
+ public getNodeForEventId(eventId: string): HTMLElement {
if (!this.eventNodes) {
return undefined;
}
@@ -253,8 +290,8 @@ export default class MessagePanel extends React.Component {
/* return true if the content is fully scrolled down right now; else false.
*/
- isAtBottom() {
- return this._scrollPanel.current && this._scrollPanel.current.isAtBottom();
+ public isAtBottom(): boolean {
+ return this.scrollPanel.current?.isAtBottom();
}
/* get the current scroll state. See ScrollPanel.getScrollState for
@@ -262,8 +299,8 @@ export default class MessagePanel extends React.Component {
*
* returns null if we are not mounted.
*/
- getScrollState() {
- return this._scrollPanel.current ? this._scrollPanel.current.getScrollState() : null;
+ public getScrollState(): IScrollState {
+ return this.scrollPanel.current?.getScrollState() ?? null;
}
// returns one of:
@@ -272,15 +309,15 @@ export default class MessagePanel extends React.Component {
// -1: read marker is above the window
// 0: read marker is within the window
// +1: read marker is below the window
- getReadMarkerPosition() {
- const readMarker = this._readMarkerNode.current;
- const messageWrapper = this._scrollPanel.current;
+ public getReadMarkerPosition(): number {
+ const readMarker = this.readMarkerNode.current;
+ const messageWrapper = this.scrollPanel.current;
if (!readMarker || !messageWrapper) {
return null;
}
- const wrapperRect = ReactDOM.findDOMNode(messageWrapper).getBoundingClientRect();
+ const wrapperRect = (ReactDOM.findDOMNode(messageWrapper) as HTMLElement).getBoundingClientRect();
const readMarkerRect = readMarker.getBoundingClientRect();
// the read-marker pretends to have zero height when it is actually
@@ -296,17 +333,17 @@ export default class MessagePanel extends React.Component {
/* jump to the top of the content.
*/
- scrollToTop() {
- if (this._scrollPanel.current) {
- this._scrollPanel.current.scrollToTop();
+ public scrollToTop(): void {
+ if (this.scrollPanel.current) {
+ this.scrollPanel.current.scrollToTop();
}
}
/* jump to the bottom of the content.
*/
- scrollToBottom() {
- if (this._scrollPanel.current) {
- this._scrollPanel.current.scrollToBottom();
+ public scrollToBottom(): void {
+ if (this.scrollPanel.current) {
+ this.scrollPanel.current.scrollToBottom();
}
}
@@ -315,9 +352,9 @@ export default class MessagePanel extends React.Component {
*
* @param {number} mult: -1 to page up, +1 to page down
*/
- scrollRelative(mult) {
- if (this._scrollPanel.current) {
- this._scrollPanel.current.scrollRelative(mult);
+ public scrollRelative(mult: number): void {
+ if (this.scrollPanel.current) {
+ this.scrollPanel.current.scrollRelative(mult);
}
}
@@ -326,9 +363,9 @@ export default class MessagePanel extends React.Component {
*
* @param {KeyboardEvent} ev: the keyboard event to handle
*/
- handleScrollKey(ev) {
- if (this._scrollPanel.current) {
- this._scrollPanel.current.handleScrollKey(ev);
+ public handleScrollKey(ev: KeyboardEvent): void {
+ if (this.scrollPanel.current) {
+ this.scrollPanel.current.handleScrollKey(ev);
}
}
@@ -342,38 +379,41 @@ export default class MessagePanel extends React.Component {
* node (specifically, the bottom of it) will be positioned. If omitted, it
* defaults to 0.
*/
- scrollToEvent(eventId, pixelOffset, offsetBase) {
- if (this._scrollPanel.current) {
- this._scrollPanel.current.scrollToToken(eventId, pixelOffset, offsetBase);
+ public scrollToEvent(eventId: string, pixelOffset: number, offsetBase: number): void {
+ if (this.scrollPanel.current) {
+ this.scrollPanel.current.scrollToToken(eventId, pixelOffset, offsetBase);
}
}
- scrollToEventIfNeeded(eventId) {
+ public scrollToEventIfNeeded(eventId: string): void {
const node = this.eventNodes[eventId];
if (node) {
- node.scrollIntoView({block: "nearest", behavior: "instant"});
+ node.scrollIntoView({
+ block: "nearest",
+ behavior: "instant",
+ });
}
}
/* check the scroll state and send out pagination requests if necessary.
*/
- checkFillState() {
- if (this._scrollPanel.current) {
- this._scrollPanel.current.checkFillState();
+ public checkFillState(): void {
+ if (this.scrollPanel.current) {
+ this.scrollPanel.current.checkFillState();
}
}
- _isUnmounting = () => {
- return !this._isMounted;
+ private isUnmounting = (): boolean => {
+ return !this.isMounted;
};
// TODO: Implement granular (per-room) hide options
- _shouldShowEvent(mxEv) {
+ public shouldShowEvent(mxEv: MatrixEvent): boolean {
if (mxEv.sender && MatrixClientPeg.get().isUserIgnored(mxEv.sender.userId)) {
return false; // ignored = no show (only happens if the ignore happens after an event was received)
}
- if (this.context?.showHiddenEventsInTimeline ?? this._showHiddenEventsInTimeline) {
+ if (this.context?.showHiddenEventsInTimeline ?? this.showHiddenEventsInTimeline) {
return true;
}
@@ -387,7 +427,7 @@ export default class MessagePanel extends React.Component {
return !shouldHideEvent(mxEv, this.context);
}
- _readMarkerForEvent(eventId, isLastEvent) {
+ public readMarkerForEvent(eventId: string, isLastEvent: boolean): ReactNode {
const visible = !isLastEvent && this.props.readMarkerVisible;
if (this.props.readMarkerEventId === eventId) {
@@ -406,7 +446,7 @@ export default class MessagePanel extends React.Component {
return (
@@ -425,8 +465,8 @@ export default class MessagePanel extends React.Component {
// transition (ie. the read markers do but the event tiles do not)
// and TransitionGroup requires that all its children are Transitions.
const hr = ;
@@ -446,7 +486,7 @@ export default class MessagePanel extends React.Component {
return null;
}
- _collectGhostReadMarker = (node) => {
+ private collectGhostReadMarker = (node: HTMLElement): void => {
if (node) {
// now the element has appeared, change the style which will trigger the CSS transition
requestAnimationFrame(() => {
@@ -456,15 +496,15 @@ export default class MessagePanel extends React.Component {
}
};
- _onGhostTransitionEnd = (ev) => {
+ private onGhostTransitionEnd = (ev: TransitionEvent): void => {
// we can now clean up the ghost element
- const finishedEventId = ev.target.dataset.eventid;
+ const finishedEventId = (ev.target as HTMLElement).dataset.eventid;
this.setState({
ghostReadMarkers: this.state.ghostReadMarkers.filter(eid => eid !== finishedEventId),
});
};
- _getNextEventInfo(arr, i) {
+ private getNextEventInfo(arr: MatrixEvent[], i: number): { nextEvent: MatrixEvent, nextTile: MatrixEvent } {
const nextEvent = i < arr.length - 1
? arr[i + 1]
: null;
@@ -473,16 +513,16 @@ export default class MessagePanel extends React.Component {
// when rendering the tile. The shouldShowEvent function is pretty quick at what
// it does, so this should have no significant cost even when a room is used for
// not-chat purposes.
- const nextTile = arr.slice(i + 1).find(e => this._shouldShowEvent(e));
+ const nextTile = arr.slice(i + 1).find(e => this.shouldShowEvent(e));
- return {nextEvent, nextTile};
+ return { nextEvent, nextTile };
}
- get _roomHasPendingEdit() {
+ private get roomHasPendingEdit(): string {
return this.props.room && localStorage.getItem(`mx_edit_room_${this.props.room.roomId}`);
}
- _getEventTiles() {
+ private getEventTiles(): ReactNode[] {
this.eventNodes = {};
let i;
@@ -498,7 +538,7 @@ export default class MessagePanel extends React.Component {
let lastShownNonLocalEchoIndex = -1;
for (i = this.props.events.length-1; i >= 0; i--) {
const mxEv = this.props.events[i];
- if (!this._shouldShowEvent(mxEv)) {
+ if (!this.shouldShowEvent(mxEv)) {
continue;
}
@@ -522,18 +562,18 @@ export default class MessagePanel extends React.Component {
// Note: the EventTile might still render a "sent/sending receipt" independent of
// this information. When not providing read receipt information, the tile is likely
// to assume that sent receipts are to be shown more often.
- this._readReceiptsByEvent = {};
+ this.readReceiptsByEvent = {};
if (this.props.showReadReceipts) {
- this._readReceiptsByEvent = this._getReadReceiptsByShownEvent();
+ this.readReceiptsByEvent = this.getReadReceiptsByShownEvent();
}
- let grouper = null;
+ let grouper: BaseGrouper = null;
for (i = 0; i < this.props.events.length; i++) {
const mxEv = this.props.events[i];
const eventId = mxEv.getId();
const last = (mxEv === lastShownEvent);
- const {nextEvent, nextTile} = this._getNextEventInfo(this.props.events, i);
+ const { nextEvent, nextTile } = this.getNextEventInfo(this.props.events, i);
if (grouper) {
if (grouper.shouldGroup(mxEv)) {
@@ -554,26 +594,25 @@ export default class MessagePanel extends React.Component {
}
}
if (!grouper) {
- const wantTile = this._shouldShowEvent(mxEv);
+ const wantTile = this.shouldShowEvent(mxEv);
const isGrouped = false;
if (wantTile) {
- // make sure we unpack the array returned by _getTilesForEvent,
+ // make sure we unpack the array returned by getTilesForEvent,
// otherwise react will auto-generate keys and we will end up
// replacing all of the DOM elements every time we paginate.
- ret.push(...this._getTilesForEvent(prevEvent, mxEv, last, isGrouped,
- nextEvent, nextTile));
+ ret.push(...this.getTilesForEvent(prevEvent, mxEv, last, isGrouped, nextEvent, nextTile));
prevEvent = mxEv;
}
- const readMarker = this._readMarkerForEvent(eventId, i >= lastShownNonLocalEchoIndex);
+ const readMarker = this.readMarkerForEvent(eventId, i >= lastShownNonLocalEchoIndex);
if (readMarker) ret.push(readMarker);
}
}
- if (!this.props.editState && this._roomHasPendingEdit) {
+ if (!this.props.editState && this.roomHasPendingEdit) {
defaultDispatcher.dispatch({
action: "edit_event",
- event: this.props.room.findEventById(this._roomHasPendingEdit),
+ event: this.props.room.findEventById(this.roomHasPendingEdit),
});
}
@@ -584,10 +623,14 @@ export default class MessagePanel extends React.Component {
return ret;
}
- _getTilesForEvent(prevEvent, mxEv, last, isGrouped=false, nextEvent, nextEventWithTile) {
- const TileErrorBoundary = sdk.getComponent('messages.TileErrorBoundary');
- const EventTile = sdk.getComponent('rooms.EventTile');
- const DateSeparator = sdk.getComponent('messages.DateSeparator');
+ public getTilesForEvent(
+ prevEvent: MatrixEvent,
+ mxEv: MatrixEvent,
+ last = false,
+ isGrouped = false,
+ nextEvent?: MatrixEvent,
+ nextEventWithTile?: MatrixEvent,
+ ): ReactNode[] {
const ret = [];
const isEditing = this.props.editState &&
@@ -602,7 +645,7 @@ export default class MessagePanel extends React.Component {
}
// do we need a date separator since the last event?
- const wantsDateSeparator = this._wantsDateSeparator(prevEvent, eventDate);
+ const wantsDateSeparator = this.wantsDateSeparator(prevEvent, eventDate);
if (wantsDateSeparator && !isGrouped) {
const dateSeparator =
;
ret.push(dateSeparator);
@@ -610,7 +653,7 @@ export default class MessagePanel extends React.Component {
let willWantDateSeparator = false;
if (nextEvent) {
- willWantDateSeparator = this._wantsDateSeparator(mxEv, nextEvent.getDate() || new Date());
+ willWantDateSeparator = this.wantsDateSeparator(mxEv, nextEvent.getDate() || new Date());
}
// is this a continuation of the previous message?
@@ -620,12 +663,12 @@ export default class MessagePanel extends React.Component {
const eventId = mxEv.getId();
const highlight = (eventId === this.props.highlightedEventId);
- const readReceipts = this._readReceiptsByEvent[eventId];
+ const readReceipts = this.readReceiptsByEvent[eventId];
let isLastSuccessful = false;
const isSentState = s => !s || s === 'sent';
const isSent = isSentState(mxEv.getAssociatedStatus());
- const hasNextEvent = nextEvent && this._shouldShowEvent(nextEvent);
+ const hasNextEvent = nextEvent && this.shouldShowEvent(nextEvent);
if (!hasNextEvent && isSent) {
isLastSuccessful = true;
} else if (hasNextEvent && isSent && !isSentState(nextEvent.getAssociatedStatus())) {
@@ -651,18 +694,18 @@ export default class MessagePanel extends React.Component {
{
if (!r.userId || r.type !== "m.read" || r.userId === myUserId) {
return; // ignore non-read receipts and receipts from self.
@@ -723,13 +766,13 @@ export default class MessagePanel extends React.Component {
// Get an object that maps from event ID to a list of read receipts that
// should be shown next to that event. If a hidden event has read receipts,
// they are folded into the receipts of the last shown event.
- _getReadReceiptsByShownEvent() {
+ private getReadReceiptsByShownEvent(): Record {
const receiptsByEvent = {};
const receiptsByUserId = {};
let lastShownEventId;
for (const event of this.props.events) {
- if (this._shouldShowEvent(event)) {
+ if (this.shouldShowEvent(event)) {
lastShownEventId = event.getId();
}
if (!lastShownEventId) {
@@ -737,7 +780,7 @@ export default class MessagePanel extends React.Component {
}
const existingReceipts = receiptsByEvent[lastShownEventId] || [];
- const newReceipts = this._getReadReceiptsForEvent(event);
+ const newReceipts = this.getReadReceiptsForEvent(event);
receiptsByEvent[lastShownEventId] = existingReceipts.concat(newReceipts);
// Record these receipts along with their last shown event ID for
@@ -756,16 +799,16 @@ export default class MessagePanel extends React.Component {
// someone which had one in the last. By looking through our previous
// mapping of receipts by user ID, we can cover recover any receipts
// that would have been lost by using the same event ID from last time.
- for (const userId in this._readReceiptsByUserId) {
+ for (const userId in this.readReceiptsByUserId) {
if (receiptsByUserId[userId]) {
continue;
}
- const { lastShownEventId, receipt } = this._readReceiptsByUserId[userId];
+ const { lastShownEventId, receipt } = this.readReceiptsByUserId[userId];
const existingReceipts = receiptsByEvent[lastShownEventId] || [];
receiptsByEvent[lastShownEventId] = existingReceipts.concat(receipt);
receiptsByUserId[userId] = { lastShownEventId, receipt };
}
- this._readReceiptsByUserId = receiptsByUserId;
+ this.readReceiptsByUserId = receiptsByUserId;
// After grouping receipts by shown events, do another pass to sort each
// receipt list.
@@ -778,21 +821,21 @@ export default class MessagePanel extends React.Component {
return receiptsByEvent;
}
- _collectEventNode = (eventId, node) => {
+ private collectEventNode = (eventId: string, node: EventTile): void => {
this.eventNodes[eventId] = node?.ref?.current;
}
// once dynamic content in the events load, make the scrollPanel check the
// scroll offsets.
- _onHeightChanged = () => {
- const scrollPanel = this._scrollPanel.current;
+ public onHeightChanged = (): void => {
+ const scrollPanel = this.scrollPanel.current;
if (scrollPanel) {
scrollPanel.checkScroll();
}
};
- _onTypingShown = () => {
- const scrollPanel = this._scrollPanel.current;
+ private onTypingShown = (): void => {
+ const scrollPanel = this.scrollPanel.current;
// this will make the timeline grow, so checkScroll
scrollPanel.checkScroll();
if (scrollPanel && scrollPanel.getScrollState().stuckAtBottom) {
@@ -800,8 +843,8 @@ export default class MessagePanel extends React.Component {
}
};
- _onTypingHidden = () => {
- const scrollPanel = this._scrollPanel.current;
+ private onTypingHidden = (): void => {
+ const scrollPanel = this.scrollPanel.current;
if (scrollPanel) {
// as hiding the typing notifications doesn't
// update the scrollPanel, we tell it to apply
@@ -813,12 +856,12 @@ export default class MessagePanel extends React.Component {
}
};
- updateTimelineMinHeight() {
- const scrollPanel = this._scrollPanel.current;
+ public updateTimelineMinHeight(): void {
+ const scrollPanel = this.scrollPanel.current;
if (scrollPanel) {
const isAtBottom = scrollPanel.isAtBottom();
- const whoIsTyping = this._whoIsTyping.current;
+ const whoIsTyping = this.whoIsTyping.current;
const isTypingVisible = whoIsTyping && whoIsTyping.isVisible();
// when messages get added to the timeline,
// but somebody else is still typing,
@@ -830,18 +873,14 @@ export default class MessagePanel extends React.Component {
}
}
- onTimelineReset() {
- const scrollPanel = this._scrollPanel.current;
+ public onTimelineReset(): void {
+ const scrollPanel = this.scrollPanel.current;
if (scrollPanel) {
scrollPanel.clearPreventShrinking();
}
}
render() {
- const ErrorBoundary = sdk.getComponent('elements.ErrorBoundary');
- const ScrollPanel = sdk.getComponent("structures.ScrollPanel");
- const WhoIsTypingTile = sdk.getComponent("rooms.WhoIsTypingTile");
- const Spinner = sdk.getComponent("elements.Spinner");
let topSpinner;
let bottomSpinner;
if (this.props.backPaginating) {
@@ -857,9 +896,9 @@ export default class MessagePanel extends React.Component {
if (this.props.room && !this.props.tileShape && this.state.showTypingNotifications) {
whoIsTyping = (
+ onShown={this.onTypingShown}
+ onHidden={this.onTypingHidden}
+ ref={this.whoIsTyping} />
);
}
@@ -875,11 +914,10 @@ export default class MessagePanel extends React.Component {
return (
{ topSpinner }
- { this._getEventTiles() }
+ { this.getEventTiles() }
{ whoIsTyping }
{ bottomSpinner }
@@ -897,6 +935,31 @@ export default class MessagePanel extends React.Component {
}
}
+abstract class BaseGrouper {
+ static canStartGroup = (panel: MessagePanel, ev: MatrixEvent): boolean => true;
+
+ public events: MatrixEvent[] = [];
+ // events that we include in the group but then eject out and place above the group.
+ public ejectedEvents: MatrixEvent[] = [];
+ public readMarker: ReactNode;
+
+ constructor(
+ public readonly panel: MessagePanel,
+ public readonly event: MatrixEvent,
+ public readonly prevEvent: MatrixEvent,
+ public readonly lastShownEvent: MatrixEvent,
+ public readonly nextEvent?: MatrixEvent,
+ public readonly nextEventTile?: MatrixEvent,
+ ) {
+ this.readMarker = panel.readMarkerForEvent(event.getId(), event === lastShownEvent);
+ }
+
+ public abstract shouldGroup(ev: MatrixEvent): boolean;
+ public abstract add(ev: MatrixEvent, showHiddenEvents?: boolean): void;
+ public abstract getTiles(): ReactNode[];
+ public abstract getNewPrevEvent(): MatrixEvent;
+}
+
/* Grouper classes determine when events can be grouped together in a summary.
* Groupers should have the following methods:
* - canStartGroup (static): determines if a new group should be started with the
@@ -912,36 +975,21 @@ export default class MessagePanel extends React.Component {
// Wrap initial room creation events into an EventListSummary
// Grouping only events sent by the same user that sent the `m.room.create` and only until
// the first non-state event or membership event which is not regarding the sender of the `m.room.create` event
-class CreationGrouper {
- static canStartGroup = function(panel, ev) {
- return ev.getType() === "m.room.create";
+class CreationGrouper extends BaseGrouper {
+ static canStartGroup = function(panel: MessagePanel, ev: MatrixEvent): boolean {
+ return ev.getType() === EventType.RoomCreate;
};
- constructor(panel, createEvent, prevEvent, lastShownEvent) {
- this.panel = panel;
- this.createEvent = createEvent;
- this.prevEvent = prevEvent;
- this.lastShownEvent = lastShownEvent;
- this.events = [];
- // events that we include in the group but then eject out and place
- // above the group.
- this.ejectedEvents = [];
- this.readMarker = panel._readMarkerForEvent(
- createEvent.getId(),
- createEvent === lastShownEvent,
- );
- }
-
- shouldGroup(ev) {
+ public shouldGroup(ev: MatrixEvent): boolean {
const panel = this.panel;
- const createEvent = this.createEvent;
- if (!panel._shouldShowEvent(ev)) {
+ const createEvent = this.event;
+ if (!panel.shouldShowEvent(ev)) {
return true;
}
- if (panel._wantsDateSeparator(this.createEvent, ev.getDate())) {
+ if (panel.wantsDateSeparator(this.event, ev.getDate())) {
return false;
}
- if (ev.getType() === "m.room.member"
+ if (ev.getType() === EventType.RoomMember
&& (ev.getStateKey() !== createEvent.getSender() || ev.getContent()["membership"] !== "join")) {
return false;
}
@@ -951,37 +999,35 @@ class CreationGrouper {
return false;
}
- add(ev) {
+ public add(ev: MatrixEvent): void {
const panel = this.panel;
- this.readMarker = this.readMarker || panel._readMarkerForEvent(
+ this.readMarker = this.readMarker || panel.readMarkerForEvent(
ev.getId(),
ev === this.lastShownEvent,
);
- if (!panel._shouldShowEvent(ev)) {
+ if (!panel.shouldShowEvent(ev)) {
return;
}
- if (ev.getType() === "m.room.encryption") {
+ if (ev.getType() === EventType.RoomEncryption) {
this.ejectedEvents.push(ev);
} else {
this.events.push(ev);
}
}
- getTiles() {
+ public getTiles(): ReactNode[] {
// If we don't have any events to group, don't even try to group them. The logic
// below assumes that we have a group of events to deal with, but we might not if
// the events we were supposed to group were redacted.
if (!this.events || !this.events.length) return [];
- const DateSeparator = sdk.getComponent('messages.DateSeparator');
- const EventListSummary = sdk.getComponent('views.elements.EventListSummary');
const panel = this.panel;
const ret = [];
const isGrouped = true;
- const createEvent = this.createEvent;
+ const createEvent = this.event;
const lastShownEvent = this.lastShownEvent;
- if (panel._wantsDateSeparator(this.prevEvent, createEvent.getDate())) {
+ if (panel.wantsDateSeparator(this.prevEvent, createEvent.getDate())) {
const ts = createEvent.getTs();
ret.push(
,
@@ -989,13 +1035,13 @@ class CreationGrouper {
}
// If this m.room.create event should be shown (room upgrade) then show it before the summary
- if (panel._shouldShowEvent(createEvent)) {
+ if (panel.shouldShowEvent(createEvent)) {
// pass in the createEvent as prevEvent as well so no extra DateSeparator is rendered
- ret.push(...panel._getTilesForEvent(createEvent, createEvent));
+ ret.push(...panel.getTilesForEvent(createEvent, createEvent));
}
for (const ejected of this.ejectedEvents) {
- ret.push(...panel._getTilesForEvent(
+ ret.push(...panel.getTilesForEvent(
createEvent, ejected, createEvent === lastShownEvent, isGrouped,
));
}
@@ -1005,7 +1051,7 @@ class CreationGrouper {
// of EventListSummary, render each member event as if the previous
// one was itself. This way, the timestamp of the previous event === the
// timestamp of the current event, and no DateSeparator is inserted.
- return panel._getTilesForEvent(e, e, e === lastShownEvent, isGrouped);
+ return panel.getTilesForEvent(e, e, e === lastShownEvent, isGrouped);
}).reduce((a, b) => a.concat(b), []);
// Get sender profile from the latest event in the summary as the m.room.create doesn't contain one
const ev = this.events[this.events.length - 1];
@@ -1025,7 +1071,7 @@ class CreationGrouper {
@@ -1040,62 +1086,59 @@ class CreationGrouper {
return ret;
}
- getNewPrevEvent() {
- return this.createEvent;
+ public getNewPrevEvent(): MatrixEvent {
+ return this.event;
}
}
-class RedactionGrouper {
- static canStartGroup = function(panel, ev) {
- return panel._shouldShowEvent(ev) && ev.isRedacted();
+class RedactionGrouper extends BaseGrouper {
+ static canStartGroup = function(panel: MessagePanel, ev: MatrixEvent): boolean {
+ return panel.shouldShowEvent(ev) && ev.isRedacted();
}
- constructor(panel, ev, prevEvent, lastShownEvent, nextEvent, nextEventTile) {
- this.panel = panel;
- this.readMarker = panel._readMarkerForEvent(
- ev.getId(),
- ev === lastShownEvent,
- );
+ constructor(
+ panel: MessagePanel,
+ ev: MatrixEvent,
+ prevEvent: MatrixEvent,
+ lastShownEvent: MatrixEvent,
+ nextEvent: MatrixEvent,
+ nextEventTile: MatrixEvent,
+ ) {
+ super(panel, ev, prevEvent, lastShownEvent, nextEvent, nextEventTile);
this.events = [ev];
- this.prevEvent = prevEvent;
- this.lastShownEvent = lastShownEvent;
- this.nextEvent = nextEvent;
- this.nextEventTile = nextEventTile;
}
- shouldGroup(ev) {
+ public shouldGroup(ev: MatrixEvent): boolean {
// absorb hidden events so that they do not break up streams of messages & redaction events being grouped
- if (!this.panel._shouldShowEvent(ev)) {
+ if (!this.panel.shouldShowEvent(ev)) {
return true;
}
- if (this.panel._wantsDateSeparator(this.events[0], ev.getDate())) {
+ if (this.panel.wantsDateSeparator(this.events[0], ev.getDate())) {
return false;
}
return ev.isRedacted();
}
- add(ev) {
- this.readMarker = this.readMarker || this.panel._readMarkerForEvent(
+ public add(ev: MatrixEvent): void {
+ this.readMarker = this.readMarker || this.panel.readMarkerForEvent(
ev.getId(),
ev === this.lastShownEvent,
);
- if (!this.panel._shouldShowEvent(ev)) {
+ if (!this.panel.shouldShowEvent(ev)) {
return;
}
this.events.push(ev);
}
- getTiles() {
+ public getTiles(): ReactNode[] {
if (!this.events || !this.events.length) return [];
- const DateSeparator = sdk.getComponent('messages.DateSeparator');
- const EventListSummary = sdk.getComponent('views.elements.EventListSummary');
const isGrouped = true;
const panel = this.panel;
const ret = [];
const lastShownEvent = this.lastShownEvent;
- if (panel._wantsDateSeparator(this.prevEvent, this.events[0].getDate())) {
+ if (panel.wantsDateSeparator(this.prevEvent, this.events[0].getDate())) {
const ts = this.events[0].getTs();
ret.push(
,
@@ -1106,11 +1149,11 @@ class RedactionGrouper {
this.prevEvent ? this.events[0].getId() : "initial"
);
- const senders = new Set();
+ const senders = new Set();
let eventTiles = this.events.map((e, i) => {
senders.add(e.sender);
const prevEvent = i === 0 ? this.prevEvent : this.events[i - 1];
- return panel._getTilesForEvent(
+ return panel.getTilesForEvent(
prevEvent, e, e === lastShownEvent, isGrouped, this.nextEvent, this.nextEventTile);
}).reduce((a, b) => a.concat(b), []);
@@ -1123,7 +1166,7 @@ class RedactionGrouper {
key={key}
threshold={2}
events={this.events}
- onToggle={panel._onHeightChanged} // Update scroll state
+ onToggle={panel.onHeightChanged} // Update scroll state
summaryMembers={Array.from(senders)}
summaryText={_t("%(count)s messages deleted.", { count: eventTiles.length })}
>
@@ -1138,61 +1181,58 @@ class RedactionGrouper {
return ret;
}
- getNewPrevEvent() {
+ public getNewPrevEvent(): MatrixEvent {
return this.events[this.events.length - 1];
}
}
// Wrap consecutive member events in a ListSummary, ignore if redacted
-class MemberGrouper {
- static canStartGroup = function(panel, ev) {
- return panel._shouldShowEvent(ev) && isMembershipChange(ev);
+class MemberGrouper extends BaseGrouper {
+ static canStartGroup = function(panel: MessagePanel, ev: MatrixEvent): boolean {
+ return panel.shouldShowEvent(ev) && isMembershipChange(ev);
}
- constructor(panel, ev, prevEvent, lastShownEvent) {
- this.panel = panel;
- this.readMarker = panel._readMarkerForEvent(
- ev.getId(),
- ev === lastShownEvent,
- );
- this.events = [ev];
- this.prevEvent = prevEvent;
- this.lastShownEvent = lastShownEvent;
+ constructor(
+ public readonly panel: MessagePanel,
+ public readonly event: MatrixEvent,
+ public readonly prevEvent: MatrixEvent,
+ public readonly lastShownEvent: MatrixEvent,
+ ) {
+ super(panel, event, prevEvent, lastShownEvent);
+ this.events = [event];
}
- shouldGroup(ev) {
- if (this.panel._wantsDateSeparator(this.events[0], ev.getDate())) {
+ public shouldGroup(ev: MatrixEvent): boolean {
+ if (this.panel.wantsDateSeparator(this.events[0], ev.getDate())) {
return false;
}
return isMembershipChange(ev);
}
- add(ev, showHiddenEvents) {
- if (ev.getType() === 'm.room.member') {
+ public add(ev: MatrixEvent, showHiddenEvents?: boolean): void {
+ if (ev.getType() === EventType.RoomMember) {
// We can ignore any events that don't actually have a message to display
if (!hasText(ev, showHiddenEvents)) return;
}
- this.readMarker = this.readMarker || this.panel._readMarkerForEvent(
+ this.readMarker = this.readMarker || this.panel.readMarkerForEvent(
ev.getId(),
ev === this.lastShownEvent,
);
this.events.push(ev);
}
- getTiles() {
+ public getTiles(): ReactNode[] {
// If we don't have any events to group, don't even try to group them. The logic
// below assumes that we have a group of events to deal with, but we might not if
// the events we were supposed to group were redacted.
if (!this.events || !this.events.length) return [];
- const DateSeparator = sdk.getComponent('messages.DateSeparator');
- const MemberEventListSummary = sdk.getComponent('views.elements.MemberEventListSummary');
const isGrouped = true;
const panel = this.panel;
const lastShownEvent = this.lastShownEvent;
const ret = [];
- if (panel._wantsDateSeparator(this.prevEvent, this.events[0].getDate())) {
+ if (panel.wantsDateSeparator(this.prevEvent, this.events[0].getDate())) {
const ts = this.events[0].getTs();
ret.push(
,
@@ -1220,7 +1260,7 @@ class MemberGrouper {
// of MemberEventListSummary, render each member event as if the previous
// one was itself. This way, the timestamp of the previous event === the
// timestamp of the current event, and no DateSeparator is inserted.
- return panel._getTilesForEvent(e, e, e === lastShownEvent, isGrouped);
+ return panel.getTilesForEvent(e, e, e === lastShownEvent, isGrouped);
}).reduce((a, b) => a.concat(b), []);
if (eventTiles.length === 0) {
@@ -1228,9 +1268,10 @@ class MemberGrouper {
}
ret.push(
-
{ eventTiles }
@@ -1244,7 +1285,7 @@ class MemberGrouper {
return ret;
}
- getNewPrevEvent() {
+ public getNewPrevEvent(): MatrixEvent {
return this.events[0];
}
}
diff --git a/src/components/structures/MyGroups.js b/src/components/structures/MyGroups.js
index 1fab6c4348..d0a2fbff41 100644
--- a/src/components/structures/MyGroups.js
+++ b/src/components/structures/MyGroups.js
@@ -82,8 +82,7 @@ export default class MyGroups extends React.Component {
{ _t(
- "To set up a filter, drag a community avatar over to the filter panel on " +
- "the far left hand side of the screen. You can click on an avatar in the " +
+ "You can click on an avatar in the " +
"filter panel at any time to see only the rooms and people associated " +
"with that community.",
) }
diff --git a/src/components/structures/NotificationPanel.tsx b/src/components/structures/NotificationPanel.tsx
index 6c22835447..8c8fab7ece 100644
--- a/src/components/structures/NotificationPanel.tsx
+++ b/src/components/structures/NotificationPanel.tsx
@@ -22,6 +22,7 @@ import BaseCard from "../views/right_panel/BaseCard";
import { replaceableComponent } from "../../utils/replaceableComponent";
import TimelinePanel from "./TimelinePanel";
import Spinner from "../views/elements/Spinner";
+import { TileShape } from "../views/rooms/EventTile";
interface IProps {
onClose(): void;
@@ -48,7 +49,7 @@ export default class NotificationPanel extends React.PureComponent {
manageReadMarkers={false}
timelineSet={timelineSet}
showUrlPreview={false}
- tileShape="notif"
+ tileShape={TileShape.Notif}
empty={emptyState}
alwaysShowTimestamps={true}
/>
diff --git a/src/components/structures/RoomDirectory.tsx b/src/components/structures/RoomDirectory.tsx
index 1e0605f263..c17bf958fd 100644
--- a/src/components/structures/RoomDirectory.tsx
+++ b/src/components/structures/RoomDirectory.tsx
@@ -207,9 +207,9 @@ export default class RoomDirectory extends React.Component {
this.getMoreRooms();
};
- private getMoreRooms() {
- if (this.state.selectedCommunityId) return Promise.resolve(); // no more rooms
- if (!MatrixClientPeg.get()) return Promise.resolve();
+ private getMoreRooms(): Promise {
+ if (this.state.selectedCommunityId) return Promise.resolve(false); // no more rooms
+ if (!MatrixClientPeg.get()) return Promise.resolve(false);
this.setState({
loading: true,
@@ -239,12 +239,12 @@ export default class RoomDirectory extends React.Component {
// if the filter or server has changed since this request was sent,
// throw away the result (don't even clear the busy flag
// since we must still have a request in flight)
- return;
+ return false;
}
if (this.unmounted) {
// if we've been unmounted, we don't care either.
- return;
+ return false;
}
if (this.state.filterString) {
@@ -264,14 +264,13 @@ export default class RoomDirectory extends React.Component {
filterString != this.state.filterString ||
roomServer != this.state.roomServer ||
nextBatch != this.nextBatch) {
- // as above: we don't care about errors for old
- // requests either
- return;
+ // as above: we don't care about errors for old requests either
+ return false;
}
if (this.unmounted) {
// if we've been unmounted, we don't care either.
- return;
+ return false;
}
console.error("Failed to get publicRooms: %s", JSON.stringify(err));
@@ -337,11 +336,10 @@ export default class RoomDirectory extends React.Component {
}
private onRoomClicked = (room: IRoom, ev: ButtonEvent) => {
+ // If room was shift-clicked, remove it from the room directory
if (ev.shiftKey && !this.state.selectedCommunityId) {
ev.preventDefault();
this.removeFromDirectory(room);
- } else {
- this.showRoom(room);
}
};
@@ -568,11 +566,11 @@ export default class RoomDirectory extends React.Component {
let avatarUrl = null;
if (room.avatar_url) avatarUrl = mediaFromMxc(room.avatar_url).getSquareThumbnailHttp(32);
+ // We use onMouseDown instead of onClick, so that we can avoid text getting selected
return [
-
,
];
}
diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx
index 68cc8022b8..177a9921f5 100644
--- a/src/components/structures/RoomView.tsx
+++ b/src/components/structures/RoomView.tsx
@@ -23,7 +23,7 @@ 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";
@@ -60,7 +60,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";
@@ -139,7 +139,7 @@ export interface IState {
draggingFile: boolean;
searching: boolean;
searchTerm?: string;
- searchScope?: "All" | "Room";
+ searchScope?: SearchScope;
searchResults?: XOR<{}, {
count: number;
highlights: 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;
@@ -1134,7 +1130,7 @@ export default class RoomView extends React.Component {
}
}
- private onSearchResultsFillRequest = (backwards: boolean) => {
+ private onSearchResultsFillRequest = (backwards: boolean): Promise => {
if (!backwards) {
return Promise.resolve(false);
}
@@ -1272,7 +1268,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,
@@ -1293,14 +1289,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;
@@ -1313,7 +1309,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
@@ -1345,6 +1341,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,
@@ -2063,7 +2060,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 73%
rename from src/components/structures/ScrollPanel.js
rename to src/components/structures/ScrollPanel.tsx
index f6e1530537..b8e0cdbc34 100644
--- a/src/components/structures/ScrollPanel.js
+++ b/src/components/structures/ScrollPanel.tsx
@@ -1,5 +1,5 @@
/*
-Copyright 2015, 2016 OpenMarket Ltd
+Copyright 2015 - 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -14,17 +14,18 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import React, {createRef} from "react";
-import PropTypes from 'prop-types';
+import React, { createRef, CSSProperties, ReactNode, SyntheticEvent, KeyboardEvent } from "react";
+
import Timer from '../../utils/Timer';
import AutoHideScrollbar from "./AutoHideScrollbar";
-import {replaceableComponent} from "../../utils/replaceableComponent";
-import {getKeyBindingsManager, RoomAction} from "../../KeyBindingsManager";
+import { replaceableComponent } from "../../utils/replaceableComponent";
+import { getKeyBindingsManager, RoomAction } from "../../KeyBindingsManager";
+import ResizeNotifier from "../../utils/ResizeNotifier";
const DEBUG_SCROLL = false;
// The amount of extra scroll distance to allow prior to unfilling.
-// See _getExcessHeight.
+// See getExcessHeight.
const UNPAGINATION_PADDING = 6000;
// The number of milliseconds to debounce calls to onUnfillRequest, to prevent
// many scroll events causing many unfilling requests.
@@ -43,6 +44,75 @@ if (DEBUG_SCROLL) {
debuglog = function() {};
}
+interface IProps {
+ /* stickyBottom: if set to true, then once the user hits the bottom of
+ * the list, any new children added to the list will cause the list to
+ * scroll down to show the new element, rather than preserving the
+ * existing view.
+ */
+ stickyBottom?: boolean;
+
+ /* startAtBottom: if set to true, the view is assumed to start
+ * scrolled to the bottom.
+ * XXX: It's likely this is unnecessary and can be derived from
+ * stickyBottom, but I'm adding an extra parameter to ensure
+ * behaviour stays the same for other uses of ScrollPanel.
+ * If so, let's remove this parameter down the line.
+ */
+ startAtBottom?: boolean;
+
+ /* className: classnames to add to the top-level div
+ */
+ className?: string;
+
+ /* style: styles to add to the top-level div
+ */
+ style?: CSSProperties;
+
+ /* resizeNotifier: ResizeNotifier to know when middle column has changed size
+ */
+ resizeNotifier?: ResizeNotifier;
+
+ /* fixedChildren: allows for children to be passed which are rendered outside
+ * of the wrapper
+ */
+ fixedChildren?: ReactNode;
+
+ /* onFillRequest(backwards): a callback which is called on scroll when
+ * the user nears the start (backwards = true) or end (backwards =
+ * false) of the list.
+ *
+ * This should return a promise; no more calls will be made until the
+ * promise completes.
+ *
+ * The promise should resolve to true if there is more data to be
+ * retrieved in this direction (in which case onFillRequest may be
+ * called again immediately), or false if there is no more data in this
+ * directon (at this time) - which will stop the pagination cycle until
+ * the user scrolls again.
+ */
+ onFillRequest?(backwards: boolean): Promise;
+
+ /* onUnfillRequest(backwards): a callback which is called on scroll when
+ * there are children elements that are far out of view and could be removed
+ * without causing pagination to occur.
+ *
+ * This function should accept a boolean, which is true to indicate the back/top
+ * of the panel and false otherwise, and a scroll token, which refers to the
+ * first element to remove if removing from the front/bottom, and last element
+ * to remove if removing from the back/top.
+ */
+ onUnfillRequest?(backwards: boolean, scrollToken: string): void;
+
+ /* onScroll: a callback which is called whenever any scroll happens.
+ */
+ onScroll?(event: Event): void;
+
+ /* onUserScroll: callback which is called when the user interacts with the room timeline
+ */
+ onUserScroll?(event: SyntheticEvent): void;
+}
+
/* This component implements an intelligent scrolling list.
*
* It wraps a list of
children; when items are added to the start or end
@@ -84,97 +154,54 @@ if (DEBUG_SCROLL) {
* offset as normal.
*/
+export interface IScrollState {
+ stuckAtBottom: boolean;
+ trackedNode?: HTMLElement;
+ trackedScrollToken?: string;
+ bottomOffset?: number;
+ pixelOffset?: number;
+}
+
+interface IPreventShrinkingState {
+ offsetFromBottom: number;
+ offsetNode: HTMLElement;
+}
+
@replaceableComponent("structures.ScrollPanel")
-export default class ScrollPanel extends React.Component {
- static propTypes = {
- /* stickyBottom: if set to true, then once the user hits the bottom of
- * the list, any new children added to the list will cause the list to
- * scroll down to show the new element, rather than preserving the
- * existing view.
- */
- stickyBottom: PropTypes.bool,
-
- /* startAtBottom: if set to true, the view is assumed to start
- * scrolled to the bottom.
- * XXX: It's likely this is unnecessary and can be derived from
- * stickyBottom, but I'm adding an extra parameter to ensure
- * behaviour stays the same for other uses of ScrollPanel.
- * If so, let's remove this parameter down the line.
- */
- startAtBottom: PropTypes.bool,
-
- /* onFillRequest(backwards): a callback which is called on scroll when
- * the user nears the start (backwards = true) or end (backwards =
- * false) of the list.
- *
- * This should return a promise; no more calls will be made until the
- * promise completes.
- *
- * The promise should resolve to true if there is more data to be
- * retrieved in this direction (in which case onFillRequest may be
- * called again immediately), or false if there is no more data in this
- * directon (at this time) - which will stop the pagination cycle until
- * the user scrolls again.
- */
- onFillRequest: PropTypes.func,
-
- /* onUnfillRequest(backwards): a callback which is called on scroll when
- * there are children elements that are far out of view and could be removed
- * without causing pagination to occur.
- *
- * This function should accept a boolean, which is true to indicate the back/top
- * of the panel and false otherwise, and a scroll token, which refers to the
- * first element to remove if removing from the front/bottom, and last element
- * to remove if removing from the back/top.
- */
- onUnfillRequest: PropTypes.func,
-
- /* onScroll: a callback which is called whenever any scroll happens.
- */
- onScroll: PropTypes.func,
-
- /* onUserScroll: callback which is called when the user interacts with the room timeline
- */
- onUserScroll: PropTypes.func,
-
- /* className: classnames to add to the top-level div
- */
- className: PropTypes.string,
-
- /* style: styles to add to the top-level div
- */
- style: PropTypes.object,
-
- /* resizeNotifier: ResizeNotifier to know when middle column has changed size
- */
- resizeNotifier: PropTypes.object,
-
- /* fixedChildren: allows for children to be passed which are rendered outside
- * of the wrapper
- */
- fixedChildren: PropTypes.node,
- };
-
+export default class ScrollPanel extends React.Component {
static defaultProps = {
stickyBottom: true,
startAtBottom: true,
- onFillRequest: function(backwards) { return Promise.resolve(false); },
- onUnfillRequest: function(backwards, scrollToken) {},
+ onFillRequest: function(backwards: boolean) { return Promise.resolve(false); },
+ onUnfillRequest: function(backwards: boolean, scrollToken: string) {},
onScroll: function() {},
};
- constructor(props) {
- super(props);
+ private readonly pendingFillRequests: Record<"b" | "f", boolean> = {
+ b: null,
+ f: null,
+ };
+ private readonly itemlist = createRef();
+ private unmounted = false;
+ private scrollTimeout: Timer;
+ private isFilling: boolean;
+ private fillRequestWhileRunning: boolean;
+ private scrollState: IScrollState;
+ private preventShrinkingState: IPreventShrinkingState;
+ private unfillDebouncer: NodeJS.Timeout;
+ private bottomGrowth: number;
+ private pages: number;
+ private heightUpdateInProgress: boolean;
+ private divScroll: HTMLDivElement;
- this._pendingFillRequests = {b: null, f: null};
+ constructor(props, context) {
+ super(props, context);
if (this.props.resizeNotifier) {
this.props.resizeNotifier.on("middlePanelResizedNoisy", this.onResize);
}
this.resetScrollState();
-
- this._itemlist = createRef();
}
componentDidMount() {
@@ -203,18 +230,18 @@ export default class ScrollPanel extends React.Component {
}
}
- onScroll = ev => {
+ private onScroll = ev => {
// skip scroll events caused by resizing
if (this.props.resizeNotifier && this.props.resizeNotifier.isResizing) return;
- debuglog("onScroll", this._getScrollNode().scrollTop);
- this._scrollTimeout.restart();
- this._saveScrollState();
+ debuglog("onScroll", this.getScrollNode().scrollTop);
+ this.scrollTimeout.restart();
+ this.saveScrollState();
this.updatePreventShrinking();
this.props.onScroll(ev);
this.checkFillState();
};
- onResize = () => {
+ private onResize = () => {
debuglog("onResize");
this.checkScroll();
// update preventShrinkingState if present
@@ -225,11 +252,11 @@ export default class ScrollPanel extends React.Component {
// after an update to the contents of the panel, check that the scroll is
// where it ought to be, and set off pagination requests if necessary.
- checkScroll = () => {
+ public checkScroll = () => {
if (this.unmounted) {
return;
}
- this._restoreSavedScrollState();
+ this.restoreSavedScrollState();
this.checkFillState();
};
@@ -238,8 +265,8 @@ export default class ScrollPanel extends React.Component {
// note that this is independent of the 'stuckAtBottom' state - it is simply
// about whether the content is scrolled down right now, irrespective of
// whether it will stay that way when the children update.
- isAtBottom = () => {
- const sn = this._getScrollNode();
+ public isAtBottom = () => {
+ const sn = this.getScrollNode();
// fractional values (both too big and too small)
// for scrollTop happen on certain browsers/platforms
// when scrolled all the way down. E.g. Chrome 72 on debian.
@@ -278,10 +305,10 @@ export default class ScrollPanel extends React.Component {
// |#########| - |
// |#########| |
// `---------' -
- _getExcessHeight(backwards) {
- const sn = this._getScrollNode();
- const contentHeight = this._getMessagesHeight();
- const listHeight = this._getListHeight();
+ private getExcessHeight(backwards: boolean): number {
+ const sn = this.getScrollNode();
+ const contentHeight = this.getMessagesHeight();
+ const listHeight = this.getListHeight();
const clippedHeight = contentHeight - listHeight;
const unclippedScrollTop = sn.scrollTop + clippedHeight;
@@ -293,13 +320,13 @@ export default class ScrollPanel extends React.Component {
}
// check the scroll state and send out backfill requests if necessary.
- checkFillState = async (depth=0) => {
+ public checkFillState = async (depth = 0): Promise => {
if (this.unmounted) {
return;
}
const isFirstCall = depth === 0;
- const sn = this._getScrollNode();
+ const sn = this.getScrollNode();
// if there is less than a screenful of messages above or below the
// viewport, try to get some more messages.
@@ -330,17 +357,17 @@ export default class ScrollPanel extends React.Component {
// do make a note when a new request comes in while already running one,
// so we can trigger a new chain of calls once done.
if (isFirstCall) {
- if (this._isFilling) {
- debuglog("_isFilling: not entering while request is ongoing, marking for a subsequent request");
- this._fillRequestWhileRunning = true;
+ if (this.isFilling) {
+ debuglog("isFilling: not entering while request is ongoing, marking for a subsequent request");
+ this.fillRequestWhileRunning = true;
return;
}
- debuglog("_isFilling: setting");
- this._isFilling = true;
+ debuglog("isFilling: setting");
+ this.isFilling = true;
}
- const itemlist = this._itemlist.current;
- const firstTile = itemlist && itemlist.firstElementChild;
+ const itemlist = this.itemlist.current;
+ const firstTile = itemlist && itemlist.firstElementChild as HTMLElement;
const contentTop = firstTile && firstTile.offsetTop;
const fillPromises = [];
@@ -348,13 +375,13 @@ export default class ScrollPanel extends React.Component {
// try backward filling
if (!firstTile || (sn.scrollTop - contentTop) < sn.clientHeight) {
// need to back-fill
- fillPromises.push(this._maybeFill(depth, true));
+ fillPromises.push(this.maybeFill(depth, true));
}
// if scrollTop gets to 2 screens from the end (so 1 screen below viewport),
// try forward filling
if ((sn.scrollHeight - sn.scrollTop) < sn.clientHeight * 2) {
// need to forward-fill
- fillPromises.push(this._maybeFill(depth, false));
+ fillPromises.push(this.maybeFill(depth, false));
}
if (fillPromises.length) {
@@ -365,26 +392,26 @@ export default class ScrollPanel extends React.Component {
}
}
if (isFirstCall) {
- debuglog("_isFilling: clearing");
- this._isFilling = false;
+ debuglog("isFilling: clearing");
+ this.isFilling = false;
}
- if (this._fillRequestWhileRunning) {
- this._fillRequestWhileRunning = false;
+ if (this.fillRequestWhileRunning) {
+ this.fillRequestWhileRunning = false;
this.checkFillState();
}
};
// check if unfilling is possible and send an unfill request if necessary
- _checkUnfillState(backwards) {
- let excessHeight = this._getExcessHeight(backwards);
+ private checkUnfillState(backwards: boolean): void {
+ let excessHeight = this.getExcessHeight(backwards);
if (excessHeight <= 0) {
return;
}
const origExcessHeight = excessHeight;
- const tiles = this._itemlist.current.children;
+ const tiles = this.itemlist.current.children;
// The scroll token of the first/last tile to be unpaginated
let markerScrollToken = null;
@@ -413,11 +440,11 @@ export default class ScrollPanel extends React.Component {
if (markerScrollToken) {
// Use a debouncer to prevent multiple unfill calls in quick succession
// This is to make the unfilling process less aggressive
- if (this._unfillDebouncer) {
- clearTimeout(this._unfillDebouncer);
+ if (this.unfillDebouncer) {
+ clearTimeout(this.unfillDebouncer);
}
- this._unfillDebouncer = setTimeout(() => {
- this._unfillDebouncer = null;
+ this.unfillDebouncer = setTimeout(() => {
+ this.unfillDebouncer = null;
debuglog("unfilling now", backwards, origExcessHeight);
this.props.onUnfillRequest(backwards, markerScrollToken);
}, UNFILL_REQUEST_DEBOUNCE_MS);
@@ -425,9 +452,9 @@ export default class ScrollPanel extends React.Component {
}
// check if there is already a pending fill request. If not, set one off.
- _maybeFill(depth, backwards) {
+ private maybeFill(depth: number, backwards: boolean): Promise {
const dir = backwards ? 'b' : 'f';
- if (this._pendingFillRequests[dir]) {
+ if (this.pendingFillRequests[dir]) {
debuglog("Already a "+dir+" fill in progress - not starting another");
return;
}
@@ -436,7 +463,7 @@ export default class ScrollPanel extends React.Component {
// onFillRequest can end up calling us recursively (via onScroll
// events) so make sure we set this before firing off the call.
- this._pendingFillRequests[dir] = true;
+ this.pendingFillRequests[dir] = true;
// wait 1ms before paginating, because otherwise
// this will block the scroll event handler for +700ms
@@ -445,13 +472,13 @@ export default class ScrollPanel extends React.Component {
return new Promise(resolve => setTimeout(resolve, 1)).then(() => {
return this.props.onFillRequest(backwards);
}).finally(() => {
- this._pendingFillRequests[dir] = false;
+ this.pendingFillRequests[dir] = false;
}).then((hasMoreResults) => {
if (this.unmounted) {
return;
}
// Unpaginate once filling is complete
- this._checkUnfillState(!backwards);
+ this.checkUnfillState(!backwards);
debuglog(""+dir+" fill complete; hasMoreResults:"+hasMoreResults);
if (hasMoreResults) {
@@ -477,7 +504,7 @@ export default class ScrollPanel extends React.Component {
* the number of pixels the bottom of the tracked child is above the
* bottom of the scroll panel.
*/
- getScrollState = () => this.scrollState;
+ public getScrollState = (): IScrollState => this.scrollState;
/* reset the saved scroll state.
*
@@ -491,35 +518,35 @@ export default class ScrollPanel extends React.Component {
* no use if no children exist yet, or if you are about to replace the
* child list.)
*/
- resetScrollState = () => {
+ public resetScrollState = (): void => {
this.scrollState = {
stuckAtBottom: this.props.startAtBottom,
};
- this._bottomGrowth = 0;
- this._pages = 0;
- this._scrollTimeout = new Timer(100);
- this._heightUpdateInProgress = false;
+ this.bottomGrowth = 0;
+ this.pages = 0;
+ this.scrollTimeout = new Timer(100);
+ this.heightUpdateInProgress = false;
};
/**
* jump to the top of the content.
*/
- scrollToTop = () => {
- this._getScrollNode().scrollTop = 0;
- this._saveScrollState();
+ public scrollToTop = (): void => {
+ this.getScrollNode().scrollTop = 0;
+ this.saveScrollState();
};
/**
* jump to the bottom of the content.
*/
- scrollToBottom = () => {
+ public scrollToBottom = (): void => {
// the easiest way to make sure that the scroll state is correctly
// saved is to do the scroll, then save the updated state. (Calculating
// it ourselves is hard, and we can't rely on an onScroll callback
// happening, since there may be no user-visible change here).
- const sn = this._getScrollNode();
+ const sn = this.getScrollNode();
sn.scrollTop = sn.scrollHeight;
- this._saveScrollState();
+ this.saveScrollState();
};
/**
@@ -527,18 +554,18 @@ export default class ScrollPanel extends React.Component {
*
* @param {number} mult: -1 to page up, +1 to page down
*/
- scrollRelative = mult => {
- const scrollNode = this._getScrollNode();
+ public scrollRelative = (mult: number): void => {
+ const scrollNode = this.getScrollNode();
const delta = mult * scrollNode.clientHeight * 0.9;
scrollNode.scrollBy(0, delta);
- this._saveScrollState();
+ this.saveScrollState();
};
/**
* Scroll up/down in response to a scroll key
* @param {object} ev the keyboard event
*/
- handleScrollKey = ev => {
+ public handleScrollKey = (ev: KeyboardEvent) => {
let isScrolling = false;
const roomAction = getKeyBindingsManager().getRoomAction(ev);
switch (roomAction) {
@@ -575,17 +602,17 @@ export default class ScrollPanel extends React.Component {
* node (specifically, the bottom of it) will be positioned. If omitted, it
* defaults to 0.
*/
- scrollToToken = (scrollToken, pixelOffset, offsetBase) => {
+ public scrollToToken = (scrollToken: string, pixelOffset: number, offsetBase: number): void => {
pixelOffset = pixelOffset || 0;
offsetBase = offsetBase || 0;
- // set the trackedScrollToken so we can get the node through _getTrackedNode
+ // set the trackedScrollToken so we can get the node through getTrackedNode
this.scrollState = {
stuckAtBottom: false,
trackedScrollToken: scrollToken,
};
- const trackedNode = this._getTrackedNode();
- const scrollNode = this._getScrollNode();
+ const trackedNode = this.getTrackedNode();
+ const scrollNode = this.getScrollNode();
if (trackedNode) {
// set the scrollTop to the position we want.
// note though, that this might not succeed if the combination of offsetBase and pixelOffset
@@ -595,34 +622,34 @@ export default class ScrollPanel extends React.Component {
// enough so it ends up in the top of the viewport.
debuglog("scrollToken: setting scrollTop", {offsetBase, pixelOffset, offsetTop: trackedNode.offsetTop});
scrollNode.scrollTop = (trackedNode.offsetTop - (scrollNode.clientHeight * offsetBase)) + pixelOffset;
- this._saveScrollState();
+ this.saveScrollState();
}
};
- _saveScrollState() {
+ private saveScrollState(): void {
if (this.props.stickyBottom && this.isAtBottom()) {
this.scrollState = { stuckAtBottom: true };
debuglog("saved stuckAtBottom state");
return;
}
- const scrollNode = this._getScrollNode();
+ const scrollNode = this.getScrollNode();
const viewportBottom = scrollNode.scrollHeight - (scrollNode.scrollTop + scrollNode.clientHeight);
- const itemlist = this._itemlist.current;
+ const itemlist = this.itemlist.current;
const messages = itemlist.children;
let node = null;
// TODO: do a binary search here, as items are sorted by offsetTop
// loop backwards, from bottom-most message (as that is the most common case)
- for (let i = messages.length-1; i >= 0; --i) {
- if (!messages[i].dataset.scrollTokens) {
+ for (let i = messages.length - 1; i >= 0; --i) {
+ if (!(messages[i] as HTMLElement).dataset.scrollTokens) {
continue;
}
node = messages[i];
// break at the first message (coming from the bottom)
// that has it's offsetTop above the bottom of the viewport.
- if (this._topFromBottom(node) > viewportBottom) {
+ if (this.topFromBottom(node) > viewportBottom) {
// Use this node as the scrollToken
break;
}
@@ -634,7 +661,7 @@ export default class ScrollPanel extends React.Component {
}
const scrollToken = node.dataset.scrollTokens.split(',')[0];
debuglog("saving anchored scroll state to message", node && node.innerText, scrollToken);
- const bottomOffset = this._topFromBottom(node);
+ const bottomOffset = this.topFromBottom(node);
this.scrollState = {
stuckAtBottom: false,
trackedNode: node,
@@ -644,35 +671,35 @@ export default class ScrollPanel extends React.Component {
};
}
- async _restoreSavedScrollState() {
+ private async restoreSavedScrollState(): Promise {
const scrollState = this.scrollState;
if (scrollState.stuckAtBottom) {
- const sn = this._getScrollNode();
+ const sn = this.getScrollNode();
if (sn.scrollTop !== sn.scrollHeight) {
sn.scrollTop = sn.scrollHeight;
}
} else if (scrollState.trackedScrollToken) {
- const itemlist = this._itemlist.current;
- const trackedNode = this._getTrackedNode();
+ const itemlist = this.itemlist.current;
+ const trackedNode = this.getTrackedNode();
if (trackedNode) {
- const newBottomOffset = this._topFromBottom(trackedNode);
+ const newBottomOffset = this.topFromBottom(trackedNode);
const bottomDiff = newBottomOffset - scrollState.bottomOffset;
- this._bottomGrowth += bottomDiff;
+ this.bottomGrowth += bottomDiff;
scrollState.bottomOffset = newBottomOffset;
- const newHeight = `${this._getListHeight()}px`;
+ const newHeight = `${this.getListHeight()}px`;
if (itemlist.style.height !== newHeight) {
itemlist.style.height = newHeight;
}
debuglog("balancing height because messages below viewport grew by", bottomDiff);
}
}
- if (!this._heightUpdateInProgress) {
- this._heightUpdateInProgress = true;
+ if (!this.heightUpdateInProgress) {
+ this.heightUpdateInProgress = true;
try {
- await this._updateHeight();
+ await this.updateHeight();
} finally {
- this._heightUpdateInProgress = false;
+ this.heightUpdateInProgress = false;
}
} else {
debuglog("not updating height because request already in progress");
@@ -680,11 +707,11 @@ export default class ScrollPanel extends React.Component {
}
// need a better name that also indicates this will change scrollTop? Rebalance height? Reveal content?
- async _updateHeight() {
+ private async updateHeight(): Promise {
// wait until user has stopped scrolling
- if (this._scrollTimeout.isRunning()) {
+ if (this.scrollTimeout.isRunning()) {
debuglog("updateHeight waiting for scrolling to end ... ");
- await this._scrollTimeout.finished();
+ await this.scrollTimeout.finished();
} else {
debuglog("updateHeight getting straight to business, no scrolling going on.");
}
@@ -694,14 +721,14 @@ export default class ScrollPanel extends React.Component {
return;
}
- const sn = this._getScrollNode();
- const itemlist = this._itemlist.current;
- const contentHeight = this._getMessagesHeight();
+ const sn = this.getScrollNode();
+ const itemlist = this.itemlist.current;
+ const contentHeight = this.getMessagesHeight();
const minHeight = sn.clientHeight;
const height = Math.max(minHeight, contentHeight);
- this._pages = Math.ceil(height / PAGE_SIZE);
- this._bottomGrowth = 0;
- const newHeight = `${this._getListHeight()}px`;
+ this.pages = Math.ceil(height / PAGE_SIZE);
+ this.bottomGrowth = 0;
+ const newHeight = `${this.getListHeight()}px`;
const scrollState = this.scrollState;
if (scrollState.stuckAtBottom) {
@@ -713,7 +740,7 @@ export default class ScrollPanel extends React.Component {
}
debuglog("updateHeight to", newHeight);
} else if (scrollState.trackedScrollToken) {
- const trackedNode = this._getTrackedNode();
+ const trackedNode = this.getTrackedNode();
// if the timeline has been reloaded
// this can be called before scrollToBottom or whatever has been called
// so don't do anything if the node has disappeared from
@@ -735,17 +762,17 @@ export default class ScrollPanel extends React.Component {
}
}
- _getTrackedNode() {
+ private getTrackedNode(): HTMLElement {
const scrollState = this.scrollState;
const trackedNode = scrollState.trackedNode;
if (!trackedNode || !trackedNode.parentElement) {
let node;
- const messages = this._itemlist.current.children;
+ const messages = this.itemlist.current.children;
const scrollToken = scrollState.trackedScrollToken;
for (let i = messages.length-1; i >= 0; --i) {
- const m = messages[i];
+ const m = messages[i] as HTMLElement;
// 'data-scroll-tokens' is a DOMString of comma-separated scroll tokens
// There might only be one scroll token
if (m.dataset.scrollTokens &&
@@ -768,45 +795,45 @@ export default class ScrollPanel extends React.Component {
return scrollState.trackedNode;
}
- _getListHeight() {
- return this._bottomGrowth + (this._pages * PAGE_SIZE);
+ private getListHeight(): number {
+ return this.bottomGrowth + (this.pages * PAGE_SIZE);
}
- _getMessagesHeight() {
- const itemlist = this._itemlist.current;
- const lastNode = itemlist.lastElementChild;
+ private getMessagesHeight(): number {
+ const itemlist = this.itemlist.current;
+ const lastNode = itemlist.lastElementChild as HTMLElement;
const lastNodeBottom = lastNode ? lastNode.offsetTop + lastNode.clientHeight : 0;
- const firstNodeTop = itemlist.firstElementChild ? itemlist.firstElementChild.offsetTop : 0;
+ const firstNodeTop = itemlist.firstElementChild ? (itemlist.firstElementChild as HTMLElement).offsetTop : 0;
// 18 is itemlist padding
return lastNodeBottom - firstNodeTop + (18 * 2);
}
- _topFromBottom(node) {
+ private topFromBottom(node: HTMLElement): number {
// current capped height - distance from top = distance from bottom of container to top of tracked element
- return this._itemlist.current.clientHeight - node.offsetTop;
+ return this.itemlist.current.clientHeight - node.offsetTop;
}
/* get the DOM node which has the scrollTop property we care about for our
* message panel.
*/
- _getScrollNode() {
+ private getScrollNode(): HTMLDivElement {
if (this.unmounted) {
// this shouldn't happen, but when it does, turn the NPE into
// something more meaningful.
- throw new Error("ScrollPanel._getScrollNode called when unmounted");
+ throw new Error("ScrollPanel.getScrollNode called when unmounted");
}
- if (!this._divScroll) {
+ if (!this.divScroll) {
// Likewise, we should have the ref by this point, but if not
// turn the NPE into something meaningful.
- throw new Error("ScrollPanel._getScrollNode called before AutoHideScrollbar ref collected");
+ throw new Error("ScrollPanel.getScrollNode called before AutoHideScrollbar ref collected");
}
- return this._divScroll;
+ return this.divScroll;
}
- _collectScroll = divScroll => {
- this._divScroll = divScroll;
+ private collectScroll = (divScroll: HTMLDivElement) => {
+ this.divScroll = divScroll;
};
/**
@@ -814,15 +841,15 @@ export default class ScrollPanel extends React.Component {
anything below it changes, by calling updatePreventShrinking, to keep
the same minimum bottom offset, effectively preventing the timeline to shrink.
*/
- preventShrinking = () => {
- const messageList = this._itemlist.current;
+ public preventShrinking = (): void => {
+ const messageList = this.itemlist.current;
const tiles = messageList && messageList.children;
if (!messageList) {
return;
}
let lastTileNode;
for (let i = tiles.length - 1; i >= 0; i--) {
- const node = tiles[i];
+ const node = tiles[i] as HTMLElement;
if (node.dataset.scrollTokens) {
lastTileNode = node;
break;
@@ -841,8 +868,8 @@ export default class ScrollPanel extends React.Component {
};
/** Clear shrinking prevention. Used internally, and when the timeline is reloaded. */
- clearPreventShrinking = () => {
- const messageList = this._itemlist.current;
+ public clearPreventShrinking = (): void => {
+ const messageList = this.itemlist.current;
const balanceElement = messageList && messageList.parentElement;
if (balanceElement) balanceElement.style.paddingBottom = null;
this.preventShrinkingState = null;
@@ -857,11 +884,11 @@ export default class ScrollPanel extends React.Component {
from the bottom of the marked tile grows larger than
what it was when marking.
*/
- updatePreventShrinking = () => {
+ public updatePreventShrinking = (): void => {
if (this.preventShrinkingState) {
- const sn = this._getScrollNode();
+ const sn = this.getScrollNode();
const scrollState = this.scrollState;
- const messageList = this._itemlist.current;
+ const messageList = this.itemlist.current;
const {offsetNode, offsetFromBottom} = this.preventShrinkingState;
// element used to set paddingBottom to balance the typing notifs disappearing
const balanceElement = messageList.parentElement;
@@ -898,13 +925,15 @@ export default class ScrollPanel extends React.Component {
// list-style-type: none; is no longer a list
return (
+ className={`mx_ScrollPanel ${this.props.className}`}
+ style={this.props.style}
+ >
{ this.props.fixedChildren }
-
+
{ this.props.children }
diff --git a/src/components/structures/SpaceRoomDirectory.tsx b/src/components/structures/SpaceRoomDirectory.tsx
index acbde0b097..4292b60f41 100644
--- a/src/components/structures/SpaceRoomDirectory.tsx
+++ b/src/components/structures/SpaceRoomDirectory.tsx
@@ -14,34 +14,34 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import React, {ReactNode, useMemo, useState} from "react";
-import {Room} from "matrix-js-sdk/src/models/room";
-import {MatrixClient} from "matrix-js-sdk/src/client";
-import {EventType, RoomType} from "matrix-js-sdk/src/@types/event";
+import React, { ReactNode, useMemo, useState } from "react";
+import { Room } from "matrix-js-sdk/src/models/room";
+import { MatrixClient } from "matrix-js-sdk/src/client";
+import { EventType, RoomType } from "matrix-js-sdk/src/@types/event";
import classNames from "classnames";
-import {sortBy} from "lodash";
+import { sortBy } from "lodash";
-import {MatrixClientPeg} from "../../MatrixClientPeg";
+import { MatrixClientPeg } from "../../MatrixClientPeg";
import dis from "../../dispatcher/dispatcher";
-import {_t} from "../../languageHandler";
-import AccessibleButton, {ButtonEvent} from "../views/elements/AccessibleButton";
+import { _t } from "../../languageHandler";
+import AccessibleButton, { ButtonEvent } from "../views/elements/AccessibleButton";
import BaseDialog from "../views/dialogs/BaseDialog";
import Spinner from "../views/elements/Spinner";
import SearchBox from "./SearchBox";
import RoomAvatar from "../views/avatars/RoomAvatar";
import RoomName from "../views/elements/RoomName";
-import {useAsyncMemo} from "../../hooks/useAsyncMemo";
-import {EnhancedMap} from "../../utils/maps";
+import { useAsyncMemo } from "../../hooks/useAsyncMemo";
+import { EnhancedMap } from "../../utils/maps";
import StyledCheckbox from "../views/elements/StyledCheckbox";
import AutoHideScrollbar from "./AutoHideScrollbar";
import BaseAvatar from "../views/avatars/BaseAvatar";
-import {mediaFromMxc} from "../../customisations/Media";
+import { mediaFromMxc } from "../../customisations/Media";
import InfoTooltip from "../views/elements/InfoTooltip";
import TextWithTooltip from "../views/elements/TextWithTooltip";
-import {useStateToggle} from "../../hooks/useStateToggle";
-import {getOrder} from "../../stores/SpaceStore";
+import { useStateToggle } from "../../hooks/useStateToggle";
+import { getChildOrder } from "../../stores/SpaceStore";
import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton";
-import {linkifyElement} from "../../HtmlUtils";
+import { linkifyElement } from "../../HtmlUtils";
interface IHierarchyProps {
space: Room;
@@ -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;
diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.tsx
similarity index 75%
rename from src/components/structures/TimelinePanel.js
rename to src/components/structures/TimelinePanel.tsx
index c97ace3d70..1904d32000 100644
--- a/src/components/structures/TimelinePanel.js
+++ b/src/components/structures/TimelinePanel.tsx
@@ -1,8 +1,5 @@
/*
-Copyright 2016 OpenMarket Ltd
-Copyright 2017 Vector Creations Ltd
-Copyright 2019 New Vector Ltd
-Copyright 2019-2020 The Matrix.org Foundation C.I.C.
+Copyright 2016 - 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -17,13 +14,16 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import SettingsStore from "../../settings/SettingsStore";
-import { LayoutPropType } from "../../settings/Layout";
-import React, { createRef } from 'react';
+import React, { createRef, ReactNode, SyntheticEvent } from 'react';
import ReactDOM from "react-dom";
-import PropTypes from 'prop-types';
+import { Room } from "matrix-js-sdk/src/models/room";
+import { MatrixEvent } from "matrix-js-sdk/src/models/event";
+import { TimelineSet } from "matrix-js-sdk/src/models/event-timeline-set";
import { EventTimeline } from "matrix-js-sdk/src/models/event-timeline";
import { TimelineWindow } from "matrix-js-sdk/src/timeline-window";
+
+import SettingsStore from "../../settings/SettingsStore";
+import { Layout } from "../../settings/Layout";
import { _t } from '../../languageHandler';
import { MatrixClientPeg } from "../../MatrixClientPeg";
import RoomContext from "../../contexts/RoomContext";
@@ -35,11 +35,19 @@ import { Key } from '../../Keyboard';
import Timer from '../../utils/Timer';
import shouldHideEvent from '../../shouldHideEvent';
import EditorStateTransfer from '../../utils/EditorStateTransfer';
-import { haveTileForEvent } from "../views/rooms/EventTile";
+import { haveTileForEvent, TileShape } from "../views/rooms/EventTile";
import { UIFeature } from "../../settings/UIFeature";
import { replaceableComponent } from "../../utils/replaceableComponent";
import { arrayFastClone } from "../../utils/arrays";
import { Action } from "../../dispatcher/actions";
+import MessagePanel from "./MessagePanel";
+import { SyncState } from 'matrix-js-sdk/src/sync.api';
+import { IScrollState } from "./ScrollPanel";
+import { ActionPayload } from "../../dispatcher/payloads";
+import { EventType } from 'matrix-js-sdk/src/@types/event';
+import ResizeNotifier from "../../utils/ResizeNotifier";
+import { RoomPermalinkCreator } from "../../utils/permalinks/Permalinks";
+import Spinner from "../views/elements/Spinner";
const PAGINATE_SIZE = 20;
const INITIAL_SIZE = 20;
@@ -47,90 +55,159 @@ const READ_RECEIPT_INTERVAL_MS = 500;
const DEBUG = false;
-let debuglog = function() {};
+let debuglog = function(...s: any[]) {};
if (DEBUG) {
// using bind means that we get to keep useful line numbers in the console
debuglog = console.log.bind(console);
}
+interface IProps {
+ // The js-sdk EventTimelineSet object for the timeline sequence we are
+ // representing. This may or may not have a room, depending on what it's
+ // a timeline representing. If it has a room, we maintain RRs etc for
+ // that room.
+ timelineSet: TimelineSet;
+ showReadReceipts?: boolean;
+ // Enable managing RRs and RMs. These require the timelineSet to have a room.
+ manageReadReceipts?: boolean;
+ sendReadReceiptOnLoad?: boolean;
+ manageReadMarkers?: boolean;
+
+ // true to give the component a 'display: none' style.
+ hidden?: boolean;
+
+ // ID of an event to highlight. If undefined, no event will be highlighted.
+ // typically this will be either 'eventId' or undefined.
+ highlightedEventId?: string;
+
+ // id of an event to jump to. If not given, will go to the end of the live timeline.
+ eventId?: string;
+
+ // where to position the event given by eventId, in pixels from the bottom of the viewport.
+ // If not given, will try to put the event half way down the viewport.
+ eventPixelOffset?: number;
+
+ // Should we show URL Previews
+ showUrlPreview?: boolean;
+
+ // maximum number of events to show in a timeline
+ timelineCap?: number;
+
+ // classname to use for the messagepanel
+ className?: string;
+
+ // shape property to be passed to EventTiles
+ tileShape?: TileShape;
+
+ // placeholder to use if the timeline is empty
+ empty?: ReactNode;
+
+ // whether to show reactions for an event
+ showReactions?: boolean;
+
+ // which layout to use
+ layout?: Layout;
+
+ // whether to always show timestamps for an event
+ alwaysShowTimestamps?: boolean;
+
+ resizeNotifier?: ResizeNotifier;
+ editState?: EditorStateTransfer;
+ permalinkCreator?: RoomPermalinkCreator;
+ membersLoaded?: boolean;
+
+ // callback which is called when the panel is scrolled.
+ onScroll?(event: Event): void;
+
+ // callback which is called when the user interacts with the room timeline
+ onUserScroll?(event: SyntheticEvent): void;
+
+ // callback which is called when the read-up-to mark is updated.
+ onReadMarkerUpdated?(): void;
+
+ // callback which is called when we wish to paginate the timeline window.
+ onPaginationRequest?(timelineWindow: TimelineWindow, direction: string, size: number): Promise,
+}
+
+interface IState {
+ events: MatrixEvent[];
+ liveEvents: MatrixEvent[];
+ // track whether our room timeline is loading
+ timelineLoading: boolean;
+
+ // the index of the first event that is to be shown
+ firstVisibleEventIndex: number;
+
+ // canBackPaginate == false may mean:
+ //
+ // * we haven't (successfully) loaded the timeline yet, or:
+ //
+ // * we have got to the point where the room was created, or:
+ //
+ // * the server indicated that there were no more visible events
+ // (normally implying we got to the start of the room), or:
+ //
+ // * we gave up asking the server for more events
+ canBackPaginate: boolean;
+
+ // canForwardPaginate == false may mean:
+ //
+ // * we haven't (successfully) loaded the timeline yet
+ //
+ // * we have got to the end of time and are now tracking the live
+ // timeline, or:
+ //
+ // * the server indicated that there were no more visible events
+ // (not sure if this ever happens when we're not at the live
+ // timeline), or:
+ //
+ // * we are looking at some historical point, but gave up asking
+ // the server for more events
+ canForwardPaginate: boolean;
+
+ // start with the read-marker visible, so that we see its animated
+ // disappearance when switching into the room.
+ readMarkerVisible: boolean;
+
+ readMarkerEventId: string;
+
+ backPaginating: boolean;
+ forwardPaginating: boolean;
+
+ // cache of matrixClient.getSyncState() (but from the 'sync' event)
+ clientSyncState: SyncState;
+
+ // should the event tiles have twelve hour times
+ isTwelveHour: boolean;
+
+ // always show timestamps on event tiles?
+ alwaysShowTimestamps: boolean;
+
+ // how long to show the RM for when it's visible in the window
+ readMarkerInViewThresholdMs: number;
+
+ // how long to show the RM for when it's scrolled off-screen
+ readMarkerOutOfViewThresholdMs: number;
+
+ editState?: EditorStateTransfer;
+}
+
+interface IEventIndexOpts {
+ ignoreOwn?: boolean;
+ allowPartial?: boolean;
+}
+
/*
* Component which shows the event timeline in a room view.
*
* Also responsible for handling and sending read receipts.
*/
@replaceableComponent("structures.TimelinePanel")
-class TimelinePanel extends React.Component {
- static propTypes = {
- // The js-sdk EventTimelineSet object for the timeline sequence we are
- // representing. This may or may not have a room, depending on what it's
- // a timeline representing. If it has a room, we maintain RRs etc for
- // that room.
- timelineSet: PropTypes.object.isRequired,
-
- showReadReceipts: PropTypes.bool,
- // Enable managing RRs and RMs. These require the timelineSet to have a room.
- manageReadReceipts: PropTypes.bool,
- sendReadReceiptOnLoad: PropTypes.bool,
- manageReadMarkers: PropTypes.bool,
-
- // true to give the component a 'display: none' style.
- hidden: PropTypes.bool,
-
- // ID of an event to highlight. If undefined, no event will be highlighted.
- // typically this will be either 'eventId' or undefined.
- highlightedEventId: PropTypes.string,
-
- // id of an event to jump to. If not given, will go to the end of the
- // live timeline.
- eventId: PropTypes.string,
-
- // where to position the event given by eventId, in pixels from the
- // bottom of the viewport. If not given, will try to put the event
- // half way down the viewport.
- eventPixelOffset: PropTypes.number,
-
- // Should we show URL Previews
- showUrlPreview: PropTypes.bool,
-
- // callback which is called when the panel is scrolled.
- onScroll: PropTypes.func,
-
- // callback which is called when the user interacts with the room timeline
- onUserScroll: PropTypes.func,
-
- // callback which is called when the read-up-to mark is updated.
- onReadMarkerUpdated: PropTypes.func,
-
- // callback which is called when we wish to paginate the timeline
- // window.
- onPaginationRequest: PropTypes.func,
-
- // maximum number of events to show in a timeline
- timelineCap: PropTypes.number,
-
- // classname to use for the messagepanel
- className: PropTypes.string,
-
- // shape property to be passed to EventTiles
- tileShape: PropTypes.string,
-
- // placeholder to use if the timeline is empty
- empty: PropTypes.node,
-
- // whether to show reactions for an event
- showReactions: PropTypes.bool,
-
- // which layout to use
- layout: LayoutPropType,
-
- // whether to always show timestamps for an event
- alwaysShowTimestamps: PropTypes.bool,
- }
-
+class TimelinePanel extends React.Component {
static contextType = RoomContext;
// a map from room id to read marker event timestamp
- static roomReadMarkerTsMap = {};
+ static roomReadMarkerTsMap: Record = {};
static defaultProps = {
// By default, disable the timelineCap in favour of unpaginating based on
@@ -140,16 +217,21 @@ class TimelinePanel extends React.Component {
sendReadReceiptOnLoad: true,
};
- constructor(props) {
- super(props);
+ private lastRRSentEventId: string = undefined;
+ private lastRMSentEventId: string = undefined;
+
+ private readonly messagePanel = createRef();
+ private readonly dispatcherRef: string;
+ private timelineWindow?: TimelineWindow;
+ private unmounted = false;
+ private readReceiptActivityTimer: Timer;
+ private readMarkerActivityTimer: Timer;
+
+ constructor(props, context) {
+ super(props, context);
debuglog("TimelinePanel: mounting");
- this.lastRRSentEventId = undefined;
- this.lastRMSentEventId = undefined;
-
- this._messagePanel = createRef();
-
// XXX: we could track RM per TimelineSet rather than per Room.
// but for now we just do it per room for simplicity.
let initialReadMarker = null;
@@ -158,82 +240,41 @@ class TimelinePanel extends React.Component {
if (readmarker) {
initialReadMarker = readmarker.getContent().event_id;
} else {
- initialReadMarker = this._getCurrentReadReceipt();
+ initialReadMarker = this.getCurrentReadReceipt();
}
}
this.state = {
events: [],
liveEvents: [],
- timelineLoading: true, // track whether our room timeline is loading
-
- // the index of the first event that is to be shown
+ timelineLoading: true,
firstVisibleEventIndex: 0,
-
- // canBackPaginate == false may mean:
- //
- // * we haven't (successfully) loaded the timeline yet, or:
- //
- // * we have got to the point where the room was created, or:
- //
- // * the server indicated that there were no more visible events
- // (normally implying we got to the start of the room), or:
- //
- // * we gave up asking the server for more events
canBackPaginate: false,
-
- // canForwardPaginate == false may mean:
- //
- // * we haven't (successfully) loaded the timeline yet
- //
- // * we have got to the end of time and are now tracking the live
- // timeline, or:
- //
- // * the server indicated that there were no more visible events
- // (not sure if this ever happens when we're not at the live
- // timeline), or:
- //
- // * we are looking at some historical point, but gave up asking
- // the server for more events
canForwardPaginate: false,
-
- // start with the read-marker visible, so that we see its animated
- // disappearance when switching into the room.
readMarkerVisible: true,
-
readMarkerEventId: initialReadMarker,
-
backPaginating: false,
forwardPaginating: false,
-
- // cache of matrixClient.getSyncState() (but from the 'sync' event)
clientSyncState: MatrixClientPeg.get().getSyncState(),
-
- // should the event tiles have twelve hour times
isTwelveHour: SettingsStore.getValue("showTwelveHourTimestamps"),
-
- // always show timestamps on event tiles?
alwaysShowTimestamps: SettingsStore.getValue("alwaysShowTimestamps"),
-
- // how long to show the RM for when it's visible in the window
readMarkerInViewThresholdMs: SettingsStore.getValue("readMarkerInViewThresholdMs"),
-
- // how long to show the RM for when it's scrolled off-screen
readMarkerOutOfViewThresholdMs: SettingsStore.getValue("readMarkerOutOfViewThresholdMs"),
};
this.dispatcherRef = dis.register(this.onAction);
- MatrixClientPeg.get().on("Room.timeline", this.onRoomTimeline);
- MatrixClientPeg.get().on("Room.timelineReset", this.onRoomTimelineReset);
- MatrixClientPeg.get().on("Room.redaction", this.onRoomRedaction);
+ const cli = MatrixClientPeg.get();
+ cli.on("Room.timeline", this.onRoomTimeline);
+ cli.on("Room.timelineReset", this.onRoomTimelineReset);
+ cli.on("Room.redaction", this.onRoomRedaction);
// same event handler as Room.redaction as for both we just do forceUpdate
- MatrixClientPeg.get().on("Room.redactionCancelled", this.onRoomRedaction);
- MatrixClientPeg.get().on("Room.receipt", this.onRoomReceipt);
- MatrixClientPeg.get().on("Room.localEchoUpdated", this.onLocalEchoUpdated);
- MatrixClientPeg.get().on("Room.accountData", this.onAccountData);
- MatrixClientPeg.get().on("Event.decrypted", this.onEventDecrypted);
- MatrixClientPeg.get().on("Event.replaced", this.onEventReplaced);
- MatrixClientPeg.get().on("sync", this.onSync);
+ cli.on("Room.redactionCancelled", this.onRoomRedaction);
+ cli.on("Room.receipt", this.onRoomReceipt);
+ cli.on("Room.localEchoUpdated", this.onLocalEchoUpdated);
+ cli.on("Room.accountData", this.onAccountData);
+ cli.on("Event.decrypted", this.onEventDecrypted);
+ cli.on("Event.replaced", this.onEventReplaced);
+ cli.on("sync", this.onSync);
}
// TODO: [REACT-WARNING] Move into constructor
@@ -246,7 +287,7 @@ class TimelinePanel extends React.Component {
this.updateReadMarkerOnUserActivity();
}
- this._initTimeline(this.props);
+ this.initTimeline(this.props);
}
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
@@ -272,7 +313,7 @@ class TimelinePanel extends React.Component {
if (differentEventId || differentHighlightedEventId) {
console.log("TimelinePanel switching to eventId " + newProps.eventId +
" (was " + this.props.eventId + ")");
- return this._initTimeline(newProps);
+ return this.initTimeline(newProps);
}
}
@@ -282,13 +323,13 @@ class TimelinePanel extends React.Component {
//
// (We could use isMounted, but facebook have deprecated that.)
this.unmounted = true;
- if (this._readReceiptActivityTimer) {
- this._readReceiptActivityTimer.abort();
- this._readReceiptActivityTimer = null;
+ if (this.readReceiptActivityTimer) {
+ this.readReceiptActivityTimer.abort();
+ this.readReceiptActivityTimer = null;
}
- if (this._readMarkerActivityTimer) {
- this._readMarkerActivityTimer.abort();
- this._readMarkerActivityTimer = null;
+ if (this.readMarkerActivityTimer) {
+ this.readMarkerActivityTimer.abort();
+ this.readMarkerActivityTimer = null;
}
dis.unregister(this.dispatcherRef);
@@ -308,7 +349,7 @@ class TimelinePanel extends React.Component {
}
}
- onMessageListUnfillRequest = (backwards, scrollToken) => {
+ private onMessageListUnfillRequest = (backwards: boolean, scrollToken: string): void => {
// If backwards, unpaginate from the back (i.e. the start of the timeline)
const dir = backwards ? EventTimeline.BACKWARDS : EventTimeline.FORWARDS;
debuglog("TimelinePanel: unpaginating events in direction", dir);
@@ -327,21 +368,30 @@ class TimelinePanel extends React.Component {
if (count > 0) {
debuglog("TimelinePanel: Unpaginating", count, "in direction", dir);
- this._timelineWindow.unpaginate(count, backwards);
+ this.timelineWindow.unpaginate(count, backwards);
- // We can now paginate in the unpaginated direction
- const canPaginateKey = (backwards) ? 'canBackPaginate' : 'canForwardPaginate';
- const { events, liveEvents, firstVisibleEventIndex } = this._getEvents();
- this.setState({
- [canPaginateKey]: true,
+ const { events, liveEvents, firstVisibleEventIndex } = this.getEvents();
+ const newState: Partial = {
events,
liveEvents,
firstVisibleEventIndex,
- });
+ }
+
+ // We can now paginate in the unpaginated direction
+ if (backwards) {
+ newState.canBackPaginate = true;
+ } else {
+ newState.canForwardPaginate = true;
+ }
+ this.setState(newState);
}
};
- onPaginationRequest = (timelineWindow, direction, size) => {
+ private onPaginationRequest = (
+ timelineWindow: TimelineWindow,
+ direction: string,
+ size: number,
+ ): Promise => {
if (this.props.onPaginationRequest) {
return this.props.onPaginationRequest(timelineWindow, direction, size);
} else {
@@ -350,8 +400,8 @@ class TimelinePanel extends React.Component {
};
// set off a pagination request.
- onMessageListFillRequest = backwards => {
- if (!this._shouldPaginate()) return Promise.resolve(false);
+ private onMessageListFillRequest = (backwards: boolean): Promise => {
+ if (!this.shouldPaginate()) return Promise.resolve(false);
const dir = backwards ? EventTimeline.BACKWARDS : EventTimeline.FORWARDS;
const canPaginateKey = backwards ? 'canBackPaginate' : 'canForwardPaginate';
@@ -362,9 +412,9 @@ class TimelinePanel extends React.Component {
return Promise.resolve(false);
}
- if (!this._timelineWindow.canPaginate(dir)) {
+ if (!this.timelineWindow.canPaginate(dir)) {
debuglog("TimelinePanel: can't", dir, "paginate any further");
- this.setState({[canPaginateKey]: false});
+ this.setState({ [canPaginateKey]: false });
return Promise.resolve(false);
}
@@ -374,15 +424,15 @@ class TimelinePanel extends React.Component {
}
debuglog("TimelinePanel: Initiating paginate; backwards:"+backwards);
- this.setState({[paginatingKey]: true});
+ this.setState({ [paginatingKey]: true });
- return this.onPaginationRequest(this._timelineWindow, dir, PAGINATE_SIZE).then((r) => {
+ return this.onPaginationRequest(this.timelineWindow, dir, PAGINATE_SIZE).then((r) => {
if (this.unmounted) { return; }
debuglog("TimelinePanel: paginate complete backwards:"+backwards+"; success:"+r);
- const { events, liveEvents, firstVisibleEventIndex } = this._getEvents();
- const newState = {
+ const { events, liveEvents, firstVisibleEventIndex } = this.getEvents();
+ const newState: Partial = {
[paginatingKey]: false,
[canPaginateKey]: r,
events,
@@ -395,7 +445,7 @@ class TimelinePanel extends React.Component {
const otherDirection = backwards ? EventTimeline.FORWARDS : EventTimeline.BACKWARDS;
const canPaginateOtherWayKey = backwards ? 'canForwardPaginate' : 'canBackPaginate';
if (!this.state[canPaginateOtherWayKey] &&
- this._timelineWindow.canPaginate(otherDirection)) {
+ this.timelineWindow.canPaginate(otherDirection)) {
debuglog('TimelinePanel: can now', otherDirection, 'paginate again');
newState[canPaginateOtherWayKey] = true;
}
@@ -406,9 +456,9 @@ class TimelinePanel extends React.Component {
// has in memory because we never gave the component a chance to scroll
// itself into the right place
return new Promise((resolve) => {
- this.setState(newState, () => {
+ this.setState(newState, () => {
// we can continue paginating in the given direction if:
- // - _timelineWindow.paginate says we can
+ // - timelineWindow.paginate says we can
// - we're paginating forwards, or we won't be trying to
// paginate backwards past the first visible event
resolve(r && (!backwards || firstVisibleEventIndex === 0));
@@ -417,7 +467,7 @@ class TimelinePanel extends React.Component {
});
};
- onMessageListScroll = e => {
+ private onMessageListScroll = e => {
if (this.props.onScroll) {
this.props.onScroll(e);
}
@@ -428,18 +478,18 @@ class TimelinePanel extends React.Component {
// it goes back off the top of the screen (presumably because the user
// clicks on the 'jump to bottom' button), we need to re-enable it.
if (rmPosition < 0) {
- this.setState({readMarkerVisible: true});
+ this.setState({ readMarkerVisible: true });
}
// if read marker position goes between 0 and -1/1,
// (and user is active), switch timeout
- const timeout = this._readMarkerTimeout(rmPosition);
+ const timeout = this.readMarkerTimeout(rmPosition);
// NO-OP when timeout already has set to the given value
- this._readMarkerActivityTimer.changeTimeout(timeout);
+ this.readMarkerActivityTimer.changeTimeout(timeout);
}
};
- onAction = payload => {
+ private onAction = (payload: ActionPayload): void => {
switch (payload.action) {
case "ignore_state_changed":
this.forceUpdate();
@@ -447,9 +497,9 @@ class TimelinePanel extends React.Component {
case "edit_event": {
const editState = payload.event ? new EditorStateTransfer(payload.event) : null;
- this.setState({editState}, () => {
- if (payload.event && this._messagePanel.current) {
- this._messagePanel.current.scrollToEventIfNeeded(
+ this.setState({ editState }, () => {
+ if (payload.event && this.messagePanel.current) {
+ this.messagePanel.current.scrollToEventIfNeeded(
payload.event.getId(),
);
}
@@ -479,7 +529,16 @@ class TimelinePanel extends React.Component {
}
};
- onRoomTimeline = (ev, room, toStartOfTimeline, removed, data) => {
+ private onRoomTimeline = (
+ ev: MatrixEvent,
+ room: Room,
+ toStartOfTimeline: boolean,
+ removed: boolean,
+ data: {
+ timeline: EventTimeline;
+ liveEvent?: boolean;
+ },
+ ): void => {
// ignore events for other timeline sets
if (data.timeline.getTimelineSet() !== this.props.timelineSet) return;
@@ -487,9 +546,9 @@ class TimelinePanel extends React.Component {
// updates from pagination will happen when the paginate completes.
if (toStartOfTimeline || !data || !data.liveEvent) return;
- if (!this._messagePanel.current) return;
+ if (!this.messagePanel.current) return;
- if (!this._messagePanel.current.getScrollState().stuckAtBottom) {
+ if (!this.messagePanel.current.getScrollState().stuckAtBottom) {
// we won't load this event now, because we don't want to push any
// events off the other end of the timeline. But we need to note
// that we can now paginate.
@@ -506,13 +565,13 @@ class TimelinePanel extends React.Component {
// timeline window.
//
// see https://github.com/vector-im/vector-web/issues/1035
- this._timelineWindow.paginate(EventTimeline.FORWARDS, 1, false).then(() => {
+ this.timelineWindow.paginate(EventTimeline.FORWARDS, 1, false).then(() => {
if (this.unmounted) { return; }
- const { events, liveEvents, firstVisibleEventIndex } = this._getEvents();
+ const { events, liveEvents, firstVisibleEventIndex } = this.getEvents();
const lastLiveEvent = liveEvents[liveEvents.length - 1];
- const updatedState = {
+ const updatedState: Partial = {
events,
liveEvents,
firstVisibleEventIndex,
@@ -537,15 +596,15 @@ class TimelinePanel extends React.Component {
// we know we're stuckAtBottom, so we can advance the RM
// immediately, to save a later render cycle
- this._setReadMarker(lastLiveEvent.getId(), lastLiveEvent.getTs(), true);
+ this.setReadMarker(lastLiveEvent.getId(), lastLiveEvent.getTs(), true);
updatedState.readMarkerVisible = false;
updatedState.readMarkerEventId = lastLiveEvent.getId();
callRMUpdated = true;
}
}
- this.setState(updatedState, () => {
- this._messagePanel.current.updateTimelineMinHeight();
+ this.setState(updatedState, () => {
+ this.messagePanel.current.updateTimelineMinHeight();
if (callRMUpdated) {
this.props.onReadMarkerUpdated();
}
@@ -553,17 +612,17 @@ class TimelinePanel extends React.Component {
});
};
- onRoomTimelineReset = (room, timelineSet) => {
+ private onRoomTimelineReset = (room: Room, timelineSet: TimelineSet): void => {
if (timelineSet !== this.props.timelineSet) return;
- if (this._messagePanel.current && this._messagePanel.current.isAtBottom()) {
- this._loadTimeline();
+ if (this.messagePanel.current && this.messagePanel.current.isAtBottom()) {
+ this.loadTimeline();
}
};
- canResetTimeline = () => this._messagePanel.current && this._messagePanel.current.isAtBottom();
+ public canResetTimeline = () => this.messagePanel?.current.isAtBottom();
- onRoomRedaction = (ev, room) => {
+ private onRoomRedaction = (ev: MatrixEvent, room: Room): void => {
if (this.unmounted) return;
// ignore events for other rooms
@@ -574,7 +633,7 @@ class TimelinePanel extends React.Component {
this.forceUpdate();
};
- onEventReplaced = (replacedEvent, room) => {
+ private onEventReplaced = (replacedEvent: MatrixEvent, room: Room): void => {
if (this.unmounted) return;
// ignore events for other rooms
@@ -585,7 +644,7 @@ class TimelinePanel extends React.Component {
this.forceUpdate();
};
- onRoomReceipt = (ev, room) => {
+ private onRoomReceipt = (ev: MatrixEvent, room: Room): void => {
if (this.unmounted) return;
// ignore events for other rooms
@@ -594,22 +653,22 @@ class TimelinePanel extends React.Component {
this.forceUpdate();
};
- onLocalEchoUpdated = (ev, room, oldEventId) => {
+ private onLocalEchoUpdated = (ev: MatrixEvent, room: Room, oldEventId: string): void => {
if (this.unmounted) return;
// ignore events for other rooms
if (room !== this.props.timelineSet.room) return;
- this._reloadEvents();
+ this.reloadEvents();
};
- onAccountData = (ev, room) => {
+ private onAccountData = (ev: MatrixEvent, room: Room): void => {
if (this.unmounted) return;
// ignore events for other rooms
if (room !== this.props.timelineSet.room) return;
- if (ev.getType() !== "m.fully_read") return;
+ if (ev.getType() !== EventType.FullyRead) return;
// XXX: roomReadMarkerTsMap not updated here so it is now inconsistent. Replace
// this mechanism of determining where the RM is relative to the view-port with
@@ -619,7 +678,7 @@ class TimelinePanel extends React.Component {
}, this.props.onReadMarkerUpdated);
};
- onEventDecrypted = ev => {
+ private onEventDecrypted = (ev: MatrixEvent): void => {
// Can be null for the notification timeline, etc.
if (!this.props.timelineSet.room) return;
@@ -634,46 +693,46 @@ class TimelinePanel extends React.Component {
}
};
- onSync = (state, prevState, data) => {
- this.setState({clientSyncState: state});
+ private onSync = (clientSyncState: SyncState, prevState: SyncState, data: object): void => {
+ this.setState({ clientSyncState });
};
- _readMarkerTimeout(readMarkerPosition) {
+ private readMarkerTimeout(readMarkerPosition: number): number {
return readMarkerPosition === 0 ?
this.state.readMarkerInViewThresholdMs :
this.state.readMarkerOutOfViewThresholdMs;
}
- async updateReadMarkerOnUserActivity() {
- const initialTimeout = this._readMarkerTimeout(this.getReadMarkerPosition());
- this._readMarkerActivityTimer = new Timer(initialTimeout);
+ private async updateReadMarkerOnUserActivity(): Promise {
+ const initialTimeout = this.readMarkerTimeout(this.getReadMarkerPosition());
+ this.readMarkerActivityTimer = new Timer(initialTimeout);
- while (this._readMarkerActivityTimer) { //unset on unmount
- UserActivity.sharedInstance().timeWhileActiveRecently(this._readMarkerActivityTimer);
+ while (this.readMarkerActivityTimer) { //unset on unmount
+ UserActivity.sharedInstance().timeWhileActiveRecently(this.readMarkerActivityTimer);
try {
- await this._readMarkerActivityTimer.finished();
+ await this.readMarkerActivityTimer.finished();
} catch (e) { continue; /* aborted */ }
// outside of try/catch to not swallow errors
this.updateReadMarker();
}
}
- async updateReadReceiptOnUserActivity() {
- this._readReceiptActivityTimer = new Timer(READ_RECEIPT_INTERVAL_MS);
- while (this._readReceiptActivityTimer) { //unset on unmount
- UserActivity.sharedInstance().timeWhileActiveNow(this._readReceiptActivityTimer);
+ private async updateReadReceiptOnUserActivity(): Promise {
+ this.readReceiptActivityTimer = new Timer(READ_RECEIPT_INTERVAL_MS);
+ while (this.readReceiptActivityTimer) { //unset on unmount
+ UserActivity.sharedInstance().timeWhileActiveNow(this.readReceiptActivityTimer);
try {
- await this._readReceiptActivityTimer.finished();
+ await this.readReceiptActivityTimer.finished();
} catch (e) { continue; /* aborted */ }
// outside of try/catch to not swallow errors
this.sendReadReceipt();
}
}
- sendReadReceipt = () => {
+ private sendReadReceipt = (): void => {
if (SettingsStore.getValue("lowBandwidth")) return;
- if (!this._messagePanel.current) return;
+ if (!this.messagePanel.current) return;
if (!this.props.manageReadReceipts) return;
// This happens on user_activity_end which is delayed, and it's
// very possible have logged out within that timeframe, so check
@@ -684,8 +743,8 @@ class TimelinePanel extends React.Component {
let shouldSendRR = true;
- const currentRREventId = this._getCurrentReadReceipt(true);
- const currentRREventIndex = this._indexForEventId(currentRREventId);
+ const currentRREventId = this.getCurrentReadReceipt(true);
+ const currentRREventIndex = this.indexForEventId(currentRREventId);
// We want to avoid sending out read receipts when we are looking at
// events in the past which are before the latest RR.
//
@@ -700,11 +759,11 @@ class TimelinePanel extends React.Component {
// the user eventually hits the live timeline.
//
if (currentRREventId && currentRREventIndex === null &&
- this._timelineWindow.canPaginate(EventTimeline.FORWARDS)) {
+ this.timelineWindow.canPaginate(EventTimeline.FORWARDS)) {
shouldSendRR = false;
}
- const lastReadEventIndex = this._getLastDisplayedEventIndex({
+ const lastReadEventIndex = this.getLastDisplayedEventIndex({
ignoreOwn: true,
});
if (lastReadEventIndex === null) {
@@ -778,7 +837,7 @@ class TimelinePanel extends React.Component {
// if the read marker is on the screen, we can now assume we've caught up to the end
// of the screen, so move the marker down to the bottom of the screen.
- updateReadMarker = () => {
+ private updateReadMarker = (): void => {
if (!this.props.manageReadMarkers) return;
if (this.getReadMarkerPosition() === 1) {
// the read marker is at an event below the viewport,
@@ -788,7 +847,7 @@ class TimelinePanel extends React.Component {
// move the RM to *after* the message at the bottom of the screen. This
// avoids a problem whereby we never advance the RM if there is a huge
// message which doesn't fit on the screen.
- const lastDisplayedIndex = this._getLastDisplayedEventIndex({
+ const lastDisplayedIndex = this.getLastDisplayedEventIndex({
allowPartial: true,
});
@@ -796,7 +855,7 @@ class TimelinePanel extends React.Component {
return;
}
const lastDisplayedEvent = this.state.events[lastDisplayedIndex];
- this._setReadMarker(
+ this.setReadMarker(
lastDisplayedEvent.getId(),
lastDisplayedEvent.getTs(),
);
@@ -815,13 +874,13 @@ class TimelinePanel extends React.Component {
// advance the read marker past any events we sent ourselves.
- _advanceReadMarkerPastMyEvents() {
+ private advanceReadMarkerPastMyEvents(): void {
if (!this.props.manageReadMarkers) return;
- // we call `_timelineWindow.getEvents()` rather than using
+ // we call `timelineWindow.getEvents()` rather than using
// `this.state.liveEvents`, because React batches the update to the
// latter, so it may not have been updated yet.
- const events = this._timelineWindow.getEvents();
+ const events = this.timelineWindow.getEvents();
// first find where the current RM is
let i;
@@ -846,22 +905,22 @@ class TimelinePanel extends React.Component {
i--;
const ev = events[i];
- this._setReadMarker(ev.getId(), ev.getTs());
+ this.setReadMarker(ev.getId(), ev.getTs());
}
/* jump down to the bottom of this room, where new events are arriving
*/
- jumpToLiveTimeline = () => {
+ public jumpToLiveTimeline = (): void => {
// if we can't forward-paginate the existing timeline, then there
// is no point reloading it - just jump straight to the bottom.
//
// Otherwise, reload the timeline rather than trying to paginate
// through all of space-time.
- if (this._timelineWindow.canPaginate(EventTimeline.FORWARDS)) {
- this._loadTimeline();
+ if (this.timelineWindow.canPaginate(EventTimeline.FORWARDS)) {
+ this.loadTimeline();
} else {
- if (this._messagePanel.current) {
- this._messagePanel.current.scrollToBottom();
+ if (this.messagePanel.current) {
+ this.messagePanel.current.scrollToBottom();
}
}
};
@@ -869,22 +928,22 @@ class TimelinePanel extends React.Component {
/* scroll to show the read-up-to marker. We put it 1/3 of the way down
* the container.
*/
- jumpToReadMarker = () => {
+ public jumpToReadMarker = (): void => {
if (!this.props.manageReadMarkers) return;
- if (!this._messagePanel.current) return;
+ if (!this.messagePanel.current) return;
if (!this.state.readMarkerEventId) return;
// we may not have loaded the event corresponding to the read-marker
- // into the _timelineWindow. In that case, attempts to scroll to it
+ // into the timelineWindow. In that case, attempts to scroll to it
// will fail.
//
// a quick way to figure out if we've loaded the relevant event is
// simply to check if the messagepanel knows where the read-marker is.
- const ret = this._messagePanel.current.getReadMarkerPosition();
+ const ret = this.messagePanel.current.getReadMarkerPosition();
if (ret !== null) {
// The messagepanel knows where the RM is, so we must have loaded
// the relevant event.
- this._messagePanel.current.scrollToEvent(this.state.readMarkerEventId,
+ this.messagePanel.current.scrollToEvent(this.state.readMarkerEventId,
0, 1/3);
return;
}
@@ -892,15 +951,15 @@ class TimelinePanel extends React.Component {
// Looks like we haven't loaded the event corresponding to the read-marker.
// As with jumpToLiveTimeline, we want to reload the timeline around the
// read-marker.
- this._loadTimeline(this.state.readMarkerEventId, 0, 1/3);
+ this.loadTimeline(this.state.readMarkerEventId, 0, 1/3);
};
/* update the read-up-to marker to match the read receipt
*/
- forgetReadMarker = () => {
+ public forgetReadMarker = (): void => {
if (!this.props.manageReadMarkers) return;
- const rmId = this._getCurrentReadReceipt();
+ const rmId = this.getCurrentReadReceipt();
// see if we know the timestamp for the rr event
const tl = this.props.timelineSet.getTimelineForEvent(rmId);
@@ -912,17 +971,17 @@ class TimelinePanel extends React.Component {
}
}
- this._setReadMarker(rmId, rmTs);
+ this.setReadMarker(rmId, rmTs);
};
/* return true if the content is fully scrolled down and we are
* at the end of the live timeline.
*/
- isAtEndOfLiveTimeline = () => {
- return this._messagePanel.current
- && this._messagePanel.current.isAtBottom()
- && this._timelineWindow
- && !this._timelineWindow.canPaginate(EventTimeline.FORWARDS);
+ public isAtEndOfLiveTimeline = (): boolean => {
+ return this.messagePanel.current
+ && this.messagePanel.current.isAtBottom()
+ && this.timelineWindow
+ && !this.timelineWindow.canPaginate(EventTimeline.FORWARDS);
}
@@ -931,9 +990,9 @@ class TimelinePanel extends React.Component {
*
* returns null if we are not mounted.
*/
- getScrollState = () => {
- if (!this._messagePanel.current) { return null; }
- return this._messagePanel.current.getScrollState();
+ public getScrollState = (): IScrollState => {
+ if (!this.messagePanel.current) { return null; }
+ return this.messagePanel.current.getScrollState();
};
// returns one of:
@@ -942,11 +1001,11 @@ class TimelinePanel extends React.Component {
// -1: read marker is above the window
// 0: read marker is visible
// +1: read marker is below the window
- getReadMarkerPosition = () => {
+ public getReadMarkerPosition = (): number => {
if (!this.props.manageReadMarkers) return null;
- if (!this._messagePanel.current) return null;
+ if (!this.messagePanel.current) return null;
- const ret = this._messagePanel.current.getReadMarkerPosition();
+ const ret = this.messagePanel.current.getReadMarkerPosition();
if (ret !== null) {
return ret;
}
@@ -965,7 +1024,7 @@ class TimelinePanel extends React.Component {
return null;
};
- canJumpToReadMarker = () => {
+ public canJumpToReadMarker = (): boolean => {
// 1. Do not show jump bar if neither the RM nor the RR are set.
// 3. We want to show the bar if the read-marker is off the top of the screen.
// 4. Also, if pos === null, the event might not be paginated - show the unread bar
@@ -980,19 +1039,19 @@ class TimelinePanel extends React.Component {
*
* We pass it down to the scroll panel.
*/
- handleScrollKey = ev => {
- if (!this._messagePanel.current) { return; }
+ public handleScrollKey = ev => {
+ if (!this.messagePanel.current) { return; }
// jump to the live timeline on ctrl-end, rather than the end of the
// timeline window.
if (ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey && ev.key === Key.END) {
this.jumpToLiveTimeline();
} else {
- this._messagePanel.current.handleScrollKey(ev);
+ this.messagePanel.current.handleScrollKey(ev);
}
};
- _initTimeline(props) {
+ private initTimeline(props: IProps): void {
const initialEvent = props.eventId;
const pixelOffset = props.eventPixelOffset;
@@ -1003,7 +1062,7 @@ class TimelinePanel extends React.Component {
offsetBase = 0.5;
}
- return this._loadTimeline(initialEvent, pixelOffset, offsetBase);
+ return this.loadTimeline(initialEvent, pixelOffset, offsetBase);
}
/**
@@ -1019,34 +1078,32 @@ class TimelinePanel extends React.Component {
* @param {number?} offsetBase the reference point for the pixelOffset. 0
* means the top of the container, 1 means the bottom, and fractional
* values mean somewhere in the middle. If omitted, it defaults to 0.
- *
- * returns a promise which will resolve when the load completes.
*/
- _loadTimeline(eventId, pixelOffset, offsetBase) {
- this._timelineWindow = new TimelineWindow(
+ private loadTimeline(eventId?: string, pixelOffset?: number, offsetBase?: number): void {
+ this.timelineWindow = new TimelineWindow(
MatrixClientPeg.get(), this.props.timelineSet,
{windowLimit: this.props.timelineCap});
const onLoaded = () => {
// clear the timeline min-height when
// (re)loading the timeline
- if (this._messagePanel.current) {
- this._messagePanel.current.onTimelineReset();
+ if (this.messagePanel.current) {
+ this.messagePanel.current.onTimelineReset();
}
- this._reloadEvents();
+ this.reloadEvents();
// If we switched away from the room while there were pending
// outgoing events, the read-marker will be before those events.
// We need to skip over any which have subsequently been sent.
- this._advanceReadMarkerPastMyEvents();
+ this.advanceReadMarkerPastMyEvents();
this.setState({
- canBackPaginate: this._timelineWindow.canPaginate(EventTimeline.BACKWARDS),
- canForwardPaginate: this._timelineWindow.canPaginate(EventTimeline.FORWARDS),
+ canBackPaginate: this.timelineWindow.canPaginate(EventTimeline.BACKWARDS),
+ canForwardPaginate: this.timelineWindow.canPaginate(EventTimeline.FORWARDS),
timelineLoading: false,
}, () => {
// initialise the scroll state of the message panel
- if (!this._messagePanel.current) {
+ if (!this.messagePanel.current) {
// this shouldn't happen - we know we're mounted because
// we're in a setState callback, and we know
// timelineLoading is now false, so render() should have
@@ -1056,10 +1113,10 @@ class TimelinePanel extends React.Component {
return;
}
if (eventId) {
- this._messagePanel.current.scrollToEvent(eventId, pixelOffset,
+ this.messagePanel.current.scrollToEvent(eventId, pixelOffset,
offsetBase);
} else {
- this._messagePanel.current.scrollToBottom();
+ this.messagePanel.current.scrollToBottom();
}
if (this.props.sendReadReceiptOnLoad) {
@@ -1121,10 +1178,10 @@ class TimelinePanel extends React.Component {
if (timeline) {
// This is a hot-path optimization by skipping a promise tick
// by repeating a no-op sync branch in TimelineSet.getTimelineForEvent & MatrixClient.getEventTimeline
- this._timelineWindow.load(eventId, INITIAL_SIZE); // in this branch this method will happen in sync time
+ this.timelineWindow.load(eventId, INITIAL_SIZE); // in this branch this method will happen in sync time
onLoaded();
} else {
- const prom = this._timelineWindow.load(eventId, INITIAL_SIZE);
+ const prom = this.timelineWindow.load(eventId, INITIAL_SIZE);
this.setState({
events: [],
liveEvents: [],
@@ -1139,17 +1196,17 @@ class TimelinePanel extends React.Component {
// handle the completion of a timeline load or localEchoUpdate, by
// reloading the events from the timelinewindow and pending event list into
// the state.
- _reloadEvents() {
+ private reloadEvents(): void {
// we might have switched rooms since the load started - just bin
// the results if so.
if (this.unmounted) return;
- this.setState(this._getEvents());
+ this.setState(this.getEvents());
}
// get the list of events from the timeline window and the pending event list
- _getEvents() {
- const events = this._timelineWindow.getEvents();
+ private getEvents(): Pick {
+ const events: MatrixEvent[] = this.timelineWindow.getEvents();
// `arrayFastClone` performs a shallow copy of the array
// we want the last event to be decrypted first but displayed last
@@ -1161,14 +1218,14 @@ class TimelinePanel extends React.Component {
client.decryptEventIfNeeded(event);
});
- const firstVisibleEventIndex = this._checkForPreJoinUISI(events);
+ const firstVisibleEventIndex = this.checkForPreJoinUISI(events);
// Hold onto the live events separately. The read receipt and read marker
// should use this list, so that they don't advance into pending events.
const liveEvents = [...events];
// if we're at the end of the live timeline, append the pending events
- if (!this._timelineWindow.canPaginate(EventTimeline.FORWARDS)) {
+ if (!this.timelineWindow.canPaginate(EventTimeline.FORWARDS)) {
events.push(...this.props.timelineSet.getPendingEvents());
}
@@ -1189,7 +1246,7 @@ class TimelinePanel extends React.Component {
* undecryptable event that was sent while the user was not in the room. If no
* such events were found, then it returns 0.
*/
- _checkForPreJoinUISI(events) {
+ private checkForPreJoinUISI(events: MatrixEvent[]): number {
const room = this.props.timelineSet.room;
if (events.length === 0 || !room ||
@@ -1253,7 +1310,7 @@ class TimelinePanel extends React.Component {
return 0;
}
- _indexForEventId(evId) {
+ private indexForEventId(evId: string): number | null {
for (let i = 0; i < this.state.events.length; ++i) {
if (evId == this.state.events[i].getId()) {
return i;
@@ -1262,15 +1319,14 @@ class TimelinePanel extends React.Component {
return null;
}
- _getLastDisplayedEventIndex(opts) {
- opts = opts || {};
+ private getLastDisplayedEventIndex(opts: IEventIndexOpts = {}): number | null {
const ignoreOwn = opts.ignoreOwn || false;
const allowPartial = opts.allowPartial || false;
- const messagePanel = this._messagePanel.current;
+ const messagePanel = this.messagePanel.current;
if (!messagePanel) return null;
- const messagePanelNode = ReactDOM.findDOMNode(messagePanel);
+ const messagePanelNode = ReactDOM.findDOMNode(messagePanel) as HTMLElement;
if (!messagePanelNode) return null; // sometimes this happens for fresh rooms/post-sync
const wrapperRect = messagePanelNode.getBoundingClientRect();
const myUserId = MatrixClientPeg.get().credentials.userId;
@@ -1348,7 +1404,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) {
@@ -1359,7 +1415,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
@@ -1384,7 +1440,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.
@@ -1395,12 +1451,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
@@ -1415,7 +1468,7 @@ class TimelinePanel extends React.Component {
if (this.state.timelineLoading) {
return (
-
+
);
}
@@ -1436,7 +1489,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.
@@ -1449,7 +1502,7 @@ class TimelinePanel extends React.Component {
: this.state.events;
return (
, "name" | "idName" | "url"> {
member: RoomMember;
diff --git a/src/components/views/avatars/RoomAvatar.tsx b/src/components/views/avatars/RoomAvatar.tsx
index 4693d907ba..bd820509c5 100644
--- a/src/components/views/avatars/RoomAvatar.tsx
+++ b/src/components/views/avatars/RoomAvatar.tsx
@@ -13,17 +13,17 @@ 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, {ComponentProps} from 'react';
-import Room from 'matrix-js-sdk/src/models/room';
+import React, { ComponentProps } from 'react';
+import { Room } from 'matrix-js-sdk/src/models/room';
+import { ResizeMethod } from 'matrix-js-sdk/src/@types/partials';
import BaseAvatar from './BaseAvatar';
import ImageView from '../elements/ImageView';
-import {MatrixClientPeg} from '../../../MatrixClientPeg';
+import { MatrixClientPeg } from '../../../MatrixClientPeg';
import Modal from '../../../Modal';
import * as Avatar from '../../../Avatar';
-import {ResizeMethod} from "../../../Avatar";
-import {replaceableComponent} from "../../../utils/replaceableComponent";
-import {mediaFromMxc} from "../../../customisations/Media";
+import { replaceableComponent } from "../../../utils/replaceableComponent";
+import { mediaFromMxc } from "../../../customisations/Media";
interface IProps extends Omit, "name" | "idName" | "url" | "onClick"> {
// Room may be left unset here, but if it is,
diff --git a/src/components/views/beta/BetaCard.tsx b/src/components/views/beta/BetaCard.tsx
index 821c448f4f..aa4fe49f63 100644
--- a/src/components/views/beta/BetaCard.tsx
+++ b/src/components/views/beta/BetaCard.tsx
@@ -25,6 +25,7 @@ import TextWithTooltip from "../elements/TextWithTooltip";
import Modal from "../../../Modal";
import BetaFeedbackDialog from "../dialogs/BetaFeedbackDialog";
import SdkConfig from "../../../SdkConfig";
+import SettingsFlag from "../elements/SettingsFlag";
interface IProps {
title?: string;
@@ -66,7 +67,7 @@ const BetaCard = ({ title: titleOverride, featureId }: IProps) => {
const info = SettingsStore.getBetaInfo(featureId);
if (!info) return null; // Beta is invalid/disabled
- const { title, caption, disclaimer, image, feedbackLabel, feedbackSubheading } = info;
+ const { title, caption, disclaimer, image, feedbackLabel, feedbackSubheading, extraSettings } = info;
const value = SettingsStore.getValue(featureId);
let feedbackButton;
@@ -82,26 +83,33 @@ const BetaCard = ({ title: titleOverride, featureId }: IProps) => {
}
return
) :
{ _t("No results") }
diff --git a/src/components/views/dialogs/InviteDialog.tsx b/src/components/views/dialogs/InviteDialog.tsx
index ffca9a88a7..553c1c544e 100644
--- a/src/components/views/dialogs/InviteDialog.tsx
+++ b/src/components/views/dialogs/InviteDialog.tsx
@@ -17,37 +17,45 @@ limitations under the License.
import React, { createRef } from 'react';
import classNames from 'classnames';
-import {_t, _td} from "../../../languageHandler";
+import { _t, _td } from "../../../languageHandler";
import * as sdk from "../../../index";
-import {MatrixClientPeg} from "../../../MatrixClientPeg";
-import {makeRoomPermalink, makeUserPermalink} from "../../../utils/permalinks/Permalinks";
+import { MatrixClientPeg } from "../../../MatrixClientPeg";
+import { makeRoomPermalink, makeUserPermalink } from "../../../utils/permalinks/Permalinks";
import DMRoomMap from "../../../utils/DMRoomMap";
-import {RoomMember} from "matrix-js-sdk/src/models/room-member";
+import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import SdkConfig from "../../../SdkConfig";
import * as Email from "../../../email";
-import {getDefaultIdentityServerUrl, useDefaultIdentityServer} from "../../../utils/IdentityServerUtils";
-import {abbreviateUrl} from "../../../utils/UrlUtils";
+import { getDefaultIdentityServerUrl, useDefaultIdentityServer } from "../../../utils/IdentityServerUtils";
+import { abbreviateUrl } from "../../../utils/UrlUtils";
import dis from "../../../dispatcher/dispatcher";
import IdentityAuthClient from "../../../IdentityAuthClient";
import Modal from "../../../Modal";
-import {humanizeTime} from "../../../utils/humanize";
+import { humanizeTime } from "../../../utils/humanize";
import createRoom, {
- canEncryptToAllUsers, ensureDMExists, findDMForUser, privateShouldBeEncrypted,
+ canEncryptToAllUsers,
+ ensureDMExists,
+ findDMForUser,
+ privateShouldBeEncrypted,
} from "../../../createRoom";
-import {inviteMultipleToRoom, showCommunityInviteDialog} from "../../../RoomInvite";
-import {Key} from "../../../Keyboard";
-import {Action} from "../../../dispatcher/actions";
-import {DefaultTagID} from "../../../stores/room-list/models";
+import {
+ IInviteResult,
+ inviteMultipleToRoom,
+ showAnyInviteErrors,
+ showCommunityInviteDialog,
+} from "../../../RoomInvite";
+import { Key } from "../../../Keyboard";
+import { Action } from "../../../dispatcher/actions";
+import { DefaultTagID } from "../../../stores/room-list/models";
import RoomListStore from "../../../stores/room-list/RoomListStore";
-import {CommunityPrototypeStore} from "../../../stores/CommunityPrototypeStore";
+import { CommunityPrototypeStore } from "../../../stores/CommunityPrototypeStore";
import SettingsStore from "../../../settings/SettingsStore";
-import {UIFeature} from "../../../settings/UIFeature";
+import { UIFeature } from "../../../settings/UIFeature";
import CountlyAnalytics from "../../../CountlyAnalytics";
-import {Room} from "matrix-js-sdk/src/models/room";
+import { Room } from "matrix-js-sdk/src/models/room";
import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
-import {replaceableComponent} from "../../../utils/replaceableComponent";
-import {mediaFromMxc} from "../../../customisations/Media";
-import {getAddressType} from "../../../UserAddress";
+import { replaceableComponent } from "../../../utils/replaceableComponent";
+import { mediaFromMxc } from "../../../customisations/Media";
+import { getAddressType } from "../../../UserAddress";
import BaseAvatar from '../avatars/BaseAvatar';
import AccessibleButton from '../elements/AccessibleButton';
import { compare } from '../../../utils/strings';
@@ -74,10 +82,10 @@ export const KIND_CALL_TRANSFER = "call_transfer";
const INITIAL_ROOMS_SHOWN = 3; // Number of rooms to show at first
const INCREMENT_ROOMS_SHOWN = 5; // Number of rooms to add when 'show more' is clicked
-// This is the interface that is expected by various components in this file. It is a bit
-// awkward because it also matches the RoomMember class from the js-sdk with some extra support
+// This is the interface that is expected by various components in the Invite Dialog and RoomInvite.
+// It is a bit awkward because it also matches the RoomMember class from the js-sdk with some extra support
// for 3PIDs/email addresses.
-abstract class Member {
+export abstract class Member {
/**
* The display name of this Member. For users this should be their profile's display
* name or user ID if none set. For 3PIDs this should be the 3PID address (email).
@@ -102,7 +110,8 @@ class DirectoryMember extends Member {
private readonly displayName: string;
private readonly avatarUrl: string;
- constructor(userDirResult: {user_id: string, display_name: string, avatar_url: string}) {
+ // eslint-disable-next-line camelcase
+ constructor(userDirResult: { user_id: string, display_name: string, avatar_url: string }) {
super();
this._userId = userDirResult.user_id;
this.displayName = userDirResult.display_name;
@@ -601,19 +610,10 @@ export default class InviteDialog extends React.PureComponent ({userId: m.member.userId, user: m.member}));
}
- private shouldAbortAfterInviteError(result): boolean {
- const failedUsers = Object.keys(result.states).filter(a => result.states[a] === 'error');
- if (failedUsers.length > 0) {
- console.log("Failed to invite users: ", result);
- this.setState({
- busy: false,
- errorText: _t("Failed to invite the following users to chat: %(csvUsers)s", {
- csvUsers: failedUsers.join(", "),
- }),
- });
- return true; // abort
- }
- return false;
+ private shouldAbortAfterInviteError(result: IInviteResult, room: Room): boolean {
+ this.setState({ busy: false });
+ const userMap = new Map(this.state.targets.map(member => [member.userId, member]));
+ return !showAnyInviteErrors(result.states, room, result.inviter, userMap);
}
private convertFilter(): Member[] {
@@ -731,7 +731,7 @@ export default class InviteDialog extends React.PureComponent = React.createRef();
state: IState = {
- disabledButtonIds: [],
+ disabledButtonIds: (this.props.widgetDefinition.buttons || []).filter(b => b.disabled)
+ .map(b => b.id),
};
constructor(props) {
diff --git a/src/components/views/dialogs/ReportEventDialog.js b/src/components/views/dialogs/ReportEventDialog.js
deleted file mode 100644
index 5454b97287..0000000000
--- a/src/components/views/dialogs/ReportEventDialog.js
+++ /dev/null
@@ -1,149 +0,0 @@
-/*
-Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
-
-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, {PureComponent} from 'react';
-import * as sdk from '../../../index';
-import { _t } from '../../../languageHandler';
-import PropTypes from "prop-types";
-import {MatrixEvent} from "matrix-js-sdk/src/models/event";
-import {MatrixClientPeg} from "../../../MatrixClientPeg";
-import SdkConfig from '../../../SdkConfig';
-import Markdown from '../../../Markdown';
-import {replaceableComponent} from "../../../utils/replaceableComponent";
-
-/*
- * A dialog for reporting an event.
- */
-@replaceableComponent("views.dialogs.ReportEventDialog")
-export default class ReportEventDialog extends PureComponent {
- static propTypes = {
- mxEvent: PropTypes.instanceOf(MatrixEvent).isRequired,
- onFinished: PropTypes.func.isRequired,
- };
-
- constructor(props) {
- super(props);
-
- this.state = {
- reason: "",
- busy: false,
- err: null,
- };
- }
-
- _onReasonChange = ({target: {value: reason}}) => {
- this.setState({ reason });
- };
-
- _onCancel = () => {
- this.props.onFinished(false);
- };
-
- _onSubmit = async () => {
- if (!this.state.reason || !this.state.reason.trim()) {
- this.setState({
- err: _t("Please fill why you're reporting."),
- });
- return;
- }
-
- this.setState({
- busy: true,
- err: null,
- });
-
- try {
- const ev = this.props.mxEvent;
- await MatrixClientPeg.get().reportEvent(ev.getRoomId(), ev.getId(), -100, this.state.reason.trim());
- this.props.onFinished(true);
- } catch (e) {
- this.setState({
- busy: false,
- err: e.message,
- });
- }
- };
-
- render() {
- const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
- const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
- const Loader = sdk.getComponent('elements.Spinner');
- const Field = sdk.getComponent('elements.Field');
-
- let error = null;
- if (this.state.err) {
- error =
- {this.state.err}
-
;
- }
-
- let progress = null;
- if (this.state.busy) {
- progress = (
-
- {
- _t("Reporting this message will send its unique 'event ID' to the administrator of " +
- "your homeserver. If messages in this room are encrypted, your homeserver " +
- "administrator will not be able to read the message text or view any files or images.")
- }
-
- {adminMessage}
-
- {progress}
- {error}
-
-
-
- );
- }
-}
diff --git a/src/components/views/dialogs/ReportEventDialog.tsx b/src/components/views/dialogs/ReportEventDialog.tsx
new file mode 100644
index 0000000000..8271239f7f
--- /dev/null
+++ b/src/components/views/dialogs/ReportEventDialog.tsx
@@ -0,0 +1,445 @@
+/*
+Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
+
+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 * as sdk from '../../../index';
+import { _t } from '../../../languageHandler';
+import { ensureDMExists } from "../../../createRoom";
+import { IDialogProps } from "./IDialogProps";
+import {MatrixEvent} from "matrix-js-sdk/src/models/event";
+import {MatrixClientPeg} from "../../../MatrixClientPeg";
+import SdkConfig from '../../../SdkConfig';
+import Markdown from '../../../Markdown';
+import {replaceableComponent} from "../../../utils/replaceableComponent";
+import SettingsStore from "../../../settings/SettingsStore";
+import StyledRadioButton from "../elements/StyledRadioButton";
+
+interface IProps extends IDialogProps {
+ mxEvent: MatrixEvent;
+}
+
+interface IState {
+ // A free-form text describing the abuse.
+ reason: string;
+ busy: boolean;
+ err?: string;
+ // If we know it, the nature of the abuse, as specified by MSC3215.
+ nature?: EXTENDED_NATURE;
+}
+
+
+const MODERATED_BY_STATE_EVENT_TYPE = [
+ "org.matrix.msc3215.room.moderation.moderated_by",
+ /**
+ * Unprefixed state event. Not ready for prime time.
+ *
+ * "m.room.moderation.moderated_by"
+ */
+];
+
+const ABUSE_EVENT_TYPE = "org.matrix.msc3215.abuse.report";
+
+// Standard abuse natures.
+enum NATURE {
+ DISAGREEMENT = "org.matrix.msc3215.abuse.nature.disagreement",
+ TOXIC = "org.matrix.msc3215.abuse.nature.toxic",
+ ILLEGAL = "org.matrix.msc3215.abuse.nature.illegal",
+ SPAM = "org.matrix.msc3215.abuse.nature.spam",
+ OTHER = "org.matrix.msc3215.abuse.nature.other",
+}
+
+enum NON_STANDARD_NATURE {
+ // Non-standard abuse nature.
+ // It should never leave the client - we use it to fallback to
+ // server-wide abuse reporting.
+ ADMIN = "non-standard.abuse.nature.admin"
+}
+
+type EXTENDED_NATURE = NATURE | NON_STANDARD_NATURE;
+
+type Moderation = {
+ // The id of the moderation room.
+ moderationRoomId: string;
+ // The id of the bot in charge of forwarding abuse reports to the moderation room.
+ moderationBotUserId: string;
+}
+/*
+ * A dialog for reporting an event.
+ *
+ * The actual content of the dialog will depend on two things:
+ *
+ * 1. Is `feature_report_to_moderators` enabled?
+ * 2. Does the room support moderation as per MSC3215, i.e. is there
+ * a well-formed state event `m.room.moderation.moderated_by`
+ * /`org.matrix.msc3215.room.moderation.moderated_by`?
+ */
+@replaceableComponent("views.dialogs.ReportEventDialog")
+export default class ReportEventDialog extends React.Component {
+ // If the room supports moderation, the moderation information.
+ private moderation?: Moderation;
+
+ constructor(props: IProps) {
+ super(props);
+
+ let moderatedByRoomId = null;
+ let moderatedByUserId = null;
+
+ if (SettingsStore.getValue("feature_report_to_moderators")) {
+ // The client supports reporting to moderators.
+ // Does the room support it, too?
+
+ // Extract state events to determine whether we should display
+ const client = MatrixClientPeg.get();
+ const room = client.getRoom(props.mxEvent.getRoomId());
+
+ for (const stateEventType of MODERATED_BY_STATE_EVENT_TYPE) {
+ const stateEvent = room.currentState.getStateEvents(stateEventType, stateEventType);
+ if (!stateEvent) {
+ continue;
+ }
+ if (Array.isArray(stateEvent)) {
+ // Internal error.
+ throw new TypeError(`getStateEvents(${stateEventType}, ${stateEventType}) ` +
+ "should return at most one state event");
+ }
+ const event = stateEvent.event;
+ if (!("content" in event) || typeof event["content"] != "object") {
+ // The room is improperly configured.
+ // Display this debug message for the sake of moderators.
+ console.debug("Moderation error", "state event", stateEventType,
+ "should have an object field `content`, got", event);
+ continue;
+ }
+ const content = event["content"];
+ if (!("room_id" in content) || typeof content["room_id"] != "string") {
+ // The room is improperly configured.
+ // Display this debug message for the sake of moderators.
+ console.debug("Moderation error", "state event", stateEventType,
+ "should have a string field `content.room_id`, got", event);
+ continue;
+ }
+ if (!("user_id" in content) || typeof content["user_id"] != "string") {
+ // The room is improperly configured.
+ // Display this debug message for the sake of moderators.
+ console.debug("Moderation error", "state event", stateEventType,
+ "should have a string field `content.user_id`, got", event);
+ continue;
+ }
+ moderatedByRoomId = content["room_id"];
+ moderatedByUserId = content["user_id"];
+ }
+
+ if (moderatedByRoomId && moderatedByUserId) {
+ // The room supports moderation.
+ this.moderation = {
+ moderationRoomId: moderatedByRoomId,
+ moderationBotUserId: moderatedByUserId,
+ };
+ }
+ }
+
+ this.state = {
+ // A free-form text describing the abuse.
+ reason: "",
+ busy: false,
+ err: null,
+ // If specified, the nature of the abuse, as specified by MSC3215.
+ nature: null,
+ };
+ }
+
+ // The user has written down a freeform description of the abuse.
+ private onReasonChange = ({target: {value: reason}}): void => {
+ this.setState({ reason });
+ };
+
+ // The user has clicked on a nature.
+ private onNatureChosen = (e: React.FormEvent): void => {
+ this.setState({ nature: e.currentTarget.value as EXTENDED_NATURE});
+ };
+
+ // The user has clicked "cancel".
+ private onCancel = (): void => {
+ this.props.onFinished(false);
+ };
+
+ // The user has clicked "submit".
+ private onSubmit = async () => {
+ let reason = this.state.reason || "";
+ reason = reason.trim();
+ if (this.moderation) {
+ // This room supports moderation.
+ // We need a nature.
+ // If the nature is `NATURE.OTHER` or `NON_STANDARD_NATURE.ADMIN`, we also need a `reason`.
+ if (!this.state.nature ||
+ ((this.state.nature == NATURE.OTHER || this.state.nature == NON_STANDARD_NATURE.ADMIN)
+ && !reason)
+ ) {
+ this.setState({
+ err: _t("Please fill why you're reporting."),
+ });
+ return;
+ }
+ } else {
+ // This room does not support moderation.
+ // We need a `reason`.
+ if (!reason) {
+ this.setState({
+ err: _t("Please fill why you're reporting."),
+ });
+ return;
+ }
+ }
+
+ this.setState({
+ busy: true,
+ err: null,
+ });
+
+ try {
+ const client = MatrixClientPeg.get();
+ const ev = this.props.mxEvent;
+ if (this.moderation && this.state.nature != NON_STANDARD_NATURE.ADMIN) {
+ const nature: NATURE = this.state.nature;
+
+ // Report to moderators through to the dedicated bot,
+ // as configured in the room's state events.
+ const dmRoomId = await ensureDMExists(client, this.moderation.moderationBotUserId);
+ await client.sendEvent(dmRoomId, ABUSE_EVENT_TYPE, {
+ event_id: ev.getId(),
+ room_id: ev.getRoomId(),
+ moderated_by_id: this.moderation.moderationRoomId,
+ nature,
+ reporter: client.getUserId(),
+ comment: this.state.reason.trim(),
+ });
+ } else {
+ // Report to homeserver admin through the dedicated Matrix API.
+ await client.reportEvent(ev.getRoomId(), ev.getId(), -100, this.state.reason.trim());
+ }
+ this.props.onFinished(true);
+ } catch (e) {
+ this.setState({
+ busy: false,
+ err: e.message,
+ });
+ }
+ };
+
+ render() {
+ const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
+ const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
+ const Loader = sdk.getComponent('elements.Spinner');
+ const Field = sdk.getComponent('elements.Field');
+
+ let error = null;
+ if (this.state.err) {
+ error =
+ {this.state.err}
+
;
+ }
+
+ let progress = null;
+ if (this.state.busy) {
+ progress = (
+
+
+
+ );
+ }
+
+ const adminMessageMD =
+ SdkConfig.get().reportEvent &&
+ SdkConfig.get().reportEvent.adminMessageMD;
+ let adminMessage;
+ if (adminMessageMD) {
+ const html = new Markdown(adminMessageMD).toHTML({ externalLinks: true });
+ adminMessage = ;
+ }
+
+ if (this.moderation) {
+ // Display report-to-moderator dialog.
+ // We let the user pick a nature.
+ const client = MatrixClientPeg.get();
+ const homeServerName = SdkConfig.get()["validated_server_config"].hsName;
+ let subtitle;
+ switch (this.state.nature) {
+ case NATURE.DISAGREEMENT:
+ subtitle = _t("What this user is writing is wrong.\n" +
+ "This will be reported to the room moderators.");
+ break;
+ case NATURE.TOXIC:
+ subtitle = _t("This user is displaying toxic behaviour, " +
+ "for instance by insulting other users or sharing " +
+ " adult-only content in a family-friendly room " +
+ " or otherwise violating the rules of this room.\n" +
+ "This will be reported to the room moderators.");
+ break;
+ case NATURE.ILLEGAL:
+ subtitle = _t("This user is displaying illegal behaviour, " +
+ "for instance by doxing people or threatening violence.\n" +
+ "This will be reported to the room moderators who may escalate this to legal authorities.");
+ break;
+ case NATURE.SPAM:
+ subtitle = _t("This user is spamming the room with ads, links to ads or to propaganda.\n" +
+ "This will be reported to the room moderators.");
+ break;
+ case NON_STANDARD_NATURE.ADMIN:
+ if (client.isRoomEncrypted(this.props.mxEvent.getRoomId())) {
+ subtitle = _t("This room is dedicated to illegal or toxic content " +
+ "or the moderators fail to moderate illegal or toxic content.\n" +
+ "This will be reported to the administrators of %(homeserver)s. " +
+ "The administrators will NOT be able to read the encrypted content of this room.",
+ { homeserver: homeServerName });
+ } else {
+ subtitle = _t("This room is dedicated to illegal or toxic content " +
+ "or the moderators fail to moderate illegal or toxic content.\n" +
+ " This will be reported to the administrators of %(homeserver)s.",
+ { homeserver: homeServerName });
+ }
+ break;
+ case NATURE.OTHER:
+ subtitle = _t("Any other reason. Please describe the problem.\n" +
+ "This will be reported to the room moderators.");
+ break;
+ default:
+ subtitle = _t("Please pick a nature and describe what makes this message abusive.");
+ break;
+ }
+
+ return (
+
+
+
+
+ );
+ }
+ // Report to homeserver admin.
+ // Currently, the API does not support natures.
+ return (
+
+
+
+ {
+ _t("Reporting this message will send its unique 'event ID' to the administrator of " +
+ "your homeserver. If messages in this room are encrypted, your homeserver " +
+ "administrator will not be able to read the message text or view any files " +
+ "or images.")
+ }
+
+ {adminMessage}
+
+ {progress}
+ {error}
+
+
+
+ );
+ }
+}
diff --git a/src/components/views/dialogs/RoomSettingsDialog.tsx b/src/components/views/dialogs/RoomSettingsDialog.tsx
index 1a664951c5..303f17c342 100644
--- a/src/components/views/dialogs/RoomSettingsDialog.tsx
+++ b/src/components/views/dialogs/RoomSettingsDialog.tsx
@@ -108,7 +108,10 @@ export default class RoomSettingsDialog extends React.Component {
ROOM_ADVANCED_TAB,
_td("Advanced"),
"mx_RoomSettingsDialog_warningIcon",
- ,
+ this.props.onFinished(true)}
+ />,
));
}
diff --git a/src/components/views/dialogs/SpaceSettingsDialog.tsx b/src/components/views/dialogs/SpaceSettingsDialog.tsx
index a135b6bc16..5e0cd96740 100644
--- a/src/components/views/dialogs/SpaceSettingsDialog.tsx
+++ b/src/components/views/dialogs/SpaceSettingsDialog.tsx
@@ -14,24 +14,27 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import React, {useState} from 'react';
-import {Room} from "matrix-js-sdk/src/models/room";
-import {MatrixClient} from "matrix-js-sdk/src/client";
-import {EventType} from "matrix-js-sdk/src/@types/event";
+import React, { useMemo } from 'react';
+import { Room } from "matrix-js-sdk/src/models/room";
+import { MatrixClient } from "matrix-js-sdk/src/client";
-import {_t} from '../../../languageHandler';
-import {IDialogProps} from "./IDialogProps";
+import { _t, _td } from '../../../languageHandler';
+import { IDialogProps } from "./IDialogProps";
import BaseDialog from "./BaseDialog";
-import DevtoolsDialog from "./DevtoolsDialog";
-import SpaceBasicSettings from '../spaces/SpaceBasicSettings';
-import {getTopic} from "../elements/RoomTopic";
-import {avatarUrlForRoom} from "../../../Avatar";
-import ToggleSwitch from "../elements/ToggleSwitch";
-import AccessibleButton from "../elements/AccessibleButton";
-import Modal from "../../../Modal";
import defaultDispatcher from "../../../dispatcher/dispatcher";
-import {useDispatcher} from "../../../hooks/useDispatcher";
-import {SpaceFeedbackPrompt} from "../../structures/SpaceRoomView";
+import { useDispatcher } from "../../../hooks/useDispatcher";
+import TabbedView, { Tab } from "../../structures/TabbedView";
+import SpaceSettingsGeneralTab from '../spaces/SpaceSettingsGeneralTab';
+import SpaceSettingsVisibilityTab from "../spaces/SpaceSettingsVisibilityTab";
+import SettingsStore from "../../../settings/SettingsStore";
+import { UIFeature } from "../../../settings/UIFeature";
+import AdvancedRoomSettingsTab from "../settings/tabs/room/AdvancedRoomSettingsTab";
+
+export enum SpaceSettingsTab {
+ General = "SPACE_GENERAL_TAB",
+ Visibility = "SPACE_VISIBILITY_TAB",
+ Advanced = "SPACE_ADVANCED_TAB",
+}
interface IProps extends IDialogProps {
matrixClient: MatrixClient;
@@ -45,63 +48,30 @@ const SpaceSettingsDialog: React.FC = ({ matrixClient: cli, space, onFin
}
});
- const [busy, setBusy] = useState(false);
- const [error, setError] = useState("");
-
- const userId = cli.getUserId();
-
- const [newAvatar, setNewAvatar] = useState(null); // undefined means to remove avatar
- const canSetAvatar = space.currentState.maySendStateEvent(EventType.RoomAvatar, userId);
- const avatarChanged = newAvatar !== null;
-
- const [name, setName] = useState(space.name);
- const canSetName = space.currentState.maySendStateEvent(EventType.RoomName, userId);
- const nameChanged = name !== space.name;
-
- const currentTopic = getTopic(space);
- const [topic, setTopic] = useState(currentTopic);
- const canSetTopic = space.currentState.maySendStateEvent(EventType.RoomTopic, userId);
- const topicChanged = topic !== currentTopic;
-
- const currentJoinRule = space.getJoinRule();
- const [joinRule, setJoinRule] = useState(currentJoinRule);
- const canSetJoinRule = space.currentState.maySendStateEvent(EventType.RoomJoinRules, userId);
- const joinRuleChanged = joinRule !== currentJoinRule;
-
- const onSave = async () => {
- setBusy(true);
- const promises = [];
-
- if (avatarChanged) {
- if (newAvatar) {
- promises.push(cli.sendStateEvent(space.roomId, EventType.RoomAvatar, {
- url: await cli.uploadContent(newAvatar),
- }, ""));
- } else {
- promises.push(cli.sendStateEvent(space.roomId, EventType.RoomAvatar, {}, ""));
- }
- }
-
- if (nameChanged) {
- promises.push(cli.setRoomName(space.roomId, name));
- }
-
- if (topicChanged) {
- promises.push(cli.setRoomTopic(space.roomId, topic));
- }
-
- if (joinRuleChanged) {
- promises.push(cli.sendStateEvent(space.roomId, EventType.RoomJoinRules, { join_rule: joinRule }, ""));
- }
-
- const results = await Promise.allSettled(promises);
- setBusy(false);
- const failures = results.filter(r => r.status === "rejected");
- if (failures.length > 0) {
- console.error("Failed to save space settings: ", failures);
- setError(_t("Failed to save space settings."));
- }
- };
+ const tabs = useMemo(() => {
+ return [
+ new Tab(
+ SpaceSettingsTab.General,
+ _td("General"),
+ "mx_SpaceSettingsDialog_generalIcon",
+ ,
+ ),
+ new Tab(
+ SpaceSettingsTab.Visibility,
+ _td("Visibility"),
+ "mx_SpaceSettingsDialog_visibilityIcon",
+ ,
+ ),
+ SettingsStore.getValue(UIFeature.AdvancedSettings)
+ ? new Tab(
+ SpaceSettingsTab.Advanced,
+ _td("Advanced"),
+ "mx_RoomSettingsDialog_warningIcon",
+ ,
+ )
+ : null,
+ ].filter(Boolean);
+ }, [cli, space, onFinished]);
return = ({ matrixClient: cli, space, onFin
onFinished={onFinished}
fixedWidth={false}
>
-
-
{ _t("Edit settings relating to your space.") }
-
- { error &&
{ error }
}
-
- onFinished(false)} />
-
-
-
-
- { _t("Make this space private") }
- setJoinRule(checked ? "invite" : "public")}
- disabled={!canSetJoinRule}
- aria-label={_t("Make this space private")}
- />
-
);
+ );
}
}
diff --git a/src/components/views/elements/ErrorBoundary.js b/src/components/views/elements/ErrorBoundary.tsx
similarity index 80%
rename from src/components/views/elements/ErrorBoundary.js
rename to src/components/views/elements/ErrorBoundary.tsx
index 9037287f49..f967b8c594 100644
--- a/src/components/views/elements/ErrorBoundary.js
+++ b/src/components/views/elements/ErrorBoundary.tsx
@@ -1,5 +1,5 @@
/*
-Copyright 2019 The Matrix.org Foundation C.I.C.
+Copyright 2019 - 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -14,21 +14,27 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import React from 'react';
-import * as sdk from '../../../index';
+import React, { ErrorInfo } from 'react';
+
import { _t } from '../../../languageHandler';
-import {MatrixClientPeg} from '../../../MatrixClientPeg';
+import { MatrixClientPeg } from '../../../MatrixClientPeg';
import PlatformPeg from '../../../PlatformPeg';
import Modal from '../../../Modal';
import SdkConfig from "../../../SdkConfig";
-import {replaceableComponent} from "../../../utils/replaceableComponent";
+import { replaceableComponent } from "../../../utils/replaceableComponent";
+import BugReportDialog from '../dialogs/BugReportDialog';
+import AccessibleButton from './AccessibleButton';
+
+interface IState {
+ error: Error;
+}
/**
* This error boundary component can be used to wrap large content areas and
* catch exceptions during rendering in the component tree below them.
*/
@replaceableComponent("views.elements.ErrorBoundary")
-export default class ErrorBoundary extends React.PureComponent {
+export default class ErrorBoundary extends React.PureComponent<{}, IState> {
constructor(props) {
super(props);
@@ -37,13 +43,13 @@ export default class ErrorBoundary extends React.PureComponent {
};
}
- static getDerivedStateFromError(error) {
+ static getDerivedStateFromError(error: Error): Partial {
// Side effects are not permitted here, so we only update the state so
// that the next render shows an error message.
return { error };
}
- componentDidCatch(error, { componentStack }) {
+ componentDidCatch(error: Error, { componentStack }: ErrorInfo): void {
// Browser consoles are better at formatting output when native errors are passed
// in their own `console.error` invocation.
console.error(error);
@@ -53,7 +59,7 @@ export default class ErrorBoundary extends React.PureComponent {
);
}
- _onClearCacheAndReload = () => {
+ private onClearCacheAndReload = (): void => {
if (!PlatformPeg.get()) return;
MatrixClientPeg.get().stopClient();
@@ -62,11 +68,7 @@ export default class ErrorBoundary extends React.PureComponent {
});
};
- _onBugReport = () => {
- const BugReportDialog = sdk.getComponent("dialogs.BugReportDialog");
- if (!BugReportDialog) {
- return;
- }
+ private onBugReport = (): void => {
Modal.createTrackedDialog('Bug Report Dialog', '', BugReportDialog, {
label: 'react-soft-crash',
});
@@ -74,7 +76,6 @@ export default class ErrorBoundary extends React.PureComponent {
render() {
if (this.state.error) {
- const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const newIssueUrl = "https://github.com/vector-im/element-web/issues/new";
let bugReportSection;
@@ -95,7 +96,7 @@ export default class ErrorBoundary extends React.PureComponent {
"the rooms or groups you have visited and the usernames of " +
"other users. They do not contain messages.",
)}
-
+
{_t("Submit debug logs")}
;
@@ -105,7 +106,7 @@ export default class ErrorBoundary extends React.PureComponent {
{_t("Something went wrong!")}
{ bugReportSection }
-
+
{_t("Clear cache and reload")}
diff --git a/src/components/views/elements/EventListSummary.tsx b/src/components/views/elements/EventListSummary.tsx
index 86d3e082ad..ab647db9ed 100644
--- a/src/components/views/elements/EventListSummary.tsx
+++ b/src/components/views/elements/EventListSummary.tsx
@@ -14,13 +14,13 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import React, {ReactChildren, useEffect} from 'react';
-import {MatrixEvent} from "matrix-js-sdk/src/models/event";
-import {RoomMember} from "matrix-js-sdk/src/models/room-member";
+import React, { ReactNode, useEffect } from 'react';
+import { MatrixEvent } from "matrix-js-sdk/src/models/event";
+import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import MemberAvatar from '../avatars/MemberAvatar';
import { _t } from '../../../languageHandler';
-import {useStateToggle} from "../../../hooks/useStateToggle";
+import { useStateToggle } from "../../../hooks/useStateToggle";
import AccessibleButton from "./AccessibleButton";
interface IProps {
@@ -31,11 +31,11 @@ interface IProps {
// Whether or not to begin with state.expanded=true
startExpanded?: boolean,
// The list of room members for which to show avatars next to the summary
- summaryMembers?: RoomMember[],
+ summaryMembers?: RoomMember[];
// The text to show as the summary of this event list
- summaryText?: string,
+ summaryText?: string;
// An array of EventTiles to render when expanded
- children: ReactChildren,
+ children: ReactNode[];
// Called when the event list expansion is toggled
onToggle?(): void;
}
diff --git a/src/components/views/elements/EventTilePreview.tsx b/src/components/views/elements/EventTilePreview.tsx
index cf3b7a6e61..0696ee566e 100644
--- a/src/components/views/elements/EventTilePreview.tsx
+++ b/src/components/views/elements/EventTilePreview.tsx
@@ -17,13 +17,14 @@ limitations under the License.
import React from 'react';
import classnames from 'classnames';
import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
+import { RoomMember } from 'matrix-js-sdk/src/models/room-member';
import * as Avatar from '../../../Avatar';
import EventTile from '../rooms/EventTile';
import SettingsStore from "../../../settings/SettingsStore";
-import {Layout} from "../../../settings/Layout";
-import {UIFeature} from "../../../settings/UIFeature";
-import {replaceableComponent} from "../../../utils/replaceableComponent";
+import { Layout } from "../../../settings/Layout";
+import { UIFeature } from "../../../settings/UIFeature";
+import { replaceableComponent } from "../../../utils/replaceableComponent";
interface IProps {
/**
@@ -101,16 +102,17 @@ export default class EventTilePreview extends React.Component {
// Fake it more
event.sender = {
- name: this.props.displayName,
+ name: this.props.displayName || this.props.userId,
+ rawDisplayName: this.props.displayName,
userId: this.props.userId,
getAvatarUrl: (..._) => {
return Avatar.avatarUrlForUser(
- { avatarUrl: this.props.avatarUrl },
+ {avatarUrl: this.props.avatarUrl},
AVATAR_SIZE, AVATAR_SIZE, "crop",
);
},
getMxcAvatarUrl: () => this.props.avatarUrl,
- };
+ } as RoomMember;
return event;
}
diff --git a/src/components/views/elements/Field.tsx b/src/components/views/elements/Field.tsx
index 59d9a11596..1373c2df0e 100644
--- a/src/components/views/elements/Field.tsx
+++ b/src/components/views/elements/Field.tsx
@@ -29,6 +29,11 @@ function getId() {
return `${BASE_ID}_${count++}`;
}
+export interface IValidateOpts {
+ focused?: boolean;
+ allowEmpty?: boolean;
+}
+
interface IProps {
// The field's ID, which binds the input and label together. Immutable.
id?: string;
@@ -180,7 +185,7 @@ export default class Field extends React.PureComponent {
}
};
- public async validate({ focused, allowEmpty = true }: {focused?: boolean, allowEmpty?: boolean}) {
+ public async validate({ focused, allowEmpty = true }: IValidateOpts) {
if (!this.props.onValidate) {
return;
}
diff --git a/src/components/views/elements/FormButton.js b/src/components/views/elements/FormButton.js
deleted file mode 100644
index f6b4c986f5..0000000000
--- a/src/components/views/elements/FormButton.js
+++ /dev/null
@@ -1,28 +0,0 @@
-/*
-Copyright 2019 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 AccessibleButton from "./AccessibleButton";
-
-export default function FormButton(props) {
- const {className, label, kind, ...restProps} = props;
- const newClassName = (className || "") + " mx_FormButton";
- const allProps = Object.assign({}, restProps,
- {className: newClassName, kind: kind || "primary", children: [label]});
- return React.createElement(AccessibleButton, allProps);
-}
-
-FormButton.propTypes = AccessibleButton.propTypes;
diff --git a/src/components/views/elements/LabelledToggleSwitch.js b/src/components/views/elements/LabelledToggleSwitch.tsx
similarity index 63%
rename from src/components/views/elements/LabelledToggleSwitch.js
rename to src/components/views/elements/LabelledToggleSwitch.tsx
index ef60eeed7b..d97b698fd8 100644
--- a/src/components/views/elements/LabelledToggleSwitch.js
+++ b/src/components/views/elements/LabelledToggleSwitch.tsx
@@ -1,5 +1,5 @@
/*
-Copyright 2019 New Vector Ltd
+Copyright 2019 - 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -14,38 +14,33 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import React from 'react';
-import PropTypes from "prop-types";
+import React from "react";
+
import ToggleSwitch from "./ToggleSwitch";
import {replaceableComponent} from "../../../utils/replaceableComponent";
+interface IProps {
+ // The value for the toggle switch
+ value: boolean;
+ // The translated label for the switch
+ label: string;
+ // Whether or not to disable the toggle switch
+ disabled?: boolean;
+ // True to put the toggle in front of the label
+ // Default false.
+ toggleInFront?: boolean;
+ // Additional class names to append to the switch. Optional.
+ className?: string;
+ // The function to call when the value changes
+ onChange(checked: boolean): void;
+}
+
@replaceableComponent("views.elements.LabelledToggleSwitch")
-export default class LabelledToggleSwitch extends React.Component {
- static propTypes = {
- // The value for the toggle switch
- value: PropTypes.bool.isRequired,
-
- // The function to call when the value changes
- onChange: PropTypes.func.isRequired,
-
- // The translated label for the switch
- label: PropTypes.string.isRequired,
-
- // Whether or not to disable the toggle switch
- disabled: PropTypes.bool,
-
- // True to put the toggle in front of the label
- // Default false.
- toggleInFront: PropTypes.bool,
-
- // Additional class names to append to the switch. Optional.
- className: PropTypes.string,
- };
-
+export default class LabelledToggleSwitch extends React.PureComponent {
render() {
// This is a minimal version of a SettingsFlag
- let firstPart = {this.props.label};
+ let firstPart = { this.props.label };
let secondPart = , "summaryText" | "summaryMembers"> {
// The maximum number of names to show in either each summary e.g. 2 would result "A, B and 234 others left"
summaryLength?: number;
// The maximum number of avatars to display in the summary
avatarsMaxLength?: number;
- // The minimum number of events needed to trigger summarisation
- threshold?: number,
- // Whether or not to begin with state.expanded=true
- startExpanded?: boolean,
- // An array of EventTiles to render when expanded
- children: ReactChildren;
- // Called when the MELS expansion is toggled
- onToggle?(): void,
}
interface IUserEvents {
diff --git a/src/components/views/elements/RoomAliasField.js b/src/components/views/elements/RoomAliasField.tsx
similarity index 65%
rename from src/components/views/elements/RoomAliasField.js
rename to src/components/views/elements/RoomAliasField.tsx
index 813dd8b5cc..74af311b47 100644
--- a/src/components/views/elements/RoomAliasField.js
+++ b/src/components/views/elements/RoomAliasField.tsx
@@ -1,5 +1,5 @@
/*
-Copyright 2019 New Vector Ltd
+Copyright 2019 - 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -13,67 +13,78 @@ 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, { createRef } from "react";
+
import { _t } from '../../../languageHandler';
-import React from 'react';
-import PropTypes from 'prop-types';
-import * as sdk from '../../../index';
import withValidation from './Validation';
-import {MatrixClientPeg} from '../../../MatrixClientPeg';
-import {replaceableComponent} from "../../../utils/replaceableComponent";
+import { MatrixClientPeg } from '../../../MatrixClientPeg';
+import { replaceableComponent } from "../../../utils/replaceableComponent";
+import Field, { IValidateOpts } from "./Field";
+
+interface IProps {
+ domain: string;
+ value: string;
+ label?: string;
+ placeholder?: string;
+ onChange?(value: string): void;
+}
+
+interface IState {
+ isValid: boolean;
+}
// Controlled form component wrapping Field for inputting a room alias scoped to a given domain
@replaceableComponent("views.elements.RoomAliasField")
-export default class RoomAliasField extends React.PureComponent {
- static propTypes = {
- domain: PropTypes.string.isRequired,
- onChange: PropTypes.func,
- value: PropTypes.string.isRequired,
- };
+export default class RoomAliasField extends React.PureComponent {
+ private fieldRef = createRef();
- constructor(props) {
- super(props);
- this.state = {isValid: true};
+ constructor(props, context) {
+ super(props, context);
+
+ this.state = {
+ isValid: true,
+ };
}
- _asFullAlias(localpart) {
+ private asFullAlias(localpart: string): string {
return `#${localpart}:${this.props.domain}`;
}
render() {
- const Field = sdk.getComponent('views.elements.Field');
const poundSign = (#);
const aliasPostfix = ":" + this.props.domain;
const domain = ({aliasPostfix});
const maxlength = 255 - this.props.domain.length - 2; // 2 for # and :
return (
this._fieldRef = ref}
- onValidate={this._onValidate}
- placeholder={_t("e.g. my-room")}
- onChange={this._onChange}
+ ref={this.fieldRef}
+ onValidate={this.onValidate}
+ placeholder={this.props.placeholder || _t("e.g. my-room")}
+ onChange={this.onChange}
value={this.props.value.substring(1, this.props.value.length - this.props.domain.length - 1)}
maxLength={maxlength}
/>
);
}
- _onChange = (ev) => {
+ private onChange = (ev) => {
if (this.props.onChange) {
- this.props.onChange(this._asFullAlias(ev.target.value));
+ this.props.onChange(this.asFullAlias(ev.target.value));
}
};
- _onValidate = async (fieldState) => {
- const result = await this._validationRules(fieldState);
+ private onValidate = async (fieldState) => {
+ const result = await this.validationRules(fieldState);
this.setState({isValid: result.valid});
return result;
};
- _validationRules = withValidation({
+ private validationRules = withValidation({
rules: [
{
key: "safeLocalpart",
@@ -81,7 +92,7 @@ export default class RoomAliasField extends React.PureComponent {
if (!value) {
return true;
}
- const fullAlias = this._asFullAlias(value);
+ const fullAlias = this.asFullAlias(value);
// XXX: FIXME https://github.com/matrix-org/matrix-doc/issues/668
return !value.includes("#") && !value.includes(":") && !value.includes(",") &&
encodeURI(fullAlias) === fullAlias;
@@ -90,7 +101,7 @@ export default class RoomAliasField extends React.PureComponent {
}, {
key: "required",
test: async ({ value, allowEmpty }) => allowEmpty || !!value,
- invalid: () => _t("Please provide a room address"),
+ invalid: () => _t("Please provide an address"),
}, {
key: "taken",
final: true,
@@ -100,7 +111,7 @@ export default class RoomAliasField extends React.PureComponent {
}
const client = MatrixClientPeg.get();
try {
- await client.getRoomIdForAlias(this._asFullAlias(value));
+ await client.getRoomIdForAlias(this.asFullAlias(value));
// we got a room id, so the alias is taken
return false;
} catch (err) {
@@ -116,15 +127,15 @@ export default class RoomAliasField extends React.PureComponent {
],
});
- get isValid() {
+ public get isValid() {
return this.state.isValid;
}
- validate(options) {
- return this._fieldRef.validate(options);
+ public validate(options: IValidateOpts) {
+ return this.fieldRef.current?.validate(options);
}
- focus() {
- this._fieldRef.focus();
+ public focus() {
+ this.fieldRef.current?.focus();
}
}
diff --git a/src/components/views/elements/RoomName.tsx b/src/components/views/elements/RoomName.tsx
index 9178155d19..cdd83aedc2 100644
--- a/src/components/views/elements/RoomName.tsx
+++ b/src/components/views/elements/RoomName.tsx
@@ -14,10 +14,10 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import {useEffect, useState} from "react";
-import {Room} from "matrix-js-sdk/src/models/room";
+import React, { useEffect, useState } from "react";
+import { Room } from "matrix-js-sdk/src/models/room";
-import {useEventEmitter} from "../../../hooks/useEventEmitter";
+import { useEventEmitter } from "../../../hooks/useEventEmitter";
interface IProps {
room: Room;
@@ -34,7 +34,7 @@ const RoomName = ({ room, children }: IProps): JSX.Element => {
}, [room]);
if (children) return children(name);
- return name || "";
+ return <>{ name || "" }>;
};
export default RoomName;
diff --git a/src/components/views/elements/SettingsFlag.tsx b/src/components/views/elements/SettingsFlag.tsx
index 4f885ab47d..24a21e1a33 100644
--- a/src/components/views/elements/SettingsFlag.tsx
+++ b/src/components/views/elements/SettingsFlag.tsx
@@ -77,9 +77,10 @@ export default class SettingsFlag extends React.Component {
public render() {
const canChange = SettingsStore.canSetValue(this.props.name, this.props.roomId, this.props.level);
- let label = this.props.label;
- if (!label) label = SettingsStore.getDisplayName(this.props.name, this.props.level);
- else label = _t(label);
+ const label = this.props.label
+ ? _t(this.props.label)
+ : SettingsStore.getDisplayName(this.props.name, this.props.level);
+ const description = SettingsStore.getDescription(this.props.name);
if (this.props.useCheckbox) {
return {
disabled={this.props.disabled || !canChange}
aria-label={label}
/>
+ { description &&
+ { description }
+
}
);
}
diff --git a/src/components/views/elements/StyledRadioGroup.tsx b/src/components/views/elements/StyledRadioGroup.tsx
index 6b9e992f92..744b6f2059 100644
--- a/src/components/views/elements/StyledRadioGroup.tsx
+++ b/src/components/views/elements/StyledRadioGroup.tsx
@@ -34,10 +34,19 @@ interface IProps {
definitions: IDefinition[];
value?: T; // if not provided no options will be selected
outlined?: boolean;
+ disabled?: boolean;
onChange(newValue: T): void;
}
-function StyledRadioGroup({name, definitions, value, className, outlined, onChange}: IProps) {
+function StyledRadioGroup({
+ name,
+ definitions,
+ value,
+ className,
+ outlined,
+ disabled,
+ onChange,
+}: IProps) {
const _onChange = e => {
onChange(e.target.value);
};
@@ -50,12 +59,12 @@ function StyledRadioGroup({name, definitions, value, className
checked={d.checked !== undefined ? d.checked : d.value === value}
name={name}
value={d.value}
- disabled={d.disabled}
+ disabled={disabled || d.disabled}
outlined={outlined}
>
- {d.label}
+ { d.label }
- {d.description}
+ { d.description ? { d.description } : null }
)}
;
}
diff --git a/src/components/views/elements/TruncatedList.js b/src/components/views/elements/TruncatedList.tsx
similarity index 65%
rename from src/components/views/elements/TruncatedList.js
rename to src/components/views/elements/TruncatedList.tsx
index 0509775545..395caa9222 100644
--- a/src/components/views/elements/TruncatedList.js
+++ b/src/components/views/elements/TruncatedList.tsx
@@ -16,31 +16,29 @@ limitations under the License.
*/
import React from 'react';
-import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler';
import {replaceableComponent} from "../../../utils/replaceableComponent";
-@replaceableComponent("views.elements.TruncatedList")
-export default class TruncatedList extends React.Component {
- static propTypes = {
- // The number of elements to show before truncating. If negative, no truncation is done.
- truncateAt: PropTypes.number,
- // The className to apply to the wrapping div
- className: PropTypes.string,
- // A function that returns the children to be rendered into the element.
- // function getChildren(start: number, end: number): Array
- // The start element is included, the end is not (as in `slice`).
- // If omitted, the React child elements will be used. This parameter can be used
- // to avoid creating unnecessary React elements.
- getChildren: PropTypes.func,
- // A function that should return the total number of child element available.
- // Required if getChildren is supplied.
- getChildCount: PropTypes.func,
- // A function which will be invoked when an overflow element is required.
- // This will be inserted after the children.
- createOverflowElement: PropTypes.func,
- };
+interface IProps {
+ // The number of elements to show before truncating. If negative, no truncation is done.
+ truncateAt?: number;
+ // The className to apply to the wrapping div
+ className?: string;
+ // A function that returns the children to be rendered into the element.
+ // The start element is included, the end is not (as in `slice`).
+ // If omitted, the React child elements will be used. This parameter can be used
+ // to avoid creating unnecessary React elements.
+ getChildren?: (start: number, end: number) => Array;
+ // A function that should return the total number of child element available.
+ // Required if getChildren is supplied.
+ getChildCount?: () => number;
+ // A function which will be invoked when an overflow element is required.
+ // This will be inserted after the children.
+ createOverflowElement?: (overflowCount: number, totalCount: number) => React.ReactNode;
+}
+@replaceableComponent("views.elements.TruncatedList")
+export default class TruncatedList extends React.Component {
static defaultProps ={
truncateAt: 2,
createOverflowElement(overflowCount, totalCount) {
@@ -50,7 +48,7 @@ export default class TruncatedList extends React.Component {
},
};
- _getChildren(start, end) {
+ private getChildren(start: number, end: number): Array {
if (this.props.getChildren && this.props.getChildCount) {
return this.props.getChildren(start, end);
} else {
@@ -63,7 +61,7 @@ export default class TruncatedList extends React.Component {
}
}
- _getChildCount() {
+ private getChildCount(): number {
if (this.props.getChildren && this.props.getChildCount) {
return this.props.getChildCount();
} else {
@@ -73,10 +71,10 @@ export default class TruncatedList extends React.Component {
}
}
- render() {
+ public render() {
let overflowNode = null;
- const totalChildren = this._getChildCount();
+ const totalChildren = this.getChildCount();
let upperBound = totalChildren;
if (this.props.truncateAt >= 0) {
const overflowCount = totalChildren - this.props.truncateAt;
@@ -87,7 +85,7 @@ export default class TruncatedList extends React.Component {
upperBound = this.props.truncateAt;
}
}
- const childNodes = this._getChildren(0, upperBound);
+ const childNodes = this.getChildren(0, upperBound);
return (
;
}
}
-
diff --git a/src/customisations/Media.ts b/src/customisations/Media.ts
index f9d957b60c..37e91fc54b 100644
--- a/src/customisations/Media.ts
+++ b/src/customisations/Media.ts
@@ -14,10 +14,11 @@
* limitations under the License.
*/
-import {MatrixClientPeg} from "../MatrixClientPeg";
-import {IMediaEventContent, IPreparedMedia, prepEventContentAsMedia} from "./models/IMediaEventContent";
-import {ResizeMethod} from "../Avatar";
-import {MatrixClient} from "matrix-js-sdk/src/client";
+import { MatrixClient } from "matrix-js-sdk/src/client";
+import { ResizeMethod } from "matrix-js-sdk/src/@types/partials";
+
+import { MatrixClientPeg } from "../MatrixClientPeg";
+import { IMediaEventContent, IPreparedMedia, prepEventContentAsMedia } from "./models/IMediaEventContent";
// Populate this class with the details of your customisations when copying it.
diff --git a/src/dispatcher/actions.ts b/src/dispatcher/actions.ts
index 30cb753975..6438ecc0f2 100644
--- a/src/dispatcher/actions.ts
+++ b/src/dispatcher/actions.ts
@@ -164,4 +164,9 @@ export enum Action {
* Inserts content into the active composer. Should be used with ComposerInsertPayload
*/
ComposerInsert = "composer_insert",
+
+ /**
+ * Switches space. Should be used with SwitchSpacePayload.
+ */
+ SwitchSpace = "switch_space",
}
diff --git a/src/dispatcher/payloads/SwitchSpacePayload.ts b/src/dispatcher/payloads/SwitchSpacePayload.ts
new file mode 100644
index 0000000000..04eb744334
--- /dev/null
+++ b/src/dispatcher/payloads/SwitchSpacePayload.ts
@@ -0,0 +1,27 @@
+/*
+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 { ActionPayload } from "../payloads";
+import { Action } from "../actions";
+
+export interface SwitchSpacePayload extends ActionPayload {
+ action: Action.SwitchSpace;
+
+ /**
+ * The number of the space to switch to, 1-indexed, 0 is Home.
+ */
+ num: number;
+}
diff --git a/src/dispatcher/payloads/ViewUserPayload.ts b/src/dispatcher/payloads/ViewUserPayload.ts
index c2838d0dbb..c4d73aea6a 100644
--- a/src/dispatcher/payloads/ViewUserPayload.ts
+++ b/src/dispatcher/payloads/ViewUserPayload.ts
@@ -15,6 +15,7 @@ limitations under the License.
*/
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
+import { User } from "matrix-js-sdk/src/models/user";
import { ActionPayload } from "../payloads";
import { Action } from "../actions";
@@ -25,5 +26,5 @@ export interface ViewUserPayload extends ActionPayload {
* The member to view. May be null or falsy to indicate that no member
* should be shown (hide whichever relevant components).
*/
- member?: RoomMember;
+ member?: RoomMember | User;
}
diff --git a/src/hooks/useRoomState.ts b/src/hooks/useRoomState.ts
new file mode 100644
index 0000000000..e778acf8a9
--- /dev/null
+++ b/src/hooks/useRoomState.ts
@@ -0,0 +1,46 @@
+/*
+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 { useCallback, useEffect, useState } from "react";
+import { Room } from "matrix-js-sdk/src/models/room";
+import { RoomState } from "matrix-js-sdk/src/models/room-state";
+
+import { useEventEmitter } from "./useEventEmitter";
+
+type Mapper = (roomState: RoomState) => T;
+const defaultMapper: Mapper = (roomState: RoomState) => roomState;
+
+// Hook to simplify watching Matrix Room state
+export const useRoomState = (
+ room: Room,
+ mapper: Mapper = defaultMapper as Mapper,
+): T => {
+ const [value, setValue] = useState(room ? mapper(room.currentState) : undefined);
+
+ const update = useCallback(() => {
+ if (!room) return;
+ setValue(mapper(room.currentState));
+ }, [room, mapper]);
+
+ useEventEmitter(room?.currentState, "RoomState.events", update);
+ useEffect(() => {
+ update();
+ return () => {
+ setValue(undefined);
+ };
+ }, [update]);
+ return value;
+};
diff --git a/src/hooks/useStateToggle.ts b/src/hooks/useStateToggle.ts
index b50a923234..33701c4f16 100644
--- a/src/hooks/useStateToggle.ts
+++ b/src/hooks/useStateToggle.ts
@@ -18,7 +18,7 @@ import {Dispatch, SetStateAction, useState} from "react";
// Hook to simplify toggling of a boolean state value
// Returns value, method to toggle boolean value and method to set the boolean value
-export const useStateToggle = (initialValue: boolean): [boolean, () => void, Dispatch>] => {
+export const useStateToggle = (initialValue = false): [boolean, () => void, Dispatch>] => {
const [value, setValue] = useState(initialValue);
const toggleValue = () => {
setValue(!value);
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index 8c4262fe44..bc62868a0f 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -396,7 +396,8 @@
"Failed to invite": "Failed to invite",
"Operation failed": "Operation failed",
"Failed to invite users to the room:": "Failed to invite users to the room:",
- "Failed to invite the following users to the %(roomName)s room:": "Failed to invite the following users to the %(roomName)s room:",
+ "We sent the others, but the below people couldn't be invited to ": "We sent the others, but the below people couldn't be invited to ",
+ "Some invites couldn't be sent": "Some invites couldn't be sent",
"You need to be logged in.": "You need to be logged in.",
"You need to be able to invite users to do that.": "You need to be able to invite users to do that.",
"Unable to create widget.": "Unable to create widget.",
@@ -489,24 +490,27 @@
"Converts the room to a DM": "Converts the room to a DM",
"Converts the DM to a room": "Converts the DM to a room",
"Displays action": "Displays action",
- "Reason": "Reason",
- "%(targetName)s accepted the invitation for %(displayName)s.": "%(targetName)s accepted the invitation for %(displayName)s.",
- "%(targetName)s accepted an invitation.": "%(targetName)s accepted an invitation.",
- "%(senderName)s invited %(targetName)s.": "%(senderName)s invited %(targetName)s.",
- "%(senderName)s banned %(targetName)s.": "%(senderName)s banned %(targetName)s.",
- "%(oldDisplayName)s changed their display name to %(displayName)s.": "%(oldDisplayName)s changed their display name to %(displayName)s.",
- "%(senderName)s set their display name to %(displayName)s.": "%(senderName)s set their display name to %(displayName)s.",
- "%(senderName)s removed their display name (%(oldDisplayName)s).": "%(senderName)s removed their display name (%(oldDisplayName)s).",
- "%(senderName)s removed their profile picture.": "%(senderName)s removed their profile picture.",
- "%(senderName)s changed their profile picture.": "%(senderName)s changed their profile picture.",
- "%(senderName)s set a profile picture.": "%(senderName)s set a profile picture.",
- "%(senderName)s made no change.": "%(senderName)s made no change.",
- "%(targetName)s joined the room.": "%(targetName)s joined the room.",
- "%(targetName)s rejected the invitation.": "%(targetName)s rejected the invitation.",
- "%(targetName)s left the room.": "%(targetName)s left the room.",
- "%(senderName)s unbanned %(targetName)s.": "%(senderName)s unbanned %(targetName)s.",
- "%(senderName)s withdrew %(targetName)s's invitation.": "%(senderName)s withdrew %(targetName)s's invitation.",
- "%(senderName)s kicked %(targetName)s.": "%(senderName)s kicked %(targetName)s.",
+ "%(targetName)s accepted the invitation for %(displayName)s": "%(targetName)s accepted the invitation for %(displayName)s",
+ "%(targetName)s accepted an invitation": "%(targetName)s accepted an invitation",
+ "%(senderName)s invited %(targetName)s": "%(senderName)s invited %(targetName)s",
+ "%(senderName)s banned %(targetName)s: %(reason)s": "%(senderName)s banned %(targetName)s: %(reason)s",
+ "%(senderName)s banned %(targetName)s": "%(senderName)s banned %(targetName)s",
+ "%(oldDisplayName)s changed their display name to %(displayName)s": "%(oldDisplayName)s changed their display name to %(displayName)s",
+ "%(senderName)s set their display name to %(displayName)s": "%(senderName)s set their display name to %(displayName)s",
+ "%(senderName)s removed their display name (%(oldDisplayName)s)": "%(senderName)s removed their display name (%(oldDisplayName)s)",
+ "%(senderName)s removed their profile picture": "%(senderName)s removed their profile picture",
+ "%(senderName)s changed their profile picture": "%(senderName)s changed their profile picture",
+ "%(senderName)s set a profile picture": "%(senderName)s set a profile picture",
+ "%(senderName)s made no change": "%(senderName)s made no change",
+ "%(targetName)s joined the room": "%(targetName)s joined the room",
+ "%(targetName)s rejected the invitation": "%(targetName)s rejected the invitation",
+ "%(targetName)s left the room: %(reason)s": "%(targetName)s left the room: %(reason)s",
+ "%(targetName)s left the room": "%(targetName)s left the room",
+ "%(senderName)s unbanned %(targetName)s": "%(senderName)s unbanned %(targetName)s",
+ "%(senderName)s withdrew %(targetName)s's invitation: %(reason)s": "%(senderName)s withdrew %(targetName)s's invitation: %(reason)s",
+ "%(senderName)s withdrew %(targetName)s's invitation": "%(senderName)s withdrew %(targetName)s's invitation",
+ "%(senderName)s kicked %(targetName)s: %(reason)s": "%(senderName)s kicked %(targetName)s: %(reason)s",
+ "%(senderName)s kicked %(targetName)s": "%(senderName)s kicked %(targetName)s",
"%(senderDisplayName)s changed the topic to \"%(topic)s\".": "%(senderDisplayName)s changed the topic to \"%(topic)s\".",
"%(senderDisplayName)s removed the room name.": "%(senderDisplayName)s removed the room name.",
"%(senderDisplayName)s changed the room name from %(oldRoomName)s to %(newRoomName)s.": "%(senderDisplayName)s changed the room name from %(oldRoomName)s to %(newRoomName)s.",
@@ -784,6 +788,7 @@
"%(senderName)s: %(reaction)s": "%(senderName)s: %(reaction)s",
"%(senderName)s: %(stickerName)s": "%(senderName)s: %(stickerName)s",
"Change notification settings": "Change notification settings",
+ "Report to moderators prototype. In rooms that support moderation, the `report` button will let you report abuse to room moderators": "Report to moderators prototype. In rooms that support moderation, the `report` button will let you report abuse to room moderators",
"Spaces prototype. Incompatible with Communities, Communities v2 and Custom Tags. Requires compatible homeserver for some features.": "Spaces prototype. Incompatible with Communities, Communities v2 and Custom Tags. Requires compatible homeserver for some features.",
"Spaces": "Spaces",
"Spaces are a new way to group rooms and people.": "Spaces are a new way to group rooms and people.",
@@ -793,6 +798,10 @@
"You can leave the beta any time from settings or tapping on a beta badge, like the one above.": "You can leave the beta any time from settings or tapping on a beta badge, like the one above.",
"Beta available for web, desktop and Android. Some features may be unavailable on your homeserver.": "Beta available for web, desktop and Android. Some features may be unavailable on your homeserver.",
"Your feedback will help make spaces better. The more detail you can go into, the better.": "Your feedback will help make spaces better. The more detail you can go into, the better.",
+ "Show all rooms in Home": "Show all rooms in Home",
+ "Show people in spaces": "Show people in spaces",
+ "If disabled, you can still add Direct Messages to Personal Spaces. If enabled, you'll automatically see everyone who is a member of the Space.": "If disabled, you can still add Direct Messages to Personal Spaces. If enabled, you'll automatically see everyone who is a member of the Space.",
+ "Show notification badges for People in Spaces": "Show notification badges for People in Spaces",
"Show options to enable 'Do not disturb' mode": "Show options to enable 'Do not disturb' mode",
"Send and receive voice messages": "Send and receive voice messages",
"Render LaTeX maths in messages": "Render LaTeX maths in messages",
@@ -1018,17 +1027,42 @@
"Your private space": "Your private space",
"Add some details to help people recognise it.": "Add some details to help people recognise it.",
"You can change these anytime.": "You can change these anytime.",
+ "e.g. my-space": "e.g. my-space",
+ "Address": "Address",
"Creating...": "Creating...",
"Create": "Create",
+ "All rooms": "All rooms",
+ "Home": "Home",
"Expand space panel": "Expand space panel",
"Collapse space panel": "Collapse space panel",
- "All rooms": "All rooms",
"Click to copy": "Click to copy",
"Copied!": "Copied!",
"Failed to copy": "Failed to copy",
"Share invite link": "Share invite link",
"Invite people": "Invite people",
"Invite with email or username": "Invite with email or username",
+ "Failed to save space settings.": "Failed to save space settings.",
+ "General": "General",
+ "Edit settings relating to your space.": "Edit settings relating to your space.",
+ "Saving...": "Saving...",
+ "Save Changes": "Save Changes",
+ "Leave Space": "Leave Space",
+ "Failed to update the visibility of this space": "Failed to update the visibility of this space",
+ "Failed to update the guest access of this space": "Failed to update the guest access of this space",
+ "Failed to update the history visibility of this space": "Failed to update the history visibility of this space",
+ "Hide advanced": "Hide advanced",
+ "Enable guest access": "Enable guest access",
+ "Guests can join a space without having an account.": "Guests can join a space without having an account.",
+ "This may be useful for public spaces.": "This may be useful for public spaces.",
+ "Show advanced": "Show advanced",
+ "Visibility": "Visibility",
+ "Decide who can view and join %(spaceName)s.": "Decide who can view and join %(spaceName)s.",
+ "anyone with the link can view and join": "anyone with the link can view and join",
+ "Invite only": "Invite only",
+ "only invited people can view and join": "only invited people can view and join",
+ "Preview Space": "Preview Space",
+ "Allow people to preview your space before they join.": "Allow people to preview your space before they join.",
+ "Recommended for public spaces.": "Recommended for public spaces.",
"Settings": "Settings",
"Leave space": "Leave space",
"Create new room": "Create new room",
@@ -1037,6 +1071,8 @@
"Manage & explore rooms": "Manage & explore rooms",
"Explore rooms": "Explore rooms",
"Space options": "Space options",
+ "Expand": "Expand",
+ "Collapse": "Collapse",
"Remove": "Remove",
"This bridge was provisioned by .": "This bridge was provisioned by .",
"This bridge is managed by .": "This bridge is managed by .",
@@ -1223,8 +1259,6 @@
"Custom theme URL": "Custom theme URL",
"Add theme": "Add theme",
"Theme": "Theme",
- "Hide advanced": "Hide advanced",
- "Show advanced": "Show advanced",
"Set the name of a font installed on your system & %(brand)s will attempt to use it.": "Set the name of a font installed on your system & %(brand)s will attempt to use it.",
"Enable experimental, compact IRC style layout": "Enable experimental, compact IRC style layout",
"Customise your appearance": "Customise your appearance",
@@ -1245,7 +1279,6 @@
"Deactivate Account": "Deactivate Account",
"Deactivate account": "Deactivate account",
"Discovery": "Discovery",
- "General": "General",
"Legal": "Legal",
"Credits": "Credits",
"For help with using %(brand)s, click here.": "For help with using %(brand)s, click here.",
@@ -1351,6 +1384,7 @@
"Upgrade this room to the recommended room version": "Upgrade this room to the recommended room version",
"this room": "this room",
"View older messages in %(roomName)s.": "View older messages in %(roomName)s.",
+ "Space information": "Space information",
"Room information": "Room information",
"Internal room ID:": "Internal room ID:",
"Room version": "Room version",
@@ -1380,6 +1414,7 @@
"Failed to unban": "Failed to unban",
"Unban": "Unban",
"Banned by %(displayName)s": "Banned by %(displayName)s",
+ "Reason": "Reason",
"Error changing power level requirement": "Error changing power level requirement",
"An error occurred changing the room's power level requirements. Ensure you have sufficient permissions and try again.": "An error occurred changing the room's power level requirements. Ensure you have sufficient permissions and try again.",
"Error changing power level": "Error changing power level",
@@ -1676,14 +1711,18 @@
"Error removing address": "Error removing address",
"Main address": "Main address",
"not specified": "not specified",
+ "This space has no local addresses": "This space has no local addresses",
"This room has no local addresses": "This room has no local addresses",
"Local address": "Local address",
"Published Addresses": "Published Addresses",
- "Published addresses can be used by anyone on any server to join your room. To publish an address, it needs to be set as a local address first.": "Published addresses can be used by anyone on any server to join your room. To publish an address, it needs to be set as a local address first.",
+ "Published addresses can be used by anyone on any server to join your space.": "Published addresses can be used by anyone on any server to join your space.",
+ "Published addresses can be used by anyone on any server to join your room.": "Published addresses can be used by anyone on any server to join your room.",
+ "To publish an address, it needs to be set as a local address first.": "To publish an address, it needs to be set as a local address first.",
"Other published addresses:": "Other published addresses:",
"No other published addresses yet, add one below": "No other published addresses yet, add one below",
"New published address (e.g. #alias:server)": "New published address (e.g. #alias:server)",
"Local Addresses": "Local Addresses",
+ "Set addresses for this space so users can find this space through your homeserver (%(localDomain)s)": "Set addresses for this space so users can find this space through your homeserver (%(localDomain)s)",
"Set addresses for this room so users can find this room through your homeserver (%(localDomain)s)": "Set addresses for this room so users can find this room through your homeserver (%(localDomain)s)",
"Show more": "Show more",
"Error updating flair": "Error updating flair",
@@ -1932,6 +1971,7 @@
"Error loading Widget": "Error loading Widget",
"Error - Mixed content": "Error - Mixed content",
"Popout widget": "Popout widget",
+ "Message search initialisation failed, check your settings for more information": "Message search initialisation failed, check your settings for more information",
"Use the Desktop app to see all encrypted files": "Use the Desktop app to see all encrypted files",
"Use the Desktop app to search encrypted messages": "Use the Desktop app to search encrypted messages",
"This version of %(brand)s does not support viewing some encrypted files": "This version of %(brand)s does not support viewing some encrypted files",
@@ -2019,7 +2059,7 @@
"Room address": "Room address",
"e.g. my-room": "e.g. my-room",
"Some characters not allowed": "Some characters not allowed",
- "Please provide a room address": "Please provide a room address",
+ "Please provide an address": "Please provide an address",
"This address is available to use": "This address is available to use",
"This address is already in use": "This address is already in use",
"Server Options": "Server Options",
@@ -2029,7 +2069,6 @@
"Continue with %(provider)s": "Continue with %(provider)s",
"Sign in with single sign-on": "Sign in with single sign-on",
"And %(count)s more...|other": "And %(count)s more...",
- "Home": "Home",
"Enter a server name": "Enter a server name",
"Looks good": "Looks good",
"You are not allowed to view this server's rooms list": "You are not allowed to view this server's rooms list",
@@ -2243,7 +2282,6 @@
"Confirm to continue": "Confirm to continue",
"Click the button below to confirm your identity.": "Click the button below to confirm your identity.",
"Invite by email": "Invite by email",
- "Failed to invite the following users to chat: %(csvUsers)s": "Failed to invite the following users to chat: %(csvUsers)s",
"We couldn't create your DM.": "We couldn't create your DM.",
"Something went wrong trying to invite the users.": "Something went wrong trying to invite the users.",
"We couldn't invite those users. Please check the users you want to invite and try again.": "We couldn't invite those users. Please check the users you want to invite and try again.",
@@ -2318,9 +2356,23 @@
"Just a heads up, if you don't add an email and forget your password, you could permanently lose access to your account.": "Just a heads up, if you don't add an email and forget your password, you could permanently lose access to your account.",
"Email (optional)": "Email (optional)",
"Please fill why you're reporting.": "Please fill why you're reporting.",
+ "What this user is writing is wrong.\nThis will be reported to the room moderators.": "What this user is writing is wrong.\nThis will be reported to the room moderators.",
+ "This user is displaying toxic behaviour, for instance by insulting other users or sharing adult-only content in a family-friendly room or otherwise violating the rules of this room.\nThis will be reported to the room moderators.": "This user is displaying toxic behaviour, for instance by insulting other users or sharing adult-only content in a family-friendly room or otherwise violating the rules of this room.\nThis will be reported to the room moderators.",
+ "This user is displaying illegal behaviour, for instance by doxing people or threatening violence.\nThis will be reported to the room moderators who may escalate this to legal authorities.": "This user is displaying illegal behaviour, for instance by doxing people or threatening violence.\nThis will be reported to the room moderators who may escalate this to legal authorities.",
+ "This user is spamming the room with ads, links to ads or to propaganda.\nThis will be reported to the room moderators.": "This user is spamming the room with ads, links to ads or to propaganda.\nThis will be reported to the room moderators.",
+ "This room is dedicated to illegal or toxic content or the moderators fail to moderate illegal or toxic content.\nThis will be reported to the administrators of %(homeserver)s. The administrators will NOT be able to read the encrypted content of this room.": "This room is dedicated to illegal or toxic content or the moderators fail to moderate illegal or toxic content.\nThis will be reported to the administrators of %(homeserver)s. The administrators will NOT be able to read the encrypted content of this room.",
+ "This room is dedicated to illegal or toxic content or the moderators fail to moderate illegal or toxic content.\n This will be reported to the administrators of %(homeserver)s.": "This room is dedicated to illegal or toxic content or the moderators fail to moderate illegal or toxic content.\n This will be reported to the administrators of %(homeserver)s.",
+ "Any other reason. Please describe the problem.\nThis will be reported to the room moderators.": "Any other reason. Please describe the problem.\nThis will be reported to the room moderators.",
+ "Please pick a nature and describe what makes this message abusive.": "Please pick a nature and describe what makes this message abusive.",
+ "Report Content": "Report Content",
+ "Disagree": "Disagree",
+ "Toxic Behaviour": "Toxic Behaviour",
+ "Illegal Content": "Illegal Content",
+ "Spam or propaganda": "Spam or propaganda",
+ "Report the entire room": "Report the entire room",
+ "Send report": "Send report",
"Report Content to Your Homeserver Administrator": "Report Content to Your Homeserver Administrator",
"Reporting this message will send its unique 'event ID' to the administrator of your homeserver. If messages in this room are encrypted, your homeserver administrator will not be able to read the message text or view any files or images.": "Reporting this message will send its unique 'event ID' to the administrator of your homeserver. If messages in this room are encrypted, your homeserver administrator will not be able to read the message text or view any files or images.",
- "Send report": "Send report",
"Room Settings - %(roomName)s": "Room Settings - %(roomName)s",
"Failed to upgrade room": "Failed to upgrade room",
"The room upgrade could not be completed": "The room upgrade could not be completed",
@@ -2383,14 +2435,8 @@
"Share Room Message": "Share Room Message",
"Link to selected message": "Link to selected message",
"Command Help": "Command Help",
- "Failed to save space settings.": "Failed to save space settings.",
"Space settings": "Space settings",
- "Edit settings relating to your space.": "Edit settings relating to your space.",
- "Make this space private": "Make this space private",
- "Leave Space": "Leave Space",
- "View dev tools": "View dev tools",
- "Saving...": "Saving...",
- "Save Changes": "Save Changes",
+ "Settings - %(spaceName)s": "Settings - %(spaceName)s",
"To help us prevent this in future, please send us logs.": "To help us prevent this in future, please send us logs.",
"Missing session data": "Missing session data",
"Some session data, including encrypted message keys, is missing. Sign out and sign in to fix this, restoring keys from backup.": "Some session data, including encrypted message keys, is missing. Sign out and sign in to fix this, restoring keys from backup.",
@@ -2487,11 +2533,12 @@
"Share Message": "Share Message",
"Source URL": "Source URL",
"Collapse Reply Thread": "Collapse Reply Thread",
- "Report Content": "Report Content",
"Clear status": "Clear status",
"Update status": "Update status",
"Set status": "Set status",
"Set a new status...": "Set a new status...",
+ "Move up": "Move up",
+ "Move down": "Move down",
"View Community": "View Community",
"Unable to start audio streaming.": "Unable to start audio streaming.",
"Failed to start livestream": "Failed to start livestream",
@@ -2638,7 +2685,7 @@
"%(count)s messages deleted.|one": "%(count)s message deleted.",
"Your Communities": "Your Communities",
"Did you know: you can use communities to filter your %(brand)s experience!": "Did you know: you can use communities to filter your %(brand)s experience!",
- "To set up a filter, drag a community avatar over to the filter panel on the far left hand side of the screen. You can click on an avatar in the filter panel at any time to see only the rooms and people associated with that community.": "To set up a filter, drag a community avatar over to the filter panel on the far left hand side of the screen. You can click on an avatar in the filter panel at any time to see only the rooms and people associated with that community.",
+ "You can click on an avatar in the filter panel at any time to see only the rooms and people associated with that community.": "You can click on an avatar in the filter panel at any time to see only the rooms and people associated with that community.",
"Error whilst fetching joined communities": "Error whilst fetching joined communities",
"Create a new community": "Create a new community",
"Create a community to group together users and rooms! Build a custom homepage to mark out your space in the Matrix universe.": "Create a community to group together users and rooms! Build a custom homepage to mark out your space in the Matrix universe.",
@@ -2993,5 +3040,6 @@
"Esc": "Esc",
"Enter": "Enter",
"Space": "Space",
- "End": "End"
+ "End": "End",
+ "[number]": "[number]"
}
diff --git a/src/indexing/BaseEventIndexManager.ts b/src/indexing/BaseEventIndexManager.ts
index debcb213ca..9478b2987b 100644
--- a/src/indexing/BaseEventIndexManager.ts
+++ b/src/indexing/BaseEventIndexManager.ts
@@ -17,7 +17,7 @@ limitations under the License.
// The following interfaces take their names and member names from seshat and the spec
/* eslint-disable camelcase */
-export interface MatrixEvent {
+export interface IMatrixEvent {
type: string;
sender: string;
content: {};
@@ -27,37 +27,37 @@ export interface MatrixEvent {
roomId: string;
}
-export interface MatrixProfile {
+export interface IMatrixProfile {
avatar_url: string;
displayname: string;
}
-export interface CrawlerCheckpoint {
+export interface ICrawlerCheckpoint {
roomId: string;
token: string;
fullCrawl?: boolean;
direction: string;
}
-export interface ResultContext {
- events_before: [MatrixEvent];
- events_after: [MatrixEvent];
- profile_info: Map;
+export interface IResultContext {
+ events_before: [IMatrixEvent];
+ events_after: [IMatrixEvent];
+ profile_info: Map;
}
-export interface ResultsElement {
+export interface IResultsElement {
rank: number;
- result: MatrixEvent;
- context: ResultContext;
+ result: IMatrixEvent;
+ context: IResultContext;
}
-export interface SearchResult {
+export interface ISearchResult {
count: number;
- results: [ResultsElement];
+ results: [IResultsElement];
highlights: [string];
}
-export interface SearchArgs {
+export interface ISearchArgs {
search_term: string;
before_limit: number;
after_limit: number;
@@ -65,19 +65,19 @@ export interface SearchArgs {
room_id?: string;
}
-export interface EventAndProfile {
- event: MatrixEvent;
- profile: MatrixProfile;
+export interface IEventAndProfile {
+ event: IMatrixEvent;
+ profile: IMatrixProfile;
}
-export interface LoadArgs {
+export interface ILoadArgs {
roomId: string;
limit: number;
fromEvent?: string;
direction?: string;
}
-export interface IndexStats {
+export interface IIndexStats {
size: number;
eventCount: number;
roomCount: number;
@@ -119,13 +119,13 @@ export default abstract class BaseEventIndexManager {
* Queue up an event to be added to the index.
*
* @param {MatrixEvent} ev The event that should be added to the index.
- * @param {MatrixProfile} profile The profile of the event sender at the
+ * @param {IMatrixProfile} profile The profile of the event sender at the
* time of the event receival.
*
* @return {Promise} A promise that will resolve when the was queued up for
* addition.
*/
- async addEventToIndex(ev: MatrixEvent, profile: MatrixProfile): Promise {
+ async addEventToIndex(ev: IMatrixEvent, profile: IMatrixProfile): Promise {
throw new Error("Unimplemented");
}
@@ -160,10 +160,10 @@ export default abstract class BaseEventIndexManager {
/**
* Get statistical information of the index.
*
- * @return {Promise} A promise that will resolve to the index
+ * @return {Promise} A promise that will resolve to the index
* statistics.
*/
- async getStats(): Promise {
+ async getStats(): Promise {
throw new Error("Unimplemented");
}
@@ -203,13 +203,13 @@ export default abstract class BaseEventIndexManager {
/**
* Search the event index using the given term for matching events.
*
- * @param {SearchArgs} searchArgs The search configuration for the search,
+ * @param {ISearchArgs} searchArgs The search configuration for the search,
* sets the search term and determines the search result contents.
*
- * @return {Promise<[SearchResult]>} A promise that will resolve to an array
+ * @return {Promise<[ISearchResult]>} A promise that will resolve to an array
* of search results once the search is done.
*/
- async searchEventIndex(searchArgs: SearchArgs): Promise {
+ async searchEventIndex(searchArgs: ISearchArgs): Promise {
throw new Error("Unimplemented");
}
@@ -218,12 +218,12 @@ export default abstract class BaseEventIndexManager {
*
* This is used to add a batch of events to the index.
*
- * @param {[EventAndProfile]} events The list of events and profiles that
+ * @param {[IEventAndProfile]} events The list of events and profiles that
* should be added to the event index.
- * @param {[CrawlerCheckpoint]} checkpoint A new crawler checkpoint that
+ * @param {[ICrawlerCheckpoint]} checkpoint A new crawler checkpoint that
* should be stored in the index which should be used to continue crawling
* the room.
- * @param {[CrawlerCheckpoint]} oldCheckpoint The checkpoint that was used
+ * @param {[ICrawlerCheckpoint]} oldCheckpoint The checkpoint that was used
* to fetch the current batch of events. This checkpoint will be removed
* from the index.
*
@@ -231,9 +231,9 @@ export default abstract class BaseEventIndexManager {
* were already added to the index, false otherwise.
*/
async addHistoricEvents(
- events: [EventAndProfile],
- checkpoint: CrawlerCheckpoint | null,
- oldCheckpoint: CrawlerCheckpoint | null,
+ events: IEventAndProfile[],
+ checkpoint: ICrawlerCheckpoint | null,
+ oldCheckpoint: ICrawlerCheckpoint | null,
): Promise {
throw new Error("Unimplemented");
}
@@ -241,36 +241,36 @@ export default abstract class BaseEventIndexManager {
/**
* Add a new crawler checkpoint to the index.
*
- * @param {CrawlerCheckpoint} checkpoint The checkpoint that should be added
+ * @param {ICrawlerCheckpoint} checkpoint The checkpoint that should be added
* to the index.
*
* @return {Promise} A promise that will resolve once the checkpoint has
* been stored.
*/
- async addCrawlerCheckpoint(checkpoint: CrawlerCheckpoint): Promise {
+ async addCrawlerCheckpoint(checkpoint: ICrawlerCheckpoint): Promise {
throw new Error("Unimplemented");
}
/**
* Add a new crawler checkpoint to the index.
*
- * @param {CrawlerCheckpoint} checkpoint The checkpoint that should be
+ * @param {ICrawlerCheckpoint} checkpoint The checkpoint that should be
* removed from the index.
*
* @return {Promise} A promise that will resolve once the checkpoint has
* been removed.
*/
- async removeCrawlerCheckpoint(checkpoint: CrawlerCheckpoint): Promise {
+ async removeCrawlerCheckpoint(checkpoint: ICrawlerCheckpoint): Promise {
throw new Error("Unimplemented");
}
/**
* Load the stored checkpoints from the index.
*
- * @return {Promise<[CrawlerCheckpoint]>} A promise that will resolve to an
+ * @return {Promise<[ICrawlerCheckpoint]>} A promise that will resolve to an
* array of crawler checkpoints once they have been loaded from the index.
*/
- async loadCheckpoints(): Promise<[CrawlerCheckpoint]> {
+ async loadCheckpoints(): Promise {
throw new Error("Unimplemented");
}
@@ -286,11 +286,11 @@ export default abstract class BaseEventIndexManager {
* @param {string} args.direction The direction to which we should continue
* loading events from. This is used only if fromEvent is used as well.
*
- * @return {Promise<[EventAndProfile]>} A promise that will resolve to an
+ * @return {Promise<[IEventAndProfile]>} A promise that will resolve to an
* array of Matrix events that contain mxc URLs accompanied with the
* historic profile of the sender.
*/
- async loadFileEvents(args: LoadArgs): Promise<[EventAndProfile]> {
+ async loadFileEvents(args: ILoadArgs): Promise {
throw new Error("Unimplemented");
}
diff --git a/src/indexing/EventIndex.ts b/src/indexing/EventIndex.ts
index c36f96f368..978a2ac813 100644
--- a/src/indexing/EventIndex.ts
+++ b/src/indexing/EventIndex.ts
@@ -28,7 +28,7 @@ import { MatrixClientPeg } from "../MatrixClientPeg";
import { sleep } from "../utils/promise";
import SettingsStore from "../settings/SettingsStore";
import { SettingLevel } from "../settings/SettingLevel";
-import {CrawlerCheckpoint, LoadArgs, SearchArgs} from "./BaseEventIndexManager";
+import { ICrawlerCheckpoint, ILoadArgs, ISearchArgs } from "./BaseEventIndexManager";
// The time in ms that the crawler will wait loop iterations if there
// have not been any checkpoints to consume in the last iteration.
@@ -45,9 +45,9 @@ interface ICrawler {
* Event indexing class that wraps the platform specific event indexing.
*/
export default class EventIndex extends EventEmitter {
- private crawlerCheckpoints: CrawlerCheckpoint[] = [];
+ private crawlerCheckpoints: ICrawlerCheckpoint[] = [];
private crawler: ICrawler = null;
- private currentCheckpoint: CrawlerCheckpoint = null;
+ private currentCheckpoint: ICrawlerCheckpoint = null;
public async init() {
const indexManager = PlatformPeg.get().getEventIndexingManager();
@@ -111,14 +111,14 @@ export default class EventIndex extends EventEmitter {
const timeline = room.getLiveTimeline();
const token = timeline.getPaginationToken("b");
- const backCheckpoint: CrawlerCheckpoint = {
+ const backCheckpoint: ICrawlerCheckpoint = {
roomId: room.roomId,
token: token,
direction: "b",
fullCrawl: true,
};
- const forwardCheckpoint: CrawlerCheckpoint = {
+ const forwardCheckpoint: ICrawlerCheckpoint = {
roomId: room.roomId,
token: token,
direction: "f",
@@ -668,13 +668,13 @@ export default class EventIndex extends EventEmitter {
/**
* Search the event index using the given term for matching events.
*
- * @param {SearchArgs} searchArgs The search configuration for the search,
+ * @param {ISearchArgs} searchArgs The search configuration for the search,
* sets the search term and determines the search result contents.
*
* @return {Promise<[SearchResult]>} A promise that will resolve to an array
* of search results once the search is done.
*/
- public async search(searchArgs: SearchArgs) {
+ public async search(searchArgs: ISearchArgs) {
const indexManager = PlatformPeg.get().getEventIndexingManager();
return indexManager.searchEventIndex(searchArgs);
}
@@ -709,7 +709,7 @@ export default class EventIndex extends EventEmitter {
const client = MatrixClientPeg.get();
const indexManager = PlatformPeg.get().getEventIndexingManager();
- const loadArgs: LoadArgs = {
+ const loadArgs: ILoadArgs = {
roomId: room.roomId,
limit: limit,
};
diff --git a/src/languageHandler.tsx b/src/languageHandler.tsx
index 16950dc008..cc129fb6b9 100644
--- a/src/languageHandler.tsx
+++ b/src/languageHandler.tsx
@@ -112,7 +112,7 @@ export interface IVariables {
[key: string]: SubstitutionValue;
}
-type Tags = Record;
+export type Tags = Record;
export type TranslatedString = string | React.ReactNode;
diff --git a/src/mjolnir/BanList.ts b/src/mjolnir/BanList.ts
index 21cd5d4cf7..89eec89500 100644
--- a/src/mjolnir/BanList.ts
+++ b/src/mjolnir/BanList.ts
@@ -92,7 +92,7 @@ export class BanList {
if (!room) return;
for (const eventType of ALL_RULE_TYPES) {
- const events = room.currentState.getStateEvents(eventType, undefined);
+ const events = room.currentState.getStateEvents(eventType);
for (const ev of events) {
if (!ev.getStateKey()) continue;
diff --git a/src/rageshake/submit-rageshake.ts b/src/rageshake/submit-rageshake.ts
index 08d8ccfd13..57d60514da 100644
--- a/src/rageshake/submit-rageshake.ts
+++ b/src/rageshake/submit-rageshake.ts
@@ -86,8 +86,8 @@ async function collectBugReport(opts: IOpts = {}, gzipLogs = true) {
body.append('cross_signing_key', client.getCrossSigningId());
// add cross-signing status information
- const crossSigning = client.crypto._crossSigningInfo;
- const secretStorage = client.crypto._secretStorage;
+ const crossSigning = client.crypto.crossSigningInfo;
+ const secretStorage = client.crypto.secretStorage;
body.append("cross_signing_ready", String(await client.isCrossSigningReady()));
body.append("cross_signing_supported_by_hs",
@@ -263,7 +263,13 @@ function uint8ToString(buf: Buffer) {
return out;
}
-export async function submitFeedback(endpoint: string, label: string, comment: string, canContact = false) {
+export async function submitFeedback(
+ endpoint: string,
+ label: string,
+ comment: string,
+ canContact = false,
+ extraData: Record = {},
+) {
let version = "UNKNOWN";
try {
version = await PlatformPeg.get().getAppVersion();
@@ -279,6 +285,10 @@ export async function submitFeedback(endpoint: string, label: string, comment: s
body.append("platform", PlatformPeg.get().getHumanReadableName());
body.append("user_id", MatrixClientPeg.get()?.getUserId());
+ for (const k in extraData) {
+ body.append(k, extraData[k]);
+ }
+
await _submitReport(SdkConfig.get().bug_report_endpoint_url, body, () => {});
}
diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx
index 155d039572..6ed5e0c3d8 100644
--- a/src/settings/Settings.tsx
+++ b/src/settings/Settings.tsx
@@ -1,6 +1,6 @@
/*
Copyright 2017 Travis Ralston
-Copyright 2018, 2019, 2020 The Matrix.org Foundation C.I.C.
+Copyright 2018 - 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.
@@ -94,6 +94,9 @@ export interface ISetting {
[level: SettingLevel]: string;
};
+ // Optional description which will be shown as microCopy under SettingsFlags
+ description?: string;
+
// The supported levels are required. Preferably, use the preset arrays
// at the top of this file to define this rather than a custom array.
supportedLevels?: SettingLevel[];
@@ -127,10 +130,18 @@ export interface ISetting {
image: string; // require(...)
feedbackSubheading?: string;
feedbackLabel?: string;
+ extraSettings?: string[];
};
}
export const SETTINGS: {[setting: string]: ISetting} = {
+ "feature_report_to_moderators": {
+ isFeature: true,
+ displayName: _td("Report to moderators prototype. " +
+ "In rooms that support moderation, the `report` button will let you report abuse to room moderators"),
+ supportedLevels: LEVELS_FEATURE,
+ default: false,
+ },
"feature_spaces": {
isFeature: true,
displayName: _td("Spaces prototype. Incompatible with Communities, Communities v2 and Custom Tags. " +
@@ -167,8 +178,33 @@ export const SETTINGS: {[setting: string]: ISetting} = {
feedbackSubheading: _td("Your feedback will help make spaces better. " +
"The more detail you can go into, the better."),
feedbackLabel: "spaces-feedback",
+ extraSettings: [
+ "feature_spaces.all_rooms",
+ "feature_spaces.space_member_dms",
+ "feature_spaces.space_dm_badges",
+ ],
},
},
+ "feature_spaces.all_rooms": {
+ displayName: _td("Show all rooms in Home"),
+ supportedLevels: LEVELS_FEATURE,
+ default: true,
+ controller: new ReloadOnChangeController(),
+ },
+ "feature_spaces.space_member_dms": {
+ displayName: _td("Show people in spaces"),
+ description: _td("If disabled, you can still add Direct Messages to Personal Spaces. " +
+ "If enabled, you'll automatically see everyone who is a member of the Space."),
+ supportedLevels: LEVELS_FEATURE,
+ default: true,
+ controller: new ReloadOnChangeController(),
+ },
+ "feature_spaces.space_dm_badges": {
+ displayName: _td("Show notification badges for People in Spaces"),
+ supportedLevels: LEVELS_FEATURE,
+ default: false,
+ controller: new ReloadOnChangeController(),
+ },
"feature_dnd": {
isFeature: true,
displayName: _td("Show options to enable 'Do not disturb' mode"),
diff --git a/src/settings/SettingsStore.ts b/src/settings/SettingsStore.ts
index e1e300e185..44f3d5d838 100644
--- a/src/settings/SettingsStore.ts
+++ b/src/settings/SettingsStore.ts
@@ -248,6 +248,16 @@ export default class SettingsStore {
return _t(displayName as string);
}
+ /**
+ * Gets the translated description for a given setting
+ * @param {string} settingName The setting to look up.
+ * @return {String} The description for the setting, or null if not found.
+ */
+ public static getDescription(settingName: string) {
+ if (!SETTINGS[settingName]?.description) return null;
+ return _t(SETTINGS[settingName].description);
+ }
+
/**
* Determines if a setting is also a feature.
* @param {string} settingName The setting to look up.
diff --git a/src/stores/CommunityPrototypeStore.ts b/src/stores/CommunityPrototypeStore.ts
index 023845c9ee..a6f4574a58 100644
--- a/src/stores/CommunityPrototypeStore.ts
+++ b/src/stores/CommunityPrototypeStore.ts
@@ -107,8 +107,9 @@ export class CommunityPrototypeStore extends AsyncStoreWithClient {
const pl = generalChat.currentState.getStateEvents("m.room.power_levels", "");
if (!pl) return this.isAdminOf(communityId);
+ const plContent = pl.getContent();
- const invitePl = isNullOrUndefined(pl.invite) ? 50 : Number(pl.invite);
+ const invitePl = isNullOrUndefined(plContent.invite) ? 50 : Number(plContent.invite);
return invitePl <= myMember.powerLevel;
}
@@ -159,10 +160,16 @@ export class CommunityPrototypeStore extends AsyncStoreWithClient {
if (SettingsStore.getValue("feature_communities_v2_prototypes")) {
const data = this.matrixClient.getAccountData("im.vector.group_info." + roomId);
if (data && data.getContent()) {
- return {displayName: data.getContent().name, avatarMxc: data.getContent().avatar_url};
+ return {
+ displayName: data.getContent().name,
+ avatarMxc: data.getContent().avatar_url,
+ };
}
}
- return {displayName: room.name, avatarMxc: room.avatar_url};
+ return {
+ displayName: room.name,
+ avatarMxc: room.getMxcAvatarUrl(),
+ };
}
protected async onReady(): Promise {
diff --git a/src/stores/RoomViewStore.tsx b/src/stores/RoomViewStore.tsx
index cc3eafffcd..87978df471 100644
--- a/src/stores/RoomViewStore.tsx
+++ b/src/stores/RoomViewStore.tsx
@@ -276,7 +276,7 @@ class RoomViewStore extends Store {
const address = this.state.roomAlias || this.state.roomId;
const viaServers = this.state.viaServers || [];
try {
- await retry(() => cli.joinRoom(address, {
+ await retry(() => cli.joinRoom(address, {
viaServers,
...payload.opts,
}), NUM_JOIN_RETRY, (err) => {
diff --git a/src/stores/SpaceStore.tsx b/src/stores/SpaceStore.tsx
index 40997d30a8..e498574467 100644
--- a/src/stores/SpaceStore.tsx
+++ b/src/stores/SpaceStore.tsx
@@ -14,36 +14,44 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import {ListIteratee, Many, sortBy, throttle} from "lodash";
-import {EventType, RoomType} from "matrix-js-sdk/src/@types/event";
-import {Room} from "matrix-js-sdk/src/models/room";
-import {MatrixEvent} from "matrix-js-sdk/src/models/event";
+import { ListIteratee, Many, sortBy, throttle } from "lodash";
+import { EventType, RoomType } from "matrix-js-sdk/src/@types/event";
+import { Room } from "matrix-js-sdk/src/models/room";
+import { MatrixEvent } from "matrix-js-sdk/src/models/event";
-import {AsyncStoreWithClient} from "./AsyncStoreWithClient";
+import { AsyncStoreWithClient } from "./AsyncStoreWithClient";
import defaultDispatcher from "../dispatcher/dispatcher";
-import {ActionPayload} from "../dispatcher/payloads";
+import { ActionPayload } from "../dispatcher/payloads";
import RoomListStore from "./room-list/RoomListStore";
import SettingsStore from "../settings/SettingsStore";
import DMRoomMap from "../utils/DMRoomMap";
-import {FetchRoomFn} from "./notifications/ListNotificationState";
-import {SpaceNotificationState} from "./notifications/SpaceNotificationState";
-import {RoomNotificationStateStore} from "./notifications/RoomNotificationStateStore";
-import {DefaultTagID} from "./room-list/models";
-import {EnhancedMap, mapDiff} from "../utils/maps";
-import {setHasDiff} from "../utils/sets";
-import {ISpaceSummaryEvent, ISpaceSummaryRoom} from "../components/structures/SpaceRoomDirectory";
+import { FetchRoomFn } from "./notifications/ListNotificationState";
+import { SpaceNotificationState } from "./notifications/SpaceNotificationState";
+import { RoomNotificationStateStore } from "./notifications/RoomNotificationStateStore";
+import { DefaultTagID } from "./room-list/models";
+import { EnhancedMap, mapDiff } from "../utils/maps";
+import { setHasDiff } from "../utils/sets";
+import { ISpaceSummaryEvent, ISpaceSummaryRoom } from "../components/structures/SpaceRoomDirectory";
import RoomViewStore from "./RoomViewStore";
+import { Action } from "../dispatcher/actions";
+import { arrayHasDiff } from "../utils/arrays";
+import { objectDiff } from "../utils/objects";
+import { arrayHasOrderChange } from "../utils/arrays";
+import { reorderLexicographically } from "../utils/stringOrderField";
+
+type SpaceKey = string | symbol;
interface IState {}
const ACTIVE_SPACE_LS_KEY = "mx_active_space";
+export const HOME_SPACE = Symbol("home-space");
export const SUGGESTED_ROOMS = Symbol("suggested-rooms");
export const UPDATE_TOP_LEVEL_SPACES = Symbol("top-level-spaces");
export const UPDATE_INVITED_SPACES = Symbol("invited-spaces");
export const UPDATE_SELECTED_SPACE = Symbol("selected-space");
-// Space Room ID will be emitted when a Space's children change
+// Space Room ID/HOME_SPACE will be emitted when a Space's children change
export interface ISuggestedRoom extends ISpaceSummaryRoom {
viaServers: string[];
@@ -51,7 +59,8 @@ export interface ISuggestedRoom extends ISpaceSummaryRoom {
const MAX_SUGGESTED_ROOMS = 20;
-const getSpaceContextKey = (space?: Room) => `mx_space_context_${space?.roomId || "ALL_ROOMS"}`;
+const homeSpaceKey = SettingsStore.getValue("feature_spaces.all_rooms") ? "ALL_ROOMS" : "HOME_SPACE";
+const getSpaceContextKey = (space?: Room) => `mx_space_context_${space?.roomId || homeSpaceKey}`;
const partitionSpacesAndRooms = (arr: Room[]): [Room[], Room[]] => { // [spaces, rooms]
return arr.reduce((result, room: Room) => {
@@ -60,18 +69,18 @@ const partitionSpacesAndRooms = (arr: Room[]): [Room[], Room[]] => { // [spaces,
}, [[], []]);
};
-// For sorting space children using a validated `order`, `m.room.create`'s `origin_server_ts`, `room_id`
-export const getOrder = (order: string, creationTs: number, roomId: string): Array>> => {
- let validatedOrder: string = null;
-
- if (typeof order === "string" && Array.from(order).every((c: string) => {
+const validOrder = (order: string): string | undefined => {
+ if (typeof order === "string" && order.length <= 50 && Array.from(order).every((c: string) => {
const charCode = c.charCodeAt(0);
return charCode >= 0x20 && charCode <= 0x7E;
})) {
- validatedOrder = order;
+ return order;
}
+};
- return [validatedOrder, creationTs, roomId];
+// For sorting space children using a validated `order`, `m.room.create`'s `origin_server_ts`, `room_id`
+export const getChildOrder = (order: string, creationTs: number, roomId: string): Array>> => {
+ return [validOrder(order), creationTs, roomId];
}
const getRoomFn: FetchRoomFn = (room: Room) => {
@@ -85,16 +94,19 @@ export class SpaceStoreClass extends AsyncStoreWithClient {
// The spaces representing the roots of the various tree-like hierarchies
private rootSpaces: Room[] = [];
+ // The list of rooms not present in any currently joined spaces
+ private orphanedRooms = new Set();
// Map from room ID to set of spaces which list it as a child
private parentMap = new EnhancedMap>();
- // Map from spaceId to SpaceNotificationState instance representing that space
- private notificationStateMap = new Map();
+ // Map from SpaceKey to SpaceNotificationState instance representing that space
+ private notificationStateMap = new Map();
// Map from space key to Set of room IDs that should be shown as part of that space's filter
- private spaceFilteredRooms = new Map>();
- // The space currently selected in the Space Panel - if null then All Rooms is selected
+ private spaceFilteredRooms = new Map>();
+ // The space currently selected in the Space Panel - if null then Home is selected
private _activeSpace?: Room = null;
private _suggestedRooms: ISuggestedRoom[] = [];
private _invitedSpaces = new Set();
+ private spaceOrderLocalEchoMap = new Map();
public get invitedSpaces(): Room[] {
return Array.from(this._invitedSpaces);
@@ -133,7 +145,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient {
// if the space being selected is an invite then always view that invite
// else if the last viewed room in this space is joined then view that
// else view space home or home depending on what is being clicked on
- if (space?.getMyMembership !== "invite" &&
+ if (space?.getMyMembership() !== "invite" &&
this.matrixClient?.getRoom(roomId)?.getMyMembership() === "join"
) {
defaultDispatcher.dispatch({
@@ -214,7 +226,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient {
const roomId = ev.getStateKey();
const childRoom = this.matrixClient?.getRoom(roomId);
const createTs = childRoom?.currentState.getStateEvents(EventType.RoomCreate, "")?.getTs();
- return getOrder(ev.getContent().order, createTs, roomId);
+ return getChildOrder(ev.getContent().order, createTs, roomId);
}).map(ev => {
return this.matrixClient.getRoom(ev.getStateKey());
}).filter(room => {
@@ -251,10 +263,10 @@ export class SpaceStoreClass extends AsyncStoreWithClient {
}
public getSpaceFilteredRoomIds = (space: Room | null): Set => {
- if (!space) {
+ if (!space && SettingsStore.getValue("feature_spaces.all_rooms")) {
return new Set(this.matrixClient.getVisibleRooms().map(r => r.roomId));
}
- return this.spaceFilteredRooms.get(space.roomId) || new Set();
+ return this.spaceFilteredRooms.get(space?.roomId || HOME_SPACE) || new Set();
};
private rebuild = throttle(() => {
@@ -285,7 +297,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient {
});
});
- const [rootSpaces] = partitionSpacesAndRooms(Array.from(unseenChildren));
+ const [rootSpaces, orphanedRooms] = partitionSpacesAndRooms(Array.from(unseenChildren));
// somewhat algorithm to handle full-cycles
const detachedNodes = new Set(spaces);
@@ -326,7 +338,8 @@ export class SpaceStoreClass extends AsyncStoreWithClient {
// rootSpaces.push(space);
// });
- this.rootSpaces = rootSpaces;
+ this.orphanedRooms = new Set(orphanedRooms.map(r => r.roomId));
+ this.rootSpaces = this.sortRootSpaces(rootSpaces);
this.parentMap = backrefs;
// if the currently selected space no longer exists, remove its selection
@@ -338,14 +351,34 @@ export class SpaceStoreClass extends AsyncStoreWithClient {
this.emit(UPDATE_TOP_LEVEL_SPACES, this.spacePanelSpaces);
// build initial state of invited spaces as we would have missed the emitted events about the room at launch
- this._invitedSpaces = new Set(invitedSpaces);
+ this._invitedSpaces = new Set(this.sortRootSpaces(invitedSpaces));
this.emit(UPDATE_INVITED_SPACES, this.invitedSpaces);
}, 100, {trailing: true, leading: true});
- onSpaceUpdate = () => {
+ private onSpaceUpdate = () => {
this.rebuild();
}
+ private showInHomeSpace = (room: Room) => {
+ if (SettingsStore.getValue("feature_spaces.all_rooms")) return true;
+ if (room.isSpaceRoom()) return false;
+ return !this.parentMap.get(room.roomId)?.size // put all orphaned rooms in the Home Space
+ || DMRoomMap.shared().getUserIdForRoomId(room.roomId) // put all DMs in the Home Space
+ || RoomListStore.instance.getTagsForRoom(room).includes(DefaultTagID.Favourite) // show all favourites
+ };
+
+ // Update a given room due to its tag changing (e.g DM-ness or Fav-ness)
+ // This can only change whether it shows up in the HOME_SPACE or not
+ private onRoomUpdate = (room: Room) => {
+ if (this.showInHomeSpace(room)) {
+ this.spaceFilteredRooms.get(HOME_SPACE)?.add(room.roomId);
+ this.emit(HOME_SPACE);
+ } else if (!this.orphanedRooms.has(room.roomId)) {
+ this.spaceFilteredRooms.get(HOME_SPACE)?.delete(room.roomId);
+ this.emit(HOME_SPACE);
+ }
+ };
+
private onSpaceMembersChange = (ev: MatrixEvent) => {
// skip this update if we do not have a DM with this user
if (DMRoomMap.shared().getDMRoomsForUserId(ev.getStateKey()).length < 1) return;
@@ -359,6 +392,18 @@ export class SpaceStoreClass extends AsyncStoreWithClient {
const oldFilteredRooms = this.spaceFilteredRooms;
this.spaceFilteredRooms = new Map();
+ if (!SettingsStore.getValue("feature_spaces.all_rooms")) {
+ // put all room invites in the Home Space
+ const invites = visibleRooms.filter(r => !r.isSpaceRoom() && r.getMyMembership() === "invite");
+ this.spaceFilteredRooms.set(HOME_SPACE, new Set(invites.map(room => room.roomId)));
+
+ visibleRooms.forEach(room => {
+ if (this.showInHomeSpace(room)) {
+ this.spaceFilteredRooms.get(HOME_SPACE).add(room.roomId);
+ }
+ });
+ }
+
this.rootSpaces.forEach(s => {
// traverse each space tree in DFS to build up the supersets as you go up,
// reusing results from like subtrees.
@@ -374,13 +419,15 @@ export class SpaceStoreClass extends AsyncStoreWithClient {
const roomIds = new Set(childRooms.map(r => r.roomId));
const space = this.matrixClient?.getRoom(spaceId);
- // Add relevant DMs
- space?.getMembers().forEach(member => {
- if (member.membership !== "join" && member.membership !== "invite") return;
- DMRoomMap.shared().getDMRoomsForUserId(member.userId).forEach(roomId => {
- roomIds.add(roomId);
+ if (SettingsStore.getValue("feature_spaces.space_member_dms")) {
+ // Add relevant DMs
+ space?.getMembers().forEach(member => {
+ if (member.membership !== "join" && member.membership !== "invite") return;
+ DMRoomMap.shared().getDMRoomsForUserId(member.userId).forEach(roomId => {
+ roomIds.add(roomId);
+ });
});
- });
+ }
const newPath = new Set(parentPath).add(spaceId);
childSpaces.forEach(childSpace => {
@@ -406,6 +453,8 @@ export class SpaceStoreClass extends AsyncStoreWithClient {
// Update NotificationStates
this.getNotificationState(s)?.setRooms(visibleRooms.filter(room => {
if (roomIds.has(room.roomId)) {
+ if (s !== HOME_SPACE && SettingsStore.getValue("feature_spaces.space_dm_badges")) return true;
+
return !DMRoomMap.shared().getUserIdForRoomId(room.roomId)
|| RoomListStore.instance.getTagsForRoom(room).includes(DefaultTagID.Favourite);
}
@@ -423,8 +472,14 @@ export class SpaceStoreClass extends AsyncStoreWithClient {
parent = this.rootSpaces.find(s => this.spaceFilteredRooms.get(s.roomId)?.has(roomId));
}
if (!parent) {
- const parents = Array.from(this.parentMap.get(roomId) || []);
- parent = parents.find(p => this.matrixClient.getRoom(p));
+ const parentIds = Array.from(this.parentMap.get(roomId) || []);
+ for (const parentId of parentIds) {
+ const room = this.matrixClient.getRoom(parentId);
+ if (room) {
+ parent = room;
+ break;
+ }
+ }
}
// don't trigger a context switch when we are switching a space to match the chosen room
@@ -472,6 +527,14 @@ export class SpaceStoreClass extends AsyncStoreWithClient {
}
};
+ private notifyIfOrderChanged(): void {
+ const rootSpaces = this.sortRootSpaces(this.rootSpaces);
+ if (arrayHasOrderChange(this.rootSpaces, rootSpaces)) {
+ this.rootSpaces = rootSpaces;
+ this.emit(UPDATE_TOP_LEVEL_SPACES, this.spacePanelSpaces);
+ }
+ }
+
private onRoomState = (ev: MatrixEvent) => {
const room = this.matrixClient.getRoom(ev.getRoomId());
if (!room) return;
@@ -489,6 +552,8 @@ export class SpaceStoreClass extends AsyncStoreWithClient {
// TODO confirm this after implementing parenting behaviour
if (room.isSpaceRoom()) {
this.onSpaceUpdate();
+ } else if (!SettingsStore.getValue("feature_spaces.all_rooms")) {
+ this.onRoomUpdate(room);
}
this.emit(room.roomId);
break;
@@ -501,8 +566,47 @@ export class SpaceStoreClass extends AsyncStoreWithClient {
}
};
+ private onRoomAccountData = (ev: MatrixEvent, room: Room, lastEv?: MatrixEvent) => {
+ if (!room.isSpaceRoom()) return;
+
+ if (ev.getType() === EventType.SpaceOrder) {
+ this.spaceOrderLocalEchoMap.delete(room.roomId); // clear any local echo
+ const order = ev.getContent()?.order;
+ const lastOrder = lastEv?.getContent()?.order;
+ if (order !== lastOrder) {
+ this.notifyIfOrderChanged();
+ }
+ } else if (ev.getType() === EventType.Tag && !SettingsStore.getValue("feature_spaces.all_rooms")) {
+ // If the room was in favourites and now isn't or the opposite then update its position in the trees
+ const oldTags = lastEv?.getContent()?.tags || {};
+ const newTags = ev.getContent()?.tags || {};
+ if (!!oldTags[DefaultTagID.Favourite] !== !!newTags[DefaultTagID.Favourite]) {
+ this.onRoomUpdate(room);
+ }
+ }
+ }
+
+ private onAccountData = (ev: MatrixEvent, lastEvent: MatrixEvent) => {
+ if (ev.getType() === EventType.Direct) {
+ const lastContent = lastEvent.getContent();
+ const content = ev.getContent();
+
+ const diff = objectDiff>(lastContent, content);
+ // filter out keys which changed by reference only by checking whether the sets differ
+ const changed = diff.changed.filter(k => arrayHasDiff(lastContent[k], content[k]));
+ // DM tag changes, refresh relevant rooms
+ new Set([...diff.added, ...diff.removed, ...changed]).forEach(roomId => {
+ const room = this.matrixClient?.getRoom(roomId);
+ if (room) {
+ this.onRoomUpdate(room);
+ }
+ });
+ }
+ };
+
protected async reset() {
this.rootSpaces = [];
+ this.orphanedRooms = new Set();
this.parentMap = new EnhancedMap();
this.notificationStateMap = new Map();
this.spaceFilteredRooms = new Map();
@@ -516,7 +620,11 @@ export class SpaceStoreClass extends AsyncStoreWithClient {
if (this.matrixClient) {
this.matrixClient.removeListener("Room", this.onRoom);
this.matrixClient.removeListener("Room.myMembership", this.onRoom);
+ this.matrixClient.removeListener("Room.accountData", this.onRoomAccountData);
this.matrixClient.removeListener("RoomState.events", this.onRoomState);
+ if (!SettingsStore.getValue("feature_spaces.all_rooms")) {
+ this.matrixClient.removeListener("accountData", this.onAccountData);
+ }
}
await this.reset();
}
@@ -525,7 +633,11 @@ export class SpaceStoreClass extends AsyncStoreWithClient {
if (!SettingsStore.getValue("feature_spaces")) return;
this.matrixClient.on("Room", this.onRoom);
this.matrixClient.on("Room.myMembership", this.onRoom);
+ this.matrixClient.on("Room.accountData", this.onRoomAccountData);
this.matrixClient.on("RoomState.events", this.onRoomState);
+ if (!SettingsStore.getValue("feature_spaces.all_rooms")) {
+ this.matrixClient.on("accountData", this.onAccountData);
+ }
await this.onSpaceUpdate(); // trigger an initial update
@@ -550,7 +662,10 @@ export class SpaceStoreClass extends AsyncStoreWithClient {
// Don't context switch when navigating to the space room
// as it will cause you to end up in the wrong room
this.setActiveSpace(room, false);
- } else if (this.activeSpace && !this.getSpaceFilteredRoomIds(this.activeSpace).has(roomId)) {
+ } else if (
+ (!SettingsStore.getValue("feature_spaces.all_rooms") || this.activeSpace) &&
+ !this.getSpaceFilteredRoomIds(this.activeSpace).has(roomId)
+ ) {
this.switchToRelatedSpace(roomId);
}
@@ -565,10 +680,16 @@ export class SpaceStoreClass extends AsyncStoreWithClient {
this.setActiveSpace(null, false);
}
break;
+ case Action.SwitchSpace:
+ if (payload.num === 0) {
+ this.setActiveSpace(null);
+ } else if (this.spacePanelSpaces.length >= payload.num) {
+ this.setActiveSpace(this.spacePanelSpaces[payload.num - 1]);
+ }
}
}
- public getNotificationState(key: string): SpaceNotificationState {
+ public getNotificationState(key: SpaceKey): SpaceNotificationState {
if (this.notificationStateMap.has(key)) {
return this.notificationStateMap.get(key);
}
@@ -599,6 +720,38 @@ export class SpaceStoreClass extends AsyncStoreWithClient {
}
childSpaces.forEach(s => this.traverseSpace(s.roomId, fn, includeRooms, newPath));
}
+
+ private getSpaceTagOrdering = (space: Room): string | undefined => {
+ if (this.spaceOrderLocalEchoMap.has(space.roomId)) return this.spaceOrderLocalEchoMap.get(space.roomId);
+ return validOrder(space.getAccountData(EventType.SpaceOrder)?.getContent()?.order);
+ };
+
+ private sortRootSpaces(spaces: Room[]): Room[] {
+ return sortBy(spaces, [this.getSpaceTagOrdering, "roomId"]);
+ }
+
+ private async setRootSpaceOrder(space: Room, order: string): Promise {
+ this.spaceOrderLocalEchoMap.set(space.roomId, order);
+ try {
+ await this.matrixClient.setRoomAccountData(space.roomId, EventType.SpaceOrder, { order });
+ } catch (e) {
+ console.warn("Failed to set root space order", e);
+ if (this.spaceOrderLocalEchoMap.get(space.roomId) === order) {
+ this.spaceOrderLocalEchoMap.delete(space.roomId);
+ }
+ }
+ }
+
+ public moveRootSpace(fromIndex: number, toIndex: number): void {
+ const currentOrders = this.rootSpaces.map(this.getSpaceTagOrdering);
+ const changes = reorderLexicographically(currentOrders, fromIndex, toIndex);
+
+ changes.forEach(({ index, order }) => {
+ this.setRootSpaceOrder(this.rootSpaces[index], order);
+ });
+
+ this.notifyIfOrderChanged();
+ }
}
export default class SpaceStore {
diff --git a/src/stores/room-list/SpaceWatcher.ts b/src/stores/room-list/SpaceWatcher.ts
index 0b1b78bc75..a1f7786578 100644
--- a/src/stores/room-list/SpaceWatcher.ts
+++ b/src/stores/room-list/SpaceWatcher.ts
@@ -19,6 +19,7 @@ import { Room } from "matrix-js-sdk/src/models/room";
import { RoomListStoreClass } from "./RoomListStore";
import { SpaceFilterCondition } from "./filters/SpaceFilterCondition";
import SpaceStore, { UPDATE_SELECTED_SPACE } from "../SpaceStore";
+import SettingsStore from "../../settings/SettingsStore";
/**
* Watches for changes in spaces to manage the filter on the provided RoomListStore
@@ -28,6 +29,11 @@ export class SpaceWatcher {
private activeSpace: Room = SpaceStore.instance.activeSpace;
constructor(private store: RoomListStoreClass) {
+ if (!SettingsStore.getValue("feature_spaces.all_rooms")) {
+ this.filter = new SpaceFilterCondition();
+ this.updateFilter();
+ store.addFilter(this.filter);
+ }
SpaceStore.instance.on(UPDATE_SELECTED_SPACE, this.onSelectedSpaceUpdated);
}
@@ -35,7 +41,7 @@ export class SpaceWatcher {
this.activeSpace = activeSpace;
if (this.filter) {
- if (activeSpace) {
+ if (activeSpace || !SettingsStore.getValue("feature_spaces.all_rooms")) {
this.updateFilter();
} else {
this.store.removeFilter(this.filter);
@@ -49,9 +55,11 @@ export class SpaceWatcher {
};
private updateFilter = () => {
- SpaceStore.instance.traverseSpace(this.activeSpace.roomId, roomId => {
- this.store.matrixClient?.getRoom(roomId)?.loadMembersIfNeeded();
- });
+ if (this.activeSpace) {
+ SpaceStore.instance.traverseSpace(this.activeSpace.roomId, roomId => {
+ this.store.matrixClient?.getRoom(roomId)?.loadMembersIfNeeded();
+ });
+ }
this.filter.updateSpace(this.activeSpace);
};
}
diff --git a/src/stores/room-list/filters/SpaceFilterCondition.ts b/src/stores/room-list/filters/SpaceFilterCondition.ts
index 6a06bee0d8..0e6965d843 100644
--- a/src/stores/room-list/filters/SpaceFilterCondition.ts
+++ b/src/stores/room-list/filters/SpaceFilterCondition.ts
@@ -19,7 +19,7 @@ import { Room } from "matrix-js-sdk/src/models/room";
import { FILTER_CHANGED, FilterKind, IFilterCondition } from "./IFilterCondition";
import { IDestroyable } from "../../../utils/IDestroyable";
-import SpaceStore from "../../SpaceStore";
+import SpaceStore, { HOME_SPACE } from "../../SpaceStore";
import { setHasDiff } from "../../../utils/sets";
/**
@@ -29,7 +29,7 @@ import { setHasDiff } from "../../../utils/sets";
* + All DMs
*/
export class SpaceFilterCondition extends EventEmitter implements IFilterCondition, IDestroyable {
- private roomIds = new Set();
+ private roomIds = new Set();
private space: Room = null;
public get kind(): FilterKind {
@@ -55,12 +55,10 @@ export class SpaceFilterCondition extends EventEmitter implements IFilterConditi
}
};
- private getSpaceEventKey = (space: Room) => space.roomId;
+ private getSpaceEventKey = (space: Room | null) => space ? space.roomId : HOME_SPACE;
public updateSpace(space: Room) {
- if (this.space) {
- SpaceStore.instance.off(this.getSpaceEventKey(this.space), this.onStoreUpdate);
- }
+ SpaceStore.instance.off(this.getSpaceEventKey(this.space), this.onStoreUpdate);
SpaceStore.instance.on(this.getSpaceEventKey(this.space = space), this.onStoreUpdate);
this.onStoreUpdate(); // initial update from the change to the space
}
diff --git a/src/utils/EditorStateTransfer.js b/src/utils/EditorStateTransfer.ts
similarity index 52%
rename from src/utils/EditorStateTransfer.js
rename to src/utils/EditorStateTransfer.ts
index c7782a9ea8..ba303f9b73 100644
--- a/src/utils/EditorStateTransfer.js
+++ b/src/utils/EditorStateTransfer.ts
@@ -1,5 +1,5 @@
/*
-Copyright 2019 The Matrix.org Foundation C.I.C.
+Copyright 2019 - 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -14,36 +14,40 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
+import { MatrixEvent } from "matrix-js-sdk/src/models/event";
+
+import { SerializedPart } from "../editor/parts";
+import { Caret } from "../editor/caret";
+
/**
* Used while editing, to pass the event, and to preserve editor state
* from one editor instance to another when remounting the editor
* upon receiving the remote echo for an unsent event.
*/
export default class EditorStateTransfer {
- constructor(event) {
- this._event = event;
- this._serializedParts = null;
- this.caret = null;
+ private serializedParts: SerializedPart[] = null;
+ private caret: Caret = null;
+
+ constructor(private readonly event: MatrixEvent) {}
+
+ public setEditorState(caret: Caret, serializedParts: SerializedPart[]) {
+ this.caret = caret;
+ this.serializedParts = serializedParts;
}
- setEditorState(caret, serializedParts) {
- this._caret = caret;
- this._serializedParts = serializedParts;
+ public hasEditorState() {
+ return !!this.serializedParts;
}
- hasEditorState() {
- return !!this._serializedParts;
+ public getSerializedParts() {
+ return this.serializedParts;
}
- getSerializedParts() {
- return this._serializedParts;
+ public getCaret() {
+ return this.caret;
}
- getCaret() {
- return this._caret;
- }
-
- getEvent() {
- return this._event;
+ public getEvent() {
+ return this.event;
}
}
diff --git a/src/utils/ErrorUtils.js b/src/utils/ErrorUtils.tsx
similarity index 84%
rename from src/utils/ErrorUtils.js
rename to src/utils/ErrorUtils.tsx
index b5bd5b0af0..c39ee21f09 100644
--- a/src/utils/ErrorUtils.js
+++ b/src/utils/ErrorUtils.tsx
@@ -1,5 +1,5 @@
/*
-Copyright 2018 New Vector Ltd
+Copyright 2018 - 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,7 +14,10 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import { _t, _td } from '../languageHandler';
+import React, { ReactNode } from "react";
+import { MatrixError } from "matrix-js-sdk/src/http-api";
+
+import { _t, _td, Tags, TranslatedString } from '../languageHandler';
/**
* Produce a translated error message for a
@@ -30,7 +33,12 @@ import { _t, _td } from '../languageHandler';
* for any tags in the strings apart from 'a'
* @returns {*} Translated string or react component
*/
-export function messageForResourceLimitError(limitType, adminContact, strings, extraTranslations) {
+export function messageForResourceLimitError(
+ limitType: string,
+ adminContact: string,
+ strings: Record,
+ extraTranslations?: Tags,
+): TranslatedString {
let errString = strings[limitType];
if (errString === undefined) errString = strings[''];
@@ -49,7 +57,7 @@ export function messageForResourceLimitError(limitType, adminContact, strings, e
}
}
-export function messageForSyncError(err) {
+export function messageForSyncError(err: MatrixError | Error): ReactNode {
if (err.errcode === 'M_RESOURCE_LIMIT_EXCEEDED') {
const limitError = messageForResourceLimitError(
err.data.limit_type,
diff --git a/src/utils/EventUtils.js b/src/utils/EventUtils.ts
similarity index 85%
rename from src/utils/EventUtils.js
rename to src/utils/EventUtils.ts
index be21896417..3d9c60d9cd 100644
--- a/src/utils/EventUtils.js
+++ b/src/utils/EventUtils.ts
@@ -1,5 +1,5 @@
/*
-Copyright 2019 New Vector Ltd
+Copyright 2019 - 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -14,9 +14,12 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import { EventStatus } from 'matrix-js-sdk/src/models/event';
-import {MatrixClientPeg} from '../MatrixClientPeg';
+import { Room } from 'matrix-js-sdk/src/models/room';
+import { MatrixEvent, EventStatus } from 'matrix-js-sdk/src/models/event';
+
+import { MatrixClientPeg } from '../MatrixClientPeg';
import shouldHideEvent from "../shouldHideEvent";
+
/**
* Returns whether an event should allow actions like reply, reactions, edit, etc.
* which effectively checks whether it's a regular message that has been sent and that we
@@ -25,7 +28,7 @@ import shouldHideEvent from "../shouldHideEvent";
* @param {MatrixEvent} mxEvent The event to check
* @returns {boolean} true if actionable
*/
-export function isContentActionable(mxEvent) {
+export function isContentActionable(mxEvent: MatrixEvent): boolean {
const { status: eventStatus } = mxEvent;
// status is SENT before remote-echo, null after
@@ -45,7 +48,7 @@ export function isContentActionable(mxEvent) {
return false;
}
-export function canEditContent(mxEvent) {
+export function canEditContent(mxEvent: MatrixEvent): boolean {
if (mxEvent.status === EventStatus.CANCELLED || mxEvent.getType() !== "m.room.message" || mxEvent.isRedacted()) {
return false;
}
@@ -56,7 +59,7 @@ export function canEditContent(mxEvent) {
mxEvent.getSender() === MatrixClientPeg.get().getUserId();
}
-export function canEditOwnEvent(mxEvent) {
+export function canEditOwnEvent(mxEvent: MatrixEvent): boolean {
// for now we only allow editing
// your own events. So this just call through
// In the future though, moderators will be able to
@@ -67,7 +70,7 @@ export function canEditOwnEvent(mxEvent) {
}
const MAX_JUMP_DISTANCE = 100;
-export function findEditableEvent(room, isForward, fromEventId = undefined) {
+export function findEditableEvent(room: Room, isForward: boolean, fromEventId: string = undefined): MatrixEvent {
const liveTimeline = room.getLiveTimeline();
const events = liveTimeline.getEvents().concat(room.getPendingEvents());
const maxIdx = events.length - 1;
diff --git a/src/utils/IdentityServerUtils.js b/src/utils/IdentityServerUtils.ts
similarity index 82%
rename from src/utils/IdentityServerUtils.js
rename to src/utils/IdentityServerUtils.ts
index 5ece308954..2476adca19 100644
--- a/src/utils/IdentityServerUtils.js
+++ b/src/utils/IdentityServerUtils.ts
@@ -1,5 +1,5 @@
/*
-Copyright 2019 The Matrix.org Foundation C.I.C.
+Copyright 2019 - 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -15,14 +15,15 @@ limitations under the License.
*/
import { SERVICE_TYPES } from 'matrix-js-sdk/src/service-types';
+
import SdkConfig from '../SdkConfig';
import {MatrixClientPeg} from '../MatrixClientPeg';
-export function getDefaultIdentityServerUrl() {
+export function getDefaultIdentityServerUrl(): string {
return SdkConfig.get()['validated_server_config']['isUrl'];
}
-export function useDefaultIdentityServer() {
+export function useDefaultIdentityServer(): void {
const url = getDefaultIdentityServerUrl();
// Account data change will update localstorage, client, etc through dispatcher
MatrixClientPeg.get().setAccountData("m.identity_server", {
@@ -30,7 +31,7 @@ export function useDefaultIdentityServer() {
});
}
-export async function doesIdentityServerHaveTerms(fullUrl) {
+export async function doesIdentityServerHaveTerms(fullUrl: string): Promise {
let terms;
try {
terms = await MatrixClientPeg.get().getTerms(SERVICE_TYPES.IS, fullUrl);
@@ -46,7 +47,7 @@ export async function doesIdentityServerHaveTerms(fullUrl) {
return terms && terms["policies"] && (Object.keys(terms["policies"]).length > 0);
}
-export function doesAccountDataHaveIdentityServer() {
+export function doesAccountDataHaveIdentityServer(): boolean {
const event = MatrixClientPeg.get().getAccountData("m.identity_server");
return event && event.getContent() && event.getContent()['base_url'];
}
diff --git a/src/utils/MessageDiffUtils.js b/src/utils/MessageDiffUtils.tsx
similarity index 87%
rename from src/utils/MessageDiffUtils.js
rename to src/utils/MessageDiffUtils.tsx
index 7398173fdd..5ee9970ec2 100644
--- a/src/utils/MessageDiffUtils.js
+++ b/src/utils/MessageDiffUtils.tsx
@@ -1,5 +1,5 @@
/*
-Copyright 2019 The Matrix.org Foundation C.I.C.
+Copyright 2019 - 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -14,31 +14,33 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import React from 'react';
+import React, { ReactNode } from 'react';
import classNames from 'classnames';
-import DiffMatchPatch from 'diff-match-patch';
-import {DiffDOM} from "diff-dom";
-import { checkBlockNode, bodyToHtml } from "../HtmlUtils";
+import { diff_match_patch as DiffMatchPatch } from 'diff-match-patch';
+import { DiffDOM, IDiff } from "diff-dom";
+import { IContent } from "matrix-js-sdk/src/models/event";
+
+import { bodyToHtml, checkBlockNode, IOptsReturnString } from "../HtmlUtils";
const decodeEntities = (function() {
let textarea = null;
- return function(string) {
+ return function(str: string): string {
if (!textarea) {
textarea = document.createElement("textarea");
}
- textarea.innerHTML = string;
+ textarea.innerHTML = str;
return textarea.value;
};
})();
-function textToHtml(text) {
+function textToHtml(text: string): string {
const container = document.createElement("div");
container.textContent = text;
return container.innerHTML;
}
-function getSanitizedHtmlBody(content) {
- const opts = {
+function getSanitizedHtmlBody(content: IContent): string {
+ const opts: IOptsReturnString = {
stripReplyFallback: true,
returnString: true,
};
@@ -57,21 +59,21 @@ function getSanitizedHtmlBody(content) {
}
}
-function wrapInsertion(child) {
+function wrapInsertion(child: Node): HTMLElement {
const wrapper = document.createElement(checkBlockNode(child) ? "div" : "span");
wrapper.className = "mx_EditHistoryMessage_insertion";
wrapper.appendChild(child);
return wrapper;
}
-function wrapDeletion(child) {
+function wrapDeletion(child: Node): HTMLElement {
const wrapper = document.createElement(checkBlockNode(child) ? "div" : "span");
wrapper.className = "mx_EditHistoryMessage_deletion";
wrapper.appendChild(child);
return wrapper;
}
-function findRefNodes(root, route, isAddition) {
+function findRefNodes(root: Node, route: number[], isAddition = false) {
let refNode = root;
let refParentNode;
const end = isAddition ? route.length - 1 : route.length;
@@ -79,7 +81,7 @@ function findRefNodes(root, route, isAddition) {
refParentNode = refNode;
refNode = refNode.childNodes[route[i]];
}
- return {refNode, refParentNode};
+ return { refNode, refParentNode };
}
function diffTreeToDOM(desc) {
@@ -101,7 +103,7 @@ function diffTreeToDOM(desc) {
}
}
-function insertBefore(parent, nextSibling, child) {
+function insertBefore(parent: Node, nextSibling: Node | null, child: Node): void {
if (nextSibling) {
parent.insertBefore(child, nextSibling);
} else {
@@ -109,7 +111,7 @@ function insertBefore(parent, nextSibling, child) {
}
}
-function isRouteOfNextSibling(route1, route2) {
+function isRouteOfNextSibling(route1: number[], route2: number[]): boolean {
// routes are arrays with indices,
// to be interpreted as a path in the dom tree
@@ -127,7 +129,7 @@ function isRouteOfNextSibling(route1, route2) {
return route2[lastD1Idx] >= route1[lastD1Idx];
}
-function adjustRoutes(diff, remainingDiffs) {
+function adjustRoutes(diff: IDiff, remainingDiffs: IDiff[]): void {
if (diff.action === "removeTextElement" || diff.action === "removeElement") {
// as removed text is not removed from the html, but marked as deleted,
// we need to readjust indices that assume the current node has been removed.
@@ -140,11 +142,11 @@ function adjustRoutes(diff, remainingDiffs) {
}
}
-function stringAsTextNode(string) {
+function stringAsTextNode(string: string): Text {
return document.createTextNode(decodeEntities(string));
}
-function renderDifferenceInDOM(originalRootNode, diff, diffMathPatch) {
+function renderDifferenceInDOM(originalRootNode: Node, diff: IDiff, diffMathPatch: DiffMatchPatch): void {
const {refNode, refParentNode} = findRefNodes(originalRootNode, diff.route);
switch (diff.action) {
case "replaceElement": {
@@ -171,7 +173,7 @@ function renderDifferenceInDOM(originalRootNode, diff, diffMathPatch) {
diffMathPatch.diff_cleanupSemantic(textDiffs);
const container = document.createElement("span");
for (const [modifier, text] of textDiffs) {
- let textDiffNode = stringAsTextNode(text);
+ let textDiffNode: Node = stringAsTextNode(text);
if (modifier < 0) {
textDiffNode = wrapDeletion(textDiffNode);
} else if (modifier > 0) {
@@ -201,7 +203,7 @@ function renderDifferenceInDOM(originalRootNode, diff, diffMathPatch) {
case "addAttribute":
case "modifyAttribute": {
const delNode = wrapDeletion(refNode.cloneNode(true));
- const updatedNode = refNode.cloneNode(true);
+ const updatedNode = refNode.cloneNode(true) as HTMLElement;
if (diff.action === "addAttribute" || diff.action === "modifyAttribute") {
updatedNode.setAttribute(diff.name, diff.newValue);
} else {
@@ -220,12 +222,12 @@ function renderDifferenceInDOM(originalRootNode, diff, diffMathPatch) {
}
}
-function routeIsEqual(r1, r2) {
+function routeIsEqual(r1: number[], r2: number[]): boolean {
return r1.length === r2.length && !r1.some((e, i) => e !== r2[i]);
}
// workaround for https://github.com/fiduswriter/diffDOM/issues/90
-function filterCancelingOutDiffs(originalDiffActions) {
+function filterCancelingOutDiffs(originalDiffActions: IDiff[]): IDiff[] {
const diffActions = originalDiffActions.slice();
for (let i = 0; i < diffActions.length; ++i) {
@@ -252,7 +254,7 @@ function filterCancelingOutDiffs(originalDiffActions) {
* @param {object} editContent the content for the edit message
* @return {object} a react element similar to what `bodyToHtml` returns
*/
-export function editBodyDiffToHtml(originalContent, editContent) {
+export function editBodyDiffToHtml(originalContent: IContent, editContent: IContent): ReactNode {
// wrap the body in a div, DiffDOM needs a root element
const originalBody = `
${getSanitizedHtmlBody(originalContent)}
`;
const editBody = `
${getSanitizedHtmlBody(editContent)}
`;
diff --git a/src/utils/MultiInviter.js b/src/utils/MultiInviter.ts
similarity index 66%
rename from src/utils/MultiInviter.js
rename to src/utils/MultiInviter.ts
index 78f956b91b..f6a994484e 100644
--- a/src/utils/MultiInviter.js
+++ b/src/utils/MultiInviter.ts
@@ -1,6 +1,5 @@
/*
-Copyright 2016 OpenMarket Ltd
-Copyright 2017, 2018 New Vector Ltd
+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.
@@ -15,23 +14,51 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import {MatrixClientPeg} from '../MatrixClientPeg';
-import {getAddressType} from '../UserAddress';
+import { MatrixError } from "matrix-js-sdk/src/http-api";
+
+import { MatrixClientPeg } from '../MatrixClientPeg';
+import { AddressType, getAddressType } from '../UserAddress';
import GroupStore from '../stores/GroupStore';
-import {_t} from "../languageHandler";
-import * as sdk from "../index";
+import { _t } from "../languageHandler";
import Modal from "../Modal";
import SettingsStore from "../settings/SettingsStore";
-import {defer} from "./promise";
+import { defer, IDeferred } from "./promise";
+import AskInviteAnywayDialog from "../components/views/dialogs/AskInviteAnywayDialog";
+
+export enum InviteState {
+ Invited = "invited",
+ Error = "error",
+}
+
+interface IError {
+ errorText: string;
+ errcode: string;
+}
+
+const UNKNOWN_PROFILE_ERRORS = ['M_NOT_FOUND', 'M_USER_NOT_FOUND', 'M_PROFILE_UNDISCLOSED', 'M_PROFILE_NOT_FOUND'];
+
+export type CompletionStates = Record;
/**
* Invites multiple addresses to a room or group, handling rate limiting from the server
*/
export default class MultiInviter {
+ private readonly roomId?: string;
+ private readonly groupId?: string;
+
+ private canceled = false;
+ private addresses: string[] = [];
+ private busy = false;
+ private _fatal = false;
+ private completionStates: CompletionStates = {}; // State of each address (invited or error)
+ private errors: Record = {}; // { address: {errorText, errcode} }
+ private deferred: IDeferred = null;
+ private reason: string = null;
+
/**
* @param {string} targetId The ID of the room or group to invite to
*/
- constructor(targetId) {
+ constructor(targetId: string) {
if (targetId[0] === '+') {
this.roomId = null;
this.groupId = targetId;
@@ -39,41 +66,38 @@ export default class MultiInviter {
this.roomId = targetId;
this.groupId = null;
}
+ }
- this.canceled = false;
- this.addrs = [];
- this.busy = false;
- this.completionStates = {}; // State of each address (invited or error)
- this.errors = {}; // { address: {errorText, errcode} }
- this.deferred = null;
+ public get fatal() {
+ return this._fatal;
}
/**
* Invite users to this room. This may only be called once per
* instance of the class.
*
- * @param {array} addrs Array of addresses to invite
+ * @param {array} addresses Array of addresses to invite
* @param {string} reason Reason for inviting (optional)
* @returns {Promise} Resolved when all invitations in the queue are complete
*/
- invite(addrs, reason) {
- if (this.addrs.length > 0) {
+ public invite(addresses, reason?: string): Promise {
+ if (this.addresses.length > 0) {
throw new Error("Already inviting/invited");
}
- this.addrs.push(...addrs);
+ this.addresses.push(...addresses);
this.reason = reason;
- for (const addr of this.addrs) {
+ for (const addr of this.addresses) {
if (getAddressType(addr) === null) {
- this.completionStates[addr] = 'error';
+ this.completionStates[addr] = InviteState.Error;
this.errors[addr] = {
errcode: 'M_INVALID',
errorText: _t('Unrecognised address'),
};
}
}
- this.deferred = defer();
- this._inviteMore(0);
+ this.deferred = defer();
+ this.inviteMore(0);
return this.deferred.promise;
}
@@ -81,33 +105,36 @@ export default class MultiInviter {
/**
* Stops inviting. Causes promises returned by invite() to be rejected.
*/
- cancel() {
+ public cancel(): void {
if (!this.busy) return;
- this._canceled = true;
+ this.canceled = true;
this.deferred.reject(new Error('canceled'));
}
- getCompletionState(addr) {
+ public getCompletionState(addr: string): InviteState {
return this.completionStates[addr];
}
- getErrorText(addr) {
+ public getErrorText(addr: string): string {
return this.errors[addr] ? this.errors[addr].errorText : null;
}
- async _inviteToRoom(roomId, addr, ignoreProfile) {
+ private async inviteToRoom(roomId: string, addr: string, ignoreProfile = false): Promise<{}> {
const addrType = getAddressType(addr);
- if (addrType === 'email') {
+ if (addrType === AddressType.Email) {
return MatrixClientPeg.get().inviteByEmail(roomId, addr);
- } else if (addrType === 'mx-user-id') {
+ } else if (addrType === AddressType.MatrixUserId) {
const room = MatrixClientPeg.get().getRoom(roomId);
if (!room) throw new Error("Room not found");
const member = room.getMember(addr);
if (member && ['join', 'invite'].includes(member.membership)) {
- throw {errcode: "RIOT.ALREADY_IN_ROOM", error: "Member already invited"};
+ throw new new MatrixError({
+ errcode: "RIOT.ALREADY_IN_ROOM",
+ error: "Member already invited",
+ });
}
if (!ignoreProfile && SettingsStore.getValue("promptBeforeInviteUnknownUsers", this.roomId)) {
@@ -124,28 +151,28 @@ export default class MultiInviter {
}
}
- _doInvite(address, ignoreProfile) {
- return new Promise((resolve, reject) => {
+ private doInvite(address: string, ignoreProfile = false): Promise {
+ return new Promise((resolve, reject) => {
console.log(`Inviting ${address}`);
let doInvite;
if (this.groupId !== null) {
doInvite = GroupStore.inviteUserToGroup(this.groupId, address);
} else {
- doInvite = this._inviteToRoom(this.roomId, address, ignoreProfile);
+ doInvite = this.inviteToRoom(this.roomId, address, ignoreProfile);
}
doInvite.then(() => {
- if (this._canceled) {
+ if (this.canceled) {
return;
}
- this.completionStates[address] = 'invited';
+ this.completionStates[address] = InviteState.Invited;
delete this.errors[address];
resolve();
}).catch((err) => {
- if (this._canceled) {
+ if (this.canceled) {
return;
}
@@ -161,7 +188,7 @@ export default class MultiInviter {
} else if (err.errcode === 'M_LIMIT_EXCEEDED') {
// we're being throttled so wait a bit & try again
setTimeout(() => {
- this._doInvite(address, ignoreProfile).then(resolve, reject);
+ this.doInvite(address, ignoreProfile).then(resolve, reject);
}, 5000);
return;
} else if (['M_NOT_FOUND', 'M_USER_NOT_FOUND'].includes(err.errcode)) {
@@ -171,7 +198,7 @@ export default class MultiInviter {
} else if (err.errcode === 'M_PROFILE_NOT_FOUND' && !ignoreProfile) {
// Invite without the profile check
console.warn(`User ${address} does not have a profile - inviting anyways automatically`);
- this._doInvite(address, true).then(resolve, reject);
+ this.doInvite(address, true).then(resolve, reject);
} else if (err.errcode === "M_BAD_STATE") {
errorText = _t("The user must be unbanned before they can be invited.");
} else if (err.errcode === "M_UNSUPPORTED_ROOM_VERSION") {
@@ -180,14 +207,14 @@ export default class MultiInviter {
errorText = _t('Unknown server error');
}
- this.completionStates[address] = 'error';
- this.errors[address] = {errorText, errcode: err.errcode};
+ this.completionStates[address] = InviteState.Error;
+ this.errors[address] = { errorText, errcode: err.errcode };
this.busy = !fatal;
- this.fatal = fatal;
+ this._fatal = fatal;
if (fatal) {
- reject();
+ reject(err);
} else {
resolve();
}
@@ -195,22 +222,22 @@ export default class MultiInviter {
});
}
- _inviteMore(nextIndex, ignoreProfile) {
- if (this._canceled) {
+ private inviteMore(nextIndex: number, ignoreProfile = false): void {
+ if (this.canceled) {
return;
}
- if (nextIndex === this.addrs.length) {
+ if (nextIndex === this.addresses.length) {
this.busy = false;
if (Object.keys(this.errors).length > 0 && !this.groupId) {
// There were problems inviting some people - see if we can invite them
// without caring if they exist or not.
- const unknownProfileErrors = ['M_NOT_FOUND', 'M_USER_NOT_FOUND', 'M_PROFILE_UNDISCLOSED', 'M_PROFILE_NOT_FOUND'];
- const unknownProfileUsers = Object.keys(this.errors).filter(a => unknownProfileErrors.includes(this.errors[a].errcode));
+ const unknownProfileUsers = Object.keys(this.errors)
+ .filter(a => UNKNOWN_PROFILE_ERRORS.includes(this.errors[a].errcode));
if (unknownProfileUsers.length > 0) {
const inviteUnknowns = () => {
- const promises = unknownProfileUsers.map(u => this._doInvite(u, true));
+ const promises = unknownProfileUsers.map(u => this.doInvite(u, true));
Promise.all(promises).then(() => this.deferred.resolve(this.completionStates));
};
@@ -219,15 +246,17 @@ export default class MultiInviter {
return;
}
- const AskInviteAnywayDialog = sdk.getComponent("dialogs.AskInviteAnywayDialog");
console.log("Showing failed to invite dialog...");
Modal.createTrackedDialog('Failed to invite', '', AskInviteAnywayDialog, {
- unknownProfileUsers: unknownProfileUsers.map(u => {return {userId: u, errorText: this.errors[u].errorText};}),
+ unknownProfileUsers: unknownProfileUsers.map(u => ({
+ userId: u,
+ errorText: this.errors[u].errorText,
+ })),
onInviteAnyways: () => inviteUnknowns(),
onGiveUp: () => {
// Fake all the completion states because we already warned the user
for (const addr of unknownProfileUsers) {
- this.completionStates[addr] = 'invited';
+ this.completionStates[addr] = InviteState.Invited;
}
this.deferred.resolve(this.completionStates);
},
@@ -239,25 +268,25 @@ export default class MultiInviter {
return;
}
- const addr = this.addrs[nextIndex];
+ const addr = this.addresses[nextIndex];
// don't try to invite it if it's an invalid address
// (it will already be marked as an error though,
// so no need to do so again)
if (getAddressType(addr) === null) {
- this._inviteMore(nextIndex + 1);
+ this.inviteMore(nextIndex + 1);
return;
}
// don't re-invite (there's no way in the UI to do this, but
// for sanity's sake)
- if (this.completionStates[addr] === 'invited') {
- this._inviteMore(nextIndex + 1);
+ if (this.completionStates[addr] === InviteState.Invited) {
+ this.inviteMore(nextIndex + 1);
return;
}
- this._doInvite(addr, ignoreProfile).then(() => {
- this._inviteMore(nextIndex + 1, ignoreProfile);
+ this.doInvite(addr, ignoreProfile).then(() => {
+ this.inviteMore(nextIndex + 1, ignoreProfile);
}).catch(() => this.deferred.resolve(this.completionStates));
}
}
diff --git a/src/utils/PinningUtils.js b/src/utils/PinningUtils.ts
similarity index 89%
rename from src/utils/PinningUtils.js
rename to src/utils/PinningUtils.ts
index 90d26cc988..ec1eeccf1f 100644
--- a/src/utils/PinningUtils.js
+++ b/src/utils/PinningUtils.ts
@@ -14,13 +14,15 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
+import { MatrixEvent } from "matrix-js-sdk/src/models/event";
+
export default class PinningUtils {
/**
* Determines if the given event may be pinned.
* @param {MatrixEvent} event The event to check.
* @return {boolean} True if the event may be pinned, false otherwise.
*/
- static isPinnable(event) {
+ static isPinnable(event: MatrixEvent): boolean {
if (!event) return false;
if (event.getType() !== "m.room.message") return false;
if (event.isRedacted()) return false;
diff --git a/src/utils/Receipt.js b/src/utils/Receipt.ts
similarity index 83%
rename from src/utils/Receipt.js
rename to src/utils/Receipt.ts
index d88c67fb18..2a626decc4 100644
--- a/src/utils/Receipt.js
+++ b/src/utils/Receipt.ts
@@ -1,5 +1,5 @@
/*
-Copyright 2016 OpenMarket Ltd
+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.
@@ -14,6 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
+import { MatrixEvent } from "matrix-js-sdk/src/models/event";
+
/**
* Given MatrixEvent containing receipts, return the first
* read receipt from the given user ID, or null if no such
@@ -23,7 +25,7 @@ limitations under the License.
* @param {string} userId A user ID
* @returns {Object} Read receipt
*/
-export function findReadReceiptFromUserId(receiptEvent, userId) {
+export function findReadReceiptFromUserId(receiptEvent: MatrixEvent, userId: string): object | null {
const receiptKeys = Object.keys(receiptEvent.getContent());
for (let i = 0; i < receiptKeys.length; ++i) {
const rcpt = receiptEvent.getContent()[receiptKeys[i]];
diff --git a/src/utils/ResizeNotifier.js b/src/utils/ResizeNotifier.ts
similarity index 62%
rename from src/utils/ResizeNotifier.js
rename to src/utils/ResizeNotifier.ts
index 4d46d10f6c..8bb7f52e57 100644
--- a/src/utils/ResizeNotifier.js
+++ b/src/utils/ResizeNotifier.ts
@@ -1,5 +1,5 @@
/*
-Copyright 2019 New Vector Ltd
+Copyright 2019 - 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -22,59 +22,58 @@ limitations under the License.
* Fires when the middle panel has been resized by a pixel.
* @event module:utils~ResizeNotifier#"middlePanelResizedNoisy"
*/
+
import { EventEmitter } from "events";
import { throttle } from "lodash";
export default class ResizeNotifier extends EventEmitter {
- constructor() {
- super();
- // with default options, will call fn once at first call, and then every x ms
- // if there was another call in that timespan
- this._throttledMiddlePanel = throttle(() => this.emit("middlePanelResized"), 200);
- this._isResizing = false;
- }
+ private _isResizing = false;
- get isResizing() {
+ // with default options, will call fn once at first call, and then every x ms
+ // if there was another call in that timespan
+ private throttledMiddlePanel = throttle(() => this.emit("middlePanelResized"), 200);
+
+ public get isResizing() {
return this._isResizing;
}
- startResizing() {
+ public startResizing() {
this._isResizing = true;
this.emit("isResizing", true);
}
- stopResizing() {
+ public stopResizing() {
this._isResizing = false;
this.emit("isResizing", false);
}
- _noisyMiddlePanel() {
+ private noisyMiddlePanel() {
this.emit("middlePanelResizedNoisy");
}
- _updateMiddlePanel() {
- this._throttledMiddlePanel();
- this._noisyMiddlePanel();
+ private updateMiddlePanel() {
+ this.throttledMiddlePanel();
+ this.noisyMiddlePanel();
}
// can be called in quick succession
- notifyLeftHandleResized() {
+ public notifyLeftHandleResized() {
// don't emit event for own region
- this._updateMiddlePanel();
+ this.updateMiddlePanel();
}
// can be called in quick succession
- notifyRightHandleResized() {
- this._updateMiddlePanel();
+ public notifyRightHandleResized() {
+ this.updateMiddlePanel();
}
- notifyTimelineHeightChanged() {
- this._updateMiddlePanel();
+ public notifyTimelineHeightChanged() {
+ this.updateMiddlePanel();
}
// can be called in quick succession
- notifyWindowResized() {
- this._updateMiddlePanel();
+ public notifyWindowResized() {
+ this.updateMiddlePanel();
}
}
diff --git a/src/utils/ShieldUtils.ts b/src/utils/ShieldUtils.ts
index 5fe653fed0..c855b81bf8 100644
--- a/src/utils/ShieldUtils.ts
+++ b/src/utils/ShieldUtils.ts
@@ -1,30 +1,31 @@
+/*
+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 { MatrixClient } from "matrix-js-sdk/src/client";
+import { Room } from "matrix-js-sdk/src/models/room";
+
import DMRoomMap from './DMRoomMap';
-/* For now, a cut-down type spec for the client */
-interface Client {
- getUserId: () => string;
- checkUserTrust: (userId: string) => {
- isCrossSigningVerified: () => boolean
- wasCrossSigningVerified: () => boolean
- };
- getStoredDevicesForUser: (userId: string) => [{ deviceId: string }];
- checkDeviceTrust: (userId: string, deviceId: string) => {
- isVerified: () => boolean
- };
-}
-
-interface Room {
- getEncryptionTargetMembers: () => Promise<[{userId: string}]>;
- roomId: string;
-}
-
export enum E2EStatus {
Warning = "warning",
Verified = "verified",
Normal = "normal"
}
-export async function shieldStatusForRoom(client: Client, room: Room): Promise {
+export async function shieldStatusForRoom(client: MatrixClient, room: Room): Promise {
const members = (await room.getEncryptionTargetMembers()).map(({userId}) => userId);
const inDMMap = !!DMRoomMap.shared().getUserIdForRoomId(room.roomId);
diff --git a/src/utils/WidgetUtils.ts b/src/utils/WidgetUtils.ts
index 7ff0529363..926278a20a 100644
--- a/src/utils/WidgetUtils.ts
+++ b/src/utils/WidgetUtils.ts
@@ -16,19 +16,20 @@ limitations under the License.
*/
import * as url from "url";
+import { Capability, IWidget, IWidgetData, MatrixCapabilities } from "matrix-widget-api";
+import { Room } from "matrix-js-sdk/src/models/room";
+import { MatrixEvent } from "matrix-js-sdk/src/models/event";
-import {MatrixClientPeg} from '../MatrixClientPeg';
+import { MatrixClientPeg } from '../MatrixClientPeg';
import SdkConfig from "../SdkConfig";
import dis from '../dispatcher/dispatcher';
import WidgetEchoStore from '../stores/WidgetEchoStore';
import SettingsStore from "../settings/SettingsStore";
-import {IntegrationManagers} from "../integrations/IntegrationManagers";
-import {Room} from "matrix-js-sdk/src/models/room";
-import {WidgetType} from "../widgets/WidgetType";
-import {objectClone} from "./objects";
-import {_t} from "../languageHandler";
-import {Capability, IWidget, IWidgetData, MatrixCapabilities} from "matrix-widget-api";
-import {IApp} from "../stores/WidgetStore";
+import { IntegrationManagers } from "../integrations/IntegrationManagers";
+import { WidgetType } from "../widgets/WidgetType";
+import { objectClone } from "./objects";
+import { _t } from "../languageHandler";
+import { IApp } from "../stores/WidgetStore";
// How long we wait for the state event echo to come back from the server
// before waitFor[Room/User]Widget rejects its promise
@@ -377,9 +378,9 @@ export default class WidgetUtils {
return widgets.filter(w => w.content && w.content.type === "m.integration_manager");
}
- static getRoomWidgetsOfType(room: Room, type: WidgetType): IWidgetEvent[] {
- const widgets = WidgetUtils.getRoomWidgets(room);
- return (widgets || []).filter(w => {
+ static getRoomWidgetsOfType(room: Room, type: WidgetType): MatrixEvent[] {
+ const widgets = WidgetUtils.getRoomWidgets(room) || [];
+ return widgets.filter(w => {
const content = w.getContent();
return content.url && type.matches(content.type);
});
diff --git a/src/utils/arrays.ts b/src/utils/arrays.ts
index e527f43c29..6524debfb7 100644
--- a/src/utils/arrays.ts
+++ b/src/utils/arrays.ts
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import {percentageOf, percentageWithin} from "./numbers";
+import { percentageOf, percentageWithin } from "./numbers";
/**
* Quickly resample an array to have less/more data points. If an input which is larger
@@ -223,6 +223,21 @@ export function arrayMerge(...a: T[][]): T[] {
}, new Set()));
}
+/**
+ * Moves a single element from fromIndex to toIndex.
+ * @param {array} list the list from which to construct the new list.
+ * @param {number} fromIndex the index of the element to move.
+ * @param {number} toIndex the index of where to put the element.
+ * @returns {array} A new array with the requested value moved.
+ */
+export function moveElement(list: T[], fromIndex: number, toIndex: number): T[] {
+ const result = Array.from(list);
+ const [removed] = result.splice(fromIndex, 1);
+ result.splice(toIndex, 0, removed);
+
+ return result;
+}
+
/**
* Helper functions to perform LINQ-like queries on arrays.
*/
diff --git a/src/utils/createMatrixClient.js b/src/utils/createMatrixClient.ts
similarity index 76%
rename from src/utils/createMatrixClient.js
rename to src/utils/createMatrixClient.ts
index f5e196d846..caaf75616d 100644
--- a/src/utils/createMatrixClient.js
+++ b/src/utils/createMatrixClient.ts
@@ -1,5 +1,5 @@
/*
-Copyright 2017 Vector Creations Ltd
+Copyright 2017 - 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,10 +14,10 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import {createClient} from "matrix-js-sdk/src/matrix";
-import {IndexedDBCryptoStore} from "matrix-js-sdk/src/crypto/store/indexeddb-crypto-store";
-import {WebStorageSessionStore} from "matrix-js-sdk/src/store/session/webstorage";
-import {IndexedDBStore} from "matrix-js-sdk/src/store/indexeddb";
+import { createClient, ICreateClientOpts } from "matrix-js-sdk/src/matrix";
+import { IndexedDBCryptoStore } from "matrix-js-sdk/src/crypto/store/indexeddb-crypto-store";
+import { WebStorageSessionStore } from "matrix-js-sdk/src/store/session/webstorage";
+import { IndexedDBStore } from "matrix-js-sdk/src/store/indexeddb";
const localStorage = window.localStorage;
@@ -41,8 +41,8 @@ try {
*
* @returns {MatrixClient} the newly-created MatrixClient
*/
-export default function createMatrixClient(opts) {
- const storeOpts = {
+export default function createMatrixClient(opts: ICreateClientOpts) {
+ const storeOpts: Partial = {
useAuthorizationHeader: true,
};
@@ -65,9 +65,10 @@ export default function createMatrixClient(opts) {
);
}
- opts = Object.assign(storeOpts, opts);
-
- return createClient(opts);
+ return createClient({
+ ...storeOpts,
+ ...opts,
+ });
}
createMatrixClient.indexedDbWorkerScript = null;
diff --git a/src/utils/stringOrderField.ts b/src/utils/stringOrderField.ts
new file mode 100644
index 0000000000..da840792ee
--- /dev/null
+++ b/src/utils/stringOrderField.ts
@@ -0,0 +1,148 @@
+/*
+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 { alphabetPad, baseToString, stringToBase, DEFAULT_ALPHABET } from "matrix-js-sdk/src/utils";
+
+import { moveElement } from "./arrays";
+
+export function midPointsBetweenStrings(
+ a: string,
+ b: string,
+ count: number,
+ maxLen: number,
+ alphabet = DEFAULT_ALPHABET,
+): string[] {
+ const padN = Math.min(Math.max(a.length, b.length), maxLen);
+ const padA = alphabetPad(a, padN, alphabet);
+ const padB = alphabetPad(b, padN, alphabet);
+ const baseA = stringToBase(padA, alphabet);
+ const baseB = stringToBase(padB, alphabet);
+
+ if (baseB - baseA - BigInt(1) < count) {
+ if (padN < maxLen) {
+ // this recurses once at most due to the new limit of n+1
+ return midPointsBetweenStrings(
+ alphabetPad(padA, padN + 1, alphabet),
+ alphabetPad(padB, padN + 1, alphabet),
+ count,
+ padN + 1,
+ alphabet,
+ );
+ }
+ return [];
+ }
+
+ const step = (baseB - baseA) / BigInt(count + 1);
+ const start = BigInt(baseA + step);
+ return Array(count).fill(undefined).map((_, i) => baseToString(start + (BigInt(i) * step), alphabet));
+}
+
+interface IEntry {
+ index: number;
+ order: string;
+}
+
+export const reorderLexicographically = (
+ orders: Array,
+ fromIndex: number,
+ toIndex: number,
+ maxLen = 50,
+): IEntry[] => {
+ // sanity check inputs
+ if (
+ fromIndex < 0 || toIndex < 0 ||
+ fromIndex > orders.length || toIndex > orders.length ||
+ fromIndex === toIndex
+ ) {
+ return [];
+ }
+
+ // zip orders with their indices to simplify later index wrangling
+ const ordersWithIndices: IEntry[] = orders.map((order, index) => ({ index, order }));
+ // apply the fundamental order update to the zipped array
+ const newOrder = moveElement(ordersWithIndices, fromIndex, toIndex);
+
+ // check if we have to fill undefined orders to complete placement
+ const orderToLeftUndefined = newOrder[toIndex - 1]?.order === undefined;
+
+ let leftBoundIdx = toIndex;
+ let rightBoundIdx = toIndex;
+
+ let canMoveLeft = true;
+ const nextBase = newOrder[toIndex + 1]?.order !== undefined
+ ? stringToBase(newOrder[toIndex + 1].order)
+ : BigInt(Number.MAX_VALUE);
+
+ // check how far left we would have to mutate to fit in that direction
+ for (let i = toIndex - 1, j = 1; i >= 0; i--, j++) {
+ if (newOrder[i]?.order !== undefined && nextBase - stringToBase(newOrder[i].order) > j) break;
+ leftBoundIdx = i;
+ }
+
+ // verify the left move would be sufficient
+ const firstOrderBase = newOrder[0].order === undefined ? undefined : stringToBase(newOrder[0].order);
+ const bigToIndex = BigInt(toIndex);
+ if (leftBoundIdx === 0 &&
+ firstOrderBase !== undefined &&
+ nextBase - firstOrderBase <= bigToIndex &&
+ firstOrderBase <= bigToIndex
+ ) {
+ canMoveLeft = false;
+ }
+
+ const canDisplaceRight = !orderToLeftUndefined;
+ let canMoveRight = canDisplaceRight;
+ if (canDisplaceRight) {
+ const prevBase = newOrder[toIndex - 1]?.order !== undefined
+ ? stringToBase(newOrder[toIndex - 1]?.order)
+ : BigInt(Number.MIN_VALUE);
+
+ // check how far right we would have to mutate to fit in that direction
+ for (let i = toIndex + 1, j = 1; i < newOrder.length; i++, j++) {
+ if (newOrder[i]?.order === undefined || stringToBase(newOrder[i].order) - prevBase > j) break;
+ rightBoundIdx = i;
+ }
+
+ // verify the right move would be sufficient
+ if (rightBoundIdx === newOrder.length - 1 &&
+ (newOrder[rightBoundIdx]
+ ? stringToBase(newOrder[rightBoundIdx].order)
+ : BigInt(Number.MAX_VALUE)) - prevBase <= (rightBoundIdx - toIndex)
+ ) {
+ canMoveRight = false;
+ }
+ }
+
+ // pick the cheaper direction
+ const leftDiff = canMoveLeft ? toIndex - leftBoundIdx : Number.MAX_SAFE_INTEGER;
+ const rightDiff = canMoveRight ? rightBoundIdx - toIndex : Number.MAX_SAFE_INTEGER;
+ if (orderToLeftUndefined || leftDiff < rightDiff) {
+ rightBoundIdx = toIndex;
+ } else {
+ leftBoundIdx = toIndex;
+ }
+
+ const prevOrder = newOrder[leftBoundIdx - 1]?.order ?? "";
+ const nextOrder = newOrder[rightBoundIdx + 1]?.order
+ ?? DEFAULT_ALPHABET.charAt(DEFAULT_ALPHABET.length - 1).repeat(prevOrder.length || 1);
+
+ const changes = midPointsBetweenStrings(prevOrder, nextOrder, 1 + rightBoundIdx - leftBoundIdx, maxLen);
+
+ return changes.map((order, i) => ({
+ index: newOrder[leftBoundIdx + i].index,
+ order,
+ }));
+};
diff --git a/src/voice/VoiceRecording.ts b/src/voice/VoiceRecording.ts
index fde5779fa2..8f9e03bb8e 100644
--- a/src/voice/VoiceRecording.ts
+++ b/src/voice/VoiceRecording.ts
@@ -17,7 +17,7 @@ limitations under the License.
import * as Recorder from 'opus-recorder';
import encoderPath from 'opus-recorder/dist/encoderWorker.min.js';
import {MatrixClient} from "matrix-js-sdk/src/client";
-import CallMediaHandler from "../CallMediaHandler";
+import MediaDeviceHandler from "../MediaDeviceHandler";
import {SimpleObservable} from "matrix-widget-api";
import {clamp, percentageOf, percentageWithin} from "../utils/numbers";
import EventEmitter from "events";
@@ -97,7 +97,7 @@ export class VoiceRecording extends EventEmitter implements IDestroyable {
audio: {
channelCount: CHANNELS,
noiseSuppression: true, // browsers ignore constraints they can't honour
- deviceId: CallMediaHandler.getAudioInput(),
+ deviceId: MediaDeviceHandler.getAudioInput(),
},
});
this.recorderContext = createAudioContext({
diff --git a/test/components/structures/MessagePanel-test.js b/test/components/structures/MessagePanel-test.js
index 8f0242eb30..d32970a278 100644
--- a/test/components/structures/MessagePanel-test.js
+++ b/test/components/structures/MessagePanel-test.js
@@ -42,7 +42,7 @@ import DMRoomMap from "../../../src/utils/DMRoomMap";
configure({ adapter: new Adapter() });
let client;
-const room = new Matrix.Room();
+const room = new Matrix.Room("!roomId:server_name");
// wrap MessagePanel with a component which provides the MatrixClient in the context.
class WrappedMessagePanel extends React.Component {
diff --git a/test/components/views/rooms/MemberList-test.js b/test/components/views/rooms/MemberList-test.tsx
similarity index 88%
rename from test/components/views/rooms/MemberList-test.js
rename to test/components/views/rooms/MemberList-test.tsx
index 28fead770c..8012c43c4b 100644
--- a/test/components/views/rooms/MemberList-test.js
+++ b/test/components/views/rooms/MemberList-test.tsx
@@ -1,21 +1,36 @@
+/*
+Copyright 2021 Šimon Brandner
+
+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 ReactTestUtils from 'react-dom/test-utils';
import ReactDOM from 'react-dom';
import * as TestUtils from '../../../test-utils';
-
-import {MatrixClientPeg} from '../../../../src/MatrixClientPeg';
import sdk from '../../../skinned-sdk';
-
-import {Room, RoomMember, User} from 'matrix-js-sdk';
-
+import { MatrixClientPeg } from '../../../../src/MatrixClientPeg';
+import { Room } from 'matrix-js-sdk/src/models/room';
+import { RoomMember } from 'matrix-js-sdk/src/models/room-member';
+import { User } from "matrix-js-sdk/src/models/user";
import { compare } from "../../../../src/utils/strings";
+import MemberList from "../../../../src/components/views/rooms/MemberList";
function generateRoomId() {
return '!' + Math.random().toString().slice(2, 10) + ':domain';
}
-
describe('MemberList', () => {
function createRoom(opts) {
const room = new Room(generateRoomId(), null, client.getUserId());
@@ -97,13 +112,19 @@ describe('MemberList', () => {
memberListRoom.currentState.members[member.userId] = member;
}
- const MemberList = sdk.getComponent('views.rooms.MemberList');
const WrappedMemberList = TestUtils.wrapInMatrixClientContext(MemberList);
const gatherWrappedRef = (r) => {
memberList = r;
};
- root = ReactDOM.render(, parentDiv);
+ root = ReactDOM.render(
+ (
+
+ ),
+ parentDiv,
+ );
});
afterEach((done) => {
@@ -213,8 +234,8 @@ describe('MemberList', () => {
});
// Bypass all the event listeners and skip to the good part
- memberList._showPresence = enablePresence;
- memberList._updateListNow();
+ memberList.showPresence = enablePresence;
+ memberList.updateListNow();
const tiles = ReactTestUtils.scryRenderedComponentsWithType(root, MemberTile);
expectOrderedByPresenceAndPowerLevel(tiles, enablePresence);
@@ -225,7 +246,7 @@ describe('MemberList', () => {
// Bypass all the event listeners and skip to the good part
memberList._showPresence = enablePresence;
- memberList._updateListNow();
+ memberList.updateListNow();
const tiles = ReactTestUtils.scryRenderedComponentsWithType(root, MemberTile);
expectOrderedByPresenceAndPowerLevel(tiles, enablePresence);
@@ -254,8 +275,8 @@ describe('MemberList', () => {
});
// Bypass all the event listeners and skip to the good part
- memberList._showPresence = enablePresence;
- memberList._updateListNow();
+ memberList.showPresence = enablePresence;
+ memberList.updateListNow();
const tiles = ReactTestUtils.scryRenderedComponentsWithType(root, MemberTile);
expectOrderedByPresenceAndPowerLevel(tiles, enablePresence);
@@ -273,8 +294,8 @@ describe('MemberList', () => {
});
// Bypass all the event listeners and skip to the good part
- memberList._showPresence = enablePresence;
- memberList._updateListNow();
+ memberList.showPresence = enablePresence;
+ memberList.updateListNow();
const tiles = ReactTestUtils.scryRenderedComponentsWithType(root, MemberTile);
expectOrderedByPresenceAndPowerLevel(tiles, enablePresence);
diff --git a/test/components/views/rooms/RoomList-test.js b/test/components/views/rooms/RoomList-test.js
index bfb8e1afd4..6aad6a90fd 100644
--- a/test/components/views/rooms/RoomList-test.js
+++ b/test/components/views/rooms/RoomList-test.js
@@ -6,7 +6,6 @@ import * as TestUtils from '../../../test-utils';
import {MatrixClientPeg} from '../../../../src/MatrixClientPeg';
import sdk from '../../../skinned-sdk';
-import { DragDropContext } from 'react-beautiful-dnd';
import dis from '../../../../src/dispatcher/dispatcher';
import DMRoomMap from '../../../../src/utils/DMRoomMap';
@@ -68,9 +67,7 @@ describe('RoomList', () => {
const RoomList = sdk.getComponent('views.rooms.RoomList');
const WrappedRoomList = TestUtils.wrapInMatrixClientContext(RoomList);
root = ReactDOM.render(
-
- {}} />
- ,
+ {}} />,
parentDiv,
);
ReactTestUtils.findRenderedComponentWithType(root, RoomList);
diff --git a/test/end-to-end-tests/src/usecases/room-settings.js b/test/end-to-end-tests/src/usecases/room-settings.js
index abd4488db2..654c461296 100644
--- a/test/end-to-end-tests/src/usecases/room-settings.js
+++ b/test/end-to-end-tests/src/usecases/room-settings.js
@@ -140,8 +140,6 @@ async function changeRoomSettings(session, settings) {
if (settings.alias) {
session.log.step(`sets alias to ${settings.alias}`);
- const summary = await session.query(".mx_RoomSettingsDialog .mx_AliasSettings summary");
- await summary.click();
const aliasField = await session.query(".mx_RoomSettingsDialog .mx_AliasSettings details input[type=text]");
await session.replaceInputText(aliasField, settings.alias.substring(1, settings.alias.lastIndexOf(":")));
const addButton = await session.query(".mx_RoomSettingsDialog .mx_AliasSettings details .mx_AccessibleButton");
diff --git a/test/stores/SpaceStore-test.ts b/test/stores/SpaceStore-test.ts
index 01bd528b87..4cbd9f43c8 100644
--- a/test/stores/SpaceStore-test.ts
+++ b/test/stores/SpaceStore-test.ts
@@ -123,8 +123,15 @@ describe("SpaceStore", () => {
jest.runAllTimers();
client.getVisibleRooms.mockReturnValue(rooms = []);
getValue.mockImplementation(settingName => {
- if (settingName === "feature_spaces") {
- return true;
+ switch (settingName) {
+ case "feature_spaces":
+ return true;
+ case "feature_spaces.all_rooms":
+ return true;
+ case "feature_spaces.space_member_dms":
+ return true;
+ case "feature_spaces.space_dm_badges":
+ return false;
}
});
});
diff --git a/test/utils/stringOrderField-test.ts b/test/utils/stringOrderField-test.ts
new file mode 100644
index 0000000000..331627dfc0
--- /dev/null
+++ b/test/utils/stringOrderField-test.ts
@@ -0,0 +1,291 @@
+/*
+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 { sortBy } from "lodash";
+import { averageBetweenStrings, DEFAULT_ALPHABET } from "matrix-js-sdk/src/utils";
+
+import { midPointsBetweenStrings, reorderLexicographically } from "../../src/utils/stringOrderField";
+
+const moveLexicographicallyTest = (
+ orders: Array,
+ fromIndex: number,
+ toIndex: number,
+ expectedChanges: number,
+ maxLength?: number,
+): void => {
+ const ops = reorderLexicographically(orders, fromIndex, toIndex, maxLength);
+
+ const zipped: Array<[number, string | undefined]> = orders.map((o, i) => [i, o]);
+ ops.forEach(({ index, order }) => {
+ zipped[index][1] = order;
+ });
+
+ const newOrders = sortBy(zipped, i => i[1]);
+ expect(newOrders[toIndex][0]).toBe(fromIndex);
+ expect(ops).toHaveLength(expectedChanges);
+};
+
+describe("stringOrderField", () => {
+ describe("midPointsBetweenStrings", () => {
+ it("should work", () => {
+ expect(averageBetweenStrings("!!", "##")).toBe('""');
+ const midpoints = ["a", ...midPointsBetweenStrings("a", "e", 3, 1), "e"].sort();
+ expect(midpoints[0]).toBe("a");
+ expect(midpoints[4]).toBe("e");
+ expect(midPointsBetweenStrings(" ", "!'Tu:}", 1, 50)).toStrictEqual([" S:J\\~"]);
+ });
+
+ it("should return empty array when the request is not possible", () => {
+ expect(midPointsBetweenStrings("a", "e", 0, 1)).toStrictEqual([]);
+ expect(midPointsBetweenStrings("a", "e", 4, 1)).toStrictEqual([]);
+ });
+ });
+
+ describe("reorderLexicographically", () => {
+ it("should work when moving left", () => {
+ moveLexicographicallyTest(["a", "c", "e", "g", "i"], 2, 1, 1);
+ });
+
+ it("should work when moving right", () => {
+ moveLexicographicallyTest(["a", "c", "e", "g", "i"], 1, 2, 1);
+ });
+
+ it("should work when all orders are undefined", () => {
+ moveLexicographicallyTest(
+ [undefined, undefined, undefined, undefined, undefined, undefined],
+ 4,
+ 1,
+ 2,
+ );
+ });
+
+ it("should work when moving to end and all orders are undefined", () => {
+ moveLexicographicallyTest(
+ [undefined, undefined, undefined, undefined, undefined, undefined],
+ 1,
+ 4,
+ 5,
+ );
+ });
+
+ it("should work when moving left and some orders are undefined", () => {
+ moveLexicographicallyTest(
+ ["a", "c", "e", undefined, undefined, undefined],
+ 5,
+ 2,
+ 1,
+ );
+
+ moveLexicographicallyTest(
+ ["a", "a", "e", undefined, undefined, undefined],
+ 5,
+ 1,
+ 2,
+ );
+ });
+
+ it("should work moving to the start when all is undefined", () => {
+ moveLexicographicallyTest(
+ [undefined, undefined, undefined, undefined],
+ 2,
+ 0,
+ 1,
+ );
+ });
+
+ it("should work moving to the end when all is undefined", () => {
+ moveLexicographicallyTest(
+ [undefined, undefined, undefined, undefined],
+ 1,
+ 3,
+ 4,
+ );
+ });
+
+ it("should work moving left when all is undefined", () => {
+ moveLexicographicallyTest(
+ [undefined, undefined, undefined, undefined, undefined, undefined],
+ 4,
+ 1,
+ 2,
+ );
+ });
+
+ it("should work moving right when all is undefined", () => {
+ moveLexicographicallyTest(
+ [undefined, undefined, undefined, undefined],
+ 1,
+ 2,
+ 3,
+ );
+ });
+
+ it("should work moving more right when all is undefined", () => {
+ moveLexicographicallyTest(
+ [undefined, undefined, undefined, undefined, undefined, /**/ undefined, undefined],
+ 1,
+ 4,
+ 5,
+ );
+ });
+
+ it("should work moving left when right is undefined", () => {
+ moveLexicographicallyTest(
+ ["20", undefined, undefined, undefined, undefined, undefined],
+ 4,
+ 2,
+ 2,
+ );
+ });
+
+ it("should work moving right when right is undefined", () => {
+ moveLexicographicallyTest(
+ ["50", undefined, undefined, undefined, undefined, /**/ undefined, undefined],
+ 1,
+ 4,
+ 4,
+ );
+ });
+
+ it("should work moving left when right is defined", () => {
+ moveLexicographicallyTest(
+ ["10", "20", "30", "40", undefined, undefined],
+ 3,
+ 1,
+ 1,
+ );
+ });
+
+ it("should work moving right when right is defined", () => {
+ moveLexicographicallyTest(
+ ["10", "20", "30", "40", "50", undefined],
+ 1,
+ 3,
+ 1,
+ );
+ });
+
+ it("should work moving left when all is defined", () => {
+ moveLexicographicallyTest(
+ ["11", "13", "15", "17", "19"],
+ 2,
+ 1,
+ 1,
+ );
+ });
+
+ it("should work moving right when all is defined", () => {
+ moveLexicographicallyTest(
+ ["11", "13", "15", "17", "19"],
+ 1,
+ 2,
+ 1,
+ );
+ });
+
+ it("should work moving left into no left space", () => {
+ moveLexicographicallyTest(
+ ["11", "12", "13", "14", "19"],
+ 3,
+ 1,
+ 2,
+ 2,
+ );
+
+ moveLexicographicallyTest(
+ [
+ DEFAULT_ALPHABET.charAt(0),
+ // Target
+ DEFAULT_ALPHABET.charAt(1),
+ DEFAULT_ALPHABET.charAt(2),
+ DEFAULT_ALPHABET.charAt(3),
+ DEFAULT_ALPHABET.charAt(4),
+ DEFAULT_ALPHABET.charAt(5),
+ ],
+ 5,
+ 1,
+ 5,
+ 1,
+ );
+ });
+
+ it("should work moving right into no right space", () => {
+ moveLexicographicallyTest(
+ ["15", "16", "17", "18", "19"],
+ 1,
+ 3,
+ 3,
+ 2,
+ );
+
+ moveLexicographicallyTest(
+ [
+ DEFAULT_ALPHABET.charAt(DEFAULT_ALPHABET.length - 5),
+ DEFAULT_ALPHABET.charAt(DEFAULT_ALPHABET.length - 4),
+ DEFAULT_ALPHABET.charAt(DEFAULT_ALPHABET.length - 3),
+ DEFAULT_ALPHABET.charAt(DEFAULT_ALPHABET.length - 2),
+ DEFAULT_ALPHABET.charAt(DEFAULT_ALPHABET.length - 1),
+ ],
+ 1,
+ 3,
+ 3,
+ 1,
+ );
+ });
+
+ it("should work moving right into no left space", () => {
+ moveLexicographicallyTest(
+ ["11", "12", "13", "14", "15", "16", undefined],
+ 1,
+ 3,
+ 3,
+ );
+
+ moveLexicographicallyTest(
+ ["0", "1", "2", "3", "4", "5"],
+ 1,
+ 3,
+ 3,
+ 1,
+ );
+ });
+
+ it("should work moving left into no right space", () => {
+ moveLexicographicallyTest(
+ ["15", "16", "17", "18", "19"],
+ 4,
+ 3,
+ 4,
+ 2,
+ );
+
+ moveLexicographicallyTest(
+ [
+ DEFAULT_ALPHABET.charAt(DEFAULT_ALPHABET.length - 5),
+ DEFAULT_ALPHABET.charAt(DEFAULT_ALPHABET.length - 4),
+ DEFAULT_ALPHABET.charAt(DEFAULT_ALPHABET.length - 3),
+ DEFAULT_ALPHABET.charAt(DEFAULT_ALPHABET.length - 2),
+ DEFAULT_ALPHABET.charAt(DEFAULT_ALPHABET.length - 1),
+ ],
+ 4,
+ 3,
+ 4,
+ 1,
+ );
+ });
+ });
+});
+
diff --git a/yarn.lock b/yarn.lock
index cd4a8b0bd6..3bcb8de404 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1017,13 +1017,20 @@
pirates "^4.0.0"
source-map-support "^0.5.16"
-"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7":
+"@babel/runtime@^7.0.0", "@babel/runtime@^7.12.5", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7":
version "7.12.5"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.12.5.tgz#410e7e487441e1b360c29be715d870d9b985882e"
integrity sha512-plcc+hbExy3McchJCEQG3knOsuh3HH+Prx1P6cLIkET/0dLuQDEnrT+s27Axgc9bqfsmNUNHfscgMUdBpC9xfg==
dependencies:
regenerator-runtime "^0.13.4"
+"@babel/runtime@^7.12.1", "@babel/runtime@^7.9.2":
+ version "7.14.6"
+ resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.14.6.tgz#535203bc0892efc7dec60bdc27b2ecf6e409062d"
+ integrity sha512-/PCB2uJ7oM44tz8YhC4Z/6PeOKXp4K588f+5M3clr1M4zbqztlo0XEfJ2LEzj/FgwfgGcIdl8n7YYjTCI0BYwg==
+ dependencies:
+ regenerator-runtime "^0.13.4"
+
"@babel/template@^7.10.4", "@babel/template@^7.12.7", "@babel/template@^7.3.3":
version "7.12.7"
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.12.7.tgz#c817233696018e39fbb6c491d2fb684e05ed43bc"
@@ -1327,6 +1334,7 @@
"@matrix-org/olm@https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.3.tgz":
version "3.2.3"
+ uid cc332fdd25c08ef0e40f4d33fc3f822a0f98b6f4
resolved "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.3.tgz#cc332fdd25c08ef0e40f4d33fc3f822a0f98b6f4"
"@nicolo-ribaudo/chokidar-2@2.1.8-no-fsevents":
@@ -1479,6 +1487,11 @@
resolved "https://registry.yarnpkg.com/@types/counterpart/-/counterpart-0.18.1.tgz#b1b784d9e54d9879f0a8cb12f2caedab65430fe8"
integrity sha512-PRuFlBBkvdDOtxlIASzTmkEFar+S66Ek48NVVTWMUjtJAdn5vyMSN8y6IZIoIymGpR36q2nZbIYazBWyFxL+IQ==
+"@types/diff-match-patch@^1.0.32":
+ version "1.0.32"
+ resolved "https://registry.yarnpkg.com/@types/diff-match-patch/-/diff-match-patch-1.0.32.tgz#d9c3b8c914aa8229485351db4865328337a3d09f"
+ integrity sha512-bPYT5ECFiblzsVzyURaNhljBH2Gh1t9LowgUwciMrNAhFewLkHT2H0Mto07Y4/3KCOGZHRQll3CTtQZ0X11D/A==
+
"@types/events@^3.0.0":
version "3.0.0"
resolved "https://registry.yarnpkg.com/@types/events/-/events-3.0.0.tgz#2862f3f58a9a7f7c3e78d79f130dd4d71c25c2a7"
@@ -1504,6 +1517,14 @@
dependencies:
"@types/node" "*"
+"@types/hoist-non-react-statics@^3.3.0":
+ version "3.3.1"
+ resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f"
+ integrity sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==
+ dependencies:
+ "@types/react" "*"
+ hoist-non-react-statics "^3.3.0"
+
"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1":
version "2.0.3"
resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz#4ba8ddb720221f432e443bd5f9117fd22cfd4762"
@@ -1620,12 +1641,29 @@
dependencies:
"@types/node" "*"
-"@types/react-dom@^16.9.10":
- version "16.9.10"
- resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-16.9.10.tgz#4485b0bec3d41f856181b717f45fd7831101156f"
- integrity sha512-ItatOrnXDMAYpv6G8UCk2VhbYVTjZT9aorLtA/OzDN9XJ2GKcfam68jutoAcILdRjsRUO8qb7AmyObF77Q8QFw==
+"@types/react-beautiful-dnd@^13.0.0":
+ version "13.0.0"
+ resolved "https://registry.yarnpkg.com/@types/react-beautiful-dnd/-/react-beautiful-dnd-13.0.0.tgz#e60d3d965312fcf1516894af92dc3e9249587db4"
+ integrity sha512-by80tJ8aTTDXT256Gl+RfLRtFjYbUWOnZuEigJgNsJrSEGxvFe5eY6k3g4VIvf0M/6+xoLgfYWoWonlOo6Wqdg==
dependencies:
- "@types/react" "^16"
+ "@types/react" "*"
+
+"@types/react-dom@^17.0.2":
+ version "17.0.8"
+ resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-17.0.8.tgz#3180de6d79bf53762001ad854e3ce49f36dd71fc"
+ integrity sha512-0ohAiJAx1DAUEcY9UopnfwCE9sSMDGnY/oXjWMax6g3RpzmTt2GMyMVAXcbn0mo8XAff0SbQJl2/SBU+hjSZ1A==
+ dependencies:
+ "@types/react" "*"
+
+"@types/react-redux@^7.1.16":
+ version "7.1.16"
+ resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-7.1.16.tgz#0fbd04c2500c12105494c83d4a3e45c084e3cb21"
+ integrity sha512-f/FKzIrZwZk7YEO9E1yoxIuDNRiDducxkFlkw/GNMGEnK9n4K8wJzlJBghpSuOVDgEUHoDkDF7Gi9lHNQR4siw==
+ dependencies:
+ "@types/hoist-non-react-statics" "^3.3.0"
+ "@types/react" "*"
+ hoist-non-react-statics "^3.3.0"
+ redux "^4.0.0"
"@types/react-transition-group@^4.4.0":
version "4.4.0"
@@ -1634,12 +1672,13 @@
dependencies:
"@types/react" "*"
-"@types/react@*", "@types/react@^16", "@types/react@^16.14", "@types/react@^16.9":
- version "16.14.2"
- resolved "https://registry.yarnpkg.com/@types/react/-/react-16.14.2.tgz#85dcc0947d0645349923c04ccef6018a1ab7538c"
- integrity sha512-BzzcAlyDxXl2nANlabtT4thtvbbnhee8hMmH/CcJrISDBVcJS1iOsP1f0OAgSdGE0MsY9tqcrb9YoZcOFv9dbQ==
+"@types/react@*", "@types/react@^17.0.2":
+ version "17.0.11"
+ resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.11.tgz#67fcd0ddbf5a0b083a0f94e926c7d63f3b836451"
+ integrity sha512-yFRQbD+whVonItSk7ZzP/L+gPTJVBkL/7shLEF+i9GC/1cV3JmUxEQz6+9ylhUpWSDuqo1N9qEvqS6vTj4USUA==
dependencies:
"@types/prop-types" "*"
+ "@types/scheduler" "*"
csstype "^3.0.2"
"@types/sanitize-html@^2.3.1":
@@ -1649,6 +1688,11 @@
dependencies:
htmlparser2 "^6.0.0"
+"@types/scheduler@*":
+ version "0.16.1"
+ resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.1.tgz#18845205e86ff0038517aab7a18a62a6b9f71275"
+ integrity sha512-EaCxbanVeyxDRTQBkdLb3Bvl/HK7PBK6UJjsSixB0iHKoWxE5uu2Q/DgtpOhPIojN0Zl1whvOd7PoHs2P0s5eA==
+
"@types/stack-utils@^1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e"
@@ -2116,14 +2160,6 @@ babel-preset-jest@^26.6.2:
babel-plugin-jest-hoist "^26.6.2"
babel-preset-current-node-syntax "^1.0.0"
-babel-runtime@^6.26.0:
- version "6.26.0"
- resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe"
- integrity sha1-llxwWGaOgrVde/4E/yM3vItWR/4=
- dependencies:
- core-js "^2.4.0"
- regenerator-runtime "^0.11.0"
-
bail@^1.0.0:
version "1.0.5"
resolved "https://registry.yarnpkg.com/bail/-/bail-1.0.5.tgz#b6fa133404a392cbc1f8c4bf63f5953351e7a776"
@@ -2642,11 +2678,6 @@ core-js@^1.0.0:
resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636"
integrity sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY=
-core-js@^2.4.0:
- version "2.6.12"
- resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.12.tgz#d9333dfa7b065e347cc5682219d6f690859cc2ec"
- integrity sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==
-
core-util-is@1.0.2, core-util-is@~1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
@@ -2706,6 +2737,13 @@ cross-spawn@^7.0.0, cross-spawn@^7.0.2:
shebang-command "^2.0.0"
which "^2.0.1"
+css-box-model@^1.2.0:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/css-box-model/-/css-box-model-1.2.1.tgz#59951d3b81fd6b2074a62d49444415b0d2b4d7c1"
+ integrity sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==
+ dependencies:
+ tiny-invariant "^1.0.6"
+
css-select@^4.1.2:
version "4.1.2"
resolved "https://registry.yarnpkg.com/css-select/-/css-select-4.1.2.tgz#8b52b6714ed3a80d8221ec971c543f3b12653286"
@@ -4235,7 +4273,7 @@ highlight.js@^10.5.0:
resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-10.5.0.tgz#3f09fede6a865757378f2d9ebdcbc15ba268f98f"
integrity sha512-xTmvd9HiIHR6L53TMC7TKolEj65zG1XU+Onr8oi86mYa+nLcIbxTTWkpW7CsEwv/vK7u1zb8alZIMLDqqN6KTw==
-hoist-non-react-statics@^3.3.0:
+hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2:
version "3.3.2"
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==
@@ -4450,13 +4488,6 @@ internal-slot@^1.0.2:
has "^1.0.3"
side-channel "^1.0.2"
-invariant@^2.2.2, invariant@^2.2.4:
- version "2.2.4"
- resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6"
- integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==
- dependencies:
- loose-envify "^1.0.0"
-
ip-regex@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-2.1.0.tgz#fa78bf5d2e6913c911ce9f819ee5146bb6d844e9"
@@ -5594,11 +5625,6 @@ locate-path@^5.0.0:
dependencies:
p-locate "^4.1.0"
-lodash-es@^4.2.1:
- version "4.17.20"
- resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.20.tgz#29f6332eefc60e849f869c264bc71126ad61e8f7"
- integrity sha512-JD1COMZsq8maT6mnuz1UMV0jvYD0E0aUsSOdrr1/nAG3dhqQXwRRgeW0cSqH1U43INKcqxaiVIQNOUDld7gRDA==
-
lodash.escape@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/lodash.escape/-/lodash.escape-4.0.1.tgz#c9044690c21e04294beaa517712fded1fa88de98"
@@ -5619,7 +5645,7 @@ lodash.sortby@^4.7.0:
resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438"
integrity sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=
-lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.2.1:
+lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20:
version "4.17.21"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
@@ -5711,9 +5737,10 @@ mathml-tag-names@^2.1.3:
resolved "https://registry.yarnpkg.com/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz#4ddadd67308e780cf16a47685878ee27b736a0a3"
integrity sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==
-"matrix-js-sdk@github:matrix-org/matrix-js-sdk#develop":
- version "11.2.0"
- resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/35ecbed29d16982deff27a8c37b05167738225a2"
+matrix-js-sdk@12.0.0:
+ version "12.0.0"
+ resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-12.0.0.tgz#8ee7cc37661476341d0c792a1a12bc78b19f9fdd"
+ integrity sha512-DHeq87Sx9Dv37FYyvZkmA1VYsQUNaVgc3QzMUkFwoHt1T4EZzgyYpdsp3uYruJzUW0ACvVJcwFdrU4e1VS97dQ==
dependencies:
"@babel/runtime" "^7.12.5"
another-json "^0.2.0"
@@ -5746,10 +5773,10 @@ matrix-react-test-utils@^0.2.3:
"@babel/traverse" "^7.13.17"
walk "^2.3.14"
-matrix-widget-api@^0.1.0-beta.14:
- version "0.1.0-beta.14"
- resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-0.1.0-beta.14.tgz#e38beed71c5ebd62c1ac1d79ef262d7150b42c70"
- integrity sha512-5tC6LO1vCblKg/Hfzf5U1eHPz1nHUZIobAm3gkEKV5vpYPgRpr8KdkLiGB78VZid0tB17CVtAb4VKI8CQ3lhAQ==
+matrix-widget-api@^0.1.0-beta.15:
+ version "0.1.0-beta.15"
+ resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-0.1.0-beta.15.tgz#b02511f93fe1a3634868b6e246d736107f182745"
+ integrity sha512-sWmtb8ZarSbHVbk5ni7IHBR9jOh7m1+5R4soky0fEO9VKl+MN7skT0+qNux3J9WuUAu2D80dZW9xPUT9cxfxbg==
dependencies:
"@types/events" "^3.0.0"
events "^3.2.0"
@@ -5787,10 +5814,10 @@ mdurl@~1.0.1:
resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e"
integrity sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4=
-memoize-one@^3.0.1:
- version "3.1.1"
- resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-3.1.1.tgz#ef609811e3bc28970eac2884eece64d167830d17"
- integrity sha512-YqVh744GsMlZu6xkhGslPSqSurOv6P+kLN2J3ysBZfagLcL5FdRK/0UpgLoL8hwjjEvvAVkjJZyFP+1T6p1vgA==
+memoize-one@^5.1.1:
+ version "5.2.1"
+ resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.2.1.tgz#8337aa3c4335581839ec01c3d594090cebe8f00e"
+ integrity sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==
meow@^9.0.0:
version "9.0.0"
@@ -6436,11 +6463,6 @@ path-type@^4.0.0:
resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b"
integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==
-performance-now@^0.2.0:
- version "0.2.0"
- resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-0.2.0.tgz#33ef30c5c77d4ea21c5a53869d91b56d8f2555e5"
- integrity sha1-M+8wxcd9TqIcWlOGnZG1bY8lVeU=
-
performance-now@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
@@ -6650,7 +6672,7 @@ prompts@^2.0.1:
kleur "^3.0.3"
sisteransi "^1.0.5"
-prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.0, prop-types@^15.7.2:
+prop-types@^15.6.2, prop-types@^15.7.0, prop-types@^15.7.2:
version "15.7.2"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5"
integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==
@@ -6727,12 +6749,12 @@ quick-lru@^4.0.1:
resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-4.0.1.tgz#5b8878f113a58217848c6482026c73e1ba57727f"
integrity sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==
-raf-schd@^2.1.0:
- version "2.1.2"
- resolved "https://registry.yarnpkg.com/raf-schd/-/raf-schd-2.1.2.tgz#ec622b5167f2912089f054dc03ebd5bcf33c8f62"
- integrity sha512-Orl0IEvMtUCgPddgSxtxreK77UiQz4nPYJy9RggVzu4mKsZkQWiAaG1y9HlYWdvm9xtN348xRaT37qkvL/+A+g==
+raf-schd@^4.0.2:
+ version "4.0.3"
+ resolved "https://registry.yarnpkg.com/raf-schd/-/raf-schd-4.0.3.tgz#5d6c34ef46f8b2a0e880a8fcdb743efc5bfdbc1a"
+ integrity sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==
-raf@^3.1.0, raf@^3.4.1:
+raf@^3.4.1:
version "3.4.1"
resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.1.tgz#0742e99a4a6552f445d73e3ee0328af0ff1ede39"
integrity sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==
@@ -6759,21 +6781,18 @@ re-resizable@^6.9.0:
dependencies:
fast-memoize "^2.5.1"
-react-beautiful-dnd@^4.0.1:
- version "4.0.1"
- resolved "https://registry.yarnpkg.com/react-beautiful-dnd/-/react-beautiful-dnd-4.0.1.tgz#3b0a49bf6be75af351176c904f012611dd292b81"
- integrity sha512-d73RMu4QOFCyjUELLWFyY/EuclnfqulI9pECx+2gIuJvV0ycf1uR88o+1x0RSB9ILD70inHMzCBKNkWVbbt+vA==
+react-beautiful-dnd@^13.1.0:
+ version "13.1.0"
+ resolved "https://registry.yarnpkg.com/react-beautiful-dnd/-/react-beautiful-dnd-13.1.0.tgz#ec97c81093593526454b0de69852ae433783844d"
+ integrity sha512-aGvblPZTJowOWUNiwd6tNfEpgkX5OxmpqxHKNW/4VmvZTNTbeiq7bA3bn5T+QSF2uibXB0D1DmJsb1aC/+3cUA==
dependencies:
- babel-runtime "^6.26.0"
- invariant "^2.2.2"
- memoize-one "^3.0.1"
- prop-types "^15.6.0"
- raf-schd "^2.1.0"
- react-motion "^0.5.2"
- react-redux "^5.0.6"
- redux "^3.7.2"
- redux-thunk "^2.2.0"
- reselect "^3.0.1"
+ "@babel/runtime" "^7.9.2"
+ css-box-model "^1.2.0"
+ memoize-one "^5.1.1"
+ raf-schd "^4.0.2"
+ react-redux "^7.2.0"
+ redux "^4.0.4"
+ use-memo-one "^1.1.1"
react-clientside-effect@^1.2.2:
version "1.2.3"
@@ -6808,7 +6827,7 @@ react-focus-lock@^2.5.0:
resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0"
integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==
-react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1:
+react-is@^16.13.1, react-is@^16.7.0, react-is@^16.8.1:
version "16.13.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
@@ -6818,32 +6837,17 @@ react-is@^17.0.1:
resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.1.tgz#5b3531bd76a645a4c9fb6e693ed36419e3301339"
integrity sha512-NAnt2iGDXohE5LI7uBnLnqvLQMtzhkiAOLXTmv+qnF9Ky7xAPcX8Up/xWIhxvLVGJvuLiNc4xQLtuqDRzb4fSA==
-react-lifecycles-compat@^3.0.0:
- version "3.0.4"
- resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362"
- integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==
-
-react-motion@^0.5.2:
- version "0.5.2"
- resolved "https://registry.yarnpkg.com/react-motion/-/react-motion-0.5.2.tgz#0dd3a69e411316567927917c6626551ba0607316"
- integrity sha512-9q3YAvHoUiWlP3cK0v+w1N5Z23HXMj4IF4YuvjvWegWqNPfLXsOBE/V7UvQGpXxHFKRQQcNcVQE31g9SB/6qgQ==
+react-redux@^7.2.0:
+ version "7.2.4"
+ resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.4.tgz#1ebb474032b72d806de2e0519cd07761e222e225"
+ integrity sha512-hOQ5eOSkEJEXdpIKbnRyl04LhaWabkDPV+Ix97wqQX3T3d2NQ8DUblNXXtNMavc7DpswyQM6xfaN4HQDKNY2JA==
dependencies:
- performance-now "^0.2.0"
- prop-types "^15.5.8"
- raf "^3.1.0"
-
-react-redux@^5.0.6:
- version "5.1.2"
- resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-5.1.2.tgz#b19cf9e21d694422727bf798e934a916c4080f57"
- integrity sha512-Ns1G0XXc8hDyH/OcBHOxNgQx9ayH3SPxBnFCOidGKSle8pKihysQw2rG/PmciUQRoclhVBO8HMhiRmGXnDja9Q==
- dependencies:
- "@babel/runtime" "^7.1.2"
- hoist-non-react-statics "^3.3.0"
- invariant "^2.2.4"
- loose-envify "^1.1.0"
- prop-types "^15.6.1"
- react-is "^16.6.0"
- react-lifecycles-compat "^3.0.0"
+ "@babel/runtime" "^7.12.1"
+ "@types/react-redux" "^7.1.16"
+ hoist-non-react-statics "^3.3.2"
+ loose-envify "^1.4.0"
+ prop-types "^15.7.2"
+ react-is "^16.13.1"
react-shallow-renderer@^16.13.1:
version "16.14.1"
@@ -6972,20 +6976,12 @@ redent@^3.0.0:
indent-string "^4.0.0"
strip-indent "^3.0.0"
-redux-thunk@^2.2.0:
- version "2.3.0"
- resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.3.0.tgz#51c2c19a185ed5187aaa9a2d08b666d0d6467622"
- integrity sha512-km6dclyFnmcvxhAcrQV2AkZmPQjzPDjgVlQtR0EQjxZPyJ0BnMf3in1ryuR8A2qU0HldVRfxYXbFSKlI3N7Slw==
-
-redux@^3.7.2:
- version "3.7.2"
- resolved "https://registry.yarnpkg.com/redux/-/redux-3.7.2.tgz#06b73123215901d25d065be342eb026bc1c8537b"
- integrity sha512-pNqnf9q1hI5HHZRBkj3bAngGZW/JMCmexDlOxw4XagXY2o1327nHH54LoTjiPJ0gizoqPDRqWyX/00g0hD6w+A==
+redux@^4.0.0, redux@^4.0.4:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/redux/-/redux-4.1.0.tgz#eb049679f2f523c379f1aff345c8612f294c88d4"
+ integrity sha512-uI2dQN43zqLWCt6B/BMGRMY6db7TTY4qeHHfGeKb3EOhmOKjU3KdWvNLJyqaHRksv/ErdNH7cFZWg9jXtewy4g==
dependencies:
- lodash "^4.2.1"
- lodash-es "^4.2.1"
- loose-envify "^1.1.0"
- symbol-observable "^1.0.3"
+ "@babel/runtime" "^7.9.2"
regenerate-unicode-properties@^8.2.0:
version "8.2.0"
@@ -6999,11 +6995,6 @@ regenerate@^1.4.0:
resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.2.tgz#b9346d8827e8f5a32f7ba29637d398b69014848a"
integrity sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==
-regenerator-runtime@^0.11.0:
- version "0.11.1"
- resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9"
- integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==
-
regenerator-runtime@^0.13.4:
version "0.13.7"
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz#cac2dacc8a1ea675feaabaeb8ae833898ae46f55"
@@ -7161,11 +7152,6 @@ require-main-filename@^2.0.0:
resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b"
integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==
-reselect@^3.0.1:
- version "3.0.1"
- resolved "https://registry.yarnpkg.com/reselect/-/reselect-3.0.1.tgz#efdaa98ea7451324d092b2b2163a6a1d7a9a2147"
- integrity sha1-79qpjqdFEyTQkrKyFjpqHXqaIUc=
-
resize-observer-polyfill@^1.5.1:
version "1.5.1"
resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464"
@@ -7888,11 +7874,6 @@ svg-tags@^1.0.0:
resolved "https://registry.yarnpkg.com/svg-tags/-/svg-tags-1.0.0.tgz#58f71cee3bd519b59d4b2a843b6c7de64ac04764"
integrity sha1-WPcc7jvVGbWdSyqEO2x95krAR2Q=
-symbol-observable@^1.0.3:
- version "1.2.0"
- resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804"
- integrity sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==
-
symbol-tree@^3.2.4:
version "3.2.4"
resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2"
@@ -7955,6 +7936,11 @@ through@^2.3.6:
resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=
+tiny-invariant@^1.0.6:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.1.0.tgz#634c5f8efdc27714b7f386c35e6760991d230875"
+ integrity sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw==
+
tmatch@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/tmatch/-/tmatch-2.0.1.tgz#0c56246f33f30da1b8d3d72895abaf16660f38cf"
@@ -8270,6 +8256,11 @@ use-callback-ref@^1.2.1:
resolved "https://registry.yarnpkg.com/use-callback-ref/-/use-callback-ref-1.2.5.tgz#6115ed242cfbaed5915499c0a9842ca2912f38a5"
integrity sha512-gN3vgMISAgacF7sqsLPByqoePooY3n2emTH59Ur5d/M8eg4WTWu1xp8i8DHjohftIyEx0S08RiYxbffr4j8Peg==
+use-memo-one@^1.1.1:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/use-memo-one/-/use-memo-one-1.1.2.tgz#0c8203a329f76e040047a35a1197defe342fab20"
+ integrity sha512-u2qFKtxLsia/r8qG0ZKkbytbztzRb317XCkT7yP8wxL0tZ/CzK2G+WWie5vWvpyeP7+YoPIwbJoIHJ4Ba4k0oQ==
+
use-sidecar@^1.0.1:
version "1.0.4"
resolved "https://registry.yarnpkg.com/use-sidecar/-/use-sidecar-1.0.4.tgz#38398c3723727f9f924bed2343dfa3db6aaaee46"