Merge pull request #6243 from matrix-org/t3chguy/ts/8

This commit is contained in:
Michael Telatynski 2021-06-29 22:27:16 +01:00 committed by GitHub
commit 3fa89f3294
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 1040 additions and 912 deletions

View file

@ -16,6 +16,7 @@ limitations under the License.
import "matrix-js-sdk/src/@types/global"; // load matrix-js-sdk's type extensions first import "matrix-js-sdk/src/@types/global"; // load matrix-js-sdk's type extensions first
import * as ModernizrStatic from "modernizr"; import * as ModernizrStatic from "modernizr";
import ContentMessages from "../ContentMessages"; import ContentMessages from "../ContentMessages";
import { IMatrixClientPeg } from "../MatrixClientPeg"; import { IMatrixClientPeg } from "../MatrixClientPeg";
import ToastStore from "../stores/ToastStore"; import ToastStore from "../stores/ToastStore";
@ -127,11 +128,24 @@ declare global {
setSinkId(outputId: string); setSinkId(outputId: string);
} }
// Add Chrome-specific `instant` ScrollBehaviour
type _ScrollBehavior = ScrollBehavior | "instant";
interface _ScrollOptions {
behavior?: _ScrollBehavior;
}
interface _ScrollIntoViewOptions extends _ScrollOptions {
block?: ScrollLogicalPosition;
inline?: ScrollLogicalPosition;
}
interface Element { interface Element {
// Safari & IE11 only have this prefixed: we used prefixed versions // Safari & IE11 only have this prefixed: we used prefixed versions
// previously so let's continue to support them for now // previously so let's continue to support them for now
webkitRequestFullScreen(options?: FullscreenOptions): Promise<void>; webkitRequestFullScreen(options?: FullscreenOptions): Promise<void>;
msRequestFullscreen(options?: FullscreenOptions): Promise<void>; msRequestFullscreen(options?: FullscreenOptions): Promise<void>;
scrollIntoView(arg?: boolean | _ScrollIntoViewOptions): void;
} }
interface Error { interface Error {

View file

@ -15,12 +15,12 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React, { HTMLAttributes } from "react"; import React, { HTMLAttributes, WheelEvent } from "react";
interface IProps extends HTMLAttributes<HTMLDivElement> { interface IProps extends Omit<HTMLAttributes<HTMLDivElement>, "onScroll"> {
className?: string; className?: string;
onScroll?: () => void; onScroll?: (event: Event) => void;
onWheel?: () => void; onWheel?: (event: WheelEvent) => void;
style?: React.CSSProperties style?: React.CSSProperties
tabIndex?: number, tabIndex?: number,
wrappedRef?: (ref: HTMLDivElement) => void; wrappedRef?: (ref: HTMLDivElement) => void;

View file

@ -22,6 +22,7 @@ import BaseCard from "../views/right_panel/BaseCard";
import { replaceableComponent } from "../../utils/replaceableComponent"; import { replaceableComponent } from "../../utils/replaceableComponent";
import TimelinePanel from "./TimelinePanel"; import TimelinePanel from "./TimelinePanel";
import Spinner from "../views/elements/Spinner"; import Spinner from "../views/elements/Spinner";
import { TileShape } from "../views/rooms/EventTile";
interface IProps { interface IProps {
onClose(): void; onClose(): void;
@ -48,7 +49,7 @@ export default class NotificationPanel extends React.PureComponent<IProps> {
manageReadMarkers={false} manageReadMarkers={false}
timelineSet={timelineSet} timelineSet={timelineSet}
showUrlPreview={false} showUrlPreview={false}
tileShape="notif" tileShape={TileShape.Notif}
empty={emptyState} empty={emptyState}
alwaysShowTimestamps={true} alwaysShowTimestamps={true}
/> />

View file

@ -206,9 +206,9 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
this.getMoreRooms(); this.getMoreRooms();
}; };
private getMoreRooms() { private getMoreRooms(): Promise<boolean> {
if (this.state.selectedCommunityId) return Promise.resolve(); // no more rooms if (this.state.selectedCommunityId) return Promise.resolve(false); // no more rooms
if (!MatrixClientPeg.get()) return Promise.resolve(); if (!MatrixClientPeg.get()) return Promise.resolve(false);
this.setState({ this.setState({
loading: true, loading: true,
@ -238,12 +238,12 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
// if the filter or server has changed since this request was sent, // if the filter or server has changed since this request was sent,
// throw away the result (don't even clear the busy flag // throw away the result (don't even clear the busy flag
// since we must still have a request in flight) // since we must still have a request in flight)
return; return false;
} }
if (this.unmounted) { if (this.unmounted) {
// if we've been unmounted, we don't care either. // if we've been unmounted, we don't care either.
return; return false;
} }
if (this.state.filterString) { if (this.state.filterString) {
@ -263,14 +263,13 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
filterString != this.state.filterString || filterString != this.state.filterString ||
roomServer != this.state.roomServer || roomServer != this.state.roomServer ||
nextBatch != this.nextBatch) { nextBatch != this.nextBatch) {
// as above: we don't care about errors for old // as above: we don't care about errors for old requests either
// requests either return false;
return;
} }
if (this.unmounted) { if (this.unmounted) {
// if we've been unmounted, we don't care either. // if we've been unmounted, we don't care either.
return; return false;
} }
console.error("Failed to get publicRooms: %s", JSON.stringify(err)); console.error("Failed to get publicRooms: %s", JSON.stringify(err));

View file

@ -1143,7 +1143,7 @@ export default class RoomView extends React.Component<IProps, IState> {
} }
} }
private onSearchResultsFillRequest = (backwards: boolean) => { private onSearchResultsFillRequest = (backwards: boolean): Promise<boolean> => {
if (!backwards) { if (!backwards) {
return Promise.resolve(false); return Promise.resolve(false);
} }
@ -1309,7 +1309,7 @@ export default class RoomView extends React.Component<IProps, IState> {
this.handleSearchResult(searchPromise); this.handleSearchResult(searchPromise);
}; };
private handleSearchResult(searchPromise: Promise<any>) { private handleSearchResult(searchPromise: Promise<any>): Promise<boolean> {
// keep a record of the current search id, so that if the search terms // 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. // change before we get a response, we can ignore the results.
const localSearchId = this.searchId; const localSearchId = this.searchId;
@ -1322,7 +1322,7 @@ export default class RoomView extends React.Component<IProps, IState> {
debuglog("search complete"); debuglog("search complete");
if (this.unmounted || !this.state.searching || this.searchId != localSearchId) { if (this.unmounted || !this.state.searching || this.searchId != localSearchId) {
console.error("Discarding stale search results"); console.error("Discarding stale search results");
return; return false;
} }
// postgres on synapse returns us precise details of the strings // postgres on synapse returns us precise details of the strings
@ -1354,6 +1354,7 @@ export default class RoomView extends React.Component<IProps, IState> {
description: ((error && error.message) ? error.message : description: ((error && error.message) ? error.message :
_t("Server may be unavailable, overloaded, or search timed out :(")), _t("Server may be unavailable, overloaded, or search timed out :(")),
}); });
return false;
}).finally(() => { }).finally(() => {
this.setState({ this.setState({
searchInProgress: false, searchInProgress: false,

View file

@ -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"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -14,17 +14,17 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React, { createRef } from "react"; import React, { createRef, CSSProperties, ReactNode, SyntheticEvent, KeyboardEvent } from "react";
import PropTypes from 'prop-types';
import Timer from '../../utils/Timer'; import Timer from '../../utils/Timer';
import AutoHideScrollbar from "./AutoHideScrollbar"; import AutoHideScrollbar from "./AutoHideScrollbar";
import { replaceableComponent } from "../../utils/replaceableComponent"; import { replaceableComponent } from "../../utils/replaceableComponent";
import { getKeyBindingsManager, RoomAction } from "../../KeyBindingsManager"; import { getKeyBindingsManager, RoomAction } from "../../KeyBindingsManager";
import ResizeNotifier from "../../utils/ResizeNotifier";
const DEBUG_SCROLL = false; const DEBUG_SCROLL = false;
// The amount of extra scroll distance to allow prior to unfilling. // The amount of extra scroll distance to allow prior to unfilling.
// See _getExcessHeight. // See getExcessHeight.
const UNPAGINATION_PADDING = 6000; const UNPAGINATION_PADDING = 6000;
// The number of milliseconds to debounce calls to onUnfillRequest, to prevent // The number of milliseconds to debounce calls to onUnfillRequest, to prevent
// many scroll events causing many unfilling requests. // many scroll events causing many unfilling requests.
@ -43,6 +43,75 @@ if (DEBUG_SCROLL) {
debuglog = function() {}; 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<boolean>;
/* 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. /* This component implements an intelligent scrolling list.
* *
* It wraps a list of <li> children; when items are added to the start or end * It wraps a list of <li> children; when items are added to the start or end
@ -84,97 +153,54 @@ if (DEBUG_SCROLL) {
* offset as normal. * 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") @replaceableComponent("structures.ScrollPanel")
export default class ScrollPanel extends React.Component { export default class ScrollPanel extends React.Component<IProps> {
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,
};
static defaultProps = { static defaultProps = {
stickyBottom: true, stickyBottom: true,
startAtBottom: true, startAtBottom: true,
onFillRequest: function(backwards) { return Promise.resolve(false); }, onFillRequest: function(backwards: boolean) { return Promise.resolve(false); },
onUnfillRequest: function(backwards, scrollToken) {}, onUnfillRequest: function(backwards: boolean, scrollToken: string) {},
onScroll: function() {}, onScroll: function() {},
}; };
constructor(props) { private readonly pendingFillRequests: Record<"b" | "f", boolean> = {
super(props); b: null,
f: null,
};
private readonly itemlist = createRef<HTMLOListElement>();
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) { if (this.props.resizeNotifier) {
this.props.resizeNotifier.on("middlePanelResizedNoisy", this.onResize); this.props.resizeNotifier.on("middlePanelResizedNoisy", this.onResize);
} }
this.resetScrollState(); this.resetScrollState();
this._itemlist = createRef();
} }
componentDidMount() { componentDidMount() {
@ -203,18 +229,18 @@ export default class ScrollPanel extends React.Component {
} }
} }
onScroll = ev => { private onScroll = ev => {
// skip scroll events caused by resizing // skip scroll events caused by resizing
if (this.props.resizeNotifier && this.props.resizeNotifier.isResizing) return; if (this.props.resizeNotifier && this.props.resizeNotifier.isResizing) return;
debuglog("onScroll", this._getScrollNode().scrollTop); debuglog("onScroll", this.getScrollNode().scrollTop);
this._scrollTimeout.restart(); this.scrollTimeout.restart();
this._saveScrollState(); this.saveScrollState();
this.updatePreventShrinking(); this.updatePreventShrinking();
this.props.onScroll(ev); this.props.onScroll(ev);
this.checkFillState(); this.checkFillState();
}; };
onResize = () => { private onResize = () => {
debuglog("onResize"); debuglog("onResize");
this.checkScroll(); this.checkScroll();
// update preventShrinkingState if present // update preventShrinkingState if present
@ -225,11 +251,11 @@ export default class ScrollPanel extends React.Component {
// after an update to the contents of the panel, check that the scroll is // 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. // where it ought to be, and set off pagination requests if necessary.
checkScroll = () => { public checkScroll = () => {
if (this.unmounted) { if (this.unmounted) {
return; return;
} }
this._restoreSavedScrollState(); this.restoreSavedScrollState();
this.checkFillState(); this.checkFillState();
}; };
@ -238,8 +264,8 @@ export default class ScrollPanel extends React.Component {
// note that this is independent of the 'stuckAtBottom' state - it is simply // note that this is independent of the 'stuckAtBottom' state - it is simply
// about whether the content is scrolled down right now, irrespective of // about whether the content is scrolled down right now, irrespective of
// whether it will stay that way when the children update. // whether it will stay that way when the children update.
isAtBottom = () => { public isAtBottom = () => {
const sn = this._getScrollNode(); const sn = this.getScrollNode();
// fractional values (both too big and too small) // fractional values (both too big and too small)
// for scrollTop happen on certain browsers/platforms // for scrollTop happen on certain browsers/platforms
// when scrolled all the way down. E.g. Chrome 72 on debian. // when scrolled all the way down. E.g. Chrome 72 on debian.
@ -278,10 +304,10 @@ export default class ScrollPanel extends React.Component {
// |#########| - | // |#########| - |
// |#########| | // |#########| |
// `---------' - // `---------' -
_getExcessHeight(backwards) { private getExcessHeight(backwards: boolean): number {
const sn = this._getScrollNode(); const sn = this.getScrollNode();
const contentHeight = this._getMessagesHeight(); const contentHeight = this.getMessagesHeight();
const listHeight = this._getListHeight(); const listHeight = this.getListHeight();
const clippedHeight = contentHeight - listHeight; const clippedHeight = contentHeight - listHeight;
const unclippedScrollTop = sn.scrollTop + clippedHeight; const unclippedScrollTop = sn.scrollTop + clippedHeight;
@ -293,13 +319,13 @@ export default class ScrollPanel extends React.Component {
} }
// check the scroll state and send out backfill requests if necessary. // check the scroll state and send out backfill requests if necessary.
checkFillState = async (depth=0) => { public checkFillState = async (depth = 0): Promise<void> => {
if (this.unmounted) { if (this.unmounted) {
return; return;
} }
const isFirstCall = depth === 0; 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 // if there is less than a screenful of messages above or below the
// viewport, try to get some more messages. // viewport, try to get some more messages.
@ -330,17 +356,17 @@ export default class ScrollPanel extends React.Component {
// do make a note when a new request comes in while already running one, // 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. // so we can trigger a new chain of calls once done.
if (isFirstCall) { if (isFirstCall) {
if (this._isFilling) { if (this.isFilling) {
debuglog("_isFilling: not entering while request is ongoing, marking for a subsequent request"); debuglog("isFilling: not entering while request is ongoing, marking for a subsequent request");
this._fillRequestWhileRunning = true; this.fillRequestWhileRunning = true;
return; return;
} }
debuglog("_isFilling: setting"); debuglog("isFilling: setting");
this._isFilling = true; this.isFilling = true;
} }
const itemlist = this._itemlist.current; const itemlist = this.itemlist.current;
const firstTile = itemlist && itemlist.firstElementChild; const firstTile = itemlist && itemlist.firstElementChild as HTMLElement;
const contentTop = firstTile && firstTile.offsetTop; const contentTop = firstTile && firstTile.offsetTop;
const fillPromises = []; const fillPromises = [];
@ -348,13 +374,13 @@ export default class ScrollPanel extends React.Component {
// try backward filling // try backward filling
if (!firstTile || (sn.scrollTop - contentTop) < sn.clientHeight) { if (!firstTile || (sn.scrollTop - contentTop) < sn.clientHeight) {
// need to back-fill // 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), // if scrollTop gets to 2 screens from the end (so 1 screen below viewport),
// try forward filling // try forward filling
if ((sn.scrollHeight - sn.scrollTop) < sn.clientHeight * 2) { if ((sn.scrollHeight - sn.scrollTop) < sn.clientHeight * 2) {
// need to forward-fill // need to forward-fill
fillPromises.push(this._maybeFill(depth, false)); fillPromises.push(this.maybeFill(depth, false));
} }
if (fillPromises.length) { if (fillPromises.length) {
@ -365,26 +391,26 @@ export default class ScrollPanel extends React.Component {
} }
} }
if (isFirstCall) { if (isFirstCall) {
debuglog("_isFilling: clearing"); debuglog("isFilling: clearing");
this._isFilling = false; this.isFilling = false;
} }
if (this._fillRequestWhileRunning) { if (this.fillRequestWhileRunning) {
this._fillRequestWhileRunning = false; this.fillRequestWhileRunning = false;
this.checkFillState(); this.checkFillState();
} }
}; };
// check if unfilling is possible and send an unfill request if necessary // check if unfilling is possible and send an unfill request if necessary
_checkUnfillState(backwards) { private checkUnfillState(backwards: boolean): void {
let excessHeight = this._getExcessHeight(backwards); let excessHeight = this.getExcessHeight(backwards);
if (excessHeight <= 0) { if (excessHeight <= 0) {
return; return;
} }
const origExcessHeight = excessHeight; 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 // The scroll token of the first/last tile to be unpaginated
let markerScrollToken = null; let markerScrollToken = null;
@ -413,11 +439,11 @@ export default class ScrollPanel extends React.Component {
if (markerScrollToken) { if (markerScrollToken) {
// Use a debouncer to prevent multiple unfill calls in quick succession // Use a debouncer to prevent multiple unfill calls in quick succession
// This is to make the unfilling process less aggressive // This is to make the unfilling process less aggressive
if (this._unfillDebouncer) { if (this.unfillDebouncer) {
clearTimeout(this._unfillDebouncer); clearTimeout(this.unfillDebouncer);
} }
this._unfillDebouncer = setTimeout(() => { this.unfillDebouncer = setTimeout(() => {
this._unfillDebouncer = null; this.unfillDebouncer = null;
debuglog("unfilling now", backwards, origExcessHeight); debuglog("unfilling now", backwards, origExcessHeight);
this.props.onUnfillRequest(backwards, markerScrollToken); this.props.onUnfillRequest(backwards, markerScrollToken);
}, UNFILL_REQUEST_DEBOUNCE_MS); }, UNFILL_REQUEST_DEBOUNCE_MS);
@ -425,9 +451,9 @@ export default class ScrollPanel extends React.Component {
} }
// check if there is already a pending fill request. If not, set one off. // check if there is already a pending fill request. If not, set one off.
_maybeFill(depth, backwards) { private maybeFill(depth: number, backwards: boolean): Promise<void> {
const dir = backwards ? 'b' : 'f'; const dir = backwards ? 'b' : 'f';
if (this._pendingFillRequests[dir]) { if (this.pendingFillRequests[dir]) {
debuglog("Already a "+dir+" fill in progress - not starting another"); debuglog("Already a "+dir+" fill in progress - not starting another");
return; return;
} }
@ -436,7 +462,7 @@ export default class ScrollPanel extends React.Component {
// onFillRequest can end up calling us recursively (via onScroll // onFillRequest can end up calling us recursively (via onScroll
// events) so make sure we set this before firing off the call. // 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 // wait 1ms before paginating, because otherwise
// this will block the scroll event handler for +700ms // this will block the scroll event handler for +700ms
@ -445,13 +471,13 @@ export default class ScrollPanel extends React.Component {
return new Promise(resolve => setTimeout(resolve, 1)).then(() => { return new Promise(resolve => setTimeout(resolve, 1)).then(() => {
return this.props.onFillRequest(backwards); return this.props.onFillRequest(backwards);
}).finally(() => { }).finally(() => {
this._pendingFillRequests[dir] = false; this.pendingFillRequests[dir] = false;
}).then((hasMoreResults) => { }).then((hasMoreResults) => {
if (this.unmounted) { if (this.unmounted) {
return; return;
} }
// Unpaginate once filling is complete // Unpaginate once filling is complete
this._checkUnfillState(!backwards); this.checkUnfillState(!backwards);
debuglog(""+dir+" fill complete; hasMoreResults:"+hasMoreResults); debuglog(""+dir+" fill complete; hasMoreResults:"+hasMoreResults);
if (hasMoreResults) { if (hasMoreResults) {
@ -477,7 +503,7 @@ export default class ScrollPanel extends React.Component {
* the number of pixels the bottom of the tracked child is above the * the number of pixels the bottom of the tracked child is above the
* bottom of the scroll panel. * bottom of the scroll panel.
*/ */
getScrollState = () => this.scrollState; public getScrollState = (): IScrollState => this.scrollState;
/* reset the saved scroll state. /* reset the saved scroll state.
* *
@ -491,35 +517,35 @@ export default class ScrollPanel extends React.Component {
* no use if no children exist yet, or if you are about to replace the * no use if no children exist yet, or if you are about to replace the
* child list.) * child list.)
*/ */
resetScrollState = () => { public resetScrollState = (): void => {
this.scrollState = { this.scrollState = {
stuckAtBottom: this.props.startAtBottom, stuckAtBottom: this.props.startAtBottom,
}; };
this._bottomGrowth = 0; this.bottomGrowth = 0;
this._pages = 0; this.pages = 0;
this._scrollTimeout = new Timer(100); this.scrollTimeout = new Timer(100);
this._heightUpdateInProgress = false; this.heightUpdateInProgress = false;
}; };
/** /**
* jump to the top of the content. * jump to the top of the content.
*/ */
scrollToTop = () => { public scrollToTop = (): void => {
this._getScrollNode().scrollTop = 0; this.getScrollNode().scrollTop = 0;
this._saveScrollState(); this.saveScrollState();
}; };
/** /**
* jump to the bottom of the content. * jump to the bottom of the content.
*/ */
scrollToBottom = () => { public scrollToBottom = (): void => {
// the easiest way to make sure that the scroll state is correctly // the easiest way to make sure that the scroll state is correctly
// saved is to do the scroll, then save the updated state. (Calculating // 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 // it ourselves is hard, and we can't rely on an onScroll callback
// happening, since there may be no user-visible change here). // happening, since there may be no user-visible change here).
const sn = this._getScrollNode(); const sn = this.getScrollNode();
sn.scrollTop = sn.scrollHeight; sn.scrollTop = sn.scrollHeight;
this._saveScrollState(); this.saveScrollState();
}; };
/** /**
@ -527,18 +553,18 @@ export default class ScrollPanel extends React.Component {
* *
* @param {number} mult: -1 to page up, +1 to page down * @param {number} mult: -1 to page up, +1 to page down
*/ */
scrollRelative = mult => { public scrollRelative = (mult: number): void => {
const scrollNode = this._getScrollNode(); const scrollNode = this.getScrollNode();
const delta = mult * scrollNode.clientHeight * 0.9; const delta = mult * scrollNode.clientHeight * 0.9;
scrollNode.scrollBy(0, delta); scrollNode.scrollBy(0, delta);
this._saveScrollState(); this.saveScrollState();
}; };
/** /**
* Scroll up/down in response to a scroll key * Scroll up/down in response to a scroll key
* @param {object} ev the keyboard event * @param {object} ev the keyboard event
*/ */
handleScrollKey = ev => { public handleScrollKey = (ev: KeyboardEvent) => {
let isScrolling = false; let isScrolling = false;
const roomAction = getKeyBindingsManager().getRoomAction(ev); const roomAction = getKeyBindingsManager().getRoomAction(ev);
switch (roomAction) { switch (roomAction) {
@ -575,17 +601,17 @@ export default class ScrollPanel extends React.Component {
* node (specifically, the bottom of it) will be positioned. If omitted, it * node (specifically, the bottom of it) will be positioned. If omitted, it
* defaults to 0. * defaults to 0.
*/ */
scrollToToken = (scrollToken, pixelOffset, offsetBase) => { public scrollToToken = (scrollToken: string, pixelOffset: number, offsetBase: number): void => {
pixelOffset = pixelOffset || 0; pixelOffset = pixelOffset || 0;
offsetBase = offsetBase || 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 = { this.scrollState = {
stuckAtBottom: false, stuckAtBottom: false,
trackedScrollToken: scrollToken, trackedScrollToken: scrollToken,
}; };
const trackedNode = this._getTrackedNode(); const trackedNode = this.getTrackedNode();
const scrollNode = this._getScrollNode(); const scrollNode = this.getScrollNode();
if (trackedNode) { if (trackedNode) {
// set the scrollTop to the position we want. // set the scrollTop to the position we want.
// note though, that this might not succeed if the combination of offsetBase and pixelOffset // note though, that this might not succeed if the combination of offsetBase and pixelOffset
@ -595,34 +621,34 @@ export default class ScrollPanel extends React.Component {
// enough so it ends up in the top of the viewport. // enough so it ends up in the top of the viewport.
debuglog("scrollToken: setting scrollTop", { offsetBase, pixelOffset, offsetTop: trackedNode.offsetTop }); debuglog("scrollToken: setting scrollTop", { offsetBase, pixelOffset, offsetTop: trackedNode.offsetTop });
scrollNode.scrollTop = (trackedNode.offsetTop - (scrollNode.clientHeight * offsetBase)) + pixelOffset; scrollNode.scrollTop = (trackedNode.offsetTop - (scrollNode.clientHeight * offsetBase)) + pixelOffset;
this._saveScrollState(); this.saveScrollState();
} }
}; };
_saveScrollState() { private saveScrollState(): void {
if (this.props.stickyBottom && this.isAtBottom()) { if (this.props.stickyBottom && this.isAtBottom()) {
this.scrollState = { stuckAtBottom: true }; this.scrollState = { stuckAtBottom: true };
debuglog("saved stuckAtBottom state"); debuglog("saved stuckAtBottom state");
return; return;
} }
const scrollNode = this._getScrollNode(); const scrollNode = this.getScrollNode();
const viewportBottom = scrollNode.scrollHeight - (scrollNode.scrollTop + scrollNode.clientHeight); const viewportBottom = scrollNode.scrollHeight - (scrollNode.scrollTop + scrollNode.clientHeight);
const itemlist = this._itemlist.current; const itemlist = this.itemlist.current;
const messages = itemlist.children; const messages = itemlist.children;
let node = null; let node = null;
// TODO: do a binary search here, as items are sorted by offsetTop // 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) // loop backwards, from bottom-most message (as that is the most common case)
for (let i = messages.length - 1; i >= 0; --i) { for (let i = messages.length - 1; i >= 0; --i) {
if (!messages[i].dataset.scrollTokens) { if (!(messages[i] as HTMLElement).dataset.scrollTokens) {
continue; continue;
} }
node = messages[i]; node = messages[i];
// break at the first message (coming from the bottom) // break at the first message (coming from the bottom)
// that has it's offsetTop above the bottom of the viewport. // 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 // Use this node as the scrollToken
break; break;
} }
@ -634,7 +660,7 @@ export default class ScrollPanel extends React.Component {
} }
const scrollToken = node.dataset.scrollTokens.split(',')[0]; const scrollToken = node.dataset.scrollTokens.split(',')[0];
debuglog("saving anchored scroll state to message", node && node.innerText, scrollToken); debuglog("saving anchored scroll state to message", node && node.innerText, scrollToken);
const bottomOffset = this._topFromBottom(node); const bottomOffset = this.topFromBottom(node);
this.scrollState = { this.scrollState = {
stuckAtBottom: false, stuckAtBottom: false,
trackedNode: node, trackedNode: node,
@ -644,35 +670,35 @@ export default class ScrollPanel extends React.Component {
}; };
} }
async _restoreSavedScrollState() { private async restoreSavedScrollState(): Promise<void> {
const scrollState = this.scrollState; const scrollState = this.scrollState;
if (scrollState.stuckAtBottom) { if (scrollState.stuckAtBottom) {
const sn = this._getScrollNode(); const sn = this.getScrollNode();
if (sn.scrollTop !== sn.scrollHeight) { if (sn.scrollTop !== sn.scrollHeight) {
sn.scrollTop = sn.scrollHeight; sn.scrollTop = sn.scrollHeight;
} }
} else if (scrollState.trackedScrollToken) { } else if (scrollState.trackedScrollToken) {
const itemlist = this._itemlist.current; const itemlist = this.itemlist.current;
const trackedNode = this._getTrackedNode(); const trackedNode = this.getTrackedNode();
if (trackedNode) { if (trackedNode) {
const newBottomOffset = this._topFromBottom(trackedNode); const newBottomOffset = this.topFromBottom(trackedNode);
const bottomDiff = newBottomOffset - scrollState.bottomOffset; const bottomDiff = newBottomOffset - scrollState.bottomOffset;
this._bottomGrowth += bottomDiff; this.bottomGrowth += bottomDiff;
scrollState.bottomOffset = newBottomOffset; scrollState.bottomOffset = newBottomOffset;
const newHeight = `${this._getListHeight()}px`; const newHeight = `${this.getListHeight()}px`;
if (itemlist.style.height !== newHeight) { if (itemlist.style.height !== newHeight) {
itemlist.style.height = newHeight; itemlist.style.height = newHeight;
} }
debuglog("balancing height because messages below viewport grew by", bottomDiff); debuglog("balancing height because messages below viewport grew by", bottomDiff);
} }
} }
if (!this._heightUpdateInProgress) { if (!this.heightUpdateInProgress) {
this._heightUpdateInProgress = true; this.heightUpdateInProgress = true;
try { try {
await this._updateHeight(); await this.updateHeight();
} finally { } finally {
this._heightUpdateInProgress = false; this.heightUpdateInProgress = false;
} }
} else { } else {
debuglog("not updating height because request already in progress"); debuglog("not updating height because request already in progress");
@ -680,11 +706,11 @@ export default class ScrollPanel extends React.Component {
} }
// need a better name that also indicates this will change scrollTop? Rebalance height? Reveal content? // need a better name that also indicates this will change scrollTop? Rebalance height? Reveal content?
async _updateHeight() { private async updateHeight(): Promise<void> {
// wait until user has stopped scrolling // wait until user has stopped scrolling
if (this._scrollTimeout.isRunning()) { if (this.scrollTimeout.isRunning()) {
debuglog("updateHeight waiting for scrolling to end ... "); debuglog("updateHeight waiting for scrolling to end ... ");
await this._scrollTimeout.finished(); await this.scrollTimeout.finished();
} else { } else {
debuglog("updateHeight getting straight to business, no scrolling going on."); debuglog("updateHeight getting straight to business, no scrolling going on.");
} }
@ -694,14 +720,14 @@ export default class ScrollPanel extends React.Component {
return; return;
} }
const sn = this._getScrollNode(); const sn = this.getScrollNode();
const itemlist = this._itemlist.current; const itemlist = this.itemlist.current;
const contentHeight = this._getMessagesHeight(); const contentHeight = this.getMessagesHeight();
const minHeight = sn.clientHeight; const minHeight = sn.clientHeight;
const height = Math.max(minHeight, contentHeight); const height = Math.max(minHeight, contentHeight);
this._pages = Math.ceil(height / PAGE_SIZE); this.pages = Math.ceil(height / PAGE_SIZE);
this._bottomGrowth = 0; this.bottomGrowth = 0;
const newHeight = `${this._getListHeight()}px`; const newHeight = `${this.getListHeight()}px`;
const scrollState = this.scrollState; const scrollState = this.scrollState;
if (scrollState.stuckAtBottom) { if (scrollState.stuckAtBottom) {
@ -713,7 +739,7 @@ export default class ScrollPanel extends React.Component {
} }
debuglog("updateHeight to", newHeight); debuglog("updateHeight to", newHeight);
} else if (scrollState.trackedScrollToken) { } else if (scrollState.trackedScrollToken) {
const trackedNode = this._getTrackedNode(); const trackedNode = this.getTrackedNode();
// if the timeline has been reloaded // if the timeline has been reloaded
// this can be called before scrollToBottom or whatever has been called // this can be called before scrollToBottom or whatever has been called
// so don't do anything if the node has disappeared from // so don't do anything if the node has disappeared from
@ -735,17 +761,17 @@ export default class ScrollPanel extends React.Component {
} }
} }
_getTrackedNode() { private getTrackedNode(): HTMLElement {
const scrollState = this.scrollState; const scrollState = this.scrollState;
const trackedNode = scrollState.trackedNode; const trackedNode = scrollState.trackedNode;
if (!trackedNode || !trackedNode.parentElement) { if (!trackedNode || !trackedNode.parentElement) {
let node; let node;
const messages = this._itemlist.current.children; const messages = this.itemlist.current.children;
const scrollToken = scrollState.trackedScrollToken; const scrollToken = scrollState.trackedScrollToken;
for (let i = messages.length-1; i >= 0; --i) { 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 // 'data-scroll-tokens' is a DOMString of comma-separated scroll tokens
// There might only be one scroll token // There might only be one scroll token
if (m.dataset.scrollTokens && if (m.dataset.scrollTokens &&
@ -768,45 +794,45 @@ export default class ScrollPanel extends React.Component {
return scrollState.trackedNode; return scrollState.trackedNode;
} }
_getListHeight() { private getListHeight(): number {
return this._bottomGrowth + (this._pages * PAGE_SIZE); return this.bottomGrowth + (this.pages * PAGE_SIZE);
} }
_getMessagesHeight() { private getMessagesHeight(): number {
const itemlist = this._itemlist.current; const itemlist = this.itemlist.current;
const lastNode = itemlist.lastElementChild; const lastNode = itemlist.lastElementChild as HTMLElement;
const lastNodeBottom = lastNode ? lastNode.offsetTop + lastNode.clientHeight : 0; 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 // 18 is itemlist padding
return lastNodeBottom - firstNodeTop + (18 * 2); 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 // 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 /* get the DOM node which has the scrollTop property we care about for our
* message panel. * message panel.
*/ */
_getScrollNode() { private getScrollNode(): HTMLDivElement {
if (this.unmounted) { if (this.unmounted) {
// this shouldn't happen, but when it does, turn the NPE into // this shouldn't happen, but when it does, turn the NPE into
// something more meaningful. // 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 // Likewise, we should have the ref by this point, but if not
// turn the NPE into something meaningful. // 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 => { private collectScroll = (divScroll: HTMLDivElement) => {
this._divScroll = divScroll; this.divScroll = divScroll;
}; };
/** /**
@ -814,15 +840,15 @@ export default class ScrollPanel extends React.Component {
anything below it changes, by calling updatePreventShrinking, to keep anything below it changes, by calling updatePreventShrinking, to keep
the same minimum bottom offset, effectively preventing the timeline to shrink. the same minimum bottom offset, effectively preventing the timeline to shrink.
*/ */
preventShrinking = () => { public preventShrinking = (): void => {
const messageList = this._itemlist.current; const messageList = this.itemlist.current;
const tiles = messageList && messageList.children; const tiles = messageList && messageList.children;
if (!messageList) { if (!messageList) {
return; return;
} }
let lastTileNode; let lastTileNode;
for (let i = tiles.length - 1; i >= 0; i--) { for (let i = tiles.length - 1; i >= 0; i--) {
const node = tiles[i]; const node = tiles[i] as HTMLElement;
if (node.dataset.scrollTokens) { if (node.dataset.scrollTokens) {
lastTileNode = node; lastTileNode = node;
break; break;
@ -841,8 +867,8 @@ export default class ScrollPanel extends React.Component {
}; };
/** Clear shrinking prevention. Used internally, and when the timeline is reloaded. */ /** Clear shrinking prevention. Used internally, and when the timeline is reloaded. */
clearPreventShrinking = () => { public clearPreventShrinking = (): void => {
const messageList = this._itemlist.current; const messageList = this.itemlist.current;
const balanceElement = messageList && messageList.parentElement; const balanceElement = messageList && messageList.parentElement;
if (balanceElement) balanceElement.style.paddingBottom = null; if (balanceElement) balanceElement.style.paddingBottom = null;
this.preventShrinkingState = null; this.preventShrinkingState = null;
@ -857,11 +883,11 @@ export default class ScrollPanel extends React.Component {
from the bottom of the marked tile grows larger than from the bottom of the marked tile grows larger than
what it was when marking. what it was when marking.
*/ */
updatePreventShrinking = () => { public updatePreventShrinking = (): void => {
if (this.preventShrinkingState) { if (this.preventShrinkingState) {
const sn = this._getScrollNode(); const sn = this.getScrollNode();
const scrollState = this.scrollState; const scrollState = this.scrollState;
const messageList = this._itemlist.current; const messageList = this.itemlist.current;
const { offsetNode, offsetFromBottom } = this.preventShrinkingState; const { offsetNode, offsetFromBottom } = this.preventShrinkingState;
// element used to set paddingBottom to balance the typing notifs disappearing // element used to set paddingBottom to balance the typing notifs disappearing
const balanceElement = messageList.parentElement; const balanceElement = messageList.parentElement;
@ -898,13 +924,15 @@ export default class ScrollPanel extends React.Component {
// list-style-type: none; is no longer a list // list-style-type: none; is no longer a list
return ( return (
<AutoHideScrollbar <AutoHideScrollbar
wrappedRef={this._collectScroll} wrappedRef={this.collectScroll}
onScroll={this.onScroll} onScroll={this.onScroll}
onWheel={this.props.onUserScroll} onWheel={this.props.onUserScroll}
className={`mx_ScrollPanel ${this.props.className}`} style={this.props.style}> className={`mx_ScrollPanel ${this.props.className}`}
style={this.props.style}
>
{ this.props.fixedChildren } { this.props.fixedChildren }
<div className="mx_RoomView_messageListWrapper"> <div className="mx_RoomView_messageListWrapper">
<ol ref={this._itemlist} className="mx_RoomView_MessageList" aria-live="polite" role="list"> <ol ref={this.itemlist} className="mx_RoomView_MessageList" aria-live="polite" role="list">
{ this.props.children } { this.props.children }
</ol> </ol>
</div> </div>

View file

@ -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"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with 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. limitations under the License.
*/ */
import React from 'react'; import React, { ErrorInfo } from 'react';
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import { MatrixClientPeg } from '../../../MatrixClientPeg'; import { MatrixClientPeg } from '../../../MatrixClientPeg';
import PlatformPeg from '../../../PlatformPeg'; import PlatformPeg from '../../../PlatformPeg';
import Modal from '../../../Modal'; import Modal from '../../../Modal';
import SdkConfig from "../../../SdkConfig"; 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 * This error boundary component can be used to wrap large content areas and
* catch exceptions during rendering in the component tree below them. * catch exceptions during rendering in the component tree below them.
*/ */
@replaceableComponent("views.elements.ErrorBoundary") @replaceableComponent("views.elements.ErrorBoundary")
export default class ErrorBoundary extends React.PureComponent { export default class ErrorBoundary extends React.PureComponent<{}, IState> {
constructor(props) { constructor(props) {
super(props); super(props);
@ -37,13 +43,13 @@ export default class ErrorBoundary extends React.PureComponent {
}; };
} }
static getDerivedStateFromError(error) { static getDerivedStateFromError(error: Error): Partial<IState> {
// Side effects are not permitted here, so we only update the state so // Side effects are not permitted here, so we only update the state so
// that the next render shows an error message. // that the next render shows an error message.
return { error }; return { error };
} }
componentDidCatch(error, { componentStack }) { componentDidCatch(error: Error, { componentStack }: ErrorInfo): void {
// Browser consoles are better at formatting output when native errors are passed // Browser consoles are better at formatting output when native errors are passed
// in their own `console.error` invocation. // in their own `console.error` invocation.
console.error(error); console.error(error);
@ -53,7 +59,7 @@ export default class ErrorBoundary extends React.PureComponent {
); );
} }
_onClearCacheAndReload = () => { private onClearCacheAndReload = (): void => {
if (!PlatformPeg.get()) return; if (!PlatformPeg.get()) return;
MatrixClientPeg.get().stopClient(); MatrixClientPeg.get().stopClient();
@ -62,11 +68,7 @@ export default class ErrorBoundary extends React.PureComponent {
}); });
}; };
_onBugReport = () => { private onBugReport = (): void => {
const BugReportDialog = sdk.getComponent("dialogs.BugReportDialog");
if (!BugReportDialog) {
return;
}
Modal.createTrackedDialog('Bug Report Dialog', '', BugReportDialog, { Modal.createTrackedDialog('Bug Report Dialog', '', BugReportDialog, {
label: 'react-soft-crash', label: 'react-soft-crash',
}); });
@ -74,7 +76,6 @@ export default class ErrorBoundary extends React.PureComponent {
render() { render() {
if (this.state.error) { if (this.state.error) {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const newIssueUrl = "https://github.com/vector-im/element-web/issues/new"; const newIssueUrl = "https://github.com/vector-im/element-web/issues/new";
let bugReportSection; let bugReportSection;
@ -95,7 +96,7 @@ export default class ErrorBoundary extends React.PureComponent {
"the rooms or groups you have visited and the usernames of " + "the rooms or groups you have visited and the usernames of " +
"other users. They do not contain messages.", "other users. They do not contain messages.",
)}</p> )}</p>
<AccessibleButton onClick={this._onBugReport} kind='primary'> <AccessibleButton onClick={this.onBugReport} kind='primary'>
{_t("Submit debug logs")} {_t("Submit debug logs")}
</AccessibleButton> </AccessibleButton>
</React.Fragment>; </React.Fragment>;
@ -105,7 +106,7 @@ export default class ErrorBoundary extends React.PureComponent {
<div className="mx_ErrorBoundary_body"> <div className="mx_ErrorBoundary_body">
<h1>{_t("Something went wrong!")}</h1> <h1>{_t("Something went wrong!")}</h1>
{ bugReportSection } { bugReportSection }
<AccessibleButton onClick={this._onClearCacheAndReload} kind='danger'> <AccessibleButton onClick={this.onClearCacheAndReload} kind='danger'>
{_t("Clear cache and reload")} {_t("Clear cache and reload")}
</AccessibleButton> </AccessibleButton>
</div> </div>

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React, { ReactChildren, useEffect } from 'react'; import React, { ReactNode, useEffect } from 'react';
import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { RoomMember } from "matrix-js-sdk/src/models/room-member"; import { RoomMember } from "matrix-js-sdk/src/models/room-member";
@ -31,11 +31,11 @@ interface IProps {
// Whether or not to begin with state.expanded=true // Whether or not to begin with state.expanded=true
startExpanded?: boolean, startExpanded?: boolean,
// The list of room members for which to show avatars next to the summary // 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 // The text to show as the summary of this event list
summaryText?: string, summaryText?: string;
// An array of EventTiles to render when expanded // An array of EventTiles to render when expanded
children: ReactChildren, children: ReactNode[];
// Called when the event list expansion is toggled // Called when the event list expansion is toggled
onToggle?(): void; onToggle?(): void;
} }

View file

@ -16,7 +16,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React, { ReactChildren } from 'react'; import React, { ComponentProps } from 'react';
import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { RoomMember } from "matrix-js-sdk/src/models/room-member"; import { RoomMember } from "matrix-js-sdk/src/models/room-member";
@ -26,21 +26,11 @@ import { isValid3pidInvite } from "../../../RoomInvite";
import EventListSummary from "./EventListSummary"; import EventListSummary from "./EventListSummary";
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
interface IProps { interface IProps extends Omit<ComponentProps<typeof EventListSummary>, "summaryText" | "summaryMembers"> {
// An array of member events to summarise
events: MatrixEvent[];
// The maximum number of names to show in either each summary e.g. 2 would result "A, B and 234 others left" // The maximum number of names to show in either each summary e.g. 2 would result "A, B and 234 others left"
summaryLength?: number; summaryLength?: number;
// The maximum number of avatars to display in the summary // The maximum number of avatars to display in the summary
avatarsMaxLength?: number; 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 { interface IUserEvents {

View file

@ -1,6 +1,6 @@
/* /*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2018 Michael Telatynski <7t3chguy@gmail.com> Copyright 2018 Michael Telatynski <7t3chguy@gmail.com>
Copyright 2015 - 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -16,12 +16,12 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import { formatFullDateNoTime } from '../../../DateUtils'; import { formatFullDateNoTime } from '../../../DateUtils';
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
function getdaysArray() { function getDaysArray(): string[] {
return [ return [
_t('Sunday'), _t('Sunday'),
_t('Monday'), _t('Monday'),
@ -33,17 +33,17 @@ function getdaysArray() {
]; ];
} }
@replaceableComponent("views.messages.DateSeparator") interface IProps {
export default class DateSeparator extends React.Component { ts: number;
static propTypes = { }
ts: PropTypes.number.isRequired,
};
getLabel() { @replaceableComponent("views.messages.DateSeparator")
export default class DateSeparator extends React.Component<IProps> {
private getLabel() {
const date = new Date(this.props.ts); const date = new Date(this.props.ts);
const today = new Date(); const today = new Date();
const yesterday = new Date(); const yesterday = new Date();
const days = getdaysArray(); const days = getDaysArray();
yesterday.setDate(today.getDate() - 1); yesterday.setDate(today.getDate() - 1);
if (date.toDateString() === today.toDateString()) { if (date.toDateString() === today.toDateString()) {

View file

@ -1,5 +1,5 @@
/* /*
Copyright 2020 The Matrix.org Foundation C.I.C. Copyright 2020 - 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -16,14 +16,24 @@ limitations under the License.
import React from 'react'; import React from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import * as sdk from '../../../index';
import Modal from '../../../Modal'; import Modal from '../../../Modal';
import SdkConfig from "../../../SdkConfig"; import SdkConfig from "../../../SdkConfig";
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import BugReportDialog from '../dialogs/BugReportDialog';
interface IProps {
mxEvent: MatrixEvent;
}
interface IState {
error: Error;
}
@replaceableComponent("views.messages.TileErrorBoundary") @replaceableComponent("views.messages.TileErrorBoundary")
export default class TileErrorBoundary extends React.Component { export default class TileErrorBoundary extends React.Component<IProps, IState> {
constructor(props) { constructor(props) {
super(props); super(props);
@ -32,17 +42,13 @@ export default class TileErrorBoundary extends React.Component {
}; };
} }
static getDerivedStateFromError(error) { static getDerivedStateFromError(error: Error): Partial<IState> {
// Side effects are not permitted here, so we only update the state so // Side effects are not permitted here, so we only update the state so
// that the next render shows an error message. // that the next render shows an error message.
return { error }; return { error };
} }
_onBugReport = () => { private onBugReport = (): void => {
const BugReportDialog = sdk.getComponent("dialogs.BugReportDialog");
if (!BugReportDialog) {
return;
}
Modal.createTrackedDialog('Bug Report Dialog', '', BugReportDialog, { Modal.createTrackedDialog('Bug Report Dialog', '', BugReportDialog, {
label: 'react-soft-crash-tile', label: 'react-soft-crash-tile',
}); });
@ -60,7 +66,7 @@ export default class TileErrorBoundary extends React.Component {
let submitLogsButton; let submitLogsButton;
if (SdkConfig.get().bug_report_endpoint_url) { if (SdkConfig.get().bug_report_endpoint_url) {
submitLogsButton = <a onClick={this._onBugReport} href="#"> submitLogsButton = <a onClick={this.onBugReport} href="#">
{_t("Submit logs")} {_t("Submit logs")}
</a>; </a>;
} }

View file

@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React from 'react'; import React, { createRef } from 'react';
import classNames from "classnames"; import classNames from "classnames";
import { EventType } from "matrix-js-sdk/src/@types/event"; import { EventType } from "matrix-js-sdk/src/@types/event";
import { EventStatus, MatrixEvent } from "matrix-js-sdk/src/models/event"; import { EventStatus, MatrixEvent } from "matrix-js-sdk/src/models/event";
@ -176,12 +176,19 @@ const MAX_READ_AVATARS = 5;
// | '--------------------------------------' | // | '--------------------------------------' |
// '----------------------------------------------------------' // '----------------------------------------------------------'
interface IReadReceiptProps { export interface IReadReceiptProps {
userId: string; userId: string;
roomMember: RoomMember; roomMember: RoomMember;
ts: number; ts: number;
} }
export enum TileShape {
Notif = "notif",
FileGrid = "file_grid",
Reply = "reply",
ReplyPreview = "reply_preview",
}
interface IProps { interface IProps {
// the MatrixEvent to show // the MatrixEvent to show
mxEvent: MatrixEvent; mxEvent: MatrixEvent;
@ -248,7 +255,7 @@ interface IProps {
// It could also be done by subclassing EventTile, but that'd be quite // It could also be done by subclassing EventTile, but that'd be quite
// boiilerplatey. So just make the necessary render decisions conditional // boiilerplatey. So just make the necessary render decisions conditional
// for now. // for now.
tileShape?: 'notif' | 'file_grid' | 'reply' | 'reply_preview'; tileShape?: TileShape;
// show twelve hour timestamps // show twelve hour timestamps
isTwelveHour?: boolean; isTwelveHour?: boolean;
@ -306,10 +313,11 @@ interface IState {
export default class EventTile extends React.Component<IProps, IState> { export default class EventTile extends React.Component<IProps, IState> {
private suppressReadReceiptAnimation: boolean; private suppressReadReceiptAnimation: boolean;
private isListeningForReceipts: boolean; private isListeningForReceipts: boolean;
private ref: React.RefObject<unknown>;
private tile = React.createRef(); private tile = React.createRef();
private replyThread = React.createRef(); private replyThread = React.createRef();
public readonly ref = createRef<HTMLElement>();
static defaultProps = { static defaultProps = {
// no-op function because onHeightChanged is optional yet some sub-components assume its existence // no-op function because onHeightChanged is optional yet some sub-components assume its existence
onHeightChanged: function() {}, onHeightChanged: function() {},
@ -345,8 +353,6 @@ export default class EventTile extends React.Component<IProps, IState> {
// to determine if we've already subscribed and use a combination of other flags to find // to determine if we've already subscribed and use a combination of other flags to find
// out if we should even be subscribed at all. // out if we should even be subscribed at all.
this.isListeningForReceipts = false; this.isListeningForReceipts = false;
this.ref = React.createRef();
} }
/** /**