Merge pull request #6243 from matrix-org/t3chguy/ts/8
This commit is contained in:
commit
3fa89f3294
14 changed files with 1040 additions and 912 deletions
14
src/@types/global.d.ts
vendored
14
src/@types/global.d.ts
vendored
|
@ -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 {
|
||||||
|
|
|
@ -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;
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -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}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -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));
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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>
|
File diff suppressed because it is too large
Load diff
|
@ -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>
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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()) {
|
|
@ -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>;
|
||||||
}
|
}
|
|
@ -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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
Loading…
Reference in a new issue