Merge branch 't3chguy/ts/8' into text-for-event-perf
This commit is contained in:
commit
f9fe28a6ad
179 changed files with 5561 additions and 3317 deletions
|
@ -15,12 +15,12 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import React, { HTMLAttributes, WheelEvent } from "react";
|
||||
|
||||
interface IProps {
|
||||
interface IProps extends Omit<HTMLAttributes<HTMLDivElement>, "onScroll"> {
|
||||
className?: string;
|
||||
onScroll?: () => void;
|
||||
onWheel?: () => void;
|
||||
onScroll?: (event: Event) => void;
|
||||
onWheel?: (event: WheelEvent) => void;
|
||||
style?: React.CSSProperties
|
||||
tabIndex?: number,
|
||||
wrappedRef?: (ref: HTMLDivElement) => void;
|
||||
|
@ -52,14 +52,18 @@ export default class AutoHideScrollbar extends React.Component<IProps> {
|
|||
}
|
||||
|
||||
public render() {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { className, onScroll, onWheel, style, tabIndex, wrappedRef, children, ...otherProps } = this.props;
|
||||
|
||||
return (<div
|
||||
{...otherProps}
|
||||
ref={this.containerRef}
|
||||
style={this.props.style}
|
||||
className={["mx_AutoHideScrollbar", this.props.className].join(" ")}
|
||||
onWheel={this.props.onWheel}
|
||||
tabIndex={this.props.tabIndex}
|
||||
style={style}
|
||||
className={["mx_AutoHideScrollbar", className].join(" ")}
|
||||
onWheel={onWheel}
|
||||
tabIndex={tabIndex}
|
||||
>
|
||||
{ this.props.children }
|
||||
{ children }
|
||||
</div>);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,7 +24,6 @@ import * as sdk from '../../index';
|
|||
import dis from '../../dispatcher/dispatcher';
|
||||
import { _t } from '../../languageHandler';
|
||||
|
||||
import { Droppable } from 'react-beautiful-dnd';
|
||||
import classNames from 'classnames';
|
||||
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
||||
import AutoHideScrollbar from "./AutoHideScrollbar";
|
||||
|
@ -83,7 +82,7 @@ class GroupFilterPanel extends React.Component {
|
|||
}
|
||||
};
|
||||
|
||||
onMouseDown = e => {
|
||||
onClick = e => {
|
||||
// only dispatch if its not a no-op
|
||||
if (this.state.selectedTags.length > 0) {
|
||||
dis.dispatch({action: 'deselect_tags'});
|
||||
|
@ -151,28 +150,15 @@ class GroupFilterPanel extends React.Component {
|
|||
return <div className={classes} onClick={this.onClearFilterClick}>
|
||||
<AutoHideScrollbar
|
||||
className="mx_GroupFilterPanel_scroller"
|
||||
// XXX: Use onMouseDown as a workaround for https://github.com/atlassian/react-beautiful-dnd/issues/273
|
||||
// instead of onClick. Otherwise we experience https://github.com/vector-im/element-web/issues/6253
|
||||
onMouseDown={this.onMouseDown}
|
||||
onClick={this.onClick}
|
||||
>
|
||||
<Droppable
|
||||
droppableId="tag-panel-droppable"
|
||||
type="draggable-TagTile"
|
||||
>
|
||||
{ (provided, snapshot) => (
|
||||
<div
|
||||
className="mx_GroupFilterPanel_tagTileContainer"
|
||||
ref={provided.innerRef}
|
||||
>
|
||||
{ this.renderGlobalIcon() }
|
||||
{ tags }
|
||||
<div>
|
||||
{createButton}
|
||||
</div>
|
||||
{ provided.placeholder }
|
||||
</div>
|
||||
) }
|
||||
</Droppable>
|
||||
<div className="mx_GroupFilterPanel_tagTileContainer">
|
||||
{ this.renderGlobalIcon() }
|
||||
{ tags }
|
||||
<div>
|
||||
{ createButton }
|
||||
</div>
|
||||
</div>
|
||||
</AutoHideScrollbar>
|
||||
</div>;
|
||||
}
|
||||
|
|
|
@ -185,21 +185,24 @@ export default class IndicatorScrollbar extends React.Component {
|
|||
};
|
||||
|
||||
render() {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const { children, trackHorizontalOverflow, verticalScrollsHorizontally, ...otherProps } = this.props;
|
||||
|
||||
const leftIndicatorStyle = {left: this.state.leftIndicatorOffset};
|
||||
const rightIndicatorStyle = {right: this.state.rightIndicatorOffset};
|
||||
const leftOverflowIndicator = this.props.trackHorizontalOverflow
|
||||
const leftOverflowIndicator = trackHorizontalOverflow
|
||||
? <div className="mx_IndicatorScrollbar_leftOverflowIndicator" style={leftIndicatorStyle} /> : null;
|
||||
const rightOverflowIndicator = this.props.trackHorizontalOverflow
|
||||
const rightOverflowIndicator = trackHorizontalOverflow
|
||||
? <div className="mx_IndicatorScrollbar_rightOverflowIndicator" style={rightIndicatorStyle} /> : null;
|
||||
|
||||
return (<AutoHideScrollbar
|
||||
ref={this._collectScrollerComponent}
|
||||
wrappedRef={this._collectScroller}
|
||||
onWheel={this.onMouseWheel}
|
||||
{...this.props}
|
||||
{...otherProps}
|
||||
>
|
||||
{ leftOverflowIndicator }
|
||||
{ this.props.children }
|
||||
{ children }
|
||||
{ rightOverflowIndicator }
|
||||
</AutoHideScrollbar>);
|
||||
}
|
||||
|
|
|
@ -19,19 +19,16 @@ limitations under the License.
|
|||
import * as React from 'react';
|
||||
import * as PropTypes from 'prop-types';
|
||||
import { MatrixClient } from 'matrix-js-sdk/src/client';
|
||||
import { DragDropContext } from 'react-beautiful-dnd';
|
||||
|
||||
import {Key} from '../../Keyboard';
|
||||
import PageTypes from '../../PageTypes';
|
||||
import CallMediaHandler from '../../CallMediaHandler';
|
||||
import MediaDeviceHandler from '../../MediaDeviceHandler';
|
||||
import { fixupColorFonts } from '../../utils/FontManager';
|
||||
import * as sdk from '../../index';
|
||||
import dis from '../../dispatcher/dispatcher';
|
||||
import { IMatrixClientCreds } from '../../MatrixClientPeg';
|
||||
import SettingsStore from "../../settings/SettingsStore";
|
||||
|
||||
import TagOrderActions from '../../actions/TagOrderActions';
|
||||
import RoomListActions from '../../actions/RoomListActions';
|
||||
import ResizeHandle from '../views/elements/ResizeHandle';
|
||||
import {Resizer, CollapseDistributor} from '../../resizer';
|
||||
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
||||
|
@ -170,7 +167,7 @@ class LoggedInView extends React.Component<IProps, IState> {
|
|||
// stash the MatrixClient in case we log out before we are unmounted
|
||||
this._matrixClient = this.props.matrixClient;
|
||||
|
||||
CallMediaHandler.loadDevices();
|
||||
MediaDeviceHandler.loadDevices();
|
||||
|
||||
fixupColorFonts();
|
||||
|
||||
|
@ -569,50 +566,6 @@ class LoggedInView extends React.Component<IProps, IState> {
|
|||
}
|
||||
};
|
||||
|
||||
_onDragEnd = (result) => {
|
||||
// Dragged to an invalid destination, not onto a droppable
|
||||
if (!result.destination) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dest = result.destination.droppableId;
|
||||
|
||||
if (dest === 'tag-panel-droppable') {
|
||||
// Could be "GroupTile +groupId:domain"
|
||||
const draggableId = result.draggableId.split(' ').pop();
|
||||
|
||||
// Dispatch synchronously so that the GroupFilterPanel receives an
|
||||
// optimistic update from GroupFilterOrderStore before the previous
|
||||
// state is shown.
|
||||
dis.dispatch(TagOrderActions.moveTag(
|
||||
this._matrixClient,
|
||||
draggableId,
|
||||
result.destination.index,
|
||||
), true);
|
||||
} else if (dest.startsWith('room-sub-list-droppable_')) {
|
||||
this._onRoomTileEndDrag(result);
|
||||
}
|
||||
};
|
||||
|
||||
_onRoomTileEndDrag = (result) => {
|
||||
let newTag = result.destination.droppableId.split('_')[1];
|
||||
let prevTag = result.source.droppableId.split('_')[1];
|
||||
if (newTag === 'undefined') newTag = undefined;
|
||||
if (prevTag === 'undefined') prevTag = undefined;
|
||||
|
||||
const roomId = result.draggableId.split('_')[1];
|
||||
|
||||
const oldIndex = result.source.index;
|
||||
const newIndex = result.destination.index;
|
||||
|
||||
dis.dispatch(RoomListActions.tagRoom(
|
||||
this._matrixClient,
|
||||
this._matrixClient.getRoom(roomId),
|
||||
prevTag, newTag,
|
||||
oldIndex, newIndex,
|
||||
), true);
|
||||
};
|
||||
|
||||
render() {
|
||||
const RoomView = sdk.getComponent('structures.RoomView');
|
||||
const UserView = sdk.getComponent('structures.UserView');
|
||||
|
@ -679,17 +632,15 @@ class LoggedInView extends React.Component<IProps, IState> {
|
|||
aria-hidden={this.props.hideToSRUsers}
|
||||
>
|
||||
<ToastContainer />
|
||||
<DragDropContext onDragEnd={this._onDragEnd}>
|
||||
<div ref={this._resizeContainer} className={bodyClasses}>
|
||||
{ SettingsStore.getValue("feature_spaces") ? <SpacePanel /> : null }
|
||||
<LeftPanel
|
||||
isMinimized={this.props.collapseLhs || false}
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
/>
|
||||
<ResizeHandle />
|
||||
{ pageElement }
|
||||
</div>
|
||||
</DragDropContext>
|
||||
<div ref={this._resizeContainer} className={bodyClasses}>
|
||||
{ SettingsStore.getValue("feature_spaces") ? <SpacePanel /> : null }
|
||||
<LeftPanel
|
||||
isMinimized={this.props.collapseLhs || false}
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
/>
|
||||
<ResizeHandle />
|
||||
{ pageElement }
|
||||
</div>
|
||||
</div>
|
||||
<CallContainer />
|
||||
<NonUrgentToastContainer />
|
||||
|
|
|
@ -48,7 +48,7 @@ import createRoom, {IOpts} from "../../createRoom";
|
|||
import {_t, _td, getCurrentLanguage} from '../../languageHandler';
|
||||
import SettingsStore from "../../settings/SettingsStore";
|
||||
import ThemeController from "../../settings/controllers/ThemeController";
|
||||
import { startAnyRegistrationFlow } from "../../Registration.js";
|
||||
import { startAnyRegistrationFlow } from "../../Registration";
|
||||
import { messageForSyncError } from '../../utils/ErrorUtils';
|
||||
import ResizeNotifier from "../../utils/ResizeNotifier";
|
||||
import AutoDiscoveryUtils, { ValidatedServerConfig } from "../../utils/AutoDiscoveryUtils";
|
||||
|
@ -1461,7 +1461,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
});
|
||||
|
||||
const dft = new DecryptionFailureTracker((total, errorCode) => {
|
||||
Analytics.trackEvent('E2E', 'Decryption failure', errorCode, total);
|
||||
Analytics.trackEvent('E2E', 'Decryption failure', errorCode, String(total));
|
||||
CountlyAnalytics.instance.track("decryption_failure", { errorCode }, null, { sum: total });
|
||||
}, (errorCode) => {
|
||||
// Map JS-SDK error codes to tracker codes for aggregation
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -82,8 +82,7 @@ export default class MyGroups extends React.Component {
|
|||
</p>
|
||||
<p>
|
||||
{ _t(
|
||||
"To set up a filter, drag a community avatar over to the filter panel on " +
|
||||
"the far left hand side of the screen. You can click on an avatar in the " +
|
||||
"You can click on an avatar in the " +
|
||||
"filter panel at any time to see only the rooms and people associated " +
|
||||
"with that community.",
|
||||
) }
|
||||
|
|
|
@ -22,6 +22,7 @@ import BaseCard from "../views/right_panel/BaseCard";
|
|||
import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||
import TimelinePanel from "./TimelinePanel";
|
||||
import Spinner from "../views/elements/Spinner";
|
||||
import { TileShape } from "../views/rooms/EventTile";
|
||||
|
||||
interface IProps {
|
||||
onClose(): void;
|
||||
|
@ -48,7 +49,7 @@ export default class NotificationPanel extends React.PureComponent<IProps> {
|
|||
manageReadMarkers={false}
|
||||
timelineSet={timelineSet}
|
||||
showUrlPreview={false}
|
||||
tileShape="notif"
|
||||
tileShape={TileShape.Notif}
|
||||
empty={emptyState}
|
||||
alwaysShowTimestamps={true}
|
||||
/>
|
||||
|
|
|
@ -207,9 +207,9 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
|
|||
this.getMoreRooms();
|
||||
};
|
||||
|
||||
private getMoreRooms() {
|
||||
if (this.state.selectedCommunityId) return Promise.resolve(); // no more rooms
|
||||
if (!MatrixClientPeg.get()) return Promise.resolve();
|
||||
private getMoreRooms(): Promise<boolean> {
|
||||
if (this.state.selectedCommunityId) return Promise.resolve(false); // no more rooms
|
||||
if (!MatrixClientPeg.get()) return Promise.resolve(false);
|
||||
|
||||
this.setState({
|
||||
loading: true,
|
||||
|
@ -239,12 +239,12 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
|
|||
// if the filter or server has changed since this request was sent,
|
||||
// throw away the result (don't even clear the busy flag
|
||||
// since we must still have a request in flight)
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.unmounted) {
|
||||
// if we've been unmounted, we don't care either.
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.state.filterString) {
|
||||
|
@ -264,14 +264,13 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
|
|||
filterString != this.state.filterString ||
|
||||
roomServer != this.state.roomServer ||
|
||||
nextBatch != this.nextBatch) {
|
||||
// as above: we don't care about errors for old
|
||||
// requests either
|
||||
return;
|
||||
// as above: we don't care about errors for old requests either
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.unmounted) {
|
||||
// if we've been unmounted, we don't care either.
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
console.error("Failed to get publicRooms: %s", JSON.stringify(err));
|
||||
|
@ -337,11 +336,10 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
private onRoomClicked = (room: IRoom, ev: ButtonEvent) => {
|
||||
// If room was shift-clicked, remove it from the room directory
|
||||
if (ev.shiftKey && !this.state.selectedCommunityId) {
|
||||
ev.preventDefault();
|
||||
this.removeFromDirectory(room);
|
||||
} else {
|
||||
this.showRoom(room);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -568,11 +566,11 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
|
|||
let avatarUrl = null;
|
||||
if (room.avatar_url) avatarUrl = mediaFromMxc(room.avatar_url).getSquareThumbnailHttp(32);
|
||||
|
||||
// We use onMouseDown instead of onClick, so that we can avoid text getting selected
|
||||
return [
|
||||
<div key={ `${room.room_id}_avatar` }
|
||||
onClick={(ev) => this.onRoomClicked(room, ev)}
|
||||
// cancel onMouseDown otherwise shift-clicking highlights text
|
||||
onMouseDown={(ev) => {ev.preventDefault();}}
|
||||
<div
|
||||
key={ `${room.room_id}_avatar` }
|
||||
onMouseDown={(ev) => this.onRoomClicked(room, ev)}
|
||||
className="mx_RoomDirectory_roomAvatar"
|
||||
>
|
||||
<BaseAvatar
|
||||
|
@ -584,42 +582,50 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
|
|||
url={avatarUrl}
|
||||
/>
|
||||
</div>,
|
||||
<div key={ `${room.room_id}_description` }
|
||||
onClick={(ev) => this.onRoomClicked(room, ev)}
|
||||
// cancel onMouseDown otherwise shift-clicking highlights text
|
||||
onMouseDown={(ev) => {ev.preventDefault();}}
|
||||
<div
|
||||
key={ `${room.room_id}_description` }
|
||||
onMouseDown={(ev) => this.onRoomClicked(room, ev)}
|
||||
className="mx_RoomDirectory_roomDescription"
|
||||
>
|
||||
<div className="mx_RoomDirectory_name">{ name }</div>
|
||||
<div className="mx_RoomDirectory_topic"
|
||||
onClick={ (ev) => { ev.stopPropagation(); } }
|
||||
<div
|
||||
className="mx_RoomDirectory_name"
|
||||
onMouseDown={(ev) => this.onRoomClicked(room, ev)}
|
||||
>
|
||||
{ name }
|
||||
</div>
|
||||
<div
|
||||
className="mx_RoomDirectory_topic"
|
||||
onMouseDown={(ev) => this.onRoomClicked(room, ev)}
|
||||
dangerouslySetInnerHTML={{ __html: topic }}
|
||||
/>
|
||||
<div className="mx_RoomDirectory_alias">{ getDisplayAliasForRoom(room) }</div>
|
||||
<div
|
||||
className="mx_RoomDirectory_alias"
|
||||
onMouseDown={(ev) => this.onRoomClicked(room, ev)}
|
||||
>
|
||||
{ getDisplayAliasForRoom(room) }
|
||||
</div>
|
||||
</div>,
|
||||
<div key={ `${room.room_id}_memberCount` }
|
||||
onClick={(ev) => this.onRoomClicked(room, ev)}
|
||||
// cancel onMouseDown otherwise shift-clicking highlights text
|
||||
onMouseDown={(ev) => {ev.preventDefault();}}
|
||||
<div
|
||||
key={ `${room.room_id}_memberCount` }
|
||||
onMouseDown={(ev) => this.onRoomClicked(room, ev)}
|
||||
className="mx_RoomDirectory_roomMemberCount"
|
||||
>
|
||||
{ room.num_joined_members }
|
||||
</div>,
|
||||
<div key={ `${room.room_id}_preview` }
|
||||
onClick={(ev) => this.onRoomClicked(room, ev)}
|
||||
<div
|
||||
key={ `${room.room_id}_preview` }
|
||||
onMouseDown={(ev) => this.onRoomClicked(room, ev)}
|
||||
// cancel onMouseDown otherwise shift-clicking highlights text
|
||||
onMouseDown={(ev) => {ev.preventDefault();}}
|
||||
className="mx_RoomDirectory_preview"
|
||||
>
|
||||
{previewButton}
|
||||
{ previewButton }
|
||||
</div>,
|
||||
<div key={ `${room.room_id}_join` }
|
||||
onClick={(ev) => this.onRoomClicked(room, ev)}
|
||||
// cancel onMouseDown otherwise shift-clicking highlights text
|
||||
onMouseDown={(ev) => {ev.preventDefault();}}
|
||||
<div
|
||||
key={ `${room.room_id}_join` }
|
||||
onMouseDown={(ev) => this.onRoomClicked(room, ev)}
|
||||
className="mx_RoomDirectory_join"
|
||||
>
|
||||
{joinOrViewButton}
|
||||
{ joinOrViewButton }
|
||||
</div>,
|
||||
];
|
||||
}
|
||||
|
|
|
@ -23,7 +23,7 @@ limitations under the License.
|
|||
|
||||
import React, { createRef } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { IRecommendedVersion, NotificationCountType, Room } from "matrix-js-sdk/src/models/room";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { SearchResult } from "matrix-js-sdk/src/models/search-result";
|
||||
import { EventSubscription } from "fbemitter";
|
||||
|
@ -60,7 +60,7 @@ import ScrollPanel from "./ScrollPanel";
|
|||
import TimelinePanel from "./TimelinePanel";
|
||||
import ErrorBoundary from "../views/elements/ErrorBoundary";
|
||||
import RoomPreviewBar from "../views/rooms/RoomPreviewBar";
|
||||
import SearchBar from "../views/rooms/SearchBar";
|
||||
import SearchBar, { SearchScope } from "../views/rooms/SearchBar";
|
||||
import RoomUpgradeWarningBar from "../views/rooms/RoomUpgradeWarningBar";
|
||||
import AuxPanel from "../views/rooms/AuxPanel";
|
||||
import RoomHeader from "../views/rooms/RoomHeader";
|
||||
|
@ -139,7 +139,7 @@ export interface IState {
|
|||
draggingFile: boolean;
|
||||
searching: boolean;
|
||||
searchTerm?: string;
|
||||
searchScope?: "All" | "Room";
|
||||
searchScope?: SearchScope;
|
||||
searchResults?: XOR<{}, {
|
||||
count: number;
|
||||
highlights: string[];
|
||||
|
@ -172,11 +172,7 @@ export interface IState {
|
|||
// We load this later by asking the js-sdk to suggest a version for us.
|
||||
// This object is the result of Room#getRecommendedVersion()
|
||||
|
||||
upgradeRecommendation?: {
|
||||
version: string;
|
||||
needsUpgrade: boolean;
|
||||
urgent: boolean;
|
||||
};
|
||||
upgradeRecommendation?: IRecommendedVersion;
|
||||
canReact: boolean;
|
||||
canReply: boolean;
|
||||
layout: Layout;
|
||||
|
@ -1134,7 +1130,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
}
|
||||
}
|
||||
|
||||
private onSearchResultsFillRequest = (backwards: boolean) => {
|
||||
private onSearchResultsFillRequest = (backwards: boolean): Promise<boolean> => {
|
||||
if (!backwards) {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
@ -1272,7 +1268,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
});
|
||||
}
|
||||
|
||||
private onSearch = (term: string, scope) => {
|
||||
private onSearch = (term: string, scope: SearchScope) => {
|
||||
this.setState({
|
||||
searchTerm: term,
|
||||
searchScope: scope,
|
||||
|
@ -1293,14 +1289,14 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
this.searchId = new Date().getTime();
|
||||
|
||||
let roomId;
|
||||
if (scope === "Room") roomId = this.state.room.roomId;
|
||||
if (scope === SearchScope.Room) roomId = this.state.room.roomId;
|
||||
|
||||
debuglog("sending search request");
|
||||
const searchPromise = eventSearch(term, roomId);
|
||||
this.handleSearchResult(searchPromise);
|
||||
};
|
||||
|
||||
private handleSearchResult(searchPromise: Promise<any>) {
|
||||
private handleSearchResult(searchPromise: Promise<any>): Promise<boolean> {
|
||||
// keep a record of the current search id, so that if the search terms
|
||||
// change before we get a response, we can ignore the results.
|
||||
const localSearchId = this.searchId;
|
||||
|
@ -1313,7 +1309,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
debuglog("search complete");
|
||||
if (this.unmounted || !this.state.searching || this.searchId != localSearchId) {
|
||||
console.error("Discarding stale search results");
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
// postgres on synapse returns us precise details of the strings
|
||||
|
@ -1345,6 +1341,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
description: ((error && error.message) ? error.message :
|
||||
_t("Server may be unavailable, overloaded, or search timed out :(")),
|
||||
});
|
||||
return false;
|
||||
}).finally(() => {
|
||||
this.setState({
|
||||
searchInProgress: false,
|
||||
|
@ -2063,7 +2060,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
if (!this.state.atEndOfLiveTimeline && !this.state.searchResults) {
|
||||
const JumpToBottomButton = sdk.getComponent('rooms.JumpToBottomButton');
|
||||
jumpToBottom = (<JumpToBottomButton
|
||||
highlight={this.state.room.getUnreadNotificationCount('highlight') > 0}
|
||||
highlight={this.state.room.getUnreadNotificationCount(NotificationCountType.Highlight) > 0}
|
||||
numUnreadMessages={this.state.numUnreadMessages}
|
||||
onScrollToBottomClick={this.jumpToLiveTimeline}
|
||||
roomId={this.state.roomId}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2015 - 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -14,17 +14,18 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, {createRef} from "react";
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { createRef, CSSProperties, ReactNode, SyntheticEvent, KeyboardEvent } from "react";
|
||||
|
||||
import Timer from '../../utils/Timer';
|
||||
import AutoHideScrollbar from "./AutoHideScrollbar";
|
||||
import {replaceableComponent} from "../../utils/replaceableComponent";
|
||||
import {getKeyBindingsManager, RoomAction} from "../../KeyBindingsManager";
|
||||
import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||
import { getKeyBindingsManager, RoomAction } from "../../KeyBindingsManager";
|
||||
import ResizeNotifier from "../../utils/ResizeNotifier";
|
||||
|
||||
const DEBUG_SCROLL = false;
|
||||
|
||||
// The amount of extra scroll distance to allow prior to unfilling.
|
||||
// See _getExcessHeight.
|
||||
// See getExcessHeight.
|
||||
const UNPAGINATION_PADDING = 6000;
|
||||
// The number of milliseconds to debounce calls to onUnfillRequest, to prevent
|
||||
// many scroll events causing many unfilling requests.
|
||||
|
@ -43,6 +44,75 @@ if (DEBUG_SCROLL) {
|
|||
debuglog = function() {};
|
||||
}
|
||||
|
||||
interface IProps {
|
||||
/* stickyBottom: if set to true, then once the user hits the bottom of
|
||||
* the list, any new children added to the list will cause the list to
|
||||
* scroll down to show the new element, rather than preserving the
|
||||
* existing view.
|
||||
*/
|
||||
stickyBottom?: boolean;
|
||||
|
||||
/* startAtBottom: if set to true, the view is assumed to start
|
||||
* scrolled to the bottom.
|
||||
* XXX: It's likely this is unnecessary and can be derived from
|
||||
* stickyBottom, but I'm adding an extra parameter to ensure
|
||||
* behaviour stays the same for other uses of ScrollPanel.
|
||||
* If so, let's remove this parameter down the line.
|
||||
*/
|
||||
startAtBottom?: boolean;
|
||||
|
||||
/* className: classnames to add to the top-level div
|
||||
*/
|
||||
className?: string;
|
||||
|
||||
/* style: styles to add to the top-level div
|
||||
*/
|
||||
style?: CSSProperties;
|
||||
|
||||
/* resizeNotifier: ResizeNotifier to know when middle column has changed size
|
||||
*/
|
||||
resizeNotifier?: ResizeNotifier;
|
||||
|
||||
/* fixedChildren: allows for children to be passed which are rendered outside
|
||||
* of the wrapper
|
||||
*/
|
||||
fixedChildren?: ReactNode;
|
||||
|
||||
/* onFillRequest(backwards): a callback which is called on scroll when
|
||||
* the user nears the start (backwards = true) or end (backwards =
|
||||
* false) of the list.
|
||||
*
|
||||
* This should return a promise; no more calls will be made until the
|
||||
* promise completes.
|
||||
*
|
||||
* The promise should resolve to true if there is more data to be
|
||||
* retrieved in this direction (in which case onFillRequest may be
|
||||
* called again immediately), or false if there is no more data in this
|
||||
* directon (at this time) - which will stop the pagination cycle until
|
||||
* the user scrolls again.
|
||||
*/
|
||||
onFillRequest?(backwards: boolean): Promise<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.
|
||||
*
|
||||
* It wraps a list of <li> children; when items are added to the start or end
|
||||
|
@ -84,97 +154,54 @@ if (DEBUG_SCROLL) {
|
|||
* offset as normal.
|
||||
*/
|
||||
|
||||
export interface IScrollState {
|
||||
stuckAtBottom: boolean;
|
||||
trackedNode?: HTMLElement;
|
||||
trackedScrollToken?: string;
|
||||
bottomOffset?: number;
|
||||
pixelOffset?: number;
|
||||
}
|
||||
|
||||
interface IPreventShrinkingState {
|
||||
offsetFromBottom: number;
|
||||
offsetNode: HTMLElement;
|
||||
}
|
||||
|
||||
@replaceableComponent("structures.ScrollPanel")
|
||||
export default class ScrollPanel extends React.Component {
|
||||
static propTypes = {
|
||||
/* stickyBottom: if set to true, then once the user hits the bottom of
|
||||
* the list, any new children added to the list will cause the list to
|
||||
* scroll down to show the new element, rather than preserving the
|
||||
* existing view.
|
||||
*/
|
||||
stickyBottom: PropTypes.bool,
|
||||
|
||||
/* startAtBottom: if set to true, the view is assumed to start
|
||||
* scrolled to the bottom.
|
||||
* XXX: It's likely this is unnecessary and can be derived from
|
||||
* stickyBottom, but I'm adding an extra parameter to ensure
|
||||
* behaviour stays the same for other uses of ScrollPanel.
|
||||
* If so, let's remove this parameter down the line.
|
||||
*/
|
||||
startAtBottom: PropTypes.bool,
|
||||
|
||||
/* onFillRequest(backwards): a callback which is called on scroll when
|
||||
* the user nears the start (backwards = true) or end (backwards =
|
||||
* false) of the list.
|
||||
*
|
||||
* This should return a promise; no more calls will be made until the
|
||||
* promise completes.
|
||||
*
|
||||
* The promise should resolve to true if there is more data to be
|
||||
* retrieved in this direction (in which case onFillRequest may be
|
||||
* called again immediately), or false if there is no more data in this
|
||||
* directon (at this time) - which will stop the pagination cycle until
|
||||
* the user scrolls again.
|
||||
*/
|
||||
onFillRequest: PropTypes.func,
|
||||
|
||||
/* onUnfillRequest(backwards): a callback which is called on scroll when
|
||||
* there are children elements that are far out of view and could be removed
|
||||
* without causing pagination to occur.
|
||||
*
|
||||
* This function should accept a boolean, which is true to indicate the back/top
|
||||
* of the panel and false otherwise, and a scroll token, which refers to the
|
||||
* first element to remove if removing from the front/bottom, and last element
|
||||
* to remove if removing from the back/top.
|
||||
*/
|
||||
onUnfillRequest: PropTypes.func,
|
||||
|
||||
/* onScroll: a callback which is called whenever any scroll happens.
|
||||
*/
|
||||
onScroll: PropTypes.func,
|
||||
|
||||
/* onUserScroll: callback which is called when the user interacts with the room timeline
|
||||
*/
|
||||
onUserScroll: PropTypes.func,
|
||||
|
||||
/* className: classnames to add to the top-level div
|
||||
*/
|
||||
className: PropTypes.string,
|
||||
|
||||
/* style: styles to add to the top-level div
|
||||
*/
|
||||
style: PropTypes.object,
|
||||
|
||||
/* resizeNotifier: ResizeNotifier to know when middle column has changed size
|
||||
*/
|
||||
resizeNotifier: PropTypes.object,
|
||||
|
||||
/* fixedChildren: allows for children to be passed which are rendered outside
|
||||
* of the wrapper
|
||||
*/
|
||||
fixedChildren: PropTypes.node,
|
||||
};
|
||||
|
||||
export default class ScrollPanel extends React.Component<IProps> {
|
||||
static defaultProps = {
|
||||
stickyBottom: true,
|
||||
startAtBottom: true,
|
||||
onFillRequest: function(backwards) { return Promise.resolve(false); },
|
||||
onUnfillRequest: function(backwards, scrollToken) {},
|
||||
onFillRequest: function(backwards: boolean) { return Promise.resolve(false); },
|
||||
onUnfillRequest: function(backwards: boolean, scrollToken: string) {},
|
||||
onScroll: function() {},
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
private readonly pendingFillRequests: Record<"b" | "f", boolean> = {
|
||||
b: null,
|
||||
f: null,
|
||||
};
|
||||
private readonly itemlist = createRef<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) {
|
||||
this.props.resizeNotifier.on("middlePanelResizedNoisy", this.onResize);
|
||||
}
|
||||
|
||||
this.resetScrollState();
|
||||
|
||||
this._itemlist = createRef();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
|
@ -203,18 +230,18 @@ export default class ScrollPanel extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
onScroll = ev => {
|
||||
private onScroll = ev => {
|
||||
// skip scroll events caused by resizing
|
||||
if (this.props.resizeNotifier && this.props.resizeNotifier.isResizing) return;
|
||||
debuglog("onScroll", this._getScrollNode().scrollTop);
|
||||
this._scrollTimeout.restart();
|
||||
this._saveScrollState();
|
||||
debuglog("onScroll", this.getScrollNode().scrollTop);
|
||||
this.scrollTimeout.restart();
|
||||
this.saveScrollState();
|
||||
this.updatePreventShrinking();
|
||||
this.props.onScroll(ev);
|
||||
this.checkFillState();
|
||||
};
|
||||
|
||||
onResize = () => {
|
||||
private onResize = () => {
|
||||
debuglog("onResize");
|
||||
this.checkScroll();
|
||||
// update preventShrinkingState if present
|
||||
|
@ -225,11 +252,11 @@ export default class ScrollPanel extends React.Component {
|
|||
|
||||
// after an update to the contents of the panel, check that the scroll is
|
||||
// where it ought to be, and set off pagination requests if necessary.
|
||||
checkScroll = () => {
|
||||
public checkScroll = () => {
|
||||
if (this.unmounted) {
|
||||
return;
|
||||
}
|
||||
this._restoreSavedScrollState();
|
||||
this.restoreSavedScrollState();
|
||||
this.checkFillState();
|
||||
};
|
||||
|
||||
|
@ -238,8 +265,8 @@ export default class ScrollPanel extends React.Component {
|
|||
// note that this is independent of the 'stuckAtBottom' state - it is simply
|
||||
// about whether the content is scrolled down right now, irrespective of
|
||||
// whether it will stay that way when the children update.
|
||||
isAtBottom = () => {
|
||||
const sn = this._getScrollNode();
|
||||
public isAtBottom = () => {
|
||||
const sn = this.getScrollNode();
|
||||
// fractional values (both too big and too small)
|
||||
// for scrollTop happen on certain browsers/platforms
|
||||
// when scrolled all the way down. E.g. Chrome 72 on debian.
|
||||
|
@ -278,10 +305,10 @@ export default class ScrollPanel extends React.Component {
|
|||
// |#########| - |
|
||||
// |#########| |
|
||||
// `---------' -
|
||||
_getExcessHeight(backwards) {
|
||||
const sn = this._getScrollNode();
|
||||
const contentHeight = this._getMessagesHeight();
|
||||
const listHeight = this._getListHeight();
|
||||
private getExcessHeight(backwards: boolean): number {
|
||||
const sn = this.getScrollNode();
|
||||
const contentHeight = this.getMessagesHeight();
|
||||
const listHeight = this.getListHeight();
|
||||
const clippedHeight = contentHeight - listHeight;
|
||||
const unclippedScrollTop = sn.scrollTop + clippedHeight;
|
||||
|
||||
|
@ -293,13 +320,13 @@ export default class ScrollPanel extends React.Component {
|
|||
}
|
||||
|
||||
// check the scroll state and send out backfill requests if necessary.
|
||||
checkFillState = async (depth=0) => {
|
||||
public checkFillState = async (depth = 0): Promise<void> => {
|
||||
if (this.unmounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isFirstCall = depth === 0;
|
||||
const sn = this._getScrollNode();
|
||||
const sn = this.getScrollNode();
|
||||
|
||||
// if there is less than a screenful of messages above or below the
|
||||
// viewport, try to get some more messages.
|
||||
|
@ -330,17 +357,17 @@ export default class ScrollPanel extends React.Component {
|
|||
// do make a note when a new request comes in while already running one,
|
||||
// so we can trigger a new chain of calls once done.
|
||||
if (isFirstCall) {
|
||||
if (this._isFilling) {
|
||||
debuglog("_isFilling: not entering while request is ongoing, marking for a subsequent request");
|
||||
this._fillRequestWhileRunning = true;
|
||||
if (this.isFilling) {
|
||||
debuglog("isFilling: not entering while request is ongoing, marking for a subsequent request");
|
||||
this.fillRequestWhileRunning = true;
|
||||
return;
|
||||
}
|
||||
debuglog("_isFilling: setting");
|
||||
this._isFilling = true;
|
||||
debuglog("isFilling: setting");
|
||||
this.isFilling = true;
|
||||
}
|
||||
|
||||
const itemlist = this._itemlist.current;
|
||||
const firstTile = itemlist && itemlist.firstElementChild;
|
||||
const itemlist = this.itemlist.current;
|
||||
const firstTile = itemlist && itemlist.firstElementChild as HTMLElement;
|
||||
const contentTop = firstTile && firstTile.offsetTop;
|
||||
const fillPromises = [];
|
||||
|
||||
|
@ -348,13 +375,13 @@ export default class ScrollPanel extends React.Component {
|
|||
// try backward filling
|
||||
if (!firstTile || (sn.scrollTop - contentTop) < sn.clientHeight) {
|
||||
// need to back-fill
|
||||
fillPromises.push(this._maybeFill(depth, true));
|
||||
fillPromises.push(this.maybeFill(depth, true));
|
||||
}
|
||||
// if scrollTop gets to 2 screens from the end (so 1 screen below viewport),
|
||||
// try forward filling
|
||||
if ((sn.scrollHeight - sn.scrollTop) < sn.clientHeight * 2) {
|
||||
// need to forward-fill
|
||||
fillPromises.push(this._maybeFill(depth, false));
|
||||
fillPromises.push(this.maybeFill(depth, false));
|
||||
}
|
||||
|
||||
if (fillPromises.length) {
|
||||
|
@ -365,26 +392,26 @@ export default class ScrollPanel extends React.Component {
|
|||
}
|
||||
}
|
||||
if (isFirstCall) {
|
||||
debuglog("_isFilling: clearing");
|
||||
this._isFilling = false;
|
||||
debuglog("isFilling: clearing");
|
||||
this.isFilling = false;
|
||||
}
|
||||
|
||||
if (this._fillRequestWhileRunning) {
|
||||
this._fillRequestWhileRunning = false;
|
||||
if (this.fillRequestWhileRunning) {
|
||||
this.fillRequestWhileRunning = false;
|
||||
this.checkFillState();
|
||||
}
|
||||
};
|
||||
|
||||
// check if unfilling is possible and send an unfill request if necessary
|
||||
_checkUnfillState(backwards) {
|
||||
let excessHeight = this._getExcessHeight(backwards);
|
||||
private checkUnfillState(backwards: boolean): void {
|
||||
let excessHeight = this.getExcessHeight(backwards);
|
||||
if (excessHeight <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const origExcessHeight = excessHeight;
|
||||
|
||||
const tiles = this._itemlist.current.children;
|
||||
const tiles = this.itemlist.current.children;
|
||||
|
||||
// The scroll token of the first/last tile to be unpaginated
|
||||
let markerScrollToken = null;
|
||||
|
@ -413,11 +440,11 @@ export default class ScrollPanel extends React.Component {
|
|||
if (markerScrollToken) {
|
||||
// Use a debouncer to prevent multiple unfill calls in quick succession
|
||||
// This is to make the unfilling process less aggressive
|
||||
if (this._unfillDebouncer) {
|
||||
clearTimeout(this._unfillDebouncer);
|
||||
if (this.unfillDebouncer) {
|
||||
clearTimeout(this.unfillDebouncer);
|
||||
}
|
||||
this._unfillDebouncer = setTimeout(() => {
|
||||
this._unfillDebouncer = null;
|
||||
this.unfillDebouncer = setTimeout(() => {
|
||||
this.unfillDebouncer = null;
|
||||
debuglog("unfilling now", backwards, origExcessHeight);
|
||||
this.props.onUnfillRequest(backwards, markerScrollToken);
|
||||
}, UNFILL_REQUEST_DEBOUNCE_MS);
|
||||
|
@ -425,9 +452,9 @@ export default class ScrollPanel extends React.Component {
|
|||
}
|
||||
|
||||
// check if there is already a pending fill request. If not, set one off.
|
||||
_maybeFill(depth, backwards) {
|
||||
private maybeFill(depth: number, backwards: boolean): Promise<void> {
|
||||
const dir = backwards ? 'b' : 'f';
|
||||
if (this._pendingFillRequests[dir]) {
|
||||
if (this.pendingFillRequests[dir]) {
|
||||
debuglog("Already a "+dir+" fill in progress - not starting another");
|
||||
return;
|
||||
}
|
||||
|
@ -436,7 +463,7 @@ export default class ScrollPanel extends React.Component {
|
|||
|
||||
// onFillRequest can end up calling us recursively (via onScroll
|
||||
// events) so make sure we set this before firing off the call.
|
||||
this._pendingFillRequests[dir] = true;
|
||||
this.pendingFillRequests[dir] = true;
|
||||
|
||||
// wait 1ms before paginating, because otherwise
|
||||
// this will block the scroll event handler for +700ms
|
||||
|
@ -445,13 +472,13 @@ export default class ScrollPanel extends React.Component {
|
|||
return new Promise(resolve => setTimeout(resolve, 1)).then(() => {
|
||||
return this.props.onFillRequest(backwards);
|
||||
}).finally(() => {
|
||||
this._pendingFillRequests[dir] = false;
|
||||
this.pendingFillRequests[dir] = false;
|
||||
}).then((hasMoreResults) => {
|
||||
if (this.unmounted) {
|
||||
return;
|
||||
}
|
||||
// Unpaginate once filling is complete
|
||||
this._checkUnfillState(!backwards);
|
||||
this.checkUnfillState(!backwards);
|
||||
|
||||
debuglog(""+dir+" fill complete; hasMoreResults:"+hasMoreResults);
|
||||
if (hasMoreResults) {
|
||||
|
@ -477,7 +504,7 @@ export default class ScrollPanel extends React.Component {
|
|||
* the number of pixels the bottom of the tracked child is above the
|
||||
* bottom of the scroll panel.
|
||||
*/
|
||||
getScrollState = () => this.scrollState;
|
||||
public getScrollState = (): IScrollState => this.scrollState;
|
||||
|
||||
/* reset the saved scroll state.
|
||||
*
|
||||
|
@ -491,35 +518,35 @@ export default class ScrollPanel extends React.Component {
|
|||
* no use if no children exist yet, or if you are about to replace the
|
||||
* child list.)
|
||||
*/
|
||||
resetScrollState = () => {
|
||||
public resetScrollState = (): void => {
|
||||
this.scrollState = {
|
||||
stuckAtBottom: this.props.startAtBottom,
|
||||
};
|
||||
this._bottomGrowth = 0;
|
||||
this._pages = 0;
|
||||
this._scrollTimeout = new Timer(100);
|
||||
this._heightUpdateInProgress = false;
|
||||
this.bottomGrowth = 0;
|
||||
this.pages = 0;
|
||||
this.scrollTimeout = new Timer(100);
|
||||
this.heightUpdateInProgress = false;
|
||||
};
|
||||
|
||||
/**
|
||||
* jump to the top of the content.
|
||||
*/
|
||||
scrollToTop = () => {
|
||||
this._getScrollNode().scrollTop = 0;
|
||||
this._saveScrollState();
|
||||
public scrollToTop = (): void => {
|
||||
this.getScrollNode().scrollTop = 0;
|
||||
this.saveScrollState();
|
||||
};
|
||||
|
||||
/**
|
||||
* jump to the bottom of the content.
|
||||
*/
|
||||
scrollToBottom = () => {
|
||||
public scrollToBottom = (): void => {
|
||||
// the easiest way to make sure that the scroll state is correctly
|
||||
// saved is to do the scroll, then save the updated state. (Calculating
|
||||
// it ourselves is hard, and we can't rely on an onScroll callback
|
||||
// happening, since there may be no user-visible change here).
|
||||
const sn = this._getScrollNode();
|
||||
const sn = this.getScrollNode();
|
||||
sn.scrollTop = sn.scrollHeight;
|
||||
this._saveScrollState();
|
||||
this.saveScrollState();
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -527,18 +554,18 @@ export default class ScrollPanel extends React.Component {
|
|||
*
|
||||
* @param {number} mult: -1 to page up, +1 to page down
|
||||
*/
|
||||
scrollRelative = mult => {
|
||||
const scrollNode = this._getScrollNode();
|
||||
public scrollRelative = (mult: number): void => {
|
||||
const scrollNode = this.getScrollNode();
|
||||
const delta = mult * scrollNode.clientHeight * 0.9;
|
||||
scrollNode.scrollBy(0, delta);
|
||||
this._saveScrollState();
|
||||
this.saveScrollState();
|
||||
};
|
||||
|
||||
/**
|
||||
* Scroll up/down in response to a scroll key
|
||||
* @param {object} ev the keyboard event
|
||||
*/
|
||||
handleScrollKey = ev => {
|
||||
public handleScrollKey = (ev: KeyboardEvent) => {
|
||||
let isScrolling = false;
|
||||
const roomAction = getKeyBindingsManager().getRoomAction(ev);
|
||||
switch (roomAction) {
|
||||
|
@ -575,17 +602,17 @@ export default class ScrollPanel extends React.Component {
|
|||
* node (specifically, the bottom of it) will be positioned. If omitted, it
|
||||
* defaults to 0.
|
||||
*/
|
||||
scrollToToken = (scrollToken, pixelOffset, offsetBase) => {
|
||||
public scrollToToken = (scrollToken: string, pixelOffset: number, offsetBase: number): void => {
|
||||
pixelOffset = pixelOffset || 0;
|
||||
offsetBase = offsetBase || 0;
|
||||
|
||||
// set the trackedScrollToken so we can get the node through _getTrackedNode
|
||||
// set the trackedScrollToken so we can get the node through getTrackedNode
|
||||
this.scrollState = {
|
||||
stuckAtBottom: false,
|
||||
trackedScrollToken: scrollToken,
|
||||
};
|
||||
const trackedNode = this._getTrackedNode();
|
||||
const scrollNode = this._getScrollNode();
|
||||
const trackedNode = this.getTrackedNode();
|
||||
const scrollNode = this.getScrollNode();
|
||||
if (trackedNode) {
|
||||
// set the scrollTop to the position we want.
|
||||
// note though, that this might not succeed if the combination of offsetBase and pixelOffset
|
||||
|
@ -595,34 +622,34 @@ export default class ScrollPanel extends React.Component {
|
|||
// enough so it ends up in the top of the viewport.
|
||||
debuglog("scrollToken: setting scrollTop", {offsetBase, pixelOffset, offsetTop: trackedNode.offsetTop});
|
||||
scrollNode.scrollTop = (trackedNode.offsetTop - (scrollNode.clientHeight * offsetBase)) + pixelOffset;
|
||||
this._saveScrollState();
|
||||
this.saveScrollState();
|
||||
}
|
||||
};
|
||||
|
||||
_saveScrollState() {
|
||||
private saveScrollState(): void {
|
||||
if (this.props.stickyBottom && this.isAtBottom()) {
|
||||
this.scrollState = { stuckAtBottom: true };
|
||||
debuglog("saved stuckAtBottom state");
|
||||
return;
|
||||
}
|
||||
|
||||
const scrollNode = this._getScrollNode();
|
||||
const scrollNode = this.getScrollNode();
|
||||
const viewportBottom = scrollNode.scrollHeight - (scrollNode.scrollTop + scrollNode.clientHeight);
|
||||
|
||||
const itemlist = this._itemlist.current;
|
||||
const itemlist = this.itemlist.current;
|
||||
const messages = itemlist.children;
|
||||
let node = null;
|
||||
|
||||
// TODO: do a binary search here, as items are sorted by offsetTop
|
||||
// loop backwards, from bottom-most message (as that is the most common case)
|
||||
for (let i = messages.length-1; i >= 0; --i) {
|
||||
if (!messages[i].dataset.scrollTokens) {
|
||||
for (let i = messages.length - 1; i >= 0; --i) {
|
||||
if (!(messages[i] as HTMLElement).dataset.scrollTokens) {
|
||||
continue;
|
||||
}
|
||||
node = messages[i];
|
||||
// break at the first message (coming from the bottom)
|
||||
// that has it's offsetTop above the bottom of the viewport.
|
||||
if (this._topFromBottom(node) > viewportBottom) {
|
||||
if (this.topFromBottom(node) > viewportBottom) {
|
||||
// Use this node as the scrollToken
|
||||
break;
|
||||
}
|
||||
|
@ -634,7 +661,7 @@ export default class ScrollPanel extends React.Component {
|
|||
}
|
||||
const scrollToken = node.dataset.scrollTokens.split(',')[0];
|
||||
debuglog("saving anchored scroll state to message", node && node.innerText, scrollToken);
|
||||
const bottomOffset = this._topFromBottom(node);
|
||||
const bottomOffset = this.topFromBottom(node);
|
||||
this.scrollState = {
|
||||
stuckAtBottom: false,
|
||||
trackedNode: node,
|
||||
|
@ -644,35 +671,35 @@ export default class ScrollPanel extends React.Component {
|
|||
};
|
||||
}
|
||||
|
||||
async _restoreSavedScrollState() {
|
||||
private async restoreSavedScrollState(): Promise<void> {
|
||||
const scrollState = this.scrollState;
|
||||
|
||||
if (scrollState.stuckAtBottom) {
|
||||
const sn = this._getScrollNode();
|
||||
const sn = this.getScrollNode();
|
||||
if (sn.scrollTop !== sn.scrollHeight) {
|
||||
sn.scrollTop = sn.scrollHeight;
|
||||
}
|
||||
} else if (scrollState.trackedScrollToken) {
|
||||
const itemlist = this._itemlist.current;
|
||||
const trackedNode = this._getTrackedNode();
|
||||
const itemlist = this.itemlist.current;
|
||||
const trackedNode = this.getTrackedNode();
|
||||
if (trackedNode) {
|
||||
const newBottomOffset = this._topFromBottom(trackedNode);
|
||||
const newBottomOffset = this.topFromBottom(trackedNode);
|
||||
const bottomDiff = newBottomOffset - scrollState.bottomOffset;
|
||||
this._bottomGrowth += bottomDiff;
|
||||
this.bottomGrowth += bottomDiff;
|
||||
scrollState.bottomOffset = newBottomOffset;
|
||||
const newHeight = `${this._getListHeight()}px`;
|
||||
const newHeight = `${this.getListHeight()}px`;
|
||||
if (itemlist.style.height !== newHeight) {
|
||||
itemlist.style.height = newHeight;
|
||||
}
|
||||
debuglog("balancing height because messages below viewport grew by", bottomDiff);
|
||||
}
|
||||
}
|
||||
if (!this._heightUpdateInProgress) {
|
||||
this._heightUpdateInProgress = true;
|
||||
if (!this.heightUpdateInProgress) {
|
||||
this.heightUpdateInProgress = true;
|
||||
try {
|
||||
await this._updateHeight();
|
||||
await this.updateHeight();
|
||||
} finally {
|
||||
this._heightUpdateInProgress = false;
|
||||
this.heightUpdateInProgress = false;
|
||||
}
|
||||
} else {
|
||||
debuglog("not updating height because request already in progress");
|
||||
|
@ -680,11 +707,11 @@ export default class ScrollPanel extends React.Component {
|
|||
}
|
||||
|
||||
// need a better name that also indicates this will change scrollTop? Rebalance height? Reveal content?
|
||||
async _updateHeight() {
|
||||
private async updateHeight(): Promise<void> {
|
||||
// wait until user has stopped scrolling
|
||||
if (this._scrollTimeout.isRunning()) {
|
||||
if (this.scrollTimeout.isRunning()) {
|
||||
debuglog("updateHeight waiting for scrolling to end ... ");
|
||||
await this._scrollTimeout.finished();
|
||||
await this.scrollTimeout.finished();
|
||||
} else {
|
||||
debuglog("updateHeight getting straight to business, no scrolling going on.");
|
||||
}
|
||||
|
@ -694,14 +721,14 @@ export default class ScrollPanel extends React.Component {
|
|||
return;
|
||||
}
|
||||
|
||||
const sn = this._getScrollNode();
|
||||
const itemlist = this._itemlist.current;
|
||||
const contentHeight = this._getMessagesHeight();
|
||||
const sn = this.getScrollNode();
|
||||
const itemlist = this.itemlist.current;
|
||||
const contentHeight = this.getMessagesHeight();
|
||||
const minHeight = sn.clientHeight;
|
||||
const height = Math.max(minHeight, contentHeight);
|
||||
this._pages = Math.ceil(height / PAGE_SIZE);
|
||||
this._bottomGrowth = 0;
|
||||
const newHeight = `${this._getListHeight()}px`;
|
||||
this.pages = Math.ceil(height / PAGE_SIZE);
|
||||
this.bottomGrowth = 0;
|
||||
const newHeight = `${this.getListHeight()}px`;
|
||||
|
||||
const scrollState = this.scrollState;
|
||||
if (scrollState.stuckAtBottom) {
|
||||
|
@ -713,7 +740,7 @@ export default class ScrollPanel extends React.Component {
|
|||
}
|
||||
debuglog("updateHeight to", newHeight);
|
||||
} else if (scrollState.trackedScrollToken) {
|
||||
const trackedNode = this._getTrackedNode();
|
||||
const trackedNode = this.getTrackedNode();
|
||||
// if the timeline has been reloaded
|
||||
// this can be called before scrollToBottom or whatever has been called
|
||||
// so don't do anything if the node has disappeared from
|
||||
|
@ -735,17 +762,17 @@ export default class ScrollPanel extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
_getTrackedNode() {
|
||||
private getTrackedNode(): HTMLElement {
|
||||
const scrollState = this.scrollState;
|
||||
const trackedNode = scrollState.trackedNode;
|
||||
|
||||
if (!trackedNode || !trackedNode.parentElement) {
|
||||
let node;
|
||||
const messages = this._itemlist.current.children;
|
||||
const messages = this.itemlist.current.children;
|
||||
const scrollToken = scrollState.trackedScrollToken;
|
||||
|
||||
for (let i = messages.length-1; i >= 0; --i) {
|
||||
const m = messages[i];
|
||||
const m = messages[i] as HTMLElement;
|
||||
// 'data-scroll-tokens' is a DOMString of comma-separated scroll tokens
|
||||
// There might only be one scroll token
|
||||
if (m.dataset.scrollTokens &&
|
||||
|
@ -768,45 +795,45 @@ export default class ScrollPanel extends React.Component {
|
|||
return scrollState.trackedNode;
|
||||
}
|
||||
|
||||
_getListHeight() {
|
||||
return this._bottomGrowth + (this._pages * PAGE_SIZE);
|
||||
private getListHeight(): number {
|
||||
return this.bottomGrowth + (this.pages * PAGE_SIZE);
|
||||
}
|
||||
|
||||
_getMessagesHeight() {
|
||||
const itemlist = this._itemlist.current;
|
||||
const lastNode = itemlist.lastElementChild;
|
||||
private getMessagesHeight(): number {
|
||||
const itemlist = this.itemlist.current;
|
||||
const lastNode = itemlist.lastElementChild as HTMLElement;
|
||||
const lastNodeBottom = lastNode ? lastNode.offsetTop + lastNode.clientHeight : 0;
|
||||
const firstNodeTop = itemlist.firstElementChild ? itemlist.firstElementChild.offsetTop : 0;
|
||||
const firstNodeTop = itemlist.firstElementChild ? (itemlist.firstElementChild as HTMLElement).offsetTop : 0;
|
||||
// 18 is itemlist padding
|
||||
return lastNodeBottom - firstNodeTop + (18 * 2);
|
||||
}
|
||||
|
||||
_topFromBottom(node) {
|
||||
private topFromBottom(node: HTMLElement): number {
|
||||
// current capped height - distance from top = distance from bottom of container to top of tracked element
|
||||
return this._itemlist.current.clientHeight - node.offsetTop;
|
||||
return this.itemlist.current.clientHeight - node.offsetTop;
|
||||
}
|
||||
|
||||
/* get the DOM node which has the scrollTop property we care about for our
|
||||
* message panel.
|
||||
*/
|
||||
_getScrollNode() {
|
||||
private getScrollNode(): HTMLDivElement {
|
||||
if (this.unmounted) {
|
||||
// this shouldn't happen, but when it does, turn the NPE into
|
||||
// something more meaningful.
|
||||
throw new Error("ScrollPanel._getScrollNode called when unmounted");
|
||||
throw new Error("ScrollPanel.getScrollNode called when unmounted");
|
||||
}
|
||||
|
||||
if (!this._divScroll) {
|
||||
if (!this.divScroll) {
|
||||
// Likewise, we should have the ref by this point, but if not
|
||||
// turn the NPE into something meaningful.
|
||||
throw new Error("ScrollPanel._getScrollNode called before AutoHideScrollbar ref collected");
|
||||
throw new Error("ScrollPanel.getScrollNode called before AutoHideScrollbar ref collected");
|
||||
}
|
||||
|
||||
return this._divScroll;
|
||||
return this.divScroll;
|
||||
}
|
||||
|
||||
_collectScroll = divScroll => {
|
||||
this._divScroll = divScroll;
|
||||
private collectScroll = (divScroll: HTMLDivElement) => {
|
||||
this.divScroll = divScroll;
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -814,15 +841,15 @@ export default class ScrollPanel extends React.Component {
|
|||
anything below it changes, by calling updatePreventShrinking, to keep
|
||||
the same minimum bottom offset, effectively preventing the timeline to shrink.
|
||||
*/
|
||||
preventShrinking = () => {
|
||||
const messageList = this._itemlist.current;
|
||||
public preventShrinking = (): void => {
|
||||
const messageList = this.itemlist.current;
|
||||
const tiles = messageList && messageList.children;
|
||||
if (!messageList) {
|
||||
return;
|
||||
}
|
||||
let lastTileNode;
|
||||
for (let i = tiles.length - 1; i >= 0; i--) {
|
||||
const node = tiles[i];
|
||||
const node = tiles[i] as HTMLElement;
|
||||
if (node.dataset.scrollTokens) {
|
||||
lastTileNode = node;
|
||||
break;
|
||||
|
@ -841,8 +868,8 @@ export default class ScrollPanel extends React.Component {
|
|||
};
|
||||
|
||||
/** Clear shrinking prevention. Used internally, and when the timeline is reloaded. */
|
||||
clearPreventShrinking = () => {
|
||||
const messageList = this._itemlist.current;
|
||||
public clearPreventShrinking = (): void => {
|
||||
const messageList = this.itemlist.current;
|
||||
const balanceElement = messageList && messageList.parentElement;
|
||||
if (balanceElement) balanceElement.style.paddingBottom = null;
|
||||
this.preventShrinkingState = null;
|
||||
|
@ -857,11 +884,11 @@ export default class ScrollPanel extends React.Component {
|
|||
from the bottom of the marked tile grows larger than
|
||||
what it was when marking.
|
||||
*/
|
||||
updatePreventShrinking = () => {
|
||||
public updatePreventShrinking = (): void => {
|
||||
if (this.preventShrinkingState) {
|
||||
const sn = this._getScrollNode();
|
||||
const sn = this.getScrollNode();
|
||||
const scrollState = this.scrollState;
|
||||
const messageList = this._itemlist.current;
|
||||
const messageList = this.itemlist.current;
|
||||
const {offsetNode, offsetFromBottom} = this.preventShrinkingState;
|
||||
// element used to set paddingBottom to balance the typing notifs disappearing
|
||||
const balanceElement = messageList.parentElement;
|
||||
|
@ -898,13 +925,15 @@ export default class ScrollPanel extends React.Component {
|
|||
// list-style-type: none; is no longer a list
|
||||
return (
|
||||
<AutoHideScrollbar
|
||||
wrappedRef={this._collectScroll}
|
||||
wrappedRef={this.collectScroll}
|
||||
onScroll={this.onScroll}
|
||||
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 }
|
||||
<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 }
|
||||
</ol>
|
||||
</div>
|
|
@ -14,34 +14,34 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, {ReactNode, useMemo, useState} from "react";
|
||||
import {Room} from "matrix-js-sdk/src/models/room";
|
||||
import {MatrixClient} from "matrix-js-sdk/src/client";
|
||||
import {EventType, RoomType} from "matrix-js-sdk/src/@types/event";
|
||||
import React, { ReactNode, useMemo, useState } from "react";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||
import { EventType, RoomType } from "matrix-js-sdk/src/@types/event";
|
||||
import classNames from "classnames";
|
||||
import {sortBy} from "lodash";
|
||||
import { sortBy } from "lodash";
|
||||
|
||||
import {MatrixClientPeg} from "../../MatrixClientPeg";
|
||||
import { MatrixClientPeg } from "../../MatrixClientPeg";
|
||||
import dis from "../../dispatcher/dispatcher";
|
||||
import {_t} from "../../languageHandler";
|
||||
import AccessibleButton, {ButtonEvent} from "../views/elements/AccessibleButton";
|
||||
import { _t } from "../../languageHandler";
|
||||
import AccessibleButton, { ButtonEvent } from "../views/elements/AccessibleButton";
|
||||
import BaseDialog from "../views/dialogs/BaseDialog";
|
||||
import Spinner from "../views/elements/Spinner";
|
||||
import SearchBox from "./SearchBox";
|
||||
import RoomAvatar from "../views/avatars/RoomAvatar";
|
||||
import RoomName from "../views/elements/RoomName";
|
||||
import {useAsyncMemo} from "../../hooks/useAsyncMemo";
|
||||
import {EnhancedMap} from "../../utils/maps";
|
||||
import { useAsyncMemo } from "../../hooks/useAsyncMemo";
|
||||
import { EnhancedMap } from "../../utils/maps";
|
||||
import StyledCheckbox from "../views/elements/StyledCheckbox";
|
||||
import AutoHideScrollbar from "./AutoHideScrollbar";
|
||||
import BaseAvatar from "../views/avatars/BaseAvatar";
|
||||
import {mediaFromMxc} from "../../customisations/Media";
|
||||
import { mediaFromMxc } from "../../customisations/Media";
|
||||
import InfoTooltip from "../views/elements/InfoTooltip";
|
||||
import TextWithTooltip from "../views/elements/TextWithTooltip";
|
||||
import {useStateToggle} from "../../hooks/useStateToggle";
|
||||
import {getOrder} from "../../stores/SpaceStore";
|
||||
import { useStateToggle } from "../../hooks/useStateToggle";
|
||||
import { getChildOrder } from "../../stores/SpaceStore";
|
||||
import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton";
|
||||
import {linkifyElement} from "../../HtmlUtils";
|
||||
import { linkifyElement } from "../../HtmlUtils";
|
||||
|
||||
interface IHierarchyProps {
|
||||
space: Room;
|
||||
|
@ -286,7 +286,7 @@ export const HierarchyLevel = ({
|
|||
const children = Array.from(relations.get(spaceId)?.values() || []);
|
||||
const sortedChildren = sortBy(children, ev => {
|
||||
// XXX: Space Summary API doesn't give the child origin_server_ts but once it does we should use it for sorting
|
||||
return getOrder(ev.content.order, null, ev.state_key);
|
||||
return getChildOrder(ev.content.order, null, ev.state_key);
|
||||
});
|
||||
const [subspaces, childRooms] = sortedChildren.reduce((result, ev: ISpaceSummaryEvent) => {
|
||||
const roomId = ev.state_key;
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -55,7 +55,7 @@ export default class ViewSource extends React.Component {
|
|||
viewSourceContent() {
|
||||
const mxEvent = this.props.mxEvent.replacingEvent() || this.props.mxEvent; // show the replacing event, not the original, if it is an edit
|
||||
const isEncrypted = mxEvent.isEncrypted();
|
||||
const decryptedEventSource = mxEvent._clearEvent; // FIXME: _clearEvent is private
|
||||
const decryptedEventSource = mxEvent.clearEvent; // FIXME: clearEvent is private
|
||||
const originalEventSource = mxEvent.event;
|
||||
|
||||
if (isEncrypted) {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue