From d282675bc643058c4835db499e68d7b34af0e13f Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sun, 13 Oct 2019 15:08:50 +0300 Subject: [PATCH 001/809] Improve reply rendering Signed-off-by: Tulir Asokan --- res/css/_components.scss | 1 + res/css/views/elements/_ReplyThread.scss | 11 +- res/css/views/rooms/_ReplyPreview.scss | 7 +- res/css/views/rooms/_ReplyTile.scss | 96 +++++++ src/components/views/elements/ReplyThread.js | 15 +- .../views/messages/MImageReplyBody.js | 33 +++ src/components/views/messages/MessageEvent.js | 9 +- src/components/views/rooms/ReplyPreview.js | 12 +- src/components/views/rooms/ReplyTile.js | 234 ++++++++++++++++++ 9 files changed, 388 insertions(+), 30 deletions(-) create mode 100644 res/css/views/rooms/_ReplyTile.scss create mode 100644 src/components/views/messages/MImageReplyBody.js create mode 100644 src/components/views/rooms/ReplyTile.js diff --git a/res/css/_components.scss b/res/css/_components.scss index 4891fd90c0..2c54c5f37f 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -153,6 +153,7 @@ @import "./views/rooms/_PinnedEventsPanel.scss"; @import "./views/rooms/_PresenceLabel.scss"; @import "./views/rooms/_ReplyPreview.scss"; +@import "./views/rooms/_ReplyTile.scss"; @import "./views/rooms/_RoomBreadcrumbs.scss"; @import "./views/rooms/_RoomDropTarget.scss"; @import "./views/rooms/_RoomHeader.scss"; diff --git a/res/css/views/elements/_ReplyThread.scss b/res/css/views/elements/_ReplyThread.scss index bf44a11728..0d53a6b6f4 100644 --- a/res/css/views/elements/_ReplyThread.scss +++ b/res/css/views/elements/_ReplyThread.scss @@ -18,20 +18,13 @@ limitations under the License. margin-top: 0; } -.mx_ReplyThread .mx_DateSeparator { - font-size: 1em !important; - margin-top: 0; - margin-bottom: 0; - padding-bottom: 1px; - bottom: -5px; -} - .mx_ReplyThread_show { cursor: pointer; } blockquote.mx_ReplyThread { margin-left: 0; + margin-bottom: 8px; padding-left: 10px; - border-left: 4px solid $blockquote-bar-color; + border-left: 4px solid $button-bg-color; } diff --git a/res/css/views/rooms/_ReplyPreview.scss b/res/css/views/rooms/_ReplyPreview.scss index 4dc4cb2c40..08fbd27808 100644 --- a/res/css/views/rooms/_ReplyPreview.scss +++ b/res/css/views/rooms/_ReplyPreview.scss @@ -32,12 +32,16 @@ limitations under the License. } .mx_ReplyPreview_header { - margin: 12px; + margin: 8px; color: $primary-fg-color; font-weight: 400; opacity: 0.4; } +.mx_ReplyPreview_tile { + margin: 0 8px; +} + .mx_ReplyPreview_title { float: left; } @@ -45,6 +49,7 @@ limitations under the License. .mx_ReplyPreview_cancel { float: right; cursor: pointer; + display: flex; } .mx_ReplyPreview_clear { diff --git a/res/css/views/rooms/_ReplyTile.scss b/res/css/views/rooms/_ReplyTile.scss new file mode 100644 index 0000000000..0a055297c6 --- /dev/null +++ b/res/css/views/rooms/_ReplyTile.scss @@ -0,0 +1,96 @@ +/* +Copyright 2019 Tulir Asokan + +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_ReplyTile { + max-width: 100%; + clear: both; + padding-top: 2px; + padding-bottom: 2px; + font-size: 14px; + position: relative; + line-height: 16px; +} + +.mx_ReplyTile > a { + display: block; + text-decoration: none; + color: $primary-fg-color; +} + +// We do reply size limiting with CSS to avoid duplicating the TextualBody component. +.mx_ReplyTile .mx_EventTile_content { + $reply-lines: 2; + $line-height: 22px; + + text-overflow: ellipsis; + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: $reply-lines; + line-height: $line-height; + + .mx_EventTile_body.mx_EventTile_bigEmoji { + line-height: $line-height !important; + // Override the big emoji override + font-size: 14px !important; + } +} + +.mx_ReplyTile.mx_ReplyTile_info { + padding-top: 0px; +} + +.mx_ReplyTile .mx_SenderProfile { + color: $primary-fg-color; + font-size: 14px; + display: inline-block; /* anti-zalgo, with overflow hidden */ + overflow: hidden; + cursor: pointer; + padding-left: 0px; /* left gutter */ + padding-bottom: 0px; + padding-top: 0px; + margin: 0px; + line-height: 17px; + /* the next three lines, along with overflow hidden, truncate long display names */ + white-space: nowrap; + text-overflow: ellipsis; + max-width: calc(100% - 65px); +} + +.mx_ReplyTile_redacted .mx_UnknownBody { + --lozenge-color: $event-redacted-fg-color; + --lozenge-border-color: $event-redacted-border-color; + display: block; + height: 22px; + width: 250px; + border-radius: 11px; + background: + repeating-linear-gradient( + -45deg, + var(--lozenge-color), + var(--lozenge-color) 3px, + transparent 3px, + transparent 6px + ); + box-shadow: 0px 0px 3px var(--lozenge-border-color) inset; +} + +.mx_ReplyTile_sending.mx_ReplyTile_redacted .mx_UnknownBody { + opacity: 0.4; +} + +.mx_ReplyTile_contextual { + opacity: 0.4; +} diff --git a/src/components/views/elements/ReplyThread.js b/src/components/views/elements/ReplyThread.js index fac0a71617..1764c008fa 100644 --- a/src/components/views/elements/ReplyThread.js +++ b/src/components/views/elements/ReplyThread.js @@ -304,20 +304,11 @@ export default class ReplyThread extends React.Component { header = ; } - const EventTile = sdk.getComponent('views.rooms.EventTile'); - const DateSeparator = sdk.getComponent('messages.DateSeparator'); + const ReplyTile = sdk.getComponent('views.rooms.ReplyTile'); const evTiles = this.state.events.map((ev) => { - let dateSep = null; - - if (wantsDateSeparator(this.props.parentEv.getDate(), ev.getDate())) { - dateSep = ; - } - return
- { dateSep } - ; }); - return
+ return
{ header }
{ evTiles }
; diff --git a/src/components/views/messages/MImageReplyBody.js b/src/components/views/messages/MImageReplyBody.js new file mode 100644 index 0000000000..bb869919fc --- /dev/null +++ b/src/components/views/messages/MImageReplyBody.js @@ -0,0 +1,33 @@ +/* +Copyright 2019 Tulir Asokan + +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 MImageBody from './MImageBody'; + +export default class MImageReplyBody extends MImageBody { + onClick(ev) { + ev.preventDefault(); + } + + wrapImage(contentUrl, children) { + return children; + } + + // Don't show "Download this_file.png ..." + getFileBody() { + return null; + } +} diff --git a/src/components/views/messages/MessageEvent.js b/src/components/views/messages/MessageEvent.js index a616dd96ed..28f2a471bb 100644 --- a/src/components/views/messages/MessageEvent.js +++ b/src/components/views/messages/MessageEvent.js @@ -43,6 +43,9 @@ module.exports = createReactClass({ /* the maximum image height to use, if the event is an image */ maxImageHeight: PropTypes.number, + + overrideBodyTypes: PropTypes.object, + overrideEventTypes: PropTypes.object, }, getEventTileOps: function() { @@ -60,9 +63,11 @@ module.exports = createReactClass({ 'm.file': sdk.getComponent('messages.MFileBody'), 'm.audio': sdk.getComponent('messages.MAudioBody'), 'm.video': sdk.getComponent('messages.MVideoBody'), + ...(this.props.overrideBodyTypes || {}), }; const evTypes = { 'm.sticker': sdk.getComponent('messages.MStickerBody'), + ...(this.props.overrideEventTypes || {}), }; const content = this.props.mxEvent.getContent(); @@ -81,7 +86,7 @@ module.exports = createReactClass({ } } - return ; + onHeightChanged={this.props.onHeightChanged} /> : null; }, }); diff --git a/src/components/views/rooms/ReplyPreview.js b/src/components/views/rooms/ReplyPreview.js index caf8feeea2..a69a286a15 100644 --- a/src/components/views/rooms/ReplyPreview.js +++ b/src/components/views/rooms/ReplyPreview.js @@ -68,7 +68,7 @@ export default class ReplyPreview extends React.Component { render() { if (!this.state.event) return null; - const EventTile = sdk.getComponent('rooms.EventTile'); + const ReplyTile = sdk.getComponent('rooms.ReplyTile'); return
@@ -80,11 +80,11 @@ export default class ReplyPreview extends React.Component { onClick={cancelQuoting} />
- +
+ +
; } diff --git a/src/components/views/rooms/ReplyTile.js b/src/components/views/rooms/ReplyTile.js new file mode 100644 index 0000000000..5a56ba9dc1 --- /dev/null +++ b/src/components/views/rooms/ReplyTile.js @@ -0,0 +1,234 @@ +/* +Copyright 2019 Tulir Asokan + +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 PropTypes from 'prop-types'; +const classNames = require("classnames"); +import { _t, _td } from '../../../languageHandler'; + +const sdk = require('../../../index'); + +import dis from '../../../dispatcher'; +import SettingsStore from "../../../settings/SettingsStore"; +import {MatrixClient} from 'matrix-js-sdk'; + +const ObjectUtils = require('../../../ObjectUtils'); + +const eventTileTypes = { + 'm.room.message': 'messages.MessageEvent', + 'm.sticker': 'messages.MessageEvent', + 'm.call.invite': 'messages.TextualEvent', + 'm.call.answer': 'messages.TextualEvent', + 'm.call.hangup': 'messages.TextualEvent', +}; + +const stateEventTileTypes = { + 'm.room.aliases': 'messages.TextualEvent', + // 'm.room.aliases': 'messages.RoomAliasesEvent', // too complex + 'm.room.canonical_alias': 'messages.TextualEvent', + 'm.room.create': 'messages.RoomCreate', + 'm.room.member': 'messages.TextualEvent', + 'm.room.name': 'messages.TextualEvent', + 'm.room.avatar': 'messages.RoomAvatarEvent', + 'm.room.third_party_invite': 'messages.TextualEvent', + 'm.room.history_visibility': 'messages.TextualEvent', + 'm.room.encryption': 'messages.TextualEvent', + 'm.room.topic': 'messages.TextualEvent', + 'm.room.power_levels': 'messages.TextualEvent', + 'm.room.pinned_events': 'messages.TextualEvent', + 'm.room.server_acl': 'messages.TextualEvent', + 'im.vector.modular.widgets': 'messages.TextualEvent', + 'm.room.tombstone': 'messages.TextualEvent', + 'm.room.join_rules': 'messages.TextualEvent', + 'm.room.guest_access': 'messages.TextualEvent', + 'm.room.related_groups': 'messages.TextualEvent', +}; + +function getHandlerTile(ev) { + const type = ev.getType(); + return ev.isState() ? stateEventTileTypes[type] : eventTileTypes[type]; +} + +class ReplyTile extends React.Component { + static contextTypes = { + matrixClient: PropTypes.instanceOf(MatrixClient).isRequired, + } + + static propTypes = { + mxEvent: PropTypes.object.isRequired, + isRedacted: PropTypes.bool, + permalinkCreator: PropTypes.object, + onHeightChanged: PropTypes.func, + } + + static defaultProps = { + onHeightChanged: function() {}, + } + + constructor(props, context) { + super(props, context); + this.state = {}; + this.onClick = this.onClick.bind(this); + } + + componentDidMount() { + this.props.mxEvent.on("Event.decrypted", this._onDecrypted); + } + + shouldComponentUpdate(nextProps, nextState) { + if (!ObjectUtils.shallowEqual(this.state, nextState)) { + return true; + } + + return !this._propsEqual(this.props, nextProps); + } + + componentWillUnmount() { + const client = this.context.matrixClient; + this.props.mxEvent.removeListener("Event.decrypted", this._onDecrypted); + } + + _onDecrypted() { + this.forceUpdate(); + } + + _propsEqual(objA, objB) { + const keysA = Object.keys(objA); + const keysB = Object.keys(objB); + + if (keysA.length !== keysB.length) { + return false; + } + + for (let i = 0; i < keysA.length; i++) { + const key = keysA[i]; + + if (!objB.hasOwnProperty(key)) { + return false; + } + + if (objA[key] !== objB[key]) { + return false; + } + } + return true; + } + + onClick(e) { + // This allows the permalink to be opened in a new tab/window or copied as + // matrix.to, but also for it to enable routing within Riot when clicked. + e.preventDefault(); + dis.dispatch({ + action: 'view_room', + event_id: this.props.mxEvent.getId(), + highlighted: true, + room_id: this.props.mxEvent.getRoomId(), + }); + } + + render() { + const SenderProfile = sdk.getComponent('messages.SenderProfile'); + + const content = this.props.mxEvent.getContent(); + const msgtype = content.msgtype; + const eventType = this.props.mxEvent.getType(); + + // Info messages are basically information about commands processed on a room + let isInfoMessage = ( + eventType !== 'm.room.message' && eventType !== 'm.sticker' && eventType !== 'm.room.create' + ); + + let tileHandler = getHandlerTile(this.props.mxEvent); + // If we're showing hidden events in the timeline, we should use the + // source tile when there's no regular tile for an event and also for + // replace relations (which otherwise would display as a confusing + // duplicate of the thing they are replacing). + const useSource = !tileHandler || this.props.mxEvent.isRelation("m.replace"); + if (useSource && SettingsStore.getValue("showHiddenEventsInTimeline")) { + tileHandler = "messages.ViewSourceEvent"; + // Reuse info message avatar and sender profile styling + isInfoMessage = true; + } + // This shouldn't happen: the caller should check we support this type + // before trying to instantiate us + if (!tileHandler) { + const {mxEvent} = this.props; + console.warn(`Event type not supported: type:${mxEvent.getType()} isState:${mxEvent.isState()}`); + return
+ { _t('This event could not be displayed') } +
; + } + const EventTileType = sdk.getComponent(tileHandler); + + const classes = classNames({ + mx_ReplyTile: true, + mx_ReplyTile_info: isInfoMessage, + mx_ReplyTile_redacted: this.props.isRedacted, + }); + + let permalink = "#"; + if (this.props.permalinkCreator) { + permalink = this.props.permalinkCreator.forEvent(this.props.mxEvent.getId()); + } + + let sender; + let needsSenderProfile = tileHandler !== 'messages.RoomCreate' && !isInfoMessage; + + if (needsSenderProfile) { + let text = null; + if (msgtype === 'm.image') text = _td('%(senderName)s sent an image'); + else if (msgtype === 'm.video') text = _td('%(senderName)s sent a video'); + else if (msgtype === 'm.file') text = _td('%(senderName)s uploaded a file'); + sender = ; + } + + const MImageReplyBody = sdk.getComponent('messages.MImageReplyBody'); + const TextualBody = sdk.getComponent('messages.TextualBody'); + const msgtypeOverrides = { + "m.image": MImageReplyBody, + // We don't want a download link for files, just the file name is enough. + "m.file": TextualBody, + "m.sticker": TextualBody, + "m.audio": TextualBody, + "m.video": TextualBody, + }; + const evOverrides = { + "m.sticker": TextualBody, + }; + + return ( + + ) + } +} + +module.exports = ReplyTile; From 03d36f30ec1093dab132c8c2bbe9414da00cb9b2 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Thu, 5 Mar 2020 13:44:54 +0200 Subject: [PATCH 002/809] Fix lint errors --- src/components/views/elements/ReplyThread.js | 1 - src/components/views/messages/MImageReplyBody.js | 1 - src/components/views/messages/MessageEvent.js | 2 +- src/components/views/rooms/ReplyPreview.js | 1 - src/components/views/rooms/ReplyTile.js | 7 +++---- 5 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/components/views/elements/ReplyThread.js b/src/components/views/elements/ReplyThread.js index 25b39a2ad4..954c6b49c4 100644 --- a/src/components/views/elements/ReplyThread.js +++ b/src/components/views/elements/ReplyThread.js @@ -20,7 +20,6 @@ import * as sdk from '../../../index'; import {_t} from '../../../languageHandler'; import PropTypes from 'prop-types'; import dis from '../../../dispatcher'; -import {wantsDateSeparator} from '../../../DateUtils'; import {MatrixEvent} from 'matrix-js-sdk'; import {makeUserPermalink, RoomPermalinkCreator} from "../../../utils/permalinks/Permalinks"; import SettingsStore from "../../../settings/SettingsStore"; diff --git a/src/components/views/messages/MImageReplyBody.js b/src/components/views/messages/MImageReplyBody.js index bb869919fc..31b4d1fa82 100644 --- a/src/components/views/messages/MImageReplyBody.js +++ b/src/components/views/messages/MImageReplyBody.js @@ -14,7 +14,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; import MImageBody from './MImageBody'; export default class MImageReplyBody extends MImageBody { diff --git a/src/components/views/messages/MessageEvent.js b/src/components/views/messages/MessageEvent.js index 14ab3c8757..3703d3a629 100644 --- a/src/components/views/messages/MessageEvent.js +++ b/src/components/views/messages/MessageEvent.js @@ -123,6 +123,6 @@ export default createReactClass({ editState={this.props.editState} onHeightChanged={this.props.onHeightChanged} onMessageAllowed={this.onTileUpdate} - /> : null + /> : null; }, }); diff --git a/src/components/views/rooms/ReplyPreview.js b/src/components/views/rooms/ReplyPreview.js index 92e3f123a0..a22a85a2f1 100644 --- a/src/components/views/rooms/ReplyPreview.js +++ b/src/components/views/rooms/ReplyPreview.js @@ -19,7 +19,6 @@ import dis from '../../../dispatcher'; import * as sdk from '../../../index'; import { _t } from '../../../languageHandler'; import RoomViewStore from '../../../stores/RoomViewStore'; -import SettingsStore from "../../../settings/SettingsStore"; import PropTypes from "prop-types"; import {RoomPermalinkCreator} from "../../../utils/permalinks/Permalinks"; diff --git a/src/components/views/rooms/ReplyTile.js b/src/components/views/rooms/ReplyTile.js index 5a56ba9dc1..36cb07f092 100644 --- a/src/components/views/rooms/ReplyTile.js +++ b/src/components/views/rooms/ReplyTile.js @@ -97,7 +97,6 @@ class ReplyTile extends React.Component { } componentWillUnmount() { - const client = this.context.matrixClient; this.props.mxEvent.removeListener("Event.decrypted", this._onDecrypted); } @@ -185,7 +184,7 @@ class ReplyTile extends React.Component { } let sender; - let needsSenderProfile = tileHandler !== 'messages.RoomCreate' && !isInfoMessage; + const needsSenderProfile = tileHandler !== 'messages.RoomCreate' && !isInfoMessage; if (needsSenderProfile) { let text = null; @@ -224,10 +223,10 @@ class ReplyTile extends React.Component { showUrlPreview={false} overrideBodyTypes={msgtypeOverrides} overrideEventTypes={evOverrides} - maxImageHeight={96}/> + maxImageHeight={96} />
- ) + ); } } From 03299a28a4f27e96a5b9b0351945b3b9c3c5218d Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Fri, 10 Apr 2020 14:23:34 +0300 Subject: [PATCH 003/809] Fix import/export things --- src/components/views/rooms/ReplyTile.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/views/rooms/ReplyTile.js b/src/components/views/rooms/ReplyTile.js index 36cb07f092..3ad6962f1a 100644 --- a/src/components/views/rooms/ReplyTile.js +++ b/src/components/views/rooms/ReplyTile.js @@ -1,5 +1,5 @@ /* -Copyright 2019 Tulir Asokan +Copyright 2020 Tulir Asokan Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -16,16 +16,16 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; -const classNames = require("classnames"); +import classNames from 'classnames'; import { _t, _td } from '../../../languageHandler'; -const sdk = require('../../../index'); +import * as sdk from '../../../index'; import dis from '../../../dispatcher'; import SettingsStore from "../../../settings/SettingsStore"; import {MatrixClient} from 'matrix-js-sdk'; -const ObjectUtils = require('../../../ObjectUtils'); +import * as ObjectUtils from '../../../ObjectUtils'; const eventTileTypes = { 'm.room.message': 'messages.MessageEvent', @@ -230,4 +230,4 @@ class ReplyTile extends React.Component { } } -module.exports = ReplyTile; +export default ReplyTile; From e64ff0f099ac6660e596fc640a839c9f76f9f79b Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Fri, 10 Apr 2020 14:39:16 +0300 Subject: [PATCH 004/809] Change score color to match sender name --- res/css/views/elements/_ReplyThread.scss | 32 ++++++++++++++++++++ src/components/views/elements/ReplyThread.js | 9 ++++-- 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/res/css/views/elements/_ReplyThread.scss b/res/css/views/elements/_ReplyThread.scss index 0d53a6b6f4..d5388e4631 100644 --- a/res/css/views/elements/_ReplyThread.scss +++ b/res/css/views/elements/_ReplyThread.scss @@ -27,4 +27,36 @@ blockquote.mx_ReplyThread { margin-bottom: 8px; padding-left: 10px; border-left: 4px solid $button-bg-color; + + &.mx_ReplyThread_color1 { + border-left-color: $username-variant1-color; + } + + &.mx_ReplyThread_color2 { + border-left-color: $username-variant2-color; + } + + &.mx_ReplyThread_color3 { + border-left-color: $username-variant3-color; + } + + &.mx_ReplyThread_color4 { + border-left-color: $username-variant4-color; + } + + &.mx_ReplyThread_color5 { + border-left-color: $username-variant5-color; + } + + &.mx_ReplyThread_color6 { + border-left-color: $username-variant6-color; + } + + &.mx_ReplyThread_color7 { + border-left-color: $username-variant7-color; + } + + &.mx_ReplyThread_color8 { + border-left-color: $username-variant8-color; + } } diff --git a/src/components/views/elements/ReplyThread.js b/src/components/views/elements/ReplyThread.js index 976b3a8815..92e87ad945 100644 --- a/src/components/views/elements/ReplyThread.js +++ b/src/components/views/elements/ReplyThread.js @@ -25,6 +25,7 @@ import {makeUserPermalink, RoomPermalinkCreator} from "../../../utils/permalinks import SettingsStore from "../../../settings/SettingsStore"; import escapeHtml from "escape-html"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; +import { getUserNameColorClass } from "../../../utils/FormattingUtils" // This component does no cycle detection, simply because the only way to make such a cycle would be to // craft event_id's, using a homeserver that generates predictable event IDs; even then the impact would @@ -285,6 +286,10 @@ export default class ReplyThread extends React.Component { dis.dispatch({action: 'focus_composer'}); } + getReplyThreadColorClass(ev) { + return getUserNameColorClass(ev.getSender()).replace("Username", "ReplyThread"); + } + render() { let header = null; @@ -299,7 +304,7 @@ export default class ReplyThread extends React.Component { const ev = this.state.loadedEv; const Pill = sdk.getComponent('elements.Pill'); const room = this.context.getRoom(ev.getRoomId()); - header =
+ header =
{ _t('In reply to ', {}, { 'a': (sub) => { sub }, @@ -315,7 +320,7 @@ export default class ReplyThread extends React.Component { const ReplyTile = sdk.getComponent('views.rooms.ReplyTile'); const evTiles = this.state.events.map((ev) => { - return
+ return
Date: Fri, 10 Apr 2020 14:52:24 +0300 Subject: [PATCH 005/809] Add missing semicolon --- src/components/views/elements/ReplyThread.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/elements/ReplyThread.js b/src/components/views/elements/ReplyThread.js index 92e87ad945..770f95f9dc 100644 --- a/src/components/views/elements/ReplyThread.js +++ b/src/components/views/elements/ReplyThread.js @@ -25,7 +25,7 @@ import {makeUserPermalink, RoomPermalinkCreator} from "../../../utils/permalinks import SettingsStore from "../../../settings/SettingsStore"; import escapeHtml from "escape-html"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; -import { getUserNameColorClass } from "../../../utils/FormattingUtils" +import { getUserNameColorClass } from "../../../utils/FormattingUtils"; // This component does no cycle detection, simply because the only way to make such a cycle would be to // craft event_id's, using a homeserver that generates predictable event IDs; even then the impact would From 25af26323c3ca9048ab7a45d63ed74116e553aa8 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Fri, 10 Apr 2020 15:45:59 +0300 Subject: [PATCH 006/809] Make image reply rendering even more compact --- res/css/_components.scss | 1 + res/css/views/messages/_MImageReplyBody.scss | 35 +++++++++++++++++++ .../views/messages/MImageReplyBody.js | 31 ++++++++++++++-- src/components/views/rooms/ReplyTile.js | 2 +- 4 files changed, 66 insertions(+), 3 deletions(-) create mode 100644 res/css/views/messages/_MImageReplyBody.scss diff --git a/res/css/_components.scss b/res/css/_components.scss index 607257400b..2d701bb1e1 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -130,6 +130,7 @@ @import "./views/messages/_MEmoteBody.scss"; @import "./views/messages/_MFileBody.scss"; @import "./views/messages/_MImageBody.scss"; +@import "./views/messages/_MImageReplyBody.scss"; @import "./views/messages/_MNoticeBody.scss"; @import "./views/messages/_MStickerBody.scss"; @import "./views/messages/_MTextBody.scss"; diff --git a/res/css/views/messages/_MImageReplyBody.scss b/res/css/views/messages/_MImageReplyBody.scss new file mode 100644 index 0000000000..8169e027d1 --- /dev/null +++ b/res/css/views/messages/_MImageReplyBody.scss @@ -0,0 +1,35 @@ +/* +Copyright 2020 Tulir Asokan + +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_MImageReplyBody { + display: grid; + grid-template: "image sender" 20px + "image filename" 20px + / 44px auto; + grid-gap: 4px; +} + +.mx_MImageReplyBody_thumbnail { + grid-area: image; +} + +.mx_MImageReplyBody_sender { + grid-area: sender; +} + +.mx_MImageReplyBody_filename { + grid-area: filename; +} diff --git a/src/components/views/messages/MImageReplyBody.js b/src/components/views/messages/MImageReplyBody.js index 31b4d1fa82..cdc78e46e8 100644 --- a/src/components/views/messages/MImageReplyBody.js +++ b/src/components/views/messages/MImageReplyBody.js @@ -1,5 +1,5 @@ /* -Copyright 2019 Tulir Asokan +Copyright 2020 Tulir Asokan Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,7 +14,11 @@ See the License for the specific language governing permissions and limitations under the License. */ +import React from "react"; +import { _td } from "../../../languageHandler"; +import * as sdk from "../../../index"; import MImageBody from './MImageBody'; +import MFileBody from "./MFileBody"; export default class MImageReplyBody extends MImageBody { onClick(ev) { @@ -27,6 +31,29 @@ export default class MImageReplyBody extends MImageBody { // Don't show "Download this_file.png ..." getFileBody() { - return null; + return MFileBody.prototype.presentableTextForFile.call(this, this.props.mxEvent.getContent()); + } + + render() { + if (this.state.error !== null) { + return super.render(); + } + + const content = this.props.mxEvent.getContent(); + + const contentUrl = this._getContentUrl(); + const thumbnail = this._messageContent(contentUrl, this._getThumbUrl(), content); + const fileBody = this.getFileBody(); + const SenderProfile = sdk.getComponent('messages.SenderProfile'); + const sender = ; + + return
+
{ thumbnail }
+
{ sender }
+
{ fileBody }
+
; } } diff --git a/src/components/views/rooms/ReplyTile.js b/src/components/views/rooms/ReplyTile.js index 3ad6962f1a..ca349baac2 100644 --- a/src/components/views/rooms/ReplyTile.js +++ b/src/components/views/rooms/ReplyTile.js @@ -184,7 +184,7 @@ class ReplyTile extends React.Component { } let sender; - const needsSenderProfile = tileHandler !== 'messages.RoomCreate' && !isInfoMessage; + const needsSenderProfile = msgtype !== 'm.image' && tileHandler !== 'messages.RoomCreate' && !isInfoMessage; if (needsSenderProfile) { let text = null; From ec7acd1c0fbaf5d96415f380a3b85b54d079aa9f Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Fri, 10 Apr 2020 15:56:09 +0300 Subject: [PATCH 007/809] Fix rendering reply after event is decrypted --- src/components/views/rooms/ReplyTile.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/components/views/rooms/ReplyTile.js b/src/components/views/rooms/ReplyTile.js index ca349baac2..34b2c6ad38 100644 --- a/src/components/views/rooms/ReplyTile.js +++ b/src/components/views/rooms/ReplyTile.js @@ -82,6 +82,7 @@ class ReplyTile extends React.Component { super(props, context); this.state = {}; this.onClick = this.onClick.bind(this); + this._onDecrypted = this._onDecrypted.bind(this); } componentDidMount() { @@ -102,6 +103,9 @@ class ReplyTile extends React.Component { _onDecrypted() { this.forceUpdate(); + if (this.props.onHeightChanged) { + this.props.onHeightChanged(); + } } _propsEqual(objA, objB) { From da3bd5ebee68dc15f04e15c3b55183f769413ce9 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Fri, 10 Apr 2020 16:03:27 +0300 Subject: [PATCH 008/809] Disable pointer events inside replies --- res/css/views/rooms/_ReplyTile.scss | 2 ++ 1 file changed, 2 insertions(+) diff --git a/res/css/views/rooms/_ReplyTile.scss b/res/css/views/rooms/_ReplyTile.scss index 0a055297c6..a6cff00ff2 100644 --- a/res/css/views/rooms/_ReplyTile.scss +++ b/res/css/views/rooms/_ReplyTile.scss @@ -35,6 +35,8 @@ limitations under the License. $reply-lines: 2; $line-height: 22px; + pointer-events: none; + text-overflow: ellipsis; display: -webkit-box; -webkit-box-orient: vertical; From 6b96a161087f4cda8ab6dcafd155e2d689a5adff Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Fri, 10 Apr 2020 16:18:06 +0300 Subject: [PATCH 009/809] Add absolute max height and some improvements for
 replies

---
 res/css/views/rooms/_ReplyTile.scss | 15 +++++++++++++++
 1 file changed, 15 insertions(+)

diff --git a/res/css/views/rooms/_ReplyTile.scss b/res/css/views/rooms/_ReplyTile.scss
index a6cff00ff2..70a383a1cf 100644
--- a/res/css/views/rooms/_ReplyTile.scss
+++ b/res/css/views/rooms/_ReplyTile.scss
@@ -34,6 +34,7 @@ limitations under the License.
 .mx_ReplyTile .mx_EventTile_content {
     $reply-lines: 2;
     $line-height: 22px;
+    $max-height: 66px;
 
     pointer-events: none;
 
@@ -42,12 +43,26 @@ limitations under the License.
     -webkit-box-orient: vertical;
     -webkit-line-clamp: $reply-lines;
     line-height: $line-height;
+    max-height: $max-height;
 
     .mx_EventTile_body.mx_EventTile_bigEmoji {
         line-height: $line-height !important;
         // Override the big emoji override
         font-size: 14px !important;
     }
+
+    // Hack to cut content in 
 tags too
+    .mx_EventTile_pre_container > pre {
+        overflow: hidden;
+        text-overflow: ellipsis;
+        display: -webkit-box;
+        -webkit-box-orient: vertical;
+        -webkit-line-clamp: $reply-lines;
+        padding: 4px;
+    }
+    .markdown-body blockquote, .markdown-body dl, .markdown-body ol, .markdown-body p, .markdown-body pre, .markdown-body table, .markdown-body ul {
+        margin-bottom: 4px;
+    }
 }
 
 .mx_ReplyTile.mx_ReplyTile_info {

From e7ad9b82e0f42e6c7ac5511ade135e71a273e414 Mon Sep 17 00:00:00 2001
From: Tulir Asokan 
Date: Fri, 10 Apr 2020 16:27:39 +0300
Subject: [PATCH 010/809] Fix stylelint issue and update header

---
 res/css/views/messages/_MImageReplyBody.scss | 7 ++++---
 res/css/views/rooms/_ReplyTile.scss          | 2 +-
 2 files changed, 5 insertions(+), 4 deletions(-)

diff --git a/res/css/views/messages/_MImageReplyBody.scss b/res/css/views/messages/_MImageReplyBody.scss
index 8169e027d1..9b25b4392a 100644
--- a/res/css/views/messages/_MImageReplyBody.scss
+++ b/res/css/views/messages/_MImageReplyBody.scss
@@ -16,9 +16,10 @@ limitations under the License.
 
 .mx_MImageReplyBody {
     display: grid;
-    grid-template: "image sender"   20px
-                   "image filename" 20px
-                  / 44px  auto;
+    grid-template:
+        "image sender"   20px
+        "image filename" 20px
+        / 44px  auto;
     grid-gap: 4px;
 }
 
diff --git a/res/css/views/rooms/_ReplyTile.scss b/res/css/views/rooms/_ReplyTile.scss
index 70a383a1cf..fd68430157 100644
--- a/res/css/views/rooms/_ReplyTile.scss
+++ b/res/css/views/rooms/_ReplyTile.scss
@@ -1,5 +1,5 @@
 /*
-Copyright 2019 Tulir Asokan 
+Copyright 2020 Tulir Asokan 
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.

From b554d59ed165d68b56a9b08faadeec86d2f7c2b7 Mon Sep 17 00:00:00 2001
From: Tulir Asokan 
Date: Fri, 10 Apr 2020 17:05:29 +0300
Subject: [PATCH 011/809] Prevent reply thumbnail image from overflowing

---
 res/css/views/messages/_MImageReplyBody.scss | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/res/css/views/messages/_MImageReplyBody.scss b/res/css/views/messages/_MImageReplyBody.scss
index 9b25b4392a..8c5cb97478 100644
--- a/res/css/views/messages/_MImageReplyBody.scss
+++ b/res/css/views/messages/_MImageReplyBody.scss
@@ -25,6 +25,10 @@ limitations under the License.
 
 .mx_MImageReplyBody_thumbnail {
     grid-area: image;
+
+    .mx_MImageBody_thumbnail_container {
+        max-height: 44px !important;
+    }
 }
 
 .mx_MImageReplyBody_sender {

From 466ecf191af65c453bb3e38d867e57fc211dc5c5 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Mon, 13 Apr 2020 21:23:40 +0100
Subject: [PATCH 012/809] move urlSearchParamsToObject and global.d.ts to
 react-sdk

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
---
 package.json                           |  1 +
 src/@types/global.d.ts                 | 31 ++++++++++++++++++++++++++
 src/utils/{UrlUtils.js => UrlUtils.ts} |  6 ++++-
 yarn.lock                              |  5 +++++
 4 files changed, 42 insertions(+), 1 deletion(-)
 create mode 100644 src/@types/global.d.ts
 rename src/utils/{UrlUtils.js => UrlUtils.ts} (89%)

diff --git a/package.json b/package.json
index 616f3f541e..11803d321d 100644
--- a/package.json
+++ b/package.json
@@ -118,6 +118,7 @@
     "@babel/register": "^7.7.4",
     "@peculiar/webcrypto": "^1.0.22",
     "@types/classnames": "^2.2.10",
+    "@types/modernizr": "^3.5.3",
     "@types/react": "16.9",
     "babel-eslint": "^10.0.3",
     "babel-jest": "^24.9.0",
diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts
new file mode 100644
index 0000000000..963ba9d702
--- /dev/null
+++ b/src/@types/global.d.ts
@@ -0,0 +1,31 @@
+/*
+Copyright 2020 New Vector Ltd
+
+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 * as ModernizrStatic from "modernizr";
+
+declare global {
+    interface Window {
+        Modernizr: ModernizrStatic;
+        Olm: {
+            init: () => Promise;
+        };
+    }
+
+    // workaround for https://github.com/microsoft/TypeScript/issues/30933
+    interface ObjectConstructor {
+        fromEntries?(xs: [string|number|symbol, any][]): object
+    }
+}
diff --git a/src/utils/UrlUtils.js b/src/utils/UrlUtils.ts
similarity index 89%
rename from src/utils/UrlUtils.js
rename to src/utils/UrlUtils.ts
index 7b207c128e..7fe5ad0c89 100644
--- a/src/utils/UrlUtils.js
+++ b/src/utils/UrlUtils.ts
@@ -14,7 +14,11 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-import url from "url";
+import * as url from "url";
+
+export function urlSearchParamsToObject(params: URLSearchParams) {
+    return Object.fromEntries([...params.entries()]);
+}
 
 /**
  * If a url has no path component, etc. abbreviate it to just the hostname
diff --git a/yarn.lock b/yarn.lock
index 538a049bff..9c57ccf649 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1257,6 +1257,11 @@
   resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d"
   integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==
 
+"@types/modernizr@^3.5.3":
+  version "3.5.3"
+  resolved "https://registry.yarnpkg.com/@types/modernizr/-/modernizr-3.5.3.tgz#8ef99e6252191c1d88647809109dc29884ba6d7a"
+  integrity sha512-jhMOZSS0UGYTS9pqvt6q3wtT3uvOSve5piTEmTMx3zzTuBLvSIMxSIBIc3d5lajVD5h4xc41AMZD2M5orN3PxA==
+
 "@types/node@*":
   version "13.11.0"
   resolved "https://registry.yarnpkg.com/@types/node/-/node-13.11.0.tgz#390ea202539c61c8fa6ba4428b57e05bc36dc47b"

From af4ef38a4110f3cd785ae826b3271f28ade11012 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Mon, 13 Apr 2020 21:28:23 +0100
Subject: [PATCH 013/809] remove dependency on `qs`

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
---
 package.json                             |   1 -
 src/components/views/elements/AppTile.js |   4 +-
 src/utils/HostingLink.js                 |   4 +-
 yarn.lock                                | 197 ++---------------------
 4 files changed, 16 insertions(+), 190 deletions(-)

diff --git a/package.json b/package.json
index 11803d321d..7b66c95d28 100644
--- a/package.json
+++ b/package.json
@@ -87,7 +87,6 @@
     "prop-types": "^15.5.8",
     "qrcode": "^1.4.4",
     "qrcode-react": "^0.1.16",
-    "qs": "^6.6.0",
     "react": "^16.9.0",
     "react-addons-css-transition-group": "15.6.2",
     "react-beautiful-dnd": "^4.0.1",
diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js
index 73ed605edd..8762eb449e 100644
--- a/src/components/views/elements/AppTile.js
+++ b/src/components/views/elements/AppTile.js
@@ -18,7 +18,6 @@ limitations under the License.
 */
 
 import url from 'url';
-import qs from 'qs';
 import React, {createRef} from 'react';
 import PropTypes from 'prop-types';
 import {MatrixClientPeg} from '../../../MatrixClientPeg';
@@ -38,6 +37,7 @@ import {IntegrationManagers} from "../../../integrations/IntegrationManagers";
 import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
 import {aboveLeftOf, ContextMenu, ContextMenuButton} from "../../structures/ContextMenu";
 import PersistedElement from "./PersistedElement";
+import {urlSearchParamsToObject} from "../../../utils/UrlUtils";
 
 const ALLOWED_APP_URL_SCHEMES = ['https:', 'http:'];
 const ENABLE_REACT_PERF = false;
@@ -234,7 +234,7 @@ export default class AppTile extends React.Component {
             // Append scalar_token as a query param if not already present
             this._scalarClient.scalarToken = token;
             const u = url.parse(this._addWurlParams(this.props.app.url));
-            const params = qs.parse(u.query);
+            const params = urlSearchParamsToObject(new URLSearchParams(u.query));
             if (!params.scalar_token) {
                 params.scalar_token = encodeURIComponent(token);
                 // u.search must be set to undefined, so that u.format() uses query parameters - https://nodejs.org/docs/latest/api/url.html#url_url_format_url_options
diff --git a/src/utils/HostingLink.js b/src/utils/HostingLink.js
index 580ed00de5..fce2f104bd 100644
--- a/src/utils/HostingLink.js
+++ b/src/utils/HostingLink.js
@@ -15,10 +15,10 @@ limitations under the License.
 */
 
 import url from 'url';
-import qs from 'qs';
 
 import SdkConfig from '../SdkConfig';
 import {MatrixClientPeg} from '../MatrixClientPeg';
+import {urlSearchParamsToObject} from "./UrlUtils";
 
 export function getHostingLink(campaign) {
     const hostingLink = SdkConfig.get().hosting_signup_link;
@@ -29,7 +29,7 @@ export function getHostingLink(campaign) {
 
     try {
         const hostingUrl = url.parse(hostingLink);
-        const params = qs.parse(hostingUrl.query);
+        const params = urlSearchParamsToObject(new URLSearchParams(hostingUrl.query));
         params.utm_campaign = campaign;
         hostingUrl.search = undefined;
         hostingUrl.query = params;
diff --git a/yarn.lock b/yarn.lock
index 9c57ccf649..8dda986b46 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1501,11 +1501,6 @@ abab@^2.0.0:
   resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.3.tgz#623e2075e02eb2d3f2475e49f99c91846467907a"
   integrity sha512-tsFzPpcttalNjFBCFMqsKYQcWxxen1pgJR56by//QwvJc4/OUS3kPOOttx2tSIfjsylB0pYu7f5D3K1RCxUnUg==
 
-abbrev@1:
-  version "1.1.1"
-  resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8"
-  integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==
-
 acorn-globals@^4.1.0:
   version "4.3.4"
   resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-4.3.4.tgz#9fa1926addc11c97308c4e66d7add0d40c3272e7"
@@ -1639,19 +1634,11 @@ anymatch@~3.1.1:
     normalize-path "^3.0.0"
     picomatch "^2.0.4"
 
-aproba@^1.0.3, aproba@^1.1.1:
+aproba@^1.1.1:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a"
   integrity sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==
 
-are-we-there-yet@~1.1.2:
-  version "1.1.5"
-  resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz#4b35c2944f062a8bfcda66410760350fe9ddfc21"
-  integrity sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==
-  dependencies:
-    delegates "^1.0.0"
-    readable-stream "^2.0.6"
-
 argparse@^1.0.7:
   version "1.0.10"
   resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911"
@@ -2558,11 +2545,6 @@ console-browserify@^1.1.0:
   resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.2.0.tgz#67063cef57ceb6cf4993a2ab3a55840ae8c49336"
   integrity sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA==
 
-console-control-strings@^1.0.0, console-control-strings@~1.1.0:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e"
-  integrity sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=
-
 constants-browserify@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/constants-browserify/-/constants-browserify-1.0.0.tgz#c20b96d8c617748aaf1c16021760cd27fcb8cb75"
@@ -2808,7 +2790,7 @@ debug@^2.2.0, debug@^2.3.3:
   dependencies:
     ms "2.0.0"
 
-debug@^3.1.0, debug@^3.2.6:
+debug@^3.1.0:
   version "3.2.6"
   resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b"
   integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==
@@ -2884,11 +2866,6 @@ delayed-stream@~1.0.0:
   resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
   integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk=
 
-delegates@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a"
-  integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=
-
 des.js@^1.0.0:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.0.1.tgz#5382142e1bdc53f85d86d53e5f4aa7deb91e0843"
@@ -2902,11 +2879,6 @@ detect-file@^1.0.0:
   resolved "https://registry.yarnpkg.com/detect-file/-/detect-file-1.0.0.tgz#f0d66d03672a825cb1b73bdb3fe62310c8e552b7"
   integrity sha1-8NZtA2cqglyxtzvbP+YjEMjlUrc=
 
-detect-libc@^1.0.2:
-  version "1.0.3"
-  resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b"
-  integrity sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=
-
 detect-newline@^2.1.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-2.1.0.tgz#f41f1c10be4b00e87b5f13da680759f2c5bfd3e2"
@@ -3921,13 +3893,6 @@ from2@^2.1.0:
     inherits "^2.0.1"
     readable-stream "^2.0.0"
 
-fs-minipass@^1.2.5:
-  version "1.2.7"
-  resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-1.2.7.tgz#ccff8570841e7fe4265693da88936c55aed7f7c7"
-  integrity sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==
-  dependencies:
-    minipass "^2.6.0"
-
 fs-readdir-recursive@^1.1.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/fs-readdir-recursive/-/fs-readdir-recursive-1.1.0.tgz#e32fc030a2ccee44a6b5371308da54be0b397d27"
@@ -3990,20 +3955,6 @@ fuse.js@^2.2.0:
   resolved "https://registry.yarnpkg.com/fuse.js/-/fuse.js-2.7.4.tgz#96e420fde7ef011ac49c258a621314fe576536f9"
   integrity sha1-luQg/efvARrEnCWKYhMU/ldlNvk=
 
-gauge@~2.7.3:
-  version "2.7.4"
-  resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7"
-  integrity sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=
-  dependencies:
-    aproba "^1.0.3"
-    console-control-strings "^1.0.0"
-    has-unicode "^2.0.0"
-    object-assign "^4.1.0"
-    signal-exit "^3.0.0"
-    string-width "^1.0.1"
-    strip-ansi "^3.0.1"
-    wide-align "^1.1.0"
-
 gensync@^1.0.0-beta.1:
   version "1.0.0-beta.1"
   resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.1.tgz#58f4361ff987e5ff6e1e7a210827aa371eaac269"
@@ -4201,11 +4152,6 @@ has-symbols@^1.0.0, has-symbols@^1.0.1:
   resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.1.tgz#9f5214758a44196c406d9bd76cebf81ec2dd31e8"
   integrity sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==
 
-has-unicode@^2.0.0:
-  version "2.0.1"
-  resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9"
-  integrity sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=
-
 has-value@^0.3.1:
   version "0.3.1"
   resolved "https://registry.yarnpkg.com/has-value/-/has-value-0.3.1.tgz#7b1f58bada62ca827ec0a2078025654845995e1f"
@@ -4393,7 +4339,7 @@ humanize-ms@^1.2.1:
   dependencies:
     ms "^2.0.0"
 
-iconv-lite@0.4.24, iconv-lite@^0.4.24, iconv-lite@^0.4.4, iconv-lite@~0.4.13:
+iconv-lite@0.4.24, iconv-lite@^0.4.24, iconv-lite@~0.4.13:
   version "0.4.24"
   resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
   integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==
@@ -4410,13 +4356,6 @@ iferr@^0.1.5:
   resolved "https://registry.yarnpkg.com/iferr/-/iferr-0.1.5.tgz#c60eed69e6d8fdb6b3104a1fcbca1c192dc5b501"
   integrity sha1-xg7taebY/bazEEofy8ocGS3FtQE=
 
-ignore-walk@^3.0.1:
-  version "3.0.3"
-  resolved "https://registry.yarnpkg.com/ignore-walk/-/ignore-walk-3.0.3.tgz#017e2447184bfeade7c238e4aefdd1e8f95b1e37"
-  integrity sha512-m7o6xuOaT1aqheYHKf8W6J5pYH85ZI9w077erOzLje3JsB1gkafkAhHHY19dqjulgIZHFm32Cp5uNZgcQqdJKw==
-  dependencies:
-    minimatch "^3.0.4"
-
 ignore@^4.0.3, ignore@^4.0.6:
   version "4.0.6"
   resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc"
@@ -5980,21 +5919,6 @@ minimist@^1.1.1, minimist@^1.2.0, minimist@^1.2.5, "minimist@~ 1.2.0":
   resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
   integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
 
-minipass@^2.6.0, minipass@^2.8.6, minipass@^2.9.0:
-  version "2.9.0"
-  resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.9.0.tgz#e713762e7d3e32fed803115cf93e04bca9fcc9a6"
-  integrity sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==
-  dependencies:
-    safe-buffer "^5.1.2"
-    yallist "^3.0.0"
-
-minizlib@^1.2.1:
-  version "1.3.3"
-  resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-1.3.3.tgz#2290de96818a34c29551c8a8d301216bd65a861d"
-  integrity sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==
-  dependencies:
-    minipass "^2.9.0"
-
 mississippi@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/mississippi/-/mississippi-3.0.0.tgz#ea0a3291f97e0b5e8776b363d5f0a12d94c67022"
@@ -6019,7 +5943,7 @@ mixin-deep@^1.2.0:
     for-in "^1.0.2"
     is-extendable "^1.0.1"
 
-mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@^0.5.3:
+mkdirp@^0.5.1, mkdirp@^0.5.3:
   version "0.5.5"
   resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def"
   integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==
@@ -6096,15 +6020,6 @@ nearley@^2.7.10:
     randexp "0.4.6"
     semver "^5.4.1"
 
-needle@^2.2.1:
-  version "2.4.1"
-  resolved "https://registry.yarnpkg.com/needle/-/needle-2.4.1.tgz#14af48732463d7475696f937626b1b993247a56a"
-  integrity sha512-x/gi6ijr4B7fwl6WYL9FwlCvRQKGlUNvnceho8wxkwXqN8jvVmmmATTmZPRRG7b/yC1eode26C2HO9jl78Du9g==
-  dependencies:
-    debug "^3.2.6"
-    iconv-lite "^0.4.4"
-    sax "^1.2.4"
-
 neo-async@^2.5.0, neo-async@^2.6.1:
   version "2.6.1"
   resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.1.tgz#ac27ada66167fa8849a6addd837f6b189ad2081c"
@@ -6182,35 +6097,11 @@ node-notifier@^5.4.2:
     shellwords "^0.1.1"
     which "^1.3.0"
 
-node-pre-gyp@*:
-  version "0.14.0"
-  resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.14.0.tgz#9a0596533b877289bcad4e143982ca3d904ddc83"
-  integrity sha512-+CvDC7ZttU/sSt9rFjix/P05iS43qHCOOGzcr3Ry99bXG7VX953+vFyEuph/tfqoYu8dttBkE86JSKBO2OzcxA==
-  dependencies:
-    detect-libc "^1.0.2"
-    mkdirp "^0.5.1"
-    needle "^2.2.1"
-    nopt "^4.0.1"
-    npm-packlist "^1.1.6"
-    npmlog "^4.0.2"
-    rc "^1.2.7"
-    rimraf "^2.6.1"
-    semver "^5.3.0"
-    tar "^4.4.2"
-
 node-releases@^1.1.53:
   version "1.1.53"
   resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.53.tgz#2d821bfa499ed7c5dffc5e2f28c88e78a08ee3f4"
   integrity sha512-wp8zyQVwef2hpZ/dJH7SfSrIPD6YoJz6BDQDpGEkcA0s3LpAQoxBIYmfIq6QAhC1DhwsyCgTaTTcONwX8qzCuQ==
 
-nopt@^4.0.1:
-  version "4.0.3"
-  resolved "https://registry.yarnpkg.com/nopt/-/nopt-4.0.3.tgz#a375cad9d02fd921278d954c2254d5aa57e15e48"
-  integrity sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==
-  dependencies:
-    abbrev "1"
-    osenv "^0.1.4"
-
 normalize-package-data@^2.3.2, normalize-package-data@^2.3.4:
   version "2.5.0"
   resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8"
@@ -6243,27 +6134,6 @@ normalize-selector@^0.2.0:
   resolved "https://registry.yarnpkg.com/normalize-selector/-/normalize-selector-0.2.0.tgz#d0b145eb691189c63a78d201dc4fdb1293ef0c03"
   integrity sha1-0LFF62kRicY6eNIB3E/bEpPvDAM=
 
-npm-bundled@^1.0.1:
-  version "1.1.1"
-  resolved "https://registry.yarnpkg.com/npm-bundled/-/npm-bundled-1.1.1.tgz#1edd570865a94cdb1bc8220775e29466c9fb234b"
-  integrity sha512-gqkfgGePhTpAEgUsGEgcq1rqPXA+tv/aVBlgEzfXwA1yiUJF7xtEt3CtVwOjNYQOVknDk0F20w58Fnm3EtG0fA==
-  dependencies:
-    npm-normalize-package-bin "^1.0.1"
-
-npm-normalize-package-bin@^1.0.1:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz#6e79a41f23fd235c0623218228da7d9c23b8f6e2"
-  integrity sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA==
-
-npm-packlist@^1.1.6:
-  version "1.4.8"
-  resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-1.4.8.tgz#56ee6cc135b9f98ad3d51c1c95da22bbb9b2ef3e"
-  integrity sha512-5+AZgwru5IevF5ZdnFglB5wNlHG1AOOuw28WhUq8/8emhBmLv6jX5by4WJCh7lW0uSYZYS6DXqIsyZVIXRZU9A==
-  dependencies:
-    ignore-walk "^3.0.1"
-    npm-bundled "^1.0.1"
-    npm-normalize-package-bin "^1.0.1"
-
 npm-run-path@^2.0.0:
   version "2.0.2"
   resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f"
@@ -6271,16 +6141,6 @@ npm-run-path@^2.0.0:
   dependencies:
     path-key "^2.0.0"
 
-npmlog@^4.0.2:
-  version "4.1.2"
-  resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b"
-  integrity sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==
-  dependencies:
-    are-we-there-yet "~1.1.2"
-    console-control-strings "~1.1.0"
-    gauge "~2.7.3"
-    set-blocking "~2.0.0"
-
 nth-check@~1.0.1:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-1.0.2.tgz#b2bd295c37e3dd58a3bf0700376663ba4d9cf05c"
@@ -6430,11 +6290,6 @@ os-browserify@^0.3.0:
   resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.3.0.tgz#854373c7f5c2315914fc9bfc6bd8238fdda1ec27"
   integrity sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=
 
-os-homedir@^1.0.0:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3"
-  integrity sha1-/7xJiDNuDoM94MFox+8VISGqf7M=
-
 os-locale@^3.0.0, os-locale@^3.1.0:
   version "3.1.0"
   resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-3.1.0.tgz#a802a6ee17f24c10483ab9935719cef4ed16bf1a"
@@ -6444,19 +6299,11 @@ os-locale@^3.0.0, os-locale@^3.1.0:
     lcid "^2.0.0"
     mem "^4.0.0"
 
-os-tmpdir@^1.0.0, os-tmpdir@~1.0.2:
+os-tmpdir@~1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274"
   integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=
 
-osenv@^0.1.4:
-  version "0.1.5"
-  resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.5.tgz#85cdfafaeb28e8677f416e287592b5f3f49ea410"
-  integrity sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==
-  dependencies:
-    os-homedir "^1.0.0"
-    os-tmpdir "^1.0.0"
-
 p-defer@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/p-defer/-/p-defer-1.0.0.tgz#9f6eb182f6c9aa8cd743004a7d4f96b196b0fb0c"
@@ -7036,7 +6883,7 @@ qrcode@^1.4.4:
     pngjs "^3.3.0"
     yargs "^13.2.4"
 
-qs@^6.5.2, qs@^6.6.0:
+qs@^6.5.2:
   version "6.9.3"
   resolved "https://registry.yarnpkg.com/qs/-/qs-6.9.3.tgz#bfadcd296c2d549f1dffa560619132c977f5008e"
   integrity sha512-EbZYNarm6138UKKq46tdx08Yo/q9ZhFoAXAI1meAFd2GtbRDhbZY2WQSICskT0c5q99aFzLG1D4nvTk9tqfXIw==
@@ -7101,7 +6948,7 @@ randomfill@^1.0.3:
     randombytes "^2.0.5"
     safe-buffer "^5.1.0"
 
-rc@1.2.8, rc@^1.2.7, rc@^1.2.8:
+rc@1.2.8, rc@^1.2.8:
   version "1.2.8"
   resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed"
   integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==
@@ -7259,7 +7106,7 @@ read-pkg@^4.0.1:
     parse-json "^4.0.0"
     pify "^3.0.0"
 
-"readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.3, readable-stream@^2.3.6, readable-stream@~2.3.6:
+"readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.3, readable-stream@^2.3.6, readable-stream@~2.3.6:
   version "2.3.7"
   resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57"
   integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==
@@ -7619,7 +7466,7 @@ rimraf@2.6.3:
   dependencies:
     glob "^7.1.3"
 
-rimraf@^2.4.3, rimraf@^2.5.4, rimraf@^2.6.1, rimraf@^2.6.3:
+rimraf@^2.4.3, rimraf@^2.5.4, rimraf@^2.6.3:
   version "2.7.1"
   resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec"
   integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==
@@ -7781,7 +7628,7 @@ serialize-javascript@^2.1.2:
   resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-2.1.2.tgz#ecec53b0e0317bdc95ef76ab7074b7384785fa61"
   integrity sha512-rs9OggEUF0V4jUSecXazOYsLfu7OGK2qIn3c7IPBiffz32XniEp/TX9Xmc9LQfK2nQ2QKHvZ2oygKUGU0lG4jQ==
 
-set-blocking@^2.0.0, set-blocking@~2.0.0:
+set-blocking@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
   integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc=
@@ -8117,7 +7964,7 @@ string-width@^1.0.1:
     is-fullwidth-code-point "^1.0.0"
     strip-ansi "^3.0.0"
 
-"string-width@^1.0.2 || 2", string-width@^2.0.0, string-width@^2.1.0, string-width@^2.1.1:
+string-width@^2.0.0, string-width@^2.1.0, string-width@^2.1.1:
   version "2.1.1"
   resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e"
   integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==
@@ -8398,19 +8245,6 @@ tapable@^1.0.0, tapable@^1.1.3:
   resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.1.3.tgz#a1fccc06b58db61fd7a45da2da44f5f3a3e67ba2"
   integrity sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==
 
-tar@^4.4.2:
-  version "4.4.13"
-  resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.13.tgz#43b364bc52888d555298637b10d60790254ab525"
-  integrity sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA==
-  dependencies:
-    chownr "^1.1.1"
-    fs-minipass "^1.2.5"
-    minipass "^2.8.6"
-    minizlib "^1.2.1"
-    mkdirp "^0.5.0"
-    safe-buffer "^5.1.2"
-    yallist "^3.0.3"
-
 terser-webpack-plugin@^1.4.3:
   version "1.4.3"
   resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-1.4.3.tgz#5ecaf2dbdc5fb99745fd06791f46fc9ddb1c9a7c"
@@ -9133,13 +8967,6 @@ which@^1.2.14, which@^1.2.9, which@^1.3.0, which@^1.3.1:
   dependencies:
     isexe "^2.0.0"
 
-wide-align@^1.1.0:
-  version "1.1.3"
-  resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.3.tgz#ae074e6bdc0c14a431e804e624549c633b000457"
-  integrity sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==
-  dependencies:
-    string-width "^1.0.2 || 2"
-
 word-wrap@~1.2.3:
   version "1.2.3"
   resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c"
@@ -9224,7 +9051,7 @@ xtend@^4.0.0, xtend@^4.0.1, xtend@~4.0.1:
   resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b"
   integrity sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==
 
-yallist@^3.0.0, yallist@^3.0.2, yallist@^3.0.3:
+yallist@^3.0.2:
   version "3.1.1"
   resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd"
   integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==

From dfe277b78d1e7fe39fe91245646b41d72bb367fc Mon Sep 17 00:00:00 2001
From: Tulir Asokan 
Date: Mon, 25 May 2020 19:24:03 +0300
Subject: [PATCH 014/809] Remove unnecessary right margin in reply blockquote

---
 res/css/views/elements/_ReplyThread.scss | 1 +
 1 file changed, 1 insertion(+)

diff --git a/res/css/views/elements/_ReplyThread.scss b/res/css/views/elements/_ReplyThread.scss
index d5388e4631..af8ca956ba 100644
--- a/res/css/views/elements/_ReplyThread.scss
+++ b/res/css/views/elements/_ReplyThread.scss
@@ -24,6 +24,7 @@ limitations under the License.
 
 blockquote.mx_ReplyThread {
     margin-left: 0;
+    margin-right: 0;
     margin-bottom: 8px;
     padding-left: 10px;
     border-left: 4px solid $button-bg-color;

From c60483728fc8526538e43c1a27834c85d8c1984c Mon Sep 17 00:00:00 2001
From: Tulir Asokan 
Date: Mon, 25 May 2020 19:33:30 +0300
Subject: [PATCH 015/809] Fix dispatcher import in ReplyTile.js

---
 src/components/views/rooms/ReplyTile.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/components/views/rooms/ReplyTile.js b/src/components/views/rooms/ReplyTile.js
index 34b2c6ad38..f6c4a69def 100644
--- a/src/components/views/rooms/ReplyTile.js
+++ b/src/components/views/rooms/ReplyTile.js
@@ -21,7 +21,7 @@ import { _t, _td } from '../../../languageHandler';
 
 import * as sdk from '../../../index';
 
-import dis from '../../../dispatcher';
+import dis from '../../../dispatcher/dispatcher';
 import SettingsStore from "../../../settings/SettingsStore";
 import {MatrixClient} from 'matrix-js-sdk';
 

From 53db386731422c88d4e24e299e9e56a150dad3cf Mon Sep 17 00:00:00 2001
From: Travis Ralston 
Date: Mon, 10 Aug 2020 22:06:30 -0600
Subject: [PATCH 016/809] Add support for blurhash (MSC2448)

MSC: https://github.com/matrix-org/matrix-doc/pull/2448

While the image loads, we can show a blurred version of it (calculated at upload time) so we don't have a blank space in the timeline.
---
 package.json                                  |  1 +
 src/ContentMessages.tsx                       | 10 +++-
 .../views/elements/BlurhashPlaceholder.tsx    | 56 +++++++++++++++++++
 src/components/views/messages/MImageBody.js   | 18 +++---
 yarn.lock                                     |  5 ++
 5 files changed, 81 insertions(+), 9 deletions(-)
 create mode 100644 src/components/views/elements/BlurhashPlaceholder.tsx

diff --git a/package.json b/package.json
index 548b33f353..989672d414 100644
--- a/package.json
+++ b/package.json
@@ -56,6 +56,7 @@
     "@babel/runtime": "^7.10.5",
     "await-lock": "^2.0.1",
     "blueimp-canvas-to-blob": "^3.27.0",
+    "blurhash": "^1.1.3",
     "browser-encrypt-attachment": "^0.3.0",
     "browser-request": "^0.3.3",
     "classnames": "^2.2.6",
diff --git a/src/ContentMessages.tsx b/src/ContentMessages.tsx
index 6f55a75d0c..7e57b34ff7 100644
--- a/src/ContentMessages.tsx
+++ b/src/ContentMessages.tsx
@@ -334,6 +334,7 @@ function uploadFile(matrixClient: MatrixClient, roomId: string, file: File | Blo
             if (file.type) {
                 encryptInfo.mimetype = file.type;
             }
+            // TODO: Blurhash for encrypted media?
             return {"file": encryptInfo};
         });
         (prom as IAbortablePromise).abort = () => {
@@ -344,11 +345,15 @@ function uploadFile(matrixClient: MatrixClient, roomId: string, file: File | Blo
     } else {
         const basePromise = matrixClient.uploadContent(file, {
             progressHandler: progressHandler,
+            onlyContentUri: false,
         });
-        const promise1 = basePromise.then(function(url) {
+        const promise1 = basePromise.then(function(body) {
             if (canceled) throw new UploadCanceledError();
             // If the attachment isn't encrypted then include the URL directly.
-            return {"url": url};
+            return {
+                "url": body.content_uri,
+                "blurhash": body["xyz.amorgan.blurhash"], // TODO: Use `body.blurhash` when MSC2448 lands
+            };
         });
         promise1.abort = () => {
             canceled = true;
@@ -550,6 +555,7 @@ export default class ContentMessages {
             return upload.promise.then(function(result) {
                 content.file = result.file;
                 content.url = result.url;
+                content.info['xyz.amorgan.blurhash'] = result.blurhash; // TODO: Use `blurhash` when MSC2448 lands
             });
         }).then(() => {
             // Await previous message being sent into the room
diff --git a/src/components/views/elements/BlurhashPlaceholder.tsx b/src/components/views/elements/BlurhashPlaceholder.tsx
new file mode 100644
index 0000000000..bed45bfe5a
--- /dev/null
+++ b/src/components/views/elements/BlurhashPlaceholder.tsx
@@ -0,0 +1,56 @@
+/*
+ Copyright 2020 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 {decode} from "blurhash";
+
+interface IProps {
+    blurhash: string;
+    width: number;
+    height: number;
+}
+
+export default class BlurhashPlaceholder extends React.PureComponent {
+    private canvas: React.RefObject = React.createRef();
+
+    public componentDidMount() {
+        this.draw();
+    }
+
+    public componentDidUpdate() {
+        this.draw();
+    }
+
+    private draw() {
+        if (!this.canvas.current) return;
+
+        try {
+            const {width, height} = this.props;
+
+            const pixels = decode(this.props.blurhash, Math.ceil(width), Math.ceil(height));
+            const ctx = this.canvas.current.getContext("2d");
+            const imgData = ctx.createImageData(width, height);
+            imgData.data.set(pixels);
+            ctx.putImageData(imgData, 0, 0);
+        } catch (e) {
+            console.error("Error rendering blurhash: ", e);
+        }
+    }
+
+    public render() {
+        return ;
+    }
+}
diff --git a/src/components/views/messages/MImageBody.js b/src/components/views/messages/MImageBody.js
index c92ae475bf..e0ac24c641 100644
--- a/src/components/views/messages/MImageBody.js
+++ b/src/components/views/messages/MImageBody.js
@@ -27,6 +27,7 @@ import { _t } from '../../../languageHandler';
 import SettingsStore from "../../../settings/SettingsStore";
 import MatrixClientContext from "../../../contexts/MatrixClientContext";
 import InlineSpinner from '../elements/InlineSpinner';
+import BlurhashPlaceholder from "../elements/BlurhashPlaceholder";
 
 export default class MImageBody extends React.Component {
     static propTypes = {
@@ -53,6 +54,8 @@ export default class MImageBody extends React.Component {
         this.onClick = this.onClick.bind(this);
         this._isGif = this._isGif.bind(this);
 
+        const imageInfo = this.props.mxEvent.getContent().info;
+
         this.state = {
             decryptedUrl: null,
             decryptedThumbnailUrl: null,
@@ -63,6 +66,7 @@ export default class MImageBody extends React.Component {
             loadedImageDimensions: null,
             hover: false,
             showImage: SettingsStore.getValue("showImages"),
+            blurhash: imageInfo ? imageInfo['xyz.amorgan.blurhash'] : null, // TODO: Use `blurhash` when MSC2448 lands.
         };
 
         this._image = createRef();
@@ -329,7 +333,8 @@ export default class MImageBody extends React.Component {
             infoWidth = content.info.w;
             infoHeight = content.info.h;
         } else {
-            // Whilst the image loads, display nothing.
+            // Whilst the image loads, display nothing. We also don't display a blurhash image
+            // because we don't really know what size of image we'll end up with.
             //
             // Once loaded, use the loaded image dimensions stored in `loadedImageDimensions`.
             //
@@ -368,8 +373,7 @@ export default class MImageBody extends React.Component {
         if (content.file !== undefined && this.state.decryptedUrl === null) {
             placeholder = ;
         } else if (!this.state.imgLoaded) {
-            // Deliberately, getSpinner is left unimplemented here, MStickerBody overides
-            placeholder = this.getPlaceholder();
+            placeholder = this.getPlaceholder(maxWidth, maxHeight);
         }
 
         let showPlaceholder = Boolean(placeholder);
@@ -391,7 +395,7 @@ export default class MImageBody extends React.Component {
 
         if (!this.state.showImage) {
             img = ;
-            showPlaceholder = false; // because we're hiding the image, so don't show the sticker icon.
+            showPlaceholder = false; // because we're hiding the image, so don't show the placeholder.
         }
 
         if (this._isGif() && !SettingsStore.getValue("autoplayGifsAndVideos") && !this.state.hover) {
@@ -433,9 +437,9 @@ export default class MImageBody extends React.Component {
     }
 
     // Overidden by MStickerBody
-    getPlaceholder() {
-        // MImageBody doesn't show a placeholder whilst the image loads, (but it could do)
-        return null;
+    getPlaceholder(width, height) {
+        if (!this.state.blurhash) return null;
+        return ;
     }
 
     // Overidden by MStickerBody
diff --git a/yarn.lock b/yarn.lock
index 98fe42ef13..f1cace67a1 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2493,6 +2493,11 @@ blueimp-canvas-to-blob@^3.27.0:
   resolved "https://registry.yarnpkg.com/blueimp-canvas-to-blob/-/blueimp-canvas-to-blob-3.27.0.tgz#a2bd5c43587b95dedf0f6998603452d1bfcc9b9e"
   integrity sha512-AcIj+hCw6WquxzJuzC6KzgYmqxLFeTWe88KuY2BEIsW1zbEOfoinDAGlhyvFNGt+U3JElkVSK7anA1FaSdmmfA==
 
+blurhash@^1.1.3:
+  version "1.1.3"
+  resolved "https://registry.yarnpkg.com/blurhash/-/blurhash-1.1.3.tgz#dc325af7da836d07a0861d830bdd63694382483e"
+  integrity sha512-yUhPJvXexbqbyijCIE/T2NCXcj9iNPhWmOKbPTuR/cm7Q5snXYIfnVnz6m7MWOXxODMz/Cr3UcVkRdHiuDVRDw==
+
 bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.4.0:
   version "4.11.9"
   resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.9.tgz#26d556829458f9d1e81fc48952493d0ba3507828"

From 6f464feded9c3ea9541bd2be9a5a704bc70ba2b1 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= 
Date: Tue, 23 Feb 2021 18:12:46 +0100
Subject: [PATCH 017/809] Quit sticker picker on m.sticker
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Šimon Brandner 
---
 src/components/views/elements/AppTile.js | 1 +
 1 file changed, 1 insertion(+)

diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js
index 213351889f..9a345a7bf6 100644
--- a/src/components/views/elements/AppTile.js
+++ b/src/components/views/elements/AppTile.js
@@ -226,6 +226,7 @@ export default class AppTile extends React.Component {
                 case 'm.sticker':
                     if (this._sgWidget.widgetApi.hasCapability(MatrixCapabilities.StickerSending)) {
                         dis.dispatch({action: 'post_sticker_message', data: payload.data});
+                        dis.dispatch({action: 'stickerpicker_close'});
                     } else {
                         console.warn('Ignoring sticker message. Invalid capability');
                     }

From 0e293bca912de8502b5a85edf209a43089576f29 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= 
Date: Wed, 10 Mar 2021 15:34:45 +0100
Subject: [PATCH 018/809] Reorganize preferences
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Šimon Brandner 
---
 .../tabs/user/PreferencesUserSettingsTab.js   | 61 +++++++++++++++----
 1 file changed, 49 insertions(+), 12 deletions(-)

diff --git a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.js b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.js
index ae9cad4cfa..49003b03f3 100644
--- a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.js
+++ b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.js
@@ -29,6 +29,10 @@ export default class PreferencesUserSettingsTab extends React.Component {
         'breadcrumbs',
     ];
 
+    static KEYBINDINGS_SETTINGS = [
+        'ctrlFForSearch',
+    ];
+
     static COMPOSER_SETTINGS = [
         'MessageComposerInput.autoReplaceEmoji',
         'MessageComposerInput.suggestEmoji',
@@ -37,26 +41,34 @@ export default class PreferencesUserSettingsTab extends React.Component {
         'MessageComposerInput.showStickersButton',
     ];
 
-    static TIMELINE_SETTINGS = [
-        'showTypingNotifications',
-        'autoplayGifsAndVideos',
-        'urlPreviewsEnabled',
-        'TextualBody.enableBigEmoji',
-        'showReadReceipts',
+    static TIME_SETTINGS = [
         'showTwelveHourTimestamps',
         'alwaysShowTimestamps',
-        'showRedactions',
+    ];
+    static CODE_BLOCKS_SETTINGS = [
         'enableSyntaxHighlightLanguageDetection',
         'expandCodeByDefault',
-        'scrollToBottomOnMessageSent',
         'showCodeLineNumbers',
-        'showJoinLeaves',
-        'showAvatarChanges',
-        'showDisplaynameChanges',
+    ];
+    static IMAGES_AND_VIDEOS_SETTINGS = [
+        'urlPreviewsEnabled',
+        'autoplayGifsAndVideos',
         'showImages',
+    ];
+    static THINGS_TO_HIDE_ON_TIMELINE_SETTINGS = [
+        'showTypingNotifications',
+        'showRedactions',
+        'showReadReceipts',
+        'showJoinLeaves',
+        'showDisplaynameChanges',
         'showChatEffects',
+        'showAvatarChanges',
+    ];
+
+    static TIMELINE_SETTINGS = [
+        'TextualBody.enableBigEmoji',
+        'scrollToBottomOnMessageSent',
         'Pill.shouldShowPillAvatar',
-        'ctrlFForSearch',
     ];
 
     static GENERAL_SETTINGS = [
@@ -184,11 +196,36 @@ export default class PreferencesUserSettingsTab extends React.Component {
                     {this._renderGroup(PreferencesUserSettingsTab.ROOM_LIST_SETTINGS)}
                 
 
+                
+ {_t("Keybindings")} + {this._renderGroup(PreferencesUserSettingsTab.KEYBINDINGS_SETTINGS)} +
+ +
+ {_t("Displaying time")} + {this._renderGroup(PreferencesUserSettingsTab.TIME_SETTINGS)} +
+
{_t("Composer")} {this._renderGroup(PreferencesUserSettingsTab.COMPOSER_SETTINGS)}
+
+ {_t("Code blocks")} + {this._renderGroup(PreferencesUserSettingsTab.CODE_BLOCKS_SETTINGS)} +
+ +
+ {_t("Images, GIFs and videos")} + {this._renderGroup(PreferencesUserSettingsTab.IMAGES_AND_VIDEOS_SETTINGS)} +
+ +
+ {_t("Hide things on the timeline")} + {this._renderGroup(PreferencesUserSettingsTab.THINGS_TO_HIDE_ON_TIMELINE_SETTINGS)} +
+
{_t("Timeline")} {this._renderGroup(PreferencesUserSettingsTab.TIMELINE_SETTINGS)} From 951a807e249d2049eeb5bba7c94876f0988cec2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Wed, 10 Mar 2021 15:34:50 +0100 Subject: [PATCH 019/809] i18n MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/i18n/strings/en_EN.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 267721b533..4f07d8fde2 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1284,7 +1284,12 @@ "Show tray icon and minimize window to it on close": "Show tray icon and minimize window to it on close", "Preferences": "Preferences", "Room list": "Room list", + "Keybindings": "Keybindings", + "Displaying time": "Displaying time", "Composer": "Composer", + "Code blocks": "Code blocks", + "Images, GIFs and videos": "Images, GIFs and videos", + "Hide things on the timeline": "Hide things on the timeline", "Timeline": "Timeline", "Autocomplete delay (ms)": "Autocomplete delay (ms)", "Read Marker lifetime (ms)": "Read Marker lifetime (ms)", From cdf8f09ec256f8bd69e478ffc11ad26ff883c398 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sat, 20 Mar 2021 13:38:42 +0200 Subject: [PATCH 020/809] Remove unused import and run yarn i18n --- src/components/views/elements/ReplyThread.js | 1 - src/i18n/strings/en_EN.json | 3 +++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/views/elements/ReplyThread.js b/src/components/views/elements/ReplyThread.js index eb29e52496..4129f1d14f 100644 --- a/src/components/views/elements/ReplyThread.js +++ b/src/components/views/elements/ReplyThread.js @@ -20,7 +20,6 @@ import * as sdk from '../../../index'; import {_t} from '../../../languageHandler'; import PropTypes from 'prop-types'; import dis from '../../../dispatcher/dispatcher'; -import {wantsDateSeparator} from '../../../DateUtils'; import {MatrixEvent} from 'matrix-js-sdk/src/models/event'; import {makeUserPermalink, RoomPermalinkCreator} from "../../../utils/permalinks/Permalinks"; import SettingsStore from "../../../settings/SettingsStore"; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 63b19831bb..66b1843e64 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1500,6 +1500,9 @@ "Seen by %(userName)s at %(dateTime)s": "Seen by %(userName)s at %(dateTime)s", "Seen by %(displayName)s (%(userName)s) at %(dateTime)s": "Seen by %(displayName)s (%(userName)s) at %(dateTime)s", "Replying": "Replying", + "%(senderName)s sent an image": "%(senderName)s sent an image", + "%(senderName)s sent a video": "%(senderName)s sent a video", + "%(senderName)s uploaded a file": "%(senderName)s uploaded a file", "Room %(name)s": "Room %(name)s", "Recently visited rooms": "Recently visited rooms", "No recently visited rooms": "No recently visited rooms", From 3df9557df2f886db13185872641b2f763baf41aa Mon Sep 17 00:00:00 2001 From: Ayush PS Date: Wed, 24 Mar 2021 14:00:09 +0530 Subject: [PATCH 021/809] Dial Pad UI fix --- res/css/views/voip/_DialPad.scss | 1 + res/css/views/voip/_DialPadContextMenu.scss | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/res/css/views/voip/_DialPad.scss b/res/css/views/voip/_DialPad.scss index 0c7bff0ce8..fd7c5f56f6 100644 --- a/res/css/views/voip/_DialPad.scss +++ b/res/css/views/voip/_DialPad.scss @@ -30,6 +30,7 @@ limitations under the License. text-align: center; vertical-align: middle; line-height: 40px; + color: #15191e; } .mx_DialPad_deleteButton, .mx_DialPad_dialButton { diff --git a/res/css/views/voip/_DialPadContextMenu.scss b/res/css/views/voip/_DialPadContextMenu.scss index 520f51cf93..c400060b6c 100644 --- a/res/css/views/voip/_DialPadContextMenu.scss +++ b/res/css/views/voip/_DialPadContextMenu.scss @@ -27,9 +27,11 @@ limitations under the License. } .mx_DialPadContextMenu_dialled { - height: 1em; + height: 1.5em; font-size: 18px; font-weight: 600; + max-width: 150px; + overflow: auto; } .mx_DialPadContextMenu_dialPad { From 1488457c3322b6d5dd6ef7882d5b14d9bf49e20f Mon Sep 17 00:00:00 2001 From: Ayush PS Date: Thu, 25 Mar 2021 01:31:45 +0530 Subject: [PATCH 022/809] Added the class -button-bg-color for all themes --- res/css/views/voip/_DialPad.scss | 3 +-- res/themes/dark/css/_dark.scss | 2 ++ res/themes/legacy-dark/css/_legacy-dark.scss | 1 + res/themes/legacy-light/css/_legacy-light.scss | 2 ++ res/themes/light/css/_light.scss | 2 ++ 5 files changed, 8 insertions(+), 2 deletions(-) diff --git a/res/css/views/voip/_DialPad.scss b/res/css/views/voip/_DialPad.scss index fd7c5f56f6..483b131bfe 100644 --- a/res/css/views/voip/_DialPad.scss +++ b/res/css/views/voip/_DialPad.scss @@ -23,14 +23,13 @@ limitations under the License. .mx_DialPad_button { width: 40px; height: 40px; - background-color: $theme-button-bg-color; + background-color: $dialpad-button-bg-color; border-radius: 40px; font-size: 18px; font-weight: 600; text-align: center; vertical-align: middle; line-height: 40px; - color: #15191e; } .mx_DialPad_deleteButton, .mx_DialPad_dialButton { diff --git a/res/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss index 7a751ad9c1..42d592c1e1 100644 --- a/res/themes/dark/css/_dark.scss +++ b/res/themes/dark/css/_dark.scss @@ -114,6 +114,8 @@ $voipcall-plinth-color: #21262c; // ******************** $theme-button-bg-color: #e3e8f0; +$dialpad-button-bg-color: #545454; + $roomlist-button-bg-color: rgba(141, 151, 165, 0.2); // Buttons include the filter box, explore button, and sublist buttons $roomlist-filter-active-bg-color: $bg-color; diff --git a/res/themes/legacy-dark/css/_legacy-dark.scss b/res/themes/legacy-dark/css/_legacy-dark.scss index 764b8f302a..ae98141d06 100644 --- a/res/themes/legacy-dark/css/_legacy-dark.scss +++ b/res/themes/legacy-dark/css/_legacy-dark.scss @@ -111,6 +111,7 @@ $voipcall-plinth-color: #f2f5f8; // ******************** $theme-button-bg-color: #e3e8f0; +$dialpad-button-bg-color: #545454; $roomlist-button-bg-color: #1A1D23; // Buttons include the filter box, explore button, and sublist buttons $roomlist-filter-active-bg-color: $roomlist-button-bg-color; diff --git a/res/themes/legacy-light/css/_legacy-light.scss b/res/themes/legacy-light/css/_legacy-light.scss index 9ad154dd93..4313e3c0b6 100644 --- a/res/themes/legacy-light/css/_legacy-light.scss +++ b/res/themes/legacy-light/css/_legacy-light.scss @@ -178,6 +178,8 @@ $voipcall-plinth-color: #f2f5f8; // ******************** $theme-button-bg-color: #e3e8f0; +$dialpad-button-bg-color: #e3e8f0; + $roomlist-button-bg-color: #fff; // Buttons include the filter box, explore button, and sublist buttons $roomlist-filter-active-bg-color: $roomlist-button-bg-color; diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss index 25fbd0201b..81330d07c9 100644 --- a/res/themes/light/css/_light.scss +++ b/res/themes/light/css/_light.scss @@ -169,6 +169,8 @@ $voipcall-plinth-color: #f2f5f8; // ******************** $theme-button-bg-color: #e3e8f0; +$dialpad-button-bg-color: #e3e8f0; + $roomlist-button-bg-color: rgba(141, 151, 165, 0.2); // Buttons include the filter box, explore button, and sublist buttons $roomlist-filter-active-bg-color: #ffffff; From 3201ed2f0fe0e378c741d57d9a147d79b267b842 Mon Sep 17 00:00:00 2001 From: Ayush PS Date: Mon, 26 Apr 2021 01:40:10 +0530 Subject: [PATCH 023/809] Added color scheme for the numbers --- res/css/views/voip/_DialPadContextMenu.scss | 2 +- res/themes/dark/css/_dark.scss | 3 ++- res/themes/legacy-dark/css/_legacy-dark.scss | 3 ++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/res/css/views/voip/_DialPadContextMenu.scss b/res/css/views/voip/_DialPadContextMenu.scss index c400060b6c..9879b7da1c 100644 --- a/res/css/views/voip/_DialPadContextMenu.scss +++ b/res/css/views/voip/_DialPadContextMenu.scss @@ -30,7 +30,7 @@ limitations under the License. height: 1.5em; font-size: 18px; font-weight: 600; - max-width: 150px; + max-width: 155px; overflow: auto; } diff --git a/res/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss index 42d592c1e1..b83bd52f76 100644 --- a/res/themes/dark/css/_dark.scss +++ b/res/themes/dark/css/_dark.scss @@ -114,7 +114,8 @@ $voipcall-plinth-color: #21262c; // ******************** $theme-button-bg-color: #e3e8f0; -$dialpad-button-bg-color: #545454; +$dialpad-button-bg-color: #6F7882; +; $roomlist-button-bg-color: rgba(141, 151, 165, 0.2); // Buttons include the filter box, explore button, and sublist buttons diff --git a/res/themes/legacy-dark/css/_legacy-dark.scss b/res/themes/legacy-dark/css/_legacy-dark.scss index ae98141d06..ff85375d35 100644 --- a/res/themes/legacy-dark/css/_legacy-dark.scss +++ b/res/themes/legacy-dark/css/_legacy-dark.scss @@ -111,7 +111,8 @@ $voipcall-plinth-color: #f2f5f8; // ******************** $theme-button-bg-color: #e3e8f0; -$dialpad-button-bg-color: #545454; +$dialpad-button-bg-color: #6F7882; +; $roomlist-button-bg-color: #1A1D23; // Buttons include the filter box, explore button, and sublist buttons $roomlist-filter-active-bg-color: $roomlist-button-bg-color; From 7509481bb9339e038027efa6564df9746b73518a Mon Sep 17 00:00:00 2001 From: Ayush PS Date: Wed, 28 Apr 2021 02:46:43 +0530 Subject: [PATCH 024/809] Added the LTR support for the dialpad --- res/css/views/voip/_DialPadContextMenu.scss | 14 ++++++++++++-- .../views/context_menus/DialpadContextMenu.tsx | 13 ++++++++++++- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/res/css/views/voip/_DialPadContextMenu.scss b/res/css/views/voip/_DialPadContextMenu.scss index 9879b7da1c..c01ce0f2d9 100644 --- a/res/css/views/voip/_DialPadContextMenu.scss +++ b/res/css/views/voip/_DialPadContextMenu.scss @@ -30,8 +30,18 @@ limitations under the License. height: 1.5em; font-size: 18px; font-weight: 600; - max-width: 155px; - overflow: auto; + max-width: 150px; + border: none; + margin: 0px; + +} +.mx_DialPadContextMenu_dialled input{ + font-size: 18px; + font-weight: 600; + overflow: hidden; + text-align: left; + direction: rtl; + background-color: rgb(0, 0, 0, 0); } .mx_DialPadContextMenu_dialPad { diff --git a/src/components/views/context_menus/DialpadContextMenu.tsx b/src/components/views/context_menus/DialpadContextMenu.tsx index 17abce0c61..0a1d8184f2 100644 --- a/src/components/views/context_menus/DialpadContextMenu.tsx +++ b/src/components/views/context_menus/DialpadContextMenu.tsx @@ -18,6 +18,7 @@ import React from 'react'; import { _t } from '../../../languageHandler'; import { ContextMenu, IProps as IContextMenuProps } from '../../structures/ContextMenu'; import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call'; +import Field from "../elements/Field"; import Dialpad from '../voip/DialPad'; import {replaceableComponent} from "../../../utils/replaceableComponent"; @@ -44,13 +45,23 @@ export default class DialpadContextMenu extends React.Component this.setState({value: this.state.value + digit}); } + onChange = (ev) => { + this.setState({value: ev.target.value}); + } + + render() { return
{_t("Dial pad")}
-
{this.state.value}
+
+ +
From bd602e7089c0fb71a12deb3ed5c43f7ab4fa1763 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Thu, 29 Apr 2021 18:13:06 -0400 Subject: [PATCH 025/809] Hide world readable history option in encrypted rooms Signed-off-by: Robin Townsend --- .../tabs/room/SecurityRoomSettingsTab.tsx | 50 +++++++++++-------- src/i18n/strings/en_EN.json | 4 +- 2 files changed, 30 insertions(+), 24 deletions(-) diff --git a/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx b/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx index 02bbcfb751..21b132bac7 100644 --- a/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx +++ b/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx @@ -324,6 +324,33 @@ export default class SecurityRoomSettingsTab extends React.Component
@@ -334,28 +361,7 @@ export default class SecurityRoomSettingsTab extends React.Component
); diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 85e8e54258..37ecfa51a8 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1396,11 +1396,11 @@ "Only people who have been invited": "Only people who have been invited", "Anyone who knows the room's link, apart from guests": "Anyone who knows the room's link, apart from guests", "Anyone who knows the room's link, including guests": "Anyone who knows the room's link, including guests", - "Changes to who can read history will only apply to future messages in this room. The visibility of existing history will be unchanged.": "Changes to who can read history will only apply to future messages in this room. The visibility of existing history will be unchanged.", - "Anyone": "Anyone", "Members only (since the point in time of selecting this option)": "Members only (since the point in time of selecting this option)", "Members only (since they were invited)": "Members only (since they were invited)", "Members only (since they joined)": "Members only (since they joined)", + "Anyone": "Anyone", + "Changes to who can read history will only apply to future messages in this room. The visibility of existing history will be unchanged.": "Changes to who can read history will only apply to future messages in this room. The visibility of existing history will be unchanged.", "Who can read history?": "Who can read history?", "Security & Privacy": "Security & Privacy", "Once enabled, encryption cannot be disabled.": "Once enabled, encryption cannot be disabled.", From 192c0c4941a1aecad61b372d94c1759641e20b7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Fri, 30 Apr 2021 11:01:34 +0200 Subject: [PATCH 026/809] Fix method name MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- .../settings/tabs/user/PreferencesUserSettingsTab.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx index 24e4d8099e..bc2c120654 100644 --- a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx @@ -236,12 +236,12 @@ export default class PreferencesUserSettingsTab extends React.Component<{}, ISta
{_t("Keybindings")} - {this._renderGroup(PreferencesUserSettingsTab.KEYBINDINGS_SETTINGS)} + {this.renderGroup(PreferencesUserSettingsTab.KEYBINDINGS_SETTINGS)}
{_t("Displaying time")} - {this._renderGroup(PreferencesUserSettingsTab.TIME_SETTINGS)} + {this.renderGroup(PreferencesUserSettingsTab.TIME_SETTINGS)}
@@ -251,17 +251,17 @@ export default class PreferencesUserSettingsTab extends React.Component<{}, ISta
{_t("Code blocks")} - {this._renderGroup(PreferencesUserSettingsTab.CODE_BLOCKS_SETTINGS)} + {this.renderGroup(PreferencesUserSettingsTab.CODE_BLOCKS_SETTINGS)}
{_t("Images, GIFs and videos")} - {this._renderGroup(PreferencesUserSettingsTab.IMAGES_AND_VIDEOS_SETTINGS)} + {this.renderGroup(PreferencesUserSettingsTab.IMAGES_AND_VIDEOS_SETTINGS)}
{_t("Hide things on the timeline")} - {this._renderGroup(PreferencesUserSettingsTab.THINGS_TO_HIDE_ON_TIMELINE_SETTINGS)} + {this.renderGroup(PreferencesUserSettingsTab.THINGS_TO_HIDE_ON_TIMELINE_SETTINGS)}
From 2c7139fb4de1ca37bb674718651d02cd2c4fcba4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Fri, 30 Apr 2021 11:03:22 +0200 Subject: [PATCH 027/809] Merge timeline section into one MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- .../tabs/user/PreferencesUserSettingsTab.tsx | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx index bc2c120654..404bc974be 100644 --- a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx @@ -71,7 +71,7 @@ export default class PreferencesUserSettingsTab extends React.Component<{}, ISta 'autoplayGifsAndVideos', 'showImages', ]; - static THINGS_TO_HIDE_ON_TIMELINE_SETTINGS = [ + static TIMELINE_SETTINGS = [ 'showTypingNotifications', 'showRedactions', 'showReadReceipts', @@ -79,14 +79,10 @@ export default class PreferencesUserSettingsTab extends React.Component<{}, ISta 'showDisplaynameChanges', 'showChatEffects', 'showAvatarChanges', - ]; - - static TIMELINE_SETTINGS = [ + 'Pill.shouldShowPillAvatar', 'TextualBody.enableBigEmoji', 'scrollToBottomOnMessageSent', - 'Pill.shouldShowPillAvatar', ]; - static GENERAL_SETTINGS = [ 'TagPanel.enableTagPanel', 'promptBeforeInviteUnknownUsers', @@ -259,11 +255,6 @@ export default class PreferencesUserSettingsTab extends React.Component<{}, ISta {this.renderGroup(PreferencesUserSettingsTab.IMAGES_AND_VIDEOS_SETTINGS)}
-
- {_t("Hide things on the timeline")} - {this.renderGroup(PreferencesUserSettingsTab.THINGS_TO_HIDE_ON_TIMELINE_SETTINGS)} -
-
{_t("Timeline")} {this.renderGroup(PreferencesUserSettingsTab.TIMELINE_SETTINGS)} From 70c8c6477248466c2bdf0528da709124d886d8c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Fri, 30 Apr 2021 11:03:55 +0200 Subject: [PATCH 028/809] Rename to Keyboard shortcuts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- .../views/settings/tabs/user/PreferencesUserSettingsTab.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx index 404bc974be..7dc25d1879 100644 --- a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx @@ -231,7 +231,7 @@ export default class PreferencesUserSettingsTab extends React.Component<{}, ISta
- {_t("Keybindings")} + {_t("Keyboard shortcuts")} {this.renderGroup(PreferencesUserSettingsTab.KEYBINDINGS_SETTINGS)}
From 5f6895487f31668cc76f35c5d504533c0caafcdb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Fri, 30 Apr 2021 11:09:35 +0200 Subject: [PATCH 029/809] Rename ctrl+f MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/settings/Settings.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/settings/Settings.ts b/src/settings/Settings.ts index 2a26eeac13..2912f0a8ee 100644 --- a/src/settings/Settings.ts +++ b/src/settings/Settings.ts @@ -384,7 +384,7 @@ export const SETTINGS: {[setting: string]: ISetting} = { }, "ctrlFForSearch": { supportedLevels: LEVELS_ACCOUNT_SETTINGS, - displayName: isMac ? _td("Use Command + F to search") : _td("Use Ctrl + F to search"), + displayName: isMac ? _td("Use Command + F to search current room") : _td("Use Ctrl + F to search current room"), default: false, }, "MessageComposerInput.ctrlEnterToSend": { From e52fd3791e18d5e42ce5f2d2a50904d4430ffacc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Fri, 30 Apr 2021 11:10:45 +0200 Subject: [PATCH 030/809] i18n MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/i18n/strings/en_EN.json | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 69e77cf1bc..2e21016933 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -823,8 +823,8 @@ "Enable big emoji in chat": "Enable big emoji in chat", "Send typing notifications": "Send typing notifications", "Show typing notifications": "Show typing notifications", - "Use Command + F to search": "Use Command + F to search", - "Use Ctrl + F to search": "Use Ctrl + F to search", + "Use Command + F to search current room": "Use Command + F to search current room", + "Use Ctrl + F to search current room": "Use Ctrl + F to search current room", "Use Command + Enter to send a message": "Use Command + Enter to send a message", "Use Ctrl + Enter to send a message": "Use Ctrl + Enter to send a message", "Automatically replace plain text Emoji": "Automatically replace plain text Emoji", @@ -1296,12 +1296,11 @@ "Show tray icon and minimize window to it on close": "Show tray icon and minimize window to it on close", "Preferences": "Preferences", "Room list": "Room list", - "Keybindings": "Keybindings", + "Keyboard shortcuts": "Keyboard shortcuts", "Displaying time": "Displaying time", "Composer": "Composer", "Code blocks": "Code blocks", "Images, GIFs and videos": "Images, GIFs and videos", - "Hide things on the timeline": "Hide things on the timeline", "Timeline": "Timeline", "Autocomplete delay (ms)": "Autocomplete delay (ms)", "Read Marker lifetime (ms)": "Read Marker lifetime (ms)", From 330f222dd1a9df7aafe4488110be747d03fc5515 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sat, 1 May 2021 16:11:30 +0300 Subject: [PATCH 031/809] Remove redundant code and move presentableTextForFile out of MFileBody Signed-off-by: Tulir Asokan --- src/components/views/messages/MFileBody.js | 62 +++++++++--------- .../views/messages/MImageReplyBody.js | 24 +++---- src/components/views/rooms/EventTile.tsx | 37 +---------- src/components/views/rooms/ReplyPreview.js | 8 ++- src/components/views/rooms/ReplyTile.js | 64 ++----------------- 5 files changed, 55 insertions(+), 140 deletions(-) diff --git a/src/components/views/messages/MFileBody.js b/src/components/views/messages/MFileBody.js index 8f464e08bd..5be4468a28 100644 --- a/src/components/views/messages/MFileBody.js +++ b/src/components/views/messages/MFileBody.js @@ -89,6 +89,35 @@ function computedStyle(element) { return cssText; } +/** + * Extracts a human readable label for the file attachment to use as + * link text. + * + * @param {Object} content The "content" key of the matrix event. + * @param {boolean} withSize Whether to include size information. Default true. + * @return {string} the human readable link text for the attachment. + */ +export function presentableTextForFile(content, withSize = true) { + let linkText = _t("Attachment"); + if (content.body && content.body.length > 0) { + // The content body should be the name of the file including a + // file extension. + linkText = content.body; + } + + if (content.info && content.info.size && withSize) { + // If we know the size of the file then add it as human readable + // string to the end of the link text so that the user knows how + // big a file they are downloading. + // The content.info also contains a MIME-type but we don't display + // it since it is "ugly", users generally aren't aware what it + // means and the type of the attachment can usually be inferrered + // from the file extension. + linkText += ' (' + filesize(content.info.size) + ')'; + } + return linkText; +} + @replaceableComponent("views.messages.MFileBody") export default class MFileBody extends React.Component { static propTypes = { @@ -119,35 +148,6 @@ export default class MFileBody extends React.Component { this._dummyLink = createRef(); } - /** - * Extracts a human readable label for the file attachment to use as - * link text. - * - * @param {Object} content The "content" key of the matrix event. - * @param {boolean} withSize Whether to include size information. Default true. - * @return {string} the human readable link text for the attachment. - */ - presentableTextForFile(content, withSize = true) { - let linkText = _t("Attachment"); - if (content.body && content.body.length > 0) { - // The content body should be the name of the file including a - // file extension. - linkText = content.body; - } - - if (content.info && content.info.size && withSize) { - // If we know the size of the file then add it as human readable - // string to the end of the link text so that the user knows how - // big a file they are downloading. - // The content.info also contains a MIME-type but we don't display - // it since it is "ugly", users generally aren't aware what it - // means and the type of the attachment can usually be inferrered - // from the file extension. - linkText += ' (' + filesize(content.info.size) + ')'; - } - return linkText; - } - _getContentUrl() { const media = mediaFromContent(this.props.mxEvent.getContent()); return media.srcHttp; @@ -161,7 +161,7 @@ export default class MFileBody extends React.Component { render() { const content = this.props.mxEvent.getContent(); - const text = this.presentableTextForFile(content); + const text = presentableTextForFile(content); const isEncrypted = content.file !== undefined; const fileName = content.body && content.body.length > 0 ? content.body : _t("Attachment"); const contentUrl = this._getContentUrl(); @@ -173,7 +173,7 @@ export default class MFileBody extends React.Component { placeholder = (
- {this.presentableTextForFile(content, false)} + {presentableTextForFile(content, false)}
); } diff --git a/src/components/views/messages/MImageReplyBody.js b/src/components/views/messages/MImageReplyBody.js index cdc78e46e8..5ace22a560 100644 --- a/src/components/views/messages/MImageReplyBody.js +++ b/src/components/views/messages/MImageReplyBody.js @@ -1,5 +1,5 @@ /* -Copyright 2020 Tulir Asokan +Copyright 2020-2021 Tulir Asokan Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -15,10 +15,10 @@ limitations under the License. */ import React from "react"; -import { _td } from "../../../languageHandler"; +import {_td} from "../../../languageHandler"; import * as sdk from "../../../index"; import MImageBody from './MImageBody'; -import MFileBody from "./MFileBody"; +import {presentableTextForFile} from "./MFileBody"; export default class MImageReplyBody extends MImageBody { onClick(ev) { @@ -31,7 +31,7 @@ export default class MImageReplyBody extends MImageBody { // Don't show "Download this_file.png ..." getFileBody() { - return MFileBody.prototype.presentableTextForFile.call(this, this.props.mxEvent.getContent()); + return presentableTextForFile(this.props.mxEvent.getContent()); } render() { @@ -45,15 +45,17 @@ export default class MImageReplyBody extends MImageBody { const thumbnail = this._messageContent(contentUrl, this._getThumbUrl(), content); const fileBody = this.getFileBody(); const SenderProfile = sdk.getComponent('messages.SenderProfile'); - const sender = ; + const sender = ; return
-
{ thumbnail }
-
{ sender }
-
{ fileBody }
+
{thumbnail}
+
{sender}
+
{fileBody}
; } } diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index 19c5a7acaa..4411f94f02 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -247,7 +247,7 @@ interface IProps { // It could also be done by subclassing EventTile, but that'd be quite // boiilerplatey. So just make the necessary render decisions conditional // for now. - tileShape?: 'notif' | 'file_grid' | 'reply' | 'reply_preview'; + tileShape?: 'notif' | 'file_grid'; // show twelve hour timestamps isTwelveHour?: boolean; @@ -940,7 +940,7 @@ export default class EventTile extends React.Component { } if (needsSenderProfile) { - if (!this.props.tileShape || this.props.tileShape === 'reply' || this.props.tileShape === 'reply_preview') { + if (!this.props.tileShape) { sender = { ); } - case 'reply': - case 'reply_preview': { - let thread; - if (this.props.tileShape === 'reply_preview') { - thread = ReplyThread.makeThread( - this.props.mxEvent, - this.props.onHeightChanged, - this.props.permalinkCreator, - this.replyThread, - ); - } - return ( -
- { ircTimestamp } - { avatar } - { sender } - { ircPadlock } -
- { groupTimestamp } - { groupPadlock } - { thread } - -
-
- ); - } default: { const thread = ReplyThread.makeThread( this.props.mxEvent, diff --git a/src/components/views/rooms/ReplyPreview.js b/src/components/views/rooms/ReplyPreview.js index 56a6609cc7..222fcea552 100644 --- a/src/components/views/rooms/ReplyPreview.js +++ b/src/components/views/rooms/ReplyPreview.js @@ -87,9 +87,11 @@ export default class ReplyPreview extends React.Component {
- +
; diff --git a/src/components/views/rooms/ReplyTile.js b/src/components/views/rooms/ReplyTile.js index 95503493f7..336c5a721b 100644 --- a/src/components/views/rooms/ReplyTile.js +++ b/src/components/views/rooms/ReplyTile.js @@ -1,5 +1,5 @@ /* -Copyright 2020 Tulir Asokan +Copyright 2020-2021 Tulir Asokan Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -25,42 +25,8 @@ import dis from '../../../dispatcher/dispatcher'; import SettingsStore from "../../../settings/SettingsStore"; import {MatrixClient} from 'matrix-js-sdk'; -import { objectHasDiff } from '../../../utils/objects'; - -const eventTileTypes = { - 'm.room.message': 'messages.MessageEvent', - 'm.sticker': 'messages.MessageEvent', - 'm.call.invite': 'messages.TextualEvent', - 'm.call.answer': 'messages.TextualEvent', - 'm.call.hangup': 'messages.TextualEvent', -}; - -const stateEventTileTypes = { - 'm.room.aliases': 'messages.TextualEvent', - // 'm.room.aliases': 'messages.RoomAliasesEvent', // too complex - 'm.room.canonical_alias': 'messages.TextualEvent', - 'm.room.create': 'messages.RoomCreate', - 'm.room.member': 'messages.TextualEvent', - 'm.room.name': 'messages.TextualEvent', - 'm.room.avatar': 'messages.RoomAvatarEvent', - 'm.room.third_party_invite': 'messages.TextualEvent', - 'm.room.history_visibility': 'messages.TextualEvent', - 'm.room.encryption': 'messages.TextualEvent', - 'm.room.topic': 'messages.TextualEvent', - 'm.room.power_levels': 'messages.TextualEvent', - 'm.room.pinned_events': 'messages.TextualEvent', - 'm.room.server_acl': 'messages.TextualEvent', - 'im.vector.modular.widgets': 'messages.TextualEvent', - 'm.room.tombstone': 'messages.TextualEvent', - 'm.room.join_rules': 'messages.TextualEvent', - 'm.room.guest_access': 'messages.TextualEvent', - 'm.room.related_groups': 'messages.TextualEvent', -}; - -function getHandlerTile(ev) { - const type = ev.getType(); - return ev.isState() ? stateEventTileTypes[type] : eventTileTypes[type]; -} +import {objectHasDiff} from '../../../utils/objects'; +import {getHandlerTile} from "./EventTile"; class ReplyTile extends React.Component { static contextTypes = { @@ -94,7 +60,7 @@ class ReplyTile extends React.Component { return true; } - return !this._propsEqual(this.props, nextProps); + return objectHasDiff(this.props, nextProps); } componentWillUnmount() { @@ -108,28 +74,6 @@ class ReplyTile extends React.Component { } } - _propsEqual(objA, objB) { - const keysA = Object.keys(objA); - const keysB = Object.keys(objB); - - if (keysA.length !== keysB.length) { - return false; - } - - for (let i = 0; i < keysA.length; i++) { - const key = keysA[i]; - - if (!objB.hasOwnProperty(key)) { - return false; - } - - if (objA[key] !== objB[key]) { - return false; - } - } - return true; - } - onClick(e) { // This allows the permalink to be opened in a new tab/window or copied as // matrix.to, but also for it to enable routing within Riot when clicked. From bebcb32e8fb4270632c4c2a2a85a2271a8b121b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sun, 2 May 2021 16:23:35 +0200 Subject: [PATCH 032/809] Add dragCallbacks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/voip/CallView.tsx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/components/views/voip/CallView.tsx b/src/components/views/voip/CallView.tsx index 6745713845..65547bb814 100644 --- a/src/components/views/voip/CallView.tsx +++ b/src/components/views/voip/CallView.tsx @@ -49,6 +49,13 @@ interface IProps { // This is sort of a proxy for a number of things but we currently have no // need to control those things separately, so this is simpler. pipMode?: boolean; + + // Callbacks for dragging the CallView in PIP mode + dragCallbacks?: { + onStartMoving: (event: React.MouseEvent) => void; + onMoving: (event: React.MouseEvent) => void; + onEndMoving: () => void; + } } interface IState { From 8948c7419cdbd587550e20720ea0bb6f11a44358 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sun, 2 May 2021 16:24:47 +0200 Subject: [PATCH 033/809] Call dragCallbacks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/voip/CallView.tsx | 30 ++++++++++++++++---------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/src/components/views/voip/CallView.tsx b/src/components/views/voip/CallView.tsx index 65547bb814..c35e62448e 100644 --- a/src/components/views/voip/CallView.tsx +++ b/src/components/views/voip/CallView.tsx @@ -623,19 +623,27 @@ export default class CallView extends React.Component { ; } - header =
- - - -
-
{callRoom.name}
-
- {callTypeText} - {secondaryCallInfo} + header = ( +
+ + + +
+
{callRoom.name}
+
+ {callTypeText} + {secondaryCallInfo} +
+ {headerControls}
- {headerControls} -
; + ); myClassName = 'mx_CallView_pip'; } From c97bbe11a93eb9c49ce45b4f1a4c2df280b344a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sun, 2 May 2021 16:26:03 +0200 Subject: [PATCH 034/809] Prep state and props for dragging MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/voip/CallPreview.tsx | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/components/views/voip/CallPreview.tsx b/src/components/views/voip/CallPreview.tsx index d31afddec9..8dca5314e5 100644 --- a/src/components/views/voip/CallPreview.tsx +++ b/src/components/views/voip/CallPreview.tsx @@ -50,6 +50,13 @@ interface IState { // Any other call we're displaying: only if the user is on two calls and not viewing either of the rooms // they belong to secondaryCall: MatrixCall; + + // Position of the CallPreview + translationX: number; + translationY: number; + + // True if the CallPreview is being dragged + moving: boolean; } // Splits a list of calls into one 'primary' one and a list @@ -106,9 +113,17 @@ export default class CallPreview extends React.Component { roomId, primaryCall: primaryCall, secondaryCall: secondaryCalls[0], + translationX: 0, + translationY: 0, + moving: false, }; } + private initX = 0; + private initY = 0; + private lastX = 0; + private lastY = 0; + public componentDidMount() { this.roomStoreToken = RoomViewStore.addListener(this.onRoomViewStoreUpdate); this.dispatcherRef = dis.register(this.onAction); From f64a9501955e5506b09e35009003133edf268dc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sun, 2 May 2021 16:26:41 +0200 Subject: [PATCH 035/809] Prep basic methods for dragging MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/voip/CallPreview.tsx | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/components/views/voip/CallPreview.tsx b/src/components/views/voip/CallPreview.tsx index 8dca5314e5..68fcef6747 100644 --- a/src/components/views/voip/CallPreview.tsx +++ b/src/components/views/voip/CallPreview.tsx @@ -184,6 +184,29 @@ export default class CallPreview extends React.Component { }); } + private onStartMoving = (event: React.MouseEvent) => { + this.setState({moving: true}); + + this.initX = event.pageX - this.lastX; + this.initY = event.pageY - this.lastY; + } + + private onMoving = (event: React.MouseEvent) => { + if (!this.state.moving) return; + + this.lastX = event.pageX - this.initX; + this.lastY = event.pageY - this.initY; + + this.setState({ + translationX: this.lastX, + translationY: this.lastY, + }); + } + + private onEndMoving = () => { + this.setState({moving: false}); + } + public render() { if (this.state.primaryCall) { return ( From 11222e7a467a6e58007ae14e539856a21251bd20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sun, 2 May 2021 16:26:54 +0200 Subject: [PATCH 036/809] Wire up dragging MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/voip/CallPreview.tsx | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/components/views/voip/CallPreview.tsx b/src/components/views/voip/CallPreview.tsx index 68fcef6747..499cdfb526 100644 --- a/src/components/views/voip/CallPreview.tsx +++ b/src/components/views/voip/CallPreview.tsx @@ -209,8 +209,26 @@ export default class CallPreview extends React.Component { public render() { if (this.state.primaryCall) { + const translatePixelsX = this.state.translationX + "px"; + const translatePixelsY = this.state.translationY + "px"; + const style = { + transform: `translateX(${translatePixelsX}) + translateY(${translatePixelsY})`, + }; + return ( - +
+ +
); } From b42872daa409d27ad4634d2418fee2bfa7dccfea Mon Sep 17 00:00:00 2001 From: Ayush PS Date: Sun, 2 May 2021 22:10:15 +0530 Subject: [PATCH 037/809] Fixed the Dial Pad and finalized the changes --- res/css/views/voip/_DialPadContextMenu.scss | 5 +++-- .../views/context_menus/DialpadContextMenu.tsx | 12 +++++------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/res/css/views/voip/_DialPadContextMenu.scss b/res/css/views/voip/_DialPadContextMenu.scss index c01ce0f2d9..31327113cf 100644 --- a/res/css/views/voip/_DialPadContextMenu.scss +++ b/res/css/views/voip/_DialPadContextMenu.scss @@ -33,14 +33,15 @@ limitations under the License. max-width: 150px; border: none; margin: 0px; - } -.mx_DialPadContextMenu_dialled input{ +.mx_DialPadContextMenu_dialled input { font-size: 18px; font-weight: 600; overflow: hidden; + max-width: 150px; text-align: left; direction: rtl; + padding: 8px 0px; background-color: rgb(0, 0, 0, 0); } diff --git a/src/components/views/context_menus/DialpadContextMenu.tsx b/src/components/views/context_menus/DialpadContextMenu.tsx index 0a1d8184f2..8879629055 100644 --- a/src/components/views/context_menus/DialpadContextMenu.tsx +++ b/src/components/views/context_menus/DialpadContextMenu.tsx @@ -48,7 +48,7 @@ export default class DialpadContextMenu extends React.Component onChange = (ev) => { this.setState({value: ev.target.value}); } - + render() { return @@ -56,12 +56,10 @@ export default class DialpadContextMenu extends React.Component
{_t("Dial pad")}
-
- - +
From 241e626e96a85fcf48a028f3af418ccd14e5235f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sun, 2 May 2021 20:55:05 +0200 Subject: [PATCH 038/809] Don't listen for onMouseLeave MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This would cause problems because the moving element wouldn't catch up with the user Signed-off-by: Šimon Brandner --- src/components/views/voip/CallView.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/views/voip/CallView.tsx b/src/components/views/voip/CallView.tsx index c35e62448e..23a4fcca59 100644 --- a/src/components/views/voip/CallView.tsx +++ b/src/components/views/voip/CallView.tsx @@ -629,7 +629,6 @@ export default class CallView extends React.Component { onMouseDown={this.props.dragCallbacks?.onStartMoving} onMouseMove={this.props.dragCallbacks?.onMoving} onMouseUp={this.props.dragCallbacks?.onEndMoving} - onMouseLeave={this.props.dragCallbacks?.onEndMoving} > From 53b8fd3072f8dddcb4e5e8b44da6d77c3a1e6ac3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sun, 2 May 2021 20:57:18 +0200 Subject: [PATCH 039/809] Listen for mousemove on document scale MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/voip/CallPreview.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/views/voip/CallPreview.tsx b/src/components/views/voip/CallPreview.tsx index 499cdfb526..17e2e9cf1a 100644 --- a/src/components/views/voip/CallPreview.tsx +++ b/src/components/views/voip/CallPreview.tsx @@ -126,12 +126,14 @@ export default class CallPreview extends React.Component { public componentDidMount() { this.roomStoreToken = RoomViewStore.addListener(this.onRoomViewStoreUpdate); + document.addEventListener("mousemove", this.onMoving); this.dispatcherRef = dis.register(this.onAction); MatrixClientPeg.get().on(CallEvent.RemoteHoldUnhold, this.onCallRemoteHold); } public componentWillUnmount() { MatrixClientPeg.get().removeListener(CallEvent.RemoteHoldUnhold, this.onCallRemoteHold); + document.removeEventListener("mousemove", this.onMoving); if (this.roomStoreToken) { this.roomStoreToken.remove(); } @@ -191,7 +193,7 @@ export default class CallPreview extends React.Component { this.initY = event.pageY - this.lastY; } - private onMoving = (event: React.MouseEvent) => { + private onMoving = (event: React.MouseEvent | MouseEvent) => { if (!this.state.moving) return; this.lastX = event.pageX - this.initX; From fca5347668465341ef0757c56de0adb78235741a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sun, 2 May 2021 21:17:59 +0200 Subject: [PATCH 040/809] Add preventDefault() and stopPropagation() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This avoids text being selected while dragging Signed-off-by: Šimon Brandner --- src/components/views/voip/CallPreview.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/components/views/voip/CallPreview.tsx b/src/components/views/voip/CallPreview.tsx index 17e2e9cf1a..761458cb4c 100644 --- a/src/components/views/voip/CallPreview.tsx +++ b/src/components/views/voip/CallPreview.tsx @@ -187,6 +187,9 @@ export default class CallPreview extends React.Component { } private onStartMoving = (event: React.MouseEvent) => { + event.preventDefault(); + event.stopPropagation(); + this.setState({moving: true}); this.initX = event.pageX - this.lastX; @@ -194,6 +197,9 @@ export default class CallPreview extends React.Component { } private onMoving = (event: React.MouseEvent | MouseEvent) => { + event.preventDefault(); + event.stopPropagation(); + if (!this.state.moving) return; this.lastX = event.pageX - this.initX; From 51e80dd17228f5645c0b7bbab42206a0bffa43fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 3 May 2021 07:50:21 +0200 Subject: [PATCH 041/809] Remove onMoving listner from CallView MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is not necessary since we already listen for it in CallPreview Signed-off-by: Šimon Brandner --- src/components/views/voip/CallView.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/components/views/voip/CallView.tsx b/src/components/views/voip/CallView.tsx index 23a4fcca59..cbedfb3a3d 100644 --- a/src/components/views/voip/CallView.tsx +++ b/src/components/views/voip/CallView.tsx @@ -53,7 +53,6 @@ interface IProps { // Callbacks for dragging the CallView in PIP mode dragCallbacks?: { onStartMoving: (event: React.MouseEvent) => void; - onMoving: (event: React.MouseEvent) => void; onEndMoving: () => void; } } @@ -627,7 +626,6 @@ export default class CallView extends React.Component {
From 7042eb38ddb7f838b2fe8dc952aef7c02f45e3e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 3 May 2021 08:12:54 +0200 Subject: [PATCH 042/809] Listen for mouseup on the document MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/voip/CallPreview.tsx | 2 ++ src/components/views/voip/CallView.tsx | 1 - 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/views/voip/CallPreview.tsx b/src/components/views/voip/CallPreview.tsx index 761458cb4c..aa2e71339e 100644 --- a/src/components/views/voip/CallPreview.tsx +++ b/src/components/views/voip/CallPreview.tsx @@ -127,6 +127,7 @@ export default class CallPreview extends React.Component { public componentDidMount() { this.roomStoreToken = RoomViewStore.addListener(this.onRoomViewStoreUpdate); document.addEventListener("mousemove", this.onMoving); + document.addEventListener("mouseup", this.onEndMoving); this.dispatcherRef = dis.register(this.onAction); MatrixClientPeg.get().on(CallEvent.RemoteHoldUnhold, this.onCallRemoteHold); } @@ -134,6 +135,7 @@ export default class CallPreview extends React.Component { public componentWillUnmount() { MatrixClientPeg.get().removeListener(CallEvent.RemoteHoldUnhold, this.onCallRemoteHold); document.removeEventListener("mousemove", this.onMoving); + document.removeEventListener("mouseup", this.onEndMoving); if (this.roomStoreToken) { this.roomStoreToken.remove(); } diff --git a/src/components/views/voip/CallView.tsx b/src/components/views/voip/CallView.tsx index cbedfb3a3d..1f555e5227 100644 --- a/src/components/views/voip/CallView.tsx +++ b/src/components/views/voip/CallView.tsx @@ -626,7 +626,6 @@ export default class CallView extends React.Component {
From 24bf1b22a440ef810fac33b975015c65d1a3f03d Mon Sep 17 00:00:00 2001 From: Ayush PS Date: Mon, 3 May 2021 13:54:42 +0530 Subject: [PATCH 043/809] Removes the override on the Bubble Container --- res/css/views/rooms/_EventTile.scss | 2 -- 1 file changed, 2 deletions(-) diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index 2b3e179c54..5e110f9c6d 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -123,8 +123,6 @@ $left-gutter: 64px; .mx_EventTile_line { margin-right: 0; grid-column: 1 / 3; - // override default padding of mx_EventTile_line so that we can be centered - padding: 0 !important; } .mx_EventTile_msgOption { From adcdd72a0838e1eddd736cbafb0cb1883cfdf1bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 3 May 2021 15:07:25 +0200 Subject: [PATCH 044/809] preventDefault() and stopPropagation() only if moving MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/voip/CallPreview.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/voip/CallPreview.tsx b/src/components/views/voip/CallPreview.tsx index aa2e71339e..3b7a297841 100644 --- a/src/components/views/voip/CallPreview.tsx +++ b/src/components/views/voip/CallPreview.tsx @@ -199,11 +199,11 @@ export default class CallPreview extends React.Component { } private onMoving = (event: React.MouseEvent | MouseEvent) => { + if (!this.state.moving) return; + event.preventDefault(); event.stopPropagation(); - if (!this.state.moving) return; - this.lastX = event.pageX - this.initX; this.lastY = event.pageY - this.initY; From 0851cf4415f3dd04502326909cee1b6400ea73ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 3 May 2021 15:16:08 +0200 Subject: [PATCH 045/809] Simplifie things MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/voip/CallPreview.tsx | 6 +----- src/components/views/voip/CallView.tsx | 9 +++------ 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/src/components/views/voip/CallPreview.tsx b/src/components/views/voip/CallPreview.tsx index 3b7a297841..153258d2c8 100644 --- a/src/components/views/voip/CallPreview.tsx +++ b/src/components/views/voip/CallPreview.tsx @@ -232,11 +232,7 @@ export default class CallPreview extends React.Component { call={this.state.primaryCall} secondaryCall={this.state.secondaryCall} pipMode={true} - dragCallbacks={{ - onStartMoving: this.onStartMoving, - onMoving: this.onMoving, - onEndMoving: this.onEndMoving, - }} + onMouseDownOnHeader={this.onStartMoving} />
); diff --git a/src/components/views/voip/CallView.tsx b/src/components/views/voip/CallView.tsx index 1f555e5227..e8d3666c53 100644 --- a/src/components/views/voip/CallView.tsx +++ b/src/components/views/voip/CallView.tsx @@ -50,11 +50,8 @@ interface IProps { // need to control those things separately, so this is simpler. pipMode?: boolean; - // Callbacks for dragging the CallView in PIP mode - dragCallbacks?: { - onStartMoving: (event: React.MouseEvent) => void; - onEndMoving: () => void; - } + // Used for dragging the PiP CallView + onMouseDownOnHeader?: (event: React.MouseEvent) => void; } interface IState { @@ -625,7 +622,7 @@ export default class CallView extends React.Component { header = (
From b8cb72345ccbe115ba3bdb861467ee139ecff20d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 3 May 2021 15:39:00 +0200 Subject: [PATCH 046/809] Remove unnecessary margin MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/css/views/voip/_CallView.scss | 1 - 1 file changed, 1 deletion(-) diff --git a/res/css/views/voip/_CallView.scss b/res/css/views/voip/_CallView.scss index 7292e325df..18e7c215cb 100644 --- a/res/css/views/voip/_CallView.scss +++ b/res/css/views/voip/_CallView.scss @@ -39,7 +39,6 @@ limitations under the License. .mx_CallView_pip { width: 320px; padding-bottom: 8px; - margin-top: 10px; background-color: $voipcall-plinth-color; box-shadow: 0px 4px 20px rgba(0, 0, 0, 0.20); border-radius: 8px; From fe5fb1885fc95243a9964942877268f7e6eef993 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 3 May 2021 15:39:24 +0200 Subject: [PATCH 047/809] Add styling for CallPreview and make it fixed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/css/views/voip/_CallPreview.scss | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 res/css/views/voip/_CallPreview.scss diff --git a/res/css/views/voip/_CallPreview.scss b/res/css/views/voip/_CallPreview.scss new file mode 100644 index 0000000000..92348fb465 --- /dev/null +++ b/res/css/views/voip/_CallPreview.scss @@ -0,0 +1,21 @@ +/* +Copyright 2021 Šimon Brandner + +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_CallPreview { + position: fixed; + left: 0; + top: 0; +} From 7faf9eb4ccd56dfe9ab308964471a5ff099c805b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 3 May 2021 15:39:37 +0200 Subject: [PATCH 048/809] Use styling for CallPreview MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/css/_components.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/res/css/_components.scss b/res/css/_components.scss index 0057f8a8fc..c476e577df 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -254,6 +254,7 @@ @import "./views/voip/_CallContainer.scss"; @import "./views/voip/_CallView.scss"; @import "./views/voip/_CallViewForRoom.scss"; +@import "./views/voip/_CallPreview.scss"; @import "./views/voip/_DialPad.scss"; @import "./views/voip/_DialPadContextMenu.scss"; @import "./views/voip/_DialPadModal.scss"; From 76f503666c5e7594751302a5a5fabc9babc8a64e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 3 May 2021 15:40:12 +0200 Subject: [PATCH 049/809] Add default offset MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/voip/CallPreview.tsx | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/components/views/voip/CallPreview.tsx b/src/components/views/voip/CallPreview.tsx index 153258d2c8..74e1815631 100644 --- a/src/components/views/voip/CallPreview.tsx +++ b/src/components/views/voip/CallPreview.tsx @@ -29,6 +29,9 @@ import { MatrixClientPeg } from '../../../MatrixClientPeg'; import {replaceableComponent} from "../../../utils/replaceableComponent"; import { Action } from '../../../dispatcher/actions'; +const DEFAULT_X_OFFSET = 64; +const DEFAULT_Y_OFFSET = 64; + const SHOW_CALL_IN_STATES = [ CallState.Connected, CallState.InviteSent, @@ -113,16 +116,16 @@ export default class CallPreview extends React.Component { roomId, primaryCall: primaryCall, secondaryCall: secondaryCalls[0], - translationX: 0, - translationY: 0, + translationX: DEFAULT_X_OFFSET, + translationY: DEFAULT_Y_OFFSET, moving: false, }; } private initX = 0; private initY = 0; - private lastX = 0; - private lastY = 0; + private lastX = DEFAULT_X_OFFSET; + private lastY = DEFAULT_Y_OFFSET; public componentDidMount() { this.roomStoreToken = RoomViewStore.addListener(this.onRoomViewStoreUpdate); From 2c9231641b57ea7d5fa577c9aa9445347360665f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 3 May 2021 15:40:59 +0200 Subject: [PATCH 050/809] Add ref to callViewWrapper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/voip/CallPreview.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/components/views/voip/CallPreview.tsx b/src/components/views/voip/CallPreview.tsx index 74e1815631..4a0ccc93b9 100644 --- a/src/components/views/voip/CallPreview.tsx +++ b/src/components/views/voip/CallPreview.tsx @@ -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 CallView from "./CallView"; import RoomViewStore from '../../../stores/RoomViewStore'; @@ -122,6 +122,8 @@ export default class CallPreview extends React.Component { }; } + private callViewWrapper = createRef(); + private initX = 0; private initY = 0; private lastX = DEFAULT_X_OFFSET; @@ -230,7 +232,11 @@ export default class CallPreview extends React.Component { }; return ( -
+
Date: Mon, 3 May 2021 15:43:13 +0200 Subject: [PATCH 051/809] Add semicolons to event listeners MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/voip/CallPreview.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/views/voip/CallPreview.tsx b/src/components/views/voip/CallPreview.tsx index 4a0ccc93b9..366b0d30b3 100644 --- a/src/components/views/voip/CallPreview.tsx +++ b/src/components/views/voip/CallPreview.tsx @@ -191,7 +191,7 @@ export default class CallPreview extends React.Component { primaryCall: primaryCall, secondaryCall: secondaryCalls[0], }); - } + }; private onStartMoving = (event: React.MouseEvent) => { event.preventDefault(); @@ -201,7 +201,7 @@ export default class CallPreview extends React.Component { this.initX = event.pageX - this.lastX; this.initY = event.pageY - this.lastY; - } + }; private onMoving = (event: React.MouseEvent | MouseEvent) => { if (!this.state.moving) return; @@ -216,11 +216,11 @@ export default class CallPreview extends React.Component { translationX: this.lastX, translationY: this.lastY, }); - } + }; private onEndMoving = () => { this.setState({moving: false}); - } + }; public render() { if (this.state.primaryCall) { From d8d380c74de6ff27d9e0b6c764e1db35b583119e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 3 May 2021 17:22:45 +0200 Subject: [PATCH 052/809] Always keep the PiP CallView on the screen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/voip/CallPreview.tsx | 25 +++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/src/components/views/voip/CallPreview.tsx b/src/components/views/voip/CallPreview.tsx index 366b0d30b3..74d43dcb19 100644 --- a/src/components/views/voip/CallPreview.tsx +++ b/src/components/views/voip/CallPreview.tsx @@ -209,8 +209,29 @@ export default class CallPreview extends React.Component { event.preventDefault(); event.stopPropagation(); - this.lastX = event.pageX - this.initX; - this.lastY = event.pageY - this.initY; + const width = this.callViewWrapper.current.clientWidth; + const height = this.callViewWrapper.current.clientHeight; + + const precalculatedLastX = event.pageX - this.initX; + const precalculatedLastY = event.pageY - this.initY; + + // Avoid overflow on the x axis + if (precalculatedLastX + width >= window.innerWidth) { + this.lastX = window.innerWidth - width; + } else if (precalculatedLastX <= 0) { + this.lastX = 0; + } else { + this.lastX = precalculatedLastX; + } + + // Avoid overflow on the y axis + if (precalculatedLastY + height >= window.innerHeight) { + this.lastY = window.innerHeight - height; + } else if (precalculatedLastY <= 0) { + this.lastY = 0; + } else { + this.lastY = precalculatedLastY; + } this.setState({ translationX: this.lastX, From be2da6376e1c5b3a6be2c23a3bfbe89766e5bbc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 3 May 2021 17:49:55 +0200 Subject: [PATCH 053/809] Simplifie translation code MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/voip/CallPreview.tsx | 25 ++++++++++++----------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/src/components/views/voip/CallPreview.tsx b/src/components/views/voip/CallPreview.tsx index 74d43dcb19..9a9ebd5e92 100644 --- a/src/components/views/voip/CallPreview.tsx +++ b/src/components/views/voip/CallPreview.tsx @@ -126,8 +126,6 @@ export default class CallPreview extends React.Component { private initX = 0; private initY = 0; - private lastX = DEFAULT_X_OFFSET; - private lastY = DEFAULT_Y_OFFSET; public componentDidMount() { this.roomStoreToken = RoomViewStore.addListener(this.onRoomViewStoreUpdate); @@ -199,8 +197,8 @@ export default class CallPreview extends React.Component { this.setState({moving: true}); - this.initX = event.pageX - this.lastX; - this.initY = event.pageY - this.lastY; + this.initX = event.pageX - this.state.translationX; + this.initY = event.pageY - this.state.translationY; }; private onMoving = (event: React.MouseEvent | MouseEvent) => { @@ -215,27 +213,30 @@ export default class CallPreview extends React.Component { const precalculatedLastX = event.pageX - this.initX; const precalculatedLastY = event.pageY - this.initY; + let translationX; + let translationY; + // Avoid overflow on the x axis if (precalculatedLastX + width >= window.innerWidth) { - this.lastX = window.innerWidth - width; + translationX = window.innerWidth - width; } else if (precalculatedLastX <= 0) { - this.lastX = 0; + translationX = 0; } else { - this.lastX = precalculatedLastX; + translationX = precalculatedLastX; } // Avoid overflow on the y axis if (precalculatedLastY + height >= window.innerHeight) { - this.lastY = window.innerHeight - height; + translationY = window.innerHeight - height; } else if (precalculatedLastY <= 0) { - this.lastY = 0; + translationY = 0; } else { - this.lastY = precalculatedLastY; + translationY = precalculatedLastY; } this.setState({ - translationX: this.lastX, - translationY: this.lastY, + translationX: translationX, + translationY: translationY, }); }; From 0bf2b01f84ec490ca48b8d07172202dab7907dcc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 3 May 2021 18:11:02 +0200 Subject: [PATCH 054/809] Keep PiP in the window when resizing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/voip/CallPreview.tsx | 41 ++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/src/components/views/voip/CallPreview.tsx b/src/components/views/voip/CallPreview.tsx index 9a9ebd5e92..410b60dcb6 100644 --- a/src/components/views/voip/CallPreview.tsx +++ b/src/components/views/voip/CallPreview.tsx @@ -29,6 +29,9 @@ import { MatrixClientPeg } from '../../../MatrixClientPeg'; import {replaceableComponent} from "../../../utils/replaceableComponent"; import { Action } from '../../../dispatcher/actions'; +const PIP_VIEW_WIDTH = 320; +const PIP_VIEW_HEIGHT = 180; + const DEFAULT_X_OFFSET = 64; const DEFAULT_Y_OFFSET = 64; @@ -116,7 +119,7 @@ export default class CallPreview extends React.Component { roomId, primaryCall: primaryCall, secondaryCall: secondaryCalls[0], - translationX: DEFAULT_X_OFFSET, + translationX: window.innerWidth - DEFAULT_X_OFFSET - PIP_VIEW_WIDTH, translationY: DEFAULT_Y_OFFSET, moving: false, }; @@ -131,6 +134,7 @@ export default class CallPreview extends React.Component { this.roomStoreToken = RoomViewStore.addListener(this.onRoomViewStoreUpdate); document.addEventListener("mousemove", this.onMoving); document.addEventListener("mouseup", this.onEndMoving); + window.addEventListener("resize", this.onWindowSizeChanged); this.dispatcherRef = dis.register(this.onAction); MatrixClientPeg.get().on(CallEvent.RemoteHoldUnhold, this.onCallRemoteHold); } @@ -139,6 +143,7 @@ export default class CallPreview extends React.Component { MatrixClientPeg.get().removeListener(CallEvent.RemoteHoldUnhold, this.onCallRemoteHold); document.removeEventListener("mousemove", this.onMoving); document.removeEventListener("mouseup", this.onEndMoving); + window.removeEventListener("resize", this.onWindowSizeChanged); if (this.roomStoreToken) { this.roomStoreToken.remove(); } @@ -146,6 +151,40 @@ export default class CallPreview extends React.Component { SettingsStore.unwatchSetting(this.settingsWatcherRef); } + private onWindowSizeChanged = () => { + const width = this.callViewWrapper.current.clientWidth || PIP_VIEW_WIDTH; + const height = this.callViewWrapper.current.clientHeight || PIP_VIEW_HEIGHT; + + const precalculatedLastX = this.state.translationX; + const precalculatedLastY = this.state.translationY; + + let translationX; + let translationY; + + // Avoid overflow on the x axis + if (precalculatedLastX + width >= window.innerWidth) { + translationX = window.innerWidth - width; + } else if (precalculatedLastX <= 0) { + translationX = 0; + } else { + translationX = precalculatedLastX; + } + + // Avoid overflow on the y axis + if (precalculatedLastY + height >= window.innerHeight) { + translationY = window.innerHeight - height; + } else if (precalculatedLastY <= 0) { + translationY = 0; + } else { + translationY = precalculatedLastY; + } + + this.setState({ + translationX: translationX, + translationY: translationY, + }); + } + private onRoomViewStoreUpdate = (payload) => { if (RoomViewStore.getRoomId() === this.state.roomId) return; From 941a6e1c1bb48f2214638768f17018f4f0b4e77f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 3 May 2021 18:16:36 +0200 Subject: [PATCH 055/809] Don't duplicate code MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/voip/CallPreview.tsx | 67 +++++++---------------- 1 file changed, 19 insertions(+), 48 deletions(-) diff --git a/src/components/views/voip/CallPreview.tsx b/src/components/views/voip/CallPreview.tsx index 410b60dcb6..9ab2b9441a 100644 --- a/src/components/views/voip/CallPreview.tsx +++ b/src/components/views/voip/CallPreview.tsx @@ -152,36 +152,37 @@ export default class CallPreview extends React.Component { } private onWindowSizeChanged = () => { + this.setTranslation(this.state.translationX, this.state.translationY); + } + + private setTranslation(inTranslationX: number, inTranslationY: number) { const width = this.callViewWrapper.current.clientWidth || PIP_VIEW_WIDTH; const height = this.callViewWrapper.current.clientHeight || PIP_VIEW_HEIGHT; - const precalculatedLastX = this.state.translationX; - const precalculatedLastY = this.state.translationY; - - let translationX; - let translationY; + let outTranslationX; + let outTranslationY; // Avoid overflow on the x axis - if (precalculatedLastX + width >= window.innerWidth) { - translationX = window.innerWidth - width; - } else if (precalculatedLastX <= 0) { - translationX = 0; + if (inTranslationX + width >= window.innerWidth) { + outTranslationX = window.innerWidth - width; + } else if (inTranslationX <= 0) { + outTranslationX = 0; } else { - translationX = precalculatedLastX; + outTranslationX = inTranslationX; } // Avoid overflow on the y axis - if (precalculatedLastY + height >= window.innerHeight) { - translationY = window.innerHeight - height; - } else if (precalculatedLastY <= 0) { - translationY = 0; + if (inTranslationY + height >= window.innerHeight) { + outTranslationY = window.innerHeight - height; + } else if (inTranslationY <= 0) { + outTranslationY = 0; } else { - translationY = precalculatedLastY; + outTranslationY = inTranslationY; } this.setState({ - translationX: translationX, - translationY: translationY, + translationX: outTranslationX, + translationY: outTranslationY, }); } @@ -246,37 +247,7 @@ export default class CallPreview extends React.Component { event.preventDefault(); event.stopPropagation(); - const width = this.callViewWrapper.current.clientWidth; - const height = this.callViewWrapper.current.clientHeight; - - const precalculatedLastX = event.pageX - this.initX; - const precalculatedLastY = event.pageY - this.initY; - - let translationX; - let translationY; - - // Avoid overflow on the x axis - if (precalculatedLastX + width >= window.innerWidth) { - translationX = window.innerWidth - width; - } else if (precalculatedLastX <= 0) { - translationX = 0; - } else { - translationX = precalculatedLastX; - } - - // Avoid overflow on the y axis - if (precalculatedLastY + height >= window.innerHeight) { - translationY = window.innerHeight - height; - } else if (precalculatedLastY <= 0) { - translationY = 0; - } else { - translationY = precalculatedLastY; - } - - this.setState({ - translationX: translationX, - translationY: translationY, - }); + this.setTranslation(event.pageX - this.initX, event.pageY - this.initY); }; private onEndMoving = () => { From 889b90fbc3cd564f6c6717d365f37e5fc21c2f79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 3 May 2021 18:33:24 +0200 Subject: [PATCH 056/809] Fix const values MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/voip/CallPreview.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/views/voip/CallPreview.tsx b/src/components/views/voip/CallPreview.tsx index 9ab2b9441a..b3e66f09b3 100644 --- a/src/components/views/voip/CallPreview.tsx +++ b/src/components/views/voip/CallPreview.tsx @@ -29,11 +29,11 @@ import { MatrixClientPeg } from '../../../MatrixClientPeg'; import {replaceableComponent} from "../../../utils/replaceableComponent"; import { Action } from '../../../dispatcher/actions'; -const PIP_VIEW_WIDTH = 320; -const PIP_VIEW_HEIGHT = 180; +const PIP_VIEW_WIDTH = 336; +const PIP_VIEW_HEIGHT = 232; -const DEFAULT_X_OFFSET = 64; -const DEFAULT_Y_OFFSET = 64; +const DEFAULT_X_OFFSET = 16; +const DEFAULT_Y_OFFSET = 48; const SHOW_CALL_IN_STATES = [ CallState.Connected, From f79339c2dadd86bac266c7f0eab7e3eb0317b479 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 3 May 2021 18:36:11 +0200 Subject: [PATCH 057/809] Add missing semicolon MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/voip/CallPreview.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/voip/CallPreview.tsx b/src/components/views/voip/CallPreview.tsx index b3e66f09b3..60153732d8 100644 --- a/src/components/views/voip/CallPreview.tsx +++ b/src/components/views/voip/CallPreview.tsx @@ -153,7 +153,7 @@ export default class CallPreview extends React.Component { private onWindowSizeChanged = () => { this.setTranslation(this.state.translationX, this.state.translationY); - } + }; private setTranslation(inTranslationX: number, inTranslationY: number) { const width = this.callViewWrapper.current.clientWidth || PIP_VIEW_WIDTH; From 9755da6f0915c8a3dec9726c33f60f4468f88037 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 3 May 2021 19:59:51 +0200 Subject: [PATCH 058/809] Add ? MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/voip/CallPreview.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/voip/CallPreview.tsx b/src/components/views/voip/CallPreview.tsx index 60153732d8..1b7c7f6e48 100644 --- a/src/components/views/voip/CallPreview.tsx +++ b/src/components/views/voip/CallPreview.tsx @@ -156,8 +156,8 @@ export default class CallPreview extends React.Component { }; private setTranslation(inTranslationX: number, inTranslationY: number) { - const width = this.callViewWrapper.current.clientWidth || PIP_VIEW_WIDTH; - const height = this.callViewWrapper.current.clientHeight || PIP_VIEW_HEIGHT; + const width = this.callViewWrapper.current?.clientWidth || PIP_VIEW_WIDTH; + const height = this.callViewWrapper.current?.clientHeight || PIP_VIEW_HEIGHT; let outTranslationX; let outTranslationY; From f367d617c506325237b4f56b76a97819e5f715f8 Mon Sep 17 00:00:00 2001 From: Ayush PS Date: Fri, 7 May 2021 14:37:28 +0530 Subject: [PATCH 059/809] Added the extra classes --- res/css/views/rooms/_EventTile.scss | 7 +++++++ src/components/views/rooms/EventTile.js | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index 5e110f9c6d..35a5d8fc15 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -123,11 +123,18 @@ $left-gutter: 64px; .mx_EventTile_line { margin-right: 0; grid-column: 1 / 3; + // override default padding of mx_EventTile_line so that we can be centered + padding: 0 !important; } .mx_EventTile_msgOption { grid-column: 2; } + + .hidden { + // override the overriden padding for hidden events + padding-left: 64px !important; + } } .mx_EventTile_reply { diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index f6fb83c064..9e8f66705c 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -1101,7 +1101,7 @@ export default class EventTile extends React.Component { { ircTimestamp } { sender } { ircPadlock } -
+
{ groupTimestamp } { groupPadlock } { thread } From c84338704393d0598eaa30a29fda21ec52a950de Mon Sep 17 00:00:00 2001 From: Ayush PS Date: Fri, 7 May 2021 18:32:38 +0530 Subject: [PATCH 060/809] Merge branch 'develop' into Bubble-bericht --- .eslintrc.js | 1 - .gitignore | 1 + CHANGELOG.md | 118 +++ README.md | 2 +- package.json | 19 +- res/css/_components.scss | 3 + res/css/structures/_RoomStatusBar.scss | 97 ++- res/css/structures/_SpacePanel.scss | 33 +- res/css/structures/_SpaceRoomDirectory.scss | 108 +-- res/css/structures/_SpaceRoomView.scss | 108 +-- .../dialogs/_AddExistingToSpaceDialog.scss | 195 +++-- res/css/views/elements/_AccessibleButton.scss | 8 +- res/css/views/elements/_ImageView.scss | 3 +- res/css/views/elements/_ProgressBar.scss | 2 +- res/css/views/messages/_MFileBody.scss | 8 +- .../views/messages/_MVoiceMessageBody.scss | 19 + res/css/views/messages/_MessageActionBar.scss | 8 + .../views/messages/_ReactionsRowButton.scss | 4 + res/css/views/rooms/_EventTile.scss | 4 - .../views/rooms/_VoiceRecordComposerTile.scss | 62 +- .../tabs/user/_HelpUserSettingsTab.scss | 31 + .../voice_messages/_PlayPauseButton.scss | 51 ++ .../voice_messages/_PlaybackContainer.scss | 53 ++ res/css/views/voip/_CallView.scss | 16 +- res/css/views/voip/_VideoFeed.scss | 24 +- res/img/element-icons/pause.svg | 4 + res/img/element-icons/play.svg | 3 + res/img/element-icons/retry.svg | 3 + res/img/element-icons/trashcan.svg | 3 + res/themes/dark/css/_dark.scss | 20 +- res/themes/legacy-dark/css/_legacy-dark.scss | 20 +- .../legacy-light/css/_legacy-light.scss | 29 +- res/themes/light/css/_light.scss | 31 +- scripts/compare-file.js | 10 - scripts/gen-i18n.js | 304 -------- scripts/prune-i18n.js | 68 -- src/@types/global.d.ts | 22 +- src/Avatar.ts | 15 +- src/BasePlatform.ts | 2 +- src/CallHandler.tsx | 119 +-- src/CallMediaHandler.js | 5 +- src/GroupAddressPicker.js | 18 +- src/IdentityAuthClient.js | 2 +- src/KeyBindingsManager.ts | 6 +- src/Login.ts | 7 +- src/PasswordReset.js | 2 +- src/Resend.js | 10 +- ...calarAuthClient.js => ScalarAuthClient.ts} | 64 +- src/SlashCommands.tsx | 4 +- src/{Terms.js => Terms.ts} | 46 +- src/TextForEvent.js | 18 +- src/Unread.js | 2 +- src/VoipUserMapper.ts | 6 +- ...exDialog.js => ManageEventIndexDialog.tsx} | 34 +- .../dialogs/security/CreateKeyBackupDialog.js | 8 +- .../security/CreateSecretStorageDialog.js | 10 +- .../dialogs/security/ExportE2eKeysDialog.js | 7 +- .../dialogs/security/ImportE2eKeysDialog.js | 56 +- src/components/structures/FilePanel.js | 8 +- src/components/structures/GroupFilterPanel.js | 20 +- src/components/structures/GroupView.js | 95 +-- src/components/structures/LeftPanel.tsx | 4 +- src/components/structures/LoggedInView.tsx | 20 + src/components/structures/MatrixChat.tsx | 4 +- src/components/structures/MessagePanel.js | 26 +- src/components/structures/RightPanel.js | 5 +- src/components/structures/RoomSearch.tsx | 21 +- src/components/structures/RoomStatusBar.js | 155 ++-- src/components/structures/RoomView.tsx | 12 +- src/components/structures/ScrollPanel.js | 26 +- .../structures/SpaceRoomDirectory.tsx | 190 +++-- src/components/structures/SpaceRoomView.tsx | 189 ++++- src/components/structures/TimelinePanel.js | 20 +- src/components/structures/auth/Login.tsx | 4 +- .../structures/auth/Registration.tsx | 4 +- .../auth/{SoftLogout.js => SoftLogout.tsx} | 49 +- .../auth/InteractiveAuthEntryComponents.js | 4 +- src/components/views/avatars/MemberAvatar.tsx | 4 +- src/components/views/avatars/RoomAvatar.tsx | 11 +- .../views/context_menus/MessageContextMenu.js | 93 +-- .../dialogs/AddExistingToSpaceDialog.tsx | 310 +++++--- .../views/dialogs/BugReportDialog.js | 2 +- .../views/dialogs/ChangelogDialog.js | 2 +- .../views/dialogs/ConfirmWipeDeviceDialog.js | 9 +- .../views/dialogs/DevtoolsDialog.js | 94 +-- .../views/dialogs/IncomingSasDialog.js | 2 +- .../dialogs/IntegrationsDisabledDialog.js | 9 +- .../dialogs/IntegrationsImpossibleDialog.js | 9 +- src/components/views/dialogs/InviteDialog.tsx | 2 +- .../dialogs/KeySignatureUploadFailedDialog.js | 10 +- .../views/dialogs/MessageEditHistoryDialog.js | 8 +- .../views/dialogs/RoomSettingsDialog.js | 8 +- .../views/dialogs/ServerPickerDialog.tsx | 6 +- .../dialogs/SessionRestoreErrorDialog.js | 2 +- .../views/dialogs/StorageEvictedDialog.js | 10 +- .../views/dialogs/UserSettingsDialog.js | 8 +- .../dialogs/VerificationRequestDialog.js | 12 +- .../dialogs/WidgetOpenIDPermissionsDialog.js | 9 +- .../ConfirmDestroyCrossSigningDialog.js | 9 +- .../security/RestoreKeyBackupDialog.js | 59 +- src/components/views/elements/ActionButton.js | 4 +- src/components/views/elements/AppTile.js | 2 +- .../views/elements/EditableItemList.js | 26 +- src/components/views/elements/EditableText.js | 16 +- src/components/views/elements/ImageView.tsx | 197 +++-- .../views/elements/LabelledToggleSwitch.js | 8 +- .../views/elements/LanguageDropdown.js | 6 +- src/components/views/elements/Pill.js | 24 +- .../views/elements/PowerSelector.js | 15 +- .../views/elements/RoomAliasField.js | 23 +- .../views/elements/ServerPicker.tsx | 4 +- src/components/views/elements/TintableSvg.js | 14 +- .../views/groups/GroupMemberList.js | 12 +- .../views/groups/GroupPublicityToggle.js | 6 +- src/components/views/groups/GroupRoomList.js | 13 +- .../views/messages/EditHistoryMessage.js | 1 - src/components/views/messages/MImageBody.js | 35 +- .../messages/MKeyVerificationConclusion.js | 4 +- .../views/messages/MVoiceMessageBody.tsx | 106 +++ .../views/messages/MVoiceOrAudioBody.tsx | 39 + .../views/messages/MessageActionBar.js | 125 ++- src/components/views/messages/MessageEvent.js | 6 +- .../views/messages/ReactionsRowButton.js | 3 +- src/components/views/messages/TextualBody.js | 11 +- src/components/views/right_panel/UserInfo.tsx | 18 +- .../views/room_settings/AliasSettings.js | 17 +- .../room_settings/RoomProfileSettings.js | 34 +- .../views/room_settings/UrlPreviewSettings.js | 14 +- src/components/views/rooms/EntityTile.js | 7 +- .../rooms/{EventTile.js => EventTile.tsx} | 461 ++++++----- .../views/rooms/LinkPreviewWidget.js | 4 +- src/components/views/rooms/MemberList.js | 5 +- ...MessageComposer.js => MessageComposer.tsx} | 160 ++-- .../views/rooms/NotificationBadge.tsx | 2 +- src/components/views/rooms/PinnedEventTile.js | 13 +- .../views/rooms/PinnedEventsPanel.js | 20 +- .../views/rooms/ReadReceiptMarker.js | 3 +- src/components/views/rooms/ReplyPreview.js | 9 +- src/components/views/rooms/RoomDetailRow.js | 8 +- src/components/views/rooms/RoomList.tsx | 8 +- .../views/rooms/RoomListNumResults.tsx | 6 +- src/components/views/rooms/RoomSublist.tsx | 8 +- src/components/views/rooms/RoomTile.tsx | 69 +- .../views/rooms/SendMessageComposer.js | 5 +- src/components/views/rooms/Stickerpicker.js | 38 +- ...MemberInfo.js => ThirdPartyMemberInfo.tsx} | 26 +- .../views/rooms/VoiceRecordComposerTile.tsx | 231 ++++-- src/components/views/settings/ChangeAvatar.js | 2 +- .../views/settings/ChangePassword.js | 4 +- .../views/settings/CrossSigningPanel.js | 2 +- src/components/views/settings/DevicesPanel.js | 2 +- ...eAdvancedPanel.js => E2eAdvancedPanel.tsx} | 0 ...EventIndexPanel.js => EventIndexPanel.tsx} | 140 ++-- .../views/settings/Notifications.js | 34 +- .../views/settings/ProfileSettings.js | 8 +- .../{SetIdServer.js => SetIdServer.tsx} | 78 +- .../views/settings/account/EmailAddresses.js | 39 +- .../views/settings/account/PhoneNumbers.js | 21 +- .../tabs/room/GeneralRoomSettingsTab.js | 12 +- .../tabs/room/NotificationSettingsTab.js | 6 +- ...ettingsTab.js => RolesRoomSettingsTab.tsx} | 71 +- ...ingsTab.js => SecurityRoomSettingsTab.tsx} | 144 ++-- .../tabs/user/GeneralUserSettingsTab.js | 19 +- ...SettingsTab.js => HelpUserSettingsTab.tsx} | 159 ++-- ...tingsTab.js => MjolnirUserSettingsTab.tsx} | 54 +- ...sTab.js => PreferencesUserSettingsTab.tsx} | 64 +- .../tabs/user/SecurityUserSettingsTab.js | 21 +- .../tabs/user/VoiceUserSettingsTab.js | 12 +- .../views/spaces/SpaceTreeLevel.tsx | 6 +- .../verification/VerificationCancelled.js | 16 +- src/components/views/voice_messages/Clock.tsx | 10 +- .../voice_messages/LiveRecordingClock.tsx | 8 +- .../voice_messages/LiveRecordingWaveform.tsx | 8 +- .../views/voice_messages/PlayPauseButton.tsx | 61 ++ .../views/voice_messages/PlaybackClock.tsx | 71 ++ .../views/voice_messages/PlaybackWaveform.tsx | 68 ++ .../voice_messages/RecordingPlayback.tsx | 62 ++ .../views/voice_messages/Waveform.tsx | 17 +- src/components/views/voip/AudioFeed.tsx | 97 +++ .../views/voip/AudioFeedArrayForCall.tsx | 60 ++ src/components/views/voip/CallPreview.tsx | 24 +- src/components/views/voip/CallView.tsx | 162 +++- src/components/views/voip/CallViewForRoom.tsx | 16 +- src/components/views/voip/IncomingCallBox.tsx | 6 +- src/components/views/voip/VideoFeed.tsx | 125 ++- src/customisations/Media.ts | 7 + src/editor/autocomplete.ts | 6 +- src/editor/model.ts | 2 +- src/editor/parts.ts | 43 +- src/editor/serialize.ts | 8 +- src/i18n/strings/cs.json | 163 ++-- src/i18n/strings/da.json | 6 +- src/i18n/strings/de_DE.json | 140 ++-- src/i18n/strings/en_EN.json | 68 +- src/i18n/strings/en_US.json | 10 +- src/i18n/strings/eo.json | 32 +- src/i18n/strings/es.json | 25 +- src/i18n/strings/et.json | 25 +- src/i18n/strings/fa.json | 322 +++++++- src/i18n/strings/fr.json | 37 +- src/i18n/strings/gl.json | 25 +- src/i18n/strings/hu.json | 25 +- src/i18n/strings/it.json | 25 +- src/i18n/strings/nl.json | 25 +- src/i18n/strings/ru.json | 3 +- src/i18n/strings/sq.json | 24 +- src/i18n/strings/sv.json | 25 +- src/i18n/strings/tzm.json | 121 ++- src/i18n/strings/zh_Hant.json | 25 +- src/indexing/BaseEventIndexManager.ts | 6 +- src/indexing/EventIndex.js | 37 +- .../{EventIndexPeg.js => EventIndexPeg.ts} | 19 +- src/linkify-matrix.js | 2 +- src/mjolnir/{BanList.js => BanList.ts} | 0 src/mjolnir/{ListRule.js => ListRule.ts} | 0 src/mjolnir/{Mjolnir.js => Mjolnir.ts} | 0 src/settings/Settings.ts | 5 +- src/stores/BreadcrumbsStore.ts | 2 +- src/stores/CustomRoomTagStore.js | 4 +- src/stores/GroupFilterOrderStore.js | 8 +- src/stores/RoomViewStore.tsx | 19 +- src/stores/SpaceStore.tsx | 172 +++-- src/stores/{TypingStore.js => TypingStore.ts} | 24 +- src/stores/VoiceRecordingStore.ts | 2 + ...{WidgetEchoStore.js => WidgetEchoStore.ts} | 39 +- .../notifications/StaticNotificationState.ts | 2 + src/stores/room-list/RoomListStore.ts | 22 +- src/stores/room-list/algorithms/Algorithm.ts | 9 +- .../algorithms/tag-sorting/RecentAlgorithm.ts | 140 ++-- .../room-list/filters/VisibilityProvider.ts | 8 +- ...scoveryUtils.js => AutoDiscoveryUtils.tsx} | 32 +- src/utils/DMRoomMap.ts | 9 + src/utils/ErrorUtils.js | 6 - src/utils/{MatrixGlob.js => MatrixGlob.ts} | 0 src/utils/MegolmExportEncryption.js | 3 +- .../{StorageManager.js => StorageManager.ts} | 12 +- src/utils/{Timer.js => Timer.ts} | 70 +- src/utils/{TypeUtils.js => TypeUtils.ts} | 0 src/utils/arrays.ts | 26 +- ...ctor.js => ElementPermalinkConstructor.ts} | 24 +- ...Constructor.js => PermalinkConstructor.ts} | 0 .../{Permalinks.js => Permalinks.ts} | 167 ++-- ...tructor.js => SpecPermalinkConstructor.ts} | 0 src/utils/space.tsx | 1 + src/voice/Playback.ts | 147 ++++ src/voice/PlaybackClock.ts | 78 ++ src/voice/VoiceRecording.ts | 69 +- test/CallHandler-test.ts | 214 ++++++ test/ScalarAuthClient-test.js | 2 +- test/autocomplete/QueryMatcher-test.js | 2 +- .../dialogs/AccessSecretStorageDialog-test.js | 24 +- .../elements/MemberEventListSummary-test.js | 9 +- .../components/views/rooms/MemberList-test.js | 2 +- test/components/views/rooms/RoomList-test.js | 10 +- test/editor/deserialize-test.js | 2 +- test/end-to-end-tests/src/session.js | 8 +- test/end-to-end-tests/yarn.lock | 6 +- test/stores/SpaceStore-test.ts | 714 ++++++++++++++++++ test/test-utils.js | 32 +- test/utils/MegolmExportEncryption-test.js | 30 +- test/utils/ShieldUtils-test.js | 6 +- test/utils/arrays-test.ts | 33 + test/utils/test-utils.ts | 33 + yarn.lock | 113 ++- 264 files changed, 7383 insertions(+), 3268 deletions(-) create mode 100644 res/css/views/messages/_MVoiceMessageBody.scss create mode 100644 res/css/views/voice_messages/_PlayPauseButton.scss create mode 100644 res/css/views/voice_messages/_PlaybackContainer.scss create mode 100644 res/img/element-icons/pause.svg create mode 100644 res/img/element-icons/play.svg create mode 100644 res/img/element-icons/retry.svg create mode 100644 res/img/element-icons/trashcan.svg delete mode 100644 scripts/compare-file.js delete mode 100755 scripts/gen-i18n.js delete mode 100755 scripts/prune-i18n.js rename src/{ScalarAuthClient.js => ScalarAuthClient.ts} (84%) rename src/{Terms.js => Terms.ts} (87%) rename src/async-components/views/dialogs/eventindex/{ManageEventIndexDialog.js => ManageEventIndexDialog.tsx} (90%) rename src/components/structures/auth/{SoftLogout.js => SoftLogout.tsx} (92%) create mode 100644 src/components/views/messages/MVoiceMessageBody.tsx create mode 100644 src/components/views/messages/MVoiceOrAudioBody.tsx rename src/components/views/rooms/{EventTile.js => EventTile.tsx} (79%) rename src/components/views/rooms/{MessageComposer.js => MessageComposer.tsx} (80%) rename src/components/views/rooms/{ThirdPartyMemberInfo.js => ThirdPartyMemberInfo.tsx} (91%) rename src/components/views/settings/{E2eAdvancedPanel.js => E2eAdvancedPanel.tsx} (100%) rename src/components/views/settings/{EventIndexPanel.js => EventIndexPanel.tsx} (65%) rename src/components/views/settings/{SetIdServer.js => SetIdServer.tsx} (89%) rename src/components/views/settings/tabs/room/{RolesRoomSettingsTab.js => RolesRoomSettingsTab.tsx} (89%) rename src/components/views/settings/tabs/room/{SecurityRoomSettingsTab.js => SecurityRoomSettingsTab.tsx} (79%) rename src/components/views/settings/tabs/user/{HelpUserSettingsTab.js => HelpUserSettingsTab.tsx} (64%) rename src/components/views/settings/tabs/user/{MjolnirUserSettingsTab.js => MjolnirUserSettingsTab.tsx} (90%) rename src/components/views/settings/tabs/user/{PreferencesUserSettingsTab.js => PreferencesUserSettingsTab.tsx} (80%) create mode 100644 src/components/views/voice_messages/PlayPauseButton.tsx create mode 100644 src/components/views/voice_messages/PlaybackClock.tsx create mode 100644 src/components/views/voice_messages/PlaybackWaveform.tsx create mode 100644 src/components/views/voice_messages/RecordingPlayback.tsx create mode 100644 src/components/views/voip/AudioFeed.tsx create mode 100644 src/components/views/voip/AudioFeedArrayForCall.tsx rename src/indexing/{EventIndexPeg.js => EventIndexPeg.ts} (94%) rename src/mjolnir/{BanList.js => BanList.ts} (100%) rename src/mjolnir/{ListRule.js => ListRule.ts} (100%) rename src/mjolnir/{Mjolnir.js => Mjolnir.ts} (100%) rename src/stores/{TypingStore.js => TypingStore.ts} (84%) rename src/stores/{WidgetEchoStore.js => WidgetEchoStore.ts} (71%) rename src/utils/{AutoDiscoveryUtils.js => AutoDiscoveryUtils.tsx} (91%) rename src/utils/{MatrixGlob.js => MatrixGlob.ts} (100%) rename src/utils/{StorageManager.js => StorageManager.ts} (96%) rename src/utils/{Timer.js => Timer.ts} (60%) rename src/utils/{TypeUtils.js => TypeUtils.ts} (100%) rename src/utils/permalinks/{ElementPermalinkConstructor.js => ElementPermalinkConstructor.ts} (82%) rename src/utils/permalinks/{PermalinkConstructor.js => PermalinkConstructor.ts} (100%) rename src/utils/permalinks/{Permalinks.js => Permalinks.ts} (76%) rename src/utils/permalinks/{SpecPermalinkConstructor.js => SpecPermalinkConstructor.ts} (100%) create mode 100644 src/voice/Playback.ts create mode 100644 src/voice/PlaybackClock.ts create mode 100644 test/CallHandler-test.ts create mode 100644 test/stores/SpaceStore-test.ts create mode 100644 test/utils/test-utils.ts diff --git a/.eslintrc.js b/.eslintrc.js index 99695b7a03..4959b133a0 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -15,7 +15,6 @@ module.exports = { "prefer-promise-reject-errors": "off", "no-async-promise-executor": "off", "quotes": "off", - "indent": "off", }, overrides: [{ diff --git a/.gitignore b/.gitignore index e1dd7726e1..50aa10fbfd 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ /*.log package-lock.json +/coverage /node_modules /lib diff --git a/CHANGELOG.md b/CHANGELOG.md index ec73756ff9..d459b4e94a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,121 @@ +Changes in [3.19.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.19.0) (2021-04-26) +===================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.19.0-rc.1...v3.19.0) + + * Upgrade to JS SDK 10.0.0 + * [Release] Dynamic max and min zoom in the new ImageView + [\#5927](https://github.com/matrix-org/matrix-react-sdk/pull/5927) + * [Release] Add a WheelEvent normalization function + [\#5911](https://github.com/matrix-org/matrix-react-sdk/pull/5911) + * Add a WheelEvent normalization function + [\#5904](https://github.com/matrix-org/matrix-react-sdk/pull/5904) + * [Release] Use floats for image background opacity + [\#5907](https://github.com/matrix-org/matrix-react-sdk/pull/5907) + +Changes in [3.19.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.19.0-rc.1) (2021-04-21) +=============================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.18.0...v3.19.0-rc.1) + + * Upgrade to JS SDK 10.0.0-rc.1 + * Translations update from Weblate + [\#5896](https://github.com/matrix-org/matrix-react-sdk/pull/5896) + * Fix sticky tags header in room list + [\#5895](https://github.com/matrix-org/matrix-react-sdk/pull/5895) + * Fix spaces filtering sometimes lagging behind or behaving oddly + [\#5893](https://github.com/matrix-org/matrix-react-sdk/pull/5893) + * Fix issue with spaces context switching looping and breaking + [\#5894](https://github.com/matrix-org/matrix-react-sdk/pull/5894) + * Improve RoomList render time when filtering + [\#5874](https://github.com/matrix-org/matrix-react-sdk/pull/5874) + * Avoid being stuck in a space + [\#5891](https://github.com/matrix-org/matrix-react-sdk/pull/5891) + * [Spaces] Context switching + [\#5795](https://github.com/matrix-org/matrix-react-sdk/pull/5795) + * Warn when you attempt to leave room that you are the only member of + [\#5415](https://github.com/matrix-org/matrix-react-sdk/pull/5415) + * Ensure PersistedElement are unmounted on application logout + [\#5884](https://github.com/matrix-org/matrix-react-sdk/pull/5884) + * Add missing space in seshat dialog and the corresponding string + [\#5866](https://github.com/matrix-org/matrix-react-sdk/pull/5866) + * A tiny change to make the Add existing rooms dialog a little nicer + [\#5885](https://github.com/matrix-org/matrix-react-sdk/pull/5885) + * Remove weird margin from the file panel + [\#5889](https://github.com/matrix-org/matrix-react-sdk/pull/5889) + * Trigger lazy loading when filtering using spaces + [\#5882](https://github.com/matrix-org/matrix-react-sdk/pull/5882) + * Fix typo in method call in add existing to space dialog + [\#5883](https://github.com/matrix-org/matrix-react-sdk/pull/5883) + * New Image View fixes/improvements + [\#5872](https://github.com/matrix-org/matrix-react-sdk/pull/5872) + * Limit voice recording length + [\#5871](https://github.com/matrix-org/matrix-react-sdk/pull/5871) + * Clean up add existing to space dialog and include DMs in it too + [\#5881](https://github.com/matrix-org/matrix-react-sdk/pull/5881) + * Fix unknown slash command error exploding + [\#5853](https://github.com/matrix-org/matrix-react-sdk/pull/5853) + * Switch to a spec conforming email validation Regexp + [\#5852](https://github.com/matrix-org/matrix-react-sdk/pull/5852) + * Cleanup unused state in MessageComposer + [\#5877](https://github.com/matrix-org/matrix-react-sdk/pull/5877) + * Pulse animation for voice messages recording state + [\#5869](https://github.com/matrix-org/matrix-react-sdk/pull/5869) + * Don't include invisible rooms in notify summary + [\#5875](https://github.com/matrix-org/matrix-react-sdk/pull/5875) + * Properly disable composer access when recording a voice message + [\#5870](https://github.com/matrix-org/matrix-react-sdk/pull/5870) + * Stabilise starting a DM with multiple people flow + [\#5862](https://github.com/matrix-org/matrix-react-sdk/pull/5862) + * Render msgOption only if showReadReceipts is enabled + [\#5864](https://github.com/matrix-org/matrix-react-sdk/pull/5864) + * Labs: Add quick/cheap "do not disturb" flag + [\#5873](https://github.com/matrix-org/matrix-react-sdk/pull/5873) + * Fix ReadReceipts animations + [\#5836](https://github.com/matrix-org/matrix-react-sdk/pull/5836) + * Add tooltips to message previews + [\#5859](https://github.com/matrix-org/matrix-react-sdk/pull/5859) + * IRC Layout fix layout spacing in replies + [\#5855](https://github.com/matrix-org/matrix-react-sdk/pull/5855) + * Move user to welcome_page if continuing with previous session + [\#5849](https://github.com/matrix-org/matrix-react-sdk/pull/5849) + * Improve image view + [\#5521](https://github.com/matrix-org/matrix-react-sdk/pull/5521) + * Add a button to reset personal encryption state during login + [\#5819](https://github.com/matrix-org/matrix-react-sdk/pull/5819) + * Fix js-sdk import in SlashCommands + [\#5850](https://github.com/matrix-org/matrix-react-sdk/pull/5850) + * Fix useRoomPowerLevels hook + [\#5854](https://github.com/matrix-org/matrix-react-sdk/pull/5854) + * Prevent state events being rendered with invalid state keys + [\#5851](https://github.com/matrix-org/matrix-react-sdk/pull/5851) + * Give server ACLs a name in 'roles & permissions' tab + [\#5838](https://github.com/matrix-org/matrix-react-sdk/pull/5838) + * Don't hide notification badge on the home space button as it has no menu + [\#5845](https://github.com/matrix-org/matrix-react-sdk/pull/5845) + * User Info hide disambiguation as we always show MXID anyway + [\#5843](https://github.com/matrix-org/matrix-react-sdk/pull/5843) + * Improve kick state to not show if the target was not joined to begin with + [\#5846](https://github.com/matrix-org/matrix-react-sdk/pull/5846) + * Fix space store wrongly switching to a non-space filter + [\#5844](https://github.com/matrix-org/matrix-react-sdk/pull/5844) + * Tweak appearance of invite reason + [\#5847](https://github.com/matrix-org/matrix-react-sdk/pull/5847) + * Update Inter font to v3.18 + [\#5840](https://github.com/matrix-org/matrix-react-sdk/pull/5840) + * Enable sharing historical keys on invite + [\#5839](https://github.com/matrix-org/matrix-react-sdk/pull/5839) + * Add ability to hide post-login encryption setup with customisation point + [\#5834](https://github.com/matrix-org/matrix-react-sdk/pull/5834) + * Use LaTeX and TeX delimiters by default + [\#5515](https://github.com/matrix-org/matrix-react-sdk/pull/5515) + * Clone author's deps fork for Netlify previews + [\#5837](https://github.com/matrix-org/matrix-react-sdk/pull/5837) + * Show drop file UI only if dragging a file + [\#5827](https://github.com/matrix-org/matrix-react-sdk/pull/5827) + * Ignore punctuation when filtering rooms + [\#5824](https://github.com/matrix-org/matrix-react-sdk/pull/5824) + * Resizable CallView + [\#5710](https://github.com/matrix-org/matrix-react-sdk/pull/5710) + Changes in [3.18.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.18.0) (2021-04-12) ===================================================================================================== [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.18.0-rc.1...v3.18.0) diff --git a/README.md b/README.md index 73afe34df0..b3e96ef001 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ Platform Targets: * WebRTC features (VoIP and Video calling) are only available in Chrome & Firefox. * Mobile Web is not currently a target platform - instead please use the native iOS (https://github.com/matrix-org/matrix-ios-kit) and Android - (https://github.com/matrix-org/matrix-android-sdk) SDKs. + (https://github.com/matrix-org/matrix-android-sdk2) SDKs. All code lands on the `develop` branch - `master` is only used for stable releases. **Please file PRs against `develop`!!** diff --git a/package.json b/package.json index 7c190c68bf..be195e2e9e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "3.18.0", + "version": "3.19.0", "description": "SDK for matrix.org using React", "author": "matrix.org", "repository": { @@ -23,9 +23,7 @@ "package.json" ], "bin": { - "reskindex": "scripts/reskindex.js", - "matrix-gen-i18n": "scripts/gen-i18n.js", - "matrix-prune-i18n": "scripts/prune-i18n.js" + "reskindex": "scripts/reskindex.js" }, "main": "./src/index.js", "matrix_src_main": "./src/index.js", @@ -35,7 +33,7 @@ "prepublishOnly": "yarn build", "i18n": "matrix-gen-i18n", "prunei18n": "matrix-prune-i18n", - "diff-i18n": "cp src/i18n/strings/en_EN.json src/i18n/strings/en_EN_orig.json && ./scripts/gen-i18n.js && node scripts/compare-file.js src/i18n/strings/en_EN_orig.json src/i18n/strings/en_EN.json", + "diff-i18n": "cp src/i18n/strings/en_EN.json src/i18n/strings/en_EN_orig.json && matrix-gen-i18n && matrix-compare-i18n-files src/i18n/strings/en_EN_orig.json src/i18n/strings/en_EN.json", "reskindex": "node scripts/reskindex.js -h header", "reskindex:watch": "node scripts/reskindex.js -h header -w", "rethemendex": "res/css/rethemendex.sh", @@ -51,7 +49,8 @@ "lint:types": "tsc --noEmit --jsx react", "lint:style": "stylelint 'res/css/**/*.scss'", "test": "jest", - "test:e2e": "./test/end-to-end-tests/run.sh --app-url http://localhost:8080" + "test:e2e": "./test/end-to-end-tests/run.sh --app-url http://localhost:8080", + "coverage": "yarn test --coverage" }, "dependencies": { "@babel/runtime": "^7.12.5", @@ -133,6 +132,7 @@ "@types/modernizr": "^3.5.3", "@types/node": "^14.14.22", "@types/pako": "^1.0.1", + "@types/parse5": "^6.0.0", "@types/qrcode": "^1.3.5", "@types/react": "^16.9", "@types/react-dom": "^16.9.10", @@ -160,6 +160,7 @@ "jest-fetch-mock": "^3.0.3", "matrix-mock-request": "^1.2.3", "matrix-react-test-utils": "^0.2.2", + "matrix-web-i18n": "github:matrix-org/matrix-web-i18n", "olm": "https://packages.matrix.org/npm/olm/olm-3.2.1.tgz", "react-test-renderer": "^16.14.0", "rimraf": "^3.0.2", @@ -189,6 +190,12 @@ }, "transformIgnorePatterns": [ "/node_modules/(?!matrix-js-sdk).+$" + ], + "collectCoverageFrom": [ + "/src/**/*.{js,ts,tsx}" + ], + "coverageReporters": [ + "text" ] } } diff --git a/res/css/_components.scss b/res/css/_components.scss index 253f97bf42..ec9592f3a1 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -161,6 +161,7 @@ @import "./views/messages/_MStickerBody.scss"; @import "./views/messages/_MTextBody.scss"; @import "./views/messages/_MVideoBody.scss"; +@import "./views/messages/_MVoiceMessageBody.scss"; @import "./views/messages/_MessageActionBar.scss"; @import "./views/messages/_MessageTimestamp.scss"; @import "./views/messages/_MjolnirBody.scss"; @@ -248,6 +249,8 @@ @import "./views/toasts/_AnalyticsToast.scss"; @import "./views/toasts/_NonUrgentEchoFailureToast.scss"; @import "./views/verification/_VerificationShowSas.scss"; +@import "./views/voice_messages/_PlayPauseButton.scss"; +@import "./views/voice_messages/_PlaybackContainer.scss"; @import "./views/voice_messages/_Waveform.scss"; @import "./views/voip/_CallContainer.scss"; @import "./views/voip/_CallView.scss"; diff --git a/res/css/structures/_RoomStatusBar.scss b/res/css/structures/_RoomStatusBar.scss index 5bf2aee3ae..8cc00aba0f 100644 --- a/res/css/structures/_RoomStatusBar.scss +++ b/res/css/structures/_RoomStatusBar.scss @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_RoomStatusBar { +.mx_RoomStatusBar:not(.mx_RoomStatusBar_unsentMessages) { margin-left: 65px; min-height: 50px; } @@ -68,6 +68,99 @@ limitations under the License. min-height: 58px; } +.mx_RoomStatusBar_unsentMessages { + > div[role="alert"] { + // cheat some basic alignment + display: flex; + align-items: center; + min-height: 70px; + margin: 12px; + padding-left: 16px; + background-color: $header-panel-bg-color; + border-radius: 4px; + } + + .mx_RoomStatusBar_unsentBadge { + margin-right: 12px; + + .mx_NotificationBadge { + // Override sizing from the default badge + width: 24px !important; + height: 24px !important; + border-radius: 24px !important; + + .mx_NotificationBadge_count { + font-size: $font-16px !important; // override default + } + } + } + + .mx_RoomStatusBar_unsentTitle { + color: $warning-color; + font-size: $font-15px; + } + + .mx_RoomStatusBar_unsentDescription { + font-size: $font-12px; + } + + .mx_RoomStatusBar_unsentButtonBar { + flex-grow: 1; + text-align: right; + margin-right: 22px; + color: $muted-fg-color; + + .mx_AccessibleButton { + padding: 5px 10px; + padding-left: 28px; // 16px for the icon, 2px margin to text, 10px regular padding + display: inline-block; + position: relative; + + &:nth-child(2) { + border-left: 1px solid $resend-button-divider-color; + } + + &::before { + content: ''; + position: absolute; + left: 10px; // inset for regular button padding + background-color: $muted-fg-color; + mask-repeat: no-repeat; + mask-position: center; + mask-size: contain; + } + + &.mx_RoomStatusBar_unsentCancelAllBtn::before { + mask-image: url('$(res)/img/element-icons/trashcan.svg'); + width: 12px; + height: 16px; + top: calc(50% - 8px); // text sizes are dynamic + } + + &.mx_RoomStatusBar_unsentResendAllBtn { + padding-left: 34px; // 28px from above, but +6px to account for the wider icon + + &::before { + mask-image: url('$(res)/img/element-icons/retry.svg'); + width: 18px; + height: 18px; + top: calc(50% - 9px); // text sizes are dynamic + } + } + } + + .mx_InlineSpinner { + vertical-align: middle; + margin-right: 5px; + top: 1px; // just to help the vertical alignment be slightly better + + & + span { + margin-right: 10px; // same margin/padding as the rightmost button + } + } + } +} + .mx_RoomStatusBar_connectionLostBar img { padding-left: 10px; padding-right: 10px; @@ -103,7 +196,7 @@ limitations under the License. } .mx_MatrixChat_useCompactLayout { - .mx_RoomStatusBar { + .mx_RoomStatusBar:not(.mx_RoomStatusBar_unsentMessages) { min-height: 40px; } diff --git a/res/css/structures/_SpacePanel.scss b/res/css/structures/_SpacePanel.scss index 59f2ea947c..c433ccf275 100644 --- a/res/css/structures/_SpacePanel.scss +++ b/res/css/structures/_SpacePanel.scss @@ -79,6 +79,10 @@ $activeBorderColor: $secondary-fg-color; .mx_SpaceItem { display: inline-flex; flex-flow: wrap; + + &.mx_SpaceItem_narrow { + align-self: baseline; + } } .mx_SpaceItem.collapsed { @@ -233,7 +237,6 @@ $activeBorderColor: $secondary-fg-color; .mx_SpacePanel_badgeContainer { position: absolute; - height: 16px; // Create a flexbox to make aligning dot badges easier display: flex; @@ -245,23 +248,37 @@ $activeBorderColor: $secondary-fg-color; .mx_NotificationBadge_dot { // make the smaller dot occupy the same width for centering - margin-left: 7px; - margin-right: 7px; + margin: 0 7px; } } &.collapsed { .mx_SpaceButton { .mx_SpacePanel_badgeContainer { - right: -3px; - top: -3px; + right: 0; + top: 0; + + .mx_NotificationBadge { + background-clip: padding-box; + } + + .mx_NotificationBadge_dot { + margin: 0 -1px 0 0; + border: 3px solid $groupFilterPanel-bg-color; + } + + .mx_NotificationBadge_2char, + .mx_NotificationBadge_3char { + margin: -5px -5px 0 0; + border: 2px solid $groupFilterPanel-bg-color; + } } &.mx_SpaceButton_active .mx_SpacePanel_badgeContainer { // when we draw the selection border we move the relative bounds of our parent // so update our position within the bounds of the parent to maintain position overall - right: -6px; - top: -6px; + right: -3px; + top: -3px; } } } @@ -275,7 +292,7 @@ $activeBorderColor: $secondary-fg-color; .mx_SpaceButton:hover, .mx_SpaceButton:focus-within, .mx_SpaceButton_hasMenuOpen { - &:not(.mx_SpaceButton_home) { + &:not(.mx_SpaceButton_home):not(.mx_SpaceButton_invite) { // Hide the badge container on hover because it'll be a menu button .mx_SpacePanel_badgeContainer { width: 0; diff --git a/res/css/structures/_SpaceRoomDirectory.scss b/res/css/structures/_SpaceRoomDirectory.scss index dcceee6371..7925686bf1 100644 --- a/res/css/structures/_SpaceRoomDirectory.scss +++ b/res/css/structures/_SpaceRoomDirectory.scss @@ -26,7 +26,10 @@ limitations under the License. word-break: break-word; display: flex; flex-direction: column; +} +.mx_SpaceRoomDirectory, +.mx_SpaceRoomView_landing { .mx_Dialog_title { display: flex; @@ -56,65 +59,68 @@ limitations under the License. } } - .mx_Dialog_content { - .mx_AccessibleButton_kind_link { - padding: 0; - } + .mx_AccessibleButton_kind_link { + padding: 0; + } - .mx_SearchBox { - margin: 24px 0 16px; - } + .mx_SearchBox { + margin: 24px 0 16px; + } - .mx_SpaceRoomDirectory_noResults { - text-align: center; + .mx_SpaceRoomDirectory_noResults { + text-align: center; - > div { - font-size: $font-15px; - line-height: $font-24px; - color: $secondary-fg-color; - } - } - - .mx_SpaceRoomDirectory_listHeader { - display: flex; - min-height: 32px; - align-items: center; + > div { font-size: $font-15px; line-height: $font-24px; - color: $primary-fg-color; + color: $secondary-fg-color; + } + } - .mx_AccessibleButton { - padding: 2px 8px; - font-weight: normal; + .mx_SpaceRoomDirectory_listHeader { + display: flex; + min-height: 32px; + align-items: center; + font-size: $font-15px; + line-height: $font-24px; + color: $primary-fg-color; - & + .mx_AccessibleButton { - margin-left: 16px; - } - } + .mx_AccessibleButton { + padding: 4px 12px; + font-weight: normal; - > span { - margin-left: auto; + & + .mx_AccessibleButton { + margin-left: 16px; } } - .mx_SpaceRoomDirectory_error { - position: relative; - font-weight: $font-semi-bold; - color: $notice-primary-color; - font-size: $font-15px; - line-height: $font-18px; - margin: 20px auto 12px; - padding-left: 24px; - width: max-content; + .mx_AccessibleButton_kind_danger_outline, + .mx_AccessibleButton_kind_primary_outline { + padding: 3px 12px; // to account for the 1px border + } - &::before { - content: ""; - position: absolute; - height: 16px; - width: 16px; - left: 0; - background-image: url("$(res)/img/element-icons/warning-badge.svg"); - } + > span { + margin-left: auto; + } + } + + .mx_SpaceRoomDirectory_error { + position: relative; + font-weight: $font-semi-bold; + color: $notice-primary-color; + font-size: $font-15px; + line-height: $font-18px; + margin: 20px auto 12px; + padding-left: 24px; + width: max-content; + + &::before { + content: ""; + position: absolute; + height: 16px; + width: 16px; + left: 0; + background-image: url("$(res)/img/element-icons/warning-badge.svg"); } } } @@ -245,11 +251,17 @@ limitations under the License. grid-row: 1/3; .mx_AccessibleButton { - padding: 8px 18px; + line-height: $font-24px; + padding: 4px 16px; display: inline-block; visibility: hidden; } + .mx_AccessibleButton_kind_danger_outline, + .mx_AccessibleButton_kind_primary_outline { + padding: 3px 16px; // to account for the 1px border + } + .mx_Checkbox { display: inline-flex; vertical-align: middle; diff --git a/res/css/structures/_SpaceRoomView.scss b/res/css/structures/_SpaceRoomView.scss index 269f16beb7..ff51e28b7b 100644 --- a/res/css/structures/_SpaceRoomView.scss +++ b/res/css/structures/_SpaceRoomView.scss @@ -81,6 +81,16 @@ $SpaceRoomViewInnerWidth: 428px; color: $secondary-fg-color; margin-top: 12px; margin-bottom: 24px; + max-width: $SpaceRoomViewInnerWidth; + } + + .mx_AddExistingToSpace { + max-width: $SpaceRoomViewInnerWidth; + + .mx_AddExistingToSpace_content { + height: calc(100vh - 360px); + max-height: 400px; + } } .mx_SpaceRoomView_buttons { @@ -228,7 +238,8 @@ $SpaceRoomViewInnerWidth: 428px; .mx_SpaceRoomView_landing_inviteButton { position: relative; - padding-left: 40px; + padding: 4px 18px 4px 40px; + line-height: $font-24px; height: min-content; &::before { @@ -244,6 +255,27 @@ $SpaceRoomViewInnerWidth: 428px; mask-image: url('$(res)/img/element-icons/room/invite.svg'); } } + + .mx_SpaceRoomView_landing_settingsButton { + position: relative; + margin-left: 16px; + width: 24px; + height: 24px; + + &::before { + position: absolute; + content: ""; + left: 0; + top: 0; + height: 24px; + width: 24px; + background: $tertiary-fg-color; + mask-position: center; + mask-size: contain; + mask-repeat: no-repeat; + mask-image: url('$(res)/img/element-icons/settings.svg'); + } + } } .mx_SpaceRoomView_landing_topic { @@ -258,80 +290,6 @@ $SpaceRoomViewInnerWidth: 428px; background-color: $groupFilterPanel-bg-color; } - .mx_SpaceRoomView_landing_adminButtons { - margin-top: 24px; - - .mx_AccessibleButton { - position: relative; - width: 160px; - height: 124px; - box-sizing: border-box; - padding: 72px 16px 0; - border-radius: 12px; - border: 1px solid $input-border-color; - margin-right: 28px; - margin-bottom: 20px; - font-size: $font-14px; - display: inline-block; - vertical-align: bottom; - - &:last-child { - margin-right: 0; - } - - &:hover { - background-color: rgba(141, 151, 165, 0.1); - } - - &::before, &::after { - position: absolute; - content: ""; - left: 16px; - top: 16px; - height: 40px; - width: 40px; - border-radius: 20px; - } - - &::after { - mask-position: center; - mask-size: 30px; - mask-repeat: no-repeat; - background: #ffffff; // white icon fill - } - - &.mx_SpaceRoomView_landing_addButton { - &::before { - background-color: #ac3ba8; - } - - &::after { - mask-image: url('$(res)/img/element-icons/roomlist/explore.svg'); - } - } - - &.mx_SpaceRoomView_landing_createButton { - &::before { - background-color: #368bd6; - } - - &::after { - mask-image: url('$(res)/img/element-icons/roomlist/explore.svg'); - } - } - - &.mx_SpaceRoomView_landing_settingsButton { - &::before { - background-color: #5c56f5; - } - - &::after { - mask-image: url('$(res)/img/element-icons/settings.svg'); - } - } - } - } - .mx_SearchBox { margin: 0 0 20px; } diff --git a/res/css/views/dialogs/_AddExistingToSpaceDialog.scss b/res/css/views/dialogs/_AddExistingToSpaceDialog.scss index 247df52b4a..524f107165 100644 --- a/res/css/views/dialogs/_AddExistingToSpaceDialog.scss +++ b/res/css/views/dialogs/_AddExistingToSpaceDialog.scss @@ -21,6 +21,66 @@ limitations under the License. } } +.mx_AddExistingToSpace { + .mx_SearchBox { + // To match the space around the title + margin: 0 0 15px 0; + flex-grow: 0; + } + + .mx_AddExistingToSpace_content { + flex-grow: 1; + } + + .mx_AddExistingToSpace_noResults { + display: block; + margin-top: 24px; + } + + .mx_AddExistingToSpace_section { + &:not(:first-child) { + margin-top: 24px; + } + + > h3 { + margin: 0; + color: $secondary-fg-color; + font-size: $font-12px; + font-weight: $font-semi-bold; + line-height: $font-15px; + } + + .mx_AddExistingToSpace_entry { + display: flex; + margin-top: 12px; + + .mx_BaseAvatar { + margin-right: 12px; + } + + .mx_AddExistingToSpace_entry_name { + font-size: $font-15px; + line-height: 30px; + flex-grow: 1; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + margin-right: 12px; + } + + .mx_Checkbox { + align-items: center; + } + } + } + + .mx_AddExistingToSpace_section_spaces { + .mx_BaseAvatar_image { + border-radius: 8px; + } + } +} + .mx_AddExistingToSpaceDialog { width: 480px; color: $primary-fg-color; @@ -41,7 +101,7 @@ limitations under the License. .mx_BaseAvatar { display: inline-flex; - margin: 5px 16px 5px 5px; + margin: auto 16px auto 5px; vertical-align: middle; } @@ -100,85 +160,32 @@ limitations under the License. } } - .mx_SearchBox { - // To match the space around the title - margin: 0 0 15px 0; - flex-grow: 0; - } - - .mx_AddExistingToSpaceDialog_errorText { - font-weight: $font-semi-bold; - font-size: $font-12px; - line-height: $font-15px; - color: $notice-primary-color; - margin-bottom: 28px; - } - - .mx_AddExistingToSpaceDialog_content { - flex-grow: 1; - - .mx_AddExistingToSpaceDialog_noResults { - display: block; - margin-top: 24px; - } - } - - .mx_AddExistingToSpaceDialog_section { - &:not(:first-child) { - margin-top: 24px; - } - - > h3 { - margin: 0; - color: $secondary-fg-color; - font-size: $font-12px; - font-weight: $font-semi-bold; - line-height: $font-15px; - } - - .mx_AddExistingToSpaceDialog_entry { - display: flex; - margin-top: 12px; - - .mx_BaseAvatar { - margin-right: 12px; - } - - .mx_AddExistingToSpaceDialog_entry_name { - font-size: $font-15px; - line-height: 30px; - flex-grow: 1; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - margin-right: 12px; - } - - .mx_Checkbox { - align-items: center; - } - } - } - - .mx_AddExistingToSpaceDialog_section_spaces { - .mx_BaseAvatar_image { - border-radius: 8px; - } + .mx_AddExistingToSpace { + display: contents; } .mx_AddExistingToSpaceDialog_footer { display: flex; - margin-top: 32px; + margin-top: 20px; > span { flex-grow: 1; - font-size: $font-14px; + font-size: $font-12px; line-height: $font-15px; - font-weight: $font-semi-bold; + color: $secondary-fg-color; - .mx_AccessibleButton { - font-size: inherit; - display: inline-block; + .mx_ProgressBar { + height: 8px; + width: 100%; + + @mixin ProgressBarBorderRadius 8px; + } + + .mx_AddExistingToSpaceDialog_progressText { + margin-top: 8px; + font-size: $font-15px; + line-height: $font-24px; + color: $primary-fg-color; } > * { @@ -186,8 +193,54 @@ limitations under the License. } } + .mx_AddExistingToSpaceDialog_error { + padding-left: 12px; + + > img { + align-self: center; + } + + .mx_AddExistingToSpaceDialog_errorHeading { + font-weight: $font-semi-bold; + font-size: $font-15px; + line-height: $font-18px; + color: $notice-primary-color; + } + + .mx_AddExistingToSpaceDialog_errorCaption { + margin-top: 4px; + font-size: $font-12px; + line-height: $font-15px; + color: $primary-fg-color; + } + } + .mx_AccessibleButton { display: inline-block; + align-self: center; + } + + .mx_AccessibleButton_kind_primary { + padding: 8px 36px; + } + + .mx_AddExistingToSpaceDialog_retryButton { + margin-left: 12px; + padding-left: 24px; + position: relative; + + &::before { + content: ''; + position: absolute; + background-color: $primary-fg-color; + mask-repeat: no-repeat; + mask-position: center; + mask-size: contain; + mask-image: url('$(res)/img/element-icons/retry.svg'); + width: 18px; + height: 18px; + left: 0; + } } .mx_AccessibleButton_kind_link { diff --git a/res/css/views/elements/_AccessibleButton.scss b/res/css/views/elements/_AccessibleButton.scss index 0075dcb511..2997c83cfd 100644 --- a/res/css/views/elements/_AccessibleButton.scss +++ b/res/css/views/elements/_AccessibleButton.scss @@ -76,12 +76,16 @@ limitations under the License. border: 1px solid $button-danger-bg-color; } -.mx_AccessibleButton_kind_danger.mx_AccessibleButton_disabled, -.mx_AccessibleButton_kind_danger_outline.mx_AccessibleButton_disabled { +.mx_AccessibleButton_kind_danger.mx_AccessibleButton_disabled { color: $button-danger-disabled-fg-color; background-color: $button-danger-disabled-bg-color; } +.mx_AccessibleButton_kind_danger_outline.mx_AccessibleButton_disabled { + color: $button-danger-disabled-bg-color; + border-color: $button-danger-disabled-bg-color; +} + .mx_AccessibleButton_hasKind.mx_AccessibleButton_kind_danger_sm { padding: 5px 12px; color: $button-danger-fg-color; diff --git a/res/css/views/elements/_ImageView.scss b/res/css/views/elements/_ImageView.scss index 93ebcc2d56..71035dadc3 100644 --- a/res/css/views/elements/_ImageView.scss +++ b/res/css/views/elements/_ImageView.scss @@ -31,8 +31,7 @@ limitations under the License. .mx_ImageView_image { pointer-events: all; - max-width: 95%; - max-height: 95%; + flex-shrink: 0; } .mx_ImageView_panel { diff --git a/res/css/views/elements/_ProgressBar.scss b/res/css/views/elements/_ProgressBar.scss index 770978e921..c075ac74ff 100644 --- a/res/css/views/elements/_ProgressBar.scss +++ b/res/css/views/elements/_ProgressBar.scss @@ -21,7 +21,7 @@ progress.mx_ProgressBar { appearance: none; border: none; - @mixin ProgressBarBorderRadius "6px"; + @mixin ProgressBarBorderRadius 6px; @mixin ProgressBarColour $progressbar-fg-color; @mixin ProgressBarBgColour $progressbar-bg-color; ::-webkit-progress-value { diff --git a/res/css/views/messages/_MFileBody.scss b/res/css/views/messages/_MFileBody.scss index b45126acf8..c215d69ec2 100644 --- a/res/css/views/messages/_MFileBody.scss +++ b/res/css/views/messages/_MFileBody.scss @@ -61,9 +61,9 @@ limitations under the License. .mx_MFileBody_info { background-color: $message-body-panel-bg-color; - border-radius: 4px; - width: 270px; - padding: 8px; + border-radius: 12px; + width: 243px; // same width as a playable voice message, accounting for padding + padding: 6px 12px; color: $message-body-panel-fg-color; .mx_MFileBody_info_icon { @@ -82,7 +82,7 @@ limitations under the License. mask-position: center; mask-size: cover; mask-image: url('$(res)/img/element-icons/room/composer/attach.svg'); - background-color: $message-body-panel-fg-color; + background-color: $message-body-panel-icon-fg-color; width: 13px; height: 15px; diff --git a/res/css/views/messages/_MVoiceMessageBody.scss b/res/css/views/messages/_MVoiceMessageBody.scss new file mode 100644 index 0000000000..3dfb98f778 --- /dev/null +++ b/res/css/views/messages/_MVoiceMessageBody.scss @@ -0,0 +1,19 @@ +/* +Copyright 2021 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +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_MVoiceMessageBody { + display: inline-block; // makes the playback controls magically line up +} diff --git a/res/css/views/messages/_MessageActionBar.scss b/res/css/views/messages/_MessageActionBar.scss index 1254b496b5..3ecbef0d1f 100644 --- a/res/css/views/messages/_MessageActionBar.scss +++ b/res/css/views/messages/_MessageActionBar.scss @@ -105,3 +105,11 @@ limitations under the License. .mx_MessageActionBar_optionsButton::after { mask-image: url('$(res)/img/element-icons/context-menu.svg'); } + +.mx_MessageActionBar_resendButton::after { + mask-image: url('$(res)/img/element-icons/retry.svg'); +} + +.mx_MessageActionBar_cancelButton::after { + mask-image: url('$(res)/img/element-icons/trashcan.svg'); +} diff --git a/res/css/views/messages/_ReactionsRowButton.scss b/res/css/views/messages/_ReactionsRowButton.scss index 7158ffc027..c132fa5a0f 100644 --- a/res/css/views/messages/_ReactionsRowButton.scss +++ b/res/css/views/messages/_ReactionsRowButton.scss @@ -34,6 +34,10 @@ limitations under the License. border-color: $reaction-row-button-selected-border-color; } + &.mx_AccessibleButton_disabled { + cursor: not-allowed; + } + .mx_ReactionsRowButton_content { max-width: 100px; overflow: hidden; diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index 35a5d8fc15..a6ccde93d3 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -219,10 +219,6 @@ $left-gutter: 64px; color: $accent-fg-color; } -.mx_EventTile_notSent { - color: $event-notsent-color; -} - .mx_EventTile_receiptSent, .mx_EventTile_receiptSending { // We don't use `position: relative` on the element because then it won't line diff --git a/res/css/views/rooms/_VoiceRecordComposerTile.scss b/res/css/views/rooms/_VoiceRecordComposerTile.scss index 8100a03890..b87211a847 100644 --- a/res/css/views/rooms/_VoiceRecordComposerTile.scss +++ b/res/css/views/rooms/_VoiceRecordComposerTile.scss @@ -35,44 +35,42 @@ limitations under the License. } } -.mx_VoiceRecordComposerTile_waveformContainer { - padding: 5px; - padding-right: 4px; // there's 1px from the waveform itself, so account for that - padding-left: 15px; // +10px for the live circle, +5px for regular padding - background-color: $voice-record-waveform-bg-color; - border-radius: 12px; - margin-right: 12px; // isolate from stop button +.mx_VoiceRecordComposerTile_delete { + width: 14px; // w&h are size of icon + height: 18px; + vertical-align: middle; + margin-right: 11px; // distance from left edge of waveform container (container has some margin too) + background-color: $voice-record-icon-color; + mask-repeat: no-repeat; + mask-size: contain; + mask-image: url('$(res)/img/element-icons/trashcan.svg'); +} - // Cheat at alignment a bit - display: flex; - align-items: center; +.mx_VoiceRecordComposerTile_recording.mx_VoiceMessagePrimaryContainer { + // Note: remaining class properties are in the PlayerContainer CSS. + + margin: 6px; // force the composer area to put a gutter around us + margin-right: 12px; // isolate from stop button position: relative; // important for the live circle - color: $voice-record-waveform-fg-color; - font-size: $font-14px; + &.mx_VoiceRecordComposerTile_recording { + // We are putting the circle in this padding, so we need +10px from the regular + // padding on the left side. + padding-left: 22px; - &::before { - animation: recording-pulse 2s infinite; + &::before { + animation: recording-pulse 2s infinite; - content: ''; - background-color: $voice-record-live-circle-color; - width: 10px; - height: 10px; - position: absolute; - left: 8px; - top: 16px; // vertically center - border-radius: 10px; - } - - .mx_Waveform_bar { - background-color: $voice-record-waveform-fg-color; - } - - .mx_Clock { - padding-right: 8px; // isolate from waveform - padding-left: 10px; // isolate from live circle - width: 42px; // we're not using a monospace font, so fake it + content: ''; + background-color: $voice-record-live-circle-color; + width: 10px; + height: 10px; + position: absolute; + left: 12px; // 12px from the left edge for container padding + top: 18px; // vertically center (middle align with clock) + border-radius: 10px; + } } } diff --git a/res/css/views/settings/tabs/user/_HelpUserSettingsTab.scss b/res/css/views/settings/tabs/user/_HelpUserSettingsTab.scss index 109edfff81..0f879d209e 100644 --- a/res/css/views/settings/tabs/user/_HelpUserSettingsTab.scss +++ b/res/css/views/settings/tabs/user/_HelpUserSettingsTab.scss @@ -22,3 +22,34 @@ limitations under the License. .mx_HelpUserSettingsTab span.mx_AccessibleButton { word-break: break-word; } + +.mx_HelpUserSettingsTab code { + word-break: break-all; + user-select: all; +} + +.mx_HelpUserSettingsTab_accessToken { + display: flex; + justify-content: space-between; + border-radius: 5px; + border: solid 1px $light-fg-color; + margin-bottom: 10px; + margin-top: 10px; + padding: 10px; +} + +.mx_HelpUserSettingsTab_accessToken_copy { + flex-shrink: 0; + cursor: pointer; + margin-left: 20px; + display: inherit; +} + +.mx_HelpUserSettingsTab_accessToken_copy > div { + mask-image: url($copy-button-url); + background-color: $message-action-bar-fg-color; + margin-left: 5px; + width: 20px; + height: 20px; + background-repeat: no-repeat; +} diff --git a/res/css/views/voice_messages/_PlayPauseButton.scss b/res/css/views/voice_messages/_PlayPauseButton.scss new file mode 100644 index 0000000000..6caedafa29 --- /dev/null +++ b/res/css/views/voice_messages/_PlayPauseButton.scss @@ -0,0 +1,51 @@ +/* +Copyright 2021 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +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_PlayPauseButton { + position: relative; + width: 32px; + height: 32px; + border-radius: 32px; + background-color: $voice-playback-button-bg-color; + + &::before { + content: ''; + position: absolute; // sizing varies by icon + background-color: $voice-playback-button-fg-color; + mask-repeat: no-repeat; + mask-size: contain; + } + + &.mx_PlayPauseButton_disabled::before { + opacity: 0.5; + } + + &.mx_PlayPauseButton_play::before { + width: 13px; + height: 16px; + top: 8px; // center + left: 12px; // center + mask-image: url('$(res)/img/element-icons/play.svg'); + } + + &.mx_PlayPauseButton_pause::before { + width: 10px; + height: 12px; + top: 10px; // center + left: 11px; // center + mask-image: url('$(res)/img/element-icons/pause.svg'); + } +} diff --git a/res/css/views/voice_messages/_PlaybackContainer.scss b/res/css/views/voice_messages/_PlaybackContainer.scss new file mode 100644 index 0000000000..64e8f445e1 --- /dev/null +++ b/res/css/views/voice_messages/_PlaybackContainer.scss @@ -0,0 +1,53 @@ +/* +Copyright 2021 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +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. +*/ + +// Dev note: there's no actual component called . These classes +// are shared amongst multiple voice message components. + +// Container for live recording and playback controls +.mx_VoiceMessagePrimaryContainer { + // 7px top and bottom for visual design. 12px left & right, but the waveform (right) + // has a 1px padding on it that we want to account for. + padding: 7px 12px 7px 11px; + background-color: $voice-record-waveform-bg-color; + border-radius: 12px; + + // Cheat at alignment a bit + display: flex; + align-items: center; + + color: $voice-record-waveform-fg-color; + font-size: $font-14px; + line-height: $font-24px; + + .mx_Waveform { + .mx_Waveform_bar { + background-color: $voice-record-waveform-incomplete-fg-color; + + &.mx_Waveform_bar_100pct { + // Small animation to remove the mechanical feel of progress + transition: background-color 250ms ease; + background-color: $voice-record-waveform-fg-color; + } + } + } + + .mx_Clock { + width: 42px; // we're not using a monospace font, so fake it + padding-right: 6px; // with the fixed width this ends up as a visual 8px most of the time, as intended. + padding-left: 8px; // isolate from recording circle / play control + } +} diff --git a/res/css/views/voip/_CallView.scss b/res/css/views/voip/_CallView.scss index d13272c8c0..0be75be28c 100644 --- a/res/css/views/voip/_CallView.scss +++ b/res/css/views/voip/_CallView.scss @@ -17,7 +17,7 @@ limitations under the License. .mx_CallView { border-radius: 8px; - background-color: $voipcall-plinth-color; + background-color: $dark-panel-bg-color; padding-left: 8px; padding-right: 8px; // XXX: CallContainer sets pointer-events: none - should probably be set back in a better place @@ -40,7 +40,8 @@ limitations under the License. width: 320px; padding-bottom: 8px; margin-top: 10px; - box-shadow: 0px 14px 24px rgba(0, 0, 0, 0.08); + background-color: $voipcall-plinth-color; + box-shadow: 0px 4px 20px rgba(0, 0, 0, 0.20); border-radius: 8px; .mx_CallView_voice { @@ -64,14 +65,17 @@ limitations under the License. } } -.mx_CallView_voice { +.mx_CallView_content { position: relative; display: flex; - flex-direction: column; + border-radius: 8px; +} + +.mx_CallView_voice { align-items: center; justify-content: center; + flex-direction: column; background-color: $inverted-bg-color; - border-radius: 8px; } .mx_CallView_voice_avatarsContainer { @@ -108,9 +112,7 @@ limitations under the License. .mx_CallView_video { width: 100%; height: 100%; - position: relative; z-index: 30; - border-radius: 8px; overflow: hidden; } diff --git a/res/css/views/voip/_VideoFeed.scss b/res/css/views/voip/_VideoFeed.scss index 8ead8bba3e..7d85ac264e 100644 --- a/res/css/views/voip/_VideoFeed.scss +++ b/res/css/views/voip/_VideoFeed.scss @@ -14,21 +14,37 @@ See the License for the specific language governing permissions and limitations under the License. */ +.mx_VideoFeed_voice { + // We don't want to collide with the call controls that have 52px of height + padding-bottom: 52px; + background-color: $inverted-bg-color; +} + + .mx_VideoFeed_remote { width: 100%; height: 100%; - background-color: #000; - z-index: 50; + display: flex; + justify-content: center; + align-items: center; + + &.mx_VideoFeed_video { + background-color: #000; + } } .mx_VideoFeed_local { - width: 25%; - height: 25%; + max-width: 25%; + max-height: 25%; position: absolute; right: 10px; top: 10px; z-index: 100; border-radius: 4px; + + &.mx_VideoFeed_video { + background-color: transparent; + } } .mx_VideoFeed_mirror { diff --git a/res/img/element-icons/pause.svg b/res/img/element-icons/pause.svg new file mode 100644 index 0000000000..293c0a10d8 --- /dev/null +++ b/res/img/element-icons/pause.svg @@ -0,0 +1,4 @@ + + + + diff --git a/res/img/element-icons/play.svg b/res/img/element-icons/play.svg new file mode 100644 index 0000000000..339e20b729 --- /dev/null +++ b/res/img/element-icons/play.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/img/element-icons/retry.svg b/res/img/element-icons/retry.svg new file mode 100644 index 0000000000..09448d6458 --- /dev/null +++ b/res/img/element-icons/retry.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/img/element-icons/trashcan.svg b/res/img/element-icons/trashcan.svg new file mode 100644 index 0000000000..f8fb8b5c46 --- /dev/null +++ b/res/img/element-icons/trashcan.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss index 925d268eb0..2d0e3d2a8b 100644 --- a/res/themes/dark/css/_dark.scss +++ b/res/themes/dark/css/_dark.scss @@ -9,6 +9,7 @@ $header-panel-text-primary-color: #B9BEC6; $header-panel-text-secondary-color: #c8c8cd; $text-primary-color: #ffffff; $text-secondary-color: #B9BEC6; +$quaternary-fg-color: #6F7882; $search-bg-color: #181b21; $search-placeholder-color: #61708b; $room-highlight-color: #343a46; @@ -63,6 +64,8 @@ $input-invalid-border-color: $warning-color; $field-focused-label-bg-color: $bg-color; +$resend-button-divider-color: #b9bec64a; // muted-text with a 4A opacity. + // scrollbars $scrollbar-thumb-color: rgba(255, 255, 255, 0.2); $scrollbar-track-color: transparent; @@ -110,7 +113,7 @@ $header-divider-color: $header-panel-text-primary-color; $composer-e2e-icon-color: $header-panel-text-primary-color; // this probably shouldn't have it's own colour -$voipcall-plinth-color: #21262c; +$voipcall-plinth-color: #394049; // ******************** @@ -203,9 +206,18 @@ $breadcrumb-placeholder-bg-color: #272c35; $user-tile-hover-bg-color: $header-panel-bg-color; -$message-body-panel-bg-color: #21262c82; -$message-body-panel-icon-bg-color: #8e99a4; -$message-body-panel-fg-color: $primary-fg-color; +$message-body-panel-fg-color: $secondary-fg-color; +$message-body-panel-bg-color: #394049; // "Dark Tile" +$message-body-panel-icon-fg-color: #21262C; // "Separator" +$message-body-panel-icon-bg-color: $tertiary-fg-color; + +$voice-record-stop-border-color: $quaternary-fg-color; +$voice-record-waveform-bg-color: $message-body-panel-bg-color; +$voice-record-waveform-fg-color: $message-body-panel-fg-color; +$voice-record-waveform-incomplete-fg-color: $quaternary-fg-color; +$voice-record-icon-color: $quaternary-fg-color; +$voice-playback-button-bg-color: $message-body-panel-icon-bg-color; +$voice-playback-button-fg-color: $message-body-panel-icon-fg-color; // Appearance tab colors $appearance-tab-border-color: $room-highlight-color; diff --git a/res/themes/legacy-dark/css/_legacy-dark.scss b/res/themes/legacy-dark/css/_legacy-dark.scss index 28e6e22326..a852ad94e9 100644 --- a/res/themes/legacy-dark/css/_legacy-dark.scss +++ b/res/themes/legacy-dark/css/_legacy-dark.scss @@ -61,6 +61,8 @@ $input-invalid-border-color: $warning-color; $field-focused-label-bg-color: $bg-color; +$resend-button-divider-color: $muted-fg-color; + // scrollbars $scrollbar-thumb-color: rgba(255, 255, 255, 0.2); $scrollbar-track-color: transparent; @@ -107,7 +109,7 @@ $header-divider-color: $header-panel-text-primary-color; $composer-e2e-icon-color: $header-panel-text-primary-color; // this probably shouldn't have it's own colour -$voipcall-plinth-color: #f2f5f8; +$voipcall-plinth-color: #394049; // ******************** @@ -198,9 +200,19 @@ $breadcrumb-placeholder-bg-color: #272c35; $user-tile-hover-bg-color: $header-panel-bg-color; -$message-body-panel-bg-color: #21262c82; -$message-body-panel-icon-bg-color: #8e99a4; -$message-body-panel-fg-color: $primary-fg-color; +$message-body-panel-fg-color: $secondary-fg-color; +$message-body-panel-bg-color: #394049; +$message-body-panel-icon-fg-color: $primary-bg-color; +$message-body-panel-icon-bg-color: $secondary-fg-color; + +// See non-legacy dark for variable information +$voice-record-stop-border-color: #6F7882; +$voice-record-waveform-bg-color: $message-body-panel-bg-color; +$voice-record-waveform-fg-color: $message-body-panel-fg-color; +$voice-record-waveform-incomplete-fg-color: #6F7882; +$voice-record-icon-color: #6F7882; +$voice-playback-button-bg-color: $tertiary-fg-color; +$voice-playback-button-fg-color: #21262C; // Appearance tab colors $appearance-tab-border-color: $room-highlight-color; diff --git a/res/themes/legacy-light/css/_legacy-light.scss b/res/themes/legacy-light/css/_legacy-light.scss index 7b6bdad4a4..84666bc662 100644 --- a/res/themes/legacy-light/css/_legacy-light.scss +++ b/res/themes/legacy-light/css/_legacy-light.scss @@ -97,6 +97,8 @@ $input-invalid-border-color: $warning-color; $field-focused-label-bg-color: #ffffff; +$resend-button-divider-color: $input-darker-bg-color; + $button-bg-color: $accent-color; $button-fg-color: white; @@ -174,7 +176,7 @@ $composer-e2e-icon-color: #91a1c0; $header-divider-color: #91a1c0; // this probably shouldn't have it's own colour -$voipcall-plinth-color: #f2f5f8; +$voipcall-plinth-color: #F4F6FA; // ******************** @@ -189,13 +191,6 @@ $roomsublist-skeleton-ui-bg: linear-gradient(180deg, #ffffff 0%, #ffffff00 100%) $groupFilterPanel-divider-color: $roomlist-header-color; -// See non-legacy _light for variable information -$voice-record-stop-border-color: #E3E8F0; -$voice-record-stop-symbol-color: #ff4b55; -$voice-record-waveform-bg-color: #E3E8F0; -$voice-record-waveform-fg-color: $muted-fg-color; -$voice-record-live-circle-color: #ff4b55; - $roomtile-preview-color: #9e9e9e; $roomtile-default-badge-bg-color: #61708b; $roomtile-selected-bg-color: #fff; @@ -328,9 +323,21 @@ $breadcrumb-placeholder-bg-color: #e8eef5; $user-tile-hover-bg-color: $header-panel-bg-color; -$message-body-panel-bg-color: #e3e8f082; -$message-body-panel-icon-bg-color: #ffffff; -$message-body-panel-fg-color: $muted-fg-color; +$message-body-panel-fg-color: $secondary-fg-color; +$message-body-panel-bg-color: #E3E8F0; +$message-body-panel-icon-fg-color: $secondary-fg-color; +$message-body-panel-icon-bg-color: $primary-bg-color; + +// See non-legacy _light for variable information +$voice-record-stop-symbol-color: #ff4b55; +$voice-record-live-circle-color: #ff4b55; +$voice-record-stop-border-color: #E3E8F0; +$voice-record-waveform-bg-color: $message-body-panel-bg-color; +$voice-record-waveform-fg-color: $message-body-panel-fg-color; +$voice-record-waveform-incomplete-fg-color: #C1C6CD; +$voice-record-icon-color: $tertiary-fg-color; +$voice-playback-button-bg-color: $message-body-panel-icon-bg-color; +$voice-playback-button-fg-color: $message-body-panel-icon-fg-color; // FontSlider colors $appearance-tab-border-color: $input-darker-bg-color; diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss index 5b46138dae..c889f43d0b 100644 --- a/res/themes/light/css/_light.scss +++ b/res/themes/light/css/_light.scss @@ -21,6 +21,7 @@ $notice-primary-bg-color: rgba(255, 75, 85, 0.16); $primary-fg-color: #2e2f32; $secondary-fg-color: #737D8C; $tertiary-fg-color: #8D99A5; +$quaternary-fg-color: #C1C6CD; $header-panel-bg-color: #f3f8fd; // typical text (dark-on-white in light skin) @@ -91,6 +92,8 @@ $field-focused-label-bg-color: #ffffff; $button-bg-color: $accent-color; $button-fg-color: white; +$resend-button-divider-color: $input-darker-bg-color; + // apart from login forms, which have stronger border $strong-input-border-color: #c7c7c7; @@ -165,7 +168,7 @@ $composer-e2e-icon-color: #91A1C0; $header-divider-color: #91A1C0; // this probably shouldn't have it's own colour -$voipcall-plinth-color: #f2f5f8; +$voipcall-plinth-color: #F4F6FA; // ******************** @@ -180,12 +183,6 @@ $roomsublist-skeleton-ui-bg: linear-gradient(180deg, #ffffff 0%, #ffffff00 100%) $groupFilterPanel-divider-color: $roomlist-header-color; -$voice-record-stop-border-color: #E3E8F0; -$voice-record-stop-symbol-color: #ff4b55; // $warning-color, but without letting people change it in themes -$voice-record-waveform-bg-color: #E3E8F0; -$voice-record-waveform-fg-color: $muted-fg-color; -$voice-record-live-circle-color: #ff4b55; // $warning-color, but without letting people change it in themes - $roomtile-preview-color: $secondary-fg-color; $roomtile-default-badge-bg-color: #61708b; $roomtile-selected-bg-color: #FFF; @@ -325,9 +322,23 @@ $breadcrumb-placeholder-bg-color: #e8eef5; $user-tile-hover-bg-color: $header-panel-bg-color; -$message-body-panel-bg-color: #e3e8f082; -$message-body-panel-icon-bg-color: #ffffff; -$message-body-panel-fg-color: $muted-fg-color; +$message-body-panel-fg-color: $secondary-fg-color; +$message-body-panel-bg-color: #E3E8F0; // "Separator" +$message-body-panel-icon-fg-color: $secondary-fg-color; +$message-body-panel-icon-bg-color: $primary-bg-color; + +// These two don't change between themes. They are the $warning-color, but we don't +// want custom themes to affect them by accident. +$voice-record-stop-symbol-color: #ff4b55; +$voice-record-live-circle-color: #ff4b55; + +$voice-record-stop-border-color: #E3E8F0; // "Separator" +$voice-record-waveform-bg-color: $message-body-panel-bg-color; +$voice-record-waveform-fg-color: $message-body-panel-fg-color; +$voice-record-waveform-incomplete-fg-color: $quaternary-fg-color; +$voice-record-icon-color: $tertiary-fg-color; +$voice-playback-button-bg-color: $message-body-panel-icon-bg-color; +$voice-playback-button-fg-color: $message-body-panel-icon-fg-color; // FontSlider colors $appearance-tab-border-color: $input-darker-bg-color; diff --git a/scripts/compare-file.js b/scripts/compare-file.js deleted file mode 100644 index f53275ebfa..0000000000 --- a/scripts/compare-file.js +++ /dev/null @@ -1,10 +0,0 @@ -const fs = require("fs"); - -if (process.argv.length < 4) throw new Error("Missing source and target file arguments"); - -const sourceFile = fs.readFileSync(process.argv[2], 'utf8'); -const targetFile = fs.readFileSync(process.argv[3], 'utf8'); - -if (sourceFile !== targetFile) { - throw new Error("Files do not match"); -} diff --git a/scripts/gen-i18n.js b/scripts/gen-i18n.js deleted file mode 100755 index 91733469f7..0000000000 --- a/scripts/gen-i18n.js +++ /dev/null @@ -1,304 +0,0 @@ -#!/usr/bin/env node - -/* -Copyright 2017 New Vector Ltd - -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. -*/ - -/** - * Regenerates the translations en_EN file by walking the source tree and - * parsing each file with the appropriate parser. Emits a JSON file with the - * translatable strings mapped to themselves in the order they appeared - * in the files and grouped by the file they appeared in. - * - * Usage: node scripts/gen-i18n.js - */ -const fs = require('fs'); -const path = require('path'); - -const walk = require('walk'); - -const parser = require("@babel/parser"); -const traverse = require("@babel/traverse"); - -const TRANSLATIONS_FUNCS = ['_t', '_td']; - -const INPUT_TRANSLATIONS_FILE = 'src/i18n/strings/en_EN.json'; -const OUTPUT_FILE = 'src/i18n/strings/en_EN.json'; - -// NB. The sync version of walk is broken for single files so we walk -// all of res rather than just res/home.html. -// https://git.daplie.com/Daplie/node-walk/merge_requests/1 fixes it, -// or if we get bored waiting for it to be merged, we could switch -// to a project that's actively maintained. -const SEARCH_PATHS = ['src', 'res']; - -function getObjectValue(obj, key) { - for (const prop of obj.properties) { - if (prop.key.type === 'Identifier' && prop.key.name === key) { - return prop.value; - } - } - return null; -} - -function getTKey(arg) { - if (arg.type === 'Literal' || arg.type === "StringLiteral") { - return arg.value; - } else if (arg.type === 'BinaryExpression' && arg.operator === '+') { - return getTKey(arg.left) + getTKey(arg.right); - } else if (arg.type === 'TemplateLiteral') { - return arg.quasis.map((q) => { - return q.value.raw; - }).join(''); - } - return null; -} - -function getFormatStrings(str) { - // Match anything that starts with % - // We could make a regex that matched the full placeholder, but this - // would just not match invalid placeholders and so wouldn't help us - // detect the invalid ones. - // Also note that for simplicity, this just matches a % character and then - // anything up to the next % character (or a single %, or end of string). - const formatStringRe = /%([^%]+|%|$)/g; - const formatStrings = new Set(); - - let match; - while ( (match = formatStringRe.exec(str)) !== null ) { - const placeholder = match[1]; // Minus the leading '%' - if (placeholder === '%') continue; // Literal % is %% - - const placeholderMatch = placeholder.match(/^\((.*?)\)(.)/); - if (placeholderMatch === null) { - throw new Error("Invalid format specifier: '"+match[0]+"'"); - } - if (placeholderMatch.length < 3) { - throw new Error("Malformed format specifier"); - } - const placeholderName = placeholderMatch[1]; - const placeholderFormat = placeholderMatch[2]; - - if (placeholderFormat !== 's') { - throw new Error(`'${placeholderFormat}' used as format character: you probably meant 's'`); - } - - formatStrings.add(placeholderName); - } - - return formatStrings; -} - -function getTranslationsJs(file) { - const contents = fs.readFileSync(file, { encoding: 'utf8' }); - - const trs = new Set(); - - try { - const plugins = [ - // https://babeljs.io/docs/en/babel-parser#plugins - "classProperties", - "objectRestSpread", - "throwExpressions", - "exportDefaultFrom", - "decorators-legacy", - ]; - - if (file.endsWith(".js") || file.endsWith(".jsx")) { - // all JS is assumed to be flow or react - plugins.push("flow", "jsx"); - } else if (file.endsWith(".ts")) { - // TS can't use JSX unless it's a TSX file (otherwise angle casts fail) - plugins.push("typescript"); - } else if (file.endsWith(".tsx")) { - // When the file is a TSX file though, enable JSX parsing - plugins.push("typescript", "jsx"); - } - - const babelParsed = parser.parse(contents, { - allowImportExportEverywhere: true, - errorRecovery: true, - sourceFilename: file, - tokens: true, - plugins, - }); - traverse.default(babelParsed, { - enter: (p) => { - const node = p.node; - if (p.isCallExpression() && node.callee && TRANSLATIONS_FUNCS.includes(node.callee.name)) { - const tKey = getTKey(node.arguments[0]); - - // This happens whenever we call _t with non-literals (ie. whenever we've - // had to use a _td to compensate) so is expected. - if (tKey === null) return; - - // check the format string against the args - // We only check _t: _td has no args - if (node.callee.name === '_t') { - try { - const placeholders = getFormatStrings(tKey); - for (const placeholder of placeholders) { - if (node.arguments.length < 2) { - throw new Error(`Placeholder found ('${placeholder}') but no substitutions given`); - } - const value = getObjectValue(node.arguments[1], placeholder); - if (value === null) { - throw new Error(`No value found for placeholder '${placeholder}'`); - } - } - - // Validate tag replacements - if (node.arguments.length > 2) { - const tagMap = node.arguments[2]; - for (const prop of tagMap.properties || []) { - if (prop.key.type === 'Literal') { - const tag = prop.key.value; - // RegExp same as in src/languageHandler.js - const regexp = new RegExp(`(<${tag}>(.*?)<\\/${tag}>|<${tag}>|<${tag}\\s*\\/>)`); - if (!tKey.match(regexp)) { - throw new Error(`No match for ${regexp} in ${tKey}`); - } - } - } - } - - } catch (e) { - console.log(); - console.error(`ERROR: ${file}:${node.loc.start.line} ${tKey}`); - console.error(e); - process.exit(1); - } - } - - let isPlural = false; - if (node.arguments.length > 1 && node.arguments[1].type === 'ObjectExpression') { - const countVal = getObjectValue(node.arguments[1], 'count'); - if (countVal) { - isPlural = true; - } - } - - if (isPlural) { - trs.add(tKey + "|other"); - const plurals = enPlurals[tKey]; - if (plurals) { - for (const pluralType of Object.keys(plurals)) { - trs.add(tKey + "|" + pluralType); - } - } - } else { - trs.add(tKey); - } - } - }, - }); - } catch (e) { - console.error(e); - process.exit(1); - } - - return trs; -} - -function getTranslationsOther(file) { - const contents = fs.readFileSync(file, { encoding: 'utf8' }); - - const trs = new Set(); - - // Taken from element-web src/components/structures/HomePage.js - const translationsRegex = /_t\(['"]([\s\S]*?)['"]\)/mg; - let matches; - while (matches = translationsRegex.exec(contents)) { - trs.add(matches[1]); - } - return trs; -} - -// gather en_EN plural strings from the input translations file: -// the en_EN strings are all in the source with the exception of -// pluralised strings, which we need to pull in from elsewhere. -const inputTranslationsRaw = JSON.parse(fs.readFileSync(INPUT_TRANSLATIONS_FILE, { encoding: 'utf8' })); -const enPlurals = {}; - -for (const key of Object.keys(inputTranslationsRaw)) { - const parts = key.split("|"); - if (parts.length > 1) { - const plurals = enPlurals[parts[0]] || {}; - plurals[parts[1]] = inputTranslationsRaw[key]; - enPlurals[parts[0]] = plurals; - } -} - -const translatables = new Set(); - -const walkOpts = { - listeners: { - names: function(root, nodeNamesArray) { - // Sort the names case insensitively and alphabetically to - // maintain some sense of order between the different strings. - nodeNamesArray.sort((a, b) => { - a = a.toLowerCase(); - b = b.toLowerCase(); - if (a > b) return 1; - if (a < b) return -1; - return 0; - }); - }, - file: function(root, fileStats, next) { - const fullPath = path.join(root, fileStats.name); - - let trs; - if (fileStats.name.endsWith('.js') || fileStats.name.endsWith('.ts') || fileStats.name.endsWith('.tsx')) { - trs = getTranslationsJs(fullPath); - } else if (fileStats.name.endsWith('.html')) { - trs = getTranslationsOther(fullPath); - } else { - return; - } - console.log(`${fullPath} (${trs.size} strings)`); - for (const tr of trs.values()) { - // Convert DOS line endings to unix - translatables.add(tr.replace(/\r\n/g, "\n")); - } - }, - } -}; - -for (const path of SEARCH_PATHS) { - if (fs.existsSync(path)) { - walk.walkSync(path, walkOpts); - } -} - -const trObj = {}; -for (const tr of translatables) { - if (tr.includes("|")) { - if (inputTranslationsRaw[tr]) { - trObj[tr] = inputTranslationsRaw[tr]; - } else { - trObj[tr] = tr.split("|")[0]; - } - } else { - trObj[tr] = tr; - } -} - -fs.writeFileSync( - OUTPUT_FILE, - JSON.stringify(trObj, translatables.values(), 4) + "\n" -); - -console.log(); -console.log(`Wrote ${translatables.size} strings to ${OUTPUT_FILE}`); diff --git a/scripts/prune-i18n.js b/scripts/prune-i18n.js deleted file mode 100755 index b4fe8d69f5..0000000000 --- a/scripts/prune-i18n.js +++ /dev/null @@ -1,68 +0,0 @@ -#!/usr/bin/env node - -/* -Copyright 2017 New Vector Ltd - -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. -*/ - -/* - * Looks through all the translation files and removes any strings - * which don't appear in en_EN.json. - * Use this if you remove a translation, but merge any outstanding changes - * from weblate first or you'll need to resolve the conflict in weblate. - */ - -const fs = require('fs'); -const path = require('path'); - -const I18NDIR = 'src/i18n/strings'; - -const enStringsRaw = JSON.parse(fs.readFileSync(path.join(I18NDIR, 'en_EN.json'))); - -const enStrings = new Set(); -for (const str of Object.keys(enStringsRaw)) { - const parts = str.split('|'); - if (parts.length > 1) { - enStrings.add(parts[0]); - } else { - enStrings.add(str); - } -} - -for (const filename of fs.readdirSync(I18NDIR)) { - if (filename === 'en_EN.json') continue; - if (filename === 'basefile.json') continue; - if (!filename.endsWith('.json')) continue; - - const trs = JSON.parse(fs.readFileSync(path.join(I18NDIR, filename))); - const oldLen = Object.keys(trs).length; - for (const tr of Object.keys(trs)) { - const parts = tr.split('|'); - const trKey = parts.length > 1 ? parts[0] : tr; - if (!enStrings.has(trKey)) { - delete trs[tr]; - } - } - - const removed = oldLen - Object.keys(trs).length; - if (removed > 0) { - console.log(`${filename}: removed ${removed} translations`); - // XXX: This is totally relying on the impl serialising the JSON object in the - // same order as they were parsed from the file. JSON.stringify() has a specific argument - // that can be used to control the order, but JSON.parse() lacks any kind of equivalent. - // Empirically this does maintain the order on my system, so I'm going to leave it like - // this for now. - fs.writeFileSync(path.join(I18NDIR, filename), JSON.stringify(trs, undefined, 4) + "\n"); - } -} diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts index 41257c21f0..e8f2f1bc08 100644 --- a/src/@types/global.d.ts +++ b/src/@types/global.d.ts @@ -1,5 +1,5 @@ /* -Copyright 2020 The Matrix.org Foundation C.I.C. +Copyright 2020-2021 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -39,7 +39,9 @@ import {ModalWidgetStore} from "../stores/ModalWidgetStore"; import { WidgetLayoutStore } from "../stores/widgets/WidgetLayoutStore"; import VoipUserMapper from "../VoipUserMapper"; import {SpaceStoreClass} from "../stores/SpaceStore"; -import {VoiceRecording} from "../voice/VoiceRecording"; +import TypingStore from "../stores/TypingStore"; +import { EventIndexPeg } from "../indexing/EventIndexPeg"; +import {VoiceRecordingStore} from "../stores/VoiceRecordingStore"; declare global { interface Window { @@ -71,12 +73,16 @@ declare global { mxModalWidgetStore: ModalWidgetStore; mxVoipUserMapper: VoipUserMapper; mxSpaceStore: SpaceStoreClass; - mxVoiceRecorder: typeof VoiceRecording; + mxVoiceRecordingStore: VoiceRecordingStore; + mxTypingStore: TypingStore; + mxEventIndexPeg: EventIndexPeg; } interface Document { // https://developer.mozilla.org/en-US/docs/Web/API/Document/hasStorageAccess hasStorageAccess?: () => Promise; + // https://developer.mozilla.org/en-US/docs/Web/API/Document/requestStorageAccess + requestStorageAccess?: () => Promise; // Safari & IE11 only have this prefixed: we used prefixed versions // previously so let's continue to support them for now @@ -112,6 +118,16 @@ declare global { interface HTMLAudioElement { type?: string; + // sinkId & setSinkId are experimental and typescript doesn't know about them + sinkId: string; + setSinkId(outputId: string); + } + + interface HTMLVideoElement { + type?: string; + // sinkId & setSinkId are experimental and typescript doesn't know about them + sinkId: string; + setSinkId(outputId: string); } interface Element { diff --git a/src/Avatar.ts b/src/Avatar.ts index 76c88faa1c..a6499c688e 100644 --- a/src/Avatar.ts +++ b/src/Avatar.ts @@ -20,6 +20,7 @@ import {Room} from "matrix-js-sdk/src/models/room"; import DMRoomMap from './utils/DMRoomMap'; import {mediaFromMxc} from "./customisations/Media"; +import SettingsStore from "./settings/SettingsStore"; export type ResizeMethod = "crop" | "scale"; @@ -27,11 +28,7 @@ export type ResizeMethod = "crop" | "scale"; export function avatarUrlForMember(member: RoomMember, width: number, height: number, resizeMethod: ResizeMethod) { let url: string; if (member?.getMxcAvatarUrl()) { - url = mediaFromMxc(member.getMxcAvatarUrl()).getThumbnailOfSourceHttp( - Math.floor(width * window.devicePixelRatio), - Math.floor(height * window.devicePixelRatio), - resizeMethod, - ); + url = mediaFromMxc(member.getMxcAvatarUrl()).getThumbnailOfSourceHttp(width, height, resizeMethod); } if (!url) { // member can be null here currently since on invites, the JS SDK @@ -44,11 +41,7 @@ export function avatarUrlForMember(member: RoomMember, width: number, height: nu export function avatarUrlForUser(user: User, width: number, height: number, resizeMethod?: ResizeMethod) { if (!user.avatarUrl) return null; - return mediaFromMxc(user.avatarUrl).getThumbnailOfSourceHttp( - Math.floor(width * window.devicePixelRatio), - Math.floor(height * window.devicePixelRatio), - resizeMethod, - ); + return mediaFromMxc(user.avatarUrl).getThumbnailOfSourceHttp(width, height, resizeMethod); } function isValidHexColor(color: string): boolean { @@ -151,7 +144,7 @@ export function avatarUrlForRoom(room: Room, width: number, height: number, resi } // space rooms cannot be DMs so skip the rest - if (room.isSpaceRoom()) return null; + if (SettingsStore.getValue("feature_spaces") && room.isSpaceRoom()) return null; let otherMember = null; const otherUserId = DMRoomMap.shared().getUserIdForRoomId(room.roomId); diff --git a/src/BasePlatform.ts b/src/BasePlatform.ts index b6012d7597..5483ea6874 100644 --- a/src/BasePlatform.ts +++ b/src/BasePlatform.ts @@ -258,7 +258,7 @@ export default abstract class BasePlatform { return null; } - setLanguage(preferredLangs: string[]) {} + async setLanguage(preferredLangs: string[]) {} setSpellCheckLanguages(preferredLangs: string[]) {} diff --git a/src/CallHandler.tsx b/src/CallHandler.tsx index be687a4474..0268ebfe46 100644 --- a/src/CallHandler.tsx +++ b/src/CallHandler.tsx @@ -59,7 +59,6 @@ import {MatrixClientPeg} from './MatrixClientPeg'; import PlatformPeg from './PlatformPeg'; import Modal from './Modal'; import { _t } from './languageHandler'; -import { createNewMatrixCall } from 'matrix-js-sdk/src/webrtc/call'; import dis from './dispatcher/dispatcher'; import WidgetUtils from './utils/WidgetUtils'; import WidgetEchoStore from './stores/WidgetEchoStore'; @@ -86,6 +85,9 @@ import { Action } from './dispatcher/actions'; import VoipUserMapper from './VoipUserMapper'; import { addManagedHybridWidget, isManagedHybridWidgetEnabled } from './widgets/ManagedHybrid'; import { randomUppercaseString, randomLowercaseString } from "matrix-js-sdk/src/randomstring"; +import EventEmitter from 'events'; +import SdkConfig from './SdkConfig'; +import { ensureDMExists, findDMForUser } from './createRoom'; export const PROTOCOL_PSTN = 'm.protocol.pstn'; export const PROTOCOL_PSTN_PREFIXED = 'im.vector.protocol.pstn'; @@ -137,22 +139,12 @@ export enum PlaceCallType { ScreenSharing = 'screensharing', } -function getRemoteAudioElement(): HTMLAudioElement { - // this needs to be somewhere at the top of the DOM which - // always exists to avoid audio interruptions. - // Might as well just use DOM. - const remoteAudioElement = document.getElementById("remoteAudio") as HTMLAudioElement; - if (!remoteAudioElement) { - console.error( - "Failed to find remoteAudio element - cannot play audio!" + - "You need to add an
), button: _t("Trust"), - }); + }); const [confirmed] = await finished; if (confirmed) { // eslint-disable-next-line react-hooks/rules-of-hooks diff --git a/src/KeyBindingsManager.ts b/src/KeyBindingsManager.ts index d862f10c02..aac14bde20 100644 --- a/src/KeyBindingsManager.ts +++ b/src/KeyBindingsManager.ts @@ -231,8 +231,10 @@ export class KeyBindingsManager { /** * Finds a matching KeyAction for a given KeyboardEvent */ - private getAction(getters: KeyBindingGetter[], ev: KeyboardEvent | React.KeyboardEvent) - : T | undefined { + private getAction( + getters: KeyBindingGetter[], + ev: KeyboardEvent | React.KeyboardEvent, + ): T | undefined { for (const getter of getters) { const bindings = getter(); const binding = bindings.find(it => isKeyComboMatch(ev, it.keyCombo, isMac)); diff --git a/src/Login.ts b/src/Login.ts index db3c4c11e4..d584df7dfe 100644 --- a/src/Login.ts +++ b/src/Login.ts @@ -1,9 +1,6 @@ /* -Copyright 2015, 2016 OpenMarket Ltd -Copyright 2017 Vector Creations Ltd -Copyright 2018 New Vector Ltd +Copyright 2015-2021 The Matrix.org Foundation C.I.C. Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> -Copyright 2020 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. @@ -59,7 +56,7 @@ export type LoginFlow = ISSOFlow | IPasswordFlow; // TODO: Move this to JS SDK /* eslint-disable camelcase */ interface ILoginParams { - identifier?: string; + identifier?: object; password?: string; token?: string; device_id?: string; diff --git a/src/PasswordReset.js b/src/PasswordReset.js index 6fe6ca82cc..88ae00d088 100644 --- a/src/PasswordReset.js +++ b/src/PasswordReset.js @@ -54,7 +54,7 @@ export default class PasswordReset { return res; }, function(err) { if (err.errcode === 'M_THREEPID_NOT_FOUND') { - err.message = _t('This email address was not found'); + err.message = _t('This email address was not found'); } else if (err.httpStatus) { err.message = err.message + ` (Status ${err.httpStatus})`; } diff --git a/src/Resend.js b/src/Resend.js index bf69e59c1a..f1e5fb38f5 100644 --- a/src/Resend.js +++ b/src/Resend.js @@ -21,11 +21,11 @@ import { EventStatus } from 'matrix-js-sdk/src/models/event'; export default class Resend { static resendUnsentEvents(room) { - room.getPendingEvents().filter(function(ev) { + return Promise.all(room.getPendingEvents().filter(function(ev) { return ev.status === EventStatus.NOT_SENT; - }).forEach(function(event) { - Resend.resend(event); - }); + }).map(function(event) { + return Resend.resend(event); + })); } static cancelUnsentEvents(room) { @@ -38,7 +38,7 @@ export default class Resend { static resend(event) { const room = MatrixClientPeg.get().getRoom(event.getRoomId()); - MatrixClientPeg.get().resendEvent(event, room).then(function(res) { + return MatrixClientPeg.get().resendEvent(event, room).then(function(res) { dis.dispatch({ action: 'message_sent', event: event, diff --git a/src/ScalarAuthClient.js b/src/ScalarAuthClient.ts similarity index 84% rename from src/ScalarAuthClient.js rename to src/ScalarAuthClient.ts index 200b4fd7b9..a09c3494a8 100644 --- a/src/ScalarAuthClient.js +++ b/src/ScalarAuthClient.ts @@ -1,6 +1,5 @@ /* -Copyright 2016 OpenMarket Ltd -Copyright 2019 The Matrix.org Foundation C.I.C. +Copyright 2016, 2019, 2021 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -17,13 +16,14 @@ limitations under the License. import url from 'url'; import SettingsStore from "./settings/SettingsStore"; -import { Service, startTermsFlow, TermsNotSignedError } from './Terms'; +import { Service, startTermsFlow, TermsInteractionCallback, TermsNotSignedError } from './Terms'; import {MatrixClientPeg} from "./MatrixClientPeg"; import request from "browser-request"; import SdkConfig from "./SdkConfig"; import {WidgetType} from "./widgets/WidgetType"; import {SERVICE_TYPES} from "matrix-js-sdk/src/service-types"; +import { Room } from "matrix-js-sdk/src/models/room"; // The version of the integration manager API we're intending to work with const imApiVersion = "1.1"; @@ -31,9 +31,11 @@ const imApiVersion = "1.1"; // TODO: Generify the name of this class and all components within - it's not just for Scalar. export default class ScalarAuthClient { - constructor(apiUrl, uiUrl) { - this.apiUrl = apiUrl; - this.uiUrl = uiUrl; + private scalarToken: string; + private termsInteractionCallback: TermsInteractionCallback; + private isDefaultManager: boolean; + + constructor(private apiUrl: string, private uiUrl: string) { this.scalarToken = null; // `undefined` to allow `startTermsFlow` to fallback to a default // callback if this is unset. @@ -46,7 +48,7 @@ export default class ScalarAuthClient { this.isDefaultManager = apiUrl === configApiUrl && configUiUrl === uiUrl; } - _writeTokenToStore() { + private writeTokenToStore() { window.localStorage.setItem("mx_scalar_token_at_" + this.apiUrl, this.scalarToken); if (this.isDefaultManager) { // We remove the old token from storage to migrate upwards. This is safe @@ -56,7 +58,7 @@ export default class ScalarAuthClient { } } - _readTokenFromStore() { + private readTokenFromStore(): string { let token = window.localStorage.getItem("mx_scalar_token_at_" + this.apiUrl); if (!token && this.isDefaultManager) { token = window.localStorage.getItem("mx_scalar_token"); @@ -64,33 +66,33 @@ export default class ScalarAuthClient { return token; } - _readToken() { + private readToken(): string { if (this.scalarToken) return this.scalarToken; - return this._readTokenFromStore(); + return this.readTokenFromStore(); } setTermsInteractionCallback(callback) { this.termsInteractionCallback = callback; } - connect() { + connect(): Promise { return this.getScalarToken().then((tok) => { this.scalarToken = tok; }); } - hasCredentials() { + hasCredentials(): boolean { return this.scalarToken != null; // undef or null } // Returns a promise that resolves to a scalar_token string - getScalarToken() { - const token = this._readToken(); + getScalarToken(): Promise { + const token = this.readToken(); if (!token) { return this.registerForToken(); } else { - return this._checkToken(token).catch((e) => { + return this.checkToken(token).catch((e) => { if (e instanceof TermsNotSignedError) { // retrying won't help this throw e; @@ -100,7 +102,7 @@ export default class ScalarAuthClient { } } - _getAccountName(token) { + private getAccountName(token: string): Promise { const url = this.apiUrl + "/account"; return new Promise(function(resolve, reject) { @@ -125,8 +127,8 @@ export default class ScalarAuthClient { }); } - _checkToken(token) { - return this._getAccountName(token).then(userId => { + private checkToken(token: string): Promise { + return this.getAccountName(token).then(userId => { const me = MatrixClientPeg.get().getUserId(); if (userId !== me) { throw new Error("Scalar token is owned by someone else: " + me); @@ -154,7 +156,7 @@ export default class ScalarAuthClient { parsedImRestUrl.pathname = ''; return startTermsFlow([new Service( SERVICE_TYPES.IM, - parsedImRestUrl.format(), + url.format(parsedImRestUrl), token, )], this.termsInteractionCallback).then(() => { return token; @@ -165,22 +167,22 @@ export default class ScalarAuthClient { }); } - registerForToken() { + registerForToken(): Promise { // Get openid bearer token from the HS as the first part of our dance return MatrixClientPeg.get().getOpenIdToken().then((tokenObject) => { // Now we can send that to scalar and exchange it for a scalar token return this.exchangeForScalarToken(tokenObject); }).then((token) => { // Validate it (this mostly checks to see if the IM needs us to agree to some terms) - return this._checkToken(token); + return this.checkToken(token); }).then((token) => { this.scalarToken = token; - this._writeTokenToStore(); + this.writeTokenToStore(); return token; }); } - exchangeForScalarToken(openidTokenObject) { + exchangeForScalarToken(openidTokenObject: any): Promise { const scalarRestUrl = this.apiUrl; return new Promise(function(resolve, reject) { @@ -194,7 +196,7 @@ export default class ScalarAuthClient { if (err) { reject(err); } else if (response.statusCode / 100 !== 2) { - reject({statusCode: response.statusCode}); + reject(new Error(`Scalar request failed: ${response.statusCode}`)); } else if (!body || !body.scalar_token) { reject(new Error("Missing scalar_token in response")); } else { @@ -204,7 +206,7 @@ export default class ScalarAuthClient { }); } - getScalarPageTitle(url) { + getScalarPageTitle(url: string): Promise { let scalarPageLookupUrl = this.apiUrl + '/widgets/title_lookup'; scalarPageLookupUrl = this.getStarterLink(scalarPageLookupUrl); scalarPageLookupUrl += '&curl=' + encodeURIComponent(url); @@ -218,7 +220,7 @@ export default class ScalarAuthClient { if (err) { reject(err); } else if (response.statusCode / 100 !== 2) { - reject({statusCode: response.statusCode}); + reject(new Error(`Scalar request failed: ${response.statusCode}`)); } else if (!body) { reject(new Error("Missing page title in response")); } else { @@ -240,10 +242,10 @@ export default class ScalarAuthClient { * @param {string} widgetId The widget ID to disable assets for * @return {Promise} Resolves on completion */ - disableWidgetAssets(widgetType: WidgetType, widgetId) { + disableWidgetAssets(widgetType: WidgetType, widgetId: string): Promise { let url = this.apiUrl + '/widgets/set_assets_state'; url = this.getStarterLink(url); - return new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { request({ method: 'GET', // XXX: Actions shouldn't be GET requests uri: url, @@ -257,7 +259,7 @@ export default class ScalarAuthClient { if (err) { reject(err); } else if (response.statusCode / 100 !== 2) { - reject({statusCode: response.statusCode}); + reject(new Error(`Scalar request failed: ${response.statusCode}`)); } else if (!body) { reject(new Error("Failed to set widget assets state")); } else { @@ -267,7 +269,7 @@ export default class ScalarAuthClient { }); } - getScalarInterfaceUrlForRoom(room, screen, id) { + getScalarInterfaceUrlForRoom(room: Room, screen: string, id: string): string { const roomId = room.roomId; const roomName = room.name; let url = this.uiUrl; @@ -284,7 +286,7 @@ export default class ScalarAuthClient { return url; } - getStarterLink(starterLinkUrl) { + getStarterLink(starterLinkUrl: string): string { return starterLinkUrl + "?scalar_token=" + encodeURIComponent(this.scalarToken); } } diff --git a/src/SlashCommands.tsx b/src/SlashCommands.tsx index 6ce1439164..4a7b37b5e5 100644 --- a/src/SlashCommands.tsx +++ b/src/SlashCommands.tsx @@ -38,7 +38,7 @@ import {isPermalinkHost, parsePermalink} from "./utils/permalinks/Permalinks"; import {inviteUsersToRoom} from "./RoomInvite"; import { WidgetType } from "./widgets/WidgetType"; import { Jitsi } from "./widgets/Jitsi"; -import { parseFragment as parseHtml } from "parse5"; +import { parseFragment as parseHtml, Element as ChildElement } from "parse5"; import BugReportDialog from "./components/views/dialogs/BugReportDialog"; import { ensureDMExists } from "./createRoom"; import { ViewUserPayload } from "./dispatcher/payloads/ViewUserPayload"; @@ -856,7 +856,7 @@ export const Commands = [ // some superfast regex over the text so we don't have to. const embed = parseHtml(widgetUrl); if (embed && embed.childNodes && embed.childNodes.length === 1) { - const iframe = embed.childNodes[0]; + const iframe = embed.childNodes[0] as ChildElement; if (iframe.tagName.toLowerCase() === 'iframe' && iframe.attrs) { const srcAttr = iframe.attrs.find(a => a.name === 'src'); console.log("Pulling URL out of iframe (embed code)"); diff --git a/src/Terms.js b/src/Terms.ts similarity index 87% rename from src/Terms.js rename to src/Terms.ts index 6ae89f9a2c..1bdff36cbc 100644 --- a/src/Terms.js +++ b/src/Terms.ts @@ -1,5 +1,5 @@ /* -Copyright 2019 The Matrix.org Foundation C.I.C. +Copyright 2019, 2021 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -17,7 +17,7 @@ limitations under the License. import classNames from 'classnames'; import {MatrixClientPeg} from './MatrixClientPeg'; -import * as sdk from './'; +import * as sdk from '.'; import Modal from './Modal'; export class TermsNotSignedError extends Error {} @@ -32,13 +32,30 @@ export class Service { * @param {string} baseUrl The Base URL of the service (ie. before '/_matrix') * @param {string} accessToken The user's access token for the service */ - constructor(serviceType, baseUrl, accessToken) { - this.serviceType = serviceType; - this.baseUrl = baseUrl; - this.accessToken = accessToken; + constructor(public serviceType: string, public baseUrl: string, public accessToken: string) { } } +interface Policy { + // @ts-ignore: No great way to express indexed types together with other keys + version: string; + [lang: string]: { + url: string; + }; +} +type Policies = { + [policy: string]: Policy, +}; + +export type TermsInteractionCallback = ( + policiesAndServicePairs: { + service: Service, + policies: Policies, + }[], + agreedUrls: string[], + extraClassNames?: string, +) => Promise; + /** * Start a flow where the user is presented with terms & conditions for some services * @@ -51,8 +68,8 @@ export class Service { * if they cancel. */ export async function startTermsFlow( - services, - interactionCallback = dialogTermsInteractionCallback, + services: Service[], + interactionCallback: TermsInteractionCallback = dialogTermsInteractionCallback, ) { const termsPromises = services.map( (s) => MatrixClientPeg.get().getTerms(s.serviceType, s.baseUrl), @@ -77,7 +94,7 @@ export async function startTermsFlow( * } */ - const terms = await Promise.all(termsPromises); + const terms: { policies: Policies }[] = await Promise.all(termsPromises); const policiesAndServicePairs = terms.map((t, i) => { return { 'service': services[i], 'policies': t.policies }; }); // fetch the set of agreed policy URLs from account data @@ -158,10 +175,13 @@ export async function startTermsFlow( } export function dialogTermsInteractionCallback( - policiesAndServicePairs, - agreedUrls, - extraClassNames, -) { + policiesAndServicePairs: { + service: Service, + policies: { [policy: string]: Policy }, + }[], + agreedUrls: string[], + extraClassNames?: string, +): Promise { return new Promise((resolve, reject) => { console.log("Terms that need agreement", policiesAndServicePairs); const TermsDialog = sdk.getComponent("views.dialogs.TermsDialog"); diff --git a/src/TextForEvent.js b/src/TextForEvent.js index a6787c647d..86f9ff20f4 100644 --- a/src/TextForEvent.js +++ b/src/TextForEvent.js @@ -547,17 +547,23 @@ function textForMjolnirEvent(event) { // else the entity !== prevEntity - count as a removal & add if (USER_RULE_TYPES.includes(event.getType())) { - return _t("%(senderName)s changed a rule that was banning users matching %(oldGlob)s to matching " + + return _t( + "%(senderName)s changed a rule that was banning users matching %(oldGlob)s to matching " + "%(newGlob)s for %(reason)s", - {senderName, oldGlob: prevEntity, newGlob: entity, reason}); + {senderName, oldGlob: prevEntity, newGlob: entity, reason}, + ); } else if (ROOM_RULE_TYPES.includes(event.getType())) { - return _t("%(senderName)s changed a rule that was banning rooms matching %(oldGlob)s to matching " + + return _t( + "%(senderName)s changed a rule that was banning rooms matching %(oldGlob)s to matching " + "%(newGlob)s for %(reason)s", - {senderName, oldGlob: prevEntity, newGlob: entity, reason}); + {senderName, oldGlob: prevEntity, newGlob: entity, reason}, + ); } else if (SERVER_RULE_TYPES.includes(event.getType())) { - return _t("%(senderName)s changed a rule that was banning servers matching %(oldGlob)s to matching " + + return _t( + "%(senderName)s changed a rule that was banning servers matching %(oldGlob)s to matching " + "%(newGlob)s for %(reason)s", - {senderName, oldGlob: prevEntity, newGlob: entity, reason}); + {senderName, oldGlob: prevEntity, newGlob: entity, reason}, + ); } // Unknown type. We'll say something but we shouldn't end up here. diff --git a/src/Unread.js b/src/Unread.js index ddf225ac64..12c15eb6af 100644 --- a/src/Unread.js +++ b/src/Unread.js @@ -45,7 +45,7 @@ export function eventTriggersUnreadCount(ev) { } export function doesRoomHaveUnreadMessages(room) { - const myUserId = MatrixClientPeg.get().credentials.userId; + const myUserId = MatrixClientPeg.get().getUserId(); // get the most recent read receipt sent by our account. // N.B. this is NOT a read marker (RM, aka "read up to marker"), diff --git a/src/VoipUserMapper.ts b/src/VoipUserMapper.ts index 4f5613b4a8..e5bed2e812 100644 --- a/src/VoipUserMapper.ts +++ b/src/VoipUserMapper.ts @@ -57,7 +57,11 @@ export default class VoipUserMapper { if (!virtualRoom) return null; const virtualRoomEvent = virtualRoom.getAccountData(VIRTUAL_ROOM_EVENT_TYPE); if (!virtualRoomEvent || !virtualRoomEvent.getContent()) return null; - return virtualRoomEvent.getContent()['native_room'] || null; + const nativeRoomID = virtualRoomEvent.getContent()['native_room']; + const nativeRoom = MatrixClientPeg.get().getRoom(nativeRoomID); + if (!nativeRoom || nativeRoom.getMyMembership() !== 'join') return null; + + return nativeRoomID; } public isVirtualRoom(room: Room): boolean { diff --git a/src/async-components/views/dialogs/eventindex/ManageEventIndexDialog.js b/src/async-components/views/dialogs/eventindex/ManageEventIndexDialog.tsx similarity index 90% rename from src/async-components/views/dialogs/eventindex/ManageEventIndexDialog.js rename to src/async-components/views/dialogs/eventindex/ManageEventIndexDialog.tsx index be3368b87b..0710c513da 100644 --- a/src/async-components/views/dialogs/eventindex/ManageEventIndexDialog.js +++ b/src/async-components/views/dialogs/eventindex/ManageEventIndexDialog.tsx @@ -1,5 +1,5 @@ /* -Copyright 2020 The Matrix.org Foundation C.I.C. +Copyright 2020-2021 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -16,7 +16,6 @@ limitations under the License. import React from 'react'; import * as sdk from '../../../../index'; -import PropTypes from 'prop-types'; import { _t } from '../../../../languageHandler'; import SdkConfig from '../../../../SdkConfig'; import SettingsStore from "../../../../settings/SettingsStore"; @@ -26,14 +25,23 @@ import {formatBytes, formatCountLong} from "../../../../utils/FormattingUtils"; import EventIndexPeg from "../../../../indexing/EventIndexPeg"; import {SettingLevel} from "../../../../settings/SettingLevel"; +interface IProps { + onFinished: (confirmed: boolean) => void; +} + +interface IState { + eventIndexSize: number; + eventCount: number; + crawlingRoomsCount: number; + roomCount: number; + currentRoom: string; + crawlerSleepTime: number; +} + /* * Allows the user to introspect the event index state and disable it. */ -export default class ManageEventIndexDialog extends React.Component { - static propTypes = { - onFinished: PropTypes.func.isRequired, - }; - +export default class ManageEventIndexDialog extends React.Component { constructor(props) { super(props); @@ -84,7 +92,7 @@ export default class ManageEventIndexDialog extends React.Component { } } - async componentDidMount(): void { + async componentDidMount(): Promise { let eventIndexSize = 0; let crawlingRoomsCount = 0; let roomCount = 0; @@ -123,14 +131,14 @@ export default class ManageEventIndexDialog extends React.Component { }); } - _onDisable = async () => { + private onDisable = async () => { Modal.createTrackedDialogAsync("Disable message search", "Disable message search", import("./DisableEventIndexDialog"), null, null, /* priority = */ false, /* static = */ true, ); }; - _onCrawlerSleepTimeChange = (e) => { + private onCrawlerSleepTimeChange = (e) => { this.setState({crawlerSleepTime: e.target.value}); SettingsStore.setValue("crawlerSleepTime", null, SettingLevel.DEVICE, e.target.value); }; @@ -144,7 +152,7 @@ export default class ManageEventIndexDialog extends React.Component { crawlerState = _t("Not currently indexing messages for any room."); } else { crawlerState = ( - _t("Currently indexing: %(currentRoom)s", { currentRoom: this.state.currentRoom }) + _t("Currently indexing: %(currentRoom)s", { currentRoom: this.state.currentRoom }) ); } @@ -169,7 +177,7 @@ export default class ManageEventIndexDialog extends React.Component { label={_t('Message downloading sleep time(ms)')} type='number' value={this.state.crawlerSleepTime} - onChange={this._onCrawlerSleepTimeChange} /> + onChange={this.onCrawlerSleepTimeChange} />
); @@ -188,7 +196,7 @@ export default class ManageEventIndexDialog extends React.Component { onPrimaryButtonClick={this.props.onFinished} primaryButtonClass="primary" cancelButton={_t("Disable")} - onCancel={this._onDisable} + onCancel={this.onDisable} cancelButtonClass="danger" /> diff --git a/src/async-components/views/dialogs/security/CreateKeyBackupDialog.js b/src/async-components/views/dialogs/security/CreateKeyBackupDialog.js index 863ee2b427..549494b5cb 100644 --- a/src/async-components/views/dialogs/security/CreateKeyBackupDialog.js +++ b/src/async-components/views/dialogs/security/CreateKeyBackupDialog.js @@ -310,7 +310,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent { const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); return

{_t( - "Please enter your Security Phrase a second time to confirm.", + "Enter your Security Phrase a second time to confirm it.", )}

@@ -498,9 +498,9 @@ export default class CreateKeyBackupDialog extends React.PureComponent { title={this._titleForPhase(this.state.phase)} hasCancel={[PHASE_PASSPHRASE, PHASE_DONE].includes(this.state.phase)} > -
- {content} -
+
+ {content} +
); } diff --git a/src/async-components/views/dialogs/security/CreateSecretStorageDialog.js b/src/async-components/views/dialogs/security/CreateSecretStorageDialog.js index 84cb58536a..6d5703a768 100644 --- a/src/async-components/views/dialogs/security/CreateSecretStorageDialog.js +++ b/src/async-components/views/dialogs/security/CreateSecretStorageDialog.js @@ -647,7 +647,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent { } return

{_t( - "Enter your recovery passphrase a second time to confirm it.", + "Enter your Security Phrase a second time to confirm it.", )}

@@ -856,9 +856,9 @@ export default class CreateSecretStorageDialog extends React.PureComponent { hasCancel={this.props.hasCancel && [PHASE_PASSPHRASE].includes(this.state.phase)} fixedWidth={false} > -
- {content} -
+
+ {content} +
); } diff --git a/src/async-components/views/dialogs/security/ExportE2eKeysDialog.js b/src/async-components/views/dialogs/security/ExportE2eKeysDialog.js index eeb68b94bd..60f2ca9168 100644 --- a/src/async-components/views/dialogs/security/ExportE2eKeysDialog.js +++ b/src/async-components/views/dialogs/security/ExportE2eKeysDialog.js @@ -170,8 +170,11 @@ export default class ExportE2eKeysDialog extends React.Component {
-
-
- -
-
- -
+
+ +
+
+ +
-
- -
-
- -
+
+ +
+
+ +
diff --git a/src/components/structures/FilePanel.js b/src/components/structures/FilePanel.js index 32db5c251c..d5e4b092e2 100644 --- a/src/components/structures/FilePanel.js +++ b/src/components/structures/FilePanel.js @@ -200,10 +200,10 @@ class FilePanel extends React.Component { previousPhase={RightPanelPhases.RoomSummary} >
- { _t("You must register to use this functionality", - {}, - { 'a': (sub) => { sub } }) - } + { _t("You must register to use this functionality", + {}, + { 'a': (sub) => { sub } }) + }
; } else if (this.noRoom) { diff --git a/src/components/structures/GroupFilterPanel.js b/src/components/structures/GroupFilterPanel.js index 976b2d81a5..7c050e7433 100644 --- a/src/components/structures/GroupFilterPanel.js +++ b/src/components/structures/GroupFilterPanel.js @@ -153,17 +153,17 @@ class GroupFilterPanel extends React.Component { type="draggable-TagTile" > { (provided, snapshot) => ( -
- { this.renderGlobalIcon() } - { tags } -
- {createButton} -
- { provided.placeholder } +
+ { this.renderGlobalIcon() } + { tags } +
+ {createButton}
+ { provided.placeholder } +
) } diff --git a/src/components/structures/GroupView.js b/src/components/structures/GroupView.js index ed6167cbe7..3ab009d7b8 100644 --- a/src/components/structures/GroupView.js +++ b/src/components/structures/GroupView.js @@ -43,7 +43,7 @@ import {mediaFromMxc} from "../../customisations/Media"; import {replaceableComponent} from "../../utils/replaceableComponent"; const LONG_DESC_PLACEHOLDER = _td( -`

HTML for your community's page

+ `

HTML for your community's page

Use the long description to introduce new members to the community, or distribute some important links @@ -110,14 +110,16 @@ class CategoryRoomList extends React.Component { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createTrackedDialog( 'Failed to add the following room to the group summary', - '', ErrorDialog, - { - title: _t( - "Failed to add the following rooms to the summary of %(groupId)s:", - {groupId: this.props.groupId}, - ), - description: errorList.join(", "), - }); + '', + ErrorDialog, + { + title: _t( + "Failed to add the following rooms to the summary of %(groupId)s:", + {groupId: this.props.groupId}, + ), + description: errorList.join(", "), + }, + ); }); }, }, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true); @@ -146,8 +148,8 @@ class CategoryRoomList extends React.Component { let catHeader =

; if (this.props.category && this.props.category.profile) { catHeader =
- { this.props.category.profile.name } -
; + { this.props.category.profile.name } +
; } return
{ catHeader } @@ -190,13 +192,14 @@ class FeaturedRoom extends React.Component { Modal.createTrackedDialog( 'Failed to remove room from group summary', '', ErrorDialog, - { - title: _t( - "Failed to remove the room from the summary of %(groupId)s", - {groupId: this.props.groupId}, - ), - description: _t("The room '%(roomName)s' could not be removed from the summary.", {roomName}), - }); + { + title: _t( + "Failed to remove the room from the summary of %(groupId)s", + {groupId: this.props.groupId}, + ), + description: _t("The room '%(roomName)s' could not be removed from the summary.", {roomName}), + }, + ); }); }; @@ -283,13 +286,14 @@ class RoleUserList extends React.Component { Modal.createTrackedDialog( 'Failed to add the following users to the community summary', '', ErrorDialog, - { - title: _t( - "Failed to add the following users to the summary of %(groupId)s:", - {groupId: this.props.groupId}, - ), - description: errorList.join(", "), - }); + { + title: _t( + "Failed to add the following users to the summary of %(groupId)s:", + {groupId: this.props.groupId}, + ), + description: errorList.join(", "), + }, + ); }); }, }, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true); @@ -299,11 +303,11 @@ class RoleUserList extends React.Component { const TintableSvg = sdk.getComponent("elements.TintableSvg"); const addButton = this.props.editing ? ( - -
- { _t('Add a User') } -
-
) :
; + +
+ { _t('Add a User') } +
+ ) :
; const userNodes = this.props.users.map((u) => { return - { _t("Leave %(groupName)s?", {groupName: this.props.groupId}) } - { warnings } + { _t("Leave %(groupName)s?", {groupName: this.props.groupId}) } + { warnings } ), button: _t("Leave"), @@ -1055,10 +1061,11 @@ export default class GroupView extends React.Component { return null; } - const membershipButtonClasses = classnames([ - 'mx_RoomHeader_textButton', - 'mx_GroupView_textButton', - ], + const membershipButtonClasses = classnames( + [ + 'mx_RoomHeader_textButton', + 'mx_GroupView_textButton', + ], membershipButtonExtraClasses, ); diff --git a/src/components/structures/LeftPanel.tsx b/src/components/structures/LeftPanel.tsx index e4762e35ad..7f9ef7516e 100644 --- a/src/components/structures/LeftPanel.tsx +++ b/src/components/structures/LeftPanel.tsx @@ -347,7 +347,7 @@ export default class LeftPanel extends React.Component { if (element) { classes = element.classList; } - } while (element && !cssClasses.some(c => classes.contains(c))); + } while (element && (!cssClasses.some(c => classes.contains(c)) || element.offsetParent === null)); if (element) { element.focus(); @@ -416,7 +416,7 @@ export default class LeftPanel extends React.Component { const roomList = ; } /** @@ -160,6 +164,7 @@ class LoggedInView extends React.Component { // use compact timeline view useCompactLayout: SettingsStore.getValue('useCompactLayout'), usageLimitDismissed: false, + activeCalls: [], }; // stash the MatrixClient in case we log out before we are unmounted @@ -175,6 +180,7 @@ class LoggedInView extends React.Component { componentDidMount() { document.addEventListener('keydown', this._onNativeKeyDown, false); + CallHandler.sharedInstance().addListener(CallHandlerEvent.CallsChanged, this.onCallsChanged); this._updateServerNoticeEvents(); @@ -199,6 +205,7 @@ class LoggedInView extends React.Component { componentWillUnmount() { document.removeEventListener('keydown', this._onNativeKeyDown, false); + CallHandler.sharedInstance().removeListener(CallHandlerEvent.CallsChanged, this.onCallsChanged); this._matrixClient.removeListener("accountData", this.onAccountData); this._matrixClient.removeListener("sync", this.onSync); this._matrixClient.removeListener("RoomState.events", this.onRoomStateEvents); @@ -206,6 +213,12 @@ class LoggedInView extends React.Component { this.resizer.detach(); } + private onCallsChanged = () => { + this.setState({ + activeCalls: CallHandler.sharedInstance().getAllActiveCalls(), + }); + }; + // 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. // @@ -661,6 +674,12 @@ class LoggedInView extends React.Component { bodyClasses += ' mx_MatrixChat_useCompactLayout'; } + const audioFeedArraysForCalls = this.state.activeCalls.map((call) => { + return ( + + ); + }); + return (
{ + {audioFeedArraysForCalls} ); } diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index 078b296295..e330dc7d38 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -1094,7 +1094,7 @@ export default class MatrixChat extends React.PureComponent { private leaveRoomWarnings(roomId: string) { const roomToLeave = MatrixClientPeg.get().getRoom(roomId); - const isSpace = roomToLeave?.isSpaceRoom(); + const isSpace = SettingsStore.getValue("feature_spaces") && roomToLeave?.isSpaceRoom(); // Show a warning if there are additional complications. const warnings = []; @@ -1133,7 +1133,7 @@ export default class MatrixChat extends React.PureComponent { const roomToLeave = MatrixClientPeg.get().getRoom(roomId); const warnings = this.leaveRoomWarnings(roomId); - const isSpace = roomToLeave?.isSpaceRoom(); + const isSpace = SettingsStore.getValue("feature_spaces") && roomToLeave?.isSpaceRoom(); Modal.createTrackedDialog(isSpace ? "Leave space" : "Leave room", '', QuestionDialog, { title: isSpace ? _t("Leave space") : _t("Leave room"), description: ( diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index 132d9ab4c3..c93f07fa0f 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -427,8 +427,10 @@ export default class MessagePanel extends React.Component { // we get a new DOM node (restarting the animation) when the ghost // moves to a different event. return ( -
  • +
  • { hr }
  • ); @@ -1014,13 +1016,13 @@ class CreationGrouper { ret.push( - { eventTiles } + { eventTiles } , ); @@ -1222,11 +1224,11 @@ class MemberGrouper { ret.push( - { eventTiles } + { eventTiles } , ); diff --git a/src/components/structures/RightPanel.js b/src/components/structures/RightPanel.js index 5bcb3b2450..d8c763eabd 100644 --- a/src/components/structures/RightPanel.js +++ b/src/components/structures/RightPanel.js @@ -35,6 +35,7 @@ import {Action} from "../../dispatcher/actions"; import RoomSummaryCard from "../views/right_panel/RoomSummaryCard"; import WidgetCard from "../views/right_panel/WidgetCard"; import {replaceableComponent} from "../../utils/replaceableComponent"; +import SettingsStore from "../../settings/SettingsStore"; @replaceableComponent("structures.RightPanel") export default class RightPanel extends React.Component { @@ -85,7 +86,9 @@ export default class RightPanel extends React.Component { return RightPanelPhases.GroupMemberList; } return rps.groupPanelPhase; - } else if (this.props.room?.isSpaceRoom() && !RIGHT_PANEL_SPACE_PHASES.includes(rps.roomPanelPhase)) { + } else if (SettingsStore.getValue("feature_spaces") && this.props.room?.isSpaceRoom() + && !RIGHT_PANEL_SPACE_PHASES.includes(rps.roomPanelPhase) + ) { return RightPanelPhases.SpaceMemberList; } else if (userForPanel) { // XXX FIXME AAAAAARGH: What is going on with this class!? It takes some of its state diff --git a/src/components/structures/RoomSearch.tsx b/src/components/structures/RoomSearch.tsx index a64feed42c..34682877e0 100644 --- a/src/components/structures/RoomSearch.tsx +++ b/src/components/structures/RoomSearch.tsx @@ -17,6 +17,8 @@ limitations under the License. import * as React from "react"; import { createRef } from "react"; import classNames from "classnames"; +import { Room } from "matrix-js-sdk/src/models/room"; + import defaultDispatcher from "../../dispatcher/dispatcher"; import { _t } from "../../languageHandler"; import { ActionPayload } from "../../dispatcher/payloads"; @@ -26,7 +28,7 @@ import RoomListStore from "../../stores/room-list/RoomListStore"; import { NameFilterCondition } from "../../stores/room-list/filters/NameFilterCondition"; import { getKeyBindingsManager, RoomListAction } from "../../KeyBindingsManager"; import {replaceableComponent} from "../../utils/replaceableComponent"; -import SpaceStore, {UPDATE_SELECTED_SPACE} from "../../stores/SpaceStore"; +import SpaceStore, {UPDATE_SELECTED_SPACE, UPDATE_TOP_LEVEL_SPACES} from "../../stores/SpaceStore"; interface IProps { isMinimized: boolean; @@ -40,6 +42,7 @@ interface IProps { interface IState { query: string; focused: boolean; + inSpaces: boolean; } @replaceableComponent("structures.RoomSearch") @@ -54,11 +57,13 @@ export default class RoomSearch extends React.PureComponent { this.state = { query: "", focused: false, + inSpaces: false, }; this.dispatcherRef = defaultDispatcher.register(this.onAction); // clear filter when changing spaces, in future we may wish to maintain a filter per-space SpaceStore.instance.on(UPDATE_SELECTED_SPACE, this.clearInput); + SpaceStore.instance.on(UPDATE_TOP_LEVEL_SPACES, this.onSpaces); } public componentDidUpdate(prevProps: Readonly, prevState: Readonly): void { @@ -79,8 +84,15 @@ export default class RoomSearch extends React.PureComponent { public componentWillUnmount() { defaultDispatcher.unregister(this.dispatcherRef); SpaceStore.instance.off(UPDATE_SELECTED_SPACE, this.clearInput); + SpaceStore.instance.off(UPDATE_TOP_LEVEL_SPACES, this.onSpaces); } + private onSpaces = (spaces: Room[]) => { + this.setState({ + inSpaces: spaces.length > 0, + }); + }; + private onAction = (payload: ActionPayload) => { if (payload.action === 'view_room' && payload.clear_search) { this.clearInput(); @@ -152,6 +164,11 @@ export default class RoomSearch extends React.PureComponent { 'mx_RoomSearch_inputExpanded': this.state.query || this.state.focused, }); + let placeholder = _t("Filter"); + if (this.state.inSpaces) { + placeholder = _t("Filter all spaces"); + } + let icon = (
    ); @@ -165,7 +182,7 @@ export default class RoomSearch extends React.PureComponent { onBlur={this.onBlur} onChange={this.onChange} onKeyDown={this.onKeyDown} - placeholder={_t("Filter")} + placeholder={placeholder} autoComplete="off" /> ); diff --git a/src/components/structures/RoomStatusBar.js b/src/components/structures/RoomStatusBar.js index 54b6fee233..b2f0c70bd7 100644 --- a/src/components/structures/RoomStatusBar.js +++ b/src/components/structures/RoomStatusBar.js @@ -1,5 +1,5 @@ /* -Copyright 2015-2020 The Matrix.org Foundation C.I.C. +Copyright 2015-2021 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -20,16 +20,20 @@ import { _t, _td } from '../../languageHandler'; import {MatrixClientPeg} from '../../MatrixClientPeg'; import Resend from '../../Resend'; import dis from '../../dispatcher/dispatcher'; -import {messageForResourceLimitError, messageForSendError} from '../../utils/ErrorUtils'; +import {messageForResourceLimitError} from '../../utils/ErrorUtils'; import {Action} from "../../dispatcher/actions"; import {replaceableComponent} from "../../utils/replaceableComponent"; import {EventStatus} from "matrix-js-sdk/src/models/event"; +import NotificationBadge from "../views/rooms/NotificationBadge"; +import {StaticNotificationState} from "../../stores/notifications/StaticNotificationState"; +import AccessibleButton from "../views/elements/AccessibleButton"; +import InlineSpinner from "../views/elements/InlineSpinner"; const STATUS_BAR_HIDDEN = 0; const STATUS_BAR_EXPANDED = 1; const STATUS_BAR_EXPANDED_LARGE = 2; -function getUnsentMessages(room) { +export function getUnsentMessages(room) { if (!room) { return []; } return room.getPendingEvents().filter(function(ev) { return ev.status === EventStatus.NOT_SENT; @@ -76,6 +80,7 @@ export default class RoomStatusBar extends React.Component { syncState: MatrixClientPeg.get().getSyncState(), syncStateData: MatrixClientPeg.get().getSyncStateData(), unsentMessages: getUnsentMessages(this.props.room), + isResending: false, }; componentDidMount() { @@ -109,7 +114,10 @@ export default class RoomStatusBar extends React.Component { }; _onResendAllClick = () => { - Resend.resendUnsentEvents(this.props.room); + Resend.resendUnsentEvents(this.props.room).then(() => { + this.setState({isResending: false}); + }); + this.setState({isResending: true}); dis.fire(Action.FocusComposer); }; @@ -120,9 +128,10 @@ export default class RoomStatusBar extends React.Component { _onRoomLocalEchoUpdated = (event, room, oldEventId, oldStatus) => { if (room.roomId !== this.props.room.roomId) return; - + const messages = getUnsentMessages(this.props.room); this.setState({ - unsentMessages: getUnsentMessages(this.props.room), + unsentMessages: messages, + isResending: messages.length > 0 && this.state.isResending, }); }; @@ -141,7 +150,7 @@ export default class RoomStatusBar extends React.Component { _getSize() { if (this._shouldShowConnectionError()) { return STATUS_BAR_EXPANDED; - } else if (this.state.unsentMessages.length > 0) { + } else if (this.state.unsentMessages.length > 0 || this.state.isResending) { return STATUS_BAR_EXPANDED_LARGE; } return STATUS_BAR_HIDDEN; @@ -162,7 +171,6 @@ export default class RoomStatusBar extends React.Component { _getUnsentMessageContent() { const unsentMessages = this.state.unsentMessages; - if (!unsentMessages.length) return null; let title; @@ -192,89 +200,92 @@ export default class RoomStatusBar extends React.Component { } else if (resourceLimitError) { title = messageForResourceLimitError( resourceLimitError.data.limit_type, - resourceLimitError.data.admin_contact, { - 'monthly_active_user': _td( - "Your message wasn't sent because this homeserver has hit its Monthly Active User Limit. " + - "Please contact your service administrator to continue using the service.", - ), - 'hs_disabled': _td( - "Your message wasn't sent because this homeserver has been blocked by it's administrator. " + - "Please contact your service administrator to continue using the service.", - ), - '': _td( - "Your message wasn't sent because this homeserver has exceeded a resource limit. " + - "Please contact your service administrator to continue using the service.", - ), - }); - } else if ( - unsentMessages.length === 1 && - unsentMessages[0].error && - unsentMessages[0].error.data && - unsentMessages[0].error.data.error - ) { - title = messageForSendError(unsentMessages[0].error.data) || unsentMessages[0].error.data.error; + resourceLimitError.data.admin_contact, + { + 'monthly_active_user': _td( + "Your message wasn't sent because this homeserver has hit its Monthly Active User Limit. " + + "Please contact your service administrator to continue using the service.", + ), + 'hs_disabled': _td( + "Your message wasn't sent because this homeserver has been blocked by it's administrator. " + + "Please contact your service administrator to continue using the service.", + ), + '': _td( + "Your message wasn't sent because this homeserver has exceeded a resource limit. " + + "Please contact your service administrator to continue using the service.", + ), + }, + ); } else { - title = _t('%(count)s of your messages have not been sent.', { count: unsentMessages.length }); + title = _t('Some of your messages have not been sent'); } - const content = _t("%(count)s Resend all or cancel all " + - "now. You can also select individual messages to resend or cancel.", - { count: unsentMessages.length }, - { - 'resendText': (sub) => - { sub }, - 'cancelText': (sub) => - { sub }, - }, - ); + let buttonRow = <> + + {_t("Delete all")} + + + {_t("Retry all")} + + ; + if (this.state.isResending) { + buttonRow = <> + + {/* span for css */} + {_t("Sending")} + ; + } - return
    - -
    -
    - { title } -
    -
    - { content } + return <> +
    +
    +
    + +
    +
    +
    + { title } +
    +
    + { _t("You can select all or individual messages to retry or delete") } +
    +
    +
    + {buttonRow} +
    -
    ; + ; } - // return suitable content for the main (text) part of the status bar. - _getContent() { + render() { if (this._shouldShowConnectionError()) { return ( -
    - /!\ -
    -
    - { _t('Connectivity to the server has been lost.') } -
    -
    - { _t('Sent messages will be stored until your connection has returned.') } +
    +
    +
    + /!\ +
    +
    + {_t('Connectivity to the server has been lost.')} +
    +
    + {_t('Sent messages will be stored until your connection has returned.')} +
    +
    ); } - if (this.state.unsentMessages.length > 0) { + if (this.state.unsentMessages.length > 0 || this.state.isResending) { return this._getUnsentMessageContent(); } return null; } - - render() { - const content = this._getContent(); - - return ( -
    -
    - { content } -
    -
    - ); - } } diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index 7168b7d139..58a87e6641 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -190,6 +190,9 @@ export interface IState { rejectError?: Error; hasPinnedWidgets?: boolean; dragCounter: number; + // whether or not a spaces context switch brought us here, + // if it did we don't want the room to be marked as read as soon as it is loaded. + wasContextSwitch?: boolean; } @replaceableComponent("structures.RoomView") @@ -326,6 +329,7 @@ export default class RoomView extends React.Component { shouldPeek: this.state.matrixClientIsReady && RoomViewStore.shouldPeek(), showingPinned: SettingsStore.getValue("PinnedEvents.isOpen", roomId), showReadReceipts: SettingsStore.getValue("showReadReceipts", roomId), + wasContextSwitch: RoomViewStore.getWasContextSwitch(), }; if (!initial && this.state.shouldPeek && !newState.shouldPeek) { @@ -1746,7 +1750,10 @@ export default class RoomView extends React.Component { } const myMembership = this.state.room.getMyMembership(); - if (myMembership === "invite" && !this.state.room.isSpaceRoom()) { // SpaceRoomView handles invites itself + if (myMembership === "invite" + // SpaceRoomView handles invites itself + && (!SettingsStore.getValue("feature_spaces") || !this.state.room.isSpaceRoom()) + ) { if (this.state.joining || this.state.rejecting) { return ( @@ -1888,7 +1895,7 @@ export default class RoomView extends React.Component { room={this.state.room} /> ); - if (!this.state.canPeek && !this.state.room?.isSpaceRoom()) { + if (!this.state.canPeek && (!SettingsStore.getValue("feature_spaces") || !this.state.room?.isSpaceRoom())) { return (
    { previewBar } @@ -2014,6 +2021,7 @@ export default class RoomView extends React.Component { timelineSet={this.state.room.getUnfilteredTimelineSet()} showReadReceipts={this.state.showReadReceipts} manageReadReceipts={!this.state.isPeeking} + sendReadReceiptOnLoad={!this.state.wasContextSwitch} manageReadMarkers={!this.state.isPeeking} hidden={hideMessagePanel} highlightedEventId={highlightedEventId} diff --git a/src/components/structures/ScrollPanel.js b/src/components/structures/ScrollPanel.js index 976734680c..5c5062633d 100644 --- a/src/components/structures/ScrollPanel.js +++ b/src/components/structures/ScrollPanel.js @@ -525,7 +525,7 @@ export default class ScrollPanel extends React.Component { */ scrollRelative = mult => { const scrollNode = this._getScrollNode(); - const delta = mult * scrollNode.clientHeight * 0.5; + const delta = mult * scrollNode.clientHeight * 0.9; scrollNode.scrollBy(0, delta); this._saveScrollState(); }; @@ -884,16 +884,20 @@ export default class ScrollPanel extends React.Component { // give the
      an explicit role=list because Safari+VoiceOver seems to think an ordered-list with // list-style-type: none; is no longer a list - return ( - { this.props.fixedChildren } -
      -
        - { this.props.children } -
      -
      -
      - ); + className={`mx_ScrollPanel ${this.props.className}`} + style={this.props.style} + > + { this.props.fixedChildren } +
      +
        + { this.props.children } +
      +
      + + ); } } diff --git a/src/components/structures/SpaceRoomDirectory.tsx b/src/components/structures/SpaceRoomDirectory.tsx index 930cfa15a9..74415cc58f 100644 --- a/src/components/structures/SpaceRoomDirectory.tsx +++ b/src/components/structures/SpaceRoomDirectory.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, {useMemo, useState} from "react"; +import React, {ReactNode, useMemo, useState} from "react"; import {Room} from "matrix-js-sdk/src/models/room"; import {MatrixClient} from "matrix-js-sdk/src/client"; import {EventType, RoomType} from "matrix-js-sdk/src/@types/event"; @@ -24,7 +24,7 @@ import {sortBy} from "lodash"; import {MatrixClientPeg} from "../../MatrixClientPeg"; import dis from "../../dispatcher/dispatcher"; import {_t} from "../../languageHandler"; -import AccessibleButton from "../views/elements/AccessibleButton"; +import AccessibleButton, {ButtonEvent} from "../views/elements/AccessibleButton"; import BaseDialog from "../views/dialogs/BaseDialog"; import Spinner from "../views/elements/Spinner"; import SearchBox from "./SearchBox"; @@ -39,11 +39,14 @@ import {mediaFromMxc} from "../../customisations/Media"; import InfoTooltip from "../views/elements/InfoTooltip"; import TextWithTooltip from "../views/elements/TextWithTooltip"; import {useStateToggle} from "../../hooks/useStateToggle"; +import {getOrder} from "../../stores/SpaceStore"; +import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton"; interface IHierarchyProps { space: Room; initialText?: string; refreshToken?: any; + additionalButtons?: ReactNode; showRoom(room: ISpaceSummaryRoom, viaServers?: string[], autoJoin?: boolean): void; } @@ -106,8 +109,16 @@ const Tile: React.FC = ({ const cliRoom = cli.getRoom(room.room_id); const myMembership = cliRoom?.getMyMembership(); - const onPreviewClick = () => onViewRoomClick(false); - const onJoinClick = () => onViewRoomClick(true); + const onPreviewClick = (ev: ButtonEvent) => { + ev.preventDefault(); + ev.stopPropagation(); + onViewRoomClick(false); + } + const onJoinClick = (ev: ButtonEvent) => { + ev.preventDefault(); + ev.stopPropagation(); + onViewRoomClick(true); + } let button; if (myMembership === "join") { @@ -136,7 +147,7 @@ const Tile: React.FC = ({ let url: string; if (room.avatar_url) { - url = mediaFromMxc(room.avatar_url).getSquareThumbnailHttp(Math.floor(20 * window.devicePixelRatio)); + url = mediaFromMxc(room.avatar_url).getSquareThumbnailHttp(20); } let description = _t("%(count)s members", { count: room.num_joined_members }); @@ -254,7 +265,11 @@ export const HierarchyLevel = ({ const space = cli.getRoom(spaceId); const hasPermissions = space?.currentState.maySendStateEvent(EventType.SpaceChild, cli.getUserId()); - const sortedChildren = sortBy([...(relations.get(spaceId)?.values() || [])], ev => ev.content.order || null); + const children = Array.from(relations.get(spaceId)?.values() || []); + const sortedChildren = sortBy(children, ev => { + // XXX: Space Summary API doesn't give the child origin_server_ts but once it does we should use it for sorting + return getOrder(ev.content.order, null, ev.state_key); + }); const [subspaces, childRooms] = sortedChildren.reduce((result, ev: ISpaceSummaryEvent) => { const roomId = ev.state_key; if (!rooms.has(roomId)) return result; @@ -312,11 +327,12 @@ export const HierarchyLevel = ({ // mutate argument refreshToken to force a reload export const useSpaceSummary = (cli: MatrixClient, space: Room, refreshToken?: any): [ + null, ISpaceSummaryRoom[], - Map>, - Map>, - Map>, -] | [] => { + Map>?, + Map>?, + Map>?, +] | [Error] => { // TODO pagination return useAsyncMemo(async () => { try { @@ -336,13 +352,12 @@ export const useSpaceSummary = (cli: MatrixClient, space: Room, refreshToken?: a } }); - return [data.rooms as ISpaceSummaryRoom[], parentChildRelations, viaMap, childParentRelations]; + return [null, data.rooms as ISpaceSummaryRoom[], parentChildRelations, viaMap, childParentRelations]; } catch (e) { console.error(e); // TODO + return [e]; } - - return []; - }, [space, refreshToken], []); + }, [space, refreshToken], [undefined]); }; export const SpaceHierarchy: React.FC = ({ @@ -350,6 +365,7 @@ export const SpaceHierarchy: React.FC = ({ initialText = "", showRoom, refreshToken, + additionalButtons, children, }) => { const cli = MatrixClientPeg.get(); @@ -358,7 +374,7 @@ export const SpaceHierarchy: React.FC = ({ const [selected, setSelected] = useState(new Map>()); // Map> - const [rooms, parentChildMap, viaMap, childParentMap] = useSpaceSummary(cli, space, refreshToken); + const [summaryError, rooms, parentChildMap, viaMap, childParentMap] = useSpaceSummary(cli, space, refreshToken); const roomsMap = useMemo(() => { if (!rooms) return null; @@ -397,6 +413,10 @@ export const SpaceHierarchy: React.FC = ({ const [removing, setRemoving] = useState(false); const [saving, setSaving] = useState(false); + if (summaryError) { + return

      {_t("Your server does not support showing space hierarchies.")}

      ; + } + let content; if (roomsMap) { const numRooms = Array.from(roomsMap.values()).filter(r => r.room_type !== RoomType.Space).length; @@ -411,78 +431,83 @@ export const SpaceHierarchy: React.FC = ({ countsStr = _t("%(count)s rooms", { count: numRooms, numSpaces }); } - let editSection; + let manageButtons; if (space.getMyMembership() === "join" && space.currentState.maySendStateEvent(EventType.SpaceChild, userId)) { const selectedRelations = Array.from(selected.keys()).flatMap(parentId => { return [...selected.get(parentId).values()].map(childId => [parentId, childId]) as [string, string][]; }); - let buttons; - if (selectedRelations.length) { - const selectionAllSuggested = selectedRelations.every(([parentId, childId]) => { - return parentChildMap.get(parentId)?.get(childId)?.content.suggested; - }); + const selectionAllSuggested = selectedRelations.every(([parentId, childId]) => { + return parentChildMap.get(parentId)?.get(childId)?.content.suggested; + }); - const disabled = removing || saving; + const disabled = !selectedRelations.length || removing || saving; - buttons = <> - { - setRemoving(true); - try { - for (const [parentId, childId] of selectedRelations) { - await cli.sendStateEvent(parentId, EventType.SpaceChild, {}, childId); - parentChildMap.get(parentId).get(childId).content = {}; - parentChildMap.set(parentId, new Map(parentChildMap.get(parentId))); - } - } catch (e) { - setError(_t("Failed to remove some rooms. Try again later")); - } - setRemoving(false); - }} - kind="danger_outline" - disabled={disabled} - > - { removing ? _t("Removing...") : _t("Remove") } - - { - setSaving(true); - try { - for (const [parentId, childId] of selectedRelations) { - const suggested = !selectionAllSuggested; - const existingContent = parentChildMap.get(parentId)?.get(childId)?.content; - if (!existingContent || existingContent.suggested === suggested) continue; - - const content = { - ...existingContent, - suggested: !selectionAllSuggested, - }; - - await cli.sendStateEvent(parentId, EventType.SpaceChild, content, childId); - - parentChildMap.get(parentId).get(childId).content = content; - parentChildMap.set(parentId, new Map(parentChildMap.get(parentId))); - } - } catch (e) { - setError("Failed to update some suggestions. Try again later"); - } - setSaving(false); - }} - kind="primary_outline" - disabled={disabled} - > - { saving - ? _t("Saving...") - : (selectionAllSuggested ? _t("Mark as not suggested") : _t("Mark as suggested")) - } - - ; + let Button: React.ComponentType> = AccessibleButton; + let props = {}; + if (!selectedRelations.length) { + Button = AccessibleTooltipButton; + props = { + tooltip: _t("Select a room below first"), + yOffset: -40, + }; } - editSection = - { buttons } - ; + manageButtons = <> + + + ; } let results; @@ -528,7 +553,10 @@ export const SpaceHierarchy: React.FC = ({ content = <>
      { countsStr } - { editSection } + + { additionalButtons } + { manageButtons } +
      { error &&
      { error } @@ -538,10 +566,8 @@ export const SpaceHierarchy: React.FC = ({ { children } ; - } else if (!rooms) { - content = ; } else { - content =

      {_t("Your server does not support showing space hierarchies.")}

      ; + content = ; } // TODO loading state/error state diff --git a/src/components/structures/SpaceRoomView.tsx b/src/components/structures/SpaceRoomView.tsx index 31358a3731..5db54815b7 100644 --- a/src/components/structures/SpaceRoomView.tsx +++ b/src/components/structures/SpaceRoomView.tsx @@ -51,6 +51,15 @@ import MemberAvatar from "../views/avatars/MemberAvatar"; import {useStateToggle} from "../../hooks/useStateToggle"; import SpaceStore from "../../stores/SpaceStore"; import FacePile from "../views/elements/FacePile"; +import {AddExistingToSpace} from "../views/dialogs/AddExistingToSpaceDialog"; +import {sleep} from "../../utils/promise"; +import {calculateRoomVia} from "../../utils/permalinks/Permalinks"; +import {ChevronFace, ContextMenuButton, useContextMenu} from "./ContextMenu"; +import IconizedContextMenu, { + IconizedContextMenuOption, + IconizedContextMenuOptionList, +} from "../views/context_menus/IconizedContextMenu"; +import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton"; interface IProps { space: Room; @@ -214,6 +223,67 @@ const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }) =>
      ; }; +const SpaceLandingAddButton = ({ space, onNewRoomAdded }) => { + const cli = useContext(MatrixClientContext); + const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu(); + + let contextMenu; + if (menuDisplayed) { + const rect = handle.current.getBoundingClientRect(); + contextMenu = + + { + e.preventDefault(); + e.stopPropagation(); + closeMenu(); + + if (await showCreateNewRoom(cli, space)) { + onNewRoomAdded(); + } + }} + /> + { + e.preventDefault(); + e.stopPropagation(); + closeMenu(); + + const [added] = await showAddExistingRooms(cli, space); + if (added) { + onNewRoomAdded(); + } + }} + /> + + ; + } + + return <> + + { _t("Add") } + + { contextMenu } + ; +}; + const SpaceLanding = ({ space }) => { const cli = useContext(MatrixClientContext); const myMembership = useMyRoomMembership(space); @@ -238,32 +308,20 @@ const SpaceLanding = ({ space }) => { const [refreshToken, forceUpdate] = useStateToggle(false); - let addRoomButtons; + let addRoomButton; if (canAddRooms) { - addRoomButtons = - { - const [added] = await showAddExistingRooms(cli, space); - if (added) { - forceUpdate(); - } - }}> - { _t("Add existing rooms & spaces") } - - { - showCreateNewRoom(cli, space); - }}> - { _t("Create a new room") } - - ; + addRoomButton = ; } let settingsButton; if (shouldShowSpaceSettings(cli, space)) { - settingsButton = { - showSpaceSettings(cli, space); - }}> - { _t("Settings") } - ; + settingsButton = { + showSpaceSettings(cli, space); + }} + title={_t("Settings")} + />; } const onMembersClick = () => { @@ -290,17 +348,19 @@ const SpaceLanding = ({ space }) => { { inviteButton } + { settingsButton }

    -
    - { addRoomButtons } - { settingsButton } -
    - +
    ; }; @@ -354,7 +414,7 @@ const SpaceSetupFirstRooms = ({ space, title, description, onFinished }) => { let buttonLabel = _t("Skip for now"); if (roomNames.some(name => name.trim())) { onClick = onNextClick; - buttonLabel = busy ? _t("Creating rooms...") : _t("Continue") + buttonLabel = busy ? _t("Creating rooms...") : _t("Continue"); } return
    @@ -376,6 +436,74 @@ const SpaceSetupFirstRooms = ({ space, title, description, onFinished }) => {
    ; }; +const SpaceAddExistingRooms = ({ space, onFinished }) => { + const [selectedToAdd, setSelectedToAdd] = useState(new Set()); + + const [busy, setBusy] = useState(false); + const [error, setError] = useState(""); + + let onClick = onFinished; + let buttonLabel = _t("Skip for now"); + if (selectedToAdd.size > 0) { + onClick = async () => { + setBusy(true); + + for (const room of selectedToAdd) { + const via = calculateRoomVia(room); + try { + await SpaceStore.instance.addRoomToSpace(space, room.roomId, via).catch(async e => { + if (e.errcode === "M_LIMIT_EXCEEDED") { + await sleep(e.data.retry_after_ms); + return SpaceStore.instance.addRoomToSpace(space, room.roomId, via); // retry + } + + throw e; + }); + } catch (e) { + console.error("Failed to add rooms to space", e); + setError(_t("Failed to add rooms to space")); + break; + } + } + setBusy(false); + }; + buttonLabel = busy ? _t("Adding...") : _t("Add"); + } + + return
    +

    { _t("What do you want to organise?") }

    +
    + { _t("Pick rooms or conversations to add. This is just a space for you, " + + "no one will be informed. You can add more later.") } +
    + + { error &&
    { error }
    } + + { + if (checked) { + selectedToAdd.add(room); + } else { + selectedToAdd.delete(room); + } + setSelectedToAdd(new Set(selectedToAdd)); + }} + /> + +
    + + { buttonLabel } + +
    +
    ; +}; + const SpaceSetupPublicShare = ({ space, onFinished }) => { return

    { _t("Share %(name)s", { name: space.name }) }

    @@ -659,7 +787,7 @@ export default class SpaceRoomView extends React.PureComponent { return { - this.setState({ phase: invite ? Phase.PrivateInvite : Phase.PrivateCreateRooms }); + this.setState({ phase: invite ? Phase.PrivateInvite : Phase.PrivateExistingRooms }); }} />; case Phase.PrivateInvite: @@ -675,6 +803,11 @@ export default class SpaceRoomView extends React.PureComponent { "You can add more later too, including already existing ones.")} onFinished={() => this.setState({ phase: Phase.Landing })} />; + case Phase.PrivateExistingRooms: + return this.setState({ phase: Phase.Landing })} + />; } } diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index 12f5d6e890..8cc344f66b 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -68,6 +68,7 @@ class TimelinePanel extends React.Component { showReadReceipts: PropTypes.bool, // Enable managing RRs and RMs. These require the timelineSet to have a room. manageReadReceipts: PropTypes.bool, + sendReadReceiptOnLoad: PropTypes.bool, manageReadMarkers: PropTypes.bool, // true to give the component a 'display: none' style. @@ -126,6 +127,7 @@ class TimelinePanel extends React.Component { // event tile heights. (See _unpaginateEvents) timelineCap: Number.MAX_VALUE, className: 'mx_RoomView_messagePanel', + sendReadReceiptOnLoad: true, }; constructor(props) { @@ -785,8 +787,10 @@ class TimelinePanel extends React.Component { return; } const lastDisplayedEvent = this.state.events[lastDisplayedIndex]; - this._setReadMarker(lastDisplayedEvent.getId(), - lastDisplayedEvent.getTs()); + this._setReadMarker( + lastDisplayedEvent.getId(), + lastDisplayedEvent.getTs(), + ); // the read-marker should become invisible, so that if the user scrolls // down, they don't see it. @@ -872,7 +876,7 @@ class TimelinePanel extends React.Component { // The messagepanel knows where the RM is, so we must have loaded // the relevant event. this._messagePanel.current.scrollToEvent(this.state.readMarkerEventId, - 0, 1/3); + 0, 1/3); return; } @@ -1044,12 +1048,14 @@ class TimelinePanel extends React.Component { } if (eventId) { this._messagePanel.current.scrollToEvent(eventId, pixelOffset, - offsetBase); + offsetBase); } else { this._messagePanel.current.scrollToBottom(); } - this.sendReadReceipt(); + if (this.props.sendReadReceiptOnLoad) { + this.sendReadReceipt(); + } }); }; @@ -1418,8 +1424,8 @@ class TimelinePanel extends React.Component { ['PREPARED', 'CATCHUP'].includes(this.state.clientSyncState) ); const events = this.state.firstVisibleEventIndex - ? this.state.events.slice(this.state.firstVisibleEventIndex) - : this.state.events; + ? this.state.events.slice(this.state.firstVisibleEventIndex) + : this.state.events; return ( void, +} + +interface IState { + loginView: number; + keyBackupNeeded: boolean; + busy: boolean; + password: string; + errorText: string; + flows: LoginFlow[]; +} + +@replaceableComponent("structures.auth.SoftLogout") +export default class SoftLogout extends React.Component { + constructor(props) { + super(props); this.state = { loginView: LOGIN_VIEW.LOADING, keyBackupNeeded: true, // assume we do while we figure it out (see componentDidMount) - busy: false, password: "", errorText: "", + flows: [], }; } @@ -72,7 +83,7 @@ export default class SoftLogout extends React.Component { return; } - this._initLogin(); + this.initLogin(); const cli = MatrixClientPeg.get(); if (cli.isCryptoEnabled()) { @@ -94,7 +105,7 @@ export default class SoftLogout extends React.Component { }); }; - async _initLogin() { + private async initLogin() { const queryParams = this.props.realQueryParams; const hasAllParams = queryParams && queryParams['loginToken']; if (hasAllParams) { @@ -189,7 +200,7 @@ export default class SoftLogout extends React.Component { }); } - _renderSignInSection() { + private renderSignInSection() { if (this.state.loginView === LOGIN_VIEW.LOADING) { const Spinner = sdk.getComponent("elements.Spinner"); return ; @@ -247,7 +258,7 @@ export default class SoftLogout extends React.Component { } // else we already have a message and should use it (key backup warning) const loginType = this.state.loginView === LOGIN_VIEW.CAS ? "cas" : "sso"; - const flow = this.state.flows.find(flow => flow.type === "m.login." + loginType); + const flow = this.state.flows.find(flow => flow.type === "m.login." + loginType) as ISSOFlow; return (
    @@ -289,7 +300,7 @@ export default class SoftLogout extends React.Component {

    {_t("Sign in")}

    - {this._renderSignInSection()} + {this.renderSignInSection()}

    {_t("Clear personal data")}

    diff --git a/src/components/views/auth/InteractiveAuthEntryComponents.js b/src/components/views/auth/InteractiveAuthEntryComponents.js index 6cbecd22ee..e34349c474 100644 --- a/src/components/views/auth/InteractiveAuthEntryComponents.js +++ b/src/components/views/auth/InteractiveAuthEntryComponents.js @@ -169,7 +169,7 @@ export class PasswordAuthEntry extends React.Component { { submitButtonOrSpinner }
    - { errorSection } + { errorSection }
    ); } @@ -375,7 +375,7 @@ export class TermsAuthEntry extends React.Component { if (this.props.showContinue !== false) { // XXX: button classes submitButton = ; + onClick={this._trySubmit} disabled={!allChecked}>{_t("Accept")}; } return ( diff --git a/src/components/views/avatars/MemberAvatar.tsx b/src/components/views/avatars/MemberAvatar.tsx index c79cbc0d32..3205ca372c 100644 --- a/src/components/views/avatars/MemberAvatar.tsx +++ b/src/components/views/avatars/MemberAvatar.tsx @@ -68,8 +68,8 @@ export default class MemberAvatar extends React.Component { let imageUrl = null; if (props.member.getMxcAvatarUrl()) { imageUrl = mediaFromMxc(props.member.getMxcAvatarUrl()).getThumbnailOfSourceHttp( - Math.floor(props.width * window.devicePixelRatio), - Math.floor(props.height * window.devicePixelRatio), + props.width, + props.height, props.resizeMethod, ); } diff --git a/src/components/views/avatars/RoomAvatar.tsx b/src/components/views/avatars/RoomAvatar.tsx index ad0eb45a52..4693d907ba 100644 --- a/src/components/views/avatars/RoomAvatar.tsx +++ b/src/components/views/avatars/RoomAvatar.tsx @@ -93,8 +93,8 @@ export default class RoomAvatar extends React.Component { let oobAvatar = null; if (props.oobData.avatarUrl) { oobAvatar = mediaFromMxc(props.oobData.avatarUrl).getThumbnailOfSourceHttp( - Math.floor(props.width * window.devicePixelRatio), - Math.floor(props.height * window.devicePixelRatio), + props.width, + props.height, props.resizeMethod, ); } @@ -109,12 +109,7 @@ export default class RoomAvatar extends React.Component { private static getRoomAvatarUrl(props: IProps): string { if (!props.room) return null; - return Avatar.avatarUrlForRoom( - props.room, - Math.floor(props.width * window.devicePixelRatio), - Math.floor(props.height * window.devicePixelRatio), - props.resizeMethod, - ); + return Avatar.avatarUrlForRoom(props.room, props.width, props.height, props.resizeMethod); } private onRoomAvatarClick = () => { diff --git a/src/components/views/context_menus/MessageContextMenu.js b/src/components/views/context_menus/MessageContextMenu.js index f86cd26f32..35efd12c9c 100644 --- a/src/components/views/context_menus/MessageContextMenu.js +++ b/src/components/views/context_menus/MessageContextMenu.js @@ -1,8 +1,6 @@ /* -Copyright 2015, 2016 OpenMarket Ltd -Copyright 2018 New Vector Ltd Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> -Copyright 2019 The Matrix.org Foundation C.I.C. +Copyright 2015, 2016, 2018, 2019, 2021 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -34,7 +32,7 @@ import {MenuItem} from "../../structures/ContextMenu"; import {EventType} from "matrix-js-sdk/src/@types/event"; import {replaceableComponent} from "../../../utils/replaceableComponent"; -function canCancel(eventStatus) { +export function canCancel(eventStatus) { return eventStatus === EventStatus.QUEUED || eventStatus === EventStatus.NOT_SENT; } @@ -98,21 +96,6 @@ export default class MessageContextMenu extends React.Component { return content.pinned && Array.isArray(content.pinned) && content.pinned.includes(this.props.mxEvent.getId()); } - onResendClick = () => { - Resend.resend(this.props.mxEvent); - this.closeMenu(); - }; - - onResendEditClick = () => { - Resend.resend(this.props.mxEvent.replacingEvent()); - this.closeMenu(); - }; - - onResendRedactionClick = () => { - Resend.resend(this.props.mxEvent.localRedactionEvent()); - this.closeMenu(); - }; - onResendReactionsClick = () => { for (const reaction of this._getUnsentReactions()) { Resend.resend(reaction); @@ -170,29 +153,6 @@ export default class MessageContextMenu extends React.Component { this.closeMenu(); }; - onCancelSendClick = () => { - const mxEvent = this.props.mxEvent; - const editEvent = mxEvent.replacingEvent(); - const redactEvent = mxEvent.localRedactionEvent(); - const pendingReactions = this._getPendingReactions(); - - if (editEvent && canCancel(editEvent.status)) { - Resend.removeFromQueue(editEvent); - } - if (redactEvent && canCancel(redactEvent.status)) { - Resend.removeFromQueue(redactEvent); - } - if (pendingReactions.length) { - for (const reaction of pendingReactions) { - Resend.removeFromQueue(reaction); - } - } - if (canCancel(mxEvent.status)) { - Resend.removeFromQueue(this.props.mxEvent); - } - this.closeMenu(); - }; - onForwardClick = () => { if (this.props.onCloseDialog) this.props.onCloseDialog(); dis.dispatch({ @@ -285,20 +245,9 @@ export default class MessageContextMenu extends React.Component { const me = cli.getUserId(); const mxEvent = this.props.mxEvent; const eventStatus = mxEvent.status; - const editStatus = mxEvent.replacingEvent() && mxEvent.replacingEvent().status; - const redactStatus = mxEvent.localRedactionEvent() && mxEvent.localRedactionEvent().status; const unsentReactionsCount = this._getUnsentReactions().length; - const pendingReactionsCount = this._getPendingReactions().length; - const allowCancel = canCancel(mxEvent.status) || - canCancel(editStatus) || - canCancel(redactStatus) || - pendingReactionsCount !== 0; - let resendButton; - let resendEditButton; let resendReactionsButton; - let resendRedactionButton; let redactButton; - let cancelButton; let forwardButton; let pinButton; let unhidePreviewButton; @@ -309,22 +258,6 @@ export default class MessageContextMenu extends React.Component { // status is SENT before remote-echo, null after const isSent = !eventStatus || eventStatus === EventStatus.SENT; if (!mxEvent.isRedacted()) { - if (eventStatus === EventStatus.NOT_SENT) { - resendButton = ( - - { _t('Resend') } - - ); - } - - if (editStatus === EventStatus.NOT_SENT) { - resendEditButton = ( - - { _t('Resend edit') } - - ); - } - if (unsentReactionsCount !== 0) { resendReactionsButton = ( @@ -334,14 +267,6 @@ export default class MessageContextMenu extends React.Component { } } - if (redactStatus === EventStatus.NOT_SENT) { - resendRedactionButton = ( - - { _t('Resend removal') } - - ); - } - if (isSent && this.state.canRedact) { redactButton = ( @@ -350,14 +275,6 @@ export default class MessageContextMenu extends React.Component { ); } - if (allowCancel) { - cancelButton = ( - - { _t('Cancel Sending') } - - ); - } - if (isContentActionable(mxEvent)) { forwardButton = ( @@ -433,7 +350,7 @@ export default class MessageContextMenu extends React.Component { > { _t('Source URL') } - ); + ); } if (this.props.collapseReplyThread) { @@ -455,12 +372,8 @@ export default class MessageContextMenu extends React.Component { return (
    - { resendButton } - { resendEditButton } { resendReactionsButton } - { resendRedactionButton } { redactButton } - { cancelButton } { forwardButton } { pinButton } { viewSourceButton } diff --git a/src/components/views/dialogs/AddExistingToSpaceDialog.tsx b/src/components/views/dialogs/AddExistingToSpaceDialog.tsx index 0f58a624f3..a33248200c 100644 --- a/src/components/views/dialogs/AddExistingToSpaceDialog.tsx +++ b/src/components/views/dialogs/AddExistingToSpaceDialog.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, {useState} from "react"; +import React, {useContext, useMemo, useState} from "react"; import classNames from "classnames"; import {Room} from "matrix-js-sdk/src/models/room"; import {MatrixClient} from "matrix-js-sdk/src/client"; @@ -29,10 +29,13 @@ import RoomAvatar from "../avatars/RoomAvatar"; import {getDisplayAliasForRoom} from "../../../Rooms"; import AccessibleButton from "../elements/AccessibleButton"; import AutoHideScrollbar from "../../structures/AutoHideScrollbar"; -import {allSettled} from "../../../utils/promise"; +import {sleep} from "../../../utils/promise"; import DMRoomMap from "../../../utils/DMRoomMap"; import {calculateRoomVia} from "../../../utils/permalinks/Permalinks"; import StyledCheckbox from "../elements/StyledCheckbox"; +import MatrixClientContext from "../../../contexts/MatrixClientContext"; +import {sortRooms} from "../../../stores/room-list/algorithms/tag-sorting/RecentAlgorithm"; +import ProgressBar from "../elements/ProgressBar"; interface IProps extends IDialogProps { matrixClient: MatrixClient; @@ -41,45 +44,128 @@ interface IProps extends IDialogProps { } const Entry = ({ room, checked, onChange }) => { - return
    + return
    ; + { room.name } + onChange(e.target.checked) : null} + checked={checked} + disabled={!onChange} + /> + ; }; -const AddExistingToSpaceDialog: React.FC = ({ matrixClient: cli, space, onCreateRoomClick, onFinished }) => { +interface IAddExistingToSpaceProps { + space: Room; + selected: Set; + onChange(checked: boolean, room: Room): void; +} + +export const AddExistingToSpace: React.FC = ({ space, selected, onChange }) => { + const cli = useContext(MatrixClientContext); + const visibleRooms = useMemo(() => sortRooms(cli.getVisibleRooms()), [cli]); + const [query, setQuery] = useState(""); const lcQuery = query.toLowerCase(); - const [selectedSpace, setSelectedSpace] = useState(space); - const [selectedToAdd, setSelectedToAdd] = useState(new Set()); - const existingSubspaces = SpaceStore.instance.getChildSpaces(space.roomId); const existingSubspacesSet = new Set(existingSubspaces); const existingRoomsSet = new Set(SpaceStore.instance.getChildRooms(space.roomId)); - const joinRule = selectedSpace.getJoinRule(); - const [spaces, rooms, dms] = cli.getVisibleRooms().reduce((arr, room) => { + const joinRule = space.getJoinRule(); + const [spaces, rooms, dms] = visibleRooms.reduce((arr, room) => { if (room.getMyMembership() !== "join") return arr; if (!room.name.toLowerCase().includes(lcQuery)) return arr; if (room.isSpaceRoom()) { - if (room !== space && room !== selectedSpace && !existingSubspacesSet.has(room)) { + if (room !== space && !existingSubspacesSet.has(room)) { arr[0].push(room); } - } else if (!existingRoomsSet.has(room) && joinRule !== "public") { - // Only show DMs for non-public spaces as they make very little sense in spaces other than "Just Me" ones. - arr[DMRoomMap.shared().getUserIdForRoomId(room.roomId) ? 2 : 1].push(room); + } else if (!existingRoomsSet.has(room)) { + if (!DMRoomMap.shared().getUserIdForRoomId(room.roomId)) { + arr[1].push(room); + } else if (joinRule !== "public") { + // Only show DMs for non-public spaces as they make very little sense in spaces other than "Just Me" ones. + arr[2].push(room); + } } return arr; }, [[], [], []]); - const [busy, setBusy] = useState(false); - const [error, setError] = useState(""); + return
    + + + { rooms.length > 0 ? ( +
    +

    { _t("Rooms") }

    + { rooms.map(room => { + return { + onChange(checked, room); + } : null} + />; + }) } +
    + ) : undefined } + + { spaces.length > 0 ? ( +
    +

    { _t("Spaces") }

    + { spaces.map(space => { + return { + onChange(checked, space); + } : null} + />; + }) } +
    + ) : null } + + { dms.length > 0 ? ( +
    +

    { _t("Direct Messages") }

    + { dms.map(room => { + return { + onChange(checked, room); + } : null} + />; + }) } +
    + ) : null } + + { spaces.length + rooms.length + dms.length < 1 ? + { _t("No results") } + : undefined } +
    +
    ; +}; + +const AddExistingToSpaceDialog: React.FC = ({ matrixClient: cli, space, onCreateRoomClick, onFinished }) => { + const [selectedSpace, setSelectedSpace] = useState(space); + const existingSubspaces = SpaceStore.instance.getChildSpaces(space.roomId); + const [selectedToAdd, setSelectedToAdd] = useState(new Set()); + + const [progress, setProgress] = useState(null); + const [error, setError] = useState(null); let spaceOptionSection; - if (existingSubspacesSet.size > 0) { + if (existingSubspaces.length > 0) { const options = [space, ...existingSubspaces].map((space) => { const classes = classNames("mx_AddExistingToSpaceDialog_dropdownOption", { mx_AddExistingToSpaceDialog_dropdownOptionActive: space === selectedSpace, @@ -116,116 +202,106 @@ const AddExistingToSpaceDialog: React.FC = ({ matrixClient: cli, space,
    ; - return - { error &&
    { error }
    } + const addRooms = async () => { + setError(null); + setProgress(0); - - - { rooms.length > 0 ? ( -
    -

    { _t("Rooms") }

    - { rooms.map(room => { - return { - if (checked) { - selectedToAdd.add(room); - } else { - selectedToAdd.delete(room); - } - setSelectedToAdd(new Set(selectedToAdd)); - }} - />; - }) } -
    - ) : undefined } + let error; - { spaces.length > 0 ? ( -
    -

    { _t("Spaces") }

    - { spaces.map(space => { - return { - if (checked) { - selectedToAdd.add(space); - } else { - selectedToAdd.delete(space); - } - setSelectedToAdd(new Set(selectedToAdd)); - }} - />; - }) } -
    - ) : null } + for (const room of selectedToAdd) { + const via = calculateRoomVia(room); + try { + await SpaceStore.instance.addRoomToSpace(space, room.roomId, via).catch(async e => { + if (e.errcode === "M_LIMIT_EXCEEDED") { + await sleep(e.data.retry_after_ms); + return SpaceStore.instance.addRoomToSpace(space, room.roomId, via); // retry + } - { dms.length > 0 ? ( -
    -

    { _t("Direct Messages") }

    - { dms.map(space => { - return { - if (checked) { - selectedToAdd.add(space); - } else { - selectedToAdd.delete(space); - } - setSelectedToAdd(new Set(selectedToAdd)); - }} - />; - }) } -
    - ) : null } + throw e; + }); + setProgress(i => i + 1); + } catch (e) { + console.error("Failed to add rooms to space", e); + setError(error = e); + break; + } + } - { spaces.length + rooms.length + dms.length < 1 ? - { _t("No results") } - : undefined } -
    + if (!error) { + onFinished(true); + } + }; -
    + const busy = progress !== null; + + let footer; + if (error) { + footer = <> + + + +
    { _t("Not all selected were added") }
    +
    { _t("Try again") }
    +
    + + + { _t("Retry") } + + ; + } else if (busy) { + footer = + +
    + { _t("Adding rooms... (%(progress)s out of %(count)s)", { + count: selectedToAdd.size, + progress, + }) } +
    +
    ; + } else { + footer = <> -
    { _t("Don't want to add an existing room?") }
    +
    { _t("Want to add a new room instead?") }
    onCreateRoomClick(cli, space)} kind="link"> { _t("Create a new room") }
    - { - setBusy(true); - try { - await allSettled(Array.from(selectedToAdd).map((room) => - SpaceStore.instance.addRoomToSpace(space, room.roomId, calculateRoomVia(room)))); - onFinished(true); - } catch (e) { - console.error("Failed to add rooms to space", e); - setError(_t("Failed to add rooms to space")); - } - setBusy(false); - }} - > - { busy ? _t("Adding...") : _t("Add") } + + { _t("Add") } + ; + } + + return + + { + if (checked) { + selectedToAdd.add(room); + } else { + selectedToAdd.delete(room); + } + setSelectedToAdd(new Set(selectedToAdd)); + } : null} + /> + + +
    + { footer }
    ; }; diff --git a/src/components/views/dialogs/BugReportDialog.js b/src/components/views/dialogs/BugReportDialog.js index 8948c14c7c..cbe0130649 100644 --- a/src/components/views/dialogs/BugReportDialog.js +++ b/src/components/views/dialogs/BugReportDialog.js @@ -184,7 +184,7 @@ export default class BugReportDialog extends React.Component { return (
    diff --git a/src/components/views/dialogs/ChangelogDialog.js b/src/components/views/dialogs/ChangelogDialog.js index 50bc13cff5..efbeba3977 100644 --- a/src/components/views/dialogs/ChangelogDialog.js +++ b/src/components/views/dialogs/ChangelogDialog.js @@ -95,7 +95,7 @@ export default class ChangelogDialog extends React.Component { description={content} button={_t("Update")} onFinished={this.props.onFinished} - /> + /> ); } } diff --git a/src/components/views/dialogs/ConfirmWipeDeviceDialog.js b/src/components/views/dialogs/ConfirmWipeDeviceDialog.js index 4faaad0f7e..333e1522f1 100644 --- a/src/components/views/dialogs/ConfirmWipeDeviceDialog.js +++ b/src/components/views/dialogs/ConfirmWipeDeviceDialog.js @@ -39,9 +39,12 @@ export default class ConfirmWipeDeviceDialog extends React.Component { const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); return ( - +

    {_t( diff --git a/src/components/views/dialogs/DevtoolsDialog.js b/src/components/views/dialogs/DevtoolsDialog.js index 9f5513e0a3..8a035263cc 100644 --- a/src/components/views/dialogs/DevtoolsDialog.js +++ b/src/components/views/dialogs/DevtoolsDialog.js @@ -70,8 +70,16 @@ class GenericEditor extends React.PureComponent { } textInput(id, label) { - return ; + return ; } } @@ -155,7 +163,7 @@ export class SendCustomEvent extends GenericEditor {
    + autoComplete="off" value={this.state.evContent} onChange={this._onChange} element="textarea" />

    @@ -239,7 +247,7 @@ class SendAccountData extends GenericEditor {
    + autoComplete="off" value={this.state.evContent} onChange={this._onChange} element="textarea" />
    @@ -315,15 +323,15 @@ class FilteredList extends React.PureComponent { const TruncatedList = sdk.getComponent("elements.TruncatedList"); return
    + type="text" autoComplete="off" value={this.props.query} onChange={this.onQuery} + className="mx_TextInputDialog_input mx_DevTools_RoomStateExplorer_query" + // force re-render so that autoFocus is applied when this component is re-used + key={this.props.children[0] ? this.props.children[0].key : ''} /> + getChildCount={this.getChildCount} + truncateAt={this.state.truncateAt} + createOverflowElement={this.createOverflowElement} />
    ; } } @@ -647,7 +655,7 @@ function VerificationRequest({txnId, request}) { /* Note that request.timeout is a getter, so its value changes */ const id = setInterval(() => { - setRequestTimeout(request.timeout); + setRequestTimeout(request.timeout); }, 500); return () => { clearInterval(id); }; @@ -941,35 +949,35 @@ class SettingsExplorer extends React.Component { /> - - - - - + + + + + - {allSettings.map(i => ( - - + - - - - ))} + + + + + + ))}
    {_t("Setting ID")}{_t("Value")}{_t("Value in this room")}
    {_t("Setting ID")}{_t("Value")}{_t("Value in this room")}
    - this.onViewClick(e, i)}> - {i} - - this.onEditClick(e, i)} - className='mx_DevTools_SettingsExplorer_edit' - > + {allSettings.map(i => ( +
    + this.onViewClick(e, i)}> + {i} + + this.onEditClick(e, i)} + className='mx_DevTools_SettingsExplorer_edit' + > ✏ - - - {this.renderSettingValue(SettingsStore.getValue(i))} - - - {this.renderSettingValue(SettingsStore.getValue(i, room.roomId))} - -
    + {this.renderSettingValue(SettingsStore.getValue(i))} + + + {this.renderSettingValue(SettingsStore.getValue(i, room.roomId))} + +
    @@ -998,11 +1006,11 @@ class SettingsExplorer extends React.Component {
    - - - - - + + + + + {LEVEL_ORDER.map(lvl => ( diff --git a/src/components/views/dialogs/IncomingSasDialog.js b/src/components/views/dialogs/IncomingSasDialog.js index f18b7a9d0c..5df02d7a6f 100644 --- a/src/components/views/dialogs/IncomingSasDialog.js +++ b/src/components/views/dialogs/IncomingSasDialog.js @@ -130,7 +130,7 @@ export default class IncomingSasDialog extends React.Component { const oppProfile = this.state.opponentProfile; if (oppProfile) { const url = oppProfile.avatar_url - ? mediaFromMxc(oppProfile.avatar_url).getSquareThumbnailHttp(Math.floor(48 * window.devicePixelRatio)) + ? mediaFromMxc(oppProfile.avatar_url).getSquareThumbnailHttp(48) : null; profile =
    +

    {_t("Enable 'Manage Integrations' in Settings to do this.")}

    diff --git a/src/components/views/dialogs/IntegrationsImpossibleDialog.js b/src/components/views/dialogs/IntegrationsImpossibleDialog.js index 9bc9d02ba6..e14d40aaef 100644 --- a/src/components/views/dialogs/IntegrationsImpossibleDialog.js +++ b/src/components/views/dialogs/IntegrationsImpossibleDialog.js @@ -37,9 +37,12 @@ export default class IntegrationsImpossibleDialog extends React.Component { const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); return ( - +

    {_t( diff --git a/src/components/views/dialogs/InviteDialog.tsx b/src/components/views/dialogs/InviteDialog.tsx index 2ebc84ec7c..ec9c71ccbe 100644 --- a/src/components/views/dialogs/InviteDialog.tsx +++ b/src/components/views/dialogs/InviteDialog.tsx @@ -1312,7 +1312,7 @@ export default class InviteDialog extends React.PureComponent {success ? - {_t("Upload completed")} : - cancelled ? - {_t("Cancelled signature upload")} : - {_t("Unable to upload")}} + {_t("Upload completed")} : + cancelled ? + {_t("Cancelled signature upload")} : + {_t("Unable to upload")}} + {content} ); diff --git a/src/components/views/dialogs/RoomSettingsDialog.js b/src/components/views/dialogs/RoomSettingsDialog.js index 9c2f23ef22..b6c4d42243 100644 --- a/src/components/views/dialogs/RoomSettingsDialog.js +++ b/src/components/views/dialogs/RoomSettingsDialog.js @@ -116,8 +116,12 @@ export default class RoomSettingsDialog extends React.Component { const roomName = MatrixClientPeg.get().getRoom(this.props.roomId).name; return ( - +

    diff --git a/src/components/views/dialogs/ServerPickerDialog.tsx b/src/components/views/dialogs/ServerPickerDialog.tsx index 4abc0a88b1..62a2b95c68 100644 --- a/src/components/views/dialogs/ServerPickerDialog.tsx +++ b/src/components/views/dialogs/ServerPickerDialog.tsx @@ -1,5 +1,5 @@ /* -Copyright 2020 The Matrix.org Foundation C.I.C. +Copyright 2020-2021 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -110,7 +110,7 @@ export default class ServerPickerDialog extends React.PureComponent diff --git a/src/components/views/dialogs/SessionRestoreErrorDialog.js b/src/components/views/dialogs/SessionRestoreErrorDialog.js index 50d7fbea09..43e73a2f83 100644 --- a/src/components/views/dialogs/SessionRestoreErrorDialog.js +++ b/src/components/views/dialogs/SessionRestoreErrorDialog.js @@ -98,7 +98,7 @@ export default class SessionRestoreErrorDialog extends React.Component { "may be incompatible with this version. Close this window and return " + "to the more recent version.", { brand }, - ) }

    + ) }

    { _t( "Clearing your browser's storage may fix the problem, but will sign you " + diff --git a/src/components/views/dialogs/StorageEvictedDialog.js b/src/components/views/dialogs/StorageEvictedDialog.js index 15c5347644..1e17ab1738 100644 --- a/src/components/views/dialogs/StorageEvictedDialog.js +++ b/src/components/views/dialogs/StorageEvictedDialog.js @@ -45,10 +45,12 @@ export default class StorageEvictedDialog extends React.Component { let logRequest; if (SdkConfig.get().bug_report_endpoint_url) { logRequest = _t( - "To help us prevent this in future, please send us logs.", {}, - { - a: text => {text}, - }); + "To help us prevent this in future, please send us logs.", + {}, + { + a: text => {text}, + }, + ); } return ( diff --git a/src/components/views/dialogs/UserSettingsDialog.js b/src/components/views/dialogs/UserSettingsDialog.js index eb9eaeb5dd..e7f6953589 100644 --- a/src/components/views/dialogs/UserSettingsDialog.js +++ b/src/components/views/dialogs/UserSettingsDialog.js @@ -155,8 +155,12 @@ export default class UserSettingsDialog extends React.Component { const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); return ( - +

    diff --git a/src/components/views/dialogs/VerificationRequestDialog.js b/src/components/views/dialogs/VerificationRequestDialog.js index 205597a1c4..9281275e6a 100644 --- a/src/components/views/dialogs/VerificationRequestDialog.js +++ b/src/components/views/dialogs/VerificationRequestDialog.js @@ -52,11 +52,13 @@ export default class VerificationRequestDialog extends React.Component { const title = request && request.isSelfVerification ? _t("Verify other login") : _t("Verification Request"); - return + return +

    {_t("The widget will verify your user ID, but won't be able to perform actions for you:")} diff --git a/src/components/views/dialogs/security/ConfirmDestroyCrossSigningDialog.js b/src/components/views/dialogs/security/ConfirmDestroyCrossSigningDialog.js index 43fb25f152..e71983b074 100644 --- a/src/components/views/dialogs/security/ConfirmDestroyCrossSigningDialog.js +++ b/src/components/views/dialogs/security/ConfirmDestroyCrossSigningDialog.js @@ -40,10 +40,11 @@ export default class ConfirmDestroyCrossSigningDialog extends React.Component { return ( + className='mx_ConfirmDestroyCrossSigningDialog' + hasCancel={true} + onFinished={this.props.onFinished} + title={_t("Destroy cross-signing keys?")} + >

    {_t( diff --git a/src/components/views/dialogs/security/RestoreKeyBackupDialog.js b/src/components/views/dialogs/security/RestoreKeyBackupDialog.js index 1fafe03d95..4ac15ab5a3 100644 --- a/src/components/views/dialogs/security/RestoreKeyBackupDialog.js +++ b/src/components/views/dialogs/security/RestoreKeyBackupDialog.js @@ -373,21 +373,24 @@ export default class RestoreKeyBackupDialog extends React.PureComponent { {_t( "If you've forgotten your Security Phrase you can "+ "use your Security Key or " + - "set up new recovery options" - , {}, { - button1: s => - {s} - , - button2: s => - {s} - , - })} + "set up new recovery options", + {}, + { + button1: s => + {s} + , + button2: s => + {s} + , + })}

    ; } else { title = _t("Enter Security Key"); @@ -435,15 +438,17 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
    {_t( "If you've forgotten your Security Key you can "+ - "" - , {}, { - button: s => - {s} - , - })} + "", + {}, + { + button: s => + {s} + , + }, + )}
    ; } @@ -452,9 +457,9 @@ export default class RestoreKeyBackupDialog extends React.PureComponent { onFinished={this.props.onFinished} title={title} > -
    - {content} -
    +
    + {content} +
    ); } diff --git a/src/components/views/elements/ActionButton.js b/src/components/views/elements/ActionButton.js index 1714891cb5..9903c631b2 100644 --- a/src/components/views/elements/ActionButton.js +++ b/src/components/views/elements/ActionButton.js @@ -70,8 +70,8 @@ export default class ActionButton extends React.Component { } const icon = this.props.iconPath ? - () : - undefined; + () : + undefined; const classNames = ["mx_RoleButton"]; if (this.props.className) { diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js index e206fda797..b898ad2ebc 100644 --- a/src/components/views/elements/AppTile.js +++ b/src/components/views/elements/AppTile.js @@ -109,7 +109,7 @@ export default class AppTile extends React.Component { const childContentProtocol = u.protocol; if (parentContentProtocol === 'https:' && childContentProtocol !== 'https:') { console.warn("Refusing to load mixed-content app:", - parentContentProtocol, childContentProtocol, window.location, this.props.app.url); + parentContentProtocol, childContentProtocol, window.location, this.props.app.url); return true; } return false; diff --git a/src/components/views/elements/EditableItemList.js b/src/components/views/elements/EditableItemList.js index ff62f169fa..d8ec5af278 100644 --- a/src/components/views/elements/EditableItemList.js +++ b/src/components/views/elements/EditableItemList.js @@ -65,12 +65,18 @@ export class EditableItem extends React.Component { {_t("Are you sure?")} - + {_t("Yes")} - + {_t("No")}
    @@ -121,11 +127,15 @@ export default class EditableItemList extends React.Component { _renderNewItemField() { return ( - + + autoComplete="off" value={this.props.newItem || ""} onChange={this._onNewItemChanged} + list={this.props.suggestionsListId} /> {_t("Add")} diff --git a/src/components/views/elements/EditableText.js b/src/components/views/elements/EditableText.js index 638fd02553..7c38ac1777 100644 --- a/src/components/views/elements/EditableText.js +++ b/src/components/views/elements/EditableText.js @@ -221,13 +221,15 @@ export default class EditableText extends React.Component { ; } else { // show the content editable div, but manually manage its contents as react and contentEditable don't play nice together - editableEl =
    ; + editableEl =
    ; } return editableEl; diff --git a/src/components/views/elements/ImageView.tsx b/src/components/views/elements/ImageView.tsx index cbced07bfe..05d487a9eb 100644 --- a/src/components/views/elements/ImageView.tsx +++ b/src/components/views/elements/ImageView.tsx @@ -34,16 +34,15 @@ import {RoomPermalinkCreator} from "../../../utils/permalinks/Permalinks" import {MatrixEvent} from "matrix-js-sdk/src/models/event"; import {normalizeWheelEvent} from "../../../utils/Mouse"; -const MIN_ZOOM = 100; -const MAX_ZOOM = 300; +// Max scale to keep gaps around the image +const MAX_SCALE = 0.95; // This is used for the buttons -const ZOOM_STEP = 10; +const ZOOM_STEP = 0.10; // This is used for mouse wheel events -const ZOOM_COEFFICIENT = 0.5; +const ZOOM_COEFFICIENT = 0.0025; // If we have moved only this much we can zoom const ZOOM_DISTANCE = 10; - interface IProps { src: string, // the source of the image being displayed name?: string, // the main title ('name') for the image @@ -62,8 +61,10 @@ interface IProps { } interface IState { - rotation: number, zoom: number, + minZoom: number, + maxZoom: number, + rotation: number, translationX: number, translationY: number, moving: boolean, @@ -75,8 +76,10 @@ export default class ImageView extends React.Component { constructor(props) { super(props); this.state = { + zoom: 0, + minZoom: MAX_SCALE, + maxZoom: MAX_SCALE, rotation: 0, - zoom: MIN_ZOOM, translationX: 0, translationY: 0, moving: false, @@ -87,6 +90,8 @@ export default class ImageView extends React.Component { // XXX: Refs to functional components private contextMenuButton = createRef(); private focusLock = createRef(); + private imageWrapper = createRef(); + private image = createRef(); private initX = 0; private initY = 0; @@ -99,12 +104,89 @@ export default class ImageView extends React.Component { // We have to use addEventListener() because the listener // needs to be passive in order to work with Chromium this.focusLock.current.addEventListener('wheel', this.onWheel, { passive: false }); + // We want to recalculate zoom whenever the window's size changes + window.addEventListener("resize", this.calculateZoom); + // After the image loads for the first time we want to calculate the zoom + this.image.current.addEventListener("load", this.calculateZoom); + // Try to precalculate the zoom from width and height props + this.calculateZoom(); } componentWillUnmount() { this.focusLock.current.removeEventListener('wheel', this.onWheel); + window.removeEventListener("resize", this.calculateZoom); + this.image.current.removeEventListener("load", this.calculateZoom); } + private calculateZoom = () => { + const image = this.image.current; + const imageWrapper = this.imageWrapper.current; + + const width = this.props.width || image.naturalWidth; + const height = this.props.height || image.naturalHeight; + + const zoomX = imageWrapper.clientWidth / width; + const zoomY = imageWrapper.clientHeight / height; + + // If the image is smaller in both dimensions set its the zoom to 1 to + // display it in its original size + if (zoomX >= 1 && zoomY >= 1) { + this.setState({ + zoom: 1, + minZoom: 1, + maxZoom: 1, + }); + return; + } + // We set minZoom to the min of the zoomX and zoomY to avoid overflow in + // any direction. We also multiply by MAX_SCALE to get a gap around the + // image by default + const minZoom = Math.min(zoomX, zoomY) * MAX_SCALE; + + if (this.state.zoom <= this.state.minZoom) this.setState({zoom: minZoom}); + this.setState({ + minZoom: minZoom, + maxZoom: 1, + }); + } + + private zoom(delta: number) { + const newZoom = this.state.zoom + delta; + + if (newZoom <= this.state.minZoom) { + this.setState({ + zoom: this.state.minZoom, + translationX: 0, + translationY: 0, + }); + return; + } + if (newZoom >= this.state.maxZoom) { + this.setState({zoom: this.state.maxZoom}); + return; + } + + this.setState({ + zoom: newZoom, + }); + } + + private onWheel = (ev: WheelEvent) => { + ev.stopPropagation(); + ev.preventDefault(); + + const {deltaY} = normalizeWheelEvent(ev); + this.zoom(-(deltaY * ZOOM_COEFFICIENT)); + }; + + private onZoomInClick = () => { + this.zoom(ZOOM_STEP); + }; + + private onZoomOutClick = () => { + this.zoom(-ZOOM_STEP); + }; + private onKeyDown = (ev: KeyboardEvent) => { if (ev.key === Key.ESCAPE) { ev.stopPropagation(); @@ -113,31 +195,6 @@ export default class ImageView extends React.Component { } }; - private onWheel = (ev: WheelEvent) => { - ev.stopPropagation(); - ev.preventDefault(); - - const {deltaY} = normalizeWheelEvent(ev); - const newZoom = this.state.zoom - (deltaY * ZOOM_COEFFICIENT); - - if (newZoom <= MIN_ZOOM) { - this.setState({ - zoom: MIN_ZOOM, - translationX: 0, - translationY: 0, - }); - return; - } - if (newZoom >= MAX_ZOOM) { - this.setState({zoom: MAX_ZOOM}); - return; - } - - this.setState({ - zoom: newZoom, - }); - }; - private onRotateCounterClockwiseClick = () => { const cur = this.state.rotation; const rotationDegrees = cur - 90; @@ -150,31 +207,6 @@ export default class ImageView extends React.Component { this.setState({ rotation: rotationDegrees }); }; - private onZoomInClick = () => { - if (this.state.zoom >= MAX_ZOOM) { - this.setState({zoom: MAX_ZOOM}); - return; - } - - this.setState({ - zoom: this.state.zoom + ZOOM_STEP, - }); - }; - - private onZoomOutClick = () => { - if (this.state.zoom <= MIN_ZOOM) { - this.setState({ - zoom: MIN_ZOOM, - translationX: 0, - translationY: 0, - }); - return; - } - this.setState({ - zoom: this.state.zoom - ZOOM_STEP, - }); - }; - private onDownloadClick = () => { const a = document.createElement("a"); a.href = this.props.src; @@ -217,8 +249,8 @@ export default class ImageView extends React.Component { if (ev.button !== 0) return; // Zoom in if we are completely zoomed out - if (this.state.zoom === MIN_ZOOM) { - this.setState({zoom: MAX_ZOOM}); + if (this.state.zoom === this.state.minZoom) { + this.setState({zoom: this.state.maxZoom}); return; } @@ -251,7 +283,7 @@ export default class ImageView extends React.Component { Math.abs(this.state.translationY - this.previousY) < ZOOM_DISTANCE ) { this.setState({ - zoom: MIN_ZOOM, + zoom: this.state.minZoom, translationX: 0, translationY: 0, }); @@ -286,17 +318,20 @@ export default class ImageView extends React.Component { render() { const showEventMeta = !!this.props.mxEvent; + const zoomingDisabled = this.state.maxZoom === this.state.minZoom; let cursor; if (this.state.moving) { cursor= "grabbing"; - } else if (this.state.zoom === MIN_ZOOM) { + } else if (zoomingDisabled) { + cursor = "default"; + } else if (this.state.zoom === this.state.minZoom) { cursor = "zoom-in"; } else { cursor = "zoom-out"; } const rotationDegrees = this.state.rotation + "deg"; - const zoomPercentage = this.state.zoom/100; + const zoom = this.state.zoom; const translatePixelsX = this.state.translationX + "px"; const translatePixelsY = this.state.translationY + "px"; // The order of the values is important! @@ -308,7 +343,7 @@ export default class ImageView extends React.Component { transition: this.state.moving ? null : "transform 200ms ease 0s", transform: `translateX(${translatePixelsX}) translateY(${translatePixelsY}) - scale(${zoomPercentage}) + scale(${zoom}) rotate(${rotationDegrees})`, }; @@ -380,6 +415,25 @@ export default class ImageView extends React.Component { ); } + let zoomOutButton; + let zoomInButton; + if (!zoomingDisabled) { + zoomOutButton = ( + + + ); + zoomInButton = ( + + + ); + } + return ( { title={_t("Rotate Left")} onClick={ this.onRotateCounterClockwiseClick }> - - - - + {zoomOutButton} + {zoomInButton} { {this.renderContextMenu()}
    -
    +
    {this.props.label}; - let secondPart = ; + let secondPart = ; if (this.props.toggleInFront) { const temp = firstPart; diff --git a/src/components/views/elements/LanguageDropdown.js b/src/components/views/elements/LanguageDropdown.js index 2e961be700..b8734c5afb 100644 --- a/src/components/views/elements/LanguageDropdown.js +++ b/src/components/views/elements/LanguageDropdown.js @@ -60,10 +60,10 @@ export default class LanguageDropdown extends React.Component { // doesn't know this, therefore we do this. const language = SettingsStore.getValue("language", null, /*excludeDefault:*/true); if (language) { - this.props.onOptionChange(language); + this.props.onOptionChange(language); } else { - const language = languageHandler.normalizeLanguageKey(languageHandler.getLanguageFromBrowser()); - this.props.onOptionChange(language); + const language = languageHandler.normalizeLanguageKey(languageHandler.getLanguageFromBrowser()); + this.props.onOptionChange(language); } } } diff --git a/src/components/views/elements/Pill.js b/src/components/views/elements/Pill.js index a8e16813e6..ace41db39d 100644 --- a/src/components/views/elements/Pill.js +++ b/src/components/views/elements/Pill.js @@ -225,19 +225,19 @@ class Pill extends React.Component { } break; case Pill.TYPE_USER_MENTION: { - // If this user is not a member of this room, default to the empty member - const member = this.state.member; - if (member) { - userId = member.userId; - member.rawDisplayName = member.rawDisplayName || ''; - linkText = member.rawDisplayName; - if (this.props.shouldShowPillAvatar) { - avatar =
    -
    {_t("Level")}{_t("Settable at global")}{_t("Settable at room")}
    {_t("Level")}{_t("Settable at global")}{_t("Settable at room")}
    {_t("Homeserver feature support:")} {homeserverSupportsCrossSigning ? _t("exists") : _t("not found")}
    + {errorSection} {actionRow} diff --git a/src/components/views/settings/DevicesPanel.js b/src/components/views/settings/DevicesPanel.js index e7d300b0f8..b1ad605a37 100644 --- a/src/components/views/settings/DevicesPanel.js +++ b/src/components/views/settings/DevicesPanel.js @@ -214,7 +214,7 @@ export default class DevicesPanel extends React.Component { const deleteButton = this.state.deleting ? : - { _t("Delete %(count)s sessions", {count: this.state.selectedDevices.length}) } + { _t("Delete %(count)s sessions", {count: this.state.selectedDevices.length})} ; const classes = classNames(this.props.className, "mx_DevicesPanel"); diff --git a/src/components/views/settings/E2eAdvancedPanel.js b/src/components/views/settings/E2eAdvancedPanel.tsx similarity index 100% rename from src/components/views/settings/E2eAdvancedPanel.js rename to src/components/views/settings/E2eAdvancedPanel.tsx diff --git a/src/components/views/settings/EventIndexPanel.js b/src/components/views/settings/EventIndexPanel.tsx similarity index 65% rename from src/components/views/settings/EventIndexPanel.js rename to src/components/views/settings/EventIndexPanel.tsx index d1a02de16d..fa84063ee8 100644 --- a/src/components/views/settings/EventIndexPanel.js +++ b/src/components/views/settings/EventIndexPanel.tsx @@ -1,5 +1,5 @@ /* -Copyright 2020 The Matrix.org Foundation C.I.C. +Copyright 2020-2021 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -28,10 +28,17 @@ import {SettingLevel} from "../../../settings/SettingLevel"; import {replaceableComponent} from "../../../utils/replaceableComponent"; import SeshatResetDialog from '../dialogs/SeshatResetDialog'; +interface IState { + enabling: boolean; + eventIndexSize: number; + roomCount: number; + eventIndexingEnabled: boolean; +} + @replaceableComponent("views.settings.EventIndexPanel") -export default class EventIndexPanel extends React.Component { - constructor() { - super(); +export default class EventIndexPanel extends React.Component<{}, IState> { + constructor(props) { + super(props); this.state = { enabling: false, @@ -68,7 +75,7 @@ export default class EventIndexPanel extends React.Component { } } - async componentDidMount(): void { + componentDidMount(): void { this.updateState(); } @@ -102,8 +109,10 @@ export default class EventIndexPanel extends React.Component { }); } - _onManage = async () => { + private onManage = async () => { Modal.createTrackedDialogAsync('Message search', 'Message search', + // @ts-ignore: TS doesn't seem to like the type of this now that it + // has also been converted to TS as well, but I can't figure out why... import('../../../async-components/views/dialogs/eventindex/ManageEventIndexDialog'), { onFinished: () => {}, @@ -111,7 +120,7 @@ export default class EventIndexPanel extends React.Component { ); } - _onEnable = async () => { + private onEnable = async () => { this.setState({ enabling: true, }); @@ -123,14 +132,13 @@ export default class EventIndexPanel extends React.Component { await this.updateState(); } - _confirmEventStoreReset = () => { - const self = this; + private confirmEventStoreReset = () => { const { close } = Modal.createDialog(SeshatResetDialog, { onFinished: async (success) => { if (success) { await SettingsStore.setValue('enableEventIndexing', null, SettingLevel.DEVICE, false); await EventIndexPeg.deleteEventIndex(); - await self._onEnable(); + await this.onEnable(); close(); } }, @@ -145,20 +153,19 @@ export default class EventIndexPanel extends React.Component { if (EventIndexPeg.get() !== null) { eventIndexingSettings = (
    -
    - {_t("Securely cache encrypted messages locally for them " + - "to appear in search results, using %(size)s to store messages from %(rooms)s rooms.", - { - size: formatBytes(this.state.eventIndexSize, 0), - // This drives the singular / plural string - // selection for "room" / "rooms" only. - count: this.state.roomCount, - rooms: formatCountLong(this.state.roomCount), - }, - )} -
    +
    {_t( + "Securely cache encrypted messages locally for them " + + "to appear in search results, using %(size)s to store messages from %(rooms)s rooms.", + { + size: formatBytes(this.state.eventIndexSize, 0), + // This drives the singular / plural string + // selection for "room" / "rooms" only. + count: this.state.roomCount, + rooms: formatCountLong(this.state.roomCount), + }, + )}
    - + {_t("Manage")}
    @@ -167,13 +174,13 @@ export default class EventIndexPanel extends React.Component { } else if (!this.state.eventIndexingEnabled && EventIndexPeg.supportIsInstalled()) { eventIndexingSettings = (
    -
    - {_t( "Securely cache encrypted messages locally for them to " + - "appear in search results.")} -
    +
    {_t( + "Securely cache encrypted messages locally for them to " + + "appear in search results.", + )}
    + onClick={this.onEnable}> {_t("Enable")} {this.state.enabling ? :
    } @@ -188,40 +195,36 @@ export default class EventIndexPanel extends React.Component { ); eventIndexingSettings = ( -
    +
    {_t( + "%(brand)s is missing some components required for securely " + + "caching encrypted messages locally. If you'd like to " + + "experiment with this feature, build a custom %(brand)s Desktop " + + "with search components added.", { - _t( "%(brand)s is missing some components required for securely " + - "caching encrypted messages locally. If you'd like to " + - "experiment with this feature, build a custom %(brand)s Desktop " + - "with search components added.", - { - brand, - }, - { - 'nativeLink': (sub) => {sub}, - }, - ) - } -
    + brand, + }, + { + nativeLink: sub => {sub}, + }, + )}
    ); } else if (!EventIndexPeg.platformHasSupport()) { eventIndexingSettings = ( -
    +
    {_t( + "%(brand)s can't securely cache encrypted messages locally " + + "while running in a web browser. Use %(brand)s Desktop " + + "for encrypted messages to appear in search results.", { - _t( "%(brand)s can't securely cache encrypted messages locally " + - "while running in a web browser. Use %(brand)s Desktop " + - "for encrypted messages to appear in search results.", - { - brand, - }, - { - 'desktopLink': (sub) => {sub}, - }, - ) - } -
    + brand, + }, + { + desktopLink: sub => {sub}, + }, + )}
    ); } else { eventIndexingSettings = ( @@ -233,19 +236,18 @@ export default class EventIndexPanel extends React.Component { }

    {EventIndexPeg.error && ( -
    - {_t("Advanced")} - - {EventIndexPeg.error.message} - -

    - - {_t("Reset")} - -

    -
    +
    + {_t("Advanced")} + + {EventIndexPeg.error.message} + +

    + + {_t("Reset")} + +

    +
    )} -
    ); } diff --git a/src/components/views/settings/Notifications.js b/src/components/views/settings/Notifications.js index 25fe434994..5756536085 100644 --- a/src/components/views/settings/Notifications.js +++ b/src/components/views/settings/Notifications.js @@ -100,7 +100,7 @@ export default class Notifications extends React.Component { MatrixClientPeg.get().setPushRuleEnabled( 'global', self.state.masterPushRule.kind, self.state.masterPushRule.rule_id, !checked, ).then(function() { - self._refreshFromServer(); + self._refreshFromServer(); }); }; @@ -580,12 +580,12 @@ export default class Notifications extends React.Component { "vectorRuleId": "_keywords", "description": ( - { _t('Messages containing keywords', - {}, - { 'span': (sub) => - {sub}, - }, - )} + { _t('Messages containing keywords', + {}, + { 'span': (sub) => + {sub}, + }, + )} ), "vectorState": self.state.vectorContentRules.vectorState, @@ -743,8 +743,8 @@ export default class Notifications extends React.Component { emailNotificationsRow(address, label) { return ; + onChange={this.onEnableEmailNotificationsChange.bind(this, address)} + label={label} key={`emailNotif_${label}`} />; } render() { @@ -757,8 +757,8 @@ export default class Notifications extends React.Component { let masterPushRuleDiv; if (this.state.masterPushRule) { masterPushRuleDiv = ; + onChange={this.onEnableNotificationsChange} + label={_t('Enable notifications for this account')} />; } let clearNotificationsButton; @@ -874,16 +874,16 @@ export default class Notifications extends React.Component { { spinner } + onChange={this.onEnableDesktopNotificationsChange} + label={_t('Enable desktop notifications for this session')} /> + onChange={this.onEnableDesktopNotificationBodyChange} + label={_t('Show message in desktop notification')} /> + onChange={this.onEnableAudioNotificationsChange} + label={_t('Enable audible notifications for this session')} /> { emailNotificationsRows } diff --git a/src/components/views/settings/ProfileSettings.js b/src/components/views/settings/ProfileSettings.js index 971b868751..9ecf369eba 100644 --- a/src/components/views/settings/ProfileSettings.js +++ b/src/components/views/settings/ProfileSettings.js @@ -170,8 +170,12 @@ export default class ProfileSettings extends React.Component { noValidate={true} className="mx_ProfileSettings_profileForm" > - +
    {_t("Profile")} diff --git a/src/components/views/settings/SetIdServer.js b/src/components/views/settings/SetIdServer.tsx similarity index 89% rename from src/components/views/settings/SetIdServer.js rename to src/components/views/settings/SetIdServer.tsx index fa2a36476d..05d1f83387 100644 --- a/src/components/views/settings/SetIdServer.js +++ b/src/components/views/settings/SetIdServer.tsx @@ -1,5 +1,5 @@ /* -Copyright 2019 The Matrix.org Foundation C.I.C. +Copyright 2019-2021 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -16,7 +16,6 @@ limitations under the License. import url from 'url'; import React from 'react'; -import PropTypes from 'prop-types'; import {_t} from "../../../languageHandler"; import * as sdk from '../../../index'; import {MatrixClientPeg} from "../../../MatrixClientPeg"; @@ -28,6 +27,7 @@ import {abbreviateUrl, unabbreviateUrl} from "../../../utils/UrlUtils"; import { getDefaultIdentityServerUrl, doesIdentityServerHaveTerms } from '../../../utils/IdentityServerUtils'; import {timeout} from "../../../utils/promise"; import {replaceableComponent} from "../../../utils/replaceableComponent"; +import { ActionPayload } from '../../../dispatcher/payloads'; // We'll wait up to this long when checking for 3PID bindings on the IS. const REACHABILITY_TIMEOUT = 10000; // ms @@ -59,16 +59,28 @@ async function checkIdentityServerUrl(u) { } } -@replaceableComponent("views.settings.SetIdServer") -export default class SetIdServer extends React.Component { - static propTypes = { - // Whether or not the ID server is missing terms. This affects the text - // shown to the user. - missingTerms: PropTypes.bool, - }; +interface IProps { + // Whether or not the ID server is missing terms. This affects the text + // shown to the user. + missingTerms: boolean; +} - constructor() { - super(); +interface IState { + defaultIdServer?: string; + currentClientIdServer: string; + idServer?: string; + error?: string; + busy: boolean; + disconnectBusy: boolean; + checking: boolean; +} + +@replaceableComponent("views.settings.SetIdServer") +export default class SetIdServer extends React.Component { + private dispatcherRef: string; + + constructor(props) { + super(props); let defaultIdServer = ''; if (!MatrixClientPeg.get().getIdentityServerUrl() && getDefaultIdentityServerUrl()) { @@ -96,7 +108,7 @@ export default class SetIdServer extends React.Component { dis.unregister(this.dispatcherRef); } - onAction = (payload) => { + private onAction = (payload: ActionPayload) => { // We react to changes in the ID server in the event the user is staring at this form // when changing their identity server on another device. if (payload.action !== "id_server_changed") return; @@ -106,13 +118,13 @@ export default class SetIdServer extends React.Component { }); }; - _onIdentityServerChanged = (ev) => { + private onIdentityServerChanged = (ev) => { const u = ev.target.value; this.setState({idServer: u}); }; - _getTooltip = () => { + private getTooltip = () => { if (this.state.checking) { const InlineSpinner = sdk.getComponent('views.elements.InlineSpinner'); return
    @@ -126,11 +138,11 @@ export default class SetIdServer extends React.Component { } }; - _idServerChangeEnabled = () => { + private idServerChangeEnabled = () => { return !!this.state.idServer && !this.state.busy; }; - _saveIdServer = (fullUrl) => { + private saveIdServer = (fullUrl) => { // Account data change will update localstorage, client, etc through dispatcher MatrixClientPeg.get().setAccountData("m.identity_server", { base_url: fullUrl, @@ -143,7 +155,7 @@ export default class SetIdServer extends React.Component { }); }; - _checkIdServer = async (e) => { + private checkIdServer = async (e) => { e.preventDefault(); const { idServer, currentClientIdServer } = this.state; @@ -166,14 +178,14 @@ export default class SetIdServer extends React.Component { // Double check that the identity server even has terms of service. const hasTerms = await doesIdentityServerHaveTerms(fullUrl); if (!hasTerms) { - const [confirmed] = await this._showNoTermsWarning(fullUrl); + const [confirmed] = await this.showNoTermsWarning(fullUrl); save = confirmed; } // Show a general warning, possibly with details about any bound // 3PIDs that would be left behind. if (save && currentClientIdServer && fullUrl !== currentClientIdServer) { - const [confirmed] = await this._showServerChangeWarning({ + const [confirmed] = await this.showServerChangeWarning({ title: _t("Change identity server"), unboundMessage: _t( "Disconnect from the identity server and " + @@ -189,7 +201,7 @@ export default class SetIdServer extends React.Component { } if (save) { - this._saveIdServer(fullUrl); + this.saveIdServer(fullUrl); } } catch (e) { console.error(e); @@ -204,7 +216,7 @@ export default class SetIdServer extends React.Component { }); }; - _showNoTermsWarning(fullUrl) { + private showNoTermsWarning(fullUrl) { const QuestionDialog = sdk.getComponent("views.dialogs.QuestionDialog"); const { finished } = Modal.createTrackedDialog('No Terms Warning', '', QuestionDialog, { title: _t("Identity server has no terms of service"), @@ -223,10 +235,10 @@ export default class SetIdServer extends React.Component { return finished; } - _onDisconnectClicked = async () => { + private onDisconnectClicked = async () => { this.setState({disconnectBusy: true}); try { - const [confirmed] = await this._showServerChangeWarning({ + const [confirmed] = await this.showServerChangeWarning({ title: _t("Disconnect identity server"), unboundMessage: _t( "Disconnect from the identity server ?", {}, @@ -235,14 +247,14 @@ export default class SetIdServer extends React.Component { button: _t("Disconnect"), }); if (confirmed) { - this._disconnectIdServer(); + this.disconnectIdServer(); } } finally { this.setState({disconnectBusy: false}); } }; - async _showServerChangeWarning({ title, unboundMessage, button }) { + private async showServerChangeWarning({ title, unboundMessage, button }) { const { currentClientIdServer } = this.state; let threepids = []; @@ -318,7 +330,7 @@ export default class SetIdServer extends React.Component { return finished; } - _disconnectIdServer = () => { + private disconnectIdServer = () => { // Account data change will update localstorage, client, etc through dispatcher MatrixClientPeg.get().setAccountData("m.identity_server", { base_url: null, // clear @@ -371,7 +383,7 @@ export default class SetIdServer extends React.Component { let discoSection; if (idServerUrl) { - let discoButtonContent = _t("Disconnect"); + let discoButtonContent: React.ReactNode = _t("Disconnect"); let discoBodyText = _t( "Disconnecting from your identity server will mean you " + "won't be discoverable by other users and you won't be " + @@ -391,14 +403,14 @@ export default class SetIdServer extends React.Component { } discoSection =
    {discoBodyText} - + {discoButtonContent}
    ; } return ( - + {sectionTitle} @@ -411,15 +423,15 @@ export default class SetIdServer extends React.Component { autoComplete="off" placeholder={this.state.defaultIdServer} value={this.state.idServer} - onChange={this._onIdentityServerChanged} - tooltipContent={this._getTooltip()} + onChange={this.onIdentityServerChanged} + tooltipContent={this.getTooltip()} tooltipClassName="mx_SetIdServer_tooltip" disabled={this.state.busy} forceValidity={this.state.error ? false : null} /> {_t("Change")} {discoSection} diff --git a/src/components/views/settings/account/EmailAddresses.js b/src/components/views/settings/account/EmailAddresses.js index 1ebd374173..a36369cf88 100644 --- a/src/components/views/settings/account/EmailAddresses.js +++ b/src/components/views/settings/account/EmailAddresses.js @@ -90,12 +90,18 @@ export class ExistingEmailAddress extends React.Component { {_t("Remove %(email)s?", {email: this.props.email.address} )} - + {_t("Remove")} - + {_t("Cancel")}
    @@ -228,21 +234,28 @@ export default class EmailAddresses extends React.Component { ); if (this.state.verifying) { addButton = ( -
    -
    {_t("We've sent you an email to verify your address. Please follow the instructions there and then click the button below.")}
    - - {_t("Continue")} - -
    +
    +
    {_t("We've sent you an email to verify your address. Please follow the instructions there and then click the button below.")}
    + + {_t("Continue")} + +
    ); } return (
    {existingEmailElements} -
    + {_t("Remove %(phone)s?", {phone: this.props.msisdn.address})} - + {_t("Remove")} - + {_t("Cancel")}
    @@ -246,8 +252,11 @@ export default class PhoneNumbers extends React.Component { value={this.state.newPhoneNumberCode} onChange={this._onChangeNewPhoneNumberCode} /> - + {_t("Continue")} diff --git a/src/components/views/settings/tabs/room/GeneralRoomSettingsTab.js b/src/components/views/settings/tabs/room/GeneralRoomSettingsTab.js index cd4a043622..139cfd5fbd 100644 --- a/src/components/views/settings/tabs/room/GeneralRoomSettingsTab.js +++ b/src/components/views/settings/tabs/room/GeneralRoomSettingsTab.js @@ -80,9 +80,11 @@ export default class GeneralRoomSettingsTab extends React.Component { flairSection = <> {_t("Flair")}
    - +
    ; } @@ -97,8 +99,8 @@ export default class GeneralRoomSettingsTab extends React.Component {
    {_t("Room Addresses")}
    + canSetCanonicalAlias={canSetCanonical} canSetAliases={canSetAliases} + canonicalAliasEvent={canonicalAliasEv} aliasEvents={aliasEvents} />
    {_t("Other")}
    { flairSection } diff --git a/src/components/views/settings/tabs/room/NotificationSettingsTab.js b/src/components/views/settings/tabs/room/NotificationSettingsTab.js index baefb5ae20..fa56fa2cb6 100644 --- a/src/components/views/settings/tabs/room/NotificationSettingsTab.js +++ b/src/components/views/settings/tabs/room/NotificationSettingsTab.js @@ -155,7 +155,7 @@ export default class NotificationsSettingsTab extends React.Component {
    {_t("Notification sound")}: {this.state.currentSound}
    - {_t("Reset")} + {_t("Reset")}
    @@ -167,11 +167,11 @@ export default class NotificationsSettingsTab extends React.Component { {currentUploadedFile} - {_t("Browse")} + {_t("Browse")} - {_t("Save")} + {_t("Save")}
    diff --git a/src/components/views/settings/tabs/room/RolesRoomSettingsTab.js b/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx similarity index 89% rename from src/components/views/settings/tabs/room/RolesRoomSettingsTab.js rename to src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx index 59a175906d..4fa521f598 100644 --- a/src/components/views/settings/tabs/room/RolesRoomSettingsTab.js +++ b/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx @@ -1,5 +1,5 @@ /* -Copyright 2019, 2021 The Matrix.org Foundation C.I.C. +Copyright 2019-2021 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -15,7 +15,6 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; import {_t, _td} from "../../../../../languageHandler"; import {MatrixClientPeg} from "../../../../../MatrixClientPeg"; import * as sdk from "../../../../.."; @@ -23,6 +22,9 @@ import AccessibleButton from "../../../elements/AccessibleButton"; import Modal from "../../../../../Modal"; import {replaceableComponent} from "../../../../../utils/replaceableComponent"; import {EventType} from "matrix-js-sdk/src/@types/event"; +import { RoomMember } from "matrix-js-sdk/src/models/room-member"; +import { MatrixEvent } from "matrix-js-sdk/src/models/event"; +import { RoomState } from "matrix-js-sdk/src/models/room-state"; const plEventsToLabels = { // These will be translated for us later. @@ -63,15 +65,15 @@ function parseIntWithDefault(val, def) { return isNaN(res) ? def : res; } -export class BannedUser extends React.Component { - static propTypes = { - canUnban: PropTypes.bool, - member: PropTypes.object.isRequired, // js-sdk RoomMember - by: PropTypes.string.isRequired, - reason: PropTypes.string, - }; +interface IBannedUserProps { + canUnban?: boolean; + member: RoomMember; + by: string; + reason?: string; +} - _onUnbanClick = (e) => { +export class BannedUser extends React.Component { + private onUnbanClick = (e) => { MatrixClientPeg.get().unban(this.props.member.roomId, this.props.member.userId).catch((err) => { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); console.error("Failed to unban: " + err); @@ -87,8 +89,10 @@ export class BannedUser extends React.Component { if (this.props.canUnban) { unbanButton = ( - + { _t('Unban') } ); @@ -107,29 +111,29 @@ export class BannedUser extends React.Component { } } -@replaceableComponent("views.settings.tabs.room.RolesRoomSettingsTab") -export default class RolesRoomSettingsTab extends React.Component { - static propTypes = { - roomId: PropTypes.string.isRequired, - }; +interface IProps { + roomId: string; +} - componentDidMount(): void { - MatrixClientPeg.get().on("RoomState.members", this._onRoomMembership); +@replaceableComponent("views.settings.tabs.room.RolesRoomSettingsTab") +export default class RolesRoomSettingsTab extends React.Component { + componentDidMount() { + MatrixClientPeg.get().on("RoomState.members", this.onRoomMembership); } - componentWillUnmount(): void { + componentWillUnmount() { const client = MatrixClientPeg.get(); if (client) { - client.removeListener("RoomState.members", this._onRoomMembership); + client.removeListener("RoomState.members", this.onRoomMembership); } } - _onRoomMembership = (event, state, member) => { + private onRoomMembership = (event: MatrixEvent, state: RoomState, member: RoomMember) => { if (state.roomId !== this.props.roomId) return; this.forceUpdate(); }; - _populateDefaultPlEvents(eventsSection, stateLevel, eventsLevel) { + private populateDefaultPlEvents(eventsSection: Record, stateLevel: number, eventsLevel: number) { for (const desiredEvent of Object.keys(plEventsToShow)) { if (!(desiredEvent in eventsSection)) { eventsSection[desiredEvent] = (plEventsToShow[desiredEvent].isState ? stateLevel : eventsLevel); @@ -137,7 +141,7 @@ export default class RolesRoomSettingsTab extends React.Component { } } - _onPowerLevelsChanged = (value, powerLevelKey) => { + private onPowerLevelsChanged = (inputValue: string, powerLevelKey: string) => { const client = MatrixClientPeg.get(); const room = client.getRoom(this.props.roomId); const plEvent = room.currentState.getStateEvents('m.room.power_levels', ''); @@ -148,7 +152,7 @@ export default class RolesRoomSettingsTab extends React.Component { const eventsLevelPrefix = "event_levels_"; - value = parseInt(value); + const value = parseInt(inputValue); if (powerLevelKey.startsWith(eventsLevelPrefix)) { // deep copy "events" object, Object.assign itself won't deep copy @@ -182,7 +186,7 @@ export default class RolesRoomSettingsTab extends React.Component { }); }; - _onUserPowerLevelChanged = (value, powerLevelKey) => { + private onUserPowerLevelChanged = (value: string, powerLevelKey: string) => { const client = MatrixClientPeg.get(); const room = client.getRoom(this.props.roomId); const plEvent = room.currentState.getStateEvents('m.room.power_levels', ''); @@ -266,7 +270,7 @@ export default class RolesRoomSettingsTab extends React.Component { currentUserLevel = defaultUserLevel; } - this._populateDefaultPlEvents( + this.populateDefaultPlEvents( eventsLevels, parseIntWithDefault(plContent.state_default, powerLevelDescriptors.state_default.defaultValue), parseIntWithDefault(plContent.events_default, powerLevelDescriptors.events_default.defaultValue), @@ -288,7 +292,7 @@ export default class RolesRoomSettingsTab extends React.Component { label={user} key={user} powerLevelKey={user} // Will be sent as the second parameter to `onChange` - onChange={this._onUserPowerLevelChanged} + onChange={this.onUserPowerLevelChanged} />, ); } else if (userLevels[user] < defaultUserLevel) { // muted @@ -299,7 +303,7 @@ export default class RolesRoomSettingsTab extends React.Component { label={user} key={user} powerLevelKey={user} // Will be sent as the second parameter to `onChange` - onChange={this._onUserPowerLevelChanged} + onChange={this.onUserPowerLevelChanged} />, ); } @@ -345,8 +349,9 @@ export default class RolesRoomSettingsTab extends React.Component { if (sender) bannedBy = sender.name; return ( + member={member} reason={banEvent.reason} + by={bannedBy} + /> ); })} @@ -373,7 +378,7 @@ export default class RolesRoomSettingsTab extends React.Component { usersDefault={defaultUserLevel} disabled={!canChangeLevels || currentUserLevel < value} powerLevelKey={key} // Will be sent as the second parameter to `onChange` - onChange={this._onPowerLevelsChanged} + onChange={this.onPowerLevelsChanged} />
    ; }); @@ -398,7 +403,7 @@ export default class RolesRoomSettingsTab extends React.Component { usersDefault={defaultUserLevel} disabled={!canChangeLevels || currentUserLevel < eventsLevels[eventType]} powerLevelKey={"event_levels_" + eventType} - onChange={this._onPowerLevelsChanged} + onChange={this.onPowerLevelsChanged} />
    ); diff --git a/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.js b/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx similarity index 79% rename from src/components/views/settings/tabs/room/SecurityRoomSettingsTab.js rename to src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx index ce883c6d23..02bbcfb751 100644 --- a/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.js +++ b/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx @@ -1,5 +1,5 @@ /* -Copyright 2019 New Vector Ltd +Copyright 2019-2021 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -15,7 +15,7 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; +import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import {_t} from "../../../../../languageHandler"; import {MatrixClientPeg} from "../../../../../MatrixClientPeg"; import * as sdk from "../../../../.."; @@ -26,64 +26,92 @@ import StyledRadioGroup from '../../../elements/StyledRadioGroup'; import {SettingLevel} from "../../../../../settings/SettingLevel"; import SettingsStore from "../../../../../settings/SettingsStore"; import {UIFeature} from "../../../../../settings/UIFeature"; -import {replaceableComponent} from "../../../../../utils/replaceableComponent"; +import { replaceableComponent } from "../../../../../utils/replaceableComponent"; + +// Knock and private are reserved keywords which are not yet implemented. +enum JoinRule { + Public = "public", + Knock = "knock", + Invite = "invite", + Private = "private", +} + +enum GuestAccess { + CanJoin = "can_join", + Forbidden = "forbidden", +} + +enum HistoryVisibility { + Invited = "invited", + Joined = "joined", + Shared = "shared", + WorldReadable = "world_readable", +} + +interface IProps { + roomId: string; +} + +interface IState { + joinRule: JoinRule; + guestAccess: GuestAccess; + history: HistoryVisibility; + hasAliases: boolean; + encrypted: boolean; +} @replaceableComponent("views.settings.tabs.room.SecurityRoomSettingsTab") -export default class SecurityRoomSettingsTab extends React.Component { - static propTypes = { - roomId: PropTypes.string.isRequired, - }; - - constructor() { - super(); +export default class SecurityRoomSettingsTab extends React.Component { + constructor(props) { + super(props); this.state = { - joinRule: "invite", - guestAccess: "can_join", - history: "shared", + joinRule: JoinRule.Invite, + guestAccess: GuestAccess.CanJoin, + history: HistoryVisibility.Shared, hasAliases: false, encrypted: false, }; } // TODO: [REACT-WARNING] Move this to constructor - async UNSAFE_componentWillMount(): void { // eslint-disable-line camelcase - MatrixClientPeg.get().on("RoomState.events", this._onStateEvent); + async UNSAFE_componentWillMount() { // eslint-disable-line camelcase + MatrixClientPeg.get().on("RoomState.events", this.onStateEvent); const room = MatrixClientPeg.get().getRoom(this.props.roomId); const state = room.currentState; - const joinRule = this._pullContentPropertyFromEvent( + const joinRule: JoinRule = this.pullContentPropertyFromEvent( state.getStateEvents("m.room.join_rules", ""), 'join_rule', - 'invite', + JoinRule.Invite, ); - const guestAccess = this._pullContentPropertyFromEvent( + const guestAccess: GuestAccess = this.pullContentPropertyFromEvent( state.getStateEvents("m.room.guest_access", ""), 'guest_access', - 'forbidden', + GuestAccess.Forbidden, ); - const history = this._pullContentPropertyFromEvent( + const history: HistoryVisibility = this.pullContentPropertyFromEvent( state.getStateEvents("m.room.history_visibility", ""), 'history_visibility', - 'shared', + HistoryVisibility.Shared, ); const encrypted = MatrixClientPeg.get().isRoomEncrypted(this.props.roomId); this.setState({joinRule, guestAccess, history, encrypted}); - const hasAliases = await this._hasAliases(); + const hasAliases = await this.hasAliases(); this.setState({hasAliases}); } - _pullContentPropertyFromEvent(event, key, defaultValue) { + private pullContentPropertyFromEvent(event: MatrixEvent, key: string, defaultValue: T): T { if (!event || !event.getContent()) return defaultValue; return event.getContent()[key] || defaultValue; } - componentWillUnmount(): void { - MatrixClientPeg.get().removeListener("RoomState.events", this._onStateEvent); + componentWillUnmount() { + MatrixClientPeg.get().removeListener("RoomState.events", this.onStateEvent); } - _onStateEvent = (e) => { + private onStateEvent = (e: MatrixEvent) => { const refreshWhenTypes = [ 'm.room.join_rules', 'm.room.guest_access', @@ -93,7 +121,7 @@ export default class SecurityRoomSettingsTab extends React.Component { if (refreshWhenTypes.includes(e.getType())) this.forceUpdate(); }; - _onEncryptionChange = (e) => { + private onEncryptionChange = (e: React.ChangeEvent) => { Modal.createTrackedDialog('Enable encryption', '', QuestionDialog, { title: _t('Enable encryption?'), description: _t( @@ -102,10 +130,9 @@ export default class SecurityRoomSettingsTab extends React.Component { "may prevent many bots and bridges from working correctly. Learn more about encryption.", {}, { - 'a': (sub) => { - return {sub}; - }, + a: sub => {sub}, }, ), onFinished: (confirm) => { @@ -127,12 +154,12 @@ export default class SecurityRoomSettingsTab extends React.Component { }); }; - _fixGuestAccess = (e) => { + private fixGuestAccess = (e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); - const joinRule = "invite"; - const guestAccess = "can_join"; + const joinRule = JoinRule.Invite; + const guestAccess = GuestAccess.CanJoin; const beforeJoinRule = this.state.joinRule; const beforeGuestAccess = this.state.guestAccess; @@ -149,7 +176,7 @@ export default class SecurityRoomSettingsTab extends React.Component { }); }; - _onRoomAccessRadioToggle = (roomAccess) => { + private onRoomAccessRadioToggle = (roomAccess: string) => { // join_rule // INVITE | PUBLIC // ----------------------+---------------- @@ -163,20 +190,20 @@ export default class SecurityRoomSettingsTab extends React.Component { // invite them, you clearly want them to join, whether they're a // guest or not. In practice, guest_access should probably have // been implemented as part of the join_rules enum. - let joinRule = "invite"; - let guestAccess = "can_join"; + let joinRule = JoinRule.Invite; + let guestAccess = GuestAccess.CanJoin; switch (roomAccess) { case "invite_only": // no change - use defaults above break; case "public_no_guests": - joinRule = "public"; - guestAccess = "forbidden"; + joinRule = JoinRule.Public; + guestAccess = GuestAccess.Forbidden; break; case "public_with_guests": - joinRule = "public"; - guestAccess = "can_join"; + joinRule = JoinRule.Public; + guestAccess = GuestAccess.CanJoin; break; } @@ -195,7 +222,7 @@ export default class SecurityRoomSettingsTab extends React.Component { }); }; - _onHistoryRadioToggle = (history) => { + private onHistoryRadioToggle = (history: HistoryVisibility) => { const beforeHistory = this.state.history; this.setState({history: history}); MatrixClientPeg.get().sendStateEvent(this.props.roomId, "m.room.history_visibility", { @@ -206,11 +233,11 @@ export default class SecurityRoomSettingsTab extends React.Component { }); }; - _updateBlacklistDevicesFlag = (checked) => { + private updateBlacklistDevicesFlag = (checked: boolean) => { MatrixClientPeg.get().getRoom(this.props.roomId).setBlacklistUnverifiedDevices(checked); }; - async _hasAliases() { + private async hasAliases(): Promise { const cli = MatrixClientPeg.get(); if (await cli.doesServerSupportUnstableFeature("org.matrix.msc2432")) { const response = await cli.unstableGetLocalAliases(this.props.roomId); @@ -224,7 +251,7 @@ export default class SecurityRoomSettingsTab extends React.Component { } } - _renderRoomAccess() { + private renderRoomAccess() { const client = MatrixClientPeg.get(); const room = client.getRoom(this.props.roomId); const joinRule = this.state.joinRule; @@ -240,7 +267,7 @@ export default class SecurityRoomSettingsTab extends React.Component { {_t("Guests cannot join this room even if explicitly invited.")}  - {_t("Click here to fix")} + {_t("Click here to fix")}
    ); @@ -265,7 +292,7 @@ export default class SecurityRoomSettingsTab extends React.Component { ; } @@ -356,7 +383,7 @@ export default class SecurityRoomSettingsTab extends React.Component { let historySection = (<> {_t("Who can read history?")}
    - {this._renderHistory()} + {this.renderHistory()}
    ); if (!SettingsStore.getValue(UIFeature.RoomHistorySettings)) { @@ -373,15 +400,16 @@ export default class SecurityRoomSettingsTab extends React.Component {
    {_t("Once enabled, encryption cannot be disabled.")}
    - +
    {encryptionSettings}
    {_t("Who can access this room?")}
    - {this._renderRoomAccess()} + {this.renderRoomAccess()}
    {historySection} diff --git a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js index b1ad9f3d23..5118414903 100644 --- a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js @@ -192,7 +192,11 @@ export default class GeneralUserSettingsTab extends React.Component { SettingsStore.setValue("language", null, SettingLevel.DEVICE, newLanguage); this.setState({language: newLanguage}); - PlatformPeg.get().reload(); + const platform = PlatformPeg.get(); + if (platform) { + platform.setLanguage(newLanguage); + platform.reload(); + } }; _onSpellCheckLanguagesChange = (languages) => { @@ -319,8 +323,11 @@ export default class GeneralUserSettingsTab extends React.Component { return (
    {_t("Language and region")} - +
    ); } @@ -329,8 +336,10 @@ export default class GeneralUserSettingsTab extends React.Component { return (
    {_t("Spell check dictionaries")} - +
    ); } diff --git a/src/components/views/settings/tabs/user/HelpUserSettingsTab.js b/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx similarity index 64% rename from src/components/views/settings/tabs/user/HelpUserSettingsTab.js rename to src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx index e16ee686f5..3fa0be478c 100644 --- a/src/components/views/settings/tabs/user/HelpUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/HelpUserSettingsTab.tsx @@ -1,6 +1,5 @@ /* -Copyright 2019 New Vector Ltd -Copyright 2020 The Matrix.org Foundation C.I.C. +Copyright 2019-2021 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -16,27 +15,37 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; import {_t, getCurrentLanguage} from "../../../../../languageHandler"; import {MatrixClientPeg} from "../../../../../MatrixClientPeg"; import AccessibleButton from "../../../elements/AccessibleButton"; +import AccessibleTooltipButton from '../../../elements/AccessibleTooltipButton'; import SdkConfig from "../../../../../SdkConfig"; import createRoom from "../../../../../createRoom"; import Modal from "../../../../../Modal"; -import * as sdk from "../../../../../"; +import * as sdk from "../../../../.."; import PlatformPeg from "../../../../../PlatformPeg"; import * as KeyboardShortcuts from "../../../../../accessibility/KeyboardShortcuts"; import UpdateCheckButton from "../../UpdateCheckButton"; -import {replaceableComponent} from "../../../../../utils/replaceableComponent"; +import { replaceableComponent } from "../../../../../utils/replaceableComponent"; +import { copyPlaintext } from "../../../../../utils/strings"; +import * as ContextMenu from "../../../../structures/ContextMenu"; +import { toRightOf } from "../../../../structures/ContextMenu"; + +interface IProps { + closeSettingsFn: () => {}; +} + +interface IState { + appVersion: string; + canUpdate: boolean; +} @replaceableComponent("views.settings.tabs.user.HelpUserSettingsTab") -export default class HelpUserSettingsTab extends React.Component { - static propTypes = { - closeSettingsFn: PropTypes.func.isRequired, - }; +export default class HelpUserSettingsTab extends React.Component { + protected closeCopiedTooltip: () => void; - constructor() { - super(); + constructor(props) { + super(props); this.state = { appVersion: null, @@ -53,7 +62,13 @@ export default class HelpUserSettingsTab extends React.Component { }); } - _onClearCacheAndReload = (e) => { + componentWillUnmount() { + // if the Copied tooltip is open then get rid of it, there are ways to close the modal which wouldn't close + // the tooltip otherwise, such as pressing Escape + if (this.closeCopiedTooltip) this.closeCopiedTooltip(); + } + + private onClearCacheAndReload = (e) => { if (!PlatformPeg.get()) return; // Dev note: please keep this log line, it's useful when troubleshooting a MatrixClient suddenly @@ -65,7 +80,7 @@ export default class HelpUserSettingsTab extends React.Component { }); }; - _onBugReport = (e) => { + private onBugReport = (e) => { const BugReportDialog = sdk.getComponent("dialogs.BugReportDialog"); if (!BugReportDialog) { return; @@ -73,7 +88,7 @@ export default class HelpUserSettingsTab extends React.Component { Modal.createTrackedDialog('Bug Report Dialog', '', BugReportDialog, {}); }; - _onStartBotChat = (e) => { + private onStartBotChat = (e) => { this.props.closeSettingsFn(); createRoom({ dmUserId: SdkConfig.get().welcomeUserId, @@ -81,7 +96,7 @@ export default class HelpUserSettingsTab extends React.Component { }); }; - _showSpoiler = (event) => { + private showSpoiler = (event) => { const target = event.target; target.innerHTML = target.getAttribute('data-spoiler'); @@ -93,7 +108,7 @@ export default class HelpUserSettingsTab extends React.Component { selection.addRange(range); }; - _renderLegal() { + private renderLegal() { const tocLinks = SdkConfig.get().terms_and_conditions_links; if (!tocLinks) return null; @@ -114,7 +129,7 @@ export default class HelpUserSettingsTab extends React.Component { ); } - _renderCredits() { + private renderCredits() { // Note: This is not translated because it is legal text. // Also,   is ugly but necessary. return ( @@ -122,34 +137,48 @@ export default class HelpUserSettingsTab extends React.Component { {_t("Credits")}
    ); } + onAccessTokenCopyClick = async (e) => { + e.preventDefault(); + const target = e.target; // copy target before we go async and React throws it away + + const successful = await copyPlaintext(MatrixClientPeg.get().getAccessToken()); + const buttonRect = target.getBoundingClientRect(); + const GenericTextContextMenu = sdk.getComponent('context_menus.GenericTextContextMenu'); + const {close} = ContextMenu.createMenu(GenericTextContextMenu, { + ...toRightOf(buttonRect, 2), + message: successful ? _t('Copied!') : _t('Failed to copy'), + }); + this.closeCopiedTooltip = target.onmouseleave = close; + } + render() { const brand = SdkConfig.get().brand; @@ -188,7 +217,7 @@ export default class HelpUserSettingsTab extends React.Component { }, )}
    - + {_t("Chat with %(brand)s Bot", { brand })}
    @@ -212,28 +241,27 @@ export default class HelpUserSettingsTab extends React.Component {
    {_t('Bug reporting')}
    - { - _t( "If you've submitted a bug via GitHub, debug logs can help " + - "us track down the problem. Debug logs contain application " + - "usage data including your username, the IDs or aliases of " + - "the rooms or groups you have visited and the usernames of " + - "other users. They do not contain messages.", - ) - } + {_t( + "If you've submitted a bug via GitHub, debug logs can help " + + "us track down the problem. Debug logs contain application " + + "usage data including your username, the IDs or aliases of " + + "the rooms or groups you have visited and the usernames of " + + "other users. They do not contain messages.", + )}
    - + {_t("Submit debug logs")}
    - { - _t( "To report a Matrix-related security issue, please read the Matrix.org " + - "Security Disclosure Policy.", {}, - { - 'a': (sub) => - {sub}, - }) - } + {_t( + "To report a Matrix-related security issue, please read the Matrix.org " + + "Security Disclosure Policy.", {}, + { + a: sub => {sub}, + }, + )}
    ); @@ -260,20 +288,29 @@ export default class HelpUserSettingsTab extends React.Component { {updateButton}
    - {this._renderLegal()} - {this._renderCredits()} + {this.renderLegal()} + {this.renderCredits()}
    {_t("Advanced")}
    {_t("Homeserver is")} {MatrixClientPeg.get().getHomeserverUrl()}
    {_t("Identity Server is")} {MatrixClientPeg.get().getIdentityServerUrl()}
    - {_t("Access Token:") + ' '} - - <{ _t("click to reveal") }> - +
    +
    + {_t("Access Token")}
    + {_t("Your access token gives full access to your account." + + " Do not share it with anyone." )} +
    + {MatrixClientPeg.get().getAccessToken()} + +
    +

    - + {_t("Clear cache and reload")}
    diff --git a/src/components/views/settings/tabs/user/MjolnirUserSettingsTab.js b/src/components/views/settings/tabs/user/MjolnirUserSettingsTab.tsx similarity index 90% rename from src/components/views/settings/tabs/user/MjolnirUserSettingsTab.js rename to src/components/views/settings/tabs/user/MjolnirUserSettingsTab.tsx index 91f6728a7a..6997defea9 100644 --- a/src/components/views/settings/tabs/user/MjolnirUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/MjolnirUserSettingsTab.tsx @@ -1,5 +1,5 @@ /* -Copyright 2019, 2020 The Matrix.org Foundation C.I.C. +Copyright 2019-2021 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -25,10 +25,16 @@ import {MatrixClientPeg} from "../../../../../MatrixClientPeg"; import * as sdk from "../../../../../index"; import {replaceableComponent} from "../../../../../utils/replaceableComponent"; +interface IState { + busy: boolean; + newPersonalRule: string; + newList: string; +} + @replaceableComponent("views.settings.tabs.user.MjolnirUserSettingsTab") -export default class MjolnirUserSettingsTab extends React.Component { - constructor() { - super(); +export default class MjolnirUserSettingsTab extends React.Component<{}, IState> { + constructor(props) { + super(props); this.state = { busy: false, @@ -37,15 +43,15 @@ export default class MjolnirUserSettingsTab extends React.Component { }; } - _onPersonalRuleChanged = (e) => { + private onPersonalRuleChanged = (e) => { this.setState({newPersonalRule: e.target.value}); }; - _onNewListChanged = (e) => { + private onNewListChanged = (e) => { this.setState({newList: e.target.value}); }; - _onAddPersonalRule = async (e) => { + private onAddPersonalRule = async (e) => { e.preventDefault(); e.stopPropagation(); @@ -72,7 +78,7 @@ export default class MjolnirUserSettingsTab extends React.Component { } }; - _onSubscribeList = async (e) => { + private onSubscribeList = async (e) => { e.preventDefault(); e.stopPropagation(); @@ -94,7 +100,7 @@ export default class MjolnirUserSettingsTab extends React.Component { } }; - async _removePersonalRule(rule: ListRule) { + private async removePersonalRule(rule: ListRule) { this.setState({busy: true}); try { const list = Mjolnir.sharedInstance().getPersonalList(); @@ -112,7 +118,7 @@ export default class MjolnirUserSettingsTab extends React.Component { } } - async _unsubscribeFromList(list: BanList) { + private async unsubscribeFromList(list: BanList) { this.setState({busy: true}); try { await Mjolnir.sharedInstance().unsubscribeFromList(list.roomId); @@ -130,7 +136,7 @@ export default class MjolnirUserSettingsTab extends React.Component { } } - _viewListRules(list: BanList) { + private viewListRules(list: BanList) { const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); const room = MatrixClientPeg.get().getRoom(list.roomId); @@ -161,7 +167,7 @@ export default class MjolnirUserSettingsTab extends React.Component { }); } - _renderPersonalBanListRules() { + private renderPersonalBanListRules() { const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); const list = Mjolnir.sharedInstance().getPersonalList(); @@ -174,7 +180,7 @@ export default class MjolnirUserSettingsTab extends React.Component {
  • this._removePersonalRule(rule)} + onClick={() => this.removePersonalRule(rule)} disabled={this.state.busy} > {_t("Remove")} @@ -192,7 +198,7 @@ export default class MjolnirUserSettingsTab extends React.Component { ); } - _renderSubscribedBanLists() { + private renderSubscribedBanLists() { const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); const personalList = Mjolnir.sharedInstance().getPersonalList(); @@ -209,14 +215,14 @@ export default class MjolnirUserSettingsTab extends React.Component {
  • this._unsubscribeFromList(list)} + onClick={() => this.unsubscribeFromList(list)} disabled={this.state.busy} > {_t("Unsubscribe")}   this._viewListRules(list)} + onClick={() => this.viewListRules(list)} disabled={this.state.busy} > {_t("View rules")} @@ -271,21 +277,21 @@ export default class MjolnirUserSettingsTab extends React.Component { )}
  • - {this._renderPersonalBanListRules()} + {this.renderPersonalBanListRules()}
    -
    + {_t("Ignore")} @@ -303,20 +309,20 @@ export default class MjolnirUserSettingsTab extends React.Component { )}
    - {this._renderSubscribedBanLists()} + {this.renderSubscribedBanLists()}
    - + {_t("Subscribe")} diff --git a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.js b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx similarity index 80% rename from src/components/views/settings/tabs/user/PreferencesUserSettingsTab.js rename to src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx index 0cd3dd6698..f02c5c9ce0 100644 --- a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx @@ -1,5 +1,5 @@ /* -Copyright 2019 New Vector Ltd +Copyright 2019-2021 The Matrix.org Foundation C.I.C. Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> Licensed under the Apache License, Version 2.0 (the "License"); @@ -23,10 +23,24 @@ import Field from "../../../elements/Field"; import * as sdk from "../../../../.."; import PlatformPeg from "../../../../../PlatformPeg"; import {SettingLevel} from "../../../../../settings/SettingLevel"; -import {replaceableComponent} from "../../../../../utils/replaceableComponent"; +import { replaceableComponent } from "../../../../../utils/replaceableComponent"; + +interface IState { + autoLaunch: boolean; + autoLaunchSupported: boolean; + warnBeforeExit: boolean; + warnBeforeExitSupported: boolean; + alwaysShowMenuBarSupported: boolean; + alwaysShowMenuBar: boolean; + minimizeToTraySupported: boolean; + minimizeToTray: boolean; + autocompleteDelay: string; + readMarkerInViewThresholdMs: string; + readMarkerOutOfViewThresholdMs: string; +} @replaceableComponent("views.settings.tabs.user.PreferencesUserSettingsTab") -export default class PreferencesUserSettingsTab extends React.Component { +export default class PreferencesUserSettingsTab extends React.Component<{}, IState> { static ROOM_LIST_SETTINGS = [ 'breadcrumbs', ]; @@ -68,8 +82,8 @@ export default class PreferencesUserSettingsTab extends React.Component { // Autocomplete delay (niche text box) ]; - constructor() { - super(); + constructor(props) { + super(props); this.state = { autoLaunch: false, @@ -89,7 +103,7 @@ export default class PreferencesUserSettingsTab extends React.Component { }; } - async componentDidMount(): void { + async componentDidMount() { const platform = PlatformPeg.get(); const autoLaunchSupported = await platform.supportsAutoLaunch(); @@ -128,38 +142,38 @@ export default class PreferencesUserSettingsTab extends React.Component { }); } - _onAutoLaunchChange = (checked) => { + private onAutoLaunchChange = (checked: boolean) => { PlatformPeg.get().setAutoLaunchEnabled(checked).then(() => this.setState({autoLaunch: checked})); }; - _onWarnBeforeExitChange = (checked) => { + private onWarnBeforeExitChange = (checked: boolean) => { PlatformPeg.get().setWarnBeforeExit(checked).then(() => this.setState({warnBeforeExit: checked})); } - _onAlwaysShowMenuBarChange = (checked) => { + private onAlwaysShowMenuBarChange = (checked: boolean) => { PlatformPeg.get().setAutoHideMenuBarEnabled(!checked).then(() => this.setState({alwaysShowMenuBar: checked})); }; - _onMinimizeToTrayChange = (checked) => { + private onMinimizeToTrayChange = (checked: boolean) => { PlatformPeg.get().setMinimizeToTrayEnabled(checked).then(() => this.setState({minimizeToTray: checked})); }; - _onAutocompleteDelayChange = (e) => { + private onAutocompleteDelayChange = (e: React.ChangeEvent) => { this.setState({autocompleteDelay: e.target.value}); SettingsStore.setValue("autocompleteDelay", null, SettingLevel.DEVICE, e.target.value); }; - _onReadMarkerInViewThresholdMs = (e) => { + private onReadMarkerInViewThresholdMs = (e: React.ChangeEvent) => { this.setState({readMarkerInViewThresholdMs: e.target.value}); SettingsStore.setValue("readMarkerInViewThresholdMs", null, SettingLevel.DEVICE, e.target.value); }; - _onReadMarkerOutOfViewThresholdMs = (e) => { + private onReadMarkerOutOfViewThresholdMs = (e: React.ChangeEvent) => { this.setState({readMarkerOutOfViewThresholdMs: e.target.value}); SettingsStore.setValue("readMarkerOutOfViewThresholdMs", null, SettingLevel.DEVICE, e.target.value); }; - _renderGroup(settingIds) { + private renderGroup(settingIds: string[]): React.ReactNodeArray { const SettingsFlag = sdk.getComponent("views.elements.SettingsFlag"); return settingIds.filter(SettingsStore.isEnabled).map(i => { return ; @@ -171,7 +185,7 @@ export default class PreferencesUserSettingsTab extends React.Component { if (this.state.autoLaunchSupported) { autoLaunchOption = ; } @@ -179,7 +193,7 @@ export default class PreferencesUserSettingsTab extends React.Component { if (this.state.warnBeforeExitSupported) { warnBeforeExitOption = ; } @@ -187,7 +201,7 @@ export default class PreferencesUserSettingsTab extends React.Component { if (this.state.alwaysShowMenuBarSupported) { autoHideMenuOption = ; } @@ -195,7 +209,7 @@ export default class PreferencesUserSettingsTab extends React.Component { if (this.state.minimizeToTraySupported) { minimizeToTrayOption = ; } @@ -205,22 +219,22 @@ export default class PreferencesUserSettingsTab extends React.Component {
    {_t("Room list")} - {this._renderGroup(PreferencesUserSettingsTab.ROOM_LIST_SETTINGS)} + {this.renderGroup(PreferencesUserSettingsTab.ROOM_LIST_SETTINGS)}
    {_t("Composer")} - {this._renderGroup(PreferencesUserSettingsTab.COMPOSER_SETTINGS)} + {this.renderGroup(PreferencesUserSettingsTab.COMPOSER_SETTINGS)}
    {_t("Timeline")} - {this._renderGroup(PreferencesUserSettingsTab.TIMELINE_SETTINGS)} + {this.renderGroup(PreferencesUserSettingsTab.TIMELINE_SETTINGS)}
    {_t("General")} - {this._renderGroup(PreferencesUserSettingsTab.GENERAL_SETTINGS)} + {this.renderGroup(PreferencesUserSettingsTab.GENERAL_SETTINGS)} {minimizeToTrayOption} {autoHideMenuOption} {autoLaunchOption} @@ -229,17 +243,17 @@ export default class PreferencesUserSettingsTab extends React.Component { label={_t('Autocomplete delay (ms)')} type='number' value={this.state.autocompleteDelay} - onChange={this._onAutocompleteDelayChange} /> + onChange={this.onAutocompleteDelayChange} /> + onChange={this.onReadMarkerInViewThresholdMs} /> + onChange={this.onReadMarkerOutOfViewThresholdMs} />
    ); diff --git a/src/components/views/settings/tabs/user/SecurityUserSettingsTab.js b/src/components/views/settings/tabs/user/SecurityUserSettingsTab.js index 8a70811399..53ed511b0a 100644 --- a/src/components/views/settings/tabs/user/SecurityUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/SecurityUserSettingsTab.js @@ -255,15 +255,18 @@ export default class SecurityUserSettingsTab extends React.Component { _renderIgnoredUsers() { const {waitingUnignored, ignoredUserIds} = this.state; - if (!ignoredUserIds || ignoredUserIds.length === 0) return null; - - const userIds = ignoredUserIds - .map((u) => ); + const userIds = !ignoredUserIds?.length + ? _t('You have no ignored users.') + : ignoredUserIds.map((u) => { + return ( + + ); + }); return (
    diff --git a/src/components/views/settings/tabs/user/VoiceUserSettingsTab.js b/src/components/views/settings/tabs/user/VoiceUserSettingsTab.js index d8adab55f6..362059f8ed 100644 --- a/src/components/views/settings/tabs/user/VoiceUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/VoiceUserSettingsTab.js @@ -176,8 +176,8 @@ export default class VoiceUserSettingsTab extends React.Component { const defaultDevice = getDefaultDevice(audioOutputs); speakerDropdown = ( + value={this.state.activeAudioOutput || defaultDevice} + onChange={this._setAudioOutput}> {this._renderDeviceOptions(audioOutputs, 'audioOutput')} ); @@ -188,8 +188,8 @@ export default class VoiceUserSettingsTab extends React.Component { const defaultDevice = getDefaultDevice(audioInputs); microphoneDropdown = ( + value={this.state.activeAudioInput || defaultDevice} + onChange={this._setAudioInput}> {this._renderDeviceOptions(audioInputs, 'audioInput')} ); @@ -200,8 +200,8 @@ export default class VoiceUserSettingsTab extends React.Component { const defaultDevice = getDefaultDevice(videoInputs); webcamDropdown = ( + value={this.state.activeVideoInput || defaultDevice} + onChange={this._setVideoInput}> {this._renderDeviceOptions(videoInputs, 'videoInput')} ); diff --git a/src/components/views/spaces/SpaceTreeLevel.tsx b/src/components/views/spaces/SpaceTreeLevel.tsx index 6825d84013..e48e1d5dc2 100644 --- a/src/components/views/spaces/SpaceTreeLevel.tsx +++ b/src/components/views/spaces/SpaceTreeLevel.tsx @@ -297,15 +297,19 @@ export class SpaceItem extends React.PureComponent { const isActive = activeSpaces.includes(space); const itemClasses = classNames({ "mx_SpaceItem": true, + "mx_SpaceItem_narrow": isNarrow, "collapsed": collapsed, "hasSubSpaces": childSpaces && childSpaces.length, }); + + const isInvite = space.getMyMembership() === "invite"; const classes = classNames("mx_SpaceButton", { mx_SpaceButton_active: isActive, mx_SpaceButton_hasMenuOpen: !!this.state.contextMenuPosition, mx_SpaceButton_narrow: isNarrow, + mx_SpaceButton_invite: isInvite, }); - const notificationState = space.getMyMembership() === "invite" + const notificationState = isInvite ? StaticNotificationState.forSymbol("!", NotificationColor.Red) : SpaceStore.instance.getNotificationState(space.roomId); diff --git a/src/components/views/verification/VerificationCancelled.js b/src/components/views/verification/VerificationCancelled.js index 0bbaea1804..c57094d9b5 100644 --- a/src/components/views/verification/VerificationCancelled.js +++ b/src/components/views/verification/VerificationCancelled.js @@ -29,14 +29,14 @@ export default class VerificationCancelled extends React.Component { render() { const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); return
    -

    {_t( - "The other party cancelled the verification.", - )}

    - +

    {_t( + "The other party cancelled the verification.", + )}

    +
    ; } } diff --git a/src/components/views/voice_messages/Clock.tsx b/src/components/views/voice_messages/Clock.tsx index 6c256957e9..23e6762c52 100644 --- a/src/components/views/voice_messages/Clock.tsx +++ b/src/components/views/voice_messages/Clock.tsx @@ -29,14 +29,20 @@ interface IState { * displayed, making it possible to see "82:29". */ @replaceableComponent("views.voice_messages.Clock") -export default class Clock extends React.PureComponent { +export default class Clock extends React.Component { public constructor(props) { super(props); } + shouldComponentUpdate(nextProps: Readonly, nextState: Readonly, nextContext: any): boolean { + const currentFloor = Math.floor(this.props.seconds); + const nextFloor = Math.floor(nextProps.seconds); + return currentFloor !== nextFloor; + } + public render() { const minutes = Math.floor(this.props.seconds / 60).toFixed(0).padStart(2, '0'); - const seconds = Math.round(this.props.seconds % 60).toFixed(0).padStart(2, '0'); // hide millis + const seconds = Math.floor(this.props.seconds % 60).toFixed(0).padStart(2, '0'); // hide millis return {minutes}:{seconds}; } } diff --git a/src/components/views/voice_messages/LiveRecordingClock.tsx b/src/components/views/voice_messages/LiveRecordingClock.tsx index 5e9006c6ab..b82539eb16 100644 --- a/src/components/views/voice_messages/LiveRecordingClock.tsx +++ b/src/components/views/voice_messages/LiveRecordingClock.tsx @@ -31,7 +31,7 @@ interface IState { * A clock for a live recording. */ @replaceableComponent("views.voice_messages.LiveRecordingClock") -export default class LiveRecordingClock extends React.Component { +export default class LiveRecordingClock extends React.PureComponent { public constructor(props) { super(props); @@ -39,12 +39,6 @@ export default class LiveRecordingClock extends React.Component this.props.recorder.liveData.onUpdate(this.onRecordingUpdate); } - shouldComponentUpdate(nextProps: Readonly, nextState: Readonly, nextContext: any): boolean { - const currentFloor = Math.floor(this.state.seconds); - const nextFloor = Math.floor(nextState.seconds); - return currentFloor !== nextFloor; - } - private onRecordingUpdate = (update: IRecordingUpdate) => { this.setState({seconds: update.timeSeconds}); }; diff --git a/src/components/views/voice_messages/LiveRecordingWaveform.tsx b/src/components/views/voice_messages/LiveRecordingWaveform.tsx index c1f5e97fff..aab89f6ab1 100644 --- a/src/components/views/voice_messages/LiveRecordingWaveform.tsx +++ b/src/components/views/voice_messages/LiveRecordingWaveform.tsx @@ -15,7 +15,7 @@ limitations under the License. */ import React from "react"; -import {IRecordingUpdate, VoiceRecording} from "../../../voice/VoiceRecording"; +import {IRecordingUpdate, RECORDING_PLAYBACK_SAMPLES, VoiceRecording} from "../../../voice/VoiceRecording"; import {replaceableComponent} from "../../../utils/replaceableComponent"; import {arrayFastResample, arraySeed} from "../../../utils/arrays"; import {percentageOf} from "../../../utils/numbers"; @@ -29,8 +29,6 @@ interface IState { heights: number[]; } -const DOWNSAMPLE_TARGET = 35; // number of bars we want - /** * A waveform which shows the waveform of a live recording */ @@ -39,14 +37,14 @@ export default class LiveRecordingWaveform extends React.PureComponent { // The waveform and the downsample target are pretty close, so we should be fine to // do this, despite the docs on arrayFastResample. - const bars = arrayFastResample(Array.from(update.waveform), DOWNSAMPLE_TARGET); + const bars = arrayFastResample(Array.from(update.waveform), RECORDING_PLAYBACK_SAMPLES); this.setState({ // The incoming data is between zero and one, but typically even screaming into a // microphone won't send you over 0.6, so we artificially adjust the gain for the diff --git a/src/components/views/voice_messages/PlayPauseButton.tsx b/src/components/views/voice_messages/PlayPauseButton.tsx new file mode 100644 index 0000000000..1f87eb012d --- /dev/null +++ b/src/components/views/voice_messages/PlayPauseButton.tsx @@ -0,0 +1,61 @@ +/* +Copyright 2021 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +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, {ReactNode} from "react"; +import {replaceableComponent} from "../../../utils/replaceableComponent"; +import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; +import {_t} from "../../../languageHandler"; +import {Playback, PlaybackState} from "../../../voice/Playback"; +import classNames from "classnames"; + +interface IProps { + // Playback instance to manipulate. Cannot change during the component lifecycle. + playback: Playback; + + // The playback phase to render. Able to change during the component lifecycle. + playbackPhase: PlaybackState; +} + +/** + * Displays a play/pause button (activating the play/pause function of the recorder) + * to be displayed in reference to a recording. + */ +@replaceableComponent("views.voice_messages.PlayPauseButton") +export default class PlayPauseButton extends React.PureComponent { + public constructor(props) { + super(props); + } + + private onClick = async () => { + await this.props.playback.toggle(); + }; + + public render(): ReactNode { + const isPlaying = this.props.playback.isPlaying; + const isDisabled = this.props.playbackPhase === PlaybackState.Decoding; + const classes = classNames('mx_PlayPauseButton', { + 'mx_PlayPauseButton_play': !isPlaying, + 'mx_PlayPauseButton_pause': isPlaying, + 'mx_PlayPauseButton_disabled': isDisabled, + }); + return ; + } +} diff --git a/src/components/views/voice_messages/PlaybackClock.tsx b/src/components/views/voice_messages/PlaybackClock.tsx new file mode 100644 index 0000000000..2e8ec9a3e7 --- /dev/null +++ b/src/components/views/voice_messages/PlaybackClock.tsx @@ -0,0 +1,71 @@ +/* +Copyright 2021 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +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 {replaceableComponent} from "../../../utils/replaceableComponent"; +import Clock from "./Clock"; +import {Playback, PlaybackState} from "../../../voice/Playback"; +import {UPDATE_EVENT} from "../../../stores/AsyncStore"; + +interface IProps { + playback: Playback; +} + +interface IState { + seconds: number; + durationSeconds: number; + playbackPhase: PlaybackState; +} + +/** + * A clock for a playback of a recording. + */ +@replaceableComponent("views.voice_messages.PlaybackClock") +export default class PlaybackClock extends React.PureComponent { + public constructor(props) { + super(props); + + this.state = { + seconds: this.props.playback.clockInfo.timeSeconds, + // we track the duration on state because we won't really know what the clip duration + // is until the first time update, and as a PureComponent we are trying to dedupe state + // updates as much as possible. This is just the easiest way to avoid a forceUpdate() or + // member property to track "did we get a duration". + durationSeconds: this.props.playback.clockInfo.durationSeconds, + playbackPhase: PlaybackState.Stopped, // assume not started, so full clock + }; + this.props.playback.on(UPDATE_EVENT, this.onPlaybackUpdate); + this.props.playback.clockInfo.liveData.onUpdate(this.onTimeUpdate); + } + + private onPlaybackUpdate = (ev: PlaybackState) => { + // Convert Decoding -> Stopped because we don't care about the distinction here + if (ev === PlaybackState.Decoding) ev = PlaybackState.Stopped; + this.setState({playbackPhase: ev}); + }; + + private onTimeUpdate = (time: number[]) => { + this.setState({seconds: time[0], durationSeconds: time[1]}); + }; + + public render() { + let seconds = this.state.seconds; + if (this.state.playbackPhase === PlaybackState.Stopped) { + seconds = this.state.durationSeconds; + } + return ; + } +} diff --git a/src/components/views/voice_messages/PlaybackWaveform.tsx b/src/components/views/voice_messages/PlaybackWaveform.tsx new file mode 100644 index 0000000000..123af5dfa5 --- /dev/null +++ b/src/components/views/voice_messages/PlaybackWaveform.tsx @@ -0,0 +1,68 @@ +/* +Copyright 2021 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +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 {replaceableComponent} from "../../../utils/replaceableComponent"; +import {arraySeed, arrayTrimFill} from "../../../utils/arrays"; +import Waveform from "./Waveform"; +import {Playback, PLAYBACK_WAVEFORM_SAMPLES} from "../../../voice/Playback"; +import {percentageOf} from "../../../utils/numbers"; + +interface IProps { + playback: Playback; +} + +interface IState { + heights: number[]; + progress: number; +} + +/** + * A waveform which shows the waveform of a previously recorded recording + */ +@replaceableComponent("views.voice_messages.PlaybackWaveform") +export default class PlaybackWaveform extends React.PureComponent { + public constructor(props) { + super(props); + + this.state = { + heights: this.toHeights(this.props.playback.waveform), + progress: 0, // default no progress + }; + + this.props.playback.waveformData.onUpdate(this.onWaveformUpdate); + this.props.playback.clockInfo.liveData.onUpdate(this.onTimeUpdate); + } + + private toHeights(waveform: number[]) { + const seed = arraySeed(0, PLAYBACK_WAVEFORM_SAMPLES); + return arrayTrimFill(waveform, PLAYBACK_WAVEFORM_SAMPLES, seed); + } + + private onWaveformUpdate = (waveform: number[]) => { + this.setState({heights: this.toHeights(waveform)}); + }; + + private onTimeUpdate = (time: number[]) => { + // Track percentages to very coarse precision, otherwise 0.002 ends up highlighting a bar. + const progress = Number(percentageOf(time[0], 0, time[1]).toFixed(1)); + this.setState({progress}); + }; + + public render() { + return ; + } +} diff --git a/src/components/views/voice_messages/RecordingPlayback.tsx b/src/components/views/voice_messages/RecordingPlayback.tsx new file mode 100644 index 0000000000..776997cec2 --- /dev/null +++ b/src/components/views/voice_messages/RecordingPlayback.tsx @@ -0,0 +1,62 @@ +/* +Copyright 2021 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +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 {Playback, PlaybackState} from "../../../voice/Playback"; +import React, {ReactNode} from "react"; +import {UPDATE_EVENT} from "../../../stores/AsyncStore"; +import PlaybackWaveform from "./PlaybackWaveform"; +import PlayPauseButton from "./PlayPauseButton"; +import PlaybackClock from "./PlaybackClock"; + +interface IProps { + // Playback instance to render. Cannot change during component lifecycle: create + // an all-new component instead. + playback: Playback; +} + +interface IState { + playbackPhase: PlaybackState; +} + +export default class RecordingPlayback extends React.PureComponent { + constructor(props: IProps) { + super(props); + + this.state = { + playbackPhase: PlaybackState.Decoding, // default assumption + }; + + // We don't need to de-register: the class handles this for us internally + this.props.playback.on(UPDATE_EVENT, this.onPlaybackUpdate); + + // Don't wait for the promise to complete - it will emit a progress update when it + // is done, and it's not meant to take long anyhow. + // noinspection JSIgnoredPromiseFromCall + this.props.playback.prepare(); + } + + private onPlaybackUpdate = (ev: PlaybackState) => { + this.setState({playbackPhase: ev}); + }; + + public render(): ReactNode { + return
    + + + +
    + } +} diff --git a/src/components/views/voice_messages/Waveform.tsx b/src/components/views/voice_messages/Waveform.tsx index 5fa68dcadc..840a5a12b3 100644 --- a/src/components/views/voice_messages/Waveform.tsx +++ b/src/components/views/voice_messages/Waveform.tsx @@ -16,9 +16,11 @@ limitations under the License. import React from "react"; import {replaceableComponent} from "../../../utils/replaceableComponent"; +import classNames from "classnames"; interface IProps { relHeights: number[]; // relative heights (0-1) + progress: number; // percent complete, 0-1, default 100% } interface IState { @@ -28,9 +30,16 @@ interface IState { * A simple waveform component. This renders bars (centered vertically) for each * height provided in the component properties. Updating the properties will update * the rendered waveform. + * + * For CSS purposes, a mx_Waveform_bar_100pct class is added when the bar should be + * "filled", as a demonstration of the progress property. */ @replaceableComponent("views.voice_messages.Waveform") export default class Waveform extends React.PureComponent { + public static defaultProps = { + progress: 1, + }; + public constructor(props) { super(props); } @@ -38,7 +47,13 @@ export default class Waveform extends React.PureComponent { public render() { return
    {this.props.relHeights.map((h, i) => { - return ; + const progress = this.props.progress; + const isCompleteBar = (i / this.props.relHeights.length) <= progress && progress > 0; + const classes = classNames({ + 'mx_Waveform_bar': true, + 'mx_Waveform_bar_100pct': isCompleteBar, + }); + return ; })}
    ; } diff --git a/src/components/views/voip/AudioFeed.tsx b/src/components/views/voip/AudioFeed.tsx new file mode 100644 index 0000000000..c78f0c0fc8 --- /dev/null +++ b/src/components/views/voip/AudioFeed.tsx @@ -0,0 +1,97 @@ +/* +Copyright 2021 Šimon Brandner + +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, {createRef} from 'react'; +import { CallFeed, CallFeedEvent } from 'matrix-js-sdk/src/webrtc/callFeed'; +import { logger } from 'matrix-js-sdk/src/logger'; +import CallMediaHandler from "../../../CallMediaHandler"; + +interface IProps { + feed: CallFeed, +} + +export default class AudioFeed extends React.Component { + private element = createRef(); + + componentDidMount() { + this.props.feed.addListener(CallFeedEvent.NewStream, this.onNewStream); + this.playMedia(); + } + + componentWillUnmount() { + this.props.feed.removeListener(CallFeedEvent.NewStream, this.onNewStream); + this.stopMedia(); + } + + private playMedia() { + const element = this.element.current; + const audioOutput = CallMediaHandler.getAudioOutput(); + + if (audioOutput) { + try { + // This seems quite unreliable in Chrome, although I haven't yet managed to make a jsfiddle where + // it fails. + // It seems reliable if you set the sink ID after setting the srcObject and then set the sink ID + // back to the default after the call is over - Dave + element.setSinkId(audioOutput); + } catch (e) { + console.error("Couldn't set requested audio output device: using default", e); + logger.warn("Couldn't set requested audio output device: using default", e); + } + } + + element.muted = false; + element.srcObject = this.props.feed.stream; + element.autoplay = true; + + try { + // A note on calling methods on media elements: + // We used to have queues per media element to serialise all calls on those elements. + // The reason given for this was that load() and play() were racing. However, we now + // never call load() explicitly so this seems unnecessary. However, serialising every + // operation was causing bugs where video would not resume because some play command + // had got stuck and all media operations were queued up behind it. If necessary, we + // should serialise the ones that need to be serialised but then be able to interrupt + // them with another load() which will cancel the pending one, but since we don't call + // load() explicitly, it shouldn't be a problem. - Dave + element.play() + } catch (e) { + logger.info("Failed to play media element with feed", this.props.feed, e); + } + } + + private stopMedia() { + const element = this.element.current; + + element.pause(); + element.src = null; + + // As per comment in componentDidMount, setting the sink ID back to the + // default once the call is over makes setSinkId work reliably. - Dave + // Since we are not using the same element anymore, the above doesn't + // seem to be necessary - Šimon + } + + private onNewStream = () => { + this.playMedia(); + }; + + render() { + return ( +
    ; + const avatarSize = this.props.pipMode ? 76 : 160; + // The 'content' for the call, ie. the videos for a video call and profile picture // for voice calls (fills the bg) let contentView: React.ReactNode; @@ -482,11 +492,13 @@ export default class CallView extends React.Component { const isOnHold = this.state.isLocalOnHold || this.state.isRemoteOnHold; let holdTransferContent; if (transfereeCall) { - const transferTargetRoom = MatrixClientPeg.get().getRoom(CallHandler.roomIdForCall(this.props.call)); + const transferTargetRoom = MatrixClientPeg.get().getRoom( + CallHandler.sharedInstance().roomIdForCall(this.props.call), + ); const transferTargetName = transferTargetRoom ? transferTargetRoom.name : _t("unknown person"); const transfereeRoom = MatrixClientPeg.get().getRoom( - CallHandler.roomIdForCall(transfereeCall), + CallHandler.sharedInstance().roomIdForCall(transfereeCall), ); const transfereeName = transfereeRoom ? transfereeRoom.name : _t("unknown person"); @@ -522,41 +534,85 @@ export default class CallView extends React.Component {
    ; } - if (this.props.call.type === CallType.Video) { - let localVideoFeed = null; - let onHoldBackground = null; - const backgroundStyle: CSSProperties = {}; - const containerClasses = classNames({ - mx_CallView_video: true, - mx_CallView_video_hold: isOnHold, - }); - if (isOnHold) { + // This is a bit messy. I can't see a reason to have two onHold/transfer screens + if (isOnHold || transfereeCall) { + if (this.props.call.type === CallType.Video) { + const containerClasses = classNames({ + mx_CallView_content: true, + mx_CallView_video: true, + mx_CallView_video_hold: isOnHold, + }); + let onHoldBackground = null; + const backgroundStyle: CSSProperties = {}; const backgroundAvatarUrl = avatarUrlForMember( - // is it worth getting the size of the div to pass here? + // is it worth getting the size of the div to pass here? this.props.call.getOpponentMember(), 1024, 1024, 'crop', ); backgroundStyle.backgroundImage = 'url(' + backgroundAvatarUrl + ')'; onHoldBackground =
    ; - } - if (!this.state.vidMuted) { - localVideoFeed = ; - } - contentView =
    - {onHoldBackground} - - {localVideoFeed} - {holdTransferContent} - {callControls} -
    ; - } else { - const avatarSize = this.props.pipMode ? 76 : 160; + contentView = ( +
    + {onHoldBackground} + {holdTransferContent} + {callControls} +
    + ); + } else { + const classes = classNames({ + mx_CallView_content: true, + mx_CallView_voice: true, + mx_CallView_voice_hold: isOnHold, + }); + + contentView =( +
    +
    +
    + +
    +
    + {holdTransferContent} + {callControls} +
    + ); + } + } else if (this.props.call.noIncomingFeeds()) { + // Here we're reusing the css classes from voice on hold, because + // I am lazy. If this gets merged, the CallView might be subject + // to change anyway - I might take an axe to this file in order to + // try to get other things working const classes = classNames({ + mx_CallView_content: true, mx_CallView_voice: true, - mx_CallView_voice_hold: isOnHold, }); + const feeds = this.props.call.getLocalFeeds().map((feed, i) => { + // Here we check to hide local audio feeds to achieve the same UI/UX + // as before. But once again this might be subject to change + if (feed.isVideoMuted()) return; + return ( + + ); + }); + + // Saying "Connecting" here isn't really true, but the best thing + // I can come up with, but this might be subject to change as well contentView =
    + {feeds}
    { />
    - {holdTransferContent} +
    {_t("Connecting")}
    + {callControls} +
    ; + } else { + const containerClasses = classNames({ + mx_CallView_content: true, + mx_CallView_video: true, + }); + + // TODO: Later the CallView should probably be reworked to support + // any number of feeds but now we can always expect there to be two + // feeds. This is because the js-sdk ignores any new incoming streams + const feeds = this.state.feeds.map((feed, i) => { + // Here we check to hide local audio feeds to achieve the same UI/UX + // as before. But once again this might be subject to change + if (feed.isVideoMuted() && feed.isLocal()) return; + return ( + + ); + }); + + contentView =
    + {feeds} {callControls}
    ; } diff --git a/src/components/views/voip/CallViewForRoom.tsx b/src/components/views/voip/CallViewForRoom.tsx index 878b6af20f..0c785f758d 100644 --- a/src/components/views/voip/CallViewForRoom.tsx +++ b/src/components/views/voip/CallViewForRoom.tsx @@ -16,7 +16,7 @@ limitations under the License. import { CallState, MatrixCall } from 'matrix-js-sdk/src/webrtc/call'; import React from 'react'; -import CallHandler from '../../../CallHandler'; +import CallHandler, { CallHandlerEvent } from '../../../CallHandler'; import CallView from './CallView'; import dis from '../../../dispatcher/dispatcher'; import {Resizable} from "re-resizable"; @@ -54,24 +54,30 @@ export default class CallViewForRoom extends React.Component { public componentDidMount() { this.dispatcherRef = dis.register(this.onAction); + CallHandler.sharedInstance().addListener(CallHandlerEvent.CallChangeRoom, this.updateCall); } public componentWillUnmount() { dis.unregister(this.dispatcherRef); + CallHandler.sharedInstance().removeListener(CallHandlerEvent.CallChangeRoom, this.updateCall); } private onAction = (payload) => { switch (payload.action) { case 'call_state': { - const newCall = this.getCall(); - if (newCall !== this.state.call) { - this.setState({call: newCall}); - } + this.updateCall(); break; } } }; + private updateCall = () => { + const newCall = this.getCall(); + if (newCall !== this.state.call) { + this.setState({call: newCall}); + } + }; + private getCall(): MatrixCall { const call = CallHandler.sharedInstance().getCallForRoom(this.props.roomId); diff --git a/src/components/views/voip/IncomingCallBox.tsx b/src/components/views/voip/IncomingCallBox.tsx index 0ca2a196c2..2abdc0641d 100644 --- a/src/components/views/voip/IncomingCallBox.tsx +++ b/src/components/views/voip/IncomingCallBox.tsx @@ -72,7 +72,7 @@ export default class IncomingCallBox extends React.Component { e.stopPropagation(); dis.dispatch({ action: 'answer', - room_id: CallHandler.roomIdForCall(this.state.incomingCall), + room_id: CallHandler.sharedInstance().roomIdForCall(this.state.incomingCall), }); }; @@ -80,7 +80,7 @@ export default class IncomingCallBox extends React.Component { e.stopPropagation(); dis.dispatch({ action: 'reject', - room_id: CallHandler.roomIdForCall(this.state.incomingCall), + room_id: CallHandler.sharedInstance().roomIdForCall(this.state.incomingCall), }); }; @@ -91,7 +91,7 @@ export default class IncomingCallBox extends React.Component { let room = null; if (this.state.incomingCall) { - room = MatrixClientPeg.get().getRoom(CallHandler.roomIdForCall(this.state.incomingCall)); + room = MatrixClientPeg.get().getRoom(CallHandler.sharedInstance().roomIdForCall(this.state.incomingCall)); } const caller = room ? room.name : _t("Unknown caller"); diff --git a/src/components/views/voip/VideoFeed.tsx b/src/components/views/voip/VideoFeed.tsx index 2981fb6c04..d22fa055ce 100644 --- a/src/components/views/voip/VideoFeed.tsx +++ b/src/components/views/voip/VideoFeed.tsx @@ -18,52 +18,102 @@ import classnames from 'classnames'; import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call'; import React, {createRef} from 'react'; import SettingsStore from "../../../settings/SettingsStore"; +import { CallFeed, CallFeedEvent } from 'matrix-js-sdk/src/webrtc/callFeed'; +import { logger } from 'matrix-js-sdk/src/logger'; +import MemberAvatar from "../avatars/MemberAvatar" import {replaceableComponent} from "../../../utils/replaceableComponent"; -export enum VideoFeedType { - Local, - Remote, -} - interface IProps { call: MatrixCall, - type: VideoFeedType, + feed: CallFeed, + + // Whether this call view is for picture-in-picture mode + // otherwise, it's the larger call view when viewing the room the call is in. + // This is sort of a proxy for a number of things but we currently have no + // need to control those things separately, so this is simpler. + pipMode?: boolean; // a callback which is called when the video element is resized // due to a change in video metadata onResize?: (e: Event) => void, } -@replaceableComponent("views.voip.VideoFeed") -export default class VideoFeed extends React.Component { - private vid = createRef(); +interface IState { + audioMuted: boolean; + videoMuted: boolean; +} - componentDidMount() { - this.vid.current.addEventListener('resize', this.onResize); - this.setVideoElement(); +@replaceableComponent("views.voip.VideoFeed") +export default class VideoFeed extends React.Component { + private element = createRef(); + + constructor(props: IProps) { + super(props); + + this.state = { + audioMuted: this.props.feed.isAudioMuted(), + videoMuted: this.props.feed.isVideoMuted(), + }; } - componentDidUpdate(prevProps) { - if (this.props.call !== prevProps.call) { - this.setVideoElement(); - } + componentDidMount() { + this.props.feed.addListener(CallFeedEvent.NewStream, this.onNewStream); + this.playMedia(); } componentWillUnmount() { - this.vid.current.removeEventListener('resize', this.onResize); + this.props.feed.removeListener(CallFeedEvent.NewStream, this.onNewStream); + this.element.current?.removeEventListener('resize', this.onResize); + this.stopMedia(); } - private setVideoElement() { - if (this.props.type === VideoFeedType.Local) { - this.props.call.setLocalVideoElement(this.vid.current); - } else { - this.props.call.setRemoteVideoElement(this.vid.current); + private playMedia() { + const element = this.element.current; + if (!element) return; + // We play audio in AudioFeed, not here + element.muted = true; + element.srcObject = this.props.feed.stream; + element.autoplay = true; + try { + // A note on calling methods on media elements: + // We used to have queues per media element to serialise all calls on those elements. + // The reason given for this was that load() and play() were racing. However, we now + // never call load() explicitly so this seems unnecessary. However, serialising every + // operation was causing bugs where video would not resume because some play command + // had got stuck and all media operations were queued up behind it. If necessary, we + // should serialise the ones that need to be serialised but then be able to interrupt + // them with another load() which will cancel the pending one, but since we don't call + // load() explicitly, it shouldn't be a problem. - Dave + element.play() + } catch (e) { + logger.info("Failed to play media element with feed", this.props.feed, e); } } - onResize = (e) => { - if (this.props.onResize) { + private stopMedia() { + const element = this.element.current; + if (!element) return; + + element.pause(); + element.src = null; + + // As per comment in componentDidMount, setting the sink ID back to the + // default once the call is over makes setSinkId work reliably. - Dave + // Since we are not using the same element anymore, the above doesn't + // seem to be necessary - Šimon + } + + private onNewStream = () => { + this.setState({ + audioMuted: this.props.feed.isAudioMuted(), + videoMuted: this.props.feed.isVideoMuted(), + }); + this.playMedia(); + }; + + private onResize = (e) => { + if (this.props.onResize && !this.props.feed.isLocal()) { this.props.onResize(e); } }; @@ -71,14 +121,33 @@ export default class VideoFeed extends React.Component { render() { const videoClasses = { mx_VideoFeed: true, - mx_VideoFeed_local: this.props.type === VideoFeedType.Local, - mx_VideoFeed_remote: this.props.type === VideoFeedType.Remote, + mx_VideoFeed_local: this.props.feed.isLocal(), + mx_VideoFeed_remote: !this.props.feed.isLocal(), + mx_VideoFeed_voice: this.state.videoMuted, + mx_VideoFeed_video: !this.state.videoMuted, mx_VideoFeed_mirror: ( - this.props.type === VideoFeedType.Local && + this.props.feed.isLocal() && SettingsStore.getValue('VideoView.flipVideoHorizontally') ), }; - return
    ; }); From 1597b2a971bb3e0194c4a8cf8bb5530923f17438 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Wed, 16 Jun 2021 10:01:23 +0100 Subject: [PATCH 143/809] Keep composer reply when scrolling away from a highlighted event --- src/components/structures/RoomView.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index fe90d2f873..c0ce6ba4c9 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -701,6 +701,7 @@ export default class RoomView extends React.Component { room_id: this.state.room.roomId, event_id: this.state.initialEventId, highlighted: false, + replyingToEvent: this.state.replyToEvent, }); } } From 7d90612371a31f699d879366262f84fd1082bd9b Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 17 Jun 2021 16:22:40 +0100 Subject: [PATCH 144/809] Iterate PR --- res/css/views/beta/_BetaCard.scss | 93 +++++++++++-------- src/components/views/beta/BetaCard.tsx | 49 +++++----- .../views/elements/SettingsFlag.tsx | 10 +- src/i18n/strings/en_EN.json | 8 +- src/settings/Settings.tsx | 11 ++- src/settings/SettingsStore.ts | 10 ++ 6 files changed, 107 insertions(+), 74 deletions(-) diff --git a/res/css/views/beta/_BetaCard.scss b/res/css/views/beta/_BetaCard.scss index fd87b1c824..1a8241b65f 100644 --- a/res/css/views/beta/_BetaCard.scss +++ b/res/css/views/beta/_BetaCard.scss @@ -19,55 +19,68 @@ limitations under the License. padding: 24px; background-color: $settings-profile-placeholder-bg-color; border-radius: 8px; - display: flex; box-sizing: border-box; - > div { - .mx_BetaCard_title { - font-weight: $font-semi-bold; - font-size: $font-18px; - line-height: $font-22px; - color: $primary-fg-color; - margin: 4px 0 14px; + .mx_BetaCard_columns { + display: flex; - .mx_BetaCard_betaPill { - margin-left: 12px; + > div { + .mx_BetaCard_title { + font-weight: $font-semi-bold; + font-size: $font-18px; + line-height: $font-22px; + color: $primary-fg-color; + margin: 4px 0 14px; + + .mx_BetaCard_betaPill { + margin-left: 12px; + } + } + + .mx_BetaCard_caption { + font-size: $font-15px; + line-height: $font-20px; + color: $secondary-fg-color; + margin-bottom: 20px; + } + + .mx_BetaCard_buttons .mx_AccessibleButton { + display: block; + margin: 12px 0; + padding: 7px 40px; + width: auto; + } + + .mx_BetaCard_disclaimer { + font-size: $font-12px; + line-height: $font-15px; + color: $secondary-fg-color; + margin-top: 20px; } } - .mx_BetaCard_caption { - font-size: $font-15px; - line-height: $font-20px; - color: $secondary-fg-color; - margin-bottom: 20px; - } - - .mx_BetaCard_buttons .mx_AccessibleButton { - display: block; - margin: 12px 0; - padding: 7px 40px; - width: auto; - } - - .mx_BetaCard_disclaimer { - font-size: $font-12px; - line-height: $font-15px; - color: $secondary-fg-color; - margin-top: 20px; - } - - .mx_BetaCard_relatedSettings { - summary + .mx_SettingsFlag { - margin-top: 4px; - } + > img { + margin: auto 0 auto 20px; + width: 300px; + object-fit: contain; + height: 100%; } } - > img { - margin: auto 0 auto 20px; - width: 300px; - object-fit: contain; - height: 100%; + .mx_BetaCard_relatedSettings { + .mx_SettingsFlag { + margin: 16px 0 0; + font-size: $font-15px; + line-height: $font-24px; + color: $primary-fg-color; + + .mx_SettingsFlag_microcopy { + margin-top: 4px; + font-size: $font-12px; + line-height: $font-15px; + color: $secondary-fg-color; + } + } } } diff --git a/src/components/views/beta/BetaCard.tsx b/src/components/views/beta/BetaCard.tsx index 56770c3385..aa4fe49f63 100644 --- a/src/components/views/beta/BetaCard.tsx +++ b/src/components/views/beta/BetaCard.tsx @@ -83,32 +83,33 @@ const BetaCard = ({ title: titleOverride, featureId }: IProps) => { } return
    -
    -

    - { titleOverride || _t(title) } - -

    - { _t(caption) } -
    - { feedbackButton } - SettingsStore.setValue(featureId, null, SettingLevel.DEVICE, !value)} - kind={feedbackButton ? "primary_outline" : "primary"} - > - { value ? _t("Leave the beta") : _t("Join the beta") } - +
    +
    +

    + { titleOverride || _t(title) } + +

    + { _t(caption) } +
    + { feedbackButton } + SettingsStore.setValue(featureId, null, SettingLevel.DEVICE, !value)} + kind={feedbackButton ? "primary_outline" : "primary"} + > + { value ? _t("Leave the beta") : _t("Join the beta") } + +
    + { disclaimer &&
    + { disclaimer(value) } +
    }
    - { disclaimer &&
    - { disclaimer(value) } -
    } - { extraSettings &&
    - { _t("Experimental options") } - { extraSettings.map(key => ( - - )) } -
    } +
    - + { extraSettings &&
    + { extraSettings.map(key => ( + + )) } +
    }
    ; }; diff --git a/src/components/views/elements/SettingsFlag.tsx b/src/components/views/elements/SettingsFlag.tsx index 4f885ab47d..24a21e1a33 100644 --- a/src/components/views/elements/SettingsFlag.tsx +++ b/src/components/views/elements/SettingsFlag.tsx @@ -77,9 +77,10 @@ export default class SettingsFlag extends React.Component { public render() { const canChange = SettingsStore.canSetValue(this.props.name, this.props.roomId, this.props.level); - let label = this.props.label; - if (!label) label = SettingsStore.getDisplayName(this.props.name, this.props.level); - else label = _t(label); + const label = this.props.label + ? _t(this.props.label) + : SettingsStore.getDisplayName(this.props.name, this.props.level); + const description = SettingsStore.getDescription(this.props.name); if (this.props.useCheckbox) { return { disabled={this.props.disabled || !canChange} aria-label={label} /> + { description &&
    + { description } +
    }
    ); } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 044e3a3079..179b58b617 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -793,9 +793,10 @@ "You can leave the beta any time from settings or tapping on a beta badge, like the one above.": "You can leave the beta any time from settings or tapping on a beta badge, like the one above.", "Beta available for web, desktop and Android. Some features may be unavailable on your homeserver.": "Beta available for web, desktop and Android. Some features may be unavailable on your homeserver.", "Your feedback will help make spaces better. The more detail you can go into, the better.": "Your feedback will help make spaces better. The more detail you can go into, the better.", - "Use an all rooms space instead of a home space.": "Use an all rooms space instead of a home space.", - "Show DMs for joined/invited members in the space.": "Show DMs for joined/invited members in the space.", - "Show notification badges for DMs in spaces.": "Show notification badges for DMs in spaces.", + "Show all rooms in Home": "Show all rooms in Home", + "Show people in spaces": "Show people in spaces", + "If disabled, you can still add Direct Messages to Personal Spaces. If enabled, you'll automatically see everyone who is a member of the Space.": "If disabled, you can still add Direct Messages to Personal Spaces. If enabled, you'll automatically see everyone who is a member of the Space.", + "Show notification badges for DMs in Spaces.": "Show notification badges for DMs in Spaces.", "Show options to enable 'Do not disturb' mode": "Show options to enable 'Do not disturb' mode", "Send and receive voice messages": "Send and receive voice messages", "Render LaTeX maths in messages": "Render LaTeX maths in messages", @@ -2510,7 +2511,6 @@ "Beta": "Beta", "Leave the beta": "Leave the beta", "Join the beta": "Join the beta", - "Experimental options": "Experimental options", "Avatar": "Avatar", "This room is public": "This room is public", "Away": "Away", diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index a291cd1fba..af026f4103 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -94,6 +94,9 @@ export interface ISetting { [level: SettingLevel]: string; }; + // Optional description which will be shown as microCopy under SettingsFlags + description?: string; + // The supported levels are required. Preferably, use the preset arrays // at the top of this file to define this rather than a custom array. supportedLevels?: SettingLevel[]; @@ -176,19 +179,21 @@ export const SETTINGS: {[setting: string]: ISetting} = { }, }, "feature_spaces.all_rooms": { - displayName: _td("Use an all rooms space instead of a home space."), + displayName: _td("Show all rooms in Home"), supportedLevels: LEVELS_FEATURE, default: true, controller: new ReloadOnChangeController(), }, "feature_spaces.space_member_dms": { - displayName: _td("Show DMs for joined/invited members in the space."), + displayName: _td("Show people in spaces"), + description: _td("If disabled, you can still add Direct Messages to Personal Spaces. " + + "If enabled, you'll automatically see everyone who is a member of the Space."), supportedLevels: LEVELS_FEATURE, default: true, controller: new ReloadOnChangeController(), }, "feature_spaces.space_dm_badges": { - displayName: _td("Show notification badges for DMs in spaces."), + displayName: _td("Show notification badges for DMs in Spaces."), supportedLevels: LEVELS_FEATURE, default: false, controller: new ReloadOnChangeController(), diff --git a/src/settings/SettingsStore.ts b/src/settings/SettingsStore.ts index e1e300e185..44f3d5d838 100644 --- a/src/settings/SettingsStore.ts +++ b/src/settings/SettingsStore.ts @@ -248,6 +248,16 @@ export default class SettingsStore { return _t(displayName as string); } + /** + * Gets the translated description for a given setting + * @param {string} settingName The setting to look up. + * @return {String} The description for the setting, or null if not found. + */ + public static getDescription(settingName: string) { + if (!SETTINGS[settingName]?.description) return null; + return _t(SETTINGS[settingName].description); + } + /** * Determines if a setting is also a feature. * @param {string} settingName The setting to look up. From 88ba24f36219a59e66efb85c187c1d615c54e120 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Thu, 17 Jun 2021 18:34:58 +0200 Subject: [PATCH 145/809] Fix bugs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/css/views/voip/_CallContainer.scss | 4 ++-- res/css/views/voip/_VideoFeed.scss | 2 -- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/res/css/views/voip/_CallContainer.scss b/res/css/views/voip/_CallContainer.scss index 8262075559..e51f20ff99 100644 --- a/res/css/views/voip/_CallContainer.scss +++ b/res/css/views/voip/_CallContainer.scss @@ -30,8 +30,8 @@ limitations under the License. pointer-events: initial; // restore pointer events so the user can leave/interact cursor: pointer; - .mx_CallView_video { - width: 350px; + .mx_VideoFeed_remote.mx_VideoFeed_voice { + min-height: 150px; } .mx_VideoFeed_local { diff --git a/res/css/views/voip/_VideoFeed.scss b/res/css/views/voip/_VideoFeed.scss index 7d85ac264e..4a3fbdf597 100644 --- a/res/css/views/voip/_VideoFeed.scss +++ b/res/css/views/voip/_VideoFeed.scss @@ -15,8 +15,6 @@ limitations under the License. */ .mx_VideoFeed_voice { - // We don't want to collide with the call controls that have 52px of height - padding-bottom: 52px; background-color: $inverted-bg-color; } From 1394e5b0a4a950cb8f2a17758b09fc72862493a2 Mon Sep 17 00:00:00 2001 From: Ayush PS Date: Fri, 18 Jun 2021 00:12:39 +0530 Subject: [PATCH 146/809] Updated changes for the tile Signed-off-by: Ayush PS --- res/css/views/rooms/_EventTile.scss | 5 ----- src/components/views/rooms/EventTile.tsx | 6 +++--- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index a6ccde93d3..5d1dd04383 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -130,11 +130,6 @@ $left-gutter: 64px; .mx_EventTile_msgOption { grid-column: 2; } - - .hidden { - // override the overriden padding for hidden events - padding-left: 64px !important; - } } .mx_EventTile_reply { diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index 533aa6cfab..35ccefde1e 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -824,7 +824,7 @@ export default class EventTile extends React.Component { let tileHandler = getHandlerTile(this.props.mxEvent); // Info messages are basically information about commands processed on a room - const isBubbleMessage = eventType.startsWith("m.key.verification") || + let isBubbleMessage = eventType.startsWith("m.key.verification") || (eventType === EventType.RoomMessage && msgtype && msgtype.startsWith("m.key.verification")) || (eventType === EventType.RoomCreate) || (eventType === EventType.RoomEncryption) || @@ -840,6 +840,7 @@ export default class EventTile extends React.Component { // duplicate of the thing they are replacing). if (SettingsStore.getValue("showHiddenEventsInTimeline") && !haveTileForEvent(this.props.mxEvent)) { tileHandler = "messages.ViewSourceEvent"; + isBubbleMessage = false; // Reuse info message avatar and sender profile styling isInfoMessage = true; } @@ -1135,8 +1136,7 @@ export default class EventTile extends React.Component { { ircTimestamp } { sender } { ircPadlock } -
    +
    { groupTimestamp } { groupPadlock } { thread } From 9cce5ef10f116a13167a04ae42954adc8569c1f3 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 18 Jun 2021 15:31:12 +0100 Subject: [PATCH 147/809] Consolidate types with js-sdk changes --- src/Avatar.ts | 3 +-- src/components/structures/RoomView.tsx | 8 ++------ 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/src/Avatar.ts b/src/Avatar.ts index 8ea0b0c9fa..4c4bd1c265 100644 --- a/src/Avatar.ts +++ b/src/Avatar.ts @@ -17,13 +17,12 @@ limitations under the License. import { RoomMember } from "matrix-js-sdk/src/models/room-member"; import { User } from "matrix-js-sdk/src/models/user"; import { Room } from "matrix-js-sdk/src/models/room"; +import { ResizeMethod } from "matrix-js-sdk/src/@types/partials"; import DMRoomMap from './utils/DMRoomMap'; import { mediaFromMxc } from "./customisations/Media"; import SettingsStore from "./settings/SettingsStore"; -export type ResizeMethod = "crop" | "scale"; - // Not to be used for BaseAvatar urls as that has similar default avatar fallback already export function avatarUrlForMember( member: RoomMember, diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index 1e3adcb518..d9f2d5231b 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -23,7 +23,7 @@ limitations under the License. import React, { createRef } from 'react'; import classNames from 'classnames'; -import { Room } from "matrix-js-sdk/src/models/room"; +import { IRecommendedVersion, Room } from "matrix-js-sdk/src/models/room"; import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { SearchResult } from "matrix-js-sdk/src/models/search-result"; import { EventSubscription } from "fbemitter"; @@ -172,11 +172,7 @@ export interface IState { // We load this later by asking the js-sdk to suggest a version for us. // This object is the result of Room#getRecommendedVersion() - upgradeRecommendation?: { - version: string; - needsUpgrade: boolean; - urgent: boolean; - }; + upgradeRecommendation?: IRecommendedVersion; canReact: boolean; canReply: boolean; layout: Layout; From 058cbbbd0c6c2534e390d000844ccde74683f690 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 18 Jun 2021 16:13:55 +0100 Subject: [PATCH 148/809] Fix imports --- src/autocomplete/Autocompleter.ts | 7 ++-- src/autocomplete/NotifProvider.tsx | 3 +- src/autocomplete/RoomProvider.tsx | 12 +++--- src/components/views/avatars/BaseAvatar.tsx | 9 +++-- src/components/views/avatars/GroupAvatar.tsx | 7 ++-- src/components/views/avatars/MemberAvatar.tsx | 10 ++--- src/components/views/avatars/RoomAvatar.tsx | 12 +++--- src/components/views/elements/RoomName.tsx | 8 ++-- .../views/rooms/WhoIsTypingTile.tsx | 2 +- src/customisations/Media.ts | 9 +++-- src/utils/ShieldUtils.ts | 39 ++++++++++--------- 11 files changed, 62 insertions(+), 56 deletions(-) diff --git a/src/autocomplete/Autocompleter.ts b/src/autocomplete/Autocompleter.ts index 5409825f45..ea8eddbb8d 100644 --- a/src/autocomplete/Autocompleter.ts +++ b/src/autocomplete/Autocompleter.ts @@ -15,8 +15,9 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {ReactElement} from 'react'; -import Room from 'matrix-js-sdk/src/models/room'; +import { ReactElement } from 'react'; +import { Room } from 'matrix-js-sdk/src/models/room'; + import CommandProvider from './CommandProvider'; import CommunityProvider from './CommunityProvider'; import DuckDuckGoProvider from './DuckDuckGoProvider'; @@ -24,7 +25,7 @@ import RoomProvider from './RoomProvider'; import UserProvider from './UserProvider'; import EmojiProvider from './EmojiProvider'; import NotifProvider from './NotifProvider'; -import {timeout} from "../utils/promise"; +import { timeout } from "../utils/promise"; import AutocompleteProvider, {ICommand} from "./AutocompleteProvider"; import SettingsStore from "../settings/SettingsStore"; import SpaceProvider from "./SpaceProvider"; diff --git a/src/autocomplete/NotifProvider.tsx b/src/autocomplete/NotifProvider.tsx index 0bc7ead097..827f4aa885 100644 --- a/src/autocomplete/NotifProvider.tsx +++ b/src/autocomplete/NotifProvider.tsx @@ -15,7 +15,8 @@ limitations under the License. */ import React from 'react'; -import Room from "matrix-js-sdk/src/models/room"; +import { Room } from "matrix-js-sdk/src/models/room"; + import AutocompleteProvider from './AutocompleteProvider'; import { _t } from '../languageHandler'; import {MatrixClientPeg} from '../MatrixClientPeg'; diff --git a/src/autocomplete/RoomProvider.tsx b/src/autocomplete/RoomProvider.tsx index ad55b19101..a82a757a78 100644 --- a/src/autocomplete/RoomProvider.tsx +++ b/src/autocomplete/RoomProvider.tsx @@ -17,16 +17,16 @@ limitations under the License. */ import React from "react"; -import {uniqBy, sortBy} from "lodash"; -import Room from "matrix-js-sdk/src/models/room"; +import { uniqBy, sortBy } from "lodash"; +import { Room } from "matrix-js-sdk/src/models/room"; import { _t } from '../languageHandler'; import AutocompleteProvider from './AutocompleteProvider'; -import {MatrixClientPeg} from '../MatrixClientPeg'; +import { MatrixClientPeg } from '../MatrixClientPeg'; import QueryMatcher from './QueryMatcher'; -import {PillCompletion} from './Components'; -import {makeRoomPermalink} from "../utils/permalinks/Permalinks"; -import {ICompletion, ISelectionRange} from "./Autocompleter"; +import { PillCompletion } from './Components'; +import { makeRoomPermalink } from "../utils/permalinks/Permalinks"; +import { ICompletion, ISelectionRange } from "./Autocompleter"; import RoomAvatar from '../components/views/avatars/RoomAvatar'; import SettingsStore from "../settings/SettingsStore"; diff --git a/src/components/views/avatars/BaseAvatar.tsx b/src/components/views/avatars/BaseAvatar.tsx index 6949c14636..f98f8c88a1 100644 --- a/src/components/views/avatars/BaseAvatar.tsx +++ b/src/components/views/avatars/BaseAvatar.tsx @@ -17,16 +17,17 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, {useCallback, useContext, useEffect, useState} from 'react'; +import React, { useCallback, useContext, useEffect, useState } from 'react'; import classNames from 'classnames'; +import { ResizeMethod } from 'matrix-js-sdk/src/@types/partials'; + import * as AvatarLogic from '../../../Avatar'; import SettingsStore from "../../../settings/SettingsStore"; import AccessibleButton from '../elements/AccessibleButton'; import RoomContext from "../../../contexts/RoomContext"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; -import {useEventEmitter} from "../../../hooks/useEventEmitter"; -import {toPx} from "../../../utils/units"; -import {ResizeMethod} from "../../../Avatar"; +import { useEventEmitter } from "../../../hooks/useEventEmitter"; +import { toPx } from "../../../utils/units"; import { _t } from '../../../languageHandler'; interface IProps { diff --git a/src/components/views/avatars/GroupAvatar.tsx b/src/components/views/avatars/GroupAvatar.tsx index 3734ba9504..13dbbfec09 100644 --- a/src/components/views/avatars/GroupAvatar.tsx +++ b/src/components/views/avatars/GroupAvatar.tsx @@ -15,10 +15,11 @@ limitations under the License. */ import React from 'react'; +import { ResizeMethod } from 'matrix-js-sdk/src/@types/partials'; + import BaseAvatar from './BaseAvatar'; -import {replaceableComponent} from "../../../utils/replaceableComponent"; -import {mediaFromMxc} from "../../../customisations/Media"; -import {ResizeMethod} from "../../../Avatar"; +import { replaceableComponent } from "../../../utils/replaceableComponent"; +import { mediaFromMxc } from "../../../customisations/Media"; export interface IProps { groupId?: string; diff --git a/src/components/views/avatars/MemberAvatar.tsx b/src/components/views/avatars/MemberAvatar.tsx index 3205ca372c..862563a8b4 100644 --- a/src/components/views/avatars/MemberAvatar.tsx +++ b/src/components/views/avatars/MemberAvatar.tsx @@ -16,14 +16,14 @@ limitations under the License. */ import React from 'react'; -import {RoomMember} from "matrix-js-sdk/src/models/room-member"; +import { RoomMember } from "matrix-js-sdk/src/models/room-member"; +import { ResizeMethod } from 'matrix-js-sdk/src/@types/partials'; import dis from "../../../dispatcher/dispatcher"; -import {Action} from "../../../dispatcher/actions"; +import { Action } from "../../../dispatcher/actions"; import BaseAvatar from "./BaseAvatar"; -import {replaceableComponent} from "../../../utils/replaceableComponent"; -import {mediaFromMxc} from "../../../customisations/Media"; -import {ResizeMethod} from "../../../Avatar"; +import { replaceableComponent } from "../../../utils/replaceableComponent"; +import { mediaFromMxc } from "../../../customisations/Media"; interface IProps extends Omit, "name" | "idName" | "url"> { member: RoomMember; diff --git a/src/components/views/avatars/RoomAvatar.tsx b/src/components/views/avatars/RoomAvatar.tsx index 4693d907ba..bd820509c5 100644 --- a/src/components/views/avatars/RoomAvatar.tsx +++ b/src/components/views/avatars/RoomAvatar.tsx @@ -13,17 +13,17 @@ 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, {ComponentProps} from 'react'; -import Room from 'matrix-js-sdk/src/models/room'; +import React, { ComponentProps } from 'react'; +import { Room } from 'matrix-js-sdk/src/models/room'; +import { ResizeMethod } from 'matrix-js-sdk/src/@types/partials'; import BaseAvatar from './BaseAvatar'; import ImageView from '../elements/ImageView'; -import {MatrixClientPeg} from '../../../MatrixClientPeg'; +import { MatrixClientPeg } from '../../../MatrixClientPeg'; import Modal from '../../../Modal'; import * as Avatar from '../../../Avatar'; -import {ResizeMethod} from "../../../Avatar"; -import {replaceableComponent} from "../../../utils/replaceableComponent"; -import {mediaFromMxc} from "../../../customisations/Media"; +import { replaceableComponent } from "../../../utils/replaceableComponent"; +import { mediaFromMxc } from "../../../customisations/Media"; interface IProps extends Omit, "name" | "idName" | "url" | "onClick"> { // Room may be left unset here, but if it is, diff --git a/src/components/views/elements/RoomName.tsx b/src/components/views/elements/RoomName.tsx index 9178155d19..cdd83aedc2 100644 --- a/src/components/views/elements/RoomName.tsx +++ b/src/components/views/elements/RoomName.tsx @@ -14,10 +14,10 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {useEffect, useState} from "react"; -import {Room} from "matrix-js-sdk/src/models/room"; +import React, { useEffect, useState } from "react"; +import { Room } from "matrix-js-sdk/src/models/room"; -import {useEventEmitter} from "../../../hooks/useEventEmitter"; +import { useEventEmitter } from "../../../hooks/useEventEmitter"; interface IProps { room: Room; @@ -34,7 +34,7 @@ const RoomName = ({ room, children }: IProps): JSX.Element => { }, [room]); if (children) return children(name); - return name || ""; + return <>{ name || "" }; }; export default RoomName; diff --git a/src/components/views/rooms/WhoIsTypingTile.tsx b/src/components/views/rooms/WhoIsTypingTile.tsx index 3a1d2051b4..93078ff645 100644 --- a/src/components/views/rooms/WhoIsTypingTile.tsx +++ b/src/components/views/rooms/WhoIsTypingTile.tsx @@ -16,7 +16,7 @@ limitations under the License. */ import React from 'react'; -import Room from "matrix-js-sdk/src/models/room"; +import { Room } from "matrix-js-sdk/src/models/room"; import { RoomMember } from "matrix-js-sdk/src/models/room-member"; import { MatrixEvent } from "matrix-js-sdk/src/models/event"; diff --git a/src/customisations/Media.ts b/src/customisations/Media.ts index f9d957b60c..37e91fc54b 100644 --- a/src/customisations/Media.ts +++ b/src/customisations/Media.ts @@ -14,10 +14,11 @@ * limitations under the License. */ -import {MatrixClientPeg} from "../MatrixClientPeg"; -import {IMediaEventContent, IPreparedMedia, prepEventContentAsMedia} from "./models/IMediaEventContent"; -import {ResizeMethod} from "../Avatar"; -import {MatrixClient} from "matrix-js-sdk/src/client"; +import { MatrixClient } from "matrix-js-sdk/src/client"; +import { ResizeMethod } from "matrix-js-sdk/src/@types/partials"; + +import { MatrixClientPeg } from "../MatrixClientPeg"; +import { IMediaEventContent, IPreparedMedia, prepEventContentAsMedia } from "./models/IMediaEventContent"; // Populate this class with the details of your customisations when copying it. diff --git a/src/utils/ShieldUtils.ts b/src/utils/ShieldUtils.ts index 5fe653fed0..c855b81bf8 100644 --- a/src/utils/ShieldUtils.ts +++ b/src/utils/ShieldUtils.ts @@ -1,30 +1,31 @@ +/* +Copyright 2021 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +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 { MatrixClient } from "matrix-js-sdk/src/client"; +import { Room } from "matrix-js-sdk/src/models/room"; + import DMRoomMap from './DMRoomMap'; -/* For now, a cut-down type spec for the client */ -interface Client { - getUserId: () => string; - checkUserTrust: (userId: string) => { - isCrossSigningVerified: () => boolean - wasCrossSigningVerified: () => boolean - }; - getStoredDevicesForUser: (userId: string) => [{ deviceId: string }]; - checkDeviceTrust: (userId: string, deviceId: string) => { - isVerified: () => boolean - }; -} - -interface Room { - getEncryptionTargetMembers: () => Promise<[{userId: string}]>; - roomId: string; -} - export enum E2EStatus { Warning = "warning", Verified = "verified", Normal = "normal" } -export async function shieldStatusForRoom(client: Client, room: Room): Promise { +export async function shieldStatusForRoom(client: MatrixClient, room: Room): Promise { const members = (await room.getEncryptionTargetMembers()).map(({userId}) => userId); const inDMMap = !!DMRoomMap.shared().getUserIdForRoomId(room.roomId); From 3b7c92fd9ee02961566eb52471f5883cbe12db8a Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 18 Jun 2021 16:18:42 +0100 Subject: [PATCH 149/809] Use new js-sdk types properly --- src/components/structures/RoomView.tsx | 4 ++-- src/mjolnir/BanList.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index d9f2d5231b..bfc7a1972d 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -23,7 +23,7 @@ limitations under the License. import React, { createRef } from 'react'; import classNames from 'classnames'; -import { IRecommendedVersion, Room } from "matrix-js-sdk/src/models/room"; +import { IRecommendedVersion, NotificationCountType, Room } from "matrix-js-sdk/src/models/room"; import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { SearchResult } from "matrix-js-sdk/src/models/search-result"; import { EventSubscription } from "fbemitter"; @@ -2054,7 +2054,7 @@ export default class RoomView extends React.Component { if (!this.state.atEndOfLiveTimeline && !this.state.searchResults) { const JumpToBottomButton = sdk.getComponent('rooms.JumpToBottomButton'); jumpToBottom = ( 0} + highlight={this.state.room.getUnreadNotificationCount(NotificationCountType.Highlight) > 0} numUnreadMessages={this.state.numUnreadMessages} onScrollToBottomClick={this.jumpToLiveTimeline} roomId={this.state.roomId} diff --git a/src/mjolnir/BanList.ts b/src/mjolnir/BanList.ts index 21cd5d4cf7..89eec89500 100644 --- a/src/mjolnir/BanList.ts +++ b/src/mjolnir/BanList.ts @@ -92,7 +92,7 @@ export class BanList { if (!room) return; for (const eventType of ALL_RULE_TYPES) { - const events = room.currentState.getStateEvents(eventType, undefined); + const events = room.currentState.getStateEvents(eventType); for (const ev of events) { if (!ev.getStateKey()) continue; From 0ae4e7b11de934dcd28645f55f73646722b507ee Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 18 Jun 2021 16:21:46 +0100 Subject: [PATCH 150/809] Fix typescript types --- src/SlashCommands.tsx | 7 +++--- .../views/right_panel/EncryptionPanel.tsx | 23 ++++++++++--------- src/components/views/right_panel/UserInfo.tsx | 4 ++-- src/components/views/rooms/NewRoomIntro.tsx | 21 +++++++++-------- src/dispatcher/payloads/ViewUserPayload.ts | 3 ++- src/stores/RoomViewStore.tsx | 2 +- .../room-list/filters/SpaceFilterCondition.ts | 2 +- src/utils/WidgetUtils.ts | 23 ++++++++++--------- 8 files changed, 44 insertions(+), 41 deletions(-) diff --git a/src/SlashCommands.tsx b/src/SlashCommands.tsx index 9700c57d67..bd2133f3d9 100644 --- a/src/SlashCommands.tsx +++ b/src/SlashCommands.tsx @@ -17,8 +17,8 @@ See the License for the specific language governing permissions and limitations under the License. */ - import * as React from 'react'; +import { User } from "matrix-js-sdk/src/models/user"; import * as ContentHelpers from 'matrix-js-sdk/src/content-helpers'; import {MatrixClientPeg} from './MatrixClientPeg'; @@ -1019,9 +1019,8 @@ export const Commands = [ const member = MatrixClientPeg.get().getRoom(roomId).getMember(userId); dis.dispatch({ action: Action.ViewUser, - // XXX: We should be using a real member object and not assuming what the - // receiver wants. - member: member || {userId}, + // XXX: We should be using a real member object and not assuming what the receiver wants. + member: member || { userId } as User, }); return success(); }, diff --git a/src/components/views/right_panel/EncryptionPanel.tsx b/src/components/views/right_panel/EncryptionPanel.tsx index c237a4ade6..3a26427246 100644 --- a/src/components/views/right_panel/EncryptionPanel.tsx +++ b/src/components/views/right_panel/EncryptionPanel.tsx @@ -14,28 +14,29 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, {useCallback, useEffect, useState} from "react"; +import React, { useCallback, useEffect, useState } from "react"; +import { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest"; +import { RoomMember } from "matrix-js-sdk/src/models/room-member"; +import { User } from "matrix-js-sdk/src/models/user"; +import { PHASE_REQUESTED, PHASE_UNSENT } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest"; import EncryptionInfo from "./EncryptionInfo"; import VerificationPanel from "./VerificationPanel"; -import {MatrixClientPeg} from "../../../MatrixClientPeg"; -import {ensureDMExists} from "../../../createRoom"; -import {useEventEmitter} from "../../../hooks/useEventEmitter"; +import { MatrixClientPeg } from "../../../MatrixClientPeg"; +import { ensureDMExists } from "../../../createRoom"; +import { useEventEmitter } from "../../../hooks/useEventEmitter"; import Modal from "../../../Modal"; -import {PHASE_REQUESTED, PHASE_UNSENT} from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest"; import * as sdk from "../../../index"; -import {_t} from "../../../languageHandler"; -import {VerificationRequest} from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest"; -import {RoomMember} from "matrix-js-sdk/src/models/room-member"; +import { _t } from "../../../languageHandler"; import dis from "../../../dispatcher/dispatcher"; -import {Action} from "../../../dispatcher/actions"; -import {RightPanelPhases} from "../../../stores/RightPanelStorePhases"; +import { Action } from "../../../dispatcher/actions"; +import { RightPanelPhases } from "../../../stores/RightPanelStorePhases"; // cancellation codes which constitute a key mismatch const MISMATCHES = ["m.key_mismatch", "m.user_error", "m.mismatched_sas"]; interface IProps { - member: RoomMember; + member: RoomMember | User; onClose: () => void; verificationRequest: VerificationRequest; verificationRequestPromise: Promise; diff --git a/src/components/views/right_panel/UserInfo.tsx b/src/components/views/right_panel/UserInfo.tsx index 0b37fb9dd6..8833cb6862 100644 --- a/src/components/views/right_panel/UserInfo.tsx +++ b/src/components/views/right_panel/UserInfo.tsx @@ -1594,7 +1594,7 @@ const UserInfo: React.FC = ({ content = ( @@ -1605,7 +1605,7 @@ const UserInfo: React.FC = ({ content = ( } - member={member} + member={member as User | RoomMember} onClose={onEncryptionPanelClose} isRoomEncrypted={isRoomEncrypted} /> diff --git a/src/components/views/rooms/NewRoomIntro.tsx b/src/components/views/rooms/NewRoomIntro.tsx index 3bf9a9db33..2b2958e3f3 100644 --- a/src/components/views/rooms/NewRoomIntro.tsx +++ b/src/components/views/rooms/NewRoomIntro.tsx @@ -14,25 +14,26 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, {useContext} from "react"; -import {EventType} from "matrix-js-sdk/src/@types/event"; +import React, { useContext } from "react"; +import { EventType } from "matrix-js-sdk/src/@types/event"; +import { MatrixClient } from "matrix-js-sdk/src/client"; +import { Room } from "matrix-js-sdk/src/models/room"; +import { User } from "matrix-js-sdk/src/models/user"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; import RoomContext from "../../../contexts/RoomContext"; import DMRoomMap from "../../../utils/DMRoomMap"; -import {_t} from "../../../languageHandler"; +import { _t } from "../../../languageHandler"; import AccessibleButton from "../elements/AccessibleButton"; -import MiniAvatarUploader, {AVATAR_SIZE} from "../elements/MiniAvatarUploader"; +import MiniAvatarUploader, { AVATAR_SIZE } from "../elements/MiniAvatarUploader"; import RoomAvatar from "../avatars/RoomAvatar"; import defaultDispatcher from "../../../dispatcher/dispatcher"; -import {ViewUserPayload} from "../../../dispatcher/payloads/ViewUserPayload"; -import {Action} from "../../../dispatcher/actions"; +import { ViewUserPayload } from "../../../dispatcher/payloads/ViewUserPayload"; +import { Action } from "../../../dispatcher/actions"; import dis from "../../../dispatcher/dispatcher"; import SpaceStore from "../../../stores/SpaceStore"; -import {showSpaceInvite} from "../../../utils/space"; - +import { showSpaceInvite } from "../../../utils/space"; import { privateShouldBeEncrypted } from "../../../createRoom"; - import EventTileBubble from "../messages/EventTileBubble"; import { ROOM_SECURITY_TAB } from "../dialogs/RoomSettingsDialog"; @@ -61,7 +62,7 @@ const NewRoomIntro = () => { defaultDispatcher.dispatch({ action: Action.ViewUser, // XXX: We should be using a real member object and not assuming what the receiver wants. - member: member || {userId: dmPartner}, + member: member || { userId: dmPartner } as User, }); }} /> diff --git a/src/dispatcher/payloads/ViewUserPayload.ts b/src/dispatcher/payloads/ViewUserPayload.ts index c2838d0dbb..c4d73aea6a 100644 --- a/src/dispatcher/payloads/ViewUserPayload.ts +++ b/src/dispatcher/payloads/ViewUserPayload.ts @@ -15,6 +15,7 @@ limitations under the License. */ import { RoomMember } from "matrix-js-sdk/src/models/room-member"; +import { User } from "matrix-js-sdk/src/models/user"; import { ActionPayload } from "../payloads"; import { Action } from "../actions"; @@ -25,5 +26,5 @@ export interface ViewUserPayload extends ActionPayload { * The member to view. May be null or falsy to indicate that no member * should be shown (hide whichever relevant components). */ - member?: RoomMember; + member?: RoomMember | User; } diff --git a/src/stores/RoomViewStore.tsx b/src/stores/RoomViewStore.tsx index cc3eafffcd..87978df471 100644 --- a/src/stores/RoomViewStore.tsx +++ b/src/stores/RoomViewStore.tsx @@ -276,7 +276,7 @@ class RoomViewStore extends Store { const address = this.state.roomAlias || this.state.roomId; const viaServers = this.state.viaServers || []; try { - await retry(() => cli.joinRoom(address, { + await retry(() => cli.joinRoom(address, { viaServers, ...payload.opts, }), NUM_JOIN_RETRY, (err) => { diff --git a/src/stores/room-list/filters/SpaceFilterCondition.ts b/src/stores/room-list/filters/SpaceFilterCondition.ts index 6a06bee0d8..79e258927d 100644 --- a/src/stores/room-list/filters/SpaceFilterCondition.ts +++ b/src/stores/room-list/filters/SpaceFilterCondition.ts @@ -29,7 +29,7 @@ import { setHasDiff } from "../../../utils/sets"; * + All DMs */ export class SpaceFilterCondition extends EventEmitter implements IFilterCondition, IDestroyable { - private roomIds = new Set(); + private roomIds = new Set(); private space: Room = null; public get kind(): FilterKind { diff --git a/src/utils/WidgetUtils.ts b/src/utils/WidgetUtils.ts index 7ff0529363..926278a20a 100644 --- a/src/utils/WidgetUtils.ts +++ b/src/utils/WidgetUtils.ts @@ -16,19 +16,20 @@ limitations under the License. */ import * as url from "url"; +import { Capability, IWidget, IWidgetData, MatrixCapabilities } from "matrix-widget-api"; +import { Room } from "matrix-js-sdk/src/models/room"; +import { MatrixEvent } from "matrix-js-sdk/src/models/event"; -import {MatrixClientPeg} from '../MatrixClientPeg'; +import { MatrixClientPeg } from '../MatrixClientPeg'; import SdkConfig from "../SdkConfig"; import dis from '../dispatcher/dispatcher'; import WidgetEchoStore from '../stores/WidgetEchoStore'; import SettingsStore from "../settings/SettingsStore"; -import {IntegrationManagers} from "../integrations/IntegrationManagers"; -import {Room} from "matrix-js-sdk/src/models/room"; -import {WidgetType} from "../widgets/WidgetType"; -import {objectClone} from "./objects"; -import {_t} from "../languageHandler"; -import {Capability, IWidget, IWidgetData, MatrixCapabilities} from "matrix-widget-api"; -import {IApp} from "../stores/WidgetStore"; +import { IntegrationManagers } from "../integrations/IntegrationManagers"; +import { WidgetType } from "../widgets/WidgetType"; +import { objectClone } from "./objects"; +import { _t } from "../languageHandler"; +import { IApp } from "../stores/WidgetStore"; // How long we wait for the state event echo to come back from the server // before waitFor[Room/User]Widget rejects its promise @@ -377,9 +378,9 @@ export default class WidgetUtils { return widgets.filter(w => w.content && w.content.type === "m.integration_manager"); } - static getRoomWidgetsOfType(room: Room, type: WidgetType): IWidgetEvent[] { - const widgets = WidgetUtils.getRoomWidgets(room); - return (widgets || []).filter(w => { + static getRoomWidgetsOfType(room: Room, type: WidgetType): MatrixEvent[] { + const widgets = WidgetUtils.getRoomWidgets(room) || []; + return widgets.filter(w => { const content = w.getContent(); return content.url && type.matches(content.type); }); From 233e2aa425266791f99c99aeb4f77f827c986ff9 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 18 Jun 2021 16:22:31 +0100 Subject: [PATCH 151/809] Fix bugs identified by the typescripting --- src/components/views/rooms/AuxPanel.tsx | 12 ++++++------ src/components/views/rooms/RoomTile.tsx | 4 ++-- src/stores/CommunityPrototypeStore.ts | 13 ++++++++++--- src/stores/SpaceStore.tsx | 12 +++++++++--- 4 files changed, 27 insertions(+), 14 deletions(-) diff --git a/src/components/views/rooms/AuxPanel.tsx b/src/components/views/rooms/AuxPanel.tsx index 74609cca13..f6cc0f4d45 100644 --- a/src/components/views/rooms/AuxPanel.tsx +++ b/src/components/views/rooms/AuxPanel.tsx @@ -15,10 +15,12 @@ limitations under the License. */ import React from 'react'; -import { MatrixClientPeg } from "../../../MatrixClientPeg"; -import { Room } from 'matrix-js-sdk/src/models/room' -import AppsDrawer from './AppsDrawer'; import classNames from 'classnames'; +import { lexicographicCompare } from 'matrix-js-sdk/src/utils'; +import { Room } from 'matrix-js-sdk/src/models/room' + +import { MatrixClientPeg } from "../../../MatrixClientPeg"; +import AppsDrawer from './AppsDrawer'; import RateLimitedFunc from '../../../ratelimitedfunc'; import SettingsStore from "../../../settings/SettingsStore"; import AutoHideScrollbar from "../../structures/AutoHideScrollbar"; @@ -106,9 +108,7 @@ export default class AuxPanel extends React.Component { if (this.props.room && SettingsStore.getValue("feature_state_counters")) { const stateEvs = this.props.room.currentState.getStateEvents('re.jki.counter'); - stateEvs.sort((a, b) => { - return a.getStateKey() < b.getStateKey(); - }); + stateEvs.sort((a, b) => lexicographicCompare(a.getStateKey(), b.getStateKey())); for (const ev of stateEvs) { const title = ev.getContent().title; diff --git a/src/components/views/rooms/RoomTile.tsx b/src/components/views/rooms/RoomTile.tsx index aae182eca4..310ff29010 100644 --- a/src/components/views/rooms/RoomTile.tsx +++ b/src/components/views/rooms/RoomTile.tsx @@ -119,7 +119,7 @@ export default class RoomTile extends React.PureComponent { }; private onLocalEchoUpdated = (ev: MatrixEvent, room: Room) => { - if (!room?.roomId === this.props.room.roomId) return; + if (room?.roomId !== this.props.room.roomId) return; this.setState({hasUnsentEvents: this.countUnsentEvents() > 0}); }; @@ -316,7 +316,7 @@ export default class RoomTile extends React.PureComponent { 0, )); } else { - console.warn(`Unexpected tag ${tagId} applied to ${this.props.room.room_id}`); + console.warn(`Unexpected tag ${tagId} applied to ${this.props.room.roomId}`); } if ((ev as React.KeyboardEvent).key === Key.ENTER) { diff --git a/src/stores/CommunityPrototypeStore.ts b/src/stores/CommunityPrototypeStore.ts index 023845c9ee..a6f4574a58 100644 --- a/src/stores/CommunityPrototypeStore.ts +++ b/src/stores/CommunityPrototypeStore.ts @@ -107,8 +107,9 @@ export class CommunityPrototypeStore extends AsyncStoreWithClient { const pl = generalChat.currentState.getStateEvents("m.room.power_levels", ""); if (!pl) return this.isAdminOf(communityId); + const plContent = pl.getContent(); - const invitePl = isNullOrUndefined(pl.invite) ? 50 : Number(pl.invite); + const invitePl = isNullOrUndefined(plContent.invite) ? 50 : Number(plContent.invite); return invitePl <= myMember.powerLevel; } @@ -159,10 +160,16 @@ export class CommunityPrototypeStore extends AsyncStoreWithClient { if (SettingsStore.getValue("feature_communities_v2_prototypes")) { const data = this.matrixClient.getAccountData("im.vector.group_info." + roomId); if (data && data.getContent()) { - return {displayName: data.getContent().name, avatarMxc: data.getContent().avatar_url}; + return { + displayName: data.getContent().name, + avatarMxc: data.getContent().avatar_url, + }; } } - return {displayName: room.name, avatarMxc: room.avatar_url}; + return { + displayName: room.name, + avatarMxc: room.getMxcAvatarUrl(), + }; } protected async onReady(): Promise { diff --git a/src/stores/SpaceStore.tsx b/src/stores/SpaceStore.tsx index 40997d30a8..9463949aff 100644 --- a/src/stores/SpaceStore.tsx +++ b/src/stores/SpaceStore.tsx @@ -133,7 +133,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { // if the space being selected is an invite then always view that invite // else if the last viewed room in this space is joined then view that // else view space home or home depending on what is being clicked on - if (space?.getMyMembership !== "invite" && + if (space?.getMyMembership() !== "invite" && this.matrixClient?.getRoom(roomId)?.getMyMembership() === "join" ) { defaultDispatcher.dispatch({ @@ -423,8 +423,14 @@ export class SpaceStoreClass extends AsyncStoreWithClient { parent = this.rootSpaces.find(s => this.spaceFilteredRooms.get(s.roomId)?.has(roomId)); } if (!parent) { - const parents = Array.from(this.parentMap.get(roomId) || []); - parent = parents.find(p => this.matrixClient.getRoom(p)); + const parentIds = Array.from(this.parentMap.get(roomId) || []); + for (const parentId of parentIds) { + const room = this.matrixClient.getRoom(parentId); + if (room) { + parent = room; + break; + } + } } // don't trigger a context switch when we are switching a space to match the chosen room From ab94b284b8325f5da9cf7e662a6f92288b887149 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 18 Jun 2021 16:29:10 +0100 Subject: [PATCH 152/809] Updates around the use of private fields out of class --- src/components/views/rooms/NewRoomIntro.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/views/rooms/NewRoomIntro.tsx b/src/components/views/rooms/NewRoomIntro.tsx index 2b2958e3f3..cae86846d9 100644 --- a/src/components/views/rooms/NewRoomIntro.tsx +++ b/src/components/views/rooms/NewRoomIntro.tsx @@ -37,8 +37,8 @@ import { privateShouldBeEncrypted } from "../../../createRoom"; import EventTileBubble from "../messages/EventTileBubble"; import { ROOM_SECURITY_TAB } from "../dialogs/RoomSettingsDialog"; -function hasExpectedEncryptionSettings(room): boolean { - const isEncrypted: boolean = room._client?.isRoomEncrypted(room.roomId); +function hasExpectedEncryptionSettings(matrixClient: MatrixClient, room: Room): boolean { + const isEncrypted: boolean = matrixClient.isRoomEncrypted(room.roomId); const isPublic: boolean = room.getJoinRule() === "public"; return isPublic || !privateShouldBeEncrypted() || isEncrypted; } @@ -195,7 +195,7 @@ const NewRoomIntro = () => { return
    - { !hasExpectedEncryptionSettings(room) && ( + { !hasExpectedEncryptionSettings(cli, room) && ( Date: Fri, 18 Jun 2021 17:29:12 +0200 Subject: [PATCH 153/809] Add PR template MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- .github/PULL_REQUEST_TEMPLATE.md | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .github/PULL_REQUEST_TEMPLATE.md diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000000..c9d11f02c8 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,3 @@ + + + From 8a3dc1bbdf6e5482e54b8bf159911a1b273a2e18 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 18 Jun 2021 17:56:18 +0100 Subject: [PATCH 154/809] fix tests --- test/components/structures/MessagePanel-test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/components/structures/MessagePanel-test.js b/test/components/structures/MessagePanel-test.js index 8f0242eb30..d32970a278 100644 --- a/test/components/structures/MessagePanel-test.js +++ b/test/components/structures/MessagePanel-test.js @@ -42,7 +42,7 @@ import DMRoomMap from "../../../src/utils/DMRoomMap"; configure({ adapter: new Adapter() }); let client; -const room = new Matrix.Room(); +const room = new Matrix.Room("!roomId:server_name"); // wrap MessagePanel with a component which provides the MatrixClient in the context. class WrappedMessagePanel extends React.Component { From 946317ddf87bdb0178a7bf92824204e00a27166f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Fri, 18 Jun 2021 19:04:55 +0200 Subject: [PATCH 155/809] Move moving out of state MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/voip/CallPreview.tsx | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/components/views/voip/CallPreview.tsx b/src/components/views/voip/CallPreview.tsx index 13c272bebb..865a3f34af 100644 --- a/src/components/views/voip/CallPreview.tsx +++ b/src/components/views/voip/CallPreview.tsx @@ -60,9 +60,6 @@ interface IState { // Position of the CallPreview translationX: number; translationY: number; - - // True if the CallPreview is being dragged - moving: boolean; } // Splits a list of calls into one 'primary' one and a list @@ -121,7 +118,6 @@ export default class CallPreview extends React.Component { secondaryCall: secondaryCalls[0], translationX: UIStore.instance.windowWidth - DEFAULT_X_OFFSET - PIP_VIEW_WIDTH, translationY: DEFAULT_Y_OFFSET, - moving: false, }; } @@ -129,6 +125,7 @@ export default class CallPreview extends React.Component { private initX = 0; private initY = 0; + private moving = false; public componentDidMount() { CallHandler.sharedInstance().addListener(CallHandlerEvent.CallChangeRoom, this.updateCalls); @@ -240,14 +237,14 @@ export default class CallPreview extends React.Component { event.preventDefault(); event.stopPropagation(); - this.setState({moving: true}); + this.moving = true; this.initX = event.pageX - this.state.translationX; this.initY = event.pageY - this.state.translationY; }; private onMoving = (event: React.MouseEvent | MouseEvent) => { - if (!this.state.moving) return; + if (!this.moving) return; event.preventDefault(); event.stopPropagation(); @@ -256,7 +253,7 @@ export default class CallPreview extends React.Component { }; private onEndMoving = () => { - this.setState({moving: false}); + this.moving = false; }; public render() { From 958d4df957f065d321c08e04e73428c5020a6455 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 18 Jun 2021 18:32:45 +0100 Subject: [PATCH 156/809] Naive attempt at improving our end-to-end tests in Github Actions --- .github/workflows/develop.yml | 7 +++++-- ...d-tests.sh => prepare-end-to-end-tests.sh} | 11 +---------- scripts/ci/run-end-to-end-tests.sh | 19 +++++++++++++++++++ 3 files changed, 25 insertions(+), 12 deletions(-) rename scripts/ci/{end-to-end-tests.sh => prepare-end-to-end-tests.sh} (65%) create mode 100755 scripts/ci/run-end-to-end-tests.sh diff --git a/.github/workflows/develop.yml b/.github/workflows/develop.yml index 6410bd28fa..3c3807e33b 100644 --- a/.github/workflows/develop.yml +++ b/.github/workflows/develop.yml @@ -11,10 +11,13 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v2 - - name: End-to-End tests - run: ./scripts/ci/end-to-end-tests.sh + - name: Prepare End-to-End tests + run: ./scripts/ci/prepare-end-to-end-tests.sh + - name: Run End-to-End tests + run: ./scripts/ci/run-end-to-end-tests.sh - name: Archive logs uses: actions/upload-artifact@v2 + if: ${{ always() }} with: path: | test/end-to-end-tests/logs/**/* diff --git a/scripts/ci/end-to-end-tests.sh b/scripts/ci/prepare-end-to-end-tests.sh similarity index 65% rename from scripts/ci/end-to-end-tests.sh rename to scripts/ci/prepare-end-to-end-tests.sh index edb8870d8e..147e1f6445 100755 --- a/scripts/ci/end-to-end-tests.sh +++ b/scripts/ci/prepare-end-to-end-tests.sh @@ -1,8 +1,4 @@ #!/bin/bash -# -# script which is run by the CI build (after `yarn test`). -# -# clones element-web develop and runs the tests against our version of react-sdk. set -ev @@ -19,7 +15,7 @@ cd element-web element_web_dir=`pwd` CI_PACKAGE=true yarn build cd .. -# run end to end tests +# prepare end to end tests pushd test/end-to-end-tests ln -s $element_web_dir element/element-web # PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true ./install.sh @@ -28,9 +24,4 @@ echo "--- Install synapse & other dependencies" ./install.sh # install static webserver to server symlinked local copy of element ./element/install-webserver.sh -rm -r logs || true -mkdir logs -echo "+++ Running end-to-end tests" -TESTS_STARTED=1 -./run.sh --no-sandbox --log-directory logs/ popd diff --git a/scripts/ci/run-end-to-end-tests.sh b/scripts/ci/run-end-to-end-tests.sh new file mode 100755 index 0000000000..3c99391fc7 --- /dev/null +++ b/scripts/ci/run-end-to-end-tests.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +set -ev + +handle_error() { + EXIT_CODE=$? + exit $EXIT_CODE +} + +trap 'handle_error' ERR + +# run end to end tests +pushd test/end-to-end-tests +rm -r logs || true +mkdir logs +echo "--- Running end-to-end tests" +TESTS_STARTED=1 +./run.sh --no-sandbox --log-directory logs/ +popd From 6271c5c3d8344e1c135f8f0736089e5d9945f39d Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Fri, 18 Jun 2021 18:59:22 +0100 Subject: [PATCH 157/809] first iteration for message bubble layout --- res/css/_components.scss | 1 + res/css/views/messages/_MImageBody.scss | 1 - res/css/views/messages/_ReactionsRow.scss | 1 + res/css/views/rooms/_EventBubbleTile.scss | 149 ++++ res/css/views/rooms/_EventTile.scss | 769 +++++++++--------- .../views/elements/EventListSummary.tsx | 8 +- src/components/views/messages/UnknownBody.js | 3 +- src/components/views/rooms/EventTile.tsx | 17 +- 8 files changed, 559 insertions(+), 390 deletions(-) create mode 100644 res/css/views/rooms/_EventBubbleTile.scss diff --git a/res/css/_components.scss b/res/css/_components.scss index 56403ea190..67831e4a60 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -194,6 +194,7 @@ @import "./views/rooms/_EditMessageComposer.scss"; @import "./views/rooms/_EntityTile.scss"; @import "./views/rooms/_EventTile.scss"; +@import "./views/rooms/_EventBubbleTile.scss"; @import "./views/rooms/_GroupLayout.scss"; @import "./views/rooms/_IRCLayout.scss"; @import "./views/rooms/_JumpToBottomButton.scss"; diff --git a/res/css/views/messages/_MImageBody.scss b/res/css/views/messages/_MImageBody.scss index 1c773c2f06..6ac6767ffa 100644 --- a/res/css/views/messages/_MImageBody.scss +++ b/res/css/views/messages/_MImageBody.scss @@ -16,7 +16,6 @@ limitations under the License. .mx_MImageBody { display: block; - margin-right: 34px; } .mx_MImageBody_thumbnail { diff --git a/res/css/views/messages/_ReactionsRow.scss b/res/css/views/messages/_ReactionsRow.scss index e05065eb02..b2bca6dfb3 100644 --- a/res/css/views/messages/_ReactionsRow.scss +++ b/res/css/views/messages/_ReactionsRow.scss @@ -26,6 +26,7 @@ limitations under the License. height: 24px; vertical-align: middle; margin-left: 4px; + margin-right: 4px; &::before { content: ''; diff --git a/res/css/views/rooms/_EventBubbleTile.scss b/res/css/views/rooms/_EventBubbleTile.scss new file mode 100644 index 0000000000..28dce730ff --- /dev/null +++ b/res/css/views/rooms/_EventBubbleTile.scss @@ -0,0 +1,149 @@ +/* +Copyright 2021 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +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_EventTile[data-layout=bubble] { + + --avatarSize: 32px; + --gutterSize: 7px; + --cornerRadius: 5px; + + --maxWidth: 70%; + + position: relative; + margin-top: var(--gutterSize); + margin-left: var(--avatarSize); + margin-right: var(--avatarSize); + padding: 2px 0; + + &:hover { + background: rgb(242, 242, 242); + } + + .mx_SenderProfile, + .mx_EventTile_line { + width: fit-content; + max-width: 70%; + background: var(--backgroundColor); + } + + .mx_SenderProfile { + display: none; + padding: var(--gutterSize) var(--gutterSize) 0 var(--gutterSize); + border-top-left-radius: var(--cornerRadius); + border-top-right-radius: var(--cornerRadius); + } + + .mx_EventTile_line { + padding: var(--gutterSize); + border-radius: var(--cornerRadius); + } + + /* + .mx_SenderProfile + .mx_EventTile_line { + padding-top: 0; + border-top-left-radius: 0; + border-top-right-radius: 0; + } + */ + + .mx_EventTile_avatar { + position: absolute; + top: 0; + img { + border: 2px solid #fff; + border-radius: 50%; + } + } + + &[data-self=true] { + .mx_EventTile_line { + float: right; + } + .mx_ReactionsRow { + float: right; + clear: right; + display: flex; + + /* Moving the "add reaction button" before the reactions */ + > :last-child { + order: -1; + } + } + .mx_EventTile_avatar { + right: calc(-1 * var(--avatarSize)); + } + --backgroundColor: #F8FDFC; + } + + &:not([data-self=true]) { + .mx_EventTile_avatar { + left: calc(-1 * var(--avatarSize)); + } + --backgroundColor: #F7F8F9; + } + + &.mx_EventTile_bubbleContainer, + &.mx_EventTile_info, + & ~ .mx_EventListSummary[data-expanded=false] { + + --backgroundColor: transparent; + + display: flex; + align-items: center; + justify-content: center; + + .mx_EventTile_avatar { + position: static; + order: -1; + } + } + + & ~ .mx_EventListSummary { + --maxWidth: 95%; + .mx_EventListSummary_toggle { + float: none; + margin: 0; + order: 9; + } + } + + & + .mx_EventListSummary { + .mx_EventTile { + margin-top: 0; + padding: 0; + } + } + + .mx_EventListSummary_toggle { + margin-right: 55px; + } + + .mx_EventTile_line { + display: flex; + gap: var(--gutterSize); + > a { + order: 999; /* always display the timestamp as the last item */ + align-self: flex-end; + } + } + + .mx_EventTile_readAvatars { + position: absolute; + right: 0; + bottom: 0; + } + +} diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index 3af266caee..303118d57c 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -1,6 +1,6 @@ /* Copyright 2015, 2016 OpenMarket Ltd -Copyright 2020 The Matrix.org Foundation C.I.C. +Copyright 2020-2021 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -18,101 +18,382 @@ limitations under the License. $left-gutter: 64px; $hover-select-border: 4px; -.mx_EventTile { +.mx_EventTile:not([data-layout=bubble]) { max-width: 100%; clear: both; padding-top: 18px; font-size: $font-14px; position: relative; -} -.mx_EventTile.mx_EventTile_info { - padding-top: 1px; -} - -.mx_EventTile_avatar { - top: 14px; - left: 8px; - cursor: pointer; - user-select: none; -} - -.mx_EventTile.mx_EventTile_info .mx_EventTile_avatar { - top: $font-6px; - left: $left-gutter; -} - -.mx_EventTile_continuation { - padding-top: 0px !important; - - &.mx_EventTile_isEditing { - padding-top: 5px !important; - margin-top: -5px; + .mx_EventTile.mx_EventTile_info { + padding-top: 1px; } -} -.mx_EventTile_isEditing { - background-color: $header-panel-bg-color; -} + .mx_EventTile_avatar { + top: 14px; + left: 8px; + cursor: pointer; + user-select: none; + } -.mx_EventTile .mx_SenderProfile { - color: $primary-fg-color; - font-size: $font-14px; - display: inline-block; /* anti-zalgo, with overflow hidden */ - overflow: hidden; - cursor: pointer; - padding-bottom: 0px; - padding-top: 0px; - margin: 0px; - /* the next three lines, along with overflow hidden, truncate long display names */ - white-space: nowrap; - text-overflow: ellipsis; - max-width: calc(100% - $left-gutter); -} + .mx_EventTile.mx_EventTile_info .mx_EventTile_avatar { + top: $font-6px; + left: $left-gutter; + } -.mx_EventTile .mx_SenderProfile .mx_Flair { - opacity: 0.7; - margin-left: 5px; - display: inline-block; - vertical-align: top; - overflow: hidden; - user-select: none; + .mx_EventTile_continuation { + padding-top: 0px !important; - img { - vertical-align: -2px; - margin-right: 2px; + &.mx_EventTile_isEditing { + padding-top: 5px !important; + margin-top: -5px; + } + } + + .mx_EventTile_isEditing { + background-color: $header-panel-bg-color; + } + + .mx_EventTile .mx_SenderProfile { + color: $primary-fg-color; + font-size: $font-14px; + display: inline-block; /* anti-zalgo, with overflow hidden */ + overflow: hidden; + cursor: pointer; + padding-bottom: 0px; + padding-top: 0px; + margin: 0px; + /* the next three lines, along with overflow hidden, truncate long display names */ + white-space: nowrap; + text-overflow: ellipsis; + max-width: calc(100% - $left-gutter); + } + + .mx_EventTile .mx_SenderProfile .mx_Flair { + opacity: 0.7; + margin-left: 5px; + display: inline-block; + vertical-align: top; + overflow: hidden; + user-select: none; + + img { + vertical-align: -2px; + margin-right: 2px; + border-radius: 8px; + } + } + + .mx_EventTile_isEditing .mx_MessageTimestamp { + visibility: hidden; + } + + .mx_EventTile .mx_MessageTimestamp { + display: block; + white-space: nowrap; + left: 0px; + text-align: center; + user-select: none; + } + + .mx_EventTile_continuation .mx_EventTile_line { + clear: both; + } + + .mx_EventTile_line, .mx_EventTile_reply { + position: relative; + padding-left: $left-gutter; border-radius: 8px; } -} -.mx_EventTile_isEditing .mx_MessageTimestamp { - visibility: hidden; -} + .mx_RoomView_timeline_rr_enabled, + // on ELS we need the margin to allow interaction with the expand/collapse button which is normally in the RR gutter + .mx_EventListSummary { + .mx_EventTile_line { + /* ideally should be 100px, but 95px gives us a max thumbnail size of 800x600, which is nice */ + margin-right: 110px; + } + } -.mx_EventTile .mx_MessageTimestamp { - display: block; - white-space: nowrap; - left: 0px; - text-align: center; - user-select: none; -} + .mx_EventTile_reply { + margin-right: 10px; + } -.mx_EventTile_continuation .mx_EventTile_line { - clear: both; -} + .mx_EventTile_selected > div > a > .mx_MessageTimestamp { + left: calc(-$hover-select-border); + } -.mx_EventTile_line, .mx_EventTile_reply { - position: relative; - padding-left: $left-gutter; - border-radius: 8px; -} + .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; + } -.mx_RoomView_timeline_rr_enabled, -// on ELS we need the margin to allow interaction with the expand/collapse button which is normally in the RR gutter -.mx_EventListSummary { - .mx_EventTile_line { - /* ideally should be 100px, but 95px gives us a max thumbnail size of 800x600, which is nice */ - margin-right: 110px; + /* this is used for the tile for the event which is selected via the URL. + * TODO: ultimately we probably want some transition on here. + */ + .mx_EventTile_selected > .mx_EventTile_line { + border-left: $accent-color 4px solid; + padding-left: calc($left-gutter - $hover-select-border); + background-color: $event-selected-color; + } + + .mx_EventTile_highlight, + .mx_EventTile_highlight .markdown-body { + color: $event-highlight-fg-color; + + .mx_EventTile_line { + background-color: $event-highlight-bg-color; + } + } + + .mx_EventTile_info .mx_EventTile_line { + padding-left: calc($left-gutter + 18px); + } + + .mx_EventTile_selected.mx_EventTile_info .mx_EventTile_line { + padding-left: calc($left-gutter + 18px - $hover-select-border); + } + + .mx_EventTile:hover .mx_EventTile_line, + .mx_EventTile.mx_EventTile_actionBarFocused .mx_EventTile_line, + .mx_EventTile.focus-visible:focus-within .mx_EventTile_line { + background-color: $event-selected-color; + } + + .mx_EventTile_searchHighlight { + background-color: $accent-color; + color: $accent-fg-color; + border-radius: 5px; + padding-left: 2px; + padding-right: 2px; + cursor: pointer; + } + + .mx_EventTile_searchHighlight a { + background-color: $accent-color; + color: $accent-fg-color; + } + + .mx_EventTile_receiptSent, + .mx_EventTile_receiptSending { + // We don't use `position: relative` on the element because then it won't line + // up with the other read receipts + + &::before { + background-color: $tertiary-fg-color; + mask-repeat: no-repeat; + mask-position: center; + mask-size: 14px; + width: 14px; + height: 14px; + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + } + } + .mx_EventTile_receiptSent::before { + mask-image: url('$(res)/img/element-icons/circle-sent.svg'); + } + .mx_EventTile_receiptSending::before { + mask-image: url('$(res)/img/element-icons/circle-sending.svg'); + } + + .mx_EventTile_contextual { + opacity: 0.4; + } + + .mx_EventTile_msgOption { + float: right; + text-align: right; + position: relative; + width: 90px; + + /* Hack to stop the height of this pushing the messages apart. + Replaces margin-top: -6px. This interacts better with a read + marker being in between. Content overflows. */ + height: 1px; + + margin-right: 10px; + } + + .mx_EventTile_msgOption a { + text-decoration: none; + } + + /* all the overflow-y: hidden; are to trap Zalgos - + but they introduce an implicit overflow-x: auto. + so make that explicitly hidden too to avoid random + horizontal scrollbars occasionally appearing, like in + https://github.com/vector-im/vector-web/issues/1154 + */ + .mx_EventTile_content { + display: block; + overflow-y: hidden; + overflow-x: hidden; + margin-right: 34px; + } + + /* De-zalgoing */ + .mx_EventTile_body { + overflow-y: hidden; + } + + /* Spoiler stuff */ + .mx_EventTile_spoiler { + cursor: pointer; + } + + .mx_EventTile_spoiler_reason { + color: $event-timestamp-color; + font-size: $font-11px; + } + + .mx_EventTile_spoiler_content { + filter: blur(5px) saturate(0.1) sepia(1); + transition-duration: 0.5s; + } + + .mx_EventTile_spoiler.visible > .mx_EventTile_spoiler_content { + filter: none; + } + + .mx_EventTile_e2eIcon { + position: absolute; + top: 6px; + left: 44px; + width: 14px; + height: 14px; + display: block; + bottom: 0; + right: 0; + opacity: 0.2; + background-repeat: no-repeat; + background-size: contain; + + &::before, &::after { + content: ""; + display: block; + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + mask-repeat: no-repeat; + mask-position: center; + mask-size: contain; + } + + &::before { + background-color: #ffffff; + mask-image: url('$(res)/img/e2e/normal.svg'); + mask-repeat: no-repeat; + mask-position: center; + mask-size: 90%; + } + } + + .mx_EventTile_e2eIcon_undecryptable, .mx_EventTile_e2eIcon_unverified { + &::after { + mask-image: url('$(res)/img/e2e/warning.svg'); + background-color: $notice-primary-color; + } + opacity: 1; + } + + .mx_EventTile_e2eIcon_unknown { + &::after { + mask-image: url('$(res)/img/e2e/warning.svg'); + background-color: $notice-primary-color; + } + opacity: 1; + } + + .mx_EventTile_e2eIcon_unencrypted { + &::after { + mask-image: url('$(res)/img/e2e/warning.svg'); + background-color: $notice-primary-color; + } + opacity: 1; + } + + .mx_EventTile_e2eIcon_unauthenticated { + &::after { + mask-image: url('$(res)/img/e2e/normal.svg'); + background-color: $composer-e2e-icon-color; + } + opacity: 1; + } + + .mx_EventTile:hover.mx_EventTile_verified .mx_EventTile_line, + .mx_EventTile:hover.mx_EventTile_unverified .mx_EventTile_line, + .mx_EventTile:hover.mx_EventTile_unknown .mx_EventTile_line { + padding-left: calc($left-gutter - $hover-select-border); + } + + .mx_EventTile:hover.mx_EventTile_verified .mx_EventTile_line { + border-left: $e2e-verified-color $EventTile_e2e_state_indicator_width solid; + } + + .mx_EventTile:hover.mx_EventTile_unverified .mx_EventTile_line { + border-left: $e2e-unverified-color $EventTile_e2e_state_indicator_width solid; + } + + .mx_EventTile:hover.mx_EventTile_unknown .mx_EventTile_line { + border-left: $e2e-unknown-color $EventTile_e2e_state_indicator_width solid; + } + + .mx_EventTile:hover.mx_EventTile_verified.mx_EventTile_info .mx_EventTile_line, + .mx_EventTile:hover.mx_EventTile_unverified.mx_EventTile_info .mx_EventTile_line, + .mx_EventTile:hover.mx_EventTile_unknown.mx_EventTile_info .mx_EventTile_line { + padding-left: calc($left-gutter + 18px - $hover-select-border); + } + + /* End to end encryption stuff */ + .mx_EventTile:hover .mx_EventTile_e2eIcon { + opacity: 1; + } + + // Explicit relationships so that it doesn't apply to nested EventTile components (e.g in Replies) + .mx_EventTile:hover.mx_EventTile_verified .mx_EventTile_line > a > .mx_MessageTimestamp, + .mx_EventTile:hover.mx_EventTile_unverified .mx_EventTile_line > a > .mx_MessageTimestamp, + .mx_EventTile:hover.mx_EventTile_unknown .mx_EventTile_line > a > .mx_MessageTimestamp { + left: calc(-$hover-select-border); + } + + // Explicit relationships so that it doesn't apply to nested EventTile components (e.g in Replies) + .mx_EventTile:hover.mx_EventTile_verified .mx_EventTile_line > .mx_EventTile_e2eIcon, + .mx_EventTile:hover.mx_EventTile_unverified .mx_EventTile_line > .mx_EventTile_e2eIcon, + .mx_EventTile:hover.mx_EventTile_unknown .mx_EventTile_line > .mx_EventTile_e2eIcon { + display: block; + left: 41px; + } + + .mx_EventTile_tileError { + color: red; + text-align: center; + + // Remove some of the default tile padding so that the error is centered + margin-right: 0; + .mx_EventTile_line { + padding-left: 0; + margin-right: 0; + } + + .mx_EventTile_line span { + padding: 4px 8px; + } + + a { + margin-left: 1em; + } + } + + .mx_MImageBody { + margin-right: 34px; } } @@ -132,121 +413,6 @@ $hover-select-border: 4px; } } -.mx_EventTile_reply { - margin-right: 10px; -} - -/* HACK to override line-height which is already marked important elsewhere */ -.mx_EventTile_bigEmoji.mx_EventTile_bigEmoji { - font-size: 48px !important; - line-height: 57px !important; -} - -.mx_EventTile_selected > div > a > .mx_MessageTimestamp { - left: calc(-$hover-select-border); -} - -.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; -} - -/* this is used for the tile for the event which is selected via the URL. - * TODO: ultimately we probably want some transition on here. - */ -.mx_EventTile_selected > .mx_EventTile_line { - border-left: $accent-color 4px solid; - padding-left: calc($left-gutter - $hover-select-border); - background-color: $event-selected-color; -} - -.mx_EventTile_highlight, -.mx_EventTile_highlight .markdown-body { - color: $event-highlight-fg-color; - - .mx_EventTile_line { - background-color: $event-highlight-bg-color; - } -} - -.mx_EventTile_info .mx_EventTile_line { - padding-left: calc($left-gutter + 18px); -} - -.mx_EventTile_selected.mx_EventTile_info .mx_EventTile_line { - padding-left: calc($left-gutter + 18px - $hover-select-border); -} - -.mx_EventTile:hover .mx_EventTile_line, -.mx_EventTile.mx_EventTile_actionBarFocused .mx_EventTile_line, -.mx_EventTile.focus-visible:focus-within .mx_EventTile_line { - background-color: $event-selected-color; -} - -.mx_EventTile_searchHighlight { - background-color: $accent-color; - color: $accent-fg-color; - border-radius: 5px; - padding-left: 2px; - padding-right: 2px; - cursor: pointer; -} - -.mx_EventTile_searchHighlight a { - background-color: $accent-color; - color: $accent-fg-color; -} - -.mx_EventTile_receiptSent, -.mx_EventTile_receiptSending { - // We don't use `position: relative` on the element because then it won't line - // up with the other read receipts - - &::before { - background-color: $tertiary-fg-color; - mask-repeat: no-repeat; - mask-position: center; - mask-size: 14px; - width: 14px; - height: 14px; - content: ''; - position: absolute; - top: 0; - left: 0; - right: 0; - } -} -.mx_EventTile_receiptSent::before { - mask-image: url('$(res)/img/element-icons/circle-sent.svg'); -} -.mx_EventTile_receiptSending::before { - mask-image: url('$(res)/img/element-icons/circle-sending.svg'); -} - -.mx_EventTile_contextual { - opacity: 0.4; -} - -.mx_EventTile_msgOption { - float: right; - text-align: right; - position: relative; - width: 90px; - - /* Hack to stop the height of this pushing the messages apart. - Replaces margin-top: -6px. This interacts better with a read - marker being in between. Content overflows. */ - height: 1px; - - margin-right: 10px; -} - -.mx_EventTile_msgOption a { - text-decoration: none; -} - .mx_EventTile_readAvatars { position: relative; display: inline-block; @@ -277,180 +443,10 @@ $hover-select-border: 4px; position: absolute; } -/* all the overflow-y: hidden; are to trap Zalgos - - but they introduce an implicit overflow-x: auto. - so make that explicitly hidden too to avoid random - horizontal scrollbars occasionally appearing, like in - https://github.com/vector-im/vector-web/issues/1154 - */ -.mx_EventTile_content { - display: block; - overflow-y: hidden; - overflow-x: hidden; - margin-right: 34px; -} - -/* De-zalgoing */ -.mx_EventTile_body { - overflow-y: hidden; -} - -/* Spoiler stuff */ -.mx_EventTile_spoiler { - cursor: pointer; -} - -.mx_EventTile_spoiler_reason { - color: $event-timestamp-color; - font-size: $font-11px; -} - -.mx_EventTile_spoiler_content { - filter: blur(5px) saturate(0.1) sepia(1); - transition-duration: 0.5s; -} - -.mx_EventTile_spoiler.visible > .mx_EventTile_spoiler_content { - filter: none; -} - -.mx_EventTile_e2eIcon { - position: absolute; - top: 6px; - left: 44px; - width: 14px; - height: 14px; - display: block; - bottom: 0; - right: 0; - opacity: 0.2; - background-repeat: no-repeat; - background-size: contain; - - &::before, &::after { - content: ""; - display: block; - position: absolute; - top: 0; - bottom: 0; - left: 0; - right: 0; - mask-repeat: no-repeat; - mask-position: center; - mask-size: contain; - } - - &::before { - background-color: #ffffff; - mask-image: url('$(res)/img/e2e/normal.svg'); - mask-repeat: no-repeat; - mask-position: center; - mask-size: 90%; - } -} - -.mx_EventTile_e2eIcon_undecryptable, .mx_EventTile_e2eIcon_unverified { - &::after { - mask-image: url('$(res)/img/e2e/warning.svg'); - background-color: $notice-primary-color; - } - opacity: 1; -} - -.mx_EventTile_e2eIcon_unknown { - &::after { - mask-image: url('$(res)/img/e2e/warning.svg'); - background-color: $notice-primary-color; - } - opacity: 1; -} - -.mx_EventTile_e2eIcon_unencrypted { - &::after { - mask-image: url('$(res)/img/e2e/warning.svg'); - background-color: $notice-primary-color; - } - opacity: 1; -} - -.mx_EventTile_e2eIcon_unauthenticated { - &::after { - mask-image: url('$(res)/img/e2e/normal.svg'); - background-color: $composer-e2e-icon-color; - } - opacity: 1; -} - -.mx_EventTile_keyRequestInfo { - font-size: $font-12px; -} - -.mx_EventTile_keyRequestInfo_text { - opacity: 0.5; -} - -.mx_EventTile_keyRequestInfo_text a { - color: $primary-fg-color; - text-decoration: underline; - cursor: pointer; -} - -.mx_EventTile_keyRequestInfo_tooltip_contents p { - text-align: auto; - margin-left: 3px; - margin-right: 3px; -} - -.mx_EventTile_keyRequestInfo_tooltip_contents p:first-child { - margin-top: 0px; -} - -.mx_EventTile_keyRequestInfo_tooltip_contents p:last-child { - margin-bottom: 0px; -} - -.mx_EventTile:hover.mx_EventTile_verified .mx_EventTile_line, -.mx_EventTile:hover.mx_EventTile_unverified .mx_EventTile_line, -.mx_EventTile:hover.mx_EventTile_unknown .mx_EventTile_line { - padding-left: calc($left-gutter - $hover-select-border); -} - -.mx_EventTile:hover.mx_EventTile_verified .mx_EventTile_line { - border-left: $e2e-verified-color $EventTile_e2e_state_indicator_width solid; -} - -.mx_EventTile:hover.mx_EventTile_unverified .mx_EventTile_line { - border-left: $e2e-unverified-color $EventTile_e2e_state_indicator_width solid; -} - -.mx_EventTile:hover.mx_EventTile_unknown .mx_EventTile_line { - border-left: $e2e-unknown-color $EventTile_e2e_state_indicator_width solid; -} - -.mx_EventTile:hover.mx_EventTile_verified.mx_EventTile_info .mx_EventTile_line, -.mx_EventTile:hover.mx_EventTile_unverified.mx_EventTile_info .mx_EventTile_line, -.mx_EventTile:hover.mx_EventTile_unknown.mx_EventTile_info .mx_EventTile_line { - padding-left: calc($left-gutter + 18px - $hover-select-border); -} - -/* End to end encryption stuff */ -.mx_EventTile:hover .mx_EventTile_e2eIcon { - opacity: 1; -} - -// Explicit relationships so that it doesn't apply to nested EventTile components (e.g in Replies) -.mx_EventTile:hover.mx_EventTile_verified .mx_EventTile_line > a > .mx_MessageTimestamp, -.mx_EventTile:hover.mx_EventTile_unverified .mx_EventTile_line > a > .mx_MessageTimestamp, -.mx_EventTile:hover.mx_EventTile_unknown .mx_EventTile_line > a > .mx_MessageTimestamp { - left: calc(-$hover-select-border); -} - -// Explicit relationships so that it doesn't apply to nested EventTile components (e.g in Replies) -.mx_EventTile:hover.mx_EventTile_verified .mx_EventTile_line > .mx_EventTile_e2eIcon, -.mx_EventTile:hover.mx_EventTile_unverified .mx_EventTile_line > .mx_EventTile_e2eIcon, -.mx_EventTile:hover.mx_EventTile_unknown .mx_EventTile_line > .mx_EventTile_e2eIcon { - display: block; - left: 41px; +/* HACK to override line-height which is already marked important elsewhere */ +.mx_EventTile_bigEmoji.mx_EventTile_bigEmoji { + font-size: 48px !important; + line-height: 57px !important; } .mx_EventTile_content .mx_EventTile_edited { @@ -601,24 +597,33 @@ $hover-select-border: 4px; /* end of overrides */ -.mx_EventTile_tileError { - color: red; - text-align: center; - // Remove some of the default tile padding so that the error is centered - margin-right: 0; - .mx_EventTile_line { - padding-left: 0; - margin-right: 0; - } +.mx_EventTile_keyRequestInfo { + font-size: $font-12px; +} - .mx_EventTile_line span { - padding: 4px 8px; - } +.mx_EventTile_keyRequestInfo_text { + opacity: 0.5; +} - a { - margin-left: 1em; - } +.mx_EventTile_keyRequestInfo_text a { + color: $primary-fg-color; + text-decoration: underline; + cursor: pointer; +} + +.mx_EventTile_keyRequestInfo_tooltip_contents p { + text-align: auto; + margin-left: 3px; + margin-right: 3px; +} + +.mx_EventTile_keyRequestInfo_tooltip_contents p:first-child { + margin-top: 0px; +} + +.mx_EventTile_keyRequestInfo_tooltip_contents p:last-child { + margin-bottom: 0px; } @media only screen and (max-width: 480px) { diff --git a/src/components/views/elements/EventListSummary.tsx b/src/components/views/elements/EventListSummary.tsx index 86d3e082ad..3c64337367 100644 --- a/src/components/views/elements/EventListSummary.tsx +++ b/src/components/views/elements/EventListSummary.tsx @@ -63,7 +63,7 @@ const EventListSummary: React.FC = ({ // If we are only given few events then just pass them through if (events.length < threshold) { return ( -
  • +
  • { children }
  • ); @@ -92,7 +92,7 @@ const EventListSummary: React.FC = ({ } return ( -
  • +
  • { expanded ? _t('collapse') : _t('expand') } @@ -101,4 +101,8 @@ const EventListSummary: React.FC = ({ ); }; +EventListSummary.defaultProps = { + startExpanded: false, +}; + export default EventListSummary; diff --git a/src/components/views/messages/UnknownBody.js b/src/components/views/messages/UnknownBody.js index 786facc340..fdf0387a69 100644 --- a/src/components/views/messages/UnknownBody.js +++ b/src/components/views/messages/UnknownBody.js @@ -17,11 +17,12 @@ limitations under the License. import React, {forwardRef} from "react"; -export default forwardRef(({mxEvent}, ref) => { +export default forwardRef(({mxEvent, children}, ref) => { const text = mxEvent.getContent().body; return ( { text } + { children } ); }); diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index 85b9cac2c4..a76cc04660 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -29,7 +29,7 @@ import { hasText } from "../../../TextForEvent"; import * as sdk from "../../../index"; import dis from '../../../dispatcher/dispatcher'; import SettingsStore from "../../../settings/SettingsStore"; -import {Layout} from "../../../settings/Layout"; +import { Layout } from "../../../settings/Layout"; import {formatTime} from "../../../DateUtils"; import {MatrixClientPeg} from '../../../MatrixClientPeg'; import {ALL_RULE_TYPES} from "../../../mjolnir/BanList"; @@ -988,8 +988,13 @@ export default class EventTile extends React.Component { onFocusChange={this.onActionBarFocusChange} /> : undefined; - const showTimestamp = this.props.mxEvent.getTs() && - (this.props.alwaysShowTimestamps || this.props.last || this.state.hover || this.state.actionBarFocused); + const showTimestamp = this.props.mxEvent.getTs() + && (this.props.alwaysShowTimestamps + || this.props.last + || this.state.hover + || this.state.actionBarFocused) + || this.props.layout === Layout.Bubble; + const timestamp = showTimestamp ? : null; @@ -1168,6 +1173,8 @@ export default class EventTile extends React.Component { this.props.alwaysShowTimestamps || this.state.hover, ); + const isOwnEvent = this.props.mxEvent.sender.userId === MatrixClientPeg.get().getUserId(); + // tab-index=-1 to allow it to be focusable but do not add tab stop for it, primarily for screen readers return ( React.createElement(this.props.as || "li", { @@ -1177,6 +1184,8 @@ export default class EventTile extends React.Component { "aria-live": ariaLive, "aria-atomic": "true", "data-scroll-tokens": scrollToken, + "data-layout": this.props.layout, + "data-self": isOwnEvent, "onMouseEnter": () => this.setState({ hover: true }), "onMouseLeave": () => this.setState({ hover: false }), }, [ @@ -1198,9 +1207,9 @@ export default class EventTile extends React.Component { onHeightChanged={this.props.onHeightChanged} /> { keyRequestInfo } - { reactionsRow } { actionBar }
  • , + reactionsRow, msgOption, avatar, From 61929e3fc2857140e12f7405cb5c3188d7327cf5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Fri, 18 Jun 2021 19:43:19 +0200 Subject: [PATCH 158/809] Implement snapping MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/voip/CallPreview.tsx | 60 +++++++++++++++++++---- 1 file changed, 50 insertions(+), 10 deletions(-) diff --git a/src/components/views/voip/CallPreview.tsx b/src/components/views/voip/CallPreview.tsx index 865a3f34af..e5a0487a4a 100644 --- a/src/components/views/voip/CallPreview.tsx +++ b/src/components/views/voip/CallPreview.tsx @@ -32,8 +32,12 @@ import UIStore from '../../../stores/UIStore'; const PIP_VIEW_WIDTH = 336; const PIP_VIEW_HEIGHT = 232; -const DEFAULT_X_OFFSET = 16; -const DEFAULT_Y_OFFSET = 48; +const PADDING = { + top: 58, + bottom: 58, + left: 76, + right: 8, +} const SHOW_CALL_IN_STATES = [ CallState.Connected, @@ -44,6 +48,7 @@ const SHOW_CALL_IN_STATES = [ CallState.WaitLocalMedia, ]; + interface IProps { } @@ -116,8 +121,8 @@ export default class CallPreview extends React.Component { roomId, primaryCall: primaryCall, secondaryCall: secondaryCalls[0], - translationX: UIStore.instance.windowWidth - DEFAULT_X_OFFSET - PIP_VIEW_WIDTH, - translationY: DEFAULT_Y_OFFSET, + translationX: UIStore.instance.windowWidth - PADDING.right - PIP_VIEW_WIDTH, + translationY: UIStore.instance.windowHeight - PADDING.bottom - PIP_VIEW_WIDTH, }; } @@ -132,7 +137,7 @@ export default class CallPreview extends React.Component { this.roomStoreToken = RoomViewStore.addListener(this.onRoomViewStoreUpdate); document.addEventListener("mousemove", this.onMoving); document.addEventListener("mouseup", this.onEndMoving); - window.addEventListener("resize", this.onWindowSizeChanged); + window.addEventListener("resize", this.snap); this.dispatcherRef = dis.register(this.onAction); MatrixClientPeg.get().on(CallEvent.RemoteHoldUnhold, this.onCallRemoteHold); } @@ -142,7 +147,7 @@ export default class CallPreview extends React.Component { MatrixClientPeg.get().removeListener(CallEvent.RemoteHoldUnhold, this.onCallRemoteHold); document.removeEventListener("mousemove", this.onMoving); document.removeEventListener("mouseup", this.onEndMoving); - window.removeEventListener("resize", this.onWindowSizeChanged); + window.removeEventListener("resize", this.snap); if (this.roomStoreToken) { this.roomStoreToken.remove(); } @@ -150,10 +155,6 @@ export default class CallPreview extends React.Component { SettingsStore.unwatchSetting(this.settingsWatcherRef); } - private onWindowSizeChanged = () => { - this.setTranslation(this.state.translationX, this.state.translationY); - }; - private setTranslation(inTranslationX: number, inTranslationY: number) { const width = this.callViewWrapper.current?.clientWidth || PIP_VIEW_WIDTH; const height = this.callViewWrapper.current?.clientHeight || PIP_VIEW_HEIGHT; @@ -185,6 +186,44 @@ export default class CallPreview extends React.Component { }); } + private snap = () => { + const translationX = this.state.translationX; + const translationY = this.state.translationY; + // We subtract the PiP size from the window size in order to calculate + // the position to snap to from the PiP center and not its top-left + // corner + const windowWidth = ( + UIStore.instance.windowWidth - + (this.callViewWrapper.current?.clientWidth || PIP_VIEW_WIDTH) + ); + const windowHeight = ( + UIStore.instance.windowHeight - + (this.callViewWrapper.current?.clientHeight || PIP_VIEW_HEIGHT) + ); + + if (translationX >= windowWidth / 2 && translationY >= windowHeight / 2) { + this.setState({ + translationX: windowWidth - PADDING.right, + translationY: windowHeight - PADDING.bottom, + }); + } else if (translationX >= windowWidth / 2 && translationY <= windowHeight / 2) { + this.setState({ + translationX: windowWidth - PADDING.right, + translationY: PADDING.top, + }); + } else if (translationX <= windowWidth / 2 && translationY >= windowHeight / 2) { + this.setState({ + translationX: PADDING.left, + translationY: windowHeight - PADDING.bottom, + }); + } else { + this.setState({ + translationX: PADDING.left, + translationY: PADDING.top, + }); + } + } + private onRoomViewStoreUpdate = (payload) => { if (RoomViewStore.getRoomId() === this.state.roomId) return; @@ -253,6 +292,7 @@ export default class CallPreview extends React.Component { }; private onEndMoving = () => { + this.snap(); this.moving = false; }; From 39ca2844bde3ecfecd9d4e6956654b047331d552 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sat, 19 Jun 2021 10:10:27 +0200 Subject: [PATCH 159/809] Add AnimationUtils MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/utils/AnimationUtils.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 src/utils/AnimationUtils.ts diff --git a/src/utils/AnimationUtils.ts b/src/utils/AnimationUtils.ts new file mode 100644 index 0000000000..9654bdeb11 --- /dev/null +++ b/src/utils/AnimationUtils.ts @@ -0,0 +1,19 @@ +/* +Copyright 2021 Šimon Brandner + +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. +*/ + +export function lerp(start: number, end: number, amt: number) { + return (1 - amt) * start + amt * end; +} From c8cf23b87c1c649a4a03b89ea8dd61f26f20c7ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sat, 19 Jun 2021 12:21:24 +0200 Subject: [PATCH 160/809] Implement LERP MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/voip/CallPreview.tsx | 96 +++++++++++++---------- 1 file changed, 54 insertions(+), 42 deletions(-) diff --git a/src/components/views/voip/CallPreview.tsx b/src/components/views/voip/CallPreview.tsx index e5a0487a4a..210569ed05 100644 --- a/src/components/views/voip/CallPreview.tsx +++ b/src/components/views/voip/CallPreview.tsx @@ -28,10 +28,14 @@ import { CallEvent, CallState, MatrixCall } from 'matrix-js-sdk/src/webrtc/call' import { MatrixClientPeg } from '../../../MatrixClientPeg'; import {replaceableComponent} from "../../../utils/replaceableComponent"; import UIStore from '../../../stores/UIStore'; +import { lerp } from '../../../utils/AnimationUtils'; const PIP_VIEW_WIDTH = 336; const PIP_VIEW_HEIGHT = 232; +const MOVING_AMT = 0.2; +const SNAPPING_AMT = 0.05; + const PADDING = { top: 58, bottom: 58, @@ -107,6 +111,12 @@ export default class CallPreview extends React.Component { private roomStoreToken: any; private dispatcherRef: string; private settingsWatcherRef: string; + private callViewWrapper = createRef(); + private initX = 0; + private initY = 0; + private desiredTranslationX = UIStore.instance.windowWidth - PADDING.right - PIP_VIEW_WIDTH; + private desiredTranslationY = UIStore.instance.windowHeight - PADDING.bottom - PIP_VIEW_WIDTH; + private moving = false; constructor(props: IProps) { super(props); @@ -126,12 +136,6 @@ export default class CallPreview extends React.Component { }; } - private callViewWrapper = createRef(); - - private initX = 0; - private initY = 0; - private moving = false; - public componentDidMount() { CallHandler.sharedInstance().addListener(CallHandlerEvent.CallChangeRoom, this.updateCalls); this.roomStoreToken = RoomViewStore.addListener(this.onRoomViewStoreUpdate); @@ -155,40 +159,52 @@ export default class CallPreview extends React.Component { SettingsStore.unwatchSetting(this.settingsWatcherRef); } + private animationCallback = () => { + // If the PiP isn't being dragged and there is only a tiny difference in + // the desiredTranslation and translation, quit the animationCallback + // loop. If that is the case, it means the PiP has snapped into its + // position and there is nothing to do. Not doing this would cause an + // infinite loop + if ( + !this.moving && + Math.abs(this.state.translationX - this.desiredTranslationX) <= 1 && + Math.abs(this.state.translationY - this.desiredTranslationY) <= 1 + ) return; + + const amt = this.moving ? MOVING_AMT : SNAPPING_AMT; + this.setState({ + translationX: lerp(this.state.translationX, this.desiredTranslationX, amt), + translationY: lerp(this.state.translationY, this.desiredTranslationY, amt), + }); + requestAnimationFrame(this.animationCallback); + } + private setTranslation(inTranslationX: number, inTranslationY: number) { const width = this.callViewWrapper.current?.clientWidth || PIP_VIEW_WIDTH; const height = this.callViewWrapper.current?.clientHeight || PIP_VIEW_HEIGHT; - let outTranslationX; - let outTranslationY; - // Avoid overflow on the x axis if (inTranslationX + width >= UIStore.instance.windowWidth) { - outTranslationX = UIStore.instance.windowWidth - width; + this.desiredTranslationX = UIStore.instance.windowWidth - width; } else if (inTranslationX <= 0) { - outTranslationX = 0; + this.desiredTranslationX = 0; } else { - outTranslationX = inTranslationX; + this.desiredTranslationX = inTranslationX; } // Avoid overflow on the y axis if (inTranslationY + height >= UIStore.instance.windowHeight) { - outTranslationY = UIStore.instance.windowHeight - height; + this.desiredTranslationY = UIStore.instance.windowHeight - height; } else if (inTranslationY <= 0) { - outTranslationY = 0; + this.desiredTranslationY = 0; } else { - outTranslationY = inTranslationY; + this.desiredTranslationY = inTranslationY; } - - this.setState({ - translationX: outTranslationX, - translationY: outTranslationY, - }); } private snap = () => { - const translationX = this.state.translationX; - const translationY = this.state.translationY; + const translationX = this.desiredTranslationX; + const translationY = this.desiredTranslationY; // We subtract the PiP size from the window size in order to calculate // the position to snap to from the PiP center and not its top-left // corner @@ -202,26 +218,22 @@ export default class CallPreview extends React.Component { ); if (translationX >= windowWidth / 2 && translationY >= windowHeight / 2) { - this.setState({ - translationX: windowWidth - PADDING.right, - translationY: windowHeight - PADDING.bottom, - }); + this.desiredTranslationX = windowWidth - PADDING.right; + this.desiredTranslationY = windowHeight - PADDING.bottom; } else if (translationX >= windowWidth / 2 && translationY <= windowHeight / 2) { - this.setState({ - translationX: windowWidth - PADDING.right, - translationY: PADDING.top, - }); + this.desiredTranslationX = windowWidth - PADDING.right; + this.desiredTranslationY = PADDING.top; } else if (translationX <= windowWidth / 2 && translationY >= windowHeight / 2) { - this.setState({ - translationX: PADDING.left, - translationY: windowHeight - PADDING.bottom, - }); + this.desiredTranslationX = PADDING.left; + this.desiredTranslationY = windowHeight - PADDING.bottom; } else { - this.setState({ - translationX: PADDING.left, - translationY: PADDING.top, - }); + this.desiredTranslationX = PADDING.left; + this.desiredTranslationY = PADDING.top; } + + // We start animating here because we want the PiP to move when we're + // resizing the window + requestAnimationFrame(this.animationCallback); } private onRoomViewStoreUpdate = (payload) => { @@ -277,9 +289,9 @@ export default class CallPreview extends React.Component { event.stopPropagation(); this.moving = true; - - this.initX = event.pageX - this.state.translationX; - this.initY = event.pageY - this.state.translationY; + this.initX = event.pageX - this.desiredTranslationX; + this.initY = event.pageY - this.desiredTranslationY; + requestAnimationFrame(this.animationCallback); }; private onMoving = (event: React.MouseEvent | MouseEvent) => { @@ -292,8 +304,8 @@ export default class CallPreview extends React.Component { }; private onEndMoving = () => { - this.snap(); this.moving = false; + this.snap(); }; public render() { From be10e77704b1635dc514b7b98201b7ee11b24d10 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Sat, 19 Jun 2021 15:37:06 +0100 Subject: [PATCH 161/809] Improve typing of Event Index Manager / Seshat --- src/indexing/BaseEventIndexManager.ts | 78 +++++++++++++-------------- src/indexing/EventIndex.ts | 16 +++--- 2 files changed, 47 insertions(+), 47 deletions(-) diff --git a/src/indexing/BaseEventIndexManager.ts b/src/indexing/BaseEventIndexManager.ts index debcb213ca..9478b2987b 100644 --- a/src/indexing/BaseEventIndexManager.ts +++ b/src/indexing/BaseEventIndexManager.ts @@ -17,7 +17,7 @@ limitations under the License. // The following interfaces take their names and member names from seshat and the spec /* eslint-disable camelcase */ -export interface MatrixEvent { +export interface IMatrixEvent { type: string; sender: string; content: {}; @@ -27,37 +27,37 @@ export interface MatrixEvent { roomId: string; } -export interface MatrixProfile { +export interface IMatrixProfile { avatar_url: string; displayname: string; } -export interface CrawlerCheckpoint { +export interface ICrawlerCheckpoint { roomId: string; token: string; fullCrawl?: boolean; direction: string; } -export interface ResultContext { - events_before: [MatrixEvent]; - events_after: [MatrixEvent]; - profile_info: Map; +export interface IResultContext { + events_before: [IMatrixEvent]; + events_after: [IMatrixEvent]; + profile_info: Map; } -export interface ResultsElement { +export interface IResultsElement { rank: number; - result: MatrixEvent; - context: ResultContext; + result: IMatrixEvent; + context: IResultContext; } -export interface SearchResult { +export interface ISearchResult { count: number; - results: [ResultsElement]; + results: [IResultsElement]; highlights: [string]; } -export interface SearchArgs { +export interface ISearchArgs { search_term: string; before_limit: number; after_limit: number; @@ -65,19 +65,19 @@ export interface SearchArgs { room_id?: string; } -export interface EventAndProfile { - event: MatrixEvent; - profile: MatrixProfile; +export interface IEventAndProfile { + event: IMatrixEvent; + profile: IMatrixProfile; } -export interface LoadArgs { +export interface ILoadArgs { roomId: string; limit: number; fromEvent?: string; direction?: string; } -export interface IndexStats { +export interface IIndexStats { size: number; eventCount: number; roomCount: number; @@ -119,13 +119,13 @@ export default abstract class BaseEventIndexManager { * Queue up an event to be added to the index. * * @param {MatrixEvent} ev The event that should be added to the index. - * @param {MatrixProfile} profile The profile of the event sender at the + * @param {IMatrixProfile} profile The profile of the event sender at the * time of the event receival. * * @return {Promise} A promise that will resolve when the was queued up for * addition. */ - async addEventToIndex(ev: MatrixEvent, profile: MatrixProfile): Promise { + async addEventToIndex(ev: IMatrixEvent, profile: IMatrixProfile): Promise { throw new Error("Unimplemented"); } @@ -160,10 +160,10 @@ export default abstract class BaseEventIndexManager { /** * Get statistical information of the index. * - * @return {Promise} A promise that will resolve to the index + * @return {Promise} A promise that will resolve to the index * statistics. */ - async getStats(): Promise { + async getStats(): Promise { throw new Error("Unimplemented"); } @@ -203,13 +203,13 @@ export default abstract class BaseEventIndexManager { /** * Search the event index using the given term for matching events. * - * @param {SearchArgs} searchArgs The search configuration for the search, + * @param {ISearchArgs} searchArgs The search configuration for the search, * sets the search term and determines the search result contents. * - * @return {Promise<[SearchResult]>} A promise that will resolve to an array + * @return {Promise<[ISearchResult]>} A promise that will resolve to an array * of search results once the search is done. */ - async searchEventIndex(searchArgs: SearchArgs): Promise { + async searchEventIndex(searchArgs: ISearchArgs): Promise { throw new Error("Unimplemented"); } @@ -218,12 +218,12 @@ export default abstract class BaseEventIndexManager { * * This is used to add a batch of events to the index. * - * @param {[EventAndProfile]} events The list of events and profiles that + * @param {[IEventAndProfile]} events The list of events and profiles that * should be added to the event index. - * @param {[CrawlerCheckpoint]} checkpoint A new crawler checkpoint that + * @param {[ICrawlerCheckpoint]} checkpoint A new crawler checkpoint that * should be stored in the index which should be used to continue crawling * the room. - * @param {[CrawlerCheckpoint]} oldCheckpoint The checkpoint that was used + * @param {[ICrawlerCheckpoint]} oldCheckpoint The checkpoint that was used * to fetch the current batch of events. This checkpoint will be removed * from the index. * @@ -231,9 +231,9 @@ export default abstract class BaseEventIndexManager { * were already added to the index, false otherwise. */ async addHistoricEvents( - events: [EventAndProfile], - checkpoint: CrawlerCheckpoint | null, - oldCheckpoint: CrawlerCheckpoint | null, + events: IEventAndProfile[], + checkpoint: ICrawlerCheckpoint | null, + oldCheckpoint: ICrawlerCheckpoint | null, ): Promise { throw new Error("Unimplemented"); } @@ -241,36 +241,36 @@ export default abstract class BaseEventIndexManager { /** * Add a new crawler checkpoint to the index. * - * @param {CrawlerCheckpoint} checkpoint The checkpoint that should be added + * @param {ICrawlerCheckpoint} checkpoint The checkpoint that should be added * to the index. * * @return {Promise} A promise that will resolve once the checkpoint has * been stored. */ - async addCrawlerCheckpoint(checkpoint: CrawlerCheckpoint): Promise { + async addCrawlerCheckpoint(checkpoint: ICrawlerCheckpoint): Promise { throw new Error("Unimplemented"); } /** * Add a new crawler checkpoint to the index. * - * @param {CrawlerCheckpoint} checkpoint The checkpoint that should be + * @param {ICrawlerCheckpoint} checkpoint The checkpoint that should be * removed from the index. * * @return {Promise} A promise that will resolve once the checkpoint has * been removed. */ - async removeCrawlerCheckpoint(checkpoint: CrawlerCheckpoint): Promise { + async removeCrawlerCheckpoint(checkpoint: ICrawlerCheckpoint): Promise { throw new Error("Unimplemented"); } /** * Load the stored checkpoints from the index. * - * @return {Promise<[CrawlerCheckpoint]>} A promise that will resolve to an + * @return {Promise<[ICrawlerCheckpoint]>} A promise that will resolve to an * array of crawler checkpoints once they have been loaded from the index. */ - async loadCheckpoints(): Promise<[CrawlerCheckpoint]> { + async loadCheckpoints(): Promise { throw new Error("Unimplemented"); } @@ -286,11 +286,11 @@ export default abstract class BaseEventIndexManager { * @param {string} args.direction The direction to which we should continue * loading events from. This is used only if fromEvent is used as well. * - * @return {Promise<[EventAndProfile]>} A promise that will resolve to an + * @return {Promise<[IEventAndProfile]>} A promise that will resolve to an * array of Matrix events that contain mxc URLs accompanied with the * historic profile of the sender. */ - async loadFileEvents(args: LoadArgs): Promise<[EventAndProfile]> { + async loadFileEvents(args: ILoadArgs): Promise { throw new Error("Unimplemented"); } diff --git a/src/indexing/EventIndex.ts b/src/indexing/EventIndex.ts index c36f96f368..978a2ac813 100644 --- a/src/indexing/EventIndex.ts +++ b/src/indexing/EventIndex.ts @@ -28,7 +28,7 @@ import { MatrixClientPeg } from "../MatrixClientPeg"; import { sleep } from "../utils/promise"; import SettingsStore from "../settings/SettingsStore"; import { SettingLevel } from "../settings/SettingLevel"; -import {CrawlerCheckpoint, LoadArgs, SearchArgs} from "./BaseEventIndexManager"; +import { ICrawlerCheckpoint, ILoadArgs, ISearchArgs } from "./BaseEventIndexManager"; // The time in ms that the crawler will wait loop iterations if there // have not been any checkpoints to consume in the last iteration. @@ -45,9 +45,9 @@ interface ICrawler { * Event indexing class that wraps the platform specific event indexing. */ export default class EventIndex extends EventEmitter { - private crawlerCheckpoints: CrawlerCheckpoint[] = []; + private crawlerCheckpoints: ICrawlerCheckpoint[] = []; private crawler: ICrawler = null; - private currentCheckpoint: CrawlerCheckpoint = null; + private currentCheckpoint: ICrawlerCheckpoint = null; public async init() { const indexManager = PlatformPeg.get().getEventIndexingManager(); @@ -111,14 +111,14 @@ export default class EventIndex extends EventEmitter { const timeline = room.getLiveTimeline(); const token = timeline.getPaginationToken("b"); - const backCheckpoint: CrawlerCheckpoint = { + const backCheckpoint: ICrawlerCheckpoint = { roomId: room.roomId, token: token, direction: "b", fullCrawl: true, }; - const forwardCheckpoint: CrawlerCheckpoint = { + const forwardCheckpoint: ICrawlerCheckpoint = { roomId: room.roomId, token: token, direction: "f", @@ -668,13 +668,13 @@ export default class EventIndex extends EventEmitter { /** * Search the event index using the given term for matching events. * - * @param {SearchArgs} searchArgs The search configuration for the search, + * @param {ISearchArgs} searchArgs The search configuration for the search, * sets the search term and determines the search result contents. * * @return {Promise<[SearchResult]>} A promise that will resolve to an array * of search results once the search is done. */ - public async search(searchArgs: SearchArgs) { + public async search(searchArgs: ISearchArgs) { const indexManager = PlatformPeg.get().getEventIndexingManager(); return indexManager.searchEventIndex(searchArgs); } @@ -709,7 +709,7 @@ export default class EventIndex extends EventEmitter { const client = MatrixClientPeg.get(); const indexManager = PlatformPeg.get().getEventIndexingManager(); - const loadArgs: LoadArgs = { + const loadArgs: ILoadArgs = { roomId: room.roomId, limit: limit, }; From a2a515841138a3c545b6a44477df3e9ef3992b93 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Sat, 19 Jun 2021 15:37:48 +0100 Subject: [PATCH 162/809] Fix View Source accessing renamed private field on MatrixEvent --- src/components/structures/ViewSource.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/structures/ViewSource.js b/src/components/structures/ViewSource.js index 6fe99dd464..b69a92dd61 100644 --- a/src/components/structures/ViewSource.js +++ b/src/components/structures/ViewSource.js @@ -55,7 +55,7 @@ export default class ViewSource extends React.Component { viewSourceContent() { const mxEvent = this.props.mxEvent.replacingEvent() || this.props.mxEvent; // show the replacing event, not the original, if it is an edit const isEncrypted = mxEvent.isEncrypted(); - const decryptedEventSource = mxEvent._clearEvent; // FIXME: _clearEvent is private + const decryptedEventSource = mxEvent.clearEvent; // FIXME: clearEvent is private const originalEventSource = mxEvent.event; if (isEncrypted) { From 2c5ddea1d9687bfe7ce406741e824074be56ddfb Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Sat, 19 Jun 2021 15:57:07 +0100 Subject: [PATCH 163/809] Fix ConfirmUserActionDialog returning an input field rather than text --- src/components/views/dialogs/ConfirmUserActionDialog.tsx | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/components/views/dialogs/ConfirmUserActionDialog.tsx b/src/components/views/dialogs/ConfirmUserActionDialog.tsx index 05f8c63ace..5cdb4c664b 100644 --- a/src/components/views/dialogs/ConfirmUserActionDialog.tsx +++ b/src/components/views/dialogs/ConfirmUserActionDialog.tsx @@ -38,7 +38,7 @@ interface IProps { // be the string entered. askReason?: boolean; danger?: boolean; - onFinished: (success: boolean, reason?: HTMLInputElement) => void; + onFinished: (success: boolean, reason?: string) => void; } /* @@ -59,11 +59,7 @@ export default class ConfirmUserActionDialog extends React.Component { }; public onOk = (): void => { - let reason; - if (this.reasonField) { - reason = this.reasonField.current; - } - this.props.onFinished(true, reason); + this.props.onFinished(true, this.reasonField.current?.value); }; public onCancel = (): void => { From 21a960acc7f1b0a55d307e9c2dc1906a60d11e2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sat, 19 Jun 2021 17:53:00 +0200 Subject: [PATCH 164/809] Fix timestamp issues MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/css/views/rooms/_IRCLayout.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/res/css/views/rooms/_IRCLayout.scss b/res/css/views/rooms/_IRCLayout.scss index 8e5a26b333..5e61c3b8a3 100644 --- a/res/css/views/rooms/_IRCLayout.scss +++ b/res/css/views/rooms/_IRCLayout.scss @@ -29,6 +29,7 @@ $irc-line-height: $font-18px; // timestamps are links which shouldn't be underlined > a { text-decoration: none; + min-width: 45px; } display: flex; From 8e2a7cc3f6badace1d1cb0dacb90d449689ac8cd Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Sat, 19 Jun 2021 19:41:45 +0100 Subject: [PATCH 165/809] Convert crypto index to TS --- src/rageshake/submit-rageshake.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/rageshake/submit-rageshake.ts b/src/rageshake/submit-rageshake.ts index 08d8ccfd13..64d7405f17 100644 --- a/src/rageshake/submit-rageshake.ts +++ b/src/rageshake/submit-rageshake.ts @@ -86,8 +86,8 @@ async function collectBugReport(opts: IOpts = {}, gzipLogs = true) { body.append('cross_signing_key', client.getCrossSigningId()); // add cross-signing status information - const crossSigning = client.crypto._crossSigningInfo; - const secretStorage = client.crypto._secretStorage; + const crossSigning = client.crypto.crossSigningInfo; + const secretStorage = client.crypto.secretStorage; body.append("cross_signing_ready", String(await client.isCrossSigningReady())); body.append("cross_signing_supported_by_hs", From 4dbf739fb8210bc851e3526f9c0c843452df488f Mon Sep 17 00:00:00 2001 From: libexus Date: Fri, 18 Jun 2021 16:48:23 +0000 Subject: [PATCH 166/809] Translated using Weblate (German) Currently translated at 99.4% (2977 of 2993 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/de/ --- src/i18n/strings/de_DE.json | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json index 9d1c1fa071..3eb0996792 100644 --- a/src/i18n/strings/de_DE.json +++ b/src/i18n/strings/de_DE.json @@ -1321,7 +1321,7 @@ "Disconnect from the identity server ?": "Verbindung zum Identitätsserver trennen?", "Add Email Address": "E-Mail-Adresse hinzufügen", "Add Phone Number": "Telefonnummer hinzufügen", - "Changes the avatar of the current room": "Ändert den Avatar für diesen Raum", + "Changes the avatar of the current room": "Ändert den Avatar des Raums", "Deactivate account": "Benutzerkonto deaktivieren", "Show previews/thumbnails for images": "Vorschauen für Bilder", "View": "Vorschau", @@ -1388,7 +1388,7 @@ "Backup key stored: ": "Backup Schlüssel gespeichert: ", "Clear notifications": "Benachrichtigungen löschen", "Disconnect from the identity server and connect to instead?": "Vom Identitätsserver trennen, und stattdessen eine Verbindung zu aufbauen?", - "The identity server you have chosen does not have any terms of service.": "Der von dir gewählte Identitätsserver hat keine Nutzungsbedingungen.", + "The identity server you have chosen does not have any terms of service.": "Der von dir gewählte Identitätsserver gibt keine Nutzungsbedingungen an.", "Disconnect identity server": "Verbindung zum Identitätsserver trennen", "contact the administrators of identity server ": "Administration des Identitätsservers kontaktieren", "wait and try again later": "warte und versuche es später erneut", @@ -1634,7 +1634,7 @@ "Show rooms with unread notifications first": "Räume mit ungelesenen Benachrichtigungen zuerst zeigen", "Show shortcuts to recently viewed rooms above the room list": "Kürzlich besuchte Räume anzeigen", "Use Single Sign On to continue": "Einmalanmeldung zum Fortfahren nutzen", - "Confirm adding this email address by using Single Sign On to prove your identity.": "Bestätige das Hinzufügen dieser E-Mail-Adresse durch Single Sign-on, um deine Identität nachzuweisen.", + "Confirm adding this email address by using Single Sign On to prove your identity.": "Bestätige die neue E-Mail-Adresse mit Single-Sign-On, um deine Identität nachzuweisen.", "Single Sign On": "Einmalanmeldung", "Confirm adding email": "Hinzugefügte E-Mail-Addresse bestätigen", "Confirm adding this phone number by using Single Sign On to prove your identity.": "Bestätige die hinzugefügte Telefonnummer, indem du deine Identität mittels der Einmalanmeldung nachweist.", @@ -3102,7 +3102,7 @@ "Your public space": "Dein öffentlicher Space", "You can change this later": "Du kannst die Sichtbarkeit später ändern", "Invite only, best for yourself or teams": "Nur Eingeladene können beitreten - am besten für dich selbst oder Teams", - "Open space for anyone, best for communities": "Öffne den Space für alle - am Besten für Communities", + "Open space for anyone, best for communities": "Öffne den Space für alle - am besten für Communities", "Private": "Privat", "Public": "Öffentlich", "Spaces are new ways to group rooms and people. To join an existing space you’ll need an invite": "Spaces sind ein neuer Weg Räume und Leute zu gruppieren. Um einen bestehenden Space zu betreten brauchst du eine Einladung", @@ -3302,7 +3302,7 @@ "Join the beta": "Beta beitreten", "Leave the beta": "Beta verlassen", "Beta": "Beta", - "Tap for more info": "Klicke für mehr Infos", + "Tap for more info": "Für mehr Infos hier klicken", "Spaces is a beta feature": "Spaces sind noch in der Entwicklung und möglicherweise instabil", "Want to add a new room instead?": "Willst du einen neuen Raum hinzufügen?", "Adding rooms... (%(progress)s out of %(count)s)|one": "Raum wird hinzugefügt...", @@ -3352,5 +3352,21 @@ "See when people join, leave, or are invited to this room": "Anzeigen, wenn Leute eingeladen werden oder den Raum betreten und verlassen", "The user you called is busy.": "Der angerufene Benutzer ist momentan beschäftigt.", "User Busy": "Benutzer beschäftigt", - "No results for \"%(query)s\"": "Keine Ergebnisse für \"%(query)s\"" + "No results for \"%(query)s\"": "Keine Ergebnisse für \"%(query)s\"", + "Some suggestions may be hidden for privacy.": "Um deine Privatsphäre zu schützen, werden einige Vorschläge möglicherweise ausgeblendet.", + "Your private messages are normally encrypted, but this room isn't. Usually this is due to an unsupported device or method being used, like email invites. Enable encryption in settings.": "Dieser Raum ist offenbar nicht verschlüsselt. Das kann an einem veralteten Gerät oder an manchen Verfahren wie E-Mail-Einladungen liegen. Verschlüsselung aktivieren.", + "We're working on this as part of the beta, but just want to let you know.": "Dies ist noch Teil der Beta, wir wollten dir es aber schon jetzt zeigen.", + "Or send invite link": "Oder versende einen Einladungslink", + "If you can't see who you’re looking for, send them your invite link below.": "Niemanden gefunden? Sende ihnen stattdessen einen Einladungslink.", + "If you have permissions, open the menu on any message and select Pin to stick them here.": "Du kannst, sofern du die Berechtigung dazu hast, Nachrichten in ihrem Menü hier anpinnen.", + "Search for rooms or people": "Räume oder Leute suchen", + "Forward message": "Nachricht weiterleiten", + "Open link": "Link öffnen", + "Sent": "Gesendet", + "You don't have permission to do this": "Du bist dazu nicht berechtigt", + "Error loading Widget": "Fehler beim Laden des Widgets", + "Pinned messages": "Angeheftete Nachrichten", + "Nothing pinned, yet": "Es ist nichts angepinnt. Noch nicht.", + "End-to-end encryption isn't enabled": "Ende-zu-Ende-Verschlüsselung ist deaktiviert", + "See when people join, leave, or are invited to your active room": "Anzeigen, wenn Leute den aktuellen Raum betreten, verlassen oder in ihn eingeladen werden" } From c4b252d941fda74b6b027d5d10e49d04c3fd1e2d Mon Sep 17 00:00:00 2001 From: Bamstam Date: Wed, 16 Jun 2021 15:22:07 +0000 Subject: [PATCH 167/809] Translated using Weblate (German) Currently translated at 99.4% (2977 of 2993 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/de/ --- src/i18n/strings/de_DE.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json index 3eb0996792..e86025dc83 100644 --- a/src/i18n/strings/de_DE.json +++ b/src/i18n/strings/de_DE.json @@ -709,7 +709,7 @@ "Error saving email notification preferences": "Fehler beim Speichern der E-Mail-Benachrichtigungseinstellungen", "Tuesday": "Dienstag", "Enter keywords separated by a comma:": "Gib die Schlüsselwörter durch einen Beistrich getrennt ein:", - "Forward Message": "Nachricht weiterleiten", + "Forward Message": "Weiterleiten", "You have successfully set a password and an email address!": "Du hast erfolgreich ein Passwort und eine E-Mail-Adresse gesetzt!", "Remove %(name)s from the directory?": "Soll der Raum %(name)s aus dem Verzeichnis entfernt werden?", "%(brand)s uses many advanced browser features, some of which are not available or experimental in your current browser.": "%(brand)s nutzt zahlreiche fortgeschrittene Browser-Funktionen, die teilweise in deinem aktuell verwendeten Browser noch nicht verfügbar sind oder sich noch im experimentellen Status befinden.", From 3d34e8e8075c0211abd8565892ad71edb6de5a9f Mon Sep 17 00:00:00 2001 From: Michael Sasser Date: Wed, 16 Jun 2021 15:14:41 +0000 Subject: [PATCH 168/809] Translated using Weblate (German) Currently translated at 99.4% (2977 of 2993 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/de/ --- src/i18n/strings/de_DE.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json index e86025dc83..90530b5b07 100644 --- a/src/i18n/strings/de_DE.json +++ b/src/i18n/strings/de_DE.json @@ -1635,7 +1635,7 @@ "Show shortcuts to recently viewed rooms above the room list": "Kürzlich besuchte Räume anzeigen", "Use Single Sign On to continue": "Einmalanmeldung zum Fortfahren nutzen", "Confirm adding this email address by using Single Sign On to prove your identity.": "Bestätige die neue E-Mail-Adresse mit Single-Sign-On, um deine Identität nachzuweisen.", - "Single Sign On": "Einmalanmeldung", + "Single Sign On": "Single Sign-on", "Confirm adding email": "Hinzugefügte E-Mail-Addresse bestätigen", "Confirm adding this phone number by using Single Sign On to prove your identity.": "Bestätige die hinzugefügte Telefonnummer, indem du deine Identität mittels der Einmalanmeldung nachweist.", "Click the button below to confirm adding this phone number.": "Klicke unten die Schaltfläche, um die hinzugefügte Telefonnummer zu bestätigen.", From 3b39eca70ff7b5ac4ead3f1a34ceb7cbf4f40874 Mon Sep 17 00:00:00 2001 From: iaiz Date: Wed, 16 Jun 2021 13:19:42 +0000 Subject: [PATCH 169/809] Translated using Weblate (Spanish) Currently translated at 100.0% (2993 of 2993 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/es/ --- src/i18n/strings/es.json | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/src/i18n/strings/es.json b/src/i18n/strings/es.json index 5e8e57bacd..c1fb8e6542 100644 --- a/src/i18n/strings/es.json +++ b/src/i18n/strings/es.json @@ -82,7 +82,7 @@ "Fill screen": "Llenar pantalla", "Filter room members": "Filtrar miembros de la sala", "Forget room": "Olvidar sala", - "For security, this session has been signed out. Please sign in again.": "Por seguridad, esta sesión ha sido cerrada. Por favor inicia sesión nuevamente.", + "For security, this session has been signed out. Please sign in again.": "Esta sesión ha sido cerrada. Por favor, inicia sesión de nuevo.", "%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s": "%(userId)s de %(fromPowerLevel)s a %(toPowerLevel)s", "Guests cannot join this room even if explicitly invited.": "Los invitados no pueden unirse a esta sala incluso si se les invita explícitamente.", "Hangup": "Colgar", @@ -168,7 +168,7 @@ "Search": "Buscar", "Search failed": "Falló la búsqueda", "Seen by %(userName)s at %(dateTime)s": "Visto por %(userName)s el %(dateTime)s", - "Send Reset Email": "Enviar correo electrónico de restauración", + "Send Reset Email": "Enviar correo de restauración", "%(senderDisplayName)s sent an image.": "%(senderDisplayName)s envió una imagen.", "%(senderName)s sent an invitation to %(targetDisplayName)s to join the room.": "%(senderName)s invitó a %(targetDisplayName)s a unirse a la sala.", "Server error": "Error del servidor", @@ -2021,7 +2021,7 @@ "Your Matrix account on ": "Su cuenta de Matrix en ", "No identity server is configured: add one in server settings to reset your password.": "No hay ningún servidor de identidad configurado: añada uno en la configuración del servidor para poder restablecer su contraseña.", "Sign in instead": "Iniciar sesión", - "A verification email will be sent to your inbox to confirm setting your new password.": "Se enviará un correo electrónico de verificación a su bandeja de entrada para confirmar la configuración de su nueva contraseña.", + "A verification email will be sent to your inbox to confirm setting your new password.": "Te enviaremos un correo electrónico de verificación para cambiar tu contraseña.", "Your password has been reset.": "Su contraseña ha sido restablecida.", "You have been logged out of all sessions and will no longer receive push notifications. To re-enable notifications, sign in again on each device.": "Ha cerrado todas las sesiones y ya no recibirá más notificaciones push. Para volver a activar las notificaciones, inicie sesión de nuevo en cada dispositivo.", "Set a new password": "Elegir una nueva contraseña", @@ -3321,5 +3321,23 @@ "Currently joining %(count)s rooms|other": "Entrando en %(count)s salas", "No results for \"%(query)s\"": "Ningún resultado para «%(query)s»", "The user you called is busy.": "La persona a la que has llamado está ocupada.", - "User Busy": "Persona ocupada" + "User Busy": "Persona ocupada", + "Your private messages are normally encrypted, but this room isn't. Usually this is due to an unsupported device or method being used, like email invites. Enable encryption in settings.": "Normalmente tus mensajes privados están cifrados, pero esta sala no lo está. Esto puede ser debido a un dispositivo o método no soportado, como las invitaciones por correo. Activa el cifrado en ajustes.", + "End-to-end encryption isn't enabled": "El cifrado de extremo a extremo no está activado", + "If you can't see who you’re looking for, send them your invite link below.": "Si no encuentras abajo a quien buscas, envíale tu enlace de invitación.", + "Teammates might not be able to view or join any private rooms you make.": "Las personas de tu equipo no podrán ver o unirse a ninguna sala privada que crees.", + "We're working on this as part of the beta, but just want to let you know.": "Estamos trabajando en ello como parte de la beta, pero te lo queríamos contar.", + "Or send invite link": "O envía un enlace de invitación", + "Some suggestions may be hidden for privacy.": "Puede que se hayan ocultado algunas sugerencias por motivos de privacidad.", + "Search for rooms or people": "Busca salas o gente", + "Message preview": "Vista previa del mensaje", + "Forward message": "Reenviar mensaje", + "Open link": "Abrir enlace", + "Sent": "Enviado", + "You don't have permission to do this": "No tienes permisos para hacer eso", + "Error - Mixed content": "Error - Contenido mezclado", + "Error loading Widget": "Error al cargar el widget", + "Pinned messages": "Mensajes fijados", + "If you have permissions, open the menu on any message and select Pin to stick them here.": "Si tienes permisos, abre el menú de cualquier mensaje y selecciona Fijar para colocarlo aquí.", + "Nothing pinned, yet": "Nada fijado, todavía" } From 103577f0e6fe5eaa6835314fdaba9693c5b07dd3 Mon Sep 17 00:00:00 2001 From: Szimszon Date: Tue, 15 Jun 2021 19:52:03 +0000 Subject: [PATCH 170/809] Translated using Weblate (Hungarian) Currently translated at 100.0% (2993 of 2993 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/hu/ --- src/i18n/strings/hu.json | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/hu.json b/src/i18n/strings/hu.json index b290f20076..cb749f12a5 100644 --- a/src/i18n/strings/hu.json +++ b/src/i18n/strings/hu.json @@ -3376,5 +3376,22 @@ "No results for \"%(query)s\"": "Nincs találat ehhez: %(query)s", "Try different words or check for typos. Some results may not be visible as they're private and you need an invite to join them.": "Próbáljon ki más szavakat vagy keressen elgépelést. Néhány találat azért nem látszik, mert privát és meghívóra van szüksége, hogy csatlakozhasson.", "The user you called is busy.": "A hívott felhasználó foglalt.", - "User Busy": "Felhasználó foglalt" + "User Busy": "Felhasználó foglalt", + "If you can't see who you’re looking for, send them your invite link below.": "Ha nem található a keresett személy, küldje el az alábbi hivatkozást neki.", + "Some suggestions may be hidden for privacy.": "Adatvédelem miatt néhány javaslat esetleg rejtve van.", + "If you have permissions, open the menu on any message and select Pin to stick them here.": "Ha van hozzá jogosultsága, nyissa meg a menüt bármelyik üzenetben és válassza a Kitűz menüpontot a kitűzéshez.", + "Your private messages are normally encrypted, but this room isn't. Usually this is due to an unsupported device or method being used, like email invites. Enable encryption in settings.": "A személyes beszélgetések általában titkosítottak, de ez a szoba nem az. Ez azért lehet, mert nem támogatott eszköz vagy mód használt mint például e-mail meghívók. Titkosítás engedélyezése a beállításokban.", + "We're working on this as part of the beta, but just want to let you know.": "A béta funkció ezen részén dolgozunk, csak tudatni szerettük volna.", + "Teammates might not be able to view or join any private rooms you make.": "Csapattagok lehet, hogy nem láthatják vagy léphetnek be az ön által készített privát szobákba.", + "Or send invite link": "Vagy meghívó link küldése", + "Search for rooms or people": "Szobák vagy emberek keresése", + "Forward message": "Üzenet továbbítása", + "Open link": "Hivatkozás megnyitása", + "Sent": "Elküldve", + "You don't have permission to do this": "Nincs jogosultsága ehhez", + "Error - Mixed content": "Hiba - Vegyes tartalom", + "Error loading Widget": "Kisalkalmazás betöltési hiba", + "Pinned messages": "Kitűzött üzenetek", + "Nothing pinned, yet": "Semmi sincs kitűzve egyenlőre", + "End-to-end encryption isn't enabled": "Végpontok közötti titkosítás nincs engedélyezve" } From ed306d74562f3be4454d8cfc6a7a8efa54ea7673 Mon Sep 17 00:00:00 2001 From: jelv Date: Wed, 16 Jun 2021 11:39:17 +0000 Subject: [PATCH 171/809] Translated using Weblate (Dutch) Currently translated at 100.0% (2993 of 2993 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/nl/ --- src/i18n/strings/nl.json | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/src/i18n/strings/nl.json b/src/i18n/strings/nl.json index 7299e9d161..1818a64e54 100644 --- a/src/i18n/strings/nl.json +++ b/src/i18n/strings/nl.json @@ -1177,7 +1177,7 @@ "Homeserver URL": "Thuisserver-URL", "Identity Server URL": "Identiteitsserver-URL", "Free": "Gratis", - "Join millions for free on the largest public server": "Doe mee met miljoenen anderen op de grootste openbare server", + "Join millions for free on the largest public server": "Neem deel aan de grootste openbare server met miljoenen anderen", "Premium": "Premium", "Premium hosting for organisations Learn more": "Premium hosting voor organisaties Lees meer", "Other": "Overige", @@ -1513,7 +1513,7 @@ "View": "Bekijken", "Find a room…": "Zoek een gesprek…", "Find a room… (e.g. %(exampleRoom)s)": "Zoek een gesprek… (bv. %(exampleRoom)s)", - "If you can't find the room you're looking for, ask for an invite or Create a new room.": "Als u het gesprek dat u zoekt niet kunt vinden, vraag dan een uitnodiging, of Maak een nieuw gesprek aan.", + "If you can't find the room you're looking for, ask for an invite or Create a new room.": "Als u het gesprek dat u zoekt niet kunt vinden, vraag dan een uitnodiging of maak een nieuw gesprek aan.", "Explore rooms": "Gesprekken ontdekken", "Show previews/thumbnails for images": "Miniaturen voor afbeeldingen tonen", "Clear cache and reload": "Cache wissen en herladen", @@ -1932,7 +1932,7 @@ "Jump to first unread room.": "Ga naar het eerste ongelezen gesprek.", "Jump to first invite.": "Ga naar de eerste uitnodiging.", "Session verified": "Sessie geverifieerd", - "Your new session is now verified. It has access to your encrypted messages, and other users will see it as trusted.": "Uw nieuwe sessie is nu geverifieerd. U heeft nu toegang tot uw versleutelde berichten, en deze sessie zal voor andere gebruikers als vertrouwd gemarkeerd worden.", + "Your new session is now verified. It has access to your encrypted messages, and other users will see it as trusted.": "Uw nieuwe sessie is nu geverifieerd. U heeft nu toegang tot uw versleutelde berichten en deze sessie zal voor andere gebruikers als vertrouwd gemarkeerd worden.", "Your new session is now verified. Other users will see it as trusted.": "Uw nieuwe sessie is nu geverifieerd. Ze zal voor andere gebruikers als vertrouwd gemarkeerd worden.", "Without completing security on this session, it won’t have access to encrypted messages.": "Als u de beveiliging van deze sessie niet vervolledigt, zal ze geen toegang hebben tot uw versleutelde berichten.", "Go Back": "Terugkeren", @@ -3267,5 +3267,23 @@ "Try different words or check for typos. Some results may not be visible as they're private and you need an invite to join them.": "Probeer andere woorden of controleer op typefouten. Sommige resultaten zijn mogelijk niet zichtbaar omdat ze privé zijn of u een uitnodiging nodig heeft om deel te nemen.", "No results for \"%(query)s\"": "Geen resultaten voor \"%(query)s\"", "The user you called is busy.": "De gebruiker die u belde is bezet.", - "User Busy": "Gebruiker Bezet" + "User Busy": "Gebruiker Bezet", + "We're working on this as part of the beta, but just want to let you know.": "We werken hieraan als onderdeel van de beta, maar we willen het u gewoon laten weten.", + "Teammates might not be able to view or join any private rooms you make.": "Teamgenoten zijn mogelijk niet in staat zijn om privégesprekken die u maakt te bekijken of er lid van te worden.", + "Or send invite link": "Of verstuur uw uitnodigingslink", + "If you can't see who you’re looking for, send them your invite link below.": "Als u niet kunt vinden wie u zoekt, stuur ze dan uw uitnodigingslink hieronder.", + "Some suggestions may be hidden for privacy.": "Sommige suggesties kunnen om privacyredenen verborgen zijn.", + "Search for rooms or people": "Zoek naar gesprekken of personen", + "Message preview": "Voorbeeld van bericht", + "Forward message": "Bericht doorsturen", + "Open link": "Koppeling openen", + "Sent": "Verstuurd", + "You don't have permission to do this": "U heeft geen rechten om dit te doen", + "Error - Mixed content": "Fout - Gemengde inhoud", + "Error loading Widget": "Fout bij laden Widget", + "Pinned messages": "Vastgeprikte berichten", + "If you have permissions, open the menu on any message and select Pin to stick them here.": "Als u de rechten heeft, open dan het menu op elk bericht en selecteer Vastprikken om ze hier te zetten.", + "Nothing pinned, yet": "Nog niks vastgeprikt", + "End-to-end encryption isn't enabled": "Eind-tot-eind-versleuteling is uitgeschakeld", + "Your private messages are normally encrypted, but this room isn't. Usually this is due to an unsupported device or method being used, like email invites. Enable encryption in settings.": "Uw privéberichten zijn normaal gesproken versleuteld, maar dit gesprek niet. Meestal is dit te wijten aan een niet-ondersteund apparaat of methode die wordt gebruikt, zoals e-mailuitnodigingen. Versleuting inschakelen in instellingen." } From 3ddb103dd6e2a494f87e57a7547a985322dbf708 Mon Sep 17 00:00:00 2001 From: LinAGKar Date: Thu, 17 Jun 2021 08:20:23 +0000 Subject: [PATCH 172/809] Translated using Weblate (Swedish) Currently translated at 100.0% (2993 of 2993 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/sv/ --- src/i18n/strings/sv.json | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/sv.json b/src/i18n/strings/sv.json index f026e0fe02..6033b561bd 100644 --- a/src/i18n/strings/sv.json +++ b/src/i18n/strings/sv.json @@ -3311,5 +3311,23 @@ "Try different words or check for typos. Some results may not be visible as they're private and you need an invite to join them.": "Testa andra ord eller kolla efter felskrivningar. Vissa resultat kanske inte visas för att de är privata och du behöver en inbjudan för att gå med i dem.", "No results for \"%(query)s\"": "Inga resultat för \"%(query)s\"", "The user you called is busy.": "Användaren du ringde är upptagen.", - "User Busy": "Användare upptagen" + "User Busy": "Användare upptagen", + "We're working on this as part of the beta, but just want to let you know.": "Vi jobbar på detta som en del av betan, men vi ville låta dig veta.", + "Teammates might not be able to view or join any private rooms you make.": "Teammedlemmar kanske inte kan se eller gå med i privata rum du skapar.", + "Or send invite link": "Eller skicka inbjudningslänk", + "If you can't see who you’re looking for, send them your invite link below.": "Om du inte kan se den du letar efter, skicka dem din inbjudningslänk nedan.", + "Some suggestions may be hidden for privacy.": "Vissa förslag kan vara dolda av sekretesskäl.", + "Search for rooms or people": "Sök efter rum eller personer", + "Message preview": "Meddelandeförhandsvisning", + "Forward message": "Vidarebefordra meddelande", + "Open link": "Öppna länk", + "Sent": "Skickat", + "You don't have permission to do this": "Du har inte behörighet att göra detta", + "Error - Mixed content": "Fel - blandat innehåll", + "Error loading Widget": "Fel vid laddning av widget", + "Pinned messages": "Fästa meddelanden", + "If you have permissions, open the menu on any message and select Pin to stick them here.": "Om du har behörighet, öppna menyn på ett meddelande och välj Fäst för att fösta dem här.", + "Nothing pinned, yet": "Inget fäst än", + "End-to-end encryption isn't enabled": "Totalsträckskryptering är inte aktiverat", + "Your private messages are normally encrypted, but this room isn't. Usually this is due to an unsupported device or method being used, like email invites. Enable encryption in settings.": "Dina privata meddelanden är normalt krypterade, men det här rummet är inte det. Oftast så beror detta på att en enhet eller metod som används ej stöds, som e-postinbjudningar. Aktivera kryptering i inställningarna." } From a9d33cbe1f258b8b37dcbfaa8c401c51bd79b95f Mon Sep 17 00:00:00 2001 From: Percy Date: Wed, 16 Jun 2021 16:00:16 +0000 Subject: [PATCH 173/809] Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (2993 of 2993 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/zh_Hans/ --- src/i18n/strings/zh_Hans.json | 40 +++++++++++++++++++++++++---------- 1 file changed, 29 insertions(+), 11 deletions(-) diff --git a/src/i18n/strings/zh_Hans.json b/src/i18n/strings/zh_Hans.json index 3267060d64..7aa0d75539 100644 --- a/src/i18n/strings/zh_Hans.json +++ b/src/i18n/strings/zh_Hans.json @@ -510,8 +510,8 @@ "Restricted": "受限用户", "To use it, just wait for autocomplete results to load and tab through them.": "若要使用自动补全,只要等待自动补全结果加载完成,按 Tab 键切换即可。", "%(oldDisplayName)s changed their display name to %(displayName)s.": "%(oldDisplayName)s 将他们的昵称修改成了 %(displayName)s 。", - "Stickerpack": "贴图集", - "You don't currently have any stickerpacks enabled": "你目前没有启用任何贴图集", + "Stickerpack": "贴纸包", + "You don't currently have any stickerpacks enabled": "你目前未启用任何贴纸包", "Key request sent.": "已发送密钥共享请求。", "You will not be able to undo this change as you are demoting yourself, if you are the last privileged user in the room it will be impossible to regain privileges.": "如果你是房间中最后一位有权限的用户,在你降低自己的权限等级后将无法撤回此修改,因为你将无法重新获得权限。", "You will not be able to undo this change as you are promoting the user to have the same power level as yourself.": "你将无法撤回此修改,因为你正在将此用户的滥权等级提升至与你相同。", @@ -1071,10 +1071,10 @@ "Ignored users": "已忽略的用户", "Bulk options": "批量选择", "Key backup": "密钥备份", - "Security & Privacy": "安全与隐私", + "Security & Privacy": "隐私安全", "Missing media permissions, click the button below to request.": "缺少媒体权限,点击下面的按钮以请求权限。", "Request media permissions": "请求媒体权限", - "Voice & Video": "语音与视频", + "Voice & Video": "语音视频", "Room information": "聊天室信息", "Internal room ID:": "内部聊天室 ID:", "Room version": "聊天室版本", @@ -1566,7 +1566,7 @@ "If you don't want to use to discover and be discoverable by existing contacts you know, enter another identity server below.": "如果你不想使用 以发现你认识的现存联系人并被其发现,请在下方输入另一个身份服务器。", "Identity Server": "身份服务器", "You are not currently using an identity server. To discover and be discoverable by existing contacts you know, add one below.": "你现在没有使用身份服务器。若想发现你认识的现存联系人并被其发现,请在下方添加一个身份服务器。", - "Disconnecting from your identity server will mean you won't be discoverable by other users and you won't be able to invite others by email or phone.": "从你的身份服务器断开连接意味着你将不可被别的用户发现,同时你也将不能用邮箱或电话邀请别人。", + "Disconnecting from your identity server will mean you won't be discoverable by other users and you won't be able to invite others by email or phone.": "断开身份服务器连接意味着你将无法被其他用户发现,同时你也将无法使用电子邮件或电话邀请别人。", "Using an identity server is optional. If you choose not to use an identity server, you won't be discoverable by other users and you won't be able to invite others by email or phone.": "使用身份服务器是可选的。如果你选择不使用身份服务器,你将不能被别的用户发现,也不能用邮箱或电话邀请别人。", "Do not use an identity server": "不使用身份服务器", "Enter a new identity server": "输入一个新的身份服务器", @@ -1686,9 +1686,9 @@ "Cannot connect to integration manager": "不能连接到集成管理器", "The integration manager is offline or it cannot reach your homeserver.": "此集成管理器为离线状态或者其不能访问你的主服务器。", "check your browser plugins for anything that might block the identity server (such as Privacy Badger)": "检查你的浏览器是否安装有可能屏蔽身份服务器的插件(例如 Privacy Badger)", - "Use an Integration Manager (%(serverName)s) to manage bots, widgets, and sticker packs.": "使用集成管理器 (%(serverName)s) 以管理机器人、挂件和贴图集。", - "Use an Integration Manager to manage bots, widgets, and sticker packs.": "使用集成管理器以管理机器人、挂件和贴图集。", - "Manage integrations": "管理集成", + "Use an Integration Manager (%(serverName)s) to manage bots, widgets, and sticker packs.": "使用集成管理器 (%(serverName)s) 以管理机器人、挂件和贴纸包。", + "Use an Integration Manager to manage bots, widgets, and sticker packs.": "使用集成管理器以管理机器人、挂件和贴纸包。", + "Manage integrations": "集成管理", "Integration Managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "集成管理器接收配置数据,并可以以你的名义修改挂件、发送聊天室邀请及设置权限级别。", "Use between %(min)s pt and %(max)s pt": "请使用介于 %(min)s pt 和 %(max)s pt 之间的大小", "Deactivate account": "停用账号", @@ -1995,7 +1995,7 @@ "Verify this device to mark it as trusted. Trusting this device gives you and other users extra peace of mind when using end-to-end encrypted messages.": "验证此设备以将其标记为已信任。在收发端对端加密消息时,信任设备可让你与其他用户更加放心。", "Verifying this device will mark it as trusted, and users who have verified with you will trust this device.": "验证此设备会将其标记为已信任,与此同时,其他验证了你的用户也会信任此设备。", "Integrations are disabled": "集成已禁用", - "Enable 'Manage Integrations' in Settings to do this.": "在设置中启用「管理集成」以执行此操作。", + "Enable 'Manage Integrations' in Settings to do this.": "在设置中启用「管理管理」以执行此操作。", "Integrations not allowed": "集成未被允许", "Your %(brand)s doesn't allow you to use an Integration Manager to do this. Please contact an admin.": "你的 %(brand)s 不允许你使用集成管理器来完成此操作。请联系管理员。", "To continue, use Single Sign On to prove your identity.": "要继续,请使用单点登录证明你的身份。", @@ -2077,7 +2077,7 @@ "Integration Manager": "集成管理器", "Find others by phone or email": "通过电话或邮箱寻找别人", "Be found by phone or email": "通过电话或邮箱被寻找", - "Use bots, bridges, widgets and sticker packs": "使用机器人、桥接、挂件和贴图集", + "Use bots, bridges, widgets and sticker packs": "使用机器人、桥接、挂件和贴纸包", "Terms of Service": "服务协议", "To continue you need to accept the terms of this service.": "要继续,你需要接受此服务协议。", "Service": "服务", @@ -3280,5 +3280,23 @@ "Try different words or check for typos. Some results may not be visible as they're private and you need an invite to join them.": "尝试不同的单词或检查拼写错误。某些结果可能不可见,因为它们属于私有的,你需要一个邀请才能加入。", "No results for \"%(query)s\"": "「%(query)s」没有结果", "The user you called is busy.": "你所拨打的用户正在忙碌中。", - "User Busy": "用户正在忙" + "User Busy": "用户正在忙", + "We're working on this as part of the beta, but just want to let you know.": "我们正在研究让它成为测试版的一部分,但只想让你找到。", + "Teammates might not be able to view or join any private rooms you make.": "队友可能无法查看或加入你所创建的任何一个私有聊天室。", + "Or send invite link": "或发送邀请链接", + "If you can't see who you’re looking for, send them your invite link below.": "如果你找不到你正在寻找的人,请在下方向他们发送你的邀请链接。", + "Some suggestions may be hidden for privacy.": "出于隐私考虑,部分建议可能会被隐藏。", + "Search for rooms or people": "搜索聊天室或用户", + "Message preview": "消息预览", + "Forward message": "转发消息", + "Open link": "打开链接", + "Sent": "已发送", + "You don't have permission to do this": "你无权执行此操作", + "Error - Mixed content": "错误 - 混合内容", + "Error loading Widget": "加载挂件时发生错误", + "Pinned messages": "已置顶的消息", + "If you have permissions, open the menu on any message and select Pin to stick them here.": "如果你拥有权限,请打开任何消息的菜单并选择置顶将它们粘贴至此。", + "Nothing pinned, yet": "没有置顶", + "End-to-end encryption isn't enabled": "未启用端对端加密", + "Your private messages are normally encrypted, but this room isn't. Usually this is due to an unsupported device or method being used, like email invites. Enable encryption in settings.": "你的私人信息通常是被加密的,但此聊天室并未加密。一般而言,这可能是因为使用了不受支持的设备或方法,如电子邮件邀请。在设置中启用加密。" } From c0bc019e660c973e7707f680125330ca9718c853 Mon Sep 17 00:00:00 2001 From: Jeff Huang Date: Wed, 16 Jun 2021 02:14:34 +0000 Subject: [PATCH 174/809] Translated using Weblate (Chinese (Traditional)) Currently translated at 100.0% (2993 of 2993 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/zh_Hant/ --- src/i18n/strings/zh_Hant.json | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/zh_Hant.json b/src/i18n/strings/zh_Hant.json index 053839f937..d9429fc1c3 100644 --- a/src/i18n/strings/zh_Hant.json +++ b/src/i18n/strings/zh_Hant.json @@ -3384,5 +3384,22 @@ "Try different words or check for typos. Some results may not be visible as they're private and you need an invite to join them.": "嘗試不同的詞或是檢查拼字。某些結果可能不可見,因為其為私人的,您必須要有邀請才能加入。", "No results for \"%(query)s\"": "「%(query)s」沒有結果", "The user you called is busy.": "您想要通話的使用者目前忙碌中。", - "User Busy": "使用者忙碌" + "User Busy": "使用者忙碌", + "We're working on this as part of the beta, but just want to let you know.": "我們正將此作為測試版的一部分來處理,但只是想讓您知道。", + "Teammates might not be able to view or join any private rooms you make.": "隊友可能無法檢視或加入您建立的任何私人聊天室。", + "Or send invite link": "或傳送邀請連結", + "If you can't see who you’re looking for, send them your invite link below.": "如果您看不到您要找的人,請在下方向他們傳送您的邀請連結。", + "Some suggestions may be hidden for privacy.": "出於隱私考量,可能會隱藏一些建議。", + "Search for rooms or people": "搜尋聊天室或夥伴", + "Forward message": "轉寄訊息", + "Open link": "開啟連結", + "Sent": "已傳送", + "You don't have permission to do this": "您無權執行此動作", + "Error - Mixed content": "錯誤 - 混合內容", + "Error loading Widget": "載入小工具時發生錯誤", + "Pinned messages": "已釘選的訊息", + "If you have permissions, open the menu on any message and select Pin to stick them here.": "如果您有權限,請開啟任何訊息的選單,並選取釘選以將它們貼到這裡。", + "Nothing pinned, yet": "尚未釘選任何東西", + "End-to-end encryption isn't enabled": "端到端加密未啟用", + "Your private messages are normally encrypted, but this room isn't. Usually this is due to an unsupported device or method being used, like email invites. Enable encryption in settings.": "您的私人訊息通常是被加密的,但此聊天室不是。一般來說,這可能是因為使用了不支援的裝置或方法,例如電子郵件邀請。在設定中啟用加密。" } From 1013c1ec2953d77951d2c3797c97c903e89142b3 Mon Sep 17 00:00:00 2001 From: random Date: Thu, 17 Jun 2021 08:27:19 +0000 Subject: [PATCH 175/809] Translated using Weblate (Italian) Currently translated at 100.0% (2993 of 2993 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/it/ --- src/i18n/strings/it.json | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/it.json b/src/i18n/strings/it.json index c83800e82a..207ff24d58 100644 --- a/src/i18n/strings/it.json +++ b/src/i18n/strings/it.json @@ -3381,5 +3381,22 @@ "Try different words or check for typos. Some results may not be visible as they're private and you need an invite to join them.": "Prova parole diverse o controlla errori di battitura. Alcuni risultati potrebbero non essere visibili dato che sono privati e ti servirebbe un invito per unirti.", "No results for \"%(query)s\"": "Nessun risultato per \"%(query)s\"", "The user you called is busy.": "L'utente che hai chiamato è occupato.", - "User Busy": "Utente occupato" + "User Busy": "Utente occupato", + "We're working on this as part of the beta, but just want to let you know.": "Stiamo lavorando a questo come parte della beta, ma vogliamo almeno fartelo sapere.", + "Teammates might not be able to view or join any private rooms you make.": "I tuoi compagni potrebbero non riuscire a vedere o unirsi a qualsiasi stanza privata che crei.", + "Or send invite link": "O manda un collegamento di invito", + "If you can't see who you’re looking for, send them your invite link below.": "Se non vedi chi stai cercando, mandagli il collegamento di invito sottostante.", + "Some suggestions may be hidden for privacy.": "Alcuni suggerimenti potrebbero essere nascosti per privacy.", + "Search for rooms or people": "Cerca stanze o persone", + "Forward message": "Inoltra messaggio", + "Open link": "Apri collegamento", + "Sent": "Inviato", + "You don't have permission to do this": "Non hai il permesso per farlo", + "Error - Mixed content": "Errore - Contenuto misto", + "Error loading Widget": "Errore di caricamento del widget", + "Nothing pinned, yet": "Non c'è ancora nulla di ancorato", + "If you have permissions, open the menu on any message and select Pin to stick them here.": "Se ne hai il permesso, apri il menu di qualsiasi messaggio e seleziona Fissa per ancorarlo qui.", + "Pinned messages": "Messaggi ancorati", + "End-to-end encryption isn't enabled": "La crittografia end-to-end non è attiva", + "Your private messages are normally encrypted, but this room isn't. Usually this is due to an unsupported device or method being used, like email invites. Enable encryption in settings.": "I tuoi messaggi privati normalmente sono cifrati, ma questa stanza non lo è. Di solito ciò è dovuto ad un dispositivo non supportato o dal metodo usato, come gli inviti per email. Attiva la crittografia nelle impostazioni." } From f1a8c838f63ca9085ad042b6d2026308112cd0cd Mon Sep 17 00:00:00 2001 From: waclaw66 Date: Wed, 16 Jun 2021 07:16:47 +0000 Subject: [PATCH 176/809] Translated using Weblate (Czech) Currently translated at 100.0% (2993 of 2993 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/cs/ --- src/i18n/strings/cs.json | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/cs.json b/src/i18n/strings/cs.json index a84a10f21c..27235665aa 100644 --- a/src/i18n/strings/cs.json +++ b/src/i18n/strings/cs.json @@ -3298,5 +3298,23 @@ "Try different words or check for typos. Some results may not be visible as they're private and you need an invite to join them.": "Vyzkoušejte jiná slova nebo zkontrolujte překlepy. Některé výsledky nemusí být viditelné, protože jsou soukromé a potřebujete k nim pozvánku.", "No results for \"%(query)s\"": "Žádné výsledky pro \"%(query)s\"", "The user you called is busy.": "Volaný uživatel je zaneprázdněn.", - "User Busy": "Uživatel zaneprázdněn" + "User Busy": "Uživatel zaneprázdněn", + "We're working on this as part of the beta, but just want to let you know.": "Pracujeme na tom v rámci beta verze, ale jen vás o tom chceme informovat.", + "Teammates might not be able to view or join any private rooms you make.": "Je možné, že spolupracovníci nebudou moci zobrazit soukromé místnosti, které jste vytvořili, nebo se k nim připojit.", + "Or send invite link": "Nebo pošlete pozvánku", + "If you can't see who you’re looking for, send them your invite link below.": "Pokud jste nenašli, koho hledáte, pošlete mu odkaz na pozvánku níže.", + "Some suggestions may be hidden for privacy.": "Některé návrhy mohou být z důvodu ochrany soukromí skryty.", + "Search for rooms or people": "Hledat místnosti nebo osoby", + "Message preview": "Náhled zprávy", + "Forward message": "Přeposlat zprávu", + "Open link": "Otevřít odkaz", + "Sent": "Odesláno", + "You don't have permission to do this": "K tomu nemáte povolení", + "Error - Mixed content": "Chyba - Smíšený obsah", + "Error loading Widget": "Chyba při načítání widgetu", + "Pinned messages": "Připnuté zprávy", + "If you have permissions, open the menu on any message and select Pin to stick them here.": "Pokud máte oprávnění, otevřete nabídku na libovolné zprávě a výběrem možnosti Připnout je sem vložte.", + "Nothing pinned, yet": "Zatím není nic připnuto", + "End-to-end encryption isn't enabled": "Není povoleno koncové šifrování", + "Your private messages are normally encrypted, but this room isn't. Usually this is due to an unsupported device or method being used, like email invites. Enable encryption in settings.": "Vaše soukromé zprávy jsou obvykle šifrované, ale tato místnost není. Obvykle je to způsobeno nepodporovaným zařízením nebo použitou metodou, například emailovými pozvánkami. Zapněte šifrování v nastavení." } From fcf31240e319f0e4b76b05d427473c3900b8c23e Mon Sep 17 00:00:00 2001 From: XoseM Date: Wed, 16 Jun 2021 06:03:11 +0000 Subject: [PATCH 177/809] Translated using Weblate (Galician) Currently translated at 100.0% (2993 of 2993 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/gl/ --- src/i18n/strings/gl.json | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/gl.json b/src/i18n/strings/gl.json index abb4776a55..b880c5b548 100644 --- a/src/i18n/strings/gl.json +++ b/src/i18n/strings/gl.json @@ -3381,5 +3381,22 @@ "Try different words or check for typos. Some results may not be visible as they're private and you need an invite to join them.": "Intentao con outras palabras e fíxate nos erros de escritura. Algúns resultados poderían non ser visibles porque son privados e precisas un convite.", "No results for \"%(query)s\"": "Sen resultados para \"%(query)s\"", "The user you called is busy.": "A persoa á que chamas está ocupada.", - "User Busy": "Usuaria ocupada" + "User Busy": "Usuaria ocupada", + "We're working on this as part of the beta, but just want to let you know.": "Estamos traballando nesto como parte da beta, só queriamos que o soubeses.", + "Teammates might not be able to view or join any private rooms you make.": "As outras compañeiras de grupo poderían non ver ou unirse ás salas privadas que creas.", + "Or send invite link": "Ou envía ligazón de convite", + "If you can't see who you’re looking for, send them your invite link below.": "Se non podes a quen estás a buscar, envíalle ti esta ligazón de convite.", + "Some suggestions may be hidden for privacy.": "Algunhas suxestións poderían estar agochadas por privacidade.", + "Search for rooms or people": "Busca salas ou persoas", + "Forward message": "Reenviar mensaxe", + "Open link": "Abrir ligazón", + "Sent": "Enviado", + "You don't have permission to do this": "Non tes permiso para facer isto", + "Error - Mixed content": "Erro - Contido variado", + "Error loading Widget": "Erro ao cargar o Widget", + "Pinned messages": "Mensaxes fixadas", + "If you have permissions, open the menu on any message and select Pin to stick them here.": "Se tes permisos, abre o menú en calquera mensaxe e elixe Fixar para pegalos aquí.", + "Nothing pinned, yet": "Nada fixado, por agora", + "End-to-end encryption isn't enabled": "Non está activado o cifrado de extremo-a-extremo", + "Your private messages are normally encrypted, but this room isn't. Usually this is due to an unsupported device or method being used, like email invites. Enable encryption in settings.": "As túas mensaxes privadas normalmente están cifradas, pero esta sala non. Habitualmente esto é debido a que se utiliza un dispositivo ou métodos no soportados, como convites por email. Activa o cifrado nos axustes." } From e7b05963802f9eb6b8b1192c9a9e95dfa8958ff0 Mon Sep 17 00:00:00 2001 From: Besnik Bleta Date: Wed, 16 Jun 2021 10:14:11 +0000 Subject: [PATCH 178/809] Translated using Weblate (Albanian) Currently translated at 99.7% (2985 of 2993 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/sq/ --- src/i18n/strings/sq.json | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/sq.json b/src/i18n/strings/sq.json index 4996decbaf..b2101151e1 100644 --- a/src/i18n/strings/sq.json +++ b/src/i18n/strings/sq.json @@ -3370,5 +3370,21 @@ "The user you called is busy.": "Përdoruesi që thirrët është i zënë.", "User Busy": "Përdoruesi Është i Zënë", "Kick, ban, or invite people to your active room, and make you leave": "Përzini, dëboni, ose ftoni persona te dhoma juaj aktive, dhe bëni largimin tuaj", - "Kick, ban, or invite people to this room, and make you leave": "Përzini, dëboni, ose ftoni persona në këtë dhomë, dhe bëni largimin tuaj" + "Kick, ban, or invite people to this room, and make you leave": "Përzini, dëboni, ose ftoni persona në këtë dhomë, dhe bëni largimin tuaj", + "We're working on this as part of the beta, but just want to let you know.": "Po merremi me këtë, si pjesë e versionit beta, thjesht duam ta dini.", + "Teammates might not be able to view or join any private rooms you make.": "Anëtarët e ekipit mund të mos jenë në gjendje të shohin ose hyjnë në çfarëdo dhome private që krijoni.", + "Or send invite link": "Ose dërgoni një lidhje ftese", + "If you can't see who you’re looking for, send them your invite link below.": "Nëse s’e shihni atë që po kërkoni, dërgojini nga më poshtë një lidhje ftese.", + "Some suggestions may be hidden for privacy.": "Disa sugjerime mund të jenë fshehur, për arsye privatësie.", + "Search for rooms or people": "Kërkoni për dhoma ose persona", + "Forward message": "Përcille mesazhin", + "Open link": "Hape lidhjen", + "You don't have permission to do this": "S’keni leje ta bëni këtë", + "Error - Mixed content": "Gabim - Lëndë e përzierë", + "Error loading Widget": "Gabim në ngarkim Widget-i", + "Pinned messages": "Mesazhe të fiksuar", + "If you have permissions, open the menu on any message and select Pin to stick them here.": "Nëse keni leje, hapni menunë për çfarëdo mesazhi dhe përzgjidhni Fiksoje, për ta ngjitur këtu.", + "Nothing pinned, yet": "Ende pa fiksuar gjë", + "End-to-end encryption isn't enabled": "Fshehtëzimi skaj-më-skaj s’është i aktivizuar", + "Your private messages are normally encrypted, but this room isn't. Usually this is due to an unsupported device or method being used, like email invites. Enable encryption in settings.": "Mesazhet tuaja private normalisht fshehtëzohen, por kjo dhomë nuk fshehtëzohet. Zakonisht kjo vjen si pasojë e përdorimit të një pajisjeje apo metode të pambuluar, bie fjala, ftesa me email. Aktivizoni fshehtëzimin që nga rregullimet." } From d33cfd6869e61e79b2f1d7d8f9da30dfbec9a943 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20J=C3=B5er=C3=BC=C3=BCt?= Date: Fri, 18 Jun 2021 21:35:18 +0000 Subject: [PATCH 179/809] Translated using Weblate (Estonian) Currently translated at 99.8% (2988 of 2993 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/et/ --- src/i18n/strings/et.json | 31 ++++++++++++++++++++++++------- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/src/i18n/strings/et.json b/src/i18n/strings/et.json index a8e267fb01..9454efd956 100644 --- a/src/i18n/strings/et.json +++ b/src/i18n/strings/et.json @@ -126,7 +126,7 @@ "Explore Public Rooms": "Sirvi avalikke jututubasid", "Explore": "Uuri", "Filter rooms…": "Filtreeri jututubasid…", - "Create a community to group together users and rooms! Build a custom homepage to mark out your space in the Matrix universe.": "Liites kokku kasutajaid ja jututubasid loo oma kogukond! Loo kogukonna koduleht, et märkida oma koht Matrix'i universumis.", + "Create a community to group together users and rooms! Build a custom homepage to mark out your space in the Matrix universe.": "Liites kokku kasutajaid ja jututubasid loo oma kogukond! Kogukonna kodulehega tähistad oma koha Matrix'i universumis.", "Explore rooms": "Uuri jututubasid", "If you've joined lots of rooms, this might take a while": "Kui oled liitunud paljude jututubadega, siis see võib natuke aega võtta", "If disabled, messages from encrypted rooms won't appear in search results.": "Kui see seadistus pole kasutusel, siis krüptitud jututubade sõnumeid otsing ei vaata.", @@ -2218,7 +2218,7 @@ "%(brand)s can't securely cache encrypted messages locally while running in a web browser. Use %(brand)s Desktop for encrypted messages to appear in search results.": "%(brand)s ei võimalda veebibrauseris töötades krüptitud sõnumeid turvaliselt puhverdada. Selleks, et krüptitud sõnumeid saaks otsida, kasuta %(brand)s Desktop rakendust Matrix'i kliendina.", "Encrypted messages are secured with end-to-end encryption. Only you and the recipient(s) have the keys to read these messages.": "Krüptitud sõnumid kasutavad läbivat krüptimist. Ainult sinul ja saaja(te)l on võtmed selliste sõnumite lugemiseks.", "Unable to load key backup status": "Võtmete varunduse oleku laadimine ei õnnestunud", - "Restore from Backup": "Taasta varundusest", + "Restore from Backup": "Taasta varukoopiast", "This session is backing up your keys. ": "See sessioon varundab sinu krüptovõtmeid. ", "This session is not backing up your keys, but you do have an existing backup you can restore from and add to going forward.": "See sessioon ei varunda sinu krüptovõtmeid, aga sul on olemas varundus, millest saad taastada ning millele saad võtmeid lisada.", "Success": "Õnnestus", @@ -3298,7 +3298,7 @@ "You have no ignored users.": "Sa ei ole veel kedagi eiranud.", "Play": "Esita", "Pause": "Peata", - "Feeling experimental? Labs are the best way to get things early, test out new features and help shape them before they actually launch. Learn more.": "Kas sa tahaksid katsetada? Sa tutvud meie rakenduse uuendustega teistest varem ja võib-olla isegi saad mõjutada arenduse lõpptulemust. Lisateavet liad siit.", + "Feeling experimental? Labs are the best way to get things early, test out new features and help shape them before they actually launch. Learn more.": "Kas sa tahaksid katsetada? Sa tutvud meie rakenduse uuendustega teistest varem ja võib-olla isegi saad mõjutada arenduse lõpptulemust. Lisateavet leiad siit.", "This is an experimental feature. For now, new users receiving an invite will have to open the invite on to actually join.": "See on katseline funktsionaalsus. Seetõttu uued kutse saanud kasutajad peavad tegelikuks liitumiseks avama kutse siin .", "To join %(spaceName)s, turn on the Spaces beta": "%(spaceName)s kogukonnakeskusega liitumiseks lülita sisse vastav katseline funktsionaalsus", "To view %(spaceName)s, turn on the Spaces beta": "%(spaceName)s kogukonnakeskuse vaatamiseks lülita sisse vastav katseline funktsionaalsus", @@ -3327,9 +3327,9 @@ "Please enter a name for the space": "Palun sisesta kogukonnakeskuse nimi", "Connecting": "Kõne on ühendamisel", "Allow Peer-to-Peer for 1:1 calls (if you enable this, the other party might be able to see your IP address)": "Kasuta võrdõigusvõrku 1:1 kõnede jaoks (kui sa P2P-võrgu sisse lülitad, siis teine osapool ilmselt näeb sinu IP-aadressi)", - "Beta available for web, desktop and Android. Some features may be unavailable on your homeserver.": "Rakenduse beetaversioon on saadaval veebirakendusena, töölauarakendusena ja Androidi jaoks. Kõik funtsionaalsused ei pruugi sinu koduserveri poolt olla toetatud.", + "Beta available for web, desktop and Android. Some features may be unavailable on your homeserver.": "Rakenduse beetaversioon on saadaval veebirakendusena, töölauarakendusena ja Androidi jaoks. Kõik funktsionaalsused ei pruugi sinu koduserveri poolt olla toetatud.", "You can leave the beta any time from settings or tapping on a beta badge, like the one above.": "Sa võid beetaversiooni kasutamise lõpetada niipea, kui tahad. Selleks klõpsi beeta-silti, mida näed siin samas ülal.", - "%(brand)s will reload with Spaces enabled. Communities and custom tags will be hidden.": "Käivitame %(brand)s uuesti nii, et kogukonnakeskused on kasutusel. Vana tüüpi kogukonnad ja kohandatud sildid on siis välja lülitatud.", + "%(brand)s will reload with Spaces enabled. Communities and custom tags will be hidden.": "Käivitame %(brand)s'i uuesti nii, et kogukonnakeskused on kasutusel. Vana tüüpi kogukonnad ja kohandatud sildid on siis välja lülitatud.", "%(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.": "Käivitame %(brand)s uuesti nii, et kogukonnakeskused ei ole kasutusel. Vana tüüpi kogukonnad ja kohandatud sildid saavad jälle olema kasutusel.", "Beta available for web, desktop and Android. Thank you for trying the beta.": "Rakenduse beetaversioon on saadaval veebirakendusena, töölauarakendusena ja Androidi jaoks. Tänud, et oled huviline katsetama meie rakendust.", "Spaces are a new way to group rooms and people.": "Kogukonnakeskused on uus viis jututubade ja inimeste ühendamiseks.", @@ -3344,7 +3344,7 @@ "Add reaction": "Lisa reaktsioon", "Send and receive voice messages": "Saada ja võta vastu häälsõnumeid", "Your feedback will help make spaces better. The more detail you can go into, the better.": "Sinu tagasiside aitab teha kogukonnakeskuseid paremaks. Mida detailsemalt sa oma arvamust kirjeldad, seda parem.", - "If you leave, %(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.": "Kui sa lahkud, siis käivitame %(brand)s uuesti nii, et kogukonnakeskused ei ole kasutusel. Vana tüüpi kogukonnad ja kohandatud sildid saavad jälle olema kasutusel.", + "If you leave, %(brand)s will reload with Spaces disabled. Communities and custom tags will be visible again.": "Kui sa lahkud, siis käivitame %(brand)s'i uuesti nii, et kogukonnakeskused ei ole kasutusel. Vana tüüpi kogukonnad ja kohandatud sildid saavad jälle olema kasutusel.", "Message search initialisation failed": "Sõnumite otsingu alustamine ei õnnestunud", "sends space invaders": "korraldab ühe pisikese tulnukate vallutusretke", "Sends the given message with a space themed effect": "Saadab antud sõnumi kosmoseteemalise efektiga", @@ -3354,5 +3354,22 @@ "No results for \"%(query)s\"": "Päringule „%(query)s“ pole vastuseid", "Try different words or check for typos. Some results may not be visible as they're private and you need an invite to join them.": "Proovi muid otsingusõnu või kontrolli, et neis polnud trükivigu. Kuna mõned otsingutulemused on privaatsed ja sa vajad kutset nende nägemiseks, siis kõiki tulemusi siin ei pruugi näha olla.", "Currently joining %(count)s rooms|other": "Parasjagu liitun %(count)s jututoaga", - "Currently joining %(count)s rooms|one": "Parasjagu liitun %(count)s jututoaga" + "Currently joining %(count)s rooms|one": "Parasjagu liitun %(count)s jututoaga", + "Your private messages are normally encrypted, but this room isn't. Usually this is due to an unsupported device or method being used, like email invites. Enable encryption in settings.": "Sinu isiklikud sõnumid on tavaliselt läbivalt krüptitud, aga see jututuba ei ole. Tavaliselt on põhjuseks, et kasutusel on mõni seade või meetod nagu e-posti põhised kutsed, mis krüptimist veel ei toeta. Läbiva krüptimise saad sisse lülitada seadistustes.", + "We're working on this as part of the beta, but just want to let you know.": "Me küll alles arendame seda beetafunktsionaalsust, aga soovime, et sa sellest juba teaksid.", + "Teammates might not be able to view or join any private rooms you make.": "Kui muudad jututoa privaatseks, siis sinu kaaslased ei pruugi seda näha ega temaga liituda.", + "If you can't see who you’re looking for, send them your invite link below.": "Kui sa ei leia otsitavaid, siis saada neile kutse.", + "Or send invite link": "Või saada kutse link", + "If you have permissions, open the menu on any message and select Pin to stick them here.": "Kui sul on vastavad õigused olemas, siis ava sõnumi juuresolev menüü ning püsisõnumi tekitamiseks vali Klammerda.", + "Pinned messages": "Klammerdatud sõnumid", + "Nothing pinned, yet": "Klammerdatud sõnumeid veel pole", + "End-to-end encryption isn't enabled": "Läbiv krüptimine pole kasutusel", + "Some suggestions may be hidden for privacy.": "Mõned soovitused võivad privaatsusseadistuste tõttu olla peidetud.", + "Search for rooms or people": "Otsi jututubasid või inimesi", + "Forward message": "Edasta sõnum", + "Open link": "Ava link", + "Sent": "Saadetud", + "You don't have permission to do this": "Sul puuduvad selleks toiminguks õigused", + "Error - Mixed content": "Viga - erinev sisu", + "Error loading Widget": "Viga vidina laadimisel" } From d0ea842f1e0aa3bbd3d99009c2ce31284c76eacf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sun, 20 Jun 2021 10:29:08 +0200 Subject: [PATCH 180/809] Remove sorting by index as it is already done here: https://github.com/matrix-org/matrix-react-sdk/blob/e9ea3cad76173c5e5b8f4d7d618a3d8a17548102/src/autocomplete/QueryMatcher.ts#L120 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/autocomplete/RoomProvider.tsx | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/autocomplete/RoomProvider.tsx b/src/autocomplete/RoomProvider.tsx index ad55b19101..04dc9720e6 100644 --- a/src/autocomplete/RoomProvider.tsx +++ b/src/autocomplete/RoomProvider.tsx @@ -32,15 +32,6 @@ import SettingsStore from "../settings/SettingsStore"; const ROOM_REGEX = /\B#\S*/g; -function score(query: string, space: string) { - const index = space.indexOf(query); - if (index === -1) { - return Infinity; - } else { - return index; - } -} - function matcherObject(room: Room, displayedAlias: string, matchName = "") { return { room, @@ -106,7 +97,6 @@ export default class RoomProvider extends AutocompleteProvider { const matchedString = command[0]; completions = this.matcher.match(matchedString, limit); completions = sortBy(completions, [ - (c) => score(matchedString, c.displayedAlias), (c) => c.displayedAlias.length, ]); completions = uniqBy(completions, (match) => match.room); From 7bf230e66502679c3f9894e8e642b215e48fd30d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sun, 20 Jun 2021 10:41:36 +0200 Subject: [PATCH 181/809] Prefer canonical aliases over non-canonical ones MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/autocomplete/RoomProvider.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/autocomplete/RoomProvider.tsx b/src/autocomplete/RoomProvider.tsx index 04dc9720e6..f784a57821 100644 --- a/src/autocomplete/RoomProvider.tsx +++ b/src/autocomplete/RoomProvider.tsx @@ -32,6 +32,11 @@ import SettingsStore from "../settings/SettingsStore"; const ROOM_REGEX = /\B#\S*/g; +// Prefer canonical aliases over non-canonical ones +function canonicalScore(displayedAlias: string, room: Room): number { + return displayedAlias === room.getCanonicalAlias() ? 0 : 1; +} + function matcherObject(room: Room, displayedAlias: string, matchName = "") { return { room, @@ -97,6 +102,7 @@ export default class RoomProvider extends AutocompleteProvider { const matchedString = command[0]; completions = this.matcher.match(matchedString, limit); completions = sortBy(completions, [ + (c) => canonicalScore(c.displayedAlias, c.room), (c) => c.displayedAlias.length, ]); completions = uniqBy(completions, (match) => match.room); From 6c64f564e4ccba567564edfaeb9417fcefa9e05b Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 18 Jun 2021 18:32:45 +0100 Subject: [PATCH 182/809] Naive attempt at improving our end-to-end tests in Github Actions --- .github/workflows/develop.yml | 7 +++++-- ...d-tests.sh => prepare-end-to-end-tests.sh} | 11 +---------- scripts/ci/run-end-to-end-tests.sh | 19 +++++++++++++++++++ 3 files changed, 25 insertions(+), 12 deletions(-) rename scripts/ci/{end-to-end-tests.sh => prepare-end-to-end-tests.sh} (65%) create mode 100755 scripts/ci/run-end-to-end-tests.sh diff --git a/.github/workflows/develop.yml b/.github/workflows/develop.yml index 6410bd28fa..3c3807e33b 100644 --- a/.github/workflows/develop.yml +++ b/.github/workflows/develop.yml @@ -11,10 +11,13 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v2 - - name: End-to-End tests - run: ./scripts/ci/end-to-end-tests.sh + - name: Prepare End-to-End tests + run: ./scripts/ci/prepare-end-to-end-tests.sh + - name: Run End-to-End tests + run: ./scripts/ci/run-end-to-end-tests.sh - name: Archive logs uses: actions/upload-artifact@v2 + if: ${{ always() }} with: path: | test/end-to-end-tests/logs/**/* diff --git a/scripts/ci/end-to-end-tests.sh b/scripts/ci/prepare-end-to-end-tests.sh similarity index 65% rename from scripts/ci/end-to-end-tests.sh rename to scripts/ci/prepare-end-to-end-tests.sh index edb8870d8e..147e1f6445 100755 --- a/scripts/ci/end-to-end-tests.sh +++ b/scripts/ci/prepare-end-to-end-tests.sh @@ -1,8 +1,4 @@ #!/bin/bash -# -# script which is run by the CI build (after `yarn test`). -# -# clones element-web develop and runs the tests against our version of react-sdk. set -ev @@ -19,7 +15,7 @@ cd element-web element_web_dir=`pwd` CI_PACKAGE=true yarn build cd .. -# run end to end tests +# prepare end to end tests pushd test/end-to-end-tests ln -s $element_web_dir element/element-web # PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true ./install.sh @@ -28,9 +24,4 @@ echo "--- Install synapse & other dependencies" ./install.sh # install static webserver to server symlinked local copy of element ./element/install-webserver.sh -rm -r logs || true -mkdir logs -echo "+++ Running end-to-end tests" -TESTS_STARTED=1 -./run.sh --no-sandbox --log-directory logs/ popd diff --git a/scripts/ci/run-end-to-end-tests.sh b/scripts/ci/run-end-to-end-tests.sh new file mode 100755 index 0000000000..3c99391fc7 --- /dev/null +++ b/scripts/ci/run-end-to-end-tests.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +set -ev + +handle_error() { + EXIT_CODE=$? + exit $EXIT_CODE +} + +trap 'handle_error' ERR + +# run end to end tests +pushd test/end-to-end-tests +rm -r logs || true +mkdir logs +echo "--- Running end-to-end tests" +TESTS_STARTED=1 +./run.sh --no-sandbox --log-directory logs/ +popd From e79b7d7adb7b8f5214d0b498cd555957ccc5f1ba Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Sat, 19 Jun 2021 15:37:48 +0100 Subject: [PATCH 183/809] Fix View Source accessing renamed private field on MatrixEvent --- src/components/structures/ViewSource.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/structures/ViewSource.js b/src/components/structures/ViewSource.js index 6fe99dd464..b69a92dd61 100644 --- a/src/components/structures/ViewSource.js +++ b/src/components/structures/ViewSource.js @@ -55,7 +55,7 @@ export default class ViewSource extends React.Component { viewSourceContent() { const mxEvent = this.props.mxEvent.replacingEvent() || this.props.mxEvent; // show the replacing event, not the original, if it is an edit const isEncrypted = mxEvent.isEncrypted(); - const decryptedEventSource = mxEvent._clearEvent; // FIXME: _clearEvent is private + const decryptedEventSource = mxEvent.clearEvent; // FIXME: clearEvent is private const originalEventSource = mxEvent.event; if (isEncrypted) { From 2d9e97a3e13bb481431f812178c47f6c75e395c6 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Mon, 21 Jun 2021 09:47:46 +0100 Subject: [PATCH 184/809] Fix branch matching to work with GitHub Actions and BuildKite --- scripts/fetchdep.sh | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/scripts/fetchdep.sh b/scripts/fetchdep.sh index fe1f49c361..9844fdc9db 100755 --- a/scripts/fetchdep.sh +++ b/scripts/fetchdep.sh @@ -22,15 +22,18 @@ clone() { } # Try the PR author's branch in case it exists on the deps as well. -# First we check if BUILDKITE_BRANCH is defined, +# First we check if GITHUB_HEAD_REF is defined, +# Then we check if BUILDKITE_BRANCH is defined, # if it isn't we can assume this is a Netlify build -if [ -z ${BUILDKITE_BRANCH+x} ]; then +if [ -n ${GITHUB_HEAD_REF+x} ]; then + head=$GITHUB_HEAD_REF +elif [ -n ${BUILDKITE_BRANCH+x} ]; then + head=$BUILDKITE_BRANCH +else # Netlify doesn't give us info about the fork so we have to get it from GitHub API apiEndpoint="https://api.github.com/repos/matrix-org/matrix-react-sdk/pulls/" apiEndpoint+=$REVIEW_ID head=$(curl $apiEndpoint | jq -r '.head.label') -else - head=$BUILDKITE_BRANCH fi # If head is set, it will contain either: @@ -39,12 +42,18 @@ fi # We can split on `:` into an array to check. BRANCH_ARRAY=(${head//:/ }) if [[ "${#BRANCH_ARRAY[@]}" == "1" ]]; then - clone $deforg $defrepo $BUILDKITE_BRANCH + clone $deforg $defrepo $head elif [[ "${#BRANCH_ARRAY[@]}" == "2" ]]; then clone ${BRANCH_ARRAY[0]} $defrepo ${BRANCH_ARRAY[1]} fi + # Try the target branch of the push or PR. -clone $deforg $defrepo $BUILDKITE_PULL_REQUEST_BASE_BRANCH +if [ -n ${GITHUB_BASE_REF+x} ]; then + clone $deforg $defrepo $GITHUB_BASE_REF +elif [ -n ${BUILDKITE_PULL_REQUEST_BASE_BRANCH+x} ]; then + clone $deforg $defrepo $BUILDKITE_PULL_REQUEST_BASE_BRANCH +fi + # Try HEAD which is the branch name in Netlify (not BRANCH which is pull/xxxx/head for PR builds) clone $deforg $defrepo $HEAD # Use the default branch as the last resort. From 9756a99220a34a1a4bae9c0b15c14871ef161818 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Mon, 21 Jun 2021 12:14:30 +0100 Subject: [PATCH 185/809] Migrate TruncatedList to TypeScript --- .../{TruncatedList.js => TruncatedList.tsx} | 50 +++++++++---------- 1 file changed, 24 insertions(+), 26 deletions(-) rename src/components/views/elements/{TruncatedList.js => TruncatedList.tsx} (65%) diff --git a/src/components/views/elements/TruncatedList.js b/src/components/views/elements/TruncatedList.tsx similarity index 65% rename from src/components/views/elements/TruncatedList.js rename to src/components/views/elements/TruncatedList.tsx index 0509775545..395caa9222 100644 --- a/src/components/views/elements/TruncatedList.js +++ b/src/components/views/elements/TruncatedList.tsx @@ -16,31 +16,29 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; import { _t } from '../../../languageHandler'; import {replaceableComponent} from "../../../utils/replaceableComponent"; -@replaceableComponent("views.elements.TruncatedList") -export default class TruncatedList extends React.Component { - static propTypes = { - // The number of elements to show before truncating. If negative, no truncation is done. - truncateAt: PropTypes.number, - // The className to apply to the wrapping div - className: PropTypes.string, - // A function that returns the children to be rendered into the element. - // function getChildren(start: number, end: number): Array - // The start element is included, the end is not (as in `slice`). - // If omitted, the React child elements will be used. This parameter can be used - // to avoid creating unnecessary React elements. - getChildren: PropTypes.func, - // A function that should return the total number of child element available. - // Required if getChildren is supplied. - getChildCount: PropTypes.func, - // A function which will be invoked when an overflow element is required. - // This will be inserted after the children. - createOverflowElement: PropTypes.func, - }; +interface IProps { + // The number of elements to show before truncating. If negative, no truncation is done. + truncateAt?: number; + // The className to apply to the wrapping div + className?: string; + // A function that returns the children to be rendered into the element. + // The start element is included, the end is not (as in `slice`). + // If omitted, the React child elements will be used. This parameter can be used + // to avoid creating unnecessary React elements. + getChildren?: (start: number, end: number) => Array; + // A function that should return the total number of child element available. + // Required if getChildren is supplied. + getChildCount?: () => number; + // A function which will be invoked when an overflow element is required. + // This will be inserted after the children. + createOverflowElement?: (overflowCount: number, totalCount: number) => React.ReactNode; +} +@replaceableComponent("views.elements.TruncatedList") +export default class TruncatedList extends React.Component { static defaultProps ={ truncateAt: 2, createOverflowElement(overflowCount, totalCount) { @@ -50,7 +48,7 @@ export default class TruncatedList extends React.Component { }, }; - _getChildren(start, end) { + private getChildren(start: number, end: number): Array { if (this.props.getChildren && this.props.getChildCount) { return this.props.getChildren(start, end); } else { @@ -63,7 +61,7 @@ export default class TruncatedList extends React.Component { } } - _getChildCount() { + private getChildCount(): number { if (this.props.getChildren && this.props.getChildCount) { return this.props.getChildCount(); } else { @@ -73,10 +71,10 @@ export default class TruncatedList extends React.Component { } } - render() { + public render() { let overflowNode = null; - const totalChildren = this._getChildCount(); + const totalChildren = this.getChildCount(); let upperBound = totalChildren; if (this.props.truncateAt >= 0) { const overflowCount = totalChildren - this.props.truncateAt; @@ -87,7 +85,7 @@ export default class TruncatedList extends React.Component { upperBound = this.props.truncateAt; } } - const childNodes = this._getChildren(0, upperBound); + const childNodes = this.getChildren(0, upperBound); return (
    From d2595dcd61876d9f1def0b5e197918588692c07b Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Mon, 21 Jun 2021 12:29:59 +0100 Subject: [PATCH 186/809] use TruncatedList to improve ForwardDialog rendering time --- .../views/dialogs/ForwardDialog.tsx | 37 ++++++++++++++----- 1 file changed, 28 insertions(+), 9 deletions(-) diff --git a/src/components/views/dialogs/ForwardDialog.tsx b/src/components/views/dialogs/ForwardDialog.tsx index a83f3f177c..b04fd9ef76 100644 --- a/src/components/views/dialogs/ForwardDialog.tsx +++ b/src/components/views/dialogs/ForwardDialog.tsx @@ -39,6 +39,9 @@ import NotificationBadge from "../rooms/NotificationBadge"; import {RoomPermalinkCreator} from "../../../utils/permalinks/Permalinks"; import {sortRooms} from "../../../stores/room-list/algorithms/tag-sorting/RecentAlgorithm"; import QueryMatcher from "../../../autocomplete/QueryMatcher"; +import TruncatedList from "../elements/TruncatedList"; +import EntityTile from "../rooms/EntityTile"; +import BaseAvatar from "../avatars/BaseAvatar"; const AVATAR_SIZE = 30; @@ -195,6 +198,17 @@ const ForwardDialog: React.FC = ({ matrixClient: cli, event, permalinkCr }).match(lcQuery); } + const [truncateAt, setTruncateAt] = useState(20); + function overflowTile(overflowCount, totalCount) { + const text = _t("and %(count)s others...", { count: overflowCount }); + return ( + + } name={text} presenceState="online" suppressOnHover={true} + onClick={() => setTruncateAt(totalCount)} /> + ); + } + return = ({ matrixClient: cli, event, permalinkCr { rooms.length > 0 ? (
    - { rooms.map(room => - , - ) } + rooms.slice(start, end).map(room => + , + )} + getChildCount={() => rooms.length} + />
    ) : { _t("No results") } From ca123d3c4daaf18e8ab0dcd9ced019fcd6762f37 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Mon, 21 Jun 2021 14:05:56 +0100 Subject: [PATCH 187/809] Migrate MKeyVerificationRequest to TypeScript --- ...Request.js => MKeyVerificationRequest.tsx} | 64 +++++++++---------- 1 file changed, 29 insertions(+), 35 deletions(-) rename src/components/views/messages/{MKeyVerificationRequest.js => MKeyVerificationRequest.tsx} (77%) diff --git a/src/components/views/messages/MKeyVerificationRequest.js b/src/components/views/messages/MKeyVerificationRequest.tsx similarity index 77% rename from src/components/views/messages/MKeyVerificationRequest.js rename to src/components/views/messages/MKeyVerificationRequest.tsx index 988606a766..df35c47706 100644 --- a/src/components/views/messages/MKeyVerificationRequest.js +++ b/src/components/views/messages/MKeyVerificationRequest.tsx @@ -15,41 +15,40 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; -import {MatrixClientPeg} from '../../../MatrixClientPeg'; +import { MatrixEvent } from 'matrix-js-sdk/src'; +import { MatrixClientPeg } from '../../../MatrixClientPeg'; import * as sdk from '../../../index'; import { _t } from '../../../languageHandler'; -import {getNameForEventRoom, userLabelForEventRoom} +import { getNameForEventRoom, userLabelForEventRoom } from '../../../utils/KeyVerificationStateObserver'; import dis from "../../../dispatcher/dispatcher"; -import {RightPanelPhases} from "../../../stores/RightPanelStorePhases"; -import {Action} from "../../../dispatcher/actions"; +import { RightPanelPhases } from "../../../stores/RightPanelStorePhases"; +import { Action } from "../../../dispatcher/actions"; import EventTileBubble from "./EventTileBubble"; -import {replaceableComponent} from "../../../utils/replaceableComponent"; +import { replaceableComponent } from "../../../utils/replaceableComponent"; + +interface IProps { + mxEvent: MatrixEvent +} @replaceableComponent("views.messages.MKeyVerificationRequest") -export default class MKeyVerificationRequest extends React.Component { - constructor(props) { - super(props); - this.state = {}; - } - - componentDidMount() { +export default class MKeyVerificationRequest extends React.Component { + public componentDidMount() { const request = this.props.mxEvent.verificationRequest; if (request) { - request.on("change", this._onRequestChanged); + request.on("change", this.onRequestChanged); } } - componentWillUnmount() { + public componentWillUnmount() { const request = this.props.mxEvent.verificationRequest; if (request) { - request.off("change", this._onRequestChanged); + request.off("change", this.onRequestChanged); } } - _openRequest = () => { - const {verificationRequest} = this.props.mxEvent; + private openRequest = () => { + const { verificationRequest } = this.props.mxEvent; const member = MatrixClientPeg.get().getUser(verificationRequest.otherUserId); dis.dispatch({ action: Action.SetRightPanelPhase, @@ -58,15 +57,15 @@ export default class MKeyVerificationRequest extends React.Component { }); }; - _onRequestChanged = () => { + private onRequestChanged = () => { this.forceUpdate(); }; - _onAcceptClicked = async () => { + private onAcceptClicked = async () => { const request = this.props.mxEvent.verificationRequest; if (request) { try { - this._openRequest(); + this.openRequest(); await request.accept(); } catch (err) { console.error(err.message); @@ -74,7 +73,7 @@ export default class MKeyVerificationRequest extends React.Component { } }; - _onRejectClicked = async () => { + private onRejectClicked = async () => { const request = this.props.mxEvent.verificationRequest; if (request) { try { @@ -85,7 +84,7 @@ export default class MKeyVerificationRequest extends React.Component { } }; - _acceptedLabel(userId) { + private acceptedLabel(userId: string) { const client = MatrixClientPeg.get(); const myUserId = client.getUserId(); if (userId === myUserId) { @@ -95,7 +94,7 @@ export default class MKeyVerificationRequest extends React.Component { } } - _cancelledLabel(userId) { + private cancelledLabel(userId: string) { const client = MatrixClientPeg.get(); const myUserId = client.getUserId(); const {cancellationCode} = this.props.mxEvent.verificationRequest; @@ -115,7 +114,7 @@ export default class MKeyVerificationRequest extends React.Component { } } - render() { + public render() { const AccessibleButton = sdk.getComponent("elements.AccessibleButton"); const FormButton = sdk.getComponent("elements.FormButton"); @@ -134,11 +133,11 @@ export default class MKeyVerificationRequest extends React.Component { let stateLabel; const accepted = request.ready || request.started || request.done; if (accepted) { - stateLabel = ( - {this._acceptedLabel(request.receivingUserId)} + stateLabel = ( + {this.acceptedLabel(request.receivingUserId)} ); } else if (request.cancelled) { - stateLabel = this._cancelledLabel(request.cancellingUserId); + stateLabel = this.cancelledLabel(request.cancellingUserId); } else if (request.accepting) { stateLabel = _t("Accepting …"); } else if (request.declining) { @@ -153,8 +152,8 @@ export default class MKeyVerificationRequest extends React.Component { subtitle = userLabelForEventRoom(request.requestingUserId, mxEvent.getRoomId()); if (request.canAccept) { stateNode = (
    - - + +
    ); } } else { // request sent by us @@ -174,8 +173,3 @@ export default class MKeyVerificationRequest extends React.Component { return null; } } - -MKeyVerificationRequest.propTypes = { - /* the MatrixEvent to show */ - mxEvent: PropTypes.object.isRequired, -}; From adb42b792782b5151773089bc5243c6563718a8c Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Mon, 21 Jun 2021 14:16:37 +0100 Subject: [PATCH 188/809] Deprecate FormButton to use AccessibleButton everywhere --- res/css/_components.scss | 1 - res/css/structures/_ToastContainer.scss | 3 +- res/css/views/elements/_FormButton.scss | 42 ------------------- res/css/views/right_panel/_UserInfo.scss | 10 ----- .../views/right_panel/_VerificationPanel.scss | 2 +- res/css/views/spaces/_SpaceBasicSettings.scss | 2 +- src/components/views/elements/FormButton.js | 28 ------------- .../messages/MKeyVerificationRequest.tsx | 9 ++-- .../views/right_panel/VerificationPanel.tsx | 20 +++------ src/components/views/toasts/GenericToast.tsx | 10 +++-- src/components/views/voip/IncomingCallBox.tsx | 16 +++---- 11 files changed, 31 insertions(+), 112 deletions(-) delete mode 100644 res/css/views/elements/_FormButton.scss delete mode 100644 src/components/views/elements/FormButton.js diff --git a/res/css/_components.scss b/res/css/_components.scss index 56403ea190..ec3af8655e 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -123,7 +123,6 @@ @import "./views/elements/_EventListSummary.scss"; @import "./views/elements/_FacePile.scss"; @import "./views/elements/_Field.scss"; -@import "./views/elements/_FormButton.scss"; @import "./views/elements/_ImageView.scss"; @import "./views/elements/_InfoTooltip.scss"; @import "./views/elements/_InlineSpinner.scss"; diff --git a/res/css/structures/_ToastContainer.scss b/res/css/structures/_ToastContainer.scss index 09f834a6e3..14e4c01389 100644 --- a/res/css/structures/_ToastContainer.scss +++ b/res/css/structures/_ToastContainer.scss @@ -134,8 +134,9 @@ limitations under the License. .mx_Toast_buttons { float: right; display: flex; + gap: 5px; - .mx_FormButton { + .mx_AccessibleButton { min-width: 96px; box-sizing: border-box; } diff --git a/res/css/views/elements/_FormButton.scss b/res/css/views/elements/_FormButton.scss deleted file mode 100644 index eda201ff03..0000000000 --- a/res/css/views/elements/_FormButton.scss +++ /dev/null @@ -1,42 +0,0 @@ -/* -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_FormButton { - line-height: $font-16px; - padding: 5px 15px; - font-size: $font-12px; - height: min-content; - - &:not(:last-child) { - margin-right: 8px; - } - - &.mx_AccessibleButton_kind_primary { - color: $accent-color; - background-color: $accent-bg-color; - } - - &.mx_AccessibleButton_kind_danger { - color: $notice-primary-color; - background-color: $notice-primary-bg-color; - } - - &.mx_AccessibleButton_kind_secondary { - color: $secondary-fg-color; - border: 1px solid $secondary-fg-color; - background-color: unset; - } -} diff --git a/res/css/views/right_panel/_UserInfo.scss b/res/css/views/right_panel/_UserInfo.scss index 87420ae4e7..6632ccddf9 100644 --- a/res/css/views/right_panel/_UserInfo.scss +++ b/res/css/views/right_panel/_UserInfo.scss @@ -259,16 +259,6 @@ limitations under the License. .mx_AccessibleButton.mx_AccessibleButton_hasKind { padding: 8px 18px; - - &.mx_AccessibleButton_kind_primary { - color: $accent-color; - background-color: $accent-bg-color; - } - - &.mx_AccessibleButton_kind_danger { - color: $notice-primary-color; - background-color: $notice-primary-bg-color; - } } .mx_VerificationShowSas .mx_AccessibleButton, diff --git a/res/css/views/right_panel/_VerificationPanel.scss b/res/css/views/right_panel/_VerificationPanel.scss index a8466a1626..12148b09de 100644 --- a/res/css/views/right_panel/_VerificationPanel.scss +++ b/res/css/views/right_panel/_VerificationPanel.scss @@ -58,7 +58,7 @@ limitations under the License. } .mx_VerificationPanel_reciprocate_section { - .mx_FormButton { + .mx_AccessibleButton { width: 100%; box-sizing: border-box; padding: 10px; diff --git a/res/css/views/spaces/_SpaceBasicSettings.scss b/res/css/views/spaces/_SpaceBasicSettings.scss index 204ccab2b7..32454b9530 100644 --- a/res/css/views/spaces/_SpaceBasicSettings.scss +++ b/res/css/views/spaces/_SpaceBasicSettings.scss @@ -73,7 +73,7 @@ limitations under the License. } } - .mx_FormButton { + .mx_AccessibleButton { padding: 8px 22px; margin-left: auto; display: block; diff --git a/src/components/views/elements/FormButton.js b/src/components/views/elements/FormButton.js deleted file mode 100644 index f6b4c986f5..0000000000 --- a/src/components/views/elements/FormButton.js +++ /dev/null @@ -1,28 +0,0 @@ -/* -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 AccessibleButton from "./AccessibleButton"; - -export default function FormButton(props) { - const {className, label, kind, ...restProps} = props; - const newClassName = (className || "") + " mx_FormButton"; - const allProps = Object.assign({}, restProps, - {className: newClassName, kind: kind || "primary", children: [label]}); - return React.createElement(AccessibleButton, allProps); -} - -FormButton.propTypes = AccessibleButton.propTypes; diff --git a/src/components/views/messages/MKeyVerificationRequest.tsx b/src/components/views/messages/MKeyVerificationRequest.tsx index df35c47706..69467cfa50 100644 --- a/src/components/views/messages/MKeyVerificationRequest.tsx +++ b/src/components/views/messages/MKeyVerificationRequest.tsx @@ -116,7 +116,6 @@ export default class MKeyVerificationRequest extends React.Component { public render() { const AccessibleButton = sdk.getComponent("elements.AccessibleButton"); - const FormButton = sdk.getComponent("elements.FormButton"); const {mxEvent} = this.props; const request = mxEvent.verificationRequest; @@ -152,8 +151,12 @@ export default class MKeyVerificationRequest extends React.Component { subtitle = userLabelForEventRoom(request.requestingUserId, mxEvent.getRoomId()); if (request.canAccept) { stateNode = (
    - - + + {_t("Decline")} + + + {_t("Accept")} +
    ); } } else { // request sent by us diff --git a/src/components/views/right_panel/VerificationPanel.tsx b/src/components/views/right_panel/VerificationPanel.tsx index edfe0e3483..ce39141391 100644 --- a/src/components/views/right_panel/VerificationPanel.tsx +++ b/src/components/views/right_panel/VerificationPanel.tsx @@ -195,14 +195,7 @@ export default class VerificationPanel extends React.PureComponent

    {description}

    - - + onClick={this.onReciprocateYesClick} />
    ; } else { diff --git a/src/components/views/toasts/GenericToast.tsx b/src/components/views/toasts/GenericToast.tsx index 209babbf9d..ae01e8bfb7 100644 --- a/src/components/views/toasts/GenericToast.tsx +++ b/src/components/views/toasts/GenericToast.tsx @@ -15,8 +15,8 @@ limitations under the License. */ import React, {ReactNode} from "react"; +import AccessibleButton from "../elements/AccessibleButton"; -import FormButton from "../elements/FormButton"; import {XOR} from "../../../@types/common"; export interface IProps { @@ -50,8 +50,12 @@ const GenericToast: React.FC> = ({ {detailContent}
    - {onReject && rejectLabel && } - + {onReject && rejectLabel && + {rejectLabel} + } + + {acceptLabel} +
    ; }; diff --git a/src/components/views/voip/IncomingCallBox.tsx b/src/components/views/voip/IncomingCallBox.tsx index a0660318bc..10b102832d 100644 --- a/src/components/views/voip/IncomingCallBox.tsx +++ b/src/components/views/voip/IncomingCallBox.tsx @@ -23,7 +23,7 @@ import { _t } from '../../../languageHandler'; import { ActionPayload } from '../../../dispatcher/payloads'; import CallHandler, { AudioID } from '../../../CallHandler'; import RoomAvatar from '../avatars/RoomAvatar'; -import FormButton from '../elements/FormButton'; +import AccesibleButton from '../elements/AccessibleButton'; import { CallState } from 'matrix-js-sdk/src/webrtc/call'; import {replaceableComponent} from "../../../utils/replaceableComponent"; import AccessibleTooltipButton from '../elements/AccessibleTooltipButton'; @@ -143,19 +143,21 @@ export default class IncomingCallBox extends React.Component { />
    - + > + {_t("Decline")} +
    - + > + {_t("Accept")} +
    ; } From 7f635c68c519e0df9ba20e34c8f278ea33b20720 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Mon, 21 Jun 2021 14:50:21 +0100 Subject: [PATCH 189/809] Migrate SearchBar to TypeScript --- src/components/structures/RoomView.tsx | 9 +-- .../rooms/{SearchBar.js => SearchBar.tsx} | 69 ++++++++++++++----- 2 files changed, 55 insertions(+), 23 deletions(-) rename src/components/views/rooms/{SearchBar.js => SearchBar.tsx} (55%) diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index 1e3adcb518..c1dcb81e08 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -60,7 +60,7 @@ import ScrollPanel from "./ScrollPanel"; import TimelinePanel from "./TimelinePanel"; import ErrorBoundary from "../views/elements/ErrorBoundary"; import RoomPreviewBar from "../views/rooms/RoomPreviewBar"; -import SearchBar from "../views/rooms/SearchBar"; +import SearchBar, { SearchScope } from "../views/rooms/SearchBar"; import RoomUpgradeWarningBar from "../views/rooms/RoomUpgradeWarningBar"; import AuxPanel from "../views/rooms/AuxPanel"; import RoomHeader from "../views/rooms/RoomHeader"; @@ -82,6 +82,7 @@ import SpaceRoomView from "./SpaceRoomView"; import { IOpts } from "../../createRoom"; import { replaceableComponent } from "../../utils/replaceableComponent"; import UIStore from "../../stores/UIStore"; +import Search from '../views/emojipicker/Search'; const DEBUG = false; let debuglog = function(msg: string) {}; @@ -139,7 +140,7 @@ export interface IState { draggingFile: boolean; searching: boolean; searchTerm?: string; - searchScope?: "All" | "Room"; + searchScope?: SearchScope; searchResults?: XOR<{}, { count: number; highlights: string[]; @@ -1267,7 +1268,7 @@ export default class RoomView extends React.Component { }); } - private onSearch = (term: string, scope) => { + private onSearch = (term: string, scope: SearchScope) => { this.setState({ searchTerm: term, searchScope: scope, @@ -1288,7 +1289,7 @@ export default class RoomView extends React.Component { this.searchId = new Date().getTime(); let roomId; - if (scope === "Room") roomId = this.state.room.roomId; + if (scope === SearchScope.Room) roomId = this.state.room.roomId; debuglog("sending search request"); const searchPromise = eventSearch(term, roomId); diff --git a/src/components/views/rooms/SearchBar.js b/src/components/views/rooms/SearchBar.tsx similarity index 55% rename from src/components/views/rooms/SearchBar.js rename to src/components/views/rooms/SearchBar.tsx index 029516c932..de99305d81 100644 --- a/src/components/views/rooms/SearchBar.js +++ b/src/components/views/rooms/SearchBar.tsx @@ -15,7 +15,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, {createRef} from 'react'; +import React, {createRef, RefObject} from 'react'; import AccessibleButton from "../elements/AccessibleButton"; import classNames from "classnames"; import { _t } from '../../../languageHandler'; @@ -23,27 +23,42 @@ import {Key} from "../../../Keyboard"; import DesktopBuildsNotice, {WarningKind} from "../elements/DesktopBuildsNotice"; import {replaceableComponent} from "../../../utils/replaceableComponent"; +interface IProps { + onCancelClick: () => void; + onSearch: (query: string, scope: string) => void; + searchInProgress?: boolean; + isRoomEncrypted?: boolean; +} + +interface IState { + scope: SearchScope; +} + +export enum SearchScope { + Room = "Room", + All = "All", +} + @replaceableComponent("views.rooms.SearchBar") -export default class SearchBar extends React.Component { - constructor(props) { +export default class SearchBar extends React.Component { + private searchTerm: RefObject = createRef(); + + constructor(props: IProps) { super(props); - - this._search_term = createRef(); - this.state = { - scope: 'Room', + scope: SearchScope.Room, }; } - onThisRoomClick = () => { - this.setState({ scope: 'Room' }, () => this._searchIfQuery()); + public onThisRoomClick = () => { + this.setState({ scope: SearchScope.Room }, () => this._searchIfQuery()); }; - onAllRoomsClick = () => { - this.setState({ scope: 'All' }, () => this._searchIfQuery()); + public onAllRoomsClick = () => { + this.setState({ scope: SearchScope.All }, () => this._searchIfQuery()); }; - onSearchChange = (e) => { + public onSearchChange = (e: React.KeyboardEvent) => { switch (e.key) { case Key.ENTER: this.onSearch(); @@ -55,13 +70,13 @@ export default class SearchBar extends React.Component { }; _searchIfQuery() { - if (this._search_term.current.value) { + if (this.searchTerm.current.value) { this.onSearch(); } } onSearch = () => { - this.props.onSearch(this._search_term.current.value, this.state.scope); + this.props.onSearch(this.searchTerm.current.value, this.state.scope); }; render() { @@ -69,25 +84,41 @@ export default class SearchBar extends React.Component { mx_SearchBar_searching: this.props.searchInProgress, }); const thisRoomClasses = classNames("mx_SearchBar_button", { - mx_SearchBar_unselected: this.state.scope !== 'Room', + mx_SearchBar_unselected: this.state.scope !== SearchScope.Room, }); const allRoomsClasses = classNames("mx_SearchBar_button", { - mx_SearchBar_unselected: this.state.scope !== 'All', + mx_SearchBar_unselected: this.state.scope !== SearchScope.All, }); return ( <>
    - + {_t("This Room")} - + {_t("All Rooms")}
    - +
    From 7825c30bf773185c967c61ef3cb2a448bdf8f791 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Mon, 21 Jun 2021 15:41:47 +0100 Subject: [PATCH 190/809] Improve event index initialisation failure message in search bar for supported platforms --- .../views/elements/DesktopBuildsNotice.tsx | 19 ++++++++++++++++++- src/i18n/strings/en_EN.json | 1 + 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/components/views/elements/DesktopBuildsNotice.tsx b/src/components/views/elements/DesktopBuildsNotice.tsx index fd1c7848aa..e5e94d4bd4 100644 --- a/src/components/views/elements/DesktopBuildsNotice.tsx +++ b/src/components/views/elements/DesktopBuildsNotice.tsx @@ -14,10 +14,15 @@ See the License for the specific language governing permissions and limitations under the License. */ +import React from "react"; import EventIndexPeg from "../../../indexing/EventIndexPeg"; import { _t } from "../../../languageHandler"; import SdkConfig from "../../../SdkConfig"; -import React from "react"; + +import dis from "../../../dispatcher/dispatcher"; +import { Action } from "../../../dispatcher/actions"; +import { UserTab } from "../dialogs/UserSettingsDialog"; + export enum WarningKind { Files, @@ -33,6 +38,18 @@ export default function DesktopBuildsNotice({isRoomEncrypted, kind}: IProps) { if (!isRoomEncrypted) return null; if (EventIndexPeg.get()) return null; + if (EventIndexPeg.error) { + return _t("Message search initialisation failed, check your settings for more information", {}, { + a: sub => ( { + evt.preventDefault(); + dis.dispatch({ + action: Action.ViewUserSettings, + initialTabId: UserTab.Security, + }); + }}>{sub}), + }); + } + const {desktopBuilds, brand} = SdkConfig.get(); let text = null; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index b88dc79da5..37f6416460 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1933,6 +1933,7 @@ "Error loading Widget": "Error loading Widget", "Error - Mixed content": "Error - Mixed content", "Popout widget": "Popout widget", + "Message search initialisation failed, check your settings for more information": "Message search initialisation failed, check your settings for more information", "Use the Desktop app to see all encrypted files": "Use the Desktop app to see all encrypted files", "Use the Desktop app to search encrypted messages": "Use the Desktop app to search encrypted messages", "This version of %(brand)s does not support viewing some encrypted files": "This version of %(brand)s does not support viewing some encrypted files", From 88d25ad6af058d098f147553e7289606cf57a38d Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Mon, 21 Jun 2021 15:44:09 +0100 Subject: [PATCH 191/809] Fix typo in default import name --- src/components/views/voip/IncomingCallBox.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/views/voip/IncomingCallBox.tsx b/src/components/views/voip/IncomingCallBox.tsx index 10b102832d..cd1a3afd10 100644 --- a/src/components/views/voip/IncomingCallBox.tsx +++ b/src/components/views/voip/IncomingCallBox.tsx @@ -23,7 +23,7 @@ import { _t } from '../../../languageHandler'; import { ActionPayload } from '../../../dispatcher/payloads'; import CallHandler, { AudioID } from '../../../CallHandler'; import RoomAvatar from '../avatars/RoomAvatar'; -import AccesibleButton from '../elements/AccessibleButton'; +import AccessibleButton from '../elements/AccessibleButton'; import { CallState } from 'matrix-js-sdk/src/webrtc/call'; import {replaceableComponent} from "../../../utils/replaceableComponent"; import AccessibleTooltipButton from '../elements/AccessibleTooltipButton'; @@ -143,21 +143,21 @@ export default class IncomingCallBox extends React.Component { />
    - {_t("Decline")} - +
    - {_t("Accept")} - +
    ; } From ca5f8f97bb80d4cdeaee584b7e9ba0c83207a2b2 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Mon, 21 Jun 2021 16:18:13 +0100 Subject: [PATCH 192/809] Branch matching support for forked repository on GitHub actions --- scripts/fetchdep.sh | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/scripts/fetchdep.sh b/scripts/fetchdep.sh index 9844fdc9db..02af402951 100755 --- a/scripts/fetchdep.sh +++ b/scripts/fetchdep.sh @@ -36,13 +36,19 @@ else head=$(curl $apiEndpoint | jq -r '.head.label') fi -# If head is set, it will contain either: +# If head is set, it will contain on BuilKite either: # * "branch" when the author's branch and target branch are in the same repo # * "fork:branch" when the author's branch is in their fork or if this is a Netlify build # We can split on `:` into an array to check. +# For GitHub Actions we need to inspect GITHUB_REPOSITORY and GITHUB_ACTOR +# to determine whether the branch is from a fork or not BRANCH_ARRAY=(${head//:/ }) if [[ "${#BRANCH_ARRAY[@]}" == "1" ]]; then - clone $deforg $defrepo $head + if [[ "$GITHUB_REPOSITORY" = "$deforg/$defrepo" ]]; then + clone $deforg $defrepo $head + else + clone $GITHUB_ACTOR $defrepo $head + fi elif [[ "${#BRANCH_ARRAY[@]}" == "2" ]]; then clone ${BRANCH_ARRAY[0]} $defrepo ${BRANCH_ARRAY[1]} fi From 174a43f1ef901168f69f535515eedb8e6c7f864c Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Mon, 21 Jun 2021 16:37:49 +0100 Subject: [PATCH 193/809] Upgrade matrix-js-sdk to 12.0.0 --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 644793e265..4a4a326a89 100644 --- a/package.json +++ b/package.json @@ -78,7 +78,7 @@ "katex": "^0.12.0", "linkifyjs": "^2.1.9", "lodash": "^4.17.20", - "matrix-js-sdk": "12.0.0-rc.1", + "matrix-js-sdk": "12.0.0", "matrix-widget-api": "^0.1.0-beta.14", "minimist": "^1.2.5", "opus-recorder": "^8.0.3", diff --git a/yarn.lock b/yarn.lock index 14cd11d769..82ee9a070e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5711,10 +5711,10 @@ mathml-tag-names@^2.1.3: resolved "https://registry.yarnpkg.com/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz#4ddadd67308e780cf16a47685878ee27b736a0a3" integrity sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg== -matrix-js-sdk@12.0.0-rc.1: - version "12.0.0-rc.1" - resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-12.0.0-rc.1.tgz#b94a72f0549f3000763efb8c7b6fa1f8808e56f6" - integrity sha512-bzozc4w9dF6Dl8xXXLXMpe3FqL/ncczKdB9Y8dL1mPaujVrmLWAai+BYmC9/c4SIw+1zUap9P5W16ej3z7prig== +matrix-js-sdk@12.0.0: + version "12.0.0" + resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-12.0.0.tgz#8ee7cc37661476341d0c792a1a12bc78b19f9fdd" + integrity sha512-DHeq87Sx9Dv37FYyvZkmA1VYsQUNaVgc3QzMUkFwoHt1T4EZzgyYpdsp3uYruJzUW0ACvVJcwFdrU4e1VS97dQ== dependencies: "@babel/runtime" "^7.12.5" another-json "^0.2.0" From ddbf0fa77fd798495ce3c5940e4c4f44a607d766 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Mon, 21 Jun 2021 16:46:20 +0100 Subject: [PATCH 194/809] Prepare changelog for v3.24.0 --- CHANGELOG.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a14a0f308e..0f979b4802 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,15 @@ +Changes in [3.24.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.24.0) (2021-06-21) +===================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.24.0-rc.1...v3.24.0) + + * Upgrade to JS SDK 12.0.0 + * [Release] Keep composer reply when scrolling away from a highlighted event + [\#6211](https://github.com/matrix-org/matrix-react-sdk/pull/6211) + * [Release] Remove stray bullet point in reply preview + [\#6210](https://github.com/matrix-org/matrix-react-sdk/pull/6210) + * [Release] Stop requesting null next replies from the server + [\#6209](https://github.com/matrix-org/matrix-react-sdk/pull/6209) + Changes in [3.24.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.24.0-rc.1) (2021-06-15) =============================================================================================================== [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.23.0...v3.24.0-rc.1) From d89710defe12a133cfbc9fee664996dcc17761e2 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Mon, 21 Jun 2021 16:53:20 +0100 Subject: [PATCH 195/809] v3.24.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4a4a326a89..d592d426a6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "3.24.0-rc.1", + "version": "3.24.0", "description": "SDK for matrix.org using React", "author": "matrix.org", "repository": { From 54c3832b5b9dde5981eb1934f599e7b75eac00e8 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Mon, 21 Jun 2021 16:54:41 +0100 Subject: [PATCH 196/809] Resetting package fields for development --- package.json | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index d592d426a6..f232d4301b 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "bin": { "reskindex": "scripts/reskindex.js" }, - "main": "./lib/index.js", + "main": "./src/index.js", "matrix_src_main": "./src/index.js", "matrix_lib_main": "./lib/index.js", "matrix_lib_typings": "./lib/index.d.ts", @@ -197,6 +197,5 @@ "coverageReporters": [ "text" ] - }, - "typings": "./lib/index.d.ts" + } } From 903f898beeb1e1ddbc3de960197b1b5cc16e36d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 21 Jun 2021 18:28:30 +0200 Subject: [PATCH 197/809] Remove ComposerInsertPayload as this is a JS file MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/context_menus/MessageContextMenu.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/views/context_menus/MessageContextMenu.js b/src/components/views/context_menus/MessageContextMenu.js index 2fb2ac4d0e..5a1da1376d 100644 --- a/src/components/views/context_menus/MessageContextMenu.js +++ b/src/components/views/context_menus/MessageContextMenu.js @@ -33,7 +33,6 @@ import { EventType } from "matrix-js-sdk/src/@types/event"; import { replaceableComponent } from "../../../utils/replaceableComponent"; import { ReadPinsEventId } from "../right_panel/PinnedMessagesCard"; import ForwardDialog from "../dialogs/ForwardDialog"; -import { ComposerInsertPayload } from "../../../dispatcher/payloads/ComposerInsertPayload"; import { Action } from "../../../dispatcher/actions"; export function canCancel(eventStatus) { @@ -201,7 +200,7 @@ export default class MessageContextMenu extends React.Component { }; onQuoteClick = () => { - dis.dispatch({ + dis.dispatch({ action: Action.ComposerInsert, event: this.props.mxEvent, }); From dda4c8ec4c9fc5e00c5d9cc5525433acbf8539d9 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 21 Jun 2021 21:04:29 +0100 Subject: [PATCH 198/809] Move Promise::allSettled typing from react-sdk to js-sdk --- src/@types/global.d.ts | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts index 0c6b63dd33..7eff341095 100644 --- a/src/@types/global.d.ts +++ b/src/@types/global.d.ts @@ -113,19 +113,6 @@ declare global { usageDetails?: {[key: string]: number}; } - export interface ISettledFulfilled { - status: "fulfilled"; - value: T; - } - export interface ISettledRejected { - status: "rejected"; - reason: any; - } - - interface PromiseConstructor { - allSettled(promises: Promise[]): Promise | ISettledRejected>>; - } - interface HTMLAudioElement { type?: string; // sinkId & setSinkId are experimental and typescript doesn't know about them From de4065719475d6f9d6214406ffbbbfd2a393f1c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 22 Jun 2021 09:12:41 +0200 Subject: [PATCH 199/809] Don't show room if we don't click on buttons MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/structures/RoomDirectory.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/structures/RoomDirectory.tsx b/src/components/structures/RoomDirectory.tsx index 1e0605f263..eb7208f98d 100644 --- a/src/components/structures/RoomDirectory.tsx +++ b/src/components/structures/RoomDirectory.tsx @@ -337,11 +337,10 @@ export default class RoomDirectory extends React.Component { } private onRoomClicked = (room: IRoom, ev: ButtonEvent) => { + // If room was shift-clicked, remove it from the room directory if (ev.shiftKey && !this.state.selectedCommunityId) { ev.preventDefault(); this.removeFromDirectory(room); - } else { - this.showRoom(room); } }; From a59deeb49165429acc91032280bf8205d2b013e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 22 Jun 2021 09:16:45 +0200 Subject: [PATCH 200/809] Add onClick handlers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/structures/RoomDirectory.tsx | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/components/structures/RoomDirectory.tsx b/src/components/structures/RoomDirectory.tsx index eb7208f98d..731e2387cc 100644 --- a/src/components/structures/RoomDirectory.tsx +++ b/src/components/structures/RoomDirectory.tsx @@ -589,12 +589,23 @@ export default class RoomDirectory extends React.Component { onMouseDown={(ev) => {ev.preventDefault();}} className="mx_RoomDirectory_roomDescription" > -
    { name }
      -
    { ev.stopPropagation(); } } +
    this.onRoomClicked(room, ev)} + > + { name } +
      +
    this.onRoomClicked(room, ev)} dangerouslySetInnerHTML={{ __html: topic }} /> -
    { getDisplayAliasForRoom(room) }
    +
    this.onRoomClicked(room, ev)} + > + { getDisplayAliasForRoom(room) } +
    ,
    this.onRoomClicked(room, ev)} From deb075777d4d59cefff26c98c7149651eb729540 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 22 Jun 2021 08:17:09 +0100 Subject: [PATCH 201/809] Upgrade @types/react and @types/react-dom --- package.json | 4 ++-- yarn.lock | 26 ++++++++++++++++++++------ 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index f232d4301b..8ebb90f342 100644 --- a/package.json +++ b/package.json @@ -132,8 +132,8 @@ "@types/pako": "^1.0.1", "@types/parse5": "^6.0.0", "@types/qrcode": "^1.3.5", - "@types/react": "^16.9", - "@types/react-dom": "^16.9.10", + "@types/react": "^17.0.2", + "@types/react-dom": "^17.0.2", "@types/react-transition-group": "^4.4.0", "@types/sanitize-html": "^2.3.1", "@types/zxcvbn": "^4.4.0", diff --git a/yarn.lock b/yarn.lock index 952d08d0f6..4f17b63337 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1620,12 +1620,12 @@ dependencies: "@types/node" "*" -"@types/react-dom@^16.9.10": - version "16.9.10" - resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-16.9.10.tgz#4485b0bec3d41f856181b717f45fd7831101156f" - integrity sha512-ItatOrnXDMAYpv6G8UCk2VhbYVTjZT9aorLtA/OzDN9XJ2GKcfam68jutoAcILdRjsRUO8qb7AmyObF77Q8QFw== +"@types/react-dom@^17.0.2": + version "17.0.8" + resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-17.0.8.tgz#3180de6d79bf53762001ad854e3ce49f36dd71fc" + integrity sha512-0ohAiJAx1DAUEcY9UopnfwCE9sSMDGnY/oXjWMax6g3RpzmTt2GMyMVAXcbn0mo8XAff0SbQJl2/SBU+hjSZ1A== dependencies: - "@types/react" "^16" + "@types/react" "*" "@types/react-transition-group@^4.4.0": version "4.4.0" @@ -1634,7 +1634,7 @@ dependencies: "@types/react" "*" -"@types/react@*", "@types/react@^16", "@types/react@^16.14", "@types/react@^16.9": +"@types/react@*", "@types/react@^16.14": version "16.14.2" resolved "https://registry.yarnpkg.com/@types/react/-/react-16.14.2.tgz#85dcc0947d0645349923c04ccef6018a1ab7538c" integrity sha512-BzzcAlyDxXl2nANlabtT4thtvbbnhee8hMmH/CcJrISDBVcJS1iOsP1f0OAgSdGE0MsY9tqcrb9YoZcOFv9dbQ== @@ -1642,6 +1642,15 @@ "@types/prop-types" "*" csstype "^3.0.2" +"@types/react@^17.0.2": + version "17.0.11" + resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.11.tgz#67fcd0ddbf5a0b083a0f94e926c7d63f3b836451" + integrity sha512-yFRQbD+whVonItSk7ZzP/L+gPTJVBkL/7shLEF+i9GC/1cV3JmUxEQz6+9ylhUpWSDuqo1N9qEvqS6vTj4USUA== + dependencies: + "@types/prop-types" "*" + "@types/scheduler" "*" + csstype "^3.0.2" + "@types/sanitize-html@^2.3.1": version "2.3.1" resolved "https://registry.yarnpkg.com/@types/sanitize-html/-/sanitize-html-2.3.1.tgz#094d696b83b7394b016e96342bbffa6a028795ce" @@ -1649,6 +1658,11 @@ dependencies: htmlparser2 "^6.0.0" +"@types/scheduler@*": + version "0.16.1" + resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.1.tgz#18845205e86ff0038517aab7a18a62a6b9f71275" + integrity sha512-EaCxbanVeyxDRTQBkdLb3Bvl/HK7PBK6UJjsSixB0iHKoWxE5uu2Q/DgtpOhPIojN0Zl1whvOd7PoHs2P0s5eA== + "@types/stack-utils@^1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e" From 066ef18db2eb56561bb5736b58c95b41426cb244 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 22 Jun 2021 09:24:42 +0200 Subject: [PATCH 202/809] Replace onClick by onMouseDown MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/structures/RoomDirectory.tsx | 45 ++++++++++----------- 1 file changed, 21 insertions(+), 24 deletions(-) diff --git a/src/components/structures/RoomDirectory.tsx b/src/components/structures/RoomDirectory.tsx index 731e2387cc..62944a9d98 100644 --- a/src/components/structures/RoomDirectory.tsx +++ b/src/components/structures/RoomDirectory.tsx @@ -567,11 +567,11 @@ export default class RoomDirectory extends React.Component { let avatarUrl = null; if (room.avatar_url) avatarUrl = mediaFromMxc(room.avatar_url).getSquareThumbnailHttp(32); + // We use onMouseDown instead of onClick, so that we can avoid text getting selected return [ -
    this.onRoomClicked(room, ev)} - // cancel onMouseDown otherwise shift-clicking highlights text - onMouseDown={(ev) => {ev.preventDefault();}} +
    this.onRoomClicked(room, ev)} className="mx_RoomDirectory_roomAvatar" > { url={avatarUrl} />
    , -
    this.onRoomClicked(room, ev)} - // cancel onMouseDown otherwise shift-clicking highlights text - onMouseDown={(ev) => {ev.preventDefault();}} +
    this.onRoomClicked(room, ev)} className="mx_RoomDirectory_roomDescription" >
    this.onRoomClicked(room, ev)} + onMouseDown={(ev) => this.onRoomClicked(room, ev)} > { name }
     
    this.onRoomClicked(room, ev)} + onMouseDown={(ev) => this.onRoomClicked(room, ev)} dangerouslySetInnerHTML={{ __html: topic }} />
    this.onRoomClicked(room, ev)} + onMouseDown={(ev) => this.onRoomClicked(room, ev)} > { getDisplayAliasForRoom(room) }
    , -
    this.onRoomClicked(room, ev)} - // cancel onMouseDown otherwise shift-clicking highlights text - onMouseDown={(ev) => {ev.preventDefault();}} +
    this.onRoomClicked(room, ev)} className="mx_RoomDirectory_roomMemberCount" > { room.num_joined_members }
    , -
    this.onRoomClicked(room, ev)} +
    this.onRoomClicked(room, ev)} // cancel onMouseDown otherwise shift-clicking highlights text - onMouseDown={(ev) => {ev.preventDefault();}} className="mx_RoomDirectory_preview" > - {previewButton} + { previewButton }
    , -
    this.onRoomClicked(room, ev)} - // cancel onMouseDown otherwise shift-clicking highlights text - onMouseDown={(ev) => {ev.preventDefault();}} +
    this.onRoomClicked(room, ev)} className="mx_RoomDirectory_join" > - {joinOrViewButton} + { joinOrViewButton }
    , ]; } From 8090d2b583f33f4baf5bf8b8dbb2ff1c6cdfa1de Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Tue, 22 Jun 2021 09:31:15 +0100 Subject: [PATCH 203/809] Fix branch matching for BuildKite --- scripts/fetchdep.sh | 31 +++++++++++++++++++------------ 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/scripts/fetchdep.sh b/scripts/fetchdep.sh index 02af402951..246add7e31 100755 --- a/scripts/fetchdep.sh +++ b/scripts/fetchdep.sh @@ -25,18 +25,20 @@ clone() { # First we check if GITHUB_HEAD_REF is defined, # Then we check if BUILDKITE_BRANCH is defined, # if it isn't we can assume this is a Netlify build -if [ -n ${GITHUB_HEAD_REF+x} ]; then - head=$GITHUB_HEAD_REF -elif [ -n ${BUILDKITE_BRANCH+x} ]; then - head=$BUILDKITE_BRANCH +if [ -z ${BUILDKITE_BRANCH+x} ]; then + if [ -z ${GITHUB_HEAD_REF+x} ]; then + # Netlify doesn't give us info about the fork so we have to get it from GitHub API + apiEndpoint="https://api.github.com/repos/matrix-org/matrix-react-sdk/pulls/" + apiEndpoint+=$REVIEW_ID + head=$(curl $apiEndpoint | jq -r '.head.label') + else + head=$GITHUB_HEAD_REF + fi else - # Netlify doesn't give us info about the fork so we have to get it from GitHub API - apiEndpoint="https://api.github.com/repos/matrix-org/matrix-react-sdk/pulls/" - apiEndpoint+=$REVIEW_ID - head=$(curl $apiEndpoint | jq -r '.head.label') + head=$BUILDKITE_BRANCH fi -# If head is set, it will contain on BuilKite either: +# If head is set, it will contain on BuildKite either: # * "branch" when the author's branch and target branch are in the same repo # * "fork:branch" when the author's branch is in their fork or if this is a Netlify build # We can split on `:` into an array to check. @@ -44,11 +46,16 @@ fi # to determine whether the branch is from a fork or not BRANCH_ARRAY=(${head//:/ }) if [[ "${#BRANCH_ARRAY[@]}" == "1" ]]; then - if [[ "$GITHUB_REPOSITORY" = "$deforg/$defrepo" ]]; then - clone $deforg $defrepo $head + if [ -z ${BUILDKITE_BRANCH+x} ]; then + if [[ "$GITHUB_REPOSITORY" == "$deforg"* ]]; then + clone $deforg $defrepo $GITHUB_HEAD_REF + else + clone $GITHUB_ACTOR $defrepo $GITHUB_HEAD_REF + fi else - clone $GITHUB_ACTOR $defrepo $head + clone $deforg $defrepo $BUILDKITE_BRANCH fi + elif [[ "${#BRANCH_ARRAY[@]}" == "2" ]]; then clone ${BRANCH_ARRAY[0]} $defrepo ${BRANCH_ARRAY[1]} fi From 3c725692704a3cecfe721c011ec4b076e816f491 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Tue, 22 Jun 2021 09:53:58 +0100 Subject: [PATCH 204/809] Fix modal opening race condition Co-authored-by: Michael Telatynski <7t3chguy@gmail.com> React 17 is hitting a race condition when a modal is closing and is trying to open another one within the same tick. A proper long term fix would be using React.createPortal to avoid manually mounting and unmounting new React roots --- src/Modal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Modal.tsx b/src/Modal.tsx index ce11c571b6..2f2d5a2d52 100644 --- a/src/Modal.tsx +++ b/src/Modal.tsx @@ -385,7 +385,7 @@ export class ModalManager {
    ); - ReactDOM.render(dialog, ModalManager.getOrCreateContainer()); + setImmediate(() => ReactDOM.render(dialog, ModalManager.getOrCreateContainer())); } else { // This is safe to call repeatedly if we happen to do that ReactDOM.unmountComponentAtNode(ModalManager.getOrCreateContainer()); From e9d87478e2c6a4b48658de90fb70862f74ca52bb Mon Sep 17 00:00:00 2001 From: Germain Date: Tue, 22 Jun 2021 10:06:31 +0100 Subject: [PATCH 205/809] Spaces before/after curlies Co-authored-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/toasts/GenericToast.tsx | 4 ++-- src/components/views/voip/IncomingCallBox.tsx | 5 ++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/components/views/toasts/GenericToast.tsx b/src/components/views/toasts/GenericToast.tsx index ae01e8bfb7..45b65ae1fb 100644 --- a/src/components/views/toasts/GenericToast.tsx +++ b/src/components/views/toasts/GenericToast.tsx @@ -51,10 +51,10 @@ const GenericToast: React.FC> = ({
    {onReject && rejectLabel && - {rejectLabel} + { rejectLabel } } - {acceptLabel} + { acceptLabel }
    ; diff --git a/src/components/views/voip/IncomingCallBox.tsx b/src/components/views/voip/IncomingCallBox.tsx index cd1a3afd10..c09043da24 100644 --- a/src/components/views/voip/IncomingCallBox.tsx +++ b/src/components/views/voip/IncomingCallBox.tsx @@ -148,7 +148,7 @@ export default class IncomingCallBox extends React.Component { onClick={this.onRejectClick} kind="danger" > - {_t("Decline")} + { _t("Decline") }
    { onClick={this.onAnswerClick} kind="primary" > - {_t("Accept")} + { _t("Accept") }
    ; } } - From db9ffe9b3ebb4c07b645808c3048fe76df9ab8c3 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Tue, 22 Jun 2021 10:18:09 +0100 Subject: [PATCH 206/809] Fix AccessibleButton label for VerificationRequest --- .../views/right_panel/VerificationPanel.tsx | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/components/views/right_panel/VerificationPanel.tsx b/src/components/views/right_panel/VerificationPanel.tsx index ce39141391..d3f2ba8cbf 100644 --- a/src/components/views/right_panel/VerificationPanel.tsx +++ b/src/components/views/right_panel/VerificationPanel.tsx @@ -209,13 +209,19 @@ export default class VerificationPanel extends React.PureComponent
    + onClick={this.onReciprocateNoClick} + > + { _t("No") } + + onClick={this.onReciprocateYesClick} + > + { _t("Yes") } +
    ; } else { From 3d3c4284555ab95a9bc798844c16db517b0f111b Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Tue, 22 Jun 2021 10:26:49 +0100 Subject: [PATCH 207/809] Fix DesktopBuildsNotice return type --- src/components/structures/RoomView.tsx | 1 - .../views/elements/DesktopBuildsNotice.tsx | 23 +++++++++++-------- src/components/views/rooms/SearchBar.tsx | 20 ++++++++-------- 3 files changed, 23 insertions(+), 21 deletions(-) diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index c1dcb81e08..a4338e832a 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -82,7 +82,6 @@ import SpaceRoomView from "./SpaceRoomView"; import { IOpts } from "../../createRoom"; import { replaceableComponent } from "../../utils/replaceableComponent"; import UIStore from "../../stores/UIStore"; -import Search from '../views/emojipicker/Search'; const DEBUG = false; let debuglog = function(msg: string) {}; diff --git a/src/components/views/elements/DesktopBuildsNotice.tsx b/src/components/views/elements/DesktopBuildsNotice.tsx index e5e94d4bd4..426554f31e 100644 --- a/src/components/views/elements/DesktopBuildsNotice.tsx +++ b/src/components/views/elements/DesktopBuildsNotice.tsx @@ -18,7 +18,6 @@ import React from "react"; import EventIndexPeg from "../../../indexing/EventIndexPeg"; import { _t } from "../../../languageHandler"; import SdkConfig from "../../../SdkConfig"; - import dis from "../../../dispatcher/dispatcher"; import { Action } from "../../../dispatcher/actions"; import { UserTab } from "../dialogs/UserSettingsDialog"; @@ -39,15 +38,19 @@ export default function DesktopBuildsNotice({isRoomEncrypted, kind}: IProps) { if (EventIndexPeg.get()) return null; if (EventIndexPeg.error) { - return _t("Message search initialisation failed, check your settings for more information", {}, { - a: sub => ( { - evt.preventDefault(); - dis.dispatch({ - action: Action.ViewUserSettings, - initialTabId: UserTab.Security, - }); - }}>{sub}), - }); + return <> + {_t("Message search initialisation failed, check your settings for more information", {}, { + a: sub => ( { + evt.preventDefault(); + dis.dispatch({ + action: Action.ViewUserSettings, + initialTabId: UserTab.Security, + }); + }}> + {sub} + ), + })} + ; } const {desktopBuilds, brand} = SdkConfig.get(); diff --git a/src/components/views/rooms/SearchBar.tsx b/src/components/views/rooms/SearchBar.tsx index de99305d81..47994f5251 100644 --- a/src/components/views/rooms/SearchBar.tsx +++ b/src/components/views/rooms/SearchBar.tsx @@ -15,13 +15,13 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, {createRef, RefObject} from 'react'; +import React, { createRef, RefObject } from 'react'; import AccessibleButton from "../elements/AccessibleButton"; import classNames from "classnames"; import { _t } from '../../../languageHandler'; import {Key} from "../../../Keyboard"; import DesktopBuildsNotice, {WarningKind} from "../elements/DesktopBuildsNotice"; -import {replaceableComponent} from "../../../utils/replaceableComponent"; +import { replaceableComponent } from "../../../utils/replaceableComponent"; interface IProps { onCancelClick: () => void; @@ -50,15 +50,15 @@ export default class SearchBar extends React.Component { }; } - public onThisRoomClick = () => { - this.setState({ scope: SearchScope.Room }, () => this._searchIfQuery()); + private onThisRoomClick = () => { + this.setState({ scope: SearchScope.Room }, () => this.searchIfQuery()); }; - public onAllRoomsClick = () => { - this.setState({ scope: SearchScope.All }, () => this._searchIfQuery()); + private onAllRoomsClick = () => { + this.setState({ scope: SearchScope.All }, () => this.searchIfQuery()); }; - public onSearchChange = (e: React.KeyboardEvent) => { + private onSearchChange = (e: React.KeyboardEvent) => { switch (e.key) { case Key.ENTER: this.onSearch(); @@ -69,17 +69,17 @@ export default class SearchBar extends React.Component { } }; - _searchIfQuery() { + private searchIfQuery(): void { if (this.searchTerm.current.value) { this.onSearch(); } } - onSearch = () => { + private onSearch = (): void => { this.props.onSearch(this.searchTerm.current.value, this.state.scope); }; - render() { + public render() { const searchButtonClasses = classNames("mx_SearchBar_searchButton", { mx_SearchBar_searching: this.props.searchInProgress, }); From dd58c9f413abd9a4c654e19c675f54864f24143b Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Tue, 22 Jun 2021 10:52:33 +0100 Subject: [PATCH 208/809] Add TruncatedList in AddExistingToSpaceDialog --- .../dialogs/AddExistingToSpaceDialog.tsx | 39 ++++++++++++++----- 1 file changed, 29 insertions(+), 10 deletions(-) diff --git a/src/components/views/dialogs/AddExistingToSpaceDialog.tsx b/src/components/views/dialogs/AddExistingToSpaceDialog.tsx index 822ffc2827..8997e4a5f8 100644 --- a/src/components/views/dialogs/AddExistingToSpaceDialog.tsx +++ b/src/components/views/dialogs/AddExistingToSpaceDialog.tsx @@ -39,6 +39,9 @@ import ProgressBar from "../elements/ProgressBar"; import {SpaceFeedbackPrompt} from "../../structures/SpaceRoomView"; import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar"; import QueryMatcher from "../../../autocomplete/QueryMatcher"; +import TruncatedList from "../elements/TruncatedList"; +import EntityTile from "../rooms/EntityTile"; +import BaseAvatar from "../avatars/BaseAvatar"; interface IProps extends IDialogProps { matrixClient: MatrixClient; @@ -204,6 +207,17 @@ export const AddExistingToSpace: React.FC = ({ setSelectedToAdd(new Set(selectedToAdd)); } : null; + const [truncateAt, setTruncateAt] = useState(20); + function overflowTile(overflowCount, totalCount) { + const text = _t("and %(count)s others...", { count: overflowCount }); + return ( + + } name={text} presenceState="online" suppressOnHover={true} + onClick={() => setTruncateAt(totalCount)} /> + ); + } + return
    = ({ { rooms.length > 0 ? (

    { _t("Rooms") }

    - { rooms.map(room => { - return { - onChange(checked, room); - } : null} - />; - }) } + rooms.slice(start, end).map(room => + { + onChange(checked, room); + } : null} + />, + )} + getChildCount={() => rooms.length} + />
    ) : undefined } From 66b3feb802b913b9500ee38c8807f2535eb4ec99 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 22 Jun 2021 11:50:00 +0100 Subject: [PATCH 209/809] Fix keyboard accessibility of the space panel --- .../views/elements/AccessibleButton.tsx | 6 + .../views/spaces/SpaceTreeLevel.tsx | 145 +++++++++++------- 2 files changed, 98 insertions(+), 53 deletions(-) diff --git a/src/components/views/elements/AccessibleButton.tsx b/src/components/views/elements/AccessibleButton.tsx index e634057a21..05bcca24b2 100644 --- a/src/components/views/elements/AccessibleButton.tsx +++ b/src/components/views/elements/AccessibleButton.tsx @@ -62,6 +62,8 @@ export default function AccessibleButton({ disabled, inputRef, className, + onKeyDown, + onKeyUp, ...restProps }: IProps) { const newProps: IAccessibleButtonProps = restProps; @@ -83,6 +85,8 @@ export default function AccessibleButton({ if (e.key === Key.SPACE) { e.stopPropagation(); e.preventDefault(); + } else { + onKeyDown?.(e); } }; newProps.onKeyUp = (e) => { @@ -94,6 +98,8 @@ export default function AccessibleButton({ if (e.key === Key.ENTER) { e.stopPropagation(); e.preventDefault(); + } else { + onKeyUp?.(e); } }; } diff --git a/src/components/views/spaces/SpaceTreeLevel.tsx b/src/components/views/spaces/SpaceTreeLevel.tsx index f34baf256b..b3577e436a 100644 --- a/src/components/views/spaces/SpaceTreeLevel.tsx +++ b/src/components/views/spaces/SpaceTreeLevel.tsx @@ -14,23 +14,22 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from "react"; +import React, { createRef } from "react"; import classNames from "classnames"; -import {Room} from "matrix-js-sdk/src/models/room"; +import { Room } from "matrix-js-sdk/src/models/room"; import RoomAvatar from "../avatars/RoomAvatar"; import SpaceStore from "../../../stores/SpaceStore"; import SpaceTreeLevelLayoutStore from "../../../stores/SpaceTreeLevelLayoutStore"; import NotificationBadge from "../rooms/NotificationBadge"; -import {RovingAccessibleButton} from "../../../accessibility/roving/RovingAccessibleButton"; -import {RovingAccessibleTooltipButton} from "../../../accessibility/roving/RovingAccessibleTooltipButton"; +import { RovingAccessibleTooltipButton } from "../../../accessibility/roving/RovingAccessibleTooltipButton"; import IconizedContextMenu, { IconizedContextMenuOption, IconizedContextMenuOptionList, } from "../context_menus/IconizedContextMenu"; -import {_t} from "../../../languageHandler"; -import {ContextMenuTooltipButton} from "../../../accessibility/context_menu/ContextMenuTooltipButton"; -import {toRightOf} from "../../structures/ContextMenu"; +import { _t } from "../../../languageHandler"; +import { ContextMenuTooltipButton } from "../../../accessibility/context_menu/ContextMenuTooltipButton"; +import { toRightOf } from "../../structures/ContextMenu"; import { shouldShowSpaceSettings, showAddExistingRooms, @@ -39,15 +38,16 @@ import { showSpaceSettings, } from "../../../utils/space"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; -import AccessibleButton, {ButtonEvent} from "../elements/AccessibleButton"; +import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton"; import defaultDispatcher from "../../../dispatcher/dispatcher"; -import {Action} from "../../../dispatcher/actions"; +import { Action } from "../../../dispatcher/actions"; import RoomViewStore from "../../../stores/RoomViewStore"; -import {SetRightPanelPhasePayload} from "../../../dispatcher/payloads/SetRightPanelPhasePayload"; -import {RightPanelPhases} from "../../../stores/RightPanelStorePhases"; -import {EventType} from "matrix-js-sdk/src/@types/event"; -import {StaticNotificationState} from "../../../stores/notifications/StaticNotificationState"; -import {NotificationColor} from "../../../stores/notifications/NotificationColor"; +import { SetRightPanelPhasePayload } from "../../../dispatcher/payloads/SetRightPanelPhasePayload"; +import { RightPanelPhases } from "../../../stores/RightPanelStorePhases"; +import { EventType } from "matrix-js-sdk/src/@types/event"; +import { StaticNotificationState } from "../../../stores/notifications/StaticNotificationState"; +import { NotificationColor } from "../../../stores/notifications/NotificationColor"; +import { getKeyBindingsManager, RoomListAction } from "../../../KeyBindingsManager"; interface IItemProps { space?: Room; @@ -61,11 +61,14 @@ interface IItemProps { interface IItemState { collapsed: boolean; contextMenuPosition: Pick; + childSpaces: Room[]; } export class SpaceItem extends React.PureComponent { static contextType = MatrixClientContext; + private buttonRef = createRef(); + constructor(props) { super(props); @@ -78,14 +81,36 @@ export class SpaceItem extends React.PureComponent { this.state = { collapsed: collapsed, contextMenuPosition: null, + childSpaces: this.childSpaces, }; + + SpaceStore.instance.on(this.props.space.roomId, this.onSpaceUpdate); } - private toggleCollapse(evt) { - if (this.props.onExpand && this.state.collapsed) { + componentWillUnmount() { + SpaceStore.instance.off(this.props.space.roomId, this.onSpaceUpdate); + } + + private onSpaceUpdate = () => { + this.setState({ + childSpaces: this.childSpaces, + }); + }; + + private get childSpaces() { + return SpaceStore.instance.getChildSpaces(this.props.space.roomId) + .filter(s => !this.props.parents?.has(s.roomId)); + } + + private get isCollapsed() { + return this.state.collapsed || this.props.isPanelCollapsed; + } + + private toggleCollapse = evt => { + if (this.props.onExpand && this.isCollapsed) { this.props.onExpand(); } - const newCollapsedState = !this.state.collapsed; + const newCollapsedState = !this.isCollapsed; SpaceTreeLevelLayoutStore.instance.setSpaceCollapsedState( this.props.space.roomId, @@ -96,7 +121,7 @@ export class SpaceItem extends React.PureComponent { // don't bubble up so encapsulating button for space // doesn't get triggered evt.stopPropagation(); - } + }; private onContextMenu = (ev: React.MouseEvent) => { if (this.props.space.getMyMembership() !== "join") return; @@ -111,6 +136,43 @@ export class SpaceItem extends React.PureComponent { }); } + private onKeyDown = (ev: React.KeyboardEvent) => { + let handled = true; + const action = getKeyBindingsManager().getRoomListAction(ev); + const hasChildren = this.state.childSpaces?.length; + switch (action) { + case RoomListAction.CollapseSection: + if (hasChildren && !this.isCollapsed) { + this.toggleCollapse(ev); + } else { + const parentItem = this.buttonRef?.current?.parentElement?.parentElement; + const parentButton = parentItem?.previousElementSibling as HTMLElement; + parentButton?.focus(); + } + break; + + case RoomListAction.ExpandSection: + if (hasChildren) { + if (this.isCollapsed) { + this.toggleCollapse(ev); + } else { + const childLevel = this.buttonRef?.current?.nextElementSibling; + const firstSpaceItemChild = childLevel?.querySelector(".mx_SpaceItem"); + firstSpaceItemChild?.querySelector(".mx_SpaceButton")?.focus(); + } + } + break; + + default: + handled = false; + } + + if (handled) { + ev.stopPropagation(); + ev.preventDefault(); + } + }; + private onClick = (ev: React.MouseEvent) => { ev.preventDefault(); ev.stopPropagation(); @@ -302,18 +364,15 @@ export class SpaceItem extends React.PureComponent { render() { const {space, activeSpaces, isNested} = this.props; - const forceCollapsed = this.props.isPanelCollapsed; const isNarrow = this.props.isPanelCollapsed; - const collapsed = this.state.collapsed || forceCollapsed; + const collapsed = this.isCollapsed; - const childSpaces = SpaceStore.instance.getChildSpaces(space.roomId) - .filter(s => !this.props.parents?.has(s.roomId)); const isActive = activeSpaces.includes(space); const itemClasses = classNames({ "mx_SpaceItem": true, "mx_SpaceItem_narrow": isNarrow, "collapsed": collapsed, - "hasSubSpaces": childSpaces && childSpaces.length, + "hasSubSpaces": this.state.childSpaces?.length, }); const isInvite = space.getMyMembership() === "invite"; @@ -328,9 +387,9 @@ export class SpaceItem extends React.PureComponent { : SpaceStore.instance.getNotificationState(space.roomId); let childItems; - if (childSpaces && !collapsed) { + if (this.state.childSpaces?.length && !collapsed) { childItems = { const avatarSize = isNested ? 24 : 32; - const toggleCollapseButton = childSpaces && childSpaces.length ? + const toggleCollapseButton = this.state.childSpaces?.length ? this.toggleCollapse(evt)} + onClick={this.toggleCollapse} /> : null; - let button; - if (isNarrow) { - button = ( + return ( +
  • { toggleCollapseButton }
    + { !isNarrow && { space.name } } { notifBadge } { this.renderContextMenu() }
    - ); - } else { - button = ( - - { toggleCollapseButton } -
    - - { space.name } - { notifBadge } - { this.renderContextMenu() } -
    -
    - ); - } - return ( -
  • - { button } { childItems }
  • ); From 1f0fdb95cd2f52731cc1ca2253a249a00fe8a83c Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 22 Jun 2021 11:59:04 +0100 Subject: [PATCH 210/809] Improve accessibility of subspaces in the space panel --- src/components/views/spaces/SpaceTreeLevel.tsx | 3 +++ src/i18n/strings/en_EN.json | 2 ++ 2 files changed, 5 insertions(+) diff --git a/src/components/views/spaces/SpaceTreeLevel.tsx b/src/components/views/spaces/SpaceTreeLevel.tsx index b3577e436a..cbc1cab86b 100644 --- a/src/components/views/spaces/SpaceTreeLevel.tsx +++ b/src/components/views/spaces/SpaceTreeLevel.tsx @@ -409,6 +409,8 @@ export class SpaceItem extends React.PureComponent { : null; return ( @@ -420,6 +422,7 @@ export class SpaceItem extends React.PureComponent { onContextMenu={this.onContextMenu} forceHide={!isNarrow || !!this.state.contextMenuPosition} role="treeitem" + aria-expanded={!collapsed} inputRef={this.buttonRef} onKeyDown={this.onKeyDown} > diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index b88dc79da5..a2fb93dc17 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1038,6 +1038,8 @@ "Manage & explore rooms": "Manage & explore rooms", "Explore rooms": "Explore rooms", "Space options": "Space options", + "Expand": "Expand", + "Collapse": "Collapse", "Remove": "Remove", "This bridge was provisioned by .": "This bridge was provisioned by .", "This bridge is managed by .": "This bridge is managed by .", From 27d255f30e1fd023bc6a2f18bf368e007d83989a Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Tue, 22 Jun 2021 12:27:54 +0100 Subject: [PATCH 211/809] Reduce audio waveform layout trashing --- .../views/voice_messages/_PlaybackContainer.scss | 4 ++++ src/components/views/voice_messages/Waveform.tsx | 15 ++++++++++----- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/res/css/views/voice_messages/_PlaybackContainer.scss b/res/css/views/voice_messages/_PlaybackContainer.scss index 20def16d6a..944378fb19 100644 --- a/res/css/views/voice_messages/_PlaybackContainer.scss +++ b/res/css/views/voice_messages/_PlaybackContainer.scss @@ -33,9 +33,13 @@ limitations under the License. font-size: $font-14px; line-height: $font-24px; + contain: content; + .mx_Waveform { .mx_Waveform_bar { background-color: $voice-record-waveform-incomplete-fg-color; + height: 100%; + transform: scaleY(max(0.05, var(--barHeight))); &.mx_Waveform_bar_100pct { // Small animation to remove the mechanical feel of progress diff --git a/src/components/views/voice_messages/Waveform.tsx b/src/components/views/voice_messages/Waveform.tsx index 840a5a12b3..ae880933e6 100644 --- a/src/components/views/voice_messages/Waveform.tsx +++ b/src/components/views/voice_messages/Waveform.tsx @@ -34,16 +34,19 @@ interface IState { * For CSS purposes, a mx_Waveform_bar_100pct class is added when the bar should be * "filled", as a demonstration of the progress property. */ + +import { CSSProperties } from "react"; + +export interface WaveformCSSProperties extends CSSProperties { + '--barHeight': number; +} + @replaceableComponent("views.voice_messages.Waveform") export default class Waveform extends React.PureComponent { public static defaultProps = { progress: 1, }; - public constructor(props) { - super(props); - } - public render() { return
    {this.props.relHeights.map((h, i) => { @@ -53,7 +56,9 @@ export default class Waveform extends React.PureComponent { 'mx_Waveform_bar': true, 'mx_Waveform_bar_100pct': isCompleteBar, }); - return ; + return ; })}
    ; } From a85c6c67e059a92b4c57dda42cd88217cb3b3c3f Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Tue, 22 Jun 2021 12:28:23 +0100 Subject: [PATCH 212/809] Make waveform update match the screen refresh rate --- .../views/rooms/VoiceRecordComposerTile.tsx | 69 ++++++++++++++++--- .../voice_messages/LiveRecordingClock.tsx | 49 ------------- .../voice_messages/LiveRecordingWaveform.tsx | 60 ---------------- 3 files changed, 59 insertions(+), 119 deletions(-) delete mode 100644 src/components/views/voice_messages/LiveRecordingClock.tsx delete mode 100644 src/components/views/voice_messages/LiveRecordingWaveform.tsx diff --git a/src/components/views/rooms/VoiceRecordComposerTile.tsx b/src/components/views/rooms/VoiceRecordComposerTile.tsx index 20d8c9c5d4..6fe6a5ab1c 100644 --- a/src/components/views/rooms/VoiceRecordComposerTile.tsx +++ b/src/components/views/rooms/VoiceRecordComposerTile.tsx @@ -15,19 +15,26 @@ limitations under the License. */ import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; -import {_t} from "../../../languageHandler"; +import { _t } from "../../../languageHandler"; import React, {ReactNode} from "react"; -import {RecordingState, VoiceRecording} from "../../../voice/VoiceRecording"; +import { + IRecordingUpdate, + RECORDING_PLAYBACK_SAMPLES, + RecordingState, + VoiceRecording, +} from "../../../voice/VoiceRecording"; import {Room} from "matrix-js-sdk/src/models/room"; -import {MatrixClientPeg} from "../../../MatrixClientPeg"; +import { MatrixClientPeg } from "../../../MatrixClientPeg"; import classNames from "classnames"; -import LiveRecordingWaveform from "../voice_messages/LiveRecordingWaveform"; -import {replaceableComponent} from "../../../utils/replaceableComponent"; -import LiveRecordingClock from "../voice_messages/LiveRecordingClock"; -import {VoiceRecordingStore} from "../../../stores/VoiceRecordingStore"; +import Waveform from "../voice_messages/Waveform"; +import { replaceableComponent } from "../../../utils/replaceableComponent"; +import { arrayFastResample, arraySeed } from "../../../utils/arrays"; +import { percentageOf } from "../../../utils/numbers"; +import Clock from "../voice_messages/Clock"; +import { VoiceRecordingStore } from "../../../stores/VoiceRecordingStore"; import {UPDATE_EVENT} from "../../../stores/AsyncStore"; import RecordingPlayback from "../voice_messages/RecordingPlayback"; -import {MsgType} from "matrix-js-sdk/src/@types/event"; +import { MsgType } from "matrix-js-sdk/src/@types/event"; import Modal from "../../../Modal"; import ErrorDialog from "../dialogs/ErrorDialog"; import CallMediaHandler from "../../../CallMediaHandler"; @@ -39,6 +46,8 @@ interface IProps { interface IState { recorder?: VoiceRecording; recordingPhase?: RecordingState; + relHeights: number[]; + seconds: number; } /** @@ -46,18 +55,58 @@ interface IState { */ @replaceableComponent("views.rooms.VoiceRecordComposerTile") export default class VoiceRecordComposerTile extends React.PureComponent { + private waveform: number[] = []; + private seconds = 0; + private scheduledAnimationFrame = false; + public constructor(props) { super(props); this.state = { recorder: null, // no recording started by default + seconds: 0, + relHeights: arraySeed(0, RECORDING_PLAYBACK_SAMPLES), }; } + public componentDidUpdate(prevProps, prevState) { + if (!prevState.recorder && this.state.recorder) { + this.state.recorder.liveData.onUpdate(this.onRecordingUpdate); + } + } + public async componentWillUnmount() { await VoiceRecordingStore.instance.disposeRecording(); } + private onRecordingUpdate = (update: IRecordingUpdate): void => { + this.waveform = update.waveform; + this.seconds = update.timeSeconds; + + if (this.scheduledAnimationFrame) { + return; + } + + this.scheduledAnimationFrame = true; + // The audio recorder flushes data faster than the screen refresh rate + // Using requestAnimationFrame makes sure that we only flush the data + // to react once per tick to avoid unneeded work. + requestAnimationFrame(() => { + // The waveform and the downsample target are pretty close, so we should be fine to + // do this, despite the docs on arrayFastResample. + const bars = arrayFastResample(Array.from(this.waveform), RECORDING_PLAYBACK_SAMPLES); + this.setState({ + // The incoming data is between zero and one, but typically even screaming into a + // microphone won't send you over 0.6, so we artificially adjust the gain for the + // waveform. This results in a slightly more cinematic/animated waveform for the + // user. + relHeights: bars.map(b => percentageOf(b, 0, 0.50)), + seconds: this.seconds, + }); + this.scheduledAnimationFrame = false; + }); + } + // called by composer public async send() { if (!this.state.recorder) { @@ -178,8 +227,8 @@ export default class VoiceRecordComposerTile extends React.PureComponent - - + +
    ; } diff --git a/src/components/views/voice_messages/LiveRecordingClock.tsx b/src/components/views/voice_messages/LiveRecordingClock.tsx deleted file mode 100644 index b82539eb16..0000000000 --- a/src/components/views/voice_messages/LiveRecordingClock.tsx +++ /dev/null @@ -1,49 +0,0 @@ -/* -Copyright 2021 The Matrix.org Foundation C.I.C. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -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 {IRecordingUpdate, VoiceRecording} from "../../../voice/VoiceRecording"; -import {replaceableComponent} from "../../../utils/replaceableComponent"; -import Clock from "./Clock"; - -interface IProps { - recorder: VoiceRecording; -} - -interface IState { - seconds: number; -} - -/** - * A clock for a live recording. - */ -@replaceableComponent("views.voice_messages.LiveRecordingClock") -export default class LiveRecordingClock extends React.PureComponent { - public constructor(props) { - super(props); - - this.state = {seconds: 0}; - this.props.recorder.liveData.onUpdate(this.onRecordingUpdate); - } - - private onRecordingUpdate = (update: IRecordingUpdate) => { - this.setState({seconds: update.timeSeconds}); - }; - - public render() { - return ; - } -} diff --git a/src/components/views/voice_messages/LiveRecordingWaveform.tsx b/src/components/views/voice_messages/LiveRecordingWaveform.tsx deleted file mode 100644 index aab89f6ab1..0000000000 --- a/src/components/views/voice_messages/LiveRecordingWaveform.tsx +++ /dev/null @@ -1,60 +0,0 @@ -/* -Copyright 2021 The Matrix.org Foundation C.I.C. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -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 {IRecordingUpdate, RECORDING_PLAYBACK_SAMPLES, VoiceRecording} from "../../../voice/VoiceRecording"; -import {replaceableComponent} from "../../../utils/replaceableComponent"; -import {arrayFastResample, arraySeed} from "../../../utils/arrays"; -import {percentageOf} from "../../../utils/numbers"; -import Waveform from "./Waveform"; - -interface IProps { - recorder: VoiceRecording; -} - -interface IState { - heights: number[]; -} - -/** - * A waveform which shows the waveform of a live recording - */ -@replaceableComponent("views.voice_messages.LiveRecordingWaveform") -export default class LiveRecordingWaveform extends React.PureComponent { - public constructor(props) { - super(props); - - this.state = {heights: arraySeed(0, RECORDING_PLAYBACK_SAMPLES)}; - this.props.recorder.liveData.onUpdate(this.onRecordingUpdate); - } - - private onRecordingUpdate = (update: IRecordingUpdate) => { - // The waveform and the downsample target are pretty close, so we should be fine to - // do this, despite the docs on arrayFastResample. - const bars = arrayFastResample(Array.from(update.waveform), RECORDING_PLAYBACK_SAMPLES); - this.setState({ - // The incoming data is between zero and one, but typically even screaming into a - // microphone won't send you over 0.6, so we artificially adjust the gain for the - // waveform. This results in a slightly more cinematic/animated waveform for the - // user. - heights: bars.map(b => percentageOf(b, 0, 0.50)), - }); - }; - - public render() { - return ; - } -} From a7daf558bb8f911eaedbf96cd7b8564869624e92 Mon Sep 17 00:00:00 2001 From: Germain Date: Tue, 22 Jun 2021 13:03:55 +0100 Subject: [PATCH 213/809] Use proper capitalisation for Buildkite Co-authored-by: J. Ryan Stinnett --- scripts/fetchdep.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/fetchdep.sh b/scripts/fetchdep.sh index 246add7e31..c7d8daeda5 100755 --- a/scripts/fetchdep.sh +++ b/scripts/fetchdep.sh @@ -38,7 +38,7 @@ else head=$BUILDKITE_BRANCH fi -# If head is set, it will contain on BuildKite either: +# If head is set, it will contain on Buildkite either: # * "branch" when the author's branch and target branch are in the same repo # * "fork:branch" when the author's branch is in their fork or if this is a Netlify build # We can split on `:` into an array to check. From 660f3900f8a1369a1dabea3d8e273a5c4861e673 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Tue, 22 Jun 2021 14:11:41 +0100 Subject: [PATCH 214/809] Change if statement syntax to use positive expressions --- scripts/fetchdep.sh | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/scripts/fetchdep.sh b/scripts/fetchdep.sh index c7d8daeda5..55f068e49d 100755 --- a/scripts/fetchdep.sh +++ b/scripts/fetchdep.sh @@ -25,17 +25,15 @@ clone() { # First we check if GITHUB_HEAD_REF is defined, # Then we check if BUILDKITE_BRANCH is defined, # if it isn't we can assume this is a Netlify build -if [ -z ${BUILDKITE_BRANCH+x} ]; then - if [ -z ${GITHUB_HEAD_REF+x} ]; then - # Netlify doesn't give us info about the fork so we have to get it from GitHub API - apiEndpoint="https://api.github.com/repos/matrix-org/matrix-react-sdk/pulls/" - apiEndpoint+=$REVIEW_ID - head=$(curl $apiEndpoint | jq -r '.head.label') - else - head=$GITHUB_HEAD_REF - fi -else +if [ -n "$BUILDKITE_BRANCH" ]; then head=$BUILDKITE_BRANCH +elif [ -n "$GITHUB_HEAD_REF" ]; then + head=$GITHUB_HEAD_REF +else + # Netlify doesn't give us info about the fork so we have to get it from GitHub API + apiEndpoint="https://api.github.com/repos/matrix-org/matrix-react-sdk/pulls/" + apiEndpoint+=$REVIEW_ID + head=$(curl $apiEndpoint | jq -r '.head.label') fi # If head is set, it will contain on Buildkite either: @@ -46,7 +44,8 @@ fi # to determine whether the branch is from a fork or not BRANCH_ARRAY=(${head//:/ }) if [[ "${#BRANCH_ARRAY[@]}" == "1" ]]; then - if [ -z ${BUILDKITE_BRANCH+x} ]; then + + if [ -n "$GITHUB_HEAD_REF" ]; then if [[ "$GITHUB_REPOSITORY" == "$deforg"* ]]; then clone $deforg $defrepo $GITHUB_HEAD_REF else @@ -61,9 +60,9 @@ elif [[ "${#BRANCH_ARRAY[@]}" == "2" ]]; then fi # Try the target branch of the push or PR. -if [ -n ${GITHUB_BASE_REF+x} ]; then +if [ -n $GITHUB_BASE_REF ]; then clone $deforg $defrepo $GITHUB_BASE_REF -elif [ -n ${BUILDKITE_PULL_REQUEST_BASE_BRANCH+x} ]; then +elif [ -n $BUILDKITE_PULL_REQUEST_BASE_BRANCH ]; then clone $deforg $defrepo $BUILDKITE_PULL_REQUEST_BASE_BRANCH fi From c42f0fd2e4774c60283e33c0233f93993604184f Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Tue, 22 Jun 2021 14:17:11 +0100 Subject: [PATCH 215/809] split GITHUB_REPOSITORY rather than using GITHUB_ACTOR --- scripts/fetchdep.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/fetchdep.sh b/scripts/fetchdep.sh index 55f068e49d..7d893a6039 100755 --- a/scripts/fetchdep.sh +++ b/scripts/fetchdep.sh @@ -49,7 +49,8 @@ if [[ "${#BRANCH_ARRAY[@]}" == "1" ]]; then if [[ "$GITHUB_REPOSITORY" == "$deforg"* ]]; then clone $deforg $defrepo $GITHUB_HEAD_REF else - clone $GITHUB_ACTOR $defrepo $GITHUB_HEAD_REF + REPO_ARRAY=(${GITHUB_REPOSITORY//\// }) + clone $REPO_ARRAY[0] $defrepo $GITHUB_HEAD_REF fi else clone $deforg $defrepo $BUILDKITE_BRANCH From ded738ce8c820cffb9b2a49f659e5febf130e4f1 Mon Sep 17 00:00:00 2001 From: Germain Date: Tue, 22 Jun 2021 14:57:44 +0100 Subject: [PATCH 216/809] Add spaces around curlies Co-authored-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/rooms/SearchBar.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/views/rooms/SearchBar.tsx b/src/components/views/rooms/SearchBar.tsx index 47994f5251..d71bb8da73 100644 --- a/src/components/views/rooms/SearchBar.tsx +++ b/src/components/views/rooms/SearchBar.tsx @@ -19,8 +19,8 @@ import React, { createRef, RefObject } from 'react'; import AccessibleButton from "../elements/AccessibleButton"; import classNames from "classnames"; import { _t } from '../../../languageHandler'; -import {Key} from "../../../Keyboard"; -import DesktopBuildsNotice, {WarningKind} from "../elements/DesktopBuildsNotice"; +import { Key } from "../../../Keyboard"; +import DesktopBuildsNotice, { WarningKind } from "../elements/DesktopBuildsNotice"; import { replaceableComponent } from "../../../utils/replaceableComponent"; interface IProps { @@ -95,7 +95,7 @@ export default class SearchBar extends React.Component {
    { {_t("This Room")} Date: Tue, 22 Jun 2021 15:29:53 +0100 Subject: [PATCH 217/809] remove spurious full stop --- src/i18n/strings/en_EN.json | 2 +- src/settings/Settings.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 179b58b617..a9a0d15ac4 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -796,7 +796,7 @@ "Show all rooms in Home": "Show all rooms in Home", "Show people in spaces": "Show people in spaces", "If disabled, you can still add Direct Messages to Personal Spaces. If enabled, you'll automatically see everyone who is a member of the Space.": "If disabled, you can still add Direct Messages to Personal Spaces. If enabled, you'll automatically see everyone who is a member of the Space.", - "Show notification badges for DMs in Spaces.": "Show notification badges for DMs in Spaces.", + "Show notification badges for DMs in Spaces": "Show notification badges for DMs in Spaces", "Show options to enable 'Do not disturb' mode": "Show options to enable 'Do not disturb' mode", "Send and receive voice messages": "Send and receive voice messages", "Render LaTeX maths in messages": "Render LaTeX maths in messages", diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index af026f4103..3937b7d821 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -193,7 +193,7 @@ export const SETTINGS: {[setting: string]: ISetting} = { controller: new ReloadOnChangeController(), }, "feature_spaces.space_dm_badges": { - displayName: _td("Show notification badges for DMs in Spaces."), + displayName: _td("Show notification badges for DMs in Spaces"), supportedLevels: LEVELS_FEATURE, default: false, controller: new ReloadOnChangeController(), From fca2feaae8ad83cdef7eb1d426873e33bc3e9c00 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Tue, 22 Jun 2021 16:09:33 +0100 Subject: [PATCH 218/809] make github env variable check first as it is new home for ci --- scripts/fetchdep.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/fetchdep.sh b/scripts/fetchdep.sh index 7d893a6039..0b15db6a23 100755 --- a/scripts/fetchdep.sh +++ b/scripts/fetchdep.sh @@ -25,10 +25,10 @@ clone() { # First we check if GITHUB_HEAD_REF is defined, # Then we check if BUILDKITE_BRANCH is defined, # if it isn't we can assume this is a Netlify build -if [ -n "$BUILDKITE_BRANCH" ]; then - head=$BUILDKITE_BRANCH -elif [ -n "$GITHUB_HEAD_REF" ]; then +if [ -n "$GITHUB_HEAD_REF" ]; then head=$GITHUB_HEAD_REF +elif [ -n "$BUILDKITE_BRANCH" ]; then + head=$BUILDKITE_BRANCH else # Netlify doesn't give us info about the fork so we have to get it from GitHub API apiEndpoint="https://api.github.com/repos/matrix-org/matrix-react-sdk/pulls/" From b092686453604cb37df602e6fcc796418688c022 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Tue, 22 Jun 2021 16:14:01 +0100 Subject: [PATCH 219/809] improve comment grammar --- scripts/fetchdep.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/fetchdep.sh b/scripts/fetchdep.sh index 0b15db6a23..0990af70ce 100755 --- a/scripts/fetchdep.sh +++ b/scripts/fetchdep.sh @@ -24,7 +24,7 @@ clone() { # Try the PR author's branch in case it exists on the deps as well. # First we check if GITHUB_HEAD_REF is defined, # Then we check if BUILDKITE_BRANCH is defined, -# if it isn't we can assume this is a Netlify build +# if they aren't we can assume this is a Netlify build if [ -n "$GITHUB_HEAD_REF" ]; then head=$GITHUB_HEAD_REF elif [ -n "$BUILDKITE_BRANCH" ]; then From 6d92953311ed49567a0dad80e2ff81df9cbcd417 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 22 Jun 2021 17:22:38 +0100 Subject: [PATCH 220/809] Convert bunch of utils to TS --- ...tateTransfer.js => EditorStateTransfer.ts} | 30 ++++---- src/utils/{ErrorUtils.js => ErrorUtils.tsx} | 16 +++-- src/utils/{EventUtils.js => EventUtils.ts} | 17 +++-- ...yServerUtils.js => IdentityServerUtils.ts} | 11 +-- ...ssageDiffUtils.js => MessageDiffUtils.tsx} | 68 ++++++++++--------- .../{PinningUtils.js => PinningUtils.ts} | 4 +- src/utils/{Receipt.js => Receipt.ts} | 6 +- .../{ResizeNotifier.js => ResizeNotifier.ts} | 45 ++++++------ ...eMatrixClient.js => createMatrixClient.ts} | 21 +++--- 9 files changed, 120 insertions(+), 98 deletions(-) rename src/utils/{EditorStateTransfer.js => EditorStateTransfer.ts} (58%) rename src/utils/{ErrorUtils.js => ErrorUtils.tsx} (84%) rename src/utils/{EventUtils.js => EventUtils.ts} (85%) rename src/utils/{IdentityServerUtils.js => IdentityServerUtils.ts} (82%) rename src/utils/{MessageDiffUtils.js => MessageDiffUtils.tsx} (84%) rename src/utils/{PinningUtils.js => PinningUtils.ts} (89%) rename src/utils/{Receipt.js => Receipt.ts} (83%) rename src/utils/{ResizeNotifier.js => ResizeNotifier.ts} (62%) rename src/utils/{createMatrixClient.js => createMatrixClient.ts} (76%) diff --git a/src/utils/EditorStateTransfer.js b/src/utils/EditorStateTransfer.ts similarity index 58% rename from src/utils/EditorStateTransfer.js rename to src/utils/EditorStateTransfer.ts index c7782a9ea8..42e1a316d6 100644 --- a/src/utils/EditorStateTransfer.js +++ b/src/utils/EditorStateTransfer.ts @@ -1,5 +1,5 @@ /* -Copyright 2019 The Matrix.org Foundation C.I.C. +Copyright 2019 - 2021 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,36 +14,40 @@ See the License for the specific language governing permissions and limitations under the License. */ +import { MatrixEvent } from "matrix-js-sdk/src/models/event"; + +import { SerializedPart } from "../editor/parts"; +import { Caret } from "../editor/caret"; + /** * Used while editing, to pass the event, and to preserve editor state * from one editor instance to another when remounting the editor * upon receiving the remote echo for an unsent event. */ export default class EditorStateTransfer { - constructor(event) { - this._event = event; - this._serializedParts = null; - this.caret = null; - } + private serializedParts: SerializedPart[] = null; + private caret: Caret = null; - setEditorState(caret, serializedParts) { - this._caret = caret; - this._serializedParts = serializedParts; + constructor(private readonly event: MatrixEvent) {} + + setEditorState(caret: Caret, serializedParts: SerializedPart[]) { + this.caret = caret; + this.serializedParts = serializedParts; } hasEditorState() { - return !!this._serializedParts; + return !!this.serializedParts; } getSerializedParts() { - return this._serializedParts; + return this.serializedParts; } getCaret() { - return this._caret; + return this.caret; } getEvent() { - return this._event; + return this.event; } } diff --git a/src/utils/ErrorUtils.js b/src/utils/ErrorUtils.tsx similarity index 84% rename from src/utils/ErrorUtils.js rename to src/utils/ErrorUtils.tsx index b5bd5b0af0..c39ee21f09 100644 --- a/src/utils/ErrorUtils.js +++ b/src/utils/ErrorUtils.tsx @@ -1,5 +1,5 @@ /* -Copyright 2018 New Vector Ltd +Copyright 2018 - 2021 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,7 +14,10 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { _t, _td } from '../languageHandler'; +import React, { ReactNode } from "react"; +import { MatrixError } from "matrix-js-sdk/src/http-api"; + +import { _t, _td, Tags, TranslatedString } from '../languageHandler'; /** * Produce a translated error message for a @@ -30,7 +33,12 @@ import { _t, _td } from '../languageHandler'; * for any tags in the strings apart from 'a' * @returns {*} Translated string or react component */ -export function messageForResourceLimitError(limitType, adminContact, strings, extraTranslations) { +export function messageForResourceLimitError( + limitType: string, + adminContact: string, + strings: Record, + extraTranslations?: Tags, +): TranslatedString { let errString = strings[limitType]; if (errString === undefined) errString = strings['']; @@ -49,7 +57,7 @@ export function messageForResourceLimitError(limitType, adminContact, strings, e } } -export function messageForSyncError(err) { +export function messageForSyncError(err: MatrixError | Error): ReactNode { if (err.errcode === 'M_RESOURCE_LIMIT_EXCEEDED') { const limitError = messageForResourceLimitError( err.data.limit_type, diff --git a/src/utils/EventUtils.js b/src/utils/EventUtils.ts similarity index 85% rename from src/utils/EventUtils.js rename to src/utils/EventUtils.ts index be21896417..3d9c60d9cd 100644 --- a/src/utils/EventUtils.js +++ b/src/utils/EventUtils.ts @@ -1,5 +1,5 @@ /* -Copyright 2019 New Vector Ltd +Copyright 2019 - 2021 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,9 +14,12 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { EventStatus } from 'matrix-js-sdk/src/models/event'; -import {MatrixClientPeg} from '../MatrixClientPeg'; +import { Room } from 'matrix-js-sdk/src/models/room'; +import { MatrixEvent, EventStatus } from 'matrix-js-sdk/src/models/event'; + +import { MatrixClientPeg } from '../MatrixClientPeg'; import shouldHideEvent from "../shouldHideEvent"; + /** * Returns whether an event should allow actions like reply, reactions, edit, etc. * which effectively checks whether it's a regular message that has been sent and that we @@ -25,7 +28,7 @@ import shouldHideEvent from "../shouldHideEvent"; * @param {MatrixEvent} mxEvent The event to check * @returns {boolean} true if actionable */ -export function isContentActionable(mxEvent) { +export function isContentActionable(mxEvent: MatrixEvent): boolean { const { status: eventStatus } = mxEvent; // status is SENT before remote-echo, null after @@ -45,7 +48,7 @@ export function isContentActionable(mxEvent) { return false; } -export function canEditContent(mxEvent) { +export function canEditContent(mxEvent: MatrixEvent): boolean { if (mxEvent.status === EventStatus.CANCELLED || mxEvent.getType() !== "m.room.message" || mxEvent.isRedacted()) { return false; } @@ -56,7 +59,7 @@ export function canEditContent(mxEvent) { mxEvent.getSender() === MatrixClientPeg.get().getUserId(); } -export function canEditOwnEvent(mxEvent) { +export function canEditOwnEvent(mxEvent: MatrixEvent): boolean { // for now we only allow editing // your own events. So this just call through // In the future though, moderators will be able to @@ -67,7 +70,7 @@ export function canEditOwnEvent(mxEvent) { } const MAX_JUMP_DISTANCE = 100; -export function findEditableEvent(room, isForward, fromEventId = undefined) { +export function findEditableEvent(room: Room, isForward: boolean, fromEventId: string = undefined): MatrixEvent { const liveTimeline = room.getLiveTimeline(); const events = liveTimeline.getEvents().concat(room.getPendingEvents()); const maxIdx = events.length - 1; diff --git a/src/utils/IdentityServerUtils.js b/src/utils/IdentityServerUtils.ts similarity index 82% rename from src/utils/IdentityServerUtils.js rename to src/utils/IdentityServerUtils.ts index 5ece308954..2476adca19 100644 --- a/src/utils/IdentityServerUtils.js +++ b/src/utils/IdentityServerUtils.ts @@ -1,5 +1,5 @@ /* -Copyright 2019 The Matrix.org Foundation C.I.C. +Copyright 2019 - 2021 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -15,14 +15,15 @@ limitations under the License. */ import { SERVICE_TYPES } from 'matrix-js-sdk/src/service-types'; + import SdkConfig from '../SdkConfig'; import {MatrixClientPeg} from '../MatrixClientPeg'; -export function getDefaultIdentityServerUrl() { +export function getDefaultIdentityServerUrl(): string { return SdkConfig.get()['validated_server_config']['isUrl']; } -export function useDefaultIdentityServer() { +export function useDefaultIdentityServer(): void { const url = getDefaultIdentityServerUrl(); // Account data change will update localstorage, client, etc through dispatcher MatrixClientPeg.get().setAccountData("m.identity_server", { @@ -30,7 +31,7 @@ export function useDefaultIdentityServer() { }); } -export async function doesIdentityServerHaveTerms(fullUrl) { +export async function doesIdentityServerHaveTerms(fullUrl: string): Promise { let terms; try { terms = await MatrixClientPeg.get().getTerms(SERVICE_TYPES.IS, fullUrl); @@ -46,7 +47,7 @@ export async function doesIdentityServerHaveTerms(fullUrl) { return terms && terms["policies"] && (Object.keys(terms["policies"]).length > 0); } -export function doesAccountDataHaveIdentityServer() { +export function doesAccountDataHaveIdentityServer(): boolean { const event = MatrixClientPeg.get().getAccountData("m.identity_server"); return event && event.getContent() && event.getContent()['base_url']; } diff --git a/src/utils/MessageDiffUtils.js b/src/utils/MessageDiffUtils.tsx similarity index 84% rename from src/utils/MessageDiffUtils.js rename to src/utils/MessageDiffUtils.tsx index 7398173fdd..b5d5e31432 100644 --- a/src/utils/MessageDiffUtils.js +++ b/src/utils/MessageDiffUtils.tsx @@ -1,5 +1,5 @@ /* -Copyright 2019 The Matrix.org Foundation C.I.C. +Copyright 2019 - 2021 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,31 +14,33 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; +import React, { ReactNode } from 'react'; import classNames from 'classnames'; -import DiffMatchPatch from 'diff-match-patch'; -import {DiffDOM} from "diff-dom"; -import { checkBlockNode, bodyToHtml } from "../HtmlUtils"; +import { diff_match_patch as DiffMatchPatch } from 'diff-match-patch'; +import { Action, DiffDOM, IDiff } from "diff-dom"; +import { IContent } from "matrix-js-sdk/src/models/event"; + +import { bodyToHtml, checkBlockNode, IOptsReturnString } from "../HtmlUtils"; const decodeEntities = (function() { let textarea = null; - return function(string) { + return function(str: string): string { if (!textarea) { textarea = document.createElement("textarea"); } - textarea.innerHTML = string; + textarea.innerHTML = str; return textarea.value; }; })(); -function textToHtml(text) { +function textToHtml(text: string): string { const container = document.createElement("div"); container.textContent = text; return container.innerHTML; } -function getSanitizedHtmlBody(content) { - const opts = { +function getSanitizedHtmlBody(content: IContent): string { + const opts: IOptsReturnString = { stripReplyFallback: true, returnString: true, }; @@ -57,21 +59,21 @@ function getSanitizedHtmlBody(content) { } } -function wrapInsertion(child) { +function wrapInsertion(child: Node): HTMLElement { const wrapper = document.createElement(checkBlockNode(child) ? "div" : "span"); wrapper.className = "mx_EditHistoryMessage_insertion"; wrapper.appendChild(child); return wrapper; } -function wrapDeletion(child) { +function wrapDeletion(child: Node): HTMLElement { const wrapper = document.createElement(checkBlockNode(child) ? "div" : "span"); wrapper.className = "mx_EditHistoryMessage_deletion"; wrapper.appendChild(child); return wrapper; } -function findRefNodes(root, route, isAddition) { +function findRefNodes(root: Node, route: number[], isAddition = false) { let refNode = root; let refParentNode; const end = isAddition ? route.length - 1 : route.length; @@ -79,7 +81,7 @@ function findRefNodes(root, route, isAddition) { refParentNode = refNode; refNode = refNode.childNodes[route[i]]; } - return {refNode, refParentNode}; + return { refNode, refParentNode }; } function diffTreeToDOM(desc) { @@ -101,7 +103,7 @@ function diffTreeToDOM(desc) { } } -function insertBefore(parent, nextSibling, child) { +function insertBefore(parent: Node, nextSibling: Node | null, child: Node): void { if (nextSibling) { parent.insertBefore(child, nextSibling); } else { @@ -109,7 +111,7 @@ function insertBefore(parent, nextSibling, child) { } } -function isRouteOfNextSibling(route1, route2) { +function isRouteOfNextSibling(route1: number[], route2: number[]): boolean { // routes are arrays with indices, // to be interpreted as a path in the dom tree @@ -127,7 +129,7 @@ function isRouteOfNextSibling(route1, route2) { return route2[lastD1Idx] >= route1[lastD1Idx]; } -function adjustRoutes(diff, remainingDiffs) { +function adjustRoutes(diff: IDiff, remainingDiffs: IDiff[]): void { if (diff.action === "removeTextElement" || diff.action === "removeElement") { // as removed text is not removed from the html, but marked as deleted, // we need to readjust indices that assume the current node has been removed. @@ -140,14 +142,14 @@ function adjustRoutes(diff, remainingDiffs) { } } -function stringAsTextNode(string) { +function stringAsTextNode(string: string): Text { return document.createTextNode(decodeEntities(string)); } -function renderDifferenceInDOM(originalRootNode, diff, diffMathPatch) { +function renderDifferenceInDOM(originalRootNode: Node, diff: IDiff, diffMathPatch: DiffMatchPatch): void { const {refNode, refParentNode} = findRefNodes(originalRootNode, diff.route); switch (diff.action) { - case "replaceElement": { + case Action.ReplaceElement: { const container = document.createElement("span"); const delNode = wrapDeletion(diffTreeToDOM(diff.oldValue)); const insNode = wrapInsertion(diffTreeToDOM(diff.newValue)); @@ -156,22 +158,22 @@ function renderDifferenceInDOM(originalRootNode, diff, diffMathPatch) { refNode.parentNode.replaceChild(container, refNode); break; } - case "removeTextElement": { + case Action.RemoveTextElement: { const delNode = wrapDeletion(stringAsTextNode(diff.value)); refNode.parentNode.replaceChild(delNode, refNode); break; } - case "removeElement": { + case Action.RemoveElement: { const delNode = wrapDeletion(diffTreeToDOM(diff.element)); refNode.parentNode.replaceChild(delNode, refNode); break; } - case "modifyTextElement": { + case Action.ModifyTextElement: { const textDiffs = diffMathPatch.diff_main(diff.oldValue, diff.newValue); diffMathPatch.diff_cleanupSemantic(textDiffs); const container = document.createElement("span"); for (const [modifier, text] of textDiffs) { - let textDiffNode = stringAsTextNode(text); + let textDiffNode: Node = stringAsTextNode(text); if (modifier < 0) { textDiffNode = wrapDeletion(textDiffNode); } else if (modifier > 0) { @@ -182,12 +184,12 @@ function renderDifferenceInDOM(originalRootNode, diff, diffMathPatch) { refNode.parentNode.replaceChild(container, refNode); break; } - case "addElement": { + case Action.AddElement: { const insNode = wrapInsertion(diffTreeToDOM(diff.element)); insertBefore(refParentNode, refNode, insNode); break; } - case "addTextElement": { + case Action.AddTextElement: { // XXX: sometimes diffDOM says insert a newline when there shouldn't be one // but we must insert the node anyway so that we don't break the route child IDs. // See https://github.com/fiduswriter/diffDOM/issues/100 @@ -197,11 +199,11 @@ function renderDifferenceInDOM(originalRootNode, diff, diffMathPatch) { } // e.g. when changing a the href of a link, // show the link with old href as removed and with the new href as added - case "removeAttribute": - case "addAttribute": - case "modifyAttribute": { + case Action.RemoveAttribute: + case Action.AddAttribute: + case Action.ModifyAttribute: { const delNode = wrapDeletion(refNode.cloneNode(true)); - const updatedNode = refNode.cloneNode(true); + const updatedNode = refNode.cloneNode(true) as HTMLElement; if (diff.action === "addAttribute" || diff.action === "modifyAttribute") { updatedNode.setAttribute(diff.name, diff.newValue); } else { @@ -220,12 +222,12 @@ function renderDifferenceInDOM(originalRootNode, diff, diffMathPatch) { } } -function routeIsEqual(r1, r2) { +function routeIsEqual(r1: number[], r2: number[]): boolean { return r1.length === r2.length && !r1.some((e, i) => e !== r2[i]); } // workaround for https://github.com/fiduswriter/diffDOM/issues/90 -function filterCancelingOutDiffs(originalDiffActions) { +function filterCancelingOutDiffs(originalDiffActions: IDiff[]): IDiff[] { const diffActions = originalDiffActions.slice(); for (let i = 0; i < diffActions.length; ++i) { @@ -252,7 +254,7 @@ function filterCancelingOutDiffs(originalDiffActions) { * @param {object} editContent the content for the edit message * @return {object} a react element similar to what `bodyToHtml` returns */ -export function editBodyDiffToHtml(originalContent, editContent) { +export function editBodyDiffToHtml(originalContent: IContent, editContent: IContent): ReactNode { // wrap the body in a div, DiffDOM needs a root element const originalBody = `
    ${getSanitizedHtmlBody(originalContent)}
    `; const editBody = `
    ${getSanitizedHtmlBody(editContent)}
    `; diff --git a/src/utils/PinningUtils.js b/src/utils/PinningUtils.ts similarity index 89% rename from src/utils/PinningUtils.js rename to src/utils/PinningUtils.ts index 90d26cc988..ec1eeccf1f 100644 --- a/src/utils/PinningUtils.js +++ b/src/utils/PinningUtils.ts @@ -14,13 +14,15 @@ See the License for the specific language governing permissions and limitations under the License. */ +import { MatrixEvent } from "matrix-js-sdk/src/models/event"; + export default class PinningUtils { /** * Determines if the given event may be pinned. * @param {MatrixEvent} event The event to check. * @return {boolean} True if the event may be pinned, false otherwise. */ - static isPinnable(event) { + static isPinnable(event: MatrixEvent): boolean { if (!event) return false; if (event.getType() !== "m.room.message") return false; if (event.isRedacted()) return false; diff --git a/src/utils/Receipt.js b/src/utils/Receipt.ts similarity index 83% rename from src/utils/Receipt.js rename to src/utils/Receipt.ts index d88c67fb18..2a626decc4 100644 --- a/src/utils/Receipt.js +++ b/src/utils/Receipt.ts @@ -1,5 +1,5 @@ /* -Copyright 2016 OpenMarket Ltd +Copyright 2016 - 2021 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,6 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ +import { MatrixEvent } from "matrix-js-sdk/src/models/event"; + /** * Given MatrixEvent containing receipts, return the first * read receipt from the given user ID, or null if no such @@ -23,7 +25,7 @@ limitations under the License. * @param {string} userId A user ID * @returns {Object} Read receipt */ -export function findReadReceiptFromUserId(receiptEvent, userId) { +export function findReadReceiptFromUserId(receiptEvent: MatrixEvent, userId: string): object | null { const receiptKeys = Object.keys(receiptEvent.getContent()); for (let i = 0; i < receiptKeys.length; ++i) { const rcpt = receiptEvent.getContent()[receiptKeys[i]]; diff --git a/src/utils/ResizeNotifier.js b/src/utils/ResizeNotifier.ts similarity index 62% rename from src/utils/ResizeNotifier.js rename to src/utils/ResizeNotifier.ts index 4d46d10f6c..8bb7f52e57 100644 --- a/src/utils/ResizeNotifier.js +++ b/src/utils/ResizeNotifier.ts @@ -1,5 +1,5 @@ /* -Copyright 2019 New Vector Ltd +Copyright 2019 - 2021 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -22,59 +22,58 @@ limitations under the License. * Fires when the middle panel has been resized by a pixel. * @event module:utils~ResizeNotifier#"middlePanelResizedNoisy" */ + import { EventEmitter } from "events"; import { throttle } from "lodash"; export default class ResizeNotifier extends EventEmitter { - constructor() { - super(); - // with default options, will call fn once at first call, and then every x ms - // if there was another call in that timespan - this._throttledMiddlePanel = throttle(() => this.emit("middlePanelResized"), 200); - this._isResizing = false; - } + private _isResizing = false; - get isResizing() { + // with default options, will call fn once at first call, and then every x ms + // if there was another call in that timespan + private throttledMiddlePanel = throttle(() => this.emit("middlePanelResized"), 200); + + public get isResizing() { return this._isResizing; } - startResizing() { + public startResizing() { this._isResizing = true; this.emit("isResizing", true); } - stopResizing() { + public stopResizing() { this._isResizing = false; this.emit("isResizing", false); } - _noisyMiddlePanel() { + private noisyMiddlePanel() { this.emit("middlePanelResizedNoisy"); } - _updateMiddlePanel() { - this._throttledMiddlePanel(); - this._noisyMiddlePanel(); + private updateMiddlePanel() { + this.throttledMiddlePanel(); + this.noisyMiddlePanel(); } // can be called in quick succession - notifyLeftHandleResized() { + public notifyLeftHandleResized() { // don't emit event for own region - this._updateMiddlePanel(); + this.updateMiddlePanel(); } // can be called in quick succession - notifyRightHandleResized() { - this._updateMiddlePanel(); + public notifyRightHandleResized() { + this.updateMiddlePanel(); } - notifyTimelineHeightChanged() { - this._updateMiddlePanel(); + public notifyTimelineHeightChanged() { + this.updateMiddlePanel(); } // can be called in quick succession - notifyWindowResized() { - this._updateMiddlePanel(); + public notifyWindowResized() { + this.updateMiddlePanel(); } } diff --git a/src/utils/createMatrixClient.js b/src/utils/createMatrixClient.ts similarity index 76% rename from src/utils/createMatrixClient.js rename to src/utils/createMatrixClient.ts index f5e196d846..caaf75616d 100644 --- a/src/utils/createMatrixClient.js +++ b/src/utils/createMatrixClient.ts @@ -1,5 +1,5 @@ /* -Copyright 2017 Vector Creations Ltd +Copyright 2017 - 2021 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,10 +14,10 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {createClient} from "matrix-js-sdk/src/matrix"; -import {IndexedDBCryptoStore} from "matrix-js-sdk/src/crypto/store/indexeddb-crypto-store"; -import {WebStorageSessionStore} from "matrix-js-sdk/src/store/session/webstorage"; -import {IndexedDBStore} from "matrix-js-sdk/src/store/indexeddb"; +import { createClient, ICreateClientOpts } from "matrix-js-sdk/src/matrix"; +import { IndexedDBCryptoStore } from "matrix-js-sdk/src/crypto/store/indexeddb-crypto-store"; +import { WebStorageSessionStore } from "matrix-js-sdk/src/store/session/webstorage"; +import { IndexedDBStore } from "matrix-js-sdk/src/store/indexeddb"; const localStorage = window.localStorage; @@ -41,8 +41,8 @@ try { * * @returns {MatrixClient} the newly-created MatrixClient */ -export default function createMatrixClient(opts) { - const storeOpts = { +export default function createMatrixClient(opts: ICreateClientOpts) { + const storeOpts: Partial = { useAuthorizationHeader: true, }; @@ -65,9 +65,10 @@ export default function createMatrixClient(opts) { ); } - opts = Object.assign(storeOpts, opts); - - return createClient(opts); + return createClient({ + ...storeOpts, + ...opts, + }); } createMatrixClient.indexedDbWorkerScript = null; From a839d0f3963eb00f22db2f205c4d285d34125a3a Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 22 Jun 2021 17:23:13 +0100 Subject: [PATCH 221/809] More typescript conversion --- package.json | 1 + src/@types/diff-dom.ts | 50 +++++++++++++++++ ...Tracker.js => DecryptionFailureTracker.ts} | 53 ++++++++++--------- src/HtmlUtils.tsx | 50 +++++++++-------- src/{Rooms.js => Rooms.ts} | 16 +++--- src/{Unread.js => Unread.ts} | 47 +++++++++------- src/components/structures/MatrixChat.tsx | 2 +- src/components/views/rooms/AuxPanel.tsx | 2 +- src/components/views/rooms/EventTile.tsx | 27 +++++----- src/components/views/rooms/RoomList.tsx | 2 +- src/components/views/rooms/RoomSublist.tsx | 2 +- src/languageHandler.tsx | 2 +- yarn.lock | 5 ++ 13 files changed, 166 insertions(+), 93 deletions(-) create mode 100644 src/@types/diff-dom.ts rename src/{DecryptionFailureTracker.js => DecryptionFailureTracker.ts} (80%) rename src/{Rooms.js => Rooms.ts} (89%) rename src/{Unread.js => Unread.ts} (75%) diff --git a/package.json b/package.json index f232d4301b..756604269d 100644 --- a/package.json +++ b/package.json @@ -123,6 +123,7 @@ "@sinonjs/fake-timers": "^7.0.2", "@types/classnames": "^2.2.11", "@types/counterpart": "^0.18.1", + "@types/diff-match-patch": "^1.0.5", "@types/flux": "^3.1.9", "@types/jest": "^26.0.20", "@types/linkifyjs": "^2.1.3", diff --git a/src/@types/diff-dom.ts b/src/@types/diff-dom.ts new file mode 100644 index 0000000000..884ee6126d --- /dev/null +++ b/src/@types/diff-dom.ts @@ -0,0 +1,50 @@ +/* +Copyright 2021 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +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. +*/ + +declare module "diff-dom" { + enum Action { + AddElement = "addElement", + AddTextElement = "addTextElement", + RemoveTextElement = "removeTextElement", + RemoveElement = "removeElement", + ReplaceElement = "replaceElement", + ModifyTextElement = "modifyTextElement", + AddAttribute = "addAttribute", + RemoveAttribute = "removeAttribute", + ModifyAttribute = "modifyAttribute", + } + + export interface IDiff { + action: Action; + name: string; + text?: string; + route: number[]; + value: string; + element: unknown; + oldValue: string; + newValue: string; + } + + interface IOpts { + } + + export class DiffDOM { + public constructor(opts?: IOpts); + public apply(tree: unknown, diffs: IDiff[]): unknown; + public undo(tree: unknown, diffs: IDiff[]): unknown; + public diff(a: HTMLElement | string, b: HTMLElement | string): IDiff[]; + } +} diff --git a/src/DecryptionFailureTracker.js b/src/DecryptionFailureTracker.ts similarity index 80% rename from src/DecryptionFailureTracker.js rename to src/DecryptionFailureTracker.ts index b02a5e937b..960d844e9e 100644 --- a/src/DecryptionFailureTracker.js +++ b/src/DecryptionFailureTracker.ts @@ -1,5 +1,5 @@ /* -Copyright 2018 New Vector Ltd +Copyright 2018 - 2021 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,34 +14,40 @@ See the License for the specific language governing permissions and limitations under the License. */ +import { MatrixError } from "matrix-js-sdk/src/http-api"; +import { MatrixEvent } from "matrix-js-sdk/src/models/event"; + export class DecryptionFailure { - constructor(failedEventId, errorCode) { - this.failedEventId = failedEventId; - this.errorCode = errorCode; + public readonly ts: number; + + constructor(public readonly failedEventId: string, public readonly errorCode: string) { this.ts = Date.now(); } } +type Fn = (count: number, trackedErrCode: string) => void; +type ErrCodeMapFn = (errcode: string) => string; + export class DecryptionFailureTracker { // Array of items of type DecryptionFailure. Every `CHECK_INTERVAL_MS`, this list // is checked for failures that happened > `GRACE_PERIOD_MS` ago. Those that did // are accumulated in `failureCounts`. - failures = []; + public failures: DecryptionFailure[] = []; // A histogram of the number of failures that will be tracked at the next tracking // interval, split by failure error code. - failureCounts = { + public failureCounts: Record = { // [errorCode]: 42 }; // Event IDs of failures that were tracked previously - trackedEventHashMap = { + public trackedEventHashMap: Record = { // [eventId]: true }; // Set to an interval ID when `start` is called - checkInterval = null; - trackInterval = null; + public checkInterval: NodeJS.Timeout = null; + public trackInterval: NodeJS.Timeout = null; // Spread the load on `Analytics` by tracking at a low frequency, `TRACK_INTERVAL_MS`. static TRACK_INTERVAL_MS = 60000; @@ -67,7 +73,7 @@ export class DecryptionFailureTracker { * @param {function?} errorCodeMapFn The function used to map error codes to the * trackedErrorCode. If not provided, the `.code` of errors will be used. */ - constructor(fn, errorCodeMapFn) { + constructor(private readonly fn: Fn, private readonly errorCodeMapFn?: ErrCodeMapFn) { if (!fn || typeof fn !== 'function') { throw new Error('DecryptionFailureTracker requires tracking function'); } @@ -75,9 +81,6 @@ export class DecryptionFailureTracker { if (errorCodeMapFn && typeof errorCodeMapFn !== 'function') { throw new Error('DecryptionFailureTracker second constructor argument should be a function'); } - - this._trackDecryptionFailure = fn; - this._mapErrorCode = errorCodeMapFn; } // loadTrackedEventHashMap() { @@ -88,7 +91,7 @@ export class DecryptionFailureTracker { // localStorage.setItem('mx-decryption-failure-event-id-hashes', JSON.stringify(this.trackedEventHashMap)); // } - eventDecrypted(e, err) { + public eventDecrypted(e: MatrixEvent, err: MatrixError | Error): void { if (err) { this.addDecryptionFailure(new DecryptionFailure(e.getId(), err.code)); } else { @@ -97,18 +100,18 @@ export class DecryptionFailureTracker { } } - addDecryptionFailure(failure) { + public addDecryptionFailure(failure: DecryptionFailure): void { this.failures.push(failure); } - removeDecryptionFailuresForEvent(e) { + public removeDecryptionFailuresForEvent(e: MatrixEvent): void { this.failures = this.failures.filter((f) => f.failedEventId !== e.getId()); } /** * Start checking for and tracking failures. */ - start() { + public start(): void { this.checkInterval = setInterval( () => this.checkFailures(Date.now()), DecryptionFailureTracker.CHECK_INTERVAL_MS, @@ -123,7 +126,7 @@ export class DecryptionFailureTracker { /** * Clear state and stop checking for and tracking failures. */ - stop() { + public stop(): void { clearInterval(this.checkInterval); clearInterval(this.trackInterval); @@ -132,11 +135,11 @@ export class DecryptionFailureTracker { } /** - * Mark failures that occured before nowTs - GRACE_PERIOD_MS as failures that should be + * Mark failures that occurred before nowTs - GRACE_PERIOD_MS as failures that should be * tracked. Only mark one failure per event ID. * @param {number} nowTs the timestamp that represents the time now. */ - checkFailures(nowTs) { + public checkFailures(nowTs: number): void { const failuresGivenGrace = []; const failuresNotReady = []; while (this.failures.length > 0) { @@ -175,10 +178,10 @@ export class DecryptionFailureTracker { const dedupedFailures = dedupedFailuresMap.values(); - this._aggregateFailures(dedupedFailures); + this.aggregateFailures(dedupedFailures); } - _aggregateFailures(failures) { + private aggregateFailures(failures: DecryptionFailure[]): void { for (const failure of failures) { const errorCode = failure.errorCode; this.failureCounts[errorCode] = (this.failureCounts[errorCode] || 0) + 1; @@ -189,12 +192,12 @@ export class DecryptionFailureTracker { * If there are failures that should be tracked, call the given trackDecryptionFailure * function with the number of failures that should be tracked. */ - trackFailures() { + public trackFailures(): void { for (const errorCode of Object.keys(this.failureCounts)) { if (this.failureCounts[errorCode] > 0) { - const trackedErrorCode = this._mapErrorCode ? this._mapErrorCode(errorCode) : errorCode; + const trackedErrorCode = this.errorCodeMapFn ? this.errorCodeMapFn(errorCode) : errorCode; - this._trackDecryptionFailure(this.failureCounts[errorCode], trackedErrorCode); + this.fn(this.failureCounts[errorCode], trackedErrorCode); this.failureCounts[errorCode] = 0; } } diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx index ef5ac383e3..5803029030 100644 --- a/src/HtmlUtils.tsx +++ b/src/HtmlUtils.tsx @@ -17,11 +17,10 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; +import React, { ReactNode } from 'react'; import sanitizeHtml from 'sanitize-html'; -import { IExtendedSanitizeOptions } from './@types/sanitize-html'; +import cheerio from 'cheerio'; import * as linkify from 'linkifyjs'; -import linkifyMatrix from './linkify-matrix'; import _linkifyElement from 'linkifyjs/element'; import _linkifyString from 'linkifyjs/string'; import classNames from 'classnames'; @@ -29,13 +28,15 @@ import EMOJIBASE_REGEX from 'emojibase-regex'; import url from 'url'; import katex from 'katex'; import { AllHtmlEntities } from 'html-entities'; -import SettingsStore from './settings/SettingsStore'; -import cheerio from 'cheerio'; +import { IContent } from 'matrix-js-sdk/src/models/event'; -import {tryTransformPermalinkToLocalHref} from "./utils/permalinks/Permalinks"; -import {SHORTCODE_TO_EMOJI, getEmojiFromUnicode} from "./emoji"; +import { IExtendedSanitizeOptions } from './@types/sanitize-html'; +import linkifyMatrix from './linkify-matrix'; +import SettingsStore from './settings/SettingsStore'; +import { tryTransformPermalinkToLocalHref } from "./utils/permalinks/Permalinks"; +import { SHORTCODE_TO_EMOJI, getEmojiFromUnicode } from "./emoji"; import ReplyThread from "./components/views/elements/ReplyThread"; -import {mediaFromMxc} from "./customisations/Media"; +import { mediaFromMxc } from "./customisations/Media"; linkifyMatrix(linkify); @@ -66,7 +67,7 @@ export const PERMITTED_URL_SCHEMES = ['http', 'https', 'ftp', 'mailto', 'magnet' * need emojification. * unicodeToImage uses this function. */ -function mightContainEmoji(str: string) { +function mightContainEmoji(str: string): boolean { return SURROGATE_PAIR_PATTERN.test(str) || SYMBOL_PATTERN.test(str); } @@ -76,7 +77,7 @@ function mightContainEmoji(str: string) { * @param {String} char The emoji character * @return {String} The shortcode (such as :thumbup:) */ -export function unicodeToShortcode(char: string) { +export function unicodeToShortcode(char: string): string { const data = getEmojiFromUnicode(char); return (data && data.shortcodes ? `:${data.shortcodes[0]}:` : ''); } @@ -87,7 +88,7 @@ export function unicodeToShortcode(char: string) { * @param {String} shortcode The shortcode (such as :thumbup:) * @return {String} The emoji character; null if none exists */ -export function shortcodeToUnicode(shortcode: string) { +export function shortcodeToUnicode(shortcode: string): string { shortcode = shortcode.slice(1, shortcode.length - 1); const data = SHORTCODE_TO_EMOJI.get(shortcode); return data ? data.unicode : null; @@ -124,13 +125,13 @@ export function processHtmlForSending(html: string): string { * Given an untrusted HTML string, return a React node with an sanitized version * of that HTML. */ -export function sanitizedHtmlNode(insaneHtml: string) { +export function sanitizedHtmlNode(insaneHtml: string): ReactNode { const saneHtml = sanitizeHtml(insaneHtml, sanitizeHtmlParams); return
    ; } -export function getHtmlText(insaneHtml: string) { +export function getHtmlText(insaneHtml: string): string { return sanitizeHtml(insaneHtml, { allowedTags: [], allowedAttributes: {}, @@ -148,7 +149,7 @@ export function getHtmlText(insaneHtml: string) { * other places we need to sanitise URLs. * @return true if permitted, otherwise false */ -export function isUrlPermitted(inputUrl: string) { +export function isUrlPermitted(inputUrl: string): boolean { try { const parsed = url.parse(inputUrl); if (!parsed.protocol) return false; @@ -351,13 +352,6 @@ class HtmlHighlighter extends BaseHighlighter { } } -interface IContent { - format?: string; - // eslint-disable-next-line camelcase - formatted_body?: string; - body: string; -} - interface IOpts { highlightLink?: string; disableBigEmoji?: boolean; @@ -367,6 +361,14 @@ interface IOpts { ref?: React.Ref; } +export interface IOptsReturnNode extends IOpts { + returnString: false; +} + +export interface IOptsReturnString extends IOpts { + returnString: true; +} + /* turn a matrix event body into html * * content: 'content' of the MatrixEvent @@ -380,6 +382,8 @@ interface IOpts { * opts.forComposerQuote: optional param to lessen the url rewriting done by sanitization, for quoting into composer * opts.ref: React ref to attach to any React components returned (not compatible with opts.returnString) */ +export function bodyToHtml(content: IContent, highlights: string[], opts: IOptsReturnString): string; +export function bodyToHtml(content: IContent, highlights: string[], opts: IOptsReturnNode): ReactNode; export function bodyToHtml(content: IContent, highlights: string[], opts: IOpts = {}) { const isHtmlMessage = content.format === "org.matrix.custom.html" && content.formatted_body; let bodyHasEmoji = false; @@ -523,7 +527,7 @@ export function linkifyElement(element: HTMLElement, options = linkifyMatrix.opt * @param {object} [options] Options for linkifyString. Default: linkifyMatrix.options * @returns {string} */ -export function linkifyAndSanitizeHtml(dirtyHtml: string, options = linkifyMatrix.options) { +export function linkifyAndSanitizeHtml(dirtyHtml: string, options = linkifyMatrix.options): string { return sanitizeHtml(linkifyString(dirtyHtml, options), sanitizeHtmlParams); } @@ -534,7 +538,7 @@ export function linkifyAndSanitizeHtml(dirtyHtml: string, options = linkifyMatri * @param {Node} node * @returns {bool} */ -export function checkBlockNode(node: Node) { +export function checkBlockNode(node: Node): boolean { switch (node.nodeName) { case "H1": case "H2": diff --git a/src/Rooms.js b/src/Rooms.ts similarity index 89% rename from src/Rooms.js rename to src/Rooms.ts index 955498faaa..19d1c9ee05 100644 --- a/src/Rooms.js +++ b/src/Rooms.ts @@ -1,5 +1,5 @@ /* -Copyright 2015, 2016 OpenMarket Ltd +Copyright 2015 - 2021 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,7 +14,9 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {MatrixClientPeg} from './MatrixClientPeg'; +import { Room } from "matrix-js-sdk/src/models/room"; + +import { MatrixClientPeg } from './MatrixClientPeg'; /** * Given a room object, return the alias we should use for it, @@ -25,11 +27,11 @@ import {MatrixClientPeg} from './MatrixClientPeg'; * @param {Object} room The room object * @returns {string} A display alias for the given room */ -export function getDisplayAliasForRoom(room) { +export function getDisplayAliasForRoom(room: Room): string { return room.getCanonicalAlias() || room.getAltAliases()[0]; } -export function looksLikeDirectMessageRoom(room, myUserId) { +export function looksLikeDirectMessageRoom(room: Room, myUserId: string): boolean { const myMembership = room.getMyMembership(); const me = room.getMember(myUserId); @@ -48,7 +50,7 @@ export function looksLikeDirectMessageRoom(room, myUserId) { return false; } -export function guessAndSetDMRoom(room, isDirect) { +export function guessAndSetDMRoom(room: Room, isDirect: boolean): Promise { let newTarget; if (isDirect) { const guessedUserId = guessDMRoomTargetId( @@ -70,7 +72,7 @@ export function guessAndSetDMRoom(room, isDirect) { this room as a DM room * @returns {object} A promise */ -export function setDMRoom(roomId, userId) { +export function setDMRoom(roomId: string, userId: string): Promise { if (MatrixClientPeg.get().isGuest()) { return Promise.resolve(); } @@ -114,7 +116,7 @@ export function setDMRoom(roomId, userId) { * @param {string} myUserId User ID of the current user * @returns {string} User ID of the user that the room is probably a DM with */ -function guessDMRoomTargetId(room, myUserId) { +function guessDMRoomTargetId(room: Room, myUserId: string): string { let oldestTs; let oldestUser; diff --git a/src/Unread.js b/src/Unread.ts similarity index 75% rename from src/Unread.js rename to src/Unread.ts index 25c425aa9a..b733f4175a 100644 --- a/src/Unread.js +++ b/src/Unread.ts @@ -1,5 +1,5 @@ /* -Copyright 2015, 2016 OpenMarket Ltd +Copyright 2015 - 2021 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,9 +14,13 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {MatrixClientPeg} from "./MatrixClientPeg"; +import { Room } from "matrix-js-sdk/src/models/room"; +import { MatrixEvent } from "matrix-js-sdk/src/models/event"; +import { EventType, MsgType } from "matrix-js-sdk/src/@types/event"; + +import { MatrixClientPeg } from "./MatrixClientPeg"; import shouldHideEvent from './shouldHideEvent'; -import {haveTileForEvent} from "./components/views/rooms/EventTile"; +import { haveTileForEvent } from "./components/views/rooms/EventTile"; /** * Returns true iff this event arriving in a room should affect the room's @@ -25,28 +29,33 @@ import {haveTileForEvent} from "./components/views/rooms/EventTile"; * @param {Object} ev The event * @returns {boolean} True if the given event should affect the unread message count */ -export function eventTriggersUnreadCount(ev) { +export function eventTriggersUnreadCount(ev: MatrixEvent): boolean { if (ev.sender && ev.sender.userId == MatrixClientPeg.get().credentials.userId) { return false; - } else if (ev.getType() == 'm.room.member') { - return false; - } else if (ev.getType() == 'm.room.third_party_invite') { - return false; - } else if (ev.getType() == 'm.call.answer' || ev.getType() == 'm.call.hangup') { - return false; - } else if (ev.getType() == 'm.room.message' && ev.getContent().msgtype == 'm.notify') { - 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; - } else if (ev.isRedacted()) { - return false; } + + switch (ev.getType()) { + case EventType.RoomMember: + case EventType.RoomThirdPartyInvite: + case EventType.CallAnswer: + case EventType.CallHangup: + case EventType.RoomAliases: + case EventType.RoomCanonicalAlias: + case EventType.RoomServerAcl: + return false; + + case EventType.RoomMessage: + if (ev.getContent().msgtype === MsgType.Notice) { + return false; + } + break; + } + + if (ev.isRedacted()) return false; return haveTileForEvent(ev); } -export function doesRoomHaveUnreadMessages(room) { +export function doesRoomHaveUnreadMessages(room: Room): boolean { const myUserId = MatrixClientPeg.get().getUserId(); // get the most recent read receipt sent by our account. diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index 0af2d3d635..27d628fecf 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -1461,7 +1461,7 @@ export default class MatrixChat extends React.PureComponent { }); const dft = new DecryptionFailureTracker((total, errorCode) => { - Analytics.trackEvent('E2E', 'Decryption failure', errorCode, total); + Analytics.trackEvent('E2E', 'Decryption failure', errorCode, String(total)); CountlyAnalytics.instance.track("decryption_failure", { errorCode }, null, { sum: total }); }, (errorCode) => { // Map JS-SDK error codes to tracker codes for aggregation diff --git a/src/components/views/rooms/AuxPanel.tsx b/src/components/views/rooms/AuxPanel.tsx index 74609cca13..cb01df05d1 100644 --- a/src/components/views/rooms/AuxPanel.tsx +++ b/src/components/views/rooms/AuxPanel.tsx @@ -23,7 +23,7 @@ import RateLimitedFunc from '../../../ratelimitedfunc'; import SettingsStore from "../../../settings/SettingsStore"; import AutoHideScrollbar from "../../structures/AutoHideScrollbar"; import { UIFeature } from "../../../settings/UIFeature"; -import { ResizeNotifier } from "../../../utils/ResizeNotifier"; +import ResizeNotifier from "../../../utils/ResizeNotifier"; import CallViewForRoom from '../voip/CallViewForRoom'; import { objectHasDiff } from "../../../utils/objects"; import { replaceableComponent } from "../../../utils/replaceableComponent"; diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index 0099bf73fb..3d674efe04 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -17,7 +17,6 @@ limitations under the License. import React from 'react'; import classNames from "classnames"; - import { EventType } from "matrix-js-sdk/src/@types/event"; import { EventStatus, MatrixEvent } from "matrix-js-sdk/src/models/event"; import { Relations } from "matrix-js-sdk/src/models/relations"; @@ -29,24 +28,24 @@ import { hasText } from "../../../TextForEvent"; import * as sdk from "../../../index"; import dis from '../../../dispatcher/dispatcher'; import SettingsStore from "../../../settings/SettingsStore"; -import {Layout} from "../../../settings/Layout"; -import {formatTime} from "../../../DateUtils"; -import {MatrixClientPeg} from '../../../MatrixClientPeg'; -import {ALL_RULE_TYPES} from "../../../mjolnir/BanList"; +import { Layout } from "../../../settings/Layout"; +import { formatTime } from "../../../DateUtils"; +import { MatrixClientPeg } from '../../../MatrixClientPeg'; +import { ALL_RULE_TYPES } from "../../../mjolnir/BanList"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; -import {E2E_STATE} from "./E2EIcon"; -import {toRem} from "../../../utils/units"; -import {WidgetType} from "../../../widgets/WidgetType"; +import { E2E_STATE } from "./E2EIcon"; +import { toRem } from "../../../utils/units"; +import { WidgetType } from "../../../widgets/WidgetType"; import RoomAvatar from "../avatars/RoomAvatar"; -import {WIDGET_LAYOUT_EVENT_TYPE} from "../../../stores/widgets/WidgetLayoutStore"; -import {objectHasDiff} from "../../../utils/objects"; -import {replaceableComponent} from "../../../utils/replaceableComponent"; +import { WIDGET_LAYOUT_EVENT_TYPE } from "../../../stores/widgets/WidgetLayoutStore"; +import { objectHasDiff } from "../../../utils/objects"; +import { replaceableComponent } from "../../../utils/replaceableComponent"; import Tooltip from "../elements/Tooltip"; -import { EditorStateTransfer } from "../../../utils/EditorStateTransfer"; +import EditorStateTransfer from "../../../utils/EditorStateTransfer"; import { RoomPermalinkCreator } from '../../../utils/permalinks/Permalinks'; -import {StaticNotificationState} from "../../../stores/notifications/StaticNotificationState"; +import { StaticNotificationState } from "../../../stores/notifications/StaticNotificationState"; import NotificationBadge from "./NotificationBadge"; -import {ComposerInsertPayload} from "../../../dispatcher/payloads/ComposerInsertPayload"; +import { ComposerInsertPayload } from "../../../dispatcher/payloads/ComposerInsertPayload"; import { Action } from '../../../dispatcher/actions'; const eventTileTypes = { diff --git a/src/components/views/rooms/RoomList.tsx b/src/components/views/rooms/RoomList.tsx index 5a1c3a24b3..d277a69907 100644 --- a/src/components/views/rooms/RoomList.tsx +++ b/src/components/views/rooms/RoomList.tsx @@ -22,7 +22,7 @@ import { EventType } from "matrix-js-sdk/src/@types/event"; import { _t, _td } from "../../../languageHandler"; import { RovingTabIndexProvider } from "../../../accessibility/RovingTabIndex"; -import { ResizeNotifier } from "../../../utils/ResizeNotifier"; +import ResizeNotifier from "../../../utils/ResizeNotifier"; import RoomListStore, { LISTS_UPDATE_EVENT } from "../../../stores/room-list/RoomListStore"; import RoomViewStore from "../../../stores/RoomViewStore"; import { ITagMap } from "../../../stores/room-list/algorithms/models"; diff --git a/src/components/views/rooms/RoomSublist.tsx b/src/components/views/rooms/RoomSublist.tsx index ba8bbffbcc..61166b4230 100644 --- a/src/components/views/rooms/RoomSublist.tsx +++ b/src/components/views/rooms/RoomSublist.tsx @@ -45,7 +45,7 @@ import { ActionPayload } from "../../../dispatcher/payloads"; import { Enable, Resizable } from "re-resizable"; import { Direction } from "re-resizable/lib/resizer"; import { polyfillTouchEvent } from "../../../@types/polyfill"; -import { ResizeNotifier } from "../../../utils/ResizeNotifier"; +import ResizeNotifier from "../../../utils/ResizeNotifier"; import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore"; import RoomListLayoutStore from "../../../stores/room-list/RoomListLayoutStore"; import { arrayFastClone, arrayHasOrderChange } from "../../../utils/arrays"; diff --git a/src/languageHandler.tsx b/src/languageHandler.tsx index 16950dc008..cc129fb6b9 100644 --- a/src/languageHandler.tsx +++ b/src/languageHandler.tsx @@ -112,7 +112,7 @@ export interface IVariables { [key: string]: SubstitutionValue; } -type Tags = Record; +export type Tags = Record; export type TranslatedString = string | React.ReactNode; diff --git a/yarn.lock b/yarn.lock index 952d08d0f6..5bd409e612 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1479,6 +1479,11 @@ resolved "https://registry.yarnpkg.com/@types/counterpart/-/counterpart-0.18.1.tgz#b1b784d9e54d9879f0a8cb12f2caedab65430fe8" integrity sha512-PRuFlBBkvdDOtxlIASzTmkEFar+S66Ek48NVVTWMUjtJAdn5vyMSN8y6IZIoIymGpR36q2nZbIYazBWyFxL+IQ== +"@types/diff-match-patch@^1.0.32": + version "1.0.32" + resolved "https://registry.yarnpkg.com/@types/diff-match-patch/-/diff-match-patch-1.0.32.tgz#d9c3b8c914aa8229485351db4865328337a3d09f" + integrity sha512-bPYT5ECFiblzsVzyURaNhljBH2Gh1t9LowgUwciMrNAhFewLkHT2H0Mto07Y4/3KCOGZHRQll3CTtQZ0X11D/A== + "@types/events@^3.0.0": version "3.0.0" resolved "https://registry.yarnpkg.com/@types/events/-/events-3.0.0.tgz#2862f3f58a9a7f7c3e78d79f130dd4d71c25c2a7" From 4a667942368305799e34ac8b0ea7d569a5207a36 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 22 Jun 2021 17:26:19 +0100 Subject: [PATCH 222/809] update copy --- src/i18n/strings/en_EN.json | 2 +- src/settings/Settings.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index a9a0d15ac4..17160a6b89 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -796,7 +796,7 @@ "Show all rooms in Home": "Show all rooms in Home", "Show people in spaces": "Show people in spaces", "If disabled, you can still add Direct Messages to Personal Spaces. If enabled, you'll automatically see everyone who is a member of the Space.": "If disabled, you can still add Direct Messages to Personal Spaces. If enabled, you'll automatically see everyone who is a member of the Space.", - "Show notification badges for DMs in Spaces": "Show notification badges for DMs in Spaces", + "Show notification badges for People in Spaces": "Show notification badges for People in Spaces", "Show options to enable 'Do not disturb' mode": "Show options to enable 'Do not disturb' mode", "Send and receive voice messages": "Send and receive voice messages", "Render LaTeX maths in messages": "Render LaTeX maths in messages", diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index 3937b7d821..f22882abc4 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -193,7 +193,7 @@ export const SETTINGS: {[setting: string]: ISetting} = { controller: new ReloadOnChangeController(), }, "feature_spaces.space_dm_badges": { - displayName: _td("Show notification badges for DMs in Spaces"), + displayName: _td("Show notification badges for People in Spaces"), supportedLevels: LEVELS_FEATURE, default: false, controller: new ReloadOnChangeController(), From 28c61cca2798f0c41fea47a468057a4a9ecf2c58 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 22 Jun 2021 17:56:52 +0100 Subject: [PATCH 223/809] Remove pinned resolution for @types/react to 16.x --- package.json | 3 --- yarn.lock | 10 +--------- 2 files changed, 1 insertion(+), 12 deletions(-) diff --git a/package.json b/package.json index 8ebb90f342..9ca1224baa 100644 --- a/package.json +++ b/package.json @@ -167,9 +167,6 @@ "typescript": "^4.1.3", "walk": "^2.3.14" }, - "resolutions": { - "**/@types/react": "^16.14" - }, "jest": { "testEnvironment": "./__test-utils__/environment.js", "testMatch": [ diff --git a/yarn.lock b/yarn.lock index 4f17b63337..b19a188014 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1634,15 +1634,7 @@ dependencies: "@types/react" "*" -"@types/react@*", "@types/react@^16.14": - version "16.14.2" - resolved "https://registry.yarnpkg.com/@types/react/-/react-16.14.2.tgz#85dcc0947d0645349923c04ccef6018a1ab7538c" - integrity sha512-BzzcAlyDxXl2nANlabtT4thtvbbnhee8hMmH/CcJrISDBVcJS1iOsP1f0OAgSdGE0MsY9tqcrb9YoZcOFv9dbQ== - dependencies: - "@types/prop-types" "*" - csstype "^3.0.2" - -"@types/react@^17.0.2": +"@types/react@*", "@types/react@^17.0.2": version "17.0.11" resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.11.tgz#67fcd0ddbf5a0b083a0f94e926c7d63f3b836451" integrity sha512-yFRQbD+whVonItSk7ZzP/L+gPTJVBkL/7shLEF+i9GC/1cV3JmUxEQz6+9ylhUpWSDuqo1N9qEvqS6vTj4USUA== From cd04fb76dc3dd2aa4b921e61a367015fd104f7df Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 22 Jun 2021 18:01:29 +0100 Subject: [PATCH 224/809] Fix type error --- src/stores/SpaceStore.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stores/SpaceStore.tsx b/src/stores/SpaceStore.tsx index d371086b45..f11589485a 100644 --- a/src/stores/SpaceStore.tsx +++ b/src/stores/SpaceStore.tsx @@ -335,7 +335,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { // rootSpaces.push(space); // }); - this.orphanedRooms = new Set(orphanedRooms); + this.orphanedRooms = new Set(orphanedRooms.map(r => r.roomId)); this.rootSpaces = rootSpaces; this.parentMap = backrefs; From cecf0ce299b7208fc2fe03e54c7c7adbeb06a98d Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 22 Jun 2021 20:41:26 +0100 Subject: [PATCH 225/809] Convert MessagePanel, TimelinePanel, ScrollPanel, and more to Typescript --- src/@types/global.d.ts | 32 +- .../structures/AutoHideScrollbar.tsx | 6 +- .../{MessagePanel.js => MessagePanel.tsx} | 723 +++++++++--------- .../structures/NotificationPanel.tsx | 3 +- src/components/structures/RoomDirectory.tsx | 17 +- src/components/structures/RoomView.tsx | 7 +- .../{ScrollPanel.js => ScrollPanel.tsx} | 431 ++++++----- .../{TimelinePanel.js => TimelinePanel.tsx} | 637 ++++++++------- .../views/dialogs/ForwardDialog.tsx | 33 +- .../{ErrorBoundary.js => ErrorBoundary.tsx} | 35 +- .../views/elements/EventListSummary.tsx | 14 +- .../views/elements/EventTilePreview.tsx | 11 +- .../views/elements/MemberEventListSummary.tsx | 14 +- .../{DateSeparator.js => DateSeparator.tsx} | 26 +- ...ErrorBoundary.js => TileErrorBoundary.tsx} | 28 +- src/components/views/rooms/EventTile.tsx | 18 +- 16 files changed, 1087 insertions(+), 948 deletions(-) rename src/components/structures/{MessagePanel.js => MessagePanel.tsx} (64%) rename src/components/structures/{ScrollPanel.js => ScrollPanel.tsx} (73%) rename src/components/structures/{TimelinePanel.js => TimelinePanel.tsx} (75%) rename src/components/views/elements/{ErrorBoundary.js => ErrorBoundary.tsx} (80%) rename src/components/views/messages/{DateSeparator.js => DateSeparator.tsx} (82%) rename src/components/views/messages/{TileErrorBoundary.js => TileErrorBoundary.tsx} (77%) diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts index 7eff341095..f75c17aaf4 100644 --- a/src/@types/global.d.ts +++ b/src/@types/global.d.ts @@ -16,6 +16,7 @@ limitations under the License. import "matrix-js-sdk/src/@types/global"; // load matrix-js-sdk's type extensions first import * as ModernizrStatic from "modernizr"; + import ContentMessages from "../ContentMessages"; import { IMatrixClientPeg } from "../MatrixClientPeg"; import ToastStore from "../stores/ToastStore"; @@ -23,25 +24,25 @@ import DeviceListener from "../DeviceListener"; import { RoomListStoreClass } from "../stores/room-list/RoomListStore"; import { PlatformPeg } from "../PlatformPeg"; import RoomListLayoutStore from "../stores/room-list/RoomListLayoutStore"; -import {IntegrationManagers} from "../integrations/IntegrationManagers"; -import {ModalManager} from "../Modal"; +import { IntegrationManagers } from "../integrations/IntegrationManagers"; +import { ModalManager } from "../Modal"; import SettingsStore from "../settings/SettingsStore"; -import {ActiveRoomObserver} from "../ActiveRoomObserver"; -import {Notifier} from "../Notifier"; -import type {Renderer} from "react-dom"; +import { ActiveRoomObserver } from "../ActiveRoomObserver"; +import { Notifier } from "../Notifier"; +import type { Renderer } from "react-dom"; import RightPanelStore from "../stores/RightPanelStore"; import WidgetStore from "../stores/WidgetStore"; import CallHandler from "../CallHandler"; -import {Analytics} from "../Analytics"; +import { Analytics } from "../Analytics"; import CountlyAnalytics from "../CountlyAnalytics"; import UserActivity from "../UserActivity"; -import {ModalWidgetStore} from "../stores/ModalWidgetStore"; +import { ModalWidgetStore } from "../stores/ModalWidgetStore"; import { WidgetLayoutStore } from "../stores/widgets/WidgetLayoutStore"; import VoipUserMapper from "../VoipUserMapper"; -import {SpaceStoreClass} from "../stores/SpaceStore"; +import { SpaceStoreClass } from "../stores/SpaceStore"; import TypingStore from "../stores/TypingStore"; import { EventIndexPeg } from "../indexing/EventIndexPeg"; -import {VoiceRecordingStore} from "../stores/VoiceRecordingStore"; +import { VoiceRecordingStore } from "../stores/VoiceRecordingStore"; import PerformanceMonitor from "../performance"; import UIStore from "../stores/UIStore"; import { SetupEncryptionStore } from "../stores/SetupEncryptionStore"; @@ -127,11 +128,24 @@ declare global { setSinkId(outputId: string); } + // Add Chrome-specific `instant` ScrollBehaviour + type _ScrollBehavior = ScrollBehavior | "instant"; + + interface _ScrollOptions { + behavior?: _ScrollBehavior; + } + + interface _ScrollIntoViewOptions extends _ScrollOptions { + block?: ScrollLogicalPosition; + inline?: ScrollLogicalPosition; + } + interface Element { // Safari & IE11 only have this prefixed: we used prefixed versions // previously so let's continue to support them for now webkitRequestFullScreen(options?: FullscreenOptions): Promise; msRequestFullscreen(options?: FullscreenOptions): Promise; + scrollIntoView(arg?: boolean | _ScrollIntoViewOptions): void; } interface Error { diff --git a/src/components/structures/AutoHideScrollbar.tsx b/src/components/structures/AutoHideScrollbar.tsx index 66f998b616..3b7fee3a08 100644 --- a/src/components/structures/AutoHideScrollbar.tsx +++ b/src/components/structures/AutoHideScrollbar.tsx @@ -15,12 +15,12 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from "react"; +import React, { WheelEvent } from "react"; interface IProps { className?: string; - onScroll?: () => void; - onWheel?: () => void; + onScroll?: (event: Event) => void; + onWheel?: (event: WheelEvent) => void; style?: React.CSSProperties tabIndex?: number, wrappedRef?: (ref: HTMLDivElement) => void; diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.tsx similarity index 64% rename from src/components/structures/MessagePanel.js rename to src/components/structures/MessagePanel.tsx index eb9611a6fc..492d9d9a53 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.tsx @@ -1,7 +1,5 @@ /* -Copyright 2016 OpenMarket Ltd -Copyright 2018 New Vector Ltd -Copyright 2019 The Matrix.org Foundation C.I.C. +Copyright 2016 - 2021 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -16,32 +14,46 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, {createRef} from 'react'; +import React, { createRef, KeyboardEvent, ReactNode, SyntheticEvent, TransitionEvent } from 'react'; import ReactDOM from 'react-dom'; -import PropTypes from 'prop-types'; -import shouldHideEvent from '../../shouldHideEvent'; -import {wantsDateSeparator} from '../../DateUtils'; -import * as sdk from '../../index'; +import { Room } from 'matrix-js-sdk/src/models/room'; +import { EventType } from 'matrix-js-sdk/src/@types/event'; +import { MatrixEvent } from 'matrix-js-sdk/src/models/event'; +import { Relations } from "matrix-js-sdk/src/models/relations"; +import { RoomMember } from 'matrix-js-sdk/src/models/room-member'; -import {MatrixClientPeg} from '../../MatrixClientPeg'; +import shouldHideEvent from '../../shouldHideEvent'; +import { wantsDateSeparator } from '../../DateUtils'; +import { MatrixClientPeg } from '../../MatrixClientPeg'; import SettingsStore from '../../settings/SettingsStore'; import RoomContext from "../../contexts/RoomContext"; -import {Layout, LayoutPropType} from "../../settings/Layout"; -import {_t} from "../../languageHandler"; -import {haveTileForEvent} from "../views/rooms/EventTile"; -import {hasText} from "../../TextForEvent"; +import { Layout } from "../../settings/Layout"; +import { _t } from "../../languageHandler"; +import EventTile, { haveTileForEvent, IReadReceiptProps, TileShape } from "../views/rooms/EventTile"; +import { hasText } from "../../TextForEvent"; import IRCTimelineProfileResizer from "../views/elements/IRCTimelineProfileResizer"; import DMRoomMap from "../../utils/DMRoomMap"; import NewRoomIntro from "../views/rooms/NewRoomIntro"; -import {replaceableComponent} from "../../utils/replaceableComponent"; +import { replaceableComponent } from "../../utils/replaceableComponent"; import defaultDispatcher from '../../dispatcher/dispatcher'; +import WhoIsTypingTile from '../views/rooms/WhoIsTypingTile'; +import ScrollPanel, { IScrollState } from "./ScrollPanel"; +import EventListSummary from '../views/elements/EventListSummary'; +import MemberEventListSummary from '../views/elements/MemberEventListSummary'; +import DateSeparator from '../views/messages/DateSeparator'; +import ErrorBoundary from '../views/elements/ErrorBoundary'; +import ResizeNotifier from "../../utils/ResizeNotifier"; +import Spinner from "../views/elements/Spinner"; +import TileErrorBoundary from '../views/messages/TileErrorBoundary'; +import { RoomPermalinkCreator } from "../../utils/permalinks/Permalinks"; +import EditorStateTransfer from "../../utils/EditorStateTransfer"; const CONTINUATION_MAX_INTERVAL = 5 * 60 * 1000; // 5 minutes -const continuedTypes = ['m.sticker', 'm.room.message']; +const continuedTypes = [EventType.Sticker, EventType.RoomMessage]; // check if there is a previous event and it has the same sender as this event // and the types are the same/is in continuedTypes and the time between them is <= CONTINUATION_MAX_INTERVAL -function shouldFormContinuation(prevEvent, mxEvent) { +function shouldFormContinuation(prevEvent: MatrixEvent, mxEvent: MatrixEvent): boolean { // sanity check inputs if (!prevEvent || !prevEvent.sender || !mxEvent.sender) return false; // check if within the max continuation period @@ -52,8 +64,8 @@ function shouldFormContinuation(prevEvent, mxEvent) { // Some events should appear as continuations from previous events of different types. if (mxEvent.getType() !== prevEvent.getType() && - (!continuedTypes.includes(mxEvent.getType()) || - !continuedTypes.includes(prevEvent.getType()))) return false; + (!continuedTypes.includes(mxEvent.getType() as EventType) || + !continuedTypes.includes(prevEvent.getType() as EventType))) return false; // Check if the sender is the same and hasn't changed their displayname/avatar between these events if (mxEvent.sender.userId !== prevEvent.sender.userId || @@ -66,96 +78,161 @@ function shouldFormContinuation(prevEvent, mxEvent) { return true; } -const isMembershipChange = (e) => e.getType() === 'm.room.member' || e.getType() === 'm.room.third_party_invite'; +const isMembershipChange = (e: MatrixEvent): boolean => { + return e.getType() === EventType.RoomMember || e.getType() === EventType.RoomThirdPartyInvite; +} + +interface IProps { + // the list of MatrixEvents to display + events: MatrixEvent[]; + + // true to give the component a 'display: none' style. + hidden?: boolean; + + // true to show a spinner at the top of the timeline to indicate + // back-pagination in progress + backPaginating?: boolean; + + // true to show a spinner at the end of the timeline to indicate + // forward-pagination in progress + forwardPaginating?: boolean; + + // ID of an event to highlight. If undefined, no event will be highlighted. + highlightedEventId?: string; + + // The room these events are all in together, if any. + // (The notification panel won't have a room here, for example.) + room?: Room; + + // Should we show URL Previews + showUrlPreview?: boolean; + + // event after which we should show a read marker + readMarkerEventId?: string; + + // whether the read marker should be visible + readMarkerVisible?: boolean; + + // the userid of our user. This is used to suppress the read marker + // for pending messages. + ourUserId?: string; + + // true to suppress the date at the start of the timeline + suppressFirstDateSeparator?: boolean; + + // whether to show read receipts + showReadReceipts?: boolean; + + // true if updates to the event list should cause the scroll panel to + // scroll down when we are at the bottom of the window. See ScrollPanel + // for more details. + stickyBottom?: boolean; + + // className for the panel + className: string; + + // shape parameter to be passed to EventTiles + tileShape?: TileShape; + + // show twelve hour timestamps + isTwelveHour?: boolean; + + // show timestamps always + alwaysShowTimestamps?: boolean; + + // whether to show reactions for an event + showReactions?: boolean; + + // which layout to use + layout?: Layout; + + // whether or not to show flair at all + enableFlair?: boolean; + + resizeNotifier: ResizeNotifier; + permalinkCreator?: RoomPermalinkCreator; + editState?: EditorStateTransfer; + + // callback which is called when the panel is scrolled. + onScroll?(event: Event): void; + + // callback which is called when the user interacts with the room timeline + onUserScroll(event: SyntheticEvent): void; + + // callback which is called when more content is needed. + onFillRequest?(backwards: boolean): Promise; + + // helper function to access relations for an event + onUnfillRequest?(backwards: boolean, scrollToken: string): void; + + getRelationsForEvent?(eventId: string, relationType: string, eventType: string): Relations; +} + +interface IState { + ghostReadMarkers: string[]; + showTypingNotifications: boolean; +} + +interface IReadReceiptForUser { + lastShownEventId: string; + receipt: IReadReceiptProps; +} /* (almost) stateless UI component which builds the event tiles in the room timeline. */ @replaceableComponent("structures.MessagePanel") -export default class MessagePanel extends React.Component { - static propTypes = { - // true to give the component a 'display: none' style. - hidden: PropTypes.bool, - - // true to show a spinner at the top of the timeline to indicate - // back-pagination in progress - backPaginating: PropTypes.bool, - - // true to show a spinner at the end of the timeline to indicate - // forward-pagination in progress - forwardPaginating: PropTypes.bool, - - // the list of MatrixEvents to display - events: PropTypes.array.isRequired, - - // ID of an event to highlight. If undefined, no event will be highlighted. - highlightedEventId: PropTypes.string, - - // The room these events are all in together, if any. - // (The notification panel won't have a room here, for example.) - room: PropTypes.object, - - // Should we show URL Previews - showUrlPreview: PropTypes.bool, - - // event after which we should show a read marker - readMarkerEventId: PropTypes.string, - - // whether the read marker should be visible - readMarkerVisible: PropTypes.bool, - - // the userid of our user. This is used to suppress the read marker - // for pending messages. - ourUserId: PropTypes.string, - - // true to suppress the date at the start of the timeline - suppressFirstDateSeparator: PropTypes.bool, - - // whether to show read receipts - showReadReceipts: PropTypes.bool, - - // true if updates to the event list should cause the scroll panel to - // scroll down when we are at the bottom of the window. See ScrollPanel - // for more details. - stickyBottom: PropTypes.bool, - - // callback which is called when the panel is scrolled. - onScroll: PropTypes.func, - - // callback which is called when the user interacts with the room timeline - onUserScroll: PropTypes.func, - - // callback which is called when more content is needed. - onFillRequest: PropTypes.func, - - // className for the panel - className: PropTypes.string.isRequired, - - // shape parameter to be passed to EventTiles - tileShape: PropTypes.string, - - // show twelve hour timestamps - isTwelveHour: PropTypes.bool, - - // show timestamps always - alwaysShowTimestamps: PropTypes.bool, - - // helper function to access relations for an event - getRelationsForEvent: PropTypes.func, - - // whether to show reactions for an event - showReactions: PropTypes.bool, - - // which layout to use - layout: LayoutPropType, - - // whether or not to show flair at all - enableFlair: PropTypes.bool, - }; - +export default class MessagePanel extends React.Component { static contextType = RoomContext; - constructor(props) { - super(props); + // opaque readreceipt info for each userId; used by ReadReceiptMarker + // to manage its animations + private readonly readReceiptMap: Record = {}; + + // Track read receipts by event ID. For each _shown_ event ID, we store + // the list of read receipts to display: + // [ + // { + // userId: string, + // member: RoomMember, + // ts: number, + // }, + // ] + // This is recomputed on each render. It's only stored on the component + // for ease of passing the data around since it's computed in one pass + // over all events. + private readReceiptsByEvent: Record = {}; + + // Track read receipts by user ID. For each user ID we've ever shown a + // a read receipt for, we store an object: + // { + // lastShownEventId: string, + // receipt: { + // userId: string, + // member: RoomMember, + // ts: number, + // }, + // } + // so that we can always keep receipts displayed by reverting back to + // the last shown event for that user ID when needed. This may feel like + // it duplicates the receipt storage in the room, but at this layer, we + // are tracking _shown_ event IDs, which the JS SDK knows nothing about. + // This is recomputed on each render, using the data from the previous + // render as our fallback for any user IDs we can't match a receipt to a + // displayed event in the current render cycle. + private readReceiptsByUserId: Record = {}; + + private readonly showHiddenEventsInTimeline: boolean; + private isMounted = false; + + private readMarkerNode = createRef(); + private whoIsTyping = createRef(); + private scrollPanel = createRef(); + + private readonly showTypingNotificationsWatcherRef: string; + private eventNodes: Record; + + constructor(props, context) { + super(props, context); this.state = { // previous positions the read marker has been in, so we can @@ -164,65 +241,21 @@ export default class MessagePanel extends React.Component { showTypingNotifications: SettingsStore.getValue("showTypingNotifications"), }; - // opaque readreceipt info for each userId; used by ReadReceiptMarker - // to manage its animations - this._readReceiptMap = {}; - - // Track read receipts by event ID. For each _shown_ event ID, we store - // the list of read receipts to display: - // [ - // { - // userId: string, - // member: RoomMember, - // ts: number, - // }, - // ] - // This is recomputed on each render. It's only stored on the component - // for ease of passing the data around since it's computed in one pass - // over all events. - this._readReceiptsByEvent = {}; - - // Track read receipts by user ID. For each user ID we've ever shown a - // a read receipt for, we store an object: - // { - // lastShownEventId: string, - // receipt: { - // userId: string, - // member: RoomMember, - // ts: number, - // }, - // } - // so that we can always keep receipts displayed by reverting back to - // the last shown event for that user ID when needed. This may feel like - // it duplicates the receipt storage in the room, but at this layer, we - // are tracking _shown_ event IDs, which the JS SDK knows nothing about. - // This is recomputed on each render, using the data from the previous - // render as our fallback for any user IDs we can't match a receipt to a - // displayed event in the current render cycle. - this._readReceiptsByUserId = {}; - // Cache hidden events setting on mount since Settings is expensive to // query, and we check this in a hot code path. - this._showHiddenEventsInTimeline = - SettingsStore.getValue("showHiddenEventsInTimeline"); + this.showHiddenEventsInTimeline = SettingsStore.getValue("showHiddenEventsInTimeline"); - this._isMounted = false; - - this._readMarkerNode = createRef(); - this._whoIsTyping = createRef(); - this._scrollPanel = createRef(); - - this._showTypingNotificationsWatcherRef = + this.showTypingNotificationsWatcherRef = SettingsStore.watchSetting("showTypingNotifications", null, this.onShowTypingNotificationsChange); } componentDidMount() { - this._isMounted = true; + this.isMounted = true; } componentWillUnmount() { - this._isMounted = false; - SettingsStore.unwatchSetting(this._showTypingNotificationsWatcherRef); + this.isMounted = false; + SettingsStore.unwatchSetting(this.showTypingNotificationsWatcherRef); } componentDidUpdate(prevProps, prevState) { @@ -235,14 +268,14 @@ export default class MessagePanel extends React.Component { } } - onShowTypingNotificationsChange = () => { + private onShowTypingNotificationsChange = (): void => { this.setState({ showTypingNotifications: SettingsStore.getValue("showTypingNotifications"), }); }; /* get the DOM node representing the given event */ - getNodeForEventId(eventId) { + public getNodeForEventId(eventId: string): HTMLElement { if (!this.eventNodes) { return undefined; } @@ -252,8 +285,8 @@ export default class MessagePanel extends React.Component { /* return true if the content is fully scrolled down right now; else false. */ - isAtBottom() { - return this._scrollPanel.current && this._scrollPanel.current.isAtBottom(); + public isAtBottom(): boolean { + return this.scrollPanel.current?.isAtBottom(); } /* get the current scroll state. See ScrollPanel.getScrollState for @@ -261,8 +294,8 @@ export default class MessagePanel extends React.Component { * * returns null if we are not mounted. */ - getScrollState() { - return this._scrollPanel.current ? this._scrollPanel.current.getScrollState() : null; + public getScrollState(): IScrollState { + return this.scrollPanel.current?.getScrollState() ?? null; } // returns one of: @@ -271,15 +304,15 @@ export default class MessagePanel extends React.Component { // -1: read marker is above the window // 0: read marker is within the window // +1: read marker is below the window - getReadMarkerPosition() { - const readMarker = this._readMarkerNode.current; - const messageWrapper = this._scrollPanel.current; + public getReadMarkerPosition(): number { + const readMarker = this.readMarkerNode.current; + const messageWrapper = this.scrollPanel.current; if (!readMarker || !messageWrapper) { return null; } - const wrapperRect = ReactDOM.findDOMNode(messageWrapper).getBoundingClientRect(); + const wrapperRect = (ReactDOM.findDOMNode(messageWrapper) as HTMLElement).getBoundingClientRect(); const readMarkerRect = readMarker.getBoundingClientRect(); // the read-marker pretends to have zero height when it is actually @@ -295,17 +328,17 @@ export default class MessagePanel extends React.Component { /* jump to the top of the content. */ - scrollToTop() { - if (this._scrollPanel.current) { - this._scrollPanel.current.scrollToTop(); + public scrollToTop(): void { + if (this.scrollPanel.current) { + this.scrollPanel.current.scrollToTop(); } } /* jump to the bottom of the content. */ - scrollToBottom() { - if (this._scrollPanel.current) { - this._scrollPanel.current.scrollToBottom(); + public scrollToBottom(): void { + if (this.scrollPanel.current) { + this.scrollPanel.current.scrollToBottom(); } } @@ -314,9 +347,9 @@ export default class MessagePanel extends React.Component { * * @param {number} mult: -1 to page up, +1 to page down */ - scrollRelative(mult) { - if (this._scrollPanel.current) { - this._scrollPanel.current.scrollRelative(mult); + public scrollRelative(mult: number): void { + if (this.scrollPanel.current) { + this.scrollPanel.current.scrollRelative(mult); } } @@ -325,9 +358,9 @@ export default class MessagePanel extends React.Component { * * @param {KeyboardEvent} ev: the keyboard event to handle */ - handleScrollKey(ev) { - if (this._scrollPanel.current) { - this._scrollPanel.current.handleScrollKey(ev); + public handleScrollKey(ev: KeyboardEvent): void { + if (this.scrollPanel.current) { + this.scrollPanel.current.handleScrollKey(ev); } } @@ -341,38 +374,41 @@ export default class MessagePanel extends React.Component { * node (specifically, the bottom of it) will be positioned. If omitted, it * defaults to 0. */ - scrollToEvent(eventId, pixelOffset, offsetBase) { - if (this._scrollPanel.current) { - this._scrollPanel.current.scrollToToken(eventId, pixelOffset, offsetBase); + public scrollToEvent(eventId: string, pixelOffset: number, offsetBase: number): void { + if (this.scrollPanel.current) { + this.scrollPanel.current.scrollToToken(eventId, pixelOffset, offsetBase); } } - scrollToEventIfNeeded(eventId) { + public scrollToEventIfNeeded(eventId: string): void { const node = this.eventNodes[eventId]; if (node) { - node.scrollIntoView({block: "nearest", behavior: "instant"}); + node.scrollIntoView({ + block: "nearest", + behavior: "instant", + }); } } /* check the scroll state and send out pagination requests if necessary. */ - checkFillState() { - if (this._scrollPanel.current) { - this._scrollPanel.current.checkFillState(); + public checkFillState(): void { + if (this.scrollPanel.current) { + this.scrollPanel.current.checkFillState(); } } - _isUnmounting = () => { - return !this._isMounted; + private isUnmounting = (): boolean => { + return !this.isMounted; }; // TODO: Implement granular (per-room) hide options - _shouldShowEvent(mxEv) { + public shouldShowEvent(mxEv: MatrixEvent): boolean { if (mxEv.sender && MatrixClientPeg.get().isUserIgnored(mxEv.sender.userId)) { return false; // ignored = no show (only happens if the ignore happens after an event was received) } - if (this._showHiddenEventsInTimeline) { + if (this.showHiddenEventsInTimeline) { return true; } @@ -386,7 +422,7 @@ export default class MessagePanel extends React.Component { return !shouldHideEvent(mxEv, this.context); } - _readMarkerForEvent(eventId, isLastEvent) { + public readMarkerForEvent(eventId: string, isLastEvent: boolean): ReactNode { const visible = !isLastEvent && this.props.readMarkerVisible; if (this.props.readMarkerEventId === eventId) { @@ -405,7 +441,7 @@ export default class MessagePanel extends React.Component { return (
  • @@ -424,8 +460,8 @@ export default class MessagePanel extends React.Component { // transition (ie. the read markers do but the event tiles do not) // and TransitionGroup requires that all its children are Transitions. const hr =
    ; @@ -445,7 +481,7 @@ export default class MessagePanel extends React.Component { return null; } - _collectGhostReadMarker = (node) => { + private collectGhostReadMarker = (node: HTMLElement): void => { if (node) { // now the element has appeared, change the style which will trigger the CSS transition requestAnimationFrame(() => { @@ -455,15 +491,15 @@ export default class MessagePanel extends React.Component { } }; - _onGhostTransitionEnd = (ev) => { + private onGhostTransitionEnd = (ev: TransitionEvent): void => { // we can now clean up the ghost element - const finishedEventId = ev.target.dataset.eventid; + const finishedEventId = (ev.target as HTMLElement).dataset.eventid; this.setState({ ghostReadMarkers: this.state.ghostReadMarkers.filter(eid => eid !== finishedEventId), }); }; - _getNextEventInfo(arr, i) { + private getNextEventInfo(arr: MatrixEvent[], i: number): { nextEvent: MatrixEvent, nextTile: MatrixEvent } { const nextEvent = i < arr.length - 1 ? arr[i + 1] : null; @@ -472,16 +508,16 @@ export default class MessagePanel extends React.Component { // when rendering the tile. The shouldShowEvent function is pretty quick at what // it does, so this should have no significant cost even when a room is used for // not-chat purposes. - const nextTile = arr.slice(i + 1).find(e => this._shouldShowEvent(e)); + const nextTile = arr.slice(i + 1).find(e => this.shouldShowEvent(e)); - return {nextEvent, nextTile}; + return { nextEvent, nextTile }; } - get _roomHasPendingEdit() { + private get roomHasPendingEdit(): string { return this.props.room && localStorage.getItem(`mx_edit_room_${this.props.room.roomId}`); } - _getEventTiles() { + private getEventTiles(): ReactNode[] { this.eventNodes = {}; let i; @@ -497,7 +533,7 @@ export default class MessagePanel extends React.Component { let lastShownNonLocalEchoIndex = -1; for (i = this.props.events.length-1; i >= 0; i--) { const mxEv = this.props.events[i]; - if (!this._shouldShowEvent(mxEv)) { + if (!this.shouldShowEvent(mxEv)) { continue; } @@ -521,18 +557,18 @@ export default class MessagePanel extends React.Component { // Note: the EventTile might still render a "sent/sending receipt" independent of // this information. When not providing read receipt information, the tile is likely // to assume that sent receipts are to be shown more often. - this._readReceiptsByEvent = {}; + this.readReceiptsByEvent = {}; if (this.props.showReadReceipts) { - this._readReceiptsByEvent = this._getReadReceiptsByShownEvent(); + this.readReceiptsByEvent = this.getReadReceiptsByShownEvent(); } - let grouper = null; + let grouper: BaseGrouper = null; for (i = 0; i < this.props.events.length; i++) { const mxEv = this.props.events[i]; const eventId = mxEv.getId(); const last = (mxEv === lastShownEvent); - const {nextEvent, nextTile} = this._getNextEventInfo(this.props.events, i); + const { nextEvent, nextTile } = this.getNextEventInfo(this.props.events, i); if (grouper) { if (grouper.shouldGroup(mxEv)) { @@ -553,26 +589,25 @@ export default class MessagePanel extends React.Component { } } if (!grouper) { - const wantTile = this._shouldShowEvent(mxEv); + const wantTile = this.shouldShowEvent(mxEv); const isGrouped = false; if (wantTile) { - // make sure we unpack the array returned by _getTilesForEvent, + // make sure we unpack the array returned by getTilesForEvent, // otherwise react will auto-generate keys and we will end up // replacing all of the DOM elements every time we paginate. - ret.push(...this._getTilesForEvent(prevEvent, mxEv, last, isGrouped, - nextEvent, nextTile)); + ret.push(...this.getTilesForEvent(prevEvent, mxEv, last, isGrouped, nextEvent, nextTile)); prevEvent = mxEv; } - const readMarker = this._readMarkerForEvent(eventId, i >= lastShownNonLocalEchoIndex); + const readMarker = this.readMarkerForEvent(eventId, i >= lastShownNonLocalEchoIndex); if (readMarker) ret.push(readMarker); } } - if (!this.props.editState && this._roomHasPendingEdit) { + if (!this.props.editState && this.roomHasPendingEdit) { defaultDispatcher.dispatch({ action: "edit_event", - event: this.props.room.findEventById(this._roomHasPendingEdit), + event: this.props.room.findEventById(this.roomHasPendingEdit), }); } @@ -583,10 +618,14 @@ export default class MessagePanel extends React.Component { return ret; } - _getTilesForEvent(prevEvent, mxEv, last, isGrouped=false, nextEvent, nextEventWithTile) { - const TileErrorBoundary = sdk.getComponent('messages.TileErrorBoundary'); - const EventTile = sdk.getComponent('rooms.EventTile'); - const DateSeparator = sdk.getComponent('messages.DateSeparator'); + public getTilesForEvent( + prevEvent: MatrixEvent, + mxEv: MatrixEvent, + last = false, + isGrouped = false, + nextEvent?: MatrixEvent, + nextEventWithTile?: MatrixEvent, + ): ReactNode[] { const ret = []; const isEditing = this.props.editState && @@ -601,7 +640,7 @@ export default class MessagePanel extends React.Component { } // do we need a date separator since the last event? - const wantsDateSeparator = this._wantsDateSeparator(prevEvent, eventDate); + const wantsDateSeparator = this.wantsDateSeparator(prevEvent, eventDate); if (wantsDateSeparator && !isGrouped) { const dateSeparator =
  • ; ret.push(dateSeparator); @@ -609,7 +648,7 @@ export default class MessagePanel extends React.Component { let willWantDateSeparator = false; if (nextEvent) { - willWantDateSeparator = this._wantsDateSeparator(mxEv, nextEvent.getDate() || new Date()); + willWantDateSeparator = this.wantsDateSeparator(mxEv, nextEvent.getDate() || new Date()); } // is this a continuation of the previous message? @@ -618,12 +657,12 @@ export default class MessagePanel extends React.Component { const eventId = mxEv.getId(); const highlight = (eventId === this.props.highlightedEventId); - const readReceipts = this._readReceiptsByEvent[eventId]; + const readReceipts = this.readReceiptsByEvent[eventId]; let isLastSuccessful = false; const isSentState = s => !s || s === 'sent'; const isSent = isSentState(mxEv.getAssociatedStatus()); - const hasNextEvent = nextEvent && this._shouldShowEvent(nextEvent); + const hasNextEvent = nextEvent && this.shouldShowEvent(nextEvent); if (!hasNextEvent && isSent) { isLastSuccessful = true; } else if (hasNextEvent && isSent && !isSentState(nextEvent.getAssociatedStatus())) { @@ -649,18 +688,18 @@ export default class MessagePanel extends React.Component { { if (!r.userId || r.type !== "m.read" || r.userId === myUserId) { return; // ignore non-read receipts and receipts from self. @@ -721,13 +760,13 @@ export default class MessagePanel extends React.Component { // Get an object that maps from event ID to a list of read receipts that // should be shown next to that event. If a hidden event has read receipts, // they are folded into the receipts of the last shown event. - _getReadReceiptsByShownEvent() { + private getReadReceiptsByShownEvent(): Record { const receiptsByEvent = {}; const receiptsByUserId = {}; let lastShownEventId; for (const event of this.props.events) { - if (this._shouldShowEvent(event)) { + if (this.shouldShowEvent(event)) { lastShownEventId = event.getId(); } if (!lastShownEventId) { @@ -735,7 +774,7 @@ export default class MessagePanel extends React.Component { } const existingReceipts = receiptsByEvent[lastShownEventId] || []; - const newReceipts = this._getReadReceiptsForEvent(event); + const newReceipts = this.getReadReceiptsForEvent(event); receiptsByEvent[lastShownEventId] = existingReceipts.concat(newReceipts); // Record these receipts along with their last shown event ID for @@ -754,16 +793,16 @@ export default class MessagePanel extends React.Component { // someone which had one in the last. By looking through our previous // mapping of receipts by user ID, we can cover recover any receipts // that would have been lost by using the same event ID from last time. - for (const userId in this._readReceiptsByUserId) { + for (const userId in this.readReceiptsByUserId) { if (receiptsByUserId[userId]) { continue; } - const { lastShownEventId, receipt } = this._readReceiptsByUserId[userId]; + const { lastShownEventId, receipt } = this.readReceiptsByUserId[userId]; const existingReceipts = receiptsByEvent[lastShownEventId] || []; receiptsByEvent[lastShownEventId] = existingReceipts.concat(receipt); receiptsByUserId[userId] = { lastShownEventId, receipt }; } - this._readReceiptsByUserId = receiptsByUserId; + this.readReceiptsByUserId = receiptsByUserId; // After grouping receipts by shown events, do another pass to sort each // receipt list. @@ -776,21 +815,21 @@ export default class MessagePanel extends React.Component { return receiptsByEvent; } - _collectEventNode = (eventId, node) => { + private collectEventNode = (eventId: string, node: EventTile): void => { this.eventNodes[eventId] = node?.ref?.current; } // once dynamic content in the events load, make the scrollPanel check the // scroll offsets. - _onHeightChanged = () => { - const scrollPanel = this._scrollPanel.current; + public onHeightChanged = (): void => { + const scrollPanel = this.scrollPanel.current; if (scrollPanel) { scrollPanel.checkScroll(); } }; - _onTypingShown = () => { - const scrollPanel = this._scrollPanel.current; + private onTypingShown = (): void => { + const scrollPanel = this.scrollPanel.current; // this will make the timeline grow, so checkScroll scrollPanel.checkScroll(); if (scrollPanel && scrollPanel.getScrollState().stuckAtBottom) { @@ -798,8 +837,8 @@ export default class MessagePanel extends React.Component { } }; - _onTypingHidden = () => { - const scrollPanel = this._scrollPanel.current; + private onTypingHidden = (): void => { + const scrollPanel = this.scrollPanel.current; if (scrollPanel) { // as hiding the typing notifications doesn't // update the scrollPanel, we tell it to apply @@ -811,12 +850,12 @@ export default class MessagePanel extends React.Component { } }; - updateTimelineMinHeight() { - const scrollPanel = this._scrollPanel.current; + public updateTimelineMinHeight(): void { + const scrollPanel = this.scrollPanel.current; if (scrollPanel) { const isAtBottom = scrollPanel.isAtBottom(); - const whoIsTyping = this._whoIsTyping.current; + const whoIsTyping = this.whoIsTyping.current; const isTypingVisible = whoIsTyping && whoIsTyping.isVisible(); // when messages get added to the timeline, // but somebody else is still typing, @@ -828,18 +867,14 @@ export default class MessagePanel extends React.Component { } } - onTimelineReset() { - const scrollPanel = this._scrollPanel.current; + public onTimelineReset(): void { + const scrollPanel = this.scrollPanel.current; if (scrollPanel) { scrollPanel.clearPreventShrinking(); } } render() { - const ErrorBoundary = sdk.getComponent('elements.ErrorBoundary'); - const ScrollPanel = sdk.getComponent("structures.ScrollPanel"); - const WhoIsTypingTile = sdk.getComponent("rooms.WhoIsTypingTile"); - const Spinner = sdk.getComponent("elements.Spinner"); let topSpinner; let bottomSpinner; if (this.props.backPaginating) { @@ -855,9 +890,9 @@ export default class MessagePanel extends React.Component { if (this.props.room && !this.props.tileShape && this.state.showTypingNotifications) { whoIsTyping = ( + onShown={this.onTypingShown} + onHidden={this.onTypingHidden} + ref={this.whoIsTyping} /> ); } @@ -873,11 +908,10 @@ export default class MessagePanel extends React.Component { return ( { topSpinner } - { this._getEventTiles() } + { this.getEventTiles() } { whoIsTyping } { bottomSpinner } @@ -895,6 +929,31 @@ export default class MessagePanel extends React.Component { } } +abstract class BaseGrouper { + static canStartGroup = (panel: MessagePanel, ev: MatrixEvent): boolean => true; + + public events: MatrixEvent[] = []; + // events that we include in the group but then eject out and place above the group. + public ejectedEvents: MatrixEvent[] = []; + public readMarker: ReactNode; + + constructor( + public readonly panel: MessagePanel, + public readonly event: MatrixEvent, + public readonly prevEvent: MatrixEvent, + public readonly lastShownEvent: MatrixEvent, + public readonly nextEvent?: MatrixEvent, + public readonly nextEventTile?: MatrixEvent, + ) { + this.readMarker = panel.readMarkerForEvent(event.getId(), event === lastShownEvent); + } + + public abstract shouldGroup(ev: MatrixEvent): boolean; + public abstract add(ev: MatrixEvent): void; + public abstract getTiles(): ReactNode[]; + public abstract getNewPrevEvent(): MatrixEvent; +} + /* Grouper classes determine when events can be grouped together in a summary. * Groupers should have the following methods: * - canStartGroup (static): determines if a new group should be started with the @@ -910,36 +969,21 @@ export default class MessagePanel extends React.Component { // Wrap initial room creation events into an EventListSummary // Grouping only events sent by the same user that sent the `m.room.create` and only until // the first non-state event or membership event which is not regarding the sender of the `m.room.create` event -class CreationGrouper { - static canStartGroup = function(panel, ev) { - return ev.getType() === "m.room.create"; +class CreationGrouper extends BaseGrouper { + static canStartGroup = function(panel: MessagePanel, ev: MatrixEvent): boolean { + return ev.getType() === EventType.RoomCreate; }; - constructor(panel, createEvent, prevEvent, lastShownEvent) { - this.panel = panel; - this.createEvent = createEvent; - this.prevEvent = prevEvent; - this.lastShownEvent = lastShownEvent; - this.events = []; - // events that we include in the group but then eject out and place - // above the group. - this.ejectedEvents = []; - this.readMarker = panel._readMarkerForEvent( - createEvent.getId(), - createEvent === lastShownEvent, - ); - } - - shouldGroup(ev) { + public shouldGroup(ev: MatrixEvent): boolean { const panel = this.panel; - const createEvent = this.createEvent; - if (!panel._shouldShowEvent(ev)) { + const createEvent = this.event; + if (!panel.shouldShowEvent(ev)) { return true; } - if (panel._wantsDateSeparator(this.createEvent, ev.getDate())) { + if (panel.wantsDateSeparator(this.event, ev.getDate())) { return false; } - if (ev.getType() === "m.room.member" + if (ev.getType() === EventType.RoomMember && (ev.getStateKey() !== createEvent.getSender() || ev.getContent()["membership"] !== "join")) { return false; } @@ -949,37 +993,35 @@ class CreationGrouper { return false; } - add(ev) { + public add(ev: MatrixEvent): void { const panel = this.panel; - this.readMarker = this.readMarker || panel._readMarkerForEvent( + this.readMarker = this.readMarker || panel.readMarkerForEvent( ev.getId(), ev === this.lastShownEvent, ); - if (!panel._shouldShowEvent(ev)) { + if (!panel.shouldShowEvent(ev)) { return; } - if (ev.getType() === "m.room.encryption") { + if (ev.getType() === EventType.RoomEncryption) { this.ejectedEvents.push(ev); } else { this.events.push(ev); } } - getTiles() { + public getTiles(): ReactNode[] { // If we don't have any events to group, don't even try to group them. The logic // below assumes that we have a group of events to deal with, but we might not if // the events we were supposed to group were redacted. if (!this.events || !this.events.length) return []; - const DateSeparator = sdk.getComponent('messages.DateSeparator'); - const EventListSummary = sdk.getComponent('views.elements.EventListSummary'); const panel = this.panel; const ret = []; const isGrouped = true; - const createEvent = this.createEvent; + const createEvent = this.event; const lastShownEvent = this.lastShownEvent; - if (panel._wantsDateSeparator(this.prevEvent, createEvent.getDate())) { + if (panel.wantsDateSeparator(this.prevEvent, createEvent.getDate())) { const ts = createEvent.getTs(); ret.push(
  • , @@ -987,13 +1029,13 @@ class CreationGrouper { } // If this m.room.create event should be shown (room upgrade) then show it before the summary - if (panel._shouldShowEvent(createEvent)) { + if (panel.shouldShowEvent(createEvent)) { // pass in the createEvent as prevEvent as well so no extra DateSeparator is rendered - ret.push(...panel._getTilesForEvent(createEvent, createEvent)); + ret.push(...panel.getTilesForEvent(createEvent, createEvent)); } for (const ejected of this.ejectedEvents) { - ret.push(...panel._getTilesForEvent( + ret.push(...panel.getTilesForEvent( createEvent, ejected, createEvent === lastShownEvent, isGrouped, )); } @@ -1003,7 +1045,7 @@ class CreationGrouper { // of EventListSummary, render each member event as if the previous // one was itself. This way, the timestamp of the previous event === the // timestamp of the current event, and no DateSeparator is inserted. - return panel._getTilesForEvent(e, e, e === lastShownEvent, isGrouped); + return panel.getTilesForEvent(e, e, e === lastShownEvent, isGrouped); }).reduce((a, b) => a.concat(b), []); // Get sender profile from the latest event in the summary as the m.room.create doesn't contain one const ev = this.events[this.events.length - 1]; @@ -1023,7 +1065,7 @@ class CreationGrouper { @@ -1038,62 +1080,59 @@ class CreationGrouper { return ret; } - getNewPrevEvent() { - return this.createEvent; + public getNewPrevEvent(): MatrixEvent { + return this.event; } } -class RedactionGrouper { - static canStartGroup = function(panel, ev) { - return panel._shouldShowEvent(ev) && ev.isRedacted(); +class RedactionGrouper extends BaseGrouper { + static canStartGroup = function(panel: MessagePanel, ev: MatrixEvent): boolean { + return panel.shouldShowEvent(ev) && ev.isRedacted(); } - constructor(panel, ev, prevEvent, lastShownEvent, nextEvent, nextEventTile) { - this.panel = panel; - this.readMarker = panel._readMarkerForEvent( - ev.getId(), - ev === lastShownEvent, - ); + constructor( + panel: MessagePanel, + ev: MatrixEvent, + prevEvent: MatrixEvent, + lastShownEvent: MatrixEvent, + nextEvent: MatrixEvent, + nextEventTile: MatrixEvent, + ) { + super(panel, ev, prevEvent, lastShownEvent, nextEvent, nextEventTile); this.events = [ev]; - this.prevEvent = prevEvent; - this.lastShownEvent = lastShownEvent; - this.nextEvent = nextEvent; - this.nextEventTile = nextEventTile; } - shouldGroup(ev) { + public shouldGroup(ev: MatrixEvent): boolean { // absorb hidden events so that they do not break up streams of messages & redaction events being grouped - if (!this.panel._shouldShowEvent(ev)) { + if (!this.panel.shouldShowEvent(ev)) { return true; } - if (this.panel._wantsDateSeparator(this.events[0], ev.getDate())) { + if (this.panel.wantsDateSeparator(this.events[0], ev.getDate())) { return false; } return ev.isRedacted(); } - add(ev) { - this.readMarker = this.readMarker || this.panel._readMarkerForEvent( + public add(ev: MatrixEvent): void { + this.readMarker = this.readMarker || this.panel.readMarkerForEvent( ev.getId(), ev === this.lastShownEvent, ); - if (!this.panel._shouldShowEvent(ev)) { + if (!this.panel.shouldShowEvent(ev)) { return; } this.events.push(ev); } - getTiles() { + public getTiles(): ReactNode[] { if (!this.events || !this.events.length) return []; - const DateSeparator = sdk.getComponent('messages.DateSeparator'); - const EventListSummary = sdk.getComponent('views.elements.EventListSummary'); const isGrouped = true; const panel = this.panel; const ret = []; const lastShownEvent = this.lastShownEvent; - if (panel._wantsDateSeparator(this.prevEvent, this.events[0].getDate())) { + if (panel.wantsDateSeparator(this.prevEvent, this.events[0].getDate())) { const ts = this.events[0].getTs(); ret.push(
  • , @@ -1104,11 +1143,11 @@ class RedactionGrouper { this.prevEvent ? this.events[0].getId() : "initial" ); - const senders = new Set(); + const senders = new Set(); let eventTiles = this.events.map((e, i) => { senders.add(e.sender); const prevEvent = i === 0 ? this.prevEvent : this.events[i - 1]; - return panel._getTilesForEvent( + return panel.getTilesForEvent( prevEvent, e, e === lastShownEvent, isGrouped, this.nextEvent, this.nextEventTile); }).reduce((a, b) => a.concat(b), []); @@ -1121,7 +1160,7 @@ class RedactionGrouper { key={key} threshold={2} events={this.events} - onToggle={panel._onHeightChanged} // Update scroll state + onToggle={panel.onHeightChanged} // Update scroll state summaryMembers={Array.from(senders)} summaryText={_t("%(count)s messages deleted.", { count: eventTiles.length })} > @@ -1136,61 +1175,58 @@ class RedactionGrouper { return ret; } - getNewPrevEvent() { + public getNewPrevEvent(): MatrixEvent { return this.events[this.events.length - 1]; } } // Wrap consecutive member events in a ListSummary, ignore if redacted -class MemberGrouper { - static canStartGroup = function(panel, ev) { - return panel._shouldShowEvent(ev) && isMembershipChange(ev); +class MemberGrouper extends BaseGrouper { + static canStartGroup = function(panel: MessagePanel, ev: MatrixEvent): boolean { + return panel.shouldShowEvent(ev) && isMembershipChange(ev); } - constructor(panel, ev, prevEvent, lastShownEvent) { - this.panel = panel; - this.readMarker = panel._readMarkerForEvent( - ev.getId(), - ev === lastShownEvent, - ); - this.events = [ev]; - this.prevEvent = prevEvent; - this.lastShownEvent = lastShownEvent; + constructor( + public readonly panel: MessagePanel, + public readonly event: MatrixEvent, + public readonly prevEvent: MatrixEvent, + public readonly lastShownEvent: MatrixEvent, + ) { + super(panel, event, prevEvent, lastShownEvent); + this.events = [event]; } - shouldGroup(ev) { - if (this.panel._wantsDateSeparator(this.events[0], ev.getDate())) { + public shouldGroup(ev: MatrixEvent): boolean { + if (this.panel.wantsDateSeparator(this.events[0], ev.getDate())) { return false; } return isMembershipChange(ev); } - add(ev) { - if (ev.getType() === 'm.room.member') { + public add(ev: MatrixEvent): void { + if (ev.getType() === EventType.RoomMember) { // We can ignore any events that don't actually have a message to display if (!hasText(ev)) return; } - this.readMarker = this.readMarker || this.panel._readMarkerForEvent( + this.readMarker = this.readMarker || this.panel.readMarkerForEvent( ev.getId(), ev === this.lastShownEvent, ); this.events.push(ev); } - getTiles() { + public getTiles(): ReactNode[] { // If we don't have any events to group, don't even try to group them. The logic // below assumes that we have a group of events to deal with, but we might not if // the events we were supposed to group were redacted. if (!this.events || !this.events.length) return []; - const DateSeparator = sdk.getComponent('messages.DateSeparator'); - const MemberEventListSummary = sdk.getComponent('views.elements.MemberEventListSummary'); const isGrouped = true; const panel = this.panel; const lastShownEvent = this.lastShownEvent; const ret = []; - if (panel._wantsDateSeparator(this.prevEvent, this.events[0].getDate())) { + if (panel.wantsDateSeparator(this.prevEvent, this.events[0].getDate())) { const ts = this.events[0].getTs(); ret.push(
  • , @@ -1218,7 +1254,7 @@ class MemberGrouper { // of MemberEventListSummary, render each member event as if the previous // one was itself. This way, the timestamp of the previous event === the // timestamp of the current event, and no DateSeparator is inserted. - return panel._getTilesForEvent(e, e, e === lastShownEvent, isGrouped); + return panel.getTilesForEvent(e, e, e === lastShownEvent, isGrouped); }).reduce((a, b) => a.concat(b), []); if (eventTiles.length === 0) { @@ -1226,9 +1262,10 @@ class MemberGrouper { } ret.push( - { eventTiles } @@ -1242,7 +1279,7 @@ class MemberGrouper { return ret; } - getNewPrevEvent() { + public getNewPrevEvent(): MatrixEvent { return this.events[0]; } } diff --git a/src/components/structures/NotificationPanel.tsx b/src/components/structures/NotificationPanel.tsx index 6c22835447..8c8fab7ece 100644 --- a/src/components/structures/NotificationPanel.tsx +++ b/src/components/structures/NotificationPanel.tsx @@ -22,6 +22,7 @@ import BaseCard from "../views/right_panel/BaseCard"; import { replaceableComponent } from "../../utils/replaceableComponent"; import TimelinePanel from "./TimelinePanel"; import Spinner from "../views/elements/Spinner"; +import { TileShape } from "../views/rooms/EventTile"; interface IProps { onClose(): void; @@ -48,7 +49,7 @@ export default class NotificationPanel extends React.PureComponent { manageReadMarkers={false} timelineSet={timelineSet} showUrlPreview={false} - tileShape="notif" + tileShape={TileShape.Notif} empty={emptyState} alwaysShowTimestamps={true} /> diff --git a/src/components/structures/RoomDirectory.tsx b/src/components/structures/RoomDirectory.tsx index 1e0605f263..7770b32f04 100644 --- a/src/components/structures/RoomDirectory.tsx +++ b/src/components/structures/RoomDirectory.tsx @@ -207,9 +207,9 @@ export default class RoomDirectory extends React.Component { this.getMoreRooms(); }; - private getMoreRooms() { - if (this.state.selectedCommunityId) return Promise.resolve(); // no more rooms - if (!MatrixClientPeg.get()) return Promise.resolve(); + private getMoreRooms(): Promise { + if (this.state.selectedCommunityId) return Promise.resolve(false); // no more rooms + if (!MatrixClientPeg.get()) return Promise.resolve(false); this.setState({ loading: true, @@ -239,12 +239,12 @@ export default class RoomDirectory extends React.Component { // if the filter or server has changed since this request was sent, // throw away the result (don't even clear the busy flag // since we must still have a request in flight) - return; + return false; } if (this.unmounted) { // if we've been unmounted, we don't care either. - return; + return false; } if (this.state.filterString) { @@ -264,14 +264,13 @@ export default class RoomDirectory extends React.Component { filterString != this.state.filterString || roomServer != this.state.roomServer || nextBatch != this.nextBatch) { - // as above: we don't care about errors for old - // requests either - return; + // as above: we don't care about errors for old requests either + return false; } if (this.unmounted) { // if we've been unmounted, we don't care either. - return; + return false; } console.error("Failed to get publicRooms: %s", JSON.stringify(err)); diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index 885851e8e6..338da29875 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -1125,7 +1125,7 @@ export default class RoomView extends React.Component { } } - private onSearchResultsFillRequest = (backwards: boolean) => { + private onSearchResultsFillRequest = (backwards: boolean): Promise => { if (!backwards) { return Promise.resolve(false); } @@ -1291,7 +1291,7 @@ export default class RoomView extends React.Component { this.handleSearchResult(searchPromise); }; - private handleSearchResult(searchPromise: Promise) { + private handleSearchResult(searchPromise: Promise): Promise { // keep a record of the current search id, so that if the search terms // change before we get a response, we can ignore the results. const localSearchId = this.searchId; @@ -1304,7 +1304,7 @@ export default class RoomView extends React.Component { debuglog("search complete"); if (this.unmounted || !this.state.searching || this.searchId != localSearchId) { console.error("Discarding stale search results"); - return; + return false; } // postgres on synapse returns us precise details of the strings @@ -1336,6 +1336,7 @@ export default class RoomView extends React.Component { description: ((error && error.message) ? error.message : _t("Server may be unavailable, overloaded, or search timed out :(")), }); + return false; }).finally(() => { this.setState({ searchInProgress: false, diff --git a/src/components/structures/ScrollPanel.js b/src/components/structures/ScrollPanel.tsx similarity index 73% rename from src/components/structures/ScrollPanel.js rename to src/components/structures/ScrollPanel.tsx index f6e1530537..b8e0cdbc34 100644 --- a/src/components/structures/ScrollPanel.js +++ b/src/components/structures/ScrollPanel.tsx @@ -1,5 +1,5 @@ /* -Copyright 2015, 2016 OpenMarket Ltd +Copyright 2015 - 2021 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,17 +14,18 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, {createRef} from "react"; -import PropTypes from 'prop-types'; +import React, { createRef, CSSProperties, ReactNode, SyntheticEvent, KeyboardEvent } from "react"; + import Timer from '../../utils/Timer'; import AutoHideScrollbar from "./AutoHideScrollbar"; -import {replaceableComponent} from "../../utils/replaceableComponent"; -import {getKeyBindingsManager, RoomAction} from "../../KeyBindingsManager"; +import { replaceableComponent } from "../../utils/replaceableComponent"; +import { getKeyBindingsManager, RoomAction } from "../../KeyBindingsManager"; +import ResizeNotifier from "../../utils/ResizeNotifier"; const DEBUG_SCROLL = false; // The amount of extra scroll distance to allow prior to unfilling. -// See _getExcessHeight. +// See getExcessHeight. const UNPAGINATION_PADDING = 6000; // The number of milliseconds to debounce calls to onUnfillRequest, to prevent // many scroll events causing many unfilling requests. @@ -43,6 +44,75 @@ if (DEBUG_SCROLL) { debuglog = function() {}; } +interface IProps { + /* stickyBottom: if set to true, then once the user hits the bottom of + * the list, any new children added to the list will cause the list to + * scroll down to show the new element, rather than preserving the + * existing view. + */ + stickyBottom?: boolean; + + /* startAtBottom: if set to true, the view is assumed to start + * scrolled to the bottom. + * XXX: It's likely this is unnecessary and can be derived from + * stickyBottom, but I'm adding an extra parameter to ensure + * behaviour stays the same for other uses of ScrollPanel. + * If so, let's remove this parameter down the line. + */ + startAtBottom?: boolean; + + /* className: classnames to add to the top-level div + */ + className?: string; + + /* style: styles to add to the top-level div + */ + style?: CSSProperties; + + /* resizeNotifier: ResizeNotifier to know when middle column has changed size + */ + resizeNotifier?: ResizeNotifier; + + /* fixedChildren: allows for children to be passed which are rendered outside + * of the wrapper + */ + fixedChildren?: ReactNode; + + /* onFillRequest(backwards): a callback which is called on scroll when + * the user nears the start (backwards = true) or end (backwards = + * false) of the list. + * + * This should return a promise; no more calls will be made until the + * promise completes. + * + * The promise should resolve to true if there is more data to be + * retrieved in this direction (in which case onFillRequest may be + * called again immediately), or false if there is no more data in this + * directon (at this time) - which will stop the pagination cycle until + * the user scrolls again. + */ + onFillRequest?(backwards: boolean): Promise; + + /* onUnfillRequest(backwards): a callback which is called on scroll when + * there are children elements that are far out of view and could be removed + * without causing pagination to occur. + * + * This function should accept a boolean, which is true to indicate the back/top + * of the panel and false otherwise, and a scroll token, which refers to the + * first element to remove if removing from the front/bottom, and last element + * to remove if removing from the back/top. + */ + onUnfillRequest?(backwards: boolean, scrollToken: string): void; + + /* onScroll: a callback which is called whenever any scroll happens. + */ + onScroll?(event: Event): void; + + /* onUserScroll: callback which is called when the user interacts with the room timeline + */ + onUserScroll?(event: SyntheticEvent): void; +} + /* This component implements an intelligent scrolling list. * * It wraps a list of
  • children; when items are added to the start or end @@ -84,97 +154,54 @@ if (DEBUG_SCROLL) { * offset as normal. */ +export interface IScrollState { + stuckAtBottom: boolean; + trackedNode?: HTMLElement; + trackedScrollToken?: string; + bottomOffset?: number; + pixelOffset?: number; +} + +interface IPreventShrinkingState { + offsetFromBottom: number; + offsetNode: HTMLElement; +} + @replaceableComponent("structures.ScrollPanel") -export default class ScrollPanel extends React.Component { - static propTypes = { - /* stickyBottom: if set to true, then once the user hits the bottom of - * the list, any new children added to the list will cause the list to - * scroll down to show the new element, rather than preserving the - * existing view. - */ - stickyBottom: PropTypes.bool, - - /* startAtBottom: if set to true, the view is assumed to start - * scrolled to the bottom. - * XXX: It's likely this is unnecessary and can be derived from - * stickyBottom, but I'm adding an extra parameter to ensure - * behaviour stays the same for other uses of ScrollPanel. - * If so, let's remove this parameter down the line. - */ - startAtBottom: PropTypes.bool, - - /* onFillRequest(backwards): a callback which is called on scroll when - * the user nears the start (backwards = true) or end (backwards = - * false) of the list. - * - * This should return a promise; no more calls will be made until the - * promise completes. - * - * The promise should resolve to true if there is more data to be - * retrieved in this direction (in which case onFillRequest may be - * called again immediately), or false if there is no more data in this - * directon (at this time) - which will stop the pagination cycle until - * the user scrolls again. - */ - onFillRequest: PropTypes.func, - - /* onUnfillRequest(backwards): a callback which is called on scroll when - * there are children elements that are far out of view and could be removed - * without causing pagination to occur. - * - * This function should accept a boolean, which is true to indicate the back/top - * of the panel and false otherwise, and a scroll token, which refers to the - * first element to remove if removing from the front/bottom, and last element - * to remove if removing from the back/top. - */ - onUnfillRequest: PropTypes.func, - - /* onScroll: a callback which is called whenever any scroll happens. - */ - onScroll: PropTypes.func, - - /* onUserScroll: callback which is called when the user interacts with the room timeline - */ - onUserScroll: PropTypes.func, - - /* className: classnames to add to the top-level div - */ - className: PropTypes.string, - - /* style: styles to add to the top-level div - */ - style: PropTypes.object, - - /* resizeNotifier: ResizeNotifier to know when middle column has changed size - */ - resizeNotifier: PropTypes.object, - - /* fixedChildren: allows for children to be passed which are rendered outside - * of the wrapper - */ - fixedChildren: PropTypes.node, - }; - +export default class ScrollPanel extends React.Component { static defaultProps = { stickyBottom: true, startAtBottom: true, - onFillRequest: function(backwards) { return Promise.resolve(false); }, - onUnfillRequest: function(backwards, scrollToken) {}, + onFillRequest: function(backwards: boolean) { return Promise.resolve(false); }, + onUnfillRequest: function(backwards: boolean, scrollToken: string) {}, onScroll: function() {}, }; - constructor(props) { - super(props); + private readonly pendingFillRequests: Record<"b" | "f", boolean> = { + b: null, + f: null, + }; + private readonly itemlist = createRef(); + private unmounted = false; + private scrollTimeout: Timer; + private isFilling: boolean; + private fillRequestWhileRunning: boolean; + private scrollState: IScrollState; + private preventShrinkingState: IPreventShrinkingState; + private unfillDebouncer: NodeJS.Timeout; + private bottomGrowth: number; + private pages: number; + private heightUpdateInProgress: boolean; + private divScroll: HTMLDivElement; - this._pendingFillRequests = {b: null, f: null}; + constructor(props, context) { + super(props, context); if (this.props.resizeNotifier) { this.props.resizeNotifier.on("middlePanelResizedNoisy", this.onResize); } this.resetScrollState(); - - this._itemlist = createRef(); } componentDidMount() { @@ -203,18 +230,18 @@ export default class ScrollPanel extends React.Component { } } - onScroll = ev => { + private onScroll = ev => { // skip scroll events caused by resizing if (this.props.resizeNotifier && this.props.resizeNotifier.isResizing) return; - debuglog("onScroll", this._getScrollNode().scrollTop); - this._scrollTimeout.restart(); - this._saveScrollState(); + debuglog("onScroll", this.getScrollNode().scrollTop); + this.scrollTimeout.restart(); + this.saveScrollState(); this.updatePreventShrinking(); this.props.onScroll(ev); this.checkFillState(); }; - onResize = () => { + private onResize = () => { debuglog("onResize"); this.checkScroll(); // update preventShrinkingState if present @@ -225,11 +252,11 @@ export default class ScrollPanel extends React.Component { // after an update to the contents of the panel, check that the scroll is // where it ought to be, and set off pagination requests if necessary. - checkScroll = () => { + public checkScroll = () => { if (this.unmounted) { return; } - this._restoreSavedScrollState(); + this.restoreSavedScrollState(); this.checkFillState(); }; @@ -238,8 +265,8 @@ export default class ScrollPanel extends React.Component { // note that this is independent of the 'stuckAtBottom' state - it is simply // about whether the content is scrolled down right now, irrespective of // whether it will stay that way when the children update. - isAtBottom = () => { - const sn = this._getScrollNode(); + public isAtBottom = () => { + const sn = this.getScrollNode(); // fractional values (both too big and too small) // for scrollTop happen on certain browsers/platforms // when scrolled all the way down. E.g. Chrome 72 on debian. @@ -278,10 +305,10 @@ export default class ScrollPanel extends React.Component { // |#########| - | // |#########| | // `---------' - - _getExcessHeight(backwards) { - const sn = this._getScrollNode(); - const contentHeight = this._getMessagesHeight(); - const listHeight = this._getListHeight(); + private getExcessHeight(backwards: boolean): number { + const sn = this.getScrollNode(); + const contentHeight = this.getMessagesHeight(); + const listHeight = this.getListHeight(); const clippedHeight = contentHeight - listHeight; const unclippedScrollTop = sn.scrollTop + clippedHeight; @@ -293,13 +320,13 @@ export default class ScrollPanel extends React.Component { } // check the scroll state and send out backfill requests if necessary. - checkFillState = async (depth=0) => { + public checkFillState = async (depth = 0): Promise => { if (this.unmounted) { return; } const isFirstCall = depth === 0; - const sn = this._getScrollNode(); + const sn = this.getScrollNode(); // if there is less than a screenful of messages above or below the // viewport, try to get some more messages. @@ -330,17 +357,17 @@ export default class ScrollPanel extends React.Component { // do make a note when a new request comes in while already running one, // so we can trigger a new chain of calls once done. if (isFirstCall) { - if (this._isFilling) { - debuglog("_isFilling: not entering while request is ongoing, marking for a subsequent request"); - this._fillRequestWhileRunning = true; + if (this.isFilling) { + debuglog("isFilling: not entering while request is ongoing, marking for a subsequent request"); + this.fillRequestWhileRunning = true; return; } - debuglog("_isFilling: setting"); - this._isFilling = true; + debuglog("isFilling: setting"); + this.isFilling = true; } - const itemlist = this._itemlist.current; - const firstTile = itemlist && itemlist.firstElementChild; + const itemlist = this.itemlist.current; + const firstTile = itemlist && itemlist.firstElementChild as HTMLElement; const contentTop = firstTile && firstTile.offsetTop; const fillPromises = []; @@ -348,13 +375,13 @@ export default class ScrollPanel extends React.Component { // try backward filling if (!firstTile || (sn.scrollTop - contentTop) < sn.clientHeight) { // need to back-fill - fillPromises.push(this._maybeFill(depth, true)); + fillPromises.push(this.maybeFill(depth, true)); } // if scrollTop gets to 2 screens from the end (so 1 screen below viewport), // try forward filling if ((sn.scrollHeight - sn.scrollTop) < sn.clientHeight * 2) { // need to forward-fill - fillPromises.push(this._maybeFill(depth, false)); + fillPromises.push(this.maybeFill(depth, false)); } if (fillPromises.length) { @@ -365,26 +392,26 @@ export default class ScrollPanel extends React.Component { } } if (isFirstCall) { - debuglog("_isFilling: clearing"); - this._isFilling = false; + debuglog("isFilling: clearing"); + this.isFilling = false; } - if (this._fillRequestWhileRunning) { - this._fillRequestWhileRunning = false; + if (this.fillRequestWhileRunning) { + this.fillRequestWhileRunning = false; this.checkFillState(); } }; // check if unfilling is possible and send an unfill request if necessary - _checkUnfillState(backwards) { - let excessHeight = this._getExcessHeight(backwards); + private checkUnfillState(backwards: boolean): void { + let excessHeight = this.getExcessHeight(backwards); if (excessHeight <= 0) { return; } const origExcessHeight = excessHeight; - const tiles = this._itemlist.current.children; + const tiles = this.itemlist.current.children; // The scroll token of the first/last tile to be unpaginated let markerScrollToken = null; @@ -413,11 +440,11 @@ export default class ScrollPanel extends React.Component { if (markerScrollToken) { // Use a debouncer to prevent multiple unfill calls in quick succession // This is to make the unfilling process less aggressive - if (this._unfillDebouncer) { - clearTimeout(this._unfillDebouncer); + if (this.unfillDebouncer) { + clearTimeout(this.unfillDebouncer); } - this._unfillDebouncer = setTimeout(() => { - this._unfillDebouncer = null; + this.unfillDebouncer = setTimeout(() => { + this.unfillDebouncer = null; debuglog("unfilling now", backwards, origExcessHeight); this.props.onUnfillRequest(backwards, markerScrollToken); }, UNFILL_REQUEST_DEBOUNCE_MS); @@ -425,9 +452,9 @@ export default class ScrollPanel extends React.Component { } // check if there is already a pending fill request. If not, set one off. - _maybeFill(depth, backwards) { + private maybeFill(depth: number, backwards: boolean): Promise { const dir = backwards ? 'b' : 'f'; - if (this._pendingFillRequests[dir]) { + if (this.pendingFillRequests[dir]) { debuglog("Already a "+dir+" fill in progress - not starting another"); return; } @@ -436,7 +463,7 @@ export default class ScrollPanel extends React.Component { // onFillRequest can end up calling us recursively (via onScroll // events) so make sure we set this before firing off the call. - this._pendingFillRequests[dir] = true; + this.pendingFillRequests[dir] = true; // wait 1ms before paginating, because otherwise // this will block the scroll event handler for +700ms @@ -445,13 +472,13 @@ export default class ScrollPanel extends React.Component { return new Promise(resolve => setTimeout(resolve, 1)).then(() => { return this.props.onFillRequest(backwards); }).finally(() => { - this._pendingFillRequests[dir] = false; + this.pendingFillRequests[dir] = false; }).then((hasMoreResults) => { if (this.unmounted) { return; } // Unpaginate once filling is complete - this._checkUnfillState(!backwards); + this.checkUnfillState(!backwards); debuglog(""+dir+" fill complete; hasMoreResults:"+hasMoreResults); if (hasMoreResults) { @@ -477,7 +504,7 @@ export default class ScrollPanel extends React.Component { * the number of pixels the bottom of the tracked child is above the * bottom of the scroll panel. */ - getScrollState = () => this.scrollState; + public getScrollState = (): IScrollState => this.scrollState; /* reset the saved scroll state. * @@ -491,35 +518,35 @@ export default class ScrollPanel extends React.Component { * no use if no children exist yet, or if you are about to replace the * child list.) */ - resetScrollState = () => { + public resetScrollState = (): void => { this.scrollState = { stuckAtBottom: this.props.startAtBottom, }; - this._bottomGrowth = 0; - this._pages = 0; - this._scrollTimeout = new Timer(100); - this._heightUpdateInProgress = false; + this.bottomGrowth = 0; + this.pages = 0; + this.scrollTimeout = new Timer(100); + this.heightUpdateInProgress = false; }; /** * jump to the top of the content. */ - scrollToTop = () => { - this._getScrollNode().scrollTop = 0; - this._saveScrollState(); + public scrollToTop = (): void => { + this.getScrollNode().scrollTop = 0; + this.saveScrollState(); }; /** * jump to the bottom of the content. */ - scrollToBottom = () => { + public scrollToBottom = (): void => { // the easiest way to make sure that the scroll state is correctly // saved is to do the scroll, then save the updated state. (Calculating // it ourselves is hard, and we can't rely on an onScroll callback // happening, since there may be no user-visible change here). - const sn = this._getScrollNode(); + const sn = this.getScrollNode(); sn.scrollTop = sn.scrollHeight; - this._saveScrollState(); + this.saveScrollState(); }; /** @@ -527,18 +554,18 @@ export default class ScrollPanel extends React.Component { * * @param {number} mult: -1 to page up, +1 to page down */ - scrollRelative = mult => { - const scrollNode = this._getScrollNode(); + public scrollRelative = (mult: number): void => { + const scrollNode = this.getScrollNode(); const delta = mult * scrollNode.clientHeight * 0.9; scrollNode.scrollBy(0, delta); - this._saveScrollState(); + this.saveScrollState(); }; /** * Scroll up/down in response to a scroll key * @param {object} ev the keyboard event */ - handleScrollKey = ev => { + public handleScrollKey = (ev: KeyboardEvent) => { let isScrolling = false; const roomAction = getKeyBindingsManager().getRoomAction(ev); switch (roomAction) { @@ -575,17 +602,17 @@ export default class ScrollPanel extends React.Component { * node (specifically, the bottom of it) will be positioned. If omitted, it * defaults to 0. */ - scrollToToken = (scrollToken, pixelOffset, offsetBase) => { + public scrollToToken = (scrollToken: string, pixelOffset: number, offsetBase: number): void => { pixelOffset = pixelOffset || 0; offsetBase = offsetBase || 0; - // set the trackedScrollToken so we can get the node through _getTrackedNode + // set the trackedScrollToken so we can get the node through getTrackedNode this.scrollState = { stuckAtBottom: false, trackedScrollToken: scrollToken, }; - const trackedNode = this._getTrackedNode(); - const scrollNode = this._getScrollNode(); + const trackedNode = this.getTrackedNode(); + const scrollNode = this.getScrollNode(); if (trackedNode) { // set the scrollTop to the position we want. // note though, that this might not succeed if the combination of offsetBase and pixelOffset @@ -595,34 +622,34 @@ export default class ScrollPanel extends React.Component { // enough so it ends up in the top of the viewport. debuglog("scrollToken: setting scrollTop", {offsetBase, pixelOffset, offsetTop: trackedNode.offsetTop}); scrollNode.scrollTop = (trackedNode.offsetTop - (scrollNode.clientHeight * offsetBase)) + pixelOffset; - this._saveScrollState(); + this.saveScrollState(); } }; - _saveScrollState() { + private saveScrollState(): void { if (this.props.stickyBottom && this.isAtBottom()) { this.scrollState = { stuckAtBottom: true }; debuglog("saved stuckAtBottom state"); return; } - const scrollNode = this._getScrollNode(); + const scrollNode = this.getScrollNode(); const viewportBottom = scrollNode.scrollHeight - (scrollNode.scrollTop + scrollNode.clientHeight); - const itemlist = this._itemlist.current; + const itemlist = this.itemlist.current; const messages = itemlist.children; let node = null; // TODO: do a binary search here, as items are sorted by offsetTop // loop backwards, from bottom-most message (as that is the most common case) - for (let i = messages.length-1; i >= 0; --i) { - if (!messages[i].dataset.scrollTokens) { + for (let i = messages.length - 1; i >= 0; --i) { + if (!(messages[i] as HTMLElement).dataset.scrollTokens) { continue; } node = messages[i]; // break at the first message (coming from the bottom) // that has it's offsetTop above the bottom of the viewport. - if (this._topFromBottom(node) > viewportBottom) { + if (this.topFromBottom(node) > viewportBottom) { // Use this node as the scrollToken break; } @@ -634,7 +661,7 @@ export default class ScrollPanel extends React.Component { } const scrollToken = node.dataset.scrollTokens.split(',')[0]; debuglog("saving anchored scroll state to message", node && node.innerText, scrollToken); - const bottomOffset = this._topFromBottom(node); + const bottomOffset = this.topFromBottom(node); this.scrollState = { stuckAtBottom: false, trackedNode: node, @@ -644,35 +671,35 @@ export default class ScrollPanel extends React.Component { }; } - async _restoreSavedScrollState() { + private async restoreSavedScrollState(): Promise { const scrollState = this.scrollState; if (scrollState.stuckAtBottom) { - const sn = this._getScrollNode(); + const sn = this.getScrollNode(); if (sn.scrollTop !== sn.scrollHeight) { sn.scrollTop = sn.scrollHeight; } } else if (scrollState.trackedScrollToken) { - const itemlist = this._itemlist.current; - const trackedNode = this._getTrackedNode(); + const itemlist = this.itemlist.current; + const trackedNode = this.getTrackedNode(); if (trackedNode) { - const newBottomOffset = this._topFromBottom(trackedNode); + const newBottomOffset = this.topFromBottom(trackedNode); const bottomDiff = newBottomOffset - scrollState.bottomOffset; - this._bottomGrowth += bottomDiff; + this.bottomGrowth += bottomDiff; scrollState.bottomOffset = newBottomOffset; - const newHeight = `${this._getListHeight()}px`; + const newHeight = `${this.getListHeight()}px`; if (itemlist.style.height !== newHeight) { itemlist.style.height = newHeight; } debuglog("balancing height because messages below viewport grew by", bottomDiff); } } - if (!this._heightUpdateInProgress) { - this._heightUpdateInProgress = true; + if (!this.heightUpdateInProgress) { + this.heightUpdateInProgress = true; try { - await this._updateHeight(); + await this.updateHeight(); } finally { - this._heightUpdateInProgress = false; + this.heightUpdateInProgress = false; } } else { debuglog("not updating height because request already in progress"); @@ -680,11 +707,11 @@ export default class ScrollPanel extends React.Component { } // need a better name that also indicates this will change scrollTop? Rebalance height? Reveal content? - async _updateHeight() { + private async updateHeight(): Promise { // wait until user has stopped scrolling - if (this._scrollTimeout.isRunning()) { + if (this.scrollTimeout.isRunning()) { debuglog("updateHeight waiting for scrolling to end ... "); - await this._scrollTimeout.finished(); + await this.scrollTimeout.finished(); } else { debuglog("updateHeight getting straight to business, no scrolling going on."); } @@ -694,14 +721,14 @@ export default class ScrollPanel extends React.Component { return; } - const sn = this._getScrollNode(); - const itemlist = this._itemlist.current; - const contentHeight = this._getMessagesHeight(); + const sn = this.getScrollNode(); + const itemlist = this.itemlist.current; + const contentHeight = this.getMessagesHeight(); const minHeight = sn.clientHeight; const height = Math.max(minHeight, contentHeight); - this._pages = Math.ceil(height / PAGE_SIZE); - this._bottomGrowth = 0; - const newHeight = `${this._getListHeight()}px`; + this.pages = Math.ceil(height / PAGE_SIZE); + this.bottomGrowth = 0; + const newHeight = `${this.getListHeight()}px`; const scrollState = this.scrollState; if (scrollState.stuckAtBottom) { @@ -713,7 +740,7 @@ export default class ScrollPanel extends React.Component { } debuglog("updateHeight to", newHeight); } else if (scrollState.trackedScrollToken) { - const trackedNode = this._getTrackedNode(); + const trackedNode = this.getTrackedNode(); // if the timeline has been reloaded // this can be called before scrollToBottom or whatever has been called // so don't do anything if the node has disappeared from @@ -735,17 +762,17 @@ export default class ScrollPanel extends React.Component { } } - _getTrackedNode() { + private getTrackedNode(): HTMLElement { const scrollState = this.scrollState; const trackedNode = scrollState.trackedNode; if (!trackedNode || !trackedNode.parentElement) { let node; - const messages = this._itemlist.current.children; + const messages = this.itemlist.current.children; const scrollToken = scrollState.trackedScrollToken; for (let i = messages.length-1; i >= 0; --i) { - const m = messages[i]; + const m = messages[i] as HTMLElement; // 'data-scroll-tokens' is a DOMString of comma-separated scroll tokens // There might only be one scroll token if (m.dataset.scrollTokens && @@ -768,45 +795,45 @@ export default class ScrollPanel extends React.Component { return scrollState.trackedNode; } - _getListHeight() { - return this._bottomGrowth + (this._pages * PAGE_SIZE); + private getListHeight(): number { + return this.bottomGrowth + (this.pages * PAGE_SIZE); } - _getMessagesHeight() { - const itemlist = this._itemlist.current; - const lastNode = itemlist.lastElementChild; + private getMessagesHeight(): number { + const itemlist = this.itemlist.current; + const lastNode = itemlist.lastElementChild as HTMLElement; const lastNodeBottom = lastNode ? lastNode.offsetTop + lastNode.clientHeight : 0; - const firstNodeTop = itemlist.firstElementChild ? itemlist.firstElementChild.offsetTop : 0; + const firstNodeTop = itemlist.firstElementChild ? (itemlist.firstElementChild as HTMLElement).offsetTop : 0; // 18 is itemlist padding return lastNodeBottom - firstNodeTop + (18 * 2); } - _topFromBottom(node) { + private topFromBottom(node: HTMLElement): number { // current capped height - distance from top = distance from bottom of container to top of tracked element - return this._itemlist.current.clientHeight - node.offsetTop; + return this.itemlist.current.clientHeight - node.offsetTop; } /* get the DOM node which has the scrollTop property we care about for our * message panel. */ - _getScrollNode() { + private getScrollNode(): HTMLDivElement { if (this.unmounted) { // this shouldn't happen, but when it does, turn the NPE into // something more meaningful. - throw new Error("ScrollPanel._getScrollNode called when unmounted"); + throw new Error("ScrollPanel.getScrollNode called when unmounted"); } - if (!this._divScroll) { + if (!this.divScroll) { // Likewise, we should have the ref by this point, but if not // turn the NPE into something meaningful. - throw new Error("ScrollPanel._getScrollNode called before AutoHideScrollbar ref collected"); + throw new Error("ScrollPanel.getScrollNode called before AutoHideScrollbar ref collected"); } - return this._divScroll; + return this.divScroll; } - _collectScroll = divScroll => { - this._divScroll = divScroll; + private collectScroll = (divScroll: HTMLDivElement) => { + this.divScroll = divScroll; }; /** @@ -814,15 +841,15 @@ export default class ScrollPanel extends React.Component { anything below it changes, by calling updatePreventShrinking, to keep the same minimum bottom offset, effectively preventing the timeline to shrink. */ - preventShrinking = () => { - const messageList = this._itemlist.current; + public preventShrinking = (): void => { + const messageList = this.itemlist.current; const tiles = messageList && messageList.children; if (!messageList) { return; } let lastTileNode; for (let i = tiles.length - 1; i >= 0; i--) { - const node = tiles[i]; + const node = tiles[i] as HTMLElement; if (node.dataset.scrollTokens) { lastTileNode = node; break; @@ -841,8 +868,8 @@ export default class ScrollPanel extends React.Component { }; /** Clear shrinking prevention. Used internally, and when the timeline is reloaded. */ - clearPreventShrinking = () => { - const messageList = this._itemlist.current; + public clearPreventShrinking = (): void => { + const messageList = this.itemlist.current; const balanceElement = messageList && messageList.parentElement; if (balanceElement) balanceElement.style.paddingBottom = null; this.preventShrinkingState = null; @@ -857,11 +884,11 @@ export default class ScrollPanel extends React.Component { from the bottom of the marked tile grows larger than what it was when marking. */ - updatePreventShrinking = () => { + public updatePreventShrinking = (): void => { if (this.preventShrinkingState) { - const sn = this._getScrollNode(); + const sn = this.getScrollNode(); const scrollState = this.scrollState; - const messageList = this._itemlist.current; + const messageList = this.itemlist.current; const {offsetNode, offsetFromBottom} = this.preventShrinkingState; // element used to set paddingBottom to balance the typing notifs disappearing const balanceElement = messageList.parentElement; @@ -898,13 +925,15 @@ export default class ScrollPanel extends React.Component { // list-style-type: none; is no longer a list return ( + className={`mx_ScrollPanel ${this.props.className}`} + style={this.props.style} + > { this.props.fixedChildren }
    -
      +
        { this.props.children }
    diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.tsx similarity index 75% rename from src/components/structures/TimelinePanel.js rename to src/components/structures/TimelinePanel.tsx index 03d0b5c6d7..c2e7a6f346 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.tsx @@ -1,8 +1,5 @@ /* -Copyright 2016 OpenMarket Ltd -Copyright 2017 Vector Creations Ltd -Copyright 2019 New Vector Ltd -Copyright 2019-2020 The Matrix.org Foundation C.I.C. +Copyright 2016 - 2021 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -17,13 +14,16 @@ See the License for the specific language governing permissions and limitations under the License. */ -import SettingsStore from "../../settings/SettingsStore"; -import { LayoutPropType } from "../../settings/Layout"; -import React, { createRef } from 'react'; +import React, { createRef, ReactNode, SyntheticEvent } from 'react'; import ReactDOM from "react-dom"; -import PropTypes from 'prop-types'; +import { Room } from "matrix-js-sdk/src/models/room"; +import { MatrixEvent } from "matrix-js-sdk/src/models/event"; +import { TimelineSet } from "matrix-js-sdk/src/models/event-timeline-set"; import { EventTimeline } from "matrix-js-sdk/src/models/event-timeline"; import { TimelineWindow } from "matrix-js-sdk/src/timeline-window"; + +import SettingsStore from "../../settings/SettingsStore"; +import { Layout } from "../../settings/Layout"; import { _t } from '../../languageHandler'; import { MatrixClientPeg } from "../../MatrixClientPeg"; import RoomContext from "../../contexts/RoomContext"; @@ -35,11 +35,19 @@ import { Key } from '../../Keyboard'; import Timer from '../../utils/Timer'; import shouldHideEvent from '../../shouldHideEvent'; import EditorStateTransfer from '../../utils/EditorStateTransfer'; -import { haveTileForEvent } from "../views/rooms/EventTile"; +import { haveTileForEvent, TileShape } from "../views/rooms/EventTile"; import { UIFeature } from "../../settings/UIFeature"; import { replaceableComponent } from "../../utils/replaceableComponent"; import { arrayFastClone } from "../../utils/arrays"; import { Action } from "../../dispatcher/actions"; +import MessagePanel from "./MessagePanel"; +import { SyncState } from 'matrix-js-sdk/src/sync.api'; +import { IScrollState } from "./ScrollPanel"; +import { ActionPayload } from "../../dispatcher/payloads"; +import { EventType } from 'matrix-js-sdk/src/@types/event'; +import ResizeNotifier from "../../utils/ResizeNotifier"; +import { RoomPermalinkCreator } from "../../utils/permalinks/Permalinks"; +import Spinner from "../views/elements/Spinner"; const PAGINATE_SIZE = 20; const INITIAL_SIZE = 20; @@ -47,90 +55,159 @@ const READ_RECEIPT_INTERVAL_MS = 500; const DEBUG = false; -let debuglog = function() {}; +let debuglog = function(...s: any[]) {}; if (DEBUG) { // using bind means that we get to keep useful line numbers in the console debuglog = console.log.bind(console); } +interface IProps { + // The js-sdk EventTimelineSet object for the timeline sequence we are + // representing. This may or may not have a room, depending on what it's + // a timeline representing. If it has a room, we maintain RRs etc for + // that room. + timelineSet: TimelineSet; + showReadReceipts?: boolean; + // Enable managing RRs and RMs. These require the timelineSet to have a room. + manageReadReceipts?: boolean; + sendReadReceiptOnLoad?: boolean; + manageReadMarkers?: boolean; + + // true to give the component a 'display: none' style. + hidden?: boolean; + + // ID of an event to highlight. If undefined, no event will be highlighted. + // typically this will be either 'eventId' or undefined. + highlightedEventId?: string; + + // id of an event to jump to. If not given, will go to the end of the live timeline. + eventId?: string; + + // where to position the event given by eventId, in pixels from the bottom of the viewport. + // If not given, will try to put the event half way down the viewport. + eventPixelOffset?: number; + + // Should we show URL Previews + showUrlPreview?: boolean; + + // maximum number of events to show in a timeline + timelineCap?: number; + + // classname to use for the messagepanel + className?: string; + + // shape property to be passed to EventTiles + tileShape?: TileShape; + + // placeholder to use if the timeline is empty + empty?: ReactNode; + + // whether to show reactions for an event + showReactions?: boolean; + + // which layout to use + layout?: Layout; + + // whether to always show timestamps for an event + alwaysShowTimestamps?: boolean; + + resizeNotifier?: ResizeNotifier; + editState?: EditorStateTransfer; + permalinkCreator?: RoomPermalinkCreator; + membersLoaded?: boolean; + + // callback which is called when the panel is scrolled. + onScroll?(event: Event): void; + + // callback which is called when the user interacts with the room timeline + onUserScroll?(event: SyntheticEvent): void; + + // callback which is called when the read-up-to mark is updated. + onReadMarkerUpdated?(): void; + + // callback which is called when we wish to paginate the timeline window. + onPaginationRequest?(timelineWindow: TimelineWindow, direction: string, size: number): Promise, +} + +interface IState { + events: MatrixEvent[]; + liveEvents: MatrixEvent[]; + // track whether our room timeline is loading + timelineLoading: boolean; + + // the index of the first event that is to be shown + firstVisibleEventIndex: number; + + // canBackPaginate == false may mean: + // + // * we haven't (successfully) loaded the timeline yet, or: + // + // * we have got to the point where the room was created, or: + // + // * the server indicated that there were no more visible events + // (normally implying we got to the start of the room), or: + // + // * we gave up asking the server for more events + canBackPaginate: boolean; + + // canForwardPaginate == false may mean: + // + // * we haven't (successfully) loaded the timeline yet + // + // * we have got to the end of time and are now tracking the live + // timeline, or: + // + // * the server indicated that there were no more visible events + // (not sure if this ever happens when we're not at the live + // timeline), or: + // + // * we are looking at some historical point, but gave up asking + // the server for more events + canForwardPaginate: boolean; + + // start with the read-marker visible, so that we see its animated + // disappearance when switching into the room. + readMarkerVisible: boolean; + + readMarkerEventId: string; + + backPaginating: boolean; + forwardPaginating: boolean; + + // cache of matrixClient.getSyncState() (but from the 'sync' event) + clientSyncState: SyncState; + + // should the event tiles have twelve hour times + isTwelveHour: boolean; + + // always show timestamps on event tiles? + alwaysShowTimestamps: boolean; + + // how long to show the RM for when it's visible in the window + readMarkerInViewThresholdMs: number; + + // how long to show the RM for when it's scrolled off-screen + readMarkerOutOfViewThresholdMs: number; + + editState?: EditorStateTransfer; +} + +interface IEventIndexOpts { + ignoreOwn?: boolean; + allowPartial?: boolean; +} + /* * Component which shows the event timeline in a room view. * * Also responsible for handling and sending read receipts. */ @replaceableComponent("structures.TimelinePanel") -class TimelinePanel extends React.Component { - static propTypes = { - // The js-sdk EventTimelineSet object for the timeline sequence we are - // representing. This may or may not have a room, depending on what it's - // a timeline representing. If it has a room, we maintain RRs etc for - // that room. - timelineSet: PropTypes.object.isRequired, - - showReadReceipts: PropTypes.bool, - // Enable managing RRs and RMs. These require the timelineSet to have a room. - manageReadReceipts: PropTypes.bool, - sendReadReceiptOnLoad: PropTypes.bool, - manageReadMarkers: PropTypes.bool, - - // true to give the component a 'display: none' style. - hidden: PropTypes.bool, - - // ID of an event to highlight. If undefined, no event will be highlighted. - // typically this will be either 'eventId' or undefined. - highlightedEventId: PropTypes.string, - - // id of an event to jump to. If not given, will go to the end of the - // live timeline. - eventId: PropTypes.string, - - // where to position the event given by eventId, in pixels from the - // bottom of the viewport. If not given, will try to put the event - // half way down the viewport. - eventPixelOffset: PropTypes.number, - - // Should we show URL Previews - showUrlPreview: PropTypes.bool, - - // callback which is called when the panel is scrolled. - onScroll: PropTypes.func, - - // callback which is called when the user interacts with the room timeline - onUserScroll: PropTypes.func, - - // callback which is called when the read-up-to mark is updated. - onReadMarkerUpdated: PropTypes.func, - - // callback which is called when we wish to paginate the timeline - // window. - onPaginationRequest: PropTypes.func, - - // maximum number of events to show in a timeline - timelineCap: PropTypes.number, - - // classname to use for the messagepanel - className: PropTypes.string, - - // shape property to be passed to EventTiles - tileShape: PropTypes.string, - - // placeholder to use if the timeline is empty - empty: PropTypes.node, - - // whether to show reactions for an event - showReactions: PropTypes.bool, - - // which layout to use - layout: LayoutPropType, - - // whether to always show timestamps for an event - alwaysShowTimestamps: PropTypes.bool, - } - +class TimelinePanel extends React.Component { static contextType = RoomContext; // a map from room id to read marker event timestamp - static roomReadMarkerTsMap = {}; + static roomReadMarkerTsMap: Record = {}; static defaultProps = { // By default, disable the timelineCap in favour of unpaginating based on @@ -140,16 +217,21 @@ class TimelinePanel extends React.Component { sendReadReceiptOnLoad: true, }; - constructor(props) { - super(props); + private lastRRSentEventId: string = undefined; + private lastRMSentEventId: string = undefined; + + private readonly messagePanel = createRef(); + private readonly dispatcherRef: string; + private timelineWindow?: TimelineWindow; + private unmounted = false; + private readReceiptActivityTimer: Timer; + private readMarkerActivityTimer: Timer; + + constructor(props, context) { + super(props, context); debuglog("TimelinePanel: mounting"); - this.lastRRSentEventId = undefined; - this.lastRMSentEventId = undefined; - - this._messagePanel = createRef(); - // XXX: we could track RM per TimelineSet rather than per Room. // but for now we just do it per room for simplicity. let initialReadMarker = null; @@ -158,82 +240,41 @@ class TimelinePanel extends React.Component { if (readmarker) { initialReadMarker = readmarker.getContent().event_id; } else { - initialReadMarker = this._getCurrentReadReceipt(); + initialReadMarker = this.getCurrentReadReceipt(); } } this.state = { events: [], liveEvents: [], - timelineLoading: true, // track whether our room timeline is loading - - // the index of the first event that is to be shown + timelineLoading: true, firstVisibleEventIndex: 0, - - // canBackPaginate == false may mean: - // - // * we haven't (successfully) loaded the timeline yet, or: - // - // * we have got to the point where the room was created, or: - // - // * the server indicated that there were no more visible events - // (normally implying we got to the start of the room), or: - // - // * we gave up asking the server for more events canBackPaginate: false, - - // canForwardPaginate == false may mean: - // - // * we haven't (successfully) loaded the timeline yet - // - // * we have got to the end of time and are now tracking the live - // timeline, or: - // - // * the server indicated that there were no more visible events - // (not sure if this ever happens when we're not at the live - // timeline), or: - // - // * we are looking at some historical point, but gave up asking - // the server for more events canForwardPaginate: false, - - // start with the read-marker visible, so that we see its animated - // disappearance when switching into the room. readMarkerVisible: true, - readMarkerEventId: initialReadMarker, - backPaginating: false, forwardPaginating: false, - - // cache of matrixClient.getSyncState() (but from the 'sync' event) clientSyncState: MatrixClientPeg.get().getSyncState(), - - // should the event tiles have twelve hour times isTwelveHour: SettingsStore.getValue("showTwelveHourTimestamps"), - - // always show timestamps on event tiles? alwaysShowTimestamps: SettingsStore.getValue("alwaysShowTimestamps"), - - // how long to show the RM for when it's visible in the window readMarkerInViewThresholdMs: SettingsStore.getValue("readMarkerInViewThresholdMs"), - - // how long to show the RM for when it's scrolled off-screen readMarkerOutOfViewThresholdMs: SettingsStore.getValue("readMarkerOutOfViewThresholdMs"), }; this.dispatcherRef = dis.register(this.onAction); - MatrixClientPeg.get().on("Room.timeline", this.onRoomTimeline); - MatrixClientPeg.get().on("Room.timelineReset", this.onRoomTimelineReset); - MatrixClientPeg.get().on("Room.redaction", this.onRoomRedaction); + const cli = MatrixClientPeg.get(); + cli.on("Room.timeline", this.onRoomTimeline); + cli.on("Room.timelineReset", this.onRoomTimelineReset); + cli.on("Room.redaction", this.onRoomRedaction); // same event handler as Room.redaction as for both we just do forceUpdate - MatrixClientPeg.get().on("Room.redactionCancelled", this.onRoomRedaction); - MatrixClientPeg.get().on("Room.receipt", this.onRoomReceipt); - MatrixClientPeg.get().on("Room.localEchoUpdated", this.onLocalEchoUpdated); - MatrixClientPeg.get().on("Room.accountData", this.onAccountData); - MatrixClientPeg.get().on("Event.decrypted", this.onEventDecrypted); - MatrixClientPeg.get().on("Event.replaced", this.onEventReplaced); - MatrixClientPeg.get().on("sync", this.onSync); + cli.on("Room.redactionCancelled", this.onRoomRedaction); + cli.on("Room.receipt", this.onRoomReceipt); + cli.on("Room.localEchoUpdated", this.onLocalEchoUpdated); + cli.on("Room.accountData", this.onAccountData); + cli.on("Event.decrypted", this.onEventDecrypted); + cli.on("Event.replaced", this.onEventReplaced); + cli.on("sync", this.onSync); } // TODO: [REACT-WARNING] Move into constructor @@ -246,7 +287,7 @@ class TimelinePanel extends React.Component { this.updateReadMarkerOnUserActivity(); } - this._initTimeline(this.props); + this.initTimeline(this.props); } // TODO: [REACT-WARNING] Replace with appropriate lifecycle event @@ -272,7 +313,7 @@ class TimelinePanel extends React.Component { if (differentEventId || differentHighlightedEventId) { console.log("TimelinePanel switching to eventId " + newProps.eventId + " (was " + this.props.eventId + ")"); - return this._initTimeline(newProps); + return this.initTimeline(newProps); } } @@ -282,13 +323,13 @@ class TimelinePanel extends React.Component { // // (We could use isMounted, but facebook have deprecated that.) this.unmounted = true; - if (this._readReceiptActivityTimer) { - this._readReceiptActivityTimer.abort(); - this._readReceiptActivityTimer = null; + if (this.readReceiptActivityTimer) { + this.readReceiptActivityTimer.abort(); + this.readReceiptActivityTimer = null; } - if (this._readMarkerActivityTimer) { - this._readMarkerActivityTimer.abort(); - this._readMarkerActivityTimer = null; + if (this.readMarkerActivityTimer) { + this.readMarkerActivityTimer.abort(); + this.readMarkerActivityTimer = null; } dis.unregister(this.dispatcherRef); @@ -308,7 +349,7 @@ class TimelinePanel extends React.Component { } } - onMessageListUnfillRequest = (backwards, scrollToken) => { + private onMessageListUnfillRequest = (backwards: boolean, scrollToken: string): void => { // If backwards, unpaginate from the back (i.e. the start of the timeline) const dir = backwards ? EventTimeline.BACKWARDS : EventTimeline.FORWARDS; debuglog("TimelinePanel: unpaginating events in direction", dir); @@ -327,21 +368,30 @@ class TimelinePanel extends React.Component { if (count > 0) { debuglog("TimelinePanel: Unpaginating", count, "in direction", dir); - this._timelineWindow.unpaginate(count, backwards); + this.timelineWindow.unpaginate(count, backwards); - // We can now paginate in the unpaginated direction - const canPaginateKey = (backwards) ? 'canBackPaginate' : 'canForwardPaginate'; - const { events, liveEvents, firstVisibleEventIndex } = this._getEvents(); - this.setState({ - [canPaginateKey]: true, + const { events, liveEvents, firstVisibleEventIndex } = this.getEvents(); + const newState: Partial = { events, liveEvents, firstVisibleEventIndex, - }); + } + + // We can now paginate in the unpaginated direction + if (backwards) { + newState.canBackPaginate = true; + } else { + newState.canForwardPaginate = true; + } + this.setState(newState); } }; - onPaginationRequest = (timelineWindow, direction, size) => { + private onPaginationRequest = ( + timelineWindow: TimelineWindow, + direction: string, + size: number, + ): Promise => { if (this.props.onPaginationRequest) { return this.props.onPaginationRequest(timelineWindow, direction, size); } else { @@ -350,8 +400,8 @@ class TimelinePanel extends React.Component { }; // set off a pagination request. - onMessageListFillRequest = backwards => { - if (!this._shouldPaginate()) return Promise.resolve(false); + private onMessageListFillRequest = (backwards: boolean): Promise => { + if (!this.shouldPaginate()) return Promise.resolve(false); const dir = backwards ? EventTimeline.BACKWARDS : EventTimeline.FORWARDS; const canPaginateKey = backwards ? 'canBackPaginate' : 'canForwardPaginate'; @@ -362,9 +412,9 @@ class TimelinePanel extends React.Component { return Promise.resolve(false); } - if (!this._timelineWindow.canPaginate(dir)) { + if (!this.timelineWindow.canPaginate(dir)) { debuglog("TimelinePanel: can't", dir, "paginate any further"); - this.setState({[canPaginateKey]: false}); + this.setState({ [canPaginateKey]: false }); return Promise.resolve(false); } @@ -374,15 +424,15 @@ class TimelinePanel extends React.Component { } debuglog("TimelinePanel: Initiating paginate; backwards:"+backwards); - this.setState({[paginatingKey]: true}); + this.setState({ [paginatingKey]: true }); - return this.onPaginationRequest(this._timelineWindow, dir, PAGINATE_SIZE).then((r) => { + return this.onPaginationRequest(this.timelineWindow, dir, PAGINATE_SIZE).then((r) => { if (this.unmounted) { return; } debuglog("TimelinePanel: paginate complete backwards:"+backwards+"; success:"+r); - const { events, liveEvents, firstVisibleEventIndex } = this._getEvents(); - const newState = { + const { events, liveEvents, firstVisibleEventIndex } = this.getEvents(); + const newState: Partial = { [paginatingKey]: false, [canPaginateKey]: r, events, @@ -395,7 +445,7 @@ class TimelinePanel extends React.Component { const otherDirection = backwards ? EventTimeline.FORWARDS : EventTimeline.BACKWARDS; const canPaginateOtherWayKey = backwards ? 'canForwardPaginate' : 'canBackPaginate'; if (!this.state[canPaginateOtherWayKey] && - this._timelineWindow.canPaginate(otherDirection)) { + this.timelineWindow.canPaginate(otherDirection)) { debuglog('TimelinePanel: can now', otherDirection, 'paginate again'); newState[canPaginateOtherWayKey] = true; } @@ -406,9 +456,9 @@ class TimelinePanel extends React.Component { // has in memory because we never gave the component a chance to scroll // itself into the right place return new Promise((resolve) => { - this.setState(newState, () => { + this.setState(newState, () => { // we can continue paginating in the given direction if: - // - _timelineWindow.paginate says we can + // - timelineWindow.paginate says we can // - we're paginating forwards, or we won't be trying to // paginate backwards past the first visible event resolve(r && (!backwards || firstVisibleEventIndex === 0)); @@ -417,7 +467,7 @@ class TimelinePanel extends React.Component { }); }; - onMessageListScroll = e => { + private onMessageListScroll = e => { if (this.props.onScroll) { this.props.onScroll(e); } @@ -428,18 +478,18 @@ class TimelinePanel extends React.Component { // it goes back off the top of the screen (presumably because the user // clicks on the 'jump to bottom' button), we need to re-enable it. if (rmPosition < 0) { - this.setState({readMarkerVisible: true}); + this.setState({ readMarkerVisible: true }); } // if read marker position goes between 0 and -1/1, // (and user is active), switch timeout - const timeout = this._readMarkerTimeout(rmPosition); + const timeout = this.readMarkerTimeout(rmPosition); // NO-OP when timeout already has set to the given value - this._readMarkerActivityTimer.changeTimeout(timeout); + this.readMarkerActivityTimer.changeTimeout(timeout); } }; - onAction = payload => { + private onAction = (payload: ActionPayload): void => { switch (payload.action) { case "ignore_state_changed": this.forceUpdate(); @@ -447,9 +497,9 @@ class TimelinePanel extends React.Component { case "edit_event": { const editState = payload.event ? new EditorStateTransfer(payload.event) : null; - this.setState({editState}, () => { - if (payload.event && this._messagePanel.current) { - this._messagePanel.current.scrollToEventIfNeeded( + this.setState({ editState }, () => { + if (payload.event && this.messagePanel.current) { + this.messagePanel.current.scrollToEventIfNeeded( payload.event.getId(), ); } @@ -479,7 +529,16 @@ class TimelinePanel extends React.Component { } }; - onRoomTimeline = (ev, room, toStartOfTimeline, removed, data) => { + private onRoomTimeline = ( + ev: MatrixEvent, + room: Room, + toStartOfTimeline: boolean, + removed: boolean, + data: { + timeline: EventTimeline; + liveEvent?: boolean; + }, + ): void => { // ignore events for other timeline sets if (data.timeline.getTimelineSet() !== this.props.timelineSet) return; @@ -487,9 +546,9 @@ class TimelinePanel extends React.Component { // updates from pagination will happen when the paginate completes. if (toStartOfTimeline || !data || !data.liveEvent) return; - if (!this._messagePanel.current) return; + if (!this.messagePanel.current) return; - if (!this._messagePanel.current.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. @@ -506,13 +565,13 @@ class TimelinePanel extends React.Component { // timeline window. // // see https://github.com/vector-im/vector-web/issues/1035 - this._timelineWindow.paginate(EventTimeline.FORWARDS, 1, false).then(() => { + this.timelineWindow.paginate(EventTimeline.FORWARDS, 1, false).then(() => { if (this.unmounted) { return; } - const { events, liveEvents, firstVisibleEventIndex } = this._getEvents(); + const { events, liveEvents, firstVisibleEventIndex } = this.getEvents(); const lastLiveEvent = liveEvents[liveEvents.length - 1]; - const updatedState = { + const updatedState: Partial = { events, liveEvents, firstVisibleEventIndex, @@ -537,15 +596,15 @@ class TimelinePanel extends React.Component { // we know we're stuckAtBottom, so we can advance the RM // immediately, to save a later render cycle - this._setReadMarker(lastLiveEvent.getId(), lastLiveEvent.getTs(), true); + this.setReadMarker(lastLiveEvent.getId(), lastLiveEvent.getTs(), true); updatedState.readMarkerVisible = false; updatedState.readMarkerEventId = lastLiveEvent.getId(); callRMUpdated = true; } } - this.setState(updatedState, () => { - this._messagePanel.current.updateTimelineMinHeight(); + this.setState(updatedState, () => { + this.messagePanel.current.updateTimelineMinHeight(); if (callRMUpdated) { this.props.onReadMarkerUpdated(); } @@ -553,17 +612,17 @@ class TimelinePanel extends React.Component { }); }; - onRoomTimelineReset = (room, timelineSet) => { + private onRoomTimelineReset = (room: Room, timelineSet: TimelineSet): void => { if (timelineSet !== this.props.timelineSet) return; - if (this._messagePanel.current && this._messagePanel.current.isAtBottom()) { - this._loadTimeline(); + if (this.messagePanel.current && this.messagePanel.current.isAtBottom()) { + this.loadTimeline(); } }; - canResetTimeline = () => this._messagePanel.current && this._messagePanel.current.isAtBottom(); + public canResetTimeline = () => this.messagePanel?.current.isAtBottom(); - onRoomRedaction = (ev, room) => { + private onRoomRedaction = (ev: MatrixEvent, room: Room): void => { if (this.unmounted) return; // ignore events for other rooms @@ -574,7 +633,7 @@ class TimelinePanel extends React.Component { this.forceUpdate(); }; - onEventReplaced = (replacedEvent, room) => { + private onEventReplaced = (replacedEvent: MatrixEvent, room: Room): void => { if (this.unmounted) return; // ignore events for other rooms @@ -585,7 +644,7 @@ class TimelinePanel extends React.Component { this.forceUpdate(); }; - onRoomReceipt = (ev, room) => { + private onRoomReceipt = (ev: MatrixEvent, room: Room): void => { if (this.unmounted) return; // ignore events for other rooms @@ -594,22 +653,22 @@ class TimelinePanel extends React.Component { this.forceUpdate(); }; - onLocalEchoUpdated = (ev, room, oldEventId) => { + private onLocalEchoUpdated = (ev: MatrixEvent, room: Room, oldEventId: string): void => { if (this.unmounted) return; // ignore events for other rooms if (room !== this.props.timelineSet.room) return; - this._reloadEvents(); + this.reloadEvents(); }; - onAccountData = (ev, room) => { + private onAccountData = (ev: MatrixEvent, room: Room): void => { if (this.unmounted) return; // ignore events for other rooms if (room !== this.props.timelineSet.room) return; - if (ev.getType() !== "m.fully_read") return; + if (ev.getType() !== EventType.FullyRead) return; // XXX: roomReadMarkerTsMap not updated here so it is now inconsistent. Replace // this mechanism of determining where the RM is relative to the view-port with @@ -619,7 +678,7 @@ class TimelinePanel extends React.Component { }, this.props.onReadMarkerUpdated); }; - onEventDecrypted = ev => { + private onEventDecrypted = (ev: MatrixEvent): void => { // Can be null for the notification timeline, etc. if (!this.props.timelineSet.room) return; @@ -634,46 +693,46 @@ class TimelinePanel extends React.Component { } }; - onSync = (state, prevState, data) => { - this.setState({clientSyncState: state}); + private onSync = (clientSyncState: SyncState, prevState: SyncState, data: object): void => { + this.setState({ clientSyncState }); }; - _readMarkerTimeout(readMarkerPosition) { + private readMarkerTimeout(readMarkerPosition: number): number { return readMarkerPosition === 0 ? this.state.readMarkerInViewThresholdMs : this.state.readMarkerOutOfViewThresholdMs; } - async updateReadMarkerOnUserActivity() { - const initialTimeout = this._readMarkerTimeout(this.getReadMarkerPosition()); - this._readMarkerActivityTimer = new Timer(initialTimeout); + private async updateReadMarkerOnUserActivity(): Promise { + const initialTimeout = this.readMarkerTimeout(this.getReadMarkerPosition()); + this.readMarkerActivityTimer = new Timer(initialTimeout); - while (this._readMarkerActivityTimer) { //unset on unmount - UserActivity.sharedInstance().timeWhileActiveRecently(this._readMarkerActivityTimer); + while (this.readMarkerActivityTimer) { //unset on unmount + UserActivity.sharedInstance().timeWhileActiveRecently(this.readMarkerActivityTimer); try { - await this._readMarkerActivityTimer.finished(); + await this.readMarkerActivityTimer.finished(); } catch (e) { continue; /* aborted */ } // outside of try/catch to not swallow errors this.updateReadMarker(); } } - async updateReadReceiptOnUserActivity() { - this._readReceiptActivityTimer = new Timer(READ_RECEIPT_INTERVAL_MS); - while (this._readReceiptActivityTimer) { //unset on unmount - UserActivity.sharedInstance().timeWhileActiveNow(this._readReceiptActivityTimer); + private async updateReadReceiptOnUserActivity(): Promise { + this.readReceiptActivityTimer = new Timer(READ_RECEIPT_INTERVAL_MS); + while (this.readReceiptActivityTimer) { //unset on unmount + UserActivity.sharedInstance().timeWhileActiveNow(this.readReceiptActivityTimer); try { - await this._readReceiptActivityTimer.finished(); + await this.readReceiptActivityTimer.finished(); } catch (e) { continue; /* aborted */ } // outside of try/catch to not swallow errors this.sendReadReceipt(); } } - sendReadReceipt = () => { + private sendReadReceipt = (): void => { if (SettingsStore.getValue("lowBandwidth")) return; - if (!this._messagePanel.current) 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 @@ -684,8 +743,8 @@ class TimelinePanel extends React.Component { let shouldSendRR = true; - const currentRREventId = this._getCurrentReadReceipt(true); - const currentRREventIndex = this._indexForEventId(currentRREventId); + const currentRREventId = this.getCurrentReadReceipt(true); + const currentRREventIndex = this.indexForEventId(currentRREventId); // We want to avoid sending out read receipts when we are looking at // events in the past which are before the latest RR. // @@ -700,11 +759,11 @@ class TimelinePanel extends React.Component { // the user eventually hits the live timeline. // if (currentRREventId && currentRREventIndex === null && - this._timelineWindow.canPaginate(EventTimeline.FORWARDS)) { + this.timelineWindow.canPaginate(EventTimeline.FORWARDS)) { shouldSendRR = false; } - const lastReadEventIndex = this._getLastDisplayedEventIndex({ + const lastReadEventIndex = this.getLastDisplayedEventIndex({ ignoreOwn: true, }); if (lastReadEventIndex === null) { @@ -778,7 +837,7 @@ class TimelinePanel extends React.Component { // if the read marker is on the screen, we can now assume we've caught up to the end // of the screen, so move the marker down to the bottom of the screen. - updateReadMarker = () => { + private updateReadMarker = (): void => { if (!this.props.manageReadMarkers) return; if (this.getReadMarkerPosition() === 1) { // the read marker is at an event below the viewport, @@ -788,7 +847,7 @@ class TimelinePanel extends React.Component { // move the RM to *after* the message at the bottom of the screen. This // avoids a problem whereby we never advance the RM if there is a huge // message which doesn't fit on the screen. - const lastDisplayedIndex = this._getLastDisplayedEventIndex({ + const lastDisplayedIndex = this.getLastDisplayedEventIndex({ allowPartial: true, }); @@ -796,7 +855,7 @@ class TimelinePanel extends React.Component { return; } const lastDisplayedEvent = this.state.events[lastDisplayedIndex]; - this._setReadMarker( + this.setReadMarker( lastDisplayedEvent.getId(), lastDisplayedEvent.getTs(), ); @@ -815,13 +874,13 @@ class TimelinePanel extends React.Component { // advance the read marker past any events we sent ourselves. - _advanceReadMarkerPastMyEvents() { + private advanceReadMarkerPastMyEvents(): void { if (!this.props.manageReadMarkers) return; - // we call `_timelineWindow.getEvents()` rather than using + // we call `timelineWindow.getEvents()` rather than using // `this.state.liveEvents`, because React batches the update to the // latter, so it may not have been updated yet. - const events = this._timelineWindow.getEvents(); + const events = this.timelineWindow.getEvents(); // first find where the current RM is let i; @@ -846,22 +905,22 @@ class TimelinePanel extends React.Component { i--; const ev = events[i]; - this._setReadMarker(ev.getId(), ev.getTs()); + this.setReadMarker(ev.getId(), ev.getTs()); } /* jump down to the bottom of this room, where new events are arriving */ - jumpToLiveTimeline = () => { + public jumpToLiveTimeline = (): void => { // if we can't forward-paginate the existing timeline, then there // is no point reloading it - just jump straight to the bottom. // // Otherwise, reload the timeline rather than trying to paginate // through all of space-time. - if (this._timelineWindow.canPaginate(EventTimeline.FORWARDS)) { - this._loadTimeline(); + if (this.timelineWindow.canPaginate(EventTimeline.FORWARDS)) { + this.loadTimeline(); } else { - if (this._messagePanel.current) { - this._messagePanel.current.scrollToBottom(); + if (this.messagePanel.current) { + this.messagePanel.current.scrollToBottom(); } } }; @@ -869,22 +928,22 @@ class TimelinePanel extends React.Component { /* scroll to show the read-up-to marker. We put it 1/3 of the way down * the container. */ - jumpToReadMarker = () => { + public jumpToReadMarker = (): void => { if (!this.props.manageReadMarkers) return; - if (!this._messagePanel.current) return; + if (!this.messagePanel.current) return; if (!this.state.readMarkerEventId) return; // we may not have loaded the event corresponding to the read-marker - // into the _timelineWindow. In that case, attempts to scroll to it + // into the timelineWindow. In that case, attempts to scroll to it // will fail. // // 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._messagePanel.current.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._messagePanel.current.scrollToEvent(this.state.readMarkerEventId, + this.messagePanel.current.scrollToEvent(this.state.readMarkerEventId, 0, 1/3); return; } @@ -892,15 +951,15 @@ class TimelinePanel extends React.Component { // Looks like we haven't loaded the event corresponding to the read-marker. // As with jumpToLiveTimeline, we want to reload the timeline around the // read-marker. - this._loadTimeline(this.state.readMarkerEventId, 0, 1/3); + this.loadTimeline(this.state.readMarkerEventId, 0, 1/3); }; /* update the read-up-to marker to match the read receipt */ - forgetReadMarker = () => { + public forgetReadMarker = (): void => { if (!this.props.manageReadMarkers) return; - const rmId = this._getCurrentReadReceipt(); + const rmId = this.getCurrentReadReceipt(); // see if we know the timestamp for the rr event const tl = this.props.timelineSet.getTimelineForEvent(rmId); @@ -912,17 +971,17 @@ class TimelinePanel extends React.Component { } } - this._setReadMarker(rmId, rmTs); + this.setReadMarker(rmId, rmTs); }; /* return true if the content is fully scrolled down and we are * at the end of the live timeline. */ - isAtEndOfLiveTimeline = () => { - return this._messagePanel.current - && this._messagePanel.current.isAtBottom() - && this._timelineWindow - && !this._timelineWindow.canPaginate(EventTimeline.FORWARDS); + public isAtEndOfLiveTimeline = (): boolean => { + return this.messagePanel.current + && this.messagePanel.current.isAtBottom() + && this.timelineWindow + && !this.timelineWindow.canPaginate(EventTimeline.FORWARDS); } @@ -931,9 +990,9 @@ class TimelinePanel extends React.Component { * * returns null if we are not mounted. */ - getScrollState = () => { - if (!this._messagePanel.current) { return null; } - return this._messagePanel.current.getScrollState(); + public getScrollState = (): IScrollState => { + if (!this.messagePanel.current) { return null; } + return this.messagePanel.current.getScrollState(); }; // returns one of: @@ -942,11 +1001,11 @@ class TimelinePanel extends React.Component { // -1: read marker is above the window // 0: read marker is visible // +1: read marker is below the window - getReadMarkerPosition = () => { + public getReadMarkerPosition = (): number => { if (!this.props.manageReadMarkers) return null; - if (!this._messagePanel.current) return null; + if (!this.messagePanel.current) return null; - const ret = this._messagePanel.current.getReadMarkerPosition(); + const ret = this.messagePanel.current.getReadMarkerPosition(); if (ret !== null) { return ret; } @@ -965,7 +1024,7 @@ class TimelinePanel extends React.Component { return null; }; - canJumpToReadMarker = () => { + public canJumpToReadMarker = (): boolean => { // 1. Do not show jump bar if neither the RM nor the RR are set. // 3. We want to show the bar if the read-marker is off the top of the screen. // 4. Also, if pos === null, the event might not be paginated - show the unread bar @@ -980,19 +1039,19 @@ class TimelinePanel extends React.Component { * * We pass it down to the scroll panel. */ - handleScrollKey = ev => { - if (!this._messagePanel.current) { return; } + public handleScrollKey = ev => { + if (!this.messagePanel.current) { return; } // jump to the live timeline on ctrl-end, rather than the end of the // timeline window. if (ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey && ev.key === Key.END) { this.jumpToLiveTimeline(); } else { - this._messagePanel.current.handleScrollKey(ev); + this.messagePanel.current.handleScrollKey(ev); } }; - _initTimeline(props) { + private initTimeline(props: IProps): void { const initialEvent = props.eventId; const pixelOffset = props.eventPixelOffset; @@ -1003,7 +1062,7 @@ class TimelinePanel extends React.Component { offsetBase = 0.5; } - return this._loadTimeline(initialEvent, pixelOffset, offsetBase); + return this.loadTimeline(initialEvent, pixelOffset, offsetBase); } /** @@ -1019,34 +1078,32 @@ class TimelinePanel extends React.Component { * @param {number?} offsetBase the reference point for the pixelOffset. 0 * means the top of the container, 1 means the bottom, and fractional * values mean somewhere in the middle. If omitted, it defaults to 0. - * - * returns a promise which will resolve when the load completes. */ - _loadTimeline(eventId, pixelOffset, offsetBase) { - this._timelineWindow = new TimelineWindow( + private loadTimeline(eventId?: string, pixelOffset?: number, offsetBase?: number): void { + this.timelineWindow = new TimelineWindow( MatrixClientPeg.get(), this.props.timelineSet, {windowLimit: this.props.timelineCap}); const onLoaded = () => { // clear the timeline min-height when // (re)loading the timeline - if (this._messagePanel.current) { - this._messagePanel.current.onTimelineReset(); + if (this.messagePanel.current) { + this.messagePanel.current.onTimelineReset(); } - this._reloadEvents(); + this.reloadEvents(); // If we switched away from the room while there were pending // outgoing events, the read-marker will be before those events. // We need to skip over any which have subsequently been sent. - this._advanceReadMarkerPastMyEvents(); + this.advanceReadMarkerPastMyEvents(); this.setState({ - canBackPaginate: this._timelineWindow.canPaginate(EventTimeline.BACKWARDS), - canForwardPaginate: this._timelineWindow.canPaginate(EventTimeline.FORWARDS), + canBackPaginate: this.timelineWindow.canPaginate(EventTimeline.BACKWARDS), + canForwardPaginate: this.timelineWindow.canPaginate(EventTimeline.FORWARDS), timelineLoading: false, }, () => { // initialise the scroll state of the message panel - if (!this._messagePanel.current) { + 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 @@ -1056,10 +1113,10 @@ class TimelinePanel extends React.Component { return; } if (eventId) { - this._messagePanel.current.scrollToEvent(eventId, pixelOffset, + this.messagePanel.current.scrollToEvent(eventId, pixelOffset, offsetBase); } else { - this._messagePanel.current.scrollToBottom(); + this.messagePanel.current.scrollToBottom(); } if (this.props.sendReadReceiptOnLoad) { @@ -1121,10 +1178,10 @@ class TimelinePanel extends React.Component { if (timeline) { // This is a hot-path optimization by skipping a promise tick // by repeating a no-op sync branch in TimelineSet.getTimelineForEvent & MatrixClient.getEventTimeline - this._timelineWindow.load(eventId, INITIAL_SIZE); // in this branch this method will happen in sync time + this.timelineWindow.load(eventId, INITIAL_SIZE); // in this branch this method will happen in sync time onLoaded(); } else { - const prom = this._timelineWindow.load(eventId, INITIAL_SIZE); + const prom = this.timelineWindow.load(eventId, INITIAL_SIZE); this.setState({ events: [], liveEvents: [], @@ -1139,17 +1196,17 @@ class TimelinePanel extends React.Component { // handle the completion of a timeline load or localEchoUpdate, by // reloading the events from the timelinewindow and pending event list into // the state. - _reloadEvents() { + private reloadEvents(): void { // we might have switched rooms since the load started - just bin // the results if so. if (this.unmounted) return; - this.setState(this._getEvents()); + this.setState(this.getEvents()); } // get the list of events from the timeline window and the pending event list - _getEvents() { - const events = this._timelineWindow.getEvents(); + private getEvents(): Pick { + const events: MatrixEvent[] = this.timelineWindow.getEvents(); // `arrayFastClone` performs a shallow copy of the array // we want the last event to be decrypted first but displayed last @@ -1161,14 +1218,14 @@ class TimelinePanel extends React.Component { client.decryptEventIfNeeded(event); }); - const firstVisibleEventIndex = this._checkForPreJoinUISI(events); + const firstVisibleEventIndex = this.checkForPreJoinUISI(events); // Hold onto the live events separately. The read receipt and read marker // should use this list, so that they don't advance into pending events. const liveEvents = [...events]; // if we're at the end of the live timeline, append the pending events - if (!this._timelineWindow.canPaginate(EventTimeline.FORWARDS)) { + if (!this.timelineWindow.canPaginate(EventTimeline.FORWARDS)) { events.push(...this.props.timelineSet.getPendingEvents()); } @@ -1189,7 +1246,7 @@ class TimelinePanel extends React.Component { * undecryptable event that was sent while the user was not in the room. If no * such events were found, then it returns 0. */ - _checkForPreJoinUISI(events) { + private checkForPreJoinUISI(events: MatrixEvent[]): number { const room = this.props.timelineSet.room; if (events.length === 0 || !room || @@ -1253,7 +1310,7 @@ class TimelinePanel extends React.Component { return 0; } - _indexForEventId(evId) { + private indexForEventId(evId: string): number | null { for (let i = 0; i < this.state.events.length; ++i) { if (evId == this.state.events[i].getId()) { return i; @@ -1262,15 +1319,14 @@ class TimelinePanel extends React.Component { return null; } - _getLastDisplayedEventIndex(opts) { - opts = opts || {}; + private getLastDisplayedEventIndex(opts: IEventIndexOpts = {}): number | null { const ignoreOwn = opts.ignoreOwn || false; const allowPartial = opts.allowPartial || false; - const messagePanel = this._messagePanel.current; + const messagePanel = this.messagePanel.current; if (!messagePanel) return null; - const messagePanelNode = ReactDOM.findDOMNode(messagePanel); + const messagePanelNode = ReactDOM.findDOMNode(messagePanel) as HTMLElement; if (!messagePanelNode) return null; // sometimes this happens for fresh rooms/post-sync const wrapperRect = messagePanelNode.getBoundingClientRect(); const myUserId = MatrixClientPeg.get().credentials.userId; @@ -1347,7 +1403,7 @@ class TimelinePanel extends React.Component { * SDK. * @return {String} the event ID */ - _getCurrentReadReceipt(ignoreSynthesized) { + private getCurrentReadReceipt(ignoreSynthesized = false): string { const client = MatrixClientPeg.get(); // the client can be null on logout if (client == null) { @@ -1358,7 +1414,7 @@ class TimelinePanel extends React.Component { return this.props.timelineSet.room.getEventReadUpTo(myUserId, ignoreSynthesized); } - _setReadMarker(eventId, eventTs, inhibitSetState) { + private setReadMarker(eventId: string, eventTs: number, inhibitSetState = false): void { const roomId = this.props.timelineSet.room.roomId; // don't update the state (and cause a re-render) if there is @@ -1383,7 +1439,7 @@ class TimelinePanel extends React.Component { }, this.props.onReadMarkerUpdated); } - _shouldPaginate() { + private shouldPaginate(): boolean { // don't try to paginate while events in the timeline are // still being decrypted. We don't render events while they're // being decrypted, so they don't take up space in the timeline. @@ -1394,12 +1450,9 @@ class TimelinePanel extends React.Component { }); } - getRelationsForEvent = (...args) => this.props.timelineSet.getRelationsForEvent(...args); + private getRelationsForEvent = (...args) => this.props.timelineSet.getRelationsForEvent(...args); render() { - const MessagePanel = sdk.getComponent("structures.MessagePanel"); - const Loader = sdk.getComponent("elements.Spinner"); - // just show a spinner while the timeline loads. // // put it in a div of the right class (mx_RoomView_messagePanel) so @@ -1414,7 +1467,7 @@ class TimelinePanel extends React.Component { if (this.state.timelineLoading) { return (
    - +
    ); } @@ -1435,7 +1488,7 @@ class TimelinePanel extends React.Component { // forwards, otherwise if somebody hits the bottom of the loaded // events when viewing historical messages, we get stuck in a loop // of paginating our way through the entire history of the room. - const stickyBottom = !this._timelineWindow.canPaginate(EventTimeline.FORWARDS); + const stickyBottom = !this.timelineWindow.canPaginate(EventTimeline.FORWARDS); // If the state is PREPARED or CATCHUP, we're still waiting for the js-sdk to sync with // the HS and fetch the latest events, so we are effectively forward paginating. @@ -1448,7 +1501,7 @@ class TimelinePanel extends React.Component { : this.state.events; return (
  • ; + unfederatableSection =
    { _t('This room is not accessible by remote Matrix servers') }
    ; } let roomUpgradeButton; @@ -110,7 +111,7 @@ export default class AdvancedRoomSettingsTab extends React.Component

    - {_t( + { _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.", @@ -118,10 +119,10 @@ export default class AdvancedRoomSettingsTab extends React.Component {sub}, "i": (sub) => {sub}, }, - )} + ) }

    - {_t("Upgrade this room to the recommended room version")} + { _t("Upgrade this room to the recommended room version") }
    ); @@ -141,21 +142,21 @@ export default class AdvancedRoomSettingsTab extends React.Component -
    {_t("Advanced")}
    +
    { _t("Advanced") }
    { room?.isSpaceRoom() ? _t("Space information") : _t("Room information") }
    - {_t("Internal room ID:")}  + { _t("Internal room ID:") }  { this.props.roomId }
    { unfederatableSection }
    - {_t("Room version")} + { _t("Room version") }
    - {_t("Room version:")}  + { _t("Room version:") }  { room.getVersion() }
    { oldRoomLink } diff --git a/src/components/views/spaces/SpaceSettingsGeneralTab.tsx b/src/components/views/spaces/SpaceSettingsGeneralTab.tsx index db0a180846..3afdc629e4 100644 --- a/src/components/views/spaces/SpaceSettingsGeneralTab.tsx +++ b/src/components/views/spaces/SpaceSettingsGeneralTab.tsx @@ -90,7 +90,7 @@ const SpaceSettingsGeneralTab = ({ matrixClient: cli, space, onFinished }: IProp }; return
    -
    {_t("General")}
    +
    { _t("General") }
    { _t("Edit settings relating to your space.") }
    diff --git a/src/components/views/spaces/SpaceSettingsVisibilityTab.tsx b/src/components/views/spaces/SpaceSettingsVisibilityTab.tsx index 2f80ad97a6..263823603b 100644 --- a/src/components/views/spaces/SpaceSettingsVisibilityTab.tsx +++ b/src/components/views/spaces/SpaceSettingsVisibilityTab.tsx @@ -137,7 +137,7 @@ const SpaceSettingsVisibilityTab = ({ matrixClient: cli, space }: IProps) => { } return
    -
    {_t("Visibility")}
    +
    { _t("Visibility") }
    { error &&
    { error }
    } From 9dc8493a5c845336dafc774193353d08e1ef5971 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 22 Jun 2021 22:07:42 +0100 Subject: [PATCH 230/809] Hide communities invites and the community autocompleter when Spaces Beta is enabled --- src/autocomplete/Autocompleter.ts | 3 ++- src/components/views/rooms/RoomList.tsx | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/autocomplete/Autocompleter.ts b/src/autocomplete/Autocompleter.ts index ea8eddbb8d..7f3f5d2c01 100644 --- a/src/autocomplete/Autocompleter.ts +++ b/src/autocomplete/Autocompleter.ts @@ -55,13 +55,14 @@ const PROVIDERS = [ EmojiProvider, NotifProvider, CommandProvider, - CommunityProvider, DuckDuckGoProvider, ]; // as the spaces feature is device configurable only, and toggling it refreshes the page, we can do this here if (SettingsStore.getValue("feature_spaces")) { PROVIDERS.push(SpaceProvider); +} else { + PROVIDERS.push(CommunityProvider); } // Providers will get rejected if they take longer than this. diff --git a/src/components/views/rooms/RoomList.tsx b/src/components/views/rooms/RoomList.tsx index 5a1c3a24b3..704c3cf620 100644 --- a/src/components/views/rooms/RoomList.tsx +++ b/src/components/views/rooms/RoomList.tsx @@ -466,6 +466,7 @@ export default class RoomList extends React.PureComponent { } private renderCommunityInvites(): ReactComponentElement[] { + if (SettingsStore.getValue("feature_spaces")) return []; // TODO: Put community invites in a more sensible place (not in the room list) // See https://github.com/vector-im/element-web/issues/14456 return MatrixClientPeg.get().getGroups().filter(g => { From 83296b74404c6a6e594c9bd382bbb9d37f4248ab Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 22 Jun 2021 22:19:01 +0100 Subject: [PATCH 231/809] Fix typing --- src/hooks/useRoomState.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/hooks/useRoomState.ts b/src/hooks/useRoomState.ts index 11ac7de49e..e778acf8a9 100644 --- a/src/hooks/useRoomState.ts +++ b/src/hooks/useRoomState.ts @@ -24,7 +24,10 @@ type Mapper = (roomState: RoomState) => T; const defaultMapper: Mapper = (roomState: RoomState) => roomState; // Hook to simplify watching Matrix Room state -export const useRoomState = (room: Room, mapper: Mapper = defaultMapper): T => { +export const useRoomState = ( + room: Room, + mapper: Mapper = defaultMapper as Mapper, +): T => { const [value, setValue] = useState(room ? mapper(room.currentState) : undefined); const update = useCallback(() => { From e0ac200e27197617d9be4df78ef957441fed2fde Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 22 Jun 2021 22:23:09 +0100 Subject: [PATCH 232/809] Iterate PR --- src/DecryptionFailureTracker.ts | 4 ++-- src/HtmlUtils.tsx | 4 ++-- src/utils/EditorStateTransfer.ts | 10 +++++----- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/DecryptionFailureTracker.ts b/src/DecryptionFailureTracker.ts index 960d844e9e..07c0c546fe 100644 --- a/src/DecryptionFailureTracker.ts +++ b/src/DecryptionFailureTracker.ts @@ -25,7 +25,7 @@ export class DecryptionFailure { } } -type Fn = (count: number, trackedErrCode: string) => void; +type TrackingFn = (count: number, trackedErrCode: string) => void; type ErrCodeMapFn = (errcode: string) => string; export class DecryptionFailureTracker { @@ -73,7 +73,7 @@ export class DecryptionFailureTracker { * @param {function?} errorCodeMapFn The function used to map error codes to the * trackedErrorCode. If not provided, the `.code` of errors will be used. */ - constructor(private readonly fn: Fn, private readonly errorCodeMapFn?: ErrCodeMapFn) { + constructor(private readonly fn: TrackingFn, private readonly errorCodeMapFn?: ErrCodeMapFn) { if (!fn || typeof fn !== 'function') { throw new Error('DecryptionFailureTracker requires tracking function'); } diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx index 5803029030..983538d65b 100644 --- a/src/HtmlUtils.tsx +++ b/src/HtmlUtils.tsx @@ -505,7 +505,7 @@ export function bodyToHtml(content: IContent, highlights: string[], opts: IOpts * @param {object} [options] Options for linkifyString. Default: linkifyMatrix.options * @returns {string} Linkified string */ -export function linkifyString(str: string, options = linkifyMatrix.options) { +export function linkifyString(str: string, options = linkifyMatrix.options): string { return _linkifyString(str, options); } @@ -516,7 +516,7 @@ export function linkifyString(str: string, options = linkifyMatrix.options) { * @param {object} [options] Options for linkifyElement. Default: linkifyMatrix.options * @returns {object} */ -export function linkifyElement(element: HTMLElement, options = linkifyMatrix.options) { +export function linkifyElement(element: HTMLElement, options = linkifyMatrix.options): HTMLElement { return _linkifyElement(element, options); } diff --git a/src/utils/EditorStateTransfer.ts b/src/utils/EditorStateTransfer.ts index 42e1a316d6..ba303f9b73 100644 --- a/src/utils/EditorStateTransfer.ts +++ b/src/utils/EditorStateTransfer.ts @@ -30,24 +30,24 @@ export default class EditorStateTransfer { constructor(private readonly event: MatrixEvent) {} - setEditorState(caret: Caret, serializedParts: SerializedPart[]) { + public setEditorState(caret: Caret, serializedParts: SerializedPart[]) { this.caret = caret; this.serializedParts = serializedParts; } - hasEditorState() { + public hasEditorState() { return !!this.serializedParts; } - getSerializedParts() { + public getSerializedParts() { return this.serializedParts; } - getCaret() { + public getCaret() { return this.caret; } - getEvent() { + public getEvent() { return this.event; } } From ffaa19ef2c27f361c8fcc0367e491dca528e697b Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 22 Jun 2021 22:27:12 +0100 Subject: [PATCH 233/809] fix typing --- .../views/dialogs/ForwardDialog.tsx | 31 ++++++++++--------- .../views/elements/EventTilePreview.tsx | 9 +++--- 2 files changed, 21 insertions(+), 19 deletions(-) diff --git a/src/components/views/dialogs/ForwardDialog.tsx b/src/components/views/dialogs/ForwardDialog.tsx index a83f3f177c..6fbed6fc8b 100644 --- a/src/components/views/dialogs/ForwardDialog.tsx +++ b/src/components/views/dialogs/ForwardDialog.tsx @@ -14,30 +14,31 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, {useMemo, useState, useEffect} from "react"; +import React, { useMemo, useState, useEffect } from "react"; import classnames from "classnames"; -import {MatrixEvent} from "matrix-js-sdk/src/models/event"; -import {Room} from "matrix-js-sdk/src/models/room"; -import {MatrixClient} from "matrix-js-sdk/src/client"; +import { MatrixEvent } from "matrix-js-sdk/src/models/event"; +import { Room } from "matrix-js-sdk/src/models/room"; +import { MatrixClient } from "matrix-js-sdk/src/client"; +import { RoomMember } from "matrix-js-sdk/src/models/room-member"; -import {_t} from "../../../languageHandler"; +import { _t } from "../../../languageHandler"; import dis from "../../../dispatcher/dispatcher"; -import {useSettingValue, useFeatureEnabled} from "../../../hooks/useSettings"; -import {UIFeature} from "../../../settings/UIFeature"; -import {Layout} from "../../../settings/Layout"; -import {IDialogProps} from "./IDialogProps"; +import { useSettingValue, useFeatureEnabled } from "../../../hooks/useSettings"; +import { UIFeature } from "../../../settings/UIFeature"; +import { Layout } from "../../../settings/Layout"; +import { IDialogProps } from "./IDialogProps"; import BaseDialog from "./BaseDialog"; -import {avatarUrlForUser} from "../../../Avatar"; +import { avatarUrlForUser } from "../../../Avatar"; import EventTile from "../rooms/EventTile"; import SearchBox from "../../structures/SearchBox"; import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar"; -import {Alignment} from '../elements/Tooltip'; +import { Alignment } from '../elements/Tooltip'; import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; import AutoHideScrollbar from "../../structures/AutoHideScrollbar"; -import {StaticNotificationState} from "../../../stores/notifications/StaticNotificationState"; +import { StaticNotificationState } from "../../../stores/notifications/StaticNotificationState"; import NotificationBadge from "../rooms/NotificationBadge"; -import {RoomPermalinkCreator} from "../../../utils/permalinks/Permalinks"; -import {sortRooms} from "../../../stores/room-list/algorithms/tag-sorting/RecentAlgorithm"; +import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks"; +import { sortRooms } from "../../../stores/room-list/algorithms/tag-sorting/RecentAlgorithm"; import QueryMatcher from "../../../autocomplete/QueryMatcher"; const AVATAR_SIZE = 30; @@ -171,7 +172,7 @@ const ForwardDialog: React.FC = ({ matrixClient: cli, event, permalinkCr ); }, getMxcAvatarUrl: () => profileInfo.avatar_url, - }; + } as RoomMember; const [query, setQuery] = useState(""); const lcQuery = query.toLowerCase(); diff --git a/src/components/views/elements/EventTilePreview.tsx b/src/components/views/elements/EventTilePreview.tsx index cf3b7a6e61..366d918bcf 100644 --- a/src/components/views/elements/EventTilePreview.tsx +++ b/src/components/views/elements/EventTilePreview.tsx @@ -17,13 +17,14 @@ limitations under the License. import React from 'react'; import classnames from 'classnames'; import { MatrixEvent } from 'matrix-js-sdk/src/models/event'; +import { RoomMember } from 'matrix-js-sdk/src/models/room-member'; import * as Avatar from '../../../Avatar'; import EventTile from '../rooms/EventTile'; import SettingsStore from "../../../settings/SettingsStore"; -import {Layout} from "../../../settings/Layout"; -import {UIFeature} from "../../../settings/UIFeature"; -import {replaceableComponent} from "../../../utils/replaceableComponent"; +import { Layout } from "../../../settings/Layout"; +import { UIFeature } from "../../../settings/UIFeature"; +import { replaceableComponent } from "../../../utils/replaceableComponent"; interface IProps { /** @@ -110,7 +111,7 @@ export default class EventTilePreview extends React.Component { ); }, getMxcAvatarUrl: () => this.props.avatarUrl, - }; + } as RoomMember; return event; } From 38d0ab3c447409e6ce2130a8be1a4f1eac1ad622 Mon Sep 17 00:00:00 2001 From: Aaron Raimist Date: Tue, 22 Jun 2021 22:35:47 -0500 Subject: [PATCH 234/809] Do not honor string power levels Signed-off-by: Aaron Raimist --- src/TextForEvent.ts | 18 +++++++++++++----- .../tabs/room/RolesRoomSettingsTab.tsx | 1 + 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/TextForEvent.ts b/src/TextForEvent.ts index 649c53664e..62f73082ed 100644 --- a/src/TextForEvent.ts +++ b/src/TextForEvent.ts @@ -427,7 +427,8 @@ function textForPowerEvent(event): () => string | null { !event.getContent() || !event.getContent().users) { return null; } - const userDefault = event.getContent().users_default || 0; + const previousUserDefault = event.getPrevContent().users_default || 0; + const currentUserDefault = event.getContent().users_default || 0; // Construct set of userIds const users = []; Object.keys(event.getContent().users).forEach( @@ -443,9 +444,16 @@ function textForPowerEvent(event): () => string | null { const diffs = []; users.forEach((userId) => { // Previous power level - const from = event.getPrevContent().users[userId]; + var from = event.getPrevContent().users[userId]; + if (!Number.isInteger(from)) { + from = previousUserDefault; + } // Current power level - const to = event.getContent().users[userId]; + var to = event.getContent().users[userId]; + if (!Number.isInteger(to)) { + to = currentUserDefault; + } + if (from === previousUserDefault && to === currentUserDefault) { return; } if (to !== from) { diffs.push({ userId, from, to }); } @@ -459,8 +467,8 @@ function textForPowerEvent(event): () => string | null { powerLevelDiffText: diffs.map(diff => _t('%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s', { userId: diff.userId, - fromPowerLevel: Roles.textualPowerLevel(diff.from, userDefault), - toPowerLevel: Roles.textualPowerLevel(diff.to, userDefault), + fromPowerLevel: Roles.textualPowerLevel(diff.from, previousUserDefault), + toPowerLevel: Roles.textualPowerLevel(diff.to, currentUserDefault), }), ).join(", "), }); diff --git a/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx b/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx index 19ebe2a77e..75e6cc3a3d 100644 --- a/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx +++ b/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx @@ -284,6 +284,7 @@ export default class RolesRoomSettingsTab extends React.Component { const mutedUsers = []; Object.keys(userLevels).forEach((user) => { + if (!Number.isInteger(userLevels[user])) { return; } const canChange = userLevels[user] < currentUserLevel && canChangeLevels; if (userLevels[user] > defaultUserLevel) { // privileged privilegedUsers.push( From 9d723cd1b697462ecd940c1264329c162e544b66 Mon Sep 17 00:00:00 2001 From: Aaron Raimist Date: Tue, 22 Jun 2021 22:48:01 -0500 Subject: [PATCH 235/809] lint Signed-off-by: Aaron Raimist --- src/TextForEvent.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/TextForEvent.ts b/src/TextForEvent.ts index 62f73082ed..e162b09ed4 100644 --- a/src/TextForEvent.ts +++ b/src/TextForEvent.ts @@ -444,12 +444,12 @@ function textForPowerEvent(event): () => string | null { const diffs = []; users.forEach((userId) => { // Previous power level - var from = event.getPrevContent().users[userId]; + let from = event.getPrevContent().users[userId]; if (!Number.isInteger(from)) { from = previousUserDefault; } // Current power level - var to = event.getContent().users[userId]; + let to = event.getContent().users[userId]; if (!Number.isInteger(to)) { to = currentUserDefault; } From 9bceb40820baaab07f5a222641777ecc98f5ba96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Wed, 23 Jun 2021 09:26:33 +0200 Subject: [PATCH 236/809] CallMediaHandler -> MediaDeviceHandler MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/CallMediaHandler.js | 85 ---------------- src/MediaDeviceHandler.ts | 96 +++++++++++++++++++ src/components/structures/LoggedInView.tsx | 4 +- .../views/rooms/VoiceRecordComposerTile.tsx | 6 +- .../tabs/user/VoiceUserSettingsTab.js | 24 ++--- src/components/views/voip/AudioFeed.tsx | 4 +- src/voice/VoiceRecording.ts | 4 +- 7 files changed, 117 insertions(+), 106 deletions(-) delete mode 100644 src/CallMediaHandler.js create mode 100644 src/MediaDeviceHandler.ts diff --git a/src/CallMediaHandler.js b/src/CallMediaHandler.js deleted file mode 100644 index 634f0bb336..0000000000 --- a/src/CallMediaHandler.js +++ /dev/null @@ -1,85 +0,0 @@ -/* - Copyright 2017 Michael Telatynski <7t3chguy@gmail.com> - - 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 SettingsStore from "./settings/SettingsStore"; -import {SettingLevel} from "./settings/SettingLevel"; -import {setMatrixCallAudioInput, setMatrixCallVideoInput} from "matrix-js-sdk/src/matrix"; - -export default { - hasAnyLabeledDevices: async function() { - const devices = await navigator.mediaDevices.enumerateDevices(); - return devices.some(d => !!d.label); - }, - - getDevices: function() { - // Only needed for Electron atm, though should work in modern browsers - // once permission has been granted to the webapp - return navigator.mediaDevices.enumerateDevices().then(function(devices) { - const audiooutput = []; - const audioinput = []; - const videoinput = []; - - devices.forEach((device) => { - switch (device.kind) { - case 'audiooutput': audiooutput.push(device); break; - case 'audioinput': audioinput.push(device); break; - case 'videoinput': videoinput.push(device); break; - } - }); - - // console.log("Loaded WebRTC Devices", mediaDevices); - return { - audiooutput, - audioinput, - videoinput, - }; - }, (error) => { console.log('Unable to refresh WebRTC Devices: ', error); }); - }, - - loadDevices: function() { - const audioDeviceId = SettingsStore.getValue("webrtc_audioinput"); - const videoDeviceId = SettingsStore.getValue("webrtc_videoinput"); - - setMatrixCallAudioInput(audioDeviceId); - setMatrixCallVideoInput(videoDeviceId); - }, - - setAudioOutput: function(deviceId) { - SettingsStore.setValue("webrtc_audiooutput", null, SettingLevel.DEVICE, deviceId); - }, - - setAudioInput: function(deviceId) { - SettingsStore.setValue("webrtc_audioinput", null, SettingLevel.DEVICE, deviceId); - setMatrixCallAudioInput(deviceId); - }, - - setVideoInput: function(deviceId) { - SettingsStore.setValue("webrtc_videoinput", null, SettingLevel.DEVICE, deviceId); - setMatrixCallVideoInput(deviceId); - }, - - getAudioOutput: function() { - return SettingsStore.getValueAt(SettingLevel.DEVICE, "webrtc_audiooutput"); - }, - - getAudioInput: function() { - return SettingsStore.getValueAt(SettingLevel.DEVICE, "webrtc_audioinput"); - }, - - getVideoInput: function() { - return SettingsStore.getValueAt(SettingLevel.DEVICE, "webrtc_videoinput"); - }, -}; diff --git a/src/MediaDeviceHandler.ts b/src/MediaDeviceHandler.ts new file mode 100644 index 0000000000..96fd764b98 --- /dev/null +++ b/src/MediaDeviceHandler.ts @@ -0,0 +1,96 @@ +/* +Copyright 2017 Michael Telatynski <7t3chguy@gmail.com> +Copyright 2021 Šimon Brandner + +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 SettingsStore from "./settings/SettingsStore"; +import { SettingLevel } from "./settings/SettingLevel"; +import { setMatrixCallAudioInput, setMatrixCallVideoInput } from "matrix-js-sdk/src/matrix"; + +interface IMediaDevices { + audioOutput: Array; + audioInput: Array; + videoInput: Array; +} + +export default class MediaDeviceHandler { + static async hasAnyLabeledDevices(): Promise { + const devices = await navigator.mediaDevices.enumerateDevices(); + return devices.some(d => Boolean(d.label)); + } + + static async getDevices(): Promise { + // Only needed for Electron atm, though should work in modern browsers + // once permission has been granted to the webapp + + try { + const devices = await navigator.mediaDevices.enumerateDevices(); + + const audioOutput = []; + const audioInput = []; + const videoInput = []; + + devices.forEach((device) => { + switch (device.kind) { + case 'audiooutput': audioOutput.push(device); break; + case 'audioinput': audioInput.push(device); break; + case 'videoinput': videoInput.push(device); break; + } + }); + + return { + audioOutput: audioOutput, + audioInput: audioInput, + videoInput: videoInput, + }; + } catch (error) { + console.log('Unable to refresh WebRTC Devices: ', error); + } + } + + static loadDevices() { + const audioDeviceId = SettingsStore.getValue("webrtc_audioinput"); + const videoDeviceId = SettingsStore.getValue("webrtc_videoinput"); + + setMatrixCallAudioInput(audioDeviceId); + setMatrixCallVideoInput(videoDeviceId); + } + + static setAudioOutput(deviceId: string) { + SettingsStore.setValue("webrtc_audiooutput", null, SettingLevel.DEVICE, deviceId); + } + + static setAudioInput(deviceId: string) { + SettingsStore.setValue("webrtc_audioinput", null, SettingLevel.DEVICE, deviceId); + setMatrixCallAudioInput(deviceId); + } + + static setVideoInput(deviceId: string) { + SettingsStore.setValue("webrtc_videoinput", null, SettingLevel.DEVICE, deviceId); + setMatrixCallVideoInput(deviceId); + } + + static getAudioOutput(): string { + return SettingsStore.getValueAt(SettingLevel.DEVICE, "webrtc_audiooutput"); + } + + static getAudioInput(): string { + return SettingsStore.getValueAt(SettingLevel.DEVICE, "webrtc_audioinput"); + } + + static getVideoInput(): string { + return SettingsStore.getValueAt(SettingLevel.DEVICE, "webrtc_videoinput"); + } +} diff --git a/src/components/structures/LoggedInView.tsx b/src/components/structures/LoggedInView.tsx index e3d6b1ab9c..5ad67232a4 100644 --- a/src/components/structures/LoggedInView.tsx +++ b/src/components/structures/LoggedInView.tsx @@ -22,7 +22,7 @@ import { MatrixClient } from 'matrix-js-sdk/src/client'; import {Key} from '../../Keyboard'; import PageTypes from '../../PageTypes'; -import CallMediaHandler from '../../CallMediaHandler'; +import MediaDeviceHandler from '../../MediaDeviceHandler'; import { fixupColorFonts } from '../../utils/FontManager'; import * as sdk from '../../index'; import dis from '../../dispatcher/dispatcher'; @@ -167,7 +167,7 @@ class LoggedInView extends React.Component { // stash the MatrixClient in case we log out before we are unmounted this._matrixClient = this.props.matrixClient; - CallMediaHandler.loadDevices(); + MediaDeviceHandler.loadDevices(); fixupColorFonts(); diff --git a/src/components/views/rooms/VoiceRecordComposerTile.tsx b/src/components/views/rooms/VoiceRecordComposerTile.tsx index 20d8c9c5d4..122ba0ca0b 100644 --- a/src/components/views/rooms/VoiceRecordComposerTile.tsx +++ b/src/components/views/rooms/VoiceRecordComposerTile.tsx @@ -30,7 +30,7 @@ import RecordingPlayback from "../voice_messages/RecordingPlayback"; import {MsgType} from "matrix-js-sdk/src/@types/event"; import Modal from "../../../Modal"; import ErrorDialog from "../dialogs/ErrorDialog"; -import CallMediaHandler from "../../../CallMediaHandler"; +import MediaDeviceHandler from "../../../MediaDeviceHandler"; interface IProps { room: Room; @@ -129,8 +129,8 @@ export default class VoiceRecordComposerTile extends React.PureComponent diff --git a/src/components/views/settings/tabs/user/VoiceUserSettingsTab.js b/src/components/views/settings/tabs/user/VoiceUserSettingsTab.js index 362059f8ed..962f1fcd44 100644 --- a/src/components/views/settings/tabs/user/VoiceUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/VoiceUserSettingsTab.js @@ -18,7 +18,7 @@ limitations under the License. import React from 'react'; import {_t} from "../../../../../languageHandler"; import SdkConfig from "../../../../../SdkConfig"; -import CallMediaHandler from "../../../../../CallMediaHandler"; +import MediaDeviceHandler from "../../../../../MediaDeviceHandler"; import Field from "../../../elements/Field"; import AccessibleButton from "../../../elements/AccessibleButton"; import {MatrixClientPeg} from "../../../../../MatrixClientPeg"; @@ -41,7 +41,7 @@ export default class VoiceUserSettingsTab extends React.Component { } async componentDidMount() { - const canSeeDeviceLabels = await CallMediaHandler.hasAnyLabeledDevices(); + const canSeeDeviceLabels = await MediaDeviceHandler.hasAnyLabeledDevices(); if (canSeeDeviceLabels) { this._refreshMediaDevices(); } @@ -49,10 +49,10 @@ export default class VoiceUserSettingsTab extends React.Component { _refreshMediaDevices = async (stream) => { this.setState({ - mediaDevices: await CallMediaHandler.getDevices(), - activeAudioOutput: CallMediaHandler.getAudioOutput(), - activeAudioInput: CallMediaHandler.getAudioInput(), - activeVideoInput: CallMediaHandler.getVideoInput(), + mediaDevices: await MediaDeviceHandler.getDevices(), + activeAudioOutput: MediaDeviceHandler.getAudioOutput(), + activeAudioInput: MediaDeviceHandler.getAudioInput(), + activeVideoInput: MediaDeviceHandler.getVideoInput(), }); if (stream) { // kill stream (after we've enumerated the devices, otherwise we'd get empty labels again) @@ -100,21 +100,21 @@ export default class VoiceUserSettingsTab extends React.Component { }; _setAudioOutput = (e) => { - CallMediaHandler.setAudioOutput(e.target.value); + MediaDeviceHandler.setAudioOutput(e.target.value); this.setState({ activeAudioOutput: e.target.value, }); }; _setAudioInput = (e) => { - CallMediaHandler.setAudioInput(e.target.value); + MediaDeviceHandler.setAudioInput(e.target.value); this.setState({ activeAudioInput: e.target.value, }); }; _setVideoInput = (e) => { - CallMediaHandler.setVideoInput(e.target.value); + MediaDeviceHandler.setVideoInput(e.target.value); this.setState({ activeVideoInput: e.target.value, }); @@ -171,7 +171,7 @@ export default class VoiceUserSettingsTab extends React.Component { } }; - const audioOutputs = this.state.mediaDevices.audiooutput.slice(0); + const audioOutputs = this.state.mediaDevices.audioOutput.slice(0); if (audioOutputs.length > 0) { const defaultDevice = getDefaultDevice(audioOutputs); speakerDropdown = ( @@ -183,7 +183,7 @@ export default class VoiceUserSettingsTab extends React.Component { ); } - const audioInputs = this.state.mediaDevices.audioinput.slice(0); + const audioInputs = this.state.mediaDevices.audioInput.slice(0); if (audioInputs.length > 0) { const defaultDevice = getDefaultDevice(audioInputs); microphoneDropdown = ( @@ -195,7 +195,7 @@ export default class VoiceUserSettingsTab extends React.Component { ); } - const videoInputs = this.state.mediaDevices.videoinput.slice(0); + const videoInputs = this.state.mediaDevices.videoInput.slice(0); if (videoInputs.length > 0) { const defaultDevice = getDefaultDevice(videoInputs); webcamDropdown = ( diff --git a/src/components/views/voip/AudioFeed.tsx b/src/components/views/voip/AudioFeed.tsx index c78f0c0fc8..80d658e7ee 100644 --- a/src/components/views/voip/AudioFeed.tsx +++ b/src/components/views/voip/AudioFeed.tsx @@ -17,7 +17,7 @@ limitations under the License. import React, {createRef} from 'react'; import { CallFeed, CallFeedEvent } from 'matrix-js-sdk/src/webrtc/callFeed'; import { logger } from 'matrix-js-sdk/src/logger'; -import CallMediaHandler from "../../../CallMediaHandler"; +import MediaDeviceHandler from "../../../MediaDeviceHandler"; interface IProps { feed: CallFeed, @@ -38,7 +38,7 @@ export default class AudioFeed extends React.Component { private playMedia() { const element = this.element.current; - const audioOutput = CallMediaHandler.getAudioOutput(); + const audioOutput = MediaDeviceHandler.getAudioOutput(); if (audioOutput) { try { diff --git a/src/voice/VoiceRecording.ts b/src/voice/VoiceRecording.ts index fde5779fa2..8f9e03bb8e 100644 --- a/src/voice/VoiceRecording.ts +++ b/src/voice/VoiceRecording.ts @@ -17,7 +17,7 @@ limitations under the License. import * as Recorder from 'opus-recorder'; import encoderPath from 'opus-recorder/dist/encoderWorker.min.js'; import {MatrixClient} from "matrix-js-sdk/src/client"; -import CallMediaHandler from "../CallMediaHandler"; +import MediaDeviceHandler from "../MediaDeviceHandler"; import {SimpleObservable} from "matrix-widget-api"; import {clamp, percentageOf, percentageWithin} from "../utils/numbers"; import EventEmitter from "events"; @@ -97,7 +97,7 @@ export class VoiceRecording extends EventEmitter implements IDestroyable { audio: { channelCount: CHANNELS, noiseSuppression: true, // browsers ignore constraints they can't honour - deviceId: CallMediaHandler.getAudioInput(), + deviceId: MediaDeviceHandler.getAudioInput(), }, }); this.recorderContext = createAudioContext({ From 057f46ad9d10370416af0ad951858f17c1caff55 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Wed, 23 Jun 2021 08:44:48 +0100 Subject: [PATCH 237/809] fix dependency and lockfile mismatch --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index bb703c33f0..6b369e9c27 100644 --- a/package.json +++ b/package.json @@ -123,7 +123,7 @@ "@sinonjs/fake-timers": "^7.0.2", "@types/classnames": "^2.2.11", "@types/counterpart": "^0.18.1", - "@types/diff-match-patch": "^1.0.5", + "@types/diff-match-patch": "^1.0.32", "@types/flux": "^3.1.9", "@types/jest": "^26.0.20", "@types/linkifyjs": "^2.1.3", From 58151d71c5b806ece26345ba2fc37d3a5dbe7a3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Wed, 23 Jun 2021 09:56:37 +0200 Subject: [PATCH 238/809] Handle mid-call output changes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/MediaDeviceHandler.ts | 27 ++++++++++++++++--- .../tabs/user/VoiceUserSettingsTab.js | 6 ++--- src/components/views/voip/AudioFeed.tsx | 18 ++++++++++--- 3 files changed, 40 insertions(+), 11 deletions(-) diff --git a/src/MediaDeviceHandler.ts b/src/MediaDeviceHandler.ts index 96fd764b98..8780cea359 100644 --- a/src/MediaDeviceHandler.ts +++ b/src/MediaDeviceHandler.ts @@ -18,6 +18,7 @@ limitations under the License. import SettingsStore from "./settings/SettingsStore"; import { SettingLevel } from "./settings/SettingLevel"; import { setMatrixCallAudioInput, setMatrixCallVideoInput } from "matrix-js-sdk/src/matrix"; +import EventEmitter from 'events'; interface IMediaDevices { audioOutput: Array; @@ -25,7 +26,22 @@ interface IMediaDevices { videoInput: Array; } -export default class MediaDeviceHandler { +export enum MediaDeviceHandlerEvent { + AudioOutputChanged = "audio_output_changed", + AudioInputChanged = "audio_input_changed", + VideoInputChanged = "video_input_changed", +} + +export default class MediaDeviceHandler extends EventEmitter { + private static internalInstance; + + public static get instance(): MediaDeviceHandler { + if (!MediaDeviceHandler.internalInstance) { + MediaDeviceHandler.internalInstance = new MediaDeviceHandler(); + } + return MediaDeviceHandler.internalInstance; + } + static async hasAnyLabeledDevices(): Promise { const devices = await navigator.mediaDevices.enumerateDevices(); return devices.some(d => Boolean(d.label)); @@ -68,18 +84,21 @@ export default class MediaDeviceHandler { setMatrixCallVideoInput(videoDeviceId); } - static setAudioOutput(deviceId: string) { + public setAudioOutput(deviceId: string) { SettingsStore.setValue("webrtc_audiooutput", null, SettingLevel.DEVICE, deviceId); + this.emit(MediaDeviceHandlerEvent.AudioOutputChanged, deviceId); } - static setAudioInput(deviceId: string) { + public setAudioInput(deviceId: string) { SettingsStore.setValue("webrtc_audioinput", null, SettingLevel.DEVICE, deviceId); setMatrixCallAudioInput(deviceId); + this.emit(MediaDeviceHandlerEvent.AudioInputChanged, deviceId); } - static setVideoInput(deviceId: string) { + public setVideoInput(deviceId: string) { SettingsStore.setValue("webrtc_videoinput", null, SettingLevel.DEVICE, deviceId); setMatrixCallVideoInput(deviceId); + this.emit(MediaDeviceHandlerEvent.VideoInputChanged, deviceId); } static getAudioOutput(): string { diff --git a/src/components/views/settings/tabs/user/VoiceUserSettingsTab.js b/src/components/views/settings/tabs/user/VoiceUserSettingsTab.js index 962f1fcd44..f730406eed 100644 --- a/src/components/views/settings/tabs/user/VoiceUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/VoiceUserSettingsTab.js @@ -100,21 +100,21 @@ export default class VoiceUserSettingsTab extends React.Component { }; _setAudioOutput = (e) => { - MediaDeviceHandler.setAudioOutput(e.target.value); + MediaDeviceHandler.instance.setAudioOutput(e.target.value); this.setState({ activeAudioOutput: e.target.value, }); }; _setAudioInput = (e) => { - MediaDeviceHandler.setAudioInput(e.target.value); + MediaDeviceHandler.instance.setAudioInput(e.target.value); this.setState({ activeAudioInput: e.target.value, }); }; _setVideoInput = (e) => { - MediaDeviceHandler.setVideoInput(e.target.value); + MediaDeviceHandler.instance.setVideoInput(e.target.value); this.setState({ activeVideoInput: e.target.value, }); diff --git a/src/components/views/voip/AudioFeed.tsx b/src/components/views/voip/AudioFeed.tsx index 80d658e7ee..d29caf789e 100644 --- a/src/components/views/voip/AudioFeed.tsx +++ b/src/components/views/voip/AudioFeed.tsx @@ -17,7 +17,7 @@ limitations under the License. import React, {createRef} from 'react'; import { CallFeed, CallFeedEvent } from 'matrix-js-sdk/src/webrtc/callFeed'; import { logger } from 'matrix-js-sdk/src/logger'; -import MediaDeviceHandler from "../../../MediaDeviceHandler"; +import MediaDeviceHandler, { MediaDeviceHandlerEvent } from "../../../MediaDeviceHandler"; interface IProps { feed: CallFeed, @@ -27,19 +27,25 @@ export default class AudioFeed extends React.Component { private element = createRef(); componentDidMount() { + MediaDeviceHandler.instance.addListener( + MediaDeviceHandlerEvent.AudioOutputChanged, + this.onAudioOutputChanged, + ); this.props.feed.addListener(CallFeedEvent.NewStream, this.onNewStream); this.playMedia(); } componentWillUnmount() { + MediaDeviceHandler.instance.removeListener( + MediaDeviceHandlerEvent.AudioOutputChanged, + this.onAudioOutputChanged, + ); this.props.feed.removeListener(CallFeedEvent.NewStream, this.onNewStream); this.stopMedia(); } - private playMedia() { + private onAudioOutputChanged = (audioOutput: string) => { const element = this.element.current; - const audioOutput = MediaDeviceHandler.getAudioOutput(); - if (audioOutput) { try { // This seems quite unreliable in Chrome, although I haven't yet managed to make a jsfiddle where @@ -52,7 +58,11 @@ export default class AudioFeed extends React.Component { logger.warn("Couldn't set requested audio output device: using default", e); } } + } + private playMedia() { + const element = this.element.current; + this.onAudioOutputChanged(MediaDeviceHandler.getAudioOutput()); element.muted = false; element.srcObject = this.props.feed.stream; element.autoplay = true; From 6c9e0e54e989eb2a1ea0d04645be6bc415380c12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Wed, 23 Jun 2021 10:27:51 +0200 Subject: [PATCH 239/809] Iterate PR MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/MediaDeviceHandler.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/MediaDeviceHandler.ts b/src/MediaDeviceHandler.ts index 8780cea359..b8b00963b6 100644 --- a/src/MediaDeviceHandler.ts +++ b/src/MediaDeviceHandler.ts @@ -42,12 +42,12 @@ export default class MediaDeviceHandler extends EventEmitter { return MediaDeviceHandler.internalInstance; } - static async hasAnyLabeledDevices(): Promise { + public static async hasAnyLabeledDevices(): Promise { const devices = await navigator.mediaDevices.enumerateDevices(); return devices.some(d => Boolean(d.label)); } - static async getDevices(): Promise { + public static async getDevices(): Promise { // Only needed for Electron atm, though should work in modern browsers // once permission has been granted to the webapp @@ -76,7 +76,7 @@ export default class MediaDeviceHandler extends EventEmitter { } } - static loadDevices() { + public static loadDevices(): void { const audioDeviceId = SettingsStore.getValue("webrtc_audioinput"); const videoDeviceId = SettingsStore.getValue("webrtc_videoinput"); @@ -84,32 +84,32 @@ export default class MediaDeviceHandler extends EventEmitter { setMatrixCallVideoInput(videoDeviceId); } - public setAudioOutput(deviceId: string) { + public setAudioOutput(deviceId: string): void { SettingsStore.setValue("webrtc_audiooutput", null, SettingLevel.DEVICE, deviceId); this.emit(MediaDeviceHandlerEvent.AudioOutputChanged, deviceId); } - public setAudioInput(deviceId: string) { + public setAudioInput(deviceId: string): void { SettingsStore.setValue("webrtc_audioinput", null, SettingLevel.DEVICE, deviceId); setMatrixCallAudioInput(deviceId); this.emit(MediaDeviceHandlerEvent.AudioInputChanged, deviceId); } - public setVideoInput(deviceId: string) { + public setVideoInput(deviceId: string): void { SettingsStore.setValue("webrtc_videoinput", null, SettingLevel.DEVICE, deviceId); setMatrixCallVideoInput(deviceId); this.emit(MediaDeviceHandlerEvent.VideoInputChanged, deviceId); } - static getAudioOutput(): string { + public static getAudioOutput(): string { return SettingsStore.getValueAt(SettingLevel.DEVICE, "webrtc_audiooutput"); } - static getAudioInput(): string { + public static getAudioInput(): string { return SettingsStore.getValueAt(SettingLevel.DEVICE, "webrtc_audioinput"); } - static getVideoInput(): string { + public static getVideoInput(): string { return SettingsStore.getValueAt(SettingLevel.DEVICE, "webrtc_videoinput"); } } From a6367c079620d95141ddcd45c99952d250818568 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Wed, 23 Jun 2021 09:33:38 +0100 Subject: [PATCH 240/809] Reintroduce LiveRecording components for maintenance reasons --- .../views/rooms/VoiceRecordComposerTile.tsx | 8 ++--- src/components/views/voice_messages/Clock.tsx | 4 +-- .../voice_messages/LiveRecordingClock.tsx | 26 +++++++++++++++++ .../voice_messages/LiveRecordingWaveform.tsx | 29 +++++++++++++++++++ .../views/voice_messages/Waveform.tsx | 4 +-- 5 files changed, 63 insertions(+), 8 deletions(-) create mode 100644 src/components/views/voice_messages/LiveRecordingClock.tsx create mode 100644 src/components/views/voice_messages/LiveRecordingWaveform.tsx diff --git a/src/components/views/rooms/VoiceRecordComposerTile.tsx b/src/components/views/rooms/VoiceRecordComposerTile.tsx index 6fe6a5ab1c..5868eed02d 100644 --- a/src/components/views/rooms/VoiceRecordComposerTile.tsx +++ b/src/components/views/rooms/VoiceRecordComposerTile.tsx @@ -26,11 +26,11 @@ import { import {Room} from "matrix-js-sdk/src/models/room"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; import classNames from "classnames"; -import Waveform from "../voice_messages/Waveform"; +import LiveRecordingWaveform from "../voice_messages/LiveRecordingWaveform"; import { replaceableComponent } from "../../../utils/replaceableComponent"; import { arrayFastResample, arraySeed } from "../../../utils/arrays"; import { percentageOf } from "../../../utils/numbers"; -import Clock from "../voice_messages/Clock"; +import LiveRecordingClock from "../voice_messages/LiveRecordingClock"; import { VoiceRecordingStore } from "../../../stores/VoiceRecordingStore"; import {UPDATE_EVENT} from "../../../stores/AsyncStore"; import RecordingPlayback from "../voice_messages/RecordingPlayback"; @@ -227,8 +227,8 @@ export default class VoiceRecordComposerTile extends React.PureComponent - - + +
    ; } diff --git a/src/components/views/voice_messages/Clock.tsx b/src/components/views/voice_messages/Clock.tsx index 23e6762c52..1e78cc7bbd 100644 --- a/src/components/views/voice_messages/Clock.tsx +++ b/src/components/views/voice_messages/Clock.tsx @@ -15,9 +15,9 @@ limitations under the License. */ import React from "react"; -import {replaceableComponent} from "../../../utils/replaceableComponent"; +import { replaceableComponent } from "../../../utils/replaceableComponent"; -interface IProps { +export interface IProps { seconds: number; } diff --git a/src/components/views/voice_messages/LiveRecordingClock.tsx b/src/components/views/voice_messages/LiveRecordingClock.tsx new file mode 100644 index 0000000000..f88bb63ac7 --- /dev/null +++ b/src/components/views/voice_messages/LiveRecordingClock.tsx @@ -0,0 +1,26 @@ +/* +Copyright 2021 The Matrix.org Foundation C.I.C. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +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 Clock, { IProps as IClockProps } from "./Clock"; +import { replaceableComponent } from "../../../utils/replaceableComponent"; + +/** + * A clock for a live recording. + */ +@replaceableComponent("views.voice_messages.LiveRecordingClock") +export default class LiveRecordingClock extends React.PureComponent { + public render() { + return ; + } +} diff --git a/src/components/views/voice_messages/LiveRecordingWaveform.tsx b/src/components/views/voice_messages/LiveRecordingWaveform.tsx new file mode 100644 index 0000000000..3d3169d764 --- /dev/null +++ b/src/components/views/voice_messages/LiveRecordingWaveform.tsx @@ -0,0 +1,29 @@ +/* +Copyright 2021 The Matrix.org Foundation C.I.C. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +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 Waveform, { IProps as IWaveformProps } from "./Waveform"; +import { replaceableComponent } from "../../../utils/replaceableComponent"; + +/** + * A waveform which shows the waveform of a live recording + */ +@replaceableComponent("views.voice_messages.LiveRecordingWaveform") +export default class LiveRecordingWaveform extends React.PureComponent { + public static defaultProps = { + progress: 1, + }; + public render() { + return ; + } +} diff --git a/src/components/views/voice_messages/Waveform.tsx b/src/components/views/voice_messages/Waveform.tsx index ae880933e6..5a4447065a 100644 --- a/src/components/views/voice_messages/Waveform.tsx +++ b/src/components/views/voice_messages/Waveform.tsx @@ -15,10 +15,10 @@ limitations under the License. */ import React from "react"; -import {replaceableComponent} from "../../../utils/replaceableComponent"; +import { replaceableComponent } from "../../../utils/replaceableComponent"; import classNames from "classnames"; -interface IProps { +export interface IProps { relHeights: number[]; // relative heights (0-1) progress: number; // percent complete, 0-1, default 100% } From 95624b3fa62e4e223911720faf5da99739ac1493 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Wed, 23 Jun 2021 10:37:15 +0200 Subject: [PATCH 241/809] Add some docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/MediaDeviceHandler.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/MediaDeviceHandler.ts b/src/MediaDeviceHandler.ts index b8b00963b6..10099bc7b8 100644 --- a/src/MediaDeviceHandler.ts +++ b/src/MediaDeviceHandler.ts @@ -76,6 +76,9 @@ export default class MediaDeviceHandler extends EventEmitter { } } + /** + * Retrieves devices from the SettingsStore and tells the js-sdk to use them + */ public static loadDevices(): void { const audioDeviceId = SettingsStore.getValue("webrtc_audioinput"); const videoDeviceId = SettingsStore.getValue("webrtc_videoinput"); @@ -89,12 +92,22 @@ export default class MediaDeviceHandler extends EventEmitter { this.emit(MediaDeviceHandlerEvent.AudioOutputChanged, deviceId); } + /** + * This will not change the device that a potential call uses. The call will + * need to be ended and started again for this change to take effect + * @param {string} deviceId + */ public setAudioInput(deviceId: string): void { SettingsStore.setValue("webrtc_audioinput", null, SettingLevel.DEVICE, deviceId); setMatrixCallAudioInput(deviceId); this.emit(MediaDeviceHandlerEvent.AudioInputChanged, deviceId); } + /** + * This will not change the device that a potential call uses. The call will + * need to be ended and started again for this change to take effect + * @param {string} deviceId + */ public setVideoInput(deviceId: string): void { SettingsStore.setValue("webrtc_videoinput", null, SettingLevel.DEVICE, deviceId); setMatrixCallVideoInput(deviceId); From 1bef985d46b507f1c0971e4b1ab54381d1c83e3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Wed, 23 Jun 2021 10:39:10 +0200 Subject: [PATCH 242/809] Remove emmiting that isn't useful for us now MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/MediaDeviceHandler.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/MediaDeviceHandler.ts b/src/MediaDeviceHandler.ts index 10099bc7b8..e0375df1a4 100644 --- a/src/MediaDeviceHandler.ts +++ b/src/MediaDeviceHandler.ts @@ -28,8 +28,6 @@ interface IMediaDevices { export enum MediaDeviceHandlerEvent { AudioOutputChanged = "audio_output_changed", - AudioInputChanged = "audio_input_changed", - VideoInputChanged = "video_input_changed", } export default class MediaDeviceHandler extends EventEmitter { @@ -100,7 +98,6 @@ export default class MediaDeviceHandler extends EventEmitter { public setAudioInput(deviceId: string): void { SettingsStore.setValue("webrtc_audioinput", null, SettingLevel.DEVICE, deviceId); setMatrixCallAudioInput(deviceId); - this.emit(MediaDeviceHandlerEvent.AudioInputChanged, deviceId); } /** @@ -111,7 +108,6 @@ export default class MediaDeviceHandler extends EventEmitter { public setVideoInput(deviceId: string): void { SettingsStore.setValue("webrtc_videoinput", null, SettingLevel.DEVICE, deviceId); setMatrixCallVideoInput(deviceId); - this.emit(MediaDeviceHandlerEvent.VideoInputChanged, deviceId); } public static getAudioOutput(): string { From 0e582c425cfb62bfef1147d7a306199d5992ab86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Wed, 23 Jun 2021 10:42:19 +0200 Subject: [PATCH 243/809] Make this look nicer Co-authored-by: Michael Telatynski <7t3chguy@googlemail.com> --- src/MediaDeviceHandler.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/MediaDeviceHandler.ts b/src/MediaDeviceHandler.ts index e0375df1a4..45ebcd22eb 100644 --- a/src/MediaDeviceHandler.ts +++ b/src/MediaDeviceHandler.ts @@ -64,11 +64,7 @@ export default class MediaDeviceHandler extends EventEmitter { } }); - return { - audioOutput: audioOutput, - audioInput: audioInput, - videoInput: videoInput, - }; + return { audioOutput, audioInput, videoInput }; } catch (error) { console.log('Unable to refresh WebRTC Devices: ', error); } From 4ab9758b671d408f47399fe744290f41507b02ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Wed, 23 Jun 2021 10:43:49 +0200 Subject: [PATCH 244/809] log -> warn MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/MediaDeviceHandler.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/MediaDeviceHandler.ts b/src/MediaDeviceHandler.ts index 45ebcd22eb..49ef123def 100644 --- a/src/MediaDeviceHandler.ts +++ b/src/MediaDeviceHandler.ts @@ -66,7 +66,7 @@ export default class MediaDeviceHandler extends EventEmitter { return { audioOutput, audioInput, videoInput }; } catch (error) { - console.log('Unable to refresh WebRTC Devices: ', error); + console.warn('Unable to refresh WebRTC Devices: ', error); } } From cdb97d549493e755922b45ff9d1526a9c9f9c5ba Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 23 Jun 2021 12:29:00 +0100 Subject: [PATCH 245/809] Fix trashcan.svg by exporting it with its viewbox then fix sizing and alignment of consumers --- res/css/structures/_RoomStatusBar.scss | 11 ++++------- res/css/views/rooms/_VoiceRecordComposerTile.scss | 6 +++--- res/img/element-icons/trashcan.svg | 4 ++-- 3 files changed, 9 insertions(+), 12 deletions(-) diff --git a/res/css/structures/_RoomStatusBar.scss b/res/css/structures/_RoomStatusBar.scss index 8cc00aba0f..a09aa9e3a5 100644 --- a/res/css/structures/_RoomStatusBar.scss +++ b/res/css/structures/_RoomStatusBar.scss @@ -112,7 +112,7 @@ limitations under the License. .mx_AccessibleButton { padding: 5px 10px; - padding-left: 28px; // 16px for the icon, 2px margin to text, 10px regular padding + padding-left: 30px; // 18px for the icon, 2px margin to text, 10px regular padding display: inline-block; position: relative; @@ -128,13 +128,13 @@ limitations under the License. mask-repeat: no-repeat; mask-position: center; mask-size: contain; + width: 18px; + height: 18px; + top: calc(50% - 9px); // text sizes are dynamic } &.mx_RoomStatusBar_unsentCancelAllBtn::before { mask-image: url('$(res)/img/element-icons/trashcan.svg'); - width: 12px; - height: 16px; - top: calc(50% - 8px); // text sizes are dynamic } &.mx_RoomStatusBar_unsentResendAllBtn { @@ -142,9 +142,6 @@ limitations under the License. &::before { mask-image: url('$(res)/img/element-icons/retry.svg'); - width: 18px; - height: 18px; - top: calc(50% - 9px); // text sizes are dynamic } } } diff --git a/res/css/views/rooms/_VoiceRecordComposerTile.scss b/res/css/views/rooms/_VoiceRecordComposerTile.scss index a3ee104bd8..5501ab343e 100644 --- a/res/css/views/rooms/_VoiceRecordComposerTile.scss +++ b/res/css/views/rooms/_VoiceRecordComposerTile.scss @@ -36,10 +36,10 @@ limitations under the License. } .mx_VoiceRecordComposerTile_delete { - width: 14px; // w&h are size of icon - height: 18px; + width: 24px; + height: 24px; vertical-align: middle; - margin-right: 11px; // distance from left edge of waveform container (container has some margin too) + margin-right: 8px; // distance from left edge of waveform container (container has some margin too) background-color: $voice-record-icon-color; mask-repeat: no-repeat; mask-size: contain; diff --git a/res/img/element-icons/trashcan.svg b/res/img/element-icons/trashcan.svg index f8fb8b5c46..4106f0bd60 100644 --- a/res/img/element-icons/trashcan.svg +++ b/res/img/element-icons/trashcan.svg @@ -1,3 +1,3 @@ - - + + From a8dfc4488ffd51f36d23e16e92d26b2d3750a328 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 23 Jun 2021 14:47:24 +0100 Subject: [PATCH 246/809] Convert more of js-sdk crypto and fix underscored field accesses --- src/SecurityManager.ts | 5 +++-- src/components/views/dialogs/DevtoolsDialog.tsx | 2 +- src/components/views/settings/CrossSigningPanel.js | 4 ++-- src/components/views/settings/SecureBackupPanel.js | 2 +- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/SecurityManager.ts b/src/SecurityManager.ts index 09c8d30614..1ba0d6439b 100644 --- a/src/SecurityManager.ts +++ b/src/SecurityManager.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { ICryptoCallbacks, IDeviceTrustLevel, ISecretStorageKeyInfo } from 'matrix-js-sdk/src/matrix'; +import { ICryptoCallbacks, ISecretStorageKeyInfo } from 'matrix-js-sdk/src/matrix'; import { MatrixClient } from 'matrix-js-sdk/src/client'; import Modal from './Modal'; import * as sdk from './index'; @@ -28,6 +28,7 @@ import AccessSecretStorageDialog from './components/views/dialogs/security/Acces import RestoreKeyBackupDialog from './components/views/dialogs/security/RestoreKeyBackupDialog'; import SettingsStore from "./settings/SettingsStore"; import SecurityCustomisations from "./customisations/Security"; +import { DeviceTrustLevel } from 'matrix-js-sdk/src/crypto/CrossSigning'; // This stores the secret storage private keys in memory for the JS SDK. This is // only meant to act as a cache to avoid prompting the user multiple times @@ -244,7 +245,7 @@ async function onSecretRequested( deviceId: string, requestId: string, name: string, - deviceTrust: IDeviceTrustLevel, + deviceTrust: DeviceTrustLevel, ): Promise { console.log("onSecretRequested", userId, deviceId, requestId, name, deviceTrust); const client = MatrixClientPeg.get(); diff --git a/src/components/views/dialogs/DevtoolsDialog.tsx b/src/components/views/dialogs/DevtoolsDialog.tsx index 2690eb67d7..b1749b370a 100644 --- a/src/components/views/dialogs/DevtoolsDialog.tsx +++ b/src/components/views/dialogs/DevtoolsDialog.tsx @@ -766,7 +766,7 @@ class VerificationExplorer extends React.PureComponent { render() { const cli = this.context; const room = this.props.room; - const inRoomChannel = cli.crypto._inRoomVerificationRequests; + const inRoomChannel = cli.crypto.inRoomVerificationRequests; const inRoomRequests = (inRoomChannel._requestsByRoomId || new Map()).get(room.roomId) || new Map(); return (
    diff --git a/src/components/views/settings/CrossSigningPanel.js b/src/components/views/settings/CrossSigningPanel.js index 0cd1a64ada..43a13a48a7 100644 --- a/src/components/views/settings/CrossSigningPanel.js +++ b/src/components/views/settings/CrossSigningPanel.js @@ -79,8 +79,8 @@ export default class CrossSigningPanel extends React.PureComponent { async _getUpdatedStatus() { const cli = MatrixClientPeg.get(); const pkCache = cli.getCrossSigningCacheCallbacks(); - const crossSigning = cli.crypto._crossSigningInfo; - const secretStorage = cli.crypto._secretStorage; + const crossSigning = cli.crypto.crossSigningInfo; + const secretStorage = cli.crypto.secretStorage; const crossSigningPublicKeysOnDevice = crossSigning.getId(); const crossSigningPrivateKeysInStorage = await crossSigning.isStoredInSecretStorage(secretStorage); const masterPrivateKeyCached = !!(pkCache && await pkCache.getCrossSigningKeyCache("master")); diff --git a/src/components/views/settings/SecureBackupPanel.js b/src/components/views/settings/SecureBackupPanel.js index 4f3eb0bdf6..abfd18f0d3 100644 --- a/src/components/views/settings/SecureBackupPanel.js +++ b/src/components/views/settings/SecureBackupPanel.js @@ -131,7 +131,7 @@ export default class SecureBackupPanel extends React.PureComponent { async _getUpdatedDiagnostics() { const cli = MatrixClientPeg.get(); - const secretStorage = cli.crypto._secretStorage; + const secretStorage = cli.crypto.secretStorage; const backupKeyStored = !!(await cli.isKeyBackupKeyStored()); const backupKeyFromCache = await cli.crypto.getSessionBackupPrivateKey(); From 99c442cea78f2e0ff3666f127d1260be2bf313e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Wed, 23 Jun 2021 13:45:55 +0200 Subject: [PATCH 247/809] Convert MemberList to TS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- .../rooms/{MemberList.js => MemberList.tsx} | 292 +++++++++++------- 1 file changed, 174 insertions(+), 118 deletions(-) rename src/components/views/rooms/{MemberList.js => MemberList.tsx} (64%) diff --git a/src/components/views/rooms/MemberList.js b/src/components/views/rooms/MemberList.tsx similarity index 64% rename from src/components/views/rooms/MemberList.js rename to src/components/views/rooms/MemberList.tsx index cb50f0fff3..5353eb94ed 100644 --- a/src/components/views/rooms/MemberList.js +++ b/src/components/views/rooms/MemberList.tsx @@ -2,6 +2,7 @@ Copyright 2015, 2016 OpenMarket Ltd Copyright 2017 Vector Creations Ltd Copyright 2017, 2018 New Vector Ltd +Copyright 2021 Šimon Brandner Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -20,17 +21,28 @@ import React from 'react'; import { _t } from '../../../languageHandler'; import SdkConfig from '../../../SdkConfig'; import dis from '../../../dispatcher/dispatcher'; -import {isValid3pidInvite} from "../../../RoomInvite"; -import rate_limited_func from "../../../ratelimitedfunc"; -import {MatrixClientPeg} from "../../../MatrixClientPeg"; -import * as sdk from "../../../index"; -import {CommunityPrototypeStore} from "../../../stores/CommunityPrototypeStore"; +import { isValid3pidInvite } from "../../../RoomInvite"; +import rateLimitedFunction from "../../../ratelimitedfunc"; +import { MatrixClientPeg } from "../../../MatrixClientPeg"; +import { CommunityPrototypeStore } from "../../../stores/CommunityPrototypeStore"; import BaseCard from "../right_panel/BaseCard"; -import {RightPanelPhases} from "../../../stores/RightPanelStorePhases"; +import { RightPanelPhases } from "../../../stores/RightPanelStorePhases"; import RoomAvatar from "../avatars/RoomAvatar"; import RoomName from "../elements/RoomName"; -import {replaceableComponent} from "../../../utils/replaceableComponent"; +import { replaceableComponent } from "../../../utils/replaceableComponent"; import SettingsStore from "../../../settings/SettingsStore"; +import { MatrixEvent } from 'matrix-js-sdk/src/models/event'; +import { Room } from 'matrix-js-sdk/src/models/room'; +import { RoomMember } from 'matrix-js-sdk/src/models/room-member'; +import { RoomState } from 'matrix-js-sdk/src/models/room-state'; +import { User } from "matrix-js-sdk/src/models/user"; +import TruncatedList from '../elements/TruncatedList'; +import Spinner from "../elements/Spinner"; +import SearchBox from "../../structures/SearchBox"; +import AccessibleButton from '../elements/AccessibleButton'; +import EntityTile from "./EntityTile"; +import MemberTile from "./MemberTile"; +import BaseAvatar from '../avatars/BaseAvatar'; const INITIAL_LOAD_NUM_MEMBERS = 30; const INITIAL_LOAD_NUM_INVITED = 5; @@ -40,41 +52,62 @@ const SHOW_MORE_INCREMENT = 100; // matches all ASCII punctuation: !"#$%&'()*+,-./:;<=>?@[\]^_`{|}~ const SORT_REGEX = /[\x21-\x2F\x3A-\x40\x5B-\x60\x7B-\x7E]+/g; +interface IProps { + roomId: string; + onClose(): void; +} + +interface IState { + loading: boolean; + members: Array; + filteredJoinedMembers: Array; + filteredInvitedMembers: Array; + canInvite: boolean; + truncateAtJoined: number; + truncateAtInvited: number; + searchQuery: string; +} + @replaceableComponent("views.rooms.MemberList") -export default class MemberList extends React.Component { +export default class MemberList extends React.Component { + private showPresence = true; + private mounted = false; + private collator: Intl.Collator; + private sortNames = new Map(); // RoomMember -> sortName + constructor(props) { super(props); const cli = MatrixClientPeg.get(); if (cli.hasLazyLoadMembersEnabled()) { // show an empty list - this.state = this._getMembersState([]); + this.state = this.getMembersState([]); } else { - this.state = this._getMembersState(this.roomMembers()); + this.state = this.getMembersState(this.roomMembers()); } cli.on("Room", this.onRoom); // invites & joining after peek const enablePresenceByHsUrl = SdkConfig.get()["enable_presence_by_hs_url"]; const hsUrl = MatrixClientPeg.get().baseUrl; - this._showPresence = true; + this.showPresence = true; if (enablePresenceByHsUrl && enablePresenceByHsUrl[hsUrl] !== undefined) { - this._showPresence = enablePresenceByHsUrl[hsUrl]; + this.showPresence = enablePresenceByHsUrl[hsUrl]; } } // eslint-disable-next-line camelcase UNSAFE_componentWillMount() { const cli = MatrixClientPeg.get(); - this._mounted = true; + this.mounted = true; if (cli.hasLazyLoadMembersEnabled()) { - this._showMembersAccordingToMembershipWithLL(); + this.showMembersAccordingToMembershipWithLL(); cli.on("Room.myMembership", this.onMyMembership); } else { - this._listenForMembersChanges(); + this.listenForMembersChanges(); } } - _listenForMembersChanges() { + private listenForMembersChanges(): void { const cli = MatrixClientPeg.get(); cli.on("RoomState.members", this.onRoomStateMember); cli.on("RoomMember.name", this.onRoomMemberName); @@ -89,7 +122,7 @@ export default class MemberList extends React.Component { } componentWillUnmount() { - this._mounted = false; + this.mounted = false; const cli = MatrixClientPeg.get(); if (cli) { cli.removeListener("RoomState.members", this.onRoomStateMember); @@ -103,7 +136,7 @@ export default class MemberList extends React.Component { } // cancel any pending calls to the rate_limited_funcs - this._updateList.cancelPendingCall(); + this.updateList.cancelPendingCall(); } /** @@ -111,7 +144,7 @@ export default class MemberList extends React.Component { * show a spinner and load the members if the user is joined, * or show the members available so far if the user is invited */ - async _showMembersAccordingToMembershipWithLL() { + private async showMembersAccordingToMembershipWithLL(): Promise { const cli = MatrixClientPeg.get(); if (cli.hasLazyLoadMembersEnabled()) { const cli = MatrixClientPeg.get(); @@ -122,31 +155,31 @@ export default class MemberList extends React.Component { try { await room.loadMembersIfNeeded(); } catch (ex) {/* already logged in RoomView */} - if (this._mounted) { - this.setState(this._getMembersState(this.roomMembers())); - this._listenForMembersChanges(); + if (this.mounted) { + this.setState(this.getMembersState(this.roomMembers())); + this.listenForMembersChanges(); } } else { // show the members we already have loaded - this.setState(this._getMembersState(this.roomMembers())); + this.setState(this.getMembersState(this.roomMembers())); } } } - get canInvite() { + private get canInvite(): boolean { const cli = MatrixClientPeg.get(); const room = cli.getRoom(this.props.roomId); return room && room.canInvite(cli.getUserId()); } - _getMembersState(members) { - // set the state after determining _showPresence to make sure it's - // taken into account while rerendering + private getMembersState(members: Array): IState { + // set the state after determining showPresence to make sure it's + // taken into account while rendering return { loading: false, members: members, - filteredJoinedMembers: this._filterMembers(members, 'join'), - filteredInvitedMembers: this._filterMembers(members, 'invite'), + filteredJoinedMembers: this.filterMembers(members, 'join'), + filteredInvitedMembers: this.filterMembers(members, 'invite'), canInvite: this.canInvite, // ideally we'd size this to the page height, but @@ -157,72 +190,72 @@ export default class MemberList extends React.Component { }; } - onUserPresenceChange = (event, user) => { + private onUserPresenceChange = (event: MatrixEvent, user: User): void => { // Attach a SINGLE listener for global presence changes then locate the // member tile and re-render it. This is more efficient than every tile // ever attaching their own listener. const tile = this.refs[user.userId]; // console.log(`Got presence update for ${user.userId}. hasTile=${!!tile}`); if (tile) { - this._updateList(); // reorder the membership list + this.updateList(); // reorder the membership list } }; - onRoom = room => { + private onRoom = (room: Room): void => { if (room.roomId !== this.props.roomId) { return; } // We listen for room events because when we accept an invite // we need to wait till the room is fully populated with state // before refreshing the member list else we get a stale list. - this._showMembersAccordingToMembershipWithLL(); + this.showMembersAccordingToMembershipWithLL(); }; - onMyMembership = (room, membership, oldMembership) => { + private onMyMembership = (room: Room, membership: string, oldMembership: string): void => { if (room.roomId === this.props.roomId && membership === "join") { - this._showMembersAccordingToMembershipWithLL(); + this.showMembersAccordingToMembershipWithLL(); } }; - onRoomStateMember = (ev, state, member) => { + private onRoomStateMember = (ev: MatrixEvent, state: RoomState, member: RoomMember): void => { if (member.roomId !== this.props.roomId) { return; } - this._updateList(); + this.updateList(); }; - onRoomMemberName = (ev, member) => { + private onRoomMemberName = (ev: MatrixEvent, member: RoomMember): void => { if (member.roomId !== this.props.roomId) { return; } - this._updateList(); + this.updateList(); }; - onRoomStateEvent = (event, state) => { + private onRoomStateEvent = (event: MatrixEvent, state: RoomState): void => { if (event.getRoomId() === this.props.roomId && event.getType() === "m.room.third_party_invite") { - this._updateList(); + this.updateList(); } if (this.canInvite !== this.state.canInvite) this.setState({ canInvite: this.canInvite }); }; - _updateList = rate_limited_func(() => { - this._updateListNow(); + private updateList = rateLimitedFunction(() => { + this.updateListNow(); }, 500); - _updateListNow() { - // console.log("Updating memberlist"); - const newState = { + private updateListNow(): void { + const members = this.roomMembers() + + this.setState({ loading: false, - members: this.roomMembers(), - }; - newState.filteredJoinedMembers = this._filterMembers(newState.members, 'join', this.state.searchQuery); - newState.filteredInvitedMembers = this._filterMembers(newState.members, 'invite', this.state.searchQuery); - this.setState(newState); + members: members, + filteredJoinedMembers: this.filterMembers(members, 'join', this.state.searchQuery), + filteredInvitedMembers: this.filterMembers(members, 'invite', this.state.searchQuery), + }); } - getMembersWithUser() { + private getMembersWithUser(): Array { if (!this.props.roomId) return []; const cli = MatrixClientPeg.get(); const room = cli.getRoom(this.props.roomId); @@ -230,15 +263,18 @@ export default class MemberList extends React.Component { const allMembers = Object.values(room.currentState.members); - allMembers.forEach(function(member) { + allMembers.forEach((member) => { // work around a race where you might have a room member object - // before the user object exists. This may or may not cause + // before the user object exists. This may or may not cause // https://github.com/vector-im/vector-web/issues/186 - if (member.user === null) { + if (!member.user) { member.user = cli.getUser(member.userId); } - member.sortName = (member.name[0] === '@' ? member.name.substr(1) : member.name).replace(SORT_REGEX, ""); + this.sortNames.set( + member, + (member.name[0] === '@' ? member.name.substr(1) : member.name).replace(SORT_REGEX, ""), + ); // XXX: this user may have no lastPresenceTs value! // the right solution here is to fix the race rather than leave it as 0 @@ -247,7 +283,7 @@ export default class MemberList extends React.Component { return allMembers; } - roomMembers() { + private roomMembers(): Array { const allMembers = this.getMembersWithUser(); const filteredAndSortedMembers = allMembers.filter((m) => { return ( @@ -255,23 +291,21 @@ export default class MemberList extends React.Component { ); }); const language = SettingsStore.getValue("language"); - this.collator = new Intl.Collator(language, { sensitivity: 'base', usePunctuation: true }); + this.collator = new Intl.Collator(language, { sensitivity: 'base', ignorePunctuation: false }); filteredAndSortedMembers.sort(this.memberSort); return filteredAndSortedMembers; } - _createOverflowTileJoined = (overflowCount, totalCount) => { - return this._createOverflowTile(overflowCount, totalCount, this._showMoreJoinedMemberList); + private createOverflowTileJoined = (overflowCount: number, totalCount: number): JSX.Element => { + return this.createOverflowTile(overflowCount, totalCount, this.showMoreJoinedMemberList); }; - _createOverflowTileInvited = (overflowCount, totalCount) => { - return this._createOverflowTile(overflowCount, totalCount, this._showMoreInvitedMemberList); + private createOverflowTileInvited = (overflowCount: number, totalCount: number): JSX.Element => { + return this.createOverflowTile(overflowCount, totalCount, this.showMoreInvitedMemberList); }; - _createOverflowTile = (overflowCount, totalCount, onClick) => { + private createOverflowTile = (overflowCount: number, totalCount: number, onClick: () => void): JSX.Element=> { // For now we'll pretend this is any entity. It should probably be a separate tile. - const EntityTile = sdk.getComponent("rooms.EntityTile"); - const BaseAvatar = sdk.getComponent("avatars.BaseAvatar"); const text = _t("and %(count)s others...", { count: overflowCount }); return ( { + private showMoreJoinedMemberList = (): void => { this.setState({ truncateAtJoined: this.state.truncateAtJoined + SHOW_MORE_INCREMENT, }); }; - _showMoreInvitedMemberList = () => { + private showMoreInvitedMemberList = (): void => { this.setState({ truncateAtInvited: this.state.truncateAtInvited + SHOW_MORE_INCREMENT, }); }; - memberString(member) { + /** + * SHOULD ONLY BE USED BY TESTS + */ + public memberString(member: RoomMember): string { if (!member) { return "(null)"; } else { const u = member.user; - return "(" + member.name + ", " + member.powerLevel + ", " + (u ? u.lastActiveAgo : "") + ", " + (u ? u.getLastActiveTs() : "") + ", " + (u ? u.currentlyActive : "") + ", " + (u ? u.presence : "") + ")"; + return ( + "(" + + member.name + + ", " + + member.powerLevel + + ", " + + (u ? u.lastActiveAgo : "") + + ", " + + (u ? u.getLastActiveTs() : "") + + ", " + + (u ? u.currentlyActive : "") + + ", " + + (u ? u.presence : "") + + ")" + ); } } // returns negative if a comes before b, // returns 0 if a and b are equivalent in ordering // returns positive if a comes after b. - memberSort = (memberA, memberB) => { + private memberSort = (memberA: RoomMember, memberB: RoomMember): number => { // order by presence, with "active now" first. // ...and then by power level // ...and then by last active @@ -325,7 +376,7 @@ export default class MemberList extends React.Component { if (!userA && userB) return 1; // First by presence - if (this._showPresence) { + if (this.showPresence) { const convertPresence = (p) => p === 'unavailable' ? 'online' : p; const presenceIndex = p => { const order = ['active', 'online', 'offline']; @@ -349,31 +400,31 @@ export default class MemberList extends React.Component { } // Third by last active - if (this._showPresence && userA.getLastActiveTs() !== userB.getLastActiveTs()) { + if (this.showPresence && userA.getLastActiveTs() !== userB.getLastActiveTs()) { // console.log("Comparing on last active timestamp - returning"); return userB.getLastActiveTs() - userA.getLastActiveTs(); } // Fourth by name (alphabetical) - return this.collator.compare(memberA.sortName, memberB.sortName); + return this.collator.compare(this.sortNames.get(memberA), this.sortNames.get(memberB)); }; - onSearchQueryChanged = searchQuery => { + private onSearchQueryChanged = (searchQuery: string): void => { this.setState({ searchQuery, - filteredJoinedMembers: this._filterMembers(this.state.members, 'join', searchQuery), - filteredInvitedMembers: this._filterMembers(this.state.members, 'invite', searchQuery), + filteredJoinedMembers: this.filterMembers(this.state.members, 'join', searchQuery), + filteredInvitedMembers: this.filterMembers(this.state.members, 'invite', searchQuery), }); }; - _onPending3pidInviteClick = inviteEvent => { + private onPending3pidInviteClick = (inviteEvent: MatrixEvent): void => { dis.dispatch({ action: 'view_3pid_invite', event: inviteEvent, }); }; - _filterMembers(members, membership, query) { + private filterMembers(members: Array, membership: string, query?: string): Array { return members.filter((m) => { if (query) { query = query.toLowerCase(); @@ -389,7 +440,7 @@ export default class MemberList extends React.Component { }); } - _getPending3PidInvites() { + private getPending3PidInvites(): Array { // include 3pid invites (m.room.third_party_invite) state events. // The HS may have already converted these into m.room.member invites so // we shouldn't add them if the 3pid invite state key (token) is in the @@ -409,42 +460,40 @@ export default class MemberList extends React.Component { } } - _makeMemberTiles(members) { - const MemberTile = sdk.getComponent("rooms.MemberTile"); - const EntityTile = sdk.getComponent("rooms.EntityTile"); - + private makeMemberTiles(members: Array) { return members.map((m) => { - if (m.userId) { + if (m instanceof RoomMember) { // Is a Matrix invite - return ; + return ; } else { // Is a 3pid invite return this._onPending3pidInviteClick(m)} />; + onClick={() => this.onPending3pidInviteClick(m)} />; } }); } - _getChildrenJoined = (start, end) => this._makeMemberTiles(this.state.filteredJoinedMembers.slice(start, end)); - - _getChildCountJoined = () => this.state.filteredJoinedMembers.length; - - _getChildrenInvited = (start, end) => { - let targets = this.state.filteredInvitedMembers; - if (end > this.state.filteredInvitedMembers.length) { - targets = targets.concat(this._getPending3PidInvites()); - } - - return this._makeMemberTiles(targets.slice(start, end)); + private getChildrenJoined = (start: number, end: number): Array => { + return this.makeMemberTiles(this.state.filteredJoinedMembers.slice(start, end)) }; - _getChildCountInvited = () => { - return this.state.filteredInvitedMembers.length + (this._getPending3PidInvites() || []).length; + private getChildCountJoined = (): number => this.state.filteredJoinedMembers.length; + + private getChildrenInvited = (start: number, end: number): Array => { + let targets = this.state.filteredInvitedMembers; + if (end > this.state.filteredInvitedMembers.length) { + targets = targets.concat(this.getPending3PidInvites()); + } + + return this.makeMemberTiles(targets.slice(start, end)); + }; + + private getChildCountInvited = (): number => { + return this.state.filteredInvitedMembers.length + (this.getPending3PidInvites() || []).length; } render() { if (this.state.loading) { - const Spinner = sdk.getComponent("elements.Spinner"); return ; } - const SearchBox = sdk.getComponent('structures.SearchBox'); - const TruncatedList = sdk.getComponent("elements.TruncatedList"); - const cli = MatrixClientPeg.get(); const room = cli.getRoom(this.props.roomId); let inviteButton; @@ -470,22 +516,30 @@ export default class MemberList extends React.Component { inviteButtonText = _t("Invite to this space"); } - const AccessibleButton = sdk.getComponent("elements.AccessibleButton"); - inviteButton = - + inviteButton = ( + { inviteButtonText } - ; + + ); } let invitedHeader; let invitedSection; - if (this._getChildCountInvited() > 0) { + if (this.getChildCountInvited() > 0) { invitedHeader =

    { _t("Invited") }

    ; - invitedSection = ; + invitedSection = ( + + ); } const footer = ( @@ -517,17 +571,19 @@ export default class MemberList extends React.Component { previousPhase={previousPhase} >
    - + { invitedHeader } { invitedSection }
    ; } - onInviteButtonClick = () => { + onInviteButtonClick = (): void => { if (MatrixClientPeg.get().isGuest()) { dis.dispatch({action: 'require_registration'}); return; From 0df6200dd0f5ac87c5897a4d7f014ce2d93afed0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Wed, 23 Jun 2021 14:32:41 +0200 Subject: [PATCH 248/809] Convert MemberList-test to TS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- ...MemberList-test.js => MemberList-test.tsx} | 53 +++++++++++++------ 1 file changed, 37 insertions(+), 16 deletions(-) rename test/components/views/rooms/{MemberList-test.js => MemberList-test.tsx} (88%) diff --git a/test/components/views/rooms/MemberList-test.js b/test/components/views/rooms/MemberList-test.tsx similarity index 88% rename from test/components/views/rooms/MemberList-test.js rename to test/components/views/rooms/MemberList-test.tsx index 28fead770c..8012c43c4b 100644 --- a/test/components/views/rooms/MemberList-test.js +++ b/test/components/views/rooms/MemberList-test.tsx @@ -1,21 +1,36 @@ +/* +Copyright 2021 Šimon Brandner + +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 ReactTestUtils from 'react-dom/test-utils'; import ReactDOM from 'react-dom'; import * as TestUtils from '../../../test-utils'; - -import {MatrixClientPeg} from '../../../../src/MatrixClientPeg'; import sdk from '../../../skinned-sdk'; - -import {Room, RoomMember, User} from 'matrix-js-sdk'; - +import { MatrixClientPeg } from '../../../../src/MatrixClientPeg'; +import { Room } from 'matrix-js-sdk/src/models/room'; +import { RoomMember } from 'matrix-js-sdk/src/models/room-member'; +import { User } from "matrix-js-sdk/src/models/user"; import { compare } from "../../../../src/utils/strings"; +import MemberList from "../../../../src/components/views/rooms/MemberList"; function generateRoomId() { return '!' + Math.random().toString().slice(2, 10) + ':domain'; } - describe('MemberList', () => { function createRoom(opts) { const room = new Room(generateRoomId(), null, client.getUserId()); @@ -97,13 +112,19 @@ describe('MemberList', () => { memberListRoom.currentState.members[member.userId] = member; } - const MemberList = sdk.getComponent('views.rooms.MemberList'); const WrappedMemberList = TestUtils.wrapInMatrixClientContext(MemberList); const gatherWrappedRef = (r) => { memberList = r; }; - root = ReactDOM.render(, parentDiv); + root = ReactDOM.render( + ( + + ), + parentDiv, + ); }); afterEach((done) => { @@ -213,8 +234,8 @@ describe('MemberList', () => { }); // Bypass all the event listeners and skip to the good part - memberList._showPresence = enablePresence; - memberList._updateListNow(); + memberList.showPresence = enablePresence; + memberList.updateListNow(); const tiles = ReactTestUtils.scryRenderedComponentsWithType(root, MemberTile); expectOrderedByPresenceAndPowerLevel(tiles, enablePresence); @@ -225,7 +246,7 @@ describe('MemberList', () => { // Bypass all the event listeners and skip to the good part memberList._showPresence = enablePresence; - memberList._updateListNow(); + memberList.updateListNow(); const tiles = ReactTestUtils.scryRenderedComponentsWithType(root, MemberTile); expectOrderedByPresenceAndPowerLevel(tiles, enablePresence); @@ -254,8 +275,8 @@ describe('MemberList', () => { }); // Bypass all the event listeners and skip to the good part - memberList._showPresence = enablePresence; - memberList._updateListNow(); + memberList.showPresence = enablePresence; + memberList.updateListNow(); const tiles = ReactTestUtils.scryRenderedComponentsWithType(root, MemberTile); expectOrderedByPresenceAndPowerLevel(tiles, enablePresence); @@ -273,8 +294,8 @@ describe('MemberList', () => { }); // Bypass all the event listeners and skip to the good part - memberList._showPresence = enablePresence; - memberList._updateListNow(); + memberList.showPresence = enablePresence; + memberList.updateListNow(); const tiles = ReactTestUtils.scryRenderedComponentsWithType(root, MemberTile); expectOrderedByPresenceAndPowerLevel(tiles, enablePresence); From b7a821a9e4d7bbe99f767b2fb86850936d99ebed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Wed, 23 Jun 2021 14:59:36 +0200 Subject: [PATCH 249/809] .tsx can also be tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6b369e9c27..f87abf9e44 100644 --- a/package.json +++ b/package.json @@ -172,7 +172,7 @@ "jest": { "testEnvironment": "./__test-utils__/environment.js", "testMatch": [ - "/test/**/*-test.[jt]s" + "/test/**/*-test.[jt]s?(x)" ], "setupFiles": [ "jest-canvas-mock" From 5d93216c94f1f76a99fd30b4bbcd73459446ee2a Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 23 Jun 2021 16:10:47 +0100 Subject: [PATCH 250/809] Decrease e2e shield fill mask size so that it doesn't overlap --- res/css/structures/_ToastContainer.scss | 2 +- res/css/views/messages/_common_CryptoEvent.scss | 2 +- res/css/views/rooms/_E2EIcon.scss | 4 ++-- res/css/views/rooms/_EventTile.scss | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/res/css/structures/_ToastContainer.scss b/res/css/structures/_ToastContainer.scss index 14e4c01389..35d6087a1b 100644 --- a/res/css/structures/_ToastContainer.scss +++ b/res/css/structures/_ToastContainer.scss @@ -71,7 +71,7 @@ limitations under the License. &::before { background-color: #ffffff; mask-image: url('$(res)/img/e2e/normal.svg'); - mask-size: 90%; + mask-size: 80%; } &::after { diff --git a/res/css/views/messages/_common_CryptoEvent.scss b/res/css/views/messages/_common_CryptoEvent.scss index 4faa4b594f..bcc40f1181 100644 --- a/res/css/views/messages/_common_CryptoEvent.scss +++ b/res/css/views/messages/_common_CryptoEvent.scss @@ -21,7 +21,7 @@ limitations under the License. mask-image: url('$(res)/img/e2e/normal.svg'); mask-repeat: no-repeat; mask-position: center; - mask-size: 90%; + mask-size: 80%; } &.mx_cryptoEvent_icon::after { diff --git a/res/css/views/rooms/_E2EIcon.scss b/res/css/views/rooms/_E2EIcon.scss index a3473dedec..68ad44cf6a 100644 --- a/res/css/views/rooms/_E2EIcon.scss +++ b/res/css/views/rooms/_E2EIcon.scss @@ -45,7 +45,7 @@ limitations under the License. mask-image: url('$(res)/img/e2e/normal.svg'); mask-repeat: no-repeat; mask-position: center; - mask-size: 90%; + mask-size: 80%; } // transparent-looking border surrounding the shield for when overlain over avatars @@ -59,7 +59,7 @@ limitations under the License. } // shrink the infill of the badge &::before { - mask-size: 65%; + mask-size: 60%; } } diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index 3af266caee..27a83e58f8 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -345,7 +345,7 @@ $hover-select-border: 4px; mask-image: url('$(res)/img/e2e/normal.svg'); mask-repeat: no-repeat; mask-position: center; - mask-size: 90%; + mask-size: 80%; } } From e696a1d5dc4d6848e47caf64e0c0c58bda659c8a Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 23 Jun 2021 10:31:08 -0600 Subject: [PATCH 251/809] Update membership reason handling, including leave reason displaying Incorporates ideas from https://github.com/matrix-org/matrix-react-sdk/pull/6198 --- src/TextForEvent.ts | 47 +++++++++++++++++++++++-------------- src/i18n/strings/en_EN.json | 6 ++++- 2 files changed, 35 insertions(+), 18 deletions(-) diff --git a/src/TextForEvent.ts b/src/TextForEvent.ts index 649c53664e..55a7813c6f 100644 --- a/src/TextForEvent.ts +++ b/src/TextForEvent.ts @@ -31,8 +31,8 @@ function textForMemberEvent(ev): () => string | null { const targetName = ev.target ? ev.target.name : ev.getStateKey(); const prevContent = ev.getPrevContent(); const content = ev.getContent(); + const reason = content.reason; - const getReason = () => content.reason ? (_t('Reason') + ': ' + content.reason) : ''; switch (content.membership) { case 'invite': { const threePidContent = content.third_party_invite; @@ -43,14 +43,16 @@ function textForMemberEvent(ev): () => string | null { displayName: threePidContent.display_name, }); } else { - return () => _t('%(targetName)s accepted an invitation.', {targetName}); + return () => _t('%(targetName)s accepted an invitation.', { targetName }); } } else { - return () => _t('%(senderName)s invited %(targetName)s.', {senderName, targetName}); + return () => _t('%(senderName)s invited %(targetName)s.', { senderName, targetName }); } } case 'ban': - return () => _t('%(senderName)s banned %(targetName)s.', {senderName, targetName}) + ' ' + getReason(); + return () => reason + ? _t('%(senderName)s banned %(targetName)s. Reason: %(reason)s', { senderName, targetName, reason }) + : _t('%(senderName)s banned %(targetName)s.', { senderName, targetName }); case 'join': if (prevContent && prevContent.membership === 'join') { if (prevContent.displayname && content.displayname && prevContent.displayname !== content.displayname) { @@ -69,38 +71,49 @@ function textForMemberEvent(ev): () => string | null { oldDisplayName: prevContent.displayname, }); } else if (prevContent.avatar_url && !content.avatar_url) { - return () => _t('%(senderName)s removed their profile picture.', {senderName}); + return () => _t('%(senderName)s removed their profile picture.', { senderName }); } else if (prevContent.avatar_url && content.avatar_url && prevContent.avatar_url !== content.avatar_url) { - return () => _t('%(senderName)s changed their profile picture.', {senderName}); + return () => _t('%(senderName)s changed their profile picture.', { senderName }); } else if (!prevContent.avatar_url && content.avatar_url) { - return () => _t('%(senderName)s set a profile picture.', {senderName}); + return () => _t('%(senderName)s set a profile picture.', { senderName }); } else if (SettingsStore.getValue("showHiddenEventsInTimeline")) { // This is a null rejoin, it will only be visible if the Labs option is enabled - return () => _t("%(senderName)s made no change.", {senderName}); + return () => _t("%(senderName)s made no change.", { senderName }); } else { return null; } } else { if (!ev.target) console.warn("Join message has no target! -- " + ev.getContent().state_key); - return () => _t('%(targetName)s joined the room.', {targetName}); + return () => _t('%(targetName)s joined the room.', { targetName }); } case 'leave': if (ev.getSender() === ev.getStateKey()) { if (prevContent.membership === "invite") { - return () => _t('%(targetName)s rejected the invitation.', {targetName}); + return () => _t('%(targetName)s rejected the invitation.', { targetName }); } else { - return () => _t('%(targetName)s left the room.', {targetName}); + return () => reason + ? _t('%(targetName)s left the room. Reason: %(reason)s', { targetName, reason }) + : _t('%(targetName)s left the room.', { targetName }); } } else if (prevContent.membership === "ban") { - return () => _t('%(senderName)s unbanned %(targetName)s.', {senderName, targetName}); + return () => _t('%(senderName)s unbanned %(targetName)s.', { senderName, targetName }); } else if (prevContent.membership === "invite") { - return () => _t('%(senderName)s withdrew %(targetName)s\'s invitation.', { - senderName, - targetName, - }) + ' ' + getReason(); + return () => reason + ? _t('%(senderName)s withdrew %(targetName)s\'s invitation. Reason: %(reason)s', { + senderName, + targetName, + reason, + }) + : _t('%(senderName)s withdrew %(targetName)s\'s invitation.', { senderName, targetName }) } else if (prevContent.membership === "join") { - return () => _t('%(senderName)s kicked %(targetName)s.', {senderName, targetName}) + ' ' + getReason(); + return () => reason + ? _t('%(senderName)s kicked %(targetName)s. Reason: %(reason)s', { + senderName, + targetName, + reason, + }) + : _t('%(senderName)s kicked %(targetName)s.', { senderName, targetName }); } else { return null; } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 7751c2eb32..5bee2e2f2c 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -489,10 +489,10 @@ "Converts the room to a DM": "Converts the room to a DM", "Converts the DM to a room": "Converts the DM to a room", "Displays action": "Displays action", - "Reason": "Reason", "%(targetName)s accepted the invitation for %(displayName)s.": "%(targetName)s accepted the invitation for %(displayName)s.", "%(targetName)s accepted an invitation.": "%(targetName)s accepted an invitation.", "%(senderName)s invited %(targetName)s.": "%(senderName)s invited %(targetName)s.", + "%(senderName)s banned %(targetName)s. Reason: %(reason)s": "%(senderName)s banned %(targetName)s. Reason: %(reason)s", "%(senderName)s banned %(targetName)s.": "%(senderName)s banned %(targetName)s.", "%(oldDisplayName)s changed their display name to %(displayName)s.": "%(oldDisplayName)s changed their display name to %(displayName)s.", "%(senderName)s set their display name to %(displayName)s.": "%(senderName)s set their display name to %(displayName)s.", @@ -503,9 +503,12 @@ "%(senderName)s made no change.": "%(senderName)s made no change.", "%(targetName)s joined the room.": "%(targetName)s joined the room.", "%(targetName)s rejected the invitation.": "%(targetName)s rejected the invitation.", + "%(targetName)s left the room. Reason: %(reason)s": "%(targetName)s left the room. Reason: %(reason)s", "%(targetName)s left the room.": "%(targetName)s left the room.", "%(senderName)s unbanned %(targetName)s.": "%(senderName)s unbanned %(targetName)s.", + "%(senderName)s withdrew %(targetName)s's invitation. Reason: %(reason)s": "%(senderName)s withdrew %(targetName)s's invitation. Reason: %(reason)s", "%(senderName)s withdrew %(targetName)s's invitation.": "%(senderName)s withdrew %(targetName)s's invitation.", + "%(senderName)s kicked %(targetName)s. Reason: %(reason)s": "%(senderName)s kicked %(targetName)s. Reason: %(reason)s", "%(senderName)s kicked %(targetName)s.": "%(senderName)s kicked %(targetName)s.", "%(senderDisplayName)s changed the topic to \"%(topic)s\".": "%(senderDisplayName)s changed the topic to \"%(topic)s\".", "%(senderDisplayName)s removed the room name.": "%(senderDisplayName)s removed the room name.", @@ -1410,6 +1413,7 @@ "Failed to unban": "Failed to unban", "Unban": "Unban", "Banned by %(displayName)s": "Banned by %(displayName)s", + "Reason": "Reason", "Error changing power level requirement": "Error changing power level requirement", "An error occurred changing the room's power level requirements. Ensure you have sufficient permissions and try again.": "An error occurred changing the room's power level requirements. Ensure you have sufficient permissions and try again.", "Error changing power level": "Error changing power level", From e290fdaabcb0259b26412b34b3ec4cd11e76164a Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 23 Jun 2021 11:21:56 -0600 Subject: [PATCH 252/809] Update widget-api for https://github.com/matrix-org/matrix-react-sdk/pull/6178 --- package.json | 2 +- yarn.lock | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 6b369e9c27..bc79b791d4 100644 --- a/package.json +++ b/package.json @@ -79,7 +79,7 @@ "linkifyjs": "^2.1.9", "lodash": "^4.17.20", "matrix-js-sdk": "12.0.0", - "matrix-widget-api": "^0.1.0-beta.14", + "matrix-widget-api": "^0.1.0-beta.15", "minimist": "^1.2.5", "opus-recorder": "^8.0.3", "pako": "^2.0.3", diff --git a/yarn.lock b/yarn.lock index 32ca30a996..3bcb8de404 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1334,6 +1334,7 @@ "@matrix-org/olm@https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.3.tgz": version "3.2.3" + uid cc332fdd25c08ef0e40f4d33fc3f822a0f98b6f4 resolved "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.3.tgz#cc332fdd25c08ef0e40f4d33fc3f822a0f98b6f4" "@nicolo-ribaudo/chokidar-2@2.1.8-no-fsevents": @@ -5772,10 +5773,10 @@ matrix-react-test-utils@^0.2.3: "@babel/traverse" "^7.13.17" walk "^2.3.14" -matrix-widget-api@^0.1.0-beta.14: - version "0.1.0-beta.14" - resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-0.1.0-beta.14.tgz#e38beed71c5ebd62c1ac1d79ef262d7150b42c70" - integrity sha512-5tC6LO1vCblKg/Hfzf5U1eHPz1nHUZIobAm3gkEKV5vpYPgRpr8KdkLiGB78VZid0tB17CVtAb4VKI8CQ3lhAQ== +matrix-widget-api@^0.1.0-beta.15: + version "0.1.0-beta.15" + resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-0.1.0-beta.15.tgz#b02511f93fe1a3634868b6e246d736107f182745" + integrity sha512-sWmtb8ZarSbHVbk5ni7IHBR9jOh7m1+5R4soky0fEO9VKl+MN7skT0+qNux3J9WuUAu2D80dZW9xPUT9cxfxbg== dependencies: "@types/events" "^3.0.0" events "^3.2.0" From d5acfc6cf453915deed0bf6ac6bca2b5920d73f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Wed, 23 Jun 2021 17:27:53 +0200 Subject: [PATCH 253/809] Convert EntityTile to TS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- .../rooms/{EntityTile.js => EntityTile.tsx} | 94 ++++++++++--------- 1 file changed, 49 insertions(+), 45 deletions(-) rename src/components/views/rooms/{EntityTile.js => EntityTile.tsx} (75%) diff --git a/src/components/views/rooms/EntityTile.js b/src/components/views/rooms/EntityTile.tsx similarity index 75% rename from src/components/views/rooms/EntityTile.js rename to src/components/views/rooms/EntityTile.tsx index a92d87643c..53c5900fbf 100644 --- a/src/components/views/rooms/EntityTile.js +++ b/src/components/views/rooms/EntityTile.tsx @@ -16,14 +16,19 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; -import PropTypes from 'prop-types'; -import * as sdk from '../../../index'; +import React, { createRef } from 'react'; import AccessibleButton from '../elements/AccessibleButton'; -import { _t } from '../../../languageHandler'; +import { _td } from '../../../languageHandler'; import classNames from "classnames"; import E2EIcon from './E2EIcon'; -import {replaceableComponent} from "../../../utils/replaceableComponent"; +import { replaceableComponent } from "../../../utils/replaceableComponent"; +import BaseAvatar from '../avatars/BaseAvatar'; +import PresenceLabel from "./PresenceLabel"; + +const PowerLabel: Record = { + "admin": _td("Admin"), + "moderator": _td("Mod"), +} const PRESENCE_CLASS = { "offline": "mx_EntityTile_offline", @@ -31,14 +36,14 @@ const PRESENCE_CLASS = { "unavailable": "mx_EntityTile_unavailable", }; -function presenceClassForMember(presenceState, lastActiveAgo, showPresence) { +function presenceClassForMember(presenceState: string, lastActiveAgo: number, showPresence: boolean) { if (showPresence === false) { return 'mx_EntityTile_online_beenactive'; } // offline is split into two categories depending on whether we have // a last_active_ago for them. - if (presenceState == 'offline') { + if (presenceState === 'offline') { if (lastActiveAgo) { return PRESENCE_CLASS['offline'] + '_beenactive'; } else { @@ -51,29 +56,34 @@ function presenceClassForMember(presenceState, lastActiveAgo, showPresence) { } } -@replaceableComponent("views.rooms.EntityTile") -class EntityTile extends React.Component { - static propTypes = { - name: PropTypes.string, - title: PropTypes.string, - avatarJsx: PropTypes.any, // - className: PropTypes.string, - presenceState: PropTypes.string, - presenceLastActiveAgo: PropTypes.number, - presenceLastTs: PropTypes.number, - presenceCurrentlyActive: PropTypes.bool, - showInviteButton: PropTypes.bool, - shouldComponentUpdate: PropTypes.func, - onClick: PropTypes.func, - suppressOnHover: PropTypes.bool, - showPresence: PropTypes.bool, - subtextLabel: PropTypes.string, - e2eStatus: PropTypes.string, - }; +interface IProps { + name?: string, + title?: string, + avatarJsx?: JSX.Element, // + className?: string, + presenceState?: string, + presenceLastActiveAgo?: number, + presenceLastTs?: number, + presenceCurrentlyActive?: boolean, + showInviteButton?: boolean, + shouldComponentUpdate?(nextProps: IProps, nextState: IState): boolean, + onClick?(): void, + suppressOnHover?: boolean, + showPresence?: boolean, + subtextLabel?: string, + e2eStatus?: string, + powerStatus?: string, +} +interface IState { + hover: boolean; +} + +@replaceableComponent("views.rooms.EntityTile") +export default class EntityTile extends React.Component { static defaultProps = { - shouldComponentUpdate: function(nextProps, nextState) { return true; }, - onClick: function() {}, + shouldComponentUpdate: (nextProps: IProps, nextState: IState) => { return true; }, + onClick: () => {}, presenceState: "offline", presenceLastActiveAgo: 0, presenceLastTs: 0, @@ -81,12 +91,17 @@ class EntityTile extends React.Component { suppressOnHover: false, showPresence: true, }; + private container = createRef(); - state = { - hover: false, - }; + constructor(props: IProps) { + super(props); - shouldComponentUpdate(nextProps, nextState) { + this.state = { + hover: false, + }; + } + + shouldComponentUpdate(nextProps: IProps, nextState: IState) { if (this.state.hover !== nextState.hover) return true; return this.props.shouldComponentUpdate(nextProps, nextState); } @@ -110,7 +125,6 @@ class EntityTile extends React.Component { const activeAgo = this.props.presenceLastActiveAgo ? (Date.now() - (this.props.presenceLastTs - this.props.presenceLastActiveAgo)) : -1; - const PresenceLabel = sdk.getComponent("rooms.PresenceLabel"); let presenceLabel = null; if (this.props.showPresence) { presenceLabel = {powerText}
    ; } @@ -168,14 +179,12 @@ class EntityTile extends React.Component { e2eIcon = ; } - const BaseAvatar = sdk.getComponent('avatars.BaseAvatar'); - const av = this.props.avatarJsx ||