diff --git a/.buildkite/pipeline.yaml b/.buildkite/pipeline.yaml index 639c7420f0..be0d5e404c 100644 --- a/.buildkite/pipeline.yaml +++ b/.buildkite/pipeline.yaml @@ -1,10 +1,6 @@ steps: - label: ":eslint: Lint" command: - # TODO: Remove hacky chmod for BuildKite - - "echo '--- Setup'" - - "chmod +x ./scripts/ci/*.sh" - - "chmod +x ./scripts/*" - "echo '--- Install js-sdk'" - "./scripts/ci/install-deps.sh" - "yarn lintwithexclusions" diff --git a/CHANGELOG.md b/CHANGELOG.md index 8fe6f80e43..5390cad319 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,117 @@ +Changes in [1.7.5](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v1.7.5) (2019-12-09) +=================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v1.7.5-rc.1...v1.7.5) + + * No changes since rc.1 + +Changes in [1.7.5-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v1.7.5-rc.1) (2019-12-04) +============================================================================================================= +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v1.7.4...v1.7.5-rc.1) + + * Remove logs before running end-to-end tests + [\#3700](https://github.com/matrix-org/matrix-react-sdk/pull/3700) + * Update from Weblate + [\#3699](https://github.com/matrix-org/matrix-react-sdk/pull/3699) + * Match e2e icons on events to expectations + [\#3697](https://github.com/matrix-org/matrix-react-sdk/pull/3697) + * Match room upgrade warning to the new design + [\#3695](https://github.com/matrix-org/matrix-react-sdk/pull/3695) + * Remove unused translations + [\#3683](https://github.com/matrix-org/matrix-react-sdk/pull/3683) + * Remove broken velocity-ui animation + [\#3678](https://github.com/matrix-org/matrix-react-sdk/pull/3678) + * Update from Weblate + [\#3696](https://github.com/matrix-org/matrix-react-sdk/pull/3696) + * Hide Remove button in message editing history if you don't have permission + to redact + [\#3685](https://github.com/matrix-org/matrix-react-sdk/pull/3685) + * Add an option to invite users to upgraded private rooms + [\#3684](https://github.com/matrix-org/matrix-react-sdk/pull/3684) + * Do not trap Key ContextMenu into composer for keyboard a11y + [\#3689](https://github.com/matrix-org/matrix-react-sdk/pull/3689) + * Make EmojiPicker filtering case-insensitive + [\#3690](https://github.com/matrix-org/matrix-react-sdk/pull/3690) + * Ensure the settings page accurately represents theme choices + [\#3686](https://github.com/matrix-org/matrix-react-sdk/pull/3686) + * Ensure read receipts end up with a valid reference to checkUnmounting + [\#3688](https://github.com/matrix-org/matrix-react-sdk/pull/3688) + * Convert Velociraptor component to a class + [\#3687](https://github.com/matrix-org/matrix-react-sdk/pull/3687) + * Add a link to the labs feature documentation + [\#3675](https://github.com/matrix-org/matrix-react-sdk/pull/3675) + * Improve translatable strings for calls + [\#3682](https://github.com/matrix-org/matrix-react-sdk/pull/3682) + * Don't assume that diffs will have an appropriate child node + [\#3680](https://github.com/matrix-org/matrix-react-sdk/pull/3680) + * Fix persisted widgets getting stuck at loading screens + [\#3681](https://github.com/matrix-org/matrix-react-sdk/pull/3681) + * Add button to clear all notification counts, sometimes stuck in historical + [\#2959](https://github.com/matrix-org/matrix-react-sdk/pull/2959) + * Fix multi-invite error dialog messaging + [\#3679](https://github.com/matrix-org/matrix-react-sdk/pull/3679) + * Make the communities button behave more like a toggle + [\#3670](https://github.com/matrix-org/matrix-react-sdk/pull/3670) + * Change read markers to use CSS transitions + [\#3674](https://github.com/matrix-org/matrix-react-sdk/pull/3674) + * fix font smoothing to match figma + [\#3677](https://github.com/matrix-org/matrix-react-sdk/pull/3677) + * Update breadcrumbs when we do eventually see upgraded rooms + [\#3669](https://github.com/matrix-org/matrix-react-sdk/pull/3669) + * Fix override behaviour of system vs defined themes + [\#3673](https://github.com/matrix-org/matrix-react-sdk/pull/3673) + * console.log doesn't take %s substitutions + [\#3671](https://github.com/matrix-org/matrix-react-sdk/pull/3671) + * EventIndex: Move the checkpoint loading logic into the init method. + [\#3648](https://github.com/matrix-org/matrix-react-sdk/pull/3648) + * Clarify that cross-signing is in development + [\#3668](https://github.com/matrix-org/matrix-react-sdk/pull/3668) + * Hide tooltips with CSS when they aren't visible + [\#3665](https://github.com/matrix-org/matrix-react-sdk/pull/3665) + * a11y: adjustments for toasts + [\#3667](https://github.com/matrix-org/matrix-react-sdk/pull/3667) + * Update from Weblate + [\#3666](https://github.com/matrix-org/matrix-react-sdk/pull/3666) + * Null check on thumbnail_file + [\#3664](https://github.com/matrix-org/matrix-react-sdk/pull/3664) + * Fix double date separator for room upgrade tiles + [\#3662](https://github.com/matrix-org/matrix-react-sdk/pull/3662) + * Show incoming verification requests in in-app notifications + [\#3661](https://github.com/matrix-org/matrix-react-sdk/pull/3661) + * Show m.room.create event before the ELS on room upgrade + [\#3655](https://github.com/matrix-org/matrix-react-sdk/pull/3655) + * Convert MessagePanel to React class + [\#3656](https://github.com/matrix-org/matrix-react-sdk/pull/3656) + * Make addEventListener conditional + [\#3657](https://github.com/matrix-org/matrix-react-sdk/pull/3657) + * Fix e2e icons + [\#3653](https://github.com/matrix-org/matrix-react-sdk/pull/3653) + * Workaround for soft-crash with calls on startup + [\#3654](https://github.com/matrix-org/matrix-react-sdk/pull/3654) + * Catch exceptions when we can't play audio + [\#3652](https://github.com/matrix-org/matrix-react-sdk/pull/3652) + * Rename section heading for integrations in settings + [\#3650](https://github.com/matrix-org/matrix-react-sdk/pull/3650) + * Update copy for widgets not using message encryption + [\#3651](https://github.com/matrix-org/matrix-react-sdk/pull/3651) + * Ignore media actions + [\#3649](https://github.com/matrix-org/matrix-react-sdk/pull/3649) + * Add an option to disable the use of integration managers for provisioning + [\#3646](https://github.com/matrix-org/matrix-react-sdk/pull/3646) + * Move many widget options to a context menu + [\#3645](https://github.com/matrix-org/matrix-react-sdk/pull/3645) + * Re-add encryption warning to widget permission prompt + [\#3644](https://github.com/matrix-org/matrix-react-sdk/pull/3644) + * Update CIDER docs now that it is used for main composer as well + [\#3647](https://github.com/matrix-org/matrix-react-sdk/pull/3647) + * get rid of bluebird + [\#3593](https://github.com/matrix-org/matrix-react-sdk/pull/3593) + * Remove getBaseTheme + [\#3638](https://github.com/matrix-org/matrix-react-sdk/pull/3638) + * ReactionsRowButtonTooltip: fix null dereference if emoji owner left room + [\#3643](https://github.com/matrix-org/matrix-react-sdk/pull/3643) + * Add eslint-plugin-jest because we inherit js-sdk's eslintrc and it wants + [\#3642](https://github.com/matrix-org/matrix-react-sdk/pull/3642) + Changes in [1.7.4](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v1.7.4) (2019-11-27) =================================================================================================== [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v1.7.3...v1.7.4) diff --git a/code_style.md b/code_style.md index 4b2338064c..3ad0d38873 100644 --- a/code_style.md +++ b/code_style.md @@ -174,12 +174,6 @@ React // Best, if onFooClick would do anything other than directly calling doStuff ``` - Not doing so is acceptable in a single case: in function-refs: - - ```jsx - this.component = self}> - ``` - - Prefer classes that extend `React.Component` (or `React.PureComponent`) instead of `React.createClass` - You can avoid the need to bind handler functions by using [property initializers](https://reactjs.org/docs/react-component.html#constructor): @@ -208,3 +202,5 @@ React ``` - Think about whether your component really needs state: are you duplicating information in component state that could be derived from the model? + +- Avoid things marked as Legacy or Deprecated in React 16 (e.g string refs and legacy contexts) diff --git a/package.json b/package.json index 5b82d9b111..29ddfff5f4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "1.7.4", + "version": "1.7.5", "description": "SDK for matrix.org using React", "author": "matrix.org", "repository": { @@ -88,7 +88,7 @@ "linkifyjs": "^2.1.6", "lodash": "^4.17.14", "lolex": "4.2", - "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop", + "matrix-js-sdk": "2.4.6", "optimist": "^0.6.1", "pako": "^1.0.5", "png-chunks-extract": "^1.0.0", @@ -110,6 +110,7 @@ "text-encoding-utf-8": "^1.0.1", "url": "^0.11.0", "velocity-animate": "^1.5.2", + "what-input": "^5.2.6", "whatwg-fetch": "^1.1.1", "zxcvbn": "^4.4.2" }, diff --git a/res/css/_components.scss b/res/css/_components.scss index b1fbe30f13..529ce9ac85 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -66,6 +66,7 @@ @import "./views/dialogs/_MessageEditHistoryDialog.scss"; @import "./views/dialogs/_RoomSettingsDialog.scss"; @import "./views/dialogs/_RoomUpgradeDialog.scss"; +@import "./views/dialogs/_RoomUpgradeWarningDialog.scss"; @import "./views/dialogs/_SetEmailDialog.scss"; @import "./views/dialogs/_SetMxIdDialog.scss"; @import "./views/dialogs/_SetPasswordDialog.scss"; diff --git a/res/css/views/context_menus/_TopLeftMenu.scss b/res/css/views/context_menus/_TopLeftMenu.scss index 9d258bcf55..d17d683e7e 100644 --- a/res/css/views/context_menus/_TopLeftMenu.scss +++ b/res/css/views/context_menus/_TopLeftMenu.scss @@ -49,23 +49,23 @@ limitations under the License. padding: 0; list-style: none; - li.mx_TopLeftMenu_icon_home::after { + .mx_TopLeftMenu_icon_home::after { mask-image: url('$(res)/img/feather-customised/home.svg'); } - li.mx_TopLeftMenu_icon_settings::after { + .mx_TopLeftMenu_icon_settings::after { mask-image: url('$(res)/img/feather-customised/settings.svg'); } - li.mx_TopLeftMenu_icon_signin::after { + .mx_TopLeftMenu_icon_signin::after { mask-image: url('$(res)/img/feather-customised/sign-in.svg'); } - li.mx_TopLeftMenu_icon_signout::after { + .mx_TopLeftMenu_icon_signout::after { mask-image: url('$(res)/img/feather-customised/sign-out.svg'); } - li::after { + .mx_AccessibleButton::after { mask-repeat: no-repeat; mask-position: 0 center; mask-size: 16px; @@ -78,14 +78,14 @@ limitations under the License. background-color: $primary-fg-color; } - li { + .mx_AccessibleButton { position: relative; cursor: pointer; white-space: nowrap; padding: 5px 20px 5px 43px; } - li:hover { + .mx_AccessibleButton:hover { background-color: $menu-selected-color; } } diff --git a/res/css/views/dialogs/_RoomUpgradeWarningDialog.scss b/res/css/views/dialogs/_RoomUpgradeWarningDialog.scss new file mode 100644 index 0000000000..5b9978eba0 --- /dev/null +++ b/res/css/views/dialogs/_RoomUpgradeWarningDialog.scss @@ -0,0 +1,37 @@ +/* +Copyright 2019 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +.mx_RoomUpgradeWarningDialog { + max-width: 38vw; + width: 38vw; +} + +.mx_RoomUpgradeWarningDialog .mx_SettingsFlag { + font-weight: 700; + + .mx_ToggleSwitch { + display: inline-block; + vertical-align: middle; + margin-left: 8px; + float: right; + } + + .mx_SettingsFlag_label { + display: inline-block; + vertical-align: middle; + } +} + diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index fb85b9cf88..5359992f84 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -169,6 +169,7 @@ limitations under the License. .mx_EventTile:hover .mx_MessageActionBar, .mx_EventTile.mx_EventTile_actionBarFocused .mx_MessageActionBar, +[data-whatinput='keyboard'] .mx_EventTile:focus-within .mx_MessageActionBar, .mx_EventTile.focus-visible:focus-within .mx_MessageActionBar { visibility: visible; } @@ -347,27 +348,28 @@ div.mx_EventTile_notSent.mx_EventTile_redacted .mx_UnknownBody { } .mx_EventTile_e2eIcon { - display: block; position: absolute; - top: 8px; + top: 6px; left: 46px; width: 15px; height: 15px; cursor: pointer; - mask-size: 14px; - mask-repeat: no-repeat; - mask-position: 0; + display: block; + bottom: 0; + right: 0; opacity: 0.2; + background-repeat: no-repeat; + background-size: contain; } .mx_EventTile_e2eIcon_undecryptable, .mx_EventTile_e2eIcon_unverified { - mask-image: url('$(res)/img/e2e/warning.svg'); - background-color: $warning-color; + background-image: url('$(res)/img/e2e/warning.svg'); + opacity: 1; } .mx_EventTile_e2eIcon_unencrypted { - mask-image: url('$(res)/img/e2e/warning.svg'); - background-color: $composer-e2e-icon-color; + background-image: url('$(res)/img/e2e/warning.svg'); + opacity: 1; } .mx_EventTile_e2eIcon_hidden { diff --git a/res/css/views/rooms/_SearchBar.scss b/res/css/views/rooms/_SearchBar.scss index 894473a5fe..b6748e5ad2 100644 --- a/res/css/views/rooms/_SearchBar.scss +++ b/res/css/views/rooms/_SearchBar.scss @@ -37,6 +37,10 @@ limitations under the License. mask-position: center; } + .mx_SearchBar_buttons { + display: inherit; + } + .mx_SearchBar_button { border: 0; margin: 0 0 0 22px; diff --git a/scripts/ci/build.sh b/scripts/ci/build.sh old mode 100644 new mode 100755 diff --git a/scripts/ci/end-to-end-tests.sh b/scripts/ci/end-to-end-tests.sh old mode 100644 new mode 100755 index 567c853c2b..ae88ef70c7 --- a/scripts/ci/end-to-end-tests.sh +++ b/scripts/ci/end-to-end-tests.sh @@ -36,7 +36,7 @@ echo "--- Install synapse & other dependencies" ./install.sh # install static webserver to server symlinked local copy of riot ./riot/install-webserver.sh -mkdir logs +mkdir logs || rm -r logs/* echo "+++ Running end-to-end tests" TESTS_STARTED=1 ./run.sh --no-sandbox --log-directory logs/ diff --git a/scripts/ci/install-deps.sh b/scripts/ci/install-deps.sh old mode 100644 new mode 100755 diff --git a/scripts/ci/riot-unit-tests.sh b/scripts/ci/riot-unit-tests.sh old mode 100644 new mode 100755 diff --git a/scripts/ci/unit-tests.sh b/scripts/ci/unit-tests.sh old mode 100644 new mode 100755 diff --git a/src/Keyboard.js b/src/Keyboard.js index f63956777f..453ddab1e2 100644 --- a/src/Keyboard.js +++ b/src/Keyboard.js @@ -78,6 +78,7 @@ export const Key = { CONTROL: "Control", META: "Meta", SHIFT: "Shift", + CONTEXT_MENU: "ContextMenu", LESS_THAN: "<", GREATER_THAN: ">", diff --git a/src/RoomInvite.js b/src/RoomInvite.js index b2b8689174..48baad5d9f 100644 --- a/src/RoomInvite.js +++ b/src/RoomInvite.js @@ -153,13 +153,8 @@ function _onStartDmFinished(shouldInvite, addrs) { } } -function _onRoomInviteFinished(roomId, shouldInvite, addrs) { - if (!shouldInvite) return; - - const addrTexts = addrs.map((addr) => addr.address); - - // Invite new users to a room - inviteMultipleToRoom(roomId, addrTexts).then((result) => { +export function inviteUsersToRoom(roomId, userIds) { + return inviteMultipleToRoom(roomId, userIds).then((result) => { const room = MatrixClientPeg.get().getRoom(roomId); return _showAnyInviteErrors(result.states, room, result.inviter); }).catch((err) => { @@ -172,6 +167,15 @@ function _onRoomInviteFinished(roomId, shouldInvite, addrs) { }); } +function _onRoomInviteFinished(roomId, shouldInvite, addrs) { + if (!shouldInvite) return; + + const addrTexts = addrs.map((addr) => addr.address); + + // Invite new users to a room + inviteUsersToRoom(roomId, addrTexts); +} + // TODO: Immutable DMs replaces this function _isDmChat(addrTexts) { if (addrTexts.length === 1 && getAddressType(addrTexts[0]) === 'mx-user-id') { diff --git a/src/SlashCommands.js b/src/SlashCommands.js index 31e7ca4f39..a9c015fdaf 100644 --- a/src/SlashCommands.js +++ b/src/SlashCommands.js @@ -32,6 +32,7 @@ import { getAddressType } from './UserAddress'; import { abbreviateUrl } from './utils/UrlUtils'; import { getDefaultIdentityServerUrl, useDefaultIdentityServer } from './utils/IdentityServerUtils'; import {isPermalinkHost, parsePermalink} from "./utils/permalinks/Permalinks"; +import {inviteUsersToRoom} from "./RoomInvite"; const singleMxcUpload = async () => { return new Promise((resolve) => { @@ -154,70 +155,58 @@ export const CommandMap = { return reject(_t("You do not have the required permissions to use this command.")); } + const RoomUpgradeWarningDialog = sdk.getComponent("dialogs.RoomUpgradeWarningDialog"); + const {finished} = Modal.createTrackedDialog('Slash Commands', 'upgrade room confirmation', - QuestionDialog, { - title: _t('Room upgrade confirmation'), - description: ( -
-

