Merge pull request #4398 from matrix-org/t3chguy/liv

Convert LoggedInView to an ES6 PureComponent Class & TypeScript
This commit is contained in:
Michael Telatynski 2020-04-14 15:08:04 +01:00 committed by GitHub
commit fc67d1d775
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23

View file

@ -1,7 +1,7 @@
/* /*
Copyright 2015, 2016 OpenMarket Ltd Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd Copyright 2017 Vector Creations Ltd
Copyright 2017, 2018 New Vector Ltd Copyright 2017, 2018, 2020 New Vector Ltd
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,10 +16,10 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { MatrixClient } from 'matrix-js-sdk'; import * as React from 'react';
import React, {createRef} from 'react'; import * as PropTypes from 'prop-types';
import createReactClass from 'create-react-class'; import { MatrixClient } from 'matrix-js-sdk/src/client';
import PropTypes from 'prop-types'; import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
import { DragDropContext } from 'react-beautiful-dnd'; import { DragDropContext } from 'react-beautiful-dnd';
import {Key, isOnlyCtrlOrCmdKeyEvent, isOnlyCtrlOrCmdIgnoreShiftKeyEvent} from '../../Keyboard'; import {Key, isOnlyCtrlOrCmdKeyEvent, isOnlyCtrlOrCmdIgnoreShiftKeyEvent} from '../../Keyboard';
@ -29,7 +29,7 @@ import { fixupColorFonts } from '../../utils/FontManager';
import * as sdk from '../../index'; import * as sdk from '../../index';
import dis from '../../dispatcher'; import dis from '../../dispatcher';
import sessionStore from '../../stores/SessionStore'; import sessionStore from '../../stores/SessionStore';
import {MatrixClientPeg} from '../../MatrixClientPeg'; import {MatrixClientPeg, MatrixClientCreds} from '../../MatrixClientPeg';
import SettingsStore from "../../settings/SettingsStore"; import SettingsStore from "../../settings/SettingsStore";
import RoomListStore from "../../stores/RoomListStore"; import RoomListStore from "../../stores/RoomListStore";
@ -40,6 +40,7 @@ import {Resizer, CollapseDistributor} from '../../resizer';
import MatrixClientContext from "../../contexts/MatrixClientContext"; import MatrixClientContext from "../../contexts/MatrixClientContext";
import * as KeyboardShortcuts from "../../accessibility/KeyboardShortcuts"; import * as KeyboardShortcuts from "../../accessibility/KeyboardShortcuts";
import HomePage from "./HomePage"; import HomePage from "./HomePage";
import ResizeNotifier from "../../utils/ResizeNotifier";
// We need to fetch each pinned message individually (if we don't already have it) // We need to fetch each pinned message individually (if we don't already have it)
// so each pinned message may trigger a request. Limit the number per room for sanity. // so each pinned message may trigger a request. Limit the number per room for sanity.
// NB. this is just for server notices rather than pinned messages in general. // NB. this is just for server notices rather than pinned messages in general.
@ -52,6 +53,52 @@ function canElementReceiveInput(el) {
!!el.getAttribute("contenteditable"); !!el.getAttribute("contenteditable");
} }
interface IProps {
matrixClient: MatrixClient;
onRegistered: (credentials: MatrixClientCreds) => Promise<MatrixClient>;
viaServers?: string[];
hideToSRUsers: boolean;
resizeNotifier: ResizeNotifier;
middleDisabled: boolean;
initialEventPixelOffset: number;
leftDisabled: boolean;
rightDisabled: boolean;
showCookieBar: boolean;
hasNewVersion: boolean;
userHasGeneratedPassword: boolean;
showNotifierToolbar: boolean;
page_type: string;
autoJoin: boolean;
thirdPartyInvite?: object;
roomOobData?: object;
currentRoomId: string;
ConferenceHandler?: object;
collapseLhs: boolean;
checkingForUpdate: boolean;
config: {
piwik: {
policyUrl: string;
},
[key: string]: any,
};
currentUserId?: string;
currentGroupId?: string;
currentGroupIsNew?: boolean;
version?: string;
newVersion?: string;
newVersionReleaseNotes?: string;
}
interface IState {
mouseDown?: {
x: number;
y: number;
};
syncErrorData: any;
useCompactLayout: boolean;
serverNoticeEvents: MatrixEvent[];
userHasGeneratedPassword: boolean;
}
/** /**
* This is what our MatrixChat shows when we are logged in. The precise view is * This is what our MatrixChat shows when we are logged in. The precise view is
* determined by the page_type property. * determined by the page_type property.
@ -61,10 +108,10 @@ function canElementReceiveInput(el) {
* *
* Components mounted below us can access the matrix client via the react context. * Components mounted below us can access the matrix client via the react context.
*/ */
const LoggedInView = createReactClass({ class LoggedInView extends React.PureComponent<IProps, IState> {
displayName: 'LoggedInView', static displayName = 'LoggedInView';
propTypes: { static propTypes = {
matrixClient: PropTypes.instanceOf(MatrixClient).isRequired, matrixClient: PropTypes.instanceOf(MatrixClient).isRequired,
page_type: PropTypes.string.isRequired, page_type: PropTypes.string.isRequired,
onRoomCreated: PropTypes.func, onRoomCreated: PropTypes.func,
@ -77,25 +124,28 @@ const LoggedInView = createReactClass({
viaServers: PropTypes.arrayOf(PropTypes.string), viaServers: PropTypes.arrayOf(PropTypes.string),
// and lots and lots of other stuff. // and lots and lots of other stuff.
}, };
getInitialState: function() { protected readonly _matrixClient: MatrixClient;
return { protected readonly _roomView: React.RefObject<any>;
protected readonly _resizeContainer: React.RefObject<ResizeHandle>;
protected readonly _sessionStore: sessionStore;
protected readonly _sessionStoreToken: { remove: () => void };
protected resizer: Resizer;
constructor(props, context) {
super(props, context);
this.state = {
mouseDown: undefined,
syncErrorData: undefined,
userHasGeneratedPassword: false,
// use compact timeline view // use compact timeline view
useCompactLayout: SettingsStore.getValue('useCompactLayout'), useCompactLayout: SettingsStore.getValue('useCompactLayout'),
// any currently active server notice events // any currently active server notice events
serverNoticeEvents: [], serverNoticeEvents: [],
}; };
},
componentDidMount: function() {
this.resizer = this._createResizer();
this.resizer.attach();
this._loadResizerPreferences();
},
// TODO: [REACT-WARNING] Replace component with real class, use constructor for refs
UNSAFE_componentWillMount: function() {
// stash the MatrixClient in case we log out before we are unmounted // stash the MatrixClient in case we log out before we are unmounted
this._matrixClient = this.props.matrixClient; this._matrixClient = this.props.matrixClient;
@ -117,22 +167,29 @@ const LoggedInView = createReactClass({
fixupColorFonts(); fixupColorFonts();
this._roomView = createRef(); this._roomView = React.createRef();
}, this._resizeContainer = React.createRef();
}
componentDidUpdate(prevProps) { componentDidMount() {
this.resizer = this._createResizer();
this.resizer.attach();
this._loadResizerPreferences();
}
componentDidUpdate(prevProps, prevState) {
// attempt to guess when a banner was opened or closed // attempt to guess when a banner was opened or closed
if ( if (
(prevProps.showCookieBar !== this.props.showCookieBar) || (prevProps.showCookieBar !== this.props.showCookieBar) ||
(prevProps.hasNewVersion !== this.props.hasNewVersion) || (prevProps.hasNewVersion !== this.props.hasNewVersion) ||
(prevProps.userHasGeneratedPassword !== this.props.userHasGeneratedPassword) || (prevState.userHasGeneratedPassword !== this.state.userHasGeneratedPassword) ||
(prevProps.showNotifierToolbar !== this.props.showNotifierToolbar) (prevProps.showNotifierToolbar !== this.props.showNotifierToolbar)
) { ) {
this.props.resizeNotifier.notifyBannersChanged(); this.props.resizeNotifier.notifyBannersChanged();
} }
}, }
componentWillUnmount: function() { componentWillUnmount() {
document.removeEventListener('keydown', this._onNativeKeyDown, false); document.removeEventListener('keydown', this._onNativeKeyDown, false);
this._matrixClient.removeListener("accountData", this.onAccountData); this._matrixClient.removeListener("accountData", this.onAccountData);
this._matrixClient.removeListener("sync", this.onSync); this._matrixClient.removeListener("sync", this.onSync);
@ -141,7 +198,7 @@ const LoggedInView = createReactClass({
this._sessionStoreToken.remove(); this._sessionStoreToken.remove();
} }
this.resizer.detach(); this.resizer.detach();
}, }
// Child components assume that the client peg will not be null, so give them some // Child components assume that the client peg will not be null, so give them some
// sort of assurance here by only allowing a re-render if the client is truthy. // sort of assurance here by only allowing a re-render if the client is truthy.
@ -149,22 +206,22 @@ const LoggedInView = createReactClass({
// This is required because `LoggedInView` maintains its own state and if this state // This is required because `LoggedInView` maintains its own state and if this state
// updates after the client peg has been made null (during logout), then it will // updates after the client peg has been made null (during logout), then it will
// attempt to re-render and the children will throw errors. // attempt to re-render and the children will throw errors.
shouldComponentUpdate: function() { shouldComponentUpdate() {
return Boolean(MatrixClientPeg.get()); return Boolean(MatrixClientPeg.get());
}, }
canResetTimelineInRoom: function(roomId) { canResetTimelineInRoom = (roomId) => {
if (!this._roomView.current) { if (!this._roomView.current) {
return true; return true;
} }
return this._roomView.current.canResetTimeline(); return this._roomView.current.canResetTimeline();
}, };
_setStateFromSessionStore() { _setStateFromSessionStore = () => {
this.setState({ this.setState({
userHasGeneratedPassword: Boolean(this._sessionStore.getCachedPassword()), userHasGeneratedPassword: Boolean(this._sessionStore.getCachedPassword()),
}); });
}, };
_createResizer() { _createResizer() {
const classNames = { const classNames = {
@ -188,24 +245,22 @@ const LoggedInView = createReactClass({
}, },
}; };
const resizer = new Resizer( const resizer = new Resizer(
this.resizeContainer, this._resizeContainer.current,
CollapseDistributor, CollapseDistributor,
collapseConfig); collapseConfig);
resizer.setClassNames(classNames); resizer.setClassNames(classNames);
return resizer; return resizer;
}, }
_loadResizerPreferences() { _loadResizerPreferences() {
let lhsSize = window.localStorage.getItem("mx_lhs_size"); let lhsSize = parseInt(window.localStorage.getItem("mx_lhs_size"), 10);
if (lhsSize !== null) { if (isNaN(lhsSize)) {
lhsSize = parseInt(lhsSize, 10);
} else {
lhsSize = 350; lhsSize = 350;
} }
this.resizer.forHandleAt(0).resize(lhsSize); this.resizer.forHandleAt(0).resize(lhsSize);
}, }
onAccountData: function(event) { onAccountData = (event) => {
if (event.getType() === "im.vector.web.settings") { if (event.getType() === "im.vector.web.settings") {
this.setState({ this.setState({
useCompactLayout: event.getContent().useCompactLayout, useCompactLayout: event.getContent().useCompactLayout,
@ -214,9 +269,9 @@ const LoggedInView = createReactClass({
if (event.getType() === "m.ignored_user_list") { if (event.getType() === "m.ignored_user_list") {
dis.dispatch({action: "ignore_state_changed"}); dis.dispatch({action: "ignore_state_changed"});
} }
}, };
onSync: function(syncState, oldSyncState, data) { onSync = (syncState, oldSyncState, data) => {
const oldErrCode = ( const oldErrCode = (
this.state.syncErrorData && this.state.syncErrorData &&
this.state.syncErrorData.error && this.state.syncErrorData.error &&
@ -238,16 +293,16 @@ const LoggedInView = createReactClass({
if (oldSyncState === 'PREPARED' && syncState === 'SYNCING') { if (oldSyncState === 'PREPARED' && syncState === 'SYNCING') {
this._updateServerNoticeEvents(); this._updateServerNoticeEvents();
} }
}, };
onRoomStateEvents: function(ev, state) { onRoomStateEvents = (ev, state) => {
const roomLists = RoomListStore.getRoomLists(); const roomLists = RoomListStore.getRoomLists();
if (roomLists['m.server_notice'] && roomLists['m.server_notice'].some(r => r.roomId === ev.getRoomId())) { if (roomLists['m.server_notice'] && roomLists['m.server_notice'].some(r => r.roomId === ev.getRoomId())) {
this._updateServerNoticeEvents(); this._updateServerNoticeEvents();
} }
}, };
_updateServerNoticeEvents: async function() { _updateServerNoticeEvents = async () => {
const roomLists = RoomListStore.getRoomLists(); const roomLists = RoomListStore.getRoomLists();
if (!roomLists['m.server_notice']) return []; if (!roomLists['m.server_notice']) return [];
@ -260,16 +315,16 @@ const LoggedInView = createReactClass({
const pinnedEventIds = pinStateEvent.getContent().pinned.slice(0, MAX_PINNED_NOTICES_PER_ROOM); const pinnedEventIds = pinStateEvent.getContent().pinned.slice(0, MAX_PINNED_NOTICES_PER_ROOM);
for (const eventId of pinnedEventIds) { for (const eventId of pinnedEventIds) {
const timeline = await this._matrixClient.getEventTimeline(room.getUnfilteredTimelineSet(), eventId, 0); const timeline = await this._matrixClient.getEventTimeline(room.getUnfilteredTimelineSet(), eventId, 0);
const ev = timeline.getEvents().find(ev => ev.getId() === eventId); const event = timeline.getEvents().find(ev => ev.getId() === eventId);
if (ev) pinnedEvents.push(ev); if (event) pinnedEvents.push(event);
} }
} }
this.setState({ this.setState({
serverNoticeEvents: pinnedEvents, serverNoticeEvents: pinnedEvents,
}); });
}, };
_onPaste: function(ev) { _onPaste = (ev) => {
let canReceiveInput = false; let canReceiveInput = false;
let element = ev.target; let element = ev.target;
// test for all parents because the target can be a child of a contenteditable element // test for all parents because the target can be a child of a contenteditable element
@ -283,7 +338,7 @@ const LoggedInView = createReactClass({
// so dispatch synchronously before paste happens // so dispatch synchronously before paste happens
dis.dispatch({action: 'focus_composer'}, true); dis.dispatch({action: 'focus_composer'}, true);
} }
}, };
/* /*
SOME HACKERY BELOW: SOME HACKERY BELOW:
@ -307,22 +362,22 @@ const LoggedInView = createReactClass({
We also listen with a native listener on the document to get keydown events when no element is focused. We also listen with a native listener on the document to get keydown events when no element is focused.
Bubbling is irrelevant here as the target is the body element. Bubbling is irrelevant here as the target is the body element.
*/ */
_onReactKeyDown: function(ev) { _onReactKeyDown = (ev) => {
// events caught while bubbling up on the root element // events caught while bubbling up on the root element
// of this component, so something must be focused. // of this component, so something must be focused.
this._onKeyDown(ev); this._onKeyDown(ev);
}, };
_onNativeKeyDown: function(ev) { _onNativeKeyDown = (ev) => {
// only pass this if there is no focused element. // only pass this if there is no focused element.
// if there is, _onKeyDown will be called by the // if there is, _onKeyDown will be called by the
// react keydown handler that respects the react bubbling order. // react keydown handler that respects the react bubbling order.
if (ev.target === document.body) { if (ev.target === document.body) {
this._onKeyDown(ev); this._onKeyDown(ev);
} }
}, };
_onKeyDown: function(ev) { _onKeyDown = (ev) => {
/* /*
// Remove this for now as ctrl+alt = alt-gr so this breaks keyboards which rely on alt-gr for numbers // Remove this for now as ctrl+alt = alt-gr so this breaks keyboards which rely on alt-gr for numbers
// Will need to find a better meta key if anyone actually cares about using this. // Will need to find a better meta key if anyone actually cares about using this.
@ -432,19 +487,19 @@ const LoggedInView = createReactClass({
// that would prevent typing in the now-focussed composer // that would prevent typing in the now-focussed composer
} }
} }
}, };
/** /**
* dispatch a page-up/page-down/etc to the appropriate component * dispatch a page-up/page-down/etc to the appropriate component
* @param {Object} ev The key event * @param {Object} ev The key event
*/ */
_onScrollKeyPressed: function(ev) { _onScrollKeyPressed = (ev) => {
if (this._roomView.current) { if (this._roomView.current) {
this._roomView.current.handleScrollKey(ev); this._roomView.current.handleScrollKey(ev);
} }
}, };
_onDragEnd: function(result) { _onDragEnd = (result) => {
// Dragged to an invalid destination, not onto a droppable // Dragged to an invalid destination, not onto a droppable
if (!result.destination) { if (!result.destination) {
return; return;
@ -467,9 +522,9 @@ const LoggedInView = createReactClass({
} else if (dest.startsWith('room-sub-list-droppable_')) { } else if (dest.startsWith('room-sub-list-droppable_')) {
this._onRoomTileEndDrag(result); this._onRoomTileEndDrag(result);
} }
}, };
_onRoomTileEndDrag: function(result) { _onRoomTileEndDrag = (result) => {
let newTag = result.destination.droppableId.split('_')[1]; let newTag = result.destination.droppableId.split('_')[1];
let prevTag = result.source.droppableId.split('_')[1]; let prevTag = result.source.droppableId.split('_')[1];
if (newTag === 'undefined') newTag = undefined; if (newTag === 'undefined') newTag = undefined;
@ -486,9 +541,9 @@ const LoggedInView = createReactClass({
prevTag, newTag, prevTag, newTag,
oldIndex, newIndex, oldIndex, newIndex,
), true); ), true);
}, };
_onMouseDown: function(ev) { _onMouseDown = (ev) => {
// When the panels are disabled, clicking on them results in a mouse event // When the panels are disabled, clicking on them results in a mouse event
// which bubbles to certain elements in the tree. When this happens, close // which bubbles to certain elements in the tree. When this happens, close
// any settings page that is currently open (user/room/group). // any settings page that is currently open (user/room/group).
@ -507,9 +562,9 @@ const LoggedInView = createReactClass({
}); });
} }
} }
}, };
_onMouseUp: function(ev) { _onMouseUp = (ev) => {
if (!this.state.mouseDown) return; if (!this.state.mouseDown) return;
const deltaX = ev.pageX - this.state.mouseDown.x; const deltaX = ev.pageX - this.state.mouseDown.x;
@ -528,13 +583,9 @@ const LoggedInView = createReactClass({
// Always clear the mouseDown state to ensure we don't accidentally // Always clear the mouseDown state to ensure we don't accidentally
// use stale values due to the mouseDown checks. // use stale values due to the mouseDown checks.
this.setState({mouseDown: null}); this.setState({mouseDown: null});
}, };
_setResizeContainerRef(div) { render() {
this.resizeContainer = div;
},
render: function() {
const LeftPanel = sdk.getComponent('structures.LeftPanel'); const LeftPanel = sdk.getComponent('structures.LeftPanel');
const RoomView = sdk.getComponent('structures.RoomView'); const RoomView = sdk.getComponent('structures.RoomView');
const UserView = sdk.getComponent('structures.UserView'); const UserView = sdk.getComponent('structures.UserView');
@ -647,7 +698,7 @@ const LoggedInView = createReactClass({
{ topBar } { topBar }
<ToastContainer /> <ToastContainer />
<DragDropContext onDragEnd={this._onDragEnd}> <DragDropContext onDragEnd={this._onDragEnd}>
<div ref={this._setResizeContainerRef} className={bodyClasses}> <div ref={this._resizeContainer} className={bodyClasses}>
<LeftPanel <LeftPanel
resizeNotifier={this.props.resizeNotifier} resizeNotifier={this.props.resizeNotifier}
collapsed={this.props.collapseLhs || false} collapsed={this.props.collapseLhs || false}
@ -660,7 +711,7 @@ const LoggedInView = createReactClass({
</div> </div>
</MatrixClientContext.Provider> </MatrixClientContext.Provider>
); );
}, }
}); }
export default LoggedInView; export default LoggedInView;