{_t("Upgrading a room can be destructive and isn't always necessary.")}

-

- {_t( - "Room upgrades are usually recommended when a room version is considered " + - "unstable. Unstable room versions might have bugs, missing features, or " + - "security vulnerabilities.", - {}, { - "i": (sub) => {sub}, - }, - )} -

-

- {_t( - "Room upgrades usually only affect server-side processing of the " + - "room. If you're having problems with your Riot client, please file an issue " + - "with .", - {}, { - "i": (sub) => {sub}, - "issueLink": () => { - return - https://github.com/vector-im/riot-web/issues/new/choose - ; - }, - }, - )} -

-

- {_t( - "Warning: Upgrading a room will not automatically migrate room " + - "members to the new version of the room. We'll post a link to the new room " + - "in the old version of the room - room members will have to click this link to " + - "join the new room.", - {}, { - "b": (sub) => {sub}, - "i": (sub) => {sub}, - }, - )} -

-

- {_t( - "Please confirm that you'd like to go forward with upgrading this room " + - "from to .", - {}, - { - oldVersion: () => {room ? room.getVersion() : "1"}, - newVersion: () => {args}, - }, - )} -

-
- ), - button: _t("Upgrade"), - }); + RoomUpgradeWarningDialog, {roomId: roomId, targetVersion: args}, /*className=*/null, + /*isPriority=*/false, /*isStatic=*/true); - return success(finished.then(([confirm]) => { - if (!confirm) return; + return success(finished.then(async ([resp]) => { + if (!resp.continue) return; - return cli.upgradeRoom(roomId, args); + let checkForUpgradeFn; + try { + const upgradePromise = cli.upgradeRoom(roomId, args); + + // We have to wait for the js-sdk to give us the room back so + // we can more effectively abuse the MultiInviter behaviour + // which heavily relies on the Room object being available. + if (resp.invite) { + checkForUpgradeFn = async (newRoom) => { + // The upgradePromise should be done by the time we await it here. + const {replacement_room: newRoomId} = await upgradePromise; + if (newRoom.roomId !== newRoomId) return; + + const toInvite = [ + ...room.getMembersWithMembership("join"), + ...room.getMembersWithMembership("invite"), + ].map(m => m.userId).filter(m => m !== cli.getUserId()); + + if (toInvite.length > 0) { + // Errors are handled internally to this function + await inviteUsersToRoom(newRoomId, toInvite); + } + + cli.removeListener('Room', checkForUpgradeFn); + }; + cli.on('Room', checkForUpgradeFn); + } + + // We have to await after so that the checkForUpgradesFn has a proper reference + // to the new room's ID. + await upgradePromise; + } catch (e) { + console.error(e); + + if (checkForUpgradeFn) cli.removeListener('Room', checkForUpgradeFn); + + const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog'); + Modal.createTrackedDialog('Slash Commands', 'room upgrade error', ErrorDialog, { + title: _t('Error upgrading room'), + description: _t( + 'Double check that your server supports the room version chosen and try again.'), + }); + } })); } return reject(this.getUsage()); diff --git a/src/Unread.js b/src/Unread.js index 01617dc1ac..d5c5993974 100644 --- a/src/Unread.js +++ b/src/Unread.js @@ -36,6 +36,8 @@ module.exports = { return false; } else if (ev.getType() == 'm.room.aliases' || ev.getType() == 'm.room.canonical_alias') { return false; + } else if (ev.getType() == 'm.room.server_acl') { + return false; } const EventTile = sdk.getComponent('rooms.EventTile'); return EventTile.haveTileForEvent(ev); diff --git a/src/Velociraptor.js b/src/Velociraptor.js index b7a2d7fb40..245ca6648b 100644 --- a/src/Velociraptor.js +++ b/src/Velociraptor.js @@ -1,7 +1,6 @@ const React = require('react'); const ReactDom = require('react-dom'); import PropTypes from 'prop-types'; -import createReactClass from 'create-react-class'; const Velocity = require('velocity-animate'); /** @@ -11,10 +10,8 @@ const Velocity = require('velocity-animate'); * from DOM order. This makes it a lot simpler and lighter: if you need fully * automatic positional animation, look at react-shuffle or similar libraries. */ -module.exports = createReactClass({ - displayName: 'Velociraptor', - - propTypes: { +export default class Velociraptor extends React.Component { + static propTypes = { // either a list of child nodes, or a single child. children: PropTypes.any, @@ -26,82 +23,71 @@ module.exports = createReactClass({ // a list of transition options from the corresponding startStyle enterTransitionOpts: PropTypes.array, - }, + }; - getDefaultProps: function() { - return { - startStyles: [], - enterTransitionOpts: [], - }; - }, + static defaultProps = { + startStyles: [], + enterTransitionOpts: [], + }; + + constructor(props) { + super(props); - componentWillMount: function() { this.nodes = {}; this._updateChildren(this.props.children); - }, + } - componentWillReceiveProps: function(nextProps) { - this._updateChildren(nextProps.children); - }, + componentDidUpdate() { + this._updateChildren(this.props.children); + } - /** - * update `this.children` according to the new list of children given - */ - _updateChildren: function(newChildren) { - const self = this; + _updateChildren(newChildren) { const oldChildren = this.children || {}; this.children = {}; - React.Children.toArray(newChildren).forEach(function(c) { + React.Children.toArray(newChildren).forEach((c) => { if (oldChildren[c.key]) { const old = oldChildren[c.key]; - const oldNode = ReactDom.findDOMNode(self.nodes[old.key]); + const oldNode = ReactDom.findDOMNode(this.nodes[old.key]); - if (oldNode && oldNode.style.left != c.props.style.left) { - Velocity(oldNode, { left: c.props.style.left }, self.props.transition).then(function() { + if (oldNode && oldNode.style.left !== c.props.style.left) { + Velocity(oldNode, { left: c.props.style.left }, this.props.transition).then(() => { // special case visibility because it's nonsensical to animate an invisible element // so we always hidden->visible pre-transition and visible->hidden after - if (oldNode.style.visibility == 'visible' && c.props.style.visibility == 'hidden') { + if (oldNode.style.visibility === 'visible' && c.props.style.visibility === 'hidden') { oldNode.style.visibility = c.props.style.visibility; } }); //console.log("translation: "+oldNode.style.left+" -> "+c.props.style.left); } - if (oldNode && oldNode.style.visibility == 'hidden' && c.props.style.visibility == 'visible') { + if (oldNode && oldNode.style.visibility === 'hidden' && c.props.style.visibility === 'visible') { oldNode.style.visibility = c.props.style.visibility; } // clone the old element with the props (and children) of the new element // so prop updates are still received by the children. - self.children[c.key] = React.cloneElement(old, c.props, c.props.children); + this.children[c.key] = React.cloneElement(old, c.props, c.props.children); } else { // new element. If we have a startStyle, use that as the style and go through // the enter animations const newProps = {}; const restingStyle = c.props.style; - const startStyles = self.props.startStyles; + const startStyles = this.props.startStyles; if (startStyles.length > 0) { const startStyle = startStyles[0]; newProps.style = startStyle; // console.log("mounted@startstyle0: "+JSON.stringify(startStyle)); } - newProps.ref = ((n) => self._collectNode( + newProps.ref = ((n) => this._collectNode( c.key, n, restingStyle, )); - self.children[c.key] = React.cloneElement(c, newProps); + this.children[c.key] = React.cloneElement(c, newProps); } }); - }, + } - /** - * called when a child element is mounted/unmounted - * - * @param {string} k key of the child - * @param {null|Object} node On mount: React node. On unmount: null - * @param {Object} restingStyle final style - */ - _collectNode: function(k, node, restingStyle) { + _collectNode(k, node, restingStyle) { if ( node && this.nodes[k] === undefined && @@ -125,12 +111,12 @@ module.exports = createReactClass({ // and then we animate to the resting state Velocity(domNode, restingStyle, - transitionOpts[i-1]) - .then(() => { - // once we've reached the resting state, hide the element if - // appropriate - domNode.style.visibility = restingStyle.visibility; - }); + transitionOpts[i-1]) + .then(() => { + // once we've reached the resting state, hide the element if + // appropriate + domNode.style.visibility = restingStyle.visibility; + }); /* console.log("enter:", @@ -153,13 +139,13 @@ module.exports = createReactClass({ if (domNode) Velocity.Utilities.removeData(domNode); } this.nodes[k] = node; - }, + } - render: function() { + render() { return ( { Object.values(this.children) } ); - }, -}); + } +} diff --git a/src/async-components/views/dialogs/ExportE2eKeysDialog.js b/src/async-components/views/dialogs/ExportE2eKeysDialog.js index 0fd412935a..ba2e985889 100644 --- a/src/async-components/views/dialogs/ExportE2eKeysDialog.js +++ b/src/async-components/views/dialogs/ExportE2eKeysDialog.js @@ -15,7 +15,7 @@ limitations under the License. */ import FileSaver from 'file-saver'; -import React from 'react'; +import React, {createRef} from 'react'; import PropTypes from 'prop-types'; import createReactClass from 'create-react-class'; import { _t } from '../../../languageHandler'; @@ -44,6 +44,9 @@ export default createReactClass({ componentWillMount: function() { this._unmounted = false; + + this._passphrase1 = createRef(); + this._passphrase2 = createRef(); }, componentWillUnmount: function() { @@ -53,8 +56,8 @@ export default createReactClass({ _onPassphraseFormSubmit: function(ev) { ev.preventDefault(); - const passphrase = this.refs.passphrase1.value; - if (passphrase !== this.refs.passphrase2.value) { + const passphrase = this._passphrase1.current.value; + if (passphrase !== this._passphrase2.current.value) { this.setState({errStr: _t('Passphrases must match')}); return false; } @@ -148,7 +151,7 @@ export default createReactClass({
- @@ -161,7 +164,7 @@ export default createReactClass({
- diff --git a/src/async-components/views/dialogs/ImportE2eKeysDialog.js b/src/async-components/views/dialogs/ImportE2eKeysDialog.js index 17f3bba117..de9e819f5a 100644 --- a/src/async-components/views/dialogs/ImportE2eKeysDialog.js +++ b/src/async-components/views/dialogs/ImportE2eKeysDialog.js @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; +import React, {createRef} from 'react'; import PropTypes from 'prop-types'; import createReactClass from 'create-react-class'; @@ -56,6 +56,9 @@ export default createReactClass({ componentWillMount: function() { this._unmounted = false; + + this._file = createRef(); + this._passphrase = createRef(); }, componentWillUnmount: function() { @@ -63,15 +66,15 @@ export default createReactClass({ }, _onFormChange: function(ev) { - const files = this.refs.file.files || []; + const files = this._file.current.files || []; this.setState({ - enableSubmit: (this.refs.passphrase.value !== "" && files.length > 0), + enableSubmit: (this._passphrase.current.value !== "" && files.length > 0), }); }, _onFormSubmit: function(ev) { ev.preventDefault(); - this._startImport(this.refs.file.files[0], this.refs.passphrase.value); + this._startImport(this._file.current.files[0], this._passphrase.current.value); return false; }, @@ -146,7 +149,10 @@ export default createReactClass({
- @@ -159,8 +165,11 @@ export default createReactClass({
-
diff --git a/src/components/structures/ContextMenu.js b/src/components/structures/ContextMenu.js new file mode 100644 index 0000000000..e861e3d45f --- /dev/null +++ b/src/components/structures/ContextMenu.js @@ -0,0 +1,484 @@ +/* +Copyright 2015, 2016 OpenMarket Ltd +Copyright 2018 New Vector Ltd +Copyright 2019 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React, {useRef, useState} from 'react'; +import ReactDOM from 'react-dom'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; +import {Key} from "../../Keyboard"; +import sdk from "../../index"; +import AccessibleButton from "../views/elements/AccessibleButton"; + +// Shamelessly ripped off Modal.js. There's probably a better way +// of doing reusable widgets like dialog boxes & menus where we go and +// pass in a custom control as the actual body. + +const ContextualMenuContainerId = "mx_ContextualMenu_Container"; + +function getOrCreateContainer() { + let container = document.getElementById(ContextualMenuContainerId); + + if (!container) { + container = document.createElement("div"); + container.id = ContextualMenuContainerId; + document.body.appendChild(container); + } + + return container; +} + +const ARIA_MENU_ITEM_ROLES = new Set(["menuitem", "menuitemcheckbox", "menuitemradio"]); +// Generic ContextMenu Portal wrapper +// all options inside the menu should be of role=menuitem/menuitemcheckbox/menuitemradiobutton and have tabIndex={-1} +// this will allow the ContextMenu to manage its own focus using arrow keys as per the ARIA guidelines. +export class ContextMenu extends React.Component { + static propTypes = { + top: PropTypes.number, + bottom: PropTypes.number, + left: PropTypes.number, + right: PropTypes.number, + menuWidth: PropTypes.number, + menuHeight: PropTypes.number, + chevronOffset: PropTypes.number, + chevronFace: PropTypes.string, // top, bottom, left, right or none + // Function to be called on menu close + onFinished: PropTypes.func.isRequired, + menuPaddingTop: PropTypes.number, + menuPaddingRight: PropTypes.number, + menuPaddingBottom: PropTypes.number, + menuPaddingLeft: PropTypes.number, + zIndex: PropTypes.number, + + // If true, insert an invisible screen-sized element behind the + // menu that when clicked will close it. + hasBackground: PropTypes.bool, + + // on resize callback + windowResize: PropTypes.func, + + catchTab: PropTypes.bool, // whether to close the ContextMenu on TAB (default=true) + }; + + static defaultProps = { + hasBackground: true, + catchTab: true, + }; + + constructor() { + super(); + this.state = { + contextMenuElem: null, + }; + + // persist what had focus when we got initialized so we can return it after + this.initialFocus = document.activeElement; + } + + componentWillUnmount() { + // return focus to the thing which had it before us + this.initialFocus.focus(); + } + + collectContextMenuRect = (element) => { + // We don't need to clean up when unmounting, so ignore + if (!element) return; + + let first = element.querySelector('[role^="menuitem"]'); + if (!first) { + first = element.querySelector('[tab-index]'); + } + if (first) { + first.focus(); + } + + this.setState({ + contextMenuElem: element, + }); + }; + + onContextMenu = (e) => { + if (this.props.onFinished) { + this.props.onFinished(); + + e.preventDefault(); + const x = e.clientX; + const y = e.clientY; + + // XXX: This isn't pretty but the only way to allow opening a different context menu on right click whilst + // a context menu and its click-guard are up without completely rewriting how the context menus work. + setImmediate(() => { + const clickEvent = document.createEvent('MouseEvents'); + clickEvent.initMouseEvent( + 'contextmenu', true, true, window, 0, + 0, 0, x, y, false, false, + false, false, 0, null, + ); + document.elementFromPoint(x, y).dispatchEvent(clickEvent); + }); + } + }; + + _onMoveFocus = (element, up) => { + let descending = false; // are we currently descending or ascending through the DOM tree? + + do { + const child = up ? element.lastElementChild : element.firstElementChild; + const sibling = up ? element.previousElementSibling : element.nextElementSibling; + + if (descending) { + if (child) { + element = child; + } else if (sibling) { + element = sibling; + } else { + descending = false; + element = element.parentElement; + } + } else { + if (sibling) { + element = sibling; + descending = true; + } else { + element = element.parentElement; + } + } + + if (element) { + if (element.classList.contains("mx_ContextualMenu")) { // we hit the top + element = up ? element.lastElementChild : element.firstElementChild; + descending = true; + } + } + } while (element && !ARIA_MENU_ITEM_ROLES.has(element.getAttribute("role"))); + + if (element) { + element.focus(); + } + }; + + _onMoveFocusHomeEnd = (element, up) => { + let results = element.querySelectorAll('[role^="menuitem"]'); + if (!results) { + results = element.querySelectorAll('[tab-index]'); + } + if (results && results.length) { + if (up) { + results[0].focus(); + } else { + results[results.length - 1].focus(); + } + } + }; + + _onKeyDown = (ev) => { + let handled = true; + + switch (ev.key) { + case Key.TAB: + if (!this.props.catchTab) { + handled = false; + break; + } + // fallthrough + case Key.ESCAPE: + this.props.onFinished(); + break; + case Key.ARROW_UP: + this._onMoveFocus(ev.target, true); + break; + case Key.ARROW_DOWN: + this._onMoveFocus(ev.target, false); + break; + case Key.HOME: + this._onMoveFocusHomeEnd(this.state.contextMenuElem, true); + break; + case Key.END: + this._onMoveFocusHomeEnd(this.state.contextMenuElem, false); + break; + default: + handled = false; + } + + if (handled) { + // consume all other keys in context menu + ev.stopPropagation(); + ev.preventDefault(); + } + }; + + renderMenu(hasBackground=this.props.hasBackground) { + const position = {}; + let chevronFace = null; + const props = this.props; + + if (props.top) { + position.top = props.top; + } else { + position.bottom = props.bottom; + } + + if (props.left) { + position.left = props.left; + chevronFace = 'left'; + } else { + position.right = props.right; + chevronFace = 'right'; + } + + const contextMenuRect = this.state.contextMenuElem ? this.state.contextMenuElem.getBoundingClientRect() : null; + const padding = 10; + + const chevronOffset = {}; + if (props.chevronFace) { + chevronFace = props.chevronFace; + } + const hasChevron = chevronFace && chevronFace !== "none"; + + if (chevronFace === 'top' || chevronFace === 'bottom') { + chevronOffset.left = props.chevronOffset; + } else { + const target = position.top; + + // By default, no adjustment is made + let adjusted = target; + + // If we know the dimensions of the context menu, adjust its position + // such that it does not leave the (padded) window. + if (contextMenuRect) { + adjusted = Math.min(position.top, document.body.clientHeight - contextMenuRect.height - padding); + } + + position.top = adjusted; + chevronOffset.top = Math.max(props.chevronOffset, props.chevronOffset + target - adjusted); + } + + let chevron; + if (hasChevron) { + chevron =
; + } + + const menuClasses = classNames({ + 'mx_ContextualMenu': true, + 'mx_ContextualMenu_left': !hasChevron && position.left, + 'mx_ContextualMenu_right': !hasChevron && position.right, + 'mx_ContextualMenu_top': !hasChevron && position.top, + 'mx_ContextualMenu_bottom': !hasChevron && position.bottom, + 'mx_ContextualMenu_withChevron_left': chevronFace === 'left', + 'mx_ContextualMenu_withChevron_right': chevronFace === 'right', + 'mx_ContextualMenu_withChevron_top': chevronFace === 'top', + 'mx_ContextualMenu_withChevron_bottom': chevronFace === 'bottom', + }); + + const menuStyle = {}; + if (props.menuWidth) { + menuStyle.width = props.menuWidth; + } + + if (props.menuHeight) { + menuStyle.height = props.menuHeight; + } + + if (!isNaN(Number(props.menuPaddingTop))) { + menuStyle["paddingTop"] = props.menuPaddingTop; + } + if (!isNaN(Number(props.menuPaddingLeft))) { + menuStyle["paddingLeft"] = props.menuPaddingLeft; + } + if (!isNaN(Number(props.menuPaddingBottom))) { + menuStyle["paddingBottom"] = props.menuPaddingBottom; + } + if (!isNaN(Number(props.menuPaddingRight))) { + menuStyle["paddingRight"] = props.menuPaddingRight; + } + + const wrapperStyle = {}; + if (!isNaN(Number(props.zIndex))) { + menuStyle["zIndex"] = props.zIndex + 1; + wrapperStyle["zIndex"] = props.zIndex; + } + + let background; + if (hasBackground) { + background = ( +
+ ); + } + + return ( +
+
+ { chevron } + { props.children } +
+ { background } +
+ ); + } + + render() { + return ReactDOM.createPortal(this.renderMenu(), getOrCreateContainer()); + } +} + +// Semantic component for representing the AccessibleButton which launches a +export const ContextMenuButton = ({ label, isExpanded, children, ...props }) => { + const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); + return ( + + { children } + + ); +}; +ContextMenuButton.propTypes = { + ...AccessibleButton.propTypes, + label: PropTypes.string.isRequired, + isExpanded: PropTypes.bool.isRequired, // whether or not the context menu is currently open +}; + +// Semantic component for representing a role=menuitem +export const MenuItem = ({children, label, ...props}) => { + const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); + return ( + + { children } + + ); +}; +MenuItem.propTypes = { + ...AccessibleButton.propTypes, + label: PropTypes.string, // optional + className: PropTypes.string, // optional + onClick: PropTypes.func.isRequired, +}; + +// Semantic component for representing a role=group for grouping menu radios/checkboxes +export const MenuGroup = ({children, label, ...props}) => { + return
+ { children } +
; +}; +MenuGroup.propTypes = { + ...AccessibleButton.propTypes, + label: PropTypes.string.isRequired, + className: PropTypes.string, // optional +}; + +// Semantic component for representing a role=menuitemcheckbox +export const MenuItemCheckbox = ({children, label, active=false, disabled=false, ...props}) => { + const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); + return ( + + { children } + + ); +}; +MenuItemCheckbox.propTypes = { + ...AccessibleButton.propTypes, + label: PropTypes.string, // optional + active: PropTypes.bool.isRequired, + disabled: PropTypes.bool, // optional + className: PropTypes.string, // optional + onClick: PropTypes.func.isRequired, +}; + +// Semantic component for representing a role=menuitemradio +export const MenuItemRadio = ({children, label, active=false, disabled=false, ...props}) => { + const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); + return ( + + { children } + + ); +}; +MenuItemRadio.propTypes = { + ...AccessibleButton.propTypes, + label: PropTypes.string, // optional + active: PropTypes.bool.isRequired, + disabled: PropTypes.bool, // optional + className: PropTypes.string, // optional + onClick: PropTypes.func.isRequired, +}; + +// Placement method for to position context menu to right of elementRect with chevronOffset +export const toRightOf = (elementRect, chevronOffset=12) => { + const left = elementRect.right + window.pageXOffset + 3; + let top = elementRect.top + (elementRect.height / 2) + window.pageYOffset; + top -= chevronOffset + 8; // where 8 is half the height of the chevron + return {left, top, chevronOffset}; +}; + +// Placement method for to position context menu right-aligned and flowing to the left of elementRect +export const aboveLeftOf = (elementRect, chevronFace="none") => { + const menuOptions = { chevronFace }; + + const buttonRight = elementRect.right + window.pageXOffset; + const buttonBottom = elementRect.bottom + window.pageYOffset; + const buttonTop = elementRect.top + window.pageYOffset; + // Align the right edge of the menu to the right edge of the button + menuOptions.right = window.innerWidth - buttonRight; + // Align the menu vertically on whichever side of the button has more space available. + if (buttonBottom < window.innerHeight / 2) { + menuOptions.top = buttonBottom; + } else { + menuOptions.bottom = window.innerHeight - buttonTop; + } + + return menuOptions; +}; + +export const useContextMenu = () => { + const button = useRef(null); + const [isOpen, setIsOpen] = useState(false); + const open = () => { + setIsOpen(true); + }; + const close = () => { + setIsOpen(false); + }; + + return [isOpen, button, open, close, setIsOpen]; +}; + +export default class LegacyContextMenu extends ContextMenu { + render() { + return this.renderMenu(false); + } +} + +// XXX: Deprecated, used only for dynamic Tooltips. Avoid using at all costs. +export function createMenu(ElementClass, props) { + const onFinished = function(...args) { + ReactDOM.unmountComponentAtNode(getOrCreateContainer()); + + if (props && props.onFinished) { + props.onFinished.apply(null, args); + } + }; + + const menu = + + ; + + ReactDOM.render(menu, getOrCreateContainer()); + + return {close: onFinished}; +} diff --git a/src/components/structures/ContextualMenu.js b/src/components/structures/ContextualMenu.js deleted file mode 100644 index 3f8c87efef..0000000000 --- a/src/components/structures/ContextualMenu.js +++ /dev/null @@ -1,253 +0,0 @@ -/* -Copyright 2015, 2016 OpenMarket Ltd -Copyright 2018 New Vector Ltd -Copyright 2019 The Matrix.org Foundation C.I.C. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import React from 'react'; -import ReactDOM from 'react-dom'; -import PropTypes from 'prop-types'; -import classNames from 'classnames'; -import {focusCapturedRef} from "../../utils/Accessibility"; -import {KeyCode} from "../../Keyboard"; - -// Shamelessly ripped off Modal.js. There's probably a better way -// of doing reusable widgets like dialog boxes & menus where we go and -// pass in a custom control as the actual body. - -const ContextualMenuContainerId = "mx_ContextualMenu_Container"; - -function getOrCreateContainer() { - let container = document.getElementById(ContextualMenuContainerId); - - if (!container) { - container = document.createElement("div"); - container.id = ContextualMenuContainerId; - document.body.appendChild(container); - } - - return container; -} - -export default class ContextualMenu extends React.Component { - propTypes: { - top: PropTypes.number, - bottom: PropTypes.number, - left: PropTypes.number, - right: PropTypes.number, - menuWidth: PropTypes.number, - menuHeight: PropTypes.number, - chevronOffset: PropTypes.number, - chevronFace: PropTypes.string, // top, bottom, left, right or none - // Function to be called on menu close - onFinished: PropTypes.func, - menuPaddingTop: PropTypes.number, - menuPaddingRight: PropTypes.number, - menuPaddingBottom: PropTypes.number, - menuPaddingLeft: PropTypes.number, - zIndex: PropTypes.number, - - // If true, insert an invisible screen-sized element behind the - // menu that when clicked will close it. - hasBackground: PropTypes.bool, - - // The component to render as the context menu - elementClass: PropTypes.element.isRequired, - // on resize callback - windowResize: PropTypes.func, - // method to close menu - closeMenu: PropTypes.func.isRequired, - }; - - constructor() { - super(); - this.state = { - contextMenuRect: null, - }; - - this.onContextMenu = this.onContextMenu.bind(this); - this.collectContextMenuRect = this.collectContextMenuRect.bind(this); - } - - collectContextMenuRect(element) { - // We don't need to clean up when unmounting, so ignore - if (!element) return; - - // For screen readers to find the thing - focusCapturedRef(element); - - this.setState({ - contextMenuRect: element.getBoundingClientRect(), - }); - } - - onContextMenu(e) { - if (this.props.closeMenu) { - this.props.closeMenu(); - - e.preventDefault(); - const x = e.clientX; - const y = e.clientY; - - // XXX: This isn't pretty but the only way to allow opening a different context menu on right click whilst - // a context menu and its click-guard are up without completely rewriting how the context menus work. - setImmediate(() => { - const clickEvent = document.createEvent('MouseEvents'); - clickEvent.initMouseEvent( - 'contextmenu', true, true, window, 0, - 0, 0, x, y, false, false, - false, false, 0, null, - ); - document.elementFromPoint(x, y).dispatchEvent(clickEvent); - }); - } - } - - _onKeyDown = (ev) => { - if (ev.keyCode === KeyCode.ESCAPE) { - ev.stopPropagation(); - ev.preventDefault(); - this.props.closeMenu(); - } - }; - - render() { - const position = {}; - let chevronFace = null; - const props = this.props; - - if (props.top) { - position.top = props.top; - } else { - position.bottom = props.bottom; - } - - if (props.left) { - position.left = props.left; - chevronFace = 'left'; - } else { - position.right = props.right; - chevronFace = 'right'; - } - - const contextMenuRect = this.state.contextMenuRect || null; - const padding = 10; - - const chevronOffset = {}; - if (props.chevronFace) { - chevronFace = props.chevronFace; - } - const hasChevron = chevronFace && chevronFace !== "none"; - - if (chevronFace === 'top' || chevronFace === 'bottom') { - chevronOffset.left = props.chevronOffset; - } else { - const target = position.top; - - // By default, no adjustment is made - let adjusted = target; - - // If we know the dimensions of the context menu, adjust its position - // such that it does not leave the (padded) window. - if (contextMenuRect) { - adjusted = Math.min(position.top, document.body.clientHeight - contextMenuRect.height - padding); - } - - position.top = adjusted; - chevronOffset.top = Math.max(props.chevronOffset, props.chevronOffset + target - adjusted); - } - - const chevron = hasChevron ? -
: - undefined; - const className = 'mx_ContextualMenu_wrapper'; - - const menuClasses = classNames({ - 'mx_ContextualMenu': true, - 'mx_ContextualMenu_left': !hasChevron && position.left, - 'mx_ContextualMenu_right': !hasChevron && position.right, - 'mx_ContextualMenu_top': !hasChevron && position.top, - 'mx_ContextualMenu_bottom': !hasChevron && position.bottom, - 'mx_ContextualMenu_withChevron_left': chevronFace === 'left', - 'mx_ContextualMenu_withChevron_right': chevronFace === 'right', - 'mx_ContextualMenu_withChevron_top': chevronFace === 'top', - 'mx_ContextualMenu_withChevron_bottom': chevronFace === 'bottom', - }); - - const menuStyle = {}; - if (props.menuWidth) { - menuStyle.width = props.menuWidth; - } - - if (props.menuHeight) { - menuStyle.height = props.menuHeight; - } - - if (!isNaN(Number(props.menuPaddingTop))) { - menuStyle["paddingTop"] = props.menuPaddingTop; - } - if (!isNaN(Number(props.menuPaddingLeft))) { - menuStyle["paddingLeft"] = props.menuPaddingLeft; - } - if (!isNaN(Number(props.menuPaddingBottom))) { - menuStyle["paddingBottom"] = props.menuPaddingBottom; - } - if (!isNaN(Number(props.menuPaddingRight))) { - menuStyle["paddingRight"] = props.menuPaddingRight; - } - - const wrapperStyle = {}; - if (!isNaN(Number(props.zIndex))) { - menuStyle["zIndex"] = props.zIndex + 1; - wrapperStyle["zIndex"] = props.zIndex; - } - - const ElementClass = props.elementClass; - - // FIXME: If a menu uses getDefaultProps it clobbers the onFinished - // property set here so you can't close the menu from a button click! - return
-
- { chevron } - -
- { props.hasBackground &&
} -
; - } -} - -export function createMenu(ElementClass, props, hasBackground=true) { - const closeMenu = function(...args) { - ReactDOM.unmountComponentAtNode(getOrCreateContainer()); - - if (props && props.onFinished) { - props.onFinished.apply(null, args); - } - }; - - // We only reference closeMenu once per call to createMenu - const menu = ; - - ReactDOM.render(menu, getOrCreateContainer()); - - return {close: closeMenu}; -} diff --git a/src/components/structures/GroupView.js b/src/components/structures/GroupView.js index a0aa36803f..d52599abe9 100644 --- a/src/components/structures/GroupView.js +++ b/src/components/structures/GroupView.js @@ -1214,25 +1214,25 @@ export default createReactClass({ const EditableText = sdk.getComponent("elements.EditableText"); - nameNode = ; + nameNode = ; - shortDescNode = ; + shortDescNode = ; } else { const onGroupHeaderItemClick = this.state.isUserMember ? this._onEditClick : null; const groupAvatarUrl = summary.profile ? summary.profile.avatar_url : null; diff --git a/src/components/structures/InteractiveAuth.js b/src/components/structures/InteractiveAuth.js index e1b02f653b..1981310a2f 100644 --- a/src/components/structures/InteractiveAuth.js +++ b/src/components/structures/InteractiveAuth.js @@ -18,7 +18,7 @@ limitations under the License. import Matrix from 'matrix-js-sdk'; const InteractiveAuth = Matrix.InteractiveAuth; -import React from 'react'; +import React, {createRef} from 'react'; import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; @@ -129,6 +129,8 @@ export default createReactClass({ this._authLogic.poll(); }, 2000); } + + this._stageComponent = createRef(); }, componentWillUnmount: function() { @@ -153,8 +155,8 @@ export default createReactClass({ }, tryContinue: function() { - if (this.refs.stageComponent && this.refs.stageComponent.tryContinue) { - this.refs.stageComponent.tryContinue(); + if (this._stageComponent.current && this._stageComponent.current.tryContinue) { + this._stageComponent.current.tryContinue(); } }, @@ -192,8 +194,8 @@ export default createReactClass({ }, _setFocus: function() { - if (this.refs.stageComponent && this.refs.stageComponent.focus) { - this.refs.stageComponent.focus(); + if (this._stageComponent.current && this._stageComponent.current.focus) { + this._stageComponent.current.focus(); } }, @@ -214,7 +216,8 @@ export default createReactClass({ const StageComponent = getEntryComponentForLoginType(stage); return ( - { hr } @@ -693,6 +695,10 @@ export default class MessagePanel extends React.Component { const readReceipts = this._readReceiptsByEvent[eventId]; + // Dev note: `this._isUnmounting.bind(this)` is important - it ensures that + // the function is run in the context of this class and not EventTile, therefore + // ensuring the right `this._mounted` variable is used by read receipts (which + // don't update their position if we, the MessagePanel, is unmounting). ret.push(
  • { - const scrollPanel = this.refs.scrollPanel; + const scrollPanel = this._scrollPanel.current; if (scrollPanel) { scrollPanel.checkScroll(); } }; _onTypingShown = () => { - const scrollPanel = this.refs.scrollPanel; + const scrollPanel = this._scrollPanel.current; // this will make the timeline grow, so checkScroll scrollPanel.checkScroll(); if (scrollPanel && scrollPanel.getScrollState().stuckAtBottom) { @@ -841,7 +847,7 @@ export default class MessagePanel extends React.Component { }; _onTypingHidden = () => { - const scrollPanel = this.refs.scrollPanel; + const scrollPanel = this._scrollPanel.current; if (scrollPanel) { // as hiding the typing notifications doesn't // update the scrollPanel, we tell it to apply @@ -854,11 +860,11 @@ export default class MessagePanel extends React.Component { }; updateTimelineMinHeight() { - const scrollPanel = this.refs.scrollPanel; + const scrollPanel = this._scrollPanel.current; if (scrollPanel) { const isAtBottom = scrollPanel.isAtBottom(); - const whoIsTyping = this.refs.whoIsTyping; + const whoIsTyping = this._whoIsTyping.current; const isTypingVisible = whoIsTyping && whoIsTyping.isVisible(); // when messages get added to the timeline, // but somebody else is still typing, @@ -871,7 +877,7 @@ export default class MessagePanel extends React.Component { } onTimelineReset() { - const scrollPanel = this.refs.scrollPanel; + const scrollPanel = this._scrollPanel.current; if (scrollPanel) { scrollPanel.clearPreventShrinking(); } @@ -905,19 +911,22 @@ export default class MessagePanel extends React.Component { room={this.props.room} onShown={this._onTypingShown} onHidden={this._onTypingHidden} - ref="whoIsTyping" /> + ref={this._whoIsTyping} /> ); } return ( - + { topSpinner } { this._getEventTiles() } { whoIsTyping } diff --git a/src/components/structures/RoomDirectory.js b/src/components/structures/RoomDirectory.js index efca8d12a8..c8863773f4 100644 --- a/src/components/structures/RoomDirectory.js +++ b/src/components/structures/RoomDirectory.js @@ -572,7 +572,7 @@ module.exports = createReactClass({ if (rows.length === 0 && !this.state.loading) { scrollpanel_content = { _t('No rooms to show') }; } else { - scrollpanel_content = + scrollpanel_content =
    { rows } diff --git a/src/components/structures/RoomSubList.js b/src/components/structures/RoomSubList.js index 921680b678..2fbd19c428 100644 --- a/src/components/structures/RoomSubList.js +++ b/src/components/structures/RoomSubList.js @@ -18,7 +18,6 @@ limitations under the License. */ import React, {createRef} from 'react'; -import createReactClass from 'create-react-class'; import classNames from 'classnames'; import sdk from '../../index'; import dis from '../../dispatcher'; @@ -36,12 +35,11 @@ import {_t} from "../../languageHandler"; // turn this on for drop & drag console debugging galore const debug = false; -const RoomSubList = createReactClass({ - displayName: 'RoomSubList', +export default class RoomSubList extends React.PureComponent { + static displayName = 'RoomSubList'; + static debug = debug; - debug: debug, - - propTypes: { + static propTypes = { list: PropTypes.arrayOf(PropTypes.object).isRequired, label: PropTypes.string.isRequired, tagName: PropTypes.string, @@ -59,10 +57,26 @@ const RoomSubList = createReactClass({ incomingCall: PropTypes.object, extraTiles: PropTypes.arrayOf(PropTypes.node), // extra elements added beneath tiles forceExpand: PropTypes.bool, - }, + }; - getInitialState: function() { + static defaultProps = { + onHeaderClick: function() { + }, // NOP + extraTiles: [], + isInvite: false, + }; + + static getDerivedStateFromProps(props, state) { return { + listLength: props.list.length, + scrollTop: props.list.length === state.listLength ? state.scrollTop : 0, + }; + } + + constructor(props) { + super(props); + + this.state = { hidden: this.props.startAsHidden || false, // some values to get LazyRenderList starting scrollerHeight: 800, @@ -71,47 +85,33 @@ const RoomSubList = createReactClass({ // we have to store the length of the list here so we can see if it's changed or not... listLength: null, }; - }, - getDefaultProps: function() { - return { - onHeaderClick: function() { - }, // NOP - extraTiles: [], - isInvite: false, - }; - }, - - componentDidMount: function() { + this._header = createRef(); + this._subList = createRef(); + this._scroller = createRef(); this._headerButton = createRef(); + } + + componentDidMount() { this.dispatcherRef = dis.register(this.onAction); - }, + } - statics: { - getDerivedStateFromProps: function(props, state) { - return { - listLength: props.list.length, - scrollTop: props.list.length === state.listLength ? state.scrollTop : 0, - }; - }, - }, - - componentWillUnmount: function() { + componentWillUnmount() { dis.unregister(this.dispatcherRef); - }, + } // The header is collapsible if it is hidden or not stuck // The dataset elements are added in the RoomList _initAndPositionStickyHeaders method - isCollapsibleOnClick: function() { - const stuck = this.refs.header.dataset.stuck; + isCollapsibleOnClick() { + const stuck = this._header.current.dataset.stuck; if (!this.props.forceExpand && (this.state.hidden || stuck === undefined || stuck === "none")) { return true; } else { return false; } - }, + } - onAction: function(payload) { + onAction = (payload) => { // XXX: Previously RoomList would forceUpdate whenever on_room_read is dispatched, // but this is no longer true, so we must do it here (and can apply the small // optimisation of checking that we care about the room being read). @@ -124,9 +124,9 @@ const RoomSubList = createReactClass({ ) { this.forceUpdate(); } - }, + }; - onClick: function(ev) { + onClick = (ev) => { if (this.isCollapsibleOnClick()) { // The header isCollapsible, so the click is to be interpreted as collapse and truncation logic const isHidden = !this.state.hidden; @@ -135,11 +135,11 @@ const RoomSubList = createReactClass({ }); } else { // The header is stuck, so the click is to be interpreted as a scroll to the header - this.props.onHeaderClick(this.state.hidden, this.refs.header.dataset.originalPosition); + this.props.onHeaderClick(this.state.hidden, this._header.current.dataset.originalPosition); } - }, + }; - onHeaderKeyDown: function(ev) { + onHeaderKeyDown = (ev) => { switch (ev.key) { case Key.TAB: // Prevent LeftPanel handling Tab if focus is on the sublist header itself @@ -159,7 +159,7 @@ const RoomSubList = createReactClass({ this.onClick(); } else if (!this.props.forceExpand) { // sublist is expanded, go to first room - const element = this.refs.subList && this.refs.subList.querySelector(".mx_RoomTile"); + const element = this._subList.current && this._subList.current.querySelector(".mx_RoomTile"); if (element) { element.focus(); } @@ -167,9 +167,9 @@ const RoomSubList = createReactClass({ break; } } - }, + }; - onKeyDown: function(ev) { + onKeyDown = (ev) => { switch (ev.key) { // On ARROW_LEFT go to the sublist header case Key.ARROW_LEFT: @@ -180,24 +180,24 @@ const RoomSubList = createReactClass({ case Key.ARROW_RIGHT: ev.stopPropagation(); } - }, + }; - onRoomTileClick(roomId, ev) { + onRoomTileClick = (roomId, ev) => { dis.dispatch({ action: 'view_room', room_id: roomId, clear_search: (ev && (ev.keyCode === KeyCode.ENTER || ev.keyCode === KeyCode.SPACE)), }); - }, + }; - _updateSubListCount: function() { + _updateSubListCount = () => { // Force an update by setting the state to the current state // Doing it this way rather than using forceUpdate(), so that the shouldComponentUpdate() // method is honoured this.setState(this.state); - }, + }; - makeRoomTile: function(room) { + makeRoomTile = (room) => { return ; - }, + }; - _onNotifBadgeClick: function(e) { + _onNotifBadgeClick = (e) => { // prevent the roomsublist collapsing e.preventDefault(); e.stopPropagation(); @@ -225,9 +225,9 @@ const RoomSubList = createReactClass({ room_id: room.roomId, }); } - }, + }; - _onInviteBadgeClick: function(e) { + _onInviteBadgeClick = (e) => { // prevent the roomsublist collapsing e.preventDefault(); e.stopPropagation(); @@ -247,14 +247,14 @@ const RoomSubList = createReactClass({ }); } } - }, + }; - onAddRoom: function(e) { + onAddRoom = (e) => { e.stopPropagation(); if (this.props.onAddRoom) this.props.onAddRoom(); - }, + }; - _getHeaderJsx: function(isCollapsed) { + _getHeaderJsx(isCollapsed) { const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); const AccessibleTooltipButton = sdk.getComponent('elements.AccessibleTooltipButton'); const subListNotifications = !this.props.isInvite ? @@ -328,7 +328,7 @@ const RoomSubList = createReactClass({ } return ( -
    +
    ); - }, + } - checkOverflow: function() { - if (this.refs.scroller) { - this.refs.scroller.checkOverflow(); + checkOverflow = () => { + if (this._scroller.current) { + this._scroller.current.checkOverflow(); } - }, + }; - setHeight: function(height) { - if (this.refs.subList) { - this.refs.subList.style.height = `${height}px`; + setHeight = (height) => { + if (this._subList.current) { + this._subList.current.style.height = `${height}px`; } this._updateLazyRenderHeight(height); - }, + }; - _updateLazyRenderHeight: function(height) { + _updateLazyRenderHeight(height) { this.setState({scrollerHeight: height}); - }, + } - _onScroll: function() { - this.setState({scrollTop: this.refs.scroller.getScrollTop()}); - }, + _onScroll = () => { + this.setState({scrollTop: this._scroller.current.getScrollTop()}); + }; _canUseLazyListRendering() { // for now disable lazy rendering as they are already rendered tiles // not rooms like props.list we pass to LazyRenderList return !this.props.extraTiles || !this.props.extraTiles.length; - }, + } - render: function() { + render() { const len = this.props.list.length + this.props.extraTiles.length; const isCollapsed = this.state.hidden && !this.props.forceExpand; @@ -391,7 +391,7 @@ const RoomSubList = createReactClass({ // no body } else if (this._canUseLazyListRendering()) { content = ( - + this.makeRoomTile(r)); const tiles = roomTiles.concat(this.props.extraTiles); content = ( - + { tiles } ); @@ -418,7 +418,7 @@ const RoomSubList = createReactClass({ return (
    ); - }, -}); - -module.exports = RoomSubList; + } +} diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 5cc1e2b765..b2c39a1225 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -23,7 +23,7 @@ limitations under the License. import shouldHideEvent from '../../shouldHideEvent'; -import React from 'react'; +import React, {createRef} from 'react'; import createReactClass from 'create-react-class'; import ReactDOM from 'react-dom'; import PropTypes from 'prop-types'; @@ -207,6 +207,9 @@ module.exports = createReactClass({ this._onCiderUpdated(); this._ciderWatcherRef = SettingsStore.watchSetting( "useCiderComposer", null, this._onCiderUpdated); + + this._roomView = createRef(); + this._searchResultsPanel = createRef(); }, _onCiderUpdated: function() { @@ -459,8 +462,8 @@ module.exports = createReactClass({ }, componentDidUpdate: function() { - if (this.refs.roomView) { - const roomView = ReactDOM.findDOMNode(this.refs.roomView); + if (this._roomView.current) { + const roomView = ReactDOM.findDOMNode(this._roomView.current); if (!roomView.ondrop) { roomView.addEventListener('drop', this.onDrop); roomView.addEventListener('dragover', this.onDragOver); @@ -474,10 +477,10 @@ module.exports = createReactClass({ // in render() prevents the ref from being set on first mount, so we try and // catch the messagePanel when it does mount. Because we only want the ref once, // we use a boolean flag to avoid duplicate work. - if (this.refs.messagePanel && !this.state.atEndOfLiveTimelineInit) { + if (this._messagePanel && !this.state.atEndOfLiveTimelineInit) { this.setState({ atEndOfLiveTimelineInit: true, - atEndOfLiveTimeline: this.refs.messagePanel.isAtEndOfLiveTimeline(), + atEndOfLiveTimeline: this._messagePanel.isAtEndOfLiveTimeline(), }); } }, @@ -499,12 +502,12 @@ module.exports = createReactClass({ // stop tracking room changes to format permalinks this._stopAllPermalinkCreators(); - if (this.refs.roomView) { + if (this._roomView.current) { // disconnect the D&D event listeners from the room view. This // is really just for hygiene - we're going to be // deleted anyway, so it doesn't matter if the event listeners // don't get cleaned up. - const roomView = ReactDOM.findDOMNode(this.refs.roomView); + const roomView = ReactDOM.findDOMNode(this._roomView.current); roomView.removeEventListener('drop', this.onDrop); roomView.removeEventListener('dragover', this.onDragOver); roomView.removeEventListener('dragleave', this.onDragLeaveOrEnd); @@ -701,10 +704,10 @@ module.exports = createReactClass({ }, canResetTimeline: function() { - if (!this.refs.messagePanel) { + if (!this._messagePanel) { return true; } - return this.refs.messagePanel.canResetTimeline(); + return this._messagePanel.canResetTimeline(); }, // called when state.room is first initialised (either at initial load, @@ -1046,7 +1049,7 @@ module.exports = createReactClass({ }, onMessageListScroll: function(ev) { - if (this.refs.messagePanel.isAtEndOfLiveTimeline()) { + if (this._messagePanel.isAtEndOfLiveTimeline()) { this.setState({ numUnreadMessages: 0, atEndOfLiveTimeline: true, @@ -1119,8 +1122,8 @@ module.exports = createReactClass({ // if we already have a search panel, we need to tell it to forget // about its scroll state. - if (this.refs.searchResultsPanel) { - this.refs.searchResultsPanel.resetScrollState(); + if (this._searchResultsPanel.current) { + this._searchResultsPanel.current.resetScrollState(); } // make sure that we don't end up showing results from @@ -1225,7 +1228,7 @@ module.exports = createReactClass({ // once dynamic content in the search results load, make the scrollPanel check // the scroll offsets. const onHeightChanged = () => { - const scrollPanel = this.refs.searchResultsPanel; + const scrollPanel = this._searchResultsPanel.current; if (scrollPanel) { scrollPanel.checkScroll(); } @@ -1370,28 +1373,28 @@ module.exports = createReactClass({ // jump down to the bottom of this room, where new events are arriving jumpToLiveTimeline: function() { - this.refs.messagePanel.jumpToLiveTimeline(); + this._messagePanel.jumpToLiveTimeline(); dis.dispatch({action: 'focus_composer'}); }, // jump up to wherever our read marker is jumpToReadMarker: function() { - this.refs.messagePanel.jumpToReadMarker(); + this._messagePanel.jumpToReadMarker(); }, // update the read marker to match the read-receipt forgetReadMarker: function(ev) { ev.stopPropagation(); - this.refs.messagePanel.forgetReadMarker(); + this._messagePanel.forgetReadMarker(); }, // decide whether or not the top 'unread messages' bar should be shown _updateTopUnreadMessagesBar: function() { - if (!this.refs.messagePanel) { + if (!this._messagePanel) { return; } - const showBar = this.refs.messagePanel.canJumpToReadMarker(); + const showBar = this._messagePanel.canJumpToReadMarker(); if (this.state.showTopUnreadMessagesBar != showBar) { this.setState({showTopUnreadMessagesBar: showBar}); } @@ -1401,7 +1404,7 @@ module.exports = createReactClass({ // restored when we switch back to it. // _getScrollState: function() { - const messagePanel = this.refs.messagePanel; + const messagePanel = this._messagePanel; if (!messagePanel) return null; // if we're following the live timeline, we want to return null; that @@ -1506,10 +1509,10 @@ module.exports = createReactClass({ */ handleScrollKey: function(ev) { let panel; - if (this.refs.searchResultsPanel) { - panel = this.refs.searchResultsPanel; - } else if (this.refs.messagePanel) { - panel = this.refs.messagePanel; + if (this._searchResultsPanel.current) { + panel = this._searchResultsPanel.current; + } else if (this._messagePanel) { + panel = this._messagePanel; } if (panel) { @@ -1530,7 +1533,7 @@ module.exports = createReactClass({ // this has to be a proper method rather than an unnamed function, // otherwise react calls it with null on each update. _gatherTimelinePanelRef: function(r) { - this.refs.messagePanel = r; + this._messagePanel = r; if (r) { console.log("updateTint from RoomView._gatherTimelinePanelRef"); this.updateTint(); @@ -1719,7 +1722,7 @@ module.exports = createReactClass({ aux = ; } else if (this.state.searching) { hideCancel = true; // has own cancel - aux = ; + aux = ; } else if (showRoomUpgradeBar) { aux = ; hideCancel = true; @@ -1775,7 +1778,7 @@ module.exports = createReactClass({ } const auxPanel = ( - ); } else { searchResultsPanel = ( - +
    - = 0; --i) { @@ -756,7 +758,7 @@ module.exports = createReactClass({ }, _getMessagesHeight() { - const itemlist = this.refs.itemlist; + const itemlist = this._itemlist.current; const lastNode = itemlist.lastElementChild; const lastNodeBottom = lastNode ? lastNode.offsetTop + lastNode.clientHeight : 0; const firstNodeTop = itemlist.firstElementChild ? itemlist.firstElementChild.offsetTop : 0; @@ -765,7 +767,7 @@ module.exports = createReactClass({ }, _topFromBottom(node) { - return this.refs.itemlist.clientHeight - node.offsetTop; + return this._itemlist.current.clientHeight - node.offsetTop; }, /* get the DOM node which has the scrollTop property we care about for our @@ -797,7 +799,7 @@ module.exports = createReactClass({ the same minimum bottom offset, effectively preventing the timeline to shrink. */ preventShrinking: function() { - const messageList = this.refs.itemlist; + const messageList = this._itemlist.current; const tiles = messageList && messageList.children; if (!messageList) { return; @@ -824,7 +826,7 @@ module.exports = createReactClass({ /** Clear shrinking prevention. Used internally, and when the timeline is reloaded. */ clearPreventShrinking: function() { - const messageList = this.refs.itemlist; + const messageList = this._itemlist.current; const balanceElement = messageList && messageList.parentElement; if (balanceElement) balanceElement.style.paddingBottom = null; this.preventShrinkingState = null; @@ -843,7 +845,7 @@ module.exports = createReactClass({ if (this.preventShrinkingState) { const sn = this._getScrollNode(); const scrollState = this.scrollState; - const messageList = this.refs.itemlist; + 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; @@ -879,7 +881,7 @@ module.exports = createReactClass({ onScroll={this.onScroll} className={`mx_ScrollPanel ${this.props.className}`} style={this.props.style}>
    -
      +
        { this.props.children }
    diff --git a/src/components/structures/SearchBox.js b/src/components/structures/SearchBox.js index 21613733db..0aa2e15f4c 100644 --- a/src/components/structures/SearchBox.js +++ b/src/components/structures/SearchBox.js @@ -15,7 +15,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; +import React, {createRef} from 'react'; import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; import { KeyCode } from '../../Keyboard'; @@ -53,6 +53,10 @@ module.exports = createReactClass({ }; }, + UNSAFE_componentWillMount: function() { + this._search = createRef(); + }, + componentDidMount: function() { this.dispatcherRef = dis.register(this.onAction); }, @@ -66,26 +70,26 @@ module.exports = createReactClass({ switch (payload.action) { case 'view_room': - if (this.refs.search && payload.clear_search) { + if (this._search.current && payload.clear_search) { this._clearSearch(); } break; case 'focus_room_filter': - if (this.refs.search) { - this.refs.search.focus(); + if (this._search.current) { + this._search.current.focus(); } break; } }, onChange: function() { - if (!this.refs.search) return; - this.setState({ searchTerm: this.refs.search.value }); + if (!this._search.current) return; + this.setState({ searchTerm: this._search.current.value }); this.onSearch(); }, onSearch: throttle(function() { - this.props.onSearch(this.refs.search.value); + this.props.onSearch(this._search.current.value); }, 200, {trailing: true, leading: true}), _onKeyDown: function(ev) { @@ -113,7 +117,7 @@ module.exports = createReactClass({ }, _clearSearch: function(source) { - this.refs.search.value = ""; + this._search.current.value = ""; this.onChange(); if (this.props.onCleared) { this.props.onCleared(source); @@ -146,7 +150,7 @@ module.exports = createReactClass({ { - if (payload.event && this.refs.messagePanel) { - this.refs.messagePanel.scrollToEventIfNeeded( + if (payload.event && this._messagePanel.current) { + this._messagePanel.current.scrollToEventIfNeeded( payload.event.getId(), ); } @@ -442,9 +444,9 @@ const TimelinePanel = createReactClass({ // updates from pagination will happen when the paginate completes. if (toStartOfTimeline || !data || !data.liveEvent) return; - if (!this.refs.messagePanel) return; + if (!this._messagePanel.current) return; - if (!this.refs.messagePanel.getScrollState().stuckAtBottom) { + if (!this._messagePanel.current.getScrollState().stuckAtBottom) { // we won't load this event now, because we don't want to push any // events off the other end of the timeline. But we need to note // that we can now paginate. @@ -499,7 +501,7 @@ const TimelinePanel = createReactClass({ } this.setState(updatedState, () => { - this.refs.messagePanel.updateTimelineMinHeight(); + this._messagePanel.current.updateTimelineMinHeight(); if (callRMUpdated) { this.props.onReadMarkerUpdated(); } @@ -510,13 +512,13 @@ const TimelinePanel = createReactClass({ onRoomTimelineReset: function(room, timelineSet) { if (timelineSet !== this.props.timelineSet) return; - if (this.refs.messagePanel && this.refs.messagePanel.isAtBottom()) { + if (this._messagePanel.current && this._messagePanel.current.isAtBottom()) { this._loadTimeline(); } }, canResetTimeline: function() { - return this.refs.messagePanel && this.refs.messagePanel.isAtBottom(); + return this._messagePanel.current && this._messagePanel.current.isAtBottom(); }, onRoomRedaction: function(ev, room) { @@ -629,7 +631,7 @@ const TimelinePanel = createReactClass({ sendReadReceipt: function() { if (SettingsStore.getValue("lowBandwidth")) return; - if (!this.refs.messagePanel) return; + if (!this._messagePanel.current) return; if (!this.props.manageReadReceipts) return; // This happens on user_activity_end which is delayed, and it's // very possible have logged out within that timeframe, so check @@ -815,8 +817,8 @@ const TimelinePanel = createReactClass({ if (this._timelineWindow.canPaginate(EventTimeline.FORWARDS)) { this._loadTimeline(); } else { - if (this.refs.messagePanel) { - this.refs.messagePanel.scrollToBottom(); + if (this._messagePanel.current) { + this._messagePanel.current.scrollToBottom(); } } }, @@ -826,7 +828,7 @@ const TimelinePanel = createReactClass({ */ jumpToReadMarker: function() { if (!this.props.manageReadMarkers) return; - if (!this.refs.messagePanel) return; + if (!this._messagePanel.current) return; if (!this.state.readMarkerEventId) return; // we may not have loaded the event corresponding to the read-marker @@ -835,11 +837,11 @@ const TimelinePanel = createReactClass({ // // a quick way to figure out if we've loaded the relevant event is // simply to check if the messagepanel knows where the read-marker is. - const ret = this.refs.messagePanel.getReadMarkerPosition(); + const ret = this._messagePanel.current.getReadMarkerPosition(); if (ret !== null) { // The messagepanel knows where the RM is, so we must have loaded // the relevant event. - this.refs.messagePanel.scrollToEvent(this.state.readMarkerEventId, + this._messagePanel.current.scrollToEvent(this.state.readMarkerEventId, 0, 1/3); return; } @@ -874,8 +876,8 @@ const TimelinePanel = createReactClass({ * at the end of the live timeline. */ isAtEndOfLiveTimeline: function() { - return this.refs.messagePanel - && this.refs.messagePanel.isAtBottom() + return this._messagePanel.current + && this._messagePanel.current.isAtBottom() && this._timelineWindow && !this._timelineWindow.canPaginate(EventTimeline.FORWARDS); }, @@ -887,8 +889,8 @@ const TimelinePanel = createReactClass({ * returns null if we are not mounted. */ getScrollState: function() { - if (!this.refs.messagePanel) { return null; } - return this.refs.messagePanel.getScrollState(); + if (!this._messagePanel.current) { return null; } + return this._messagePanel.current.getScrollState(); }, // returns one of: @@ -899,9 +901,9 @@ const TimelinePanel = createReactClass({ // +1: read marker is below the window getReadMarkerPosition: function() { if (!this.props.manageReadMarkers) return null; - if (!this.refs.messagePanel) return null; + if (!this._messagePanel.current) return null; - const ret = this.refs.messagePanel.getReadMarkerPosition(); + const ret = this._messagePanel.current.getReadMarkerPosition(); if (ret !== null) { return ret; } @@ -936,7 +938,7 @@ const TimelinePanel = createReactClass({ * We pass it down to the scroll panel. */ handleScrollKey: function(ev) { - if (!this.refs.messagePanel) { return; } + if (!this._messagePanel.current) { return; } // jump to the live timeline on ctrl-end, rather than the end of the // timeline window. @@ -944,7 +946,7 @@ const TimelinePanel = createReactClass({ ev.keyCode == KeyCode.END) { this.jumpToLiveTimeline(); } else { - this.refs.messagePanel.handleScrollKey(ev); + this._messagePanel.current.handleScrollKey(ev); } }, @@ -986,8 +988,8 @@ const TimelinePanel = createReactClass({ const onLoaded = () => { // clear the timeline min-height when // (re)loading the timeline - if (this.refs.messagePanel) { - this.refs.messagePanel.onTimelineReset(); + if (this._messagePanel.current) { + this._messagePanel.current.onTimelineReset(); } this._reloadEvents(); @@ -1002,7 +1004,7 @@ const TimelinePanel = createReactClass({ timelineLoading: false, }, () => { // initialise the scroll state of the message panel - if (!this.refs.messagePanel) { + if (!this._messagePanel.current) { // this shouldn't happen - we know we're mounted because // we're in a setState callback, and we know // timelineLoading is now false, so render() should have @@ -1012,10 +1014,10 @@ const TimelinePanel = createReactClass({ return; } if (eventId) { - this.refs.messagePanel.scrollToEvent(eventId, pixelOffset, + this._messagePanel.current.scrollToEvent(eventId, pixelOffset, offsetBase); } else { - this.refs.messagePanel.scrollToBottom(); + this._messagePanel.current.scrollToBottom(); } this.sendReadReceipt(); @@ -1134,7 +1136,7 @@ const TimelinePanel = createReactClass({ const ignoreOwn = opts.ignoreOwn || false; const allowPartial = opts.allowPartial || false; - const messagePanel = this.refs.messagePanel; + const messagePanel = this._messagePanel.current; if (messagePanel === undefined) return null; const EventTile = sdk.getComponent('rooms.EventTile'); @@ -1313,7 +1315,8 @@ const TimelinePanel = createReactClass({ ['PREPARED', 'CATCHUP'].includes(this.state.clientSyncState) ); return ( -