From d282675bc643058c4835db499e68d7b34af0e13f Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sun, 13 Oct 2019 15:08:50 +0300 Subject: [PATCH 0001/1171] 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 0002/1171] 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 0003/1171] 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 0004/1171] 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 0005/1171] 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 0006/1171] 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 0007/1171] 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 0008/1171] 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 0009/1171] 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 0010/1171] 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 0011/1171] 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 0012/1171] 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 0013/1171] 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 0014/1171] 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 0015/1171] 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 ee8d1f51c2733e1e2550fc06868fc50266f69d0f Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Tue, 3 Nov 2020 15:51:23 +0000
Subject: [PATCH 0016/1171] Fix onPaste handler to work with copying files from
 Finder

---
 src/components/views/rooms/SendMessageComposer.js | 12 +++++-------
 1 file changed, 5 insertions(+), 7 deletions(-)

diff --git a/src/components/views/rooms/SendMessageComposer.js b/src/components/views/rooms/SendMessageComposer.js
index 9438cceef5..c816c84c9d 100644
--- a/src/components/views/rooms/SendMessageComposer.js
+++ b/src/components/views/rooms/SendMessageComposer.js
@@ -445,13 +445,11 @@ export default class SendMessageComposer extends React.Component {
 
     _onPaste = (event) => {
         const {clipboardData} = event;
-        // Prioritize text on the clipboard over files as Office on macOS puts a bitmap
-        // in the clipboard as well as the content being copied.
-        if (clipboardData.files.length && !clipboardData.types.some(t => t === "text/plain")) {
-            // This actually not so much for 'files' as such (at time of writing
-            // neither chrome nor firefox let you paste a plain file copied
-            // from Finder) but more images copied from a different website
-            // / word processor etc.
+        // Prioritize text on the clipboard over files if RTF is present as Office on macOS puts a bitmap
+        // in the clipboard as well as the content being copied. Modern versions of Office seem to not do this anymore.
+        // We check text/rtf instead of text/plain as when copy+pasting a file from Finder it puts the filename
+        // in as text/plain which we want to ignore.
+        if (clipboardData.files.length && !clipboardData.types.includes("text/rtf")) {
             ContentMessages.sharedInstance().sendContentListToRoom(
                 Array.from(clipboardData.files), this.props.room.roomId, this.context,
             );

From c6a058fb6fa0af190c117c2f3b5bc01a6eab17e0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= 
Date: Sat, 19 Dec 2020 19:32:58 +0100
Subject: [PATCH 0017/1171] Added surround with
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Šimon Brandner 
---
 .../views/rooms/BasicMessageComposer.tsx      | 41 +++++++++++++++++++
 1 file changed, 41 insertions(+)

diff --git a/src/components/views/rooms/BasicMessageComposer.tsx b/src/components/views/rooms/BasicMessageComposer.tsx
index 2ececdeaed..cd34e25926 100644
--- a/src/components/views/rooms/BasicMessageComposer.tsx
+++ b/src/components/views/rooms/BasicMessageComposer.tsx
@@ -418,6 +418,10 @@ export default class BasicMessageEditor extends React.Component
     };
 
     private onKeyDown = (event: React.KeyboardEvent) => {
+        const selectionRange = getRangeForSelection(this.editorRef.current, this.props.model, document.getSelection());
+        // trim the range as we want it to exclude leading/trailing spaces
+        selectionRange.trim();
+
         const model = this.props.model;
         const modKey = IS_MAC ? event.metaKey : event.ctrlKey;
         let handled = false;
@@ -471,6 +475,43 @@ export default class BasicMessageEditor extends React.Component
             });
             handled = true;
         // autocomplete or enter to send below shouldn't have any modifier keys pressed.
+        } else if (document.getSelection().type != "Caret") {
+            if (event.key === '(') {
+                this.historyManager.ensureLastChangesPushed(this.props.model);
+                this.modifiedFlag = true;
+                toggleInlineFormat(selectionRange, "(", ")");
+                handled = true;
+            } else if (event.key === '[') {
+                this.historyManager.ensureLastChangesPushed(this.props.model);
+                this.modifiedFlag = true;
+                toggleInlineFormat(selectionRange, "[", "]");
+                handled = true;
+            } else if (event.key === '{') {
+                this.historyManager.ensureLastChangesPushed(this.props.model);
+                this.modifiedFlag = true;
+                toggleInlineFormat(selectionRange, "{", "}");
+                handled = true;
+            } else if (event.key === '<') {
+                this.historyManager.ensureLastChangesPushed(this.props.model);
+                this.modifiedFlag = true;
+                toggleInlineFormat(selectionRange, "<", ">");
+                handled = true;
+            } else if (event.key === '"') {
+                this.historyManager.ensureLastChangesPushed(this.props.model);
+                this.modifiedFlag = true;
+                toggleInlineFormat(selectionRange, "\"");
+                handled = true;
+            } else if (event.key === '`') {
+                this.historyManager.ensureLastChangesPushed(this.props.model);
+                this.modifiedFlag = true;
+                toggleInlineFormat(selectionRange, "`");
+                handled = true;
+            } else if (event.key === '\'') {
+                this.historyManager.ensureLastChangesPushed(this.props.model);
+                this.modifiedFlag = true;
+                toggleInlineFormat(selectionRange, "'");
+                handled = true;
+            }
         } else {
             const metaOrAltPressed = event.metaKey || event.altKey;
             const modifierPressed = metaOrAltPressed || event.shiftKey;

From e90f5ddf5b6ac14648d7fc36ecf414a04d668c4a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= 
Date: Sat, 19 Dec 2020 19:36:56 +0100
Subject: [PATCH 0018/1171] Added a comment
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Šimon Brandner 
---
 src/components/views/rooms/BasicMessageComposer.tsx | 1 +
 1 file changed, 1 insertion(+)

diff --git a/src/components/views/rooms/BasicMessageComposer.tsx b/src/components/views/rooms/BasicMessageComposer.tsx
index cd34e25926..587f13e8c2 100644
--- a/src/components/views/rooms/BasicMessageComposer.tsx
+++ b/src/components/views/rooms/BasicMessageComposer.tsx
@@ -512,6 +512,7 @@ export default class BasicMessageEditor extends React.Component
                 toggleInlineFormat(selectionRange, "'");
                 handled = true;
             }
+        // Surround selected text with a character
         } else {
             const metaOrAltPressed = event.metaKey || event.altKey;
             const modifierPressed = metaOrAltPressed || event.shiftKey;

From b330dd55a0cd61fe9b014d47c6dcfb085e835b93 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= 
Date: Fri, 12 Feb 2021 07:53:09 +0100
Subject: [PATCH 0019/1171] Hide surround with behind a setting
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Šimon Brandner 
---
 src/components/views/rooms/BasicMessageComposer.tsx          | 3 ++-
 .../views/settings/tabs/user/PreferencesUserSettingsTab.js   | 1 +
 src/settings/Settings.ts                                     | 5 +++++
 3 files changed, 8 insertions(+), 1 deletion(-)

diff --git a/src/components/views/rooms/BasicMessageComposer.tsx b/src/components/views/rooms/BasicMessageComposer.tsx
index 587f13e8c2..a91e92123b 100644
--- a/src/components/views/rooms/BasicMessageComposer.tsx
+++ b/src/components/views/rooms/BasicMessageComposer.tsx
@@ -418,6 +418,7 @@ export default class BasicMessageEditor extends React.Component
     };
 
     private onKeyDown = (event: React.KeyboardEvent) => {
+        const surroundWith = SettingsStore.getValue("MessageComposerInput.surroundWith");
         const selectionRange = getRangeForSelection(this.editorRef.current, this.props.model, document.getSelection());
         // trim the range as we want it to exclude leading/trailing spaces
         selectionRange.trim();
@@ -475,7 +476,7 @@ export default class BasicMessageEditor extends React.Component
             });
             handled = true;
         // autocomplete or enter to send below shouldn't have any modifier keys pressed.
-        } else if (document.getSelection().type != "Caret") {
+        } else if (surroundWith && document.getSelection().type != "Caret") {
             if (event.key === '(') {
                 this.historyManager.ensureLastChangesPushed(this.props.model);
                 this.modifiedFlag = true;
diff --git a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.js b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.js
index 4d8493401e..2544c03a22 100644
--- a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.js
+++ b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.js
@@ -34,6 +34,7 @@ export default class PreferencesUserSettingsTab extends React.Component {
         'MessageComposerInput.suggestEmoji',
         'sendTypingNotifications',
         'MessageComposerInput.ctrlEnterToSend',
+        'MessageComposerInput.surroundWith',
     ];
 
     static TIMELINE_SETTINGS = [
diff --git a/src/settings/Settings.ts b/src/settings/Settings.ts
index b239b809fe..ed9b37d632 100644
--- a/src/settings/Settings.ts
+++ b/src/settings/Settings.ts
@@ -336,6 +336,11 @@ export const SETTINGS: {[setting: string]: ISetting} = {
         displayName: isMac ? _td("Use Command + Enter to send a message") : _td("Use Ctrl + Enter to send a message"),
         default: false,
     },
+    "MessageComposerInput.surroundWith": {
+        supportedLevels: LEVELS_ACCOUNT_SETTINGS,
+        displayName: _td("Use surround with"),
+        default: false,
+    },
     "MessageComposerInput.autoReplaceEmoji": {
         supportedLevels: LEVELS_ACCOUNT_SETTINGS,
         displayName: _td('Automatically replace plain text Emoji'),

From 3f0d7673725f12b99e48b0f2e94c9c0f78f9c5d8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=A0imon=20Brandner?= 
Date: Fri, 12 Feb 2021 07:57:15 +0100
Subject: [PATCH 0020/1171] 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 | 1 +
 1 file changed, 1 insertion(+)

diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index a9d31bb9f2..3af2a62c94 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -816,6 +816,7 @@
     "Use Ctrl + F to search": "Use Ctrl + F to search",
     "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",
+    "Use surround with": "Use surround with",
     "Automatically replace plain text Emoji": "Automatically replace plain text Emoji",
     "Mirror local video feed": "Mirror local video feed",
     "Enable Community Filter Panel": "Enable Community Filter Panel",

From cdf8f09ec256f8bd69e478ffc11ad26ff883c398 Mon Sep 17 00:00:00 2001
From: Tulir Asokan 
Date: Sat, 20 Mar 2021 13:38:42 +0200
Subject: [PATCH 0021/1171] 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 330f222dd1a9df7aafe4488110be747d03fc5515 Mon Sep 17 00:00:00 2001
From: Tulir Asokan 
Date: Sat, 1 May 2021 16:11:30 +0300
Subject: [PATCH 0022/1171] 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 73b9ad41da12e1092a850efa32c4e6a296342103 Mon Sep 17 00:00:00 2001 From: Jaiwanth Date: Wed, 5 May 2021 12:38:44 +0530 Subject: [PATCH 0023/1171] Navigate to room with maximum notifications when clicked on already selected space --- src/stores/SpaceStore.tsx | 15 +++++++++++++-- .../notifications/SpaceNotificationState.ts | 5 +++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/stores/SpaceStore.tsx b/src/stores/SpaceStore.tsx index 43822007c9..7c0f8cf59b 100644 --- a/src/stores/SpaceStore.tsx +++ b/src/stores/SpaceStore.tsx @@ -120,8 +120,19 @@ export class SpaceStoreClass extends AsyncStoreWithClient { * should not be done when the space switch is done implicitly due to another event like switching room. */ public async setActiveSpace(space: Room | null, contextSwitch = true) { - if (space === this.activeSpace || (space && !space?.isSpaceRoom())) return; - + if (space && !space?.isSpaceRoom()) return; + if (space === this.activeSpace) { + const notificationState = this.getNotificationState(space.roomId); + if (notificationState.count) { + const roomId = notificationState.getRoomWithMaxNotifications(); + defaultDispatcher.dispatch({ + action: "view_room", + room_id: roomId, + context_switch: true, + }); + } + return; + } this._activeSpace = space; this.emit(UPDATE_SELECTED_SPACE, this.activeSpace); this.emit(SUGGESTED_ROOMS, this._suggestedRooms = []); diff --git a/src/stores/notifications/SpaceNotificationState.ts b/src/stores/notifications/SpaceNotificationState.ts index 61a9701a07..fb04648a2a 100644 --- a/src/stores/notifications/SpaceNotificationState.ts +++ b/src/stores/notifications/SpaceNotificationState.ts @@ -53,6 +53,11 @@ export class SpaceNotificationState extends NotificationState { this.calculateTotalState(); } + public getRoomWithMaxNotifications() { + return this.rooms.reduce((prev, curr) => + (prev._notificationCounts.total > curr._notificationCounts.total ? prev : curr)).roomId; + } + public destroy() { super.destroy(); for (const state of Object.values(this.states)) { From bcd1005e3c2ef17e8d6b9212a72d238a639ecbfa Mon Sep 17 00:00:00 2001 From: Jaiwanth Date: Wed, 5 May 2021 13:01:14 +0530 Subject: [PATCH 0024/1171] Check truthiness of space --- 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 7c0f8cf59b..d72ee93956 100644 --- a/src/stores/SpaceStore.tsx +++ b/src/stores/SpaceStore.tsx @@ -121,7 +121,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { */ public async setActiveSpace(space: Room | null, contextSwitch = true) { if (space && !space?.isSpaceRoom()) return; - if (space === this.activeSpace) { + if (space && space === this.activeSpace) { const notificationState = this.getNotificationState(space.roomId); if (notificationState.count) { const roomId = notificationState.getRoomWithMaxNotifications(); From d3fc047b584836cc2a272c521a6a859eacf89290 Mon Sep 17 00:00:00 2001 From: Jaiwanth Date: Wed, 5 May 2021 13:24:06 +0530 Subject: [PATCH 0025/1171] Handle home space --- src/stores/SpaceStore.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/stores/SpaceStore.tsx b/src/stores/SpaceStore.tsx index d72ee93956..5e6d4c8488 100644 --- a/src/stores/SpaceStore.tsx +++ b/src/stores/SpaceStore.tsx @@ -132,7 +132,8 @@ export class SpaceStoreClass extends AsyncStoreWithClient { }); } return; - } + } else if (space === this.activeSpace) return; + this._activeSpace = space; this.emit(UPDATE_SELECTED_SPACE, this.activeSpace); this.emit(SUGGESTED_ROOMS, this._suggestedRooms = []); From 49b61d512f26182e5992b3f2b24193e5e16ff70f Mon Sep 17 00:00:00 2001 From: Jaiwanth Date: Wed, 5 May 2021 13:46:11 +0530 Subject: [PATCH 0026/1171] Replicate same behaviour for the home space --- src/stores/SpaceStore.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/stores/SpaceStore.tsx b/src/stores/SpaceStore.tsx index 5e6d4c8488..d307c56889 100644 --- a/src/stores/SpaceStore.tsx +++ b/src/stores/SpaceStore.tsx @@ -121,8 +121,8 @@ export class SpaceStoreClass extends AsyncStoreWithClient { */ public async setActiveSpace(space: Room | null, contextSwitch = true) { if (space && !space?.isSpaceRoom()) return; - if (space && space === this.activeSpace) { - const notificationState = this.getNotificationState(space.roomId); + if (space === this.activeSpace) { + const notificationState = this.getNotificationState(space ? space.roomId : HOME_SPACE); if (notificationState.count) { const roomId = notificationState.getRoomWithMaxNotifications(); defaultDispatcher.dispatch({ @@ -132,7 +132,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { }); } return; - } else if (space === this.activeSpace) return; + } this._activeSpace = space; this.emit(UPDATE_SELECTED_SPACE, this.activeSpace); From 4a6d8ebdf0d7143c6e41a09aa62dfbcf2620b11b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Fri, 7 May 2021 20:29:26 +0200 Subject: [PATCH 0027/1171] Add screensharing icons MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/img/voip/screensharing-off.svg | 17 +++++++++++++++++ res/img/voip/screensharing-on.svg | 17 +++++++++++++++++ 2 files changed, 34 insertions(+) create mode 100644 res/img/voip/screensharing-off.svg create mode 100644 res/img/voip/screensharing-on.svg diff --git a/res/img/voip/screensharing-off.svg b/res/img/voip/screensharing-off.svg new file mode 100644 index 0000000000..c05ccdf9aa --- /dev/null +++ b/res/img/voip/screensharing-off.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/res/img/voip/screensharing-on.svg b/res/img/voip/screensharing-on.svg new file mode 100644 index 0000000000..1436ca7e37 --- /dev/null +++ b/res/img/voip/screensharing-on.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + From 198722eb41ac39bde68c41a63567dcf0c30ef33a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Fri, 7 May 2021 20:29:45 +0200 Subject: [PATCH 0028/1171] Add classes for screensharing buttons 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 | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/res/css/views/voip/_CallView.scss b/res/css/views/voip/_CallView.scss index 0be75be28c..8ebe7aa607 100644 --- a/res/css/views/voip/_CallView.scss +++ b/res/css/views/voip/_CallView.scss @@ -353,6 +353,18 @@ limitations under the License. } } +.mx_CallView_callControls_button_screensharingOn { + &::before { + background-image: url('$(res)/img/voip/screensharing-on.svg'); + } +} + +.mx_CallView_callControls_button_screensharingOff { + &::before { + background-image: url('$(res)/img/voip/screensharing-off.svg'); + } +} + .mx_CallView_callControls_button_hangup { &::before { background-image: url('$(res)/img/voip/hangup.svg'); From 1f27354439a1572040bcf7c863b1a7298edf4d59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Fri, 7 May 2021 21:34:56 +0200 Subject: [PATCH 0029/1171] Add support for up to 4 feeds MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/css/views/voip/_VideoFeed.scss | 32 +++++++++++- src/components/views/voip/CallView.tsx | 69 ++++++++++++++----------- src/components/views/voip/VideoFeed.tsx | 5 +- 3 files changed, 71 insertions(+), 35 deletions(-) diff --git a/res/css/views/voip/_VideoFeed.scss b/res/css/views/voip/_VideoFeed.scss index 7d85ac264e..170bd89652 100644 --- a/res/css/views/voip/_VideoFeed.scss +++ b/res/css/views/voip/_VideoFeed.scss @@ -21,7 +21,7 @@ limitations under the License. } -.mx_VideoFeed_remote { +.mx_VideoFeed_primary { width: 100%; height: 100%; display: flex; @@ -33,7 +33,7 @@ limitations under the License. } } -.mx_VideoFeed_local { +.mx_VideoFeed_secondary { max-width: 25%; max-height: 25%; position: absolute; @@ -47,6 +47,34 @@ limitations under the License. } } +.mx_VideoFeed_tertiary { + max-width: 25%; + max-height: 25%; + position: absolute; + right: 10px; + bottom: 10px; + z-index: 100; + border-radius: 4px; + + &.mx_VideoFeed_video { + background-color: transparent; + } +} + +.mx_VideoFeed_quaternary { + max-width: 25%; + max-height: 25%; + position: absolute; + left: 10px; + top: 10px; + z-index: 100; + border-radius: 4px; + + &.mx_VideoFeed_video { + background-color: transparent; + } +} + .mx_VideoFeed_mirror { transform: scale(-1, 1); } diff --git a/src/components/views/voip/CallView.tsx b/src/components/views/voip/CallView.tsx index c084dacaa8..2027e997e1 100644 --- a/src/components/views/voip/CallView.tsx +++ b/src/components/views/voip/CallView.tsx @@ -33,6 +33,13 @@ import DialpadContextMenu from '../context_menus/DialpadContextMenu'; import { CallFeed } from 'matrix-js-sdk/src/webrtc/callFeed'; import {replaceableComponent} from "../../../utils/replaceableComponent"; +const FEED_CLASS_NAMES = [ + "mx_VideoFeed_primary", + "mx_VideoFeed_secondary", + "mx_VideoFeed_tertiary", + "mx_VideoFeed_quaternary", +]; + interface IProps { // The call for us to display call: MatrixCall, @@ -371,6 +378,34 @@ export default class CallView extends React.Component { this.props.call.transferToCall(transfereeCall); } + private renderFeeds(feeds: Array, offset = 0) { + const sortedFeeds = [...feeds].sort((a, b) => { + if (b.purpose === SDPStreamMetadataPurpose.Screenshare && !b.isLocal()) return 1; + if (a.isLocal() && !b.isLocal()) return 1; + return 0; + }); + + return sortedFeeds.map((feed, i) => { + i += offset; + // TODO: Later the CallView should probably be reworked to support + // any number of feeds but now we can't render more than 4 feeds + if (i >= 4) return; + // 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 ( + + ); + }); + } + public render() { const client = MatrixClientPeg.get(); const callRoomId = CallHandler.sharedInstance().roomIdForCall(this.props.call); @@ -594,20 +629,8 @@ export default class CallView extends React.Component { mx_CallView_voice: true, }); - 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 ( - - ); - }); + // We pass offset of one to avoid a feed being rendered as primary + const feeds = this.renderFeeds(this.props.call.getLocalFeeds(), 1); // 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 @@ -631,23 +654,7 @@ export default class CallView extends React.Component { 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 ( - - ); - }); + const feeds = this.renderFeeds(this.state.feeds); contentView =
{feeds} diff --git a/src/components/views/voip/VideoFeed.tsx b/src/components/views/voip/VideoFeed.tsx index d22fa055ce..3a7d49cfd4 100644 --- a/src/components/views/voip/VideoFeed.tsx +++ b/src/components/views/voip/VideoFeed.tsx @@ -37,6 +37,8 @@ interface IProps { // a callback which is called when the video element is resized // due to a change in video metadata onResize?: (e: Event) => void, + + className: string, } interface IState { @@ -121,8 +123,7 @@ export default class VideoFeed extends React.Component { render() { const videoClasses = { mx_VideoFeed: true, - mx_VideoFeed_local: this.props.feed.isLocal(), - mx_VideoFeed_remote: !this.props.feed.isLocal(), + [this.props.className]: true, mx_VideoFeed_voice: this.state.videoMuted, mx_VideoFeed_video: !this.state.videoMuted, mx_VideoFeed_mirror: ( From 5b2f941ce2b24d8c0909f4a40c50065ff6451c58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Fri, 7 May 2021 21:35:32 +0200 Subject: [PATCH 0030/1171] Add button to screenshare 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 | 29 ++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/components/views/voip/CallView.tsx b/src/components/views/voip/CallView.tsx index 2027e997e1..30f5db8593 100644 --- a/src/components/views/voip/CallView.tsx +++ b/src/components/views/voip/CallView.tsx @@ -32,6 +32,9 @@ import { avatarUrlForMember } from '../../../Avatar'; import DialpadContextMenu from '../context_menus/DialpadContextMenu'; import { CallFeed } from 'matrix-js-sdk/src/webrtc/callFeed'; import {replaceableComponent} from "../../../utils/replaceableComponent"; +import DesktopCapturerSourcePicker from "../elements/DesktopCapturerSourcePicker"; +import Modal from '../../../Modal'; +import { SDPStreamMetadataPurpose } from 'matrix-js-sdk/src/webrtc/callEventTypes'; const FEED_CLASS_NAMES = [ "mx_VideoFeed_primary", @@ -63,6 +66,7 @@ interface IState { isRemoteOnHold: boolean, micMuted: boolean, vidMuted: boolean, + screensharing: boolean, callState: CallState, controlsVisible: boolean, showMoreMenu: boolean, @@ -119,6 +123,7 @@ export default class CallView extends React.Component { isRemoteOnHold: this.props.call.isRemoteOnHold(), micMuted: this.props.call.isMicrophoneMuted(), vidMuted: this.props.call.isLocalVideoMuted(), + screensharing: this.props.call.isScreensharing(), callState: this.props.call.state, controlsVisible: true, showMoreMenu: false, @@ -292,6 +297,18 @@ export default class CallView extends React.Component { this.setState({vidMuted: newVal}); } + private onScreenshareClick = async () => { + const isScreensharing = await this.props.call.setScreensharingEnabled( + !this.state.screensharing, + async (): Promise => { + const {finished} = Modal.createDialog(DesktopCapturerSourcePicker); + const [source] = await finished; + return source; + }, + ); + this.setState({screensharing: isScreensharing}) + } + private onMoreClick = () => { if (this.controlsHideTimer) { clearTimeout(this.controlsHideTimer); @@ -452,6 +469,12 @@ export default class CallView extends React.Component { mx_CallView_callControls_button_vidOff: this.state.vidMuted, }); + const screensharingClasses = classNames({ + mx_CallView_callControls_button: true, + mx_CallView_callControls_button_screensharingOn: this.state.screensharing, + mx_CallView_callControls_button_screensharingOff: !this.state.screensharing, + }); + // Put the other states of the mic/video icons in the document to make sure they're cached // (otherwise the icon disappears briefly when toggled) const micCacheClasses = classNames({ @@ -478,6 +501,11 @@ export default class CallView extends React.Component { onClick={this.onVidMuteClick} /> : null; + const screensharingButton = this.props.call.opponentSupportsSDPStreamMetadata() ? : null; + // The dial pad & 'more' button actions are only relevant in a connected call // When not connected, we have to put something there to make the flexbox alignment correct const dialpadButton = this.state.callState === CallState.Connected ? { }} /> {vidMuteButton} + {screensharingButton}
{contextMenuButton} From 4c9d9dd2140c8ff645874fcb036eeb3e06994773 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sat, 8 May 2021 10:04:26 +0200 Subject: [PATCH 0031/1171] Enable screenshare in all video calls 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 | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/components/views/voip/CallView.tsx b/src/components/views/voip/CallView.tsx index 30f5db8593..90e442c641 100644 --- a/src/components/views/voip/CallView.tsx +++ b/src/components/views/voip/CallView.tsx @@ -501,10 +501,15 @@ export default class CallView extends React.Component { onClick={this.onVidMuteClick} /> : null; - const screensharingButton = this.props.call.opponentSupportsSDPStreamMetadata() ? : null; + let screensharingButton; + if (this.props.call.opponentSupportsSDPStreamMetadata() || this.props.call.type === CallType.Video) { + screensharingButton = ( + + ); + } // The dial pad & 'more' button actions are only relevant in a connected call // When not connected, we have to put something there to make the flexbox alignment correct From 430808ae2efa6248e49301a8ed7dfe8454073a41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sat, 8 May 2021 16:49:47 +0200 Subject: [PATCH 0032/1171] Simplifie CSS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/css/views/voip/_VideoFeed.scss | 28 ++++++------------------- src/components/views/voip/VideoFeed.tsx | 1 + 2 files changed, 7 insertions(+), 22 deletions(-) diff --git a/res/css/views/voip/_VideoFeed.scss b/res/css/views/voip/_VideoFeed.scss index 170bd89652..3d39735519 100644 --- a/res/css/views/voip/_VideoFeed.scss +++ b/res/css/views/voip/_VideoFeed.scss @@ -20,7 +20,6 @@ limitations under the License. background-color: $inverted-bg-color; } - .mx_VideoFeed_primary { width: 100%; height: 100%; @@ -33,12 +32,10 @@ limitations under the License. } } -.mx_VideoFeed_secondary { +.mx_VideoFeed_nonPrimary { max-width: 25%; max-height: 25%; position: absolute; - right: 10px; - top: 10px; z-index: 100; border-radius: 4px; @@ -47,32 +44,19 @@ limitations under the License. } } +.mx_VideoFeed_secondary { + right: 10px; + top: 10px; +} + .mx_VideoFeed_tertiary { - max-width: 25%; - max-height: 25%; - position: absolute; right: 10px; bottom: 10px; - z-index: 100; - border-radius: 4px; - - &.mx_VideoFeed_video { - background-color: transparent; - } } .mx_VideoFeed_quaternary { - max-width: 25%; - max-height: 25%; - position: absolute; left: 10px; top: 10px; - z-index: 100; - border-radius: 4px; - - &.mx_VideoFeed_video { - background-color: transparent; - } } .mx_VideoFeed_mirror { diff --git a/src/components/views/voip/VideoFeed.tsx b/src/components/views/voip/VideoFeed.tsx index 3a7d49cfd4..a545f100c8 100644 --- a/src/components/views/voip/VideoFeed.tsx +++ b/src/components/views/voip/VideoFeed.tsx @@ -123,6 +123,7 @@ export default class VideoFeed extends React.Component { render() { const videoClasses = { mx_VideoFeed: true, + mx_VideoFeed_nonPrimary: this.props.className !== "mx_VideoFeed_primary", [this.props.className]: true, mx_VideoFeed_voice: this.state.videoMuted, mx_VideoFeed_video: !this.state.videoMuted, From 69b0425c1088704106b986775309c58ad89f1216 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sat, 8 May 2021 18:50:13 +0200 Subject: [PATCH 0033/1171] Improve and fix sorting 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 | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/components/views/voip/CallView.tsx b/src/components/views/voip/CallView.tsx index 90e442c641..0b3e1d5f48 100644 --- a/src/components/views/voip/CallView.tsx +++ b/src/components/views/voip/CallView.tsx @@ -203,7 +203,14 @@ export default class CallView extends React.Component { }; private onFeedsChanged = (newFeeds: Array) => { - this.setState({feeds: newFeeds}); + // Sort the feeds so that screensharing and remote feeds have priority + const sortedFeeds = [...newFeeds].sort((a, b) => { + if (b.purpose === SDPStreamMetadataPurpose.Screenshare && !b.isLocal()) return 1; + if (a.isLocal() && !b.isLocal()) return 1; + return -1; + }); + + this.setState({feeds: sortedFeeds}); }; private onCallLocalHoldUnhold = () => { @@ -396,13 +403,7 @@ export default class CallView extends React.Component { } private renderFeeds(feeds: Array, offset = 0) { - const sortedFeeds = [...feeds].sort((a, b) => { - if (b.purpose === SDPStreamMetadataPurpose.Screenshare && !b.isLocal()) return 1; - if (a.isLocal() && !b.isLocal()) return 1; - return 0; - }); - - return sortedFeeds.map((feed, i) => { + return feeds.map((feed, i) => { i += offset; // TODO: Later the CallView should probably be reworked to support // any number of feeds but now we can't render more than 4 feeds From 2749715050d0e55d0c155c0b67cfdddc149dd5b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 10 May 2021 12:26:28 +0200 Subject: [PATCH 0034/1171] Remove screensharing call type MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/CallHandler.tsx | 25 ------------------------ src/components/views/rooms/RoomHeader.js | 9 +++++++-- 2 files changed, 7 insertions(+), 27 deletions(-) diff --git a/src/CallHandler.tsx b/src/CallHandler.tsx index 0268ebfe46..7184deb0e7 100644 --- a/src/CallHandler.tsx +++ b/src/CallHandler.tsx @@ -80,7 +80,6 @@ import CountlyAnalytics from "./CountlyAnalytics"; import {UIFeature} from "./settings/UIFeature"; import { CallError } from "matrix-js-sdk/src/webrtc/call"; import { logger } from 'matrix-js-sdk/src/logger'; -import DesktopCapturerSourcePicker from "./components/views/elements/DesktopCapturerSourcePicker" import { Action } from './dispatcher/actions'; import VoipUserMapper from './VoipUserMapper'; import { addManagedHybridWidget, isManagedHybridWidgetEnabled } from './widgets/ManagedHybrid'; @@ -129,14 +128,9 @@ interface ThirdpartyLookupResponse { fields: ThirdpartyLookupResponseFields, } -// Unlike 'CallType' in js-sdk, this one includes screen sharing -// (because a screen sharing call is only a screen sharing call to the caller, -// to the callee it's just a video call, at least as far as the current impl -// is concerned). export enum PlaceCallType { Voice = 'voice', Video = 'video', - ScreenSharing = 'screensharing', } export enum CallHandlerEvent { @@ -689,25 +683,6 @@ export default class CallHandler extends EventEmitter { call.placeVoiceCall(); } else if (type === 'video') { call.placeVideoCall(); - } else if (type === PlaceCallType.ScreenSharing) { - const screenCapErrorString = PlatformPeg.get().screenCaptureErrorString(); - if (screenCapErrorString) { - this.removeCallForRoom(roomId); - console.log("Can't capture screen: " + screenCapErrorString); - Modal.createTrackedDialog('Call Handler', 'Unable to capture screen', ErrorDialog, { - title: _t('Unable to capture screen'), - description: screenCapErrorString, - }); - return; - } - - call.placeScreenSharingCall( - async (): Promise => { - const {finished} = Modal.createDialog(DesktopCapturerSourcePicker); - const [source] = await finished; - return source; - }, - ); } else { console.error("Unknown conf call type: " + type); } diff --git a/src/components/views/rooms/RoomHeader.js b/src/components/views/rooms/RoomHeader.js index f856f7f6ef..476b0e1edb 100644 --- a/src/components/views/rooms/RoomHeader.js +++ b/src/components/views/rooms/RoomHeader.js @@ -33,6 +33,7 @@ import RoomTopic from "../elements/RoomTopic"; import RoomName from "../elements/RoomName"; import {PlaceCallType} from "../../../CallHandler"; import {replaceableComponent} from "../../../utils/replaceableComponent"; +import Modal from '../../../Modal'; @replaceableComponent("views.rooms.RoomHeader") export default class RoomHeader extends React.Component { @@ -118,6 +119,10 @@ export default class RoomHeader extends React.Component { return !(currentPinEvent.getContent().pinned && currentPinEvent.getContent().pinned.length <= 0); } + _displayInfoDialogAboutScreensharing() { + + } + render() { let searchStatus = null; let cancelButton = null; @@ -241,8 +246,8 @@ export default class RoomHeader extends React.Component { videoCallButton = this.props.onCallPlaced( - ev.shiftKey ? PlaceCallType.ScreenSharing : PlaceCallType.Video)} + onClick={(ev) => ev.shiftKey ? + this._displayInfoDialogAboutScreensharing() : this.props.onCallPlaced(PlaceCallType.Video)} title={_t("Video call")} />; } From 135cdb2255ad32aba23f8c3f0d4ba059914b4f19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 10 May 2021 12:37:40 +0200 Subject: [PATCH 0035/1171] Add dialog with info about the screensharing change MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/rooms/RoomHeader.js | 7 ++++++- src/i18n/strings/en_EN.json | 3 ++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/components/views/rooms/RoomHeader.js b/src/components/views/rooms/RoomHeader.js index 476b0e1edb..126f5e6c15 100644 --- a/src/components/views/rooms/RoomHeader.js +++ b/src/components/views/rooms/RoomHeader.js @@ -34,6 +34,7 @@ import RoomName from "../elements/RoomName"; import {PlaceCallType} from "../../../CallHandler"; import {replaceableComponent} from "../../../utils/replaceableComponent"; import Modal from '../../../Modal'; +import InfoDialog from "../dialogs/InfoDialog"; @replaceableComponent("views.rooms.RoomHeader") export default class RoomHeader extends React.Component { @@ -120,7 +121,11 @@ export default class RoomHeader extends React.Component { } _displayInfoDialogAboutScreensharing() { - + Modal.createDialog(InfoDialog, { + title: _t("Screensharing has changed"), + description: _t("You don't have to shift-click anymore! You can now share " + + "your screen in any video call and in voice calls if other side supports it."), + }); } render() { diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index deeee2e60e..7175dd6910 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -53,7 +53,6 @@ "A microphone and webcam are plugged in and set up correctly": "A microphone and webcam are plugged in and set up correctly", "Permission is granted to use the webcam": "Permission is granted to use the webcam", "No other application is using the webcam": "No other application is using the webcam", - "Unable to capture screen": "Unable to capture screen", "VoIP is unsupported": "VoIP is unsupported", "You cannot place VoIP calls in this browser.": "You cannot place VoIP calls in this browser.", "Too Many Calls": "Too Many Calls", @@ -1522,6 +1521,8 @@ "Unnamed room": "Unnamed room", "World readable": "World readable", "Guests can join": "Guests can join", + "Screensharing has changed": "Screensharing has changed", + "You don't have to shift-click anymore! You can now share your screen in any video call and in voice calls if other side supports it.": "You don't have to shift-click anymore! You can now share your screen in any video call and in voice calls if other side supports it.", "(~%(count)s results)|other": "(~%(count)s results)", "(~%(count)s results)|one": "(~%(count)s result)", "Join Room": "Join Room", From 0eeb21dfaca6554eb8df8251604eeadeba3bb635 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 10 May 2021 12:41:29 +0200 Subject: [PATCH 0036/1171] Remove unnecessary import MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/CallHandler.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/CallHandler.tsx b/src/CallHandler.tsx index 7184deb0e7..4e20238431 100644 --- a/src/CallHandler.tsx +++ b/src/CallHandler.tsx @@ -56,7 +56,6 @@ limitations under the License. import React from 'react'; import {MatrixClientPeg} from './MatrixClientPeg'; -import PlatformPeg from './PlatformPeg'; import Modal from './Modal'; import { _t } from './languageHandler'; import dis from './dispatcher/dispatcher'; From 228b2ccf2d3c749d229a2a83508701ab3309da20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 10 May 2021 13:06:25 +0200 Subject: [PATCH 0037/1171] Increase z-index of call controls 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 insertion(+) diff --git a/res/css/views/voip/_CallView.scss b/res/css/views/voip/_CallView.scss index 8ebe7aa607..21c948511d 100644 --- a/res/css/views/voip/_CallView.scss +++ b/res/css/views/voip/_CallView.scss @@ -291,6 +291,7 @@ limitations under the License. width: 100%; opacity: 1; transition: opacity 0.5s; + z-index: 200; // To be above _all_ feeds } .mx_CallView_callControls_hidden { From 90f4ad7a830e2f7c4c4150d2d0f6b83fcc6c311c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 10 May 2021 13:21:02 +0200 Subject: [PATCH 0038/1171] Always sort feeds 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 | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/components/views/voip/CallView.tsx b/src/components/views/voip/CallView.tsx index 0b3e1d5f48..a7ad6ef8c6 100644 --- a/src/components/views/voip/CallView.tsx +++ b/src/components/views/voip/CallView.tsx @@ -128,7 +128,7 @@ export default class CallView extends React.Component { controlsVisible: true, showMoreMenu: false, showDialpad: false, - feeds: this.props.call.getFeeds(), + feeds: this.sortFeeds(this.props.call.getFeeds()), } this.updateCallListeners(null, this.props.call); @@ -203,14 +203,7 @@ export default class CallView extends React.Component { }; private onFeedsChanged = (newFeeds: Array) => { - // Sort the feeds so that screensharing and remote feeds have priority - const sortedFeeds = [...newFeeds].sort((a, b) => { - if (b.purpose === SDPStreamMetadataPurpose.Screenshare && !b.isLocal()) return 1; - if (a.isLocal() && !b.isLocal()) return 1; - return -1; - }); - - this.setState({feeds: sortedFeeds}); + this.setState({feeds: this.sortFeeds(newFeeds)}); }; private onCallLocalHoldUnhold = () => { @@ -253,6 +246,15 @@ export default class CallView extends React.Component { this.showControls(); } + private sortFeeds(feeds: Array) { + // Sort the feeds so that screensharing and remote feeds have priority + return [...feeds].sort((a, b) => { + if (b.purpose === SDPStreamMetadataPurpose.Screenshare && !b.isLocal()) return 1; + if (a.isLocal() && !b.isLocal()) return 1; + return -1; + }); + } + private showControls() { if (this.state.showMoreMenu || this.state.showDialpad) return; From acd0fa4c0e929fa4293ba430257d13f4749d0c11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 10 May 2021 14:42:06 +0200 Subject: [PATCH 0039/1171] Add a comment about when it is possible to screenshare 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 | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/components/views/voip/CallView.tsx b/src/components/views/voip/CallView.tsx index a7ad6ef8c6..b49e729244 100644 --- a/src/components/views/voip/CallView.tsx +++ b/src/components/views/voip/CallView.tsx @@ -505,6 +505,9 @@ export default class CallView extends React.Component { /> : null; let screensharingButton; + // Screensharing is possible, if we can send a second stream and identify + // it using SDPStreamMetadata or if we can replace the already existing + // usermedia track by a screensharing track if (this.props.call.opponentSupportsSDPStreamMetadata() || this.props.call.type === CallType.Video) { screensharingButton = ( Date: Tue, 11 May 2021 10:41:31 +0530 Subject: [PATCH 0040/1171] Remove excessive null check Co-authored-by: Travis Ralston --- 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 d307c56889..d906157435 100644 --- a/src/stores/SpaceStore.tsx +++ b/src/stores/SpaceStore.tsx @@ -120,7 +120,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { * should not be done when the space switch is done implicitly due to another event like switching room. */ public async setActiveSpace(space: Room | null, contextSwitch = true) { - if (space && !space?.isSpaceRoom()) return; + if (!space?.isSpaceRoom()) return; if (space === this.activeSpace) { const notificationState = this.getNotificationState(space ? space.roomId : HOME_SPACE); if (notificationState.count) { From 07a952a1bbca35fcd57e130d45ddc5e45d13fde6 Mon Sep 17 00:00:00 2001 From: Jaiwanth Date: Tue, 11 May 2021 11:01:28 +0530 Subject: [PATCH 0041/1171] Update src/stores/SpaceStore.tsx MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Šimon Brandner --- 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 d906157435..2f52061783 100644 --- a/src/stores/SpaceStore.tsx +++ b/src/stores/SpaceStore.tsx @@ -120,7 +120,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { * should not be done when the space switch is done implicitly due to another event like switching room. */ public async setActiveSpace(space: Room | null, contextSwitch = true) { - if (!space?.isSpaceRoom()) return; + if (space && !space.isSpaceRoom()) return; if (space === this.activeSpace) { const notificationState = this.getNotificationState(space ? space.roomId : HOME_SPACE); if (notificationState.count) { From 3e8863fc9af0d5932c3393d0156ab6063baee524 Mon Sep 17 00:00:00 2001 From: Jaiwanth Date: Tue, 11 May 2021 13:00:42 +0530 Subject: [PATCH 0042/1171] Adjust behaviour for the home space --- src/stores/SpaceStore.tsx | 5 ++++- .../notifications/SummarizedNotificationState.ts | 12 +++++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/stores/SpaceStore.tsx b/src/stores/SpaceStore.tsx index edc6bbef77..b1993d9625 100644 --- a/src/stores/SpaceStore.tsx +++ b/src/stores/SpaceStore.tsx @@ -118,7 +118,10 @@ export class SpaceStoreClass extends AsyncStoreWithClient { public async setActiveSpace(space: Room | null, contextSwitch = true) { if (space && !space.isSpaceRoom()) return; if (space === this.activeSpace) { - const notificationState = this.getNotificationState(space ? space.roomId : HOME_SPACE); + const notificationState = space + ? this.getNotificationState(space.roomId) + : RoomNotificationStateStore.instance.globalState; + if (notificationState.count) { const roomId = notificationState.getRoomWithMaxNotifications(); defaultDispatcher.dispatch({ diff --git a/src/stores/notifications/SummarizedNotificationState.ts b/src/stores/notifications/SummarizedNotificationState.ts index 372da74f36..4a3473792a 100644 --- a/src/stores/notifications/SummarizedNotificationState.ts +++ b/src/stores/notifications/SummarizedNotificationState.ts @@ -16,6 +16,8 @@ limitations under the License. import { NotificationColor } from "./NotificationColor"; import { NotificationState } from "./NotificationState"; +import { Room } from "matrix-js-sdk/src/models/room"; +import { RoomNotificationState } from "./RoomNotificationState"; /** * Summarizes a number of states into a unique snapshot. To populate, call @@ -25,11 +27,13 @@ import { NotificationState } from "./NotificationState"; */ export class SummarizedNotificationState extends NotificationState { private totalStatesWithUnread = 0; + unreadRooms: Room[]; constructor() { super(); this._symbol = null; this._count = 0; + this.unreadRooms = []; this._color = NotificationColor.None; } @@ -37,6 +41,11 @@ export class SummarizedNotificationState extends NotificationState { return this.totalStatesWithUnread; } + public getRoomWithMaxNotifications() { + return this.unreadRooms.reduce((prev, curr) => + (prev._notificationCounts.total > curr._notificationCounts.total ? prev : curr)).roomId; + } + /** * Append a notification state to this snapshot, taking the loudest NotificationColor * of the two. By default this will not adopt the symbol of the other notification @@ -45,7 +54,7 @@ export class SummarizedNotificationState extends NotificationState { * @param includeSymbol If true, the notification state's symbol will be taken if one * is present. */ - public add(other: NotificationState, includeSymbol = false) { + public add(other: RoomNotificationState, includeSymbol = false) { if (other.symbol && includeSymbol) { this._symbol = other.symbol; } @@ -56,6 +65,7 @@ export class SummarizedNotificationState extends NotificationState { this._color = other.color; } if (other.hasUnreadCount) { + this.unreadRooms.push(other.room); this.totalStatesWithUnread++; } } From 834579f7785e304b32f1f8070a1ca7d564c63da1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 11 May 2021 13:07:30 +0200 Subject: [PATCH 0043/1171] Don't render any audio non-primary feeds 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, 4 insertions(+), 3 deletions(-) diff --git a/src/components/views/voip/CallView.tsx b/src/components/views/voip/CallView.tsx index b49e729244..dc84cbed60 100644 --- a/src/components/views/voip/CallView.tsx +++ b/src/components/views/voip/CallView.tsx @@ -410,9 +410,10 @@ export default class CallView extends React.Component { // TODO: Later the CallView should probably be reworked to support // any number of feeds but now we can't render more than 4 feeds if (i >= 4) return; - // 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; + // Here we check to hide any non-main audio feeds from the UI + // This is because we don't want them to obstruct the view + // But once again this might be subject to change + if (feed.isVideoMuted() && i > 0) return; return ( Date: Thu, 13 May 2021 18:11:47 +0200 Subject: [PATCH 0044/1171] Show screensharign button only if connected 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 | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/components/views/voip/CallView.tsx b/src/components/views/voip/CallView.tsx index dc84cbed60..a9d605c9f1 100644 --- a/src/components/views/voip/CallView.tsx +++ b/src/components/views/voip/CallView.tsx @@ -506,10 +506,14 @@ export default class CallView extends React.Component { /> : null; let screensharingButton; - // Screensharing is possible, if we can send a second stream and identify - // it using SDPStreamMetadata or if we can replace the already existing - // usermedia track by a screensharing track - if (this.props.call.opponentSupportsSDPStreamMetadata() || this.props.call.type === CallType.Video) { + // Screensharing is possible, if we can send a second stream and + // identify it using SDPStreamMetadata or if we can replace the already + // existing usermedia track by a screensharing track. We also need to be + // connected to know the state of the other side + if ( + (this.props.call.opponentSupportsSDPStreamMetadata() || this.props.call.type === CallType.Video) && + this.props.call.state === CallState.Connected + ) { screensharingButton = ( Date: Wed, 19 May 2021 08:46:04 +0200 Subject: [PATCH 0045/1171] Do not render the audio element if there is no audio track MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/voip/AudioFeed.tsx | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/src/components/views/voip/AudioFeed.tsx b/src/components/views/voip/AudioFeed.tsx index c78f0c0fc8..7fb57abcd2 100644 --- a/src/components/views/voip/AudioFeed.tsx +++ b/src/components/views/voip/AudioFeed.tsx @@ -23,9 +23,21 @@ interface IProps { feed: CallFeed, } -export default class AudioFeed extends React.Component { +interface IState { + audioMuted: boolean; +} + +export default class AudioFeed extends React.Component { private element = createRef(); + constructor(props: IProps) { + super(props); + + this.state = { + audioMuted: this.props.feed.isAudioMuted(), + }; + } + componentDidMount() { this.props.feed.addListener(CallFeedEvent.NewStream, this.onNewStream); this.playMedia(); @@ -38,6 +50,7 @@ export default class AudioFeed extends React.Component { private playMedia() { const element = this.element.current; + if (!element) return; const audioOutput = CallMediaHandler.getAudioOutput(); if (audioOutput) { @@ -75,6 +88,7 @@ export default class AudioFeed extends React.Component { private stopMedia() { const element = this.element.current; + if (!element) return; element.pause(); element.src = null; @@ -86,10 +100,16 @@ export default class AudioFeed extends React.Component { } private onNewStream = () => { + this.setState({ + audioMuted: this.props.feed.isAudioMuted(), + }); this.playMedia(); }; render() { + // Do not render the audio element if there is no audio track + if (this.state.audioMuted) return null; + return (
; }); From 090acc4811f2dc3f03089cb5346a7bee44db9033 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Fri, 2 Jul 2021 15:41:36 +0200 Subject: [PATCH 0191/1171] Unused import MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/elements/ReplyThread.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/views/elements/ReplyThread.js b/src/components/views/elements/ReplyThread.js index 585c4bbdc0..b6368eb5b3 100644 --- a/src/components/views/elements/ReplyThread.js +++ b/src/components/views/elements/ReplyThread.js @@ -29,7 +29,6 @@ import MatrixClientContext from "../../../contexts/MatrixClientContext"; import { getUserNameColorClass } from "../../../utils/FormattingUtils"; import { Action } from "../../../dispatcher/actions"; import sanitizeHtml from "sanitize-html"; -import { UIFeature } from "../../../settings/UIFeature"; import { PERMITTED_URL_SCHEMES } from "../../../HtmlUtils"; import { replaceableComponent } from "../../../utils/replaceableComponent"; From e8f0412fe30abe28036e421f22ea12079f0332ca Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 2 Jul 2021 14:51:55 +0100 Subject: [PATCH 0192/1171] Add way to manage Restricted join rule in Room Settings --- res/css/_components.scss | 1 + .../_ManageRestrictedJoinRuleDialog.scss | 147 +++++++ res/css/views/settings/tabs/_SettingsTab.scss | 4 +- .../tabs/room/_SecurityRoomSettingsTab.scss | 86 +++- src/SlashCommands.tsx | 49 +-- src/components/views/avatars/RoomAvatar.tsx | 21 +- .../ManageRestrictedJoinRuleDialog.tsx | 182 ++++++++ ...Dialog.js => RoomUpgradeWarningDialog.tsx} | 97 +++-- .../views/elements/StyledRadioGroup.tsx | 6 +- .../tabs/room/SecurityRoomSettingsTab.tsx | 396 +++++++++++------- src/createRoom.ts | 16 +- src/i18n/strings/en_EN.json | 43 +- src/utils/RoomUpgrade.ts | 74 ++++ 13 files changed, 857 insertions(+), 265 deletions(-) create mode 100644 res/css/views/dialogs/_ManageRestrictedJoinRuleDialog.scss create mode 100644 src/components/views/dialogs/ManageRestrictedJoinRuleDialog.tsx rename src/components/views/dialogs/{RoomUpgradeWarningDialog.js => RoomUpgradeWarningDialog.tsx} (59%) create mode 100644 src/utils/RoomUpgrade.ts diff --git a/res/css/_components.scss b/res/css/_components.scss index 1517527034..7463f92037 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -87,6 +87,7 @@ @import "./views/dialogs/_IncomingSasDialog.scss"; @import "./views/dialogs/_InviteDialog.scss"; @import "./views/dialogs/_KeyboardShortcutsDialog.scss"; +@import "./views/dialogs/_ManageRestrictedJoinRuleDialog.scss"; @import "./views/dialogs/_MessageEditHistoryDialog.scss"; @import "./views/dialogs/_ModalWidgetDialog.scss"; @import "./views/dialogs/_NewSessionReviewDialog.scss"; diff --git a/res/css/views/dialogs/_ManageRestrictedJoinRuleDialog.scss b/res/css/views/dialogs/_ManageRestrictedJoinRuleDialog.scss new file mode 100644 index 0000000000..6606f78a8a --- /dev/null +++ b/res/css/views/dialogs/_ManageRestrictedJoinRuleDialog.scss @@ -0,0 +1,147 @@ +/* +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_ManageRestrictedJoinRuleDialog_wrapper { + .mx_Dialog { + display: flex; + flex-direction: column; + } +} + +.mx_ManageRestrictedJoinRuleDialog { + width: 480px; + color: $primary-fg-color; + display: flex; + flex-direction: column; + flex-wrap: nowrap; + min-height: 0; + height: 60vh; + + .mx_SearchBox { + // To match the space around the title + margin: 0 0 15px 0; + flex-grow: 0; + } + + .mx_ManageRestrictedJoinRuleDialog_content { + flex-grow: 1; + } + + .mx_ManageRestrictedJoinRuleDialog_noResults { + display: block; + margin-top: 24px; + } + + .mx_ManageRestrictedJoinRuleDialog_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_ManageRestrictedJoinRuleDialog_entry { + display: flex; + margin-top: 12px; + + > div { + flex-grow: 1; + } + + img.mx_RoomAvatar_isSpaceRoom, + .mx_RoomAvatar_isSpaceRoom img { + border-radius: 4px; + } + + .mx_ManageRestrictedJoinRuleDialog_entry_name { + margin: 0 8px; + font-size: $font-15px; + line-height: 30px; + flex-grow: 1; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + } + + .mx_ManageRestrictedJoinRuleDialog_entry_description { + margin-top: 8px; + font-size: $font-12px; + line-height: $font-15px; + color: $tertiary-fg-color; + } + + .mx_Checkbox { + align-items: center; + } + } + } + + .mx_ManageRestrictedJoinRuleDialog_section_spaces { + .mx_BaseAvatar { + margin-right: 12px; + } + + .mx_BaseAvatar_image { + border-radius: 8px; + } + } + + .mx_ManageRestrictedJoinRuleDialog_section_experimental { + position: relative; + border-radius: 8px; + margin: 12px 0; + padding: 8px 8px 8px 42px; + background-color: $header-panel-bg-color; + + font-size: $font-12px; + line-height: $font-15px; + color: $secondary-fg-color; + + &::before { + content: ''; + position: absolute; + left: 10px; + top: calc(50% - 8px); // vertical centering + height: 16px; + width: 16px; + background-color: $secondary-fg-color; + mask-repeat: no-repeat; + mask-size: contain; + mask-image: url('$(res)/img/element-icons/room/room-summary.svg'); + mask-position: center; + } + } + + .mx_ManageRestrictedJoinRuleDialog_footer { + display: flex; + margin-top: 20px; + align-self: end; + + .mx_AccessibleButton { + display: inline-block; + align-self: center; + + & + .mx_AccessibleButton { + margin-left: 24px; + } + } + } +} diff --git a/res/css/views/settings/tabs/_SettingsTab.scss b/res/css/views/settings/tabs/_SettingsTab.scss index 892f5fe744..0d679af4e5 100644 --- a/res/css/views/settings/tabs/_SettingsTab.scss +++ b/res/css/views/settings/tabs/_SettingsTab.scss @@ -47,14 +47,14 @@ limitations under the License. color: $settings-subsection-fg-color; font-size: $font-14px; display: block; - margin: 10px 100px 10px 0; // Align with the rest of the view + margin: 10px 80px 10px 0; // Align with the rest of the view } .mx_SettingsTab_section { margin-bottom: 24px; .mx_SettingsFlag { - margin-right: 100px; + margin-right: 80px; margin-bottom: 10px; } diff --git a/res/css/views/settings/tabs/room/_SecurityRoomSettingsTab.scss b/res/css/views/settings/tabs/room/_SecurityRoomSettingsTab.scss index 23dcc532b2..2aab201352 100644 --- a/res/css/views/settings/tabs/room/_SecurityRoomSettingsTab.scss +++ b/res/css/views/settings/tabs/room/_SecurityRoomSettingsTab.scss @@ -14,6 +14,44 @@ See the License for the specific language governing permissions and limitations under the License. */ +.mx_SecurityRoomSettingsTab { + .mx_SettingsTab_showAdvanced { + padding: 0; + margin-bottom: 16px; + } + + .mx_SecurityRoomSettingsTab_spacesWithAccess { + > h4 { + color: $secondary-fg-color; + font-weight: $font-semi-bold; + font-size: $font-12px; + line-height: $font-15px; + text-transform: uppercase; + } + + > span { + font-weight: 500; + font-size: $font-14px; + line-height: 32px; // matches height of avatar for v-align + color: $secondary-fg-color; + display: inline-block; + + img.mx_RoomAvatar_isSpaceRoom, + .mx_RoomAvatar_isSpaceRoom img { + border-radius: 8px; + } + + .mx_BaseAvatar { + margin-right: 8px; + } + + & + span { + margin-left: 16px; + } + } + } +} + .mx_SecurityRoomSettingsTab_warning { display: block; @@ -26,5 +64,51 @@ limitations under the License. } .mx_SecurityRoomSettingsTab_encryptionSection { - margin-bottom: 25px; + padding-bottom: 24px; + border-bottom: 1px solid $menu-border-color; + margin-bottom: 32px; +} + +.mx_SecurityRoomSettingsTab_upgradeRequired { + margin-left: 16px; + padding: 4px 16px; + border: 1px solid $accent-color; + border-radius: 8px; + color: $accent-color; + font-size: $font-12px; + line-height: $font-15px; +} + +.mx_SecurityRoomSettingsTab_joinRule { + .mx_RadioButton { + padding-top: 16px; + margin-bottom: 8px; + + .mx_RadioButton_content { + margin-left: 14px; + font-weight: $font-semi-bold; + font-size: $font-15px; + line-height: $font-24px; + color: $primary-fg-color; + display: block; + } + } + + > span { + display: inline-block; + margin-left: 34px; + margin-bottom: 16px; + font-size: $font-15px; + line-height: $font-24px; + color: $secondary-fg-color; + + & + .mx_RadioButton { + border-top: 1px solid $menu-border-color; + } + } + + .mx_AccessibleButton_kind_link { + padding: 0; + font-size: inherit; + } } diff --git a/src/SlashCommands.tsx b/src/SlashCommands.tsx index 128ca9e5e2..b8f500ba84 100644 --- a/src/SlashCommands.tsx +++ b/src/SlashCommands.tsx @@ -35,7 +35,6 @@ import { getAddressType } from './UserAddress'; import { abbreviateUrl } from './utils/UrlUtils'; import { getDefaultIdentityServerUrl, useDefaultIdentityServer } from './utils/IdentityServerUtils'; import { isPermalinkHost, parsePermalink } from "./utils/permalinks/Permalinks"; -import { inviteUsersToRoom } from "./RoomInvite"; import { WidgetType } from "./widgets/WidgetType"; import { Jitsi } from "./widgets/Jitsi"; import { parseFragment as parseHtml, Element as ChildElement } from "parse5"; @@ -50,6 +49,7 @@ import { UIFeature } from "./settings/UIFeature"; import { CHAT_EFFECTS } from "./effects"; import CallHandler from "./CallHandler"; import { guessAndSetDMRoom } from "./Rooms"; +import { upgradeRoom } from './utils/RoomUpgrade'; // XXX: workaround for https://github.com/microsoft/TypeScript/issues/31816 interface HTMLInputEvent extends Event { @@ -276,51 +276,8 @@ export const Commands = [ /*isPriority=*/false, /*isStatic=*/true); return success(finished.then(async ([resp]) => { - if (!resp.continue) return; - - let checkForUpgradeFn; - try { - const upgradePromise = cli.upgradeRoom(roomId, args); - - // We have to wait for the js-sdk to give us the room back so - // we can more effectively abuse the MultiInviter behaviour - // which heavily relies on the Room object being available. - if (resp.invite) { - checkForUpgradeFn = async (newRoom) => { - // The upgradePromise should be done by the time we await it here. - const { replacement_room: newRoomId } = await upgradePromise; - if (newRoom.roomId !== newRoomId) return; - - const toInvite = [ - ...room.getMembersWithMembership("join"), - ...room.getMembersWithMembership("invite"), - ].map(m => m.userId).filter(m => m !== cli.getUserId()); - - if (toInvite.length > 0) { - // Errors are handled internally to this function - await inviteUsersToRoom(newRoomId, toInvite); - } - - cli.removeListener('Room', checkForUpgradeFn); - }; - cli.on('Room', checkForUpgradeFn); - } - - // We have to await after so that the checkForUpgradesFn has a proper reference - // to the new room's ID. - await upgradePromise; - } catch (e) { - console.error(e); - - if (checkForUpgradeFn) cli.removeListener('Room', checkForUpgradeFn); - - const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog'); - Modal.createTrackedDialog('Slash Commands', 'room upgrade error', ErrorDialog, { - title: _t('Error upgrading room'), - description: _t( - 'Double check that your server supports the room version chosen and try again.'), - }); - } + if (!resp?.continue) return; + await upgradeRoom(room, args, resp.invite); })); } return reject(this.getUsage()); diff --git a/src/components/views/avatars/RoomAvatar.tsx b/src/components/views/avatars/RoomAvatar.tsx index 8ac8de8233..bd776953a6 100644 --- a/src/components/views/avatars/RoomAvatar.tsx +++ b/src/components/views/avatars/RoomAvatar.tsx @@ -13,9 +13,11 @@ 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 { ResizeMethod } from 'matrix-js-sdk/src/@types/partials'; +import classNames from "classnames"; import BaseAvatar from './BaseAvatar'; import ImageView from '../elements/ImageView'; @@ -31,11 +33,14 @@ interface IProps extends Omit, "name" | "idNam // oobData.avatarUrl should be set (else there // would be nowhere to get the avatar from) room?: Room; - oobData?: IOOBData; + oobData?: IOOBData & { + roomId?: string; + }; width?: number; height?: number; resizeMethod?: ResizeMethod; viewAvatarOnClick?: boolean; + className?: string; onClick?(): void; } @@ -128,14 +133,16 @@ export default class RoomAvatar extends React.Component { }; public render() { - const { room, oobData, viewAvatarOnClick, onClick, ...otherProps } = this.props; - - const roomName = room ? room.name : oobData.name; + const { room, oobData, viewAvatarOnClick, onClick, className, ...otherProps } = this.props; return ( - diff --git a/src/components/views/dialogs/ManageRestrictedJoinRuleDialog.tsx b/src/components/views/dialogs/ManageRestrictedJoinRuleDialog.tsx new file mode 100644 index 0000000000..79a6fb7f24 --- /dev/null +++ b/src/components/views/dialogs/ManageRestrictedJoinRuleDialog.tsx @@ -0,0 +1,182 @@ +/* +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, { useMemo, useState } from "react"; +import { Room } from "matrix-js-sdk/src/models/room"; + +import { _t } from '../../../languageHandler'; +import { IDialogProps } from "./IDialogProps"; +import BaseDialog from "./BaseDialog"; +import SearchBox from "../../structures/SearchBox"; +import SpaceStore from "../../../stores/SpaceStore"; +import RoomAvatar from "../avatars/RoomAvatar"; +import AccessibleButton from "../elements/AccessibleButton"; +import AutoHideScrollbar from "../../structures/AutoHideScrollbar"; +import StyledCheckbox from "../elements/StyledCheckbox"; +import MatrixClientContext from "../../../contexts/MatrixClientContext"; + +interface IProps extends IDialogProps { + room: Room; + selected?: string[]; +} + +const Entry = ({ room, checked, onChange }) => { + const localRoom = room instanceof Room; + + let description; + if (localRoom) { + description = _t("%(count)s members", { count: room.getJoinedMemberCount() }); + const numChildRooms = SpaceStore.instance.getChildRooms(room.roomId).length; + if (numChildRooms > 0) { + description += " · " + _t("%(count)s rooms", { count: numChildRooms }); + } + } + + return ; +}; + +const ManageRestrictedJoinRuleDialog: React.FC = ({ room, selected = [], onFinished }) => { + const cli = room.client; + const [newSelected, setNewSelected] = useState(new Set(selected)); + const [query, setQuery] = useState(""); + const lcQuery = query.toLowerCase().trim(); + + const [spacesContainingRoom, otherEntries] = useMemo(() => { + const spaces = cli.getVisibleRooms().filter(r => r.getMyMembership() === "join" && r.isSpaceRoom()); + return [ + spaces.filter(r => SpaceStore.instance.getSpaceFilteredRoomIds(r).has(room.roomId)), + selected.map(roomId => { + const room = cli.getRoom(roomId); + if (!room) { + return { roomId, name: roomId } as Room; + } + if (room.getMyMembership() !== "join" || !room.isSpaceRoom()) { + return room; + } + }).filter(Boolean), + ]; + }, [cli, selected, room.roomId]); + + const [filteredSpacesContainingRooms, filteredOtherEntries] = useMemo(() => [ + spacesContainingRoom.filter(r => r.name.toLowerCase().includes(lcQuery)), + otherEntries.filter(r => r.name.toLowerCase().includes(lcQuery)), + ], [spacesContainingRoom, otherEntries, lcQuery]); + + const onChange = (checked: boolean, room: Room): void => { + if (checked) { + newSelected.add(room.roomId); + } else { + newSelected.delete(room.roomId); + } + setNewSelected(new Set(newSelected)); + }; + + return +

+ { _t("Decide which spaces can access this room. " + + "If a space is selected its members will be able to find and join .", {}, { + RoomName: () => { room.name }, + })} +

+ + + + { filteredSpacesContainingRooms.length > 0 ? ( +
+

{ _t("Spaces you know that contain this room") }

+ { filteredSpacesContainingRooms.map(space => { + return { + onChange(checked, space); + }} + />; + }) } +
+ ) : undefined } + + { filteredOtherEntries.length > 0 ? ( +
+

{ _t("Other spaces or rooms you might not know") }

+
+
{ _t("These are likely ones other room admins are a part of.") }
+
+ { filteredOtherEntries.map(space => { + return { + onChange(checked, space); + }} + />; + }) } +
+ ) : null } + + { filteredSpacesContainingRooms.length + filteredOtherEntries.length < 1 + ? + { _t("No results") } + + : undefined + } +
+ +
+ onFinished()}> + { _t("Cancel") } + + onFinished(Array.from(newSelected))}> + { _t("Confirm") } + +
+
+
; +}; + +export default ManageRestrictedJoinRuleDialog; + diff --git a/src/components/views/dialogs/RoomUpgradeWarningDialog.js b/src/components/views/dialogs/RoomUpgradeWarningDialog.tsx similarity index 59% rename from src/components/views/dialogs/RoomUpgradeWarningDialog.js rename to src/components/views/dialogs/RoomUpgradeWarningDialog.tsx index c73edcd871..6bc770c05b 100644 --- a/src/components/views/dialogs/RoomUpgradeWarningDialog.js +++ b/src/components/views/dialogs/RoomUpgradeWarningDialog.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. @@ -14,86 +14,95 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; -import PropTypes from 'prop-types'; +import React, { ReactNode } from 'react'; +import { EventType } from 'matrix-js-sdk/src/@types/event'; +import { JoinRule } from 'matrix-js-sdk/src/@types/partials'; + import { _t } from "../../../languageHandler"; import SdkConfig from "../../../SdkConfig"; -import * as sdk from "../../../index"; import LabelledToggleSwitch from "../elements/LabelledToggleSwitch"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; import Modal from "../../../Modal"; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import { IDialogProps } from "./IDialogProps"; +import BugReportDialog from './BugReportDialog'; +import BaseDialog from "./BaseDialog"; +import DialogButtons from "../elements/DialogButtons"; + +interface IProps extends IDialogProps { + roomId: string; + targetVersion: string; + description?: ReactNode; +} + +interface IState { + inviteUsersToNewRoom: boolean; +} @replaceableComponent("views.dialogs.RoomUpgradeWarningDialog") -export default class RoomUpgradeWarningDialog extends React.Component { - static propTypes = { - onFinished: PropTypes.func.isRequired, - roomId: PropTypes.string.isRequired, - targetVersion: PropTypes.string.isRequired, - }; +export default class RoomUpgradeWarningDialog extends React.Component { + private readonly isPrivate: boolean; + private readonly currentVersion: string; constructor(props) { super(props); const room = MatrixClientPeg.get().getRoom(this.props.roomId); - const joinRules = room ? room.currentState.getStateEvents("m.room.join_rules", "") : null; - const isPrivate = joinRules ? joinRules.getContent()['join_rule'] !== 'public' : true; + const joinRules = room?.currentState.getStateEvents(EventType.RoomJoinRules, ""); + this.isPrivate = joinRules?.getContent()['join_rule'] !== JoinRule.Public ?? true; + this.currentVersion = room?.getVersion() || "1"; + this.state = { - currentVersion: room ? room.getVersion() : "1", - isPrivate, inviteUsersToNewRoom: true, }; } - _onContinue = () => { - this.props.onFinished({ continue: true, invite: this.state.isPrivate && this.state.inviteUsersToNewRoom }); + private onContinue = () => { + this.props.onFinished({ continue: true, invite: this.isPrivate && this.state.inviteUsersToNewRoom }); }; - _onCancel = () => { + private onCancel = () => { this.props.onFinished({ continue: false, invite: false }); }; - _onInviteUsersToggle = (newVal) => { - this.setState({ inviteUsersToNewRoom: newVal }); + private onInviteUsersToggle = (inviteUsersToNewRoom: boolean) => { + this.setState({ inviteUsersToNewRoom }); }; - _openBugReportDialog = (e) => { + private openBugReportDialog = (e) => { e.preventDefault(); e.stopPropagation(); - const BugReportDialog = sdk.getComponent("dialogs.BugReportDialog"); Modal.createTrackedDialog('Bug Report Dialog', '', BugReportDialog, {}); }; render() { const brand = SdkConfig.get().brand; - const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); - const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); let inviteToggle = null; - if (this.state.isPrivate) { + if (this.isPrivate) { inviteToggle = ( + onChange={this.onInviteUsersToggle} + label={_t("Automatically invite members from this room to the new one")} /> ); } - const title = this.state.isPrivate ? _t("Upgrade private room") : _t("Upgrade public room"); + const title = this.isPrivate ? _t("Upgrade private room") : _t("Upgrade public room"); let bugReports = (

- {_t( + { _t( "This usually only affects how the room is processed on the server. If you're " + "having problems with your %(brand)s, please report a bug.", { brand }, - )} + ) }

); if (SdkConfig.get().bug_report_endpoint_url) { bugReports = (

- {_t( + { _t( "This usually only affects how the room is processed on the server. If you're " + "having problems with your %(brand)s, please report a bug.", { @@ -101,10 +110,10 @@ export default class RoomUpgradeWarningDialog extends React.Component { }, { "a": (sub) => { - return {sub}; + return {sub}; }, }, - )} + ) }

); } @@ -119,29 +128,37 @@ export default class RoomUpgradeWarningDialog extends React.Component { >

- {_t( + { this.props.description || _t( "Upgrading a room is an advanced action and is usually recommended when a room " + "is unstable due to bugs, missing features or security vulnerabilities.", - )} + ) }

- {bugReports} +

+ { _t( + "Please note upgrading will make a new version of the room. " + + "All current messages will stay in this archived room.", {}, { + b: sub => { sub }, + }, + ) } +

+ { bugReports }

{_t( "You'll upgrade this room from to .", {}, { - oldVersion: () => {this.state.currentVersion}, - newVersion: () => {this.props.targetVersion}, + oldVersion: () => { this.currentVersion }, + newVersion: () => { this.props.targetVersion }, }, )}

- {inviteToggle} + { inviteToggle }
); diff --git a/src/components/views/elements/StyledRadioGroup.tsx b/src/components/views/elements/StyledRadioGroup.tsx index af152b82da..efd278c991 100644 --- a/src/components/views/elements/StyledRadioGroup.tsx +++ b/src/components/views/elements/StyledRadioGroup.tsx @@ -14,7 +14,7 @@ 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 StyledRadioButton from "./StyledRadioButton"; @@ -23,8 +23,8 @@ export interface IDefinition { value: T; className?: string; disabled?: boolean; - label: React.ReactChild; - description?: React.ReactChild; + label: ReactNode; + description?: ReactNode; checked?: boolean; // If provided it will override the value comparison done in the group } diff --git a/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx b/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx index 2c2c336d24..34f5b8c94c 100644 --- a/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx +++ b/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx @@ -15,9 +15,8 @@ limitations under the License. */ import React from 'react'; -import { GuestAccess, HistoryVisibility, JoinRule } from "matrix-js-sdk/src/@types/partials"; -import { MatrixEvent } from "matrix-js-sdk/src/models/event"; -import { IRoomVersionsCapability } from 'matrix-js-sdk/src/client'; +import { GuestAccess, HistoryVisibility, JoinRule, RestrictedAllowType } from "matrix-js-sdk/src/@types/partials"; +import { IContent, MatrixEvent } from "matrix-js-sdk/src/models/event"; import { EventType } from 'matrix-js-sdk/src/@types/event'; import { _t } from "../../../../../languageHandler"; @@ -31,6 +30,12 @@ import { SettingLevel } from "../../../../../settings/SettingLevel"; import SettingsStore from "../../../../../settings/SettingsStore"; import { UIFeature } from "../../../../../settings/UIFeature"; import { replaceableComponent } from "../../../../../utils/replaceableComponent"; +import AccessibleButton from "../../../elements/AccessibleButton"; +import SpaceStore from "../../../../../stores/SpaceStore"; +import RoomAvatar from "../../../avatars/RoomAvatar"; +import ManageRestrictedJoinRuleDialog from '../../../dialogs/ManageRestrictedJoinRuleDialog'; +import RoomUpgradeWarningDialog from '../../../dialogs/RoomUpgradeWarningDialog'; +import { upgradeRoom } from "../../../../../utils/RoomUpgrade"; interface IProps { roomId: string; @@ -38,18 +43,14 @@ interface IProps { interface IState { joinRule: JoinRule; + restrictedAllowRoomIds?: string[]; guestAccess: GuestAccess; history: HistoryVisibility; hasAliases: boolean; encrypted: boolean; - roomVersionsCapability?: IRoomVersionsCapability; -} - -enum RoomVisibility { - InviteOnly = "invite_only", - PublicNoGuests = "public_no_guests", - PublicWithGuests = "public_with_guests", - Restricted = "restricted", + roomSupportsRestricted?: boolean; + preferredRestrictionVersion?: string; + showAdvancedSection: boolean; } @replaceableComponent("views.settings.tabs.room.SecurityRoomSettingsTab") @@ -59,10 +60,11 @@ export default class SecurityRoomSettingsTab extends React.Component( + joinRuleEvent, 'join_rule', JoinRule.Invite, ); - const guestAccess: GuestAccess = this.pullContentPropertyFromEvent( + const restrictedAllowRoomIds = joinRule === JoinRule.Restricted + ? joinRuleEvent?.getContent().allow + ?.filter(a => a.type === RestrictedAllowType.RoomMembership) + ?.map(a => a.room_id) + : undefined; + + const guestAccess: GuestAccess = this.pullContentPropertyFromEvent( state.getStateEvents(EventType.RoomGuestAccess, ""), 'guest_access', GuestAccess.Forbidden, ); - const history: HistoryVisibility = this.pullContentPropertyFromEvent( + const history: HistoryVisibility = this.pullContentPropertyFromEvent( state.getStateEvents(EventType.RoomHistoryVisibility, ""), 'history_visibility', HistoryVisibility.Shared, ); const encrypted = MatrixClientPeg.get().isRoomEncrypted(this.props.roomId); - this.setState({ joinRule, guestAccess, history, encrypted }); + this.setState({ joinRule, restrictedAllowRoomIds, guestAccess, history, encrypted }); this.hasAliases().then(hasAliases => this.setState({ hasAliases })); - cli.getCapabilities().then(capabilities => this.setState({ - roomVersionsCapability: capabilities["m.room_versions"], - })); + cli.getCapabilities().then(capabilities => { + const roomCapabilities = capabilities["org.matrix.msc3244.room_capabilities"]; + const roomSupportsRestricted = roomCapabilities && Array.isArray(roomCapabilities["restricted"]?.support) && + roomCapabilities["restricted"].support.includes(room.getVersion()); + const preferredRestrictionVersion = roomSupportsRestricted + ? roomCapabilities?.["restricted"].preferred + : undefined; + + this.setState({ roomSupportsRestricted, preferredRestrictionVersion }); + }); } private pullContentPropertyFromEvent(event: MatrixEvent, key: string, defaultValue: T): T { - if (!event || !event.getContent()) return defaultValue; - return event.getContent()[key] || defaultValue; + return event?.getContent()[key] || defaultValue; } componentWillUnmount() { @@ -151,81 +166,80 @@ export default class SecurityRoomSettingsTab extends React.Component { - e.preventDefault(); - e.stopPropagation(); + private onJoinRuleChange = (joinRule: JoinRule) => { + if (joinRule === JoinRule.Restricted && + !this.state.roomSupportsRestricted && + this.state.preferredRestrictionVersion + ) { + const cli = MatrixClientPeg.get(); + const roomId = this.props.roomId; + const room = cli.getRoom(roomId); + const targetVersion = this.state.preferredRestrictionVersion; + const activeSpace = SpaceStore.instance.activeSpace; + Modal.createTrackedDialog('Restricted join rule upgrade', '', RoomUpgradeWarningDialog, { + roomId, + targetVersion, + description: _t("This upgrade will allow members of selected spaces " + + "access to this room without an invite."), + onFinished: async (resp) => { + if (!resp?.continue) return; + const { replacement_room: newRoomId } = await upgradeRoom(room, targetVersion, resp.invite); - const guestAccess = GuestAccess.CanJoin; + const content: IContent = { + join_rule: JoinRule.Restricted, + }; + if (activeSpace) { + content.allow = [{ + "type": RestrictedAllowType.RoomMembership, + "room_id": activeSpace.roomId, + }]; + } + + cli.sendStateEvent(newRoomId, EventType.RoomJoinRules, content); + }, + }); + return; + } + + const beforeJoinRule = this.state.joinRule; + this.setState({ joinRule }); + + const client = MatrixClientPeg.get(); + client.sendStateEvent(this.props.roomId, EventType.RoomJoinRules, { + join_rule: joinRule, + }, "").catch((e) => { + console.error(e); + this.setState({ joinRule: beforeJoinRule }); + }); + }; + + private onRestrictedRoomIdsChange = (restrictedAllowRoomIds: string[]) => { + const beforeRestrictedAllowRoomIds = this.state.restrictedAllowRoomIds; + this.setState({ restrictedAllowRoomIds }); + + const client = MatrixClientPeg.get(); + client.sendStateEvent(this.props.roomId, EventType.RoomJoinRules, { + join_rule: JoinRule.Restricted, + allow: restrictedAllowRoomIds.map(roomId => ({ + "type": RestrictedAllowType.RoomMembership, + "room_id": roomId, + })), + }, "").catch((e) => { + console.error(e); + this.setState({ restrictedAllowRoomIds: beforeRestrictedAllowRoomIds }); + }); + }; + + private onGuestAccessChange = (allowed: boolean) => { + const guestAccess = allowed ? GuestAccess.CanJoin : GuestAccess.Forbidden; const beforeGuestAccess = this.state.guestAccess; this.setState({ guestAccess }); const client = MatrixClientPeg.get(); - client.sendStateEvent( - this.props.roomId, - EventType.RoomGuestAccess, - { guest_access: guestAccess, }, - "", - ).catch((e) => { - console.error(e); - this.setState({ guestAccess: beforeGuestAccess }); - }); - }; - - private onRoomAccessRadioToggle = (roomAccess: RoomVisibility) => { - // join_rule - // INVITE | PUBLIC | RESTRICTED - // -----------+----------+----------------+------------- - // guest CAN_JOIN | inv_only | pub_with_guest | restricted - // access -----------+----------+----------------+------------- - // FORBIDDEN | inv_only | pub_no_guest | restricted - // -----------+----------+----------------+------------- - - // we always set guests can_join here as it makes no sense to have - // an invite-only room that guests can't join. If you explicitly - // 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 = JoinRule.Invite; - let guestAccess = GuestAccess.CanJoin; - - switch (roomAccess) { - case RoomVisibility.InviteOnly: - // no change - use defaults above - break; - case RoomVisibility.Restricted: - joinRule = JoinRule.Restricted; - break; - case RoomVisibility.PublicNoGuests: - joinRule = JoinRule.Public; - guestAccess = GuestAccess.Forbidden; - break; - case RoomVisibility.PublicWithGuests: - joinRule = JoinRule.Public; - guestAccess = GuestAccess.CanJoin; - break; - } - - const beforeJoinRule = this.state.joinRule; - const beforeGuestAccess = this.state.guestAccess; - this.setState({ joinRule, guestAccess }); - - const client = MatrixClientPeg.get(); - client.sendStateEvent( - this.props.roomId, - EventType.RoomJoinRules, { - join_rule: joinRule, - }, "", - ).catch((e) => { - console.error(e); - this.setState({ joinRule: beforeJoinRule }); - }); - client.sendStateEvent( - this.props.roomId, - EventType.RoomGuestAccess, { + client.sendStateEvent(this.props.roomId, EventType.RoomGuestAccess, { guest_access: guestAccess, - }, "", - ).catch((e) => { + }, "").catch((e) => { console.error(e); this.setState({ guestAccess: beforeGuestAccess }); }); @@ -260,27 +274,25 @@ export default class SecurityRoomSettingsTab extends React.Component { + const matrixClient = MatrixClientPeg.get(); + Modal.createTrackedDialog('Edit restricted', '', ManageRestrictedJoinRuleDialog, { + matrixClient, + room: matrixClient.getRoom(this.props.roomId), + selected: this.state.restrictedAllowRoomIds, + onFinished: (restrictedAllowRoomIds?: string[]) => { + if (!Array.isArray(restrictedAllowRoomIds)) return; + this.onRestrictedRoomIdsChange(restrictedAllowRoomIds); + }, + }, "mx_ManageRestrictedJoinRuleDialog_wrapper"); + }; + + private renderJoinRule() { const client = MatrixClientPeg.get(); const room = client.getRoom(this.props.roomId); const joinRule = this.state.joinRule; - const guestAccess = this.state.guestAccess; - const canChangeAccess = room.currentState.mayClientSendStateEvent(EventType.RoomJoinRules, client) - && room.currentState.mayClientSendStateEvent(EventType.RoomGuestAccess, client); - - let guestWarning = null; - if (joinRule !== JoinRule.Public && guestAccess === GuestAccess.Forbidden) { - guestWarning = ( -
- - - {_t("Guests cannot join this room even if explicitly invited.")}  - {_t("Click here to fix")} - -
- ); - } + const canChangeJoinRule = room.currentState.mayClientSendStateEvent(EventType.RoomJoinRules, client); let aliasWarning = null; if (joinRule === JoinRule.Public && !this.state.hasAliases) { @@ -294,46 +306,98 @@ export default class SecurityRoomSettingsTab extends React.Component[] = [ - { - value: RoomVisibility.InviteOnly, - label: _t('Only people who have been invited'), - checked: joinRule !== JoinRule.Public && joinRule !== JoinRule.Restricted, - }, - { - value: RoomVisibility.PublicNoGuests, - label: _t('Anyone who knows the room\'s link, apart from guests'), - checked: joinRule === JoinRule.Public && guestAccess !== GuestAccess.CanJoin, - }, - { - value: RoomVisibility.PublicWithGuests, - label: _t("Anyone who knows the room's link, including guests"), - checked: joinRule === JoinRule.Public && guestAccess === GuestAccess.CanJoin, - }, - ]; + const radioDefinitions: IDefinition[] = [{ + value: JoinRule.Invite, + label: _t("Private (invite only)"), + description: _t("Only invited people can join."), + }, { + value: JoinRule.Public, + label: _t("Public (anyone)"), + description: _t("Anyone can find and join."), + }]; - const roomCapabilities = this.state.roomVersionsCapability?.["org.matrix.msc3244.room_capabilities"]; - if (roomCapabilities?.["restricted"]) { - if (Array.isArray(roomCapabilities["restricted"]?.support) && - roomCapabilities["restricted"].support.includes(room.getVersion() ?? "1") - ) { - radioDefinitions.unshift({ - value: RoomVisibility.Restricted, - label: _t("Only people in certain spaces or those who have been invited (TODO copy)"), - checked: joinRule === JoinRule.Restricted, - }); + if (this.state.roomSupportsRestricted || + this.state.preferredRestrictionVersion || + joinRule === JoinRule.Restricted + ) { + let upgradeRequiredPill; + if (this.state.preferredRestrictionVersion) { + upgradeRequiredPill = + { _t("Upgrade required") } + ; } + + let description; + if (joinRule === JoinRule.Restricted) { + let spacesWhichCanAccess; + if (this.state.restrictedAllowRoomIds?.length) { + const shownSpaces = this.state.restrictedAllowRoomIds + .map(roomId => client.getRoom(roomId)) + .filter(Boolean) + .slice(0, 4); + + spacesWhichCanAccess =
+

{ _t("Spaces with access") }

+ { shownSpaces.map(room => { + return + + { room.name } + ; + })} + { shownSpaces.length < this.state.restrictedAllowRoomIds.length && + { _t("& %(count)s more", { + count: this.state.restrictedAllowRoomIds.length - shownSpaces.length, + }) } + } +
; + } + + description =
+ + { _t("Anyone in a space can find and join. Edit which spaces can access here.", {}, { + a: sub => + { sub } + , + }) } + + { spacesWhichCanAccess } +
; + } else if (SpaceStore.instance.activeSpace) { + description = _t("Anyone in %(spaceName)s can find and join. You can select other spaces too.", { + spaceName: SpaceStore.instance.activeSpace.name, + }); + } else { + description = _t("Anyone in a space can find and join. You can select multiple spaces."); + } + + radioDefinitions.splice(1, 0, { + value: JoinRule.Restricted, + label: <> + { _t("Space members") } + { upgradeRequiredPill } + , + description, + }); } return ( -
- { guestWarning } +
+
+ { _t("Decide who can view and join %(roomName)s.", { + roomName: client.getRoom(this.props.roomId)?.name, + }) } +
{ aliasWarning }
); @@ -382,6 +446,30 @@ export default class SecurityRoomSettingsTab extends React.Component { + this.setState({ showAdvancedSection: !this.state.showAdvancedSection }); + }; + + private renderAdvanced() { + const client = MatrixClientPeg.get(); + const guestAccess = this.state.guestAccess; + const state = client.getRoom(this.props.roomId).currentState; + const canSetGuestAccess = state.mayClientSendStateEvent(EventType.RoomGuestAccess, client); + + return <> + +

+ { _t("People with supported clients will be able to join " + + "the room without having a registered account.") } +

+ ; + } + render() { const SettingsFlag = sdk.getComponent("elements.SettingsFlag"); @@ -413,27 +501,39 @@ export default class SecurityRoomSettingsTab extends React.Component -
{_t("Security & Privacy")}
+
{ _t("Security & Privacy") }
- {_t("Encryption")} + { _t("Encryption") }
- {_t("Once enabled, encryption cannot be disabled.")} + { _t("Once enabled, encryption cannot be disabled.") }
-
- {encryptionSettings} + { encryptionSettings }
- {_t("Who can access this room?")} + {_t("Access")}
- {this.renderRoomAccess()} + { this.renderJoinRule() }
- {historySection} + + { this.state.showAdvancedSection ? _t("Hide advanced") : _t("Show advanced") } + + { this.state.showAdvancedSection && this.renderAdvanced() } + + { historySection }
); } diff --git a/src/createRoom.ts b/src/createRoom.ts index f8a2665704..0a88e2cef7 100644 --- a/src/createRoom.ts +++ b/src/createRoom.ts @@ -37,8 +37,6 @@ import { VIRTUAL_ROOM_EVENT_TYPE } from "./CallHandler"; import SpaceStore from "./stores/SpaceStore"; import { makeSpaceParentEvent } from "./utils/space"; import { Action } from "./dispatcher/actions"; -import { ICreateRoomOpts } from "matrix-js-sdk/src/@types/requests"; -import { Preset, Visibility } from "matrix-js-sdk/src/@types/partials"; // we define a number of interfaces which take their names from the js-sdk /* eslint-disable camelcase */ @@ -53,6 +51,7 @@ export interface IOpts { andView?: boolean; associatedWithCommunity?: string; parentSpace?: Room; + joinRule?: JoinRule; } /** @@ -153,10 +152,10 @@ export default async function createRoom(opts: IOpts): Promise { }, }); - if (opts.parentSpace.getJoinRule() !== JoinRule.Public && opts.createOpts.preset !== Preset.PublicChat) { + if (opts.joinRule === JoinRule.Restricted) { const serverCapabilities = await client.getCapabilities(); const roomCapabilities = serverCapabilities?.["m.room_versions"]?.["org.matrix.msc3244.room_capabilities"]; - if (roomCapabilities?.["restricted"]) { + if (roomCapabilities?.["restricted"]?.preferred) { opts.createOpts.room_version = roomCapabilities?.["restricted"].preferred; opts.createOpts.initial_state.push({ @@ -168,11 +167,18 @@ export default async function createRoom(opts: IOpts): Promise { "room_id": opts.parentSpace.roomId, }], }, - }) + }); } } } + if (opts.joinRule !== JoinRule.Restricted) { + opts.createOpts.initial_state.push({ + type: EventType.RoomJoinRules, + content: { join_rule: opts.joinRule }, + }); + } + let modal; if (opts.spinner) modal = Modal.createDialog(Loader, null, 'mx_Dialog_spinner'); diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 618d5763fa..860fea32d8 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -434,8 +434,6 @@ "To use it, just wait for autocomplete results to load and tab through them.": "To use it, just wait for autocomplete results to load and tab through them.", "Upgrades a room to a new version": "Upgrades a room to a new version", "You do not have the required permissions to use this command.": "You do not have the required permissions to use this command.", - "Error upgrading room": "Error upgrading room", - "Double check that your server supports the room version chosen and try again.": "Double check that your server supports the room version chosen and try again.", "Changes your display nickname": "Changes your display nickname", "Changes your display nickname in the current room only": "Changes your display nickname in the current room only", "Changes the avatar of the current room": "Changes the avatar of the current room", @@ -728,6 +726,8 @@ "Common names and surnames are easy to guess": "Common names and surnames are easy to guess", "Straight rows of keys are easy to guess": "Straight rows of keys are easy to guess", "Short keyboard patterns are easy to guess": "Short keyboard patterns are easy to guess", + "Error upgrading room": "Error upgrading room", + "Double check that your server supports the room version chosen and try again.": "Double check that your server supports the room version chosen and try again.", "Invite to %(spaceName)s": "Invite to %(spaceName)s", "Share your public space": "Share your public space", "Unknown App": "Unknown App", @@ -1435,22 +1435,31 @@ "Select the roles required to change various parts of the room": "Select the roles required to change various parts of the room", "Enable encryption?": "Enable encryption?", "Once enabled, encryption for a room cannot be disabled. Messages sent in an encrypted room cannot be seen by the server, only by the participants of the room. Enabling encryption may prevent many bots and bridges from working correctly. Learn more about encryption.": "Once enabled, encryption for a room cannot be disabled. Messages sent in an encrypted room cannot be seen by the server, only by the participants of the room. Enabling encryption may prevent many bots and bridges from working correctly. Learn more about encryption.", - "Guests cannot join this room even if explicitly invited.": "Guests cannot join this room even if explicitly invited.", - "Click here to fix": "Click here to fix", + "This upgrade will allow members of selected spaces access to this room without an invite.": "This upgrade will allow members of selected spaces access to this room without an invite.", "To link to this room, please add an address.": "To link to this room, please add an address.", - "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", + "Private (invite only)": "Private (invite only)", + "Only invited people can join.": "Only invited people can join.", + "Public (anyone)": "Public (anyone)", + "Anyone can find and join.": "Anyone can find and join.", + "Upgrade required": "Upgrade required", + "Spaces with access": "Spaces with access", + "& %(count)s more|other": "& %(count)s more", + "Anyone in a space can find and join. Edit which spaces can access here.": "Anyone in a space can find and join. Edit which spaces can access here.", + "Anyone in %(spaceName)s can find and join. You can select other spaces too.": "Anyone in %(spaceName)s can find and join. You can select other spaces too.", + "Anyone in a space can find and join. You can select multiple spaces.": "Anyone in a space can find and join. You can select multiple spaces.", + "Space members": "Space members", + "Decide who can view and join %(roomName)s.": "Decide who can view and join %(roomName)s.", "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)", + "People with supported clients will be able to join the room without having a registered account.": "People with supported clients will be able to join the room without having a registered account.", "Who can read history?": "Who can read history?", "Security & Privacy": "Security & Privacy", "Once enabled, encryption cannot be disabled.": "Once enabled, encryption cannot be disabled.", "Encrypted": "Encrypted", - "Who can access this room?": "Who can access this room?", + "Access": "Access", "Unable to revoke sharing for email address": "Unable to revoke sharing for email address", "Unable to share email address": "Unable to share email address", "Your email address hasn't been verified yet": "Your email address hasn't been verified yet", @@ -2331,6 +2340,16 @@ "Manually export keys": "Manually export keys", "You'll lose access to your encrypted messages": "You'll lose access to your encrypted messages", "Are you sure you want to sign out?": "Are you sure you want to sign out?", + "%(count)s members|other": "%(count)s members", + "%(count)s members|one": "%(count)s member", + "%(count)s rooms|other": "%(count)s rooms", + "%(count)s rooms|one": "%(count)s room", + "Select spaces": "Select spaces", + "Decide which spaces can access this room. If a space is selected its members will be able to find and join .": "Decide which spaces can access this room. If a space is selected its members will be able to find and join .", + "Search spaces": "Search spaces", + "Spaces you know that contain this room": "Spaces you know that contain this room", + "Other spaces or rooms you might not know": "Other spaces or rooms you might not know", + "These are likely ones other room admins are a part of.": "These are likely ones other room admins are a part of.", "Confirm by comparing the following with the User Settings in your other session:": "Confirm by comparing the following with the User Settings in your other session:", "Confirm this user's session by comparing the following with their User Settings:": "Confirm this user's session by comparing the following with their User Settings:", "Session name": "Session name", @@ -2374,12 +2393,13 @@ "Update any local room aliases to point to the new room": "Update any local room aliases to point to the new room", "Stop users from speaking in the old version of the room, and post a message advising users to move to the new room": "Stop users from speaking in the old version of the room, and post a message advising users to move to the new room", "Put a link back to the old room at the start of the new room so people can see old messages": "Put a link back to the old room at the start of the new room so people can see old messages", - "Automatically invite users": "Automatically invite users", + "Automatically invite members from this room to the new one": "Automatically invite members from this room to the new one", "Upgrade private room": "Upgrade private room", "Upgrade public room": "Upgrade public room", "This usually only affects how the room is processed on the server. If you're having problems with your %(brand)s, please report a bug.": "This usually only affects how the room is processed on the server. If you're having problems with your %(brand)s, please report a bug.", "This usually only affects how the room is processed on the server. If you're having problems with your %(brand)s, please report a bug.": "This usually only affects how the room is processed on the server. If you're having problems with your %(brand)s, please report a bug.", "Upgrading a room is an advanced action and is usually recommended when a room is unstable due to bugs, missing features or security vulnerabilities.": "Upgrading a room is an advanced action and is usually recommended when a room is unstable due to bugs, missing features or security vulnerabilities.", + "Please note upgrading will make a new version of the room. All current messages will stay in this archived room.": "Please note upgrading will make a new version of the room. All current messages will stay in this archived room.", "You'll upgrade this room from to .": "You'll upgrade this room from to .", "Resend": "Resend", "You're all caught up.": "You're all caught up.", @@ -2636,6 +2656,7 @@ "You are an administrator of this community": "You are an administrator of this community", "You are a member of this community": "You are a member of this community", "Who can join this community?": "Who can join this community?", + "Only people who have been invited": "Only people who have been invited", "Everyone": "Everyone", "Your community hasn't got a Long Description, a HTML page to show to community members.
Click here to open settings and give it one!": "Your community hasn't got a Long Description, a HTML page to show to community members.
Click here to open settings and give it one!", "Long Description (HTML)": "Long Description (HTML)", @@ -2733,10 +2754,6 @@ "You have %(count)s unread notifications in a prior version of this room.|other": "You have %(count)s unread notifications in a prior version of this room.", "You have %(count)s unread notifications in a prior version of this room.|one": "You have %(count)s unread notification in a prior version of this room.", "You don't have permission": "You don't have permission", - "%(count)s members|other": "%(count)s members", - "%(count)s members|one": "%(count)s member", - "%(count)s rooms|other": "%(count)s rooms", - "%(count)s rooms|one": "%(count)s room", "This room is suggested as a good one to join": "This room is suggested as a good one to join", "Suggested": "Suggested", "Your server does not support showing space hierarchies.": "Your server does not support showing space hierarchies.", diff --git a/src/utils/RoomUpgrade.ts b/src/utils/RoomUpgrade.ts new file mode 100644 index 0000000000..7330b23863 --- /dev/null +++ b/src/utils/RoomUpgrade.ts @@ -0,0 +1,74 @@ +/* +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 { Room } from "matrix-js-sdk/src/models/room"; + +import { inviteUsersToRoom } from "../RoomInvite"; +import Modal from "../Modal"; +import { _t } from "../languageHandler"; +import ErrorDialog from "../components/views/dialogs/ErrorDialog"; + +export async function upgradeRoom( + room: Room, + targetVersion: string, + inviteUsers = false, + // eslint-disable-next-line camelcase +): Promise<{ replacement_room: string }> { + const cli = room.client; + + let checkForUpgradeFn: (room: Room) => Promise; + try { + const upgradePromise = cli.upgradeRoom(room.roomId, targetVersion); + + // We have to wait for the js-sdk to give us the room back so + // we can more effectively abuse the MultiInviter behaviour + // which heavily relies on the Room object being available. + if (inviteUsers) { + checkForUpgradeFn = async (newRoom: Room) => { + // The upgradePromise should be done by the time we await it here. + const { replacement_room: newRoomId } = await upgradePromise; + if (newRoom.roomId !== newRoomId) return; + + const toInvite = [ + ...room.getMembersWithMembership("join"), + ...room.getMembersWithMembership("invite"), + ].map(m => m.userId).filter(m => m !== cli.getUserId()); + + if (toInvite.length > 0) { + // Errors are handled internally to this function + await inviteUsersToRoom(newRoomId, toInvite); + } + + cli.removeListener('Room', checkForUpgradeFn); + }; + cli.on('Room', checkForUpgradeFn); + } + + // We have to await after so that the checkForUpgradesFn has a proper reference + // to the new room's ID. + return upgradePromise; + } catch (e) { + console.error(e); + + if (checkForUpgradeFn) cli.removeListener('Room', checkForUpgradeFn); + + Modal.createTrackedDialog('Slash Commands', 'room upgrade error', ErrorDialog, { + title: _t('Error upgrading room'), + description: _t('Double check that your server supports the room version chosen and try again.'), + }); + throw e; + } +} From 912e192dc64f780b091cca9cccacf9bb4fcf7240 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 2 Jul 2021 15:18:27 +0100 Subject: [PATCH 0193/1171] Tweak behaviour of setting restricted join rule --- .../views/elements/MiniAvatarUploader.tsx | 2 +- .../tabs/room/SecurityRoomSettingsTab.tsx | 96 ++++++++++++------- .../spaces/SpaceSettingsVisibilityTab.tsx | 2 +- src/settings/handlers/RoomSettingsHandler.ts | 4 +- 4 files changed, 63 insertions(+), 41 deletions(-) diff --git a/src/components/views/elements/MiniAvatarUploader.tsx b/src/components/views/elements/MiniAvatarUploader.tsx index 83fc1ebefd..b38e21977c 100644 --- a/src/components/views/elements/MiniAvatarUploader.tsx +++ b/src/components/views/elements/MiniAvatarUploader.tsx @@ -32,7 +32,7 @@ interface IProps { hasAvatar: boolean; noAvatarLabel?: string; hasAvatarLabel?: string; - setAvatarUrl(url: string): Promise; + setAvatarUrl(url: string): Promise; } const MiniAvatarUploader: React.FC = ({ hasAvatar, hasAvatarLabel, noAvatarLabel, setAvatarUrl, children }) => { diff --git a/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx b/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx index 34f5b8c94c..ceb7dd21bd 100644 --- a/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx +++ b/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx @@ -36,6 +36,7 @@ import RoomAvatar from "../../../avatars/RoomAvatar"; import ManageRestrictedJoinRuleDialog from '../../../dialogs/ManageRestrictedJoinRuleDialog'; import RoomUpgradeWarningDialog from '../../../dialogs/RoomUpgradeWarningDialog'; import { upgradeRoom } from "../../../../../utils/RoomUpgrade"; +import { arrayHasDiff } from "../../../../../utils/arrays"; interface IProps { roomId: string; @@ -166,56 +167,73 @@ export default class SecurityRoomSettingsTab extends React.Component { - if (joinRule === JoinRule.Restricted && - !this.state.roomSupportsRestricted && - this.state.preferredRestrictionVersion - ) { - const cli = MatrixClientPeg.get(); + private onJoinRuleChange = async (joinRule: JoinRule) => { + const beforeJoinRule = this.state.joinRule; + if (beforeJoinRule === joinRule) return; + + if (joinRule === JoinRule.Restricted) { + const matrixClient = MatrixClientPeg.get(); const roomId = this.props.roomId; - const room = cli.getRoom(roomId); - const targetVersion = this.state.preferredRestrictionVersion; - const activeSpace = SpaceStore.instance.activeSpace; - Modal.createTrackedDialog('Restricted join rule upgrade', '', RoomUpgradeWarningDialog, { - roomId, - targetVersion, - description: _t("This upgrade will allow members of selected spaces " + - "access to this room without an invite."), - onFinished: async (resp) => { - if (!resp?.continue) return; - const { replacement_room: newRoomId } = await upgradeRoom(room, targetVersion, resp.invite); + const room = matrixClient.getRoom(roomId); - const content: IContent = { - join_rule: JoinRule.Restricted, - }; + if (this.state.roomSupportsRestricted) { + // Have the user pick which spaces to allow joins from + const { finished } = Modal.createTrackedDialog('Set restricted', '', ManageRestrictedJoinRuleDialog, { + matrixClient, + room, + // if they have are viewing this room from the context of a space then default to that + selected: SpaceStore.instance.activeSpace ? [SpaceStore.instance.activeSpace.roomId] : [], + }, "mx_ManageRestrictedJoinRuleDialog_wrapper"); - if (activeSpace) { - content.allow = [{ - "type": RestrictedAllowType.RoomMembership, - "room_id": activeSpace.roomId, - }]; - } - - cli.sendStateEvent(newRoomId, EventType.RoomJoinRules, content); - }, - }); - return; + const [restrictedAllowRoomIds] = await finished; + if (!Array.isArray(restrictedAllowRoomIds)) return; + } else if (this.state.preferredRestrictionVersion) { + // Block this action on a room upgrade otherwise it'd make their room unjoinable + const targetVersion = this.state.preferredRestrictionVersion; + Modal.createTrackedDialog('Restricted join rule upgrade', '', RoomUpgradeWarningDialog, { + roomId, + targetVersion, + description: _t("This upgrade will allow members of selected spaces " + + "access to this room without an invite."), + onFinished: (resp) => { + if (!resp?.continue) return; + upgradeRoom(room, targetVersion, resp.invite); + }, + }); + return; + } } - const beforeJoinRule = this.state.joinRule; - this.setState({ joinRule }); + const content: IContent = { + join_rule: joinRule, + }; + + let restrictedAllowRoomIds: string[]; + // pre-set the accepted spaces with the currently viewed one as per the microcopy + if (joinRule === JoinRule.Restricted && SpaceStore.instance.activeSpace) { + const spaceRoomId = SpaceStore.instance.activeSpace.roomId; + restrictedAllowRoomIds = [spaceRoomId]; + content.allow = [{ + "type": RestrictedAllowType.RoomMembership, + "room_id": spaceRoomId, + }]; + } + + this.setState({ joinRule, restrictedAllowRoomIds }); const client = MatrixClientPeg.get(); - client.sendStateEvent(this.props.roomId, EventType.RoomJoinRules, { - join_rule: joinRule, - }, "").catch((e) => { + client.sendStateEvent(this.props.roomId, EventType.RoomJoinRules, content, "").catch((e) => { console.error(e); - this.setState({ joinRule: beforeJoinRule }); + this.setState({ + joinRule: beforeJoinRule, + restrictedAllowRoomIds: undefined, + }); }); }; private onRestrictedRoomIdsChange = (restrictedAllowRoomIds: string[]) => { const beforeRestrictedAllowRoomIds = this.state.restrictedAllowRoomIds; + if (!arrayHasDiff(beforeRestrictedAllowRoomIds || [], restrictedAllowRoomIds)) return; this.setState({ restrictedAllowRoomIds }); const client = MatrixClientPeg.get(); @@ -234,6 +252,8 @@ export default class SecurityRoomSettingsTab extends React.Component { const guestAccess = allowed ? GuestAccess.CanJoin : GuestAccess.Forbidden; const beforeGuestAccess = this.state.guestAccess; + if (beforeGuestAccess === guestAccess) return; + this.setState({ guestAccess }); const client = MatrixClientPeg.get(); @@ -247,6 +267,8 @@ export default class SecurityRoomSettingsTab extends React.Component { const beforeHistory = this.state.history; + if (beforeHistory === history) return; + this.setState({ history: history }); MatrixClientPeg.get().sendStateEvent(this.props.roomId, EventType.RoomHistoryVisibility, { history_visibility: history, diff --git a/src/components/views/spaces/SpaceSettingsVisibilityTab.tsx b/src/components/views/spaces/SpaceSettingsVisibilityTab.tsx index 5d76f7d2c2..3257ce8fb0 100644 --- a/src/components/views/spaces/SpaceSettingsVisibilityTab.tsx +++ b/src/components/views/spaces/SpaceSettingsVisibilityTab.tsx @@ -39,7 +39,7 @@ enum SpaceVisibility { const useLocalEcho = ( currentFactory: () => T, - setterFn: (value: T) => Promise, + setterFn: (value: T) => Promise, errorFn: (error: Error) => void, ): [value: T, handler: (value: T) => void] => { const [value, setValue] = useState(currentFactory); diff --git a/src/settings/handlers/RoomSettingsHandler.ts b/src/settings/handlers/RoomSettingsHandler.ts index 3315e40a65..b8db07f6bb 100644 --- a/src/settings/handlers/RoomSettingsHandler.ts +++ b/src/settings/handlers/RoomSettingsHandler.ts @@ -92,12 +92,12 @@ export default class RoomSettingsHandler extends MatrixClientBackedSettingsHandl if (settingName === "urlPreviewsEnabled") { const content = this.getSettings(roomId, "org.matrix.room.preview_urls") || {}; content['disable'] = !newValue; - return MatrixClientPeg.get().sendStateEvent(roomId, "org.matrix.room.preview_urls", content); + return MatrixClientPeg.get().sendStateEvent(roomId, "org.matrix.room.preview_urls", content).then(); } const content = this.getSettings(roomId) || {}; content[settingName] = newValue; - return MatrixClientPeg.get().sendStateEvent(roomId, "im.vector.web.settings", content, ""); + return MatrixClientPeg.get().sendStateEvent(roomId, "im.vector.web.settings", content, "").then(); } public canSetValue(settingName: string, roomId: string): boolean { From 89949bd884ee39d131d51d38a1b8861da2e82549 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 2 Jul 2021 16:07:17 +0100 Subject: [PATCH 0194/1171] Add new in the spaces beta toast & explanatory modal --- src/components/structures/SpaceRoomView.tsx | 9 ++- .../dialogs/AddExistingToSpaceDialog.tsx | 10 ++- .../dialogs/{InfoDialog.js => InfoDialog.tsx} | 35 +++++----- src/components/views/rooms/RoomList.tsx | 4 +- .../views/spaces/SpaceTreeLevel.tsx | 8 +-- src/i18n/strings/en_EN.json | 15 ++-- src/stores/SpaceStore.tsx | 70 +++++++++++++++++++ src/utils/space.tsx | 17 +++-- 8 files changed, 119 insertions(+), 49 deletions(-) rename src/components/views/dialogs/{InfoDialog.js => InfoDialog.tsx} (77%) diff --git a/src/components/structures/SpaceRoomView.tsx b/src/components/structures/SpaceRoomView.tsx index 3880e014aa..cb440ca576 100644 --- a/src/components/structures/SpaceRoomView.tsx +++ b/src/components/structures/SpaceRoomView.tsx @@ -307,7 +307,6 @@ const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }) => }; const SpaceLandingAddButton = ({ space, onNewRoomAdded }) => { - const cli = useContext(MatrixClientContext); const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu(); let contextMenu; @@ -330,7 +329,7 @@ const SpaceLandingAddButton = ({ space, onNewRoomAdded }) => { e.stopPropagation(); closeMenu(); - if (await showCreateNewRoom(cli, space)) { + if (await showCreateNewRoom(space)) { onNewRoomAdded(); } }} @@ -343,7 +342,7 @@ const SpaceLandingAddButton = ({ space, onNewRoomAdded }) => { e.stopPropagation(); closeMenu(); - const [added] = await showAddExistingRooms(cli, space); + const [added] = await showAddExistingRooms(space); if (added) { onNewRoomAdded(); } @@ -397,11 +396,11 @@ const SpaceLanding = ({ space }) => { } let settingsButton; - if (shouldShowSpaceSettings(cli, space)) { + if (shouldShowSpaceSettings(space)) { settingsButton = { - showSpaceSettings(cli, space); + showSpaceSettings(space); }} title={_t("Settings")} />; diff --git a/src/components/views/dialogs/AddExistingToSpaceDialog.tsx b/src/components/views/dialogs/AddExistingToSpaceDialog.tsx index c09097c4b4..adb36485a7 100644 --- a/src/components/views/dialogs/AddExistingToSpaceDialog.tsx +++ b/src/components/views/dialogs/AddExistingToSpaceDialog.tsx @@ -17,7 +17,6 @@ limitations under the License. import React, { ReactNode, 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"; import { _t } from '../../../languageHandler'; import { IDialogProps } from "./IDialogProps"; @@ -44,9 +43,8 @@ import EntityTile from "../rooms/EntityTile"; import BaseAvatar from "../avatars/BaseAvatar"; interface IProps extends IDialogProps { - matrixClient: MatrixClient; space: Room; - onCreateRoomClick(cli: MatrixClient, space: Room): void; + onCreateRoomClick(space: Room): void; } const Entry = ({ room, checked, onChange }) => { @@ -295,7 +293,7 @@ export const AddExistingToSpace: React.FC = ({ ; }; -const AddExistingToSpaceDialog: React.FC = ({ matrixClient: cli, space, onCreateRoomClick, onFinished }) => { +const AddExistingToSpaceDialog: React.FC = ({ space, onCreateRoomClick, onFinished }) => { const [selectedSpace, setSelectedSpace] = useState(space); const existingSubspaces = SpaceStore.instance.getChildSpaces(space.roomId); @@ -344,13 +342,13 @@ const AddExistingToSpaceDialog: React.FC = ({ matrixClient: cli, space, onFinished={onFinished} fixedWidth={false} > - +
{ _t("Want to add a new room instead?") }
- onCreateRoomClick(cli, space)} kind="link"> + onCreateRoomClick(space)} kind="link"> { _t("Create a new room") } } diff --git a/src/components/views/dialogs/InfoDialog.js b/src/components/views/dialogs/InfoDialog.tsx similarity index 77% rename from src/components/views/dialogs/InfoDialog.js rename to src/components/views/dialogs/InfoDialog.tsx index 8207d334d3..8570f46d27 100644 --- a/src/components/views/dialogs/InfoDialog.js +++ b/src/components/views/dialogs/InfoDialog.tsx @@ -1,7 +1,6 @@ /* -Copyright 2015, 2016 OpenMarket Ltd -Copyright 2017 New Vector Ltd. Copyright 2019 Bastian Masanek, Noxware IT +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. @@ -16,31 +15,31 @@ 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 { _t } from '../../../languageHandler'; +import React, { ReactNode, KeyboardEvent } from 'react'; import classNames from "classnames"; -export default class InfoDialog extends React.Component { - static propTypes = { - className: PropTypes.string, - title: PropTypes.string, - description: PropTypes.node, - button: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]), - onFinished: PropTypes.func, - hasCloseButton: PropTypes.bool, - onKeyDown: PropTypes.func, - fixedWidth: PropTypes.bool, - }; +import { _t } from '../../../languageHandler'; +import * as sdk from '../../../index'; +import { IDialogProps } from "./IDialogProps"; +interface IProps extends IDialogProps { + title?: string; + description?: ReactNode; + className?: string; + button?: boolean | string; + hasCloseButton?: boolean; + fixedWidth?: boolean; + onKeyDown?(event: KeyboardEvent): void; +} + +export default class InfoDialog extends React.Component { static defaultProps = { title: '', description: '', hasCloseButton: false, }; - onFinished = () => { + private onFinished = () => { this.props.onFinished(); }; diff --git a/src/components/views/rooms/RoomList.tsx b/src/components/views/rooms/RoomList.tsx index c94256800d..5c683711fc 100644 --- a/src/components/views/rooms/RoomList.tsx +++ b/src/components/views/rooms/RoomList.tsx @@ -140,7 +140,7 @@ const TAG_AESTHETICS: ITagAestheticsMap = { e.preventDefault(); e.stopPropagation(); onFinished(); - showCreateNewRoom(MatrixClientPeg.get(), SpaceStore.instance.activeSpace); + showCreateNewRoom(SpaceStore.instance.activeSpace); }} disabled={!canAddRooms} tooltip={canAddRooms ? undefined @@ -153,7 +153,7 @@ const TAG_AESTHETICS: ITagAestheticsMap = { e.preventDefault(); e.stopPropagation(); onFinished(); - showAddExistingRooms(MatrixClientPeg.get(), SpaceStore.instance.activeSpace); + showAddExistingRooms(SpaceStore.instance.activeSpace); }} disabled={!canAddRooms} tooltip={canAddRooms ? undefined diff --git a/src/components/views/spaces/SpaceTreeLevel.tsx b/src/components/views/spaces/SpaceTreeLevel.tsx index 486a988b93..908506aa3a 100644 --- a/src/components/views/spaces/SpaceTreeLevel.tsx +++ b/src/components/views/spaces/SpaceTreeLevel.tsx @@ -203,7 +203,7 @@ export class SpaceItem extends React.PureComponent { ev.preventDefault(); ev.stopPropagation(); - showSpaceSettings(this.context, this.props.space); + showSpaceSettings(this.props.space); this.setState({ contextMenuPosition: null }); // also close the menu }; @@ -222,7 +222,7 @@ export class SpaceItem extends React.PureComponent { ev.preventDefault(); ev.stopPropagation(); - showCreateNewRoom(this.context, this.props.space); + showCreateNewRoom(this.props.space); this.setState({ contextMenuPosition: null }); // also close the menu }; @@ -230,7 +230,7 @@ export class SpaceItem extends React.PureComponent { ev.preventDefault(); ev.stopPropagation(); - showAddExistingRooms(this.context, this.props.space); + showAddExistingRooms(this.props.space); this.setState({ contextMenuPosition: null }); // also close the menu }; @@ -285,7 +285,7 @@ export class SpaceItem extends React.PureComponent { let settingsOption; let leaveSection; - if (shouldShowSpaceSettings(this.context, this.props.space)) { + if (shouldShowSpaceSettings(this.props.space)) { settingsOption = ( { window.localStorage.removeItem(ACTIVE_SPACE_LS_KEY); } + // New in Spaces beta toast for Restricted Join Rule + (async () => { + const lsKey = "mx_SpaceBeta_restrictedJoinRuleToastSeen"; + if (contextSwitch && space?.getJoinRule() === JoinRule.Invite && shouldShowSpaceSettings(space) && + space.getJoinedMemberCount() > 1 && !localStorage.getItem(lsKey) /*&& + (await this.matrixClient.getCapabilities()) + ?.["m.room_versions"]?.["org.matrix.msc3244.room_capabilities"]?.["restricted"]?.preferred*/ + ) { + const toastKey = "restrictedjoinrule"; + ToastStore.sharedInstance().addOrReplaceToast({ + key: toastKey, + title: _t("New in the Spaces beta"), + props: { + description: _t("Help people in spaces to find and join private rooms"), + acceptLabel: _t("Learn more"), + onAccept: () => { + localStorage.setItem(lsKey, "true"); + ToastStore.sharedInstance().dismissToast(toastKey); + + Modal.createTrackedDialog("New in the Spaces beta", "restricted join rule", InfoDialog, { + title: _t("Help space members find private rooms"), + description: <> +

{ _t("To help space members find and join a private room, " + + "go to that room's Security & Privacy settings.") }

+ + { /* Reuses classes from TabbedView for simplicity, non-interactive */ } +
+
+ + { _t("General") } +
+
+ + { _t("Security & Privacy") } +
+
+ + { _t("Roles & Permissions") } +
+
+ +

{ _t("This make it easy for rooms to stay private to a space, " + + "while letting people in the space find and join them. " + + "All new rooms in a space will have this option available.")}

+ , + button: _t("OK"), + hasCloseButton: false, + fixedWidth: true, + }); + }, + rejectLabel: _t("Skip"), + onReject: () => { + localStorage.setItem(lsKey, "true"); + ToastStore.sharedInstance().dismissToast(toastKey); + }, + }, + component: GenericToast, + priority: 35, + }); + } + })().then(); + if (space) { const suggestedRooms = await this.fetchSuggestedRooms(space); if (this._activeSpace === space) { diff --git a/src/utils/space.tsx b/src/utils/space.tsx index 38f6e348d7..c238a83bc2 100644 --- a/src/utils/space.tsx +++ b/src/utils/space.tsx @@ -16,10 +16,9 @@ limitations under the License. import React from "react"; import { Room } from "matrix-js-sdk/src/models/room"; -import { MatrixClient } from "matrix-js-sdk/src/client"; import { EventType } from "matrix-js-sdk/src/@types/event"; -import { calculateRoomVia } from "../utils/permalinks/Permalinks"; +import { calculateRoomVia } from "./permalinks/Permalinks"; import Modal from "../Modal"; import SpaceSettingsDialog from "../components/views/dialogs/SpaceSettingsDialog"; import AddExistingToSpaceDialog from "../components/views/dialogs/AddExistingToSpaceDialog"; @@ -30,8 +29,8 @@ import SpacePublicShare from "../components/views/spaces/SpacePublicShare"; import InfoDialog from "../components/views/dialogs/InfoDialog"; import { showRoomInviteDialog } from "../RoomInvite"; -export const shouldShowSpaceSettings = (cli: MatrixClient, space: Room) => { - const userId = cli.getUserId(); +export const shouldShowSpaceSettings = (space: Room) => { + const userId = space.client.getUserId(); return space.getMyMembership() === "join" && (space.currentState.maySendStateEvent(EventType.RoomAvatar, userId) || space.currentState.maySendStateEvent(EventType.RoomName, userId) @@ -48,20 +47,20 @@ export const makeSpaceParentEvent = (room: Room, canonical = false) => ({ state_key: room.roomId, }); -export const showSpaceSettings = (cli: MatrixClient, space: Room) => { +export const showSpaceSettings = (space: Room) => { Modal.createTrackedDialog("Space Settings", "", SpaceSettingsDialog, { - matrixClient: cli, + matrixClient: space.client, space, }, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true); }; -export const showAddExistingRooms = async (cli: MatrixClient, space: Room) => { +export const showAddExistingRooms = async (space: Room) => { return Modal.createTrackedDialog( "Space Landing", "Add Existing", AddExistingToSpaceDialog, { - matrixClient: cli, + matrixClient: space.client, onCreateRoomClick: showCreateNewRoom, space, }, @@ -69,7 +68,7 @@ export const showAddExistingRooms = async (cli: MatrixClient, space: Room) => { ).finished; }; -export const showCreateNewRoom = async (cli: MatrixClient, space: Room) => { +export const showCreateNewRoom = async (space: Room) => { const modal = Modal.createTrackedDialog<[boolean, IOpts]>( "Space Landing", "Create Room", From de875bbe1d39286c8a164520a3a1dd0c76aae52c Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Mon, 5 Jul 2021 16:22:18 +0200 Subject: [PATCH 0195/1171] fix avatar position and outline --- res/css/views/rooms/_EventBubbleTile.scss | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/res/css/views/rooms/_EventBubbleTile.scss b/res/css/views/rooms/_EventBubbleTile.scss index 0c204a19ae..c548bfae56 100644 --- a/res/css/views/rooms/_EventBubbleTile.scss +++ b/res/css/views/rooms/_EventBubbleTile.scss @@ -17,7 +17,7 @@ limitations under the License. .mx_EventTile[data-layout=bubble], .mx_EventTile[data-layout=bubble] ~ .mx_EventListSummary { --avatarSize: 32px; - --gutterSize: 7px; + --gutterSize: 11px; --cornerRadius: 12px; --maxWidth: 70%; } @@ -55,9 +55,10 @@ limitations under the License. background: var(--backgroundColor); display: flex; gap: var(--gutterSize); + margin: 0 calc(-2 * var(--gutterSize)); > a { position: absolute; - left: -33px; + left: -50px; } } @@ -66,7 +67,7 @@ limitations under the License. top: 0; line-height: 1; img { - box-shadow: 0 0 0 2px $eventbubble-avatar-outline; + box-shadow: 0 0 0 3px $eventbubble-avatar-outline; border-radius: 50%; } } @@ -89,6 +90,7 @@ limitations under the License. } } .mx_EventTile_avatar { + top: -19px; // height of the sender block right: calc(-1 * var(--avatarSize)); } From 44bbf609732e4169e2c6e979f8d17d750c40ff38 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 6 Jul 2021 09:56:02 +0100 Subject: [PATCH 0196/1171] Convert Dropdown to Typescript --- .../elements/{Dropdown.js => Dropdown.tsx} | 239 ++++++++---------- 1 file changed, 108 insertions(+), 131 deletions(-) rename src/components/views/elements/{Dropdown.js => Dropdown.tsx} (68%) diff --git a/src/components/views/elements/Dropdown.js b/src/components/views/elements/Dropdown.tsx similarity index 68% rename from src/components/views/elements/Dropdown.js rename to src/components/views/elements/Dropdown.tsx index f95247e9ae..c2ff59a2d3 100644 --- a/src/components/views/elements/Dropdown.js +++ b/src/components/views/elements/Dropdown.tsx @@ -1,7 +1,6 @@ /* -Copyright 2017 Vector Creations Ltd Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> -Copyright 2019 The Matrix.org Foundation C.I.C. +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. @@ -16,34 +15,38 @@ 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, { ChangeEvent, createRef, CSSProperties, ReactElement, ReactNode, Ref } from 'react'; import classnames from 'classnames'; + import AccessibleButton from './AccessibleButton'; import { _t } from '../../../languageHandler'; import { Key } from "../../../Keyboard"; import { replaceableComponent } from "../../../utils/replaceableComponent"; -class MenuOption extends React.Component { - constructor(props) { - super(props); - this._onMouseEnter = this._onMouseEnter.bind(this); - this._onClick = this._onClick.bind(this); - } +interface IMenuOptionProps { + children: ReactElement; + highlighted?: boolean; + dropdownKey: string; + id?: string; + inputRef?: Ref; + onClick(dropdownKey: string): void; + onMouseEnter(dropdownKey: string): void; +} +class MenuOption extends React.Component { static defaultProps = { disabled: false, }; - _onMouseEnter() { + private onMouseEnter = () => { this.props.onMouseEnter(this.props.dropdownKey); - } + }; - _onClick(e) { + private onClick = (e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); this.props.onClick(this.props.dropdownKey); - } + }; render() { const optClasses = classnames({ @@ -54,8 +57,8 @@ class MenuOption extends React.Component { return
{ + private readonly buttonRef = createRef(); + private dropdownRootElement: HTMLDivElement = null; + private ignoreEvent: MouseEvent = null; + private childrenByKey: Record = {}; + + constructor(props: IProps) { super(props); - this.dropdownRootElement = null; - this.ignoreEvent = null; + this.reindexChildren(this.props.children); - this._onInputClick = this._onInputClick.bind(this); - this._onRootClick = this._onRootClick.bind(this); - this._onDocumentClick = this._onDocumentClick.bind(this); - this._onMenuOptionClick = this._onMenuOptionClick.bind(this); - this._onInputChange = this._onInputChange.bind(this); - this._collectRoot = this._collectRoot.bind(this); - this._collectInputTextBox = this._collectInputTextBox.bind(this); - this._setHighlightedOption = this._setHighlightedOption.bind(this); - - this.inputTextBox = null; - - this._reindexChildren(this.props.children); - - const firstChild = React.Children.toArray(props.children)[0]; + const firstChild = React.Children.toArray(props.children)[0] as ReactElement; this.state = { // True if the menu is dropped-down expanded: false, // The key of the highlighted option // (the option that would become selected if you pressed enter) - highlightedOption: firstChild ? firstChild.key : null, + highlightedOption: firstChild ? firstChild.key as string : null, // the current search query searchQuery: '', }; - } - // TODO: [REACT-WARNING] Replace component with real class, use constructor for refs - UNSAFE_componentWillMount() { // eslint-disable-line camelcase - this._button = createRef(); // Listen for all clicks on the document so we can close the // menu when the user clicks somewhere else - document.addEventListener('click', this._onDocumentClick, false); + document.addEventListener('click', this.onDocumentClick, false); } componentWillUnmount() { - document.removeEventListener('click', this._onDocumentClick, false); + document.removeEventListener('click', this.onDocumentClick, false); } // TODO: [REACT-WARNING] Replace with appropriate lifecycle event @@ -135,21 +144,21 @@ export default class Dropdown extends React.Component { if (!nextProps.children || nextProps.children.length === 0) { return; } - this._reindexChildren(nextProps.children); + this.reindexChildren(nextProps.children); const firstChild = nextProps.children[0]; this.setState({ highlightedOption: firstChild ? firstChild.key : null, }); } - _reindexChildren(children) { + private reindexChildren(children: ReactElement[]): void { this.childrenByKey = {}; React.Children.forEach(children, (child) => { this.childrenByKey[child.key] = child; }); } - _onDocumentClick(ev) { + private onDocumentClick = (ev: MouseEvent) => { // Close the dropdown if the user clicks anywhere that isn't // within our root element if (ev !== this.ignoreEvent) { @@ -157,9 +166,9 @@ export default class Dropdown extends React.Component { expanded: false, }); } - } + }; - _onRootClick(ev) { + private onRootClick = (ev: MouseEvent) => { // This captures any clicks that happen within our elements, // such that we can then ignore them when they're seen by the // click listener on the document handler, ie. not close the @@ -167,9 +176,9 @@ export default class Dropdown extends React.Component { // NB. We can't just stopPropagation() because then the event // doesn't reach the React onClick(). this.ignoreEvent = ev; - } + }; - _onInputClick(ev) { + private onInputClick = (ev: React.MouseEvent) => { if (this.props.disabled) return; if (!this.state.expanded) { @@ -178,24 +187,24 @@ export default class Dropdown extends React.Component { }); ev.preventDefault(); } - } + }; - _close() { + private close() { this.setState({ expanded: false, }); // their focus was on the input, its getting unmounted, move it to the button - if (this._button.current) { - this._button.current.focus(); + if (this.buttonRef.current) { + this.buttonRef.current.focus(); } } - _onMenuOptionClick(dropdownKey) { - this._close(); + private onMenuOptionClick = (dropdownKey: string) => { + this.close(); this.props.onOptionChange(dropdownKey); - } + }; - _onInputKeyDown = (e) => { + private onInputKeyDown = (e: React.KeyboardEvent) => { let handled = true; // These keys don't generate keypress events and so needs to be on keyup @@ -204,16 +213,16 @@ export default class Dropdown extends React.Component { this.props.onOptionChange(this.state.highlightedOption); // fallthrough case Key.ESCAPE: - this._close(); + this.close(); break; case Key.ARROW_DOWN: this.setState({ - highlightedOption: this._nextOption(this.state.highlightedOption), + highlightedOption: this.nextOption(this.state.highlightedOption), }); break; case Key.ARROW_UP: this.setState({ - highlightedOption: this._prevOption(this.state.highlightedOption), + highlightedOption: this.prevOption(this.state.highlightedOption), }); break; default: @@ -224,53 +233,46 @@ export default class Dropdown extends React.Component { e.preventDefault(); e.stopPropagation(); } - } + }; - _onInputChange(e) { + private onInputChange = (e: ChangeEvent) => { this.setState({ - searchQuery: e.target.value, + searchQuery: e.currentTarget.value, }); if (this.props.onSearchChange) { - this.props.onSearchChange(e.target.value); + this.props.onSearchChange(e.currentTarget.value); } - } + }; - _collectRoot(e) { + private collectRoot = (e: HTMLDivElement) => { if (this.dropdownRootElement) { - this.dropdownRootElement.removeEventListener( - 'click', this._onRootClick, false, - ); + this.dropdownRootElement.removeEventListener('click', this.onRootClick, false); } if (e) { - e.addEventListener('click', this._onRootClick, false); + e.addEventListener('click', this.onRootClick, false); } this.dropdownRootElement = e; - } + }; - _collectInputTextBox(e) { - this.inputTextBox = e; - if (e) e.focus(); - } - - _setHighlightedOption(optionKey) { + private setHighlightedOption = (optionKey: string) => { this.setState({ highlightedOption: optionKey, }); - } + }; - _nextOption(optionKey) { + private nextOption(optionKey: string): string { const keys = Object.keys(this.childrenByKey); const index = keys.indexOf(optionKey); return keys[(index + 1) % keys.length]; } - _prevOption(optionKey) { + private prevOption(optionKey: string): string { const keys = Object.keys(this.childrenByKey); const index = keys.indexOf(optionKey); return keys[(index - 1) % keys.length]; } - _scrollIntoView(node) { + private scrollIntoView(node: Element) { if (node) { node.scrollIntoView({ block: "nearest", @@ -279,18 +281,18 @@ export default class Dropdown extends React.Component { } } - _getMenuOptions() { + private getMenuOptions() { const options = React.Children.map(this.props.children, (child) => { const highlighted = this.state.highlightedOption === child.key; return ( { child } @@ -307,7 +309,7 @@ export default class Dropdown extends React.Component { render() { let currentValue; - const menuStyle = {}; + const menuStyle: CSSProperties = {}; if (this.props.menuWidth) menuStyle.width = this.props.menuWidth; let menu; @@ -316,10 +318,10 @@ export default class Dropdown extends React.Component { currentValue = ( - { this._getMenuOptions() } + { this.getMenuOptions() }
); } @@ -356,14 +358,14 @@ export default class Dropdown extends React.Component { // Note the menu sits inside the AccessibleButton div so it's anchored // to the input, but overflows below it. The root contains both. - return
+ return
@@ -374,28 +376,3 @@ export default class Dropdown extends React.Component {
; } } - -Dropdown.propTypes = { - id: PropTypes.string.isRequired, - // The width that the dropdown should be. If specified, - // the dropped-down part of the menu will be set to this - // width. - menuWidth: PropTypes.number, - // Called when the selected option changes - onOptionChange: PropTypes.func.isRequired, - // Called when the value of the search field changes - onSearchChange: PropTypes.func, - searchEnabled: PropTypes.bool, - // Function that, given the key of an option, returns - // a node representing that option to be displayed in the - // box itself as the currently-selected option (ie. as - // opposed to in the actual dropped-down part). If - // unspecified, the appropriate child element is used as - // in the dropped-down menu. - getShortOption: PropTypes.func, - value: PropTypes.string, - // negative for consistency with HTML - disabled: PropTypes.bool, - // ARIA label - label: PropTypes.string.isRequired, -}; From 82100df9eaf22d07e3de4d8e605bb277c287c4ef Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 6 Jul 2021 10:08:57 +0100 Subject: [PATCH 0197/1171] Bring dropdown styling into closer line with rest of our styling --- res/css/views/elements/_Dropdown.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/res/css/views/elements/_Dropdown.scss b/res/css/views/elements/_Dropdown.scss index 2a2508c17c..3b67e0191e 100644 --- a/res/css/views/elements/_Dropdown.scss +++ b/res/css/views/elements/_Dropdown.scss @@ -27,7 +27,7 @@ limitations under the License. display: flex; align-items: center; position: relative; - border-radius: 3px; + border-radius: 4px; border: 1px solid $strong-input-border-color; font-size: $font-12px; user-select: none; @@ -109,7 +109,7 @@ input.mx_Dropdown_option:focus { z-index: 2; margin: 0; padding: 0px; - border-radius: 3px; + border-radius: 4px; border: 1px solid $input-focused-border-color; background-color: $primary-bg-color; max-height: 200px; From 692347843d485ab1d990d29658be1f61741d4adc Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 6 Jul 2021 10:09:35 +0100 Subject: [PATCH 0198/1171] Track restricted join rule support in the SpaceStore for sync access --- src/stores/SpaceStore.tsx | 120 ++++++++++++++++++++------------------ 1 file changed, 64 insertions(+), 56 deletions(-) diff --git a/src/stores/SpaceStore.tsx b/src/stores/SpaceStore.tsx index e6fc793c4f..9a2dc027c2 100644 --- a/src/stores/SpaceStore.tsx +++ b/src/stores/SpaceStore.tsx @@ -19,6 +19,8 @@ import { ListIteratee, Many, sortBy, throttle } from "lodash"; import { EventType, RoomType } from "matrix-js-sdk/src/@types/event"; import { Room } from "matrix-js-sdk/src/models/room"; import { MatrixEvent } from "matrix-js-sdk/src/models/event"; +import { JoinRule } from "matrix-js-sdk/src/@types/partials"; +import { IRoomCapability } from "matrix-js-sdk/src/client"; import { AsyncStoreWithClient } from "./AsyncStoreWithClient"; import defaultDispatcher from "../dispatcher/dispatcher"; @@ -45,7 +47,6 @@ import { _t } from "../languageHandler"; import GenericToast from "../components/views/toasts/GenericToast"; import Modal from "../Modal"; import InfoDialog from "../components/views/dialogs/InfoDialog"; -import { JoinRule } from "../../../matrix-js-sdk/src/@types/partials"; type SpaceKey = string | symbol; @@ -115,6 +116,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { private _suggestedRooms: ISuggestedRoom[] = []; private _invitedSpaces = new Set(); private spaceOrderLocalEchoMap = new Map(); + private _restrictedJoinRuleSupport?: IRoomCapability; public get invitedSpaces(): Room[] { return Array.from(this._invitedSpaces); @@ -132,6 +134,10 @@ export class SpaceStoreClass extends AsyncStoreWithClient { return this._suggestedRooms; } + public get restrictedJoinRuleSupport(): IRoomCapability { + return this._restrictedJoinRuleSupport; + } + /** * Sets the active space, updates room list filters, * optionally switches the user's room back to where they were when they last viewed that space. @@ -182,66 +188,63 @@ export class SpaceStoreClass extends AsyncStoreWithClient { } // New in Spaces beta toast for Restricted Join Rule - (async () => { - const lsKey = "mx_SpaceBeta_restrictedJoinRuleToastSeen"; - if (contextSwitch && space?.getJoinRule() === JoinRule.Invite && shouldShowSpaceSettings(space) && - space.getJoinedMemberCount() > 1 && !localStorage.getItem(lsKey) /*&& - (await this.matrixClient.getCapabilities()) - ?.["m.room_versions"]?.["org.matrix.msc3244.room_capabilities"]?.["restricted"]?.preferred*/ - ) { - const toastKey = "restrictedjoinrule"; - ToastStore.sharedInstance().addOrReplaceToast({ - key: toastKey, - title: _t("New in the Spaces beta"), - props: { - description: _t("Help people in spaces to find and join private rooms"), - acceptLabel: _t("Learn more"), - onAccept: () => { - localStorage.setItem(lsKey, "true"); - ToastStore.sharedInstance().dismissToast(toastKey); + const lsKey = "mx_SpaceBeta_restrictedJoinRuleToastSeen"; + if (contextSwitch && space?.getJoinRule() === JoinRule.Invite && shouldShowSpaceSettings(space) && + space.getJoinedMemberCount() > 1 && !localStorage.getItem(lsKey) + && this.restrictedJoinRuleSupport?.preferred + ) { + const toastKey = "restrictedjoinrule"; + ToastStore.sharedInstance().addOrReplaceToast({ + key: toastKey, + title: _t("New in the Spaces beta"), + props: { + description: _t("Help people in spaces to find and join private rooms"), + acceptLabel: _t("Learn more"), + onAccept: () => { + localStorage.setItem(lsKey, "true"); + ToastStore.sharedInstance().dismissToast(toastKey); - Modal.createTrackedDialog("New in the Spaces beta", "restricted join rule", InfoDialog, { - title: _t("Help space members find private rooms"), - description: <> -

{ _t("To help space members find and join a private room, " + - "go to that room's Security & Privacy settings.") }

+ Modal.createTrackedDialog("New in the Spaces beta", "restricted join rule", InfoDialog, { + title: _t("Help space members find private rooms"), + description: <> +

{ _t("To help space members find and join a private room, " + + "go to that room's Security & Privacy settings.") }

- { /* Reuses classes from TabbedView for simplicity, non-interactive */ } -
-
- - { _t("General") } -
-
- - { _t("Security & Privacy") } -
-
- - { _t("Roles & Permissions") } -
+ { /* Reuses classes from TabbedView for simplicity, non-interactive */ } +
+
+ + { _t("General") }
+
+ + { _t("Security & Privacy") } +
+
+ + { _t("Roles & Permissions") } +
+
-

{ _t("This make it easy for rooms to stay private to a space, " + - "while letting people in the space find and join them. " + - "All new rooms in a space will have this option available.")}

- , - button: _t("OK"), - hasCloseButton: false, - fixedWidth: true, - }); - }, - rejectLabel: _t("Skip"), - onReject: () => { - localStorage.setItem(lsKey, "true"); - ToastStore.sharedInstance().dismissToast(toastKey); - }, +

{ _t("This make it easy for rooms to stay private to a space, " + + "while letting people in the space find and join them. " + + "All new rooms in a space will have this option available.")}

+ , + button: _t("OK"), + hasCloseButton: false, + fixedWidth: true, + }); }, - component: GenericToast, - priority: 35, - }); - } - })().then(); + rejectLabel: _t("Skip"), + onReject: () => { + localStorage.setItem(lsKey, "true"); + ToastStore.sharedInstance().dismissToast(toastKey); + }, + }, + component: GenericToast, + priority: 35, + }); + } if (space) { const suggestedRooms = await this.fetchSuggestedRooms(space); @@ -709,6 +712,11 @@ export class SpaceStoreClass extends AsyncStoreWithClient { this.matrixClient.on("accountData", this.onAccountData); } + this.matrixClient.getCapabilities().then(capabilities => { + this._restrictedJoinRuleSupport = capabilities + ?.["m.room_versions"]?.["org.matrix.msc3244.room_capabilities"]?.["restricted"]; + }); + await this.onSpaceUpdate(); // trigger an initial update // restore selected state from last session if any and still valid From c5ca98a3ad090d38e9fcd840ffbea585afa973b7 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 6 Jul 2021 10:10:25 +0100 Subject: [PATCH 0199/1171] Iterate SecurityRoomSettingsTab and ManageRestrictedJoinRuleDialog --- .../_ManageRestrictedJoinRuleDialog.scss | 19 +-- .../ManageRestrictedJoinRuleDialog.tsx | 24 +++- .../tabs/room/SecurityRoomSettingsTab.tsx | 128 ++++++++++-------- src/i18n/strings/en_EN.json | 11 +- 4 files changed, 106 insertions(+), 76 deletions(-) diff --git a/res/css/views/dialogs/_ManageRestrictedJoinRuleDialog.scss b/res/css/views/dialogs/_ManageRestrictedJoinRuleDialog.scss index 6606f78a8a..91df76675a 100644 --- a/res/css/views/dialogs/_ManageRestrictedJoinRuleDialog.scss +++ b/res/css/views/dialogs/_ManageRestrictedJoinRuleDialog.scss @@ -104,7 +104,7 @@ limitations under the License. } } - .mx_ManageRestrictedJoinRuleDialog_section_experimental { + .mx_ManageRestrictedJoinRuleDialog_section_info { position: relative; border-radius: 8px; margin: 12px 0; @@ -131,16 +131,19 @@ limitations under the License. } .mx_ManageRestrictedJoinRuleDialog_footer { - display: flex; margin-top: 20px; - align-self: end; - .mx_AccessibleButton { - display: inline-block; - align-self: center; + .mx_ManageRestrictedJoinRuleDialog_footer_buttons { + display: flex; + width: max-content; + margin-left: auto; - & + .mx_AccessibleButton { - margin-left: 24px; + .mx_AccessibleButton { + display: inline-block; + + & + .mx_AccessibleButton { + margin-left: 24px; + } } } } diff --git a/src/components/views/dialogs/ManageRestrictedJoinRuleDialog.tsx b/src/components/views/dialogs/ManageRestrictedJoinRuleDialog.tsx index 79a6fb7f24..ff08ae5d28 100644 --- a/src/components/views/dialogs/ManageRestrictedJoinRuleDialog.tsx +++ b/src/components/views/dialogs/ManageRestrictedJoinRuleDialog.tsx @@ -102,6 +102,13 @@ const ManageRestrictedJoinRuleDialog: React.FC = ({ room, selected = [], setNewSelected(new Set(newSelected)); }; + let inviteOnlyWarning; + if (newSelected.size < 1) { + inviteOnlyWarning =
+ { _t("You're removing all spaces. Access will default to invite only") } +
; + } + return = ({ room, selected = [], { filteredOtherEntries.length > 0 ? (

{ _t("Other spaces or rooms you might not know") }

-
+
{ _t("These are likely ones other room admins are a part of.") }
{ filteredOtherEntries.map(space => { @@ -167,12 +174,15 @@ const ManageRestrictedJoinRuleDialog: React.FC = ({ room, selected = [],
- onFinished()}> - { _t("Cancel") } - - onFinished(Array.from(newSelected))}> - { _t("Confirm") } - + { inviteOnlyWarning } +
+ onFinished()}> + { _t("Cancel") } + + onFinished(Array.from(newSelected))}> + { _t("Confirm") } + +
; diff --git a/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx b/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx index ceb7dd21bd..a05bae30c2 100644 --- a/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx +++ b/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx @@ -101,19 +101,14 @@ export default class SecurityRoomSettingsTab extends React.Component this.setState({ hasAliases })); - cli.getCapabilities().then(capabilities => { - const roomCapabilities = capabilities["org.matrix.msc3244.room_capabilities"]; - const roomSupportsRestricted = roomCapabilities && Array.isArray(roomCapabilities["restricted"]?.support) && - roomCapabilities["restricted"].support.includes(room.getVersion()); - const preferredRestrictionVersion = roomSupportsRestricted - ? roomCapabilities?.["restricted"].preferred - : undefined; - - this.setState({ roomSupportsRestricted, preferredRestrictionVersion }); - }); } private pullContentPropertyFromEvent(event: MatrixEvent, key: string, defaultValue: T): T { @@ -169,23 +164,16 @@ export default class SecurityRoomSettingsTab extends React.Component { const beforeJoinRule = this.state.joinRule; - if (beforeJoinRule === joinRule) return; + let restrictedAllowRoomIds: string[]; if (joinRule === JoinRule.Restricted) { const matrixClient = MatrixClientPeg.get(); const roomId = this.props.roomId; const room = matrixClient.getRoom(roomId); - if (this.state.roomSupportsRestricted) { + if (beforeJoinRule === JoinRule.Restricted || this.state.roomSupportsRestricted) { // Have the user pick which spaces to allow joins from - const { finished } = Modal.createTrackedDialog('Set restricted', '', ManageRestrictedJoinRuleDialog, { - matrixClient, - room, - // if they have are viewing this room from the context of a space then default to that - selected: SpaceStore.instance.activeSpace ? [SpaceStore.instance.activeSpace.roomId] : [], - }, "mx_ManageRestrictedJoinRuleDialog_wrapper"); - - const [restrictedAllowRoomIds] = await finished; + restrictedAllowRoomIds = await this.editRestrictedRoomIds(); if (!Array.isArray(restrictedAllowRoomIds)) return; } else if (this.state.preferredRestrictionVersion) { // Block this action on a room upgrade otherwise it'd make their room unjoinable @@ -204,19 +192,18 @@ export default class SecurityRoomSettingsTab extends React.Component ({ "type": RestrictedAllowType.RoomMembership, - "room_id": spaceRoomId, - }]; + "room_id": roomId, + })); } this.setState({ joinRule, restrictedAllowRoomIds }); @@ -296,17 +283,31 @@ export default class SecurityRoomSettingsTab extends React.Component { + private editRestrictedRoomIds = async (): Promise => { + let selected = this.state.restrictedAllowRoomIds; + if (!selected?.length && SpaceStore.instance.activeSpace) { + selected = [SpaceStore.instance.activeSpace.roomId]; + } + const matrixClient = MatrixClientPeg.get(); - Modal.createTrackedDialog('Edit restricted', '', ManageRestrictedJoinRuleDialog, { + const { finished } = Modal.createTrackedDialog('Edit restricted', '', ManageRestrictedJoinRuleDialog, { matrixClient, room: matrixClient.getRoom(this.props.roomId), - selected: this.state.restrictedAllowRoomIds, - onFinished: (restrictedAllowRoomIds?: string[]) => { - if (!Array.isArray(restrictedAllowRoomIds)) return; - this.onRestrictedRoomIdsChange(restrictedAllowRoomIds); - }, + selected, }, "mx_ManageRestrictedJoinRuleDialog_wrapper"); + + const [restrictedAllowRoomIds] = await finished; + return restrictedAllowRoomIds; + }; + + private onEditRestrictedClick = async () => { + const restrictedAllowRoomIds = await this.editRestrictedRoomIds(); + if (!Array.isArray(restrictedAllowRoomIds)) return; + if (restrictedAllowRoomIds.length > 0) { + this.onRestrictedRoomIdsChange(restrictedAllowRoomIds); + } else { + this.onJoinRuleChange(JoinRule.Invite); + } }; private renderJoinRule() { @@ -332,6 +333,8 @@ export default class SecurityRoomSettingsTab extends React.Component client.getRoom(roomId)) - .filter(Boolean) - .slice(0, 4); + if (joinRule === JoinRule.Restricted && this.state.restrictedAllowRoomIds?.length) { + const shownSpaces = this.state.restrictedAllowRoomIds + .map(roomId => client.getRoom(roomId)) + .filter(room => room?.isSpaceRoom()) + .slice(0, 4); - spacesWhichCanAccess =
-

{ _t("Spaces with access") }

- { shownSpaces.map(room => { - return - - { room.name } - ; - })} - { shownSpaces.length < this.state.restrictedAllowRoomIds.length && - { _t("& %(count)s more", { - count: this.state.restrictedAllowRoomIds.length - shownSpaces.length, - }) } - } -
; + let moreText; + if (shownSpaces.length < this.state.restrictedAllowRoomIds.length) { + if (shownSpaces.length > 0) { + moreText = _t("& %(count)s more", { + count: this.state.restrictedAllowRoomIds.length - shownSpaces.length, + }); + } else { + moreText = _t("Currently, %(count)s spaces have access", { + count: this.state.restrictedAllowRoomIds.length, + }); + } } description =
@@ -386,7 +384,17 @@ export default class SecurityRoomSettingsTab extends React.Component, }) } - { spacesWhichCanAccess } + +
+

{ _t("Spaces with access") }

+ { shownSpaces.map(room => { + return + + { room.name } + ; + })} + { moreText && { moreText } } +
; } else if (SpaceStore.instance.activeSpace) { description = _t("Anyone in %(spaceName)s can find and join. You can select other spaces too.", { @@ -403,6 +411,8 @@ export default class SecurityRoomSettingsTab extends React.Component, description, + // if there are 0 allowed spaces then render it as invite only instead + checked: this.state.joinRule === JoinRule.Restricted && !!this.state.restrictedAllowRoomIds?.length, }); } @@ -478,7 +488,7 @@ export default class SecurityRoomSettingsTab extends React.Component + return
- ; +
; } render() { diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 7d0f863b7f..12b0c40d75 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1455,9 +1455,12 @@ "Public (anyone)": "Public (anyone)", "Anyone can find and join.": "Anyone can find and join.", "Upgrade required": "Upgrade required", - "Spaces with access": "Spaces with access", "& %(count)s more|other": "& %(count)s more", + "& %(count)s more|one": "& %(count)s more", + "Currently, %(count)s spaces have access|other": "Currently, %(count)s spaces have access", + "Currently, %(count)s spaces have access|one": "Currently, %(count)s space has access", "Anyone in a space can find and join. Edit which spaces can access here.": "Anyone in a space can find and join. Edit which spaces can access here.", + "Spaces with access": "Spaces with access", "Anyone in %(spaceName)s can find and join. You can select other spaces too.": "Anyone in %(spaceName)s can find and join. You can select other spaces too.", "Anyone in a space can find and join. You can select multiple spaces.": "Anyone in a space can find and join. You can select multiple spaces.", "Space members": "Space members", @@ -2187,8 +2190,11 @@ "Community ID": "Community ID", "example": "example", "Please enter a name for the room": "Please enter a name for the room", - "Private rooms can be found and joined by invitation only. Public rooms can be found and joined by anyone.": "Private rooms can be found and joined by invitation only. Public rooms can be found and joined by anyone.", "Private rooms can be found and joined by invitation only. Public rooms can be found and joined by anyone in this community.": "Private rooms can be found and joined by invitation only. Public rooms can be found and joined by anyone in this community.", + "Everyone in will be able to find and join this room.": "Everyone in will be able to find and join this room.", + "You can change this at any time from room settings.": "You can change this at any time from room settings.", + "Anyone will be able to find and join this room, not just members of .": "Anyone will be able to find and join this room, not just members of .", + "Only people invited will be able to find and join this room.": "Only people invited will be able to find and join this room.", "You can’t disable this later. Bridges & most bots won’t work yet.": "You can’t disable this later. Bridges & most bots won’t work yet.", "Your server requires encryption to be enabled in private rooms.": "Your server requires encryption to be enabled in private rooms.", "Enable end-to-end encryption": "Enable end-to-end encryption", @@ -2355,6 +2361,7 @@ "%(count)s members|one": "%(count)s member", "%(count)s rooms|other": "%(count)s rooms", "%(count)s rooms|one": "%(count)s room", + "You're removing all spaces. Access will default to invite only": "You're removing all spaces. Access will default to invite only", "Select spaces": "Select spaces", "Decide which spaces can access this room. If a space is selected its members will be able to find and join .": "Decide which spaces can access this room. If a space is selected its members will be able to find and join .", "Search spaces": "Search spaces", From eb9f4c609a22c8493c9021d20958ccdf4286529c Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 6 Jul 2021 10:10:47 +0100 Subject: [PATCH 0200/1171] Make CreateRoomDialog capable of creating restricted rooms in spaces --- res/css/views/dialogs/_CreateRoomDialog.scss | 18 ++- .../views/dialogs/CreateRoomDialog.tsx | 117 ++++++++++++++---- src/createRoom.ts | 6 +- src/i18n/strings/en_EN.json | 8 +- 4 files changed, 111 insertions(+), 38 deletions(-) diff --git a/res/css/views/dialogs/_CreateRoomDialog.scss b/res/css/views/dialogs/_CreateRoomDialog.scss index 2678f7b4ad..adba58cbb9 100644 --- a/res/css/views/dialogs/_CreateRoomDialog.scss +++ b/res/css/views/dialogs/_CreateRoomDialog.scss @@ -65,7 +65,7 @@ limitations under the License. .mx_CreateRoomDialog_aliasContainer { display: flex; // put margin on container so it can collapse with siblings - margin: 10px 0; + margin: 24px 0 10px; .mx_RoomAliasField { margin: 0; @@ -101,10 +101,6 @@ limitations under the License. margin-left: 30px; } - .mx_CreateRoomDialog_topic { - margin-bottom: 36px; - } - .mx_Dialog_content > .mx_SettingsFlag { margin-top: 24px; } @@ -113,5 +109,17 @@ limitations under the License. margin: 0 85px 0 0; font-size: $font-12px; } + + .mx_Dropdown { + margin-bottom: 8px; + font-weight: normal; + font-family: $font-family; + font-size: $font-14px; + color: $primary-fg-color; + + .mx_Dropdown_input { + border: 1px solid $input-border-color; + } + } } diff --git a/src/components/views/dialogs/CreateRoomDialog.tsx b/src/components/views/dialogs/CreateRoomDialog.tsx index b5c0096771..eecddf7f31 100644 --- a/src/components/views/dialogs/CreateRoomDialog.tsx +++ b/src/components/views/dialogs/CreateRoomDialog.tsx @@ -17,6 +17,7 @@ limitations under the License. import React, { ChangeEvent, createRef, KeyboardEvent, SyntheticEvent } from "react"; import { Room } from "matrix-js-sdk/src/models/room"; +import { JoinRule, Preset, Visibility } from "matrix-js-sdk/src/@types/partials"; import SdkConfig from '../../../SdkConfig'; import withValidation, { IFieldState } from '../elements/Validation'; @@ -31,7 +32,8 @@ import RoomAliasField from "../elements/RoomAliasField"; import LabelledToggleSwitch from "../elements/LabelledToggleSwitch"; import DialogButtons from "../elements/DialogButtons"; import BaseDialog from "../dialogs/BaseDialog"; -import { Preset, Visibility } from "matrix-js-sdk/src/@types/partials"; +import Dropdown from "../elements/Dropdown"; +import SpaceStore from "../../../stores/SpaceStore"; interface IProps { defaultPublic?: boolean; @@ -41,7 +43,7 @@ interface IProps { } interface IState { - isPublic: boolean; + joinRule: JoinRule; isEncrypted: boolean; name: string; topic: string; @@ -54,15 +56,25 @@ interface IState { @replaceableComponent("views.dialogs.CreateRoomDialog") export default class CreateRoomDialog extends React.Component { + private readonly supportsRestricted: boolean; private nameField = createRef(); private aliasField = createRef(); constructor(props) { super(props); + this.supportsRestricted = this.props.parentSpace && !!SpaceStore.instance.restrictedJoinRuleSupport?.preferred; + + let joinRule = JoinRule.Invite; + if (this.props.defaultPublic) { + joinRule = JoinRule.Public; + } else if (this.supportsRestricted) { + joinRule = JoinRule.Restricted; + } + const config = SdkConfig.get(); this.state = { - isPublic: this.props.defaultPublic || false, + joinRule, isEncrypted: privateShouldBeEncrypted(), name: this.props.defaultName || "", topic: "", @@ -81,7 +93,7 @@ export default class CreateRoomDialog extends React.Component { const opts: IOpts = {}; const createOpts: IOpts["createOpts"] = opts.createOpts = {}; createOpts.name = this.state.name; - if (this.state.isPublic) { + if (this.state.joinRule === JoinRule.Public) { createOpts.visibility = Visibility.Public; createOpts.preset = Preset.PublicChat; opts.guestAccess = false; @@ -95,7 +107,7 @@ export default class CreateRoomDialog extends React.Component { createOpts.creation_content = { 'm.federate': false }; } - if (!this.state.isPublic) { + if (this.state.joinRule !== JoinRule.Public) { if (this.state.canChangeEncryption) { opts.encryption = this.state.isEncrypted; } else { @@ -109,8 +121,9 @@ export default class CreateRoomDialog extends React.Component { opts.associatedWithCommunity = CommunityPrototypeStore.instance.getSelectedCommunityId(); } - if (this.props.parentSpace) { + if (this.props.parentSpace && this.state.joinRule === JoinRule.Restricted) { opts.parentSpace = this.props.parentSpace; + opts.joinRule = JoinRule.Restricted; } return opts; @@ -172,8 +185,8 @@ export default class CreateRoomDialog extends React.Component { this.setState({ topic: ev.target.value }); }; - private onPublicChange = (isPublic: boolean) => { - this.setState({ isPublic }); + private onJoinRuleChange = (joinRule: JoinRule) => { + this.setState({ joinRule }); }; private onEncryptedChange = (isEncrypted: boolean) => { @@ -210,7 +223,7 @@ export default class CreateRoomDialog extends React.Component { render() { let aliasField; - if (this.state.isPublic) { + if (this.state.joinRule === JoinRule.Public) { const domain = MatrixClientPeg.get().getDomain(); aliasField = (
@@ -224,19 +237,46 @@ export default class CreateRoomDialog extends React.Component { ); } - let publicPrivateLabel =

{_t( - "Private rooms can be found and joined by invitation only. Public rooms can be " + - "found and joined by anyone.", - )}

; + let publicPrivateLabel: JSX.Element; if (CommunityPrototypeStore.instance.getSelectedCommunityId()) { - publicPrivateLabel =

{_t( - "Private rooms can be found and joined by invitation only. Public rooms can be " + - "found and joined by anyone in this community.", - )}

; + publicPrivateLabel =

+ { _t( + "Private rooms can be found and joined by invitation only. Public rooms can be " + + "found and joined by anyone in this community.", + ) } +

; + } else if (this.state.joinRule === JoinRule.Restricted) { + publicPrivateLabel =

+ { _t( + "Everyone in will be able to find and join this room.", {}, { + SpaceName: () => this.props.parentSpace.name, + }, + ) } +   + { _t("You can change this at any time from room settings.") } +

; + } else if (this.state.joinRule === JoinRule.Public) { + publicPrivateLabel =

+ { _t( + "Anyone will be able to find and join this room, not just members of .", {}, { + SpaceName: () => this.props.parentSpace.name, + }, + ) } +   + { _t("You can change this at any time from room settings.") } +

; + } else if (this.state.joinRule === JoinRule.Invite) { + publicPrivateLabel =

+ { _t( + "Only people invited will be able to find and join this room.", + ) } +   + { _t("You can change this at any time from room settings.") } +

; } let e2eeSection; - if (!this.state.isPublic) { + if (this.state.joinRule !== JoinRule.Public) { let microcopy; if (privateShouldBeEncrypted()) { if (this.state.canChangeEncryption) { @@ -273,15 +313,31 @@ export default class CreateRoomDialog extends React.Component { ); } - let title = this.state.isPublic ? _t('Create a public room') : _t('Create a private room'); + let title = _t("Create a room"); if (CommunityPrototypeStore.instance.getSelectedCommunityId()) { const name = CommunityPrototypeStore.instance.getSelectedCommunityName(); title = _t("Create a room in %(communityName)s", { communityName: name }); + } else if (!this.props.parentSpace) { + title = this.state.joinRule === JoinRule.Public ? _t('Create a public room') : _t('Create a private room'); } + + const options = [ +
+ { _t("Private room (invite only)") } +
, +
+ { _t("Public room") } +
, + ]; + + if (this.supportsRestricted) { + options.unshift(
+ { _t("Visible to space members") } +
); + } + return ( - +
{ value={this.state.topic} className="mx_CreateRoomDialog_topic" /> - + + + { options } + + { publicPrivateLabel } { e2eeSection } { aliasField } diff --git a/src/createRoom.ts b/src/createRoom.ts index 0a88e2cef7..e809f5ff0a 100644 --- a/src/createRoom.ts +++ b/src/createRoom.ts @@ -153,10 +153,8 @@ export default async function createRoom(opts: IOpts): Promise { }); if (opts.joinRule === JoinRule.Restricted) { - const serverCapabilities = await client.getCapabilities(); - const roomCapabilities = serverCapabilities?.["m.room_versions"]?.["org.matrix.msc3244.room_capabilities"]; - if (roomCapabilities?.["restricted"]?.preferred) { - opts.createOpts.room_version = roomCapabilities?.["restricted"].preferred; + if (SpaceStore.instance.restrictedJoinRuleSupport?.preferred) { + opts.createOpts.room_version = SpaceStore.instance.restrictedJoinRuleSupport.preferred; opts.createOpts.initial_state.push({ type: EventType.RoomJoinRules, diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 12b0c40d75..c7992d93c3 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2200,11 +2200,15 @@ "Enable end-to-end encryption": "Enable end-to-end encryption", "You might enable this if the room will only be used for collaborating with internal teams on your homeserver. This cannot be changed later.": "You might enable this if the room will only be used for collaborating with internal teams on your homeserver. This cannot be changed later.", "You might disable this if the room will be used for collaborating with external teams who have their own homeserver. This cannot be changed later.": "You might disable this if the room will be used for collaborating with external teams who have their own homeserver. This cannot be changed later.", + "Create a room": "Create a room", + "Create a room in %(communityName)s": "Create a room in %(communityName)s", "Create a public room": "Create a public room", "Create a private room": "Create a private room", - "Create a room in %(communityName)s": "Create a room in %(communityName)s", + "Private room (invite only)": "Private room (invite only)", + "Public room": "Public room", + "Visible to space members": "Visible to space members", "Topic (optional)": "Topic (optional)", - "Make this room public": "Make this room public", + "Room visibility": "Room visibility", "Block anyone not part of %(serverName)s from ever joining this room.": "Block anyone not part of %(serverName)s from ever joining this room.", "Create Room": "Create Room", "Sign out": "Sign out", From 3301763f12954c95156000dd59e9f83c0a197203 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 6 Jul 2021 10:19:33 +0100 Subject: [PATCH 0201/1171] stub getCapabilities in tests --- test/test-utils.js | 1 + 1 file changed, 1 insertion(+) diff --git a/test/test-utils.js b/test/test-utils.js index ad56522965..900c870f68 100644 --- a/test/test-utils.js +++ b/test/test-utils.js @@ -96,6 +96,7 @@ export function createTestClient() { }, }, decryptEventIfNeeded: () => Promise.resolve(), + getCapabilities: jest.fn().mockReturnValue({}), }; } From 0ca4a958f7002c4036e8cb835b3867b362886c66 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 6 Jul 2021 10:34:50 +0100 Subject: [PATCH 0202/1171] fix getCapabilities stub --- test/test-utils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test-utils.js b/test/test-utils.js index 900c870f68..35c4441077 100644 --- a/test/test-utils.js +++ b/test/test-utils.js @@ -96,7 +96,7 @@ export function createTestClient() { }, }, decryptEventIfNeeded: () => Promise.resolve(), - getCapabilities: jest.fn().mockReturnValue({}), + getCapabilities: jest.fn().mockResolvedValue({}), }; } From 9d8acd1af0178b999e14845c42e8ad507032a371 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 6 Jul 2021 10:44:09 +0100 Subject: [PATCH 0203/1171] stub getJoinRule --- test/test-utils.js | 1 + 1 file changed, 1 insertion(+) diff --git a/test/test-utils.js b/test/test-utils.js index 35c4441077..a07994af20 100644 --- a/test/test-utils.js +++ b/test/test-utils.js @@ -269,6 +269,7 @@ export function mkStubRoom(roomId = null, name) { getCanonicalAlias: jest.fn(), getAltAliases: jest.fn().mockReturnValue([]), timeline: [], + getJoinRule: jest.fn().mockReturnValue("invite"), }; } From 04c923bd75e9758187314e5a64e41938bb0c81b1 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 6 Jul 2021 11:35:56 +0100 Subject: [PATCH 0204/1171] fix tests by including client field on the Room stub and stubbing getJoinedMemberCount --- test/stores/SpaceStore-test.ts | 52 +++++++++++++++++----------------- test/test-utils.js | 4 ++- 2 files changed, 29 insertions(+), 27 deletions(-) diff --git a/test/stores/SpaceStore-test.ts b/test/stores/SpaceStore-test.ts index 4cbd9f43c8..e6d8dc144e 100644 --- a/test/stores/SpaceStore-test.ts +++ b/test/stores/SpaceStore-test.ts @@ -53,32 +53,6 @@ const emitPromise = (e: EventEmitter, k: string | symbol) => new Promise(r => e. const testUserId = "@test:user"; -let rooms = []; - -const mkRoom = (roomId: string) => { - const room = mkStubRoom(roomId); - room.currentState.getStateEvents.mockImplementation(mockStateEventImplementation([])); - rooms.push(room); - return room; -}; - -const mkSpace = (spaceId: string, children: string[] = []) => { - const space = mkRoom(spaceId); - space.isSpaceRoom.mockReturnValue(true); - space.currentState.getStateEvents.mockImplementation(mockStateEventImplementation(children.map(roomId => - mkEvent({ - event: true, - type: EventType.SpaceChild, - room: spaceId, - user: testUserId, - skey: roomId, - content: { via: [] }, - ts: Date.now(), - }), - ))); - return space; -}; - const getValue = jest.fn(); SettingsStore.getValue = getValue; @@ -111,6 +85,32 @@ describe("SpaceStore", () => { const store = SpaceStore.instance; const client = MatrixClientPeg.get(); + let rooms = []; + + const mkRoom = (roomId: string) => { + const room = mkStubRoom(roomId, roomId, client); + room.currentState.getStateEvents.mockImplementation(mockStateEventImplementation([])); + rooms.push(room); + return room; + }; + + const mkSpace = (spaceId: string, children: string[] = []) => { + const space = mkRoom(spaceId); + space.isSpaceRoom.mockReturnValue(true); + space.currentState.getStateEvents.mockImplementation(mockStateEventImplementation(children.map(roomId => + mkEvent({ + event: true, + type: EventType.SpaceChild, + room: spaceId, + user: testUserId, + skey: roomId, + content: { via: [] }, + ts: Date.now(), + }), + ))); + return space; + }; + const viewRoom = roomId => defaultDispatcher.dispatch({ action: "view_room", room_id: roomId }, true); const run = async () => { diff --git a/test/test-utils.js b/test/test-utils.js index a07994af20..33001e39d1 100644 --- a/test/test-utils.js +++ b/test/test-utils.js @@ -220,7 +220,7 @@ export function mkMessage(opts) { return mkEvent(opts); } -export function mkStubRoom(roomId = null, name) { +export function mkStubRoom(roomId = null, name, client) { const stubTimeline = { getEvents: () => [] }; return { roomId, @@ -235,6 +235,7 @@ export function mkStubRoom(roomId = null, name) { }), getMembersWithMembership: jest.fn().mockReturnValue([]), getJoinedMembers: jest.fn().mockReturnValue([]), + getJoinedMemberCount: jest.fn().mockReturnValue(1), getMembers: jest.fn().mockReturnValue([]), getPendingEvents: () => [], getLiveTimeline: () => stubTimeline, @@ -270,6 +271,7 @@ export function mkStubRoom(roomId = null, name) { getAltAliases: jest.fn().mockReturnValue([]), timeline: [], getJoinRule: jest.fn().mockReturnValue("invite"), + client, }; } From 06284fe73d65fb5b9014b517a2ab57aebadeb7cf Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 6 Jul 2021 12:05:06 +0100 Subject: [PATCH 0205/1171] Update e2e tests --- .../src/scenarios/directory.js | 2 +- .../src/scenarios/lazy-loading.js | 2 +- .../src/usecases/room-settings.js | 28 +++++++------------ 3 files changed, 12 insertions(+), 20 deletions(-) diff --git a/test/end-to-end-tests/src/scenarios/directory.js b/test/end-to-end-tests/src/scenarios/directory.js index 53b790c174..fffca2b05c 100644 --- a/test/end-to-end-tests/src/scenarios/directory.js +++ b/test/end-to-end-tests/src/scenarios/directory.js @@ -25,7 +25,7 @@ module.exports = async function roomDirectoryScenarios(alice, bob) { console.log(" creating a public room and join through directory:"); const room = 'test'; await createRoom(alice, room); - await changeRoomSettings(alice, { directory: true, visibility: "public_no_guests", alias: "#test" }); + await changeRoomSettings(alice, { directory: true, visibility: "public", alias: "#test" }); await join(bob, room); //looks up room in directory const bobMessage = "hi Alice!"; await sendMessage(bob, bobMessage); diff --git a/test/end-to-end-tests/src/scenarios/lazy-loading.js b/test/end-to-end-tests/src/scenarios/lazy-loading.js index 1b5d449af9..406f7b24a3 100644 --- a/test/end-to-end-tests/src/scenarios/lazy-loading.js +++ b/test/end-to-end-tests/src/scenarios/lazy-loading.js @@ -51,7 +51,7 @@ const charlyMsg2 = "how's it going??"; async function setupRoomWithBobAliceAndCharlies(alice, bob, charlies) { await createRoom(bob, room); - await changeRoomSettings(bob, { directory: true, visibility: "public_no_guests", alias }); + await changeRoomSettings(bob, { directory: true, visibility: "public", alias }); // wait for alias to be set by server after clicking "save" // so the charlies can join it. await bob.delay(500); diff --git a/test/end-to-end-tests/src/usecases/room-settings.js b/test/end-to-end-tests/src/usecases/room-settings.js index b40afe76bf..01431197a7 100644 --- a/test/end-to-end-tests/src/usecases/room-settings.js +++ b/test/end-to-end-tests/src/usecases/room-settings.js @@ -98,18 +98,14 @@ async function checkRoomSettings(session, expectedSettings) { if (expectedSettings.visibility) { session.log.step(`checks visibility is ${expectedSettings.visibility}`); const radios = await session.queryAll(".mx_RoomSettingsDialog input[type=radio]"); - assert.equal(radios.length, 7); - const inviteOnly = radios[0]; - const publicNoGuests = radios[1]; - const publicWithGuests = radios[2]; + assert.equal(radios.length, 6); + const [inviteOnlyRoom, publicRoom] = radios; let expectedRadio = null; if (expectedSettings.visibility === "invite_only") { - expectedRadio = inviteOnly; - } else if (expectedSettings.visibility === "public_no_guests") { - expectedRadio = publicNoGuests; - } else if (expectedSettings.visibility === "public_with_guests") { - expectedRadio = publicWithGuests; + expectedRadio = inviteOnlyRoom; + } else if (expectedSettings.visibility === "public") { + expectedRadio = publicRoom; } else { throw new Error(`unrecognized room visibility setting: ${expectedSettings.visibility}`); } @@ -165,17 +161,13 @@ async function changeRoomSettings(session, settings) { if (settings.visibility) { session.log.step(`sets visibility to ${settings.visibility}`); const radios = await session.queryAll(".mx_RoomSettingsDialog label"); - assert.equal(radios.length, 7); - const inviteOnly = radios[0]; - const publicNoGuests = radios[1]; - const publicWithGuests = radios[2]; + assert.equal(radios.length, 6); + const [inviteOnlyRoom, publicRoom] = radios; if (settings.visibility === "invite_only") { - await inviteOnly.click(); - } else if (settings.visibility === "public_no_guests") { - await publicNoGuests.click(); - } else if (settings.visibility === "public_with_guests") { - await publicWithGuests.click(); + await inviteOnlyRoom.click(); + } else if (settings.visibility === "public") { + await publicRoom.click(); } else { throw new Error(`unrecognized room visibility setting: ${settings.visibility}`); } From d004163177a5da6303a9a5f270dbdf5377a217a6 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 6 Jul 2021 12:05:30 +0100 Subject: [PATCH 0206/1171] Fix 2 new NPEs --- .../settings/tabs/room/SecurityRoomSettingsTab.tsx | 2 +- src/createRoom.ts | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx b/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx index a05bae30c2..82eeef111d 100644 --- a/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx +++ b/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx @@ -104,7 +104,7 @@ export default class SecurityRoomSettingsTab extends React.Component { } if (opts.parentSpace) { - opts.createOpts.initial_state.push(makeSpaceParentEvent(opts.parentSpace, true)); - opts.createOpts.initial_state.push({ + createOpts.initial_state.push(makeSpaceParentEvent(opts.parentSpace, true)); + createOpts.initial_state.push({ type: EventType.RoomHistoryVisibility, content: { - "history_visibility": opts.createOpts.preset === Preset.PublicChat ? "world_readable" : "invited", + "history_visibility": createOpts.preset === Preset.PublicChat ? "world_readable" : "invited", }, }); if (opts.joinRule === JoinRule.Restricted) { if (SpaceStore.instance.restrictedJoinRuleSupport?.preferred) { - opts.createOpts.room_version = SpaceStore.instance.restrictedJoinRuleSupport.preferred; + createOpts.room_version = SpaceStore.instance.restrictedJoinRuleSupport.preferred; - opts.createOpts.initial_state.push({ + createOpts.initial_state.push({ type: EventType.RoomJoinRules, content: { "join_rule": JoinRule.Restricted, @@ -171,7 +171,7 @@ export default async function createRoom(opts: IOpts): Promise { } if (opts.joinRule !== JoinRule.Restricted) { - opts.createOpts.initial_state.push({ + createOpts.initial_state.push({ type: EventType.RoomJoinRules, content: { join_rule: opts.joinRule }, }); From 894bce7813c4dee78e951ed5397c9d61e9f8538e Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Tue, 6 Jul 2021 14:54:06 +0200 Subject: [PATCH 0207/1171] Update lockfile --- yarn.lock | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/yarn.lock b/yarn.lock index c8c3315855..ea4adfb09f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1677,6 +1677,11 @@ "@types/scheduler" "*" csstype "^3.0.2" +"@types/retry@^0.12.0": + version "0.12.0" + resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.0.tgz#2b35eccfcee7d38cd72ad99232fbd58bffb3c84d" + integrity sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA== + "@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" @@ -5445,10 +5450,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: - version "12.0.0" - resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-12.0.0.tgz#8ee7cc37661476341d0c792a1a12bc78b19f9fdd" - integrity sha512-DHeq87Sx9Dv37FYyvZkmA1VYsQUNaVgc3QzMUkFwoHt1T4EZzgyYpdsp3uYruJzUW0ACvVJcwFdrU4e1VS97dQ== +matrix-js-sdk@12.0.1: + version "12.0.1" + resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-12.0.1.tgz#3a63881f743420a4d39474daa39bd0fb90930d43" + integrity sha512-HkOWv8QHojceo3kPbC+vAIFUjsRAig6MBvEY35UygS3g2dL0UcJ5Qx09/2wcXtu6dowlDnWsz2HHk62tS2cklA== dependencies: "@babel/runtime" "^7.12.5" another-json "^0.2.0" @@ -5456,6 +5461,7 @@ matrix-js-sdk@12.0.0: bs58 "^4.0.1" content-type "^1.0.4" loglevel "^1.7.1" + p-retry "^4.5.0" qs "^6.9.6" request "^2.88.2" unhomoglyph "^1.0.6" @@ -6007,6 +6013,14 @@ p-locate@^4.1.0: dependencies: p-limit "^2.2.0" +p-retry@^4.5.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/p-retry/-/p-retry-4.6.0.tgz#9de15ae696278cffe86fce2d8f73b7f894f8bc9e" + integrity sha512-SAHbQEwg3X5DRNaLmWjT+DlGc93ba5i+aP3QLfVNDncQEQO4xjbYW4N/lcVTSuP0aJietGfx2t94dJLzfBMpXw== + dependencies: + "@types/retry" "^0.12.0" + retry "^0.13.1" + p-try@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" @@ -6816,6 +6830,11 @@ ret@~0.1.10: resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg== +retry@^0.13.1: + version "0.13.1" + resolved "https://registry.yarnpkg.com/retry/-/retry-0.13.1.tgz#185b1587acf67919d63b357349e03537b2484658" + integrity sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg== + reusify@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" From 657896c0b9c72756d61e5a43d1171c04ff17bad6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Wed, 7 Jul 2021 11:00:42 +0200 Subject: [PATCH 0208/1171] Delint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/CallHandler.tsx | 1 - src/components/views/voip/VideoFeed.tsx | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/CallHandler.tsx b/src/CallHandler.tsx index 88ef72e9d9..c6ae6ae183 100644 --- a/src/CallHandler.tsx +++ b/src/CallHandler.tsx @@ -56,7 +56,6 @@ limitations under the License. import React from 'react'; import { MatrixClientPeg } from './MatrixClientPeg'; -import PlatformPeg from './PlatformPeg'; import Modal from './Modal'; import { _t } from './languageHandler'; import dis from './dispatcher/dispatcher'; diff --git a/src/components/views/voip/VideoFeed.tsx b/src/components/views/voip/VideoFeed.tsx index 6b33e7c931..272107e6de 100644 --- a/src/components/views/voip/VideoFeed.tsx +++ b/src/components/views/voip/VideoFeed.tsx @@ -36,9 +36,9 @@ interface IProps { // a callback which is called when the video element is resized // due to a change in video metadata - onResize?: (e: Event) => void, + onResize?: (e: Event) => void; - primary: boolean, + primary: boolean; } interface IState { From a03b48d5a4750086310e81af537f8d3ead7228ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Wed, 7 Jul 2021 11:11:00 +0200 Subject: [PATCH 0209/1171] Add missing null guard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/voip/AudioFeed.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/views/voip/AudioFeed.tsx b/src/components/views/voip/AudioFeed.tsx index e656b3594f..3049d80c72 100644 --- a/src/components/views/voip/AudioFeed.tsx +++ b/src/components/views/voip/AudioFeed.tsx @@ -74,6 +74,7 @@ export default class AudioFeed extends React.Component { private playMedia() { const element = this.element.current; + if (!element) return; this.onAudioOutputChanged(MediaDeviceHandler.getAudioOutput()); element.muted = false; element.srcObject = this.props.feed.stream; From b0a1fc7b9785814aa205047e2956ff40e393a244 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Wed, 7 Jul 2021 11:23:38 +0200 Subject: [PATCH 0210/1171] Updated color scheme and spacing --- res/css/views/rooms/_EventBubbleTile.scss | 37 +++++++++++++++++------ res/themes/dark/css/_dark.scss | 8 ++--- res/themes/light/css/_light.scss | 4 +-- 3 files changed, 33 insertions(+), 16 deletions(-) diff --git a/res/css/views/rooms/_EventBubbleTile.scss b/res/css/views/rooms/_EventBubbleTile.scss index c548bfae56..936092db7a 100644 --- a/res/css/views/rooms/_EventBubbleTile.scss +++ b/res/css/views/rooms/_EventBubbleTile.scss @@ -26,9 +26,12 @@ limitations under the License. position: relative; margin-top: var(--gutterSize); - margin-left: calc(var(--avatarSize) + var(--gutterSize)); - margin-right: calc(var(--gutterSize) + var(--avatarSize)); - padding: 2px 0; + margin-left: 50px; + margin-right: 50px; + + &.mx_EventTile_continuation { + margin-top: 2px; + } /* For replies */ .mx_EventTile { @@ -36,7 +39,23 @@ limitations under the License. } &:hover { - background: $eventbubble-bg-hover; + &::before { + content: ''; + position: absolute; + top: -1px; + bottom: -1px; + left: -60px; + right: -65px; + z-index: -1; + background: $eventbubble-bg-hover; + border-radius: 4px; + } + + .mx_EventTile_avatar { + img { + box-shadow: 0 0 0 3px $eventbubble-bg-hover; + } + } } .mx_SenderProfile, @@ -55,10 +74,10 @@ limitations under the License. background: var(--backgroundColor); display: flex; gap: var(--gutterSize); - margin: 0 calc(-2 * var(--gutterSize)); + margin: 0 -12px 0 -22px; > a { position: absolute; - left: -50px; + left: -57px; } } @@ -91,7 +110,7 @@ limitations under the License. } .mx_EventTile_avatar { top: -19px; // height of the sender block - right: calc(-1 * var(--avatarSize)); + right: -45px; } --backgroundColor: $eventbubble-self-bg; @@ -104,8 +123,6 @@ limitations under the License. .mx_ReplyThread_show { order: 99999; - /* background: white; - box-shadow: 0 0 0 var(--gutterSize) white; */ } .mx_ReplyThread { @@ -190,7 +207,7 @@ limitations under the License. .mx_EventTile_readAvatars { position: absolute; - right: -45px; + right: -60px; bottom: 0; top: auto; } diff --git a/res/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss index e2ea8478d2..5ded90230b 100644 --- a/res/themes/dark/css/_dark.scss +++ b/res/themes/dark/css/_dark.scss @@ -232,10 +232,10 @@ $groupFilterPanel-background-blur-amount: 30px; $composer-shadow-color: rgba(0, 0, 0, 0.28); // Bubble tiles -$eventbubble-self-bg: rgba(141, 151, 165, 0.3); -$eventbubble-others-bg: rgba(141, 151, 165, 0.3); -$eventbubble-bg-hover: rgba(141, 151, 165, 0.1); -$eventbubble-avatar-outline: #15191E; +$eventbubble-self-bg: #143A34; +$eventbubble-others-bg: #394049; +$eventbubble-bg-hover: #433C23; +$eventbubble-avatar-outline: $bg-color; // ***** Mixins! ***** diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss index 4b1c56bd51..c84126909e 100644 --- a/res/themes/light/css/_light.scss +++ b/res/themes/light/css/_light.scss @@ -354,8 +354,8 @@ $composer-shadow-color: rgba(0, 0, 0, 0.04); // Bubble tiles $eventbubble-self-bg: #F8FDFC; $eventbubble-others-bg: #F7F8F9; -$eventbubble-bg-hover: rgb(242, 242, 242); -$eventbubble-avatar-outline: #fff; +$eventbubble-bg-hover: #FEFCF5; +$eventbubble-avatar-outline: $primary-bg-color; // ***** Mixins! ***** From 7d946ee0db5f7f1579df3955ce70403bfede0388 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Wed, 7 Jul 2021 12:04:28 +0200 Subject: [PATCH 0211/1171] Restore action bar --- res/css/views/rooms/_EventBubbleTile.scss | 27 ++++++++++++++++++++-- res/css/views/rooms/_EventTile.scss | 14 +++++------ src/components/structures/MessagePanel.tsx | 4 +++- 3 files changed, 35 insertions(+), 10 deletions(-) diff --git a/res/css/views/rooms/_EventBubbleTile.scss b/res/css/views/rooms/_EventBubbleTile.scss index 936092db7a..aa59f53b72 100644 --- a/res/css/views/rooms/_EventBubbleTile.scss +++ b/res/css/views/rooms/_EventBubbleTile.scss @@ -69,18 +69,32 @@ limitations under the License. } .mx_EventTile_line { + position: relative; padding: var(--gutterSize); - border-radius: var(--cornerRadius); + border-top-left-radius: var(--cornerRadius); + border-top-right-radius: var(--cornerRadius); + border-bottom-right-radius: var(--cornerRadius); background: var(--backgroundColor); display: flex; gap: var(--gutterSize); margin: 0 -12px 0 -22px; > a { position: absolute; - left: -57px; + left: -35px; } } + &.mx_EventTile_continuation .mx_EventTile_line { + border-top-left-radius: 0; + } + + &.mx_EventTile_lastInSection .mx_EventTile_line { + border-bottom-left-radius: var(--cornerRadius); + } + + + + .mx_EventTile_avatar { position: absolute; top: 0; @@ -94,6 +108,10 @@ limitations under the License. &[data-self=true] { .mx_EventTile_line { float: right; + > a { + left: auto; + right: -35px; + } } .mx_SenderProfile { display: none; @@ -153,6 +171,11 @@ limitations under the License. left: calc(-1 * var(--avatarSize)); } + .mx_MessageActionBar { + right: 0; + transform: translate3d(50%, 50%, 0); + } + --backgroundColor: $eventbubble-others-bg; } diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index 446c524e81..548a852190 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -123,13 +123,6 @@ $hover-select-border: 4px; 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. */ @@ -626,6 +619,13 @@ $hover-select-border: 4px; } } +.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; +} + @media only screen and (max-width: 480px) { .mx_EventTile_line, .mx_EventTile_reply { padding-left: 0; diff --git a/src/components/structures/MessagePanel.tsx b/src/components/structures/MessagePanel.tsx index a0a1ac9b10..e811a8c1ce 100644 --- a/src/components/structures/MessagePanel.tsx +++ b/src/components/structures/MessagePanel.tsx @@ -644,8 +644,10 @@ export default class MessagePanel extends React.Component { } let willWantDateSeparator = false; + let lastInSection = true; if (nextEvent) { willWantDateSeparator = this.wantsDateSeparator(mxEv, nextEvent.getDate() || new Date()); + lastInSection = willWantDateSeparator || mxEv.getSender() !== nextEvent.getSender(); } // is this a continuation of the previous message? @@ -702,7 +704,7 @@ export default class MessagePanel extends React.Component { isTwelveHour={this.props.isTwelveHour} permalinkCreator={this.props.permalinkCreator} last={last} - lastInSection={willWantDateSeparator} + lastInSection={lastInSection} lastSuccessful={isLastSuccessful} isSelectedEvent={highlight} getRelationsForEvent={this.props.getRelationsForEvent} From 870857f3213332c82d257f61e286a5ec52eac502 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Wed, 7 Jul 2021 13:00:31 +0200 Subject: [PATCH 0212/1171] Right hand side border radius --- res/css/views/rooms/_EventBubbleTile.scss | 25 +++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/res/css/views/rooms/_EventBubbleTile.scss b/res/css/views/rooms/_EventBubbleTile.scss index aa59f53b72..4d189f78a3 100644 --- a/res/css/views/rooms/_EventBubbleTile.scss +++ b/res/css/views/rooms/_EventBubbleTile.scss @@ -68,12 +68,22 @@ limitations under the License. padding: 0 var(--gutterSize); } + &[data-self=false] { + .mx_EventTile_line { + border-bottom-right-radius: var(--cornerRadius); + } + } + &[data-self=true] { + .mx_EventTile_line { + border-bottom-left-radius: var(--cornerRadius); + } + } + .mx_EventTile_line { position: relative; padding: var(--gutterSize); border-top-left-radius: var(--cornerRadius); border-top-right-radius: var(--cornerRadius); - border-bottom-right-radius: var(--cornerRadius); background: var(--backgroundColor); display: flex; gap: var(--gutterSize); @@ -84,16 +94,19 @@ limitations under the License. } } - &.mx_EventTile_continuation .mx_EventTile_line { + &.mx_EventTile_continuation[data-self=false] .mx_EventTile_line { border-top-left-radius: 0; } - - &.mx_EventTile_lastInSection .mx_EventTile_line { + &.mx_EventTile_lastInSection[data-self=false] .mx_EventTile_line { border-bottom-left-radius: var(--cornerRadius); } - - + &.mx_EventTile_continuation[data-self=true] .mx_EventTile_line { + border-top-right-radius: 0; + } + &.mx_EventTile_lastInSection[data-self=true] .mx_EventTile_line { + border-bottom-right-radius: var(--cornerRadius); + } .mx_EventTile_avatar { position: absolute; From 6a03ab825f2478595930dd5d3d73a9b13b4dbb89 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Wed, 7 Jul 2021 13:15:25 +0200 Subject: [PATCH 0213/1171] Fix style linting --- res/css/views/rooms/_EventBubbleTile.scss | 76 ++++++++++------------- 1 file changed, 34 insertions(+), 42 deletions(-) diff --git a/res/css/views/rooms/_EventBubbleTile.scss b/res/css/views/rooms/_EventBubbleTile.scss index 4d189f78a3..d78210a154 100644 --- a/res/css/views/rooms/_EventBubbleTile.scss +++ b/res/css/views/rooms/_EventBubbleTile.scss @@ -72,11 +72,45 @@ limitations under the License. .mx_EventTile_line { border-bottom-right-radius: var(--cornerRadius); } + .mx_EventTile_avatar { + left: calc(-1 * var(--avatarSize)); + } + + .mx_MessageActionBar { + right: 0; + transform: translate3d(50%, 50%, 0); + } + + --backgroundColor: $eventbubble-others-bg; } &[data-self=true] { .mx_EventTile_line { border-bottom-left-radius: var(--cornerRadius); + float: right; + > a { + left: auto; + right: -35px; + } } + .mx_SenderProfile { + display: none; + } + .mx_ReactionsRow { + float: right; + clear: right; + display: flex; + + /* Moving the "add reaction button" before the reactions */ + > :last-child { + order: -1; + } + } + .mx_EventTile_avatar { + top: -19px; // height of the sender block + right: -45px; + } + + --backgroundColor: $eventbubble-self-bg; } .mx_EventTile_line { @@ -118,35 +152,6 @@ limitations under the License. } } - &[data-self=true] { - .mx_EventTile_line { - float: right; - > a { - left: auto; - right: -35px; - } - } - .mx_SenderProfile { - display: none; - } - .mx_ReactionsRow { - float: right; - clear: right; - display: flex; - - /* Moving the "add reaction button" before the reactions */ - > :last-child { - order: -1; - } - } - .mx_EventTile_avatar { - top: -19px; // height of the sender block - right: -45px; - } - - --backgroundColor: $eventbubble-self-bg; - } - &[data-has-reply=true] { > .mx_EventTile_line { flex-direction: column; @@ -179,19 +184,6 @@ limitations under the License. } } - &[data-self=false] { - .mx_EventTile_avatar { - left: calc(-1 * var(--avatarSize)); - } - - .mx_MessageActionBar { - right: 0; - transform: translate3d(50%, 50%, 0); - } - - --backgroundColor: $eventbubble-others-bg; - } - &.mx_EventTile_bubbleContainer, &.mx_EventTile_info, & ~ .mx_EventListSummary[data-expanded=false] { From 55896223aa23b48c18472880ea338b8b7c8ea7ef Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Wed, 7 Jul 2021 15:13:58 +0200 Subject: [PATCH 0214/1171] unbubble some type of events --- res/css/views/rooms/_EventBubbleTile.scss | 58 +++++++-- res/css/views/rooms/_EventTile.scss | 139 +++++++++++---------- src/components/structures/MessagePanel.tsx | 1 + src/components/views/rooms/EventTile.tsx | 5 +- 4 files changed, 127 insertions(+), 76 deletions(-) diff --git a/res/css/views/rooms/_EventBubbleTile.scss b/res/css/views/rooms/_EventBubbleTile.scss index d78210a154..313027bde6 100644 --- a/res/css/views/rooms/_EventBubbleTile.scss +++ b/res/css/views/rooms/_EventBubbleTile.scss @@ -27,7 +27,7 @@ limitations under the License. position: relative; margin-top: var(--gutterSize); margin-left: 50px; - margin-right: 50px; + margin-right: 100px; &.mx_EventTile_continuation { margin-top: 2px; @@ -45,7 +45,7 @@ limitations under the License. top: -1px; bottom: -1px; left: -60px; - right: -65px; + right: -60px; z-index: -1; background: $eventbubble-bg-hover; border-radius: 4px; @@ -65,7 +65,9 @@ limitations under the License. } .mx_SenderProfile { - padding: 0 var(--gutterSize); + position: relative; + top: -2px; + left: calc(-1 * var(--gutterSize)); } &[data-self=false] { @@ -73,7 +75,7 @@ limitations under the License. border-bottom-right-radius: var(--cornerRadius); } .mx_EventTile_avatar { - left: calc(-1 * var(--avatarSize)); + left: -48px; } .mx_MessageActionBar { @@ -107,7 +109,7 @@ limitations under the License. } .mx_EventTile_avatar { top: -19px; // height of the sender block - right: -45px; + right: -35px; } --backgroundColor: $eventbubble-self-bg; @@ -120,7 +122,7 @@ limitations under the License. border-top-right-radius: var(--cornerRadius); background: var(--backgroundColor); display: flex; - gap: var(--gutterSize); + gap: 5px; margin: 0 -12px 0 -22px; > a { position: absolute; @@ -214,6 +216,29 @@ limitations under the License. .mx_EventListSummary_avatars { padding-top: 0; } + + &::after { + content: ""; + clear: both; + } + + .mx_EventTile { + margin: 0 58px; + } + } + + /* events that do not require bubble layout */ + & ~ .mx_EventListSummary, + &.mx_EventTile_bad { + .mx_EventTile_line { + background: transparent; + } + + &:hover { + &::before { + background: transparent; + } + } } & + .mx_EventListSummary { @@ -229,13 +254,30 @@ limitations under the License. /* Special layout scenario for "Unable To Decrypt (UTD)" events */ &.mx_EventTile_bad > .mx_EventTile_line { - flex-direction: column; + display: grid; + grid-template: + "reply reply" auto + "shield body" auto + "shield link" auto + / auto 1fr; + .mx_EventTile_e2eIcon { + grid-area: shield; + } + .mx_UnknownBody { + grid-area: body; + } + .mx_EventTile_keyRequestInfo { + grid-area: link; + } + .mx_ReplyThread_wrapper { + grid-area: reply; + } } .mx_EventTile_readAvatars { position: absolute; - right: -60px; + right: -110px; bottom: 0; top: auto; } diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index 548a852190..ca94ce86c8 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -254,73 +254,6 @@ $hover-select-border: 4px; 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: 80%; - } - } - - .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 { @@ -368,6 +301,14 @@ $hover-select-border: 4px; .mx_MImageBody { margin-right: 34px; } + + .mx_EventTile_e2eIcon { + position: absolute; + top: 6px; + left: 44px; + bottom: 0; + right: 0; + } } .mx_EventTile_bubbleContainer { @@ -431,6 +372,70 @@ $hover-select-border: 4px; cursor: pointer; } + +.mx_EventTile_e2eIcon { + position: relative; + width: 14px; + height: 14px; + display: block; + 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: 80%; + } +} + +.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; +} + /* Various markdown overrides */ .mx_EventTile_body pre { diff --git a/src/components/structures/MessagePanel.tsx b/src/components/structures/MessagePanel.tsx index e811a8c1ce..cee6011e4a 100644 --- a/src/components/structures/MessagePanel.tsx +++ b/src/components/structures/MessagePanel.tsx @@ -712,6 +712,7 @@ export default class MessagePanel extends React.Component { layout={this.props.layout} enableFlair={this.props.enableFlair} showReadReceipts={this.props.showReadReceipts} + hideSender={this.props.room.getMembers().length <= 2} /> , ); diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index a474686333..6db32a1ad5 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -289,6 +289,9 @@ interface IProps { // whether or not to always show timestamps alwaysShowTimestamps?: boolean; + + // whether or not to display the sender + hideSender?: boolean; } interface IState { @@ -978,7 +981,7 @@ export default class EventTile extends React.Component { ); } - if (needsSenderProfile) { + if (needsSenderProfile && this.props.hideSender !== true) { if (!this.props.tileShape || this.props.tileShape === 'reply' || this.props.tileShape === 'reply_preview') { sender = Date: Thu, 8 Jul 2021 13:43:59 +0200 Subject: [PATCH 0215/1171] Fix call button spacing issues MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/css/views/messages/_CallEvent.scss | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/res/css/views/messages/_CallEvent.scss b/res/css/views/messages/_CallEvent.scss index d83dfb39ad..c700eec42e 100644 --- a/res/css/views/messages/_CallEvent.scss +++ b/res/css/views/messages/_CallEvent.scss @@ -90,6 +90,7 @@ limitations under the License. .mx_CallEvent_content_button { height: 24px; padding: 0px 12px; + margin-left: 8px; } .mx_CallEvent_content_button_callBack { @@ -102,7 +103,7 @@ limitations under the License. .mx_CallEvent_iconButton { display: inline-flex; - margin-right: 16px; + margin-right: 8px; &::before { content: ''; From 9ec3d93402222a83883424ceddbaa09214c0f077 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Thu, 8 Jul 2021 14:19:02 +0200 Subject: [PATCH 0216/1171] Better handling of call types MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/css/views/messages/_CallEvent.scss | 20 ++++++++++++-------- src/components/views/messages/CallEvent.tsx | 12 ++++++------ 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/res/css/views/messages/_CallEvent.scss b/res/css/views/messages/_CallEvent.scss index c700eec42e..9c5de99aba 100644 --- a/res/css/views/messages/_CallEvent.scss +++ b/res/css/views/messages/_CallEvent.scss @@ -27,6 +27,18 @@ limitations under the License. box-sizing: border-box; height: 60px; + &.mx_CallEvent_voice { + .mx_CallEvent_type_icon::before { + mask-image: url('$(res)/img/element-icons/call/voice-call.svg'); + } + } + + &.mx_CallEvent_video { + .mx_CallEvent_type_icon::before { + mask-image: url('$(res)/img/element-icons/call/video-call.svg'); + } + } + .mx_CallEvent_info { display: flex; flex-direction: row; @@ -68,14 +80,6 @@ limitations under the License. mask-size: contain; } } - - .mx_CallEvent_type_icon_voice::before { - mask-image: url('$(res)/img/element-icons/call/voice-call.svg'); - } - - .mx_CallEvent_type_icon_video::before { - mask-image: url('$(res)/img/element-icons/call/video-call.svg'); - } } } } diff --git a/src/components/views/messages/CallEvent.tsx b/src/components/views/messages/CallEvent.tsx index d4781a7872..edbfdff6de 100644 --- a/src/components/views/messages/CallEvent.tsx +++ b/src/components/views/messages/CallEvent.tsx @@ -187,14 +187,14 @@ export default class CallEvent extends React.Component { const isVoice = this.props.callEventGrouper.isVoice; const callType = isVoice ? _t("Voice call") : _t("Video call"); const content = this.renderContent(this.state.callState); - const callTypeIconClass = classNames({ - mx_CallEvent_type_icon: true, - mx_CallEvent_type_icon_voice: isVoice, - mx_CallEvent_type_icon_video: !isVoice, + const className = classNames({ + mx_CallEvent: true, + mx_CallEvent_voice: isVoice, + mx_CallEvent_video: !isVoice, }); return ( -
+
{ { sender }
-
+
{ callType }
From 2615ea7f3f162dafa97868077d4a22217b0b773c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Thu, 8 Jul 2021 14:35:06 +0200 Subject: [PATCH 0217/1171] Add icons to buttons MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/css/views/messages/_CallEvent.scss | 30 ++++++++++++++++++--- src/components/views/messages/CallEvent.tsx | 10 +++---- 2 files changed, 31 insertions(+), 9 deletions(-) diff --git a/res/css/views/messages/_CallEvent.scss b/res/css/views/messages/_CallEvent.scss index 9c5de99aba..fec5114a1c 100644 --- a/res/css/views/messages/_CallEvent.scss +++ b/res/css/views/messages/_CallEvent.scss @@ -28,13 +28,17 @@ limitations under the License. height: 60px; &.mx_CallEvent_voice { - .mx_CallEvent_type_icon::before { + .mx_CallEvent_type_icon::before, + .mx_CallEvent_content_button_callBack span::before, + .mx_CallEvent_content_button_answer span::before { mask-image: url('$(res)/img/element-icons/call/voice-call.svg'); } } &.mx_CallEvent_video { - .mx_CallEvent_type_icon::before { + .mx_CallEvent_type_icon::before, + .mx_CallEvent_content_button_callBack span::before, + .mx_CallEvent_content_button_answer span::before { mask-image: url('$(res)/img/element-icons/call/video-call.svg'); } } @@ -95,10 +99,28 @@ limitations under the License. height: 24px; padding: 0px 12px; margin-left: 8px; + + span { + padding: 8px 0; + display: flex; + align-items: center; + + &::before { + content: ''; + display: inline-block; + background-color: $button-fg-color; + mask-position: center; + mask-repeat: no-repeat; + mask-size: 16px; + width: 16px; + height: 16px; + margin-right: 8px; + } + } } - .mx_CallEvent_content_button_callBack { - margin-left: 10px; // To match mx_callEvent + .mx_CallEvent_content_button_reject span::before { + mask-image: url('$(res)/img/element-icons/call/hangup.svg'); } .mx_CallEvent_content_tooltip { diff --git a/src/components/views/messages/CallEvent.tsx b/src/components/views/messages/CallEvent.tsx index edbfdff6de..2d40d8cac1 100644 --- a/src/components/views/messages/CallEvent.tsx +++ b/src/components/views/messages/CallEvent.tsx @@ -85,18 +85,18 @@ export default class CallEvent extends React.Component { title={this.state.silenced ? _t("Sound on"): _t("Silence call")} /> - { _t("Decline") } + { _t("Decline") } - { _t("Accept") } + { _t("Accept") }
); @@ -168,7 +168,7 @@ export default class CallEvent extends React.Component { onClick={ this.props.callEventGrouper.callBack } kind="primary" > - { _t("Call back") } + { _t("Call back") }
); From 722c360de0a0cb13688a42219d1142a182ea61b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Thu, 8 Jul 2021 14:42:05 +0200 Subject: [PATCH 0218/1171] Use the correct color for silence button MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/css/views/messages/_CallEvent.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/css/views/messages/_CallEvent.scss b/res/css/views/messages/_CallEvent.scss index fec5114a1c..54c7df3e0b 100644 --- a/res/css/views/messages/_CallEvent.scss +++ b/res/css/views/messages/_CallEvent.scss @@ -136,7 +136,7 @@ limitations under the License. height: 16px; width: 16px; - background-color: $icon-button-color; + background-color: $tertiary-fg-color; mask-repeat: no-repeat; mask-size: contain; mask-position: center; From 8f0d72335d381ad870c61ade9fcbe4edbacd272e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Thu, 8 Jul 2021 17:16:02 +0200 Subject: [PATCH 0219/1171] Rework call silencing once again MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/CallHandler.tsx | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/CallHandler.tsx b/src/CallHandler.tsx index 3125f11440..24efdd7ab5 100644 --- a/src/CallHandler.tsx +++ b/src/CallHandler.tsx @@ -165,7 +165,7 @@ export default class CallHandler extends EventEmitter { // do the async lookup when we get new information and then store these mappings here private assertedIdentityNativeUsers = new Map(); - private silencedCalls = new Map(); // callId -> silenced + private silencedCalls = new Set(); // callIds static sharedInstance() { if (!window.mxCallHandler) { @@ -228,7 +228,7 @@ export default class CallHandler extends EventEmitter { } public silenceCall(callId: string) { - this.silencedCalls.set(callId, true); + this.silencedCalls.add(callId); this.emit(CallHandlerEvent.SilencedCallsChanged, this.silencedCalls); // Don't pause audio if we have calls which are still ringing @@ -237,13 +237,13 @@ export default class CallHandler extends EventEmitter { } public unSilenceCall(callId: string) { - this.silencedCalls.set(callId, false); + this.silencedCalls.delete(callId); this.emit(CallHandlerEvent.SilencedCallsChanged, this.silencedCalls); this.play(AudioID.Ring); } public isCallSilenced(callId: string): boolean { - return this.silencedCalls.get(callId); + return this.silencedCalls.has(callId); } /** @@ -251,7 +251,7 @@ export default class CallHandler extends EventEmitter { * @returns {boolean} */ private areAnyCallsUnsilenced(): boolean { - return [...this.silencedCalls.values()].includes(false); + return this.calls.size > this.silencedCalls.size; } private async checkProtocols(maxTries) { @@ -478,6 +478,10 @@ export default class CallHandler extends EventEmitter { break; } + if (newState !== CallState.Ringing) { + this.silencedCalls.delete(call.callId); + } + switch (newState) { case CallState.Ringing: this.play(AudioID.Ring); @@ -646,8 +650,6 @@ export default class CallHandler extends EventEmitter { private removeCallForRoom(roomId: string) { console.log("Removing call for room ", roomId); - this.silencedCalls.delete(this.calls.get(roomId).callId); - this.emit(CallHandlerEvent.SilencedCallsChanged, this.silencedCalls); this.calls.delete(roomId); this.emit(CallHandlerEvent.CallsChanged, this.calls); } @@ -857,8 +859,6 @@ export default class CallHandler extends EventEmitter { console.log("Adding call for room ", mappedRoomId); this.calls.set(mappedRoomId, call); this.emit(CallHandlerEvent.CallsChanged, this.calls); - this.silencedCalls.set(call.callId, false); - this.emit(CallHandlerEvent.SilencedCallsChanged, this.silencedCalls); this.setCallListeners(call); // get ready to send encrypted events in the room, so if the user does answer From 437d53d1ccff764978613da396165d27257436f6 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 9 Jul 2021 08:43:41 +0100 Subject: [PATCH 0220/1171] Update space children (best effort) when upgrading a room --- .../views/dialogs/RoomUpgradeDialog.js | 4 +- src/stores/SpaceStore.tsx | 4 + src/utils/RoomUpgrade.ts | 89 +++++++++++-------- 3 files changed, 60 insertions(+), 37 deletions(-) diff --git a/src/components/views/dialogs/RoomUpgradeDialog.js b/src/components/views/dialogs/RoomUpgradeDialog.js index 90092df7a5..acbb99099f 100644 --- a/src/components/views/dialogs/RoomUpgradeDialog.js +++ b/src/components/views/dialogs/RoomUpgradeDialog.js @@ -17,10 +17,10 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; import * as sdk from '../../../index'; -import { MatrixClientPeg } from '../../../MatrixClientPeg'; import Modal from '../../../Modal'; import { _t } from '../../../languageHandler'; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import { upgradeRoom } from "../../../utils/RoomUpgrade"; @replaceableComponent("views.dialogs.RoomUpgradeDialog") export default class RoomUpgradeDialog extends React.Component { @@ -45,7 +45,7 @@ export default class RoomUpgradeDialog extends React.Component { _onUpgradeClick = () => { this.setState({ busy: true }); - MatrixClientPeg.get().upgradeRoom(this.props.room.roomId, this._targetVersion).then(() => { + upgradeRoom(this.props.room, this._targetVersion, false, false).then(() => { this.props.onFinished(true); }).catch((err) => { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); diff --git a/src/stores/SpaceStore.tsx b/src/stores/SpaceStore.tsx index 9a2dc027c2..91bc0a027c 100644 --- a/src/stores/SpaceStore.tsx +++ b/src/stores/SpaceStore.tsx @@ -335,6 +335,10 @@ export class SpaceStoreClass extends AsyncStoreWithClient { return sortBy(parents, r => r.roomId)?.[0] || null; } + public getKnownParents(roomId: string): Set { + return this.parentMap.get(roomId) || new Set(); + } + public getSpaceFilteredRoomIds = (space: Room | null): Set => { if (!space && SettingsStore.getValue("feature_spaces.all_rooms")) { return new Set(this.matrixClient.getVisibleRooms().map(r => r.roomId)); diff --git a/src/utils/RoomUpgrade.ts b/src/utils/RoomUpgrade.ts index 7330b23863..e632ec6345 100644 --- a/src/utils/RoomUpgrade.ts +++ b/src/utils/RoomUpgrade.ts @@ -15,60 +15,79 @@ limitations under the License. */ import { Room } from "matrix-js-sdk/src/models/room"; +import { EventType } from "matrix-js-sdk/src/@types/event"; import { inviteUsersToRoom } from "../RoomInvite"; import Modal from "../Modal"; import { _t } from "../languageHandler"; import ErrorDialog from "../components/views/dialogs/ErrorDialog"; +import SpaceStore from "../stores/SpaceStore"; export async function upgradeRoom( room: Room, targetVersion: string, inviteUsers = false, - // eslint-disable-next-line camelcase -): Promise<{ replacement_room: string }> { + handleError = true, + updateSpaces = true, +): Promise { const cli = room.client; - let checkForUpgradeFn: (room: Room) => Promise; + let newRoomId: string; try { - const upgradePromise = cli.upgradeRoom(room.roomId, targetVersion); - - // We have to wait for the js-sdk to give us the room back so - // we can more effectively abuse the MultiInviter behaviour - // which heavily relies on the Room object being available. - if (inviteUsers) { - checkForUpgradeFn = async (newRoom: Room) => { - // The upgradePromise should be done by the time we await it here. - const { replacement_room: newRoomId } = await upgradePromise; - if (newRoom.roomId !== newRoomId) return; - - const toInvite = [ - ...room.getMembersWithMembership("join"), - ...room.getMembersWithMembership("invite"), - ].map(m => m.userId).filter(m => m !== cli.getUserId()); - - if (toInvite.length > 0) { - // Errors are handled internally to this function - await inviteUsersToRoom(newRoomId, toInvite); - } - - cli.removeListener('Room', checkForUpgradeFn); - }; - cli.on('Room', checkForUpgradeFn); - } - - // We have to await after so that the checkForUpgradesFn has a proper reference - // to the new room's ID. - return upgradePromise; + ({ replacement_room: newRoomId } = await cli.upgradeRoom(room.roomId, targetVersion)); } catch (e) { + if (!handleError) throw e; console.error(e); - if (checkForUpgradeFn) cli.removeListener('Room', checkForUpgradeFn); - - Modal.createTrackedDialog('Slash Commands', 'room upgrade error', ErrorDialog, { + Modal.createTrackedDialog("Room Upgrade Error", "", ErrorDialog, { title: _t('Error upgrading room'), description: _t('Double check that your server supports the room version chosen and try again.'), }); throw e; } + + // We have to wait for the js-sdk to give us the room back so + // we can more effectively abuse the MultiInviter behaviour + // which heavily relies on the Room object being available. + if (inviteUsers) { + const checkForUpgradeFn = async (newRoom: Room): Promise => { + // The upgradePromise should be done by the time we await it here. + if (newRoom.roomId !== newRoomId) return; + + const toInvite = [ + ...room.getMembersWithMembership("join"), + ...room.getMembersWithMembership("invite"), + ].map(m => m.userId).filter(m => m !== cli.getUserId()); + + if (toInvite.length > 0) { + // Errors are handled internally to this function + await inviteUsersToRoom(newRoomId, toInvite); + } + + cli.removeListener('Room', checkForUpgradeFn); + }; + cli.on('Room', checkForUpgradeFn); + } + + if (updateSpaces) { + const parents = SpaceStore.instance.getKnownParents(room.roomId); + try { + for (const parentId of parents) { + const parent = cli.getRoom(parentId); + if (!parent?.currentState.maySendStateEvent(EventType.SpaceChild, cli.getUserId())) continue; + + const currentEv = parent.currentState.getStateEvents(EventType.SpaceChild, room.roomId); + await cli.sendStateEvent(parentId, EventType.SpaceChild, { + ...(currentEv?.getContent() || {}), // copy existing attributes like suggested + via: [cli.getDomain()], + }, newRoomId); + await cli.sendStateEvent(parentId, EventType.SpaceChild, {}, room.roomId); + } + } catch (e) { + // These errors are not critical to the room upgrade itself + console.warn("Failed to update parent spaces during room upgrade", e); + } + } + + return newRoomId; } From 6fe00d12ea444d82942263c04054f2f0cc080a53 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 9 Jul 2021 08:47:18 +0100 Subject: [PATCH 0221/1171] Convert RoomUpgradeDialog to TS --- ...UpgradeDialog.js => RoomUpgradeDialog.tsx} | 68 ++++++++++--------- 1 file changed, 36 insertions(+), 32 deletions(-) rename src/components/views/dialogs/{RoomUpgradeDialog.js => RoomUpgradeDialog.tsx} (58%) diff --git a/src/components/views/dialogs/RoomUpgradeDialog.js b/src/components/views/dialogs/RoomUpgradeDialog.tsx similarity index 58% rename from src/components/views/dialogs/RoomUpgradeDialog.js rename to src/components/views/dialogs/RoomUpgradeDialog.tsx index acbb99099f..bcca0e3829 100644 --- a/src/components/views/dialogs/RoomUpgradeDialog.js +++ b/src/components/views/dialogs/RoomUpgradeDialog.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. @@ -15,19 +15,29 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; -import * as sdk from '../../../index'; +import { Room } from "matrix-js-sdk/src/models/room"; + import Modal from '../../../Modal'; import { _t } from '../../../languageHandler'; import { replaceableComponent } from "../../../utils/replaceableComponent"; import { upgradeRoom } from "../../../utils/RoomUpgrade"; +import { IDialogProps } from "./IDialogProps"; +import BaseDialog from "./BaseDialog"; +import ErrorDialog from './ErrorDialog'; +import DialogButtons from '../elements/DialogButtons'; +import Spinner from "../elements/Spinner"; + +interface IProps extends IDialogProps { + room: Room; +} + +interface IState { + busy: boolean; +} @replaceableComponent("views.dialogs.RoomUpgradeDialog") -export default class RoomUpgradeDialog extends React.Component { - static propTypes = { - room: PropTypes.object.isRequired, - onFinished: PropTypes.func.isRequired, - }; +export default class RoomUpgradeDialog extends React.Component { + private targetVersion: string; state = { busy: true, @@ -35,20 +45,19 @@ export default class RoomUpgradeDialog extends React.Component { async componentDidMount() { const recommended = await this.props.room.getRecommendedVersion(); - this._targetVersion = recommended.version; + this.targetVersion = recommended.version; this.setState({ busy: false }); } - _onCancelClick = () => { + private onCancelClick = (): void => { this.props.onFinished(false); }; - _onUpgradeClick = () => { + private onUpgradeClick = (): void => { this.setState({ busy: true }); - upgradeRoom(this.props.room, this._targetVersion, false, false).then(() => { + upgradeRoom(this.props.room, this.targetVersion, false, false).then(() => { this.props.onFinished(true); }).catch((err) => { - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); Modal.createTrackedDialog('Failed to upgrade room', '', ErrorDialog, { title: _t("Failed to upgrade room"), description: ((err && err.message) ? err.message : _t("The room upgrade could not be completed")), @@ -59,48 +68,43 @@ export default class RoomUpgradeDialog extends React.Component { }; render() { - const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); - const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); - const Spinner = sdk.getComponent('views.elements.Spinner'); - let buttons; if (this.state.busy) { buttons = ; } else { buttons = ; } return ( -

- {_t( + { _t( "Upgrading this room requires closing down the current " + "instance of the room and creating a new room in its place. " + "To give room members the best possible experience, we will:", - )} + ) }

    -
  1. {_t("Create a new room with the same name, description and avatar")}
  2. -
  3. {_t("Update any local room aliases to point to the new room")}
  4. -
  5. {_t("Stop users from speaking in the old version of the room, and post a message advising users to move to the new room")}
  6. -
  7. {_t("Put a link back to the old room at the start of the new room so people can see old messages")}
  8. +
  9. { _t("Create a new room with the same name, description and avatar") }
  10. +
  11. { _t("Update any local room aliases to point to the new room") }
  12. +
  13. { _t("Stop users from speaking in the old version of the room, " + + "and post a message advising users to move to the new room") }
  14. +
  15. { _t("Put a link back to the old room at the start of the new room " + + "so people can see old messages") }
- {buttons} + { buttons }
); } From 718887dd272ca108b48385a65737124931d96fd4 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Sat, 10 Jul 2021 22:40:30 -0400 Subject: [PATCH 0222/1171] Update Emojibase and switch to IamCal (Slack-style) shortcodes for consistency with shortcodes commonly used by other platforms, as was decided in https://github.com/vector-im/element-web/issues/13857. One thing to be aware of is that the currently used version of Twemoji does not support a few of the newer emoji present in Emojibase, so these look a little out of place in the emoji picker. Optimally Twemoji would be updated at the same time, though I don't know how to do that. Signed-off-by: Robin Townsend --- package.json | 4 +- src/HtmlUtils.tsx | 18 +---- src/autocomplete/EmojiProvider.tsx | 69 ++++++++++--------- src/components/views/emojipicker/Preview.tsx | 12 ++-- .../views/emojipicker/QuickReactions.tsx | 7 +- src/emoji.ts | 25 ++++--- yarn.lock | 16 ++--- 7 files changed, 72 insertions(+), 79 deletions(-) diff --git a/package.json b/package.json index bb92ad11d8..4506579747 100644 --- a/package.json +++ b/package.json @@ -64,8 +64,8 @@ "counterpart": "^0.18.6", "diff-dom": "^4.2.2", "diff-match-patch": "^1.0.5", - "emojibase-data": "^5.1.1", - "emojibase-regex": "^4.1.1", + "emojibase-data": "^6.2.0", + "emojibase-regex": "^5.1.3", "escape-html": "^1.0.3", "file-saver": "^2.0.5", "filesize": "6.1.0", diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx index 016b557477..26aeef9dd8 100644 --- a/src/HtmlUtils.tsx +++ b/src/HtmlUtils.tsx @@ -34,7 +34,7 @@ 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 { getEmojiFromUnicode, getShortcodes } from "./emoji"; import ReplyThread from "./components/views/elements/ReplyThread"; import { mediaFromMxc } from "./customisations/Media"; @@ -78,20 +78,8 @@ function mightContainEmoji(str: string): boolean { * @return {String} The shortcode (such as :thumbup:) */ export function unicodeToShortcode(char: string): string { - const data = getEmojiFromUnicode(char); - return (data && data.shortcodes ? `:${data.shortcodes[0]}:` : ''); -} - -/** - * Returns the unicode character for an emoji shortcode - * - * @param {String} shortcode The shortcode (such as :thumbup:) - * @return {String} The emoji character; null if none exists - */ -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; + const shortcodes = getShortcodes(getEmojiFromUnicode(char)); + return shortcodes.length > 0 ? `:${shortcodes[0]}:` : ''; } export function processHtmlForSending(html: string): string { diff --git a/src/autocomplete/EmojiProvider.tsx b/src/autocomplete/EmojiProvider.tsx index 2fc77e9a17..edf691e151 100644 --- a/src/autocomplete/EmojiProvider.tsx +++ b/src/autocomplete/EmojiProvider.tsx @@ -25,8 +25,7 @@ import { PillCompletion } from './Components'; import { ICompletion, ISelectionRange } from './Autocompleter'; import { uniq, sortBy } from 'lodash'; import SettingsStore from "../settings/SettingsStore"; -import { shortcodeToUnicode } from '../HtmlUtils'; -import { EMOJI, IEmoji } from '../emoji'; +import { EMOJI, IEmoji, getShortcodes } from '../emoji'; import EMOTICON_REGEX from 'emojibase-regex/emoticon'; @@ -38,21 +37,26 @@ const EMOJI_REGEX = new RegExp('(' + EMOTICON_REGEX.source + '|(?:^|\\s):[+-\\w] interface IEmojiShort { emoji: IEmoji; - shortname: string; + shortcode: string; + altShortcodes: string[]; _orderBy: number; } -const EMOJI_SHORTNAMES: IEmojiShort[] = EMOJI.sort((a, b) => { +const EMOJI_SHORTCODES: IEmojiShort[] = EMOJI.sort((a, b) => { if (a.group === b.group) { return a.order - b.order; } return a.group - b.group; -}).map((emoji, index) => ({ - emoji, - shortname: `:${emoji.shortcodes[0]}:`, - // Include the index so that we can preserve the original order - _orderBy: index, -})); +}).map((emoji, index) => { + const [shortcode, ...altShortcodes] = getShortcodes(emoji); + return { + emoji, + shortcode: shortcode ? `:${shortcode}:` : undefined, + altShortcodes: altShortcodes.map(s => `:${s}:`), + // Include the index so that we can preserve the original order + _orderBy: index, + }; +}).filter(emoji => emoji.shortcode); function score(query, space) { const index = space.indexOf(query); @@ -69,15 +73,15 @@ export default class EmojiProvider extends AutocompleteProvider { constructor() { super(EMOJI_REGEX); - this.matcher = new QueryMatcher(EMOJI_SHORTNAMES, { - keys: ['emoji.emoticon', 'shortname'], + this.matcher = new QueryMatcher(EMOJI_SHORTCODES, { + keys: ['emoji.emoticon', 'shortcode'], funcs: [ - (o) => o.emoji.shortcodes.length > 1 ? o.emoji.shortcodes.slice(1).map(s => `:${s}:`).join(" ") : "", // aliases + o => o.altShortcodes.join(" "), // aliases ], // For matching against ascii equivalents shouldMatchWordsOnly: false, }); - this.nameMatcher = new QueryMatcher(EMOJI_SHORTNAMES, { + this.nameMatcher = new QueryMatcher(EMOJI_SHORTCODES, { keys: ['emoji.annotation'], // For removing punctuation shouldMatchWordsOnly: true, @@ -105,34 +109,33 @@ export default class EmojiProvider extends AutocompleteProvider { const sorters = []; // make sure that emoticons come first - sorters.push((c) => score(matchedString, c.emoji.emoticon || "")); + sorters.push(c => score(matchedString, c.emoji.emoticon || "")); - // then sort by score (Infinity if matchedString not in shortname) - sorters.push((c) => score(matchedString, c.shortname)); + // then sort by score (Infinity if matchedString not in shortcode) + sorters.push(c => score(matchedString, c.shortcode)); // then sort by max score of all shortcodes, trim off the `:` - sorters.push((c) => Math.min(...c.emoji.shortcodes.map(s => score(matchedString.substring(1), s)))); - // If the matchedString is not empty, sort by length of shortname. Example: + sorters.push(c => Math.min( + ...[c.shortcode, ...c.altShortcodes].map(s => score(matchedString.substring(1), s)), + )); + // If the matchedString is not empty, sort by length of shortcode. Example: // matchedString = ":bookmark" // completions = [":bookmark:", ":bookmark_tabs:", ...] if (matchedString.length > 1) { - sorters.push((c) => c.shortname.length); + sorters.push(c => c.shortcode.length); } // Finally, sort by original ordering - sorters.push((c) => c._orderBy); + sorters.push(c => c._orderBy); completions = sortBy(uniq(completions), sorters); - completions = completions.map(({ shortname }) => { - const unicode = shortcodeToUnicode(shortname); - return { - completion: unicode, - component: ( - - { unicode } - - ), - range, - }; - }).slice(0, LIMIT); + completions = completions.map(c => ({ + completion: c.emoji.unicode, + component: ( + + { c.emoji.unicode } + + ), + range, + })).slice(0, LIMIT); } return completions; } diff --git a/src/components/views/emojipicker/Preview.tsx b/src/components/views/emojipicker/Preview.tsx index 9c2dbb9cbd..bd9982e50f 100644 --- a/src/components/views/emojipicker/Preview.tsx +++ b/src/components/views/emojipicker/Preview.tsx @@ -17,7 +17,7 @@ limitations under the License. import React from 'react'; -import { IEmoji } from "../../../emoji"; +import { IEmoji, getShortcodes } from "../../../emoji"; import { replaceableComponent } from "../../../utils/replaceableComponent"; interface IProps { @@ -30,8 +30,8 @@ class Preview extends React.PureComponent { const { unicode = "", annotation = "", - shortcodes: [shortcode = ""], - } = this.props.emoji || {}; + } = this.props.emoji; + const shortcode = getShortcodes(this.props.emoji)[0]; return (
@@ -42,9 +42,9 @@ class Preview extends React.PureComponent {
{annotation}
-
- {shortcode} -
+ { shortcode ? +
{shortcode}
: + null }
); diff --git a/src/components/views/emojipicker/QuickReactions.tsx b/src/components/views/emojipicker/QuickReactions.tsx index ffd3ce9760..2d78e3e4cf 100644 --- a/src/components/views/emojipicker/QuickReactions.tsx +++ b/src/components/views/emojipicker/QuickReactions.tsx @@ -18,7 +18,7 @@ limitations under the License. import React from 'react'; import { _t } from '../../../languageHandler'; -import { getEmojiFromUnicode, IEmoji } from "../../../emoji"; +import { getEmojiFromUnicode, getShortcodes, IEmoji } from "../../../emoji"; import Emoji from "./Emoji"; import { replaceableComponent } from "../../../utils/replaceableComponent"; @@ -62,6 +62,7 @@ class QuickReactions extends React.Component { }; render() { + const shortcode = this.state.hover ? getShortcodes(this.state.hover)[0] : undefined; return (

@@ -69,7 +70,9 @@ class QuickReactions extends React.Component { ? _t("Quick Reactions") : {this.state.hover.annotation} - {this.state.hover.shortcodes[0]} + { shortcode ? + {shortcode} : + null } }

diff --git a/src/emoji.ts b/src/emoji.ts index 7caeb06d21..ac4de654f7 100644 --- a/src/emoji.ts +++ b/src/emoji.ts @@ -15,14 +15,14 @@ limitations under the License. */ import EMOJIBASE from 'emojibase-data/en/compact.json'; +import SHORTCODES from 'emojibase-data/en/shortcodes/iamcal.json'; export interface IEmoji { annotation: string; - group: number; + group?: number; hexcode: string; - order: number; - shortcodes: string[]; - tags: string[]; + order?: number; + tags?: string[]; unicode: string; emoticon?: string; } @@ -34,10 +34,14 @@ interface IEmojiWithFilterString extends IEmoji { // The unicode is stored without the variant selector const UNICODE_TO_EMOJI = new Map(); // not exported as gets for it are handled by getEmojiFromUnicode export const EMOTICON_TO_EMOJI = new Map(); -export const SHORTCODE_TO_EMOJI = new Map(); export const getEmojiFromUnicode = unicode => UNICODE_TO_EMOJI.get(stripVariation(unicode)); +const toArray = (shortcodes?: string | string[]): string[] => + typeof shortcodes === "string" ? [shortcodes] : (shortcodes ?? []); +export const getShortcodes = (emoji: IEmoji): string[] => + toArray(SHORTCODES[emoji.hexcode]); + const EMOJIBASE_GROUP_ID_TO_CATEGORY = [ "people", // smileys "people", // actually people @@ -66,12 +70,14 @@ const ZERO_WIDTH_JOINER = "\u200D"; // Store various mappings from unicode/emoticon/shortcode to the Emoji objects EMOJIBASE.forEach((emoji: IEmojiWithFilterString) => { + const shortcodes = getShortcodes(emoji); const categoryId = EMOJIBASE_GROUP_ID_TO_CATEGORY[emoji.group]; if (DATA_BY_CATEGORY.hasOwnProperty(categoryId)) { DATA_BY_CATEGORY[categoryId].push(emoji); } + // This is used as the string to match the query against when filtering emojis - emoji.filterString = (`${emoji.annotation}\n${emoji.shortcodes.join('\n')}}\n${emoji.emoticon || ''}\n` + + emoji.filterString = (`${emoji.annotation}\n${shortcodes.join('\n')}}\n${emoji.emoticon || ''}\n` + `${emoji.unicode.split(ZERO_WIDTH_JOINER).join("\n")}`).toLowerCase(); // Add mapping from unicode to Emoji object @@ -87,13 +93,6 @@ EMOJIBASE.forEach((emoji: IEmojiWithFilterString) => { // Add mapping from emoticon to Emoji object EMOTICON_TO_EMOJI.set(emoji.emoticon, emoji); } - - if (emoji.shortcodes) { - // Add mapping from each shortcode to Emoji object - emoji.shortcodes.forEach(shortcode => { - SHORTCODE_TO_EMOJI.set(shortcode, emoji); - }); - } }); /** diff --git a/yarn.lock b/yarn.lock index 90f415673d..21dc0f8fd5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3022,15 +3022,15 @@ emoji-regex@^8.0.0: resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== -emojibase-data@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/emojibase-data/-/emojibase-data-5.1.1.tgz#0a0d63dd07ce1376b3d27642f28cafa46f651de6" - integrity sha512-za/ma5SfogHjwUmGFnDbTvSfm8GGFvFaPS27GPti16YZSp5EPgz+UDsZCATXvJGit+oRNBbG/FtybXHKi2UQgQ== +emojibase-data@^6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/emojibase-data/-/emojibase-data-6.2.0.tgz#db6c75c36905284fa623f4aa5f468d2be6ed364a" + integrity sha512-SWKaXD2QeQs06IE7qfJftsI5924Dqzp+V9xaa5RzZIEWhmlrG6Jt2iKwfgOPHu+5S8MEtOI7GdpKsXj46chXOw== -emojibase-regex@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/emojibase-regex/-/emojibase-regex-4.1.1.tgz#6e781aca520281600fe7a177f1582c33cf1fc545" - integrity sha512-KSigB1zQkNKFygLZ5bAfHs87LJa1ni8QTQtq8lc53Y74NF3Dk2r7kfa8MpooTO8JBb5Xz660X4tSjDB+I+7elA== +emojibase-regex@^5.1.3: + version "5.1.3" + resolved "https://registry.yarnpkg.com/emojibase-regex/-/emojibase-regex-5.1.3.tgz#f0ef621ed6ec624becd2326f999fd4ea01b94554" + integrity sha512-gT8T9LxLA8VJdI+8KQtyykB9qKzd7WuUL3M2yw6y9tplFeufOUANg3UKVaKUvkMcRNvZsSElWhxcJrx8WPE12g== encoding@^0.1.11: version "0.1.13" From 7a329b7a018d9343c7514c3c5ef83edce97c345e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sun, 11 Jul 2021 10:31:24 +0200 Subject: [PATCH 0223/1171] Add ReactUtils MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/utils/ReactUtils.tsx | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 src/utils/ReactUtils.tsx diff --git a/src/utils/ReactUtils.tsx b/src/utils/ReactUtils.tsx new file mode 100644 index 0000000000..ce92dd8a51 --- /dev/null +++ b/src/utils/ReactUtils.tsx @@ -0,0 +1,33 @@ +/* +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"; + +/** + * Joins an array into one value with a joiner. E.g. join(["hello", "world"], " ") -> hello world + * @param array the array of element to join + * @param joiner the string/JSX.Element to join with + * @returns the joined array + */ +export function join(array: Array, joiner?: string | JSX.Element): JSX.Element { + const newArray = []; + array.forEach((element, index) => { + newArray.push(element, (index === array.length - 1) ? null : joiner); + }); + return ( + { newArray } + ); +} From 3e95cd1854017de2cf7cd3e6ecda9c4c09992bda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sun, 11 Jul 2021 10:34:15 +0200 Subject: [PATCH 0224/1171] Handle JSX in MELS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/elements/EventListSummary.tsx | 2 +- .../views/elements/MemberEventListSummary.tsx | 14 +++++++++++--- src/utils/FormattingUtils.ts | 7 ++++--- 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/src/components/views/elements/EventListSummary.tsx b/src/components/views/elements/EventListSummary.tsx index 681817ca86..4f6df2aa0e 100644 --- a/src/components/views/elements/EventListSummary.tsx +++ b/src/components/views/elements/EventListSummary.tsx @@ -33,7 +33,7 @@ interface IProps { // The list of room members for which to show avatars next to the summary summaryMembers?: RoomMember[]; // The text to show as the summary of this event list - summaryText?: string; + summaryText?: string | JSX.Element; // An array of EventTiles to render when expanded children: ReactNode[]; // Called when the event list expansion is toggled diff --git a/src/components/views/elements/MemberEventListSummary.tsx b/src/components/views/elements/MemberEventListSummary.tsx index d52462f629..cef6195067 100644 --- a/src/components/views/elements/MemberEventListSummary.tsx +++ b/src/components/views/elements/MemberEventListSummary.tsx @@ -25,6 +25,7 @@ import { formatCommaSeparatedList } from '../../../utils/FormattingUtils'; import { isValid3pidInvite } from "../../../RoomInvite"; import EventListSummary from "./EventListSummary"; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import { join } from '../../../utils/ReactUtils'; interface IProps extends Omit, "summaryText" | "summaryMembers"> { // The maximum number of names to show in either each summary e.g. 2 would result "A, B and 234 others left" @@ -89,7 +90,10 @@ export default class MemberEventListSummary extends React.Component { * `Object.keys(eventAggregates)`. * @returns {string} the textual summary of the aggregated events that occurred. */ - private generateSummary(eventAggregates: Record, orderedTransitionSequences: string[]) { + private generateSummary( + eventAggregates: Record, + orderedTransitionSequences: string[], + ): string | JSX.Element { const summaries = orderedTransitionSequences.map((transitions) => { const userNames = eventAggregates[transitions]; const nameList = this.renderNameList(userNames); @@ -118,7 +122,7 @@ export default class MemberEventListSummary extends React.Component { return null; } - return summaries.join(", "); + return join(summaries, ", "); } /** @@ -212,7 +216,11 @@ export default class MemberEventListSummary extends React.Component { * @param {number} repeats the number of times the transition was repeated in a row. * @returns {string} the written Human Readable equivalent of the transition. */ - private static getDescriptionForTransition(t: TransitionType, userCount: number, repeats: number) { + private static getDescriptionForTransition( + t: TransitionType, + userCount: number, + repeats: number, + ): string | JSX.Element { // The empty interpolations 'severalUsers' and 'oneUser' // are there only to show translators to non-English languages // that the verb is conjugated to plural or singular Subject. diff --git a/src/utils/FormattingUtils.ts b/src/utils/FormattingUtils.ts index 1fe3669f26..53a4adb238 100644 --- a/src/utils/FormattingUtils.ts +++ b/src/utils/FormattingUtils.ts @@ -16,6 +16,7 @@ limitations under the License. */ import { _t } from '../languageHandler'; +import { join } from './ReactUtils'; /** * formats numbers to fit into ~3 characters, suitable for badge counts @@ -103,7 +104,7 @@ export function getUserNameColorClass(userId: string): string { * @returns {string} a string constructed by joining `items` with a comma * between each item, but with the last item appended as " and [lastItem]". */ -export function formatCommaSeparatedList(items: string[], itemLimit?: number): string { +export function formatCommaSeparatedList(items: Array, itemLimit?: number): string | JSX.Element { const remaining = itemLimit === undefined ? 0 : Math.max( items.length - itemLimit, 0, ); @@ -113,9 +114,9 @@ export function formatCommaSeparatedList(items: string[], itemLimit?: number): s return items[0]; } else if (remaining > 0) { items = items.slice(0, itemLimit); - return _t("%(items)s and %(count)s others", { items: items.join(', '), count: remaining } ); + return _t("%(items)s and %(count)s others", { items: join(items, ', '), count: remaining } ); } else { const lastItem = items.pop(); - return _t("%(items)s and %(lastItem)s", { items: items.join(', '), lastItem: lastItem }); + return _t("%(items)s and %(lastItem)s", { items: join(items, ', '), lastItem: lastItem }); } } From f80f4620dfec779c6b47c3bc059e6a4d86f124d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sun, 11 Jul 2021 10:35:20 +0200 Subject: [PATCH 0225/1171] Add pinned messages to MELS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/structures/MessagePanel.tsx | 7 +++- .../views/elements/MemberEventListSummary.tsx | 39 +++++++++++++++---- 2 files changed, 38 insertions(+), 8 deletions(-) diff --git a/src/components/structures/MessagePanel.tsx b/src/components/structures/MessagePanel.tsx index a0a1ac9b10..16b1c0064b 100644 --- a/src/components/structures/MessagePanel.tsx +++ b/src/components/structures/MessagePanel.tsx @@ -50,7 +50,12 @@ import EditorStateTransfer from "../../utils/EditorStateTransfer"; const CONTINUATION_MAX_INTERVAL = 5 * 60 * 1000; // 5 minutes const continuedTypes = [EventType.Sticker, EventType.RoomMessage]; -const membershipTypes = [EventType.RoomMember, EventType.RoomThirdPartyInvite, EventType.RoomServerAcl]; +const membershipTypes = [ + EventType.RoomMember, + EventType.RoomThirdPartyInvite, + EventType.RoomServerAcl, + EventType.RoomPinnedEvents, +]; // 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 diff --git a/src/components/views/elements/MemberEventListSummary.tsx b/src/components/views/elements/MemberEventListSummary.tsx index cef6195067..80efb2bea8 100644 --- a/src/components/views/elements/MemberEventListSummary.tsx +++ b/src/components/views/elements/MemberEventListSummary.tsx @@ -25,7 +25,22 @@ import { formatCommaSeparatedList } from '../../../utils/FormattingUtils'; import { isValid3pidInvite } from "../../../RoomInvite"; import EventListSummary from "./EventListSummary"; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import defaultDispatcher from '../../../dispatcher/dispatcher'; +import { RightPanelPhases } from '../../../stores/RightPanelStorePhases'; +import { Action } from '../../../dispatcher/actions'; +import { SetRightPanelPhasePayload } from '../../../dispatcher/payloads/SetRightPanelPhasePayload'; import { join } from '../../../utils/ReactUtils'; +import { EventType } from '../../../../../matrix-js-sdk/src/@types/event'; + +const onPinnedMessagesClick = (): void => { + defaultDispatcher.dispatch({ + action: Action.SetRightPanelPhase, + phase: RightPanelPhases.PinnedMessages, + allowClose: false, + }); +}; + +const SENDER_AS_DISPLAY_NAME_EVENTS = [EventType.RoomServerAcl, EventType.RoomPinnedEvents]; interface IProps extends Omit, "summaryText" | "summaryMembers"> { // The maximum number of names to show in either each summary e.g. 2 would result "A, B and 234 others left" @@ -58,6 +73,7 @@ enum TransitionType { ChangedAvatar = "changed_avatar", NoChange = "no_change", ServerAcl = "server_acl", + PinnedMessages = "pinned_messages" } const SEP = ","; @@ -303,6 +319,15 @@ export default class MemberEventListSummary extends React.Component { { severalUsers: "", count: repeats }) : _t("%(oneUser)schanged the server ACLs %(count)s times", { oneUser: "", count: repeats }); break; + case "pinned_messages": + res = (userCount > 1) + ? _t("%(severalUsers)schanged the pinned messages for the room %(count)s times.", + { severalUsers: "", count: repeats }, + { "a": (sub) => { sub } }) + : _t("%(oneUser)schanged the pinned messages for the room %(count)s times.", + { oneUser: "", count: repeats }, + { "a": (sub) => { sub } }); + break; } return res; @@ -321,16 +346,16 @@ export default class MemberEventListSummary extends React.Component { * if a transition is not recognised. */ private static getTransition(e: IUserEvents): TransitionType { - if (e.mxEvent.getType() === 'm.room.third_party_invite') { + if (e.mxEvent.getType() === EventType.RoomThirdPartyInvite) { // Handle 3pid invites the same as invites so they get bundled together if (!isValid3pidInvite(e.mxEvent)) { return TransitionType.InviteWithdrawal; } return TransitionType.Invited; - } - - if (e.mxEvent.getType() === 'm.room.server_acl') { + } else if (e.mxEvent.getType() === EventType.RoomServerAcl) { return TransitionType.ServerAcl; + } else if (e.mxEvent.getType() === EventType.RoomPinnedEvents) { + return TransitionType.PinnedMessages; } switch (e.mxEvent.getContent().membership) { @@ -425,16 +450,16 @@ export default class MemberEventListSummary extends React.Component { userEvents[userId] = []; } - if (e.getType() === 'm.room.server_acl') { + if (SENDER_AS_DISPLAY_NAME_EVENTS.includes(e.getType() as EventType)) { latestUserAvatarMember.set(userId, e.sender); } else if (e.target) { latestUserAvatarMember.set(userId, e.target); } let displayName = userId; - if (e.getType() === 'm.room.third_party_invite') { + if (e.getType() === EventType.RoomThirdPartyInvite) { displayName = e.getContent().display_name; - } else if (e.getType() === 'm.room.server_acl') { + } else if (SENDER_AS_DISPLAY_NAME_EVENTS.includes(e.getType() as EventType)) { displayName = e.sender.name; } else if (e.target) { displayName = e.target.name; From 1b993ef1bde87f797ce4af6dbb9b5708ca56c430 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sun, 11 Jul 2021 10:36:56 +0200 Subject: [PATCH 0226/1171] 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 | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 7795bb2610..9c22efdb3e 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2058,6 +2058,8 @@ "%(severalUsers)schanged the server ACLs %(count)s times|one": "%(severalUsers)schanged the server ACLs", "%(oneUser)schanged the server ACLs %(count)s times|other": "%(oneUser)schanged the server ACLs %(count)s times", "%(oneUser)schanged the server ACLs %(count)s times|one": "%(oneUser)schanged the server ACLs", + "%(severalUsers)schanged the pinned messages for the room %(count)s times.|other": "%(severalUsers)schanged the pinned messages for the room %(count)s times.", + "%(oneUser)schanged the pinned messages for the room %(count)s times.|other": "%(oneUser)schanged the pinned messages for the room %(count)s times.", "Power level": "Power level", "Custom level": "Custom level", "QR Code": "QR Code", From db8ebd6df009f268ced03b2f1bacedec5b3c300f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sun, 11 Jul 2021 10:45:16 +0200 Subject: [PATCH 0227/1171] Fix import MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/elements/MemberEventListSummary.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/elements/MemberEventListSummary.tsx b/src/components/views/elements/MemberEventListSummary.tsx index 80efb2bea8..e3dbd0a906 100644 --- a/src/components/views/elements/MemberEventListSummary.tsx +++ b/src/components/views/elements/MemberEventListSummary.tsx @@ -30,7 +30,7 @@ import { RightPanelPhases } from '../../../stores/RightPanelStorePhases'; import { Action } from '../../../dispatcher/actions'; import { SetRightPanelPhasePayload } from '../../../dispatcher/payloads/SetRightPanelPhasePayload'; import { join } from '../../../utils/ReactUtils'; -import { EventType } from '../../../../../matrix-js-sdk/src/@types/event'; +import { EventType } from 'matrix-js-sdk/src/@types/event'; const onPinnedMessagesClick = (): void => { defaultDispatcher.dispatch({ From 29ef5905d60b14f3751698afb96cffe3c663b6b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sun, 11 Jul 2021 10:45:30 +0200 Subject: [PATCH 0228/1171] Export type into a var MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- .../views/elements/MemberEventListSummary.tsx | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/components/views/elements/MemberEventListSummary.tsx b/src/components/views/elements/MemberEventListSummary.tsx index e3dbd0a906..4ae64b65a5 100644 --- a/src/components/views/elements/MemberEventListSummary.tsx +++ b/src/components/views/elements/MemberEventListSummary.tsx @@ -346,15 +346,17 @@ export default class MemberEventListSummary extends React.Component { * if a transition is not recognised. */ private static getTransition(e: IUserEvents): TransitionType { - if (e.mxEvent.getType() === EventType.RoomThirdPartyInvite) { + const type = e.mxEvent.getType(); + + if (type === EventType.RoomThirdPartyInvite) { // Handle 3pid invites the same as invites so they get bundled together if (!isValid3pidInvite(e.mxEvent)) { return TransitionType.InviteWithdrawal; } return TransitionType.Invited; - } else if (e.mxEvent.getType() === EventType.RoomServerAcl) { + } else if (type === EventType.RoomServerAcl) { return TransitionType.ServerAcl; - } else if (e.mxEvent.getType() === EventType.RoomPinnedEvents) { + } else if (type === EventType.RoomPinnedEvents) { return TransitionType.PinnedMessages; } @@ -444,22 +446,23 @@ export default class MemberEventListSummary extends React.Component { // Object mapping user IDs to an array of IUserEvents const userEvents: Record = {}; eventsToRender.forEach((e, index) => { - const userId = e.getType() === 'm.room.server_acl' ? e.getSender() : e.getStateKey(); + const type = e.getType(); + const userId = type === EventType.RoomServerAcl ? e.getSender() : e.getStateKey(); // Initialise a user's events if (!userEvents[userId]) { userEvents[userId] = []; } - if (SENDER_AS_DISPLAY_NAME_EVENTS.includes(e.getType() as EventType)) { + if (SENDER_AS_DISPLAY_NAME_EVENTS.includes(type as EventType)) { latestUserAvatarMember.set(userId, e.sender); } else if (e.target) { latestUserAvatarMember.set(userId, e.target); } let displayName = userId; - if (e.getType() === EventType.RoomThirdPartyInvite) { + if (type === EventType.RoomThirdPartyInvite) { displayName = e.getContent().display_name; - } else if (SENDER_AS_DISPLAY_NAME_EVENTS.includes(e.getType() as EventType)) { + } else if (SENDER_AS_DISPLAY_NAME_EVENTS.includes(type as EventType)) { displayName = e.sender.name; } else if (e.target) { displayName = e.target.name; From 19f14e4b2e6495cbbe9b7c70f9753b1005ed2e98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sun, 11 Jul 2021 10:58:04 +0200 Subject: [PATCH 0229/1171] Fix tests? MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/utils/ReactUtils.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/ReactUtils.tsx b/src/utils/ReactUtils.tsx index ce92dd8a51..25669d2d9b 100644 --- a/src/utils/ReactUtils.tsx +++ b/src/utils/ReactUtils.tsx @@ -28,6 +28,6 @@ export function join(array: Array, joiner?: string | JSX.E newArray.push(element, (index === array.length - 1) ? null : joiner); }); return ( - { newArray } + { newArray } ); } From 98808aabcab35f38ceb0c8d8f7ae6e3fc59cb3e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sun, 11 Jul 2021 18:53:29 +0200 Subject: [PATCH 0230/1171] Set contentEditable for PillParts to false MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/editor/parts.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/editor/parts.ts b/src/editor/parts.ts index 351df5062f..39e92ded1c 100644 --- a/src/editor/parts.ts +++ b/src/editor/parts.ts @@ -249,6 +249,7 @@ abstract class PillPart extends BasePart implements IPillPart { toDOMNode() { const container = document.createElement("span"); container.setAttribute("spellcheck", "false"); + container.setAttribute("contentEditable", "false"); container.className = this.className; container.appendChild(document.createTextNode(this.text)); this.setAvatar(container); From 5423421240cf1abbe749eb0dde82c8255753784a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sun, 11 Jul 2021 19:09:39 +0200 Subject: [PATCH 0231/1171] Give singletonRoomViewStore a type MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/stores/RoomViewStore.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stores/RoomViewStore.tsx b/src/stores/RoomViewStore.tsx index 10f42f3166..1a85ff59b1 100644 --- a/src/stores/RoomViewStore.tsx +++ b/src/stores/RoomViewStore.tsx @@ -429,7 +429,7 @@ class RoomViewStore extends Store { } } -let singletonRoomViewStore = null; +let singletonRoomViewStore: RoomViewStore = null; if (!singletonRoomViewStore) { singletonRoomViewStore = new RoomViewStore(); } From 667abca31f42bfba9c055e521afb1540429dd840 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sun, 11 Jul 2021 20:02:32 +0200 Subject: [PATCH 0232/1171] Handle pill onclick MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/css/views/rooms/_BasicMessageComposer.scss | 1 + src/editor/parts.ts | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/res/css/views/rooms/_BasicMessageComposer.scss b/res/css/views/rooms/_BasicMessageComposer.scss index e1ba468204..d87444441a 100644 --- a/res/css/views/rooms/_BasicMessageComposer.scss +++ b/res/css/views/rooms/_BasicMessageComposer.scss @@ -47,6 +47,7 @@ limitations under the License. &.mx_BasicMessageComposer_input_shouldShowPillAvatar { span.mx_UserPill, span.mx_RoomPill { position: relative; + cursor: pointer; // avatar psuedo element &::before { diff --git a/src/editor/parts.ts b/src/editor/parts.ts index 39e92ded1c..8f662f9367 100644 --- a/src/editor/parts.ts +++ b/src/editor/parts.ts @@ -25,6 +25,10 @@ import AutocompleteWrapperModel, { UpdateQuery, } from "./autocomplete"; import * as Avatar from "../Avatar"; +import defaultDispatcher from "../dispatcher/dispatcher"; +import { Action } from "../dispatcher/actions"; +import singletonRoomViewStore from "../stores/RoomViewStore"; +import { MatrixClientPeg } from "../MatrixClientPeg"; interface ISerializedPart { type: Type.Plain | Type.Newline | Type.Command | Type.PillCandidate; @@ -74,6 +78,7 @@ interface IPillCandidatePart extends Omit { type: Type.AtRoomPill | Type.RoomPill | Type.UserPill; resourceId: string; + onClick?(): void; } export type Part = IBasePart | IPillCandidatePart | IPillPart; @@ -250,6 +255,7 @@ abstract class PillPart extends BasePart implements IPillPart { const container = document.createElement("span"); container.setAttribute("spellcheck", "false"); container.setAttribute("contentEditable", "false"); + container.onclick = this.onClick; container.className = this.className; container.appendChild(document.createTextNode(this.text)); this.setAvatar(container); @@ -304,6 +310,8 @@ abstract class PillPart extends BasePart implements IPillPart { abstract get className(): string; + abstract onClick?(): void; + abstract setAvatar(node: HTMLElement): void; } @@ -365,6 +373,9 @@ class RoomPillPart extends PillPart { get className() { return "mx_RoomPill mx_Pill"; } + + // FIXME: We do this to shut up the linter, is there a way to do this properly + onClick = undefined; } class AtRoomPillPart extends RoomPillPart { @@ -403,6 +414,13 @@ class UserPillPart extends PillPart { this._setAvatarVars(node, avatarUrl, initialLetter); } + onClick = () => { + defaultDispatcher.dispatch({ + action: Action.ViewUser, + member: MatrixClientPeg.get().getRoom(singletonRoomViewStore.getRoomId()).getMember(this.resourceId), + }); + }; + get type(): IPillPart["type"] { return Type.UserPill; } From 780f9b6add39c4ca9cb9df80c1c22509fbbffc30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 12 Jul 2021 09:29:41 +0200 Subject: [PATCH 0233/1171] Handle pill deletion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- .../views/rooms/BasicMessageComposer.tsx | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/components/views/rooms/BasicMessageComposer.tsx b/src/components/views/rooms/BasicMessageComposer.tsx index 3258674cf6..d707a25e44 100644 --- a/src/components/views/rooms/BasicMessageComposer.tsx +++ b/src/components/views/rooms/BasicMessageComposer.tsx @@ -507,6 +507,7 @@ export default class BasicMessageEditor extends React.Component handled = true; } else if (event.key === Key.BACKSPACE || event.key === Key.DELETE) { this.formatBarRef.current.hide(); + handled = this.fakeDeletion(event.key === Key.BACKSPACE ? "deleteContentBackward" : "deleteContentForward"); } if (handled) { @@ -515,6 +516,29 @@ export default class BasicMessageEditor extends React.Component } }; + /** + * Because pills have contentEditable="false" there is no event emitted when + * the user tries to delete them. Therefore we need to fake what would + * normally happen + * @param direction in which to delete + * @returns handled + */ + private fakeDeletion(direction: "deleteContentForward" | "deleteContentBackward" ): boolean { + const selection = document.getSelection(); + // Use the default handling for ranges + if (selection.type === "Range") return false; + + this.modifiedFlag = true; + const { caret, text } = getCaretOffsetAndText(this.editorRef.current, selection); + + // Do the deletion itself + if (direction === "deleteContentBackward") caret.offset--; + const newText = text.slice(0, caret.offset) + text.slice(caret.offset + 1); + + this.props.model.update(newText, direction, caret); + return true; + } + private async tabCompleteName(): Promise { try { await new Promise(resolve => this.setState({ showVisualBell: false }, resolve)); From 113b6319b129ab550669ed506b5f4b10e8b7ed60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 12 Jul 2021 09:52:01 +0200 Subject: [PATCH 0234/1171] This looks a bit nicer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/editor/parts.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/editor/parts.ts b/src/editor/parts.ts index 8f662f9367..3d50c39cd0 100644 --- a/src/editor/parts.ts +++ b/src/editor/parts.ts @@ -27,7 +27,7 @@ import AutocompleteWrapperModel, { import * as Avatar from "../Avatar"; import defaultDispatcher from "../dispatcher/dispatcher"; import { Action } from "../dispatcher/actions"; -import singletonRoomViewStore from "../stores/RoomViewStore"; +import RoomViewStore from "../stores/RoomViewStore"; import { MatrixClientPeg } from "../MatrixClientPeg"; interface ISerializedPart { @@ -417,7 +417,7 @@ class UserPillPart extends PillPart { onClick = () => { defaultDispatcher.dispatch({ action: Action.ViewUser, - member: MatrixClientPeg.get().getRoom(singletonRoomViewStore.getRoomId()).getMember(this.resourceId), + member: MatrixClientPeg.get().getRoom(RoomViewStore.getRoomId()).getMember(this.resourceId), }); }; From 4ef8c9fd297a616658152bba6e0406a301cbe948 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 12 Jul 2021 09:52:32 +0200 Subject: [PATCH 0235/1171] Delint 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 ddcb9057ec..f3b580cdca 100644 --- a/src/components/views/voip/CallPreview.tsx +++ b/src/components/views/voip/CallPreview.tsx @@ -240,7 +240,7 @@ export default class CallPreview extends React.Component { this.scheduledUpdate.mark(); }; - private onRoomViewStoreUpdate = (payload) => { + private onRoomViewStoreUpdate = () => { if (RoomViewStore.getRoomId() === this.state.roomId) return; const roomId = RoomViewStore.getRoomId(); From d7811d9db7f1ac6f2d5a75efb49fd3bd38fbdfb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 12 Jul 2021 09:59:31 +0200 Subject: [PATCH 0236/1171] Maybe this shuts it up MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/ActiveRoomObserver.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ActiveRoomObserver.ts b/src/ActiveRoomObserver.ts index 1126dc9496..c7423fab8f 100644 --- a/src/ActiveRoomObserver.ts +++ b/src/ActiveRoomObserver.ts @@ -14,6 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ +import { EventSubscription } from 'fbemitter'; import RoomViewStore from './stores/RoomViewStore'; type Listener = (isActive: boolean) => void; @@ -30,7 +31,7 @@ type Listener = (isActive: boolean) => void; export class ActiveRoomObserver { private listeners: {[key: string]: Listener[]} = {}; private _activeRoomId = RoomViewStore.getRoomId(); - private readonly roomStoreToken: string; + private readonly roomStoreToken: EventSubscription; constructor() { // TODO: We could self-destruct when the last listener goes away, or at least stop listening. From b79f2d06991711a2233e1832e7a37a4d614c7b46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 12 Jul 2021 12:21:59 +0200 Subject: [PATCH 0237/1171] Fix the ugly solution MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/editor/parts.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/editor/parts.ts b/src/editor/parts.ts index 3d50c39cd0..af741c4502 100644 --- a/src/editor/parts.ts +++ b/src/editor/parts.ts @@ -78,7 +78,6 @@ interface IPillCandidatePart extends Omit { type: Type.AtRoomPill | Type.RoomPill | Type.UserPill; resourceId: string; - onClick?(): void; } export type Part = IBasePart | IPillCandidatePart | IPillPart; @@ -310,7 +309,7 @@ abstract class PillPart extends BasePart implements IPillPart { abstract get className(): string; - abstract onClick?(): void; + protected onClick?: () => void; abstract setAvatar(node: HTMLElement): void; } @@ -373,9 +372,6 @@ class RoomPillPart extends PillPart { get className() { return "mx_RoomPill mx_Pill"; } - - // FIXME: We do this to shut up the linter, is there a way to do this properly - onClick = undefined; } class AtRoomPillPart extends RoomPillPart { @@ -414,7 +410,7 @@ class UserPillPart extends PillPart { this._setAvatarVars(node, avatarUrl, initialLetter); } - onClick = () => { + protected onClick = () => { defaultDispatcher.dispatch({ action: Action.ViewUser, member: MatrixClientPeg.get().getRoom(RoomViewStore.getRoomId()).getMember(this.resourceId), From 3921e42e8a753d2f393675b678daa218d403db0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 12 Jul 2021 12:32:30 +0200 Subject: [PATCH 0238/1171] Make diff colors in codeblocks more pleseant MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/themes/dark/css/_dark.scss | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/res/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss index 57cbc7efa9..1f814f08b8 100644 --- a/res/themes/dark/css/_dark.scss +++ b/res/themes/dark/css/_dark.scss @@ -288,3 +288,11 @@ $composer-shadow-color: rgba(0, 0, 0, 0.28); .hljs-tag { color: inherit; // Without this they'd be weirdly blue which doesn't match the theme } + +.hljs-addition { + background: #1a4b59; +} + +.hljs-deletion { + background: #53232a; +} From 48a6a83745a7f799a0647509b4a796ca4cbfb8ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 12 Jul 2021 12:41:58 +0200 Subject: [PATCH 0239/1171] Set cursor for each pill type separately MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/css/views/rooms/_BasicMessageComposer.scss | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/res/css/views/rooms/_BasicMessageComposer.scss b/res/css/views/rooms/_BasicMessageComposer.scss index d87444441a..544a96daba 100644 --- a/res/css/views/rooms/_BasicMessageComposer.scss +++ b/res/css/views/rooms/_BasicMessageComposer.scss @@ -47,7 +47,6 @@ limitations under the License. &.mx_BasicMessageComposer_input_shouldShowPillAvatar { span.mx_UserPill, span.mx_RoomPill { position: relative; - cursor: pointer; // avatar psuedo element &::before { @@ -66,6 +65,14 @@ limitations under the License. font-size: $font-10-4px; } } + + span.mx_UserPill { + cursor: pointer; + } + + span.mx_RoomPill { + cursor: default; + } } &.mx_BasicMessageComposer_input_disabled { From 069c1f466520ddfe424b5b1b58e9054e01b1f7e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 12 Jul 2021 12:52:05 +0200 Subject: [PATCH 0240/1171] Make code a bit cleaner MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/rooms/BasicMessageComposer.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/views/rooms/BasicMessageComposer.tsx b/src/components/views/rooms/BasicMessageComposer.tsx index d707a25e44..81211c57b7 100644 --- a/src/components/views/rooms/BasicMessageComposer.tsx +++ b/src/components/views/rooms/BasicMessageComposer.tsx @@ -507,7 +507,7 @@ export default class BasicMessageEditor extends React.Component handled = true; } else if (event.key === Key.BACKSPACE || event.key === Key.DELETE) { this.formatBarRef.current.hide(); - handled = this.fakeDeletion(event.key === Key.BACKSPACE ? "deleteContentBackward" : "deleteContentForward"); + handled = this.fakeDeletion(event.key === Key.BACKSPACE); } if (handled) { @@ -523,7 +523,7 @@ export default class BasicMessageEditor extends React.Component * @param direction in which to delete * @returns handled */ - private fakeDeletion(direction: "deleteContentForward" | "deleteContentBackward" ): boolean { + private fakeDeletion(backward: boolean): boolean { const selection = document.getSelection(); // Use the default handling for ranges if (selection.type === "Range") return false; @@ -532,10 +532,10 @@ export default class BasicMessageEditor extends React.Component const { caret, text } = getCaretOffsetAndText(this.editorRef.current, selection); // Do the deletion itself - if (direction === "deleteContentBackward") caret.offset--; + if (backward) caret.offset--; const newText = text.slice(0, caret.offset) + text.slice(caret.offset + 1); - this.props.model.update(newText, direction, caret); + this.props.model.update(newText, backward ? "deleteContentBackward" : "deleteContentForward", caret); return true; } From 3515b2ca05e25d43f1d1ff3bb803e319f5b93c63 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 12 Jul 2021 12:51:27 +0100 Subject: [PATCH 0241/1171] Fix edge case behaviour caused by our weird reuse of DOM nodes between owners --- src/editor/parts.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/editor/parts.ts b/src/editor/parts.ts index af741c4502..c1724c09bb 100644 --- a/src/editor/parts.ts +++ b/src/editor/parts.ts @@ -269,6 +269,9 @@ abstract class PillPart extends BasePart implements IPillPart { if (node.className !== this.className) { node.className = this.className; } + if (node.onclick !== this.onClick) { + node.onclick = this.onClick; + } this.setAvatar(node); } From 8139aeb073a6a3b018e10614a4a39fd0a1841623 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 12 Jul 2021 12:51:49 +0100 Subject: [PATCH 0242/1171] skip loading room & finding member, use existing member field --- src/editor/parts.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/editor/parts.ts b/src/editor/parts.ts index c1724c09bb..688116ab90 100644 --- a/src/editor/parts.ts +++ b/src/editor/parts.ts @@ -27,8 +27,6 @@ import AutocompleteWrapperModel, { import * as Avatar from "../Avatar"; import defaultDispatcher from "../dispatcher/dispatcher"; import { Action } from "../dispatcher/actions"; -import RoomViewStore from "../stores/RoomViewStore"; -import { MatrixClientPeg } from "../MatrixClientPeg"; interface ISerializedPart { type: Type.Plain | Type.Newline | Type.Command | Type.PillCandidate; @@ -416,7 +414,7 @@ class UserPillPart extends PillPart { protected onClick = () => { defaultDispatcher.dispatch({ action: Action.ViewUser, - member: MatrixClientPeg.get().getRoom(RoomViewStore.getRoomId()).getMember(this.resourceId), + member: this.member, }); }; From 51f0f5718a8882897d99174a0879e610cc158223 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 12 Jul 2021 13:26:34 +0100 Subject: [PATCH 0243/1171] improve types --- .../views/rooms/BasicMessageComposer.tsx | 10 +- .../views/rooms/EditMessageComposer.tsx | 8 +- .../views/rooms/SendMessageComposer.tsx | 6 +- src/editor/autocomplete.ts | 24 +-- src/editor/caret.ts | 6 +- src/editor/deserialize.ts | 4 +- src/editor/diff.ts | 2 +- src/editor/history.ts | 14 +- src/editor/offset.ts | 5 +- src/editor/operations.ts | 26 +-- src/editor/parts.ts | 162 +++++++++--------- src/editor/position.ts | 14 +- src/editor/range.ts | 20 +-- src/editor/render.ts | 32 ++-- src/editor/serialize.ts | 49 +++--- 15 files changed, 196 insertions(+), 186 deletions(-) diff --git a/src/components/views/rooms/BasicMessageComposer.tsx b/src/components/views/rooms/BasicMessageComposer.tsx index 81211c57b7..bf6a6a27d2 100644 --- a/src/components/views/rooms/BasicMessageComposer.tsx +++ b/src/components/views/rooms/BasicMessageComposer.tsx @@ -32,7 +32,7 @@ import { } from '../../../editor/operations'; import { getCaretOffsetAndText, getRangeForSelection } from '../../../editor/dom'; import Autocomplete, { generateCompletionDomId } from '../rooms/Autocomplete'; -import { getAutoCompleteCreator } from '../../../editor/parts'; +import { getAutoCompleteCreator, Type } from '../../../editor/parts'; import { parseEvent, parsePlainTextMessage } from '../../../editor/deserialize'; import { renderModel } from '../../../editor/render'; import TypingStore from "../../../stores/TypingStore"; @@ -157,7 +157,7 @@ export default class BasicMessageEditor extends React.Component range.expandBackwardsWhile((index, offset) => { const part = model.parts[index]; n -= 1; - return n >= 0 && (part.type === "plain" || part.type === "pill-candidate"); + return n >= 0 && (part.type === Type.Plain || part.type === Type.PillCandidate); }); const emoticonMatch = REGEX_EMOTICON_WHITESPACE.exec(range.text); if (emoticonMatch) { @@ -548,9 +548,9 @@ export default class BasicMessageEditor extends React.Component const range = model.startRange(position); range.expandBackwardsWhile((index, offset, part) => { return part.text[offset] !== " " && part.text[offset] !== "+" && ( - part.type === "plain" || - part.type === "pill-candidate" || - part.type === "command" + part.type === Type.Plain || + part.type === Type.PillCandidate || + part.type === Type.Command ); }); const { partCreator } = model; diff --git a/src/components/views/rooms/EditMessageComposer.tsx b/src/components/views/rooms/EditMessageComposer.tsx index e4b13e2155..b7e067ee93 100644 --- a/src/components/views/rooms/EditMessageComposer.tsx +++ b/src/components/views/rooms/EditMessageComposer.tsx @@ -25,7 +25,7 @@ import { getCaretOffsetAndText } from '../../../editor/dom'; import { htmlSerializeIfNeeded, textSerialize, containsEmote, stripEmoteCommand } from '../../../editor/serialize'; import { findEditableEvent } from '../../../utils/EventUtils'; import { parseEvent } from '../../../editor/deserialize'; -import { CommandPartCreator, Part, PartCreator } from '../../../editor/parts'; +import { CommandPartCreator, Part, PartCreator, Type } from '../../../editor/parts'; import EditorStateTransfer from '../../../utils/EditorStateTransfer'; import BasicMessageComposer from "./BasicMessageComposer"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; @@ -242,12 +242,12 @@ export default class EditMessageComposer extends React.Component const parts = this.model.parts; const firstPart = parts[0]; if (firstPart) { - if (firstPart.type === "command" && firstPart.text.startsWith("/") && !firstPart.text.startsWith("//")) { + if (firstPart.type === Type.Command && firstPart.text.startsWith("/") && !firstPart.text.startsWith("//")) { return true; } if (firstPart.text.startsWith("/") && !firstPart.text.startsWith("//") - && (firstPart.type === "plain" || firstPart.type === "pill-candidate")) { + && (firstPart.type === Type.Plain || firstPart.type === Type.PillCandidate)) { return true; } } @@ -268,7 +268,7 @@ export default class EditMessageComposer extends React.Component private getSlashCommand(): [Command, string, string] { const commandText = this.model.parts.reduce((text, part) => { // use mxid to textify user pills in a command - if (part.type === "user-pill") { + if (part.type === Type.UserPill) { return text + part.resourceId; } return text + part.text; diff --git a/src/components/views/rooms/SendMessageComposer.tsx b/src/components/views/rooms/SendMessageComposer.tsx index 0639c20fef..76e33ce4b7 100644 --- a/src/components/views/rooms/SendMessageComposer.tsx +++ b/src/components/views/rooms/SendMessageComposer.tsx @@ -31,7 +31,7 @@ import { textSerialize, unescapeMessage, } from '../../../editor/serialize'; -import { CommandPartCreator, Part, PartCreator, SerializedPart } from '../../../editor/parts'; +import { CommandPartCreator, Part, PartCreator, SerializedPart, Type } from '../../../editor/parts'; import BasicMessageComposer from "./BasicMessageComposer"; import ReplyThread from "../elements/ReplyThread"; import { findEditableEvent } from '../../../utils/EventUtils'; @@ -240,14 +240,14 @@ export default class SendMessageComposer extends React.Component { const parts = this.model.parts; const firstPart = parts[0]; if (firstPart) { - if (firstPart.type === "command" && firstPart.text.startsWith("/") && !firstPart.text.startsWith("//")) { + if (firstPart.type === Type.Command && firstPart.text.startsWith("/") && !firstPart.text.startsWith("//")) { return true; } // be extra resilient when somehow the AutocompleteWrapperModel or // CommandPartCreator fails to insert a command part, so we don't send // a command as a message if (firstPart.text.startsWith("/") && !firstPart.text.startsWith("//") - && (firstPart.type === "plain" || firstPart.type === "pill-candidate")) { + && (firstPart.type === Type.Plain || firstPart.type === Type.PillCandidate)) { return true; } } diff --git a/src/editor/autocomplete.ts b/src/editor/autocomplete.ts index 518c77fa6c..bf8f457d0c 100644 --- a/src/editor/autocomplete.ts +++ b/src/editor/autocomplete.ts @@ -43,7 +43,7 @@ export default class AutocompleteWrapperModel { ) { } - public onEscape(e: KeyboardEvent) { + public onEscape(e: KeyboardEvent): void { this.getAutocompleterComponent().onEscape(e); this.updateCallback({ replaceParts: [this.partCreator.plain(this.queryPart.text)], @@ -51,27 +51,27 @@ export default class AutocompleteWrapperModel { }); } - public close() { + public close(): void { this.updateCallback({ close: true }); } - public hasSelection() { + public hasSelection(): boolean { return this.getAutocompleterComponent().hasSelection(); } - public hasCompletions() { + public hasCompletions(): boolean { const ac = this.getAutocompleterComponent(); return ac && ac.countCompletions() > 0; } - public onEnter() { + public onEnter(): void { this.updateCallback({ close: true }); } /** * If there is no current autocompletion, start one and move to the first selection. */ - public async startSelection() { + public async startSelection(): Promise { const acComponent = this.getAutocompleterComponent(); if (acComponent.countCompletions() === 0) { // Force completions to show for the text currently entered @@ -81,15 +81,15 @@ export default class AutocompleteWrapperModel { } } - public selectPreviousSelection() { + public selectPreviousSelection(): void { this.getAutocompleterComponent().moveSelection(-1); } - public selectNextSelection() { + public selectNextSelection(): void { this.getAutocompleterComponent().moveSelection(+1); } - public onPartUpdate(part: Part, pos: DocumentPosition) { + public onPartUpdate(part: Part, pos: DocumentPosition): Promise { // cache the typed value and caret here // so we can restore it in onComponentSelectionChange when the value is undefined (meaning it should be the typed text) this.queryPart = part; @@ -97,7 +97,7 @@ export default class AutocompleteWrapperModel { return this.updateQuery(part.text); } - public onComponentSelectionChange(completion: ICompletion) { + public onComponentSelectionChange(completion: ICompletion): void { if (!completion) { this.updateCallback({ replaceParts: [this.queryPart], @@ -109,14 +109,14 @@ export default class AutocompleteWrapperModel { } } - public onComponentConfirm(completion: ICompletion) { + public onComponentConfirm(completion: ICompletion): void { this.updateCallback({ replaceParts: this.partForCompletion(completion), close: true, }); } - private partForCompletion(completion: ICompletion) { + private partForCompletion(completion: ICompletion): Part[] { const { completionId } = completion; const text = completion.completion; switch (completion.type) { diff --git a/src/editor/caret.ts b/src/editor/caret.ts index 67d10ddbb5..2b5035b567 100644 --- a/src/editor/caret.ts +++ b/src/editor/caret.ts @@ -19,7 +19,7 @@ import { needsCaretNodeBefore, needsCaretNodeAfter } from "./render"; import Range from "./range"; import EditorModel from "./model"; import DocumentPosition, { IPosition } from "./position"; -import { Part } from "./parts"; +import { Part, Type } from "./parts"; export type Caret = Range | DocumentPosition; @@ -113,7 +113,7 @@ function findNodeInLineForPart(parts: Part[], partIndex: number) { // to find newline parts for (let i = 0; i <= partIndex; ++i) { const part = parts[i]; - if (part.type === "newline") { + if (part.type === Type.Newline) { lineIndex += 1; nodeIndex = -1; prevPart = null; @@ -128,7 +128,7 @@ function findNodeInLineForPart(parts: Part[], partIndex: number) { // and not an adjacent caret node if (i < partIndex) { const nextPart = parts[i + 1]; - const isLastOfLine = !nextPart || nextPart.type === "newline"; + const isLastOfLine = !nextPart || nextPart.type === Type.Newline; if (needsCaretNodeAfter(part, isLastOfLine)) { nodeIndex += 1; } diff --git a/src/editor/deserialize.ts b/src/editor/deserialize.ts index eb8adfda9d..beef3be5cf 100644 --- a/src/editor/deserialize.ts +++ b/src/editor/deserialize.ts @@ -20,7 +20,7 @@ import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { walkDOMDepthFirst } from "./dom"; import { checkBlockNode } from "../HtmlUtils"; import { getPrimaryPermalinkEntity } from "../utils/permalinks/Permalinks"; -import { PartCreator } from "./parts"; +import { PartCreator, Type } from "./parts"; import SdkConfig from "../SdkConfig"; function parseAtRoomMentions(text: string, partCreator: PartCreator) { @@ -200,7 +200,7 @@ function prefixQuoteLines(isFirstNode, parts, partCreator) { parts.splice(0, 0, partCreator.plain(QUOTE_LINE_PREFIX)); } for (let i = 0; i < parts.length; i += 1) { - if (parts[i].type === "newline") { + if (parts[i].type === Type.Newline) { parts.splice(i + 1, 0, partCreator.plain(QUOTE_LINE_PREFIX)); i += 1; } diff --git a/src/editor/diff.ts b/src/editor/diff.ts index de8efc9c21..5cf94560ce 100644 --- a/src/editor/diff.ts +++ b/src/editor/diff.ts @@ -21,7 +21,7 @@ export interface IDiff { at?: number; } -function firstDiff(a: string, b: string) { +function firstDiff(a: string, b: string): number { const compareLen = Math.min(a.length, b.length); for (let i = 0; i < compareLen; ++i) { if (a[i] !== b[i]) { diff --git a/src/editor/history.ts b/src/editor/history.ts index 350ba6c99a..7764dbf682 100644 --- a/src/editor/history.ts +++ b/src/editor/history.ts @@ -36,7 +36,7 @@ export default class HistoryManager { private addedSinceLastPush = false; private removedSinceLastPush = false; - clear() { + public clear(): void { this.stack = []; this.newlyTypedCharCount = 0; this.currentIndex = -1; @@ -103,7 +103,7 @@ export default class HistoryManager { } // needs to persist parts and caret position - tryPush(model: EditorModel, caret: Caret, inputType: string, diff: IDiff) { + public tryPush(model: EditorModel, caret: Caret, inputType: string, diff: IDiff): boolean { // ignore state restoration echos. // these respect the inputType values of the input event, // but are actually passed in from MessageEditor calling model.reset() @@ -121,22 +121,22 @@ export default class HistoryManager { return shouldPush; } - ensureLastChangesPushed(model: EditorModel) { + public ensureLastChangesPushed(model: EditorModel): void { if (this.changedSinceLastPush) { this.pushState(model, this.lastCaret); } } - canUndo() { + public canUndo(): boolean { return this.currentIndex >= 1 || this.changedSinceLastPush; } - canRedo() { + public canRedo(): boolean { return this.currentIndex < (this.stack.length - 1); } // returns state that should be applied to model - undo(model: EditorModel) { + public undo(model: EditorModel): IHistory { if (this.canUndo()) { this.ensureLastChangesPushed(model); this.currentIndex -= 1; @@ -145,7 +145,7 @@ export default class HistoryManager { } // returns state that should be applied to model - redo() { + public redo(): IHistory { if (this.canRedo()) { this.changedSinceLastPush = false; this.currentIndex += 1; diff --git a/src/editor/offset.ts b/src/editor/offset.ts index 413a22c71b..2e6e0ffe21 100644 --- a/src/editor/offset.ts +++ b/src/editor/offset.ts @@ -15,16 +15,17 @@ limitations under the License. */ import EditorModel from "./model"; +import DocumentPosition from "./position"; export default class DocumentOffset { constructor(public offset: number, public readonly atNodeEnd: boolean) { } - asPosition(model: EditorModel) { + public asPosition(model: EditorModel): DocumentPosition { return model.positionForOffset(this.offset, this.atNodeEnd); } - add(delta: number, atNodeEnd = false) { + public add(delta: number, atNodeEnd = false): DocumentOffset { return new DocumentOffset(this.offset + delta, atNodeEnd); } } diff --git a/src/editor/operations.ts b/src/editor/operations.ts index a738f2d111..2ff09ccce6 100644 --- a/src/editor/operations.ts +++ b/src/editor/operations.ts @@ -15,13 +15,13 @@ limitations under the License. */ import Range from "./range"; -import { Part } from "./parts"; +import { Part, Type } from "./parts"; /** * Some common queries and transformations on the editor model */ -export function replaceRangeAndExpandSelection(range: Range, newParts: Part[]) { +export function replaceRangeAndExpandSelection(range: Range, newParts: Part[]): void { const { model } = range; model.transform(() => { const oldLen = range.length; @@ -32,7 +32,7 @@ export function replaceRangeAndExpandSelection(range: Range, newParts: Part[]) { }); } -export function replaceRangeAndMoveCaret(range: Range, newParts: Part[]) { +export function replaceRangeAndMoveCaret(range: Range, newParts: Part[]): void { const { model } = range; model.transform(() => { const oldLen = range.length; @@ -43,29 +43,29 @@ export function replaceRangeAndMoveCaret(range: Range, newParts: Part[]) { }); } -export function rangeStartsAtBeginningOfLine(range: Range) { +export function rangeStartsAtBeginningOfLine(range: Range): boolean { const { model } = range; const startsWithPartial = range.start.offset !== 0; const isFirstPart = range.start.index === 0; - const previousIsNewline = !isFirstPart && model.parts[range.start.index - 1].type === "newline"; + const previousIsNewline = !isFirstPart && model.parts[range.start.index - 1].type === Type.Newline; return !startsWithPartial && (isFirstPart || previousIsNewline); } -export function rangeEndsAtEndOfLine(range: Range) { +export function rangeEndsAtEndOfLine(range: Range): boolean { const { model } = range; const lastPart = model.parts[range.end.index]; const endsWithPartial = range.end.offset !== lastPart.text.length; const isLastPart = range.end.index === model.parts.length - 1; - const nextIsNewline = !isLastPart && model.parts[range.end.index + 1].type === "newline"; + const nextIsNewline = !isLastPart && model.parts[range.end.index + 1].type === Type.Newline; return !endsWithPartial && (isLastPart || nextIsNewline); } -export function formatRangeAsQuote(range: Range) { +export function formatRangeAsQuote(range: Range): void { const { model, parts } = range; const { partCreator } = model; for (let i = 0; i < parts.length; ++i) { const part = parts[i]; - if (part.type === "newline") { + if (part.type === Type.Newline) { parts.splice(i + 1, 0, partCreator.plain("> ")); } } @@ -81,10 +81,10 @@ export function formatRangeAsQuote(range: Range) { replaceRangeAndExpandSelection(range, parts); } -export function formatRangeAsCode(range: Range) { +export function formatRangeAsCode(range: Range): void { const { model, parts } = range; const { partCreator } = model; - const needsBlock = parts.some(p => p.type === "newline"); + const needsBlock = parts.some(p => p.type === Type.Newline); if (needsBlock) { parts.unshift(partCreator.plain("```"), partCreator.newline()); if (!rangeStartsAtBeginningOfLine(range)) { @@ -105,9 +105,9 @@ export function formatRangeAsCode(range: Range) { // parts helper methods const isBlank = part => !part.text || !/\S/.test(part.text); -const isNL = part => part.type === "newline"; +const isNL = part => part.type === Type.Newline; -export function toggleInlineFormat(range: Range, prefix: string, suffix = prefix) { +export function toggleInlineFormat(range: Range, prefix: string, suffix = prefix): void { const { model, parts } = range; const { partCreator } = model; diff --git a/src/editor/parts.ts b/src/editor/parts.ts index 688116ab90..4e0235bdf7 100644 --- a/src/editor/parts.ts +++ b/src/editor/parts.ts @@ -41,7 +41,7 @@ interface ISerializedPillPart { export type SerializedPart = ISerializedPart | ISerializedPillPart; -enum Type { +export enum Type { Plain = "plain", Newline = "newline", Command = "command", @@ -59,12 +59,12 @@ interface IBasePart { createAutoComplete(updateCallback: UpdateCallback): void; serialize(): SerializedPart; - remove(offset: number, len: number): string; + remove(offset: number, len: number): string | undefined; split(offset: number): IBasePart; validateAndInsert(offset: number, str: string, inputType: string): boolean; - appendUntilRejected(str: string, inputType: string): string; - updateDOMNode(node: Node); - canUpdateDOMNode(node: Node); + appendUntilRejected(str: string, inputType: string): string | undefined; + updateDOMNode(node: Node): void; + canUpdateDOMNode(node: Node): boolean; toDOMNode(): Node; } @@ -87,19 +87,19 @@ abstract class BasePart { this._text = text; } - acceptsInsertion(chr: string, offset: number, inputType: string) { + protected acceptsInsertion(chr: string, offset: number, inputType: string): boolean { return true; } - acceptsRemoval(position: number, chr: string) { + protected acceptsRemoval(position: number, chr: string): boolean { return true; } - merge(part: Part) { + public merge(part: Part): boolean { return false; } - split(offset: number) { + public split(offset: number): IBasePart { const splitText = this.text.substr(offset); this._text = this.text.substr(0, offset); return new PlainPart(splitText); @@ -107,7 +107,7 @@ abstract class BasePart { // removes len chars, or returns the plain text this part should be replaced with // if the part would become invalid if it removed everything. - remove(offset: number, len: number) { + public remove(offset: number, len: number): string | undefined { // validate const strWithRemoval = this.text.substr(0, offset) + this.text.substr(offset + len); for (let i = offset; i < (len + offset); ++i) { @@ -120,7 +120,7 @@ abstract class BasePart { } // append str, returns the remaining string if a character was rejected. - appendUntilRejected(str: string, inputType: string) { + public appendUntilRejected(str: string, inputType: string): string | undefined { const offset = this.text.length; for (let i = 0; i < str.length; ++i) { const chr = str.charAt(i); @@ -134,7 +134,7 @@ abstract class BasePart { // inserts str at offset if all the characters in str were accepted, otherwise don't do anything // return whether the str was accepted or not. - validateAndInsert(offset: number, str: string, inputType: string) { + public validateAndInsert(offset: number, str: string, inputType: string): boolean { for (let i = 0; i < str.length; ++i) { const chr = str.charAt(i); if (!this.acceptsInsertion(chr, offset + i, inputType)) { @@ -147,42 +147,42 @@ abstract class BasePart { return true; } - createAutoComplete(updateCallback: UpdateCallback): void {} + public createAutoComplete(updateCallback: UpdateCallback): void {} - trim(len: number) { + protected trim(len: number): string { const remaining = this._text.substr(len); this._text = this._text.substr(0, len); return remaining; } - get text() { + public get text(): string { return this._text; } - abstract get type(): Type; + public abstract get type(): Type; - get canEdit() { + public get canEdit(): boolean { return true; } - toString() { + public toString(): string { return `${this.type}(${this.text})`; } - serialize(): SerializedPart { + public serialize(): SerializedPart { return { type: this.type as ISerializedPart["type"], text: this.text, }; } - abstract updateDOMNode(node: Node); - abstract canUpdateDOMNode(node: Node); - abstract toDOMNode(): Node; + public abstract updateDOMNode(node: Node): void; + public abstract canUpdateDOMNode(node: Node): boolean; + public abstract toDOMNode(): Node; } abstract class PlainBasePart extends BasePart { - acceptsInsertion(chr: string, offset: number, inputType: string) { + protected acceptsInsertion(chr: string, offset: number, inputType: string): boolean { if (chr === "\n") { return false; } @@ -205,11 +205,11 @@ abstract class PlainBasePart extends BasePart { return true; } - toDOMNode() { + public toDOMNode(): Node { return document.createTextNode(this.text); } - merge(part) { + public merge(part): boolean { if (part.type === this.type) { this._text = this.text + part.text; return true; @@ -217,38 +217,38 @@ abstract class PlainBasePart extends BasePart { return false; } - updateDOMNode(node: Node) { + public updateDOMNode(node: Node): void { if (node.textContent !== this.text) { node.textContent = this.text; } } - canUpdateDOMNode(node: Node) { + public canUpdateDOMNode(node: Node): boolean { return node.nodeType === Node.TEXT_NODE; } } // exported for unit tests, should otherwise only be used through PartCreator export class PlainPart extends PlainBasePart implements IBasePart { - get type(): IBasePart["type"] { + public get type(): IBasePart["type"] { return Type.Plain; } } -abstract class PillPart extends BasePart implements IPillPart { +export abstract class PillPart extends BasePart implements IPillPart { constructor(public resourceId: string, label) { super(label); } - acceptsInsertion(chr: string) { + protected acceptsInsertion(chr: string): boolean { return chr !== " "; } - acceptsRemoval(position: number, chr: string) { + protected acceptsRemoval(position: number, chr: string): boolean { return position !== 0; //if you remove initial # or @, pill should become plain } - toDOMNode() { + public toDOMNode(): Node { const container = document.createElement("span"); container.setAttribute("spellcheck", "false"); container.setAttribute("contentEditable", "false"); @@ -259,7 +259,7 @@ abstract class PillPart extends BasePart implements IPillPart { return container; } - updateDOMNode(node: HTMLElement) { + public updateDOMNode(node: HTMLElement): void { const textNode = node.childNodes[0]; if (textNode.textContent !== this.text) { textNode.textContent = this.text; @@ -273,7 +273,7 @@ abstract class PillPart extends BasePart implements IPillPart { this.setAvatar(node); } - canUpdateDOMNode(node: HTMLElement) { + public canUpdateDOMNode(node: HTMLElement): boolean { return node.nodeType === Node.ELEMENT_NODE && node.nodeName === "SPAN" && node.childNodes.length === 1 && @@ -281,7 +281,7 @@ abstract class PillPart extends BasePart implements IPillPart { } // helper method for subclasses - _setAvatarVars(node: HTMLElement, avatarUrl: string, initialLetter: string) { + protected setAvatarVars(node: HTMLElement, avatarUrl: string, initialLetter: string): void { const avatarBackground = `url('${avatarUrl}')`; const avatarLetter = `'${initialLetter}'`; // check if the value is changing, @@ -294,7 +294,7 @@ abstract class PillPart extends BasePart implements IPillPart { } } - serialize(): ISerializedPillPart { + public serialize(): ISerializedPillPart { return { type: this.type, text: this.text, @@ -302,43 +302,43 @@ abstract class PillPart extends BasePart implements IPillPart { }; } - get canEdit() { + public get canEdit(): boolean { return false; } - abstract get type(): IPillPart["type"]; + public abstract get type(): IPillPart["type"]; - abstract get className(): string; + protected abstract get className(): string; protected onClick?: () => void; - abstract setAvatar(node: HTMLElement): void; + protected abstract setAvatar(node: HTMLElement): void; } class NewlinePart extends BasePart implements IBasePart { - acceptsInsertion(chr: string, offset: number) { + protected acceptsInsertion(chr: string, offset: number): boolean { return offset === 0 && chr === "\n"; } - acceptsRemoval(position: number, chr: string) { + protected acceptsRemoval(position: number, chr: string): boolean { return true; } - toDOMNode() { + public toDOMNode(): Node { return document.createElement("br"); } - merge() { + public merge(): boolean { return false; } - updateDOMNode() {} + public updateDOMNode(): void {} - canUpdateDOMNode(node: HTMLElement) { + public canUpdateDOMNode(node: HTMLElement): boolean { return node.tagName === "BR"; } - get type(): IBasePart["type"] { + public get type(): IBasePart["type"] { return Type.Newline; } @@ -346,7 +346,7 @@ class NewlinePart extends BasePart implements IBasePart { // rather than trying to append to it, which is what we want. // As a newline can also be only one character, it makes sense // as it can only be one character long. This caused #9741. - get canEdit() { + public get canEdit(): boolean { return false; } } @@ -356,21 +356,21 @@ class RoomPillPart extends PillPart { super(resourceId, label); } - setAvatar(node: HTMLElement) { + protected setAvatar(node: HTMLElement): void { let initialLetter = ""; let avatarUrl = Avatar.avatarUrlForRoom(this.room, 16, 16, "crop"); if (!avatarUrl) { initialLetter = Avatar.getInitialLetter(this.room ? this.room.name : this.resourceId); avatarUrl = Avatar.defaultAvatarUrlForString(this.room ? this.room.roomId : this.resourceId); } - this._setAvatarVars(node, avatarUrl, initialLetter); + this.setAvatarVars(node, avatarUrl, initialLetter); } - get type(): IPillPart["type"] { + public get type(): IPillPart["type"] { return Type.RoomPill; } - get className() { + protected get className() { return "mx_RoomPill mx_Pill"; } } @@ -380,11 +380,11 @@ class AtRoomPillPart extends RoomPillPart { super(text, text, room); } - get type(): IPillPart["type"] { + public get type(): IPillPart["type"] { return Type.AtRoomPill; } - serialize(): ISerializedPillPart { + public serialize(): ISerializedPillPart { return { type: this.type, text: this.text, @@ -397,7 +397,7 @@ class UserPillPart extends PillPart { super(userId, displayName); } - setAvatar(node: HTMLElement) { + protected setAvatar(node: HTMLElement): void { if (!this.member) { return; } @@ -408,21 +408,21 @@ class UserPillPart extends PillPart { if (avatarUrl === defaultAvatarUrl) { initialLetter = Avatar.getInitialLetter(name); } - this._setAvatarVars(node, avatarUrl, initialLetter); + this.setAvatarVars(node, avatarUrl, initialLetter); } - protected onClick = () => { + protected onClick = (): void => { defaultDispatcher.dispatch({ action: Action.ViewUser, member: this.member, }); }; - get type(): IPillPart["type"] { + public get type(): IPillPart["type"] { return Type.UserPill; } - get className() { + protected get className() { return "mx_UserPill mx_Pill"; } } @@ -432,11 +432,11 @@ class PillCandidatePart extends PlainBasePart implements IPillCandidatePart { super(text); } - createAutoComplete(updateCallback: UpdateCallback): AutocompleteWrapperModel { + public createAutoComplete(updateCallback: UpdateCallback): AutocompleteWrapperModel { return this.autoCompleteCreator.create(updateCallback); } - acceptsInsertion(chr: string, offset: number, inputType: string) { + protected acceptsInsertion(chr: string, offset: number, inputType: string): boolean { if (offset === 0) { return true; } else { @@ -444,11 +444,11 @@ class PillCandidatePart extends PlainBasePart implements IPillCandidatePart { } } - merge() { + public merge(): boolean { return false; } - acceptsRemoval(position: number, chr: string) { + protected acceptsRemoval(position: number, chr: string): boolean { return true; } @@ -479,17 +479,21 @@ interface IAutocompleteCreator { export class PartCreator { protected readonly autoCompleteCreator: IAutocompleteCreator; - constructor(private room: Room, private client: MatrixClient, autoCompleteCreator: AutoCompleteCreator = null) { + constructor( + private readonly room: Room, + private readonly client: MatrixClient, + autoCompleteCreator: AutoCompleteCreator = null, + ) { // pre-create the creator as an object even without callback so it can already be passed // to PillCandidatePart (e.g. while deserializing) and set later on - this.autoCompleteCreator = { create: autoCompleteCreator && autoCompleteCreator(this) }; + this.autoCompleteCreator = { create: autoCompleteCreator?.(this) }; } - setAutoCompleteCreator(autoCompleteCreator: AutoCompleteCreator) { + public setAutoCompleteCreator(autoCompleteCreator: AutoCompleteCreator): void { this.autoCompleteCreator.create = autoCompleteCreator(this); } - createPartForInput(input: string, partIndex: number, inputType?: string): Part { + public createPartForInput(input: string, partIndex: number, inputType?: string): Part { switch (input[0]) { case "#": case "@": @@ -503,11 +507,11 @@ export class PartCreator { } } - createDefaultPart(text: string) { + public createDefaultPart(text: string): Part { return this.plain(text); } - deserializePart(part: SerializedPart): Part { + public deserializePart(part: SerializedPart): Part { switch (part.type) { case Type.Plain: return this.plain(part.text); @@ -524,19 +528,19 @@ export class PartCreator { } } - plain(text: string) { + public plain(text: string): PlainPart { return new PlainPart(text); } - newline() { + public newline(): NewlinePart { return new NewlinePart("\n"); } - pillCandidate(text: string) { + public pillCandidate(text: string): PillCandidatePart { return new PillCandidatePart(text, this.autoCompleteCreator); } - roomPill(alias: string, roomId?: string) { + public roomPill(alias: string, roomId?: string): RoomPillPart { let room; if (roomId || alias[0] !== "#") { room = this.client.getRoom(roomId || alias); @@ -549,16 +553,20 @@ export class PartCreator { return new RoomPillPart(alias, room ? room.name : alias, room); } - atRoomPill(text: string) { + public atRoomPill(text: string): AtRoomPillPart { return new AtRoomPillPart(text, this.room); } - userPill(displayName: string, userId: string) { + public userPill(displayName: string, userId: string): UserPillPart { const member = this.room.getMember(userId); return new UserPillPart(userId, displayName, member); } - createMentionParts(insertTrailingCharacter: boolean, displayName: string, userId: string) { + public createMentionParts( + insertTrailingCharacter: boolean, + displayName: string, + userId: string, + ): [UserPillPart, PlainPart] { const pill = this.userPill(displayName, userId); const postfix = this.plain(insertTrailingCharacter ? ": " : " "); return [pill, postfix]; @@ -583,7 +591,7 @@ export class CommandPartCreator extends PartCreator { } public deserializePart(part: SerializedPart): Part { - if (part.type === "command") { + if (part.type === Type.Command) { return this.command(part.text); } else { return super.deserializePart(part); diff --git a/src/editor/position.ts b/src/editor/position.ts index 37d2a07b43..50dc283eb3 100644 --- a/src/editor/position.ts +++ b/src/editor/position.ts @@ -30,7 +30,7 @@ export default class DocumentPosition implements IPosition { constructor(public readonly index: number, public readonly offset: number) { } - compare(otherPos: DocumentPosition) { + public compare(otherPos: DocumentPosition): number { if (this.index === otherPos.index) { return this.offset - otherPos.offset; } else { @@ -38,7 +38,7 @@ export default class DocumentPosition implements IPosition { } } - iteratePartsBetween(other: DocumentPosition, model: EditorModel, callback: Callback) { + public iteratePartsBetween(other: DocumentPosition, model: EditorModel, callback: Callback): void { if (this.index === -1 || other.index === -1) { return; } @@ -57,7 +57,7 @@ export default class DocumentPosition implements IPosition { } } - forwardsWhile(model: EditorModel, predicate: Predicate) { + public forwardsWhile(model: EditorModel, predicate: Predicate): DocumentPosition { if (this.index === -1) { return this; } @@ -82,7 +82,7 @@ export default class DocumentPosition implements IPosition { } } - backwardsWhile(model: EditorModel, predicate: Predicate) { + public backwardsWhile(model: EditorModel, predicate: Predicate): DocumentPosition { if (this.index === -1) { return this; } @@ -107,7 +107,7 @@ export default class DocumentPosition implements IPosition { } } - asOffset(model: EditorModel) { + public asOffset(model: EditorModel): DocumentOffset { if (this.index === -1) { return new DocumentOffset(0, true); } @@ -121,7 +121,7 @@ export default class DocumentPosition implements IPosition { return new DocumentOffset(offset, atEnd); } - isAtEnd(model: EditorModel) { + public isAtEnd(model: EditorModel): boolean { if (model.parts.length === 0) { return true; } @@ -130,7 +130,7 @@ export default class DocumentPosition implements IPosition { return this.index === lastPartIdx && this.offset === lastPart.text.length; } - isAtStart() { + public isAtStart(): boolean { return this.index === 0 && this.offset === 0; } } diff --git a/src/editor/range.ts b/src/editor/range.ts index 634805702f..13776177a7 100644 --- a/src/editor/range.ts +++ b/src/editor/range.ts @@ -32,23 +32,23 @@ export default class Range { this._end = bIsLarger ? positionB : positionA; } - moveStart(delta: number) { + public moveStart(delta: number): void { this._start = this._start.forwardsWhile(this.model, () => { delta -= 1; return delta >= 0; }); } - trim() { + public trim(): void { this._start = this._start.forwardsWhile(this.model, whitespacePredicate); this._end = this._end.backwardsWhile(this.model, whitespacePredicate); } - expandBackwardsWhile(predicate: Predicate) { + public expandBackwardsWhile(predicate: Predicate): void { this._start = this._start.backwardsWhile(this.model, predicate); } - get text() { + public get text(): string { let text = ""; this._start.iteratePartsBetween(this._end, this.model, (part, startIdx, endIdx) => { const t = part.text.substring(startIdx, endIdx); @@ -63,7 +63,7 @@ export default class Range { * @param {Part[]} parts the parts to replace the range with * @return {Number} the net amount of characters added, can be negative. */ - replace(parts: Part[]) { + public replace(parts: Part[]): number { const newLength = parts.reduce((sum, part) => sum + part.text.length, 0); let oldLength = 0; this._start.iteratePartsBetween(this._end, this.model, (part, startIdx, endIdx) => { @@ -77,8 +77,8 @@ export default class Range { * Returns a copy of the (partial) parts within the range. * For partial parts, only the text is adjusted to the part that intersects with the range. */ - get parts() { - const parts = []; + public get parts(): Part[] { + const parts: Part[] = []; this._start.iteratePartsBetween(this._end, this.model, (part, startIdx, endIdx) => { const serializedPart = part.serialize(); serializedPart.text = part.text.substring(startIdx, endIdx); @@ -88,7 +88,7 @@ export default class Range { return parts; } - get length() { + public get length(): number { let len = 0; this._start.iteratePartsBetween(this._end, this.model, (part, startIdx, endIdx) => { len += endIdx - startIdx; @@ -96,11 +96,11 @@ export default class Range { return len; } - get start() { + public get start(): DocumentPosition { return this._start; } - get end() { + public get end(): DocumentPosition { return this._end; } } diff --git a/src/editor/render.ts b/src/editor/render.ts index 0e0b7d2145..d9997de855 100644 --- a/src/editor/render.ts +++ b/src/editor/render.ts @@ -15,19 +15,19 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { Part } from "./parts"; +import { Part, Type } from "./parts"; import EditorModel from "./model"; -export function needsCaretNodeBefore(part: Part, prevPart: Part) { - const isFirst = !prevPart || prevPart.type === "newline"; +export function needsCaretNodeBefore(part: Part, prevPart: Part): boolean { + const isFirst = !prevPart || prevPart.type === Type.Newline; return !part.canEdit && (isFirst || !prevPart.canEdit); } -export function needsCaretNodeAfter(part: Part, isLastOfLine: boolean) { +export function needsCaretNodeAfter(part: Part, isLastOfLine: boolean): boolean { return !part.canEdit && isLastOfLine; } -function insertAfter(node: HTMLElement, nodeToInsert: HTMLElement) { +function insertAfter(node: HTMLElement, nodeToInsert: HTMLElement): void { const next = node.nextSibling; if (next) { node.parentElement.insertBefore(nodeToInsert, next); @@ -44,25 +44,25 @@ export const CARET_NODE_CHAR = "\ufeff"; // a caret node is a node that allows the caret to be placed // where otherwise it wouldn't be possible // (e.g. next to a pill span without adjacent text node) -function createCaretNode() { +function createCaretNode(): HTMLElement { const span = document.createElement("span"); span.className = "caretNode"; span.appendChild(document.createTextNode(CARET_NODE_CHAR)); return span; } -function updateCaretNode(node: HTMLElement) { +function updateCaretNode(node: HTMLElement): void { // ensure the caret node contains only a zero-width space if (node.textContent !== CARET_NODE_CHAR) { node.textContent = CARET_NODE_CHAR; } } -export function isCaretNode(node: HTMLElement) { +export function isCaretNode(node: HTMLElement): boolean { return node && node.tagName === "SPAN" && node.className === "caretNode"; } -function removeNextSiblings(node: ChildNode) { +function removeNextSiblings(node: ChildNode): void { if (!node) { return; } @@ -74,7 +74,7 @@ function removeNextSiblings(node: ChildNode) { } } -function removeChildren(parent: HTMLElement) { +function removeChildren(parent: HTMLElement): void { const firstChild = parent.firstChild; if (firstChild) { removeNextSiblings(firstChild); @@ -82,7 +82,7 @@ function removeChildren(parent: HTMLElement) { } } -function reconcileLine(lineContainer: ChildNode, parts: Part[]) { +function reconcileLine(lineContainer: ChildNode, parts: Part[]): void { let currentNode; let prevPart; const lastPart = parts[parts.length - 1]; @@ -131,13 +131,13 @@ function reconcileLine(lineContainer: ChildNode, parts: Part[]) { removeNextSiblings(currentNode); } -function reconcileEmptyLine(lineContainer) { +function reconcileEmptyLine(lineContainer: HTMLElement): void { // empty div needs to have a BR in it to give it height let foundBR = false; let partNode = lineContainer.firstChild; while (partNode) { const nextNode = partNode.nextSibling; - if (!foundBR && partNode.tagName === "BR") { + if (!foundBR && (partNode as HTMLElement).tagName === "BR") { foundBR = true; } else { partNode.remove(); @@ -149,9 +149,9 @@ function reconcileEmptyLine(lineContainer) { } } -export function renderModel(editor: HTMLDivElement, model: EditorModel) { +export function renderModel(editor: HTMLDivElement, model: EditorModel): void { const lines = model.parts.reduce((linesArr, part) => { - if (part.type === "newline") { + if (part.type === Type.Newline) { linesArr.push([]); } else { const lastLine = linesArr[linesArr.length - 1]; @@ -175,7 +175,7 @@ export function renderModel(editor: HTMLDivElement, model: EditorModel) { if (parts.length) { reconcileLine(lineContainer, parts); } else { - reconcileEmptyLine(lineContainer); + reconcileEmptyLine(lineContainer as HTMLElement); } }); if (lines.length) { diff --git a/src/editor/serialize.ts b/src/editor/serialize.ts index f68173ae29..38a73cc945 100644 --- a/src/editor/serialize.ts +++ b/src/editor/serialize.ts @@ -22,30 +22,31 @@ import { AllHtmlEntities } from 'html-entities'; import SettingsStore from '../settings/SettingsStore'; import SdkConfig from '../SdkConfig'; import cheerio from 'cheerio'; +import { Type } from './parts'; -export function mdSerialize(model: EditorModel) { +export function mdSerialize(model: EditorModel): string { return model.parts.reduce((html, part) => { switch (part.type) { - case "newline": + case Type.Newline: return html + "\n"; - case "plain": - case "command": - case "pill-candidate": - case "at-room-pill": + case Type.Plain: + case Type.Command: + case Type.PillCandidate: + case Type.AtRoomPill: return html + part.text; - case "room-pill": + case Type.RoomPill: // Here we use the resourceId for compatibility with non-rich text clients // See https://github.com/vector-im/element-web/issues/16660 return html + `[${part.resourceId.replace(/[[\\\]]/g, c => "\\" + c)}](${makeGenericPermalink(part.resourceId)})`; - case "user-pill": + case Type.UserPill: return html + `[${part.text.replace(/[[\\\]]/g, c => "\\" + c)}](${makeGenericPermalink(part.resourceId)})`; } }, ""); } -export function htmlSerializeIfNeeded(model: EditorModel, { forceHTML = false } = {}) { +export function htmlSerializeIfNeeded(model: EditorModel, { forceHTML = false } = {}): string { let md = mdSerialize(model); // copy of raw input to remove unwanted math later const orig = md; @@ -156,31 +157,31 @@ export function htmlSerializeIfNeeded(model: EditorModel, { forceHTML = false } } } -export function textSerialize(model: EditorModel) { +export function textSerialize(model: EditorModel): string { return model.parts.reduce((text, part) => { switch (part.type) { - case "newline": + case Type.Newline: return text + "\n"; - case "plain": - case "command": - case "pill-candidate": - case "at-room-pill": + case Type.Plain: + case Type.Command: + case Type.PillCandidate: + case Type.AtRoomPill: return text + part.text; - case "room-pill": + case Type.RoomPill: // Here we use the resourceId for compatibility with non-rich text clients // See https://github.com/vector-im/element-web/issues/16660 return text + `${part.resourceId}`; - case "user-pill": + case Type.UserPill: return text + `${part.text}`; } }, ""); } -export function containsEmote(model: EditorModel) { +export function containsEmote(model: EditorModel): boolean { return startsWith(model, "/me ", false); } -export function startsWith(model: EditorModel, prefix: string, caseSensitive = true) { +export function startsWith(model: EditorModel, prefix: string, caseSensitive = true): boolean { const firstPart = model.parts[0]; // part type will be "plain" while editing, // and "command" while composing a message. @@ -190,26 +191,26 @@ export function startsWith(model: EditorModel, prefix: string, caseSensitive = t text = text.toLowerCase(); } - return firstPart && (firstPart.type === "plain" || firstPart.type === "command") && text.startsWith(prefix); + return firstPart && (firstPart.type === Type.Plain || firstPart.type === Type.Command) && text.startsWith(prefix); } -export function stripEmoteCommand(model: EditorModel) { +export function stripEmoteCommand(model: EditorModel): EditorModel { // trim "/me " return stripPrefix(model, "/me "); } -export function stripPrefix(model: EditorModel, prefix: string) { +export function stripPrefix(model: EditorModel, prefix: string): EditorModel { model = model.clone(); model.removeText({ index: 0, offset: 0 }, prefix.length); return model; } -export function unescapeMessage(model: EditorModel) { +export function unescapeMessage(model: EditorModel): EditorModel { const { parts } = model; if (parts.length) { const firstPart = parts[0]; // only unescape \/ to / at start of editor - if (firstPart.type === "plain" && firstPart.text.startsWith("\\/")) { + if (firstPart.type === Type.Plain && firstPart.text.startsWith("\\/")) { model = model.clone(); model.removeText({ index: 0, offset: 0 }, 1); } From 4ddcb9a484fe0a0b4b4e2afd39640bb3742a76db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 12 Jul 2021 16:59:16 +0200 Subject: [PATCH 0244/1171] Make diffs look a bit better MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/css/views/rooms/_EventTile.scss | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index 55f73c0315..ebd5002843 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -480,6 +480,11 @@ $hover-select-border: 4px; background-color: $header-panel-bg-color; } + pre code > * { + display: inline-block; + width: 100%; + } + pre { // have to use overlay rather than auto otherwise Linux and Windows // Chrome gets very confused about vertical spacing: From 59e48ee0ba5e7fb7461dae1a032ccd07267a8ac4 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 1 Jul 2021 21:42:56 -0600 Subject: [PATCH 0245/1171] Convert NotificationUserSettingsTab to TS --- ...serSettingsTab.js => NotificationUserSettingsTab.tsx} | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) rename src/components/views/settings/tabs/user/{NotificationUserSettingsTab.js => NotificationUserSettingsTab.tsx} (86%) diff --git a/src/components/views/settings/tabs/user/NotificationUserSettingsTab.js b/src/components/views/settings/tabs/user/NotificationUserSettingsTab.tsx similarity index 86% rename from src/components/views/settings/tabs/user/NotificationUserSettingsTab.js rename to src/components/views/settings/tabs/user/NotificationUserSettingsTab.tsx index 0aabdd24e2..a0f4e330bb 100644 --- a/src/components/views/settings/tabs/user/NotificationUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/NotificationUserSettingsTab.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. @@ -16,17 +16,12 @@ limitations under the License. import React from 'react'; import { _t } from "../../../../../languageHandler"; -import * as sdk from "../../../../../index"; import { replaceableComponent } from "../../../../../utils/replaceableComponent"; +import Notifications from "../../Notifications"; @replaceableComponent("views.settings.tabs.user.NotificationUserSettingsTab") export default class NotificationUserSettingsTab extends React.Component { - constructor() { - super(); - } - render() { - const Notifications = sdk.getComponent("views.settings.Notifications"); return (
{_t("Notifications")}
From 436563be7b90a7a021d8e027654bb39095829ae4 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 1 Jul 2021 21:43:52 -0600 Subject: [PATCH 0246/1171] Change label on notification dropdown for a room by request of design, to reduce mental load --- src/components/views/rooms/RoomTile.tsx | 2 +- src/i18n/strings/en_EN.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/rooms/RoomTile.tsx b/src/components/views/rooms/RoomTile.tsx index 9be0274dd5..580ea01073 100644 --- a/src/components/views/rooms/RoomTile.tsx +++ b/src/components/views/rooms/RoomTile.tsx @@ -408,7 +408,7 @@ export default class RoomTile extends React.PureComponent { > Date: Thu, 1 Jul 2021 21:49:36 -0600 Subject: [PATCH 0247/1171] Convert Spinner to TS --- src/components/views/elements/Spinner.js | 39 -------------------- src/components/views/elements/Spinner.tsx | 45 +++++++++++++++++++++++ 2 files changed, 45 insertions(+), 39 deletions(-) delete mode 100644 src/components/views/elements/Spinner.js create mode 100644 src/components/views/elements/Spinner.tsx diff --git a/src/components/views/elements/Spinner.js b/src/components/views/elements/Spinner.js deleted file mode 100644 index 75f85d0441..0000000000 --- a/src/components/views/elements/Spinner.js +++ /dev/null @@ -1,39 +0,0 @@ -/* -Copyright 2015, 2016 OpenMarket Ltd -Copyright 2019 The Matrix.org Foundation C.I.C. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import React from "react"; -import PropTypes from "prop-types"; -import { _t } from "../../../languageHandler"; - -const Spinner = ({ w = 32, h = 32, message }) => ( -
- { message &&
{ message }
 
} -
-
-); - -Spinner.propTypes = { - w: PropTypes.number, - h: PropTypes.number, - message: PropTypes.node, -}; - -export default Spinner; diff --git a/src/components/views/elements/Spinner.tsx b/src/components/views/elements/Spinner.tsx new file mode 100644 index 0000000000..93c8f9e5d4 --- /dev/null +++ b/src/components/views/elements/Spinner.tsx @@ -0,0 +1,45 @@ +/* +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. +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 { _t } from "../../../languageHandler"; + +interface IProps { + w?: number; + h?: number; + message?: string; +} + +export default class Spinner extends React.PureComponent { + public static defaultProps: Partial = { + w: 32, + h: 32, + }; + + public render() { + const { w, h, message } = this.props; + return ( +
+ { message &&
{ message }
 
} +
+
+ ); + } +} From 9556b610415d60e2a95fc69a7b98206a3dbf6292 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 1 Jul 2021 21:58:03 -0600 Subject: [PATCH 0248/1171] Crude conversion of Notifications.js to TS + cut out legacy code This is to make the file clearer during development and serves no practical purpose --- .../{Notifications.js => Notifications.tsx} | 78 +++---------------- 1 file changed, 9 insertions(+), 69 deletions(-) rename src/components/views/settings/{Notifications.js => Notifications.tsx} (92%) diff --git a/src/components/views/settings/Notifications.js b/src/components/views/settings/Notifications.tsx similarity index 92% rename from src/components/views/settings/Notifications.js rename to src/components/views/settings/Notifications.tsx index c263ff50c8..9f1929a35f 100644 --- a/src/components/views/settings/Notifications.js +++ b/src/components/views/settings/Notifications.tsx @@ -22,7 +22,6 @@ import { MatrixClientPeg } from '../../../MatrixClientPeg'; import SettingsStore from '../../../settings/SettingsStore'; import Modal from '../../../Modal'; import { - NotificationUtils, VectorPushRulesDefinitions, PushRuleVectorState, ContentRules, @@ -40,31 +39,6 @@ import { replaceableComponent } from "../../../utils/replaceableComponent"; // TODO: this component also does a lot of direct poking into this.state, which // is VERY NAUGHTY. -/** - * Rules that Vector used to set in order to override the actions of default rules. - * These are used to port peoples existing overrides to match the current API. - * These can be removed and forgotten once everyone has moved to the new client. - */ -const LEGACY_RULES = { - "im.vector.rule.contains_display_name": ".m.rule.contains_display_name", - "im.vector.rule.room_one_to_one": ".m.rule.room_one_to_one", - "im.vector.rule.room_message": ".m.rule.message", - "im.vector.rule.invite_for_me": ".m.rule.invite_for_me", - "im.vector.rule.call": ".m.rule.call", - "im.vector.rule.notices": ".m.rule.suppress_notices", -}; - -function portLegacyActions(actions) { - const decoded = NotificationUtils.decodeActions(actions); - if (decoded !== null) { - return NotificationUtils.encodeActions(decoded); - } else { - // We don't recognise one of the actions here, so we don't try to - // canonicalise them. - return actions; - } -} - @replaceableComponent("views.settings.Notifications") export default class Notifications extends React.Component { static phases = { @@ -84,6 +58,7 @@ export default class Notifications extends React.Component { externalPushRules: [], // Push rules (except content rule) that have been defined outside Vector UI externalContentRules: [], // Keyword push rules that have been defined outside Vector UI threepids: [], // used for email notifications + pushers: undefined, }; componentDidMount() { @@ -199,7 +174,7 @@ export default class Notifications extends React.Component { onKeywordsClicked = (event) => { // Compute the keywords list to display - let keywords = []; + let keywords: any[]|string = []; for (const i in this.state.vectorContentRules.rules) { const rule = this.state.vectorContentRules.rules[i]; keywords.push(rule.pattern); @@ -448,48 +423,9 @@ export default class Notifications extends React.Component { ); } - // Check if any legacy im.vector rules need to be ported to the new API - // for overriding the actions of default rules. - _portRulesToNewAPI(rulesets) { - const needsUpdate = []; - const cli = MatrixClientPeg.get(); - - for (const kind in rulesets.global) { - const ruleset = rulesets.global[kind]; - for (let i = 0; i < ruleset.length; ++i) { - const rule = ruleset[i]; - if (rule.rule_id in LEGACY_RULES) { - console.log("Porting legacy rule", rule); - needsUpdate.push( function(kind, rule) { - return cli.setPushRuleActions( - 'global', kind, LEGACY_RULES[rule.rule_id], portLegacyActions(rule.actions), - ).then(() => - cli.deletePushRule('global', kind, rule.rule_id), - ).catch( (e) => { - console.warn(`Error when porting legacy rule: ${e}`); - }); - }(kind, rule)); - } - } - } - - if (needsUpdate.length > 0) { - // If some of the rules need to be ported then wait for the porting - // to happen and then fetch the rules again. - return Promise.all(needsUpdate).then(() => - cli.getPushRules(), - ); - } else { - // Otherwise return the rules that we already have. - return rulesets; - } - } - _refreshFromServer = () => { const self = this; - const pushRulesPromise = MatrixClientPeg.get().getPushRules().then( - self._portRulesToNewAPI, - ).then(function(rulesets) { + const pushRulesPromise = MatrixClientPeg.get().getPushRules().then(function(rulesets) { /// XXX seriously? wtf is this? MatrixClientPeg.get().pushRules = rulesets; @@ -803,7 +739,7 @@ export default class Notifications extends React.Component { } // Show keywords not displayed by the vector UI as a single external push rule - let externalKeywords = []; + let externalKeywords: any[]|string = []; for (const i in this.state.externalContentRules) { const rule = this.state.externalContentRules[i]; externalKeywords.push(rule.pattern); @@ -890,9 +826,13 @@ export default class Notifications extends React.Component { - + {/* @ts-ignore*/} + + {/* @ts-ignore*/} + {/* @ts-ignore*/} From 5b9fca3b91964d294e3d0f69bd5a93ae75dc3809 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Sun, 11 Jul 2021 20:03:07 -0600 Subject: [PATCH 0249/1171] Migrate to js-sdk types for push rules --- src/notifications/ContentRules.ts | 19 ++-- src/notifications/NotificationUtils.ts | 21 ++--- src/notifications/PushRuleVectorState.ts | 5 +- src/notifications/types.ts | 114 ----------------------- 4 files changed, 21 insertions(+), 138 deletions(-) delete mode 100644 src/notifications/types.ts diff --git a/src/notifications/ContentRules.ts b/src/notifications/ContentRules.ts index 5f1281e58c..fe27bfd67b 100644 --- a/src/notifications/ContentRules.ts +++ b/src/notifications/ContentRules.ts @@ -1,6 +1,5 @@ /* -Copyright 2016 OpenMarket 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. @@ -16,12 +15,12 @@ limitations under the License. */ import { PushRuleVectorState, State } from "./PushRuleVectorState"; -import { IExtendedPushRule, IRuleSets } from "./types"; +import { IAnnotatedPushRule, IPushRules, PushRuleKind } from "matrix-js-sdk/src/@types/PushRules"; export interface IContentRules { vectorState: State; - rules: IExtendedPushRule[]; - externalRules: IExtendedPushRule[]; + rules: IAnnotatedPushRule[]; + externalRules: IAnnotatedPushRule[]; } export const SCOPE = "global"; @@ -39,9 +38,9 @@ export class ContentRules { * externalRules: a list of other keyword rules, with states other than * vectorState */ - static parseContentRules(rulesets: IRuleSets): IContentRules { + public static parseContentRules(rulesets: IPushRules): IContentRules { // first categorise the keyword rules in terms of their actions - const contentRules = this._categoriseContentRules(rulesets); + const contentRules = ContentRules.categoriseContentRules(rulesets); // Decide which content rules to display in Vector UI. // Vector displays a single global rule for a list of keywords @@ -95,8 +94,8 @@ export class ContentRules { } } - static _categoriseContentRules(rulesets: IRuleSets) { - const contentRules: Record<"on"|"on_but_disabled"|"loud"|"loud_but_disabled"|"other", IExtendedPushRule[]> = { + private static categoriseContentRules(rulesets: IPushRules) { + const contentRules: Record<"on"|"on_but_disabled"|"loud"|"loud_but_disabled"|"other", IAnnotatedPushRule[]> = { on: [], on_but_disabled: [], loud: [], @@ -109,7 +108,7 @@ export class ContentRules { const r = rulesets.global[kind][i]; // check it's not a default rule - if (r.rule_id[0] === '.' || kind !== "content") { + if (r.rule_id[0] === '.' || kind !== PushRuleKind.ContentSpecific) { continue; } diff --git a/src/notifications/NotificationUtils.ts b/src/notifications/NotificationUtils.ts index 1d5356e16b..fa7aa1186d 100644 --- a/src/notifications/NotificationUtils.ts +++ b/src/notifications/NotificationUtils.ts @@ -1,6 +1,5 @@ /* -Copyright 2016 OpenMarket 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. @@ -15,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { Action, Actions } from "./types"; +import { PushRuleAction, PushRuleActionName, TweakHighlight, TweakSound } from "matrix-js-sdk/src/@types/PushRules"; interface IEncodedActions { notify: boolean; @@ -35,18 +34,18 @@ export class NotificationUtils { const sound = action.sound; const highlight = action.highlight; if (notify) { - const actions: Action[] = [Actions.Notify]; + const actions: PushRuleAction[] = [PushRuleActionName.Notify]; if (sound) { - actions.push({ "set_tweak": "sound", "value": sound }); + actions.push({ "set_tweak": "sound", "value": sound } as TweakSound); } if (highlight) { - actions.push({ "set_tweak": "highlight" }); + actions.push({ "set_tweak": "highlight" } as TweakHighlight); } else { - actions.push({ "set_tweak": "highlight", "value": false }); + actions.push({ "set_tweak": "highlight", "value": false } as TweakHighlight); } return actions; } else { - return [Actions.DontNotify]; + return [PushRuleActionName.DontNotify]; } } @@ -56,16 +55,16 @@ export class NotificationUtils { // "highlight: true/false, // } // If the actions couldn't be decoded then returns null. - static decodeActions(actions: Action[]): IEncodedActions { + static decodeActions(actions: PushRuleAction[]): IEncodedActions { let notify = false; let sound = null; let highlight = false; for (let i = 0; i < actions.length; ++i) { const action = actions[i]; - if (action === Actions.Notify) { + if (action === PushRuleActionName.Notify) { notify = true; - } else if (action === Actions.DontNotify) { + } else if (action === PushRuleActionName.DontNotify) { notify = false; } else if (typeof action === "object") { if (action.set_tweak === "sound") { diff --git a/src/notifications/PushRuleVectorState.ts b/src/notifications/PushRuleVectorState.ts index 78c7e4b43b..c0855af0b9 100644 --- a/src/notifications/PushRuleVectorState.ts +++ b/src/notifications/PushRuleVectorState.ts @@ -1,6 +1,5 @@ /* -Copyright 2016 OpenMarket 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,7 +16,7 @@ limitations under the License. import { StandardActions } from "./StandardActions"; import { NotificationUtils } from "./NotificationUtils"; -import { IPushRule } from "./types"; +import { IPushRule } from "matrix-js-sdk/src/@types/PushRules"; export enum State { /** The push rule is disabled */ diff --git a/src/notifications/types.ts b/src/notifications/types.ts deleted file mode 100644 index ea46552947..0000000000 --- a/src/notifications/types.ts +++ /dev/null @@ -1,114 +0,0 @@ -/* -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. -*/ - -export enum NotificationSetting { - AllMessages = "all_messages", // .m.rule.message = notify - DirectMessagesMentionsKeywords = "dm_mentions_keywords", // .m.rule.message = mark_unread. This is the new default. - MentionsKeywordsOnly = "mentions_keywords", // .m.rule.message = mark_unread; .m.rule.room_one_to_one = mark_unread - Never = "never", // .m.rule.master = enabled (dont_notify) -} - -export interface ISoundTweak { - // eslint-disable-next-line camelcase - set_tweak: "sound"; - value: string; -} -export interface IHighlightTweak { - // eslint-disable-next-line camelcase - set_tweak: "highlight"; - value?: boolean; -} - -export type Tweak = ISoundTweak | IHighlightTweak; - -export enum Actions { - Notify = "notify", - DontNotify = "dont_notify", // no-op - Coalesce = "coalesce", // unused - MarkUnread = "mark_unread", // new -} - -export type Action = Actions | Tweak; - -// Push rule kinds in descending priority order -export enum Kind { - Override = "override", - ContentSpecific = "content", - RoomSpecific = "room", - SenderSpecific = "sender", - Underride = "underride", -} - -export interface IEventMatchCondition { - kind: "event_match"; - key: string; - pattern: string; -} - -export interface IContainsDisplayNameCondition { - kind: "contains_display_name"; -} - -export interface IRoomMemberCountCondition { - kind: "room_member_count"; - is: string; -} - -export interface ISenderNotificationPermissionCondition { - kind: "sender_notification_permission"; - key: string; -} - -export type Condition = - IEventMatchCondition | - IContainsDisplayNameCondition | - IRoomMemberCountCondition | - ISenderNotificationPermissionCondition; - -export enum RuleIds { - MasterRule = ".m.rule.master", // The master rule (all notifications disabling) - MessageRule = ".m.rule.message", - EncryptedMessageRule = ".m.rule.encrypted", - RoomOneToOneRule = ".m.rule.room_one_to_one", - EncryptedRoomOneToOneRule = ".m.rule.room_one_to_one", -} - -export interface IPushRule { - enabled: boolean; - // eslint-disable-next-line camelcase - rule_id: RuleIds | string; - actions: Action[]; - default: boolean; - conditions?: Condition[]; // only applicable to `underride` and `override` rules - pattern?: string; // only applicable to `content` rules -} - -// push rule extended with kind, used by ContentRules and js-sdk's pushprocessor -export interface IExtendedPushRule extends IPushRule { - kind: Kind; -} - -export interface IPushRuleSet { - override: IPushRule[]; - content: IPushRule[]; - room: IPushRule[]; - sender: IPushRule[]; - underride: IPushRule[]; -} - -export interface IRuleSets { - global: IPushRuleSet; -} From 0e749e32ac3824c885fe529fa8294de09de83879 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Sun, 11 Jul 2021 20:53:12 -0600 Subject: [PATCH 0250/1171] Clarify that vectorState is a VectorState --- src/notifications/ContentRules.ts | 18 +++++++++--------- src/notifications/PushRuleVectorState.ts | 22 +++++++++++----------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/src/notifications/ContentRules.ts b/src/notifications/ContentRules.ts index fe27bfd67b..2b45065568 100644 --- a/src/notifications/ContentRules.ts +++ b/src/notifications/ContentRules.ts @@ -14,11 +14,11 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { PushRuleVectorState, State } from "./PushRuleVectorState"; +import { PushRuleVectorState, VectorState } from "./PushRuleVectorState"; import { IAnnotatedPushRule, IPushRules, PushRuleKind } from "matrix-js-sdk/src/@types/PushRules"; export interface IContentRules { - vectorState: State; + vectorState: VectorState; rules: IAnnotatedPushRule[]; externalRules: IAnnotatedPushRule[]; } @@ -58,7 +58,7 @@ export class ContentRules { if (contentRules.loud.length) { return { - vectorState: State.Loud, + vectorState: VectorState.Loud, rules: contentRules.loud, externalRules: [ ...contentRules.loud_but_disabled, @@ -69,25 +69,25 @@ export class ContentRules { }; } else if (contentRules.loud_but_disabled.length) { return { - vectorState: State.Off, + vectorState: VectorState.Off, rules: contentRules.loud_but_disabled, externalRules: [...contentRules.on, ...contentRules.on_but_disabled, ...contentRules.other], }; } else if (contentRules.on.length) { return { - vectorState: State.On, + vectorState: VectorState.On, rules: contentRules.on, externalRules: [...contentRules.on_but_disabled, ...contentRules.other], }; } else if (contentRules.on_but_disabled.length) { return { - vectorState: State.Off, + vectorState: VectorState.Off, rules: contentRules.on_but_disabled, externalRules: contentRules.other, }; } else { return { - vectorState: State.On, + vectorState: VectorState.On, rules: [], externalRules: contentRules.other, }; @@ -116,14 +116,14 @@ export class ContentRules { r.kind = kind; switch (PushRuleVectorState.contentRuleVectorStateKind(r)) { - case State.On: + case VectorState.On: if (r.enabled) { contentRules.on.push(r); } else { contentRules.on_but_disabled.push(r); } break; - case State.Loud: + case VectorState.Loud: if (r.enabled) { contentRules.loud.push(r); } else { diff --git a/src/notifications/PushRuleVectorState.ts b/src/notifications/PushRuleVectorState.ts index c0855af0b9..34f7dcf786 100644 --- a/src/notifications/PushRuleVectorState.ts +++ b/src/notifications/PushRuleVectorState.ts @@ -18,7 +18,7 @@ import { StandardActions } from "./StandardActions"; import { NotificationUtils } from "./NotificationUtils"; import { IPushRule } from "matrix-js-sdk/src/@types/PushRules"; -export enum State { +export enum VectorState { /** The push rule is disabled */ Off = "off", /** The user will receive push notification for this rule */ @@ -30,26 +30,26 @@ export enum State { export class PushRuleVectorState { // Backwards compatibility (things should probably be using the enum above instead) - static OFF = State.Off; - static ON = State.On; - static LOUD = State.Loud; + static OFF = VectorState.Off; + static ON = VectorState.On; + static LOUD = VectorState.Loud; /** * Enum for state of a push rule as defined by the Vector UI. * @readonly * @enum {string} */ - static states = State; + static states = VectorState; /** * Convert a PushRuleVectorState to a list of actions * * @return [object] list of push-rule actions */ - static actionsFor(pushRuleVectorState: State) { - if (pushRuleVectorState === State.On) { + static actionsFor(pushRuleVectorState: VectorState) { + if (pushRuleVectorState === VectorState.On) { return StandardActions.ACTION_NOTIFY; - } else if (pushRuleVectorState === State.Loud) { + } else if (pushRuleVectorState === VectorState.Loud) { return StandardActions.ACTION_HIGHLIGHT_DEFAULT_SOUND; } } @@ -61,7 +61,7 @@ export class PushRuleVectorState { * category or in PushRuleVectorState.LOUD, regardless of its enabled * state. Returns null if it does not match these categories. */ - static contentRuleVectorStateKind(rule: IPushRule): State { + static contentRuleVectorStateKind(rule: IPushRule): VectorState { const decoded = NotificationUtils.decodeActions(rule.actions); if (!decoded) { @@ -79,10 +79,10 @@ export class PushRuleVectorState { let stateKind = null; switch (tweaks) { case 0: - stateKind = State.On; + stateKind = VectorState.On; break; case 2: - stateKind = State.Loud; + stateKind = VectorState.Loud; break; } return stateKind; From fd5a36fd0cf6131b25008d02fa0e6769b3e3633d Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 12 Jul 2021 21:48:20 -0600 Subject: [PATCH 0251/1171] Fix more types around notifications --- src/notifications/NotificationUtils.ts | 2 +- .../VectorPushRulesDefinitions.ts | 117 ++++++++---------- 2 files changed, 56 insertions(+), 63 deletions(-) diff --git a/src/notifications/NotificationUtils.ts b/src/notifications/NotificationUtils.ts index fa7aa1186d..3f07c56972 100644 --- a/src/notifications/NotificationUtils.ts +++ b/src/notifications/NotificationUtils.ts @@ -29,7 +29,7 @@ export class NotificationUtils { // "highlight: true/false, // } // to a list of push actions. - static encodeActions(action: IEncodedActions) { + static encodeActions(action: IEncodedActions): PushRuleAction[] { const notify = action.notify; const sound = action.sound; const highlight = action.highlight; diff --git a/src/notifications/VectorPushRulesDefinitions.ts b/src/notifications/VectorPushRulesDefinitions.ts index 38dd88e6c6..a8c617e786 100644 --- a/src/notifications/VectorPushRulesDefinitions.ts +++ b/src/notifications/VectorPushRulesDefinitions.ts @@ -1,6 +1,5 @@ /* -Copyright 2016 OpenMarket 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. @@ -17,19 +16,24 @@ limitations under the License. import { _td } from '../languageHandler'; import { StandardActions } from "./StandardActions"; -import { PushRuleVectorState } from "./PushRuleVectorState"; +import { PushRuleVectorState, VectorState } from "./PushRuleVectorState"; import { NotificationUtils } from "./NotificationUtils"; +import { PushRuleAction, PushRuleKind } from "matrix-js-sdk/src/@types/PushRules"; + +type StateToActionsMap = { + [state in VectorState]?: PushRuleAction[]; +}; interface IProps { - kind: Kind; + kind: PushRuleKind; description: string; - vectorStateToActions: Action; + vectorStateToActions: StateToActionsMap; } class VectorPushRuleDefinition { - private kind: Kind; + private kind: PushRuleKind; private description: string; - private vectorStateToActions: Action; + public readonly vectorStateToActions: StateToActionsMap; constructor(opts: IProps) { this.kind = opts.kind; @@ -73,73 +77,62 @@ class VectorPushRuleDefinition { } } -enum Kind { - Override = "override", - Underride = "underride", -} - -interface Action { - on: StandardActions; - loud: StandardActions; - off: StandardActions; -} - /** * The descriptions of rules managed by the Vector UI. */ export const VectorPushRulesDefinitions = { // Messages containing user's display name ".m.rule.contains_display_name": new VectorPushRuleDefinition({ - kind: Kind.Override, + kind: PushRuleKind.Override, description: _td("Messages containing my display name"), // passed through _t() translation in src/components/views/settings/Notifications.js vectorStateToActions: { // The actions for each vector state, or null to disable the rule. - on: StandardActions.ACTION_NOTIFY, - loud: StandardActions.ACTION_HIGHLIGHT_DEFAULT_SOUND, - off: StandardActions.ACTION_DISABLED, + [VectorState.On]: StandardActions.ACTION_NOTIFY, + [VectorState.Loud]: StandardActions.ACTION_HIGHLIGHT_DEFAULT_SOUND, + [VectorState.Off]: StandardActions.ACTION_DISABLED, }, }), // Messages containing user's username (localpart/MXID) ".m.rule.contains_user_name": new VectorPushRuleDefinition({ - kind: Kind.Override, + kind: PushRuleKind.Override, description: _td("Messages containing my username"), // passed through _t() translation in src/components/views/settings/Notifications.js vectorStateToActions: { // The actions for each vector state, or null to disable the rule. - on: StandardActions.ACTION_NOTIFY, - loud: StandardActions.ACTION_HIGHLIGHT_DEFAULT_SOUND, - off: StandardActions.ACTION_DISABLED, + [VectorState.On]: StandardActions.ACTION_NOTIFY, + [VectorState.Loud]: StandardActions.ACTION_HIGHLIGHT_DEFAULT_SOUND, + [VectorState.Off]: StandardActions.ACTION_DISABLED, }, }), // Messages containing @room ".m.rule.roomnotif": new VectorPushRuleDefinition({ - kind: Kind.Override, + kind: PushRuleKind.Override, description: _td("Messages containing @room"), // passed through _t() translation in src/components/views/settings/Notifications.js vectorStateToActions: { // The actions for each vector state, or null to disable the rule. - on: StandardActions.ACTION_NOTIFY, - loud: StandardActions.ACTION_HIGHLIGHT, - off: StandardActions.ACTION_DISABLED, + [VectorState.On]: StandardActions.ACTION_NOTIFY, + [VectorState.Loud]: StandardActions.ACTION_HIGHLIGHT, + [VectorState.Off]: StandardActions.ACTION_DISABLED, }, }), // Messages just sent to the user in a 1:1 room ".m.rule.room_one_to_one": new VectorPushRuleDefinition({ - kind: Kind.Underride, + kind: PushRuleKind.Underride, description: _td("Messages in one-to-one chats"), // passed through _t() translation in src/components/views/settings/Notifications.js vectorStateToActions: { - on: StandardActions.ACTION_NOTIFY, - loud: StandardActions.ACTION_NOTIFY_DEFAULT_SOUND, - off: StandardActions.ACTION_DONT_NOTIFY, + [VectorState.On]: StandardActions.ACTION_NOTIFY, + [VectorState.Loud]: StandardActions.ACTION_NOTIFY_DEFAULT_SOUND, + [VectorState.Off]: StandardActions.ACTION_DONT_NOTIFY, }, }), // Encrypted messages just sent to the user in a 1:1 room ".m.rule.encrypted_room_one_to_one": new VectorPushRuleDefinition({ - kind: Kind.Underride, + kind: PushRuleKind.Underride, description: _td("Encrypted messages in one-to-one chats"), // passed through _t() translation in src/components/views/settings/Notifications.js vectorStateToActions: { - on: StandardActions.ACTION_NOTIFY, - loud: StandardActions.ACTION_NOTIFY_DEFAULT_SOUND, - off: StandardActions.ACTION_DONT_NOTIFY, + [VectorState.On]: StandardActions.ACTION_NOTIFY, + [VectorState.Loud]: StandardActions.ACTION_NOTIFY_DEFAULT_SOUND, + [VectorState.Off]: StandardActions.ACTION_DONT_NOTIFY, }, }), @@ -147,12 +140,12 @@ export const VectorPushRulesDefinitions = { // 1:1 room messages are catched by the .m.rule.room_one_to_one rule if any defined // By opposition, all other room messages are from group chat rooms. ".m.rule.message": new VectorPushRuleDefinition({ - kind: Kind.Underride, + kind: PushRuleKind.Underride, description: _td("Messages in group chats"), // passed through _t() translation in src/components/views/settings/Notifications.js vectorStateToActions: { - on: StandardActions.ACTION_NOTIFY, - loud: StandardActions.ACTION_NOTIFY_DEFAULT_SOUND, - off: StandardActions.ACTION_DONT_NOTIFY, + [VectorState.On]: StandardActions.ACTION_NOTIFY, + [VectorState.Loud]: StandardActions.ACTION_NOTIFY_DEFAULT_SOUND, + [VectorState.Off]: StandardActions.ACTION_DONT_NOTIFY, }, }), @@ -160,57 +153,57 @@ export const VectorPushRulesDefinitions = { // Encrypted 1:1 room messages are catched by the .m.rule.encrypted_room_one_to_one rule if any defined // By opposition, all other room messages are from group chat rooms. ".m.rule.encrypted": new VectorPushRuleDefinition({ - kind: Kind.Underride, + kind: PushRuleKind.Underride, description: _td("Encrypted messages in group chats"), // passed through _t() translation in src/components/views/settings/Notifications.js vectorStateToActions: { - on: StandardActions.ACTION_NOTIFY, - loud: StandardActions.ACTION_NOTIFY_DEFAULT_SOUND, - off: StandardActions.ACTION_DONT_NOTIFY, + [VectorState.On]: StandardActions.ACTION_NOTIFY, + [VectorState.Loud]: StandardActions.ACTION_NOTIFY_DEFAULT_SOUND, + [VectorState.Off]: StandardActions.ACTION_DONT_NOTIFY, }, }), // Invitation for the user ".m.rule.invite_for_me": new VectorPushRuleDefinition({ - kind: Kind.Underride, + kind: PushRuleKind.Underride, description: _td("When I'm invited to a room"), // passed through _t() translation in src/components/views/settings/Notifications.js vectorStateToActions: { - on: StandardActions.ACTION_NOTIFY, - loud: StandardActions.ACTION_NOTIFY_DEFAULT_SOUND, - off: StandardActions.ACTION_DISABLED, + [VectorState.On]: StandardActions.ACTION_NOTIFY, + [VectorState.Loud]: StandardActions.ACTION_NOTIFY_DEFAULT_SOUND, + [VectorState.Off]: StandardActions.ACTION_DISABLED, }, }), // Incoming call ".m.rule.call": new VectorPushRuleDefinition({ - kind: Kind.Underride, + kind: PushRuleKind.Underride, description: _td("Call invitation"), // passed through _t() translation in src/components/views/settings/Notifications.js vectorStateToActions: { - on: StandardActions.ACTION_NOTIFY, - loud: StandardActions.ACTION_NOTIFY_RING_SOUND, - off: StandardActions.ACTION_DISABLED, + [VectorState.On]: StandardActions.ACTION_NOTIFY, + [VectorState.Loud]: StandardActions.ACTION_NOTIFY_RING_SOUND, + [VectorState.Off]: StandardActions.ACTION_DISABLED, }, }), // Notifications from bots ".m.rule.suppress_notices": new VectorPushRuleDefinition({ - kind: Kind.Override, + kind: PushRuleKind.Override, description: _td("Messages sent by bot"), // passed through _t() translation in src/components/views/settings/Notifications.js vectorStateToActions: { // .m.rule.suppress_notices is a "negative" rule, we have to invert its enabled value for vector UI - on: StandardActions.ACTION_DISABLED, - loud: StandardActions.ACTION_NOTIFY_DEFAULT_SOUND, - off: StandardActions.ACTION_DONT_NOTIFY, + [VectorState.On]: StandardActions.ACTION_DISABLED, + [VectorState.Loud]: StandardActions.ACTION_NOTIFY_DEFAULT_SOUND, + [VectorState.Off]: StandardActions.ACTION_DONT_NOTIFY, }, }), // Room upgrades (tombstones) ".m.rule.tombstone": new VectorPushRuleDefinition({ - kind: Kind.Override, + kind: PushRuleKind.Override, description: _td("When rooms are upgraded"), // passed through _t() translation in src/components/views/settings/Notifications.js vectorStateToActions: { // The actions for each vector state, or null to disable the rule. - on: StandardActions.ACTION_NOTIFY, - loud: StandardActions.ACTION_HIGHLIGHT, - off: StandardActions.ACTION_DISABLED, + [VectorState.On]: StandardActions.ACTION_NOTIFY, + [VectorState.Loud]: StandardActions.ACTION_HIGHLIGHT, + [VectorState.Off]: StandardActions.ACTION_DISABLED, }, }), }; From 3ae76c84f6fae2292df8fb678f6034c07652e292 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 12 Jul 2021 23:55:08 -0600 Subject: [PATCH 0252/1171] Add a simple TagComposer for the keywords entry --- res/css/_components.scss | 3 +- res/css/views/elements/_TagComposer.scss | 77 ++++++++++++++++ res/img/subtract.svg | 3 + src/components/views/elements/TagComposer.tsx | 91 +++++++++++++++++++ 4 files changed, 173 insertions(+), 1 deletion(-) create mode 100644 res/css/views/elements/_TagComposer.scss create mode 100644 res/img/subtract.svg create mode 100644 src/components/views/elements/TagComposer.tsx diff --git a/res/css/_components.scss b/res/css/_components.scss index 8f80f1bf97..c623eba9d8 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -148,6 +148,7 @@ @import "./views/elements/_StyledCheckbox.scss"; @import "./views/elements/_StyledRadioButton.scss"; @import "./views/elements/_SyntaxHighlight.scss"; +@import "./views/elements/_TagComposer.scss"; @import "./views/elements/_TextWithTooltip.scss"; @import "./views/elements/_ToggleSwitch.scss"; @import "./views/elements/_Tooltip.scss"; @@ -260,9 +261,9 @@ @import "./views/toasts/_NonUrgentEchoFailureToast.scss"; @import "./views/verification/_VerificationShowSas.scss"; @import "./views/voip/_CallContainer.scss"; +@import "./views/voip/_CallPreview.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"; diff --git a/res/css/views/elements/_TagComposer.scss b/res/css/views/elements/_TagComposer.scss new file mode 100644 index 0000000000..2ffd601765 --- /dev/null +++ b/res/css/views/elements/_TagComposer.scss @@ -0,0 +1,77 @@ +/* +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_TagComposer { + .mx_TagComposer_input { + display: flex; + + .mx_Field { + flex: 1; + margin: 0; // override from field styles + } + + .mx_AccessibleButton { + min-width: 70px; + padding: 0; // override from button styles + margin-left: 16px; // distance from + } + + .mx_Field, .mx_Field input, .mx_AccessibleButton { + // So they look related to each other by feeling the same + border-radius: 8px; + } + } + + .mx_TagComposer_tags { + display: flex; + flex-wrap: wrap; + margin-top: 12px; // this plus 12px from the tags makes 24px from the input + + .mx_TagComposer_tag { + padding: 6px 8px 8px 12px; + position: relative; + margin-right: 12px; + margin-top: 12px; + + // Cheaty way to get an opacified variable colour background + &::before { + content: ''; + border-radius: 20px; + background-color: $tertiary-fg-color; + opacity: 0.15; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + + // Pass through the pointer otherwise we have effectively put a whole div + // on top of the component, which makes it hard to interact with buttons. + pointer-events: none; + } + } + + .mx_AccessibleButton { + background-image: url('$(res)/img/subtract.svg'); + width: 16px; + height: 16px; + margin-left: 8px; + display: inline-block; + vertical-align: middle; + cursor: pointer; + } + } +} diff --git a/res/img/subtract.svg b/res/img/subtract.svg new file mode 100644 index 0000000000..55e25831ef --- /dev/null +++ b/res/img/subtract.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/components/views/elements/TagComposer.tsx b/src/components/views/elements/TagComposer.tsx new file mode 100644 index 0000000000..ff104748a0 --- /dev/null +++ b/src/components/views/elements/TagComposer.tsx @@ -0,0 +1,91 @@ +/* +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, { ChangeEvent, FormEvent } from "react"; +import { replaceableComponent } from "../../../utils/replaceableComponent"; +import Field from "./Field"; +import { _t } from "../../../languageHandler"; +import AccessibleButton from "./AccessibleButton"; + +interface IProps { + tags: string[]; + onAdd: (tag: string) => void; + onRemove: (tag: string) => void; + disabled?: boolean; + label?: string; + placeholder?: string; +} + +interface IState { + newTag: string; +} + +/** + * A simple, controlled, composer for entering string tags. Contains a simple + * input, add button, and per-tag remove button. + */ +@replaceableComponent("views.elements.TagComposer") +export default class TagComposer extends React.PureComponent { + public constructor(props: IProps) { + super(props); + + this.state = { + newTag: "", + }; + } + + private onInputChange = (ev: ChangeEvent) => { + this.setState({ newTag: ev.target.value }); + }; + + private onAdd = (ev: FormEvent) => { + ev.preventDefault(); + if (!this.state.newTag) return; + + this.props.onAdd(this.state.newTag); + this.setState({ newTag: "" }); + }; + + private onRemove = (tag: string) => { + // We probably don't need to proxy this, but for + // sanity of `this` we'll do so anyways. + this.props.onRemove(tag); + }; + + public render() { + return
+ + + + { _t("Add") } + + +
+ { this.props.tags.map((t, i) => (
+ { t } + +
)) } +
+
; + } +} From ff7a18da562ae6559769e4a2f3ecb637c293ddf1 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 12 Jul 2021 23:57:54 -0600 Subject: [PATCH 0253/1171] Rewrite Notifications component for modern UI & processing --- res/css/views/settings/_Notifications.scss | 125 +- .../views/settings/Notifications.tsx | 1292 +++++++---------- src/i18n/strings/en_EN.json | 11 +- 3 files changed, 612 insertions(+), 816 deletions(-) diff --git a/res/css/views/settings/_Notifications.scss b/res/css/views/settings/_Notifications.scss index 77a7bc5b68..2ec9f3fbea 100644 --- a/res/css/views/settings/_Notifications.scss +++ b/res/css/views/settings/_Notifications.scss @@ -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,82 +14,79 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_UserNotifSettings_tableRow { - display: table-row; -} +.mx_UserNotifSettings { + color: $primary-fg-color; // override from default settings page styles -.mx_UserNotifSettings_inputCell { - display: table-cell; - padding-bottom: 8px; - padding-right: 8px; - width: 16px; -} + .mx_UserNotifSettings_pushRulesTable { + width: calc(100% + 12px); // +12px to line up center of 'Noisy' column with toggle switches + table-layout: fixed; + border-collapse: collapse; + border-spacing: 0; + margin-top: 40px; -.mx_UserNotifSettings_labelCell { - padding-bottom: 8px; - width: 400px; - display: table-cell; -} + tr > th { + font-weight: 600; // semi bold + } -.mx_UserNotifSettings_pushRulesTableWrapper { - padding-bottom: 8px; -} + tr > th:first-child { + text-align: left; + font-size: $font-18px; + } -.mx_UserNotifSettings_pushRulesTable { - width: 100%; - table-layout: fixed; -} + tr > th:nth-child(n + 2) { + color: $secondary-fg-color; + font-size: $font-12px; + vertical-align: middle; + width: 66px; + } -.mx_UserNotifSettings_pushRulesTable thead { - font-weight: bold; -} + tr > td:nth-child(n + 2) { + text-align: center; + } -.mx_UserNotifSettings_pushRulesTable tbody th { - font-weight: 400; -} + tr > td { + padding-top: 8px; + } -.mx_UserNotifSettings_pushRulesTable tbody th:first-child { - text-align: left; -} + // Override StyledRadioButton default styles + .mx_RadioButton { + justify-content: center; -.mx_UserNotifSettings_keywords { - cursor: pointer; - color: $accent-color; -} + .mx_RadioButton_content { + display: none; + } -.mx_UserNotifSettings_devicesTable td { - padding-left: 20px; - padding-right: 20px; -} + .mx_RadioButton_spacer { + display: none; + } + } + } -.mx_UserNotifSettings_notifTable { - display: table; - position: relative; -} + .mx_UserNotifSettings_floatingSection { + margin-top: 40px; -.mx_UserNotifSettings_notifTable .mx_Spinner { - position: absolute; -} + & > div:first-child { // section header + font-size: $font-18px; + font-weight: 600; // semi bold + } -.mx_NotificationSound_soundUpload { - display: none; -} + > table { + border-collapse: collapse; + border-spacing: 0; + margin-top: 8px; -.mx_NotificationSound_browse { - color: $accent-color; - border: 1px solid $accent-color; - background-color: transparent; -} + tr > td:first-child { + // Just for a bit of spacing + padding-right: 8px; + } + } + } -.mx_NotificationSound_save { - margin-left: 5px; - color: white; - background-color: $accent-color; -} + .mx_UserNotifSettings_clearNotifsButton { + margin-top: 8px; + } -.mx_NotificationSound_resetSound { - margin-top: 5px; - color: white; - border: $warning-color; - background-color: $warning-color; + .mx_TagComposer { + margin-top: 35px; // lots of distance from the last line of the table + } } diff --git a/src/components/views/settings/Notifications.tsx b/src/components/views/settings/Notifications.tsx index 9f1929a35f..4a733d7bf5 100644 --- a/src/components/views/settings/Notifications.tsx +++ b/src/components/views/settings/Notifications.tsx @@ -1,6 +1,5 @@ /* -Copyright 2016 OpenMarket Ltd -Copyright 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. @@ -15,539 +14,240 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; -import * as sdk from '../../../index'; -import { _t } from '../../../languageHandler'; -import { MatrixClientPeg } from '../../../MatrixClientPeg'; -import SettingsStore from '../../../settings/SettingsStore'; -import Modal from '../../../Modal'; +import React from "react"; +import Spinner from "../elements/Spinner"; +import { MatrixClientPeg } from "../../../MatrixClientPeg"; +import { IAnnotatedPushRule, IPusher, PushRuleKind, RuleId, } from "matrix-js-sdk/src/@types/PushRules"; import { - VectorPushRulesDefinitions, - PushRuleVectorState, ContentRules, -} from '../../../notifications'; -import SdkConfig from "../../../SdkConfig"; + IContentRules, + PushRuleVectorState, + VectorPushRulesDefinitions, + VectorState, +} from "../../../notifications"; +import { _t, TranslatedString } from "../../../languageHandler"; +import { IThirdPartyIdentifier, ThreepidMedium } from "matrix-js-sdk/src/@types/threepids"; import LabelledToggleSwitch from "../elements/LabelledToggleSwitch"; -import AccessibleButton from "../elements/AccessibleButton"; +import SettingsStore from "../../../settings/SettingsStore"; +import StyledRadioButton from "../elements/StyledRadioButton"; import { SettingLevel } from "../../../settings/SettingLevel"; -import { UIFeature } from "../../../settings/UIFeature"; -import { replaceableComponent } from "../../../utils/replaceableComponent"; +import Modal from "../../../Modal"; +import ErrorDialog from "../dialogs/ErrorDialog"; +import SdkConfig from "../../../SdkConfig"; +import AccessibleButton from "../elements/AccessibleButton"; +import TagComposer from "../elements/TagComposer"; +import { objectClone } from "../../../utils/objects"; +import { arrayDiff } from "../../../utils/arrays"; // TODO: this "view" component still has far too much application logic in it, // which should be factored out to other files. -// TODO: this component also does a lot of direct poking into this.state, which -// is VERY NAUGHTY. +enum Phase { + Loading = "loading", + Ready = "ready", + Persisting = "persisting", // technically a meta-state for Ready, but whatever + Error = "error", +} -@replaceableComponent("views.settings.Notifications") -export default class Notifications extends React.Component { - static phases = { - LOADING: "LOADING", // The component is loading or sending data to the hs - DISPLAY: "DISPLAY", // The component is ready and display data - ERROR: "ERROR", // There was an error +enum RuleClass { + Master = "master", + + // The vector sections map approximately to UI sections + VectorGlobal = "vector_global", + VectorMentions = "vector_mentions", + VectorOther = "vector_other", + Other = "other", // unknown rules, essentially +} + +const KEYWORD_RULE_ID = "_keywords"; // used as a placeholder "Rule ID" throughout this component +const KEYWORD_RULE_CATEGORY = RuleClass.VectorMentions; + +// This array doesn't care about categories: it's just used for a simple sort +const RULE_DISPLAY_ORDER: string[] = [ + // Global + RuleId.DM, + RuleId.EncryptedDM, + RuleId.Message, + RuleId.EncryptedMessage, + + // Mentions + RuleId.ContainsDisplayName, + RuleId.ContainsUserName, + RuleId.AtRoomNotification, + + // Other + RuleId.InviteToSelf, + RuleId.IncomingCall, + RuleId.SuppressNotices, + RuleId.Tombstone, +] + +interface IVectorPushRule { + ruleId: RuleId | typeof KEYWORD_RULE_ID | string; + rule?: IAnnotatedPushRule; + description: TranslatedString | string; + vectorState: VectorState; +} + +interface IProps {} + +interface IState { + phase: Phase; + + // Optional stuff is required when `phase === Ready` + masterPushRule?: IAnnotatedPushRule; + vectorKeywordRuleInfo?: IContentRules; + vectorPushRules?: { + [category in RuleClass]?: IVectorPushRule[]; }; + pushers?: IPusher[]; + threepids?: IThirdPartyIdentifier[]; +} - state = { - phase: Notifications.phases.LOADING, - masterPushRule: undefined, // The master rule ('.m.rule.master') - vectorPushRules: [], // HS default push rules displayed in Vector UI - vectorContentRules: { // Keyword push rules displayed in Vector UI - vectorState: PushRuleVectorState.ON, - rules: [], - }, - externalPushRules: [], // Push rules (except content rule) that have been defined outside Vector UI - externalContentRules: [], // Keyword push rules that have been defined outside Vector UI - threepids: [], // used for email notifications - pushers: undefined, - }; +export default class Notifications extends React.PureComponent { + public constructor(props: IProps) { + super(props); - componentDidMount() { - this._refreshFromServer(); + this.state = { + phase: Phase.Loading, + }; } - onEnableNotificationsChange = (checked) => { - const self = this; - this.setState({ - phase: Notifications.phases.LOADING, - }); - - MatrixClientPeg.get().setPushRuleEnabled( - 'global', self.state.masterPushRule.kind, self.state.masterPushRule.rule_id, !checked, - ).then(function() { - self._refreshFromServer(); - }); - }; - - onEnableDesktopNotificationsChange = (checked) => { - SettingsStore.setValue( - "notificationsEnabled", null, - SettingLevel.DEVICE, - checked, - ).finally(() => { - this.forceUpdate(); - }); - }; - - onEnableDesktopNotificationBodyChange = (checked) => { - SettingsStore.setValue( - "notificationBodyEnabled", null, - SettingLevel.DEVICE, - checked, - ).finally(() => { - this.forceUpdate(); - }); - }; - - onEnableAudioNotificationsChange = (checked) => { - SettingsStore.setValue( - "audioNotificationsEnabled", null, - SettingLevel.DEVICE, - checked, - ).finally(() => { - this.forceUpdate(); - }); - }; - - /* - * Returns the email pusher (pusher of type 'email') for a given - * email address. Email pushers all have the same app ID, so since - * pushers are unique over (app ID, pushkey), there will be at most - * one such pusher. - */ - getEmailPusher(pushers, address) { - if (pushers === undefined) { - return undefined; - } - for (let i = 0; i < pushers.length; ++i) { - if (pushers[i].kind === 'email' && pushers[i].pushkey === address) { - return pushers[i]; - } - } - return undefined; + private get isInhibited(): boolean { + // Caution: The master rule's enabled state is inverted from expectation. When + // the master rule is *enabled* it means all other rules are *disabled* (or + // inhibited). Conversely, when the master rule is *disabled* then all other rules + // are *enabled* (or operate fine). + return this.state.masterPushRule?.enabled; } - onEnableEmailNotificationsChange = (address, checked) => { - let emailPusherPromise; - if (checked) { - const data = {}; - data['brand'] = SdkConfig.get().brand; - emailPusherPromise = MatrixClientPeg.get().setPusher({ - kind: 'email', - app_id: 'm.email', - pushkey: address, - app_display_name: 'Email Notifications', - device_display_name: address, - lang: navigator.language, - data: data, - append: true, // We always append for email pushers since we don't want to stop other accounts notifying to the same email address - }); - } else { - const emailPusher = this.getEmailPusher(this.state.pushers, address); - emailPusher.kind = null; - emailPusherPromise = MatrixClientPeg.get().setPusher(emailPusher); - } - emailPusherPromise.then(() => { - this._refreshFromServer(); - }, (error) => { - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - Modal.createTrackedDialog('Error saving email notification preferences', '', ErrorDialog, { - title: _t('Error saving email notification preferences'), - description: _t('An error occurred whilst saving your email notification preferences.'), - }); - }); - }; - - onNotifStateButtonClicked = (event) => { - // FIXME: use .bind() rather than className metadata here surely - const vectorRuleId = event.target.className.split("-")[0]; - const newPushRuleVectorState = event.target.className.split("-")[1]; - - if ("_keywords" === vectorRuleId) { - this._setKeywordsPushRuleVectorState(newPushRuleVectorState); - } else { - const rule = this.getRule(vectorRuleId); - if (rule) { - this._setPushRuleVectorState(rule, newPushRuleVectorState); - } - } - }; - - onKeywordsClicked = (event) => { - // Compute the keywords list to display - let keywords: any[]|string = []; - for (const i in this.state.vectorContentRules.rules) { - const rule = this.state.vectorContentRules.rules[i]; - keywords.push(rule.pattern); - } - if (keywords.length) { - // As keeping the order of per-word push rules hs side is a bit tricky to code, - // display the keywords in alphabetical order to the user - keywords.sort(); - - keywords = keywords.join(", "); - } else { - keywords = ""; - } - - const TextInputDialog = sdk.getComponent("dialogs.TextInputDialog"); - Modal.createTrackedDialog('Keywords Dialog', '', TextInputDialog, { - title: _t('Keywords'), - description: _t('Enter keywords separated by a comma:'), - button: _t('OK'), - value: keywords, - onFinished: (shouldLeave, newValue) => { - if (shouldLeave && newValue !== keywords) { - let newKeywords = newValue.split(','); - for (const i in newKeywords) { - newKeywords[i] = newKeywords[i].trim(); - } - - // Remove duplicates and empty - newKeywords = newKeywords.reduce(function(array, keyword) { - if (keyword !== "" && array.indexOf(keyword) < 0) { - array.push(keyword); - } - return array; - }, []); - - this._setKeywords(newKeywords); - } - }, - }); - }; - - getRule(vectorRuleId) { - for (const i in this.state.vectorPushRules) { - const rule = this.state.vectorPushRules[i]; - if (rule.vectorRuleId === vectorRuleId) { - return rule; - } - } + public componentDidMount() { + // noinspection JSIgnoredPromiseFromCall + this.refreshFromServer(); } - _setPushRuleVectorState(rule, newPushRuleVectorState) { - if (rule && rule.vectorState !== newPushRuleVectorState) { + private async refreshFromServer() { + try { + const newState = (await Promise.all([ + this.refreshRules(), + this.refreshPushers(), + this.refreshThreepids(), + ])).reduce((p, c) => Object.assign(c, p), {}); + this.setState({ - phase: Notifications.phases.LOADING, - }); - - const self = this; - const cli = MatrixClientPeg.get(); - const deferreds = []; - const ruleDefinition = VectorPushRulesDefinitions[rule.vectorRuleId]; - - if (rule.rule) { - const actions = ruleDefinition.vectorStateToActions[newPushRuleVectorState]; - - if (!actions) { - // The new state corresponds to disabling the rule. - deferreds.push(cli.setPushRuleEnabled('global', rule.rule.kind, rule.rule.rule_id, false)); - } else { - // The new state corresponds to enabling the rule and setting specific actions - deferreds.push(this._updatePushRuleActions(rule.rule, actions, true)); - } - } - - Promise.all(deferreds).then(function() { - self._refreshFromServer(); - }, function(error) { - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - console.error("Failed to change settings: " + error); - Modal.createTrackedDialog('Failed to change settings', '', ErrorDialog, { - title: _t('Failed to change settings'), - description: ((error && error.message) ? error.message : _t('Operation failed')), - onFinished: self._refreshFromServer, - }); + ...newState, + phase: Phase.Ready, }); + } catch (e) { + console.error("Error setting up notifications for settings: ", e); + this.setState({ phase: Phase.Error }); } } - _setKeywordsPushRuleVectorState(newPushRuleVectorState) { - // Is there really a change? - if (this.state.vectorContentRules.vectorState === newPushRuleVectorState - || this.state.vectorContentRules.rules.length === 0) { - return; - } + private async refreshRules(): Promise> { + const ruleSets = await MatrixClientPeg.get().getPushRules(); - const self = this; - const cli = MatrixClientPeg.get(); + const categories = { + [RuleId.Master]: RuleClass.Master, - this.setState({ - phase: Notifications.phases.LOADING, - }); + [RuleId.DM]: RuleClass.VectorGlobal, + [RuleId.EncryptedDM]: RuleClass.VectorGlobal, + [RuleId.Message]: RuleClass.VectorGlobal, + [RuleId.EncryptedMessage]: RuleClass.VectorGlobal, - // Update all rules in self.state.vectorContentRules - const deferreds = []; - for (const i in this.state.vectorContentRules.rules) { - const rule = this.state.vectorContentRules.rules[i]; + [RuleId.ContainsDisplayName]: RuleClass.VectorMentions, + [RuleId.ContainsUserName]: RuleClass.VectorMentions, + [RuleId.AtRoomNotification]: RuleClass.VectorMentions, - let enabled; let actions; - switch (newPushRuleVectorState) { - case PushRuleVectorState.ON: - if (rule.actions.length !== 1) { - actions = PushRuleVectorState.actionsFor(PushRuleVectorState.ON); - } + [RuleId.InviteToSelf]: RuleClass.VectorOther, + [RuleId.IncomingCall]: RuleClass.VectorOther, + [RuleId.SuppressNotices]: RuleClass.VectorOther, + [RuleId.Tombstone]: RuleClass.VectorOther, - if (this.state.vectorContentRules.vectorState === PushRuleVectorState.OFF) { - enabled = true; - } - break; - - case PushRuleVectorState.LOUD: - if (rule.actions.length !== 3) { - actions = PushRuleVectorState.actionsFor(PushRuleVectorState.LOUD); - } - - if (this.state.vectorContentRules.vectorState === PushRuleVectorState.OFF) { - enabled = true; - } - break; - - case PushRuleVectorState.OFF: - enabled = false; - break; - } - - if (actions) { - // Note that the workaround in _updatePushRuleActions will automatically - // enable the rule - deferreds.push(this._updatePushRuleActions(rule, actions, enabled)); - } else if (enabled != undefined) { - deferreds.push(cli.setPushRuleEnabled('global', rule.kind, rule.rule_id, enabled)); - } - } - - Promise.all(deferreds).then(function(resps) { - self._refreshFromServer(); - }, function(error) { - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - console.error("Can't update user notification settings: " + error); - Modal.createTrackedDialog('Can\'t update user notifcation settings', '', ErrorDialog, { - title: _t('Can\'t update user notification settings'), - description: ((error && error.message) ? error.message : _t('Operation failed')), - onFinished: self._refreshFromServer, - }); - }); - } - - _setKeywords(newKeywords) { - this.setState({ - phase: Notifications.phases.LOADING, - }); - - const self = this; - const cli = MatrixClientPeg.get(); - const removeDeferreds = []; - - // Remove per-word push rules of keywords that are no more in the list - const vectorContentRulesPatterns = []; - for (const i in self.state.vectorContentRules.rules) { - const rule = self.state.vectorContentRules.rules[i]; - - vectorContentRulesPatterns.push(rule.pattern); - - if (newKeywords.indexOf(rule.pattern) < 0) { - removeDeferreds.push(cli.deletePushRule('global', rule.kind, rule.rule_id)); - } - } - - // If the keyword is part of `externalContentRules`, remove the rule - // before recreating it in the right Vector path - for (const i in self.state.externalContentRules) { - const rule = self.state.externalContentRules[i]; - - if (newKeywords.indexOf(rule.pattern) >= 0) { - removeDeferreds.push(cli.deletePushRule('global', rule.kind, rule.rule_id)); - } - } - - const onError = function(error) { - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - console.error("Failed to update keywords: " + error); - Modal.createTrackedDialog('Failed to update keywords', '', ErrorDialog, { - title: _t('Failed to update keywords'), - description: ((error && error.message) ? error.message : _t('Operation failed')), - onFinished: self._refreshFromServer, - }); + // Everything maps to a generic "other" (unknown rule) }; - // Then, add the new ones - Promise.all(removeDeferreds).then(function(resps) { - const deferreds = []; + const defaultRules: { + [k in RuleClass]: IAnnotatedPushRule[]; + } = { + [RuleClass.Master]: [], + [RuleClass.VectorGlobal]: [], + [RuleClass.VectorMentions]: [], + [RuleClass.VectorOther]: [], + [RuleClass.Other]: [], + }; - let pushRuleVectorStateKind = self.state.vectorContentRules.vectorState; - if (pushRuleVectorStateKind === PushRuleVectorState.OFF) { - // When the current global keywords rule is OFF, we need to look at - // the flavor of rules in 'vectorContentRules' to apply the same actions - // when creating the new rule. - // Thus, this new rule will join the 'vectorContentRules' set. - if (self.state.vectorContentRules.rules.length) { - pushRuleVectorStateKind = PushRuleVectorState.contentRuleVectorStateKind( - self.state.vectorContentRules.rules[0], - ); - } else { - // ON is default - pushRuleVectorStateKind = PushRuleVectorState.ON; + for (const k in ruleSets.global) { + // noinspection JSUnfilteredForInLoop + const kind = k as PushRuleKind; + for (const r of ruleSets.global[kind]) { + const rule: IAnnotatedPushRule = Object.assign(r, {kind}); + const category = categories[rule.rule_id] ?? RuleClass.Other; + + if (rule.rule_id[0] === '.') { + defaultRules[category].push(rule); } } + } - for (const i in newKeywords) { - const keyword = newKeywords[i]; + const preparedNewState: Partial = {}; + if (defaultRules.master.length > 0) { + preparedNewState.masterPushRule = defaultRules.master[0]; + } else { + // XXX: Can this even happen? How do we safely recover? + throw new Error("Failed to locate a master push rule"); + } - if (vectorContentRulesPatterns.indexOf(keyword) < 0) { - if (self.state.vectorContentRules.vectorState !== PushRuleVectorState.OFF) { - deferreds.push(cli.addPushRule('global', 'content', keyword, { - actions: PushRuleVectorState.actionsFor(pushRuleVectorStateKind), - pattern: keyword, - })); - } else { - deferreds.push(self._addDisabledPushRule('global', 'content', keyword, { - actions: PushRuleVectorState.actionsFor(pushRuleVectorStateKind), - pattern: keyword, - })); - } - } - } + // Parse keyword rules + preparedNewState.vectorKeywordRuleInfo = ContentRules.parseContentRules(ruleSets); - Promise.all(deferreds).then(function(resps) { - self._refreshFromServer(); - }, onError); - }, onError); - } - - // Create a push rule but disabled - _addDisabledPushRule(scope, kind, ruleId, body) { - const cli = MatrixClientPeg.get(); - return cli.addPushRule(scope, kind, ruleId, body).then(() => - cli.setPushRuleEnabled(scope, kind, ruleId, false), - ); - } - - _refreshFromServer = () => { - const self = this; - const pushRulesPromise = MatrixClientPeg.get().getPushRules().then(function(rulesets) { - /// XXX seriously? wtf is this? - MatrixClientPeg.get().pushRules = rulesets; - - // Get homeserver default rules and triage them by categories - const ruleCategories = { - // The master rule (all notifications disabling) - '.m.rule.master': 'master', - - // The default push rules displayed by Vector UI - '.m.rule.contains_display_name': 'vector', - '.m.rule.contains_user_name': 'vector', - '.m.rule.roomnotif': 'vector', - '.m.rule.room_one_to_one': 'vector', - '.m.rule.encrypted_room_one_to_one': 'vector', - '.m.rule.message': 'vector', - '.m.rule.encrypted': 'vector', - '.m.rule.invite_for_me': 'vector', - //'.m.rule.member_event': 'vector', - '.m.rule.call': 'vector', - '.m.rule.suppress_notices': 'vector', - '.m.rule.tombstone': 'vector', - - // Others go to others - }; - - // HS default rules - const defaultRules = { master: [], vector: {}, others: [] }; - - for (const kind in rulesets.global) { - for (let i = 0; i < Object.keys(rulesets.global[kind]).length; ++i) { - const r = rulesets.global[kind][i]; - const cat = ruleCategories[r.rule_id]; - r.kind = kind; - - if (r.rule_id[0] === '.') { - if (cat === 'vector') { - defaultRules.vector[r.rule_id] = r; - } else if (cat === 'master') { - defaultRules.master.push(r); - } else { - defaultRules['others'].push(r); - } - } - } - } - - // Get the master rule if any defined by the hs - if (defaultRules.master.length > 0) { - self.state.masterPushRule = defaultRules.master[0]; - } - - // parse the keyword rules into our state - const contentRules = ContentRules.parseContentRules(rulesets); - self.state.vectorContentRules = { - vectorState: contentRules.vectorState, - rules: contentRules.rules, - }; - self.state.externalContentRules = contentRules.externalRules; - - // Build the rules displayed in the Vector UI matrix table - self.state.vectorPushRules = []; - self.state.externalPushRules = []; - - const vectorRuleIds = [ - '.m.rule.contains_display_name', - '.m.rule.contains_user_name', - '.m.rule.roomnotif', - '_keywords', - '.m.rule.room_one_to_one', - '.m.rule.encrypted_room_one_to_one', - '.m.rule.message', - '.m.rule.encrypted', - '.m.rule.invite_for_me', - //'im.vector.rule.member_event', - '.m.rule.call', - '.m.rule.suppress_notices', - '.m.rule.tombstone', - ]; - for (const i in vectorRuleIds) { - const vectorRuleId = vectorRuleIds[i]; - - if (vectorRuleId === '_keywords') { - // keywords needs a special handling - // For Vector UI, this is a single global push rule but translated in Matrix, - // it corresponds to all content push rules (stored in self.state.vectorContentRule) - self.state.vectorPushRules.push({ - "vectorRuleId": "_keywords", - "description": ( - - { _t('Messages containing keywords', - {}, - { 'span': (sub) => - {sub}, - }, - )} - - ), - "vectorState": self.state.vectorContentRules.vectorState, - }); - } else { - const ruleDefinition = VectorPushRulesDefinitions[vectorRuleId]; - const rule = defaultRules.vector[vectorRuleId]; - - const vectorState = ruleDefinition.ruleToVectorState(rule); - - //console.log("Refreshing vectorPushRules for " + vectorRuleId +", "+ ruleDefinition.description +", " + rule +", " + vectorState); - - self.state.vectorPushRules.push({ - "vectorRuleId": vectorRuleId, - "description": _t(ruleDefinition.description), // Text from VectorPushRulesDefinitions.js - "rule": rule, - "vectorState": vectorState, - }); + // Prepare rendering for all of our known rules + preparedNewState.vectorPushRules = {}; + const vectorCategories = [RuleClass.VectorGlobal, RuleClass.VectorMentions, RuleClass.VectorOther]; + for (const category of vectorCategories) { + preparedNewState.vectorPushRules[category] = []; + for (const rule of defaultRules[category]) { + const definition = VectorPushRulesDefinitions[rule.rule_id]; + const vectorState = definition.ruleToVectorState(rule); + preparedNewState.vectorPushRules[category].push({ + ruleId: rule.rule_id, + rule, vectorState, + description: _t(definition.description), + }); + // XXX: Do we need this block from the previous component? + /* // if there was a rule which we couldn't parse, add it to the external list if (rule && !vectorState) { rule.description = ruleDefinition.description; self.state.externalPushRules.push(rule); } - } + */ } + // Quickly sort the rules for display purposes + preparedNewState.vectorPushRules[category].sort((a, b) => { + let idxA = RULE_DISPLAY_ORDER.indexOf(a.ruleId); + let idxB = RULE_DISPLAY_ORDER.indexOf(b.ruleId); + + // Assume unknown things go at the end + if (idxA < 0) idxA = RULE_DISPLAY_ORDER.length; + if (idxB < 0) idxB = RULE_DISPLAY_ORDER.length; + + return idxA - idxB; + }); + + if (category === KEYWORD_RULE_CATEGORY) { + preparedNewState.vectorPushRules[category].push({ + ruleId: KEYWORD_RULE_ID, + description: _t("Messages containing keywords"), + vectorState: preparedNewState.vectorKeywordRuleInfo.vectorState, + }); + } + } + + // XXX: Do we need this block from the previous component? + /* // Build the rules not managed by Vector UI const otherRulesDescriptions = { '.m.rule.message': _t('Notify for all other messages/rooms'), @@ -564,294 +264,384 @@ export default class Notifications extends React.Component { self.state.externalPushRules.push(rule); } } - }); + */ - const pushersPromise = MatrixClientPeg.get().getPushers().then(function(resp) { - self.setState({ pushers: resp.pushers }); - }); + return preparedNewState; + } - Promise.all([pushRulesPromise, pushersPromise]).then(function() { - self.setState({ - phase: Notifications.phases.DISPLAY, - }); - }, function(error) { - console.error(error); - self.setState({ - phase: Notifications.phases.ERROR, - }); - }).finally(() => { - // actually explicitly update our state having been deep-manipulating it - self.setState({ - masterPushRule: self.state.masterPushRule, - vectorContentRules: self.state.vectorContentRules, - vectorPushRules: self.state.vectorPushRules, - externalContentRules: self.state.externalContentRules, - externalPushRules: self.state.externalPushRules, - }); - }); + private async refreshPushers(): Promise> { + return { ...(await MatrixClientPeg.get().getPushers()) }; + } - MatrixClientPeg.get().getThreePids().then((r) => this.setState({ threepids: r.threepids })); + private async refreshThreepids(): Promise> { + return { ...(await MatrixClientPeg.get().getThreePids()) }; + } + + private showSaveError() { + Modal.createTrackedDialog('Error saving notification preferences', '', ErrorDialog, { + title: _t('Error saving notification preferences'), + description: _t('An error occurred whilst saving your notification preferences.'), + }); + } + + private onMasterRuleChanged = async (checked: boolean) => { + this.setState({ phase: Phase.Persisting }); + + try { + const masterRule = this.state.masterPushRule; + await MatrixClientPeg.get().setPushRuleEnabled('global', masterRule.kind, masterRule.rule_id, !checked); + await this.refreshFromServer(); + } catch (e) { + this.setState({ phase: Phase.Error }); + console.error("Error updating master push rule:", e); + this.showSaveError(); + } }; - _onClearNotifications = () => { - const cli = MatrixClientPeg.get(); + private onEmailNotificationsChanged = async (email: string, checked: boolean) => { + this.setState({ phase: Phase.Persisting }); - cli.getRooms().forEach(r => { + try { + if (checked) { + await MatrixClientPeg.get().setPusher({ + kind: "email", + app_id: "m.email", + pushkey: email, + app_display_name: "Email Notifications", + device_display_name: email, + lang: navigator.language, + data: { + brand: SdkConfig.get().brand, + }, + + // We always append for email pushers since we don't want to stop other + // accounts notifying to the same email address + append: true, + }); + } else { + const pusher = this.state.pushers.find(p => p.kind === "email" && p.pushkey === email); + pusher.kind = null; // flag for delete + await MatrixClientPeg.get().setPusher(pusher); + } + + await this.refreshFromServer(); + } catch (e) { + this.setState({ phase: Phase.Error }); + console.error("Error updating email pusher:", e); + this.showSaveError(); + } + }; + + private onDesktopNotificationsChanged = async (checked: boolean) => { + await SettingsStore.setValue("notificationsEnabled", null, SettingLevel.DEVICE, checked); + this.forceUpdate(); // the toggle is controlled by SettingsStore#getValue() + }; + + private onDesktopShowBodyChanged = async (checked: boolean) => { + await SettingsStore.setValue("notificationBodyEnabled", null, SettingLevel.DEVICE, checked); + this.forceUpdate(); // the toggle is controlled by SettingsStore#getValue() + }; + + private onAudioNotificationsChanged = async (checked: boolean) => { + await SettingsStore.setValue("audioNotificationsEnabled", null, SettingLevel.DEVICE, checked); + this.forceUpdate(); // the toggle is controlled by SettingsStore#getValue() + }; + + private onRadioChecked = async (rule: IVectorPushRule, checkedState: VectorState) => { + this.setState({ phase: Phase.Persisting }); + + try { + if (rule.ruleId === KEYWORD_RULE_ID) { + console.log("@@ KEYWORDS"); + } else { + const definition = VectorPushRulesDefinitions[rule.ruleId]; + const actions = definition.vectorStateToActions[checkedState]; + if (!actions) { + await MatrixClientPeg.get().setPushRuleEnabled('global', rule.rule.kind, rule.rule.rule_id, false); + } else { + await MatrixClientPeg.get().setPushRuleActions('global', rule.rule.kind, rule.rule.rule_id, actions); + await MatrixClientPeg.get().setPushRuleEnabled('global', rule.rule.kind, rule.rule.rule_id, true); + } + } + + await this.refreshFromServer(); + } catch (e) { + this.setState({ phase: Phase.Error }); + console.error("Error updating push rule:", e); + this.showSaveError(); + } + }; + + private onClearNotificationsClicked = () => { + MatrixClientPeg.get().getRooms().forEach(r => { if (r.getUnreadNotificationCount() > 0) { const events = r.getLiveTimeline().getEvents(); - if (events.length) cli.sendReadReceipt(events.pop()); + if (events.length) { + // noinspection JSIgnoredPromiseFromCall + MatrixClientPeg.get().sendReadReceipt(events[events.length - 1]); + } } }); }; - _updatePushRuleActions(rule, actions, enabled) { - const cli = MatrixClientPeg.get(); + private async setKeywords(keywords: string[], originalRules: IAnnotatedPushRule[]) { + try { + // De-duplicate and remove empties + keywords = Array.from(new Set(keywords)).filter(k => !!k); + const oldKeywords = Array.from(new Set(originalRules.map(r => r.pattern))).filter(k => !!k); - return cli.setPushRuleActions( - 'global', rule.kind, rule.rule_id, actions, - ).then( function() { - // Then, if requested, enabled or disabled the rule - if (undefined != enabled) { - return cli.setPushRuleEnabled( - 'global', rule.kind, rule.rule_id, enabled, - ); + // Note: Technically because of the UI interaction (at the time of writing), the diff + // will only ever be +/-1 so we don't really have to worry about efficiently handling + // tons of keyword changes. + + const diff = arrayDiff(oldKeywords, keywords); + + for (const word of diff.removed) { + for (const rule of originalRules.filter(r => r.pattern === word)) { + await MatrixClientPeg.get().deletePushRule('global', rule.kind, rule.rule_id); + } } + + let ruleVectorState = this.state.vectorKeywordRuleInfo.vectorState; + if (ruleVectorState === VectorState.Off) { + // When the current global keywords rule is OFF, we need to look at + // the flavor of existing rules to apply the same actions + // when creating the new rule. + if (originalRules.length) { + ruleVectorState = PushRuleVectorState.contentRuleVectorStateKind(originalRules[0]); + } else { + ruleVectorState = VectorState.On; // default + } + } + const kind = PushRuleKind.ContentSpecific; + for (const word of diff.added) { + await MatrixClientPeg.get().addPushRule('global', kind, word, { + actions: PushRuleVectorState.actionsFor(ruleVectorState), + pattern: word, + }); + if (ruleVectorState === VectorState.Off) { + await MatrixClientPeg.get().setPushRuleEnabled('global', kind, word, false); + } + } + + await this.refreshFromServer(); + } catch (e) { + this.setState({ phase: Phase.Error }); + console.error("Error updating keyword push rules:", e); + this.showSaveError(); + } + } + + private onKeywordAdd = (keyword: string) => { + const originalRules = objectClone(this.state.vectorKeywordRuleInfo.rules); + + // We add the keyword immediately as a sort of local echo effect + this.setState({ + phase: Phase.Persisting, + vectorKeywordRuleInfo: { + ...this.state.vectorKeywordRuleInfo, + rules: [ + ...this.state.vectorKeywordRuleInfo.rules, + + // XXX: Horrible assumption that we don't need the remaining fields + { pattern: keyword } as IAnnotatedPushRule, + ], + }, + }, async () => { + await this.setKeywords(this.state.vectorKeywordRuleInfo.rules.map(r => r.pattern), originalRules); }); + }; + + private onKeywordRemove = (keyword: string) => { + const originalRules = objectClone(this.state.vectorKeywordRuleInfo.rules); + + // We remove the keyword immediately as a sort of local echo effect + this.setState({ + phase: Phase.Persisting, + vectorKeywordRuleInfo: { + ...this.state.vectorKeywordRuleInfo, + rules: this.state.vectorKeywordRuleInfo.rules.filter(r => r.pattern !== keyword), + }, + }, async () => { + await this.setKeywords(this.state.vectorKeywordRuleInfo.rules.map(r => r.pattern), originalRules); + }); + }; + + private renderTopSection() { + const masterSwitch = ; + + // If all the rules are inhibited, don't show anything. + if (this.isInhibited) { + return masterSwitch; + } + + const emailSwitches = this.state.threepids.filter(t => t.medium === ThreepidMedium.Email) + .map(e => p.kind === "email" && p.pushkey === e.address)} + label={_t("Enable email notifications for %(email)s", { email: e.address })} + onChange={this.onEmailNotificationsChanged.bind(this, e.address)} + disabled={this.state.phase === Phase.Persisting} + />); + + return <> + { masterSwitch } + + + + + + + + { emailSwitches } + ; } - renderNotifRulesTableRow(title, className, pushRuleVectorState) { - return ( -
- + private renderCategory(category: RuleClass) { + if (category !== RuleClass.VectorOther && this.isInhibited) { + return null; // nothing to show for the section + } - + let clearNotifsButton: JSX.Element; + if ( + category === RuleClass.VectorOther + && MatrixClientPeg.get().getRooms().some(r => r.getUnreadNotificationCount() > 0) + ) { + clearNotifsButton = { _t("Clear notifications") }; + } - + if (category === RuleClass.VectorOther && this.isInhibited) { + // only render the utility buttons (if needed) + if (clearNotifsButton) { + return
+
{ _t("Other") }
+ { clearNotifsButton } +
; + } + return null; + } - - + let keywordComposer: JSX.Element; + if (category === RuleClass.VectorMentions) { + keywordComposer = r.pattern)} + onAdd={this.onKeywordAdd} + onRemove={this.onKeywordRemove} + disabled={this.state.phase === Phase.Persisting} + label={_t("Keyword")} + placeholder={_t("New keyword")} + />; + } + + const makeRadio = (r: IVectorPushRule, s: VectorState) => ( + ); - } - renderNotifRulesTableRows() { - const rows = []; - for (const i in this.state.vectorPushRules) { - const rule = this.state.vectorPushRules[i]; - if (rule.rule === undefined && rule.vectorRuleId.startsWith(".m.")) { - console.warn(`Skipping render of rule ${rule.vectorRuleId} due to no underlying rule`); - continue; - } - //console.log("rendering: " + rule.description + ", " + rule.vectorRuleId + ", " + rule.vectorState); - rows.push(this.renderNotifRulesTableRow(rule.description, rule.vectorRuleId, rule.vectorState)); - } - return rows; - } + const rows = this.state.vectorPushRules[category].map(r => + + + + + ); - hasEmailPusher(pushers, address) { - if (pushers === undefined) { - return false; - } - for (let i = 0; i < pushers.length; ++i) { - if (pushers[i].kind === 'email' && pushers[i].pushkey === address) { - return true; - } - } - return false; - } - - emailNotificationsRow(address, label) { - return ; - } - - render() { - let spinner; - if (this.state.phase === Notifications.phases.LOADING) { - const Loader = sdk.getComponent("elements.Spinner"); - spinner = ; + let sectionName: TranslatedString; + switch (category) { + case RuleClass.VectorGlobal: + sectionName = _t("Global"); + break; + case RuleClass.VectorMentions: + sectionName = _t("Mentions & keywords"); + break; + case RuleClass.VectorOther: + sectionName = _t("Other"); + break; + default: + throw new Error("Developer error: Unnamed notifications section: " + category); } - let masterPushRuleDiv; - if (this.state.masterPushRule) { - masterPushRuleDiv = ; - } - - let clearNotificationsButton; - if (MatrixClientPeg.get().getRooms().some(r => r.getUnreadNotificationCount() > 0)) { - clearNotificationsButton = - {_t("Clear notifications")} - ; - } - - // When enabled, the master rule inhibits all existing rules - // So do not show all notification settings - if (this.state.masterPushRule && this.state.masterPushRule.enabled) { - return ( -
- {masterPushRuleDiv} - -
- { _t('All notifications are currently disabled for all targets.') } -
- - {clearNotificationsButton} -
- ); - } - - const emailThreepids = this.state.threepids.filter((tp) => tp.medium === "email"); - let emailNotificationsRows; - if (emailThreepids.length > 0) { - emailNotificationsRows = emailThreepids.map((threePid) => this.emailNotificationsRow( - threePid.address, `${_t('Enable email notifications')} (${threePid.address})`, - )); - } else if (SettingsStore.getValue(UIFeature.ThirdPartyID)) { - emailNotificationsRows =
- { _t('Add an email address to configure email notifications') } -
; - } - - // Build external push rules - const externalRules = []; - for (const i in this.state.externalPushRules) { - const rule = this.state.externalPushRules[i]; - externalRules.push(
  • { _t(rule.description) }
  • ); - } - - // Show keywords not displayed by the vector UI as a single external push rule - let externalKeywords: any[]|string = []; - for (const i in this.state.externalContentRules) { - const rule = this.state.externalContentRules[i]; - externalKeywords.push(rule.pattern); - } - if (externalKeywords.length) { - externalKeywords = externalKeywords.join(", "); - externalRules.push(
  • - {_t('Notifications on the following keywords follow rules which can’t be displayed here:') } - { externalKeywords } -
  • ); - } - - let devicesSection; - if (this.state.pushers === undefined) { - devicesSection =
    { _t('Unable to fetch notification target list') }
    ; - } else if (this.state.pushers.length === 0) { - devicesSection = null; - } else { - // TODO: It would be great to be able to delete pushers from here too, - // and this wouldn't be hard to add. - const rows = []; - for (let i = 0; i < this.state.pushers.length; ++i) { - rows.push(
    - - - ); - } - devicesSection = (
    + {/* @ts-ignore*/} { _t('Off') }{ _t('On') }{ _t('Noisy') }
    - { title } - - - - - - -
    { r.description }{ makeRadio(r, VectorState.On) }{ makeRadio(r, VectorState.Off) }{ makeRadio(r, VectorState.Loud) }
    {this.state.pushers[i].app_display_name}{this.state.pushers[i].device_display_name}
    + return <> +
    + + + + + + + + - {rows} + { rows } -
    { sectionName }{ _t("On") }{ _t("Off") }{ _t("Noisy") }
    ); - } - if (devicesSection) { - devicesSection = (
    -

    { _t('Notification targets') }

    - { devicesSection } -
    ); + + { clearNotifsButton } + { keywordComposer } + ; + } + + private renderTargets() { + if (this.isInhibited) return null; // no targets if there's no notifications + + const rows = this.state.pushers.map(p => + { p.app_display_name } + { p.device_display_name } + ); + + if (!rows.length) return null; // no targets to show + + return
    +
    { _t("Notification targets") }
    + + + { rows } + +
    +
    ; + } + + public render() { + if (this.state.phase === Phase.Loading) { + // Ends up default centered + return ; + } else if (this.state.phase === Phase.Error) { + return

    { _t("There was an error loading your notification settings.") }

    ; } - let advancedSettings; - if (externalRules.length) { - const brand = SdkConfig.get().brand; - advancedSettings = ( -
    -

    { _t('Advanced notification settings') }

    - { _t('There are advanced notifications which are not shown here.') }
    - {_t( - 'You might have configured them in a client other than %(brand)s. ' + - 'You cannot tune them in %(brand)s but they still apply.', - { brand }, - )} -
      - { externalRules } -
    -
    - ); - } - - return ( -
    - - {masterPushRuleDiv} - -
    - - { spinner } - - - - - - - - { emailNotificationsRows } - -
    - - - - {/* @ts-ignore*/} - - {/* @ts-ignore*/} - - {/* @ts-ignore*/} - - - - - - { this.renderNotifRulesTableRows() } - - -
    - {/* @ts-ignore*/} - { _t('Off') }{ _t('On') }{ _t('Noisy') }
    -
    - - { advancedSettings } - - { devicesSection } - - { clearNotificationsButton } -
    - -
    - ); + return
    + { this.renderTopSection() } + { this.renderCategory(RuleClass.VectorGlobal) } + { this.renderCategory(RuleClass.VectorMentions) } + { this.renderCategory(RuleClass.VectorOther) } + { this.renderTargets() } +
    ; } } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 761d48e51b..cfee47e361 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1158,6 +1158,16 @@ "Off": "Off", "On": "On", "Noisy": "Noisy", + "Messages containing keywords": "Messages containing keywords", + "Error saving notification preferences": "Error saving notification preferences", + "An error occurred whilst saving your notification preferences.": "An error occurred whilst saving your notification preferences.", + "Enable for this account": "Enable for this account", + "Enable email notifications for %(email)s": "Enable email notifications for %(email)s", + "Keyword": "Keyword", + "New keyword": "New keyword", + "Global": "Global", + "Mentions & keywords": "Mentions & keywords", + "There was an error loading your notification settings.": "There was an error loading your notification settings.", "Failed to save your profile": "Failed to save your profile", "The operation could not be completed": "The operation could not be completed", "Upgrade to your own domain": "Upgrade to your own domain", @@ -1656,7 +1666,6 @@ "Show %(count)s more|other": "Show %(count)s more", "Show %(count)s more|one": "Show %(count)s more", "Show less": "Show less", - "Global": "Global", "All messages": "All messages", "Mentions & Keywords": "Mentions & Keywords", "Notification options": "Notification options", From 4444ccb0794f77b60937282bbd9f78b8a3b100c9 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 13 Jul 2021 00:02:44 -0600 Subject: [PATCH 0254/1171] Appease the linter --- src/components/views/elements/Spinner.tsx | 2 +- src/components/views/settings/Notifications.tsx | 13 +++++++------ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/components/views/elements/Spinner.tsx b/src/components/views/elements/Spinner.tsx index 93c8f9e5d4..ee43a5bf0e 100644 --- a/src/components/views/elements/Spinner.tsx +++ b/src/components/views/elements/Spinner.tsx @@ -36,7 +36,7 @@ export default class Spinner extends React.PureComponent { { message &&
    { message }
     
    }
    diff --git a/src/components/views/settings/Notifications.tsx b/src/components/views/settings/Notifications.tsx index 4a733d7bf5..6d74e19ab1 100644 --- a/src/components/views/settings/Notifications.tsx +++ b/src/components/views/settings/Notifications.tsx @@ -17,7 +17,7 @@ limitations under the License. import React from "react"; import Spinner from "../elements/Spinner"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; -import { IAnnotatedPushRule, IPusher, PushRuleKind, RuleId, } from "matrix-js-sdk/src/@types/PushRules"; +import { IAnnotatedPushRule, IPusher, PushRuleKind, RuleId } from "matrix-js-sdk/src/@types/PushRules"; import { ContentRules, IContentRules, @@ -80,7 +80,7 @@ const RULE_DISPLAY_ORDER: string[] = [ RuleId.IncomingCall, RuleId.SuppressNotices, RuleId.Tombstone, -] +]; interface IVectorPushRule { ruleId: RuleId | typeof KEYWORD_RULE_ID | string; @@ -181,7 +181,7 @@ export default class Notifications extends React.PureComponent { // noinspection JSUnfilteredForInLoop const kind = k as PushRuleKind; for (const r of ruleSets.global[kind]) { - const rule: IAnnotatedPushRule = Object.assign(r, {kind}); + const rule: IAnnotatedPushRule = Object.assign(r, { kind }); const category = categories[rule.rule_id] ?? RuleClass.Other; if (rule.rule_id[0] === '.') { @@ -356,11 +356,12 @@ export default class Notifications extends React.PureComponent { } else { const definition = VectorPushRulesDefinitions[rule.ruleId]; const actions = definition.vectorStateToActions[checkedState]; + const cli = MatrixClientPeg.get(); if (!actions) { - await MatrixClientPeg.get().setPushRuleEnabled('global', rule.rule.kind, rule.rule.rule_id, false); + await cli.setPushRuleEnabled('global', rule.rule.kind, rule.rule.rule_id, false); } else { - await MatrixClientPeg.get().setPushRuleActions('global', rule.rule.kind, rule.rule.rule_id, actions); - await MatrixClientPeg.get().setPushRuleEnabled('global', rule.rule.kind, rule.rule.rule_id, true); + await cli.setPushRuleActions('global', rule.rule.kind, rule.rule.rule_id, actions); + await cli.setPushRuleEnabled('global', rule.rule.kind, rule.rule.rule_id, true); } } From 9d60d29368290fa33dfc2eb8a4129ac99f136bab Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 13 Jul 2021 00:04:07 -0600 Subject: [PATCH 0255/1171] Clean up i18n --- src/i18n/strings/en_EN.json | 35 ++++++++--------------------------- 1 file changed, 8 insertions(+), 27 deletions(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index cfee47e361..ed794068e0 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1131,42 +1131,23 @@ "Connecting to integration manager...": "Connecting to integration manager...", "Cannot connect to integration manager": "Cannot connect to integration manager", "The integration manager is offline or it cannot reach your homeserver.": "The integration manager is offline or it cannot reach your homeserver.", - "Error saving email notification preferences": "Error saving email notification preferences", - "An error occurred whilst saving your email notification preferences.": "An error occurred whilst saving your email notification preferences.", - "Keywords": "Keywords", - "Enter keywords separated by a comma:": "Enter keywords separated by a comma:", - "Failed to change settings": "Failed to change settings", - "Can't update user notification settings": "Can't update user notification settings", - "Failed to update keywords": "Failed to update keywords", - "Messages containing keywords": "Messages containing keywords", - "Notify for all other messages/rooms": "Notify for all other messages/rooms", - "Notify me for anything else": "Notify me for anything else", - "Enable notifications for this account": "Enable notifications for this account", - "Clear notifications": "Clear notifications", - "All notifications are currently disabled for all targets.": "All notifications are currently disabled for all targets.", - "Enable email notifications": "Enable email notifications", - "Add an email address to configure email notifications": "Add an email address to configure email notifications", - "Notifications on the following keywords follow rules which can’t be displayed here:": "Notifications on the following keywords follow rules which can’t be displayed here:", - "Unable to fetch notification target list": "Unable to fetch notification target list", - "Notification targets": "Notification targets", - "Advanced notification settings": "Advanced notification settings", - "There are advanced notifications which are not shown here.": "There are advanced notifications which are not shown here.", - "You might have configured them in a client other than %(brand)s. You cannot tune them in %(brand)s but they still apply.": "You might have configured them in a client other than %(brand)s. You cannot tune them in %(brand)s but they still apply.", - "Enable desktop notifications for this session": "Enable desktop notifications for this session", - "Show message in desktop notification": "Show message in desktop notification", - "Enable audible notifications for this session": "Enable audible notifications for this session", - "Off": "Off", - "On": "On", - "Noisy": "Noisy", "Messages containing keywords": "Messages containing keywords", "Error saving notification preferences": "Error saving notification preferences", "An error occurred whilst saving your notification preferences.": "An error occurred whilst saving your notification preferences.", "Enable for this account": "Enable for this account", "Enable email notifications for %(email)s": "Enable email notifications for %(email)s", + "Enable desktop notifications for this session": "Enable desktop notifications for this session", + "Show message in desktop notification": "Show message in desktop notification", + "Enable audible notifications for this session": "Enable audible notifications for this session", + "Clear notifications": "Clear notifications", "Keyword": "Keyword", "New keyword": "New keyword", "Global": "Global", "Mentions & keywords": "Mentions & keywords", + "On": "On", + "Off": "Off", + "Noisy": "Noisy", + "Notification targets": "Notification targets", "There was an error loading your notification settings.": "There was an error loading your notification settings.", "Failed to save your profile": "Failed to save your profile", "The operation could not be completed": "The operation could not be completed", From 2e295a94ed61abc21ea1e404eaa5d2fda166cbd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 13 Jul 2021 08:17:51 +0200 Subject: [PATCH 0256/1171] Don't export IProps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/elements/ImageView.tsx | 2 +- src/components/views/messages/MImageBody.tsx | 8 +++----- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/components/views/elements/ImageView.tsx b/src/components/views/elements/ImageView.tsx index 9d9559cdd1..91206e67e8 100644 --- a/src/components/views/elements/ImageView.tsx +++ b/src/components/views/elements/ImageView.tsx @@ -43,7 +43,7 @@ const ZOOM_COEFFICIENT = 0.0025; // If we have moved only this much we can zoom const ZOOM_DISTANCE = 10; -export interface IProps { +interface IProps { src: string; // the source of the image being displayed name?: string; // the main title ('name') for the image link?: string; // the link (if any) applied to the name of the image diff --git a/src/components/views/messages/MImageBody.tsx b/src/components/views/messages/MImageBody.tsx index cd0e259bef..48e5743212 100644 --- a/src/components/views/messages/MImageBody.tsx +++ b/src/components/views/messages/MImageBody.tsx @@ -16,12 +16,11 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { createRef } from 'react'; +import React, { ComponentProps, createRef } from 'react'; import { Blurhash } from "react-blurhash"; import MFileBody from './MFileBody'; import Modal from '../../../Modal'; -import * as sdk from '../../../index'; import { decryptFile } from '../../../utils/DecryptFile'; import { _t } from '../../../languageHandler'; import SettingsStore from "../../../settings/SettingsStore"; @@ -33,7 +32,7 @@ import { BLURHASH_FIELD } from "../../../ContentMessages"; import { MatrixEvent } from 'matrix-js-sdk/src/models/event'; import { RoomPermalinkCreator } from '../../../utils/permalinks/Permalinks'; import { IMediaEventContent } from '../../../customisations/models/IMediaEventContent'; -import { IProps as ImageViewIProps } from "../elements/ImageView"; +import ImageView from '../elements/ImageView'; export interface IProps { /* the MatrixEvent to show */ @@ -115,8 +114,7 @@ export default class MImageBody extends React.Component { const content = this.props.mxEvent.getContent() as IMediaEventContent; const httpUrl = this.getContentUrl(); - const ImageView = sdk.getComponent("elements.ImageView"); - const params: ImageViewIProps = { + const params: ComponentProps = { src: httpUrl, name: content.body?.length > 0 ? content.body : _t('Attachment'), mxEvent: this.props.mxEvent, From 7bd7f704f91cd37dd2dd6b2c54ce073f8b774d9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 13 Jul 2021 08:20:17 +0200 Subject: [PATCH 0257/1171] Extend IDialogProps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/elements/ImageView.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/elements/ImageView.tsx b/src/components/views/elements/ImageView.tsx index 91206e67e8..94f60d29eb 100644 --- a/src/components/views/elements/ImageView.tsx +++ b/src/components/views/elements/ImageView.tsx @@ -33,6 +33,7 @@ import { replaceableComponent } from "../../../utils/replaceableComponent"; import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks"; import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { normalizeWheelEvent } from "../../../utils/Mouse"; +import { IDialogProps } from '../dialogs/IDialogProps'; // Max scale to keep gaps around the image const MAX_SCALE = 0.95; @@ -43,14 +44,13 @@ const ZOOM_COEFFICIENT = 0.0025; // If we have moved only this much we can zoom const ZOOM_DISTANCE = 10; -interface IProps { +interface IProps extends IDialogProps { src: string; // the source of the image being displayed name?: string; // the main title ('name') for the image link?: string; // the link (if any) applied to the name of the image width?: number; // width of the image src in pixels height?: number; // height of the image src in pixels fileSize?: number; // size of the image src in bytes - onFinished?(): void; // callback when the lightbox is dismissed // the event (if any) that the Image is displaying. Used for event-specific stuff like // redactions, senders, timestamps etc. Other descriptors are taken from the explicit From cbe94c3c5fbd84b1f24ddf79a94d6e90c0ae37ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 13 Jul 2021 08:23:01 +0200 Subject: [PATCH 0258/1171] Kill-off sdk.getComponent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/elements/ReplyThread.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/components/views/elements/ReplyThread.js b/src/components/views/elements/ReplyThread.js index b6368eb5b3..c22225f766 100644 --- a/src/components/views/elements/ReplyThread.js +++ b/src/components/views/elements/ReplyThread.js @@ -16,7 +16,6 @@ See the License for the specific language governing permissions and limitations under the License. */ import React from 'react'; -import * as sdk from '../../../index'; import { _t } from '../../../languageHandler'; import PropTypes from 'prop-types'; import dis from '../../../dispatcher/dispatcher'; @@ -31,6 +30,9 @@ import { Action } from "../../../dispatcher/actions"; import sanitizeHtml from "sanitize-html"; import { PERMITTED_URL_SCHEMES } from "../../../HtmlUtils"; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import Spinner from './Spinner'; +import ReplyTile from "../rooms/ReplyTile"; +import Pill from './Pill'; // 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 @@ -352,7 +354,6 @@ export default class ReplyThread extends React.Component {
    ; } else if (this.state.loadedEv) { const ev = this.state.loadedEv; - const Pill = sdk.getComponent('elements.Pill'); const room = this.context.getRoom(ev.getRoomId()); header =
    { @@ -370,11 +371,9 @@ export default class ReplyThread extends React.Component { }
    ; } else if (this.state.loading) { - const Spinner = sdk.getComponent("elements.Spinner"); header = ; } - const ReplyTile = sdk.getComponent('views.rooms.ReplyTile'); const evTiles = this.state.events.map((ev) => { return
    Date: Tue, 13 Jul 2021 00:23:56 -0600 Subject: [PATCH 0259/1171] Copy over the whole feature of changing the state for keywords entirely --- .../views/settings/Notifications.tsx | 34 +++++++++++++++++-- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/src/components/views/settings/Notifications.tsx b/src/components/views/settings/Notifications.tsx index 6d74e19ab1..6baac8892e 100644 --- a/src/components/views/settings/Notifications.tsx +++ b/src/components/views/settings/Notifications.tsx @@ -17,7 +17,7 @@ limitations under the License. import React from "react"; import Spinner from "../elements/Spinner"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; -import { IAnnotatedPushRule, IPusher, PushRuleKind, RuleId } from "matrix-js-sdk/src/@types/PushRules"; +import { IAnnotatedPushRule, IPusher, PushRuleAction, PushRuleKind, RuleId } from "matrix-js-sdk/src/@types/PushRules"; import { ContentRules, IContentRules, @@ -351,12 +351,40 @@ export default class Notifications extends React.PureComponent { this.setState({ phase: Phase.Persisting }); try { + const cli = MatrixClientPeg.get(); if (rule.ruleId === KEYWORD_RULE_ID) { - console.log("@@ KEYWORDS"); + // Update all the keywords + for (const rule of this.state.vectorKeywordRuleInfo.rules) { + let enabled: boolean; + let actions: PushRuleAction[]; + if (checkedState === VectorState.On) { + if (rule.actions.length !== 1) { // XXX: Magic number + actions = PushRuleVectorState.actionsFor(checkedState); + } + if (this.state.vectorKeywordRuleInfo.vectorState === VectorState.Off) { + enabled = true; + } + } else if (checkedState === VectorState.Loud) { + if (rule.actions.length !== 3) { // XXX: Magic number + actions = PushRuleVectorState.actionsFor(checkedState); + } + if (this.state.vectorKeywordRuleInfo.vectorState === VectorState.Off) { + enabled = true; + } + } else { + enabled = false; + } + + if (actions) { + await cli.setPushRuleActions('global', rule.kind, rule.rule_id, actions); + } + if (enabled !== undefined) { + await cli.setPushRuleEnabled('global', rule.kind, rule.rule_id, enabled); + } + } } else { const definition = VectorPushRulesDefinitions[rule.ruleId]; const actions = definition.vectorStateToActions[checkedState]; - const cli = MatrixClientPeg.get(); if (!actions) { await cli.setPushRuleEnabled('global', rule.rule.kind, rule.rule.rule_id, false); } else { From 5f81cfe9d91316b71b099870014df3443bc4c8c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 13 Jul 2021 08:24:18 +0200 Subject: [PATCH 0260/1171] Nicer formatting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/css/views/rooms/_ReplyTile.scss | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/res/css/views/rooms/_ReplyTile.scss b/res/css/views/rooms/_ReplyTile.scss index d8184d01be..8bf1d168f3 100644 --- a/res/css/views/rooms/_ReplyTile.scss +++ b/res/css/views/rooms/_ReplyTile.scss @@ -70,7 +70,14 @@ limitations under the License. -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 { + + .markdown-body blockquote, + .markdown-body dl, + .markdown-body ol, + .markdown-body p, + .markdown-body pre, + .markdown-body table, + .markdown-body ul { margin-bottom: 4px; } } From ae5e10ff0cefa79c22b584d018cea6738eeb833f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 13 Jul 2021 08:45:36 +0200 Subject: [PATCH 0261/1171] Burn sdk.getComponent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/rooms/ReplyPreview.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/components/views/rooms/ReplyPreview.js b/src/components/views/rooms/ReplyPreview.js index ca95dbb62f..2e06cb57bd 100644 --- a/src/components/views/rooms/ReplyPreview.js +++ b/src/components/views/rooms/ReplyPreview.js @@ -16,12 +16,12 @@ limitations under the License. import React from 'react'; import dis from '../../../dispatcher/dispatcher'; -import * as sdk from '../../../index'; import { _t } from '../../../languageHandler'; import RoomViewStore from '../../../stores/RoomViewStore'; import PropTypes from "prop-types"; import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks"; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import ReplyTile from './ReplyTile'; function cancelQuoting() { dis.dispatch({ @@ -69,8 +69,6 @@ export default class ReplyPreview extends React.Component { render() { if (!this.state.event) return null; - const ReplyTile = sdk.getComponent('rooms.ReplyTile'); - return
    From 04098dc74cd106eadf58338de6b64d49971aea68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 13 Jul 2021 08:46:45 +0200 Subject: [PATCH 0262/1171] Remove unnecessary constructor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/messages/MImageReplyBody.tsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/components/views/messages/MImageReplyBody.tsx b/src/components/views/messages/MImageReplyBody.tsx index da720fc00f..cf60ef2ed0 100644 --- a/src/components/views/messages/MImageReplyBody.tsx +++ b/src/components/views/messages/MImageReplyBody.tsx @@ -15,16 +15,12 @@ limitations under the License. */ import React from "react"; -import MImageBody, { IProps as MImageBodyIProps } from "./MImageBody"; +import MImageBody from "./MImageBody"; import { presentableTextForFile } from "./MFileBody"; import { IMediaEventContent } from "../../../customisations/models/IMediaEventContent"; import SenderProfile from "./SenderProfile"; export default class MImageReplyBody extends MImageBody { - constructor(props: MImageBodyIProps) { - super(props); - } - public onClick = (ev: React.MouseEvent): void => { ev.preventDefault(); }; From b5baf404be3124faf64d2a7d5e00f55abb2f798c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 13 Jul 2021 08:47:37 +0200 Subject: [PATCH 0263/1171] Don't use as MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/messages/MImageReplyBody.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/messages/MImageReplyBody.tsx b/src/components/views/messages/MImageReplyBody.tsx index cf60ef2ed0..9a12cd454c 100644 --- a/src/components/views/messages/MImageReplyBody.tsx +++ b/src/components/views/messages/MImageReplyBody.tsx @@ -39,7 +39,7 @@ export default class MImageReplyBody extends MImageBody { return super.render(); } - const content = this.props.mxEvent.getContent() as IMediaEventContent; + const content = this.props.mxEvent.getContent(); const contentUrl = this.getContentUrl(); const thumbnail = this.messageContent(contentUrl, this.getThumbUrl(), content); From 70e94f9af5d7b18d8855bd13bb0cabfb170a4fca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 13 Jul 2021 08:48:43 +0200 Subject: [PATCH 0264/1171] Formatting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/messages/MImageReplyBody.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/views/messages/MImageReplyBody.tsx b/src/components/views/messages/MImageReplyBody.tsx index 9a12cd454c..74cb8ac7a9 100644 --- a/src/components/views/messages/MImageReplyBody.tsx +++ b/src/components/views/messages/MImageReplyBody.tsx @@ -50,9 +50,9 @@ export default class MImageReplyBody extends MImageBody { />; return
    -
    {thumbnail}
    -
    {sender}
    -
    {fileBody}
    +
    { thumbnail }
    +
    { sender }
    +
    { fileBody }
    ; } } From 8f8377a71ccd5d6345457ad698632d1a2a365ef1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 13 Jul 2021 09:16:01 +0200 Subject: [PATCH 0265/1171] Types MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/messages/MImageBody.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/views/messages/MImageBody.tsx b/src/components/views/messages/MImageBody.tsx index 48e5743212..c56ec2f6c8 100644 --- a/src/components/views/messages/MImageBody.tsx +++ b/src/components/views/messages/MImageBody.tsx @@ -33,6 +33,7 @@ import { MatrixEvent } from 'matrix-js-sdk/src/models/event'; import { RoomPermalinkCreator } from '../../../utils/permalinks/Permalinks'; import { IMediaEventContent } from '../../../customisations/models/IMediaEventContent'; import ImageView from '../elements/ImageView'; +import { SyncState } from 'matrix-js-sdk/src/sync.api'; export interface IProps { /* the MatrixEvent to show */ @@ -85,7 +86,7 @@ export default class MImageBody extends React.Component { } // FIXME: factor this out and apply it to MVideoBody and MAudioBody too! - private onClientSync = (syncState, prevState): void => { + private onClientSync = (syncState: SyncState, prevState: SyncState): void => { if (this.unmounted) return; // Consider the client reconnected if there is no error with syncing. // This means the state could be RECONNECTING, SYNCING, PREPARED or CATCHUP. From 5fc35565df19698fab4528175a3326ef8a472036 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 13 Jul 2021 09:16:53 +0200 Subject: [PATCH 0266/1171] More TS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/messages/MImageBody.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/views/messages/MImageBody.tsx b/src/components/views/messages/MImageBody.tsx index c56ec2f6c8..b4cb67e055 100644 --- a/src/components/views/messages/MImageBody.tsx +++ b/src/components/views/messages/MImageBody.tsx @@ -113,13 +113,14 @@ export default class MImageBody extends React.Component { return; } - const content = this.props.mxEvent.getContent() as IMediaEventContent; + const content = this.props.mxEvent.getContent(); const httpUrl = this.getContentUrl(); const params: ComponentProps = { src: httpUrl, name: content.body?.length > 0 ? content.body : _t('Attachment'), mxEvent: this.props.mxEvent, permalinkCreator: this.props.permalinkCreator, + onFinished: () => {}, }; if (content.info) { From 2a403f6cfef977372af42eac6610822c33fa9b3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 13 Jul 2021 09:17:18 +0200 Subject: [PATCH 0267/1171] Remove additional ? MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/messages/MImageBody.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/messages/MImageBody.tsx b/src/components/views/messages/MImageBody.tsx index b4cb67e055..a72cfa01d4 100644 --- a/src/components/views/messages/MImageBody.tsx +++ b/src/components/views/messages/MImageBody.tsx @@ -135,7 +135,7 @@ export default class MImageBody extends React.Component { private isGif = (): boolean => { const content = this.props.mxEvent.getContent(); - return content?.info?.mimetype === "image/gif"; + return content.info?.mimetype === "image/gif"; }; private onImageEnter = (e: React.MouseEvent): void => { From bdbd03c4ff0eb7080faa4171c1de4f56d82056da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 13 Jul 2021 09:18:05 +0200 Subject: [PATCH 0268/1171] Types MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/messages/MImageBody.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/messages/MImageBody.tsx b/src/components/views/messages/MImageBody.tsx index a72cfa01d4..f3ef1bf304 100644 --- a/src/components/views/messages/MImageBody.tsx +++ b/src/components/views/messages/MImageBody.tsx @@ -138,7 +138,7 @@ export default class MImageBody extends React.Component { return content.info?.mimetype === "image/gif"; }; - private onImageEnter = (e: React.MouseEvent): void => { + private onImageEnter = (e: React.MouseEvent): void => { this.setState({ hover: true }); if (!this.state.showImage || !this.isGif() || SettingsStore.getValue("autoplayGifsAndVideos")) { From fa4977c4da0b9e9875bafb567358c75e46a4a71e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 13 Jul 2021 09:18:34 +0200 Subject: [PATCH 0269/1171] Use current target MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/messages/MImageBody.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/messages/MImageBody.tsx b/src/components/views/messages/MImageBody.tsx index f3ef1bf304..91f1315f7a 100644 --- a/src/components/views/messages/MImageBody.tsx +++ b/src/components/views/messages/MImageBody.tsx @@ -144,7 +144,7 @@ export default class MImageBody extends React.Component { if (!this.state.showImage || !this.isGif() || SettingsStore.getValue("autoplayGifsAndVideos")) { return; } - const imgElement = e.target as HTMLImageElement; + const imgElement = e.currentTarget; imgElement.src = this.getContentUrl(); }; From 6193bc2a828aab69251e4fb09ef0c0b4731bbf82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 13 Jul 2021 09:19:19 +0200 Subject: [PATCH 0270/1171] Types MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/messages/MImageBody.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/messages/MImageBody.tsx b/src/components/views/messages/MImageBody.tsx index 91f1315f7a..35975109e7 100644 --- a/src/components/views/messages/MImageBody.tsx +++ b/src/components/views/messages/MImageBody.tsx @@ -148,7 +148,7 @@ export default class MImageBody extends React.Component { imgElement.src = this.getContentUrl(); }; - private onImageLeave = (e: React.MouseEvent): void => { + private onImageLeave = (e: React.MouseEvent): void => { this.setState({ hover: false }); if (!this.state.showImage || !this.isGif() || SettingsStore.getValue("autoplayGifsAndVideos")) { From 86580f3f20f7bef1937a8416c07a541514ea0c91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 13 Jul 2021 09:19:45 +0200 Subject: [PATCH 0271/1171] current target MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/messages/MImageBody.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/messages/MImageBody.tsx b/src/components/views/messages/MImageBody.tsx index 35975109e7..a669505181 100644 --- a/src/components/views/messages/MImageBody.tsx +++ b/src/components/views/messages/MImageBody.tsx @@ -154,7 +154,7 @@ export default class MImageBody extends React.Component { if (!this.state.showImage || !this.isGif() || SettingsStore.getValue("autoplayGifsAndVideos")) { return; } - const imgElement = e.target as HTMLImageElement; + const imgElement = e.currentTarget; imgElement.src = this.getThumbUrl(); }; From af7769ce935a39c525adede743056963f765d8c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 13 Jul 2021 09:20:13 +0200 Subject: [PATCH 0272/1171] Types! MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/messages/MImageBody.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/messages/MImageBody.tsx b/src/components/views/messages/MImageBody.tsx index a669505181..1e9678dbef 100644 --- a/src/components/views/messages/MImageBody.tsx +++ b/src/components/views/messages/MImageBody.tsx @@ -195,7 +195,7 @@ export default class MImageBody extends React.Component { const thumbWidth = 800; const thumbHeight = 600; - const content = this.props.mxEvent.getContent() as IMediaEventContent; + const content = this.props.mxEvent.getContent(); const media = mediaFromContent(content); if (media.isEncrypted) { From 4cf4ab2266959370f78eea4919cb0237813085ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 13 Jul 2021 09:20:40 +0200 Subject: [PATCH 0273/1171] Return type MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/messages/MImageBody.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/messages/MImageBody.tsx b/src/components/views/messages/MImageBody.tsx index 1e9678dbef..a4a615fa65 100644 --- a/src/components/views/messages/MImageBody.tsx +++ b/src/components/views/messages/MImageBody.tsx @@ -439,7 +439,7 @@ export default class MImageBody extends React.Component { } // Overidden by MStickerBody - protected getPlaceholder(width: number, height: number) { + protected getPlaceholder(width: number, height: number): JSX.Element { const blurhash = this.props.mxEvent.getContent().info[BLURHASH_FIELD]; if (blurhash) return ; return
    From e4d1859fb70d9ffa9b8e8c7516e793ed56224df2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 13 Jul 2021 09:21:05 +0200 Subject: [PATCH 0274/1171] Ret type MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/messages/MImageBody.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/messages/MImageBody.tsx b/src/components/views/messages/MImageBody.tsx index a4a615fa65..2062191303 100644 --- a/src/components/views/messages/MImageBody.tsx +++ b/src/components/views/messages/MImageBody.tsx @@ -448,7 +448,7 @@ export default class MImageBody extends React.Component { } // Overidden by MStickerBody - protected getTooltip() { + protected getTooltip(): JSX.Element { return null; } From ef1a1ebe12c5033cfe01a47f3cf0bb88125c715c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 13 Jul 2021 09:21:33 +0200 Subject: [PATCH 0275/1171] TS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/messages/MImageBody.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/messages/MImageBody.tsx b/src/components/views/messages/MImageBody.tsx index 2062191303..3f5f27eca8 100644 --- a/src/components/views/messages/MImageBody.tsx +++ b/src/components/views/messages/MImageBody.tsx @@ -458,7 +458,7 @@ export default class MImageBody extends React.Component { } render() { - const content = this.props.mxEvent.getContent() as IMediaEventContent; + const content = this.props.mxEvent.getContent(); if (this.state.error !== null) { return ( From 931bba747abbf9e2fe7f4974eed55441ff71125d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 13 Jul 2021 09:22:13 +0200 Subject: [PATCH 0276/1171] Replaceable component MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/rooms/ReplyTile.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/views/rooms/ReplyTile.tsx b/src/components/views/rooms/ReplyTile.tsx index 757c273b50..227c5b6585 100644 --- a/src/components/views/rooms/ReplyTile.tsx +++ b/src/components/views/rooms/ReplyTile.tsx @@ -35,6 +35,7 @@ interface IProps { onHeightChanged?(): void; } +@replaceableComponent("views.rooms.ReplyTile") export default class ReplyTile extends React.PureComponent { static defaultProps = { onHeightChanged: () => {}, From 63ad95246a0a62bf5e24a207c5a054dd2764c89d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 13 Jul 2021 09:23:57 +0200 Subject: [PATCH 0277/1171] EventType enum! MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/rooms/ReplyTile.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/components/views/rooms/ReplyTile.tsx b/src/components/views/rooms/ReplyTile.tsx index 227c5b6585..775091a59f 100644 --- a/src/components/views/rooms/ReplyTile.tsx +++ b/src/components/views/rooms/ReplyTile.tsx @@ -26,6 +26,7 @@ import SenderProfile from "../messages/SenderProfile"; import TextualBody from "../messages/TextualBody"; import MImageReplyBody from "../messages/MImageReplyBody"; import * as sdk from '../../../index'; +import { EventType } from 'matrix-js-sdk/src/@types/event'; interface IProps { mxEvent: MatrixEvent; @@ -78,9 +79,11 @@ export default class ReplyTile extends React.PureComponent { 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 isInfoMessage = [ + EventType.RoomMessage, + EventType.Sticker, + EventType.RoomCreate, + ].includes(eventType as EventType); let tileHandler = getHandlerTile(this.props.mxEvent); // If we're showing hidden events in the timeline, we should use the From 22b029d11672186296558f4e570e76f0b851e925 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 13 Jul 2021 09:24:40 +0200 Subject: [PATCH 0278/1171] Relation type MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/rooms/ReplyTile.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/rooms/ReplyTile.tsx b/src/components/views/rooms/ReplyTile.tsx index 775091a59f..8807be680c 100644 --- a/src/components/views/rooms/ReplyTile.tsx +++ b/src/components/views/rooms/ReplyTile.tsx @@ -26,7 +26,7 @@ import SenderProfile from "../messages/SenderProfile"; import TextualBody from "../messages/TextualBody"; import MImageReplyBody from "../messages/MImageReplyBody"; import * as sdk from '../../../index'; -import { EventType } from 'matrix-js-sdk/src/@types/event'; +import { EventType, RelationType } from 'matrix-js-sdk/src/@types/event'; interface IProps { mxEvent: MatrixEvent; @@ -90,7 +90,7 @@ export default class ReplyTile extends React.PureComponent { // 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"); + const useSource = !tileHandler || this.props.mxEvent.isRelation(RelationType.Replace); if (useSource && SettingsStore.getValue("showHiddenEventsInTimeline")) { tileHandler = "messages.ViewSourceEvent"; // Reuse info message avatar and sender profile styling From 0bf595d8d494f6bcd2e9d38c2147be1eb39099f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 13 Jul 2021 09:26:27 +0200 Subject: [PATCH 0279/1171] Enums MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/rooms/ReplyTile.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/rooms/ReplyTile.tsx b/src/components/views/rooms/ReplyTile.tsx index 8807be680c..f6a4bd7a18 100644 --- a/src/components/views/rooms/ReplyTile.tsx +++ b/src/components/views/rooms/ReplyTile.tsx @@ -26,7 +26,7 @@ import SenderProfile from "../messages/SenderProfile"; import TextualBody from "../messages/TextualBody"; import MImageReplyBody from "../messages/MImageReplyBody"; import * as sdk from '../../../index'; -import { EventType, RelationType } from 'matrix-js-sdk/src/@types/event'; +import { EventType, MsgType, RelationType } from 'matrix-js-sdk/src/@types/event'; interface IProps { mxEvent: MatrixEvent; @@ -119,7 +119,7 @@ export default class ReplyTile extends React.PureComponent { } let sender; - const needsSenderProfile = msgtype !== 'm.image' && tileHandler !== 'messages.RoomCreate' && !isInfoMessage; + const needsSenderProfile = msgtype !== MsgType.Image && tileHandler !== EventType.RoomCreate && !isInfoMessage; if (needsSenderProfile) { sender = Date: Tue, 13 Jul 2021 09:27:22 +0200 Subject: [PATCH 0280/1171] More compact classNames MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/rooms/ReplyTile.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/views/rooms/ReplyTile.tsx b/src/components/views/rooms/ReplyTile.tsx index f6a4bd7a18..1b9f3e2fac 100644 --- a/src/components/views/rooms/ReplyTile.tsx +++ b/src/components/views/rooms/ReplyTile.tsx @@ -108,8 +108,7 @@ export default class ReplyTile extends React.PureComponent { const EventTileType = sdk.getComponent(tileHandler); - const classes = classNames({ - mx_ReplyTile: true, + const classes = classNames("mx_ReplyTile", { mx_ReplyTile_info: isInfoMessage, }); From c44de3bea817bab53c3bcc74ecad7ef993d97a63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 13 Jul 2021 09:30:52 +0200 Subject: [PATCH 0281/1171] Enums MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/rooms/ReplyTile.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/views/rooms/ReplyTile.tsx b/src/components/views/rooms/ReplyTile.tsx index 1b9f3e2fac..593ebffedd 100644 --- a/src/components/views/rooms/ReplyTile.tsx +++ b/src/components/views/rooms/ReplyTile.tsx @@ -128,15 +128,15 @@ export default class ReplyTile extends React.PureComponent { } const msgtypeOverrides = { - "m.image": MImageReplyBody, + [MsgType.Image]: MImageReplyBody, // We don't want a download link for files, just the file name is enough. - "m.file": TextualBody, + [MsgType.File]: TextualBody, "m.sticker": TextualBody, - "m.audio": TextualBody, - "m.video": TextualBody, + [MsgType.Audio]: TextualBody, + [MsgType.Video]: TextualBody, }; const evOverrides = { - "m.sticker": TextualBody, + [EventType.Sticker]: TextualBody, }; return ( From 069180b16dda2cf02c97569017cbd27064f534ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 13 Jul 2021 09:31:28 +0200 Subject: [PATCH 0282/1171] Remove contructor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/rooms/ReplyTile.tsx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/components/views/rooms/ReplyTile.tsx b/src/components/views/rooms/ReplyTile.tsx index 593ebffedd..9cc42faca3 100644 --- a/src/components/views/rooms/ReplyTile.tsx +++ b/src/components/views/rooms/ReplyTile.tsx @@ -42,10 +42,6 @@ export default class ReplyTile extends React.PureComponent { onHeightChanged: () => {}, }; - constructor(props: IProps) { - super(props); - } - componentDidMount() { this.props.mxEvent.on("Event.decrypted", this.onDecrypted); } From 43cf7bc6110cdbe6750f5a239224a0813e758ebb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 13 Jul 2021 09:33:45 +0200 Subject: [PATCH 0283/1171] Remove 0px MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/css/views/rooms/_ReplyTile.scss | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/res/css/views/rooms/_ReplyTile.scss b/res/css/views/rooms/_ReplyTile.scss index 8bf1d168f3..04dc34092a 100644 --- a/res/css/views/rooms/_ReplyTile.scss +++ b/res/css/views/rooms/_ReplyTile.scss @@ -83,7 +83,7 @@ limitations under the License. } .mx_ReplyTile.mx_ReplyTile_info { - padding-top: 0px; + padding-top: 0; } .mx_ReplyTile .mx_SenderProfile { @@ -92,10 +92,10 @@ limitations under the License. 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; + padding-left: 0; /* left gutter */ + padding-bottom: 0; + padding-top: 0; + margin: 0; line-height: 17px; /* the next three lines, along with overflow hidden, truncate long display names */ white-space: nowrap; From e01d1572ac1521d2b4f845c794bd0a81762fb53d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 13 Jul 2021 09:34:43 +0200 Subject: [PATCH 0284/1171] Formatting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/messages/MFileBody.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/views/messages/MFileBody.js b/src/components/views/messages/MFileBody.js index f6346e56d9..e95f397e40 100644 --- a/src/components/views/messages/MFileBody.js +++ b/src/components/views/messages/MFileBody.js @@ -173,7 +173,9 @@ export default class MFileBody extends React.Component { placeholder = (
    - {presentableTextForFile(content, false)} + + { presentableTextForFile(content, false) } +
    ); } From 562d43e81c48fae61a51a0561b28f248ff086238 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 13 Jul 2021 09:36:31 +0200 Subject: [PATCH 0285/1171] Font MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/css/views/rooms/_ReplyTile.scss | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/res/css/views/rooms/_ReplyTile.scss b/res/css/views/rooms/_ReplyTile.scss index 04dc34092a..ff3a0d07d1 100644 --- a/res/css/views/rooms/_ReplyTile.scss +++ b/res/css/views/rooms/_ReplyTile.scss @@ -19,7 +19,7 @@ limitations under the License. clear: both; padding-top: 2px; padding-bottom: 2px; - font-size: 14px; + font-size: $font-14px; position: relative; line-height: 16px; } @@ -43,7 +43,7 @@ limitations under the License. // We do reply size limiting with CSS to avoid duplicating the TextualBody component. .mx_ReplyTile .mx_EventTile_content { $reply-lines: 2; - $line-height: 22px; + $line-height: $font-22px; $max-height: 66px; pointer-events: none; @@ -58,7 +58,7 @@ limitations under the License. .mx_EventTile_body.mx_EventTile_bigEmoji { line-height: $line-height !important; // Override the big emoji override - font-size: 14px !important; + font-size: $font-14px !important; } // Hack to cut content in
     tags too
    @@ -88,7 +88,7 @@ limitations under the License.
     
     .mx_ReplyTile .mx_SenderProfile {
         color: $primary-fg-color;
    -    font-size: 14px;
    +    font-size: $font-14px;
         display: inline-block; /* anti-zalgo, with overflow hidden */
         overflow: hidden;
         cursor: pointer;
    @@ -96,7 +96,7 @@ limitations under the License.
         padding-bottom: 0;
         padding-top: 0;
         margin: 0;
    -    line-height: 17px;
    +    line-height: $font-17px;
         /* the next three lines, along with overflow hidden, truncate long display names */
         white-space: nowrap;
         text-overflow: ellipsis;
    
    From 9455a6d77270595d1b8c07d17c4d00a0a1332293 Mon Sep 17 00:00:00 2001
    From: =?UTF-8?q?=C5=A0imon=20Brandner?= 
    Date: Tue, 13 Jul 2021 09:40:29 +0200
    Subject: [PATCH 0286/1171] Import replaceableComponent
    MIME-Version: 1.0
    Content-Type: text/plain; charset=UTF-8
    Content-Transfer-Encoding: 8bit
    
    Signed-off-by: Šimon Brandner 
    ---
     src/components/views/rooms/ReplyTile.tsx | 1 +
     1 file changed, 1 insertion(+)
    
    diff --git a/src/components/views/rooms/ReplyTile.tsx b/src/components/views/rooms/ReplyTile.tsx
    index 9cc42faca3..fdd43e3200 100644
    --- a/src/components/views/rooms/ReplyTile.tsx
    +++ b/src/components/views/rooms/ReplyTile.tsx
    @@ -27,6 +27,7 @@ import TextualBody from "../messages/TextualBody";
     import MImageReplyBody from "../messages/MImageReplyBody";
     import * as sdk from '../../../index';
     import { EventType, MsgType, RelationType } from 'matrix-js-sdk/src/@types/event';
    +import { replaceableComponent } from '../../../utils/replaceableComponent';
     
     interface IProps {
         mxEvent: MatrixEvent;
    
    From bc7a8f8406e960772e16932dd4df96daf38ba6b8 Mon Sep 17 00:00:00 2001
    From: =?UTF-8?q?=C5=A0imon=20Brandner?= 
    Date: Tue, 13 Jul 2021 10:12:24 +0200
    Subject: [PATCH 0287/1171] Handle redaction
    MIME-Version: 1.0
    Content-Type: text/plain; charset=UTF-8
    Content-Transfer-Encoding: 8bit
    
    Signed-off-by: Šimon Brandner 
    ---
     src/components/views/rooms/ReplyTile.tsx | 6 ++++++
     1 file changed, 6 insertions(+)
    
    diff --git a/src/components/views/rooms/ReplyTile.tsx b/src/components/views/rooms/ReplyTile.tsx
    index fdd43e3200..fd90d2d536 100644
    --- a/src/components/views/rooms/ReplyTile.tsx
    +++ b/src/components/views/rooms/ReplyTile.tsx
    @@ -45,6 +45,7 @@ export default class ReplyTile extends React.PureComponent {
     
         componentDidMount() {
             this.props.mxEvent.on("Event.decrypted", this.onDecrypted);
    +        this.props.mxEvent.on("Event.beforeRedaction", this.onBeforeRedaction);
         }
     
         componentWillUnmount() {
    @@ -58,6 +59,11 @@ export default class ReplyTile extends React.PureComponent {
             }
         };
     
    +    private onBeforeRedaction = (): void => {
    +        // When the event gets redacted, update it, so that a different tile handler is used
    +        this.forceUpdate();
    +    };
    +
         private onClick = (e: React.MouseEvent): void => {
             // 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 1061cb0ffb2a2122411f4b075848edd442356407 Mon Sep 17 00:00:00 2001
    From: Germain Souquet 
    Date: Tue, 13 Jul 2021 10:15:12 +0200
    Subject: [PATCH 0288/1171] Fix layout regressions in message bubbles
    
    ---
     res/css/views/rooms/_EventBubbleTile.scss     | 44 ++++++++++++++++---
     res/themes/dark/css/_dark.scss                |  1 +
     .../legacy-light/css/_legacy-light.scss       |  1 +
     res/themes/light/css/_light.scss              |  1 +
     4 files changed, 40 insertions(+), 7 deletions(-)
    
    diff --git a/res/css/views/rooms/_EventBubbleTile.scss b/res/css/views/rooms/_EventBubbleTile.scss
    index 313027bde6..48011951cc 100644
    --- a/res/css/views/rooms/_EventBubbleTile.scss
    +++ b/res/css/views/rooms/_EventBubbleTile.scss
    @@ -67,7 +67,7 @@ limitations under the License.
         .mx_SenderProfile {
             position: relative;
             top: -2px;
    -        left: calc(-1 * var(--gutterSize));
    +        left: 2px;
         }
     
         &[data-self=false] {
    @@ -75,7 +75,7 @@ limitations under the License.
                 border-bottom-right-radius: var(--cornerRadius);
             }
             .mx_EventTile_avatar {
    -            left: -48px;
    +            left: -34px;
             }
     
             .mx_MessageActionBar {
    @@ -91,7 +91,7 @@ limitations under the License.
                 float: right;
                 > a {
                     left: auto;
    -                right: -35px;
    +                right: -48px;
                 }
             }
             .mx_SenderProfile {
    @@ -123,10 +123,10 @@ limitations under the License.
             background: var(--backgroundColor);
             display: flex;
             gap: 5px;
    -        margin: 0 -12px 0 -22px;
    +        margin: 0 -12px 0 -9px;
             > a {
                 position: absolute;
    -            left: -35px;
    +            left: -48px;
             }
         }
     
    @@ -167,6 +167,7 @@ limitations under the License.
                 margin: 0 calc(-1 * var(--gutterSize));
     
                 .mx_EventTile_reply {
    +                max-width: 90%;
                     padding: 0;
                     > a {
                         display: none !important;
    @@ -186,6 +187,23 @@ limitations under the License.
             }
         }
     
    +    .mx_EditMessageComposer_buttons {
    +        position: static;
    +        padding: 0;
    +        margin: 0;
    +        background: transparent;
    +    }
    +
    +    .mx_ReactionsRow {
    +        margin-right: -18px;
    +        margin-left: -9px;
    +    }
    +
    +    .mx_ReplyThread {
    +        border-left-width: 2px;
    +        border-left-color: $eventbubble-reply-color;
    +    }
    +
         &.mx_EventTile_bubbleContainer,
         &.mx_EventTile_info,
         & ~ .mx_EventListSummary[data-expanded=false] {
    @@ -225,6 +243,19 @@ limitations under the License.
             .mx_EventTile {
                 margin: 0 58px;
             }
    +
    +        .mx_EventTile_line {
    +            margin: 0 5px;
    +            > a {
    +                left: auto;
    +                right: 0;
    +                transform: translateX(calc(100% + 5px));
    +            }
    +        }
    +
    +        .mx_MessageActionBar {
    +            transform: translate3d(50%, 0, 0);
    +        }
         }
     
         /* events that do not require bubble layout */
    @@ -283,7 +314,6 @@ limitations under the License.
         }
     
         .mx_MTextBody {
    -        /* 30px equates to the width of the timestamp */
    -        max-width: calc(100% - 35px - var(--gutterSize));
    +        max-width: 100%;
         }
     }
    diff --git a/res/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss
    index 0b3444c95b..a43936c46e 100644
    --- a/res/themes/dark/css/_dark.scss
    +++ b/res/themes/dark/css/_dark.scss
    @@ -234,6 +234,7 @@ $eventbubble-self-bg: #143A34;
     $eventbubble-others-bg: #394049;
     $eventbubble-bg-hover: #433C23;
     $eventbubble-avatar-outline: $bg-color;
    +$eventbubble-reply-color: #C1C6CD;
     
     // ***** Mixins! *****
     
    diff --git a/res/themes/legacy-light/css/_legacy-light.scss b/res/themes/legacy-light/css/_legacy-light.scss
    index e485028774..f349a804a8 100644
    --- a/res/themes/legacy-light/css/_legacy-light.scss
    +++ b/res/themes/legacy-light/css/_legacy-light.scss
    @@ -352,6 +352,7 @@ $eventbubble-self-bg: #F8FDFC;
     $eventbubble-others-bg: #F7F8F9;
     $eventbubble-bg-hover: rgb(242, 242, 242);
     $eventbubble-avatar-outline: #fff;
    +$eventbubble-reply-color: #C1C6CD;
     
     // ***** Mixins! *****
     
    diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss
    index 6f0bcadaf7..ef5f4d8c86 100644
    --- a/res/themes/light/css/_light.scss
    +++ b/res/themes/light/css/_light.scss
    @@ -354,6 +354,7 @@ $eventbubble-self-bg: #F8FDFC;
     $eventbubble-others-bg: #F7F8F9;
     $eventbubble-bg-hover: #FEFCF5;
     $eventbubble-avatar-outline: $primary-bg-color;
    +$eventbubble-reply-color: #C1C6CD;
     
     // ***** Mixins! *****
     
    
    From 290174b0313cf42391525d6b2798fa4ac1a287c7 Mon Sep 17 00:00:00 2001
    From: Germain Souquet 
    Date: Tue, 13 Jul 2021 10:36:35 +0200
    Subject: [PATCH 0289/1171] fix group layout and IRC layout regressions
    
    ---
     res/css/views/rooms/_EventTile.scss | 70 ++++++++++++++---------------
     1 file changed, 35 insertions(+), 35 deletions(-)
    
    diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss
    index bd5b8113a9..e9d71d557c 100644
    --- a/res/css/views/rooms/_EventTile.scss
    +++ b/res/css/views/rooms/_EventTile.scss
    @@ -25,7 +25,7 @@ $hover-select-border: 4px;
         font-size: $font-14px;
         position: relative;
     
    -    .mx_EventTile.mx_EventTile_info {
    +    &.mx_EventTile_info {
             padding-top: 1px;
         }
     
    @@ -36,12 +36,12 @@ $hover-select-border: 4px;
             user-select: none;
         }
     
    -    .mx_EventTile.mx_EventTile_info .mx_EventTile_avatar {
    +    &.mx_EventTile_info .mx_EventTile_avatar {
             top: $font-6px;
             left: $left-gutter;
         }
     
    -    .mx_EventTile_continuation {
    +    &.mx_EventTile_continuation {
             padding-top: 0px !important;
     
             &.mx_EventTile_isEditing {
    @@ -50,11 +50,11 @@ $hover-select-border: 4px;
             }
         }
     
    -    .mx_EventTile_isEditing {
    +    &.mx_EventTile_isEditing {
             background-color: $header-panel-bg-color;
         }
     
    -    .mx_EventTile .mx_SenderProfile {
    +    .mx_SenderProfile {
             color: $primary-fg-color;
             font-size: $font-14px;
             display: inline-block; /* anti-zalgo, with overflow hidden */
    @@ -69,7 +69,7 @@ $hover-select-border: 4px;
             max-width: calc(100% - $left-gutter);
         }
     
    -    .mx_EventTile .mx_SenderProfile .mx_Flair {
    +    .mx_SenderProfile .mx_Flair {
             opacity: 0.7;
             margin-left: 5px;
             display: inline-block;
    @@ -84,11 +84,11 @@ $hover-select-border: 4px;
             }
         }
     
    -    .mx_EventTile_isEditing .mx_MessageTimestamp {
    +    &.mx_EventTile_isEditing .mx_MessageTimestamp {
             visibility: hidden;
         }
     
    -    .mx_EventTile .mx_MessageTimestamp {
    +    .mx_MessageTimestamp {
             display: block;
             white-space: nowrap;
             left: 0px;
    @@ -96,7 +96,7 @@ $hover-select-border: 4px;
             user-select: none;
         }
     
    -    .mx_EventTile_continuation .mx_EventTile_line {
    +    &.mx_EventTile_continuation .mx_EventTile_line {
             clear: both;
         }
     
    @@ -119,21 +119,21 @@ $hover-select-border: 4px;
             margin-right: 10px;
         }
     
    -    .mx_EventTile_selected > div > a > .mx_MessageTimestamp {
    +    &.mx_EventTile_selected > div > a > .mx_MessageTimestamp {
             left: calc(-$hover-select-border);
         }
     
         /* 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 {
    +    &.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 {
    +    &.mx_EventTile_highlight,
    +    &.mx_EventTile_highlight .markdown-body {
             color: $event-highlight-fg-color;
     
             .mx_EventTile_line {
    @@ -141,17 +141,17 @@ $hover-select-border: 4px;
             }
         }
     
    -    .mx_EventTile_info .mx_EventTile_line {
    +    &.mx_EventTile_info .mx_EventTile_line {
             padding-left: calc($left-gutter + 18px);
         }
     
    -    .mx_EventTile_selected.mx_EventTile_info .mx_EventTile_line {
    +    &.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 {
    +    &.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;
         }
     
    @@ -195,7 +195,7 @@ $hover-select-border: 4px;
             mask-image: url('$(res)/img/element-icons/circle-sending.svg');
         }
     
    -    .mx_EventTile_contextual {
    +    &.mx_EventTile_contextual {
             opacity: 0.4;
         }
     
    @@ -254,46 +254,46 @@ $hover-select-border: 4px;
             filter: none;
         }
     
    -    .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 {
    +    &:hover.mx_EventTile_verified .mx_EventTile_line,
    +    &:hover.mx_EventTile_unverified .mx_EventTile_line,
    +    &:hover.mx_EventTile_unknown .mx_EventTile_line {
             padding-left: calc($left-gutter - $hover-select-border);
         }
     
    -    .mx_EventTile:hover.mx_EventTile_verified .mx_EventTile_line {
    +    &: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 {
    +    &: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 {
    +    &: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 {
    +    &:hover.mx_EventTile_verified.mx_EventTile_info .mx_EventTile_line,
    +    &:hover.mx_EventTile_unverified.mx_EventTile_info .mx_EventTile_line,
    +    &: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 {
    +    &: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 {
    +    &:hover.mx_EventTile_verified .mx_EventTile_line > a > .mx_MessageTimestamp,
    +    &:hover.mx_EventTile_unverified .mx_EventTile_line > a > .mx_MessageTimestamp,
    +    &: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 {
    +    &:hover.mx_EventTile_verified .mx_EventTile_line > .mx_EventTile_e2eIcon,
    +    &:hover.mx_EventTile_unverified .mx_EventTile_line > .mx_EventTile_e2eIcon,
    +    &:hover.mx_EventTile_unknown .mx_EventTile_line > .mx_EventTile_e2eIcon {
             display: block;
             left: 41px;
         }
    
    From fca5125c5b1fc47c21d1cee9b856db7fd25f46a7 Mon Sep 17 00:00:00 2001
    From: =?UTF-8?q?=C5=A0imon=20Brandner?= 
    Date: Tue, 13 Jul 2021 10:36:44 +0200
    Subject: [PATCH 0290/1171] Improve redacted body look
    MIME-Version: 1.0
    Content-Type: text/plain; charset=UTF-8
    Content-Transfer-Encoding: 8bit
    
    Signed-off-by: Šimon Brandner 
    ---
     res/css/views/rooms/_ReplyTile.scss      | 2 +-
     src/components/views/rooms/ReplyTile.tsx | 2 +-
     2 files changed, 2 insertions(+), 2 deletions(-)
    
    diff --git a/res/css/views/rooms/_ReplyTile.scss b/res/css/views/rooms/_ReplyTile.scss
    index ff3a0d07d1..dee68871c2 100644
    --- a/res/css/views/rooms/_ReplyTile.scss
    +++ b/res/css/views/rooms/_ReplyTile.scss
    @@ -21,7 +21,7 @@ limitations under the License.
         padding-bottom: 2px;
         font-size: $font-14px;
         position: relative;
    -    line-height: 16px;
    +    line-height: $font-16px;
     }
     
     .mx_ReplyTile > a {
    diff --git a/src/components/views/rooms/ReplyTile.tsx b/src/components/views/rooms/ReplyTile.tsx
    index fd90d2d536..78e630a0a2 100644
    --- a/src/components/views/rooms/ReplyTile.tsx
    +++ b/src/components/views/rooms/ReplyTile.tsx
    @@ -112,7 +112,7 @@ export default class ReplyTile extends React.PureComponent {
             const EventTileType = sdk.getComponent(tileHandler);
     
             const classes = classNames("mx_ReplyTile", {
    -            mx_ReplyTile_info: isInfoMessage,
    +            mx_ReplyTile_info: isInfoMessage && !this.props.mxEvent.isRedacted(),
             });
     
             let permalink = "#";
    
    From 866a11d7e39b6746689453639018d221f40f94f3 Mon Sep 17 00:00:00 2001
    From: =?UTF-8?q?=C5=A0imon=20Brandner?= 
    Date: Tue, 13 Jul 2021 11:49:49 +0200
    Subject: [PATCH 0291/1171] Fix image alignment issues
    MIME-Version: 1.0
    Content-Type: text/plain; charset=UTF-8
    Content-Transfer-Encoding: 8bit
    
    Signed-off-by: Šimon Brandner 
    ---
     res/css/views/messages/_MImageReplyBody.scss      | 14 +++-----------
     src/components/views/messages/MImageBody.tsx      | 13 +++++++++----
     src/components/views/messages/MImageReplyBody.tsx | 10 ++++++----
     3 files changed, 18 insertions(+), 19 deletions(-)
    
    diff --git a/res/css/views/messages/_MImageReplyBody.scss b/res/css/views/messages/_MImageReplyBody.scss
    index 8c5cb97478..f0401d21db 100644
    --- a/res/css/views/messages/_MImageReplyBody.scss
    +++ b/res/css/views/messages/_MImageReplyBody.scss
    @@ -15,19 +15,11 @@ 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;
    +    display: flex;
     
         .mx_MImageBody_thumbnail_container {
    -        max-height: 44px !important;
    +        flex: 1;
    +        padding-right: 4px;
         }
     }
     
    diff --git a/src/components/views/messages/MImageBody.tsx b/src/components/views/messages/MImageBody.tsx
    index 3f5f27eca8..0acdbaf253 100644
    --- a/src/components/views/messages/MImageBody.tsx
    +++ b/src/components/views/messages/MImageBody.tsx
    @@ -332,7 +332,12 @@ export default class MImageBody extends React.Component {
         _afterComponentWillUnmount() {
         }
     
    -    protected messageContent(contentUrl: string, thumbUrl: string, content: IMediaEventContent): JSX.Element {
    +    protected messageContent(
    +        contentUrl: string,
    +        thumbUrl: string,
    +        content: IMediaEventContent,
    +        forcedHeight?: number,
    +    ): JSX.Element {
             let infoWidth;
             let infoHeight;
     
    @@ -367,7 +372,7 @@ export default class MImageBody extends React.Component {
             }
     
             // The maximum height of the thumbnail as it is rendered as an 
    -        const maxHeight = Math.min(this.props.maxImageHeight || 600, infoHeight);
    +        const maxHeight = forcedHeight || Math.min((this.props.maxImageHeight || 600), infoHeight);
             // The maximum width of the thumbnail, as dictated by its natural
             // maximum height.
             const maxWidth = infoWidth * maxHeight / infoHeight;
    @@ -407,9 +412,9 @@ export default class MImageBody extends React.Component {
             }
     
             const thumbnail = (
    -            
    +
    { /* Calculate aspect ratio, using %padding will size _container correctly */ } -
    +
    { showPlaceholder &&
    (); const contentUrl = this.getContentUrl(); - const thumbnail = this.messageContent(contentUrl, this.getThumbUrl(), content); + const thumbnail = this.messageContent(contentUrl, this.getThumbUrl(), content, 44); const fileBody = this.getFileBody(); const sender = ; return
    -
    { thumbnail }
    -
    { sender }
    -
    { fileBody }
    + { thumbnail } +
    +
    { sender }
    +
    { fileBody }
    +
    ; } } From 75e7948ca8eed1ea98925af32cfbe62024f634b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 13 Jul 2021 11:57:40 +0200 Subject: [PATCH 0292/1171] Handle event edits MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/rooms/ReplyTile.tsx | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/components/views/rooms/ReplyTile.tsx b/src/components/views/rooms/ReplyTile.tsx index 78e630a0a2..e751a8ddc3 100644 --- a/src/components/views/rooms/ReplyTile.tsx +++ b/src/components/views/rooms/ReplyTile.tsx @@ -45,11 +45,14 @@ export default class ReplyTile extends React.PureComponent { componentDidMount() { this.props.mxEvent.on("Event.decrypted", this.onDecrypted); - this.props.mxEvent.on("Event.beforeRedaction", this.onBeforeRedaction); + this.props.mxEvent.on("Event.beforeRedaction", this.onEventRequiresUpdate); + this.props.mxEvent.on("Event.replaced", this.onEventRequiresUpdate); } componentWillUnmount() { this.props.mxEvent.removeListener("Event.decrypted", this.onDecrypted); + this.props.mxEvent.removeListener("Event.beforeRedaction", this.onEventRequiresUpdate); + this.props.mxEvent.removeListener("Event.replaced", this.onEventRequiresUpdate); } private onDecrypted = (): void => { @@ -59,8 +62,8 @@ export default class ReplyTile extends React.PureComponent { } }; - private onBeforeRedaction = (): void => { - // When the event gets redacted, update it, so that a different tile handler is used + private onEventRequiresUpdate = (): void => { + // Force update when necessary - redactions and edits this.forceUpdate(); }; @@ -155,6 +158,7 @@ export default class ReplyTile extends React.PureComponent { showUrlPreview={false} overrideBodyTypes={msgtypeOverrides} overrideEventTypes={evOverrides} + replacingEventId={this.props.mxEvent.replacingEventId()} maxImageHeight={96} />
    From b4ae54dcce460a6147fed525705c35f7224f62e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 13 Jul 2021 12:15:22 +0200 Subject: [PATCH 0293/1171] Remove unused CSS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/css/views/rooms/_ReplyTile.scss | 6 ------ 1 file changed, 6 deletions(-) diff --git a/res/css/views/rooms/_ReplyTile.scss b/res/css/views/rooms/_ReplyTile.scss index dee68871c2..d059d553a9 100644 --- a/res/css/views/rooms/_ReplyTile.scss +++ b/res/css/views/rooms/_ReplyTile.scss @@ -15,8 +15,6 @@ limitations under the License. */ .mx_ReplyTile { - max-width: 100%; - clear: both; padding-top: 2px; padding-bottom: 2px; font-size: $font-14px; @@ -102,7 +100,3 @@ limitations under the License. text-overflow: ellipsis; max-width: calc(100% - 65px); } - -.mx_ReplyTile_contextual { - opacity: 0.4; -} From 8fc90e1d5341f1977cf93779897d508f57488389 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 13 Jul 2021 12:26:14 +0200 Subject: [PATCH 0294/1171] Fix isInfoMessage regression MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/rooms/ReplyTile.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/ReplyTile.tsx b/src/components/views/rooms/ReplyTile.tsx index e751a8ddc3..19da345579 100644 --- a/src/components/views/rooms/ReplyTile.tsx +++ b/src/components/views/rooms/ReplyTile.tsx @@ -85,7 +85,7 @@ export default class ReplyTile extends React.PureComponent { const eventType = this.props.mxEvent.getType(); // Info messages are basically information about commands processed on a room - let isInfoMessage = [ + let isInfoMessage = ![ EventType.RoomMessage, EventType.Sticker, EventType.RoomCreate, From d149cead5fb0348ba0c6cc8013a2e78beb4675ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 13 Jul 2021 12:27:03 +0200 Subject: [PATCH 0295/1171] Remove unused CSS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/css/views/rooms/_ReplyTile.scss | 1 - 1 file changed, 1 deletion(-) diff --git a/res/css/views/rooms/_ReplyTile.scss b/res/css/views/rooms/_ReplyTile.scss index d059d553a9..21e5fedea9 100644 --- a/res/css/views/rooms/_ReplyTile.scss +++ b/res/css/views/rooms/_ReplyTile.scss @@ -98,5 +98,4 @@ limitations under the License. /* the next three lines, along with overflow hidden, truncate long display names */ white-space: nowrap; text-overflow: ellipsis; - max-width: calc(100% - 65px); } From 8e456b062ad5909dee2f3f3f009a2d051dedad55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 13 Jul 2021 12:32:17 +0200 Subject: [PATCH 0296/1171] More unused CSS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/css/views/rooms/_ReplyTile.scss | 2 -- 1 file changed, 2 deletions(-) diff --git a/res/css/views/rooms/_ReplyTile.scss b/res/css/views/rooms/_ReplyTile.scss index 21e5fedea9..007ed35ecf 100644 --- a/res/css/views/rooms/_ReplyTile.scss +++ b/res/css/views/rooms/_ReplyTile.scss @@ -42,7 +42,6 @@ limitations under the License. .mx_ReplyTile .mx_EventTile_content { $reply-lines: 2; $line-height: $font-22px; - $max-height: 66px; pointer-events: none; @@ -51,7 +50,6 @@ 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; From 2660e25d6e932627a0814cd7d3b34a4d26a9865a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 13 Jul 2021 13:04:37 +0200 Subject: [PATCH 0297/1171] Deduplicate some code MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/rooms/EventTile.tsx | 31 ++----------------- src/components/views/rooms/ReplyTile.tsx | 24 ++------------- src/utils/EventUtils.ts | 38 ++++++++++++++++++++++++ 3 files changed, 44 insertions(+), 49 deletions(-) diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index bf2438d267..b1e75443a0 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -54,6 +54,7 @@ import TooltipButton from '../elements/TooltipButton'; import ReadReceiptMarker from "./ReadReceiptMarker"; import MessageActionBar from "../messages/MessageActionBar"; import ReactionsRow from '../messages/ReactionsRow'; +import { getEventDisplayInfo } from '../../../utils/EventUtils'; const eventTileTypes = { [EventType.RoomMessage]: 'messages.MessageEvent', @@ -845,35 +846,9 @@ export default class EventTile extends React.Component { }; render() { - //console.info("EventTile showUrlPreview for %s is %s", this.props.mxEvent.getId(), this.props.showUrlPreview); + const msgtype = this.props.mxEvent.getContent().msgtype; + const { tileHandler, isBubbleMessage, isInfoMessage } = getEventDisplayInfo(this.props.mxEvent); - const content = this.props.mxEvent.getContent(); - const msgtype = content.msgtype; - const eventType = this.props.mxEvent.getType(); - - let tileHandler = getHandlerTile(this.props.mxEvent); - - // Info messages are basically information about commands processed on a room - let isBubbleMessage = eventType.startsWith("m.key.verification") || - (eventType === EventType.RoomMessage && msgtype && msgtype.startsWith("m.key.verification")) || - (eventType === EventType.RoomCreate) || - (eventType === EventType.RoomEncryption) || - (tileHandler === "messages.MJitsiWidgetEvent"); - let isInfoMessage = ( - !isBubbleMessage && eventType !== EventType.RoomMessage && - eventType !== EventType.Sticker && eventType !== EventType.RoomCreate - ); - - // 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). - if (SettingsStore.getValue("showHiddenEventsInTimeline") && !haveTileForEvent(this.props.mxEvent)) { - tileHandler = "messages.ViewSourceEvent"; - isBubbleMessage = false; - // 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) { diff --git a/src/components/views/rooms/ReplyTile.tsx b/src/components/views/rooms/ReplyTile.tsx index 19da345579..054a920d64 100644 --- a/src/components/views/rooms/ReplyTile.tsx +++ b/src/components/views/rooms/ReplyTile.tsx @@ -28,6 +28,7 @@ import MImageReplyBody from "../messages/MImageReplyBody"; import * as sdk from '../../../index'; import { EventType, MsgType, RelationType } from 'matrix-js-sdk/src/@types/event'; import { replaceableComponent } from '../../../utils/replaceableComponent'; +import { getEventDisplayInfo } from '../../../utils/EventUtils'; interface IProps { mxEvent: MatrixEvent; @@ -80,28 +81,9 @@ export default class ReplyTile extends React.PureComponent { }; render() { - const content = this.props.mxEvent.getContent(); - const msgtype = content.msgtype; - const eventType = this.props.mxEvent.getType(); + const msgtype = this.props.mxEvent.getContent().msgtype; - // Info messages are basically information about commands processed on a room - let isInfoMessage = ![ - EventType.RoomMessage, - EventType.Sticker, - EventType.RoomCreate, - ].includes(eventType as EventType); - - 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(RelationType.Replace); - if (useSource && SettingsStore.getValue("showHiddenEventsInTimeline")) { - tileHandler = "messages.ViewSourceEvent"; - // Reuse info message avatar and sender profile styling - isInfoMessage = true; - } + const { tileHandler, isInfoMessage } = getEventDisplayInfo(this.props.mxEvent); // This shouldn't happen: the caller should check we support this type // before trying to instantiate us if (!tileHandler) { diff --git a/src/utils/EventUtils.ts b/src/utils/EventUtils.ts index 1a467b157f..d69c285e18 100644 --- a/src/utils/EventUtils.ts +++ b/src/utils/EventUtils.ts @@ -19,6 +19,9 @@ import { MatrixEvent, EventStatus } from 'matrix-js-sdk/src/models/event'; import { MatrixClientPeg } from '../MatrixClientPeg'; import shouldHideEvent from "../shouldHideEvent"; +import { getHandlerTile, haveTileForEvent } from "../components/views/rooms/EventTile"; +import SettingsStore from "../settings/SettingsStore"; +import { EventType } from "matrix-js-sdk/src/@types/event"; /** * Returns whether an event should allow actions like reply, reactions, edit, etc. @@ -96,3 +99,38 @@ export function findEditableEvent(room: Room, isForward: boolean, fromEventId: s } } +export function getEventDisplayInfo(mxEvent: MatrixEvent): { + isInfoMessage: boolean; + tileHandler; + isBubbleMessage: boolean; +} { + const content = mxEvent.getContent(); + const msgtype = content.msgtype; + const eventType = mxEvent.getType(); + + let tileHandler = getHandlerTile(mxEvent); + + // Info messages are basically information about commands processed on a room + let isBubbleMessage = eventType.startsWith("m.key.verification") || + (eventType === EventType.RoomMessage && msgtype && msgtype.startsWith("m.key.verification")) || + (eventType === EventType.RoomCreate) || + (eventType === EventType.RoomEncryption) || + (tileHandler === "messages.MJitsiWidgetEvent"); + let isInfoMessage = ( + !isBubbleMessage && eventType !== EventType.RoomMessage && + eventType !== EventType.Sticker && eventType !== EventType.RoomCreate + ); + + // 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). + if (SettingsStore.getValue("showHiddenEventsInTimeline") && !haveTileForEvent(mxEvent)) { + tileHandler = "messages.ViewSourceEvent"; + isBubbleMessage = false; + // Reuse info message avatar and sender profile styling + isInfoMessage = true; + } + + return { tileHandler, isInfoMessage, isBubbleMessage }; +} From 1ec4ead62d38e63851e55ff3bcb7f51c4e9cbe09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 13 Jul 2021 13:04:59 +0200 Subject: [PATCH 0298/1171] Unused imports MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/rooms/EventTile.tsx | 1 - src/components/views/rooms/ReplyTile.tsx | 4 +--- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index b1e75443a0..553b7801cc 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -27,7 +27,6 @@ import { _t } from '../../../languageHandler'; 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'; diff --git a/src/components/views/rooms/ReplyTile.tsx b/src/components/views/rooms/ReplyTile.tsx index 054a920d64..c875553a96 100644 --- a/src/components/views/rooms/ReplyTile.tsx +++ b/src/components/views/rooms/ReplyTile.tsx @@ -18,15 +18,13 @@ import React from 'react'; import classNames from 'classnames'; import { _t } from '../../../languageHandler'; import dis from '../../../dispatcher/dispatcher'; -import SettingsStore from "../../../settings/SettingsStore"; -import { getHandlerTile } from "./EventTile"; import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { RoomPermalinkCreator } from '../../../utils/permalinks/Permalinks'; import SenderProfile from "../messages/SenderProfile"; import TextualBody from "../messages/TextualBody"; import MImageReplyBody from "../messages/MImageReplyBody"; import * as sdk from '../../../index'; -import { EventType, MsgType, RelationType } from 'matrix-js-sdk/src/@types/event'; +import { EventType, MsgType } from 'matrix-js-sdk/src/@types/event'; import { replaceableComponent } from '../../../utils/replaceableComponent'; import { getEventDisplayInfo } from '../../../utils/EventUtils'; From 8f831a89f62769e360546dbe5c56cd21a8e7d6c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 13 Jul 2021 13:07:47 +0200 Subject: [PATCH 0299/1171] Remove unused code MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/elements/ReplyThread.js | 31 -------------------- 1 file changed, 31 deletions(-) diff --git a/src/components/views/elements/ReplyThread.js b/src/components/views/elements/ReplyThread.js index c22225f766..434900c8de 100644 --- a/src/components/views/elements/ReplyThread.js +++ b/src/components/views/elements/ReplyThread.js @@ -70,10 +70,7 @@ export default class ReplyThread extends React.Component { }; this.unmounted = false; - this.context.on("Event.replaced", this.onEventReplaced); this.room = this.context.getRoom(this.props.parentEv.getRoomId()); - this.room.on("Room.redaction", this.onRoomRedaction); - this.room.on("Room.redactionCancelled", this.onRoomRedaction); this.onQuoteClick = this.onQuoteClick.bind(this); this.canCollapse = this.canCollapse.bind(this); @@ -239,36 +236,8 @@ export default class ReplyThread extends React.Component { componentWillUnmount() { this.unmounted = true; - this.context.removeListener("Event.replaced", this.onEventReplaced); - if (this.room) { - this.room.removeListener("Room.redaction", this.onRoomRedaction); - this.room.removeListener("Room.redactionCancelled", this.onRoomRedaction); - } } - updateForEventId = (eventId) => { - if (this.state.events.some(event => event.getId() === eventId)) { - this.forceUpdate(); - } - }; - - onEventReplaced = (ev) => { - if (this.unmounted) return; - - // If one of the events we are rendering gets replaced, force a re-render - this.updateForEventId(ev.getId()); - }; - - onRoomRedaction = (ev) => { - if (this.unmounted) return; - - const eventId = ev.getAssociatedId(); - if (!eventId) return; - - // If one of the events we are rendering gets redacted, force a re-render - this.updateForEventId(eventId); - }; - async initialize() { const { parentEv } = this.props; // at time of making this component we checked that props.parentEv has a parentEventId From 1bca5371d1f6f92cff106e78861676390fa79801 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 13 Jul 2021 13:18:01 +0200 Subject: [PATCH 0300/1171] Fix redacted messages for the 100th #*&@*%^ time MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/css/views/rooms/_ReplyTile.scss | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/res/css/views/rooms/_ReplyTile.scss b/res/css/views/rooms/_ReplyTile.scss index 007ed35ecf..517ef79ef0 100644 --- a/res/css/views/rooms/_ReplyTile.scss +++ b/res/css/views/rooms/_ReplyTile.scss @@ -29,12 +29,13 @@ limitations under the License. color: $primary-fg-color; } -.mx_ReplyTile > .mx_RedactedBody { - padding: 18px; +.mx_ReplyTile .mx_RedactedBody { + padding: 4px 0 2px 20px; &::before { height: 13px; width: 13px; + top: 5px; } } From 6c4f0526d7c2949ba4f39809dd51af03f5a0aae0 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Tue, 13 Jul 2021 23:26:09 -0400 Subject: [PATCH 0301/1171] Coalesce falsy values from TextForEvent handlers Signed-off-by: Robin Townsend --- src/TextForEvent.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/TextForEvent.tsx b/src/TextForEvent.tsx index 3e3b5aa2e0..0056a37c85 100644 --- a/src/TextForEvent.tsx +++ b/src/TextForEvent.tsx @@ -705,5 +705,5 @@ export function textForEvent(ev: MatrixEvent): string; export function textForEvent(ev: MatrixEvent, allowJSX: true, showHiddenEvents?: boolean): string | JSX.Element; export function textForEvent(ev: MatrixEvent, allowJSX = false, showHiddenEvents?: boolean): string | JSX.Element { const handler = (ev.isState() ? stateHandlers : handlers)[ev.getType()]; - return handler?.(ev, allowJSX, showHiddenEvents)?.() ?? ''; + return handler?.(ev, allowJSX, showHiddenEvents)?.() || ''; } From deab0407cb0d8f60ac6c5897d7b50db091207173 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Tue, 13 Jul 2021 23:27:49 -0400 Subject: [PATCH 0302/1171] Pull another settings lookup out of SearchResultTile loop Signed-off-by: Robin Townsend --- src/components/views/rooms/SearchResultTile.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/views/rooms/SearchResultTile.tsx b/src/components/views/rooms/SearchResultTile.tsx index 47e9849214..c033855eb5 100644 --- a/src/components/views/rooms/SearchResultTile.tsx +++ b/src/components/views/rooms/SearchResultTile.tsx @@ -50,6 +50,7 @@ export default class SearchResultTile extends React.Component { const layout = SettingsStore.getValue("layout"); const isTwelveHour = SettingsStore.getValue("showTwelveHourTimestamps"); const alwaysShowTimestamps = SettingsStore.getValue("alwaysShowTimestamps"); + const enableFlair = SettingsStore.getValue(UIFeature.Flair); const timeline = result.context.getTimeline(); for (let j = 0; j < timeline.length; j++) { @@ -72,7 +73,7 @@ export default class SearchResultTile extends React.Component { onHeightChanged={this.props.onHeightChanged} isTwelveHour={isTwelveHour} alwaysShowTimestamps={alwaysShowTimestamps} - enableFlair={SettingsStore.getValue(UIFeature.Flair)} + enableFlair={enableFlair} />, ); } From 9aae33e076443a9f9b38eff7426cb4cdfc59433b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Wed, 14 Jul 2021 09:28:37 +0200 Subject: [PATCH 0303/1171] Use string[] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/rooms/ReplyTile.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/ReplyTile.tsx b/src/components/views/rooms/ReplyTile.tsx index c875553a96..cb2815ee6a 100644 --- a/src/components/views/rooms/ReplyTile.tsx +++ b/src/components/views/rooms/ReplyTile.tsx @@ -31,7 +31,7 @@ import { getEventDisplayInfo } from '../../../utils/EventUtils'; interface IProps { mxEvent: MatrixEvent; permalinkCreator?: RoomPermalinkCreator; - highlights?: Array; + highlights?: string[]; highlightLink?: string; onHeightChanged?(): void; } From 74ff85ae305c03e96647620fbc01b8b91bf5a132 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Wed, 14 Jul 2021 09:52:45 +0200 Subject: [PATCH 0304/1171] Remove m.sticker since it's not a message type MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/rooms/ReplyTile.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/views/rooms/ReplyTile.tsx b/src/components/views/rooms/ReplyTile.tsx index cb2815ee6a..41fc61aa7f 100644 --- a/src/components/views/rooms/ReplyTile.tsx +++ b/src/components/views/rooms/ReplyTile.tsx @@ -117,7 +117,6 @@ export default class ReplyTile extends React.PureComponent { [MsgType.Image]: MImageReplyBody, // We don't want a download link for files, just the file name is enough. [MsgType.File]: TextualBody, - "m.sticker": TextualBody, [MsgType.Audio]: TextualBody, [MsgType.Video]: TextualBody, }; From 58dedbeeffdaa090d0ee0deebbac929b7ab2f753 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Wed, 14 Jul 2021 09:52:56 +0200 Subject: [PATCH 0305/1171] Add missing type MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/utils/EventUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/EventUtils.ts b/src/utils/EventUtils.ts index d69c285e18..849e546485 100644 --- a/src/utils/EventUtils.ts +++ b/src/utils/EventUtils.ts @@ -101,7 +101,7 @@ export function findEditableEvent(room: Room, isForward: boolean, fromEventId: s export function getEventDisplayInfo(mxEvent: MatrixEvent): { isInfoMessage: boolean; - tileHandler; + tileHandler: string; isBubbleMessage: boolean; } { const content = mxEvent.getContent(); From 7b35d2c27046c432dcdb9b768c1de40a1ca03c4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Wed, 14 Jul 2021 09:53:40 +0200 Subject: [PATCH 0306/1171] FORCED_IMAGE_HEIGHT into a const MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/messages/MImageReplyBody.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/views/messages/MImageReplyBody.tsx b/src/components/views/messages/MImageReplyBody.tsx index b0f7415347..44acf18004 100644 --- a/src/components/views/messages/MImageReplyBody.tsx +++ b/src/components/views/messages/MImageReplyBody.tsx @@ -20,6 +20,8 @@ import { presentableTextForFile } from "./MFileBody"; import { IMediaEventContent } from "../../../customisations/models/IMediaEventContent"; import SenderProfile from "./SenderProfile"; +const FORCED_IMAGE_HEIGHT = 44; + export default class MImageReplyBody extends MImageBody { public onClick = (ev: React.MouseEvent): void => { ev.preventDefault(); @@ -42,7 +44,7 @@ export default class MImageReplyBody extends MImageBody { const content = this.props.mxEvent.getContent(); const contentUrl = this.getContentUrl(); - const thumbnail = this.messageContent(contentUrl, this.getThumbUrl(), content, 44); + const thumbnail = this.messageContent(contentUrl, this.getThumbUrl(), content, FORCED_IMAGE_HEIGHT); const fileBody = this.getFileBody(); const sender = Date: Wed, 14 Jul 2021 09:54:33 +0200 Subject: [PATCH 0307/1171] Omit onFinished MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/messages/MImageBody.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/views/messages/MImageBody.tsx b/src/components/views/messages/MImageBody.tsx index 0acdbaf253..74d15dd9b5 100644 --- a/src/components/views/messages/MImageBody.tsx +++ b/src/components/views/messages/MImageBody.tsx @@ -115,12 +115,11 @@ export default class MImageBody extends React.Component { const content = this.props.mxEvent.getContent(); const httpUrl = this.getContentUrl(); - const params: ComponentProps = { + const params: Omit, "onFinished"> = { src: httpUrl, name: content.body?.length > 0 ? content.body : _t('Attachment'), mxEvent: this.props.mxEvent, permalinkCreator: this.props.permalinkCreator, - onFinished: () => {}, }; if (content.info) { From 4afd985e7e63e03586b8e7d003690f9ef6653621 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Wed, 14 Jul 2021 09:55:14 +0200 Subject: [PATCH 0308/1171] Kill off _afterComponentWillUnmount MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/messages/MImageBody.tsx | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/components/views/messages/MImageBody.tsx b/src/components/views/messages/MImageBody.tsx index 74d15dd9b5..96c8652aee 100644 --- a/src/components/views/messages/MImageBody.tsx +++ b/src/components/views/messages/MImageBody.tsx @@ -316,7 +316,6 @@ export default class MImageBody extends React.Component { componentWillUnmount() { this.unmounted = true; this.context.removeListener('sync', this.onClientSync); - this._afterComponentWillUnmount(); if (this.state.decryptedUrl) { URL.revokeObjectURL(this.state.decryptedUrl); @@ -326,11 +325,6 @@ export default class MImageBody extends React.Component { } } - // To be overridden by subclasses (e.g. MStickerBody) for further - // cleanup after componentWillUnmount - _afterComponentWillUnmount() { - } - protected messageContent( contentUrl: string, thumbUrl: string, From 18355599e88107f342dcef78dc6e5aa58704b4c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Wed, 14 Jul 2021 10:07:41 +0200 Subject: [PATCH 0309/1171] Fix senderProfile getting cutoff MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/css/views/messages/_MImageReplyBody.scss | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/res/css/views/messages/_MImageReplyBody.scss b/res/css/views/messages/_MImageReplyBody.scss index f0401d21db..0b18308847 100644 --- a/res/css/views/messages/_MImageReplyBody.scss +++ b/res/css/views/messages/_MImageReplyBody.scss @@ -21,12 +21,17 @@ limitations under the License. flex: 1; padding-right: 4px; } + + .mx_MImageReplyBody_info { + flex: 1; + + .mx_MImageReplyBody_sender { + grid-area: sender; + } + + .mx_MImageReplyBody_filename { + grid-area: filename; + } + } } -.mx_MImageReplyBody_sender { - grid-area: sender; -} - -.mx_MImageReplyBody_filename { - grid-area: filename; -} From 586e85cbff97da634fb7bf19491cffb2618487ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Wed, 14 Jul 2021 10:14:44 +0200 Subject: [PATCH 0310/1171] Use MFileBody in replies MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/css/views/rooms/_ReplyTile.scss | 4 ++++ src/components/views/rooms/ReplyTile.tsx | 3 ++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/res/css/views/rooms/_ReplyTile.scss b/res/css/views/rooms/_ReplyTile.scss index 517ef79ef0..552d54367e 100644 --- a/res/css/views/rooms/_ReplyTile.scss +++ b/res/css/views/rooms/_ReplyTile.scss @@ -20,6 +20,10 @@ limitations under the License. font-size: $font-14px; position: relative; line-height: $font-16px; + + .mx_MFileBody_info { + margin: 5px 0; + } } .mx_ReplyTile > a { diff --git a/src/components/views/rooms/ReplyTile.tsx b/src/components/views/rooms/ReplyTile.tsx index 41fc61aa7f..2911e538fc 100644 --- a/src/components/views/rooms/ReplyTile.tsx +++ b/src/components/views/rooms/ReplyTile.tsx @@ -27,6 +27,7 @@ import * as sdk from '../../../index'; import { EventType, MsgType } from 'matrix-js-sdk/src/@types/event'; import { replaceableComponent } from '../../../utils/replaceableComponent'; import { getEventDisplayInfo } from '../../../utils/EventUtils'; +import MFileBody from "../messages/MFileBody"; interface IProps { mxEvent: MatrixEvent; @@ -116,7 +117,7 @@ export default class ReplyTile extends React.PureComponent { const msgtypeOverrides = { [MsgType.Image]: MImageReplyBody, // We don't want a download link for files, just the file name is enough. - [MsgType.File]: TextualBody, + [MsgType.File]: MFileBody, [MsgType.Audio]: TextualBody, [MsgType.Video]: TextualBody, }; From f26c75bdcc35f98c36ee4816ff72848f8c7ac9f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Wed, 14 Jul 2021 10:23:10 +0200 Subject: [PATCH 0311/1171] Use margin instead of padding MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/css/views/messages/_MImageReplyBody.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/css/views/messages/_MImageReplyBody.scss b/res/css/views/messages/_MImageReplyBody.scss index 0b18308847..70c53f8c9c 100644 --- a/res/css/views/messages/_MImageReplyBody.scss +++ b/res/css/views/messages/_MImageReplyBody.scss @@ -19,7 +19,7 @@ limitations under the License. .mx_MImageBody_thumbnail_container { flex: 1; - padding-right: 4px; + margin-right: 4px; } .mx_MImageReplyBody_info { From ae4d8c291daf667a422dc6596d16ace2c3e5f927 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Wed, 14 Jul 2021 10:23:24 +0200 Subject: [PATCH 0312/1171] It's not an override MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/rooms/ReplyTile.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/components/views/rooms/ReplyTile.tsx b/src/components/views/rooms/ReplyTile.tsx index 2911e538fc..a22fbc4494 100644 --- a/src/components/views/rooms/ReplyTile.tsx +++ b/src/components/views/rooms/ReplyTile.tsx @@ -27,7 +27,6 @@ import * as sdk from '../../../index'; import { EventType, MsgType } from 'matrix-js-sdk/src/@types/event'; import { replaceableComponent } from '../../../utils/replaceableComponent'; import { getEventDisplayInfo } from '../../../utils/EventUtils'; -import MFileBody from "../messages/MFileBody"; interface IProps { mxEvent: MatrixEvent; @@ -117,7 +116,6 @@ export default class ReplyTile extends React.PureComponent { const msgtypeOverrides = { [MsgType.Image]: MImageReplyBody, // We don't want a download link for files, just the file name is enough. - [MsgType.File]: MFileBody, [MsgType.Audio]: TextualBody, [MsgType.Video]: TextualBody, }; From 04db6beb108fea542db2bb3f25afc93dfe8caed4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Wed, 14 Jul 2021 10:30:24 +0200 Subject: [PATCH 0313/1171] Remove stale comment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/rooms/ReplyTile.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/views/rooms/ReplyTile.tsx b/src/components/views/rooms/ReplyTile.tsx index a22fbc4494..8ac34afa28 100644 --- a/src/components/views/rooms/ReplyTile.tsx +++ b/src/components/views/rooms/ReplyTile.tsx @@ -115,7 +115,6 @@ export default class ReplyTile extends React.PureComponent { const msgtypeOverrides = { [MsgType.Image]: MImageReplyBody, - // We don't want a download link for files, just the file name is enough. [MsgType.Audio]: TextualBody, [MsgType.Video]: TextualBody, }; From 782563af5356281ef2a8264b32235554cec9a061 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Wed, 14 Jul 2021 10:47:29 +0200 Subject: [PATCH 0314/1171] Override audio and video body with file body MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/css/views/rooms/_ReplyTile.scss | 10 ++++++++-- src/components/views/rooms/ReplyTile.tsx | 6 ++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/res/css/views/rooms/_ReplyTile.scss b/res/css/views/rooms/_ReplyTile.scss index 552d54367e..8fe3a3e94c 100644 --- a/res/css/views/rooms/_ReplyTile.scss +++ b/res/css/views/rooms/_ReplyTile.scss @@ -21,8 +21,14 @@ limitations under the License. position: relative; line-height: $font-16px; - .mx_MFileBody_info { - margin: 5px 0; + .mx_MFileBody { + .mx_MFileBody_info { + margin: 5px 0; + } + + .mx_MFileBody_download { + display: none; + } } } diff --git a/src/components/views/rooms/ReplyTile.tsx b/src/components/views/rooms/ReplyTile.tsx index 8ac34afa28..f2f75c4918 100644 --- a/src/components/views/rooms/ReplyTile.tsx +++ b/src/components/views/rooms/ReplyTile.tsx @@ -27,6 +27,7 @@ import * as sdk from '../../../index'; import { EventType, MsgType } from 'matrix-js-sdk/src/@types/event'; import { replaceableComponent } from '../../../utils/replaceableComponent'; import { getEventDisplayInfo } from '../../../utils/EventUtils'; +import MFileBody from "../messages/MFileBody"; interface IProps { mxEvent: MatrixEvent; @@ -115,8 +116,9 @@ export default class ReplyTile extends React.PureComponent { const msgtypeOverrides = { [MsgType.Image]: MImageReplyBody, - [MsgType.Audio]: TextualBody, - [MsgType.Video]: TextualBody, + // Override audio and video body with file body. We also hide the download/decrypt button using CSS + [MsgType.Audio]: MFileBody, + [MsgType.Video]: MFileBody, }; const evOverrides = { [EventType.Sticker]: TextualBody, From 6c801fea53530f37b8d309e871869d81d46d3e2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Wed, 14 Jul 2021 12:15:13 +0200 Subject: [PATCH 0315/1171] Use MImageReplyBody for stickers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/rooms/ReplyTile.tsx | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/components/views/rooms/ReplyTile.tsx b/src/components/views/rooms/ReplyTile.tsx index f2f75c4918..49c904a940 100644 --- a/src/components/views/rooms/ReplyTile.tsx +++ b/src/components/views/rooms/ReplyTile.tsx @@ -80,7 +80,9 @@ export default class ReplyTile extends React.PureComponent { }; render() { - const msgtype = this.props.mxEvent.getContent().msgtype; + const mxEvent = this.props.mxEvent; + const msgtype = mxEvent.getContent().msgtype; + const evType = mxEvent.getType() as EventType; const { tileHandler, isInfoMessage } = getEventDisplayInfo(this.props.mxEvent); // This shouldn't happen: the caller should check we support this type @@ -105,7 +107,12 @@ export default class ReplyTile extends React.PureComponent { } let sender; - const needsSenderProfile = msgtype !== MsgType.Image && tileHandler !== EventType.RoomCreate && !isInfoMessage; + const needsSenderProfile = ( + !isInfoMessage && + msgtype !== MsgType.Image && + tileHandler !== EventType.RoomCreate && + evType !== EventType.Sticker + ); if (needsSenderProfile) { sender = { [MsgType.Video]: MFileBody, }; const evOverrides = { - [EventType.Sticker]: TextualBody, + // Use MImageReplyBody so that the sticker isn't taking up a lot of space + [EventType.Sticker]: MImageReplyBody, }; return ( From 54d2784818e7c6908052997266a3613de97b575f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Wed, 14 Jul 2021 12:19:16 +0200 Subject: [PATCH 0316/1171] Remove unused import MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/rooms/ReplyTile.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/views/rooms/ReplyTile.tsx b/src/components/views/rooms/ReplyTile.tsx index 49c904a940..f44a75a264 100644 --- a/src/components/views/rooms/ReplyTile.tsx +++ b/src/components/views/rooms/ReplyTile.tsx @@ -21,7 +21,6 @@ import dis from '../../../dispatcher/dispatcher'; import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { RoomPermalinkCreator } from '../../../utils/permalinks/Permalinks'; import SenderProfile from "../messages/SenderProfile"; -import TextualBody from "../messages/TextualBody"; import MImageReplyBody from "../messages/MImageReplyBody"; import * as sdk from '../../../index'; import { EventType, MsgType } from 'matrix-js-sdk/src/@types/event'; From 226224b0394e9100fb56f1e7f80e9212c37b47fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Wed, 14 Jul 2021 15:12:35 +0200 Subject: [PATCH 0317/1171] Allow sending hidden RRs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/structures/TimelinePanel.tsx | 8 ++++++-- .../views/settings/tabs/user/LabsUserSettingsTab.js | 1 + src/settings/Settings.tsx | 7 +++++++ 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/components/structures/TimelinePanel.tsx b/src/components/structures/TimelinePanel.tsx index 85a048e9b8..bd90b637d6 100644 --- a/src/components/structures/TimelinePanel.tsx +++ b/src/components/structures/TimelinePanel.tsx @@ -758,16 +758,20 @@ class TimelinePanel extends React.Component { } this.lastRMSentEventId = this.state.readMarkerEventId; + const roomId = this.props.timelineSet.room.roomId; + const hiddenRR = !SettingsStore.getValue("sendReadReceipts", roomId); + debuglog('TimelinePanel: Sending Read Markers for ', this.props.timelineSet.room.roomId, 'rm', this.state.readMarkerEventId, lastReadEvent ? 'rr ' + lastReadEvent.getId() : '', + ' hidden:' + hiddenRR, ); MatrixClientPeg.get().setRoomReadMarkers( - this.props.timelineSet.room.roomId, + roomId, this.state.readMarkerEventId, lastReadEvent, // Could be null, in which case no RR is sent - {}, + { hidden: hiddenRR }, ).catch((e) => { // /read_markers API is not implemented on this HS, fallback to just RR if (e.errcode === 'M_UNRECOGNIZED' && lastReadEvent) { diff --git a/src/components/views/settings/tabs/user/LabsUserSettingsTab.js b/src/components/views/settings/tabs/user/LabsUserSettingsTab.js index abf9709f50..e57ad80bbc 100644 --- a/src/components/views/settings/tabs/user/LabsUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/LabsUserSettingsTab.js @@ -74,6 +74,7 @@ export default class LabsUserSettingsTab extends React.Component { +
    ; } diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index 1751eddb2c..163bfad2b3 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -325,6 +325,13 @@ export const SETTINGS: {[setting: string]: ISetting} = { supportedLevels: LEVELS_ACCOUNT_SETTINGS, default: null, }, + "sendReadReceipts": { + supportedLevels: LEVELS_ROOM_SETTINGS, + displayName: _td( + "Send read receipts for messages (requires compatible homeserver to disable)", + ), + default: true, + }, "baseFontSize": { displayName: _td("Font size"), supportedLevels: LEVELS_ACCOUNT_SETTINGS, From fc270b435cd559972cff5ece78613de2dc869433 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Wed, 14 Jul 2021 15:32:35 +0200 Subject: [PATCH 0318/1171] fix group layout --- res/css/views/rooms/_EventBubbleTile.scss | 6 +++++- res/css/views/rooms/_EventTile.scss | 26 +++++++++++++++-------- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/res/css/views/rooms/_EventBubbleTile.scss b/res/css/views/rooms/_EventBubbleTile.scss index 48011951cc..c66f635ffe 100644 --- a/res/css/views/rooms/_EventBubbleTile.scss +++ b/res/css/views/rooms/_EventBubbleTile.scss @@ -241,7 +241,7 @@ limitations under the License. } .mx_EventTile { - margin: 0 58px; + margin: 0 6px; } .mx_EventTile_line { @@ -258,6 +258,10 @@ limitations under the License. } } + & ~ .mx_EventListSummary[data-expanded=false] { + padding: 0 34px; + } + /* events that do not require bubble layout */ & ~ .mx_EventListSummary, &.mx_EventTile_bad { diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index e9d71d557c..d6ad37f6bb 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -106,15 +106,6 @@ $hover-select-border: 4px; border-radius: 8px; } - .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_reply { margin-right: 10px; } @@ -309,6 +300,23 @@ $hover-select-border: 4px; bottom: 0; right: 0; } + + .mx_ReactionsRow { + margin: 0; + padding: 6px 60px; + } +} + +.mx_RoomView_timeline_rr_enabled { + + .mx_EventTile:not([data-layout=bubble]) { + .mx_EventTile_line { + /* ideally should be 100px, but 95px gives us a max thumbnail size of 800x600, which is nice */ + margin-right: 110px; + } + } + + // on ELS we need the margin to allow interaction with the expand/collapse button which is normally in the RR gutter } .mx_EventTile_bubbleContainer { From 8bf5e61acc72168a9f61a356806949c3af212ae8 Mon Sep 17 00:00:00 2001 From: James Salter Date: Wed, 14 Jul 2021 14:58:18 +0100 Subject: [PATCH 0319/1171] Add "Copy" to room context menu. This menu item creates a matrix.to link for the room and copies it to the clipboard. --- src/components/structures/MatrixChat.tsx | 16 ++++++++++++++++ src/components/views/rooms/RoomTile.tsx | 16 ++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index d692b0fa7f..02558a3838 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -105,6 +105,8 @@ import VerificationRequestToast from '../views/toasts/VerificationRequestToast'; import PerformanceMonitor, { PerformanceEntryNames } from "../../performance"; import UIStore, { UI_EVENTS } from "../../stores/UIStore"; import SoftLogout from './auth/SoftLogout'; +import { makeRoomPermalink } from "../../utils/permalinks/Permalinks"; +import { copyPlaintext } from "../../utils/strings"; /** constants for MatrixChat.state.view */ export enum Views { @@ -627,6 +629,9 @@ export default class MatrixChat extends React.PureComponent { case 'forget_room': this.forgetRoom(payload.room_id); break; + case 'copy_room': + this.copyRoom(payload.room_id); + break; case 'reject_invite': Modal.createTrackedDialog('Reject invitation', '', QuestionDialog, { title: _t('Reject invitation'), @@ -1193,6 +1198,17 @@ export default class MatrixChat extends React.PureComponent { }); } + private async copyRoom(roomId: string) { + const roomLink = makeRoomPermalink(roomId); + const success = await copyPlaintext(roomLink); + if (!success) { + Modal.createTrackedDialog("Unable to copy room", "", ErrorDialog, { + title: _t("Unable to copy room"), + description: _t("Unable to copy room"), + }); + } + } + /** * Starts a chat with the welcome user, if the user doesn't already have one * @returns {string} The room ID of the new room, or null if no room was created diff --git a/src/components/views/rooms/RoomTile.tsx b/src/components/views/rooms/RoomTile.tsx index 9be0274dd5..8fb4d04791 100644 --- a/src/components/views/rooms/RoomTile.tsx +++ b/src/components/views/rooms/RoomTile.tsx @@ -358,6 +358,17 @@ export default class RoomTile extends React.PureComponent { this.setState({ generalMenuPosition: null }); // hide the menu }; + private onCopyRoomClick = (ev: ButtonEvent) => { + ev.preventDefault(); + ev.stopPropagation(); + + dis.dispatch({ + action: 'copy_room', + room_id: this.props.room.roomId, + }); + this.setState({ generalMenuPosition: null }); // hide the menu + }; + private onInviteClick = (ev: ButtonEvent) => { ev.preventDefault(); ev.stopPropagation(); @@ -522,6 +533,11 @@ export default class RoomTile extends React.PureComponent { label={_t("Settings")} iconClassName="mx_RoomTile_iconSettings" /> + Date: Wed, 14 Jul 2021 15:02:59 +0100 Subject: [PATCH 0320/1171] Add English i18n string --- src/i18n/strings/en_EN.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index ced24e2547..ea52d779c3 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -3043,5 +3043,6 @@ "Enter": "Enter", "Space": "Space", "End": "End", - "[number]": "[number]" + "[number]": "[number]", + "Unable to copy room": "Unable to copy room" } From 2fe5ad5d4b5cf36d80ac26c1b323606d8ce808d4 Mon Sep 17 00:00:00 2001 From: James Salter Date: Wed, 14 Jul 2021 15:07:22 +0100 Subject: [PATCH 0321/1171] Slightly refine error message --- src/components/structures/MatrixChat.tsx | 2 +- src/i18n/strings/en_EN.json | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index 02558a3838..cadf66d11e 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -1204,7 +1204,7 @@ export default class MatrixChat extends React.PureComponent { if (!success) { Modal.createTrackedDialog("Unable to copy room", "", ErrorDialog, { title: _t("Unable to copy room"), - description: _t("Unable to copy room"), + description: _t("Unable to copy the room to the clipboard."), }); } } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index ea52d779c3..03801a9899 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -3044,5 +3044,6 @@ "Space": "Space", "End": "End", "[number]": "[number]", - "Unable to copy room": "Unable to copy room" + "Unable to copy room": "Unable to copy room", + "Unable to copy the room to the clipboard.": "Unable to copy the room to the clipboard." } From f4dfe9832bce35ebb15287eaba39e9c0c24d44e6 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Wed, 14 Jul 2021 16:20:25 +0200 Subject: [PATCH 0322/1171] change labs flag wording --- 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 4c113aae18..0839c7eec4 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -820,7 +820,7 @@ "Offline encrypted messaging using dehydrated devices": "Offline encrypted messaging using dehydrated devices", "Enable advanced debugging for the room list": "Enable advanced debugging for the room list", "Show info about bridges in room settings": "Show info about bridges in room settings", - "Explore new ways switching layouts (including a new bubble layout)": "Explore new ways switching layouts (including a new bubble layout)", + "New layout switcher (with message bubbles)": "New layout switcher (with message bubbles)", "Font size": "Font size", "Use custom size": "Use custom size", "Enable Emoji suggestions while typing": "Enable Emoji suggestions while typing", diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index a3a184b908..c15ec684ad 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -325,7 +325,7 @@ export const SETTINGS: {[setting: string]: ISetting} = { "feature_new_layout_switcher": { isFeature: true, supportedLevels: LEVELS_FEATURE, - displayName: _td("Explore new ways switching layouts (including a new bubble layout)"), + displayName: _td("New layout switcher (with message bubbles)"), default: false, controller: new NewLayoutSwitcherController(), }, From a6120ef3b780a586e148dd21237e30c26a57f363 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Wed, 14 Jul 2021 16:32:29 +0200 Subject: [PATCH 0323/1171] Revert fetchdep script diff --- scripts/fetchdep.sh | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/scripts/fetchdep.sh b/scripts/fetchdep.sh index 07efee69e6..0990af70ce 100755 --- a/scripts/fetchdep.sh +++ b/scripts/fetchdep.sh @@ -46,7 +46,12 @@ BRANCH_ARRAY=(${head//:/ }) if [[ "${#BRANCH_ARRAY[@]}" == "1" ]]; then if [ -n "$GITHUB_HEAD_REF" ]; then - clone $deforg $defrepo $GITHUB_HEAD_REF + if [[ "$GITHUB_REPOSITORY" == "$deforg"* ]]; then + clone $deforg $defrepo $GITHUB_HEAD_REF + else + REPO_ARRAY=(${GITHUB_REPOSITORY//\// }) + clone $REPO_ARRAY[0] $defrepo $GITHUB_HEAD_REF + fi else clone $deforg $defrepo $BUILDKITE_BRANCH fi From e054af7f38d3f60eb7cd62acc0a31ccaa93f947c Mon Sep 17 00:00:00 2001 From: James Salter Date: Wed, 14 Jul 2021 15:35:57 +0100 Subject: [PATCH 0324/1171] Run yarn i18n --- src/i18n/strings/en_EN.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 03801a9899..d82d19fe3d 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2671,6 +2671,8 @@ "Are you sure you want to leave the space '%(spaceName)s'?": "Are you sure you want to leave the space '%(spaceName)s'?", "Are you sure you want to leave the room '%(roomName)s'?": "Are you sure you want to leave the room '%(roomName)s'?", "Failed to forget room %(errCode)s": "Failed to forget room %(errCode)s", + "Unable to copy room": "Unable to copy room", + "Unable to copy the room to the clipboard.": "Unable to copy the room to the clipboard.", "Signed Out": "Signed Out", "For security, this session has been signed out. Please sign in again.": "For security, this session has been signed out. Please sign in again.", "Terms and Conditions": "Terms and Conditions", @@ -3043,7 +3045,5 @@ "Enter": "Enter", "Space": "Space", "End": "End", - "[number]": "[number]", - "Unable to copy room": "Unable to copy room", - "Unable to copy the room to the clipboard.": "Unable to copy the room to the clipboard." + "[number]": "[number]" } From dde58d449dd22410b9d2fabf8359c2781d020b34 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Wed, 14 Jul 2021 17:16:13 +0200 Subject: [PATCH 0325/1171] Only hide sender when in bubble mode --- src/components/structures/MessagePanel.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/structures/MessagePanel.tsx b/src/components/structures/MessagePanel.tsx index cee6011e4a..bf5a47cff3 100644 --- a/src/components/structures/MessagePanel.tsx +++ b/src/components/structures/MessagePanel.tsx @@ -712,7 +712,7 @@ export default class MessagePanel extends React.Component { layout={this.props.layout} enableFlair={this.props.enableFlair} showReadReceipts={this.props.showReadReceipts} - hideSender={this.props.room.getMembers().length <= 2} + hideSender={this.props.room.getMembers().length <= 2 && this.props.layout === Layout.Bubble} /> , ); From d3823305ccb68e2639f6c0ee0e4e860ce42b3f5a Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Wed, 14 Jul 2021 16:21:02 +0100 Subject: [PATCH 0326/1171] Upgrade matrix-js-sdk to 12.1.0-rc.1 --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 27c4f39a09..d7933e4c59 100644 --- a/package.json +++ b/package.json @@ -79,7 +79,7 @@ "katex": "^0.12.0", "linkifyjs": "^2.1.9", "lodash": "^4.17.20", - "matrix-js-sdk": "12.0.1", + "matrix-js-sdk": "12.1.0-rc.1", "matrix-widget-api": "^0.1.0-beta.15", "minimist": "^1.2.5", "opus-recorder": "^8.0.3", diff --git a/yarn.lock b/yarn.lock index 96c02681fd..432e25cf34 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5455,10 +5455,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.1: - version "12.0.1" - resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-12.0.1.tgz#3a63881f743420a4d39474daa39bd0fb90930d43" - integrity sha512-HkOWv8QHojceo3kPbC+vAIFUjsRAig6MBvEY35UygS3g2dL0UcJ5Qx09/2wcXtu6dowlDnWsz2HHk62tS2cklA== +matrix-js-sdk@12.1.0-rc.1: + version "12.1.0-rc.1" + resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-12.1.0-rc.1.tgz#4bc4e2342525c622e1a87b264e6f55560632b90c" + integrity sha512-F7d1e1Bm8zZqXkTKIyNeT4uA85u65nfrW2b8NwDMV+gtKNF0DOzUfUzOGD7CnjJKpyKNTQluUiwka+bXiGAVkw== dependencies: "@babel/runtime" "^7.12.5" another-json "^0.2.0" From 70c93a6fee88325d954a78fce9ecaf5815a6eb53 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Wed, 14 Jul 2021 16:27:34 +0100 Subject: [PATCH 0327/1171] Prepare changelog for v3.26.0-rc.1 --- CHANGELOG.md | 142 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 142 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 22b35b7c59..392968c906 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,145 @@ +Changes in [3.26.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.26.0-rc.1) (2021-07-14) +=============================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.25.0...v3.26.0-rc.1) + + * Fix voice messages in right panels + [\#6370](https://github.com/matrix-org/matrix-react-sdk/pull/6370) + * Use TileShape enum more universally + [\#6369](https://github.com/matrix-org/matrix-react-sdk/pull/6369) + * Translations update from Weblate + [\#6373](https://github.com/matrix-org/matrix-react-sdk/pull/6373) + * Hide world readable history option in encrypted rooms + [\#5947](https://github.com/matrix-org/matrix-react-sdk/pull/5947) + * Make the Image View buttons easier to hit + [\#6372](https://github.com/matrix-org/matrix-react-sdk/pull/6372) + * Reorder buttons in the Image View + [\#6368](https://github.com/matrix-org/matrix-react-sdk/pull/6368) + * Add VS Code to gitignore + [\#6367](https://github.com/matrix-org/matrix-react-sdk/pull/6367) + * Fix inviter exploding due to member being null + [\#6362](https://github.com/matrix-org/matrix-react-sdk/pull/6362) + * Increase sample count in voice message thumbnail + [\#6359](https://github.com/matrix-org/matrix-react-sdk/pull/6359) + * Improve arraySeed utility + [\#6360](https://github.com/matrix-org/matrix-react-sdk/pull/6360) + * Convert FontManager to TS and stub it out for tests + [\#6358](https://github.com/matrix-org/matrix-react-sdk/pull/6358) + * Adjust recording waveform behaviour for voice messages + [\#6357](https://github.com/matrix-org/matrix-react-sdk/pull/6357) + * Do not honor string power levels + [\#6245](https://github.com/matrix-org/matrix-react-sdk/pull/6245) + * Add alias and directory customisation points + [\#6343](https://github.com/matrix-org/matrix-react-sdk/pull/6343) + * Fix multiinviter user already in room and clean up code + [\#6354](https://github.com/matrix-org/matrix-react-sdk/pull/6354) + * Fix right panel not closing user info when changing rooms + [\#6341](https://github.com/matrix-org/matrix-react-sdk/pull/6341) + * Quit sticker picker on m.sticker + [\#5679](https://github.com/matrix-org/matrix-react-sdk/pull/5679) + * Don't autodetect language in inline code blocks + [\#6350](https://github.com/matrix-org/matrix-react-sdk/pull/6350) + * Make ghost button background transparent + [\#6331](https://github.com/matrix-org/matrix-react-sdk/pull/6331) + * only consider valid & loaded url previews for show N more prompt + [\#6346](https://github.com/matrix-org/matrix-react-sdk/pull/6346) + * Extract MXCs from _matrix/media/r0/ URLs for inline images in messages + [\#6335](https://github.com/matrix-org/matrix-react-sdk/pull/6335) + * Fix small visual regression with the site name on url previews + [\#6342](https://github.com/matrix-org/matrix-react-sdk/pull/6342) + * Make PIP CallView draggable/movable + [\#5952](https://github.com/matrix-org/matrix-react-sdk/pull/5952) + * Convert VoiceUserSettingsTab to TS + [\#6340](https://github.com/matrix-org/matrix-react-sdk/pull/6340) + * Simplify typescript definition for Modernizr + [\#6339](https://github.com/matrix-org/matrix-react-sdk/pull/6339) + * Remember the last used server for room directory searches + [\#6322](https://github.com/matrix-org/matrix-react-sdk/pull/6322) + * Focus composer after reacting + [\#6332](https://github.com/matrix-org/matrix-react-sdk/pull/6332) + * Fix bug which prevented more than one event getting pinned + [\#6336](https://github.com/matrix-org/matrix-react-sdk/pull/6336) + * Make DeviceListener also update on megolm key in SSSS + [\#6337](https://github.com/matrix-org/matrix-react-sdk/pull/6337) + * Improve URL previews + [\#6326](https://github.com/matrix-org/matrix-react-sdk/pull/6326) + * Don't close settings dialog when opening spaces feedback prompt + [\#6334](https://github.com/matrix-org/matrix-react-sdk/pull/6334) + * Update import location for types + [\#6330](https://github.com/matrix-org/matrix-react-sdk/pull/6330) + * Improve blurhash rendering performance + [\#6329](https://github.com/matrix-org/matrix-react-sdk/pull/6329) + * Use a proper color scheme for codeblocks + [\#6320](https://github.com/matrix-org/matrix-react-sdk/pull/6320) + * Burn `sdk.getComponent()` with 🔥 + [\#6308](https://github.com/matrix-org/matrix-react-sdk/pull/6308) + * Fix instances of the Edit Message Composer's save button being wrongly + disabled + [\#6307](https://github.com/matrix-org/matrix-react-sdk/pull/6307) + * Do not generate a lockfile when running in CI + [\#6327](https://github.com/matrix-org/matrix-react-sdk/pull/6327) + * Update lockfile with correct dependencies + [\#6324](https://github.com/matrix-org/matrix-react-sdk/pull/6324) + * Clarify the keys we use when submitting rageshakes + [\#6321](https://github.com/matrix-org/matrix-react-sdk/pull/6321) + * Fix ImageView context menu + [\#6318](https://github.com/matrix-org/matrix-react-sdk/pull/6318) + * TypeScript migration + [\#6315](https://github.com/matrix-org/matrix-react-sdk/pull/6315) + * Move animation to compositor + [\#6310](https://github.com/matrix-org/matrix-react-sdk/pull/6310) + * Reorganize preferences + [\#5742](https://github.com/matrix-org/matrix-react-sdk/pull/5742) + * Fix being able to un-rotate images + [\#6313](https://github.com/matrix-org/matrix-react-sdk/pull/6313) + * Fix icon size in passphrase prompt + [\#6312](https://github.com/matrix-org/matrix-react-sdk/pull/6312) + * Use sleep & defer from js-sdk instead of duplicating it + [\#6305](https://github.com/matrix-org/matrix-react-sdk/pull/6305) + * Convert EventTimeline, EventTimelineSet and TimelineWindow to TS + [\#6295](https://github.com/matrix-org/matrix-react-sdk/pull/6295) + * Comply with new member-delimiter-style rule + [\#6306](https://github.com/matrix-org/matrix-react-sdk/pull/6306) + * Fix Test Linting + [\#6304](https://github.com/matrix-org/matrix-react-sdk/pull/6304) + * Convert Markdown to TypeScript + [\#6303](https://github.com/matrix-org/matrix-react-sdk/pull/6303) + * Convert RoomHeader to TS + [\#6302](https://github.com/matrix-org/matrix-react-sdk/pull/6302) + * Prevent RoomDirectory from exploding when filterString is wrongly nulled + [\#6296](https://github.com/matrix-org/matrix-react-sdk/pull/6296) + * Add support for blurhash (MSC2448) + [\#5099](https://github.com/matrix-org/matrix-react-sdk/pull/5099) + * Remove rateLimitedFunc + [\#6300](https://github.com/matrix-org/matrix-react-sdk/pull/6300) + * Convert some Key Verification classes to TypeScript + [\#6299](https://github.com/matrix-org/matrix-react-sdk/pull/6299) + * Typescript conversion of Composer components and more + [\#6292](https://github.com/matrix-org/matrix-react-sdk/pull/6292) + * Upgrade browserlist target versions + [\#6298](https://github.com/matrix-org/matrix-react-sdk/pull/6298) + * Fix browser crashing when searching for a malformed HTML tag + [\#6297](https://github.com/matrix-org/matrix-react-sdk/pull/6297) + * Add custom audio player + [\#6264](https://github.com/matrix-org/matrix-react-sdk/pull/6264) + * Lint MXC APIs to centralise access + [\#6293](https://github.com/matrix-org/matrix-react-sdk/pull/6293) + * Remove reminescent references to the tinter + [\#6290](https://github.com/matrix-org/matrix-react-sdk/pull/6290) + * More js-sdk type consolidation + [\#6263](https://github.com/matrix-org/matrix-react-sdk/pull/6263) + * Convert MessagePanel, TimelinePanel, ScrollPanel, and more to Typescript + [\#6243](https://github.com/matrix-org/matrix-react-sdk/pull/6243) + * Migrate to `eslint-plugin-matrix-org` + [\#6285](https://github.com/matrix-org/matrix-react-sdk/pull/6285) + * Avoid cyclic dependencies by moving watchers out of constructor + [\#6287](https://github.com/matrix-org/matrix-react-sdk/pull/6287) + * Add spacing between toast buttons with cross browser support in mind + [\#6284](https://github.com/matrix-org/matrix-react-sdk/pull/6284) + * Deprecate Tinter and TintableSVG + [\#6279](https://github.com/matrix-org/matrix-react-sdk/pull/6279) + * Migrate FilePanel to TypeScript + [\#6283](https://github.com/matrix-org/matrix-react-sdk/pull/6283) + Changes in [3.25.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.25.0) (2021-07-05) ===================================================================================================== [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.25.0-rc.1...v3.25.0) From 0fe91c07b854adfed9d927310fd79a0a0077c056 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Wed, 14 Jul 2021 16:27:35 +0100 Subject: [PATCH 0328/1171] v3.26.0-rc.1 --- package.json | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index d7933e4c59..a47a337273 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "3.25.0", + "version": "3.26.0-rc.1", "description": "SDK for matrix.org using React", "author": "matrix.org", "repository": { @@ -25,7 +25,7 @@ "bin": { "reskindex": "scripts/reskindex.js" }, - "main": "./src/index.js", + "main": "./lib/index.js", "matrix_src_main": "./src/index.js", "matrix_lib_main": "./lib/index.js", "matrix_lib_typings": "./lib/index.d.ts", @@ -198,5 +198,6 @@ "coverageReporters": [ "text" ] - } + }, + "typings": "./lib/index.d.ts" } From 4d16cfc951fb1c427e843965f06f8a9b1f9a558c Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 14 Jul 2021 16:41:01 +0100 Subject: [PATCH 0329/1171] Fix 'User' type import --- src/components/views/dialogs/VerificationRequestDialog.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/dialogs/VerificationRequestDialog.tsx b/src/components/views/dialogs/VerificationRequestDialog.tsx index 4d3123c274..65b7f71dbd 100644 --- a/src/components/views/dialogs/VerificationRequestDialog.tsx +++ b/src/components/views/dialogs/VerificationRequestDialog.tsx @@ -21,7 +21,7 @@ import { replaceableComponent } from "../../../utils/replaceableComponent"; import { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest"; import BaseDialog from "./BaseDialog"; import EncryptionPanel from "../right_panel/EncryptionPanel"; -import { User } from 'matrix-js-sdk'; +import { User } from 'matrix-js-sdk/src/models/user'; interface IProps { verificationRequest: VerificationRequest; From f42382edbd8922a8d02549b767e1060a5b70e72e Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 14 Jul 2021 23:29:54 +0100 Subject: [PATCH 0330/1171] Update PR template for new changelog stuff --- .github/PULL_REQUEST_TEMPLATE.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index fb237a5845..e9ede862d2 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,3 +1,15 @@ + + From 18343d839c9756785ecffdf5d294b0504c597525 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Thu, 15 Jul 2021 09:23:15 +0200 Subject: [PATCH 0331/1171] Show MSC2285 only if supported by server MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- .../settings/tabs/user/LabsUserSettingsTab.js | 23 +++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/src/components/views/settings/tabs/user/LabsUserSettingsTab.js b/src/components/views/settings/tabs/user/LabsUserSettingsTab.js index e57ad80bbc..18e78ae7b0 100644 --- a/src/components/views/settings/tabs/user/LabsUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/LabsUserSettingsTab.js @@ -19,11 +19,12 @@ import { _t } from "../../../../../languageHandler"; import PropTypes from "prop-types"; import SettingsStore from "../../../../../settings/SettingsStore"; import LabelledToggleSwitch from "../../../elements/LabelledToggleSwitch"; -import * as sdk from "../../../../../index"; import { SettingLevel } from "../../../../../settings/SettingLevel"; import { replaceableComponent } from "../../../../../utils/replaceableComponent"; import SdkConfig from "../../../../../SdkConfig"; import BetaCard from "../../../beta/BetaCard"; +import SettingsFlag from '../../../elements/SettingsFlag'; +import { MatrixClientPeg } from '../../../../../MatrixClientPeg'; export class LabsSettingToggle extends React.Component { static propTypes = { @@ -47,6 +48,14 @@ export class LabsSettingToggle extends React.Component { export default class LabsUserSettingsTab extends React.Component { constructor() { super(); + + MatrixClientPeg.get().doesServerSupportUnstableFeature("org.matrix.msc2285").then((showHiddenReadReceipts) => { + this.setState({ showHiddenReadReceipts }); + }); + + this.state = { + showHiddenReadReceipts: false, + }; } render() { @@ -65,16 +74,22 @@ export default class LabsUserSettingsTab extends React.Component { let labsSection; if (SdkConfig.get()['showLabsSettings']) { - const SettingsFlag = sdk.getComponent("views.elements.SettingsFlag"); const flags = labs.map(f => ); + let hiddenReadReceipts; + if (this.state.showHiddenReadReceipts) { + hiddenReadReceipts = ( + + ); + } + labsSection =
    - {flags} + { flags } - + { hiddenReadReceipts }
    ; } From 90d380c8aeb686963dfdef616b2bbf8222e74687 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 15 Jul 2021 08:26:49 +0100 Subject: [PATCH 0332/1171] Cache value of feature_spaces* flags as they cause page refresh so are immutable --- src/Avatar.ts | 4 +- src/autocomplete/Autocompleter.ts | 5 +-- src/autocomplete/RoomProvider.tsx | 5 ++- src/components/structures/LoggedInView.tsx | 3 +- src/components/structures/MatrixChat.tsx | 8 ++-- src/components/structures/RightPanel.tsx | 3 +- src/components/structures/RoomView.tsx | 9 ++--- src/components/structures/SpaceRoomView.tsx | 5 +-- src/components/structures/UserMenu.tsx | 4 +- .../views/dialogs/ForwardDialog.tsx | 3 +- src/components/views/dialogs/InviteDialog.tsx | 3 +- src/components/views/right_panel/UserInfo.tsx | 17 ++++---- src/components/views/rooms/MemberList.tsx | 5 ++- src/components/views/rooms/RoomList.tsx | 2 +- .../views/rooms/ThirdPartyMemberInfo.tsx | 4 +- src/components/views/spaces/SpacePanel.tsx | 5 +-- src/stores/BreadcrumbsStore.ts | 3 +- src/stores/SpaceStore.tsx | 39 ++++++++++++------- src/stores/room-list/RoomListStore.ts | 11 +++--- src/stores/room-list/SpaceWatcher.ts | 5 +-- src/stores/room-list/algorithms/Algorithm.ts | 3 +- .../room-list/filters/VisibilityProvider.ts | 4 +- 22 files changed, 82 insertions(+), 68 deletions(-) diff --git a/src/Avatar.ts b/src/Avatar.ts index 4c4bd1c265..198d4162a0 100644 --- a/src/Avatar.ts +++ b/src/Avatar.ts @@ -21,7 +21,7 @@ import { ResizeMethod } from "matrix-js-sdk/src/@types/partials"; import DMRoomMap from './utils/DMRoomMap'; import { mediaFromMxc } from "./customisations/Media"; -import SettingsStore from "./settings/SettingsStore"; +import SpaceStore from "./stores/SpaceStore"; // Not to be used for BaseAvatar urls as that has similar default avatar fallback already export function avatarUrlForMember( @@ -153,7 +153,7 @@ export function avatarUrlForRoom(room: Room, width: number, height: number, resi } // space rooms cannot be DMs so skip the rest - if (SettingsStore.getValue("feature_spaces") && room.isSpaceRoom()) return null; + if (SpaceStore.spacesEnabled && room.isSpaceRoom()) return null; let otherMember = null; const otherUserId = DMRoomMap.shared().getUserIdForRoomId(room.roomId); diff --git a/src/autocomplete/Autocompleter.ts b/src/autocomplete/Autocompleter.ts index 7ab2ae70ea..acc7846510 100644 --- a/src/autocomplete/Autocompleter.ts +++ b/src/autocomplete/Autocompleter.ts @@ -27,8 +27,8 @@ import EmojiProvider from './EmojiProvider'; import NotifProvider from './NotifProvider'; import { timeout } from "../utils/promise"; import AutocompleteProvider, { ICommand } from "./AutocompleteProvider"; -import SettingsStore from "../settings/SettingsStore"; import SpaceProvider from "./SpaceProvider"; +import SpaceStore from "../stores/SpaceStore"; export interface ISelectionRange { beginning?: boolean; // whether the selection is in the first block of the editor or not @@ -58,8 +58,7 @@ const PROVIDERS = [ 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")) { +if (SpaceStore.spacesEnabled) { PROVIDERS.push(SpaceProvider); } else { PROVIDERS.push(CommunityProvider); diff --git a/src/autocomplete/RoomProvider.tsx b/src/autocomplete/RoomProvider.tsx index 7865a76daa..37ddf2c387 100644 --- a/src/autocomplete/RoomProvider.tsx +++ b/src/autocomplete/RoomProvider.tsx @@ -28,7 +28,7 @@ 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"; +import SpaceStore from "../stores/SpaceStore"; const ROOM_REGEX = /\B#\S*/g; @@ -59,7 +59,8 @@ export default class RoomProvider extends AutocompleteProvider { const cli = MatrixClientPeg.get(); let rooms = cli.getVisibleRooms(); - if (SettingsStore.getValue("feature_spaces")) { + // if spaces are enabled then filter them out here as they get their own autocomplete provider + if (SpaceStore.spacesEnabled) { rooms = rooms.filter(r => !r.isSpaceRoom()); } diff --git a/src/components/structures/LoggedInView.tsx b/src/components/structures/LoggedInView.tsx index 89fa8db376..6c086ed17c 100644 --- a/src/components/structures/LoggedInView.tsx +++ b/src/components/structures/LoggedInView.tsx @@ -63,6 +63,7 @@ import ToastContainer from './ToastContainer'; import MyGroups from "./MyGroups"; import UserView from "./UserView"; import GroupView from "./GroupView"; +import SpaceStore from "../../stores/SpaceStore"; // We need to fetch each pinned message individually (if we don't already have it) // so each pinned message may trigger a request. Limit the number per room for sanity. @@ -631,7 +632,7 @@ class LoggedInView extends React.Component { >
    - { SettingsStore.getValue("feature_spaces") ? : null } + { SpaceStore.spacesEnabled ? : null } { private leaveRoomWarnings(roomId: string) { const roomToLeave = MatrixClientPeg.get().getRoom(roomId); - const isSpace = SettingsStore.getValue("feature_spaces") && roomToLeave?.isSpaceRoom(); + const isSpace = SpaceStore.spacesEnabled && roomToLeave?.isSpaceRoom(); // Show a warning if there are additional complications. const warnings = []; @@ -1137,7 +1137,7 @@ export default class MatrixChat extends React.PureComponent { const roomToLeave = MatrixClientPeg.get().getRoom(roomId); const warnings = this.leaveRoomWarnings(roomId); - const isSpace = SettingsStore.getValue("feature_spaces") && roomToLeave?.isSpaceRoom(); + const isSpace = SpaceStore.spacesEnabled && roomToLeave?.isSpaceRoom(); Modal.createTrackedDialog(isSpace ? "Leave space" : "Leave room", '', QuestionDialog, { title: isSpace ? _t("Leave space") : _t("Leave room"), description: ( @@ -1687,7 +1687,7 @@ export default class MatrixChat extends React.PureComponent { const type = screen === "start_sso" ? "sso" : "cas"; PlatformPeg.get().startSingleSignOn(cli, type, this.getFragmentAfterLogin()); } else if (screen === 'groups') { - if (SettingsStore.getValue("feature_spaces")) { + if (SpaceStore.spacesEnabled) { dis.dispatch({ action: "view_home_page" }); return; } @@ -1774,7 +1774,7 @@ export default class MatrixChat extends React.PureComponent { subAction: params.action, }); } else if (screen.indexOf('group/') === 0) { - if (SettingsStore.getValue("feature_spaces")) { + if (SpaceStore.spacesEnabled) { dis.dispatch({ action: "view_home_page" }); return; } diff --git a/src/components/structures/RightPanel.tsx b/src/components/structures/RightPanel.tsx index 63027ab627..2a3448b017 100644 --- a/src/components/structures/RightPanel.tsx +++ b/src/components/structures/RightPanel.tsx @@ -48,6 +48,7 @@ import NotificationPanel from "./NotificationPanel"; import ResizeNotifier from "../../utils/ResizeNotifier"; import PinnedMessagesCard from "../views/right_panel/PinnedMessagesCard"; import { throttle } from 'lodash'; +import SpaceStore from "../../stores/SpaceStore"; interface IProps { room?: Room; // if showing panels for a given room, this is set @@ -107,7 +108,7 @@ export default class RightPanel extends React.Component { return RightPanelPhases.GroupMemberList; } return rps.groupPanelPhase; - } else if (SettingsStore.getValue("feature_spaces") && this.props.room?.isSpaceRoom() + } else if (SpaceStore.spacesEnabled && this.props.room?.isSpaceRoom() && !RIGHT_PANEL_SPACE_PHASES.includes(rps.roomPanelPhase) ) { return RightPanelPhases.SpaceMemberList; diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index 2c118149a0..a8f9e7ccb6 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -89,6 +89,7 @@ import RoomStatusBar from "./RoomStatusBar"; import MessageComposer from '../views/rooms/MessageComposer'; import JumpToBottomButton from "../views/rooms/JumpToBottomButton"; import TopUnreadMessagesBar from "../views/rooms/TopUnreadMessagesBar"; +import SpaceStore from "../../stores/SpaceStore"; const DEBUG = false; let debuglog = function(msg: string) {}; @@ -1748,10 +1749,8 @@ export default class RoomView extends React.Component { } const myMembership = this.state.room.getMyMembership(); - if (myMembership === "invite" - // SpaceRoomView handles invites itself - && (!SettingsStore.getValue("feature_spaces") || !this.state.room.isSpaceRoom()) - ) { + // SpaceRoomView handles invites itself + if (myMembership === "invite" && (!SpaceStore.spacesEnabled || !this.state.room.isSpaceRoom())) { if (this.state.joining || this.state.rejecting) { return ( @@ -1882,7 +1881,7 @@ export default class RoomView extends React.Component { room={this.state.room} /> ); - if (!this.state.canPeek && (!SettingsStore.getValue("feature_spaces") || !this.state.room?.isSpaceRoom())) { + if (!this.state.canPeek && (!SpaceStore.spacesEnabled || !this.state.room?.isSpaceRoom())) { return (
    { previewBar } diff --git a/src/components/structures/SpaceRoomView.tsx b/src/components/structures/SpaceRoomView.tsx index 24b460284f..0ee68a9578 100644 --- a/src/components/structures/SpaceRoomView.tsx +++ b/src/components/structures/SpaceRoomView.tsx @@ -62,7 +62,6 @@ import IconizedContextMenu, { import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton"; import { BetaPill } from "../views/beta/BetaCard"; import { UserTab } from "../views/dialogs/UserSettingsDialog"; -import SettingsStore from "../../settings/SettingsStore"; import Modal from "../../Modal"; import BetaFeedbackDialog from "../views/dialogs/BetaFeedbackDialog"; import SdkConfig from "../../SdkConfig"; @@ -178,7 +177,7 @@ const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }) => const [busy, setBusy] = useState(false); - const spacesEnabled = SettingsStore.getValue("feature_spaces"); + const spacesEnabled = SpaceStore.spacesEnabled; const cannotJoin = getEffectiveMembership(myMembership) === EffectiveMembership.Leave && space.getJoinRule() !== JoinRule.Public; @@ -854,7 +853,7 @@ export default class SpaceRoomView extends React.PureComponent { private renderBody() { switch (this.state.phase) { case Phase.Landing: - if (this.state.myMembership === "join" && SettingsStore.getValue("feature_spaces")) { + if (this.state.myMembership === "join" && SpaceStore.spacesEnabled) { return ; } else { return { }; OwnProfileStore.instance.on(UPDATE_EVENT, this.onProfileUpdate); - if (SettingsStore.getValue("feature_spaces")) { + if (SpaceStore.spacesEnabled) { SpaceStore.instance.on(UPDATE_SELECTED_SPACE, this.onSelectedSpaceUpdate); } @@ -115,7 +115,7 @@ export default class UserMenu extends React.Component { if (this.dispatcherRef) defaultDispatcher.unregister(this.dispatcherRef); OwnProfileStore.instance.off(UPDATE_EVENT, this.onProfileUpdate); this.tagStoreRef.remove(); - if (SettingsStore.getValue("feature_spaces")) { + if (SpaceStore.spacesEnabled) { SpaceStore.instance.off(UPDATE_SELECTED_SPACE, this.onSelectedSpaceUpdate); } MatrixClientPeg.get().removeListener("Room", this.onRoom); diff --git a/src/components/views/dialogs/ForwardDialog.tsx b/src/components/views/dialogs/ForwardDialog.tsx index ba06436ae2..839ca6da2f 100644 --- a/src/components/views/dialogs/ForwardDialog.tsx +++ b/src/components/views/dialogs/ForwardDialog.tsx @@ -43,6 +43,7 @@ import QueryMatcher from "../../../autocomplete/QueryMatcher"; import TruncatedList from "../elements/TruncatedList"; import EntityTile from "../rooms/EntityTile"; import BaseAvatar from "../avatars/BaseAvatar"; +import SpaceStore from "../../../stores/SpaceStore"; const AVATAR_SIZE = 30; @@ -180,7 +181,7 @@ const ForwardDialog: React.FC = ({ matrixClient: cli, event, permalinkCr const [query, setQuery] = useState(""); const lcQuery = query.toLowerCase(); - const spacesEnabled = useFeatureEnabled("feature_spaces"); + const spacesEnabled = SpaceStore.spacesEnabled; const flairEnabled = useFeatureEnabled(UIFeature.Flair); const previewLayout = useSettingValue("layout"); diff --git a/src/components/views/dialogs/InviteDialog.tsx b/src/components/views/dialogs/InviteDialog.tsx index c9475d4849..2aa14449df 100644 --- a/src/components/views/dialogs/InviteDialog.tsx +++ b/src/components/views/dialogs/InviteDialog.tsx @@ -67,6 +67,7 @@ import GenericTextContextMenu from "../context_menus/GenericTextContextMenu"; import QuestionDialog from "./QuestionDialog"; import Spinner from "../elements/Spinner"; import BaseDialog from "./BaseDialog"; +import SpaceStore from "../../../stores/SpaceStore"; // we have a number of types defined from the Matrix spec which can't reasonably be altered here. /* eslint-disable camelcase */ @@ -1364,7 +1365,7 @@ export default class InviteDialog extends React.PureComponent; } else if (this.props.kind === KIND_INVITE) { const room = MatrixClientPeg.get()?.getRoom(this.props.roomId); - const isSpace = SettingsStore.getValue("feature_spaces") && room?.isSpaceRoom(); + const isSpace = SpaceStore.spacesEnabled && room?.isSpaceRoom(); title = isSpace ? _t("Invite to %(spaceName)s", { spaceName: room.name || _t("Unnamed Space"), diff --git a/src/components/views/right_panel/UserInfo.tsx b/src/components/views/right_panel/UserInfo.tsx index e9d80d49c5..fc3814136d 100644 --- a/src/components/views/right_panel/UserInfo.tsx +++ b/src/components/views/right_panel/UserInfo.tsx @@ -69,6 +69,7 @@ import RoomName from "../elements/RoomName"; import { mediaFromMxc } from "../../../customisations/Media"; import UIStore from "../../../stores/UIStore"; import { ComposerInsertPayload } from "../../../dispatcher/payloads/ComposerInsertPayload"; +import SpaceStore from "../../../stores/SpaceStore"; export interface IDevice { deviceId: string; @@ -728,7 +729,7 @@ const MuteToggleButton: React.FC = ({ member, room, powerLevels, // if muting self, warn as it may be irreversible if (target === cli.getUserId()) { try { - if (!(await warnSelfDemote(SettingsStore.getValue("feature_spaces") && room?.isSpaceRoom()))) return; + if (!(await warnSelfDemote(SpaceStore.spacesEnabled && room?.isSpaceRoom()))) return; } catch (e) { console.error("Failed to warn about self demotion: ", e); return; @@ -817,7 +818,7 @@ const RoomAdminToolsContainer: React.FC = ({ if (canAffectUser && me.powerLevel >= kickPowerLevel) { kickButton = ; } - if (me.powerLevel >= redactPowerLevel && (!SettingsStore.getValue("feature_spaces") || !room.isSpaceRoom())) { + if (me.powerLevel >= redactPowerLevel && (!SpaceStore.spacesEnabled || !room.isSpaceRoom())) { redactButton = ( ); @@ -1096,7 +1097,7 @@ const PowerLevelEditor: React.FC<{ } else if (myUserId === target) { // If we are changing our own PL it can only ever be decreasing, which we cannot reverse. try { - if (!(await warnSelfDemote(SettingsStore.getValue("feature_spaces") && room?.isSpaceRoom()))) return; + if (!(await warnSelfDemote(SpaceStore.spacesEnabled && room?.isSpaceRoom()))) return; } catch (e) { console.error("Failed to warn about self demotion: ", e); } @@ -1326,10 +1327,10 @@ const BasicUserInfo: React.FC<{ if (!isRoomEncrypted) { if (!cryptoEnabled) { text = _t("This client does not support end-to-end encryption."); - } else if (room && (!SettingsStore.getValue("feature_spaces") || !room.isSpaceRoom())) { + } else if (room && (!SpaceStore.spacesEnabled || !room.isSpaceRoom())) { text = _t("Messages in this room are not end-to-end encrypted."); } - } else if (!SettingsStore.getValue("feature_spaces") || !room.isSpaceRoom()) { + } else if (!SpaceStore.spacesEnabled || !room.isSpaceRoom()) { text = _t("Messages in this room are end-to-end encrypted."); } @@ -1405,7 +1406,7 @@ const BasicUserInfo: React.FC<{ canInvite={roomPermissions.canInvite} isIgnored={isIgnored} member={member as RoomMember} - isSpace={SettingsStore.getValue("feature_spaces") && room?.isSpaceRoom()} + isSpace={SpaceStore.spacesEnabled && room?.isSpaceRoom()} /> { adminToolsContainer } @@ -1568,7 +1569,7 @@ const UserInfo: React.FC = ({ previousPhase = RightPanelPhases.RoomMemberInfo; refireParams = { member: member }; } else if (room) { - previousPhase = previousPhase = SettingsStore.getValue("feature_spaces") && room.isSpaceRoom() + previousPhase = previousPhase = SpaceStore.spacesEnabled && room.isSpaceRoom() ? RightPanelPhases.SpaceMemberList : RightPanelPhases.RoomMemberList; } @@ -1617,7 +1618,7 @@ const UserInfo: React.FC = ({ } let scopeHeader; - if (SettingsStore.getValue("feature_spaces") && room?.isSpaceRoom()) { + if (SpaceStore.spacesEnabled && room?.isSpaceRoom()) { scopeHeader =
    diff --git a/src/components/views/rooms/MemberList.tsx b/src/components/views/rooms/MemberList.tsx index f4df70c7ee..71e54404c0 100644 --- a/src/components/views/rooms/MemberList.tsx +++ b/src/components/views/rooms/MemberList.tsx @@ -43,6 +43,7 @@ import EntityTile from "./EntityTile"; import MemberTile from "./MemberTile"; import BaseAvatar from '../avatars/BaseAvatar'; import { throttle } from 'lodash'; +import SpaceStore from "../../../stores/SpaceStore"; const INITIAL_LOAD_NUM_MEMBERS = 30; const INITIAL_LOAD_NUM_INVITED = 5; @@ -509,7 +510,7 @@ export default class MemberList extends React.Component { const chat = CommunityPrototypeStore.instance.getSelectedCommunityGeneralChat(); if (chat && chat.roomId === this.props.roomId) { inviteButtonText = _t("Invite to this community"); - } else if (SettingsStore.getValue("feature_spaces") && room.isSpaceRoom()) { + } else if (SpaceStore.spacesEnabled && room.isSpaceRoom()) { inviteButtonText = _t("Invite to this space"); } @@ -549,7 +550,7 @@ export default class MemberList extends React.Component { let previousPhase = RightPanelPhases.RoomSummary; // We have no previousPhase for when viewing a MemberList from a Space let scopeHeader; - if (SettingsStore.getValue("feature_spaces") && room?.isSpaceRoom()) { + if (SpaceStore.spacesEnabled && room?.isSpaceRoom()) { previousPhase = undefined; scopeHeader =
    diff --git a/src/components/views/rooms/RoomList.tsx b/src/components/views/rooms/RoomList.tsx index c94256800d..7ece6add9c 100644 --- a/src/components/views/rooms/RoomList.tsx +++ b/src/components/views/rooms/RoomList.tsx @@ -417,7 +417,7 @@ export default class RoomList extends React.PureComponent { } private renderCommunityInvites(): ReactComponentElement[] { - if (SettingsStore.getValue("feature_spaces")) return []; + if (SpaceStore.spacesEnabled) 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 => { diff --git a/src/components/views/rooms/ThirdPartyMemberInfo.tsx b/src/components/views/rooms/ThirdPartyMemberInfo.tsx index 2bcc3ead57..51bb891c62 100644 --- a/src/components/views/rooms/ThirdPartyMemberInfo.tsx +++ b/src/components/views/rooms/ThirdPartyMemberInfo.tsx @@ -25,9 +25,9 @@ import { isValid3pidInvite } from "../../../RoomInvite"; import RoomAvatar from "../avatars/RoomAvatar"; import RoomName from "../elements/RoomName"; import { replaceableComponent } from "../../../utils/replaceableComponent"; -import SettingsStore from "../../../settings/SettingsStore"; import ErrorDialog from '../dialogs/ErrorDialog'; import AccessibleButton from '../elements/AccessibleButton'; +import SpaceStore from "../../../stores/SpaceStore"; interface IProps { event: MatrixEvent; @@ -134,7 +134,7 @@ export default class ThirdPartyMemberInfo extends React.Component diff --git a/src/components/views/spaces/SpacePanel.tsx b/src/components/views/spaces/SpacePanel.tsx index 5b3cf31cad..9cefbbd94c 100644 --- a/src/components/views/spaces/SpacePanel.tsx +++ b/src/components/views/spaces/SpacePanel.tsx @@ -42,7 +42,6 @@ import { import { Key } from "../../../Keyboard"; import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore"; import { NotificationState } from "../../../stores/notifications/NotificationState"; -import SettingsStore from "../../../settings/SettingsStore"; interface IButtonProps { space?: Room; @@ -134,7 +133,7 @@ const InnerSpacePanel = React.memo(({ children, isPanelCo const [invites, spaces, activeSpace] = useSpaces(); const activeSpaces = activeSpace ? [activeSpace] : []; - const homeNotificationState = SettingsStore.getValue("feature_spaces.all_rooms") + const homeNotificationState = SpaceStore.spacesTweakAllRoomsEnabled ? RoomNotificationStateStore.instance.globalState : SpaceStore.instance.getNotificationState(HOME_SPACE); return
    @@ -142,7 +141,7 @@ const InnerSpacePanel = React.memo(({ children, isPanelCo className="mx_SpaceButton_home" onClick={() => SpaceStore.instance.setActiveSpace(null)} selected={!activeSpace} - tooltip={SettingsStore.getValue("feature_spaces.all_rooms") ? _t("All rooms") : _t("Home")} + tooltip={SpaceStore.spacesTweakAllRoomsEnabled ? _t("All rooms") : _t("Home")} notificationState={homeNotificationState} isNarrow={isPanelCollapsed} /> diff --git a/src/stores/BreadcrumbsStore.ts b/src/stores/BreadcrumbsStore.ts index a3b07435c6..aceaf8b898 100644 --- a/src/stores/BreadcrumbsStore.ts +++ b/src/stores/BreadcrumbsStore.ts @@ -22,6 +22,7 @@ import defaultDispatcher from "../dispatcher/dispatcher"; import { arrayHasDiff } from "../utils/arrays"; import { isNullOrUndefined } from "matrix-js-sdk/src/utils"; import { SettingLevel } from "../settings/SettingLevel"; +import SpaceStore from "./SpaceStore"; const MAX_ROOMS = 20; // arbitrary const AUTOJOIN_WAIT_THRESHOLD_MS = 90000; // 90s, the time we wait for an autojoined room to show up @@ -122,7 +123,7 @@ export class BreadcrumbsStore extends AsyncStoreWithClient { } private async appendRoom(room: Room) { - if (SettingsStore.getValue("feature_spaces") && room.isSpaceRoom()) return; // hide space rooms + if (SpaceStore.spacesEnabled && room.isSpaceRoom()) return; // hide space rooms let updated = false; const rooms = (this.state.rooms || []).slice(); // cheap clone diff --git a/src/stores/SpaceStore.tsx b/src/stores/SpaceStore.tsx index 99705a7aba..1a6b5109ec 100644 --- a/src/stores/SpaceStore.tsx +++ b/src/stores/SpaceStore.tsx @@ -59,7 +59,13 @@ export interface ISuggestedRoom extends ISpaceSummaryRoom { const MAX_SUGGESTED_ROOMS = 20; -const homeSpaceKey = SettingsStore.getValue("feature_spaces.all_rooms") ? "ALL_ROOMS" : "HOME_SPACE"; +// All of these settings cause the page to reload and can be costly if read frequently, so read them here only +const spacesEnabled = SettingsStore.getValue("feature_spaces"); +const spacesTweakAllRoomsEnabled = SettingsStore.getValue("feature_spaces.all_rooms"); +const spacesTweakSpaceMemberDMsEnabled = SettingsStore.getValue("feature_spaces.space_member_dms"); +const spacesTweakSpaceDMBadgesEnabled = SettingsStore.getValue("feature_spaces.space_dm_badges"); + +const homeSpaceKey = spacesTweakAllRoomsEnabled ? "ALL_ROOMS" : "HOME_SPACE"; const getSpaceContextKey = (space?: Room) => `mx_space_context_${space?.roomId || homeSpaceKey}`; const partitionSpacesAndRooms = (arr: Room[]): [Room[], Room[]] => { // [spaces, rooms] @@ -260,7 +266,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { } public getSpaceFilteredRoomIds = (space: Room | null): Set => { - if (!space && SettingsStore.getValue("feature_spaces.all_rooms")) { + if (!space && spacesTweakAllRoomsEnabled) { return new Set(this.matrixClient.getVisibleRooms().map(r => r.roomId)); } return this.spaceFilteredRooms.get(space?.roomId || HOME_SPACE) || new Set(); @@ -357,7 +363,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { }; private showInHomeSpace = (room: Room) => { - if (SettingsStore.getValue("feature_spaces.all_rooms")) return true; + if (spacesTweakAllRoomsEnabled) return true; if (room.isSpaceRoom()) return false; return !this.parentMap.get(room.roomId)?.size // put all orphaned rooms in the Home Space || DMRoomMap.shared().getUserIdForRoomId(room.roomId) // put all DMs in the Home Space @@ -389,7 +395,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { const oldFilteredRooms = this.spaceFilteredRooms; this.spaceFilteredRooms = new Map(); - if (!SettingsStore.getValue("feature_spaces.all_rooms")) { + if (!spacesTweakAllRoomsEnabled) { // put all room invites in the Home Space const invites = visibleRooms.filter(r => !r.isSpaceRoom() && r.getMyMembership() === "invite"); this.spaceFilteredRooms.set(HOME_SPACE, new Set(invites.map(room => room.roomId))); @@ -416,7 +422,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { const roomIds = new Set(childRooms.map(r => r.roomId)); const space = this.matrixClient?.getRoom(spaceId); - if (SettingsStore.getValue("feature_spaces.space_member_dms")) { + if (spacesTweakSpaceMemberDMsEnabled) { // Add relevant DMs space?.getMembers().forEach(member => { if (member.membership !== "join" && member.membership !== "invite") return; @@ -450,7 +456,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { // Update NotificationStates this.getNotificationState(s)?.setRooms(visibleRooms.filter(room => { if (roomIds.has(room.roomId)) { - if (s !== HOME_SPACE && SettingsStore.getValue("feature_spaces.space_dm_badges")) return true; + if (s !== HOME_SPACE && spacesTweakSpaceDMBadgesEnabled) return true; return !DMRoomMap.shared().getUserIdForRoomId(room.roomId) || RoomListStore.instance.getTagsForRoom(room).includes(DefaultTagID.Favourite); @@ -549,7 +555,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { // TODO confirm this after implementing parenting behaviour if (room.isSpaceRoom()) { this.onSpaceUpdate(); - } else if (!SettingsStore.getValue("feature_spaces.all_rooms")) { + } else if (!spacesTweakAllRoomsEnabled) { this.onRoomUpdate(room); } this.emit(room.roomId); @@ -573,7 +579,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { if (order !== lastOrder) { this.notifyIfOrderChanged(); } - } else if (ev.getType() === EventType.Tag && !SettingsStore.getValue("feature_spaces.all_rooms")) { + } else if (ev.getType() === EventType.Tag && !spacesTweakAllRoomsEnabled) { // If the room was in favourites and now isn't or the opposite then update its position in the trees const oldTags = lastEv?.getContent()?.tags || {}; const newTags = ev.getContent()?.tags || {}; @@ -613,13 +619,13 @@ export class SpaceStoreClass extends AsyncStoreWithClient { } protected async onNotReady() { - if (!SettingsStore.getValue("feature_spaces")) return; + if (!SpaceStore.spacesEnabled) return; if (this.matrixClient) { this.matrixClient.removeListener("Room", this.onRoom); this.matrixClient.removeListener("Room.myMembership", this.onRoom); this.matrixClient.removeListener("Room.accountData", this.onRoomAccountData); this.matrixClient.removeListener("RoomState.events", this.onRoomState); - if (!SettingsStore.getValue("feature_spaces.all_rooms")) { + if (!spacesTweakAllRoomsEnabled) { this.matrixClient.removeListener("accountData", this.onAccountData); } } @@ -627,12 +633,12 @@ export class SpaceStoreClass extends AsyncStoreWithClient { } protected async onReady() { - if (!SettingsStore.getValue("feature_spaces")) return; + if (!spacesEnabled) return; this.matrixClient.on("Room", this.onRoom); this.matrixClient.on("Room.myMembership", this.onRoom); this.matrixClient.on("Room.accountData", this.onRoomAccountData); this.matrixClient.on("RoomState.events", this.onRoomState); - if (!SettingsStore.getValue("feature_spaces.all_rooms")) { + if (!spacesTweakAllRoomsEnabled) { this.matrixClient.on("accountData", this.onAccountData); } @@ -646,7 +652,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { } protected async onAction(payload: ActionPayload) { - if (!SettingsStore.getValue("feature_spaces")) return; + if (!spacesEnabled) return; switch (payload.action) { case "view_room": { // Don't auto-switch rooms when reacting to a context-switch @@ -660,7 +666,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { // as it will cause you to end up in the wrong room this.setActiveSpace(room, false); } else if ( - (!SettingsStore.getValue("feature_spaces.all_rooms") || this.activeSpace) && + (!spacesTweakAllRoomsEnabled || this.activeSpace) && !this.getSpaceFilteredRoomIds(this.activeSpace).has(roomId) ) { this.switchToRelatedSpace(roomId); @@ -752,6 +758,11 @@ export class SpaceStoreClass extends AsyncStoreWithClient { } export default class SpaceStore { + public static spacesEnabled = spacesEnabled; + public static spacesTweakAllRoomsEnabled = spacesTweakAllRoomsEnabled; + public static spacesTweakSpaceMemberDMsEnabled = spacesTweakSpaceMemberDMsEnabled; + public static spacesTweakSpaceDMBadgesEnabled = spacesTweakSpaceDMBadgesEnabled; + private static internalInstance = new SpaceStoreClass(); public static get instance(): SpaceStoreClass { diff --git a/src/stores/room-list/RoomListStore.ts b/src/stores/room-list/RoomListStore.ts index e26c80bb2d..a87e45acb7 100644 --- a/src/stores/room-list/RoomListStore.ts +++ b/src/stores/room-list/RoomListStore.ts @@ -35,6 +35,7 @@ import { NameFilterCondition } from "./filters/NameFilterCondition"; import { RoomNotificationStateStore } from "../notifications/RoomNotificationStateStore"; import { VisibilityProvider } from "./filters/VisibilityProvider"; import { SpaceWatcher } from "./SpaceWatcher"; +import SpaceStore from "../SpaceStore"; interface IState { tagsEnabled?: boolean; @@ -76,7 +77,7 @@ export class RoomListStoreClass extends AsyncStoreWithClient { } private setupWatchers() { - if (SettingsStore.getValue("feature_spaces")) { + if (SpaceStore.spacesEnabled) { this.spaceWatcher = new SpaceWatcher(this); } else { this.tagWatcher = new TagWatcher(this); @@ -608,9 +609,7 @@ export class RoomListStoreClass extends AsyncStoreWithClient { // if spaces are enabled only consider the prefilter conditions when there are no runtime conditions // for the search all spaces feature - if (this.prefilterConditions.length > 0 - && (!SettingsStore.getValue("feature_spaces") || !this.filterConditions.length) - ) { + if (this.prefilterConditions.length > 0 && (!SpaceStore.spacesEnabled || !this.filterConditions.length)) { rooms = rooms.filter(r => { for (const filter of this.prefilterConditions) { if (!filter.isVisible(r)) { @@ -682,7 +681,7 @@ export class RoomListStoreClass extends AsyncStoreWithClient { } else { this.filterConditions.push(filter); // Runtime filters with spaces disable prefiltering for the search all spaces feature - if (SettingsStore.getValue("feature_spaces")) { + if (SpaceStore.spacesEnabled) { // this has to be awaited so that `setKnownRooms` is called in time for the `addFilterCondition` below // this way the runtime filters are only evaluated on one dataset and not both. await this.recalculatePrefiltering(); @@ -715,7 +714,7 @@ export class RoomListStoreClass extends AsyncStoreWithClient { this.algorithm.removeFilterCondition(filter); } // Runtime filters with spaces disable prefiltering for the search all spaces feature - if (SettingsStore.getValue("feature_spaces")) { + if (SpaceStore.spacesEnabled) { promise = this.recalculatePrefiltering(); } } diff --git a/src/stores/room-list/SpaceWatcher.ts b/src/stores/room-list/SpaceWatcher.ts index a1f7786578..1cec612e6f 100644 --- a/src/stores/room-list/SpaceWatcher.ts +++ b/src/stores/room-list/SpaceWatcher.ts @@ -19,7 +19,6 @@ import { Room } from "matrix-js-sdk/src/models/room"; import { RoomListStoreClass } from "./RoomListStore"; import { SpaceFilterCondition } from "./filters/SpaceFilterCondition"; import SpaceStore, { UPDATE_SELECTED_SPACE } from "../SpaceStore"; -import SettingsStore from "../../settings/SettingsStore"; /** * Watches for changes in spaces to manage the filter on the provided RoomListStore @@ -29,7 +28,7 @@ export class SpaceWatcher { private activeSpace: Room = SpaceStore.instance.activeSpace; constructor(private store: RoomListStoreClass) { - if (!SettingsStore.getValue("feature_spaces.all_rooms")) { + if (!SpaceStore.spacesTweakAllRoomsEnabled) { this.filter = new SpaceFilterCondition(); this.updateFilter(); store.addFilter(this.filter); @@ -41,7 +40,7 @@ export class SpaceWatcher { this.activeSpace = activeSpace; if (this.filter) { - if (activeSpace || !SettingsStore.getValue("feature_spaces.all_rooms")) { + if (activeSpace || !SpaceStore.spacesTweakAllRoomsEnabled) { this.updateFilter(); } else { this.store.removeFilter(this.filter); diff --git a/src/stores/room-list/algorithms/Algorithm.ts b/src/stores/room-list/algorithms/Algorithm.ts index 024c484c41..f50d112248 100644 --- a/src/stores/room-list/algorithms/Algorithm.ts +++ b/src/stores/room-list/algorithms/Algorithm.ts @@ -34,6 +34,7 @@ import { OrderingAlgorithm } from "./list-ordering/OrderingAlgorithm"; import { getListAlgorithmInstance } from "./list-ordering"; import SettingsStore from "../../../settings/SettingsStore"; import { VisibilityProvider } from "../filters/VisibilityProvider"; +import SpaceStore from "../../SpaceStore"; /** * Fired when the Algorithm has determined a list has been updated. @@ -199,7 +200,7 @@ export class Algorithm extends EventEmitter { } private async doUpdateStickyRoom(val: Room) { - if (SettingsStore.getValue("feature_spaces") && val?.isSpaceRoom() && val.getMyMembership() !== "invite") { + if (SpaceStore.spacesEnabled && val?.isSpaceRoom() && val.getMyMembership() !== "invite") { // no-op sticky rooms for spaces - they're effectively virtual rooms val = null; } diff --git a/src/stores/room-list/filters/VisibilityProvider.ts b/src/stores/room-list/filters/VisibilityProvider.ts index a6c55226b0..f63b622053 100644 --- a/src/stores/room-list/filters/VisibilityProvider.ts +++ b/src/stores/room-list/filters/VisibilityProvider.ts @@ -18,7 +18,7 @@ import { Room } from "matrix-js-sdk/src/models/room"; import CallHandler from "../../../CallHandler"; import { RoomListCustomisations } from "../../../customisations/RoomList"; import VoipUserMapper from "../../../VoipUserMapper"; -import SettingsStore from "../../../settings/SettingsStore"; +import SpaceStore from "../../SpaceStore"; export class VisibilityProvider { private static internalInstance: VisibilityProvider; @@ -50,7 +50,7 @@ export class VisibilityProvider { } // hide space rooms as they'll be shown in the SpacePanel - if (SettingsStore.getValue("feature_spaces") && room.isSpaceRoom()) { + if (SpaceStore.spacesEnabled && room.isSpaceRoom()) { return false; } From 80f9793c733866a4292103bb1a8f52febba32bed Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 15 Jul 2021 08:29:50 +0100 Subject: [PATCH 0333/1171] only show space beta tweaks if you have the beta enabled as they do nothing otherwise --- src/components/views/beta/BetaCard.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/beta/BetaCard.tsx b/src/components/views/beta/BetaCard.tsx index 3127e1a915..ec662d831b 100644 --- a/src/components/views/beta/BetaCard.tsx +++ b/src/components/views/beta/BetaCard.tsx @@ -105,7 +105,7 @@ const BetaCard = ({ title: titleOverride, featureId }: IProps) => {
    - { extraSettings &&
    + { extraSettings && value &&
    { extraSettings.map(key => ( )) } From 5986609731daae10b35dbb6847208e251876b659 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 15 Jul 2021 10:05:37 +0100 Subject: [PATCH 0334/1171] i18n --- src/i18n/strings/en_EN.json | 73 ++++++++++++++++++++++++++----------- 1 file changed, 52 insertions(+), 21 deletions(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 5cc900a21b..50c5abbcf4 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -437,8 +437,6 @@ "To use it, just wait for autocomplete results to load and tab through them.": "To use it, just wait for autocomplete results to load and tab through them.", "Upgrades a room to a new version": "Upgrades a room to a new version", "You do not have the required permissions to use this command.": "You do not have the required permissions to use this command.", - "Error upgrading room": "Error upgrading room", - "Double check that your server supports the room version chosen and try again.": "Double check that your server supports the room version chosen and try again.", "Changes your display nickname": "Changes your display nickname", "Changes your display nickname in the current room only": "Changes your display nickname in the current room only", "Changes the avatar of the current room": "Changes the avatar of the current room", @@ -732,6 +730,8 @@ "Common names and surnames are easy to guess": "Common names and surnames are easy to guess", "Straight rows of keys are easy to guess": "Straight rows of keys are easy to guess", "Short keyboard patterns are easy to guess": "Short keyboard patterns are easy to guess", + "Error upgrading room": "Error upgrading room", + "Double check that your server supports the room version chosen and try again.": "Double check that your server supports the room version chosen and try again.", "Invite to %(spaceName)s": "Invite to %(spaceName)s", "Share your public space": "Share your public space", "Unknown App": "Unknown App", @@ -778,6 +778,16 @@ "The person who invited you already left the room.": "The person who invited you already left the room.", "The person who invited you already left the room, or their server is offline.": "The person who invited you already left the room, or their server is offline.", "Failed to join room": "Failed to join room", + "New in the Spaces beta": "New in the Spaces beta", + "Help people in spaces to find and join private rooms": "Help people in spaces to find and join private rooms", + "Learn more": "Learn more", + "Help space members find private rooms": "Help space members find private rooms", + "To help space members find and join a private room, go to that room's Security & Privacy settings.": "To help space members find and join a private room, go to that room's Security & Privacy settings.", + "General": "General", + "Security & Privacy": "Security & Privacy", + "Roles & Permissions": "Roles & Permissions", + "This make it easy for rooms to stay private to a space, while letting people in the space find and join them. All new rooms in a space will have this option available.": "This make it easy for rooms to stay private to a space, while letting people in the space find and join them. All new rooms in a space will have this option available.", + "Skip": "Skip", "You joined the call": "You joined the call", "%(senderName)s joined the call": "%(senderName)s joined the call", "Call in progress": "Call in progress", @@ -1037,7 +1047,6 @@ "Invite people": "Invite people", "Invite with email or username": "Invite with email or username", "Failed to save space settings.": "Failed to save space settings.", - "General": "General", "Edit settings relating to your space.": "Edit settings relating to your space.", "Saving...": "Saving...", "Save Changes": "Save Changes", @@ -1432,27 +1441,35 @@ "Muted Users": "Muted Users", "Banned users": "Banned users", "Send %(eventType)s events": "Send %(eventType)s events", - "Roles & Permissions": "Roles & Permissions", "Permissions": "Permissions", "Select the roles required to change various parts of the room": "Select the roles required to change various parts of the room", "Enable encryption?": "Enable encryption?", "Once enabled, encryption for a room cannot be disabled. Messages sent in an encrypted room cannot be seen by the server, only by the participants of the room. Enabling encryption may prevent many bots and bridges from working correctly. Learn more about encryption.": "Once enabled, encryption for a room cannot be disabled. Messages sent in an encrypted room cannot be seen by the server, only by the participants of the room. Enabling encryption may prevent many bots and bridges from working correctly. Learn more about encryption.", - "Guests cannot join this room even if explicitly invited.": "Guests cannot join this room even if explicitly invited.", - "Click here to fix": "Click here to fix", + "This upgrade will allow members of selected spaces access to this room without an invite.": "This upgrade will allow members of selected spaces access to this room without an invite.", "To link to this room, please add an address.": "To link to this room, please add an address.", - "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", + "Private (invite only)": "Private (invite only)", + "Only invited people can join.": "Only invited people can join.", + "Public (anyone)": "Public (anyone)", + "Anyone can find and join.": "Anyone can find and join.", + "Upgrade required": "Upgrade required", + "& %(count)s more|other": "& %(count)s more", + "Currently, %(count)s spaces have access|other": "Currently, %(count)s spaces have access", + "Anyone in a space can find and join. Edit which spaces can access here.": "Anyone in a space can find and join. Edit which spaces can access here.", + "Spaces with access": "Spaces with access", + "Anyone in %(spaceName)s can find and join. You can select other spaces too.": "Anyone in %(spaceName)s can find and join. You can select other spaces too.", + "Anyone in a space can find and join. You can select multiple spaces.": "Anyone in a space can find and join. You can select multiple spaces.", + "Space members": "Space members", + "Decide who can view and join %(roomName)s.": "Decide who can view and join %(roomName)s.", "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.", + "People with supported clients will be able to join the room without having a registered account.": "People with supported clients will be able to join the room without having a registered account.", "Who can read history?": "Who can read history?", - "Security & Privacy": "Security & Privacy", "Once enabled, encryption cannot be disabled.": "Once enabled, encryption cannot be disabled.", "Encrypted": "Encrypted", - "Who can access this room?": "Who can access this room?", + "Access": "Access", "Unable to revoke sharing for email address": "Unable to revoke sharing for email address", "Unable to share email address": "Unable to share email address", "Your email address hasn't been verified yet": "Your email address hasn't been verified yet", @@ -2148,7 +2165,6 @@ "People you know on %(brand)s": "People you know on %(brand)s", "Hide": "Hide", "Show": "Show", - "Skip": "Skip", "Send %(count)s invites|other": "Send %(count)s invites", "Send %(count)s invites|one": "Send %(count)s invite", "Invite people to join %(communityName)s": "Invite people to join %(communityName)s", @@ -2177,18 +2193,25 @@ "Community ID": "Community ID", "example": "example", "Please enter a name for the room": "Please enter a name for the room", - "Private rooms can be found and joined by invitation only. Public rooms can be found and joined by anyone.": "Private rooms can be found and joined by invitation only. Public rooms can be found and joined by anyone.", "Private rooms can be found and joined by invitation only. Public rooms can be found and joined by anyone in this community.": "Private rooms can be found and joined by invitation only. Public rooms can be found and joined by anyone in this community.", + "Everyone in will be able to find and join this room.": "Everyone in will be able to find and join this room.", + "You can change this at any time from room settings.": "You can change this at any time from room settings.", + "Anyone will be able to find and join this room, not just members of .": "Anyone will be able to find and join this room, not just members of .", + "Only people invited will be able to find and join this room.": "Only people invited will be able to find and join this room.", "You can’t disable this later. Bridges & most bots won’t work yet.": "You can’t disable this later. Bridges & most bots won’t work yet.", "Your server requires encryption to be enabled in private rooms.": "Your server requires encryption to be enabled in private rooms.", "Enable end-to-end encryption": "Enable end-to-end encryption", "You might enable this if the room will only be used for collaborating with internal teams on your homeserver. This cannot be changed later.": "You might enable this if the room will only be used for collaborating with internal teams on your homeserver. This cannot be changed later.", "You might disable this if the room will be used for collaborating with external teams who have their own homeserver. This cannot be changed later.": "You might disable this if the room will be used for collaborating with external teams who have their own homeserver. This cannot be changed later.", + "Create a room": "Create a room", + "Create a room in %(communityName)s": "Create a room in %(communityName)s", "Create a public room": "Create a public room", "Create a private room": "Create a private room", - "Create a room in %(communityName)s": "Create a room in %(communityName)s", + "Private room (invite only)": "Private room (invite only)", + "Public room": "Public room", + "Visible to space members": "Visible to space members", "Topic (optional)": "Topic (optional)", - "Make this room public": "Make this room public", + "Room visibility": "Room visibility", "Block anyone not part of %(serverName)s from ever joining this room.": "Block anyone not part of %(serverName)s from ever joining this room.", "Create Room": "Create Room", "Sign out": "Sign out", @@ -2342,6 +2365,17 @@ "Manually export keys": "Manually export keys", "You'll lose access to your encrypted messages": "You'll lose access to your encrypted messages", "Are you sure you want to sign out?": "Are you sure you want to sign out?", + "%(count)s members|other": "%(count)s members", + "%(count)s members|one": "%(count)s member", + "%(count)s rooms|other": "%(count)s rooms", + "%(count)s rooms|one": "%(count)s room", + "You're removing all spaces. Access will default to invite only": "You're removing all spaces. Access will default to invite only", + "Select spaces": "Select spaces", + "Decide which spaces can access this room. If a space is selected its members will be able to find and join .": "Decide which spaces can access this room. If a space is selected its members will be able to find and join .", + "Search spaces": "Search spaces", + "Spaces you know that contain this room": "Spaces you know that contain this room", + "Other spaces or rooms you might not know": "Other spaces or rooms you might not know", + "These are likely ones other room admins are a part of.": "These are likely ones other room admins are a part of.", "Confirm by comparing the following with the User Settings in your other session:": "Confirm by comparing the following with the User Settings in your other session:", "Confirm this user's session by comparing the following with their User Settings:": "Confirm this user's session by comparing the following with their User Settings:", "Session name": "Session name", @@ -2385,12 +2419,13 @@ "Update any local room aliases to point to the new room": "Update any local room aliases to point to the new room", "Stop users from speaking in the old version of the room, and post a message advising users to move to the new room": "Stop users from speaking in the old version of the room, and post a message advising users to move to the new room", "Put a link back to the old room at the start of the new room so people can see old messages": "Put a link back to the old room at the start of the new room so people can see old messages", - "Automatically invite users": "Automatically invite users", + "Automatically invite members from this room to the new one": "Automatically invite members from this room to the new one", "Upgrade private room": "Upgrade private room", "Upgrade public room": "Upgrade public room", "This usually only affects how the room is processed on the server. If you're having problems with your %(brand)s, please report a bug.": "This usually only affects how the room is processed on the server. If you're having problems with your %(brand)s, please report a bug.", "This usually only affects how the room is processed on the server. If you're having problems with your %(brand)s, please report a bug.": "This usually only affects how the room is processed on the server. If you're having problems with your %(brand)s, please report a bug.", "Upgrading a room is an advanced action and is usually recommended when a room is unstable due to bugs, missing features or security vulnerabilities.": "Upgrading a room is an advanced action and is usually recommended when a room is unstable due to bugs, missing features or security vulnerabilities.", + "Please note upgrading will make a new version of the room. All current messages will stay in this archived room.": "Please note upgrading will make a new version of the room. All current messages will stay in this archived room.", "You'll upgrade this room from to .": "You'll upgrade this room from to .", "Resend": "Resend", "You're all caught up.": "You're all caught up.", @@ -2413,7 +2448,6 @@ "We call the places where you can host your account ‘homeservers’.": "We call the places where you can host your account ‘homeservers’.", "Other homeserver": "Other homeserver", "Use your preferred Matrix homeserver if you have one, or host your own.": "Use your preferred Matrix homeserver if you have one, or host your own.", - "Learn more": "Learn more", "About homeservers": "About homeservers", "Reset event store?": "Reset event store?", "You most likely do not want to reset your event index store": "You most likely do not want to reset your event index store", @@ -2647,6 +2681,7 @@ "You are an administrator of this community": "You are an administrator of this community", "You are a member of this community": "You are a member of this community", "Who can join this community?": "Who can join this community?", + "Only people who have been invited": "Only people who have been invited", "Everyone": "Everyone", "Your community hasn't got a Long Description, a HTML page to show to community members.
    Click here to open settings and give it one!": "Your community hasn't got a Long Description, a HTML page to show to community members.
    Click here to open settings and give it one!", "Long Description (HTML)": "Long Description (HTML)", @@ -2744,10 +2779,6 @@ "You have %(count)s unread notifications in a prior version of this room.|other": "You have %(count)s unread notifications in a prior version of this room.", "You have %(count)s unread notifications in a prior version of this room.|one": "You have %(count)s unread notification in a prior version of this room.", "You don't have permission": "You don't have permission", - "%(count)s members|other": "%(count)s members", - "%(count)s members|one": "%(count)s member", - "%(count)s rooms|other": "%(count)s rooms", - "%(count)s rooms|one": "%(count)s room", "This room is suggested as a good one to join": "This room is suggested as a good one to join", "Suggested": "Suggested", "Your server does not support showing space hierarchies.": "Your server does not support showing space hierarchies.", From 316b21408dc42c81b1933df05ac5d4cbe8839322 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 15 Jul 2021 10:59:52 +0100 Subject: [PATCH 0335/1171] Fix tests --- test/editor/caret-test.js | 1 + test/editor/model-test.js | 1 + test/editor/operations-test.js | 1 + test/editor/position-test.js | 1 + test/editor/range-test.js | 1 + test/editor/serialize-test.js | 1 + test/stores/SpaceStore-setup.ts | 23 +++++++++++++++++++++++ test/stores/SpaceStore-test.ts | 20 ++------------------ 8 files changed, 31 insertions(+), 18 deletions(-) create mode 100644 test/stores/SpaceStore-setup.ts diff --git a/test/editor/caret-test.js b/test/editor/caret-test.js index e1a66a4431..33b40e1c64 100644 --- a/test/editor/caret-test.js +++ b/test/editor/caret-test.js @@ -14,6 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ +import "../skinned-sdk"; // Must be first for skinning to work import { getLineAndNodePosition } from "../../src/editor/caret"; import EditorModel from "../../src/editor/model"; import { createPartCreator } from "./mock"; diff --git a/test/editor/model-test.js b/test/editor/model-test.js index 35bd4143a7..15c5af5806 100644 --- a/test/editor/model-test.js +++ b/test/editor/model-test.js @@ -14,6 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ +import "../skinned-sdk"; // Must be first for skinning to work import EditorModel from "../../src/editor/model"; import { createPartCreator, createRenderer } from "./mock"; diff --git a/test/editor/operations-test.js b/test/editor/operations-test.js index 32ccaa5440..17a4c8ba11 100644 --- a/test/editor/operations-test.js +++ b/test/editor/operations-test.js @@ -14,6 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ +import "../skinned-sdk"; // Must be first for skinning to work import EditorModel from "../../src/editor/model"; import { createPartCreator, createRenderer } from "./mock"; import { toggleInlineFormat } from "../../src/editor/operations"; diff --git a/test/editor/position-test.js b/test/editor/position-test.js index 813a8e9f7f..ea8658b216 100644 --- a/test/editor/position-test.js +++ b/test/editor/position-test.js @@ -14,6 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ +import "../skinned-sdk"; // Must be first for skinning to work import EditorModel from "../../src/editor/model"; import { createPartCreator } from "./mock"; diff --git a/test/editor/range-test.js b/test/editor/range-test.js index d411a0d911..87c5b06e44 100644 --- a/test/editor/range-test.js +++ b/test/editor/range-test.js @@ -14,6 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ +import "../skinned-sdk"; // Must be first for skinning to work import EditorModel from "../../src/editor/model"; import { createPartCreator, createRenderer } from "./mock"; diff --git a/test/editor/serialize-test.js b/test/editor/serialize-test.js index 691130bd34..085a8afdba 100644 --- a/test/editor/serialize-test.js +++ b/test/editor/serialize-test.js @@ -14,6 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ +import "../skinned-sdk"; // Must be first for skinning to work import EditorModel from "../../src/editor/model"; import { htmlSerializeIfNeeded } from "../../src/editor/serialize"; import { createPartCreator } from "./mock"; diff --git a/test/stores/SpaceStore-setup.ts b/test/stores/SpaceStore-setup.ts new file mode 100644 index 0000000000..67d492255f --- /dev/null +++ b/test/stores/SpaceStore-setup.ts @@ -0,0 +1,23 @@ +/* +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. +*/ + +// This needs to be executed before the SpaceStore gets imported but due to ES6 import hoisting we have to do this here. +// SpaceStore reads the SettingsStore which needs the localStorage values set at init time. + +localStorage.setItem("mx_labs_feature_feature_spaces", "true"); +localStorage.setItem("mx_labs_feature_feature_spaces.all_rooms", "true"); +localStorage.setItem("mx_labs_feature_feature_spaces.space_member_dms", "true"); +localStorage.setItem("mx_labs_feature_feature_spaces.space_dm_badges", "false"); diff --git a/test/stores/SpaceStore-test.ts b/test/stores/SpaceStore-test.ts index 4cbd9f43c8..eb28a72d67 100644 --- a/test/stores/SpaceStore-test.ts +++ b/test/stores/SpaceStore-test.ts @@ -16,7 +16,9 @@ limitations under the License. import { EventEmitter } from "events"; import { EventType } from "matrix-js-sdk/src/@types/event"; +import { MatrixEvent } from "matrix-js-sdk/src/models/event"; +import "./SpaceStore-setup"; // enable space lab import "../skinned-sdk"; // Must be first for skinning to work import SpaceStore, { UPDATE_INVITED_SPACES, @@ -26,13 +28,10 @@ import SpaceStore, { import { resetAsyncStoreWithClient, setupAsyncStoreWithClient } from "../utils/test-utils"; import { mkEvent, mkStubRoom, stubClient } from "../test-utils"; import { EnhancedMap } from "../../src/utils/maps"; -import SettingsStore from "../../src/settings/SettingsStore"; import DMRoomMap from "../../src/utils/DMRoomMap"; import { MatrixClientPeg } from "../../src/MatrixClientPeg"; import defaultDispatcher from "../../src/dispatcher/dispatcher"; -type MatrixEvent = any; // importing from js-sdk upsets things - jest.useFakeTimers(); const mockStateEventImplementation = (events: MatrixEvent[]) => { @@ -79,9 +78,6 @@ const mkSpace = (spaceId: string, children: string[] = []) => { return space; }; -const getValue = jest.fn(); -SettingsStore.getValue = getValue; - const getUserIdForRoomId = jest.fn(); // @ts-ignore DMRoomMap.sharedInstance = { getUserIdForRoomId }; @@ -122,18 +118,6 @@ describe("SpaceStore", () => { beforeEach(() => { jest.runAllTimers(); client.getVisibleRooms.mockReturnValue(rooms = []); - getValue.mockImplementation(settingName => { - switch (settingName) { - case "feature_spaces": - return true; - case "feature_spaces.all_rooms": - return true; - case "feature_spaces.space_member_dms": - return true; - case "feature_spaces.space_dm_badges": - return false; - } - }); }); afterEach(async () => { await resetAsyncStoreWithClient(store); From 59feff376306c64e94c8c8606e8252aa0863904c Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 15 Jul 2021 11:49:15 +0100 Subject: [PATCH 0336/1171] Silence RoomListStore possible memory leak warning --- src/stores/room-list/RoomListStore.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/stores/room-list/RoomListStore.ts b/src/stores/room-list/RoomListStore.ts index e26c80bb2d..5d26056a7d 100644 --- a/src/stores/room-list/RoomListStore.ts +++ b/src/stores/room-list/RoomListStore.ts @@ -73,6 +73,7 @@ export class RoomListStoreClass extends AsyncStoreWithClient { constructor() { super(defaultDispatcher); + this.setMaxListeners(20); // CustomRoomTagStore + RoomList + LeftPanel + 8xRoomSubList + spares } private setupWatchers() { From b8ac40ae55a169dc9c27cabeaeb1fe5e21f99f2f Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 15 Jul 2021 11:49:44 +0100 Subject: [PATCH 0337/1171] Fix React missing key error --- src/components/views/rooms/EventTile.tsx | 31 ++++++++++++------------ 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index b5a4bc41db..14eab5da2e 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -1152,11 +1152,11 @@ export default class EventTile extends React.Component { "aria-live": ariaLive, "aria-atomic": true, "data-scroll-tokens": scrollToken, - }, [ - ircTimestamp, - avatar, - sender, - ircPadlock, + }, <> + { ircTimestamp } + { avatar } + { sender } + { ircPadlock }
    { groupTimestamp } { groupPadlock } @@ -1169,8 +1169,8 @@ export default class EventTile extends React.Component { replacingEventId={this.props.replacingEventId} showUrlPreview={false} /> -
    , - ]); +
    + ); } default: { const thread = ReplyThread.makeThread( @@ -1193,10 +1193,10 @@ export default class EventTile extends React.Component { "data-scroll-tokens": scrollToken, "onMouseEnter": () => this.setState({ hover: true }), "onMouseLeave": () => this.setState({ hover: false }), - }, [ - ircTimestamp, - sender, - ircPadlock, + }, <> + { ircTimestamp } + { sender } + { ircPadlock }
    { groupTimestamp } { groupPadlock } @@ -1214,11 +1214,10 @@ export default class EventTile extends React.Component { { keyRequestInfo } { reactionsRow } { actionBar } -
    , - msgOption, - avatar, - - ]) +
    + { msgOption } + { avatar } + ) ); } } From 1eaf6dd4ed260f64b993c6be82a261e466e79da6 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 15 Jul 2021 11:49:55 +0100 Subject: [PATCH 0338/1171] Improve TS in SenderProfile --- .../views/messages/SenderProfile.tsx | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/components/views/messages/SenderProfile.tsx b/src/components/views/messages/SenderProfile.tsx index bdae9cec4a..5198effb32 100644 --- a/src/components/views/messages/SenderProfile.tsx +++ b/src/components/views/messages/SenderProfile.tsx @@ -15,12 +15,14 @@ */ import React from 'react'; +import { MatrixEvent } from "matrix-js-sdk/src/models/event"; + import Flair from '../elements/Flair'; import FlairStore from '../../../stores/FlairStore'; import { getUserNameColorClass } from '../../../utils/FormattingUtils'; import MatrixClientContext from "../../../contexts/MatrixClientContext"; import { replaceableComponent } from "../../../utils/replaceableComponent"; -import { MatrixEvent } from "matrix-js-sdk/src/models/event"; +import { MsgType } from "matrix-js-sdk/lib/@types/event"; interface IProps { mxEvent: MatrixEvent; @@ -50,7 +52,7 @@ export default class SenderProfile extends React.Component { componentDidMount() { this.unmounted = false; - this._updateRelatedGroups(); + this.updateRelatedGroups(); if (this.state.userGroups.length === 0) { this.getPublicisedGroups(); @@ -64,7 +66,7 @@ export default class SenderProfile extends React.Component { this.context.removeListener('RoomState.events', this.onRoomStateEvents); } - async getPublicisedGroups() { + private async getPublicisedGroups() { if (!this.unmounted) { const userGroups = await FlairStore.getPublicisedGroupsCached( this.context, this.props.mxEvent.getSender(), @@ -73,15 +75,15 @@ export default class SenderProfile extends React.Component { } } - onRoomStateEvents = event => { + private onRoomStateEvents = (event: MatrixEvent) => { if (event.getType() === 'm.room.related_groups' && event.getRoomId() === this.props.mxEvent.getRoomId() ) { - this._updateRelatedGroups(); + this.updateRelatedGroups(); } }; - _updateRelatedGroups() { + private updateRelatedGroups() { if (this.unmounted) return; const room = this.context.getRoom(this.props.mxEvent.getRoomId()); if (!room) return; @@ -92,7 +94,7 @@ export default class SenderProfile extends React.Component { }); } - _getDisplayedGroups(userGroups, relatedGroups) { + private getDisplayedGroups(userGroups?: string[], relatedGroups?: string[]) { let displayedGroups = userGroups || []; if (relatedGroups && relatedGroups.length > 0) { displayedGroups = relatedGroups.filter((groupId) => { @@ -113,7 +115,7 @@ export default class SenderProfile extends React.Component { const displayName = mxEvent.sender?.rawDisplayName || mxEvent.getSender() || ""; const mxid = mxEvent.sender?.userId || mxEvent.getSender() || ""; - if (msgtype === 'm.emote') { + if (msgtype === MsgType.Emote) { return null; // emote message must include the name so don't duplicate it } @@ -128,7 +130,7 @@ export default class SenderProfile extends React.Component { let flair; if (this.props.enableFlair) { - const displayedGroups = this._getDisplayedGroups( + const displayedGroups = this.getDisplayedGroups( this.state.userGroups, this.state.relatedGroups, ); From e9d56d4f135c7e61df1434722aed997fdd16c70d Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 15 Jul 2021 12:10:01 +0100 Subject: [PATCH 0339/1171] Fix possible uncaught exception for getUrlPreview which would cause 0 url previews if one url was faulty --- src/components/views/rooms/LinkPreviewGroup.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/components/views/rooms/LinkPreviewGroup.tsx b/src/components/views/rooms/LinkPreviewGroup.tsx index 2541b2e375..c9842bdd33 100644 --- a/src/components/views/rooms/LinkPreviewGroup.tsx +++ b/src/components/views/rooms/LinkPreviewGroup.tsx @@ -40,10 +40,12 @@ const LinkPreviewGroup: React.FC = ({ links, mxEvent, onCancelClick, onH const ts = mxEvent.getTs(); const previews = useAsyncMemo<[string, IPreviewUrlResponse][]>(async () => { - return Promise.all<[string, IPreviewUrlResponse] | void>(links.map(link => { - return cli.getUrlPreview(link, ts).then(preview => [link, preview], error => { + return Promise.all<[string, IPreviewUrlResponse] | void>(links.map(async link => { + try { + return [link, await cli.getUrlPreview(link, ts)]; + } catch (error) { console.error("Failed to get URL preview: " + error); - }); + } })).then(a => a.filter(Boolean)) as Promise<[string, IPreviewUrlResponse][]>; }, [links, ts], []); From 7c3c04d340e2cd255e4f5d271a5c80dc870ba82b Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 15 Jul 2021 12:10:54 +0100 Subject: [PATCH 0340/1171] Fix instances of setState calls after unmount --- src/components/structures/RoomView.tsx | 33 +++++++++---------- src/components/structures/TimelinePanel.tsx | 4 +++ .../views/messages/SenderProfile.tsx | 19 ++++------- .../tabs/user/AppearanceUserSettingsTab.tsx | 6 ++++ 4 files changed, 31 insertions(+), 31 deletions(-) diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index 2c118149a0..2d264b00e9 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -916,6 +916,7 @@ export default class RoomView extends React.Component { // called when state.room is first initialised (either at initial load, // after a successful peek, or after we join the room). private onRoomLoaded = (room: Room) => { + if (this.unmounted) return; // Attach a widget store listener only when we get a room WidgetLayoutStore.instance.on(WidgetLayoutStore.emissionForRoom(room), this.onWidgetLayoutChange); this.onWidgetLayoutChange(); // provoke an update @@ -930,9 +931,9 @@ export default class RoomView extends React.Component { }; private async calculateRecommendedVersion(room: Room) { - this.setState({ - upgradeRecommendation: await room.getRecommendedVersion(), - }); + const upgradeRecommendation = await room.getRecommendedVersion(); + if (this.unmounted) return; + this.setState({ upgradeRecommendation }); } private async loadMembersIfJoined(room: Room) { @@ -1022,23 +1023,19 @@ export default class RoomView extends React.Component { }; private async updateE2EStatus(room: Room) { - if (!this.context.isRoomEncrypted(room.roomId)) { - return; - } - if (!this.context.isCryptoEnabled()) { - // If crypto is not currently enabled, we aren't tracking devices at all, - // so we don't know what the answer is. Let's error on the safe side and show - // a warning for this case. - this.setState({ - e2eStatus: E2EStatus.Warning, - }); - return; + if (!this.context.isRoomEncrypted(room.roomId)) return; + + // If crypto is not currently enabled, we aren't tracking devices at all, + // so we don't know what the answer is. Let's error on the safe side and show + // a warning for this case. + let e2eStatus = E2EStatus.Warning; + if (this.context.isCryptoEnabled()) { + /* At this point, the user has encryption on and cross-signing on */ + e2eStatus = await shieldStatusForRoom(this.context, room); } - /* At this point, the user has encryption on and cross-signing on */ - this.setState({ - e2eStatus: await shieldStatusForRoom(this.context, room), - }); + if (this.unmounted) return; + this.setState({ e2eStatus }); } private onAccountData = (event: MatrixEvent) => { diff --git a/src/components/structures/TimelinePanel.tsx b/src/components/structures/TimelinePanel.tsx index 85a048e9b8..c21aac790b 100644 --- a/src/components/structures/TimelinePanel.tsx +++ b/src/components/structures/TimelinePanel.tsx @@ -1051,6 +1051,8 @@ class TimelinePanel extends React.Component { { windowLimit: this.props.timelineCap }); const onLoaded = () => { + if (this.unmounted) return; + // clear the timeline min-height when // (re)loading the timeline if (this.messagePanel.current) { @@ -1092,6 +1094,8 @@ class TimelinePanel extends React.Component { }; const onError = (error) => { + if (this.unmounted) return; + this.setState({ timelineLoading: false }); console.error( `Error loading timeline panel at ${eventId}: ${error}`, diff --git a/src/components/views/messages/SenderProfile.tsx b/src/components/views/messages/SenderProfile.tsx index 5198effb32..d62c10427d 100644 --- a/src/components/views/messages/SenderProfile.tsx +++ b/src/components/views/messages/SenderProfile.tsx @@ -38,7 +38,7 @@ interface IState { @replaceableComponent("views.messages.SenderProfile") export default class SenderProfile extends React.Component { static contextType = MatrixClientContext; - private unmounted: boolean; + private unmounted = false; constructor(props: IProps) { super(props); @@ -51,7 +51,6 @@ export default class SenderProfile extends React.Component { } componentDidMount() { - this.unmounted = false; this.updateRelatedGroups(); if (this.state.userGroups.length === 0) { @@ -67,30 +66,24 @@ export default class SenderProfile extends React.Component { } private async getPublicisedGroups() { - if (!this.unmounted) { - const userGroups = await FlairStore.getPublicisedGroupsCached( - this.context, this.props.mxEvent.getSender(), - ); - this.setState({ userGroups }); - } + const userGroups = await FlairStore.getPublicisedGroupsCached(this.context, this.props.mxEvent.getSender()); + if (this.unmounted) return; + this.setState({ userGroups }); } private onRoomStateEvents = (event: MatrixEvent) => { - if (event.getType() === 'm.room.related_groups' && - event.getRoomId() === this.props.mxEvent.getRoomId() - ) { + if (event.getType() === 'm.room.related_groups' && event.getRoomId() === this.props.mxEvent.getRoomId()) { this.updateRelatedGroups(); } }; private updateRelatedGroups() { - if (this.unmounted) return; const room = this.context.getRoom(this.props.mxEvent.getRoomId()); if (!room) return; const relatedGroupsEvent = room.currentState.getStateEvents('m.room.related_groups', ''); this.setState({ - relatedGroups: relatedGroupsEvent ? relatedGroupsEvent.getContent().groups || [] : [], + relatedGroups: relatedGroupsEvent?.getContent().groups || [], }); } diff --git a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx index 17aa9e5561..a94821e94a 100644 --- a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx @@ -76,6 +76,7 @@ export default class AppearanceUserSettingsTab extends React.Component Date: Thu, 15 Jul 2021 12:18:17 +0100 Subject: [PATCH 0341/1171] improve types --- src/TextForEvent.tsx | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/TextForEvent.tsx b/src/TextForEvent.tsx index ef24fb8e48..95341705bf 100644 --- a/src/TextForEvent.tsx +++ b/src/TextForEvent.tsx @@ -32,7 +32,7 @@ import { MatrixEvent } from "matrix-js-sdk/src/models/event"; // any text to display at all. For this reason they return deferred values // to avoid the expense of looking up translations when they're not needed. -function textForMemberEvent(ev): () => string | null { +function textForMemberEvent(ev: MatrixEvent): () => string | null { // XXX: SYJS-16 "sender is sometimes null for join messages" const senderName = ev.sender ? ev.sender.name : ev.getSender(); const targetName = ev.target ? ev.target.name : ev.getStateKey(); @@ -127,7 +127,7 @@ function textForMemberEvent(ev): () => string | null { } } -function textForTopicEvent(ev): () => string | null { +function textForTopicEvent(ev: MatrixEvent): () => string | null { const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender(); return () => _t('%(senderDisplayName)s changed the topic to "%(topic)s".', { senderDisplayName, @@ -135,7 +135,7 @@ function textForTopicEvent(ev): () => string | null { }); } -function textForRoomNameEvent(ev): () => string | null { +function textForRoomNameEvent(ev: MatrixEvent): () => string | null { const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender(); if (!ev.getContent().name || ev.getContent().name.trim().length === 0) { @@ -154,12 +154,12 @@ function textForRoomNameEvent(ev): () => string | null { }); } -function textForTombstoneEvent(ev): () => string | null { +function textForTombstoneEvent(ev: MatrixEvent): () => string | null { const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender(); return () => _t('%(senderDisplayName)s upgraded this room.', { senderDisplayName }); } -function textForJoinRulesEvent(ev): () => string | null { +function textForJoinRulesEvent(ev: MatrixEvent): () => string | null { const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender(); switch (ev.getContent().join_rule) { case "public": @@ -179,7 +179,7 @@ function textForJoinRulesEvent(ev): () => string | null { } } -function textForGuestAccessEvent(ev): () => string | null { +function textForGuestAccessEvent(ev: MatrixEvent): () => string | null { const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender(); switch (ev.getContent().guest_access) { case "can_join": @@ -195,7 +195,7 @@ function textForGuestAccessEvent(ev): () => string | null { } } -function textForRelatedGroupsEvent(ev): () => string | null { +function textForRelatedGroupsEvent(ev: MatrixEvent): () => string | null { const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender(); const groups = ev.getContent().groups || []; const prevGroups = ev.getPrevContent().groups || []; @@ -225,7 +225,7 @@ function textForRelatedGroupsEvent(ev): () => string | null { } } -function textForServerACLEvent(ev): () => string | null { +function textForServerACLEvent(ev: MatrixEvent): () => string | null { const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender(); const prevContent = ev.getPrevContent(); const current = ev.getContent(); @@ -255,7 +255,7 @@ function textForServerACLEvent(ev): () => string | null { return getText; } -function textForMessageEvent(ev): () => string | null { +function textForMessageEvent(ev: MatrixEvent): () => string | null { return () => { const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender(); let message = senderDisplayName + ': ' + ev.getContent().body; @@ -268,7 +268,7 @@ function textForMessageEvent(ev): () => string | null { }; } -function textForCanonicalAliasEvent(ev): () => string | null { +function textForCanonicalAliasEvent(ev: MatrixEvent): () => string | null { const senderName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender(); const oldAlias = ev.getPrevContent().alias; const oldAltAliases = ev.getPrevContent().alt_aliases || []; @@ -682,7 +682,7 @@ for (const evType of ALL_RULE_TYPES) { stateHandlers[evType] = textForMjolnirEvent; } -export function hasText(ev): boolean { +export function hasText(ev: MatrixEvent): boolean { const handler = (ev.isState() ? stateHandlers : handlers)[ev.getType()]; return Boolean(handler?.(ev)); } From 20e0356eb1b7e6623325c2be5b2ba94bd5e168bb Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 15 Jul 2021 12:25:26 +0100 Subject: [PATCH 0342/1171] why do my IDE be dumb --- src/components/views/messages/SenderProfile.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/messages/SenderProfile.tsx b/src/components/views/messages/SenderProfile.tsx index d62c10427d..d4b74db6d0 100644 --- a/src/components/views/messages/SenderProfile.tsx +++ b/src/components/views/messages/SenderProfile.tsx @@ -16,13 +16,13 @@ import React from 'react'; import { MatrixEvent } from "matrix-js-sdk/src/models/event"; +import { MsgType } from "matrix-js-sdk/src/@types/event"; import Flair from '../elements/Flair'; import FlairStore from '../../../stores/FlairStore'; import { getUserNameColorClass } from '../../../utils/FormattingUtils'; import MatrixClientContext from "../../../contexts/MatrixClientContext"; import { replaceableComponent } from "../../../utils/replaceableComponent"; -import { MsgType } from "matrix-js-sdk/lib/@types/event"; interface IProps { mxEvent: MatrixEvent; From c8bd37513026590d08c156104323bb1c80a88552 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Thu, 15 Jul 2021 15:11:45 +0200 Subject: [PATCH 0343/1171] Migrate DisableEventIndexDialog to TypeScript --- ...xDialog.js => DisableEventIndexDialog.tsx} | 28 ++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) rename src/async-components/views/dialogs/eventindex/{DisableEventIndexDialog.js => DisableEventIndexDialog.tsx} (86%) diff --git a/src/async-components/views/dialogs/eventindex/DisableEventIndexDialog.js b/src/async-components/views/dialogs/eventindex/DisableEventIndexDialog.tsx similarity index 86% rename from src/async-components/views/dialogs/eventindex/DisableEventIndexDialog.js rename to src/async-components/views/dialogs/eventindex/DisableEventIndexDialog.tsx index a19494c753..2be5ddaa43 100644 --- a/src/async-components/views/dialogs/eventindex/DisableEventIndexDialog.js +++ b/src/async-components/views/dialogs/eventindex/DisableEventIndexDialog.tsx @@ -16,7 +16,6 @@ limitations under the License. import React from 'react'; import * as sdk from '../../../../index'; -import PropTypes from 'prop-types'; import dis from "../../../../dispatcher/dispatcher"; import { _t } from '../../../../languageHandler'; @@ -25,34 +24,37 @@ import EventIndexPeg from "../../../../indexing/EventIndexPeg"; import { Action } from "../../../../dispatcher/actions"; import { SettingLevel } from "../../../../settings/SettingLevel"; +interface IProps { + onFinished: (success: boolean) => void; +} + +interface IState { + disabling: boolean; +} + /* * Allows the user to disable the Event Index. */ -export default class DisableEventIndexDialog extends React.Component { - static propTypes = { - onFinished: PropTypes.func.isRequired, - } - - constructor(props) { +export default class DisableEventIndexDialog extends React.Component { + constructor(props: IProps) { super(props); - this.state = { disabling: false, }; } - _onDisable = async () => { + private onDisable = async (): Promise => { this.setState({ disabling: true, }); await SettingsStore.setValue('enableEventIndexing', null, SettingLevel.DEVICE, false); await EventIndexPeg.deleteEventIndex(); - this.props.onFinished(); + this.props.onFinished(true); dis.fire(Action.ViewUserSettings); - } + }; - render() { + public render(): React.ReactNode { const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); const Spinner = sdk.getComponent('elements.Spinner'); const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); @@ -63,7 +65,7 @@ export default class DisableEventIndexDialog extends React.Component { {this.state.disabling ? :
    } Date: Thu, 15 Jul 2021 15:15:48 +0200 Subject: [PATCH 0344/1171] Add speaker icon MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/img/element-icons/speaker.svg | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 res/img/element-icons/speaker.svg diff --git a/res/img/element-icons/speaker.svg b/res/img/element-icons/speaker.svg new file mode 100644 index 0000000000..fd811d2cda --- /dev/null +++ b/res/img/element-icons/speaker.svg @@ -0,0 +1,5 @@ + + + + + From 68640a4dbd8f140013fd857334f42413acd4ede2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Thu, 15 Jul 2021 15:16:05 +0200 Subject: [PATCH 0345/1171] Fix icon postion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/css/views/messages/_MFileBody.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/res/css/views/messages/_MFileBody.scss b/res/css/views/messages/_MFileBody.scss index c215d69ec2..b91c461ce5 100644 --- a/res/css/views/messages/_MFileBody.scss +++ b/res/css/views/messages/_MFileBody.scss @@ -83,12 +83,12 @@ limitations under the License. mask-size: cover; mask-image: url('$(res)/img/element-icons/room/composer/attach.svg'); background-color: $message-body-panel-icon-fg-color; - width: 13px; + width: 15px; height: 15px; position: absolute; top: 8px; - left: 9px; + left: 8px; } } From 88da0f4dcf500c44c5bae2673b538f0f47ac76b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Thu, 15 Jul 2021 15:17:41 +0200 Subject: [PATCH 0346/1171] Give audio and video replies an icon MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/css/views/rooms/_ReplyTile.scss | 8 ++++++++ src/components/views/rooms/ReplyTile.tsx | 6 ++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/res/css/views/rooms/_ReplyTile.scss b/res/css/views/rooms/_ReplyTile.scss index 8fe3a3e94c..ccb0069190 100644 --- a/res/css/views/rooms/_ReplyTile.scss +++ b/res/css/views/rooms/_ReplyTile.scss @@ -21,6 +21,14 @@ limitations under the License. position: relative; line-height: $font-16px; + &.mx_ReplyTile_audio .mx_MFileBody_info_icon::before { + mask-image: url("$(res)/img/element-icons/speaker.svg"); + } + + &.mx_ReplyTile_video .mx_MFileBody_info_icon::before { + mask-image: url("$(res)/img/element-icons/call/video-call.svg"); + } + .mx_MFileBody { .mx_MFileBody_info { margin: 5px 0; diff --git a/src/components/views/rooms/ReplyTile.tsx b/src/components/views/rooms/ReplyTile.tsx index f44a75a264..18b30d33d5 100644 --- a/src/components/views/rooms/ReplyTile.tsx +++ b/src/components/views/rooms/ReplyTile.tsx @@ -80,7 +80,7 @@ export default class ReplyTile extends React.PureComponent { render() { const mxEvent = this.props.mxEvent; - const msgtype = mxEvent.getContent().msgtype; + const msgType = mxEvent.getContent().msgtype; const evType = mxEvent.getType() as EventType; const { tileHandler, isInfoMessage } = getEventDisplayInfo(this.props.mxEvent); @@ -98,6 +98,8 @@ export default class ReplyTile extends React.PureComponent { const classes = classNames("mx_ReplyTile", { mx_ReplyTile_info: isInfoMessage && !this.props.mxEvent.isRedacted(), + mx_ReplyTile_audio: msgType === MsgType.Audio, + mx_ReplyTile_video: msgType === MsgType.Video, }); let permalink = "#"; @@ -108,7 +110,7 @@ export default class ReplyTile extends React.PureComponent { let sender; const needsSenderProfile = ( !isInfoMessage && - msgtype !== MsgType.Image && + msgType !== MsgType.Image && tileHandler !== EventType.RoomCreate && evType !== EventType.Sticker ); From 7dbff52aa47580eaec5bdb861df42224f836259f Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Thu, 15 Jul 2021 15:19:48 +0200 Subject: [PATCH 0347/1171] Migrate AuthBody to TypeScript --- src/components/views/auth/{AuthBody.js => AuthBody.tsx} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/components/views/auth/{AuthBody.js => AuthBody.tsx} (100%) diff --git a/src/components/views/auth/AuthBody.js b/src/components/views/auth/AuthBody.tsx similarity index 100% rename from src/components/views/auth/AuthBody.js rename to src/components/views/auth/AuthBody.tsx From 59316e4820961667813ad5dff9aee69c56010bdd Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Thu, 15 Jul 2021 15:20:43 +0200 Subject: [PATCH 0348/1171] Migrate AuthFooter to TypeScript --- src/components/views/auth/AuthBody.tsx | 2 +- src/components/views/auth/{AuthFooter.js => AuthFooter.tsx} | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename src/components/views/auth/{AuthFooter.js => AuthFooter.tsx} (96%) diff --git a/src/components/views/auth/AuthBody.tsx b/src/components/views/auth/AuthBody.tsx index abe7fd2fd3..3543a573d7 100644 --- a/src/components/views/auth/AuthBody.tsx +++ b/src/components/views/auth/AuthBody.tsx @@ -19,7 +19,7 @@ import { replaceableComponent } from "../../../utils/replaceableComponent"; @replaceableComponent("views.auth.AuthBody") export default class AuthBody extends React.PureComponent { - render() { + public render(): React.ReactNode { return
    { this.props.children }
    ; diff --git a/src/components/views/auth/AuthFooter.js b/src/components/views/auth/AuthFooter.tsx similarity index 96% rename from src/components/views/auth/AuthFooter.js rename to src/components/views/auth/AuthFooter.tsx index e81d2cd969..00bced8c39 100644 --- a/src/components/views/auth/AuthFooter.js +++ b/src/components/views/auth/AuthFooter.tsx @@ -22,7 +22,7 @@ import { replaceableComponent } from "../../../utils/replaceableComponent"; @replaceableComponent("views.auth.AuthFooter") export default class AuthFooter extends React.Component { - render() { + public render(): React.ReactNode { return (
    { _t("powered by Matrix") } From 5783a382070ea568fd1107f06be1d0a077f9b2cd Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Thu, 15 Jul 2021 15:21:33 +0200 Subject: [PATCH 0349/1171] Migrate AuthHeader to TypeScript --- .../views/auth/{AuthHeader.js => AuthHeader.tsx} | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) rename src/components/views/auth/{AuthHeader.js => AuthHeader.tsx} (85%) diff --git a/src/components/views/auth/AuthHeader.js b/src/components/views/auth/AuthHeader.tsx similarity index 85% rename from src/components/views/auth/AuthHeader.js rename to src/components/views/auth/AuthHeader.tsx index d9bd81adcb..6f071c8f61 100644 --- a/src/components/views/auth/AuthHeader.js +++ b/src/components/views/auth/AuthHeader.tsx @@ -16,17 +16,16 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; import * as sdk from '../../../index'; import { replaceableComponent } from "../../../utils/replaceableComponent"; -@replaceableComponent("views.auth.AuthHeader") -export default class AuthHeader extends React.Component { - static propTypes = { - disableLanguageSelector: PropTypes.bool, - }; +interface IProps { + disableLanguageSelector?: boolean; +} - render() { +@replaceableComponent("views.auth.AuthHeader") +export default class AuthHeader extends React.Component { + public render(): React.ReactNode { const AuthHeaderLogo = sdk.getComponent('auth.AuthHeaderLogo'); const LanguageSelector = sdk.getComponent('views.auth.LanguageSelector'); From 13c5adbb6ca050a3130996646fe10e09885665f9 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Thu, 15 Jul 2021 15:22:02 +0200 Subject: [PATCH 0350/1171] Migrate AuthHeaderLogo to TypeScript --- .../views/auth/{AuthHeaderLogo.js => AuthHeaderLogo.tsx} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename src/components/views/auth/{AuthHeaderLogo.js => AuthHeaderLogo.tsx} (95%) diff --git a/src/components/views/auth/AuthHeaderLogo.js b/src/components/views/auth/AuthHeaderLogo.tsx similarity index 95% rename from src/components/views/auth/AuthHeaderLogo.js rename to src/components/views/auth/AuthHeaderLogo.tsx index 0adf18dc1c..b6724793a5 100644 --- a/src/components/views/auth/AuthHeaderLogo.js +++ b/src/components/views/auth/AuthHeaderLogo.tsx @@ -19,7 +19,7 @@ import { replaceableComponent } from "../../../utils/replaceableComponent"; @replaceableComponent("views.auth.AuthHeaderLogo") export default class AuthHeaderLogo extends React.PureComponent { - render() { + public render(): React.ReactNode { return
    Matrix
    ; From e495cbce373b6f2f55e89ea300d598c37945c372 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Thu, 15 Jul 2021 15:22:34 +0200 Subject: [PATCH 0351/1171] Migrate AuthPage to TypeScript --- src/components/views/auth/{AuthPage.js => AuthPage.tsx} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename src/components/views/auth/{AuthPage.js => AuthPage.tsx} (96%) diff --git a/src/components/views/auth/AuthPage.js b/src/components/views/auth/AuthPage.tsx similarity index 96% rename from src/components/views/auth/AuthPage.js rename to src/components/views/auth/AuthPage.tsx index 6ba47e5288..9957c1d6d0 100644 --- a/src/components/views/auth/AuthPage.js +++ b/src/components/views/auth/AuthPage.tsx @@ -22,7 +22,7 @@ import { replaceableComponent } from "../../../utils/replaceableComponent"; @replaceableComponent("views.auth.AuthPage") export default class AuthPage extends React.PureComponent { - render() { + public render(): React.ReactNode { const AuthFooter = sdk.getComponent('auth.AuthFooter'); return ( From 1f9b423baceed95dd59d56db7c5779f8986917f1 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Thu, 15 Jul 2021 15:30:39 +0200 Subject: [PATCH 0352/1171] Migrate CaptchaForm to TypeScript --- src/@types/global.d.ts | 2 + .../auth/{CaptchaForm.js => CaptchaForm.tsx} | 59 +++++++++---------- 2 files changed, 31 insertions(+), 30 deletions(-) rename src/components/views/auth/{CaptchaForm.js => CaptchaForm.tsx} (74%) diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts index 7192eb81cc..7f78d96642 100644 --- a/src/@types/global.d.ts +++ b/src/@types/global.d.ts @@ -90,6 +90,8 @@ declare global { mxUIStore: UIStore; mxSetupEncryptionStore?: SetupEncryptionStore; mxRoomScrollStateStore?: RoomScrollStateStore; + grecaptcha: any; + mx_on_recaptcha_loaded: () => void; } interface Document { diff --git a/src/components/views/auth/CaptchaForm.js b/src/components/views/auth/CaptchaForm.tsx similarity index 74% rename from src/components/views/auth/CaptchaForm.js rename to src/components/views/auth/CaptchaForm.tsx index bea4f89f53..f7386be5b0 100644 --- a/src/components/views/auth/CaptchaForm.js +++ b/src/components/views/auth/CaptchaForm.tsx @@ -15,25 +15,28 @@ limitations under the License. */ import React, { createRef } from 'react'; -import PropTypes from 'prop-types'; import { _t } from '../../../languageHandler'; import CountlyAnalytics from "../../../CountlyAnalytics"; import { replaceableComponent } from "../../../utils/replaceableComponent"; const DIV_ID = 'mx_recaptcha'; +interface IProps { + sitePublicKey?: string; + onCaptchaResponse: () => void; +} + +interface IState { + errorText: string; +} + /** * A pure UI component which displays a captcha form. */ @replaceableComponent("views.auth.CaptchaForm") -export default class CaptchaForm extends React.Component { - static propTypes = { - sitePublicKey: PropTypes.string, - - // called with the captcha response - onCaptchaResponse: PropTypes.func, - }; - +export default class CaptchaForm extends React.Component { + private captchaWidgetId: string; + private recaptchaContainer = createRef(); static defaultProps = { onCaptchaResponse: () => {}, }; @@ -45,36 +48,32 @@ export default class CaptchaForm extends React.Component { errorText: null, }; - this._captchaWidgetId = null; - - this._recaptchaContainer = createRef(); - CountlyAnalytics.instance.track("onboarding_grecaptcha_begin"); } - componentDidMount() { + public componentDidMount(): void { // Just putting a script tag into the returned jsx doesn't work, annoyingly, // so we do this instead. - if (global.grecaptcha) { + if (window.grecaptcha) { // TODO: Properly find the type of `grecaptcha` // already loaded - this._onCaptchaLoaded(); + this.onCaptchaLoaded(); } else { console.log("Loading recaptcha script..."); - window.mx_on_recaptcha_loaded = () => {this._onCaptchaLoaded();}; + window.mx_on_recaptcha_loaded = () => {this.onCaptchaLoaded();}; const scriptTag = document.createElement('script'); scriptTag.setAttribute( 'src', `https://www.recaptcha.net/recaptcha/api.js?onload=mx_on_recaptcha_loaded&render=explicit`, ); - this._recaptchaContainer.current.appendChild(scriptTag); + this.recaptchaContainer.current.appendChild(scriptTag); } } - componentWillUnmount() { - this._resetRecaptcha(); + public componentWillUnmount(): void { + this.resetRecaptcha(); } - _renderRecaptcha(divId) { - if (!global.grecaptcha) { + private renderRecaptcha(divId): void { + if (!window.grecaptcha) { console.error("grecaptcha not loaded!"); throw new Error("Recaptcha did not load successfully"); } @@ -88,22 +87,22 @@ export default class CaptchaForm extends React.Component { } console.info("Rendering to %s", divId); - this._captchaWidgetId = global.grecaptcha.render(divId, { + this.captchaWidgetId = window.grecaptcha.render(divId, { sitekey: publicKey, callback: this.props.onCaptchaResponse, }); } - _resetRecaptcha() { - if (this._captchaWidgetId !== null) { - global.grecaptcha.reset(this._captchaWidgetId); + private resetRecaptcha(): void { + if (this.captchaWidgetId !== null) { + window.grecaptcha.reset(this.captchaWidgetId); } } - _onCaptchaLoaded() { + private onCaptchaLoaded(): void { console.log("Loaded recaptcha script."); try { - this._renderRecaptcha(DIV_ID); + this.renderRecaptcha(DIV_ID); // clear error if re-rendered this.setState({ errorText: null, @@ -117,7 +116,7 @@ export default class CaptchaForm extends React.Component { } } - render() { + public render(): React.ReactNode { let error = null; if (this.state.errorText) { error = ( @@ -128,7 +127,7 @@ export default class CaptchaForm extends React.Component { } return ( -
    +

    {_t( "This homeserver would like to make sure you are not a robot.", )}

    From c6dd9bc5261f35563c2a8c493a1042e26ad85c20 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Thu, 15 Jul 2021 15:32:24 +0200 Subject: [PATCH 0353/1171] Migrate CompleteSecurityBody to TypeScript --- .../auth/{CompleteSecurityBody.js => CompleteSecurityBody.tsx} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename src/components/views/auth/{CompleteSecurityBody.js => CompleteSecurityBody.tsx} (95%) diff --git a/src/components/views/auth/CompleteSecurityBody.js b/src/components/views/auth/CompleteSecurityBody.tsx similarity index 95% rename from src/components/views/auth/CompleteSecurityBody.js rename to src/components/views/auth/CompleteSecurityBody.tsx index 745d7abbf2..8f6affb64e 100644 --- a/src/components/views/auth/CompleteSecurityBody.js +++ b/src/components/views/auth/CompleteSecurityBody.tsx @@ -19,7 +19,7 @@ import { replaceableComponent } from "../../../utils/replaceableComponent"; @replaceableComponent("views.auth.CompleteSecurityBody") export default class CompleteSecurityBody extends React.PureComponent { - render() { + public render(): React.ReactNode { return
    { this.props.children }
    ; From 5d0afdb70673fb1401c9986ba1f72c843a8b8593 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Thu, 15 Jul 2021 15:38:07 +0200 Subject: [PATCH 0354/1171] Don't show line number in replies MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/css/views/rooms/_ReplyTile.scss | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/res/css/views/rooms/_ReplyTile.scss b/res/css/views/rooms/_ReplyTile.scss index ccb0069190..c8f76ee995 100644 --- a/res/css/views/rooms/_ReplyTile.scss +++ b/res/css/views/rooms/_ReplyTile.scss @@ -76,6 +76,11 @@ limitations under the License. font-size: $font-14px !important; } + // Hide line numbers + .mx_EventTile_lineNumbers { + display: none; + } + // Hack to cut content in
     tags too
         .mx_EventTile_pre_container > pre {
             overflow: hidden;
    
    From 8ef9c3dfebc1b9c79d2a543e167d77568b34a75e Mon Sep 17 00:00:00 2001
    From: Germain Souquet 
    Date: Thu, 15 Jul 2021 15:42:11 +0200
    Subject: [PATCH 0355/1171] Migrate CountryDropdown to TypeScript
    
    ---
     ...CountryDropdown.js => CountryDropdown.tsx} | 59 +++++++++++--------
     src/phonenumber.ts                            |  8 ++-
     2 files changed, 42 insertions(+), 25 deletions(-)
     rename src/components/views/auth/{CountryDropdown.js => CountryDropdown.tsx} (78%)
    
    diff --git a/src/components/views/auth/CountryDropdown.js b/src/components/views/auth/CountryDropdown.tsx
    similarity index 78%
    rename from src/components/views/auth/CountryDropdown.js
    rename to src/components/views/auth/CountryDropdown.tsx
    index cbc19e0f8d..2e85356e38 100644
    --- a/src/components/views/auth/CountryDropdown.js
    +++ b/src/components/views/auth/CountryDropdown.tsx
    @@ -19,7 +19,7 @@ import PropTypes from 'prop-types';
     
     import * as sdk from '../../../index';
     
    -import { COUNTRIES, getEmojiFlag } from '../../../phonenumber';
    +import { COUNTRIES, getEmojiFlag, PhoneNumberCountryDefinition } from '../../../phonenumber';
     import SdkConfig from "../../../SdkConfig";
     import { _t } from "../../../languageHandler";
     import { replaceableComponent } from "../../../utils/replaceableComponent";
    @@ -29,7 +29,7 @@ for (const c of COUNTRIES) {
         COUNTRIES_BY_ISO2[c.iso2] = c;
     }
     
    -function countryMatchesSearchQuery(query, country) {
    +function countryMatchesSearchQuery(query: string, country: PhoneNumberCountryDefinition): boolean {
         // Remove '+' if present (when searching for a prefix)
         if (query[0] === '+') {
             query = query.slice(1);
    @@ -41,15 +41,26 @@ function countryMatchesSearchQuery(query, country) {
         return false;
     }
     
    -@replaceableComponent("views.auth.CountryDropdown")
    -export default class CountryDropdown extends React.Component {
    -    constructor(props) {
    -        super(props);
    -        this._onSearchChange = this._onSearchChange.bind(this);
    -        this._onOptionChange = this._onOptionChange.bind(this);
    -        this._getShortOption = this._getShortOption.bind(this);
    +interface IProps {
    +    value?: string;
    +    onOptionChange: (country: PhoneNumberCountryDefinition) => void;
    +    isSmall: boolean;
    +    showPrefix: boolean;
    +    className?: string;
    +    disabled?: boolean;
    +}
     
    -        let defaultCountry = COUNTRIES[0];
    +interface IState {
    +    searchQuery: string;
    +    defaultCountry: PhoneNumberCountryDefinition;
    +}
    +
    +@replaceableComponent("views.auth.CountryDropdown")
    +export default class CountryDropdown extends React.Component {
    +    constructor(props: IProps) {
    +        super(props);
    +
    +        let defaultCountry: PhoneNumberCountryDefinition = COUNTRIES[0];
             const defaultCountryCode = SdkConfig.get()["defaultCountryCode"];
             if (defaultCountryCode) {
                 const country = COUNTRIES.find(c => c.iso2 === defaultCountryCode.toUpperCase());
    @@ -62,7 +73,7 @@ export default class CountryDropdown extends React.Component {
             };
         }
     
    -    componentDidMount() {
    +    public componentDidMount(): void {
             if (!this.props.value) {
                 // If no value is given, we start with the default
                 // country selected, but our parent component
    @@ -71,21 +82,21 @@ export default class CountryDropdown extends React.Component {
             }
         }
     
    -    _onSearchChange(search) {
    +    private onSearchChange = (search: string): void => {
             this.setState({
                 searchQuery: search,
             });
    -    }
    +    };
     
    -    _onOptionChange(iso2) {
    +    private onOptionChange = (iso2: string): void => {
             this.props.onOptionChange(COUNTRIES_BY_ISO2[iso2]);
    -    }
    +    };
     
    -    _flagImgForIso2(iso2) {
    +    private flagImgForIso2(iso2: string): React.ReactNode {
             return 
    { getEmojiFlag(iso2) }
    ; } - _getShortOption(iso2) { + private getShortOption = (iso2: string): React.ReactNode => { if (!this.props.isSmall) { return undefined; } @@ -94,12 +105,12 @@ export default class CountryDropdown extends React.Component { countryPrefix = '+' + COUNTRIES_BY_ISO2[iso2].prefix; } return - { this._flagImgForIso2(iso2) } + { this.flagImgForIso2(iso2) } { countryPrefix } ; - } + }; - render() { + public render(): React.ReactNode { const Dropdown = sdk.getComponent('elements.Dropdown'); let displayedCountries; @@ -124,7 +135,7 @@ export default class CountryDropdown extends React.Component { const options = displayedCountries.map((country) => { return
    - { this._flagImgForIso2(country.iso2) } + { this.flagImgForIso2(country.iso2) } { _t(country.name) } (+{ country.prefix })
    ; }); @@ -136,10 +147,10 @@ export default class CountryDropdown extends React.Component { return { return String.fromCodePoint(...countryCode.split('').map(l => UNICODE_BASE + l.charCodeAt(0))); }; -export const COUNTRIES = [ +export interface PhoneNumberCountryDefinition { + iso2: string; + name: string; + prefix: string; +} + +export const COUNTRIES: PhoneNumberCountryDefinition[] = [ { "iso2": "GB", "name": _td("United Kingdom"), From 3b5266071e5fbdba252610a0588e8b04de4fa21b Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Thu, 15 Jul 2021 15:44:44 +0200 Subject: [PATCH 0356/1171] Migrate LanguageSelector to TypeScript --- .../auth/{LanguageSelector.js => LanguageSelector.tsx} | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) rename src/components/views/auth/{LanguageSelector.js => LanguageSelector.tsx} (89%) diff --git a/src/components/views/auth/LanguageSelector.js b/src/components/views/auth/LanguageSelector.tsx similarity index 89% rename from src/components/views/auth/LanguageSelector.js rename to src/components/views/auth/LanguageSelector.tsx index 88293310e7..fc4f4ba5ca 100644 --- a/src/components/views/auth/LanguageSelector.js +++ b/src/components/views/auth/LanguageSelector.tsx @@ -22,14 +22,18 @@ import * as sdk from '../../../index'; import React from 'react'; import { SettingLevel } from "../../../settings/SettingLevel"; -function onChange(newLang) { +function onChange(newLang: string): void { if (getCurrentLanguage() !== newLang) { SettingsStore.setValue("language", null, SettingLevel.DEVICE, newLang); PlatformPeg.get().reload(); } } -export default function LanguageSelector({ disabled }) { +interface IProps { + disabled?: boolean; +} + +export default function LanguageSelector({ disabled }: IProps): React.ReactNode { if (SdkConfig.get()['disable_login_language_selector']) return
    ; const LanguageDropdown = sdk.getComponent('views.elements.LanguageDropdown'); From 54bfe8ec1ed6fdeb14c6ef872f8ab4ffe1a4e544 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Thu, 15 Jul 2021 15:45:36 +0200 Subject: [PATCH 0357/1171] Migrate Welcome to TypeScript --- src/components/views/auth/CaptchaForm.tsx | 2 +- src/components/views/auth/CountryDropdown.tsx | 10 ---------- src/components/views/auth/{Welcome.js => Welcome.tsx} | 10 +++++++--- 3 files changed, 8 insertions(+), 14 deletions(-) rename src/components/views/auth/{Welcome.js => Welcome.tsx} (93%) diff --git a/src/components/views/auth/CaptchaForm.tsx b/src/components/views/auth/CaptchaForm.tsx index f7386be5b0..d71d8a6b15 100644 --- a/src/components/views/auth/CaptchaForm.tsx +++ b/src/components/views/auth/CaptchaForm.tsx @@ -23,7 +23,7 @@ const DIV_ID = 'mx_recaptcha'; interface IProps { sitePublicKey?: string; - onCaptchaResponse: () => void; + onCaptchaResponse: (response: string) => void; } interface IState { diff --git a/src/components/views/auth/CountryDropdown.tsx b/src/components/views/auth/CountryDropdown.tsx index 2e85356e38..e0eed5b430 100644 --- a/src/components/views/auth/CountryDropdown.tsx +++ b/src/components/views/auth/CountryDropdown.tsx @@ -160,13 +160,3 @@ export default class CountryDropdown extends React.Component { ; } } - -CountryDropdown.propTypes = { - className: PropTypes.string, - isSmall: PropTypes.bool, - // if isSmall, show +44 in the selected value - showPrefix: PropTypes.bool, - onOptionChange: PropTypes.func.isRequired, - value: PropTypes.string, - disabled: PropTypes.bool, -}; diff --git a/src/components/views/auth/Welcome.js b/src/components/views/auth/Welcome.tsx similarity index 93% rename from src/components/views/auth/Welcome.js rename to src/components/views/auth/Welcome.tsx index e3f7a601f2..1b02d0d2b5 100644 --- a/src/components/views/auth/Welcome.js +++ b/src/components/views/auth/Welcome.tsx @@ -29,15 +29,19 @@ import { replaceableComponent } from "../../../utils/replaceableComponent"; // translatable strings for Welcome pages _td("Sign in with SSO"); +interface IProps { + +} + @replaceableComponent("views.auth.Welcome") -export default class Welcome extends React.PureComponent { - constructor(props) { +export default class Welcome extends React.PureComponent { + constructor(props: IProps) { super(props); CountlyAnalytics.instance.track("onboarding_welcome"); } - render() { + public render(): React.ReactNode { const EmbeddedPage = sdk.getComponent('structures.EmbeddedPage'); const LanguageSelector = sdk.getComponent('auth.LanguageSelector'); From b0053f36d3630f91e3f29694366ef87aff8a2b59 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 15 Jul 2021 17:43:24 +0100 Subject: [PATCH 0358/1171] Fix instances of event.sender being read for just the userId - this field may not be set in time --- src/Notifier.ts | 2 +- src/Unread.ts | 6 ++---- src/components/structures/MessagePanel.tsx | 2 +- src/components/structures/TimelinePanel.tsx | 7 +++---- 4 files changed, 7 insertions(+), 10 deletions(-) diff --git a/src/Notifier.ts b/src/Notifier.ts index 415adcafc8..1137e44aec 100644 --- a/src/Notifier.ts +++ b/src/Notifier.ts @@ -328,7 +328,7 @@ export const Notifier = { onEvent: function(ev: MatrixEvent) { if (!this.isSyncing) return; // don't alert for any messages initially - if (ev.sender && ev.sender.userId === MatrixClientPeg.get().credentials.userId) return; + if (ev.getSender() === MatrixClientPeg.get().credentials.userId) return; MatrixClientPeg.get().decryptEventIfNeeded(ev); diff --git a/src/Unread.ts b/src/Unread.ts index 72f0bb4642..da5b883f92 100644 --- a/src/Unread.ts +++ b/src/Unread.ts @@ -30,7 +30,7 @@ import { haveTileForEvent } from "./components/views/rooms/EventTile"; * @returns {boolean} True if the given event should affect the unread message count */ export function eventTriggersUnreadCount(ev: MatrixEvent): boolean { - if (ev.sender && ev.sender.userId == MatrixClientPeg.get().credentials.userId) { + if (ev.getSender() === MatrixClientPeg.get().credentials.userId) { return false; } @@ -63,9 +63,7 @@ export function doesRoomHaveUnreadMessages(room: Room): boolean { // https://github.com/vector-im/element-web/issues/2427 // ...and possibly some of the others at // https://github.com/vector-im/element-web/issues/3363 - if (room.timeline.length && - room.timeline[room.timeline.length - 1].sender && - room.timeline[room.timeline.length - 1].sender.userId === myUserId) { + if (room.timeline.length && room.timeline[room.timeline.length - 1].getSender() === myUserId) { return false; } diff --git a/src/components/structures/MessagePanel.tsx b/src/components/structures/MessagePanel.tsx index a0a1ac9b10..47f8c218dc 100644 --- a/src/components/structures/MessagePanel.tsx +++ b/src/components/structures/MessagePanel.tsx @@ -401,7 +401,7 @@ export default class MessagePanel extends React.Component { // TODO: Implement granular (per-room) hide options public shouldShowEvent(mxEv: MatrixEvent): boolean { - if (mxEv.sender && MatrixClientPeg.get().isUserIgnored(mxEv.sender.userId)) { + if (MatrixClientPeg.get().isUserIgnored(mxEv.getSender())) { return false; // ignored = no show (only happens if the ignore happens after an event was received) } diff --git a/src/components/structures/TimelinePanel.tsx b/src/components/structures/TimelinePanel.tsx index c21aac790b..5f9d9b7026 100644 --- a/src/components/structures/TimelinePanel.tsx +++ b/src/components/structures/TimelinePanel.tsx @@ -555,9 +555,8 @@ class TimelinePanel extends React.Component { // more than the timeout on userActiveRecently. // const myUserId = MatrixClientPeg.get().credentials.userId; - const sender = ev.sender ? ev.sender.userId : null; callRMUpdated = false; - if (sender != myUserId && !UserActivity.sharedInstance().userActiveRecently()) { + if (ev.getSender() !== myUserId && !UserActivity.sharedInstance().userActiveRecently()) { updatedState.readMarkerVisible = true; } else if (lastLiveEvent && this.getReadMarkerPosition() === 0) { // we know we're stuckAtBottom, so we can advance the RM @@ -863,7 +862,7 @@ class TimelinePanel extends React.Component { const myUserId = MatrixClientPeg.get().credentials.userId; for (i++; i < events.length; i++) { const ev = events[i]; - if (!ev.sender || ev.sender.userId != myUserId) { + if (ev.getSender() !== myUserId) { break; } } @@ -1337,7 +1336,7 @@ class TimelinePanel extends React.Component { } const shouldIgnore = !!ev.status || // local echo - (ignoreOwn && ev.sender && ev.sender.userId == myUserId); // own message + (ignoreOwn && ev.getSender() === myUserId); // own message const isWithoutTile = !haveTileForEvent(ev) || shouldHideEvent(ev, this.context); if (isWithoutTile || !node) { From 923d68a0fa8f014f28a31b0156ccd0b00e08ed90 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 15 Jul 2021 17:46:46 +0100 Subject: [PATCH 0359/1171] Fix EventIndex handling events twice It awaits the decryption in onRoomTimeline as well as subscribing to EVent.decrypted --- src/indexing/EventIndex.ts | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/src/indexing/EventIndex.ts b/src/indexing/EventIndex.ts index a5827fc599..a7142010f2 100644 --- a/src/indexing/EventIndex.ts +++ b/src/indexing/EventIndex.ts @@ -67,7 +67,6 @@ export default class EventIndex extends EventEmitter { client.on('sync', this.onSync); client.on('Room.timeline', this.onRoomTimeline); - client.on('Event.decrypted', this.onEventDecrypted); client.on('Room.timelineReset', this.onTimelineReset); client.on('Room.redaction', this.onRedaction); client.on('RoomState.events', this.onRoomStateEvent); @@ -82,7 +81,6 @@ export default class EventIndex extends EventEmitter { client.removeListener('sync', this.onSync); client.removeListener('Room.timeline', this.onRoomTimeline); - client.removeListener('Event.decrypted', this.onEventDecrypted); client.removeListener('Room.timelineReset', this.onTimelineReset); client.removeListener('Room.redaction', this.onRedaction); client.removeListener('RoomState.events', this.onRoomStateEvent); @@ -221,18 +219,6 @@ export default class EventIndex extends EventEmitter { } }; - /* - * The Event.decrypted listener. - * - * Checks if the event was marked for addition in the Room.timeline - * listener, if so queues it up to be added to the index. - */ - private onEventDecrypted = async (ev: MatrixEvent, err: Error) => { - // If the event isn't in our live event set, ignore it. - if (err) return; - await this.addLiveEventToIndex(ev); - }; - /* * The Room.redaction listener. * From 14371882828acb8aa7abedc76e87715a49563a8e Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 15 Jul 2021 18:02:02 +0100 Subject: [PATCH 0360/1171] Also move effects handling from `event` to `Room.timeline` to wake up less --- src/components/structures/RoomView.tsx | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index 2fe694a435..7e3bcbc962 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -253,7 +253,6 @@ export default class RoomView extends React.Component { this.context.on("userTrustStatusChanged", this.onUserVerificationChanged); this.context.on("crossSigning.keysChanged", this.onCrossSigningKeysChanged); this.context.on("Event.decrypted", this.onEventDecrypted); - this.context.on("event", this.onEvent); // Start listening for RoomViewStore updates this.roomStoreToken = RoomViewStore.addListener(this.onRoomViewStoreUpdate); this.rightPanelStoreToken = RightPanelStore.getSharedInstance().addListener(this.onRightPanelStoreUpdate); @@ -637,7 +636,6 @@ export default class RoomView extends React.Component { this.context.removeListener("userTrustStatusChanged", this.onUserVerificationChanged); this.context.removeListener("crossSigning.keysChanged", this.onCrossSigningKeysChanged); this.context.removeListener("Event.decrypted", this.onEventDecrypted); - this.context.removeListener("event", this.onEvent); } window.removeEventListener('beforeunload', this.onPageUnload); @@ -837,8 +835,7 @@ export default class RoomView extends React.Component { if (this.unmounted) return; // ignore events for other rooms - if (!room) return; - if (!this.state.room || room.roomId != this.state.room.roomId) return; + if (!room || room.roomId !== this.state.room?.roomId) return; // ignore events from filtered timelines if (data.timeline.getTimelineSet() !== room.getUnfilteredTimelineSet()) return; @@ -859,6 +856,10 @@ export default class RoomView extends React.Component { // we'll only be showing a spinner. if (this.state.joining) return; + if (!ev.isBeingDecrypted() && !ev.isDecryptionFailure()) { + this.handleEffects(ev); + } + if (ev.getSender() !== this.context.credentials.userId) { // update unread count when scrolled up if (!this.state.searchResults && this.state.atEndOfLiveTimeline) { @@ -871,20 +872,14 @@ export default class RoomView extends React.Component { } }; - private onEventDecrypted = (ev) => { + private onEventDecrypted = (ev: MatrixEvent) => { + if (!this.state.room || !this.state.matrixClientIsReady) return; // not ready at all + if (ev.getRoomId() !== this.state.room.roomId) return; // not for us if (ev.isDecryptionFailure()) return; this.handleEffects(ev); }; - private onEvent = (ev) => { - if (ev.isBeingDecrypted() || ev.isDecryptionFailure()) return; - this.handleEffects(ev); - }; - - private handleEffects = (ev) => { - if (!this.state.room || !this.state.matrixClientIsReady) return; // not ready at all - if (ev.getRoomId() !== this.state.room.roomId) return; // not for us - + private handleEffects = (ev: MatrixEvent) => { const notifState = RoomNotificationStateStore.instance.getRoomState(this.state.room); if (!notifState.isUnread) return; From 831c4823715f2bfae710d6a652417967eb9ad99f Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 15 Jul 2021 18:17:07 +0100 Subject: [PATCH 0361/1171] Stub out MatrixClient::isUserIgnored for tests --- test/test-utils.js | 1 + 1 file changed, 1 insertion(+) diff --git a/test/test-utils.js b/test/test-utils.js index ad56522965..d75abc80f0 100644 --- a/test/test-utils.js +++ b/test/test-utils.js @@ -96,6 +96,7 @@ export function createTestClient() { }, }, decryptEventIfNeeded: () => Promise.resolve(), + isUserIgnored: jest.fn().mockReturnValue(false), }; } From 2690bb56f9f08c114d56c8e25a88b1af36285e2c Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 15 Jul 2021 13:39:54 -0600 Subject: [PATCH 0362/1171] Remove code we don't seem to need --- .../views/settings/Notifications.tsx | 29 ------------------- 1 file changed, 29 deletions(-) diff --git a/src/components/views/settings/Notifications.tsx b/src/components/views/settings/Notifications.tsx index 6baac8892e..0cfcdd61af 100644 --- a/src/components/views/settings/Notifications.tsx +++ b/src/components/views/settings/Notifications.tsx @@ -214,15 +214,6 @@ export default class Notifications extends React.PureComponent { rule, vectorState, description: _t(definition.description), }); - - // XXX: Do we need this block from the previous component? - /* - // if there was a rule which we couldn't parse, add it to the external list - if (rule && !vectorState) { - rule.description = ruleDefinition.description; - self.state.externalPushRules.push(rule); - } - */ } // Quickly sort the rules for display purposes @@ -246,26 +237,6 @@ export default class Notifications extends React.PureComponent { } } - // XXX: Do we need this block from the previous component? - /* - // Build the rules not managed by Vector UI - const otherRulesDescriptions = { - '.m.rule.message': _t('Notify for all other messages/rooms'), - '.m.rule.fallback': _t('Notify me for anything else'), - }; - - for (const i in defaultRules.others) { - const rule = defaultRules.others[i]; - const ruleDescription = otherRulesDescriptions[rule.rule_id]; - - // Show enabled default rules that was modified by the user - if (ruleDescription && rule.enabled && !rule.default) { - rule.description = ruleDescription; - self.state.externalPushRules.push(rule); - } - } - */ - return preparedNewState; } From a3792b75e2b1e47cecb63e622966e749f1e8fdc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Fri, 16 Jul 2021 07:53:20 +0200 Subject: [PATCH 0363/1171] Fix IRC layout replies 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 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/res/css/views/rooms/_IRCLayout.scss b/res/css/views/rooms/_IRCLayout.scss index 5e61c3b8a3..97190807ca 100644 --- a/res/css/views/rooms/_IRCLayout.scss +++ b/res/css/views/rooms/_IRCLayout.scss @@ -198,8 +198,9 @@ $irc-line-height: $font-18px; .mx_ReplyThread { margin: 0; .mx_SenderProfile { + order: unset; + max-width: unset; width: unset; - max-width: var(--name-width); background: transparent; } From 8f6458a79c8ba4fa52e88ca79411ba5ea831e722 Mon Sep 17 00:00:00 2001 From: Aaron Raimist Date: Fri, 16 Jul 2021 01:43:03 -0500 Subject: [PATCH 0364/1171] Add matrix: to the list of permitted URL schemes Signed-off-by: Aaron Raimist --- src/HtmlUtils.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx index 5e83fdc2a0..dfe5cba3fd 100644 --- a/src/HtmlUtils.tsx +++ b/src/HtmlUtils.tsx @@ -58,7 +58,7 @@ const BIGEMOJI_REGEX = new RegExp(`^(${EMOJIBASE_REGEX.source})+$`, 'i'); const COLOR_REGEX = /^#[0-9a-fA-F]{6}$/; -export const PERMITTED_URL_SCHEMES = ['http', 'https', 'ftp', 'mailto', 'magnet']; +export const PERMITTED_URL_SCHEMES = ['http', 'https', 'ftp', 'mailto', 'magnet', 'matrix']; const MEDIA_API_MXC_REGEX = /\/_matrix\/media\/r0\/(?:download|thumbnail)\/(.+?)\/(.+?)(?:[?/]|$)/; From 32cc48ff7a714fab5ef802cf4e94358475ebe73b Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 16 Jul 2021 08:49:19 +0100 Subject: [PATCH 0365/1171] Fix issue with room duplication caused by filtering and selecting room using keyboard Wrap sticky room updates in lock to prevent setStickyRoom running in middle of setKnownRooms --- src/stores/room-list/algorithms/Algorithm.ts | 147 ++++++++++--------- 1 file changed, 80 insertions(+), 67 deletions(-) diff --git a/src/stores/room-list/algorithms/Algorithm.ts b/src/stores/room-list/algorithms/Algorithm.ts index f50d112248..2acce1ecd7 100644 --- a/src/stores/room-list/algorithms/Algorithm.ts +++ b/src/stores/room-list/algorithms/Algorithm.ts @@ -16,8 +16,10 @@ limitations under the License. import { Room } from "matrix-js-sdk/src/models/room"; import { isNullOrUndefined } from "matrix-js-sdk/src/utils"; -import DMRoomMap from "../../../utils/DMRoomMap"; import { EventEmitter } from "events"; +import AwaitLock from "await-lock"; + +import DMRoomMap from "../../../utils/DMRoomMap"; import { arrayDiff, arrayHasDiff } from "../../../utils/arrays"; import { DefaultTagID, RoomUpdateCause, TagID } from "../models"; import { @@ -78,6 +80,7 @@ export class Algorithm extends EventEmitter { } = {}; private allowedByFilter: Map = new Map(); private allowedRoomsByFilters: Set = new Set(); + private stickyLock = new AwaitLock(); /** * Set to true to suspend emissions of algorithm updates. @@ -123,7 +126,12 @@ export class Algorithm extends EventEmitter { * @param val The new room to sticky. */ public async setStickyRoom(val: Room) { - await this.updateStickyRoom(val); + await this.stickyLock.acquireAsync(); + try { + await this.updateStickyRoom(val); + } finally { + this.stickyLock.release(); + } } public getTagSorting(tagId: TagID): SortAlgorithm { @@ -519,82 +527,87 @@ export class Algorithm extends EventEmitter { if (isNullOrUndefined(rooms)) throw new Error(`Array of rooms cannot be null`); if (!this.sortAlgorithms) throw new Error(`Cannot set known rooms without a tag sorting map`); - if (!this.updatesInhibited) { - // We only log this if we're expecting to be publishing updates, which means that - // this could be an unexpected invocation. If we're inhibited, then this is probably - // an intentional invocation. - console.warn("Resetting known rooms, initiating regeneration"); - } + await this.stickyLock.acquireAsync(); + try { + if (!this.updatesInhibited) { + // We only log this if we're expecting to be publishing updates, which means that + // this could be an unexpected invocation. If we're inhibited, then this is probably + // an intentional invocation. + console.warn("Resetting known rooms, initiating regeneration"); + } - // Before we go any further we need to clear (but remember) the sticky room to - // avoid accidentally duplicating it in the list. - const oldStickyRoom = this._stickyRoom; - await this.updateStickyRoom(null); + // Before we go any further we need to clear (but remember) the sticky room to + // avoid accidentally duplicating it in the list. + const oldStickyRoom = this._stickyRoom; + if (oldStickyRoom) await this.updateStickyRoom(null); - this.rooms = rooms; + this.rooms = rooms; - const newTags: ITagMap = {}; - for (const tagId in this.sortAlgorithms) { - // noinspection JSUnfilteredForInLoop - newTags[tagId] = []; - } + const newTags: ITagMap = {}; + for (const tagId in this.sortAlgorithms) { + // noinspection JSUnfilteredForInLoop + newTags[tagId] = []; + } - // If we can avoid doing work, do so. - if (!rooms.length) { - await this.generateFreshTags(newTags); // just in case it wants to do something - this.cachedRooms = newTags; - return; - } + // If we can avoid doing work, do so. + if (!rooms.length) { + await this.generateFreshTags(newTags); // just in case it wants to do something + this.cachedRooms = newTags; + return; + } - // Split out the easy rooms first (leave and invite) - const memberships = splitRoomsByMembership(rooms); - for (const room of memberships[EffectiveMembership.Invite]) { - newTags[DefaultTagID.Invite].push(room); - } - for (const room of memberships[EffectiveMembership.Leave]) { - newTags[DefaultTagID.Archived].push(room); - } + // Split out the easy rooms first (leave and invite) + const memberships = splitRoomsByMembership(rooms); + for (const room of memberships[EffectiveMembership.Invite]) { + newTags[DefaultTagID.Invite].push(room); + } + for (const room of memberships[EffectiveMembership.Leave]) { + newTags[DefaultTagID.Archived].push(room); + } - // Now process all the joined rooms. This is a bit more complicated - for (const room of memberships[EffectiveMembership.Join]) { - const tags = this.getTagsOfJoinedRoom(room); + // Now process all the joined rooms. This is a bit more complicated + for (const room of memberships[EffectiveMembership.Join]) { + const tags = this.getTagsOfJoinedRoom(room); - let inTag = false; - if (tags.length > 0) { - for (const tag of tags) { - if (!isNullOrUndefined(newTags[tag])) { - newTags[tag].push(room); - inTag = true; + let inTag = false; + if (tags.length > 0) { + for (const tag of tags) { + if (!isNullOrUndefined(newTags[tag])) { + newTags[tag].push(room); + inTag = true; + } + } + } + + if (!inTag) { + if (DMRoomMap.shared().getUserIdForRoomId(room.roomId)) { + newTags[DefaultTagID.DM].push(room); + } else { + newTags[DefaultTagID.Untagged].push(room); } } } - if (!inTag) { - if (DMRoomMap.shared().getUserIdForRoomId(room.roomId)) { - newTags[DefaultTagID.DM].push(room); - } else { - newTags[DefaultTagID.Untagged].push(room); - } - } - } - - await this.generateFreshTags(newTags); - - this.cachedRooms = newTags; // this recalculates the filtered rooms for us - this.updateTagsFromCache(); - - // Now that we've finished generation, we need to update the sticky room to what - // it was. It's entirely possible that it changed lists though, so if it did then - // we also have to update the position of it. - if (oldStickyRoom && oldStickyRoom.room) { - await this.updateStickyRoom(oldStickyRoom.room); - if (this._stickyRoom && this._stickyRoom.room) { // just in case the update doesn't go according to plan - if (this._stickyRoom.tag !== oldStickyRoom.tag) { - // We put the sticky room at the top of the list to treat it as an obvious tag change. - this._stickyRoom.position = 0; - this.recalculateStickyRoom(this._stickyRoom.tag); + await this.generateFreshTags(newTags); + + this.cachedRooms = newTags; // this recalculates the filtered rooms for us + this.updateTagsFromCache(); + + // Now that we've finished generation, we need to update the sticky room to what + // it was. It's entirely possible that it changed lists though, so if it did then + // we also have to update the position of it. + if (oldStickyRoom && oldStickyRoom.room) { + await this.updateStickyRoom(oldStickyRoom.room); + if (this._stickyRoom && this._stickyRoom.room) { // just in case the update doesn't go according to plan + if (this._stickyRoom.tag !== oldStickyRoom.tag) { + // We put the sticky room at the top of the list to treat it as an obvious tag change. + this._stickyRoom.position = 0; + this.recalculateStickyRoom(this._stickyRoom.tag); + } } } + } finally { + this.stickyLock.release(); } } @@ -685,9 +698,9 @@ export class Algorithm extends EventEmitter { if (!this.algorithms) throw new Error("Not ready: no algorithms to determine tags from"); // Note: check the isSticky against the room ID just in case the reference is wrong - const isSticky = this._stickyRoom && this._stickyRoom.room && this._stickyRoom.room.roomId === room.roomId; + const isSticky = this._stickyRoom?.room?.roomId === room.roomId; if (cause === RoomUpdateCause.NewRoom) { - const isForLastSticky = this._lastStickyRoom && this._lastStickyRoom.room === room; + const isForLastSticky = this._lastStickyRoom?.room === room; const roomTags = this.roomIdsToTags[room.roomId]; const hasTags = roomTags && roomTags.length > 0; From 7464900f95976447fbd66b45b5d6814d7ee7675c Mon Sep 17 00:00:00 2001 From: James Salter Date: Fri, 16 Jul 2021 09:05:01 +0100 Subject: [PATCH 0366/1171] Change menu label to Copy Link --- src/components/views/rooms/RoomTile.tsx | 2 +- src/i18n/strings/en_EN.json | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/views/rooms/RoomTile.tsx b/src/components/views/rooms/RoomTile.tsx index 8fb4d04791..aa56412149 100644 --- a/src/components/views/rooms/RoomTile.tsx +++ b/src/components/views/rooms/RoomTile.tsx @@ -535,7 +535,7 @@ export default class RoomTile extends React.PureComponent { /> diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index d82d19fe3d..41839a1b2a 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1665,6 +1665,7 @@ "Favourite": "Favourite", "Low Priority": "Low Priority", "Invite People": "Invite People", + "Copy Link": "Copy Link", "Leave Room": "Leave Room", "Room options": "Room options", "%(count)s unread messages including mentions.|other": "%(count)s unread messages including mentions.", From a1c658f187830c1105b62be4a6ec29e8b5474203 Mon Sep 17 00:00:00 2001 From: James Salter Date: Fri, 16 Jul 2021 09:07:52 +0100 Subject: [PATCH 0367/1171] Set Menu icon to link --- res/css/views/rooms/_RoomTile.scss | 4 ++++ src/components/views/rooms/RoomTile.tsx | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/res/css/views/rooms/_RoomTile.scss b/res/css/views/rooms/_RoomTile.scss index 03146e0325..b8f4aeb6e7 100644 --- a/res/css/views/rooms/_RoomTile.scss +++ b/res/css/views/rooms/_RoomTile.scss @@ -193,6 +193,10 @@ limitations under the License. mask-image: url('$(res)/img/element-icons/settings.svg'); } + .mx_RoomTile_iconCopyLink::before { + mask-image: url('$(res)/img/element-icons/link.svg'); + } + .mx_RoomTile_iconInvite::before { mask-image: url('$(res)/img/element-icons/room/invite.svg'); } diff --git a/src/components/views/rooms/RoomTile.tsx b/src/components/views/rooms/RoomTile.tsx index aa56412149..2417b4c6f3 100644 --- a/src/components/views/rooms/RoomTile.tsx +++ b/src/components/views/rooms/RoomTile.tsx @@ -536,7 +536,7 @@ export default class RoomTile extends React.PureComponent { From ff80bbc4a5a862de25b5b1f4eeb7e51d8cf657e3 Mon Sep 17 00:00:00 2001 From: James Salter Date: Fri, 16 Jul 2021 09:10:20 +0100 Subject: [PATCH 0368/1171] Move copy link to be before settings --- src/components/views/rooms/RoomTile.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/views/rooms/RoomTile.tsx b/src/components/views/rooms/RoomTile.tsx index 2417b4c6f3..aade665b6b 100644 --- a/src/components/views/rooms/RoomTile.tsx +++ b/src/components/views/rooms/RoomTile.tsx @@ -528,16 +528,16 @@ export default class RoomTile extends React.PureComponent { iconClassName="mx_RoomTile_iconInvite" /> ) : null} - + Date: Fri, 16 Jul 2021 09:15:56 +0100 Subject: [PATCH 0369/1171] Make the critical sections of the RLS synchronous to avoid needing to mutex everything --- src/components/views/rooms/RoomSublist.tsx | 4 +- src/stores/room-list/RoomListStore.ts | 54 ++--- src/stores/room-list/algorithms/Algorithm.ts | 200 ++++++++---------- .../list-ordering/ImportanceAlgorithm.ts | 98 ++++----- .../list-ordering/NaturalAlgorithm.ts | 58 +++-- .../list-ordering/OrderingAlgorithm.ts | 10 +- .../tag-sorting/AlphabeticAlgorithm.ts | 2 +- .../algorithms/tag-sorting/IAlgorithm.ts | 4 +- .../algorithms/tag-sorting/ManualAlgorithm.ts | 2 +- .../algorithms/tag-sorting/RecentAlgorithm.ts | 2 +- .../room-list/algorithms/tag-sorting/index.ts | 4 +- 11 files changed, 199 insertions(+), 239 deletions(-) diff --git a/src/components/views/rooms/RoomSublist.tsx b/src/components/views/rooms/RoomSublist.tsx index fce9e297a1..8d825a2b53 100644 --- a/src/components/views/rooms/RoomSublist.tsx +++ b/src/components/views/rooms/RoomSublist.tsx @@ -408,10 +408,10 @@ export default class RoomSublist extends React.Component { this.setState({ addRoomContextMenuPosition: null }); }; - private onUnreadFirstChanged = async () => { + private onUnreadFirstChanged = () => { const isUnreadFirst = RoomListStore.instance.getListOrder(this.props.tagId) === ListAlgorithm.Importance; const newAlgorithm = isUnreadFirst ? ListAlgorithm.Natural : ListAlgorithm.Importance; - await RoomListStore.instance.setListOrder(this.props.tagId, newAlgorithm); + RoomListStore.instance.setListOrder(this.props.tagId, newAlgorithm); this.forceUpdate(); // because if the sublist doesn't have any changes then we will miss the list order change }; diff --git a/src/stores/room-list/RoomListStore.ts b/src/stores/room-list/RoomListStore.ts index bedbfebd7f..3913a2220f 100644 --- a/src/stores/room-list/RoomListStore.ts +++ b/src/stores/room-list/RoomListStore.ts @@ -132,8 +132,8 @@ export class RoomListStoreClass extends AsyncStoreWithClient { // Update any settings here, as some may have happened before we were logically ready. console.log("Regenerating room lists: Startup"); await this.readAndCacheSettingsFromStore(); - await this.regenerateAllLists({ trigger: false }); - await this.handleRVSUpdate({ trigger: false }); // fake an RVS update to adjust sticky room, if needed + this.regenerateAllLists({ trigger: false }); + this.handleRVSUpdate({ trigger: false }); // fake an RVS update to adjust sticky room, if needed this.updateFn.mark(); // we almost certainly want to trigger an update. this.updateFn.trigger(); @@ -150,7 +150,7 @@ export class RoomListStoreClass extends AsyncStoreWithClient { await this.updateState({ tagsEnabled, }); - await this.updateAlgorithmInstances(); + this.updateAlgorithmInstances(); } /** @@ -158,23 +158,23 @@ export class RoomListStoreClass extends AsyncStoreWithClient { * @param trigger Set to false to prevent a list update from being sent. Should only * be used if the calling code will manually trigger the update. */ - private async handleRVSUpdate({ trigger = true }) { + private handleRVSUpdate({ trigger = true }) { if (!this.matrixClient) return; // We assume there won't be RVS updates without a client const activeRoomId = RoomViewStore.getRoomId(); if (!activeRoomId && this.algorithm.stickyRoom) { - await this.algorithm.setStickyRoom(null); + this.algorithm.setStickyRoom(null); } else if (activeRoomId) { const activeRoom = this.matrixClient.getRoom(activeRoomId); if (!activeRoom) { console.warn(`${activeRoomId} is current in RVS but missing from client - clearing sticky room`); - await this.algorithm.setStickyRoom(null); + this.algorithm.setStickyRoom(null); } else if (activeRoom !== this.algorithm.stickyRoom) { if (SettingsStore.getValue("advancedRoomListLogging")) { // TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602 console.log(`Changing sticky room to ${activeRoomId}`); } - await this.algorithm.setStickyRoom(activeRoom); + this.algorithm.setStickyRoom(activeRoom); } } @@ -226,7 +226,7 @@ export class RoomListStoreClass extends AsyncStoreWithClient { console.log("Regenerating room lists: Settings changed"); await this.readAndCacheSettingsFromStore(); - await this.regenerateAllLists({ trigger: false }); // regenerate the lists now + this.regenerateAllLists({ trigger: false }); // regenerate the lists now this.updateFn.trigger(); } } @@ -368,7 +368,7 @@ export class RoomListStoreClass extends AsyncStoreWithClient { // TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602 console.log(`[RoomListDebug] Clearing sticky room due to room upgrade`); } - await this.algorithm.setStickyRoom(null); + this.algorithm.setStickyRoom(null); } // Note: we hit the algorithm instead of our handleRoomUpdate() function to @@ -377,7 +377,7 @@ export class RoomListStoreClass extends AsyncStoreWithClient { // TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602 console.log(`[RoomListDebug] Removing previous room from room list`); } - await this.algorithm.handleRoomUpdate(prevRoom, RoomUpdateCause.RoomRemoved); + this.algorithm.handleRoomUpdate(prevRoom, RoomUpdateCause.RoomRemoved); } } @@ -433,7 +433,7 @@ export class RoomListStoreClass extends AsyncStoreWithClient { return; // don't do anything on new/moved rooms which ought not to be shown } - const shouldUpdate = await this.algorithm.handleRoomUpdate(room, cause); + const shouldUpdate = this.algorithm.handleRoomUpdate(room, cause); if (shouldUpdate) { if (SettingsStore.getValue("advancedRoomListLogging")) { // TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602 @@ -462,13 +462,13 @@ export class RoomListStoreClass extends AsyncStoreWithClient { // Reset the sticky room before resetting the known rooms so the algorithm // doesn't freak out. - await this.algorithm.setStickyRoom(null); - await this.algorithm.setKnownRooms(rooms); + this.algorithm.setStickyRoom(null); + this.algorithm.setKnownRooms(rooms); // Set the sticky room back, if needed, now that we have updated the store. // This will use relative stickyness to the new room set. if (stickyIsStillPresent) { - await this.algorithm.setStickyRoom(currentSticky); + this.algorithm.setStickyRoom(currentSticky); } // Finally, mark an update and resume updates from the algorithm @@ -477,12 +477,12 @@ export class RoomListStoreClass extends AsyncStoreWithClient { } public async setTagSorting(tagId: TagID, sort: SortAlgorithm) { - await this.setAndPersistTagSorting(tagId, sort); + this.setAndPersistTagSorting(tagId, sort); this.updateFn.trigger(); } - private async setAndPersistTagSorting(tagId: TagID, sort: SortAlgorithm) { - await this.algorithm.setTagSorting(tagId, sort); + private setAndPersistTagSorting(tagId: TagID, sort: SortAlgorithm) { + this.algorithm.setTagSorting(tagId, sort); // TODO: Per-account? https://github.com/vector-im/element-web/issues/14114 localStorage.setItem(`mx_tagSort_${tagId}`, sort); } @@ -520,13 +520,13 @@ export class RoomListStoreClass extends AsyncStoreWithClient { return tagSort; } - public async setListOrder(tagId: TagID, order: ListAlgorithm) { - await this.setAndPersistListOrder(tagId, order); + public setListOrder(tagId: TagID, order: ListAlgorithm) { + this.setAndPersistListOrder(tagId, order); this.updateFn.trigger(); } - private async setAndPersistListOrder(tagId: TagID, order: ListAlgorithm) { - await this.algorithm.setListOrdering(tagId, order); + private setAndPersistListOrder(tagId: TagID, order: ListAlgorithm) { + this.algorithm.setListOrdering(tagId, order); // TODO: Per-account? https://github.com/vector-im/element-web/issues/14114 localStorage.setItem(`mx_listOrder_${tagId}`, order); } @@ -563,7 +563,7 @@ export class RoomListStoreClass extends AsyncStoreWithClient { return listOrder; } - private async updateAlgorithmInstances() { + private updateAlgorithmInstances() { // We'll require an update, so mark for one. Marking now also prevents the calls // to setTagSorting and setListOrder from causing triggers. this.updateFn.mark(); @@ -576,10 +576,10 @@ export class RoomListStoreClass extends AsyncStoreWithClient { const listOrder = this.calculateListOrder(tag); if (tagSort !== definedSort) { - await this.setAndPersistTagSorting(tag, tagSort); + this.setAndPersistTagSorting(tag, tagSort); } if (listOrder !== definedOrder) { - await this.setAndPersistListOrder(tag, listOrder); + this.setAndPersistListOrder(tag, listOrder); } } } @@ -632,7 +632,7 @@ export class RoomListStoreClass extends AsyncStoreWithClient { * @param trigger Set to false to prevent a list update from being sent. Should only * be used if the calling code will manually trigger the update. */ - public async regenerateAllLists({ trigger = true }) { + public regenerateAllLists({ trigger = true }) { console.warn("Regenerating all room lists"); const rooms = this.getPlausibleRooms(); @@ -656,8 +656,8 @@ export class RoomListStoreClass extends AsyncStoreWithClient { RoomListLayoutStore.instance.ensureLayoutExists(tagId); } - await this.algorithm.populateTags(sorts, orders); - await this.algorithm.setKnownRooms(rooms); + this.algorithm.populateTags(sorts, orders); + this.algorithm.setKnownRooms(rooms); this.initialListsGenerated = true; diff --git a/src/stores/room-list/algorithms/Algorithm.ts b/src/stores/room-list/algorithms/Algorithm.ts index 2acce1ecd7..8574f095d6 100644 --- a/src/stores/room-list/algorithms/Algorithm.ts +++ b/src/stores/room-list/algorithms/Algorithm.ts @@ -17,7 +17,6 @@ limitations under the License. import { Room } from "matrix-js-sdk/src/models/room"; import { isNullOrUndefined } from "matrix-js-sdk/src/utils"; import { EventEmitter } from "events"; -import AwaitLock from "await-lock"; import DMRoomMap from "../../../utils/DMRoomMap"; import { arrayDiff, arrayHasDiff } from "../../../utils/arrays"; @@ -80,7 +79,6 @@ export class Algorithm extends EventEmitter { } = {}; private allowedByFilter: Map = new Map(); private allowedRoomsByFilters: Set = new Set(); - private stickyLock = new AwaitLock(); /** * Set to true to suspend emissions of algorithm updates. @@ -125,13 +123,8 @@ export class Algorithm extends EventEmitter { * Awaitable version of the sticky room setter. * @param val The new room to sticky. */ - public async setStickyRoom(val: Room) { - await this.stickyLock.acquireAsync(); - try { - await this.updateStickyRoom(val); - } finally { - this.stickyLock.release(); - } + public setStickyRoom(val: Room) { + this.updateStickyRoom(val); } public getTagSorting(tagId: TagID): SortAlgorithm { @@ -139,13 +132,13 @@ export class Algorithm extends EventEmitter { return this.sortAlgorithms[tagId]; } - public async setTagSorting(tagId: TagID, sort: SortAlgorithm) { + public setTagSorting(tagId: TagID, sort: SortAlgorithm) { if (!tagId) throw new Error("Tag ID must be defined"); if (!sort) throw new Error("Algorithm must be defined"); this.sortAlgorithms[tagId] = sort; const algorithm: OrderingAlgorithm = this.algorithms[tagId]; - await algorithm.setSortAlgorithm(sort); + algorithm.setSortAlgorithm(sort); this._cachedRooms[tagId] = algorithm.orderedRooms; this.recalculateFilteredRoomsForTag(tagId); // update filter to re-sort the list this.recalculateStickyRoom(tagId); // update sticky room to make sure it appears if needed @@ -156,7 +149,7 @@ export class Algorithm extends EventEmitter { return this.listAlgorithms[tagId]; } - public async setListOrdering(tagId: TagID, order: ListAlgorithm) { + public setListOrdering(tagId: TagID, order: ListAlgorithm) { if (!tagId) throw new Error("Tag ID must be defined"); if (!order) throw new Error("Algorithm must be defined"); this.listAlgorithms[tagId] = order; @@ -164,7 +157,7 @@ export class Algorithm extends EventEmitter { const algorithm = getListAlgorithmInstance(order, tagId, this.sortAlgorithms[tagId]); this.algorithms[tagId] = algorithm; - await algorithm.setRooms(this._cachedRooms[tagId]); + algorithm.setRooms(this._cachedRooms[tagId]); this._cachedRooms[tagId] = algorithm.orderedRooms; this.recalculateFilteredRoomsForTag(tagId); // update filter to re-sort the list this.recalculateStickyRoom(tagId); // update sticky room to make sure it appears if needed @@ -191,31 +184,25 @@ export class Algorithm extends EventEmitter { } } - private async handleFilterChange() { - await this.recalculateFilteredRooms(); + private handleFilterChange() { + this.recalculateFilteredRooms(); // re-emit the update so the list store can fire an off-cycle update if needed if (this.updatesInhibited) return; this.emit(FILTER_CHANGED); } - private async updateStickyRoom(val: Room) { - try { - return await this.doUpdateStickyRoom(val); - } finally { - this._lastStickyRoom = null; // clear to indicate we're done changing - } + private updateStickyRoom(val: Room) { + this.doUpdateStickyRoom(val); + this._lastStickyRoom = null; // clear to indicate we're done changing } - private async doUpdateStickyRoom(val: Room) { + private doUpdateStickyRoom(val: Room) { if (SpaceStore.spacesEnabled && val?.isSpaceRoom() && val.getMyMembership() !== "invite") { // no-op sticky rooms for spaces - they're effectively virtual rooms val = null; } - // Note throughout: We need async so we can wait for handleRoomUpdate() to do its thing, - // otherwise we risk duplicating rooms. - if (val && !VisibilityProvider.instance.isRoomVisible(val)) { val = null; // the room isn't visible - lie to the rest of this function } @@ -231,7 +218,7 @@ export class Algorithm extends EventEmitter { this._stickyRoom = null; // clear before we go to update the algorithm // Lie to the algorithm and re-add the room to the algorithm - await this.handleRoomUpdate(stickyRoom, RoomUpdateCause.NewRoom); + this.handleRoomUpdate(stickyRoom, RoomUpdateCause.NewRoom); return; } return; @@ -277,10 +264,10 @@ export class Algorithm extends EventEmitter { // referential checks as the references can differ through the lifecycle. if (lastStickyRoom && lastStickyRoom.room && lastStickyRoom.room.roomId !== val.roomId) { // Lie to the algorithm and re-add the room to the algorithm - await this.handleRoomUpdate(lastStickyRoom.room, RoomUpdateCause.NewRoom); + this.handleRoomUpdate(lastStickyRoom.room, RoomUpdateCause.NewRoom); } // Lie to the algorithm and remove the room from it's field of view - await this.handleRoomUpdate(val, RoomUpdateCause.RoomRemoved); + this.handleRoomUpdate(val, RoomUpdateCause.RoomRemoved); // Check for tag & position changes while we're here. We also check the room to ensure // it is still the same room. @@ -470,9 +457,8 @@ export class Algorithm extends EventEmitter { * them. * @param {ITagSortingMap} tagSortingMap The tags to generate. * @param {IListOrderingMap} listOrderingMap The ordering of those tags. - * @returns {Promise<*>} A promise which resolves when complete. */ - public async populateTags(tagSortingMap: ITagSortingMap, listOrderingMap: IListOrderingMap): Promise { + public populateTags(tagSortingMap: ITagSortingMap, listOrderingMap: IListOrderingMap): void { if (!tagSortingMap) throw new Error(`Sorting map cannot be null or empty`); if (!listOrderingMap) throw new Error(`Ordering ma cannot be null or empty`); if (arrayHasDiff(Object.keys(tagSortingMap), Object.keys(listOrderingMap))) { @@ -521,93 +507,87 @@ export class Algorithm extends EventEmitter { * Seeds the Algorithm with a set of rooms. The algorithm will discard all * previously known information and instead use these rooms instead. * @param {Room[]} rooms The rooms to force the algorithm to use. - * @returns {Promise<*>} A promise which resolves when complete. */ - public async setKnownRooms(rooms: Room[]): Promise { + public setKnownRooms(rooms: Room[]): void { if (isNullOrUndefined(rooms)) throw new Error(`Array of rooms cannot be null`); if (!this.sortAlgorithms) throw new Error(`Cannot set known rooms without a tag sorting map`); - await this.stickyLock.acquireAsync(); - try { - if (!this.updatesInhibited) { - // We only log this if we're expecting to be publishing updates, which means that - // this could be an unexpected invocation. If we're inhibited, then this is probably - // an intentional invocation. - console.warn("Resetting known rooms, initiating regeneration"); - } + if (!this.updatesInhibited) { + // We only log this if we're expecting to be publishing updates, which means that + // this could be an unexpected invocation. If we're inhibited, then this is probably + // an intentional invocation. + console.warn("Resetting known rooms, initiating regeneration"); + } - // Before we go any further we need to clear (but remember) the sticky room to - // avoid accidentally duplicating it in the list. - const oldStickyRoom = this._stickyRoom; - if (oldStickyRoom) await this.updateStickyRoom(null); + // Before we go any further we need to clear (but remember) the sticky room to + // avoid accidentally duplicating it in the list. + const oldStickyRoom = this._stickyRoom; + if (oldStickyRoom) this.updateStickyRoom(null); - this.rooms = rooms; + this.rooms = rooms; - const newTags: ITagMap = {}; - for (const tagId in this.sortAlgorithms) { - // noinspection JSUnfilteredForInLoop - newTags[tagId] = []; - } + const newTags: ITagMap = {}; + for (const tagId in this.sortAlgorithms) { + // noinspection JSUnfilteredForInLoop + newTags[tagId] = []; + } - // If we can avoid doing work, do so. - if (!rooms.length) { - await this.generateFreshTags(newTags); // just in case it wants to do something - this.cachedRooms = newTags; - return; - } + // If we can avoid doing work, do so. + if (!rooms.length) { + this.generateFreshTags(newTags); // just in case it wants to do something + this.cachedRooms = newTags; + return; + } - // Split out the easy rooms first (leave and invite) - const memberships = splitRoomsByMembership(rooms); - for (const room of memberships[EffectiveMembership.Invite]) { - newTags[DefaultTagID.Invite].push(room); - } - for (const room of memberships[EffectiveMembership.Leave]) { - newTags[DefaultTagID.Archived].push(room); - } + // Split out the easy rooms first (leave and invite) + const memberships = splitRoomsByMembership(rooms); + for (const room of memberships[EffectiveMembership.Invite]) { + newTags[DefaultTagID.Invite].push(room); + } + for (const room of memberships[EffectiveMembership.Leave]) { + newTags[DefaultTagID.Archived].push(room); + } - // Now process all the joined rooms. This is a bit more complicated - for (const room of memberships[EffectiveMembership.Join]) { - const tags = this.getTagsOfJoinedRoom(room); + // Now process all the joined rooms. This is a bit more complicated + for (const room of memberships[EffectiveMembership.Join]) { + const tags = this.getTagsOfJoinedRoom(room); - let inTag = false; - if (tags.length > 0) { - for (const tag of tags) { - if (!isNullOrUndefined(newTags[tag])) { - newTags[tag].push(room); - inTag = true; - } - } - } - - if (!inTag) { - if (DMRoomMap.shared().getUserIdForRoomId(room.roomId)) { - newTags[DefaultTagID.DM].push(room); - } else { - newTags[DefaultTagID.Untagged].push(room); + let inTag = false; + if (tags.length > 0) { + for (const tag of tags) { + if (!isNullOrUndefined(newTags[tag])) { + newTags[tag].push(room); + inTag = true; } } } - await this.generateFreshTags(newTags); - - this.cachedRooms = newTags; // this recalculates the filtered rooms for us - this.updateTagsFromCache(); - - // Now that we've finished generation, we need to update the sticky room to what - // it was. It's entirely possible that it changed lists though, so if it did then - // we also have to update the position of it. - if (oldStickyRoom && oldStickyRoom.room) { - await this.updateStickyRoom(oldStickyRoom.room); - if (this._stickyRoom && this._stickyRoom.room) { // just in case the update doesn't go according to plan - if (this._stickyRoom.tag !== oldStickyRoom.tag) { - // We put the sticky room at the top of the list to treat it as an obvious tag change. - this._stickyRoom.position = 0; - this.recalculateStickyRoom(this._stickyRoom.tag); - } + if (!inTag) { + if (DMRoomMap.shared().getUserIdForRoomId(room.roomId)) { + newTags[DefaultTagID.DM].push(room); + } else { + newTags[DefaultTagID.Untagged].push(room); + } + } + } + + this.generateFreshTags(newTags); + + this.cachedRooms = newTags; // this recalculates the filtered rooms for us + this.updateTagsFromCache(); + + // Now that we've finished generation, we need to update the sticky room to what + // it was. It's entirely possible that it changed lists though, so if it did then + // we also have to update the position of it. + if (oldStickyRoom && oldStickyRoom.room) { + this.updateStickyRoom(oldStickyRoom.room); + if (this._stickyRoom && this._stickyRoom.room) { // just in case the update doesn't go according to plan + if (this._stickyRoom.tag !== oldStickyRoom.tag) { + // We put the sticky room at the top of the list to treat it as an obvious tag change. + this._stickyRoom.position = 0; + this.recalculateStickyRoom(this._stickyRoom.tag); } } - } finally { - this.stickyLock.release(); } } @@ -665,16 +645,15 @@ export class Algorithm extends EventEmitter { * @param {ITagMap} updatedTagMap The tag map which needs populating. Each tag * will already have the rooms which belong to it - they just need ordering. Must * be mutated in place. - * @returns {Promise<*>} A promise which resolves when complete. */ - private async generateFreshTags(updatedTagMap: ITagMap): Promise { + private generateFreshTags(updatedTagMap: ITagMap): void { if (!this.algorithms) throw new Error("Not ready: no algorithms to determine tags from"); for (const tag of Object.keys(updatedTagMap)) { const algorithm: OrderingAlgorithm = this.algorithms[tag]; if (!algorithm) throw new Error(`No algorithm for ${tag}`); - await algorithm.setRooms(updatedTagMap[tag]); + algorithm.setRooms(updatedTagMap[tag]); updatedTagMap[tag] = algorithm.orderedRooms; } } @@ -686,11 +665,10 @@ export class Algorithm extends EventEmitter { * may no-op this request if no changes are required. * @param {Room} room The room which might have affected sorting. * @param {RoomUpdateCause} cause The reason for the update being triggered. - * @returns {Promise} A promise which resolve to true or false - * depending on whether or not getOrderedRooms() should be called after - * processing. + * @returns {Promise} A boolean of whether or not getOrderedRooms() + * should be called after processing. */ - public async handleRoomUpdate(room: Room, cause: RoomUpdateCause): Promise { + public handleRoomUpdate(room: Room, cause: RoomUpdateCause): boolean { if (SettingsStore.getValue("advancedRoomListLogging")) { // TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602 console.log(`Handle room update for ${room.roomId} called with cause ${cause}`); @@ -757,7 +735,7 @@ export class Algorithm extends EventEmitter { } const algorithm: OrderingAlgorithm = this.algorithms[rmTag]; if (!algorithm) throw new Error(`No algorithm for ${rmTag}`); - await algorithm.handleRoomUpdate(room, RoomUpdateCause.RoomRemoved); + algorithm.handleRoomUpdate(room, RoomUpdateCause.RoomRemoved); this._cachedRooms[rmTag] = algorithm.orderedRooms; this.recalculateFilteredRoomsForTag(rmTag); // update filter to re-sort the list this.recalculateStickyRoom(rmTag); // update sticky room to make sure it moves if needed @@ -769,7 +747,7 @@ export class Algorithm extends EventEmitter { } const algorithm: OrderingAlgorithm = this.algorithms[addTag]; if (!algorithm) throw new Error(`No algorithm for ${addTag}`); - await algorithm.handleRoomUpdate(room, RoomUpdateCause.NewRoom); + algorithm.handleRoomUpdate(room, RoomUpdateCause.NewRoom); this._cachedRooms[addTag] = algorithm.orderedRooms; } @@ -802,7 +780,7 @@ export class Algorithm extends EventEmitter { }; } else { // We have to clear the lock as the sticky room change will trigger updates. - await this.setStickyRoom(room); + this.setStickyRoom(room); } } } @@ -865,7 +843,7 @@ export class Algorithm extends EventEmitter { const algorithm: OrderingAlgorithm = this.algorithms[tag]; if (!algorithm) throw new Error(`No algorithm for ${tag}`); - await algorithm.handleRoomUpdate(room, cause); + algorithm.handleRoomUpdate(room, cause); this._cachedRooms[tag] = algorithm.orderedRooms; // Flag that we've done something diff --git a/src/stores/room-list/algorithms/list-ordering/ImportanceAlgorithm.ts b/src/stores/room-list/algorithms/list-ordering/ImportanceAlgorithm.ts index 80bdf74afb..1d35df331d 100644 --- a/src/stores/room-list/algorithms/list-ordering/ImportanceAlgorithm.ts +++ b/src/stores/room-list/algorithms/list-ordering/ImportanceAlgorithm.ts @@ -94,15 +94,15 @@ export class ImportanceAlgorithm extends OrderingAlgorithm { return state.color; } - public async setRooms(rooms: Room[]): Promise { + public setRooms(rooms: Room[]): void { if (this.sortingAlgorithm === SortAlgorithm.Manual) { - this.cachedOrderedRooms = await sortRoomsWithAlgorithm(rooms, this.tagId, this.sortingAlgorithm); + this.cachedOrderedRooms = sortRoomsWithAlgorithm(rooms, this.tagId, this.sortingAlgorithm); } else { // Every other sorting type affects the categories, not the whole tag. const categorized = this.categorizeRooms(rooms); for (const category of Object.keys(categorized)) { const roomsToOrder = categorized[category]; - categorized[category] = await sortRoomsWithAlgorithm(roomsToOrder, this.tagId, this.sortingAlgorithm); + categorized[category] = sortRoomsWithAlgorithm(roomsToOrder, this.tagId, this.sortingAlgorithm); } const newlyOrganized: Room[] = []; @@ -118,12 +118,12 @@ export class ImportanceAlgorithm extends OrderingAlgorithm { } } - private async handleSplice(room: Room, cause: RoomUpdateCause): Promise { + private handleSplice(room: Room, cause: RoomUpdateCause): boolean { if (cause === RoomUpdateCause.NewRoom) { const category = this.getRoomCategory(room); this.alterCategoryPositionBy(category, 1, this.indices); this.cachedOrderedRooms.splice(this.indices[category], 0, room); // splice in the new room (pre-adjusted) - await this.sortCategory(category); + this.sortCategory(category); } else if (cause === RoomUpdateCause.RoomRemoved) { const roomIdx = this.getRoomIndex(room); if (roomIdx === -1) { @@ -141,55 +141,49 @@ export class ImportanceAlgorithm extends OrderingAlgorithm { return true; } - public async handleRoomUpdate(room: Room, cause: RoomUpdateCause): Promise { - try { - await this.updateLock.acquireAsync(); - - if (cause === RoomUpdateCause.NewRoom || cause === RoomUpdateCause.RoomRemoved) { - return this.handleSplice(room, cause); - } - - if (cause !== RoomUpdateCause.Timeline && cause !== RoomUpdateCause.ReadReceipt) { - throw new Error(`Unsupported update cause: ${cause}`); - } - - const category = this.getRoomCategory(room); - if (this.sortingAlgorithm === SortAlgorithm.Manual) { - return; // Nothing to do here. - } - - const roomIdx = this.getRoomIndex(room); - if (roomIdx === -1) { - throw new Error(`Room ${room.roomId} has no index in ${this.tagId}`); - } - - // Try to avoid doing array operations if we don't have to: only move rooms within - // the categories if we're jumping categories - const oldCategory = this.getCategoryFromIndices(roomIdx, this.indices); - if (oldCategory !== category) { - // Move the room and update the indices - this.moveRoomIndexes(1, oldCategory, category, this.indices); - this.cachedOrderedRooms.splice(roomIdx, 1); // splice out the old index (fixed position) - this.cachedOrderedRooms.splice(this.indices[category], 0, room); // splice in the new room (pre-adjusted) - // Note: if moveRoomIndexes() is called after the splice then the insert operation - // will happen in the wrong place. Because we would have already adjusted the index - // for the category, we don't need to determine how the room is moving in the list. - // If we instead tried to insert before updating the indices, we'd have to determine - // whether the room was moving later (towards IDLE) or earlier (towards RED) from its - // current position, as it'll affect the category's start index after we remove the - // room from the array. - } - - // Sort the category now that we've dumped the room in - await this.sortCategory(category); - - return true; // change made - } finally { - await this.updateLock.release(); + public handleRoomUpdate(room: Room, cause: RoomUpdateCause): boolean { + if (cause === RoomUpdateCause.NewRoom || cause === RoomUpdateCause.RoomRemoved) { + return this.handleSplice(room, cause); } + + if (cause !== RoomUpdateCause.Timeline && cause !== RoomUpdateCause.ReadReceipt) { + throw new Error(`Unsupported update cause: ${cause}`); + } + + const category = this.getRoomCategory(room); + if (this.sortingAlgorithm === SortAlgorithm.Manual) { + return; // Nothing to do here. + } + + const roomIdx = this.getRoomIndex(room); + if (roomIdx === -1) { + throw new Error(`Room ${room.roomId} has no index in ${this.tagId}`); + } + + // Try to avoid doing array operations if we don't have to: only move rooms within + // the categories if we're jumping categories + const oldCategory = this.getCategoryFromIndices(roomIdx, this.indices); + if (oldCategory !== category) { + // Move the room and update the indices + this.moveRoomIndexes(1, oldCategory, category, this.indices); + this.cachedOrderedRooms.splice(roomIdx, 1); // splice out the old index (fixed position) + this.cachedOrderedRooms.splice(this.indices[category], 0, room); // splice in the new room (pre-adjusted) + // Note: if moveRoomIndexes() is called after the splice then the insert operation + // will happen in the wrong place. Because we would have already adjusted the index + // for the category, we don't need to determine how the room is moving in the list. + // If we instead tried to insert before updating the indices, we'd have to determine + // whether the room was moving later (towards IDLE) or earlier (towards RED) from its + // current position, as it'll affect the category's start index after we remove the + // room from the array. + } + + // Sort the category now that we've dumped the room in + this.sortCategory(category); + + return true; // change made } - private async sortCategory(category: NotificationColor) { + private sortCategory(category: NotificationColor) { // This should be relatively quick because the room is usually inserted at the top of the // category, and most popular sorting algorithms will deal with trying to keep the active // room at the top/start of the category. For the few algorithms that will have to move the @@ -201,7 +195,7 @@ export class ImportanceAlgorithm extends OrderingAlgorithm { const startIdx = this.indices[category]; const numSort = nextCategoryStartIdx - startIdx; // splice() returns up to the max, so MAX_SAFE_INT is fine const unsortedSlice = this.cachedOrderedRooms.splice(startIdx, numSort); - const sorted = await sortRoomsWithAlgorithm(unsortedSlice, this.tagId, this.sortingAlgorithm); + const sorted = sortRoomsWithAlgorithm(unsortedSlice, this.tagId, this.sortingAlgorithm); this.cachedOrderedRooms.splice(startIdx, 0, ...sorted); } diff --git a/src/stores/room-list/algorithms/list-ordering/NaturalAlgorithm.ts b/src/stores/room-list/algorithms/list-ordering/NaturalAlgorithm.ts index cc2a28d892..91182dee16 100644 --- a/src/stores/room-list/algorithms/list-ordering/NaturalAlgorithm.ts +++ b/src/stores/room-list/algorithms/list-ordering/NaturalAlgorithm.ts @@ -29,42 +29,32 @@ export class NaturalAlgorithm extends OrderingAlgorithm { super(tagId, initialSortingAlgorithm); } - public async setRooms(rooms: Room[]): Promise { - this.cachedOrderedRooms = await sortRoomsWithAlgorithm(rooms, this.tagId, this.sortingAlgorithm); + public setRooms(rooms: Room[]): void { + this.cachedOrderedRooms = sortRoomsWithAlgorithm(rooms, this.tagId, this.sortingAlgorithm); } - public async handleRoomUpdate(room, cause): Promise { - try { - await this.updateLock.acquireAsync(); - - const isSplice = cause === RoomUpdateCause.NewRoom || cause === RoomUpdateCause.RoomRemoved; - const isInPlace = cause === RoomUpdateCause.Timeline || cause === RoomUpdateCause.ReadReceipt; - if (!isSplice && !isInPlace) { - throw new Error(`Unsupported update cause: ${cause}`); - } - - if (cause === RoomUpdateCause.NewRoom) { - this.cachedOrderedRooms.push(room); - } else if (cause === RoomUpdateCause.RoomRemoved) { - const idx = this.getRoomIndex(room); - if (idx >= 0) { - this.cachedOrderedRooms.splice(idx, 1); - } else { - console.warn(`Tried to remove unknown room from ${this.tagId}: ${room.roomId}`); - } - } - - // TODO: Optimize this to avoid useless operations: https://github.com/vector-im/element-web/issues/14457 - // For example, we can skip updates to alphabetic (sometimes) and manually ordered tags - this.cachedOrderedRooms = await sortRoomsWithAlgorithm( - this.cachedOrderedRooms, - this.tagId, - this.sortingAlgorithm, - ); - - return true; - } finally { - await this.updateLock.release(); + public handleRoomUpdate(room, cause): boolean { + const isSplice = cause === RoomUpdateCause.NewRoom || cause === RoomUpdateCause.RoomRemoved; + const isInPlace = cause === RoomUpdateCause.Timeline || cause === RoomUpdateCause.ReadReceipt; + if (!isSplice && !isInPlace) { + throw new Error(`Unsupported update cause: ${cause}`); } + + if (cause === RoomUpdateCause.NewRoom) { + this.cachedOrderedRooms.push(room); + } else if (cause === RoomUpdateCause.RoomRemoved) { + const idx = this.getRoomIndex(room); + if (idx >= 0) { + this.cachedOrderedRooms.splice(idx, 1); + } else { + console.warn(`Tried to remove unknown room from ${this.tagId}: ${room.roomId}`); + } + } + + // TODO: Optimize this to avoid useless operations: https://github.com/vector-im/element-web/issues/14457 + // For example, we can skip updates to alphabetic (sometimes) and manually ordered tags + this.cachedOrderedRooms = sortRoomsWithAlgorithm(this.cachedOrderedRooms, this.tagId, this.sortingAlgorithm); + + return true; } } diff --git a/src/stores/room-list/algorithms/list-ordering/OrderingAlgorithm.ts b/src/stores/room-list/algorithms/list-ordering/OrderingAlgorithm.ts index c47a35523c..23a8e33a41 100644 --- a/src/stores/room-list/algorithms/list-ordering/OrderingAlgorithm.ts +++ b/src/stores/room-list/algorithms/list-ordering/OrderingAlgorithm.ts @@ -26,7 +26,6 @@ import AwaitLock from "await-lock"; export abstract class OrderingAlgorithm { protected cachedOrderedRooms: Room[]; protected sortingAlgorithm: SortAlgorithm; - protected readonly updateLock = new AwaitLock(); protected constructor(protected tagId: TagID, initialSortingAlgorithm: SortAlgorithm) { // noinspection JSIgnoredPromiseFromCall @@ -45,21 +44,20 @@ export abstract class OrderingAlgorithm { * @param newAlgorithm The new algorithm. Must be defined. * @returns Resolves when complete. */ - public async setSortAlgorithm(newAlgorithm: SortAlgorithm) { + public setSortAlgorithm(newAlgorithm: SortAlgorithm) { if (!newAlgorithm) throw new Error("A sorting algorithm must be defined"); this.sortingAlgorithm = newAlgorithm; // Force regeneration of the rooms - await this.setRooms(this.orderedRooms); + this.setRooms(this.orderedRooms); } /** * Sets the rooms the algorithm should be handling, implying a reconstruction * of the ordering. * @param rooms The rooms to use going forward. - * @returns Resolves when complete. */ - public abstract setRooms(rooms: Room[]): Promise; + public abstract setRooms(rooms: Room[]): void; /** * Handle a room update. The Algorithm will only call this for causes which @@ -69,7 +67,7 @@ export abstract class OrderingAlgorithm { * @param cause The cause of the update. * @returns True if the update requires the Algorithm to update the presentation layers. */ - public abstract handleRoomUpdate(room: Room, cause: RoomUpdateCause): Promise; + public abstract handleRoomUpdate(room: Room, cause: RoomUpdateCause): boolean; protected getRoomIndex(room: Room): number { let roomIdx = this.cachedOrderedRooms.indexOf(room); diff --git a/src/stores/room-list/algorithms/tag-sorting/AlphabeticAlgorithm.ts b/src/stores/room-list/algorithms/tag-sorting/AlphabeticAlgorithm.ts index b016a4256c..45f6eaf843 100644 --- a/src/stores/room-list/algorithms/tag-sorting/AlphabeticAlgorithm.ts +++ b/src/stores/room-list/algorithms/tag-sorting/AlphabeticAlgorithm.ts @@ -23,7 +23,7 @@ import { compare } from "../../../../utils/strings"; * Sorts rooms according to the browser's determination of alphabetic. */ export class AlphabeticAlgorithm implements IAlgorithm { - public async sortRooms(rooms: Room[], tagId: TagID): Promise { + public sortRooms(rooms: Room[], tagId: TagID): Room[] { return rooms.sort((a, b) => { return compare(a.name, b.name); }); diff --git a/src/stores/room-list/algorithms/tag-sorting/IAlgorithm.ts b/src/stores/room-list/algorithms/tag-sorting/IAlgorithm.ts index 6c22ee0c9c..588bbbffc9 100644 --- a/src/stores/room-list/algorithms/tag-sorting/IAlgorithm.ts +++ b/src/stores/room-list/algorithms/tag-sorting/IAlgorithm.ts @@ -25,7 +25,7 @@ export interface IAlgorithm { * Sorts the given rooms according to the sorting rules of the algorithm. * @param {Room[]} rooms The rooms to sort. * @param {TagID} tagId The tag ID in which the rooms are being sorted. - * @returns {Promise} Resolves to the sorted rooms. + * @returns {Room[]} Returns the sorted rooms. */ - sortRooms(rooms: Room[], tagId: TagID): Promise; + sortRooms(rooms: Room[], tagId: TagID): Room[]; } diff --git a/src/stores/room-list/algorithms/tag-sorting/ManualAlgorithm.ts b/src/stores/room-list/algorithms/tag-sorting/ManualAlgorithm.ts index b8c0357633..9be8ba5262 100644 --- a/src/stores/room-list/algorithms/tag-sorting/ManualAlgorithm.ts +++ b/src/stores/room-list/algorithms/tag-sorting/ManualAlgorithm.ts @@ -22,7 +22,7 @@ import { IAlgorithm } from "./IAlgorithm"; * Sorts rooms according to the tag's `order` property on the room. */ export class ManualAlgorithm implements IAlgorithm { - public async sortRooms(rooms: Room[], tagId: TagID): Promise { + public sortRooms(rooms: Room[], tagId: TagID): Room[] { const getOrderProp = (r: Room) => r.tags[tagId].order || 0; return rooms.sort((a, b) => { return getOrderProp(a) - getOrderProp(b); diff --git a/src/stores/room-list/algorithms/tag-sorting/RecentAlgorithm.ts b/src/stores/room-list/algorithms/tag-sorting/RecentAlgorithm.ts index 49cfd9e520..f47458d1b1 100644 --- a/src/stores/room-list/algorithms/tag-sorting/RecentAlgorithm.ts +++ b/src/stores/room-list/algorithms/tag-sorting/RecentAlgorithm.ts @@ -97,7 +97,7 @@ export const sortRooms = (rooms: Room[]): Room[] => { * useful to the user. */ export class RecentAlgorithm implements IAlgorithm { - public async sortRooms(rooms: Room[], tagId: TagID): Promise { + public sortRooms(rooms: Room[], tagId: TagID): Room[] { return sortRooms(rooms); } } diff --git a/src/stores/room-list/algorithms/tag-sorting/index.ts b/src/stores/room-list/algorithms/tag-sorting/index.ts index c22865f5ba..368c76f111 100644 --- a/src/stores/room-list/algorithms/tag-sorting/index.ts +++ b/src/stores/room-list/algorithms/tag-sorting/index.ts @@ -46,8 +46,8 @@ export function getSortingAlgorithmInstance(algorithm: SortAlgorithm): IAlgorith * @param {Room[]} rooms The rooms to sort. * @param {TagID} tagId The tag in which the sorting is occurring. * @param {SortAlgorithm} algorithm The algorithm to use for sorting. - * @returns {Promise} Resolves to the sorted rooms. + * @returns {Room[]} Returns the sorted rooms. */ -export function sortRoomsWithAlgorithm(rooms: Room[], tagId: TagID, algorithm: SortAlgorithm): Promise { +export function sortRoomsWithAlgorithm(rooms: Room[], tagId: TagID, algorithm: SortAlgorithm): Room[] { return getSortingAlgorithmInstance(algorithm).sortRooms(rooms, tagId); } From d9b3c4d19ccf002968a1d242f7f88c462178b66f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Fri, 16 Jul 2021 10:22:02 +0200 Subject: [PATCH 0370/1171] 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 | 1 + 1 file changed, 1 insertion(+) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 5cc900a21b..cad6e438d8 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -823,6 +823,7 @@ "Offline encrypted messaging using dehydrated devices": "Offline encrypted messaging using dehydrated devices", "Enable advanced debugging for the room list": "Enable advanced debugging for the room list", "Show info about bridges in room settings": "Show info about bridges in room settings", + "Send read receipts for messages (requires compatible homeserver to disable)": "Send read receipts for messages (requires compatible homeserver to disable)", "Font size": "Font size", "Use custom size": "Use custom size", "Enable Emoji suggestions while typing": "Enable Emoji suggestions while typing", From 329f1c9d6a34167d97061cdc16e4cdfdbff84517 Mon Sep 17 00:00:00 2001 From: James Salter Date: Fri, 16 Jul 2021 09:22:17 +0100 Subject: [PATCH 0371/1171] Update src/components/views/rooms/RoomTile.tsx Co-authored-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/rooms/RoomTile.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/RoomTile.tsx b/src/components/views/rooms/RoomTile.tsx index aade665b6b..b1c9ed4d98 100644 --- a/src/components/views/rooms/RoomTile.tsx +++ b/src/components/views/rooms/RoomTile.tsx @@ -537,7 +537,7 @@ export default class RoomTile extends React.PureComponent { onClick={this.onOpenRoomSettings} label={_t("Settings")} iconClassName="mx_RoomTile_iconSettings" - /> + /> Date: Fri, 16 Jul 2021 09:22:25 +0100 Subject: [PATCH 0372/1171] remove unused import --- .../room-list/algorithms/list-ordering/OrderingAlgorithm.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/stores/room-list/algorithms/list-ordering/OrderingAlgorithm.ts b/src/stores/room-list/algorithms/list-ordering/OrderingAlgorithm.ts index 23a8e33a41..9d7b5f9ddb 100644 --- a/src/stores/room-list/algorithms/list-ordering/OrderingAlgorithm.ts +++ b/src/stores/room-list/algorithms/list-ordering/OrderingAlgorithm.ts @@ -17,7 +17,6 @@ limitations under the License. import { Room } from "matrix-js-sdk/src/models/room"; import { RoomUpdateCause, TagID } from "../../models"; import { SortAlgorithm } from "../models"; -import AwaitLock from "await-lock"; /** * Represents a list ordering algorithm. Subclasses should populate the From 685b59235dfbc99109bc8d8f153b96750ad2523e Mon Sep 17 00:00:00 2001 From: James Salter Date: Fri, 16 Jul 2021 09:24:14 +0100 Subject: [PATCH 0373/1171] Make error message consistent with menu title --- src/components/structures/MatrixChat.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index cadf66d11e..c6ca965934 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -1202,9 +1202,9 @@ export default class MatrixChat extends React.PureComponent { const roomLink = makeRoomPermalink(roomId); const success = await copyPlaintext(roomLink); if (!success) { - Modal.createTrackedDialog("Unable to copy room", "", ErrorDialog, { - title: _t("Unable to copy room"), - description: _t("Unable to copy the room to the clipboard."), + Modal.createTrackedDialog("Unable to copy room link", "", ErrorDialog, { + title: _t("Unable to copy room link"), + description: _t("Unable to copy a link to the room to the clipboard."), }); } } From 767d97065d1367bd96c379b001e0d4a6547d0c38 Mon Sep 17 00:00:00 2001 From: James Salter Date: Fri, 16 Jul 2021 09:36:59 +0100 Subject: [PATCH 0374/1171] Run i18n --- src/i18n/strings/en_EN.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index edbb7719eb..abdb8c2fb2 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2675,8 +2675,8 @@ "Are you sure you want to leave the space '%(spaceName)s'?": "Are you sure you want to leave the space '%(spaceName)s'?", "Are you sure you want to leave the room '%(roomName)s'?": "Are you sure you want to leave the room '%(roomName)s'?", "Failed to forget room %(errCode)s": "Failed to forget room %(errCode)s", - "Unable to copy room": "Unable to copy room", - "Unable to copy the room to the clipboard.": "Unable to copy the room to the clipboard.", + "Unable to copy room link": "Unable to copy room link", + "Unable to copy a link to the room to the clipboard.": "Unable to copy a link to the room to the clipboard.", "Signed Out": "Signed Out", "For security, this session has been signed out. Please sign in again.": "For security, this session has been signed out. Please sign in again.", "Terms and Conditions": "Terms and Conditions", From e6007874d9c68315cc4b709d4d506ff0cb10ac4c Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 16 Jul 2021 11:43:06 +0100 Subject: [PATCH 0375/1171] post-merge fixup --- src/components/views/rooms/SendMessageComposer.tsx | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/components/views/rooms/SendMessageComposer.tsx b/src/components/views/rooms/SendMessageComposer.tsx index 0639c20fef..04f74fb2b2 100644 --- a/src/components/views/rooms/SendMessageComposer.tsx +++ b/src/components/views/rooms/SendMessageComposer.tsx @@ -514,13 +514,11 @@ export default class SendMessageComposer extends React.Component { private onPaste = (event: ClipboardEvent): boolean => { const { clipboardData } = event; - // Prioritize text on the clipboard over files as Office on macOS puts a bitmap - // in the clipboard as well as the content being copied. - if (clipboardData.files.length && !clipboardData.types.some(t => t === "text/plain")) { - // This actually not so much for 'files' as such (at time of writing - // neither chrome nor firefox let you paste a plain file copied - // from Finder) but more images copied from a different website - // / word processor etc. + // Prioritize text on the clipboard over files if RTF is present as Office on macOS puts a bitmap + // in the clipboard as well as the content being copied. Modern versions of Office seem to not do this anymore. + // We check text/rtf instead of text/plain as when copy+pasting a file from Finder or Gnome Image Viewer + // it puts the filename in as text/plain which we want to ignore. + if (clipboardData.files.length && !clipboardData.types.includes("text/rtf")) { ContentMessages.sharedInstance().sendContentListToRoom( Array.from(clipboardData.files), this.props.room.roomId, this.context, ); From 9d45a3760fd38928543f20343d4f27a48c7dbb42 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 16 Jul 2021 13:11:43 +0100 Subject: [PATCH 0376/1171] Fix types of the various query params dicts, arrays can be included e.g via --- src/Lifecycle.ts | 11 ++++++----- src/components/structures/MatrixChat.tsx | 16 ++++++++-------- src/components/views/elements/AppTile.js | 1 - 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/Lifecycle.ts b/src/Lifecycle.ts index 61ded93833..410124a637 100644 --- a/src/Lifecycle.ts +++ b/src/Lifecycle.ts @@ -21,6 +21,7 @@ import { createClient } from 'matrix-js-sdk/src/matrix'; import { InvalidStoreError } from "matrix-js-sdk/src/errors"; import { MatrixClient } from "matrix-js-sdk/src/client"; import { decryptAES, encryptAES, IEncryptedPayload } from "matrix-js-sdk/src/crypto/aes"; +import { QueryDict } from 'matrix-js-sdk/src/utils'; import { IMatrixClientCreds, MatrixClientPeg } from './MatrixClientPeg'; import SecurityCustomisations from "./customisations/Security"; @@ -65,7 +66,7 @@ interface ILoadSessionOpts { guestIsUrl?: string; ignoreGuest?: boolean; defaultDeviceDisplayName?: string; - fragmentQueryParams?: Record; + fragmentQueryParams?: QueryDict; } /** @@ -118,8 +119,8 @@ export async function loadSession(opts: ILoadSessionOpts = {}): Promise ) { console.log("Using guest access credentials"); return doSetLoggedIn({ - userId: fragmentQueryParams.guest_user_id, - accessToken: fragmentQueryParams.guest_access_token, + userId: fragmentQueryParams.guest_user_id as string, + accessToken: fragmentQueryParams.guest_access_token as string, homeserverUrl: guestHsUrl, identityServerUrl: guestIsUrl, guest: true, @@ -173,7 +174,7 @@ export async function getStoredSessionOwner(): Promise<[string, boolean]> { * login, else false */ export function attemptTokenLogin( - queryParams: Record, + queryParams: QueryDict, defaultDeviceDisplayName?: string, fragmentAfterLogin?: string, ): Promise { @@ -198,7 +199,7 @@ export function attemptTokenLogin( homeserver, identityServer, "m.login.token", { - token: queryParams.loginToken, + token: queryParams.loginToken as string, initial_device_display_name: defaultDeviceDisplayName, }, ).then(function(creds) { diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index 15536f260d..785838ffca 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -19,7 +19,7 @@ import { createClient } from "matrix-js-sdk/src/matrix"; import { InvalidStoreError } from "matrix-js-sdk/src/errors"; import { RoomMember } from "matrix-js-sdk/src/models/room-member"; import { MatrixEvent } from "matrix-js-sdk/src/models/event"; -import { sleep, defer, IDeferred } from "matrix-js-sdk/src/utils"; +import { sleep, defer, IDeferred, QueryDict } from "matrix-js-sdk/src/utils"; // focus-visible is a Polyfill for the :focus-visible CSS pseudo-attribute used by _AccessibleButton.scss import 'focus-visible'; @@ -155,7 +155,7 @@ const ONBOARDING_FLOW_STARTERS = [ interface IScreen { screen: string; - params?: object; + params?: QueryDict; } /* eslint-disable camelcase */ @@ -185,9 +185,9 @@ interface IProps { // TODO type things better onNewScreen: (screen: string, replaceLast: boolean) => void; enableGuest?: boolean; // the queryParams extracted from the [real] query-string of the URI - realQueryParams?: Record; + realQueryParams?: QueryDict; // the initial queryParams extracted from the hash-fragment of the URI - startingFragmentQueryParams?: Record; + startingFragmentQueryParams?: QueryDict; // called when we have completed a token login onTokenLoginCompleted?: () => void; // Represents the screen to display as a result of parsing the initial window.location @@ -195,7 +195,7 @@ interface IProps { // TODO type things better // displayname, if any, to set on the device when logging in/registering. defaultDeviceDisplayName?: string; // A function that makes a registration URL - makeRegistrationUrl: (object) => string; + makeRegistrationUrl: (params: QueryDict) => string; } interface IState { @@ -298,7 +298,7 @@ export default class MatrixChat extends React.PureComponent { if (this.screenAfterLogin.screen.startsWith("room/") && params['signurl'] && params['email']) { // probably a threepid invite - try to store it const roomId = this.screenAfterLogin.screen.substring("room/".length); - ThreepidInviteStore.instance.storeInvite(roomId, params as IThreepidInviteWireFormat); + ThreepidInviteStore.instance.storeInvite(roomId, params as unknown as IThreepidInviteWireFormat); } } @@ -1952,7 +1952,7 @@ export default class MatrixChat extends React.PureComponent { this.setState({ serverConfig }); }; - private makeRegistrationUrl = (params: {[key: string]: string}) => { + private makeRegistrationUrl = (params: QueryDict) => { if (this.props.startingFragmentQueryParams.referrer) { params.referrer = this.props.startingFragmentQueryParams.referrer; } @@ -2107,7 +2107,7 @@ export default class MatrixChat extends React.PureComponent { onForgotPasswordClick={showPasswordReset ? this.onForgotPasswordClick : undefined} onServerConfigChange={this.onServerConfigChange} fragmentAfterLogin={fragmentAfterLogin} - defaultUsername={this.props.startingFragmentQueryParams.defaultUsername} + defaultUsername={this.props.startingFragmentQueryParams.defaultUsername as string} {...this.getServerProperties()} /> ); diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js index 1ddca61c22..7e98537180 100644 --- a/src/components/views/elements/AppTile.js +++ b/src/components/views/elements/AppTile.js @@ -39,7 +39,6 @@ import { MatrixCapabilities } from "matrix-widget-api"; import RoomWidgetContextMenu from "../context_menus/WidgetContextMenu"; import WidgetAvatar from "../avatars/WidgetAvatar"; import { replaceableComponent } from "../../../utils/replaceableComponent"; -import { urlSearchParamsToObject } from "../../../utils/UrlUtils"; @replaceableComponent("views.elements.AppTile") export default class AppTile extends React.Component { From dfbe6bcda6f7bbe3e19f0bcd6ddcfade21e29730 Mon Sep 17 00:00:00 2001 From: libexus Date: Wed, 14 Jul 2021 18:16:45 +0000 Subject: [PATCH 0377/1171] Translated using Weblate (German) Currently translated at 99.4% (3029 of 3045 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 | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json index 1def5b300e..af7dade32f 100644 --- a/src/i18n/strings/de_DE.json +++ b/src/i18n/strings/de_DE.json @@ -3442,7 +3442,7 @@ "%(senderName)s removed their profile picture": "%(senderName)s hat das Profilbild entfernt", "%(senderName)s removed their display name (%(oldDisplayName)s)": "%(senderName)s hat den alten Nicknamen %(oldDisplayName)s entfernt", "%(senderName)s set their display name to %(displayName)s": "%(senderName)s hat den Nicknamen zu %(displayName)s geändert", - "%(oldDisplayName)s changed their display name to %(displayName)s": "%(oldDisplayName)s hat den Nicknamen zu%(displayName)s geändert", + "%(oldDisplayName)s changed their display name to %(displayName)s": "%(oldDisplayName)s hat den Nicknamen zu %(displayName)s geändert", "%(senderName)s banned %(targetName)s": "%(senderName)s hat %(targetName)s gebannt", "%(senderName)s banned %(targetName)s: %(reason)s": "%(senderName)s hat %(targetName)s gebannt: %(reason)s", "%(targetName)s accepted an invitation": "%(targetName)s hat die Einladung akzeptiert", @@ -3452,5 +3452,12 @@ "Message search initialisation failed, check your settings for more information": "Initialisierung der Nachrichtensuche fehlgeschlagen. Öffne die Einstellungen für mehr Information.", "This room is dedicated to illegal or toxic content or the moderators fail to moderate illegal or toxic content.\n This will be reported to the administrators of %(homeserver)s.": "Der Raum beinhaltet illegale oder toxische Nachrichten und die Raummoderation verhindert es nicht.\nDies wird an die Betreiber von %(homeserver)s gemeldet werden.", "This room is dedicated to illegal or toxic content or the moderators fail to moderate illegal or toxic content.\nThis will be reported to the administrators of %(homeserver)s. The administrators will NOT be able to read the encrypted content of this room.": "Der Raum beinhaltet illegale oder toxische Nachrichten und die Raummoderation verhindert es nicht.\nDies wird an die Betreiber von %(homeserver)s gemeldet werden. Diese können jedoch die verschlüsselten Nachrichten nicht lesen.", - "This user is displaying illegal behaviour, for instance by doxing people or threatening violence.\nThis will be reported to the room moderators who may escalate this to legal authorities.": "Diese Person zeigt illegales Verhalten, beispielsweise das Leaken persönlicher Daten oder Gewaltdrohungen.\nDies wird an die Raummoderation gemeldet, welche dies an die Justiz weitergeben kann." + "This user is displaying illegal behaviour, for instance by doxing people or threatening violence.\nThis will be reported to the room moderators who may escalate this to legal authorities.": "Diese Person zeigt illegales Verhalten, beispielsweise das Leaken persönlicher Daten oder Gewaltdrohungen.\nDies wird an die Raummoderation gemeldet, welche dies an die Justiz weitergeben kann.", + "Unnamed audio": "Unbenannte Audiodatei", + "Show %(count)s other previews|one": "%(count)s andere Vorschau zeigen", + "Show %(count)s other previews|other": "%(count)s andere Vorschauen zeigen", + "Images, GIFs and videos": "Mediendateien", + "To view all keyboard shortcuts, click here.": "Alle Tastenkombinationen anzeigen", + "Keyboard shortcuts": "Tastenkombinationen", + "User %(userId)s is already invited to the room": "%(userId)s ist schon eingeladen" } From 87e1bd71495319cbb3f3b752a61f01ff5521b339 Mon Sep 17 00:00:00 2001 From: Szimszon Date: Wed, 14 Jul 2021 11:18:08 +0000 Subject: [PATCH 0378/1171] Translated using Weblate (Hungarian) Currently translated at 100.0% (3045 of 3045 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 | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/hu.json b/src/i18n/strings/hu.json index 683f825187..ffad836a2b 100644 --- a/src/i18n/strings/hu.json +++ b/src/i18n/strings/hu.json @@ -3476,5 +3476,17 @@ "Address": "Cím", "e.g. my-space": "pl. én-terem", "Silence call": "Némít", - "Sound on": "Hang be" + "Sound on": "Hang be", + "Use Command + F to search timeline": "Command + F az idővonalon való kereséshez", + "Unnamed audio": "Névtelen hang", + "Error processing audio message": "Hiba a hangüzenet feldolgozásánál", + "Show %(count)s other previews|one": "%(count)s további előnézet megjelenítése", + "Show %(count)s other previews|other": "%(count)s további előnézet megjelenítése", + "Images, GIFs and videos": "Képek, GIFek és videók", + "Code blocks": "Kód blokkok", + "Displaying time": "Idő megjelenítése", + "To view all keyboard shortcuts, click here.": "A billentyűzet kombinációk megjelenítéséhez kattintson ide.", + "Keyboard shortcuts": "Billentyűzet kombinációk", + "Use Ctrl + F to search timeline": "Ctrl + F az idővonalon való kereséshez", + "User %(userId)s is already invited to the room": "%(userId)s felhasználó már kapott meghívót a szobába" } From a778d680c68b680d97aa46b4bfebb8d6b5e9b10d Mon Sep 17 00:00:00 2001 From: jelv Date: Thu, 15 Jul 2021 06:34:01 +0000 Subject: [PATCH 0379/1171] Translated using Weblate (Dutch) Currently translated at 100.0% (3045 of 3045 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 | 36 ++++++++++++++++++++++++------------ 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/src/i18n/strings/nl.json b/src/i18n/strings/nl.json index 72168eb5ff..cd70f4c9bb 100644 --- a/src/i18n/strings/nl.json +++ b/src/i18n/strings/nl.json @@ -171,7 +171,7 @@ "Fill screen": "Scherm vullen", "Filter room members": "Gespreksleden filteren", "Forget room": "Gesprek vergeten", - "For security, this session has been signed out. Please sign in again.": "Wegens veiligheidsredenen is deze sessie uitgelogd. Gelieve opnieuw inloggen.", + "For security, this session has been signed out. Please sign in again.": "Wegens veiligheidsredenen is deze sessie uitgelogd. Log opnieuw in.", "%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s": "%(userId)s van %(fromPowerLevel)s naar %(toPowerLevel)s", "Guests cannot join this room even if explicitly invited.": "Gasten - zelfs speficiek uitgenodigde - kunnen niet aan dit gesprek deelnemen.", "Hangup": "Ophangen", @@ -1034,7 +1034,7 @@ "Legal": "Juridisch", "Credits": "Met dank aan", "For help with using %(brand)s, click here.": "Klik hier voor hulp bij het gebruiken van %(brand)s.", - "For help with using %(brand)s, click here or start a chat with our bot using the button below.": "Klik hier voor hulp bij het gebruiken van %(brand)s, of begin een gesprek met onze robot met de knop hieronder.", + "For help with using %(brand)s, click here or start a chat with our bot using the button below.": "Klik hier voor hulp bij het gebruiken van %(brand)s of begin een gesprek met onze robot met de knop hieronder.", "Help & About": "Hulp & info", "Bug reporting": "Bug meldingen", "FAQ": "FAQ", @@ -1199,7 +1199,7 @@ "Invalid homeserver discovery response": "Ongeldig homeserver-vindbaarheids-antwoord", "Invalid identity server discovery response": "Ongeldig identiteitsserver-vindbaarheidsantwoord", "General failure": "Algemene fout", - "This homeserver does not support login using email address.": "Deze homeserver biedt geen ondersteuning voor inloggen met e-mailadres.", + "This homeserver does not support login using email address.": "Deze homeserver biedt geen ondersteuning voor inloggen met een e-mailadres.", "Please contact your service administrator to continue using this service.": "Gelieve contact op te nemen met uw dienstbeheerder om deze dienst te blijven gebruiken.", "Failed to perform homeserver discovery": "Ontdekken van homeserver is mislukt", "Sign in with single sign-on": "Inloggen met eenmalig inloggen", @@ -1272,7 +1272,7 @@ "Upload files (%(current)s of %(total)s)": "Bestanden versturen (%(current)s van %(total)s)", "Upload files": "Bestanden versturen", "Upload": "Versturen", - "This file is too large to upload. The file size limit is %(limit)s but this file is %(sizeOfThisFile)s.": "Dit bestand is te groot om te versturen. De bestandsgroottelimiet is %(limit)s, maar dit bestand is %(sizeOfThisFile)s.", + "This file is too large to upload. The file size limit is %(limit)s but this file is %(sizeOfThisFile)s.": "Dit bestand is te groot om te versturen. Het limiet is %(limit)s en dit bestand is %(sizeOfThisFile)s.", "These files are too large to upload. The file size limit is %(limit)s.": "Deze bestanden zijn te groot om te versturen. De bestandsgroottelimiet is %(limit)s.", "Some files are too large to be uploaded. The file size limit is %(limit)s.": "Sommige bestanden zijn te groot om te versturen. De bestandsgroottelimiet is %(limit)s.", "Upload %(count)s other files|other": "%(count)s overige bestanden versturen", @@ -1402,7 +1402,7 @@ "Summary": "Samenvatting", "Sign in and regain access to your account.": "Meld u aan en herkrijg toegang tot uw account.", "You cannot sign in to your account. Please contact your homeserver admin for more information.": "U kunt niet inloggen met uw account. Neem voor meer informatie contact op met de beheerder van uw homeserver.", - "This account has been deactivated.": "Deze account is gesloten.", + "This account has been deactivated.": "Dit account is gesloten.", "Messages": "Berichten", "Actions": "Acties", "Displays list of commands with usages and descriptions": "Toont een lijst van beschikbare opdrachten, met hun gebruiken en beschrijvingen", @@ -1497,7 +1497,7 @@ "Share this email in Settings to receive invites directly in %(brand)s.": "Deel in de instellingen dit e-mailadres om uitnodigingen direct in %(brand)s te ontvangen.", "Use an identity server to invite by email. Use the default (%(defaultIdentityServerName)s) or manage in Settings.": "Gebruik een identiteitsserver om uit te nodigen op e-mailadres. Gebruik de standaardserver (%(defaultIdentityServerName)s) of beheer de server in de Instellingen.", "Use an identity server to invite by email. Manage in Settings.": "Gebruik een identiteitsserver om anderen uit te nodigen via e-mail. Beheer de server in de Instellingen.", - "Please fill why you're reporting.": "Gelieve aan te geven waarom u deze melding indient.", + "Please fill why you're reporting.": "Geef aan waarom u deze melding indient.", "Report Content to Your Homeserver Administrator": "Inhoud melden aan de beheerder van uw homeserver", "Reporting this message will send its unique 'event ID' to the administrator of your homeserver. If messages in this room are encrypted, your homeserver administrator will not be able to read the message text or view any files or images.": "Dit bericht melden zal zijn unieke ‘gebeurtenis-ID’ versturen naar de beheerder van uw homeserver. Als de berichten in dit gesprek versleuteld zijn, zal de beheerder van uw homeserver het bericht niet kunnen lezen, noch enige bestanden of afbeeldingen zien.", "Send report": "Rapport versturen", @@ -1564,7 +1564,7 @@ "Session already verified!": "Sessie al geverifieerd!", "WARNING: Session already verified, but keys do NOT MATCH!": "PAS OP: de sessie is al geverifieerd, maar de sleutels komen NIET OVEREEN!", "WARNING: KEY VERIFICATION FAILED! The signing key for %(userId)s and session %(deviceId)s is \"%(fprint)s\" which does not match the provided key \"%(fingerprint)s\". This could mean your communications are being intercepted!": "PAS OP: sleutelverificatie MISLUKT! De combinatie %(userId)s + sessie %(deviceId)s is ondertekend met ‘%(fprint)s’ - maar de opgegeven sleutel is ‘%(fingerprint)s’. Wellicht worden uw berichten onderschept!", - "The signing key you provided matches the signing key you received from %(userId)s's session %(deviceId)s. Session marked as verified.": "De door u verschafte en de van %(userId)ss sessie %(deviceId)s verkregen sleutels komen overeen. De sessie is daarmee geverifieerd.", + "The signing key you provided matches the signing key you received from %(userId)s's session %(deviceId)s. Session marked as verified.": "De door u verschafte sleutel en de van %(userId)ss sessie %(deviceId)s verkregen sleutels komen overeen. De sessie is daarmee geverifieerd.", "%(senderName)s placed a voice call.": "%(senderName)s probeert u te bellen.", "%(senderName)s placed a voice call. (not supported by this browser)": "%(senderName)s poogt u te bellen, maar uw browser ondersteunt dat niet", "%(senderName)s placed a video call.": "%(senderName)s doet een video-oproep.", @@ -2564,7 +2564,7 @@ "Use app for a better experience": "Gebruik de app voor een betere ervaring", "Enable desktop notifications": "Bureaubladmeldingen inschakelen", "Don't miss a reply": "Mis geen antwoord", - "Unknown App": "Onbekende App", + "Unknown App": "Onbekende app", "Error leaving room": "Fout bij verlaten gesprek", "Unexpected server error trying to leave the room": "Onverwachte serverfout bij het verlaten van dit gesprek", "See %(msgtype)s messages posted to your active room": "Zie %(msgtype)s-berichten verstuurd in uw actieve gesprek", @@ -2803,7 +2803,7 @@ "Successfully restored %(sessionCount)s keys": "Succesvol %(sessionCount)s sleutels hersteld", "Keys restored": "Sleutels hersteld", "Backup could not be decrypted with this Security Phrase: please verify that you entered the correct Security Phrase.": "Back-up kon niet worden ontsleuteld met dit veiligheidswachtwoord: controleer of u het juiste veiligheidswachtwoord hebt ingevoerd.", - "Incorrect Security Phrase": "Onjuist Veiligheidswachtwoord", + "Incorrect Security Phrase": "Onjuist veiligheidswachtwoord", "Backup could not be decrypted with this Security Key: please verify that you entered the correct Security Key.": "Back-up kon niet worden ontcijferd met deze veiligheidssleutel: controleer of u de juiste veiligheidssleutel hebt ingevoerd.", "Security Key mismatch": "Verkeerde veiligheidssleutel", "%(completed)s of %(total)s keys restored": "%(completed)s van %(total)s sleutels hersteld", @@ -3034,7 +3034,7 @@ "Edit settings relating to your space.": "Bewerk instellingen gerelateerd aan uw space.", "Invite someone using their name, username (like ) or share this space.": "Nodig iemand uit per naam, gebruikersnaam (zoals ) of deel deze space.", "Invite someone using their name, email address, username (like ) or share this space.": "Nodig iemand uit per naam, e-mailadres, gebruikersnaam (zoals ) of deel deze space.", - "Unnamed Space": "Naamloze Space", + "Unnamed Space": "Naamloze space", "Invite to %(spaceName)s": "Voor %(spaceName)s uitnodigen", "Failed to add rooms to space": "Het toevoegen van gesprekken aan de space is mislukt", "Apply": "Toepassen", @@ -3176,7 +3176,7 @@ "If you reset everything, you will restart with no trusted sessions, no trusted users, and might not be able to see past messages.": "Als u alles reset, zult u opnieuw opstarten zonder vertrouwde sessies, zonder vertrouwde gebruikers, en zult u misschien geen vroegere berichten meer kunnen zien.", "Only do this if you have no other device to complete verification with.": "Doe dit alleen als u geen ander apparaat hebt om de verificatie mee uit te voeren.", "Reset everything": "Alles opnieuw instellen", - "Forgotten or lost all recovery methods? Reset all": "Alles vergeten of alle herstelmethoden verloren? Alles opnieuw instellen", + "Forgotten or lost all recovery methods? Reset all": "Alles vergeten en alle herstelmethoden verloren? Alles opnieuw instellen", "If you do, please note that none of your messages will be deleted, but the search experience might be degraded for a few moments whilst the index is recreated": "Als u dat doet, let wel geen van uw berichten wordt verwijderd, maar de zoekresultaten zullen gedurende enkele ogenblikken verslechteren terwijl de index opnieuw wordt aangemaakt", "View message": "Bericht bekijken", "Zoom in": "Inzoomen", @@ -3369,5 +3369,17 @@ "%(targetName)s accepted an invitation": "%(targetName)s accepteerde de uitnodiging", "%(targetName)s accepted the invitation for %(displayName)s": "%(targetName)s accepteerde de uitnodiging voor %(displayName)s", "Some invites couldn't be sent": "Sommige uitnodigingen konden niet verstuurd worden", - "We sent the others, but the below people couldn't be invited to ": "De anderen zijn verstuurd, maar de volgende mensen konden niet worden uitgenodigd voor " + "We sent the others, but the below people couldn't be invited to ": "De anderen zijn verstuurd, maar de volgende mensen konden niet worden uitgenodigd voor ", + "Unnamed audio": "Naamloze audio", + "Error processing audio message": "Fout bij verwerking audiobericht", + "Show %(count)s other previews|one": "%(count)s andere preview weergeven", + "Show %(count)s other previews|other": "%(count)s andere previews weergeven", + "Images, GIFs and videos": "Afbeeldingen, GIF's en video's", + "Code blocks": "Codeblokken", + "Displaying time": "Tijdsweergave", + "To view all keyboard shortcuts, click here.": "Om alle sneltoetsen te zien, klik hier.", + "Keyboard shortcuts": "Sneltoetsen", + "Use Ctrl + F to search timeline": "Gebruik Ctrl +F om te zoeken in de tijdlijn", + "Use Command + F to search timeline": "Gebruik Command + F om te zoeken in de tijdlijn", + "User %(userId)s is already invited to the room": "De gebruiker %(userId)s is al uitgenodigd voor dit gesprek" } From 50d5aab1262475dc2d4ac3ecd4d556d389c2164a Mon Sep 17 00:00:00 2001 From: Jeff Huang Date: Thu, 15 Jul 2021 02:55:04 +0000 Subject: [PATCH 0380/1171] Translated using Weblate (Chinese (Traditional)) Currently translated at 100.0% (3045 of 3045 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 | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/zh_Hant.json b/src/i18n/strings/zh_Hant.json index 03cebcb083..b3213e9190 100644 --- a/src/i18n/strings/zh_Hant.json +++ b/src/i18n/strings/zh_Hant.json @@ -3484,5 +3484,17 @@ "%(targetName)s accepted an invitation": "%(targetName)s 接受了邀請", "%(targetName)s accepted the invitation for %(displayName)s": "%(targetName)s 已接受 %(displayName)s 的邀請", "Some invites couldn't be sent": "部份邀請無法傳送", - "We sent the others, but the below people couldn't be invited to ": "我們已將邀請傳送給其他人,但以下的人無法邀請至 " + "We sent the others, but the below people couldn't be invited to ": "我們已將邀請傳送給其他人,但以下的人無法邀請至 ", + "Unnamed audio": "未命名的音訊", + "Error processing audio message": "處理音訊訊息時出現問題", + "Show %(count)s other previews|one": "顯示 %(count)s 個其他預覽", + "Show %(count)s other previews|other": "顯示 %(count)s 個其他預覽", + "Images, GIFs and videos": "圖片、GIF 與影片", + "Code blocks": "程式碼區塊", + "Displaying time": "顯示時間", + "To view all keyboard shortcuts, click here.": "要檢視所有鍵盤快捷鍵,請點擊此處。", + "Keyboard shortcuts": "鍵盤快捷鍵", + "Use Ctrl + F to search timeline": "使用 Ctrl + F 來搜尋時間軸", + "Use Command + F to search timeline": "使用 Command + F 來搜尋時間軸", + "User %(userId)s is already invited to the room": "使用者 %(userId)s 已被邀請至聊天室" } From 2a980ea15a315c50e2a2347c1a559760383985a5 Mon Sep 17 00:00:00 2001 From: Ihor Hordiichuk Date: Thu, 15 Jul 2021 18:25:46 +0000 Subject: [PATCH 0381/1171] Translated using Weblate (Ukrainian) Currently translated at 48.0% (1464 of 3045 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/uk/ --- src/i18n/strings/uk.json | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/i18n/strings/uk.json b/src/i18n/strings/uk.json index 92da704837..67ee136515 100644 --- a/src/i18n/strings/uk.json +++ b/src/i18n/strings/uk.json @@ -506,7 +506,7 @@ "Upload Error": "Помилка відвантаження", "Failed to upload image": "Не вдалось відвантажити зображення", "Upload avatar": "Завантажити аватар", - "For security, this session has been signed out. Please sign in again.": "З метою безпеки вашу сесію було завершено. Зайдіть, будь ласка, знову.", + "For security, this session has been signed out. Please sign in again.": "З метою безпеки ваш сеанс було завершено. Увійдіть знову.", "Upload an avatar:": "Завантажити аватар:", "Custom (%(level)s)": "Власний (%(level)s)", "Error upgrading room": "Помилка оновлення кімнати", @@ -541,7 +541,7 @@ "Cancel entering passphrase?": "Скасувати введення парольної фрази?", "Enter passphrase": "Введіть парольну фразу", "Setting up keys": "Налаштовування ключів", - "Verify this session": "Звірити цю сесію", + "Verify this session": "Звірити цей сеанс", "Sign In or Create Account": "Увійти або створити обліковий запис", "Use your account or create a new one to continue.": "Скористайтесь вашим обліковим записом або створіть нову, щоб продовжити.", "Create Account": "Створити обліковий запис", @@ -564,10 +564,10 @@ "For help with using %(brand)s, click here.": "Якщо необхідна допомога у користуванні %(brand)s'ом, клацніть тут.", "For help with using %(brand)s, click here or start a chat with our bot using the button below.": "Якщо необхідна допомога у користуванні %(brand)s'ом, клацніть тут або розпочніть балачку з нашим ботом, клацнувши на кнопці нижче.", "Join the conversation with an account": "Приєднатись до бесіди з обліковим записом", - "Unable to restore session": "Неможливо відновити сесію", - "We encountered an error trying to restore your previous session.": "Ми натрапили на помилку, намагаючись відновити вашу попередню сесію.", + "Unable to restore session": "Не вдалося відновити сеанс", + "We encountered an error trying to restore your previous session.": "Ми натрапили на помилку, намагаючись відновити ваш попередній сеанс.", "Please install Chrome, Firefox, or Safari for the best experience.": "Для найкращих вражень від користування встановіть, будь ласка, Chrome, Firefox, або Safari.", - "Your account has a cross-signing identity in secret storage, but it is not yet trusted by this session.": "Ваш обліковий запис має перехресно-підписувану ідентичність у таємному сховищі, але вона ще не є довіреною у цій сесії.", + "Your account has a cross-signing identity in secret storage, but it is not yet trusted by this session.": "Ваш обліковий запис має перехресно-підписувану ідентичність у таємному сховищі, але воно ще не є довіреним у цьому сеансі.", "in account data": "у даних облікового запису", "Clear notifications": "Очистити сповіщення", "Add an email address to configure email notifications": "Додати адресу е-пошти для налаштування поштових сповіщень", @@ -585,8 +585,8 @@ "Confirm account deactivation": "Підтвердьте знедіювання облікового запису", "To continue, please enter your password:": "Щоб продовжити, введіть, будь ласка, ваш пароль:", "This will make your account permanently unusable. You will not be able to log in, and no one will be able to re-register the same user ID. This will cause your account to leave all rooms it is participating in, and it will remove your account details from your identity server. This action is irreversible.": "Ваш обліковий запис стане назавжди невикористовним. Ви не матимете змоги увійти в нього і ніхто не зможе перереєструватись під цим користувацьким ID. Це призведе до виходу вашого облікового запису з усіх кімнат та до видалення деталей вашого облікового запису з вашого серверу ідентифікації. Ця дія є безповоротною.", - "Verify session": "Звірити сесію", - "Session name": "Назва сесії", + "Verify session": "Звірити сеанс", + "Session name": "Назва сеансу", "Session ID": "ID сеансу", "Session key": "Ключ сеансу", "%(count)s of your messages have not been sent.|one": "Ваше повідомлення не було надіслано.", @@ -697,7 +697,7 @@ "You signed in to a new session without verifying it:": "Ви увійшли в новий сеанс, не підтвердивши його:", "Verify your other session using one of the options below.": "Перевірте інший сеанс за допомогою одного із варіантів знизу.", "%(name)s (%(userId)s) signed in to a new session without verifying it:": "%(name)s (%(userId)s) починає новий сеанс без його підтвердження:", - "Ask this user to verify their session, or manually verify it below.": "Попросіть цього користувача підтвердити сесію, або підтвердіть її власноруч нижче.", + "Ask this user to verify their session, or manually verify it below.": "Попросіть цього користувача підтвердити сеанс, або підтвердьте його власноруч унизу.", "Not Trusted": "Недовірене", "Manually Verify by Text": "Ручна перевірка за допомогою тексту", "Interactively verify by Emoji": "Інтерактивно звірити за допомогою емодзі", @@ -973,10 +973,10 @@ "not found": "не знайдено", "Cross-signing private keys:": "Приватні ключі для кросс-підпису:", "exists": "існує", - "Delete sessions|other": "Видалити сесії", - "Delete sessions|one": "Видалити сесію", - "Delete %(count)s sessions|other": "Видалити %(count)s сесій", - "Delete %(count)s sessions|one": "Видалити %(count)s сесій", + "Delete sessions|other": "Видалити сеанси", + "Delete sessions|one": "Видалити сеанс", + "Delete %(count)s sessions|other": "Видалити %(count)s сеансів", + "Delete %(count)s sessions|one": "Видалити %(count)s сеансів", "ID": "ID", "Public Name": "Публічне ім'я", " to store messages from ": " зберігання повідомлень від ", From efc9c31e85e3bd59a8550f85ca8f062d243608b5 Mon Sep 17 00:00:00 2001 From: Desc4rtes Date: Thu, 15 Jul 2021 11:48:07 +0000 Subject: [PATCH 0382/1171] Translated using Weblate (Turkish) Currently translated at 75.1% (2287 of 3045 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/tr/ --- src/i18n/strings/tr.json | 41 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 38 insertions(+), 3 deletions(-) diff --git a/src/i18n/strings/tr.json b/src/i18n/strings/tr.json index 0458d3226a..f295263d1e 100644 --- a/src/i18n/strings/tr.json +++ b/src/i18n/strings/tr.json @@ -101,7 +101,7 @@ "Failed to set display name": "Görünür ismi ayarlama başarısız oldu", "Failed to unban": "Yasağı kaldırmak başarısız oldu", "Failed to upload profile picture!": "Profil resmi yükleme başarısız oldu!", - "Failed to verify email address: make sure you clicked the link in the email": "Eposta adresini doğrulamadı: epostadaki bağlantıya tıkladığınızdan emin olun", + "Failed to verify email address: make sure you clicked the link in the email": "E-posta adresi doğrulanamadı: E-postadaki bağlantıya tıkladığınızdan emin olun", "Failure to create room": "Oda oluşturulamadı", "Favourite": "Favori", "Favourites": "Favoriler", @@ -1695,7 +1695,7 @@ "Visibility in Room List": "Oda Listesindeki Görünürlük", "Confirm adding email": "E-posta adresini eklemeyi onayla", "Click the button below to confirm adding this email address.": "E-posta adresini eklemeyi kabul etmek için aşağıdaki tuşa tıklayın.", - "Confirm adding phone number": "Telefon numayasını ekleyi onayla", + "Confirm adding phone number": "Telefon numarası eklemeyi onayla", "Click the button below to confirm adding this phone number.": "Telefon numarasını eklemeyi kabul etmek için aşağıdaki tuşa tıklayın.", "Are you sure you want to cancel entering passphrase?": "Parola girmeyi iptal etmek istediğinizden emin misiniz?", "Room name or address": "Oda adı ya da adresi", @@ -2544,5 +2544,40 @@ "We couldn't log you in": "Sizin girişinizi yapamadık", "You're already in a call with this person.": "Bu kişi ile halihazırda çağrıdasınız.", "The user you called is busy.": "Aradığınız kullanıcı meşgul.", - "User Busy": "Kullanıcı Meşgul" + "User Busy": "Kullanıcı Meşgul", + "Got it": "Anlaşıldı", + "Verified": "Doğrulanmış", + "You've successfully verified %(displayName)s!": "%(displayName)s başarıyla doğruladınız!", + "You've successfully verified %(deviceName)s (%(deviceId)s)!": "%(deviceName)s (%(deviceId)s) başarıyla doğruladınız!", + "You've successfully verified your device!": "Cihazınızı başarıyla doğruladınız!", + "Edit devices": "Cihazları düzenle", + "Delete recording": "Kaydı sil", + "Stop the recording": "Kaydı durdur", + "We didn't find a microphone on your device. Please check your settings and try again.": "Cihazınızda bir mikrofon bulamadık. Lütfen ayarlarınızı kontrol edin ve tekrar deneyin.", + "No microphone found": "Mikrofon bulunamadı", + "Empty room": "Boş oda", + "Suggested Rooms": "Önerilen Odalar", + "View message": "Mesajı görüntüle", + "Invite to just this room": "Sadece bu odaya davet et", + "%(seconds)ss left": "%(seconds)s saniye kaldı", + "Send message": "Mesajı gönder", + "Your message was sent": "Mesajınız gönderildi", + "Encrypting your message...": "Mesajınız şifreleniyor...", + "Sending your message...": "Mesajınız gönderiliyor...", + "Code blocks": "Kod blokları", + "Displaying time": "Zamanı görüntüle", + "To view all keyboard shortcuts, click here.": "Tüm klavye kısayollarını görmek için buraya tıklayın.", + "Keyboard shortcuts": "Klavye kısayolları", + "Visibility": "Görünürlük", + "Save Changes": "Değişiklikleri Kaydet", + "Saving...": "Kaydediliyor...", + "Invite with email or username": "E-posta veya kullanıcı adı ile davet et", + "Invite people": "İnsanları davet et", + "Share invite link": "Davet bağlantısını paylaş", + "Click to copy": "Kopyalamak için tıklayın", + "You can change these anytime.": "Bunları istediğiniz zaman değiştirebilirsiniz.", + "You can change this later": "Bunu daha sonra değiştirebilirsiniz", + "Change which room, message, or user you're viewing": "Görüntülediğiniz odayı, mesajı veya kullanıcıyı değiştirin", + "%(targetName)s accepted an invitation": "%(targetName)s daveti kabul etti", + "%(targetName)s accepted the invitation for %(displayName)s": "%(targetName)s, %(displayName)s kişisinin davetini kabul etti" } From fd7b24d5ddc1e4c43c063afb874a2a58a6049318 Mon Sep 17 00:00:00 2001 From: justin-cv Date: Fri, 16 Jul 2021 07:56:24 +0000 Subject: [PATCH 0383/1171] Translated using Weblate (Indonesian) Currently translated at 7.8% (239 of 3045 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/id/ --- src/i18n/strings/id.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/id.json b/src/i18n/strings/id.json index 2de350bae3..499f625b75 100644 --- a/src/i18n/strings/id.json +++ b/src/i18n/strings/id.json @@ -279,5 +279,8 @@ "A call is currently being placed!": "Sedang melakukan panggilan sekarang!", "A call is already in progress!": "Masih ada panggilan berlangsung!", "Permission Required": "Permisi Dibutuhkan", - "You do not have permission to start a conference call in this room": "Anda tidak memiliki permisi untuk memulai panggilan massal di ruang ini" + "You do not have permission to start a conference call in this room": "Anda tidak memiliki permisi untuk memulai panggilan massal di ruang ini", + "Explore rooms": "Jelajahi ruang", + "Sign In": "Masuk", + "Create Account": "Buat Akun" } From c1a85e50c6841825f7002ce301e8e48402e0458b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Wed, 14 Jul 2021 11:21:57 +0000 Subject: [PATCH 0384/1171] Translated using Weblate (Czech) Currently translated at 99.9% (3042 of 3045 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 | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/cs.json b/src/i18n/strings/cs.json index 266fa339d2..66f0dba9aa 100644 --- a/src/i18n/strings/cs.json +++ b/src/i18n/strings/cs.json @@ -3400,5 +3400,14 @@ "Some invites couldn't be sent": "Některé pozvánky nebylo možné odeslat", "We sent the others, but the below people couldn't be invited to ": "Poslali jsme ostatním, ale níže uvedení lidé nemohli být pozváni do ", "Visibility": "Viditelnost", - "Address": "Adresa" + "Address": "Adresa", + "To view all keyboard shortcuts, click here.": "Pro zobrazení všech klávesových zkratek, klikněte zde.", + "Unnamed audio": "Nepojmenovaný audio soubor", + "Error processing audio message": "Došlo k chybě při zpracovávání hlasové zprávy", + "Images, GIFs and videos": "Obrázky, GIFy a videa", + "Code blocks": "Bloky kódu", + "Displaying time": "Zobrazování času", + "Keyboard shortcuts": "Klávesové zkratky", + "Use Ctrl + F to search timeline": "Stiskněte Ctrl + F k vyhledávání v časové ose", + "Use Command + F to search timeline": "Stiskněte Command + F k vyhledávání v časové ose" } From cb3673a2129fc16bfd1f9f137179b5167278c6b4 Mon Sep 17 00:00:00 2001 From: Besnik Bleta Date: Wed, 14 Jul 2021 11:18:48 +0000 Subject: [PATCH 0385/1171] Translated using Weblate (Albanian) Currently translated at 99.7% (3037 of 3045 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 | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/sq.json b/src/i18n/strings/sq.json index e6f27a955d..3a0663e85a 100644 --- a/src/i18n/strings/sq.json +++ b/src/i18n/strings/sq.json @@ -3468,5 +3468,18 @@ "%(targetName)s accepted an invitation": "%(targetName)s pranoi një ftesë", "%(targetName)s accepted the invitation for %(displayName)s": "%(targetName)s pranoi ftesën për %(displayName)s", "Some invites couldn't be sent": "S’u dërguan dot disa nga ftesat", - "We sent the others, but the below people couldn't be invited to ": "I dërguam të tjerat, por personat më poshtë s’u ftuan dot te " + "We sent the others, but the below people couldn't be invited to ": "I dërguam të tjerat, por personat më poshtë s’u ftuan dot te ", + "Unnamed audio": "Audio pa emër", + "Forward": "Përcille", + "Sent": "U dërgua", + "Error processing audio message": "Gabim në përpunim mesazhi audio", + "Show %(count)s other previews|one": "Shfaq %(count)s paraparje tjetër", + "Show %(count)s other previews|other": "Shfaq %(count)s paraparje të tjera", + "Images, GIFs and videos": "Figura, GIF-e dhe video", + "Code blocks": "Blloqe kodi", + "To view all keyboard shortcuts, click here.": "Që të shihni krejt shkurtoret e tastierës, klikoni këtu.", + "Keyboard shortcuts": "Shkurtore tastiere", + "Use Ctrl + F to search timeline": "Përdorni Ctrl + F që të kërkohet te rrjedha kohore", + "Use Command + F to search timeline": "Përdorni Command + F që të kërkohet te rrjedha kohore", + "User %(userId)s is already invited to the room": "Përdoruesi %(userId)s është ftuar tashmë te dhoma" } From bdfb4bd68e09729a24bf5e0b24a847c43967aaab Mon Sep 17 00:00:00 2001 From: Nils Haugen Date: Wed, 14 Jul 2021 15:59:49 +0000 Subject: [PATCH 0386/1171] Translated using Weblate (Norwegian Nynorsk) Currently translated at 39.2% (1196 of 3045 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/nn/ --- src/i18n/strings/nn.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/nn.json b/src/i18n/strings/nn.json index 478f05b5cb..7b02407ea9 100644 --- a/src/i18n/strings/nn.json +++ b/src/i18n/strings/nn.json @@ -1376,5 +1376,10 @@ "Identity Server": "Identitetstenar", "Email Address": "E-postadresse", "Go Back": "Gå attende", - "Notification settings": "Varslingsinnstillingar" + "Notification settings": "Varslingsinnstillingar", + "You should remove your personal data from identity server before disconnecting. Unfortunately, identity server is currently offline or cannot be reached.": "Du bør fjerne dine personlege data frå identitetstenaren før du koplar frå. Dessverre er identitetstenaren utilgjengeleg og kan ikkje nåast akkurat no.", + "We recommend that you remove your email addresses and phone numbers from the identity server before disconnecting.": "Vi tilrår at du slettar personleg informasjon, som e-postadresser og telefonnummer frå identitetstenaren før du koplar frå.", + "Privacy": "Personvern", + "Versions": "Versjonar", + "Legal": "Juridisk" } From a3eed6a68977efa5f7ce4ad59c5659d59dd1d85e Mon Sep 17 00:00:00 2001 From: Phuc D** Date: Thu, 15 Jul 2021 04:00:42 +0000 Subject: [PATCH 0387/1171] Translated using Weblate (Vietnamese) Currently translated at 10.2% (311 of 3045 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/vi/ --- src/i18n/strings/vi.json | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/vi.json b/src/i18n/strings/vi.json index aec8580ef1..9bcb16c061 100644 --- a/src/i18n/strings/vi.json +++ b/src/i18n/strings/vi.json @@ -342,5 +342,19 @@ "Confirm adding email": "Xác nhận việc thêm email", "Add Phone Number": "Thêm Số Điện Thoại", "Click the button below to confirm adding this phone number.": "Nhấn vào nút dưới đây để xác nhận việc thêm số điện thoại này.", - "Confirm": "Xác nhận" + "Confirm": "Xác nhận", + "No other application is using the webcam": "Không có ứng dụng nào khác đang sử dụng webcam", + "Permission is granted to use the webcam": "Quyền được cấp để sử dụng webcam", + "A microphone and webcam are plugged in and set up correctly": "Micro và webcam đã được cắm và thiết lập đúng cách", + "Call failed because webcam or microphone could not be accessed. Check that:": "Cuộc gọi không thành công vì không thể truy cập webcam hoặc micrô. Kiểm tra xem:", + "Unable to access webcam / microphone": "Không thể truy cập webcam / micro", + "The call could not be established": "Không thể thiết lập cuộc gọi", + "The user you called is busy.": "Người dùng mà bạn gọi đang bận", + "User Busy": "Người dùng đang bận", + "The other party declined the call.": "Bên kia đã từ chối cuộc gọi.", + "Call Declined": "Cuộc gọi bị từ chối", + "Your user agent": "Hành động của bạn", + "Single Sign On": "Single Sign On", + "Confirm adding this email address by using Single Sign On to prove your identity.": "Xác nhận việc thêm địa chỉ email này bằng cách sử dụng Single Sign On để chứng minh danh tính của bạn.", + "Use Single Sign On to continue": "Sử dụng Signle Sign On để tiếp tục" } From 9ae5f0ab4dd9eb86bf53161aba44881b239ee31b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20J=C3=B5er=C3=BC=C3=BCt?= Date: Thu, 15 Jul 2021 07:08:59 +0000 Subject: [PATCH 0388/1171] Translated using Weblate (Estonian) Currently translated at 99.5% (3030 of 3045 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 | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/i18n/strings/et.json b/src/i18n/strings/et.json index ce262233b8..58b9f0bf9b 100644 --- a/src/i18n/strings/et.json +++ b/src/i18n/strings/et.json @@ -2829,7 +2829,7 @@ "Messages here are end-to-end encrypted. Verify %(displayName)s in their profile - tap on their avatar.": "Sõnumid siin jututoas on läbivalt krüptitud. Klõpsides tunnuspilti saad kontrollida kasutaja %(displayName)s profiili.", "%(creator)s created this DM.": "%(creator)s alustas seda otsesuhtlust.", "This is the start of .": "See on jututoa algus.", - "Add a photo, so people can easily spot your room.": "Selle, et teised märkaks sinu jututuba lihtsamini, palun lisa üks pilt.", + "Add a photo, so people can easily spot your room.": "Selleks, et teised märkaks sinu jututuba lihtsamini, palun lisa üks pilt.", "%(displayName)s created this room.": "%(displayName)s lõi selle jututoa.", "You created this room.": "Sa lõid selle jututoa.", "Add a topic to help people know what it is about.": "Selleks, et teised teaks millega on tegemist, palun lisa teema.", @@ -3450,5 +3450,11 @@ "This user is spamming the room with ads, links to ads or to propaganda.\nThis will be reported to the room moderators.": "See kasutaja spämmib jututuba reklaamidega, reklaamlinkidega või propagandaga.\nJututoa moderaatorid saavad selle kohta teate.", "This user is displaying toxic behaviour, for instance by insulting other users or sharing adult-only content in a family-friendly room or otherwise violating the rules of this room.\nThis will be reported to the room moderators.": "Selle kasutaja tegevus on äärmiselt ebasobilik, milleks võib olla teiste jututoas osalejate solvamine, peresõbralikku jututuppa täiskasvanutele mõeldud sisu lisamine või muul viisil jututoa reeglite rikkumine.\nJututoa moderaatorid saavad selle kohta teate.", "Please provide an address": "Palun sisesta aadress", - "Report to moderators prototype. In rooms that support moderation, the `report` button will let you report abuse to room moderators": "Meie esimene katsetus modereerimisega. Kui jututoas on modereerimine toetatud, siis „Teata moderaatorile“ nupust võid saada teate ebasobiliku sisu kohta" + "Report to moderators prototype. In rooms that support moderation, the `report` button will let you report abuse to room moderators": "Meie esimene katsetus modereerimisega. Kui jututoas on modereerimine toetatud, siis „Teata moderaatorile“ nupust võid saada teate ebasobiliku sisu kohta", + "Unnamed audio": "Nimetu helifail", + "Code blocks": "Lähtekoodi lõigud", + "Images, GIFs and videos": "Pildid, gif'id ja videod", + "Show %(count)s other previews|other": "Näita %(count)s muud eelvaadet", + "Show %(count)s other previews|one": "Näita veel %(count)s eelvaadet", + "Error processing audio message": "Viga häälsõnumi töötlemisel" } From 3b13eb7b44debf727c0ed7c75b42d0ea3c0a17b2 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 16 Jul 2021 13:18:12 +0100 Subject: [PATCH 0389/1171] Prefer URL constructor over `url` dependency --- src/HtmlUtils.tsx | 5 +---- src/utils/HostingLink.js | 10 ++-------- src/utils/UrlUtils.ts | 4 ---- 3 files changed, 3 insertions(+), 16 deletions(-) diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx index 5e83fdc2a0..a37b7f0ac9 100644 --- a/src/HtmlUtils.tsx +++ b/src/HtmlUtils.tsx @@ -25,7 +25,6 @@ import _linkifyElement from 'linkifyjs/element'; import _linkifyString from 'linkifyjs/string'; import classNames from 'classnames'; import EMOJIBASE_REGEX from 'emojibase-regex'; -import url from 'url'; import katex from 'katex'; import { AllHtmlEntities } from 'html-entities'; import { IContent } from 'matrix-js-sdk/src/models/event'; @@ -153,10 +152,8 @@ export function getHtmlText(insaneHtml: string): string { */ export function isUrlPermitted(inputUrl: string): boolean { try { - const parsed = url.parse(inputUrl); - if (!parsed.protocol) return false; // URL parser protocol includes the trailing colon - return PERMITTED_URL_SCHEMES.includes(parsed.protocol.slice(0, -1)); + return PERMITTED_URL_SCHEMES.includes(new URL(inputUrl).protocol.slice(0, -1)); } catch (e) { return false; } diff --git a/src/utils/HostingLink.js b/src/utils/HostingLink.js index 7595bdd482..134e045ca2 100644 --- a/src/utils/HostingLink.js +++ b/src/utils/HostingLink.js @@ -14,11 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ -import url from 'url'; - import SdkConfig from '../SdkConfig'; import { MatrixClientPeg } from '../MatrixClientPeg'; -import { urlSearchParamsToObject } from "./UrlUtils"; export function getHostingLink(campaign) { const hostingLink = SdkConfig.get().hosting_signup_link; @@ -28,11 +25,8 @@ export function getHostingLink(campaign) { if (MatrixClientPeg.get().getDomain() !== 'matrix.org') return null; try { - const hostingUrl = url.parse(hostingLink); - const params = urlSearchParamsToObject(new URLSearchParams(hostingUrl.query)); - params.utm_campaign = campaign; - hostingUrl.search = undefined; - hostingUrl.query = params; + const hostingUrl = new URL(hostingLink); + hostingUrl.searchParams.set("utm_campaign", campaign); return hostingUrl.format(); } catch (e) { return hostingLink; diff --git a/src/utils/UrlUtils.ts b/src/utils/UrlUtils.ts index 392b44c5e9..ba43340ff5 100644 --- a/src/utils/UrlUtils.ts +++ b/src/utils/UrlUtils.ts @@ -16,10 +16,6 @@ limitations under the License. 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 * From 74bd7cad3f768be14abf57280887c4c463a19c66 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 16 Jul 2021 13:40:53 +0100 Subject: [PATCH 0390/1171] remove unrelated change --- src/utils/UrlUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/UrlUtils.ts b/src/utils/UrlUtils.ts index ba43340ff5..6f441ff98e 100644 --- a/src/utils/UrlUtils.ts +++ b/src/utils/UrlUtils.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import * as url from "url"; +import url from "url"; /** * If a url has no path component, etc. abbreviate it to just the hostname From 498a59a4972ab1f44c581716f0fea517580fe9f2 Mon Sep 17 00:00:00 2001 From: libexus Date: Fri, 16 Jul 2021 15:45:42 +0000 Subject: [PATCH 0391/1171] Translated using Weblate (German) Currently translated at 99.1% (3025 of 3051 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 | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json index af7dade32f..879c6fd8a1 100644 --- a/src/i18n/strings/de_DE.json +++ b/src/i18n/strings/de_DE.json @@ -3459,5 +3459,13 @@ "Images, GIFs and videos": "Mediendateien", "To view all keyboard shortcuts, click here.": "Alle Tastenkombinationen anzeigen", "Keyboard shortcuts": "Tastenkombinationen", - "User %(userId)s is already invited to the room": "%(userId)s ist schon eingeladen" + "User %(userId)s is already invited to the room": "%(userId)s ist schon eingeladen", + "Unable to copy a link to the room to the clipboard.": "Der Link zum Raum konnte nicht kopiert werden.", + "Unable to copy room link": "Raumlink konnte nicht kopiert werden", + "Integration manager": "Integrationsmanager", + "User Directory": "Benutzerverzeichnis", + "Your %(brand)s doesn't allow you to use an integration manager to do this. Please contact an admin.": "%(brand)s erlaubt dir nicht, einen Integrationsmanager dafür zu verwenden. Bitte kontaktiere einen Admin.", + "Copy Link": "Link kopieren", + "Transfer Failed": "Übertragen fehlgeschlagen", + "Unable to transfer call": "Übertragen des Anrufs fehlgeschlagen" } From 90c5e96433f1bc661f2794ca3933dd88f5800f1e Mon Sep 17 00:00:00 2001 From: Paulo Pinto Date: Fri, 16 Jul 2021 15:37:29 +0000 Subject: [PATCH 0392/1171] Translated using Weblate (German) Currently translated at 99.1% (3025 of 3051 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 | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json index 879c6fd8a1..1a977aec90 100644 --- a/src/i18n/strings/de_DE.json +++ b/src/i18n/strings/de_DE.json @@ -3467,5 +3467,15 @@ "Your %(brand)s doesn't allow you to use an integration manager to do this. Please contact an admin.": "%(brand)s erlaubt dir nicht, einen Integrationsmanager dafür zu verwenden. Bitte kontaktiere einen Admin.", "Copy Link": "Link kopieren", "Transfer Failed": "Übertragen fehlgeschlagen", - "Unable to transfer call": "Übertragen des Anrufs fehlgeschlagen" + "Unable to transfer call": "Übertragen des Anrufs fehlgeschlagen", + "Using this widget may share data with %(widgetDomain)s & your integration manager.": "Wenn du dieses Widget verwendest, können Daten zu %(widgetDomain)s und deinem Integrationsserver übertragen werden.", + "Identity server is": "Der Identitätsserver ist", + "Integration managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "Integrationsverwalter erhalten Konfigurationsdaten und können Widgets modifizieren, Raumeinladungen verschicken und in deinem Namen Berechtigungslevel setzen.", + "Use an integration manager to manage bots, widgets, and sticker packs.": "Verwende einen Integrationsverwalter, um Bots, Widgets und Stickerpakete zu verwalten.", + "Use an integration manager (%(serverName)s) to manage bots, widgets, and sticker packs.": "Nutze einen Integrationsverwalter (%(serverName)s), um Bots, Widgets und Stickerpakete zu verwalten.", + "Identity server": "Identitätsserver", + "Identity server (%(server)s)": "Identitätsserver (%(server)s)", + "Could not connect to identity server": "Verbindung zum Identitätsserver konnte nicht hergestellt werden", + "Not a valid identity server (status code %(code)s)": "Ungültiger Identitätsserver (Fehlercode %(code)s)", + "Identity server URL must be HTTPS": "Identitätsserver-URL muss HTTPS sein" } From 41d5865dd72e4a9fb9c67932d39531097ce909e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Fri, 16 Jul 2021 19:26:04 +0200 Subject: [PATCH 0393/1171] Cleanup _ReplyTile.scss MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/css/views/rooms/_ReplyTile.scss | 142 ++++++++++++++-------------- 1 file changed, 69 insertions(+), 73 deletions(-) diff --git a/res/css/views/rooms/_ReplyTile.scss b/res/css/views/rooms/_ReplyTile.scss index c8f76ee995..f3e204e415 100644 --- a/res/css/views/rooms/_ReplyTile.scss +++ b/res/css/views/rooms/_ReplyTile.scss @@ -15,10 +15,9 @@ limitations under the License. */ .mx_ReplyTile { - padding-top: 2px; - padding-bottom: 2px; - font-size: $font-14px; position: relative; + padding: 2px 0; + font-size: $font-14px; line-height: $font-16px; &.mx_ReplyTile_audio .mx_MFileBody_info_icon::before { @@ -38,86 +37,83 @@ limitations under the License. display: none; } } -} -.mx_ReplyTile > a { - display: flex; - flex-direction: column; - text-decoration: none; - color: $primary-fg-color; -} - -.mx_ReplyTile .mx_RedactedBody { - padding: 4px 0 2px 20px; - - &::before { - height: 13px; - width: 13px; - top: 5px; - } -} - -// We do reply size limiting with CSS to avoid duplicating the TextualBody component. -.mx_ReplyTile .mx_EventTile_content { - $reply-lines: 2; - $line-height: $font-22px; - - pointer-events: none; - - 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: $font-14px !important; + > a { + display: flex; + flex-direction: column; + text-decoration: none; + color: $primary-fg-color; } - // Hide line numbers - .mx_EventTile_lineNumbers { - display: none; + .mx_RedactedBody { + padding: 4px 0 2px 20px; + + &::before { + height: 13px; + width: 13px; + top: 5px; + } } - // Hack to cut content in
     tags too
    -    .mx_EventTile_pre_container > pre {
    -        overflow: hidden;
    +    // We do reply size limiting with CSS to avoid duplicating the TextualBody component.
    +    .mx_EventTile_content {
    +        $reply-lines: 2;
    +        $line-height: $font-22px;
    +
    +        pointer-events: none;
    +
             text-overflow: ellipsis;
             display: -webkit-box;
             -webkit-box-orient: vertical;
             -webkit-line-clamp: $reply-lines;
    -        padding: 4px;
    +        line-height: $line-height;
    +
    +        .mx_EventTile_body.mx_EventTile_bigEmoji {
    +            line-height: $line-height !important;
    +            font-size: $font-14px !important; // Override the big emoji override
    +        }
    +
    +        // Hide line numbers
    +        .mx_EventTile_lineNumbers {
    +            display: none;
    +        }
    +
    +        // 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;
    +        }
         }
     
    -    .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_info {
    +        padding-top: 0;
    +    }
    +
    +    .mx_SenderProfile {
    +        font-size: $font-14px;
    +        line-height: $font-17px;
    +
    +        display: inline-block; // anti-zalgo, with overflow hidden
    +        padding: 0;
    +        margin: 0;
    +
    +        // truncate long display names
    +        overflow: hidden;
    +        white-space: nowrap;
    +        text-overflow: ellipsis;
         }
     }
    -
    -.mx_ReplyTile.mx_ReplyTile_info {
    -    padding-top: 0;
    -}
    -
    -.mx_ReplyTile .mx_SenderProfile {
    -    color: $primary-fg-color;
    -    font-size: $font-14px;
    -    display: inline-block; /* anti-zalgo, with overflow hidden */
    -    overflow: hidden;
    -    cursor: pointer;
    -    padding-left: 0; /* left gutter */
    -    padding-bottom: 0;
    -    padding-top: 0;
    -    margin: 0;
    -    line-height: $font-17px;
    -    /* the next three lines, along with overflow hidden, truncate long display names */
    -    white-space: nowrap;
    -    text-overflow: ellipsis;
    -}
    
    From 25e6a0e5705e27223b98a46fe0d84dd78412702a Mon Sep 17 00:00:00 2001
    From: Robin Townsend 
    Date: Fri, 16 Jul 2021 14:19:36 -0400
    Subject: [PATCH 0394/1171] Match colors of room and user avatars in DMs
    
    Signed-off-by: Robin Townsend 
    ---
     src/components/views/avatars/RoomAvatar.tsx | 6 +++++-
     1 file changed, 5 insertions(+), 1 deletion(-)
    
    diff --git a/src/components/views/avatars/RoomAvatar.tsx b/src/components/views/avatars/RoomAvatar.tsx
    index 8ac8de8233..a07990c3bb 100644
    --- a/src/components/views/avatars/RoomAvatar.tsx
    +++ b/src/components/views/avatars/RoomAvatar.tsx
    @@ -22,6 +22,7 @@ import ImageView from '../elements/ImageView';
     import { MatrixClientPeg } from '../../../MatrixClientPeg';
     import Modal from '../../../Modal';
     import * as Avatar from '../../../Avatar';
    +import DMRoomMap from "../../../utils/DMRoomMap";
     import { replaceableComponent } from "../../../utils/replaceableComponent";
     import { mediaFromMxc } from "../../../customisations/Media";
     import { IOOBData } from '../../../stores/ThreepidInviteStore';
    @@ -131,11 +132,14 @@ export default class RoomAvatar extends React.Component {
             const { room, oobData, viewAvatarOnClick, onClick, ...otherProps } = this.props;
     
             const roomName = room ? room.name : oobData.name;
    +        // If the room is a DM, we use the other user's ID for the color hash
    +        // in order to match the room avatar with their avatar
    +        const idName = room ? (DMRoomMap.shared().getUserIdForRoomId(room.roomId) ?? room.roomId) : null;
     
             return (
                 
    
    From 77c8425c3ce0ab642826c3d0baa996c14ec6260f Mon Sep 17 00:00:00 2001
    From: =?UTF-8?q?=C5=A0imon=20Brandner?= 
    Date: Fri, 16 Jul 2021 20:50:29 +0200
    Subject: [PATCH 0395/1171] Rewrite using TabbedView and improve TS
    MIME-Version: 1.0
    Content-Type: text/plain; charset=UTF-8
    Content-Transfer-Encoding: 8bit
    
    Signed-off-by: Šimon Brandner 
    ---
     .../elements/DesktopCapturerSourcePicker.tsx  | 78 +++++++------------
     1 file changed, 29 insertions(+), 49 deletions(-)
    
    diff --git a/src/components/views/elements/DesktopCapturerSourcePicker.tsx b/src/components/views/elements/DesktopCapturerSourcePicker.tsx
    index 8b7eaca7ea..49a2bda1f7 100644
    --- a/src/components/views/elements/DesktopCapturerSourcePicker.tsx
    +++ b/src/components/views/elements/DesktopCapturerSourcePicker.tsx
    @@ -22,6 +22,7 @@ import classNames from 'classnames';
     import AccessibleButton from './AccessibleButton';
     import { getDesktopCapturerSources } from "matrix-js-sdk/src/webrtc/call";
     import { replaceableComponent } from "../../../utils/replaceableComponent";
    +import TabbedView, { Tab, TabLocation } from '../../structures/TabbedView';
     
     export interface DesktopCapturerSource {
         id: string;
    @@ -41,11 +42,11 @@ export interface ExistingSourceIProps {
     }
     
     export class ExistingSource extends React.Component {
    -    constructor(props) {
    +    constructor(props: ExistingSourceIProps) {
             super(props);
         }
     
    -    onClick = (ev) => {
    +    private onClick = (): void => {
             this.props.onSelect(this.props.source);
         };
     
    @@ -59,7 +60,8 @@ export class ExistingSource extends React.Component {
                 
    +                onClick={this.onClick}
    +            >
                      {
    -    interval;
    +> {
    +    interval: number;
     
    -    constructor(props) {
    +    constructor(props: PickerIProps) {
             super(props);
     
             this.state = {
    @@ -116,39 +118,24 @@ export default class DesktopCapturerSourcePicker extends React.Component<
             clearInterval(this.interval);
         }
     
    -    onSelect = (source) => {
    +    private onSelect = (source: DesktopCapturerSource): void => {
             this.setState({ selectedSource: source });
         };
     
    -    onShare = () => {
    +    private onShare = (): void => {
             this.props.onFinished(this.state.selectedSource);
         };
     
    -    onScreensClick = () => {
    -        if (this.state.selectedTab === Tabs.Screens) return;
    -        this.setState({
    -            selectedTab: Tabs.Screens,
    -            selectedSource: null,
    -        });
    +    private onTabChange = (): void => {
    +        this.setState({ selectedSource: null });
         };
     
    -    onWindowsClick = () => {
    -        if (this.state.selectedTab === Tabs.Windows) return;
    -        this.setState({
    -            selectedTab: Tabs.Windows,
    -            selectedSource: null,
    -        });
    -    };
    -
    -    onCloseClick = () => {
    +    private onCloseClick = (): void => {
             this.props.onFinished(null);
         };
     
    -    render() {
    -        const sources = this.state.sources.filter((source) => {
    -            return source.id.startsWith(this.state.selectedTab);
    -        });
    -        const sourceElements = sources.map((source) => {
    +    private getTab(type: "screen" | "window", label: string): Tab {
    +        const sources = this.state.sources.filter((source) => source.id.startsWith(type)).map((source) => {
                 return (
                     
    +                { sources }
    +            
    + )); + } + + render() { + const tabs = [ + this.getTab("screen", _t("Share entire screen")), + this.getTab("window", _t("Application window")), + ]; return ( -
    - - {_t("Screens")} - - - {_t("Windows")} - -
    -
    - { sourceElements } -
    + Date: Fri, 16 Jul 2021 15:52:31 +0000 Subject: [PATCH 0396/1171] Translated using Weblate (German) Currently translated at 99.0% (3023 of 3051 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 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json index 1a977aec90..44376996c4 100644 --- a/src/i18n/strings/de_DE.json +++ b/src/i18n/strings/de_DE.json @@ -3462,9 +3462,9 @@ "User %(userId)s is already invited to the room": "%(userId)s ist schon eingeladen", "Unable to copy a link to the room to the clipboard.": "Der Link zum Raum konnte nicht kopiert werden.", "Unable to copy room link": "Raumlink konnte nicht kopiert werden", - "Integration manager": "Integrationsmanager", + "Integration manager": "Integrationsverwaltung", "User Directory": "Benutzerverzeichnis", - "Your %(brand)s doesn't allow you to use an integration manager to do this. Please contact an admin.": "%(brand)s erlaubt dir nicht, einen Integrationsmanager dafür zu verwenden. Bitte kontaktiere einen Admin.", + "Your %(brand)s doesn't allow you to use an integration manager to do this. Please contact an admin.": "Dein %(brand)s erlaubt dir nicht, eine Integrationsverwaltung zu verwenden, um dies zu tun. Bitte kontaktiere einen Administrator.", "Copy Link": "Link kopieren", "Transfer Failed": "Übertragen fehlgeschlagen", "Unable to transfer call": "Übertragen des Anrufs fehlgeschlagen", From 0c522ca4dfcc6c3d61dd217b054d227efa8d707b Mon Sep 17 00:00:00 2001 From: Paulo Pinto Date: Fri, 16 Jul 2021 15:29:05 +0000 Subject: [PATCH 0397/1171] Translated using Weblate (Greek) Currently translated at 26.7% (817 of 3051 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/el/ --- src/i18n/strings/el.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/el.json b/src/i18n/strings/el.json index 8700abbff1..4a485ad7b4 100644 --- a/src/i18n/strings/el.json +++ b/src/i18n/strings/el.json @@ -925,5 +925,6 @@ "Done": "Τέλος", "Not Trusted": "Μη Έμπιστο", "You're already in a call with this person.": "Είστε ήδη σε κλήση με αυτόν τον χρήστη.", - "Already in call": "Ήδη σε κλήση" + "Already in call": "Ήδη σε κλήση", + "Identity server is": "Ο διακομιστής ταυτοποίησης είναι" } From 26beef9cbf3893ece8dcc2c1507d162f0ad38c9c Mon Sep 17 00:00:00 2001 From: Paulo Pinto Date: Fri, 16 Jul 2021 15:55:25 +0000 Subject: [PATCH 0398/1171] Translated using Weblate (Spanish) Currently translated at 99.0% (3021 of 3051 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 | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/es.json b/src/i18n/strings/es.json index a06de53821..f98b3584ef 100644 --- a/src/i18n/strings/es.json +++ b/src/i18n/strings/es.json @@ -3420,5 +3420,16 @@ "%(targetName)s accepted an invitation": "%(targetName)s ha aceptado una invitación", "%(targetName)s accepted the invitation for %(displayName)s": "%(targetName)s ha aceptado la invitación a %(displayName)s", "We sent the others, but the below people couldn't be invited to ": "Hemos enviado el resto, pero no hemos podido invitar las siguientes personas a la sala ", - "Some invites couldn't be sent": "No se han podido enviar algunas invitaciones" + "Some invites couldn't be sent": "No se han podido enviar algunas invitaciones", + "Integration manager": "Administrador de integración", + "Your %(brand)s doesn't allow you to use an integration manager to do this. Please contact an admin.": "%(brand)s no utilizar un \"gestor de integración\" para hacer esto. Por favor, contacta con un administrador.", + "Using this widget may share data with %(widgetDomain)s & your integration manager.": "Usar este widget puede resultar en que se compartan datos con %(widgetDomain)s y su administrador de integración.", + "Integration managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "Los administradores de integración reciben datos de configuración, y pueden modificar widgets, enviar invitaciones de sala, y establecer niveles de poder en tu nombre.", + "Use an integration manager to manage bots, widgets, and sticker packs.": "Utiliza un administrador de integración para gestionar los bots, los widgets y los paquetes de pegatinas.", + "Use an integration manager (%(serverName)s) to manage bots, widgets, and sticker packs.": "Usar un gestor de integraciones (%(serverName)s) para manejar los bots, widgets y paquetes de pegatinas.", + "Identity server": "Servidor de identidad", + "Identity server (%(server)s)": "Servidor de identidad %(server)s", + "Could not connect to identity server": "No se ha podido conectar al servidor de identidad", + "Not a valid identity server (status code %(code)s)": "No es un servidor de identidad válido (código de estado %(code)s)", + "Identity server URL must be HTTPS": "La URL del servidor de identidad debe ser tipo HTTPS" } From 19d12ea13998480a076314fe04715d09dc3da8a5 Mon Sep 17 00:00:00 2001 From: Paulo Pinto Date: Fri, 16 Jul 2021 15:51:55 +0000 Subject: [PATCH 0399/1171] Translated using Weblate (French) Currently translated at 99.2% (3029 of 3051 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/fr/ --- src/i18n/strings/fr.json | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/fr.json b/src/i18n/strings/fr.json index 9d047887ba..7ad2489b70 100644 --- a/src/i18n/strings/fr.json +++ b/src/i18n/strings/fr.json @@ -3456,5 +3456,17 @@ "%(targetName)s accepted an invitation": "%(targetName)s a accepté une invitation", "%(targetName)s accepted the invitation for %(displayName)s": "%(targetName)s a accepté l’invitation pour %(displayName)s", "Some invites couldn't be sent": "Certaines invitations n’ont pas pu être envoyées", - "We sent the others, but the below people couldn't be invited to ": "Nous avons envoyé les invitations, mais les personnes ci-dessous n’ont pas pu être invitées à rejoindre " + "We sent the others, but the below people couldn't be invited to ": "Nous avons envoyé les invitations, mais les personnes ci-dessous n’ont pas pu être invitées à rejoindre ", + "Integration manager": "Gestionnaire d’intégration", + "Your %(brand)s doesn't allow you to use an integration manager to do this. Please contact an admin.": "Votre %(brand)s ne vous autorise pas à utiliser un gestionnaire d’intégrations pour faire ça. Contactez un administrateur.", + "Using this widget may share data with %(widgetDomain)s & your integration manager.": "L’utilisation de ce widget pourrait partager des données avec %(widgetDomain)s et votre gestionnaire d’intégrations.", + "Identity server is": "Le serveur d'identité est", + "Integration managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "Les gestionnaires d’intégrations reçoivent les données de configuration et peuvent modifier les widgets, envoyer des invitations aux salons et définir les rangs à votre place.", + "Use an integration manager to manage bots, widgets, and sticker packs.": "Utilisez un gestionnaire d’intégrations pour gérer les robots, les widgets et les jeux d’autocollants.", + "Use an integration manager (%(serverName)s) to manage bots, widgets, and sticker packs.": "Utilisez un gestionnaire d’intégrations (%(serverName)s) pour gérer les robots, les widgets et les jeux d’autocollants.", + "Identity server": "Serveur d’identité", + "Identity server (%(server)s)": "Serveur d’identité (%(server)s)", + "Could not connect to identity server": "Impossible de se connecter au serveur d’identité", + "Not a valid identity server (status code %(code)s)": "Serveur d’identité non valide (code de statut %(code)s)", + "Identity server URL must be HTTPS": "L’URL du serveur d’identité doit être en HTTPS" } From 43d8de5cd2e97d3af85e17662a3fedc7c796ee3f Mon Sep 17 00:00:00 2001 From: Paulo Pinto Date: Fri, 16 Jul 2021 15:52:36 +0000 Subject: [PATCH 0400/1171] Translated using Weblate (Hebrew) Currently translated at 85.7% (2617 of 3051 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/he/ --- src/i18n/strings/he.json | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/he.json b/src/i18n/strings/he.json index 5baa1d7c67..8845de8374 100644 --- a/src/i18n/strings/he.json +++ b/src/i18n/strings/he.json @@ -2785,5 +2785,17 @@ "Your homeserver was unreachable and was not able to log you in. Please try again. If this continues, please contact your homeserver administrator.": "לא ניתן היה להגיע לשרת הבית שלך ולא היה ניתן להתחבר. נסה שוב. אם זה נמשך, אנא פנה למנהל שרת הבית שלך.", "Try again": "נסה שוב", "We asked the browser to remember which homeserver you use to let you sign in, but unfortunately your browser has forgotten it. Go to the sign in page and try again.": "ביקשנו מהדפדפן לזכור באיזה שרת בית אתה משתמש כדי לאפשר לך להיכנס, אך למרבה הצער הדפדפן שלך שכח אותו. עבור לדף הכניסה ונסה שוב.", - "We couldn't log you in": "לא הצלחנו להתחבר אליך" + "We couldn't log you in": "לא הצלחנו להתחבר אליך", + "Integration manager": "מנהל אינטגרציה", + "Your %(brand)s doesn't allow you to use an integration manager to do this. Please contact an admin.": "%(brand)s שלכם אינו מאפשר לך להשתמש במנהל שילוב לשם כך. אנא צרו קשר עם מנהל מערכת.", + "Using this widget may share data with %(widgetDomain)s & your integration manager.": "שימוש ביישומון זה עשוי לשתף נתונים עם %(widgetDomain)s ומנהל האינטגרציה שלך.", + "Identity server is": "שרת ההזדהות הינו", + "Integration managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "מנהלי שילוב מקבלים נתוני תצורה ויכולים לשנות ווידג'טים, לשלוח הזמנות לחדר ולהגדיר רמות הספק מטעמכם.", + "Use an integration manager to manage bots, widgets, and sticker packs.": "השתמש במנהל שילוב לניהול בוטים, ווידג'טים וחבילות מדבקות.", + "Use an integration manager (%(serverName)s) to manage bots, widgets, and sticker packs.": "השתמש במנהל שילוב (%(serverName)s) לניהול בוטים, ווידג'טים וחבילות מדבקות.", + "Identity server": "שרת הזדהות", + "Identity server (%(server)s)": "שרת הזדהות (%(server)s)", + "Could not connect to identity server": "לא ניתן להתחבר אל שרת הזיהוי", + "Not a valid identity server (status code %(code)s)": "שרת זיהוי לא מאושר(קוד סטטוס %(code)s)", + "Identity server URL must be HTTPS": "הזיהוי של כתובת השרת חייבת להיות מאובטחת ב- HTTPS" } From 393bc0328e4095f83ddf6de8594b7fd002df0ec1 Mon Sep 17 00:00:00 2001 From: Paulo Pinto Date: Fri, 16 Jul 2021 15:52:41 +0000 Subject: [PATCH 0401/1171] Translated using Weblate (Hungarian) Currently translated at 99.4% (3033 of 3051 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 | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/hu.json b/src/i18n/strings/hu.json index ffad836a2b..55adba87b2 100644 --- a/src/i18n/strings/hu.json +++ b/src/i18n/strings/hu.json @@ -3488,5 +3488,17 @@ "To view all keyboard shortcuts, click here.": "A billentyűzet kombinációk megjelenítéséhez kattintson ide.", "Keyboard shortcuts": "Billentyűzet kombinációk", "Use Ctrl + F to search timeline": "Ctrl + F az idővonalon való kereséshez", - "User %(userId)s is already invited to the room": "%(userId)s felhasználó már kapott meghívót a szobába" + "User %(userId)s is already invited to the room": "%(userId)s felhasználó már kapott meghívót a szobába", + "Integration manager": "Integrációs Menedzser", + "Your %(brand)s doesn't allow you to use an integration manager to do this. Please contact an admin.": "A %(brand)sod nem használhat ehhez Integrációs Menedzsert. Kérlek vedd fel a kapcsolatot az adminisztrátorral.", + "Using this widget may share data with %(widgetDomain)s & your integration manager.": "Ennek a kisalkalmazásnak a használata adatot oszthat meg a(z) %(widgetDomain)s oldallal és az Integrációkezelővel.", + "Identity server is": "Azonosítási szerver", + "Integration managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "Integrációs Menedzser megkapja a konfigurációt, módosíthat kisalkalmazásokat, szobához meghívót küldhet és a hozzáférési szintet beállíthatja helyetted.", + "Use an integration manager to manage bots, widgets, and sticker packs.": "Használj Integrációs Menedzsert a botok, kisalkalmazások és matrica csomagok kezeléséhez.", + "Use an integration manager (%(serverName)s) to manage bots, widgets, and sticker packs.": "Használj Integrációs Menedzsert (%(serverName)s) a botok, kisalkalmazások és matrica csomagok kezeléséhez.", + "Identity server": "Azonosító szerver", + "Identity server (%(server)s)": "Azonosítási kiszolgáló (%(server)s)", + "Could not connect to identity server": "Az Azonosítási Szerverhez nem lehet csatlakozni", + "Not a valid identity server (status code %(code)s)": "Az Azonosítási Szerver nem érvényes (státusz kód: %(code)s)", + "Identity server URL must be HTTPS": "Az Azonosítási Szerver URL-jének HTTPS-nek kell lennie" } From 14846d5e14340bace54d40c54fd7d219e18bce88 Mon Sep 17 00:00:00 2001 From: Paulo Pinto Date: Fri, 16 Jul 2021 15:53:55 +0000 Subject: [PATCH 0402/1171] Translated using Weblate (Malayalam) Currently translated at 3.4% (105 of 3051 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/ml/ --- src/i18n/strings/ml.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/ml.json b/src/i18n/strings/ml.json index 6183fe7de2..0aee8b5581 100644 --- a/src/i18n/strings/ml.json +++ b/src/i18n/strings/ml.json @@ -130,5 +130,7 @@ "Checking for an update...": "അപ്ഡേറ്റ് ഉണ്ടോ എന്ന് തിരയുന്നു...", "Explore rooms": "മുറികൾ കണ്ടെത്തുക", "Sign In": "പ്രവേശിക്കുക", - "Create Account": "അക്കൗണ്ട് സൃഷ്ടിക്കുക" + "Create Account": "അക്കൗണ്ട് സൃഷ്ടിക്കുക", + "Integration manager": "സംയോജക മാനേജർ", + "Identity server": "തിരിച്ചറിയൽ സെർവർ" } From 58377f32c2afea4105f9d1f3f3f8adc75cbe01f5 Mon Sep 17 00:00:00 2001 From: Paulo Pinto Date: Fri, 16 Jul 2021 15:51:01 +0000 Subject: [PATCH 0403/1171] Translated using Weblate (Dutch) Currently translated at 99.4% (3033 of 3051 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 | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/nl.json b/src/i18n/strings/nl.json index cd70f4c9bb..dff6a15d28 100644 --- a/src/i18n/strings/nl.json +++ b/src/i18n/strings/nl.json @@ -3381,5 +3381,17 @@ "Keyboard shortcuts": "Sneltoetsen", "Use Ctrl + F to search timeline": "Gebruik Ctrl +F om te zoeken in de tijdlijn", "Use Command + F to search timeline": "Gebruik Command + F om te zoeken in de tijdlijn", - "User %(userId)s is already invited to the room": "De gebruiker %(userId)s is al uitgenodigd voor dit gesprek" + "User %(userId)s is already invited to the room": "De gebruiker %(userId)s is al uitgenodigd voor dit gesprek", + "Integration manager": "Integratiebeheerder", + "Your %(brand)s doesn't allow you to use an integration manager to do this. Please contact an admin.": "Uw %(brand)s laat u geen integratiebeheerder gebruiken om dit te doen. Neem contact op met een beheerder.", + "Using this widget may share data with %(widgetDomain)s & your integration manager.": "Deze widget gebruiken deelt mogelijk gegevens met %(widgetDomain)s en uw integratiebeheerder.", + "Identity server is": "Identiteitsserver is", + "Integration managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "Integratiebeheerders ontvangen configuratie-informatie en kunnen widgets aanpassen, gespreksuitnodigingen versturen en machtsniveau’s namens u aanpassen.", + "Use an integration manager to manage bots, widgets, and sticker packs.": "Gebruik een integratiebeheerder om robots, widgets en stickerpakketten te beheren.", + "Use an integration manager (%(serverName)s) to manage bots, widgets, and sticker packs.": "Gebruik een integratiebeheerder (%(serverName)s) om robots, widgets en stickerpakketten te beheren.", + "Identity server": "Identiteitsserver", + "Identity server (%(server)s)": "Identiteitsserver (%(server)s)", + "Could not connect to identity server": "Kon geen verbinding maken met de identiteitsserver", + "Not a valid identity server (status code %(code)s)": "Geen geldige identiteitsserver (statuscode %(code)s)", + "Identity server URL must be HTTPS": "Identiteitsserver-URL moet HTTPS zijn" } From 571f48cad97da5d2aa57013764792c1d45a48d0c Mon Sep 17 00:00:00 2001 From: Paulo Pinto Date: Fri, 16 Jul 2021 14:03:20 +0000 Subject: [PATCH 0404/1171] Translated using Weblate (Portuguese) Currently translated at 15.2% (465 of 3051 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/pt/ --- src/i18n/strings/pt.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/pt.json b/src/i18n/strings/pt.json index 4047aae760..32984092e4 100644 --- a/src/i18n/strings/pt.json +++ b/src/i18n/strings/pt.json @@ -572,5 +572,7 @@ "Your user agent": "O seu user agent", "Explore rooms": "Explorar rooms", "Sign In": "Iniciar sessão", - "Create Account": "Criar conta" + "Create Account": "Criar conta", + "Not a valid identity server (status code %(code)s)": "Servidor de Identidade inválido (código de status %(code)s)", + "Identity server URL must be HTTPS": "O link do servidor de identidade deve começar com HTTPS" } From b54ec69c9efc9b9d568b5ef67836e08f8fa03928 Mon Sep 17 00:00:00 2001 From: Paulo Pinto Date: Fri, 16 Jul 2021 15:54:47 +0000 Subject: [PATCH 0405/1171] Translated using Weblate (Portuguese (Brazil)) Currently translated at 90.2% (2754 of 3051 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/pt_BR/ --- src/i18n/strings/pt_BR.json | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/pt_BR.json b/src/i18n/strings/pt_BR.json index e19febd6ef..7b45b30f4b 100644 --- a/src/i18n/strings/pt_BR.json +++ b/src/i18n/strings/pt_BR.json @@ -3110,5 +3110,17 @@ "Inviting...": "Convidando...", "Invite by username": "Convidar por nome de usuário", "Support": "Suporte", - "Original event source": "Fonte do evento original" + "Original event source": "Fonte do evento original", + "Integration manager": "Gerenciador de integrações", + "Your %(brand)s doesn't allow you to use an integration manager to do this. Please contact an admin.": "Seu %(brand)s não permite que você use o gerenciador de integrações para fazer isso. Entre em contato com o administrador.", + "Using this widget may share data with %(widgetDomain)s & your integration manager.": "Se você usar esse widget, os dados poderão ser compartilhados com %(widgetDomain)s & seu gerenciador de integrações.", + "Identity server is": "O servidor de identificação é", + "Integration managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "O gerenciador de integrações recebe dados de configuração e pode modificar widgets, enviar convites para salas e definir níveis de permissão em seu nome.", + "Use an integration manager to manage bots, widgets, and sticker packs.": "Use o gerenciador de integrações para gerenciar bots, widgets e pacotes de figurinhas.", + "Use an integration manager (%(serverName)s) to manage bots, widgets, and sticker packs.": "Use o gerenciador de integrações em (%(serverName)s) para gerenciar bots, widgets e pacotes de figurinhas.", + "Identity server": "Servidor de identidade", + "Identity server (%(server)s)": "Servidor de identidade (%(server)s)", + "Could not connect to identity server": "Não foi possível conectar-se ao servidor de identidade", + "Not a valid identity server (status code %(code)s)": "Servidor de identidade inválido (código de status %(code)s)", + "Identity server URL must be HTTPS": "O link do servidor de identidade deve começar com HTTPS" } From 0bc7830f5b0cedefc1b5b13b4042283d0e10d38d Mon Sep 17 00:00:00 2001 From: Paulo Pinto Date: Fri, 16 Jul 2021 15:54:52 +0000 Subject: [PATCH 0406/1171] Translated using Weblate (Russian) Currently translated at 91.3% (2787 of 3051 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/ru/ --- src/i18n/strings/ru.json | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/ru.json b/src/i18n/strings/ru.json index 91b9919d0a..e562ce074b 100644 --- a/src/i18n/strings/ru.json +++ b/src/i18n/strings/ru.json @@ -3219,5 +3219,17 @@ "Send and receive voice messages": "Отправлять и получать голосовые сообщения", "%(deviceId)s from %(ip)s": "%(deviceId)s с %(ip)s", "The user you called is busy.": "Вызываемый пользователь занят.", - "User Busy": "Пользователь занят" + "User Busy": "Пользователь занят", + "Integration manager": "Менеджер интеграции", + "Your %(brand)s doesn't allow you to use an integration manager to do this. Please contact an admin.": "Ваш %(brand)s не позволяет вам использовать для этого Менеджер Интеграции. Пожалуйста, свяжитесь с администратором.", + "Using this widget may share data with %(widgetDomain)s & your integration manager.": "Используя этот виджет, вы можете делиться данными с %(widgetDomain)s и вашим Менеджером Интеграции.", + "Identity server is": "Сервер идентификации", + "Integration managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "Менеджеры интеграции получают данные конфигурации и могут изменять виджеты, отправлять приглашения в комнаты и устанавливать уровни доступа от вашего имени.", + "Use an integration manager to manage bots, widgets, and sticker packs.": "Используйте Менеджер интеграциями для управления ботами, виджетами и стикерами.", + "Use an integration manager (%(serverName)s) to manage bots, widgets, and sticker packs.": "Используйте менеджер интеграций %(serverName)s для управления ботами, виджетами и стикерами.", + "Identity server": "Сервер идентификаций", + "Identity server (%(server)s)": "Сервер идентификации (%(server)s)", + "Could not connect to identity server": "Не смог подключиться к серверу идентификации", + "Not a valid identity server (status code %(code)s)": "Неправильный Сервер идентификации (код статуса %(code)s)", + "Identity server URL must be HTTPS": "URL-адрес сервера идентификации должен быть HTTPS" } From d9acbb85b592ee5369500d3df006d3de36d55c11 Mon Sep 17 00:00:00 2001 From: Paulo Pinto Date: Fri, 16 Jul 2021 15:55:31 +0000 Subject: [PATCH 0407/1171] Translated using Weblate (Swedish) Currently translated at 97.1% (2963 of 3051 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 | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/sv.json b/src/i18n/strings/sv.json index b36af42f5e..e5e9cc5d34 100644 --- a/src/i18n/strings/sv.json +++ b/src/i18n/strings/sv.json @@ -3356,5 +3356,16 @@ "We sent the others, but the below people couldn't be invited to ": "Vi skickade de andra, men personerna nedan kunde inte bjudas in till ", "What this user is writing is wrong.\nThis will be reported to the room moderators.": "Vad användaren skriver är fel.\nDet här kommer att anmälas till rumsmoderatorerna.", "Report to moderators prototype. In rooms that support moderation, the `report` button will let you report abuse to room moderators": "Prototyp av anmälan till moderatorer. I rum som söder moderering så kommer `anmäl`-knappen att låta dig anmäla olämpligt beteende till rummets moderatorer", - "Report": "Rapportera" + "Report": "Rapportera", + "Integration manager": "Integrationshanterare", + "Your %(brand)s doesn't allow you to use an integration manager to do this. Please contact an admin.": "Din %(brand)s tillåter dig inte att använda en integrationshanterare för att göra detta. Vänligen kontakta en administratör.", + "Using this widget may share data with %(widgetDomain)s & your integration manager.": "Att använda denna widget kan dela data med %(widgetDomain)s och din integrationshanterare.", + "Integration managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "Integrationshanterare får konfigurationsdata och kan ändra widgetar, skicka rumsinbjudningar och ställa in behörighetsnivåer å dina vägnar.", + "Use an integration manager to manage bots, widgets, and sticker packs.": "Använd en integrationshanterare för att hantera bottar, widgets och dekalpaket.", + "Use an integration manager (%(serverName)s) to manage bots, widgets, and sticker packs.": "Använd en integrationshanterare (%(serverName)s) för att hantera bottar, widgets och dekalpaket.", + "Identity server": "Identitetsserver", + "Identity server (%(server)s)": "Identitetsserver (%(server)s)", + "Could not connect to identity server": "Kunde inte ansluta till identitetsservern", + "Not a valid identity server (status code %(code)s)": "Inte en giltig identitetsserver (statuskod %(code)s)", + "Identity server URL must be HTTPS": "URL för identitetsserver måste vara HTTPS" } From d74a8574b404089e70a5e44164cedb3eda4b7aab Mon Sep 17 00:00:00 2001 From: Paulo Pinto Date: Fri, 16 Jul 2021 15:50:36 +0000 Subject: [PATCH 0408/1171] Translated using Weblate (Chinese (Simplified)) Currently translated at 99.0% (3021 of 3051 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 | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/zh_Hans.json b/src/i18n/strings/zh_Hans.json index 88ebb8f4cf..2472ac479e 100644 --- a/src/i18n/strings/zh_Hans.json +++ b/src/i18n/strings/zh_Hans.json @@ -3383,5 +3383,17 @@ "%(targetName)s accepted an invitation": "%(targetName)s 已接受邀请", "%(targetName)s accepted the invitation for %(displayName)s": "%(targetName)s 已接受 %(displayName)s 的邀请", "Some invites couldn't be sent": "部分邀请无法送达", - "We sent the others, but the below people couldn't be invited to ": "我们已向其他人发送邀请,除了以下无法邀请至 的人" + "We sent the others, but the below people couldn't be invited to ": "我们已向其他人发送邀请,除了以下无法邀请至 的人", + "Integration manager": "集成管理器", + "Your %(brand)s doesn't allow you to use an integration manager to do this. Please contact an admin.": "你的 %(brand)s 不允许你使用集成管理器来完成此操作。请联系管理员。", + "Using this widget may share data with %(widgetDomain)s & your integration manager.": "使用此挂件可能会和 %(widgetDomain)s 及您的集成管理器共享数据 。", + "Identity server is": "身份认证服务器是", + "Integration managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "集成管理器接收配置数据,并可以以你的名义修改挂件、发送聊天室邀请及设置权限级别。", + "Use an integration manager to manage bots, widgets, and sticker packs.": "使用集成管理器以管理机器人、挂件和贴纸包。", + "Use an integration manager (%(serverName)s) to manage bots, widgets, and sticker packs.": "使用集成管理器 (%(serverName)s) 以管理机器人、挂件和贴纸包。", + "Identity server": "身份服务器", + "Identity server (%(server)s)": "身份服务器(%(server)s)", + "Could not connect to identity server": "无法连接到身份服务器", + "Not a valid identity server (status code %(code)s)": "不是有效的身份服务器(状态码 %(code)s)", + "Identity server URL must be HTTPS": "身份服务器连接必须是 HTTPS" } From 3240af946966e06e4d9da0a63e52120ecf6476c0 Mon Sep 17 00:00:00 2001 From: Paulo Pinto Date: Fri, 16 Jul 2021 15:50:42 +0000 Subject: [PATCH 0409/1171] Translated using Weblate (Chinese (Traditional)) Currently translated at 99.4% (3033 of 3051 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 | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/zh_Hant.json b/src/i18n/strings/zh_Hant.json index b3213e9190..cf0fa2d365 100644 --- a/src/i18n/strings/zh_Hant.json +++ b/src/i18n/strings/zh_Hant.json @@ -3496,5 +3496,17 @@ "Keyboard shortcuts": "鍵盤快捷鍵", "Use Ctrl + F to search timeline": "使用 Ctrl + F 來搜尋時間軸", "Use Command + F to search timeline": "使用 Command + F 來搜尋時間軸", - "User %(userId)s is already invited to the room": "使用者 %(userId)s 已被邀請至聊天室" + "User %(userId)s is already invited to the room": "使用者 %(userId)s 已被邀請至聊天室", + "Integration manager": "整合管理員", + "Your %(brand)s doesn't allow you to use an integration manager to do this. Please contact an admin.": "您的 %(brand)s 不允許您使用整合管理員來執行此動作。請聯絡管理員。", + "Using this widget may share data with %(widgetDomain)s & your integration manager.": "使用這個小工具可能會與 %(widgetDomain)s 以及您的整合管理員分享資料 。", + "Identity server is": "身分認證伺服器是", + "Integration managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "整合管理員接收設定資料,並可以修改小工具、傳送聊天室邀請並設定權限等級。", + "Use an integration manager to manage bots, widgets, and sticker packs.": "使用整合管理員以管理機器人、小工具與貼紙包。", + "Use an integration manager (%(serverName)s) to manage bots, widgets, and sticker packs.": "使用整合管理員 (%(serverName)s) 以管理機器人、小工具與貼紙包。", + "Identity server": "身份識別伺服器", + "Identity server (%(server)s)": "身份識別伺服器 (%(server)s)", + "Could not connect to identity server": "無法連線至身份識別伺服器", + "Not a valid identity server (status code %(code)s)": "不是有效的身份識別伺服器(狀態碼 %(code)s)", + "Identity server URL must be HTTPS": "身份識別伺服器 URL 必須為 HTTPS" } From 657962fdfc95bdf719d305173103d803a3385080 Mon Sep 17 00:00:00 2001 From: Paulo Pinto Date: Fri, 16 Jul 2021 15:34:13 +0000 Subject: [PATCH 0410/1171] Translated using Weblate (Arabic) Currently translated at 47.6% (1455 of 3051 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/ar/ --- src/i18n/strings/ar.json | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/ar.json b/src/i18n/strings/ar.json index cc63995e0f..a14e6f8ce8 100644 --- a/src/i18n/strings/ar.json +++ b/src/i18n/strings/ar.json @@ -1552,5 +1552,15 @@ "Too Many Calls": "مكالمات كثيرة جدا", "Call failed because webcam or microphone could not be accessed. Check that:": "فشلت المكالمة لعدم امكانية الوصل للميكروفون او الكاميرا , من فضلك قم بالتأكد.", "Call failed because microphone could not be accessed. Check that a microphone is plugged in and set up correctly.": "فشلت المكالمة لعدم امكانية الوصل للميكروفون , تأكد من ان المكروفون متصل وتم اعداده بشكل صحيح.", - "Explore rooms": "استكشِف الغرف" + "Explore rooms": "استكشِف الغرف", + "Using this widget may share data with %(widgetDomain)s & your integration manager.": "قد يؤدي استخدام عنصر واجهة المستخدم هذا إلى مشاركة البيانات مع %(widgetDomain)s ومدير التكامل الخاص بك.", + "Identity server is": "خادم الهوية هو", + "Integration managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "يتلقى مديرو التكامل بيانات الضبط ، ويمكنهم تعديل عناصر واجهة المستخدم ، وإرسال دعوات الغرف ، وتعيين مستويات القوة نيابة عنك.", + "Use an integration manager to manage bots, widgets, and sticker packs.": "استخدم مدير التكامل لإدارة الروبوتات وعناصر الواجهة وحزم الملصقات.", + "Use an integration manager (%(serverName)s) to manage bots, widgets, and sticker packs.": "استخدم مدير التكامل (%(serverName)s) لإدارة الروبوتات وعناصر الواجهة وحزم الملصقات.", + "Identity server": "خادم الهوية", + "Identity server (%(server)s)": "خادمة الهوية (%(server)s)", + "Could not connect to identity server": "تعذر الاتصال بخادم هوية", + "Not a valid identity server (status code %(code)s)": "خادم هوية مردود (رقم الحال %(code)s)", + "Identity server URL must be HTTPS": "يجب أن يكون رابط (URL) خادم الهوية HTTPS" } From a4e7a10d880ca73dc576417d1db096cf8ce0483a Mon Sep 17 00:00:00 2001 From: Paulo Pinto Date: Fri, 16 Jul 2021 15:55:47 +0000 Subject: [PATCH 0411/1171] Translated using Weblate (Ukrainian) Currently translated at 47.6% (1455 of 3051 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/uk/ --- src/i18n/strings/uk.json | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/uk.json b/src/i18n/strings/uk.json index 67ee136515..df0fd45b5c 100644 --- a/src/i18n/strings/uk.json +++ b/src/i18n/strings/uk.json @@ -1630,5 +1630,14 @@ "Send text messages as you in this room": "Надіслати текстові повідомлення у цю кімнату від свого імені", "Send messages as you in your active room": "Надіслати повідомлення у свою активну кімнату від свого імені", "Send messages as you in this room": "Надіслати повідомлення у цю кімнату від свого імені", - "Sends the given message as a spoiler": "Надсилає вказане повідомлення згорненим" + "Sends the given message as a spoiler": "Надсилає вказане повідомлення згорненим", + "Integration manager": "Менеджер інтеграцій", + "Your %(brand)s doesn't allow you to use an integration manager to do this. Please contact an admin.": "Ваш %(brand)s не дозволяє вам використовувати для цього менеджер інтеграцій. Зверніться, будь ласка, до адміністратора.", + "Using this widget may share data with %(widgetDomain)s & your integration manager.": "Користування цим знадобом може призвести до поширення ваших даних з %(widgetDomain)s та вашим менеджером інтеграцій.", + "Integration managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "Менеджери інтеграцій отримують дані конфігурації та можуть змінювати знадоби, надсилати запрошення у кімнати й встановлювати рівні повноважень від вашого імені.", + "Use an integration manager to manage bots, widgets, and sticker packs.": "Використовувати менеджер інтеграцій для керування ботами, знадобами та паками наліпок.", + "Use an integration manager (%(serverName)s) to manage bots, widgets, and sticker packs.": "Використовувати менеджер інтеграцій %(serverName)s для керування ботами, знадобами та паками наліпок.", + "Identity server": "Сервер ідентифікації", + "Identity server (%(server)s)": "Сервер ідентифікації (%(server)s)", + "Could not connect to identity server": "Неможливо під'єднатись до сервера ідентифікації" } From 1ccdb9dda884f6f52097a740090b689fb22dbfac Mon Sep 17 00:00:00 2001 From: Paulo Pinto Date: Fri, 16 Jul 2021 15:53:20 +0000 Subject: [PATCH 0412/1171] Translated using Weblate (Korean) Currently translated at 47.7% (1457 of 3051 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/ko/ --- src/i18n/strings/ko.json | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/ko.json b/src/i18n/strings/ko.json index f817dbc26b..c6e48d2a58 100644 --- a/src/i18n/strings/ko.json +++ b/src/i18n/strings/ko.json @@ -1667,5 +1667,13 @@ "The signing key you provided matches the signing key you received from %(userId)s's session %(deviceId)s. Session marked as verified.": "사용자 %(userId)s의 세션 %(deviceId)s에서 받은 서명 키와 당신이 제공한 서명 키가 일치합니다. 세션이 검증되었습니다.", "Show more": "더 보기", "Changing password will currently reset any end-to-end encryption keys on all sessions, making encrypted chat history unreadable, unless you first export your room keys and re-import them afterwards. In future this will be improved.": "비밀번호를 변경한다면 방의 암호화 키를 내보낸 후 다시 가져오지 않는 이상 모든 종단간 암호화 키는 초기화 될 것이고, 암호화된 대화 내역은 읽을 수 없게 될 것입니다. 이 문제는 추후에 개선될 것입니다.", - "Create Account": "계정 만들기" + "Create Account": "계정 만들기", + "Integration manager": "통합 관리자", + "Using this widget may share data with %(widgetDomain)s & your integration manager.": "이 위젯을 사용하면 %(widgetDomain)s & 통합 관리자와 데이터를 공유합니다.", + "Identity server is": "ID 서버:", + "Identity server": "ID 서버", + "Identity server (%(server)s)": "ID 서버 (%(server)s)", + "Could not connect to identity server": "ID 서버에 연결할 수 없음", + "Not a valid identity server (status code %(code)s)": "올바르지 않은 ID 서버 (상태 코드 %(code)s)", + "Identity server URL must be HTTPS": "ID 서버 URL은 HTTPS이어야 함" } From 48a5faaccf5bd74416a68e94b548fd6433908850 Mon Sep 17 00:00:00 2001 From: Paulo Pinto Date: Fri, 16 Jul 2021 15:55:40 +0000 Subject: [PATCH 0413/1171] Translated using Weblate (Turkish) Currently translated at 74.6% (2279 of 3051 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/tr/ --- src/i18n/strings/tr.json | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/tr.json b/src/i18n/strings/tr.json index f295263d1e..687273729b 100644 --- a/src/i18n/strings/tr.json +++ b/src/i18n/strings/tr.json @@ -2579,5 +2579,12 @@ "You can change this later": "Bunu daha sonra değiştirebilirsiniz", "Change which room, message, or user you're viewing": "Görüntülediğiniz odayı, mesajı veya kullanıcıyı değiştirin", "%(targetName)s accepted an invitation": "%(targetName)s daveti kabul etti", - "%(targetName)s accepted the invitation for %(displayName)s": "%(targetName)s, %(displayName)s kişisinin davetini kabul etti" + "%(targetName)s accepted the invitation for %(displayName)s": "%(targetName)s, %(displayName)s kişisinin davetini kabul etti", + "Integration manager": "Bütünleştirme Yöneticisi", + "Use an integration manager to manage bots, widgets, and sticker packs.": "Botları, görsel bileşenleri ve çıkartma paketlerini yönetmek için bir entegrasyon yöneticisi kullanın.", + "Identity server": "Kimlik sunucusu", + "Identity server (%(server)s)": "(%(server)s) Kimlik Sunucusu", + "Could not connect to identity server": "Kimlik Sunucusuna bağlanılamadı", + "Not a valid identity server (status code %(code)s)": "Geçerli bir Kimlik Sunucu değil ( durum kodu %(code)s )", + "Identity server URL must be HTTPS": "Kimlik Sunucu URL adresi HTTPS olmak zorunda" } From 59f9bf807f1bae8b6bd213d0abc76d16adb3003b Mon Sep 17 00:00:00 2001 From: Paulo Pinto Date: Fri, 16 Jul 2021 15:51:27 +0000 Subject: [PATCH 0414/1171] Translated using Weblate (Esperanto) Currently translated at 95.7% (2920 of 3051 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/eo/ --- src/i18n/strings/eo.json | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/eo.json b/src/i18n/strings/eo.json index 41bb44ed83..d70b933e31 100644 --- a/src/i18n/strings/eo.json +++ b/src/i18n/strings/eo.json @@ -3326,5 +3326,16 @@ "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.": "Provu aliajn vortojn aŭ kontorolu, ĉu vi ne tajperaris. Iuj rezultoj eble ne videblos, ĉar ili estas privataj kaj vi bezonus inviton por aliĝi.", "No results for \"%(query)s\"": "Neniuj rezultoj por «%(query)s»", "The user you called is busy.": "La uzanto, kiun vi vokis, estas okupata.", - "User Busy": "Uzanto estas okupata" + "User Busy": "Uzanto estas okupata", + "Integration manager": "Kunigilo", + "Your %(brand)s doesn't allow you to use an integration manager to do this. Please contact an admin.": "Via %(brand)so ne permesas al vi uzi kunigilon por tio. Bonvolu kontakti administranton.", + "Using this widget may share data with %(widgetDomain)s & your integration manager.": "Uzo de tiu ĉi fenestraĵo eble havigos datumojn kun %(widgetDomain)s kaj via kunigilo.", + "Identity server is": "Identiga servilo estas", + "Integration managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "Kunigiloj ricevas agordajn datumojn, kaj povas modifi fenestraĵojn, sendi invitojn al ĉambroj, kaj vianome agordi povnivelojn.", + "Use an integration manager to manage bots, widgets, and sticker packs.": "Uzu kunigilon por administrado de robotoj, fenestraĵoj, kaj glumarkaroj.", + "Use an integration manager (%(serverName)s) to manage bots, widgets, and sticker packs.": "Uzu kunigilon (%(serverName)s) por administrado de robotoj, fenestraĵoj, kaj glumarkaroj.", + "Identity server": "Identiga servilo", + "Identity server (%(server)s)": "Identiga servilo (%(server)s)", + "Could not connect to identity server": "Ne povis konektiĝi al identiga servilo", + "Not a valid identity server (status code %(code)s)": "Nevalida identiga servilo (statkodo %(code)s)" } From 41b1f47139ae5edc80bc3d8e3b22ac06200427be Mon Sep 17 00:00:00 2001 From: Paulo Pinto Date: Fri, 16 Jul 2021 15:54:36 +0000 Subject: [PATCH 0415/1171] Translated using Weblate (Polish) Currently translated at 70.4% (2149 of 3051 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/pl/ --- src/i18n/strings/pl.json | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/pl.json b/src/i18n/strings/pl.json index 784307acff..524d73eeca 100644 --- a/src/i18n/strings/pl.json +++ b/src/i18n/strings/pl.json @@ -2368,5 +2368,15 @@ "Some suggestions may be hidden for privacy.": "Niektóre propozycje mogą być ukryte z uwagi na prywatność.", "If you can't see who you’re looking for, send them your invite link below.": "Jeżeli nie możesz zobaczyć osób, których szukasz, wyślij im poniższy odnośnik z zaproszeniem.", "Or send invite link": "Lub wyślij odnośnik z zaproszeniem", - "We're working on this as part of the beta, but just want to let you know.": "Pracujemy nad tym w ramach bety, ale chcemy, żebyś wiedział(a)." + "We're working on this as part of the beta, but just want to let you know.": "Pracujemy nad tym w ramach bety, ale chcemy, żebyś wiedział(a).", + "Integration manager": "Menedżer Integracji", + "Identity server is": "Serwer tożsamości to", + "Integration managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "Zarządcy integracji otrzymują dane konfiguracji, mogą modyfikować widżety, wysyłać zaproszenia do pokoi i ustawiać poziom uprawnień w Twoim imieniu.", + "Use an integration manager to manage bots, widgets, and sticker packs.": "Użyj Zarządcy Integracji aby zarządzać botami, widżetami i pakietami naklejek.", + "Use an integration manager (%(serverName)s) to manage bots, widgets, and sticker packs.": "Użyj Zarządcy Integracji %(serverName)s aby zarządzać botami, widżetami i pakietami naklejek.", + "Identity server": "Serwer toższamości", + "Identity server (%(server)s)": "Serwer tożsamości (%(server)s)", + "Could not connect to identity server": "Nie można połączyć z serwerem tożsamości", + "Not a valid identity server (status code %(code)s)": "Nieprawidłowy serwer tożsamości (kod statusu %(code)s)", + "Identity server URL must be HTTPS": "URL serwera tożsamości musi być HTTPS" } From 1ae46102cd929faf1a3a9e8608b2f49e72b95ab0 Mon Sep 17 00:00:00 2001 From: Paulo Pinto Date: Fri, 16 Jul 2021 15:54:02 +0000 Subject: [PATCH 0416/1171] =?UTF-8?q?Translated=20using=20Weblate=20(Norwe?= =?UTF-8?q?gian=20Bokm=C3=A5l)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 58.3% (1779 of 3051 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/nb_NO/ --- src/i18n/strings/nb_NO.json | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/nb_NO.json b/src/i18n/strings/nb_NO.json index d3be9cd2ea..0ea13d1a1b 100644 --- a/src/i18n/strings/nb_NO.json +++ b/src/i18n/strings/nb_NO.json @@ -1981,5 +1981,13 @@ "Costa Rica": "Costa Rica", "Cook Islands": "Cook-øyene", "All keys backed up": "Alle nøkler er sikkerhetskopiert", - "Secret storage:": "Hemmelig lagring:" + "Secret storage:": "Hemmelig lagring:", + "Integration manager": "Integreringsbehandler", + "Identity server is": "Identitetstjeneren er", + "Integration managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "Integreringsbehandlere mottar oppsettsdata, og kan endre på moduler, sende rominvitasjoner, og bestemme styrkenivåer på dine vegne.", + "Use an integration manager to manage bots, widgets, and sticker packs.": "Bruk en integreringsbehandler til å behandle botter, moduler, og klistremerkepakker.", + "Use an integration manager (%(serverName)s) to manage bots, widgets, and sticker packs.": "Bruk en integreringsbehandler (%(serverName)s) til å behandle botter, moduler, og klistremerkepakker.", + "Identity server": "Identitetstjener", + "Identity server (%(server)s)": "Identitetstjener (%(server)s)", + "Could not connect to identity server": "Kunne ikke koble til identitetsserveren" } From d7f481343d9d4be48c0a8a4db6cd5782533b46ad Mon Sep 17 00:00:00 2001 From: Paulo Pinto Date: Fri, 16 Jul 2021 15:52:57 +0000 Subject: [PATCH 0417/1171] Translated using Weblate (Italian) Currently translated at 99.0% (3021 of 3051 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 | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/it.json b/src/i18n/strings/it.json index 2d98072f78..e082363709 100644 --- a/src/i18n/strings/it.json +++ b/src/i18n/strings/it.json @@ -3481,5 +3481,17 @@ "%(senderName)s changed their profile picture": "%(senderName)s ha cambiato la propria immagine del profilo", "%(senderName)s removed their profile picture": "%(senderName)s ha rimosso la propria immagine del profilo", "%(senderName)s removed their display name (%(oldDisplayName)s)": "%(senderName)s ha rimosso il proprio nome (%(oldDisplayName)s)", - "%(senderName)s set their display name to %(displayName)s": "%(senderName)s ha impostato il proprio nome a %(displayName)s" + "%(senderName)s set their display name to %(displayName)s": "%(senderName)s ha impostato il proprio nome a %(displayName)s", + "Integration manager": "Gestore dell'integrazione", + "Your %(brand)s doesn't allow you to use an integration manager to do this. Please contact an admin.": "Il tuo %(brand)s non ti permette di usare il gestore di integrazioni per questa azione. Contatta un amministratore.", + "Using this widget may share data with %(widgetDomain)s & your integration manager.": "Usando questo widget i dati possono essere condivisi con %(widgetDomain)s e il tuo Gestore di Integrazione.", + "Identity server is": "Il server di identità è", + "Integration managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "I gestori di integrazione ricevono dati di configurazione e possono modificare widget, inviare inviti alla stanza, assegnare permessi a tuo nome.", + "Use an integration manager to manage bots, widgets, and sticker packs.": "Usa un gestore di integrazioni per gestire bot, widget e pacchetti di adesivi.", + "Use an integration manager (%(serverName)s) to manage bots, widgets, and sticker packs.": "Usa un gestore di integrazioni (%(serverName)s) per gestire bot, widget e pacchetti di adesivi.", + "Identity server": "Server di identità", + "Identity server (%(server)s)": "Server di identità (%(server)s)", + "Could not connect to identity server": "Impossibile connettersi al server di identità", + "Not a valid identity server (status code %(code)s)": "Non è un server di identità valido (codice di stato %(code)s)", + "Identity server URL must be HTTPS": "L'URL di Identita' Server deve essere HTTPS" } From 1d81feed8daf1a75587a29752a885f0b676d0ea6 Mon Sep 17 00:00:00 2001 From: Paulo Pinto Date: Fri, 16 Jul 2021 15:54:27 +0000 Subject: [PATCH 0418/1171] Translated using Weblate (Persian) Currently translated at 95.4% (2912 of 3051 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/fa/ --- src/i18n/strings/fa.json | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/fa.json b/src/i18n/strings/fa.json index 46dde79945..1cea57d440 100644 --- a/src/i18n/strings/fa.json +++ b/src/i18n/strings/fa.json @@ -3007,5 +3007,15 @@ "This won't invite them to %(communityName)s. To invite someone to %(communityName)s, click here": "این کار آنها را به %(communityName)s دعوت نمی‌کند. برای دعوت افراد به %(communityName)s،اینجا کلیک کنید", "Start a conversation with someone using their name or username (like ).": "با استفاده از نام یا نام کاربری (مانند )، گفتگوی جدیدی را با دیگران شروع کنید.", "Start a conversation with someone using their name, email address or username (like ).": "با استفاده از نام، آدرس ایمیل و یا نام کاربری (مانند )، یک گفتگوی جدید را شروع کنید.", - "May include members not in %(communityName)s": "ممکن شامل اعضایی که در %(communityName)s نیستند نیز شود" + "May include members not in %(communityName)s": "ممکن شامل اعضایی که در %(communityName)s نیستند نیز شود", + "Integration manager": "مدیر یکپارچه‌سازی", + "Your %(brand)s doesn't allow you to use an integration manager to do this. Please contact an admin.": "%(brand)s شما اجازه استفاده از سیستم مدیریت ادغام را برای این کار نمی دهد. لطفا با ادمین تماس بگیرید.", + "Using this widget may share data with %(widgetDomain)s & your integration manager.": "استفاده از این ابزارک ممکن است داده‌هایی را با %(widgetDomain)s و سیستم مدیریت ادغام به اشتراک بگذارد.", + "Use an integration manager to manage bots, widgets, and sticker packs.": "از یک مدیر پکپارچه‌سازی برای مدیریت بات‌ها، ویجت‌ها و پک‌های استیکر مورد نظرتان استفاده نمائید.", + "Use an integration manager (%(serverName)s) to manage bots, widgets, and sticker packs.": "از یک مدیر پکپارچه‌سازی (%(serverName)s) برای مدیریت بات‌ها، ویجت‌ها و پک‌های استیکر استفاده کنید.", + "Identity server": "سرور هویت‌سنجی", + "Identity server (%(server)s)": "سرور هویت‌سنجی (%(server)s)", + "Could not connect to identity server": "اتصال به سرور هیوت‌سنجی امکان پذیر نیست", + "Not a valid identity server (status code %(code)s)": "سرور هویت‌سنجی معتبر نیست (کد وضعیت %(code)s)", + "Identity server URL must be HTTPS": "پروتکل آدرس سرور هویت‌سنجی باید HTTPS باشد" } From 2ebef1c60966fcdf1d027e7749a5fa59f9028c01 Mon Sep 17 00:00:00 2001 From: Paulo Pinto Date: Fri, 16 Jul 2021 15:53:43 +0000 Subject: [PATCH 0419/1171] Translated using Weblate (Latvian) Currently translated at 47.0% (1436 of 3051 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/lv/ --- src/i18n/strings/lv.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/lv.json b/src/i18n/strings/lv.json index b56599f26e..00c140c0a9 100644 --- a/src/i18n/strings/lv.json +++ b/src/i18n/strings/lv.json @@ -1582,5 +1582,9 @@ "Upload files": "Failu augšupielāde", "These files are too large to upload. The file size limit is %(limit)s.": "Šie faili pārsniedz augšupielādes izmēra limitu %(limit)s.", "Upload files (%(current)s of %(total)s)": "Failu augšupielāde (%(current)s no %(total)s)", - "Check your devices": "Pārskatiet savas ierīces" + "Check your devices": "Pārskatiet savas ierīces", + "Integration manager": "Integrācija pārvaldnieks", + "Identity server is": "Indentifikācijas serveris ir", + "Identity server": "Identitāšu serveris", + "Could not connect to identity server": "Neizdevās pieslēgties identitāšu serverim" } From f27b4d558a23e445a826b0647e32f953249f12ee Mon Sep 17 00:00:00 2001 From: Paulo Pinto Date: Fri, 16 Jul 2021 15:49:57 +0000 Subject: [PATCH 0420/1171] Translated using Weblate (Basque) Currently translated at 64.9% (1982 of 3051 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/eu/ --- src/i18n/strings/eu.json | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/eu.json b/src/i18n/strings/eu.json index 2740ea2079..704db34bfd 100644 --- a/src/i18n/strings/eu.json +++ b/src/i18n/strings/eu.json @@ -2293,5 +2293,17 @@ "Wrong file type": "Okerreko fitxategi-mota", "Looks good!": "Itxura ona du!", "Search rooms": "Bilatu gelak", - "User menu": "Erabiltzailea-menua" + "User menu": "Erabiltzailea-menua", + "Integration manager": "Integrazio-kudeatzailea", + "Your %(brand)s doesn't allow you to use an integration manager to do this. Please contact an admin.": "Zure %(brand)s aplikazioak ez dizu hau egiteko integrazio kudeatzaile bat erabiltzen uzten. Kontaktatu administratzaileren batekin.", + "Using this widget may share data with %(widgetDomain)s & your integration manager.": "Trepeta hau erabiltzean %(widgetDomain)s domeinuarekin eta zure integrazio kudeatzailearekin datuak partekatu daitezke.", + "Identity server is": "Identitate zerbitzaria", + "Integration managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "Integrazio kudeatzaileek konfigurazio datuak jasotzen dituzte, eta trepetak aldatu ditzakete, gelara gonbidapenak bidali, eta botere mailak zure izenean ezarri.", + "Use an integration manager to manage bots, widgets, and sticker packs.": "Erabili integrazio kudeatzaile bat botak, trepetak eta eranskailu multzoak kudeatzeko.", + "Use an integration manager (%(serverName)s) to manage bots, widgets, and sticker packs.": "Erabili (%(serverName)s) integrazio kudeatzailea botak, trepetak eta eranskailu multzoak kudeatzeko.", + "Identity server": "Identitate zerbitzaria", + "Identity server (%(server)s)": "Identitate-zerbitzaria (%(server)s)", + "Could not connect to identity server": "Ezin izan da identitate-zerbitzarira konektatu", + "Not a valid identity server (status code %(code)s)": "Ez da identitate zerbitzari baliogarria (egoera-mezua %(code)s)", + "Identity server URL must be HTTPS": "Identitate zerbitzariaren URL-a HTTPS motakoa izan behar du" } From 9e2684f58d692a2bdc03f5d577065e34237f579b Mon Sep 17 00:00:00 2001 From: Paulo Pinto Date: Fri, 16 Jul 2021 15:53:09 +0000 Subject: [PATCH 0421/1171] Translated using Weblate (Japanese) Currently translated at 74.7% (2280 of 3051 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/ja/ --- src/i18n/strings/ja.json | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/ja.json b/src/i18n/strings/ja.json index e395c51254..6f18b8e384 100644 --- a/src/i18n/strings/ja.json +++ b/src/i18n/strings/ja.json @@ -2504,5 +2504,15 @@ "You can change these anytime.": "ここで入力した情報はいつでも編集できます。", "Add some details to help people recognise it.": "情報を入力してください。", "View dev tools": "開発者ツールを表示", - "To view %(spaceName)s, you need an invite": "%(spaceName)s を閲覧するには招待が必要です" + "To view %(spaceName)s, you need an invite": "%(spaceName)s を閲覧するには招待が必要です", + "Integration manager": "インテグレーションマネージャ", + "Identity server is": "アイデンティティ・サーバー", + "Integration managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "インテグレーションマネージャは設定データを受け取り、ユーザーの代わりにウィジェットの変更、部屋への招待の送信、権限レベルの設定を行うことができます。", + "Use an integration manager to manage bots, widgets, and sticker packs.": "インテグレーションマネージャを使用して、ボット、ウィジェット、ステッカーパックを管理します。", + "Use an integration manager (%(serverName)s) to manage bots, widgets, and sticker packs.": "インテグレーションマネージャ (%(serverName)s) を使用して、ボット、ウィジェット、ステッカーパックを管理します。", + "Identity server": "認証サーバ", + "Identity server (%(server)s)": "identity サーバー (%(server)s)", + "Could not connect to identity server": "identity サーバーに接続できませんでした", + "Not a valid identity server (status code %(code)s)": "有効な identity サーバーではありません (ステータスコード %(code)s)", + "Identity server URL must be HTTPS": "identityサーバーのURLは HTTPS スキーマである必要があります" } From 3b3005a7ae85b4ce9c534c395a6c6a89fff28d43 Mon Sep 17 00:00:00 2001 From: Paulo Pinto Date: Fri, 16 Jul 2021 14:52:16 +0000 Subject: [PATCH 0422/1171] Translated using Weblate (Indonesian) Currently translated at 7.8% (239 of 3051 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/id/ --- src/i18n/strings/id.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/id.json b/src/i18n/strings/id.json index 499f625b75..b6ec8e2fa6 100644 --- a/src/i18n/strings/id.json +++ b/src/i18n/strings/id.json @@ -282,5 +282,6 @@ "You do not have permission to start a conference call in this room": "Anda tidak memiliki permisi untuk memulai panggilan massal di ruang ini", "Explore rooms": "Jelajahi ruang", "Sign In": "Masuk", - "Create Account": "Buat Akun" + "Create Account": "Buat Akun", + "Identity server": "Server Identitas" } From 1b7c01504beae048539eaa098b6f912679fe961f Mon Sep 17 00:00:00 2001 From: Paulo Pinto Date: Fri, 16 Jul 2021 15:50:55 +0000 Subject: [PATCH 0423/1171] Translated using Weblate (Czech) Currently translated at 99.3% (3030 of 3051 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 | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/cs.json b/src/i18n/strings/cs.json index 66f0dba9aa..3b2822b7c9 100644 --- a/src/i18n/strings/cs.json +++ b/src/i18n/strings/cs.json @@ -3409,5 +3409,17 @@ "Displaying time": "Zobrazování času", "Keyboard shortcuts": "Klávesové zkratky", "Use Ctrl + F to search timeline": "Stiskněte Ctrl + F k vyhledávání v časové ose", - "Use Command + F to search timeline": "Stiskněte Command + F k vyhledávání v časové ose" + "Use Command + F to search timeline": "Stiskněte Command + F k vyhledávání v časové ose", + "Integration manager": "Správce integrací", + "Your %(brand)s doesn't allow you to use an integration manager to do this. Please contact an admin.": "Váš %(brand)s neumožňuje použít správce integrací. Kontaktujte prosím správce.", + "Using this widget may share data with %(widgetDomain)s & your integration manager.": "Použití tohoto widgetu může sdílet data s %(widgetDomain)s a vaším správcem integrací.", + "Identity server is": "Server identity je", + "Integration managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "Správce integrací dostává konfigurační data a může za vás modifikovat widgety, posílat pozvánky a nastavovat úrovně oprávnění.", + "Use an integration manager to manage bots, widgets, and sticker packs.": "Použít správce integrací na správu botů, widgetů a samolepek.", + "Use an integration manager (%(serverName)s) to manage bots, widgets, and sticker packs.": "Použít správce integrací (%(serverName)s) na správu botů, widgetů a samolepek.", + "Identity server": "Server identit", + "Identity server (%(server)s)": "Server identit (%(server)s)", + "Could not connect to identity server": "Nepovedlo se připojení k serveru identit", + "Not a valid identity server (status code %(code)s)": "Toto není validní server identit (stavový kód %(code)s)", + "Identity server URL must be HTTPS": "Adresa serveru identit musí být na HTTPS" } From 97792789225716ae1cb7ef5ce173601999244e0a Mon Sep 17 00:00:00 2001 From: Paulo Pinto Date: Fri, 16 Jul 2021 15:51:43 +0000 Subject: [PATCH 0424/1171] Translated using Weblate (Finnish) Currently translated at 86.6% (2644 of 3051 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/fi/ --- src/i18n/strings/fi.json | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/fi.json b/src/i18n/strings/fi.json index 23140846b3..77252f339b 100644 --- a/src/i18n/strings/fi.json +++ b/src/i18n/strings/fi.json @@ -3003,5 +3003,17 @@ "Allow Peer-to-Peer for 1:1 calls (if you enable this, the other party might be able to see your IP address)": "Salli vertaisyhteydet 1:1-puheluille (jos otat tämän käyttöön, toinen osapuoli saattaa nähdä IP-osoitteesi)", "Send and receive voice messages": "Lähetä ja vastaanota ääniviestejä", "Show options to enable 'Do not disturb' mode": "Näytä asetukset Älä häiritse -tilan ottamiseksi käyttöön", - "%(deviceId)s from %(ip)s": "%(deviceId)s osoitteesta %(ip)s" + "%(deviceId)s from %(ip)s": "%(deviceId)s osoitteesta %(ip)s", + "Integration manager": "Integraatioiden lähde", + "Your %(brand)s doesn't allow you to use an integration manager to do this. Please contact an admin.": "%(brand)s-instanssisi ei salli sinun käyttävän integraatioiden lähdettä tämän tekemiseen. Ota yhteys ylläpitäjääsi.", + "Using this widget may share data with %(widgetDomain)s & your integration manager.": "Tämän sovelman käyttäminen saattaa jakaa tietoa osoitteille %(widgetDomain)s ja käyttämällesi integraatioiden lähteelle.", + "Identity server is": "Identiteettipalvelin on", + "Integration managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "Integraatioiden lähteet vastaanottavat asetusdataa ja voivat muokata sovelmia, lähettää kutsuja huoneeseen ja asettaa oikeustasoja puolestasi.", + "Use an integration manager to manage bots, widgets, and sticker packs.": "Käytä integraatioiden lähdettä bottien, sovelmien ja tarrapakettien hallintaan.", + "Use an integration manager (%(serverName)s) to manage bots, widgets, and sticker packs.": "Käytä integraatioiden lähdettä (%(serverName)s) bottien, sovelmien ja tarrapakettien hallintaan.", + "Identity server": "Identiteettipalvelin", + "Identity server (%(server)s)": "Identiteettipalvelin (%(server)s)", + "Could not connect to identity server": "Identiteettipalvelimeen ei saatu yhteyttä", + "Not a valid identity server (status code %(code)s)": "Ei kelvollinen identiteettipalvelin (tilakoodi %(code)s)", + "Identity server URL must be HTTPS": "Identiteettipalvelimen URL-osoitteen täytyy olla HTTPS-alkuinen" } From 6468fae7c83ff5f4cfb0a5b0d58c5aac5ca76908 Mon Sep 17 00:00:00 2001 From: Paulo Pinto Date: Fri, 16 Jul 2021 15:50:30 +0000 Subject: [PATCH 0425/1171] Translated using Weblate (Catalan) Currently translated at 26.0% (794 of 3051 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/ca/ --- src/i18n/strings/ca.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/ca.json b/src/i18n/strings/ca.json index 945b5a10cc..01d082b6a2 100644 --- a/src/i18n/strings/ca.json +++ b/src/i18n/strings/ca.json @@ -953,5 +953,10 @@ "Unable to access microphone": "No s'ha pogut accedir al micròfon", "Explore rooms": "Explora sales", "%(oneUser)smade no changes %(count)s times|one": "%(oneUser)sno ha fet canvis", - "%(oneUser)smade no changes %(count)s times|other": "%(oneUser)sno ha fet canvis %(count)s cops" + "%(oneUser)smade no changes %(count)s times|other": "%(oneUser)sno ha fet canvis %(count)s cops", + "Integration manager": "Gestor d'integracions", + "Identity server is": "El servidor d'identitat és", + "Integration managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "Els gestors d'integracions reben dades de configuració i poden modificar ginys, enviar invitacions a sales i establir nivells d'autoritat en nom teu.", + "Identity server": "Servidor d'identitat", + "Could not connect to identity server": "No s'ha pogut connectar amb el servidor d'identitat" } From 44311802b8ed2cff08329ab89b9a7878e73a4c70 Mon Sep 17 00:00:00 2001 From: Paulo Pinto Date: Fri, 16 Jul 2021 15:55:15 +0000 Subject: [PATCH 0426/1171] Translated using Weblate (Slovak) Currently translated at 57.1% (1744 of 3051 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/sk/ --- src/i18n/strings/sk.json | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/sk.json b/src/i18n/strings/sk.json index 0ee0c6cbc3..d8902a3784 100644 --- a/src/i18n/strings/sk.json +++ b/src/i18n/strings/sk.json @@ -2080,5 +2080,14 @@ "The call was answered on another device.": "Hovor bol prijatý na inom zariadení.", "The call could not be established": "Hovor nemohol byť realizovaný", "The other party declined the call.": "Druhá strana odmietla hovor.", - "Call Declined": "Hovor odmietnutý" + "Call Declined": "Hovor odmietnutý", + "Integration manager": "Správca integrácií", + "Integration managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "Integračné servery zhromažďujú údaje nastavení, môžu spravovať widgety, odosielať vo vašom mene pozvánky alebo meniť úroveň moci.", + "Use an integration manager to manage bots, widgets, and sticker packs.": "Použiť integračný server na správu botov, widgetov a balíčkov s nálepkami.", + "Use an integration manager (%(serverName)s) to manage bots, widgets, and sticker packs.": "Použiť integračný server (%(serverName)s) na správu botov, widgetov a balíčkov s nálepkami.", + "Identity server": "Server totožností", + "Identity server (%(server)s)": "Server totožností (%(server)s)", + "Could not connect to identity server": "Nie je možné sa pripojiť k serveru totožností", + "Not a valid identity server (status code %(code)s)": "Toto nie je funkčný server totožností (kód stavu %(code)s)", + "Identity server URL must be HTTPS": "URL adresa servera totožností musí začínať HTTPS" } From 3d7bb9004739096e70fe6d7ca098df2079b61a3d Mon Sep 17 00:00:00 2001 From: Paulo Pinto Date: Fri, 16 Jul 2021 15:52:00 +0000 Subject: [PATCH 0427/1171] Translated using Weblate (Galician) Currently translated at 96.3% (2940 of 3051 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 | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/gl.json b/src/i18n/strings/gl.json index b880c5b548..bf8911d621 100644 --- a/src/i18n/strings/gl.json +++ b/src/i18n/strings/gl.json @@ -3398,5 +3398,17 @@ "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." + "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.", + "Integration manager": "Xestor de Integracións", + "Your %(brand)s doesn't allow you to use an integration manager to do this. Please contact an admin.": "O teu %(brand)s non permite que uses o Xestor de Integracións, contacta coa administración.", + "Using this widget may share data with %(widgetDomain)s & your integration manager.": "Ao utilizar este widget poderías compartir datos con %(widgetDomain)s e o teu Xestor de integracións.", + "Identity server is": "O servidor de identidade é", + "Integration managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "Os xestores de integracións reciben datos de configuración, e poden modificar os widgets, enviar convites das salas, e establecer roles no teu nome.", + "Use an integration manager to manage bots, widgets, and sticker packs.": "Usa un Xestor de Integracións para xestionar bots, widgets e paquetes de pegatinas.", + "Use an integration manager (%(serverName)s) to manage bots, widgets, and sticker packs.": "Usa un Xestor de Integración (%(serverName)s) para xestionar bots, widgets e paquetes de pegatinas.", + "Identity server": "Servidor de identidade", + "Identity server (%(server)s)": "Servidor de Identidade (%(server)s)", + "Could not connect to identity server": "Non hai conexión co Servidor de Identidade", + "Not a valid identity server (status code %(code)s)": "Servidor de Identidade non válido (código de estado %(code)s)", + "Identity server URL must be HTTPS": "O URL do servidor de identidade debe comezar HTTPS" } From 795ba0dc104b66fa0a0fff78576963ef5ed8ae24 Mon Sep 17 00:00:00 2001 From: Paulo Pinto Date: Fri, 16 Jul 2021 15:39:56 +0000 Subject: [PATCH 0428/1171] Translated using Weblate (Serbian) Currently translated at 52.3% (1598 of 3051 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/sr/ --- src/i18n/strings/sr.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/sr.json b/src/i18n/strings/sr.json index 49f87321f7..5af8ffe820 100644 --- a/src/i18n/strings/sr.json +++ b/src/i18n/strings/sr.json @@ -1760,5 +1760,7 @@ "You're already in a call with this person.": "Већ разговарате са овом особом.", "Already in call": "Већ у позиву", "Whether you're using %(brand)s as an installed Progressive Web App": "Без обзира да ли користите %(brand)s као инсталирану Прогресивну веб апликацију", - "Whether or not you're using the 'breadcrumbs' feature (avatars above the room list)": "Без обзира да ли користите функцију „breadcrumbs“ (аватари изнад листе соба)" + "Whether or not you're using the 'breadcrumbs' feature (avatars above the room list)": "Без обзира да ли користите функцију „breadcrumbs“ (аватари изнад листе соба)", + "Using this widget may share data with %(widgetDomain)s & your integration manager.": "Коришћење овог виџета може да дели податке са %(widgetDomain)s и вашим интеграционим менаџером.", + "Identity server is": "Идентитетски сервер је" } From 55a8178b1539566a3755378e5cb104bbaa785c86 Mon Sep 17 00:00:00 2001 From: Paulo Pinto Date: Fri, 16 Jul 2021 15:50:17 +0000 Subject: [PATCH 0429/1171] Translated using Weblate (Bulgarian) Currently translated at 83.4% (2545 of 3051 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/bg/ --- src/i18n/strings/bg.json | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/bg.json b/src/i18n/strings/bg.json index 294d5a4979..19d95842c8 100644 --- a/src/i18n/strings/bg.json +++ b/src/i18n/strings/bg.json @@ -2897,5 +2897,17 @@ "Already in call": "Вече в разговор", "You're already in a call with this person.": "Вече сте в разговор в този човек.", "Too Many Calls": "Твърде много повиквания", - "Call failed because microphone could not be accessed. Check that a microphone is plugged in and set up correctly.": "Неуспешно повикване поради неуспешен достъп до микрофон. Проверете дали микрофонът е включен и настроен правилно." + "Call failed because microphone could not be accessed. Check that a microphone is plugged in and set up correctly.": "Неуспешно повикване поради неуспешен достъп до микрофон. Проверете дали микрофонът е включен и настроен правилно.", + "Integration manager": "Мениджър на интеграции", + "Your %(brand)s doesn't allow you to use an integration manager to do this. Please contact an admin.": "Вашият %(brand)s не позволява да използвате мениджъра на интеграции за да направите това. Свържете се с администратор.", + "Using this widget may share data with %(widgetDomain)s & your integration manager.": "Използването на това приспособление може да сподели данни с %(widgetDomain)s и с мениджъра на интеграции.", + "Identity server is": "Сървър за самоличност:", + "Integration managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "Мениджърът на интеграции получава конфигурационни данни, може да модифицира приспособления, да изпраща покани за стаи и да настройва нива на достъп от ваше име.", + "Use an integration manager to manage bots, widgets, and sticker packs.": "Използвай мениджър на интеграции за управление на ботове, приспособления и стикери.", + "Use an integration manager (%(serverName)s) to manage bots, widgets, and sticker packs.": "Използвай мениджър на интеграции %(serverName)s за управление на ботове, приспособления и стикери.", + "Identity server": "Сървър за самоличност", + "Identity server (%(server)s)": "Сървър за самоличност (%(server)s)", + "Could not connect to identity server": "Неуспешна връзка със сървъра за самоличност", + "Not a valid identity server (status code %(code)s)": "Невалиден сървър за самоличност (статус код %(code)s)", + "Identity server URL must be HTTPS": "Адресът на сървъра за самоличност трябва да бъде HTTPS" } From 1bdce387a02e4d2c6aae2c55026ecfae2ad46c32 Mon Sep 17 00:00:00 2001 From: Paulo Pinto Date: Fri, 16 Jul 2021 15:53:48 +0000 Subject: [PATCH 0430/1171] Translated using Weblate (Lithuanian) Currently translated at 70.6% (2157 of 3051 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/lt/ --- src/i18n/strings/lt.json | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/lt.json b/src/i18n/strings/lt.json index 4449ef97c2..870396cd4c 100644 --- a/src/i18n/strings/lt.json +++ b/src/i18n/strings/lt.json @@ -2421,5 +2421,17 @@ "New Zealand": "Naujoji Zelandija", "New Caledonia": "Naujoji Kaledonija", "Netherlands": "Nyderlandai", - "Cayman Islands": "Kaimanų Salos" + "Cayman Islands": "Kaimanų Salos", + "Integration manager": "Integracijų tvarkytuvas", + "Your %(brand)s doesn't allow you to use an integration manager to do this. Please contact an admin.": "Jūsų %(brand)s neleidžia jums naudoti integracijų tvarkytuvo tam atlikti. Susisiekite su administratoriumi.", + "Using this widget may share data with %(widgetDomain)s & your integration manager.": "Naudojimasis šiuo valdikliu gali pasidalinti duomenimis su %(widgetDomain)s ir jūsų integracijų tvarkytuvu.", + "Identity server is": "Tapatybės serveris yra", + "Integration managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "Integracijų Tvarkytuvai gauna konfigūracijos duomenis ir jūsų vardu gali keisti valdiklius, siųsti kambario pakvietimus ir nustatyti galios lygius.", + "Use an integration manager to manage bots, widgets, and sticker packs.": "Naudokite Integracijų Tvarkytuvą botų, valdiklių ir lipdukų pakuočių tvarkymui.", + "Use an integration manager (%(serverName)s) to manage bots, widgets, and sticker packs.": "Naudokite Integracijų Tvarkytuvą (%(serverName)s) botų, valdiklių ir lipdukų pakuočių tvarkymui.", + "Identity server": "Tapatybės serveris", + "Identity server (%(server)s)": "Tapatybės serveris (%(server)s)", + "Could not connect to identity server": "Nepavyko prisijungti prie tapatybės serverio", + "Not a valid identity server (status code %(code)s)": "Netinkamas tapatybės serveris (statuso kodas %(code)s)", + "Identity server URL must be HTTPS": "Tapatybės Serverio URL privalo būti HTTPS" } From dff8fecc9610a3e094700c6b21a93c2ec41d343d Mon Sep 17 00:00:00 2001 From: Paulo Pinto Date: Fri, 16 Jul 2021 15:49:35 +0000 Subject: [PATCH 0431/1171] Translated using Weblate (Albanian) Currently translated at 99.1% (3026 of 3051 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 | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/sq.json b/src/i18n/strings/sq.json index 3a0663e85a..f6a9d260c8 100644 --- a/src/i18n/strings/sq.json +++ b/src/i18n/strings/sq.json @@ -3481,5 +3481,17 @@ "Keyboard shortcuts": "Shkurtore tastiere", "Use Ctrl + F to search timeline": "Përdorni Ctrl + F që të kërkohet te rrjedha kohore", "Use Command + F to search timeline": "Përdorni Command + F që të kërkohet te rrjedha kohore", - "User %(userId)s is already invited to the room": "Përdoruesi %(userId)s është ftuar tashmë te dhoma" + "User %(userId)s is already invited to the room": "Përdoruesi %(userId)s është ftuar tashmë te dhoma", + "Integration manager": "Përgjegjës Integrimesh", + "Your %(brand)s doesn't allow you to use an integration manager to do this. Please contact an admin.": "%(brand)s-i juah nuk ju lejon të përdorni një Përgjegjës Integrimesh për të bërë këtë. Ju lutemi, lidhuni me përgjegjësin.", + "Using this widget may share data with %(widgetDomain)s & your integration manager.": "Përdorimi i këtij widget-i mund të sjellë ndarje të dhënash me %(widgetDomain)s & Përgjegjësin tuaj të Integrimeve.", + "Identity server is": "Shërbyes Identitetesh është", + "Integration managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "Përgjegjësit e Integrimeve marrin të dhëna formësimi, dhe mund të ndryshojnë widget-e, të dërgojnë ftesa dhome, dhe të caktojnë shkallë pushteti në emër tuajin.", + "Use an integration manager to manage bots, widgets, and sticker packs.": "Përdorni një Përgjegjës Integrimesh që të administroni robotë, widget-e dhe paketa ngjitësish.", + "Use an integration manager (%(serverName)s) to manage bots, widgets, and sticker packs.": "Përdorni një Përgjegjës Integrimesh (%(serverName)s) që të administroni robotë, widget-e dhe paketa ngjitësish.", + "Identity server": "Shërbyes identitetesh", + "Identity server (%(server)s)": "Shërbyes Identitetesh (%(server)s)", + "Could not connect to identity server": "S’u lidh dot te shërbyes identitetesh", + "Not a valid identity server (status code %(code)s)": "Shërbyes Identitetesh i pavlefshëm (kod gjendjeje %(code)s)", + "Identity server URL must be HTTPS": "URL-ja e Shërbyesit të Identiteteve duhet të jetë HTTPS" } From d548a88a7a94e7bb9b1e9de79835b2a56f4f8d75 Mon Sep 17 00:00:00 2001 From: Paulo Pinto Date: Fri, 16 Jul 2021 15:25:49 +0000 Subject: [PATCH 0432/1171] Translated using Weblate (Azerbaijani) Currently translated at 11.0% (336 of 3051 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/az/ --- src/i18n/strings/az.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/az.json b/src/i18n/strings/az.json index 987cef73b2..b460df0bf8 100644 --- a/src/i18n/strings/az.json +++ b/src/i18n/strings/az.json @@ -383,5 +383,7 @@ "%(senderDisplayName)s enabled flair for %(newGroups)s and disabled flair for %(oldGroups)s in this room.": "Bu otaqda %(newGroups)s üçün aktiv və %(oldGroups)s üçün %(senderDisplayName)s deaktiv oldu.", "Create Account": "Hesab Aç", "Explore rooms": "Otaqları kəşf edin", - "Sign In": "Daxil ol" + "Sign In": "Daxil ol", + "Identity server is": "Eyniləşdirmənin serveri bu", + "Identity server": "Eyniləşdirmənin serveri" } From 45c92f79f5263f2ac301975aa614c52e931fdca6 Mon Sep 17 00:00:00 2001 From: Paulo Pinto Date: Fri, 16 Jul 2021 15:50:03 +0000 Subject: [PATCH 0433/1171] Translated using Weblate (Bengali (Bangladesh)) Currently translated at 0.0% (0 of 3051 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/bn_BD/ --- src/i18n/strings/bn_BD.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/bn_BD.json b/src/i18n/strings/bn_BD.json index 9e26dfeeb6..5ceda07ab4 100644 --- a/src/i18n/strings/bn_BD.json +++ b/src/i18n/strings/bn_BD.json @@ -1 +1,4 @@ -{} \ No newline at end of file +{ + "Integration manager": "ইন্টিগ্রেশন ম্যানেজার", + "Identity server": "পরিচয় সার্ভার" +} From ffc7e7cbb0233fb74cfd7360b33242810dcd1f44 Mon Sep 17 00:00:00 2001 From: Paulo Pinto Date: Fri, 16 Jul 2021 15:50:08 +0000 Subject: [PATCH 0434/1171] Translated using Weblate (Bengali (India)) Currently translated at 0.0% (0 of 3051 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/bn_IN/ --- src/i18n/strings/bn_IN.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/bn_IN.json b/src/i18n/strings/bn_IN.json index 0967ef424b..5ceda07ab4 100644 --- a/src/i18n/strings/bn_IN.json +++ b/src/i18n/strings/bn_IN.json @@ -1 +1,4 @@ -{} +{ + "Integration manager": "ইন্টিগ্রেশন ম্যানেজার", + "Identity server": "পরিচয় সার্ভার" +} From be15ff6de348abb8637fe71829292469ddb2df81 Mon Sep 17 00:00:00 2001 From: Paulo Pinto Date: Fri, 16 Jul 2021 14:47:44 +0000 Subject: [PATCH 0435/1171] Translated using Weblate (Bosnian) Currently translated at 0.1% (4 of 3051 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/bs/ --- src/i18n/strings/bs.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/bs.json b/src/i18n/strings/bs.json index dc4ebda993..a7891ebdcd 100644 --- a/src/i18n/strings/bs.json +++ b/src/i18n/strings/bs.json @@ -2,5 +2,6 @@ "Dismiss": "Odbaci", "Create Account": "Otvori račun", "Sign In": "Prijavite se", - "Explore rooms": "Istražite sobe" + "Explore rooms": "Istražite sobe", + "Identity server": "Identifikacioni Server" } From 43d4c09ea41193affd40c763738b0bbac0cb2cb3 Mon Sep 17 00:00:00 2001 From: Paulo Pinto Date: Fri, 16 Jul 2021 15:29:16 +0000 Subject: [PATCH 0436/1171] Translated using Weblate (Hindi) Currently translated at 17.4% (531 of 3051 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/hi/ --- src/i18n/strings/hi.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/hi.json b/src/i18n/strings/hi.json index f71c024342..eb0da42ae5 100644 --- a/src/i18n/strings/hi.json +++ b/src/i18n/strings/hi.json @@ -588,5 +588,6 @@ "The user must be unbanned before they can be invited.": "उपयोगकर्ता को आमंत्रित करने से पहले उन्हें प्रतिबंधित किया जाना चाहिए।", "Explore rooms": "रूम का अन्वेषण करें", "Sign In": "साइन करना", - "Create Account": "खाता बनाएं" + "Create Account": "खाता बनाएं", + "Identity server is": "आइडेंटिटी सर्वर हैं" } From f82d1246ec939826a57e5602eb1b169e8a343493 Mon Sep 17 00:00:00 2001 From: Paulo Pinto Date: Fri, 16 Jul 2021 15:29:40 +0000 Subject: [PATCH 0437/1171] Translated using Weblate (Icelandic) Currently translated at 21.6% (660 of 3051 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/is/ --- src/i18n/strings/is.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/is.json b/src/i18n/strings/is.json index e8718c941a..a20a30cb52 100644 --- a/src/i18n/strings/is.json +++ b/src/i18n/strings/is.json @@ -728,5 +728,7 @@ "Explore all public rooms": "Kanna öll almenningsherbergi", "Liberate your communication": "Frelsaðu samskipti þín", "Welcome to ": "Velkomin til ", - "Welcome to %(appName)s": "Velkomin til %(appName)s" + "Welcome to %(appName)s": "Velkomin til %(appName)s", + "Identity server is": "Auðkennisþjónn er", + "Identity server": "Auðkennisþjónn" } From a593c3470f3d787f1828c47ee09187e1f99cab27 Mon Sep 17 00:00:00 2001 From: Paulo Pinto Date: Fri, 16 Jul 2021 15:31:16 +0000 Subject: [PATCH 0438/1171] Translated using Weblate (Norwegian Nynorsk) Currently translated at 39.1% (1194 of 3051 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/nn/ --- src/i18n/strings/nn.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/nn.json b/src/i18n/strings/nn.json index 7b02407ea9..f53b092d5f 100644 --- a/src/i18n/strings/nn.json +++ b/src/i18n/strings/nn.json @@ -1381,5 +1381,7 @@ "We recommend that you remove your email addresses and phone numbers from the identity server before disconnecting.": "Vi tilrår at du slettar personleg informasjon, som e-postadresser og telefonnummer frå identitetstenaren før du koplar frå.", "Privacy": "Personvern", "Versions": "Versjonar", - "Legal": "Juridisk" + "Legal": "Juridisk", + "Identity server is": "Identitetstenaren er", + "Identity server": "Identitetstenar" } From 41240a7bb23fd6dd279a19cb7851b5ca4be8a664 Mon Sep 17 00:00:00 2001 From: Paulo Pinto Date: Fri, 16 Jul 2021 15:50:51 +0000 Subject: [PATCH 0439/1171] Translated using Weblate (Croatian) Currently translated at 6.7% (207 of 3051 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/hr/ --- src/i18n/strings/hr.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/hr.json b/src/i18n/strings/hr.json index 8070757426..abf903be63 100644 --- a/src/i18n/strings/hr.json +++ b/src/i18n/strings/hr.json @@ -205,5 +205,8 @@ "Add Email Address": "Dodaj email adresu", "Confirm": "Potvrdi", "Click the button below to confirm adding this email address.": "Kliknite gumb ispod da biste potvrdili dodavanje ove email adrese.", - "Confirm adding email": "Potvrdite dodavanje email adrese" + "Confirm adding email": "Potvrdite dodavanje email adrese", + "Integration manager": "Upravitelj integracijama", + "Identity server": "Poslužitelj identiteta", + "Could not connect to identity server": "Nije moguće spojiti se na poslužitelja identiteta" } From fa601798898f97758f47a027d7e77eb1d7939c10 Mon Sep 17 00:00:00 2001 From: Paulo Pinto Date: Fri, 16 Jul 2021 15:55:54 +0000 Subject: [PATCH 0440/1171] Translated using Weblate (West Flemish) Currently translated at 41.0% (1252 of 3051 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/vls/ --- src/i18n/strings/vls.json | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/vls.json b/src/i18n/strings/vls.json index 75ab903ebe..24129dc6c3 100644 --- a/src/i18n/strings/vls.json +++ b/src/i18n/strings/vls.json @@ -1445,5 +1445,11 @@ "Remove %(email)s?": "%(email)s verwydern?", "Remove %(phone)s?": "%(phone)s verwydern?", "Explore rooms": "Gesprekkn ountdekkn", - "Create Account": "Account anmoakn" + "Create Account": "Account anmoakn", + "Integration manager": "Integroasjebeheerder", + "Identity server": "Identiteitsserver", + "Identity server (%(server)s)": "Identiteitsserver (%(server)s)", + "Could not connect to identity server": "Kostege geen verbindienge moakn me den identiteitsserver", + "Not a valid identity server (status code %(code)s)": "Geen geldigen identiteitsserver (statuscode %(code)s)", + "Identity server URL must be HTTPS": "Den identiteitsserver-URL moet HTTPS zyn" } From a604217336619a055d665e70237e63b9cc1d7be9 Mon Sep 17 00:00:00 2001 From: Paulo Pinto Date: Fri, 16 Jul 2021 14:57:54 +0000 Subject: [PATCH 0441/1171] Translated using Weblate (Welsh) Currently translated at 0.4% (13 of 3051 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/cy/ --- src/i18n/strings/cy.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/cy.json b/src/i18n/strings/cy.json index b99b834636..2b4af70877 100644 --- a/src/i18n/strings/cy.json +++ b/src/i18n/strings/cy.json @@ -11,5 +11,6 @@ "Sign In": "Mewngofnodi", "Create Account": "Creu Cyfrif", "Dismiss": "Wfftio", - "Explore rooms": "Archwilio Ystafelloedd" + "Explore rooms": "Archwilio Ystafelloedd", + "Identity server": "Gweinydd Adnabod" } From 8e5befb62631b33be92569ac1dc194fa524c1c54 Mon Sep 17 00:00:00 2001 From: Paulo Pinto Date: Fri, 16 Jul 2021 15:51:34 +0000 Subject: [PATCH 0442/1171] Translated using Weblate (Estonian) Currently translated at 98.9% (3018 of 3051 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 | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/et.json b/src/i18n/strings/et.json index 58b9f0bf9b..3160727a6a 100644 --- a/src/i18n/strings/et.json +++ b/src/i18n/strings/et.json @@ -3456,5 +3456,17 @@ "Images, GIFs and videos": "Pildid, gif'id ja videod", "Show %(count)s other previews|other": "Näita %(count)s muud eelvaadet", "Show %(count)s other previews|one": "Näita veel %(count)s eelvaadet", - "Error processing audio message": "Viga häälsõnumi töötlemisel" + "Error processing audio message": "Viga häälsõnumi töötlemisel", + "Integration manager": "Lõiminguhaldur", + "Your %(brand)s doesn't allow you to use an integration manager to do this. Please contact an admin.": "Sinu %(brand)s ei võimalda selle tegevuse jaoks kasutada Lõimingute haldurit. Palun küsi lisateavet administraatorilt.", + "Using this widget may share data with %(widgetDomain)s & your integration manager.": "Selle vidina kasutamisel võidakse jagada andmeid saitidega %(widgetDomain)s ning sinu vidinahalduriga.", + "Identity server is": "Isikutuvastusserver on", + "Integration managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "Lõiminguhalduritel on laiad volitused - nad võivad sinu nimel lugeda seadistusi, kohandada vidinaid, saata jututubade kutseid ning määrata õigusi.", + "Use an integration manager to manage bots, widgets, and sticker packs.": "Robotite, vidinate ja kleepsupakkide seadistamiseks kasuta lõiminguhaldurit.", + "Use an integration manager (%(serverName)s) to manage bots, widgets, and sticker packs.": "Robotite, vidinate ja kleepsupakkide jaoks kasuta lõiminguhaldurit (%(serverName)s).", + "Identity server": "Isikutuvastusserver", + "Identity server (%(server)s)": "Isikutuvastusserver %(server)s", + "Could not connect to identity server": "Ei saanud ühendust isikutuvastusserveriga", + "Not a valid identity server (status code %(code)s)": "See ei ole sobilik isikutuvastusserver (staatuskood %(code)s)", + "Identity server URL must be HTTPS": "Isikutuvastusserveri URL peab kasutama HTTPS-protokolli" } From a13013493d93701b4c25b3e53421a3d650d3b89d Mon Sep 17 00:00:00 2001 From: Paulo Pinto Date: Fri, 16 Jul 2021 15:53:15 +0000 Subject: [PATCH 0443/1171] Translated using Weblate (Kabyle) Currently translated at 79.0% (2412 of 3051 strings) Translation: Element Web/matrix-react-sdk Translate-URL: https://translate.element.io/projects/element-web/matrix-react-sdk/kab/ --- src/i18n/strings/kab.json | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/kab.json b/src/i18n/strings/kab.json index b6e1b3020f..3a7daa3b4c 100644 --- a/src/i18n/strings/kab.json +++ b/src/i18n/strings/kab.json @@ -2754,5 +2754,17 @@ "(an error occurred)": "(tella-d tuccḍa)", "(connection failed)": "(tuqqna ur teddi ara)", "🎉 All servers are banned from participating! This room can no longer be used.": "🎉 Iqeddcen akk ttwagedlen seg uttekki! Taxxamt-a dayen ur tettuseqdac ara.", - "Try again": "Ɛreḍ tikkelt-nniḍen" + "Try again": "Ɛreḍ tikkelt-nniḍen", + "Integration manager": "Amsefrak n umsidef", + "Your %(brand)s doesn't allow you to use an integration manager to do this. Please contact an admin.": "%(brand)s-ik·im ur ak·am yefki ara tisirag i useqdec n umsefrak n umsidef i wakken ad tgeḍ aya. Ttxil-k·m nermes anedbal.", + "Using this widget may share data with %(widgetDomain)s & your integration manager.": "Aseqdec n uwiǧit-a yezmer ad yebḍu isefka d %(widgetDomain)s & amsefrak-inek·inem n umsidef.", + "Identity server is": "Aqeddac n timagit d", + "Integration managers receive configuration data, and can modify widgets, send room invites, and set power levels on your behalf.": "Imsefrak n yimsidaf remmsen-d isefka n uswel, syen ad uɣalen zemren ad beddlen iwiǧiten, ad aznen tinubgiwin ɣer texxamin, ad yesbadu daɣen tazmert n yiswiren s yiswiren deg ubdil-ik·im.", + "Use an integration manager to manage bots, widgets, and sticker packs.": "Seqdec amsefrak n umsidef i usefrek n yibuten, n yiwiǧiten d tɣawsiwin n usenteḍ.", + "Use an integration manager (%(serverName)s) to manage bots, widgets, and sticker packs.": "Seqdec amsefrak n umsidef (%(serverName)s) i usefrek n yibuten, n yiwiǧiten d tɣawsiwin n usenteḍ.", + "Identity server": "Aqeddac n timagit", + "Identity server (%(server)s)": "Aqeddac n timagit (%(server)s)", + "Could not connect to identity server": "Ur izmir ara ad yeqqen ɣer uqeddac n timagit", + "Not a valid identity server (status code %(code)s)": "Aqeddac n timagit mačči d ameɣtu (status code %(code)s)", + "Identity server URL must be HTTPS": "URL n uqeddac n timagit ilaq ad yili d HTTPS" } From 31f5d012b8516db71adb8f11f416b70c60a87541 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Fri, 16 Jul 2021 21:16:07 +0200 Subject: [PATCH 0444/1171] Improve the look and feel of the picker MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- .../_DesktopCapturerSourcePicker.scss | 82 ++++++++----------- .../elements/DesktopCapturerSourcePicker.tsx | 14 ++-- 2 files changed, 40 insertions(+), 56 deletions(-) diff --git a/res/css/views/elements/_DesktopCapturerSourcePicker.scss b/res/css/views/elements/_DesktopCapturerSourcePicker.scss index 489a9f3a79..dc435bb386 100644 --- a/res/css/views/elements/_DesktopCapturerSourcePicker.scss +++ b/res/css/views/elements/_DesktopCapturerSourcePicker.scss @@ -16,58 +16,42 @@ limitations under the License. .mx_desktopCapturerSourcePicker { overflow: hidden; -} -.mx_desktopCapturerSourcePicker_tabLabels { - display: flex; - padding: 0 0 8px 0; -} + .mx_desktopCapturerSourcePicker_tab { + display: flex; + flex-wrap: wrap; + justify-content: center; + align-items: flex-start; + height: 500px; + overflow: overlay; + } -.mx_desktopCapturerSourcePicker_tabLabel, -.mx_desktopCapturerSourcePicker_tabLabel_selected { - width: 100%; - text-align: center; - border-radius: 8px; - padding: 8px 0; - font-size: $font-13px; -} + .mx_desktopCapturerSourcePicker_source { + display: flex; + flex-direction: column; + margin: 8px; + } -.mx_desktopCapturerSourcePicker_tabLabel_selected { - background-color: $tab-label-active-bg-color; - color: $tab-label-active-fg-color; -} + .mx_desktopCapturerSourcePicker_source_thumbnail { + margin: 4px; + width: 312px; + border-width: 2px; + border-radius: 8px; + border-style: solid; + border-color: transparent; -.mx_desktopCapturerSourcePicker_panel { - display: flex; - flex-wrap: wrap; - justify-content: center; - align-items: flex-start; - height: 500px; - overflow: overlay; -} + &.mx_desktopCapturerSourcePicker_source_thumbnail_selected, + &:hover, + &:focus { + border-color: $accent-color; + } + } -.mx_desktopCapturerSourcePicker_stream_button { - display: flex; - flex-direction: column; - margin: 8px; - border-radius: 4px; -} - -.mx_desktopCapturerSourcePicker_stream_button_selected, -.mx_desktopCapturerSourcePicker_stream_button:hover, -.mx_desktopCapturerSourcePicker_stream_button:focus { - background: $roomtile-selected-bg-color; -} - -.mx_desktopCapturerSourcePicker_stream_thumbnail { - margin: 4px; - width: 312px; -} - -.mx_desktopCapturerSourcePicker_stream_name { - margin: 0 4px; - white-space: nowrap; - text-overflow: ellipsis; - overflow: hidden; - width: 312px; + .mx_desktopCapturerSourcePicker_source_name { + margin: 0 4px; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + width: 312px; + } } diff --git a/src/components/views/elements/DesktopCapturerSourcePicker.tsx b/src/components/views/elements/DesktopCapturerSourcePicker.tsx index 49a2bda1f7..0fdddaa3ef 100644 --- a/src/components/views/elements/DesktopCapturerSourcePicker.tsx +++ b/src/components/views/elements/DesktopCapturerSourcePicker.tsx @@ -51,22 +51,22 @@ export class ExistingSource extends React.Component { }; render() { - const classes = classNames({ - mx_desktopCapturerSourcePicker_stream_button: true, - mx_desktopCapturerSourcePicker_stream_button_selected: this.props.selected, + const thumbnailClasses = classNames({ + mx_desktopCapturerSourcePicker_source_thumbnail: true, + mx_desktopCapturerSourcePicker_source_thumbnail_selected: this.props.selected, }); return ( - {this.props.source.name} + {this.props.source.name} ); } @@ -147,7 +147,7 @@ export default class DesktopCapturerSourcePicker extends React.Component< }); return new Tab(type, label, null, ( -
    +
    { sources }
    )); From 27b02617c273d34fee2c4480f5e68cb27116f316 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Fri, 16 Jul 2021 21:16:14 +0200 Subject: [PATCH 0445/1171] 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 | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index b3be39972f..d91bfda81a 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1985,9 +1985,9 @@ "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", "This version of %(brand)s does not support searching encrypted messages": "This version of %(brand)s does not support searching encrypted messages", - "Share your screen": "Share your screen", - "Screens": "Screens", - "Windows": "Windows", + "Share entire screen": "Share entire screen", + "Application window": "Application window", + "Share content": "Share content", "Join": "Join", "No results": "No results", "Please create a new issue on GitHub so that we can investigate this bug.": "Please create a new issue on GitHub so that we can investigate this bug.", From eefadf6a4653d0acbe9858a8960b64d2e52ac196 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Fri, 16 Jul 2021 15:30:26 -0400 Subject: [PATCH 0446/1171] Fix tests Signed-off-by: Robin Townsend --- test/components/views/messages/TextualBody-test.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/test/components/views/messages/TextualBody-test.js b/test/components/views/messages/TextualBody-test.js index fd11a9d46b..85a02aad7b 100644 --- a/test/components/views/messages/TextualBody-test.js +++ b/test/components/views/messages/TextualBody-test.js @@ -23,6 +23,7 @@ import { mkEvent, mkStubRoom } from "../../../test-utils"; import { MatrixClientPeg } from "../../../../src/MatrixClientPeg"; import * as languageHandler from "../../../../src/languageHandler"; import * as TestUtils from "../../../test-utils"; +import DMRoomMap from "../../../../src/utils/DMRoomMap"; const _TextualBody = sdk.getComponent("views.messages.TextualBody"); const TextualBody = TestUtils.wrapInMatrixClientContext(_TextualBody); @@ -41,6 +42,7 @@ describe("", () => { isGuest: () => false, mxcUrlToHttp: (s) => s, }; + DMRoomMap.makeShared(); const ev = mkEvent({ type: "m.room.message", @@ -66,6 +68,7 @@ describe("", () => { isGuest: () => false, mxcUrlToHttp: (s) => s, }; + DMRoomMap.makeShared(); const ev = mkEvent({ type: "m.room.message", @@ -92,6 +95,7 @@ describe("", () => { isGuest: () => false, mxcUrlToHttp: (s) => s, }; + DMRoomMap.makeShared(); }); it("simple message renders as expected", () => { @@ -146,6 +150,7 @@ describe("", () => { isGuest: () => false, mxcUrlToHttp: (s) => s, }; + DMRoomMap.makeShared(); }); it("italics, bold, underline and strikethrough render as expected", () => { @@ -292,6 +297,7 @@ describe("", () => { isGuest: () => false, mxcUrlToHttp: (s) => s, }; + DMRoomMap.makeShared(); const ev = mkEvent({ type: "m.room.message", From 18ba7a280d3ff892d627a91566890ddd2a917572 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Fri, 16 Jul 2021 21:30:50 +0200 Subject: [PATCH 0447/1171] Give sources a little padding MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/css/views/elements/_DesktopCapturerSourcePicker.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/res/css/views/elements/_DesktopCapturerSourcePicker.scss b/res/css/views/elements/_DesktopCapturerSourcePicker.scss index dc435bb386..49a0a44417 100644 --- a/res/css/views/elements/_DesktopCapturerSourcePicker.scss +++ b/res/css/views/elements/_DesktopCapturerSourcePicker.scss @@ -34,6 +34,7 @@ limitations under the License. .mx_desktopCapturerSourcePicker_source_thumbnail { margin: 4px; + padding: 4px; width: 312px; border-width: 2px; border-radius: 8px; From f88d5dd24e2ad6c761c0427aca1895597d6f8f02 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Fri, 16 Jul 2021 16:36:03 -0400 Subject: [PATCH 0448/1171] Zip shortcodes in with emoji objects Signed-off-by: Robin Townsend --- src/HtmlUtils.tsx | 4 +- src/autocomplete/EmojiProvider.tsx | 33 ++++++---------- src/components/views/emojipicker/Preview.tsx | 8 +--- .../views/emojipicker/QuickReactions.tsx | 4 +- src/emoji.ts | 38 +++++++++---------- 5 files changed, 37 insertions(+), 50 deletions(-) diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx index 93cb498d21..cba9eb79b3 100644 --- a/src/HtmlUtils.tsx +++ b/src/HtmlUtils.tsx @@ -34,7 +34,7 @@ import { IExtendedSanitizeOptions } from './@types/sanitize-html'; import linkifyMatrix from './linkify-matrix'; import SettingsStore from './settings/SettingsStore'; import { tryTransformPermalinkToLocalHref } from "./utils/permalinks/Permalinks"; -import { getEmojiFromUnicode, getShortcodes } from "./emoji"; +import { getEmojiFromUnicode } from "./emoji"; import ReplyThread from "./components/views/elements/ReplyThread"; import { mediaFromMxc } from "./customisations/Media"; @@ -80,7 +80,7 @@ function mightContainEmoji(str: string): boolean { * @return {String} The shortcode (such as :thumbup:) */ export function unicodeToShortcode(char: string): string { - const shortcodes = getShortcodes(getEmojiFromUnicode(char)); + const shortcodes = getEmojiFromUnicode(char).shortcodes; return shortcodes.length > 0 ? `:${shortcodes[0]}:` : ''; } diff --git a/src/autocomplete/EmojiProvider.tsx b/src/autocomplete/EmojiProvider.tsx index edf691e151..8a81acd498 100644 --- a/src/autocomplete/EmojiProvider.tsx +++ b/src/autocomplete/EmojiProvider.tsx @@ -25,7 +25,7 @@ import { PillCompletion } from './Components'; import { ICompletion, ISelectionRange } from './Autocompleter'; import { uniq, sortBy } from 'lodash'; import SettingsStore from "../settings/SettingsStore"; -import { EMOJI, IEmoji, getShortcodes } from '../emoji'; +import { EMOJI, IEmoji } from '../emoji'; import EMOTICON_REGEX from 'emojibase-regex/emoticon'; @@ -37,8 +37,6 @@ const EMOJI_REGEX = new RegExp('(' + EMOTICON_REGEX.source + '|(?:^|\\s):[+-\\w] interface IEmojiShort { emoji: IEmoji; - shortcode: string; - altShortcodes: string[]; _orderBy: number; } @@ -47,16 +45,11 @@ const EMOJI_SHORTCODES: IEmojiShort[] = EMOJI.sort((a, b) => { return a.order - b.order; } return a.group - b.group; -}).map((emoji, index) => { - const [shortcode, ...altShortcodes] = getShortcodes(emoji); - return { - emoji, - shortcode: shortcode ? `:${shortcode}:` : undefined, - altShortcodes: altShortcodes.map(s => `:${s}:`), - // Include the index so that we can preserve the original order - _orderBy: index, - }; -}).filter(emoji => emoji.shortcode); +}).map((emoji, index) => ({ + emoji, + // Include the index so that we can preserve the original order + _orderBy: index, +})).filter(o => o.emoji.shortcodes[0]); function score(query, space) { const index = space.indexOf(query); @@ -74,10 +67,8 @@ export default class EmojiProvider extends AutocompleteProvider { constructor() { super(EMOJI_REGEX); this.matcher = new QueryMatcher(EMOJI_SHORTCODES, { - keys: ['emoji.emoticon', 'shortcode'], - funcs: [ - o => o.altShortcodes.join(" "), // aliases - ], + keys: ['emoji.emoticon'], + funcs: [o => o.emoji.shortcodes.map(s => `:${s}:`).join(" ")], // For matching against ascii equivalents shouldMatchWordsOnly: false, }); @@ -112,16 +103,16 @@ export default class EmojiProvider extends AutocompleteProvider { sorters.push(c => score(matchedString, c.emoji.emoticon || "")); // then sort by score (Infinity if matchedString not in shortcode) - sorters.push(c => score(matchedString, c.shortcode)); + sorters.push(c => score(matchedString, c.emoji.shortcodes[0])); // then sort by max score of all shortcodes, trim off the `:` sorters.push(c => Math.min( - ...[c.shortcode, ...c.altShortcodes].map(s => score(matchedString.substring(1), s)), + ...c.emoji.shortcodes.map(s => score(matchedString.substring(1), s)), )); // If the matchedString is not empty, sort by length of shortcode. Example: // matchedString = ":bookmark" // completions = [":bookmark:", ":bookmark_tabs:", ...] if (matchedString.length > 1) { - sorters.push(c => c.shortcode.length); + sorters.push(c => c.emoji.shortcodes[0].length); } // Finally, sort by original ordering sorters.push(c => c._orderBy); @@ -130,7 +121,7 @@ export default class EmojiProvider extends AutocompleteProvider { completions = completions.map(c => ({ completion: c.emoji.unicode, component: ( - + { c.emoji.unicode } ), diff --git a/src/components/views/emojipicker/Preview.tsx b/src/components/views/emojipicker/Preview.tsx index bd9982e50f..da2f8dcd89 100644 --- a/src/components/views/emojipicker/Preview.tsx +++ b/src/components/views/emojipicker/Preview.tsx @@ -17,7 +17,7 @@ limitations under the License. import React from 'react'; -import { IEmoji, getShortcodes } from "../../../emoji"; +import { IEmoji } from "../../../emoji"; import { replaceableComponent } from "../../../utils/replaceableComponent"; interface IProps { @@ -27,11 +27,7 @@ interface IProps { @replaceableComponent("views.emojipicker.Preview") class Preview extends React.PureComponent { render() { - const { - unicode = "", - annotation = "", - } = this.props.emoji; - const shortcode = getShortcodes(this.props.emoji)[0]; + const { unicode, annotation, shortcodes: [shortcode] } = this.props.emoji; return (
    diff --git a/src/components/views/emojipicker/QuickReactions.tsx b/src/components/views/emojipicker/QuickReactions.tsx index 2d78e3e4cf..9321450fc1 100644 --- a/src/components/views/emojipicker/QuickReactions.tsx +++ b/src/components/views/emojipicker/QuickReactions.tsx @@ -18,7 +18,7 @@ limitations under the License. import React from 'react'; import { _t } from '../../../languageHandler'; -import { getEmojiFromUnicode, getShortcodes, IEmoji } from "../../../emoji"; +import { getEmojiFromUnicode, IEmoji } from "../../../emoji"; import Emoji from "./Emoji"; import { replaceableComponent } from "../../../utils/replaceableComponent"; @@ -62,7 +62,7 @@ class QuickReactions extends React.Component { }; render() { - const shortcode = this.state.hover ? getShortcodes(this.state.hover)[0] : undefined; + const shortcode = this.state.hover?.shortcodes?.[0]; return (

    diff --git a/src/emoji.ts b/src/emoji.ts index ac4de654f7..fe9e52d35f 100644 --- a/src/emoji.ts +++ b/src/emoji.ts @@ -22,26 +22,19 @@ export interface IEmoji { group?: number; hexcode: string; order?: number; + shortcodes: string[]; tags?: string[]; unicode: string; emoticon?: string; -} - -interface IEmojiWithFilterString extends IEmoji { - filterString?: string; + filterString: string; } // The unicode is stored without the variant selector -const UNICODE_TO_EMOJI = new Map(); // not exported as gets for it are handled by getEmojiFromUnicode -export const EMOTICON_TO_EMOJI = new Map(); +const UNICODE_TO_EMOJI = new Map(); // not exported as gets for it are handled by getEmojiFromUnicode +export const EMOTICON_TO_EMOJI = new Map(); export const getEmojiFromUnicode = unicode => UNICODE_TO_EMOJI.get(stripVariation(unicode)); -const toArray = (shortcodes?: string | string[]): string[] => - typeof shortcodes === "string" ? [shortcodes] : (shortcodes ?? []); -export const getShortcodes = (emoji: IEmoji): string[] => - toArray(SHORTCODES[emoji.hexcode]); - const EMOJIBASE_GROUP_ID_TO_CATEGORY = [ "people", // smileys "people", // actually people @@ -69,17 +62,24 @@ export const DATA_BY_CATEGORY = { const ZERO_WIDTH_JOINER = "\u200D"; // Store various mappings from unicode/emoticon/shortcode to the Emoji objects -EMOJIBASE.forEach((emoji: IEmojiWithFilterString) => { - const shortcodes = getShortcodes(emoji); +export const EMOJI: IEmoji[] = EMOJIBASE.map(emojiData => { + const shortcodeData = SHORTCODES[emojiData.hexcode]; + // Homogenize shortcodes by ensuring that everything is an array + const shortcodes = typeof shortcodeData === "string" ? [shortcodeData] : (shortcodeData ?? []); + + const emoji: IEmoji = { + ...emojiData, + shortcodes, + // This is used as the string to match the query against when filtering emojis + filterString: (`${emojiData.annotation}\n${shortcodes.join('\n')}}\n${emojiData.emoticon || ''}\n` + + `${emojiData.unicode.split(ZERO_WIDTH_JOINER).join("\n")}`).toLowerCase(), + }; + const categoryId = EMOJIBASE_GROUP_ID_TO_CATEGORY[emoji.group]; if (DATA_BY_CATEGORY.hasOwnProperty(categoryId)) { DATA_BY_CATEGORY[categoryId].push(emoji); } - // This is used as the string to match the query against when filtering emojis - emoji.filterString = (`${emoji.annotation}\n${shortcodes.join('\n')}}\n${emoji.emoticon || ''}\n` + - `${emoji.unicode.split(ZERO_WIDTH_JOINER).join("\n")}`).toLowerCase(); - // Add mapping from unicode to Emoji object // The 'unicode' field that we use in emojibase has either // VS15 or VS16 appended to any characters that can take @@ -93,6 +93,8 @@ EMOJIBASE.forEach((emoji: IEmojiWithFilterString) => { // Add mapping from emoticon to Emoji object EMOTICON_TO_EMOJI.set(emoji.emoticon, emoji); } + + return emoji; }); /** @@ -106,5 +108,3 @@ EMOJIBASE.forEach((emoji: IEmojiWithFilterString) => { function stripVariation(str) { return str.replace(/[\uFE00-\uFE0F]$/, ""); } - -export const EMOJI: IEmoji[] = EMOJIBASE; From 0a99f76e7fc91be25a379a47f2217b2ae8f9fa4c Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 14 Jul 2021 20:51:20 -0600 Subject: [PATCH 0449/1171] Simple POC for moving download button to action bar --- src/components/views/messages/IMediaBody.ts | 32 +++++++ src/components/views/messages/MVideoBody.tsx | 50 ++++++----- .../views/messages/MessageActionBar.js | 22 +++++ src/components/views/messages/MessageEvent.js | 8 ++ src/utils/LazyValue.ts | 59 ++++++++++++ src/utils/MediaEventHelper.ts | 90 +++++++++++++++++++ 6 files changed, 237 insertions(+), 24 deletions(-) create mode 100644 src/components/views/messages/IMediaBody.ts create mode 100644 src/utils/LazyValue.ts create mode 100644 src/utils/MediaEventHelper.ts diff --git a/src/components/views/messages/IMediaBody.ts b/src/components/views/messages/IMediaBody.ts new file mode 100644 index 0000000000..dcbdfff284 --- /dev/null +++ b/src/components/views/messages/IMediaBody.ts @@ -0,0 +1,32 @@ +/* +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 EventTile from "../rooms/EventTile"; +import { MediaEventHelper } from "../../../utils/MediaEventHelper"; + +export interface IMediaBody { + getMediaHelper(): MediaEventHelper; +} + +export function canTileDownload(tile: EventTile): boolean { + if (!tile) return false; + + // Cast so we can check for IMediaBody interface safely. + // Note that we don't cast to the IMediaBody interface as that causes IDEs + // to complain about conditions always being true. + const tileAsAny = tile; + return !!tileAsAny.getMediaHelper; +} diff --git a/src/components/views/messages/MVideoBody.tsx b/src/components/views/messages/MVideoBody.tsx index d882bb1eb0..bb58a13c4d 100644 --- a/src/components/views/messages/MVideoBody.tsx +++ b/src/components/views/messages/MVideoBody.tsx @@ -26,10 +26,14 @@ import InlineSpinner from '../elements/InlineSpinner'; import { replaceableComponent } from "../../../utils/replaceableComponent"; import { mediaFromContent } from "../../../customisations/Media"; import { BLURHASH_FIELD } from "../../../ContentMessages"; +import { IMediaBody } from "./IMediaBody"; +import { MediaEventHelper } from "../../../utils/MediaEventHelper"; +import { IMediaEventContent } from "../../../customisations/models/IMediaEventContent"; +import { MatrixEvent } from "matrix-js-sdk/src"; interface IProps { /* the MatrixEvent to show */ - mxEvent: any; + mxEvent: MatrixEvent; /* called when the video has loaded */ onHeightChanged: () => void; } @@ -45,11 +49,13 @@ interface IState { } @replaceableComponent("views.messages.MVideoBody") -export default class MVideoBody extends React.PureComponent { +export default class MVideoBody extends React.PureComponent implements IMediaBody { private videoRef = React.createRef(); + private mediaHelper: MediaEventHelper; constructor(props) { super(props); + this.state = { fetchingData: false, decryptedUrl: null, @@ -59,6 +65,8 @@ export default class MVideoBody extends React.PureComponent { posterLoading: false, blurhashUrl: null, }; + + this.mediaHelper = new MediaEventHelper(this.props.mxEvent); } thumbScale(fullWidth: number, fullHeight: number, thumbWidth = 480, thumbHeight = 360) { @@ -82,6 +90,10 @@ export default class MVideoBody extends React.PureComponent { } } + public getMediaHelper(): MediaEventHelper { + return this.mediaHelper; + } + private getContentUrl(): string|null { const media = mediaFromContent(this.props.mxEvent.getContent()); if (media.isEncrypted) { @@ -97,7 +109,7 @@ export default class MVideoBody extends React.PureComponent { } private getThumbUrl(): string|null { - const content = this.props.mxEvent.getContent(); + const content = this.props.mxEvent.getContent(); const media = mediaFromContent(content); if (media.isEncrypted && this.state.decryptedThumbnailUrl) { @@ -139,7 +151,7 @@ export default class MVideoBody extends React.PureComponent { posterLoading: true, }); - const content = this.props.mxEvent.getContent(); + const content = this.props.mxEvent.getContent(); const media = mediaFromContent(content); if (media.hasThumbnail) { const image = new Image(); @@ -152,30 +164,22 @@ export default class MVideoBody extends React.PureComponent { async componentDidMount() { const autoplay = SettingsStore.getValue("autoplayGifsAndVideos") as boolean; - const content = this.props.mxEvent.getContent(); this.loadBlurhash(); - if (content.file !== undefined && this.state.decryptedUrl === null) { - let thumbnailPromise = Promise.resolve(null); - if (content?.info?.thumbnail_file) { - thumbnailPromise = decryptFile(content.info.thumbnail_file) - .then(blob => URL.createObjectURL(blob)); - } - + if (this.mediaHelper.media.isEncrypted && this.state.decryptedUrl === null) { try { - const thumbnailUrl = await thumbnailPromise; + const thumbnailUrl = await this.mediaHelper.thumbnailUrl.value; if (autoplay) { console.log("Preloading video"); - const decryptedBlob = await decryptFile(content.file); - const contentUrl = URL.createObjectURL(decryptedBlob); this.setState({ - decryptedUrl: contentUrl, + decryptedUrl: await this.mediaHelper.sourceUrl.value, decryptedThumbnailUrl: thumbnailUrl, - decryptedBlob: decryptedBlob, + decryptedBlob: await this.mediaHelper.sourceBlob.value, }); this.props.onHeightChanged(); } else { console.log("NOT preloading video"); + const content = this.props.mxEvent.getContent(); this.setState({ // For Chrome and Electron, we need to set some non-empty `src` to // enable the play button. Firefox does not seem to care either @@ -202,6 +206,7 @@ export default class MVideoBody extends React.PureComponent { if (this.state.decryptedThumbnailUrl) { URL.revokeObjectURL(this.state.decryptedThumbnailUrl); } + this.mediaHelper.destroy(); } private videoOnPlay = async () => { @@ -213,18 +218,15 @@ export default class MVideoBody extends React.PureComponent { // To stop subsequent download attempts fetchingData: true, }); - const content = this.props.mxEvent.getContent(); - if (!content.file) { + if (!this.mediaHelper.media.isEncrypted) { this.setState({ error: "No file given in content", }); return; } - const decryptedBlob = await decryptFile(content.file); - const contentUrl = URL.createObjectURL(decryptedBlob); this.setState({ - decryptedUrl: contentUrl, - decryptedBlob: decryptedBlob, + decryptedUrl: await this.mediaHelper.sourceUrl.value, + decryptedBlob: await this.mediaHelper.sourceBlob.value, fetchingData: false, }, () => { if (!this.videoRef.current) return; @@ -295,7 +297,7 @@ export default class MVideoBody extends React.PureComponent { onPlay={this.videoOnPlay} > - + {/**/} ); } diff --git a/src/components/views/messages/MessageActionBar.js b/src/components/views/messages/MessageActionBar.js index 7532554666..13854aebfc 100644 --- a/src/components/views/messages/MessageActionBar.js +++ b/src/components/views/messages/MessageActionBar.js @@ -32,6 +32,7 @@ import { replaceableComponent } from "../../../utils/replaceableComponent"; import { canCancel } from "../context_menus/MessageContextMenu"; import Resend from "../../../Resend"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; +import { canTileDownload } from "./IMediaBody"; const OptionsButton = ({ mxEvent, getTile, getReplyThread, permalinkCreator, onFocusChange }) => { const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu(); @@ -175,6 +176,16 @@ export default class MessageActionBar extends React.PureComponent { }); }; + onDownloadClick = async (ev) => { + // TODO: Maybe just call into MFileBody and render it as null + const src = this.props.getTile().getMediaHelper(); + const a = document.createElement("a"); + a.href = await src.sourceUrl.value; + a.download = "todo.png"; + a.target = "_blank"; + a.click(); + }; + /** * Runs a given fn on the set of possible events to test. The first event * that passes the checkFn will have fn executed on it. Both functions take @@ -267,6 +278,17 @@ export default class MessageActionBar extends React.PureComponent { key="react" />); } + + const tile = this.props.getTile && this.props.getTile(); + if (canTileDownload(tile)) { + toolbarOpts.splice(0, 0, ); + } } if (allowCancel) { diff --git a/src/components/views/messages/MessageEvent.js b/src/components/views/messages/MessageEvent.js index cd071ebb34..49b50b610c 100644 --- a/src/components/views/messages/MessageEvent.js +++ b/src/components/views/messages/MessageEvent.js @@ -22,6 +22,7 @@ import { Mjolnir } from "../../../mjolnir/Mjolnir"; import RedactedBody from "./RedactedBody"; import UnknownBody from "./UnknownBody"; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import { IMediaBody } from "./IMediaBody"; @replaceableComponent("views.messages.MessageEvent") export default class MessageEvent extends React.Component { @@ -69,6 +70,13 @@ export default class MessageEvent extends React.Component { this.forceUpdate(); }; + getMediaHelper() { + if (!this._body.current || !this._body.current.getMediaHelper) { + return null; + } + return this._body.current.getMediaHelper(); + } + render() { const bodyTypes = { 'm.text': sdk.getComponent('messages.TextualBody'), diff --git a/src/utils/LazyValue.ts b/src/utils/LazyValue.ts new file mode 100644 index 0000000000..9cdcda489a --- /dev/null +++ b/src/utils/LazyValue.ts @@ -0,0 +1,59 @@ +/* +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. +*/ + +/** + * Utility class for lazily getting a variable. + */ +export class LazyValue { + private val: T; + private prom: Promise; + private done = false; + + public constructor(private getFn: () => Promise) { + } + + /** + * Whether or not a cached value is present. + */ + public get present(): boolean { + // we use a tracking variable just in case the final value is falsey + return this.done; + } + + /** + * Gets the value without invoking a get. May be undefined until the + * value is fetched properly. + */ + public get cachedValue(): T { + return this.val; + } + + /** + * Gets a promise which resolves to the value, eventually. + */ + public get value(): Promise { + if (this.prom) return this.prom; + this.prom = this.getFn(); + + // Fork the promise chain to avoid accidentally making it return undefined always. + this.prom.then(v => { + this.val = v; + this.done = true; + }); + + return this.prom; + } +} diff --git a/src/utils/MediaEventHelper.ts b/src/utils/MediaEventHelper.ts new file mode 100644 index 0000000000..316ee54edf --- /dev/null +++ b/src/utils/MediaEventHelper.ts @@ -0,0 +1,90 @@ +/* +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 { MatrixEvent } from "matrix-js-sdk/src"; +import { LazyValue } from "./LazyValue"; +import { Media, mediaFromContent } from "../customisations/Media"; +import { decryptFile } from "./DecryptFile"; +import { IMediaEventContent } from "../customisations/models/IMediaEventContent"; +import { IDestroyable } from "./IDestroyable"; + +// TODO: We should consider caching the blobs. https://github.com/vector-im/element-web/issues/17192 + +export class MediaEventHelper implements IDestroyable { + public readonly sourceUrl: LazyValue; + public readonly thumbnailUrl: LazyValue; + public readonly sourceBlob: LazyValue; + public readonly thumbnailBlob: LazyValue; + public readonly media: Media; + + public constructor(private event: MatrixEvent) { + this.sourceUrl = new LazyValue(this.prepareSourceUrl); + this.thumbnailUrl = new LazyValue(this.prepareThumbnailUrl); + this.sourceBlob = new LazyValue(this.fetchSource); + this.thumbnailBlob = new LazyValue(this.fetchThumbnail); + + this.media = mediaFromContent(this.event.getContent()); + } + + public destroy() { + if (this.media.isEncrypted) { + if (this.sourceUrl.present) URL.revokeObjectURL(this.sourceUrl.cachedValue); + if (this.thumbnailUrl.present) URL.revokeObjectURL(this.thumbnailUrl.cachedValue); + } + } + + private prepareSourceUrl = async () => { + if (this.media.isEncrypted) { + const blob = await this.sourceBlob.value; + return URL.createObjectURL(blob); + } else { + return this.media.srcHttp; + } + }; + + private prepareThumbnailUrl = async () => { + if (this.media.isEncrypted) { + const blob = await this.thumbnailBlob.value; + return URL.createObjectURL(blob); + } else { + return this.media.thumbnailHttp; + } + }; + + private fetchSource = () => { + if (this.media.isEncrypted) { + return decryptFile(this.event.getContent().file); + } + return this.media.downloadSource().then(r => r.blob()); + }; + + private fetchThumbnail = () => { + if (!this.media.hasThumbnail) return Promise.resolve(null); + + if (this.media.isEncrypted) { + const content = this.event.getContent(); + if (content.info?.thumbnail_file) { + return decryptFile(content.info.thumbnail_file); + } else { + // "Should never happen" + console.warn("Media claims to have thumbnail and is encrypted, but no thumbnail_file found"); + return Promise.resolve(null); + } + } + + return fetch(this.media.thumbnailHttp).then(r => r.blob()); + }; +} From 703cf7375912898597ec42a92ca85833c242e79a Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 15 Jul 2021 14:19:07 -0600 Subject: [PATCH 0450/1171] Convert MessageEvent to TS and hoist MediaEventHelper --- src/@types/common.ts | 3 +- .../context_menus/MessageContextMenu.tsx | 6 +- src/components/views/messages/MVideoBody.tsx | 37 +--- src/components/views/messages/MessageEvent.js | 146 --------------- .../views/messages/MessageEvent.tsx | 176 ++++++++++++++++++ src/utils/MediaEventHelper.ts | 21 +++ 6 files changed, 213 insertions(+), 176 deletions(-) delete mode 100644 src/components/views/messages/MessageEvent.js create mode 100644 src/components/views/messages/MessageEvent.tsx diff --git a/src/@types/common.ts b/src/@types/common.ts index 1fb9ba4303..36ef7a9ace 100644 --- a/src/@types/common.ts +++ b/src/@types/common.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { JSXElementConstructor } from "react"; +import React, { JSXElementConstructor } from "react"; // Based on https://stackoverflow.com/a/53229857/3532235 export type Without = {[P in Exclude]?: never}; @@ -22,3 +22,4 @@ export type XOR = (T | U) extends object ? (Without & U) | (Without< export type Writeable = { -readonly [P in keyof T]: T[P] }; export type ComponentClass = keyof JSX.IntrinsicElements | JSXElementConstructor; +export type ReactAnyComponent = React.Component | React.ExoticComponent; diff --git a/src/components/views/context_menus/MessageContextMenu.tsx b/src/components/views/context_menus/MessageContextMenu.tsx index 999e98f4ad..7092be43e9 100644 --- a/src/components/views/context_menus/MessageContextMenu.tsx +++ b/src/components/views/context_menus/MessageContextMenu.tsx @@ -43,11 +43,15 @@ export function canCancel(eventStatus: EventStatus): boolean { return eventStatus === EventStatus.QUEUED || eventStatus === EventStatus.NOT_SENT; } -interface IEventTileOps { +export interface IEventTileOps { isWidgetHidden(): boolean; unhideWidget(): void; } +export interface IOperableEventTile { + getEventTileOps(): IEventTileOps; +} + interface IProps { /* the MatrixEvent associated with the context menu */ mxEvent: MatrixEvent; diff --git a/src/components/views/messages/MVideoBody.tsx b/src/components/views/messages/MVideoBody.tsx index bb58a13c4d..2b873f6506 100644 --- a/src/components/views/messages/MVideoBody.tsx +++ b/src/components/views/messages/MVideoBody.tsx @@ -18,15 +18,12 @@ limitations under the License. import React from 'react'; import { decode } from "blurhash"; -import MFileBody from './MFileBody'; -import { decryptFile } from '../../../utils/DecryptFile'; import { _t } from '../../../languageHandler'; import SettingsStore from "../../../settings/SettingsStore"; import InlineSpinner from '../elements/InlineSpinner'; import { replaceableComponent } from "../../../utils/replaceableComponent"; import { mediaFromContent } from "../../../customisations/Media"; import { BLURHASH_FIELD } from "../../../ContentMessages"; -import { IMediaBody } from "./IMediaBody"; import { MediaEventHelper } from "../../../utils/MediaEventHelper"; import { IMediaEventContent } from "../../../customisations/models/IMediaEventContent"; import { MatrixEvent } from "matrix-js-sdk/src"; @@ -36,6 +33,7 @@ interface IProps { mxEvent: MatrixEvent; /* called when the video has loaded */ onHeightChanged: () => void; + mediaEventHelper: MediaEventHelper; } interface IState { @@ -49,9 +47,8 @@ interface IState { } @replaceableComponent("views.messages.MVideoBody") -export default class MVideoBody extends React.PureComponent implements IMediaBody { +export default class MVideoBody extends React.PureComponent { private videoRef = React.createRef(); - private mediaHelper: MediaEventHelper; constructor(props) { super(props); @@ -65,8 +62,6 @@ export default class MVideoBody extends React.PureComponent impl posterLoading: false, blurhashUrl: null, }; - - this.mediaHelper = new MediaEventHelper(this.props.mxEvent); } thumbScale(fullWidth: number, fullHeight: number, thumbWidth = 480, thumbHeight = 360) { @@ -90,10 +85,6 @@ export default class MVideoBody extends React.PureComponent impl } } - public getMediaHelper(): MediaEventHelper { - return this.mediaHelper; - } - private getContentUrl(): string|null { const media = mediaFromContent(this.props.mxEvent.getContent()); if (media.isEncrypted) { @@ -166,15 +157,15 @@ export default class MVideoBody extends React.PureComponent impl const autoplay = SettingsStore.getValue("autoplayGifsAndVideos") as boolean; this.loadBlurhash(); - if (this.mediaHelper.media.isEncrypted && this.state.decryptedUrl === null) { + if (this.props.mediaEventHelper.media.isEncrypted && this.state.decryptedUrl === null) { try { - const thumbnailUrl = await this.mediaHelper.thumbnailUrl.value; + const thumbnailUrl = await this.props.mediaEventHelper.thumbnailUrl.value; if (autoplay) { console.log("Preloading video"); this.setState({ - decryptedUrl: await this.mediaHelper.sourceUrl.value, + decryptedUrl: await this.props.mediaEventHelper.sourceUrl.value, decryptedThumbnailUrl: thumbnailUrl, - decryptedBlob: await this.mediaHelper.sourceBlob.value, + decryptedBlob: await this.props.mediaEventHelper.sourceBlob.value, }); this.props.onHeightChanged(); } else { @@ -199,16 +190,6 @@ export default class MVideoBody extends React.PureComponent impl } } - componentWillUnmount() { - if (this.state.decryptedUrl) { - URL.revokeObjectURL(this.state.decryptedUrl); - } - if (this.state.decryptedThumbnailUrl) { - URL.revokeObjectURL(this.state.decryptedThumbnailUrl); - } - this.mediaHelper.destroy(); - } - private videoOnPlay = async () => { if (this.hasContentUrl() || this.state.fetchingData || this.state.error) { // We have the file, we are fetching the file, or there is an error. @@ -218,15 +199,15 @@ export default class MVideoBody extends React.PureComponent impl // To stop subsequent download attempts fetchingData: true, }); - if (!this.mediaHelper.media.isEncrypted) { + if (!this.props.mediaEventHelper.media.isEncrypted) { this.setState({ error: "No file given in content", }); return; } this.setState({ - decryptedUrl: await this.mediaHelper.sourceUrl.value, - decryptedBlob: await this.mediaHelper.sourceBlob.value, + decryptedUrl: await this.props.mediaEventHelper.sourceUrl.value, + decryptedBlob: await this.props.mediaEventHelper.sourceBlob.value, fetchingData: false, }, () => { if (!this.videoRef.current) return; diff --git a/src/components/views/messages/MessageEvent.js b/src/components/views/messages/MessageEvent.js deleted file mode 100644 index 49b50b610c..0000000000 --- a/src/components/views/messages/MessageEvent.js +++ /dev/null @@ -1,146 +0,0 @@ -/* -Copyright 2015, 2016 OpenMarket 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 React, { createRef } from 'react'; -import PropTypes from 'prop-types'; -import * as sdk from '../../../index'; -import SettingsStore from "../../../settings/SettingsStore"; -import { Mjolnir } from "../../../mjolnir/Mjolnir"; -import RedactedBody from "./RedactedBody"; -import UnknownBody from "./UnknownBody"; -import { replaceableComponent } from "../../../utils/replaceableComponent"; -import { IMediaBody } from "./IMediaBody"; - -@replaceableComponent("views.messages.MessageEvent") -export default class MessageEvent extends React.Component { - static propTypes = { - /* the MatrixEvent to show */ - mxEvent: PropTypes.object.isRequired, - - /* a list of words to highlight */ - highlights: PropTypes.array, - - /* link URL for the highlights */ - highlightLink: PropTypes.string, - - /* should show URL previews for this event */ - showUrlPreview: PropTypes.bool, - - /* callback called when dynamic content in events are loaded */ - onHeightChanged: PropTypes.func, - - /* the shape of the tile, used */ - tileShape: PropTypes.string, // TODO: Use TileShape enum - - /* the maximum image height to use, if the event is an image */ - maxImageHeight: PropTypes.number, - - /* overrides for the msgtype-specific components, used by ReplyTile to override file rendering */ - overrideBodyTypes: PropTypes.object, - overrideEventTypes: PropTypes.object, - - /* the permalinkCreator */ - permalinkCreator: PropTypes.object, - }; - - constructor(props) { - super(props); - - this._body = createRef(); - } - - getEventTileOps = () => { - return this._body.current && this._body.current.getEventTileOps ? this._body.current.getEventTileOps() : null; - }; - - onTileUpdate = () => { - this.forceUpdate(); - }; - - getMediaHelper() { - if (!this._body.current || !this._body.current.getMediaHelper) { - return null; - } - return this._body.current.getMediaHelper(); - } - - render() { - const bodyTypes = { - 'm.text': sdk.getComponent('messages.TextualBody'), - 'm.notice': sdk.getComponent('messages.TextualBody'), - 'm.emote': sdk.getComponent('messages.TextualBody'), - 'm.image': sdk.getComponent('messages.MImageBody'), - 'm.file': sdk.getComponent('messages.MFileBody'), - 'm.audio': sdk.getComponent('messages.MVoiceOrAudioBody'), - '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(); - const type = this.props.mxEvent.getType(); - const msgtype = content.msgtype; - let BodyType = RedactedBody; - if (!this.props.mxEvent.isRedacted()) { - // only resolve BodyType if event is not redacted - if (type && evTypes[type]) { - BodyType = evTypes[type]; - } else if (msgtype && bodyTypes[msgtype]) { - BodyType = bodyTypes[msgtype]; - } else if (content.url) { - // Fallback to MFileBody if there's a content URL - BodyType = bodyTypes['m.file']; - } else { - // Fallback to UnknownBody otherwise if not redacted - BodyType = UnknownBody; - } - } - - if (SettingsStore.getValue("feature_mjolnir")) { - const key = `mx_mjolnir_render_${this.props.mxEvent.getRoomId()}__${this.props.mxEvent.getId()}`; - const allowRender = localStorage.getItem(key) === "true"; - - if (!allowRender) { - const userDomain = this.props.mxEvent.getSender().split(':').slice(1).join(':'); - const userBanned = Mjolnir.sharedInstance().isUserBanned(this.props.mxEvent.getSender()); - const serverBanned = Mjolnir.sharedInstance().isServerBanned(userDomain); - - if (userBanned || serverBanned) { - BodyType = sdk.getComponent('messages.MjolnirBody'); - } - } - } - - return BodyType ? : null; - } -} diff --git a/src/components/views/messages/MessageEvent.tsx b/src/components/views/messages/MessageEvent.tsx new file mode 100644 index 0000000000..3c59e68c8b --- /dev/null +++ b/src/components/views/messages/MessageEvent.tsx @@ -0,0 +1,176 @@ +/* +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. +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 * as sdk from '../../../index'; +import SettingsStore from "../../../settings/SettingsStore"; +import { Mjolnir } from "../../../mjolnir/Mjolnir"; +import RedactedBody from "./RedactedBody"; +import UnknownBody from "./UnknownBody"; +import { replaceableComponent } from "../../../utils/replaceableComponent"; +import { IMediaBody } from "./IMediaBody"; +import { MatrixEvent } from "matrix-js-sdk/src"; +import { TileShape } from "../rooms/EventTile"; +import PermalinkConstructor from "../../../utils/permalinks/PermalinkConstructor"; +import { IOperableEventTile } from "../context_menus/MessageContextMenu"; +import { MediaEventHelper } from "../../../utils/MediaEventHelper"; +import { ReactAnyComponent } from "../../../@types/common"; +import { EventType, MsgType } from "matrix-js-sdk/src/@types/event"; + +interface IProps { + /* the MatrixEvent to show */ + mxEvent: MatrixEvent; + + /* a list of words to highlight */ + highlights: string[]; + + /* link URL for the highlights */ + highlightLink: string; + + /* should show URL previews for this event */ + showUrlPreview: boolean; + + /* callback called when dynamic content in events are loaded */ + onHeightChanged: () => void; + + /* the shape of the tile, used */ + tileShape: TileShape; + + /* the maximum image height to use, if the event is an image */ + maxImageHeight?: number; + + /* overrides for the msgtype-specific components, used by ReplyTile to override file rendering */ + overrideBodyTypes?: Record; + overrideEventTypes?: Record; + + /* the permalinkCreator */ + permalinkCreator: PermalinkConstructor; + + replacingEventId?: string; + editState?: unknown; +} + +@replaceableComponent("views.messages.MessageEvent") +export default class MessageEvent extends React.Component implements IMediaBody, IOperableEventTile { + private body: React.RefObject = createRef(); + private mediaHelper: MediaEventHelper; + + public constructor(props: IProps) { + super(props); + + if (MediaEventHelper.isEligible(this.props.mxEvent)) { + this.mediaHelper = new MediaEventHelper(this.props.mxEvent); + } + } + + public componentWillUnmount() { + this.mediaHelper?.destroy(); + } + + public componentDidUpdate(prevProps: Readonly) { + if (this.props.mxEvent !== prevProps.mxEvent && MediaEventHelper.isEligible(this.props.mxEvent)) { + this.mediaHelper?.destroy(); + this.mediaHelper = new MediaEventHelper(this.props.mxEvent); + } + } + + private get bodyTypes(): Record { + return { + [MsgType.Text]: sdk.getComponent('messages.TextualBody'), + [MsgType.Notice]: sdk.getComponent('messages.TextualBody'), + [MsgType.Emote]: sdk.getComponent('messages.TextualBody'), + [MsgType.Image]: sdk.getComponent('messages.MImageBody'), + [MsgType.File]: sdk.getComponent('messages.MFileBody'), + [MsgType.Audio]: sdk.getComponent('messages.MVoiceOrAudioBody'), + [MsgType.Video]: sdk.getComponent('messages.MVideoBody'), + + ...(this.props.overrideBodyTypes || {}), + }; + } + + private get evTypes(): Record { + return { + [EventType.Sticker]: sdk.getComponent('messages.MStickerBody'), + + ...(this.props.overrideEventTypes || {}), + }; + } + + public getEventTileOps = () => { + return (this.body.current as IOperableEventTile)?.getEventTileOps?.() || null; + }; + + public getMediaHelper() { + return this.mediaHelper; + } + + private onTileUpdate = () => { + this.forceUpdate(); + }; + + public render() { + const content = this.props.mxEvent.getContent(); + const type = this.props.mxEvent.getType(); + const msgtype = content.msgtype; + let BodyType: ReactAnyComponent = RedactedBody; + if (!this.props.mxEvent.isRedacted()) { + // only resolve BodyType if event is not redacted + if (type && this.evTypes[type]) { + BodyType = this.evTypes[type]; + } else if (msgtype && this.bodyTypes[msgtype]) { + BodyType = this.bodyTypes[msgtype]; + } else if (content.url) { + // Fallback to MFileBody if there's a content URL + BodyType = this.bodyTypes[MsgType.File]; + } else { + // Fallback to UnknownBody otherwise if not redacted + BodyType = UnknownBody; + } + } + + if (SettingsStore.getValue("feature_mjolnir")) { + const key = `mx_mjolnir_render_${this.props.mxEvent.getRoomId()}__${this.props.mxEvent.getId()}`; + const allowRender = localStorage.getItem(key) === "true"; + + if (!allowRender) { + const userDomain = this.props.mxEvent.getSender().split(':').slice(1).join(':'); + const userBanned = Mjolnir.sharedInstance().isUserBanned(this.props.mxEvent.getSender()); + const serverBanned = Mjolnir.sharedInstance().isServerBanned(userDomain); + + if (userBanned || serverBanned) { + BodyType = sdk.getComponent('messages.MjolnirBody'); + } + } + } + + // @ts-ignore - this is a dynamic react component + return BodyType ? : null; + } +} diff --git a/src/utils/MediaEventHelper.ts b/src/utils/MediaEventHelper.ts index 316ee54edf..b4deb1a8ce 100644 --- a/src/utils/MediaEventHelper.ts +++ b/src/utils/MediaEventHelper.ts @@ -20,6 +20,7 @@ import { Media, mediaFromContent } from "../customisations/Media"; import { decryptFile } from "./DecryptFile"; import { IMediaEventContent } from "../customisations/models/IMediaEventContent"; import { IDestroyable } from "./IDestroyable"; +import { EventType, MsgType } from "matrix-js-sdk/src/@types/event"; // TODO: We should consider caching the blobs. https://github.com/vector-im/element-web/issues/17192 @@ -87,4 +88,24 @@ export class MediaEventHelper implements IDestroyable { return fetch(this.media.thumbnailHttp).then(r => r.blob()); }; + + public static isEligible(event: MatrixEvent): boolean { + if (!event) return false; + if (event.isRedacted()) return false; + if (event.getType() === EventType.Sticker) return true; + if (event.getType() !== EventType.RoomMessage) return false; + + const content = event.getContent(); + const mediaMsgTypes: string[] = [ + MsgType.Video, + MsgType.Audio, + MsgType.Image, + MsgType.File, + ]; + if (mediaMsgTypes.includes(content.msgtype)) return true; + if (typeof(content.url) === 'string') return true; + + // Finally, it's probably not media + return false; + } } From 584ffbd32777199263ad67c6b5331edc1a03b6a2 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 15 Jul 2021 14:25:43 -0600 Subject: [PATCH 0451/1171] Fix refreshing the page not showing a download --- src/components/views/messages/MessageActionBar.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/components/views/messages/MessageActionBar.js b/src/components/views/messages/MessageActionBar.js index 13854aebfc..730a929ddd 100644 --- a/src/components/views/messages/MessageActionBar.js +++ b/src/components/views/messages/MessageActionBar.js @@ -33,6 +33,7 @@ import { canCancel } from "../context_menus/MessageContextMenu"; import Resend from "../../../Resend"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; import { canTileDownload } from "./IMediaBody"; +import {MediaEventHelper} from "../../../utils/MediaEventHelper"; const OptionsButton = ({ mxEvent, getTile, getReplyThread, permalinkCreator, onFocusChange }) => { const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu(); @@ -177,6 +178,11 @@ export default class MessageActionBar extends React.PureComponent { }; onDownloadClick = async (ev) => { + if (!this.props.getTile || !this.props.getTile().getMediaHelper) { + console.warn("Action bar triggered a download but the event tile is missing a media helper"); + return; + } + // TODO: Maybe just call into MFileBody and render it as null const src = this.props.getTile().getMediaHelper(); const a = document.createElement("a"); @@ -279,8 +285,8 @@ export default class MessageActionBar extends React.PureComponent { />); } - const tile = this.props.getTile && this.props.getTile(); - if (canTileDownload(tile)) { + // XXX: Assuming that the underlying tile will be a media event if it is eligible media. + if (MediaEventHelper.isEligible(this.props.mxEvent)) { toolbarOpts.splice(0, 0, Date: Thu, 15 Jul 2021 14:34:40 -0600 Subject: [PATCH 0452/1171] Clean up after POC --- src/components/views/messages/IMediaBody.ts | 11 ----------- src/components/views/messages/MessageActionBar.js | 3 +-- 2 files changed, 1 insertion(+), 13 deletions(-) diff --git a/src/components/views/messages/IMediaBody.ts b/src/components/views/messages/IMediaBody.ts index dcbdfff284..27b5f24275 100644 --- a/src/components/views/messages/IMediaBody.ts +++ b/src/components/views/messages/IMediaBody.ts @@ -14,19 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ -import EventTile from "../rooms/EventTile"; import { MediaEventHelper } from "../../../utils/MediaEventHelper"; export interface IMediaBody { getMediaHelper(): MediaEventHelper; } - -export function canTileDownload(tile: EventTile): boolean { - if (!tile) return false; - - // Cast so we can check for IMediaBody interface safely. - // Note that we don't cast to the IMediaBody interface as that causes IDEs - // to complain about conditions always being true. - const tileAsAny = tile; - return !!tileAsAny.getMediaHelper; -} diff --git a/src/components/views/messages/MessageActionBar.js b/src/components/views/messages/MessageActionBar.js index 730a929ddd..1cb86f168d 100644 --- a/src/components/views/messages/MessageActionBar.js +++ b/src/components/views/messages/MessageActionBar.js @@ -32,8 +32,7 @@ import { replaceableComponent } from "../../../utils/replaceableComponent"; import { canCancel } from "../context_menus/MessageContextMenu"; import Resend from "../../../Resend"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; -import { canTileDownload } from "./IMediaBody"; -import {MediaEventHelper} from "../../../utils/MediaEventHelper"; +import { MediaEventHelper } from "../../../utils/MediaEventHelper"; const OptionsButton = ({ mxEvent, getTile, getReplyThread, permalinkCreator, onFocusChange }) => { const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu(); From 5fce0ccd9d23e5dd8c5114b4cf2a50f506f608a5 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 15 Jul 2021 16:37:48 -0600 Subject: [PATCH 0453/1171] Convert images, audio, and voice messages over to the new helper --- src/components/views/messages/MAudioBody.tsx | 43 +++++++------ src/components/views/messages/MImageBody.tsx | 51 +++++---------- .../views/messages/MVoiceMessageBody.tsx | 64 ++----------------- 3 files changed, 41 insertions(+), 117 deletions(-) diff --git a/src/components/views/messages/MAudioBody.tsx b/src/components/views/messages/MAudioBody.tsx index bc7216f42c..4f688fd136 100644 --- a/src/components/views/messages/MAudioBody.tsx +++ b/src/components/views/messages/MAudioBody.tsx @@ -18,22 +18,22 @@ import React from "react"; import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { replaceableComponent } from "../../../utils/replaceableComponent"; import { Playback } from "../../../voice/Playback"; -import MFileBody from "./MFileBody"; import InlineSpinner from '../elements/InlineSpinner'; import { _t } from "../../../languageHandler"; -import { mediaFromContent } from "../../../customisations/Media"; -import { decryptFile } from "../../../utils/DecryptFile"; -import { IMediaEventContent } from "../../../customisations/models/IMediaEventContent"; import AudioPlayer from "../audio_messages/AudioPlayer"; +import { MediaEventHelper } from "../../../utils/MediaEventHelper"; +import { IMediaEventContent } from "../../../customisations/models/IMediaEventContent"; +import { TileShape } from "../rooms/EventTile"; interface IProps { mxEvent: MatrixEvent; + tileShape?: TileShape; + mediaEventHelper: MediaEventHelper; } interface IState { error?: Error; playback?: Playback; - decryptedBlob?: Blob; } @replaceableComponent("views.messages.MAudioBody") @@ -46,33 +46,34 @@ export default class MAudioBody extends React.PureComponent { public async componentDidMount() { let buffer: ArrayBuffer; - const content: IMediaEventContent = this.props.mxEvent.getContent(); - const media = mediaFromContent(content); - if (media.isEncrypted) { + + try { try { - const blob = await decryptFile(content.file); + const blob = await this.props.mediaEventHelper.sourceBlob.value; buffer = await blob.arrayBuffer(); - this.setState({ decryptedBlob: blob }); } catch (e) { this.setState({ error: e }); console.warn("Unable to decrypt audio message", e); return; // stop processing the audio file } - } else { - try { - buffer = await media.downloadSource().then(r => r.blob()).then(r => r.arrayBuffer()); - } catch (e) { - this.setState({ error: e }); - console.warn("Unable to download audio message", e); - return; // stop processing the audio file - } + } catch (e) { + this.setState({ error: e }); + console.warn("Unable to decrypt/download audio message", e); + return; // stop processing the audio file } // We should have a buffer to work with now: let's set it up - const playback = new Playback(buffer); + + // Note: we don't actually need a waveform to render an audio event, but voice messages do. + const content = this.props.mxEvent.getContent(); + const waveform = content?.["org.matrix.msc1767.audio"]?.waveform?.map(p => p / 1024); + + // We should have a buffer to work with now: let's set it up + const playback = new Playback(buffer, waveform); playback.clockInfo.populatePlaceholdersFrom(this.props.mxEvent); this.setState({ playback }); - // Note: the RecordingPlayback component will handle preparing the Playback class for us. + + // Note: the components later on will handle preparing the Playback class for us. } public componentWillUnmount() { @@ -103,7 +104,7 @@ export default class MAudioBody extends React.PureComponent { return ( - + {/**/} ); } diff --git a/src/components/views/messages/MImageBody.tsx b/src/components/views/messages/MImageBody.tsx index 96c8652aee..9325c39982 100644 --- a/src/components/views/messages/MImageBody.tsx +++ b/src/components/views/messages/MImageBody.tsx @@ -1,6 +1,5 @@ /* -Copyright 2015, 2016 OpenMarket Ltd -Copyright 2018 New Vector Ltd +Copyright 2015 - 2021 The Matrix.org Foundation C.I.C. Copyright 2018, 2019 Michael Telatynski <7t3chguy@gmail.com> Licensed under the Apache License, Version 2.0 (the "License"); @@ -21,7 +20,6 @@ import { Blurhash } from "react-blurhash"; import MFileBody from './MFileBody'; import Modal from '../../../Modal'; -import { decryptFile } from '../../../utils/DecryptFile'; import { _t } from '../../../languageHandler'; import SettingsStore from "../../../settings/SettingsStore"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; @@ -34,6 +32,7 @@ import { RoomPermalinkCreator } from '../../../utils/permalinks/Permalinks'; import { IMediaEventContent } from '../../../customisations/models/IMediaEventContent'; import ImageView from '../elements/ImageView'; import { SyncState } from 'matrix-js-sdk/src/sync.api'; +import { MediaEventHelper } from "../../../utils/MediaEventHelper"; export interface IProps { /* the MatrixEvent to show */ @@ -46,6 +45,7 @@ export interface IProps { /* the permalinkCreator */ permalinkCreator?: RoomPermalinkCreator; + mediaEventHelper: MediaEventHelper; } interface IState { @@ -257,38 +257,24 @@ export default class MImageBody extends React.Component { } } - private downloadImage(): void { + private async downloadImage() { const content = this.props.mxEvent.getContent(); - if (content.file !== undefined && this.state.decryptedUrl === null) { - let thumbnailPromise = Promise.resolve(null); - if (content.info && content.info.thumbnail_file) { - thumbnailPromise = decryptFile( - content.info.thumbnail_file, - ).then(function(blob) { - return URL.createObjectURL(blob); + if (this.props.mediaEventHelper.media.isEncrypted && this.state.decryptedUrl === null) { + try { + const thumbnailUrl = await this.props.mediaEventHelper.thumbnailUrl.value; + this.setState({ + decryptedUrl: await this.props.mediaEventHelper.sourceUrl.value, + decryptedThumbnailUrl: thumbnailUrl, + decryptedBlob: await this.props.mediaEventHelper.sourceBlob.value, }); - } - let decryptedBlob; - thumbnailPromise.then((thumbnailUrl) => { - return decryptFile(content.file).then(function(blob) { - decryptedBlob = blob; - return URL.createObjectURL(blob); - }).then((contentUrl) => { - if (this.unmounted) return; - this.setState({ - decryptedUrl: contentUrl, - decryptedThumbnailUrl: thumbnailUrl, - decryptedBlob: decryptedBlob, - }); - }); - }).catch((err) => { + } catch (err) { if (this.unmounted) return; console.warn("Unable to decrypt attachment: ", err); // Set a placeholder image when we can't decrypt the image. this.setState({ error: err, }); - }); + } } } @@ -300,10 +286,10 @@ export default class MImageBody extends React.Component { localStorage.getItem("mx_ShowImage_" + this.props.mxEvent.getId()) === "true"; if (showImage) { - // Don't download anything becaue we don't want to display anything. + // noinspection JSIgnoredPromiseFromCall this.downloadImage(); this.setState({ showImage: true }); - } + } // else don't download anything because we don't want to display anything. this._afterComponentDidMount(); } @@ -316,13 +302,6 @@ export default class MImageBody extends React.Component { componentWillUnmount() { this.unmounted = true; this.context.removeListener('sync', this.onClientSync); - - if (this.state.decryptedUrl) { - URL.revokeObjectURL(this.state.decryptedUrl); - } - if (this.state.decryptedThumbnailUrl) { - URL.revokeObjectURL(this.state.decryptedThumbnailUrl); - } } protected messageContent( diff --git a/src/components/views/messages/MVoiceMessageBody.tsx b/src/components/views/messages/MVoiceMessageBody.tsx index bec224dd2d..65426cdad2 100644 --- a/src/components/views/messages/MVoiceMessageBody.tsx +++ b/src/components/views/messages/MVoiceMessageBody.tsx @@ -15,72 +15,16 @@ limitations under the License. */ import React from "react"; -import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { replaceableComponent } from "../../../utils/replaceableComponent"; -import { Playback } from "../../../voice/Playback"; -import MFileBody from "./MFileBody"; import InlineSpinner from '../elements/InlineSpinner'; import { _t } from "../../../languageHandler"; -import { mediaFromContent } from "../../../customisations/Media"; -import { decryptFile } from "../../../utils/DecryptFile"; import RecordingPlayback from "../audio_messages/RecordingPlayback"; -import { IMediaEventContent } from "../../../customisations/models/IMediaEventContent"; -import { TileShape } from "../rooms/EventTile"; - -interface IProps { - mxEvent: MatrixEvent; - tileShape?: TileShape; -} - -interface IState { - error?: Error; - playback?: Playback; - decryptedBlob?: Blob; -} +import MAudioBody from "./MAudioBody"; @replaceableComponent("views.messages.MVoiceMessageBody") -export default class MVoiceMessageBody extends React.PureComponent { - constructor(props: IProps) { - super(props); +export default class MVoiceMessageBody extends MAudioBody { - this.state = {}; - } - - public async componentDidMount() { - let buffer: ArrayBuffer; - const content: IMediaEventContent = this.props.mxEvent.getContent(); - const media = mediaFromContent(content); - if (media.isEncrypted) { - try { - const blob = await decryptFile(content.file); - buffer = await blob.arrayBuffer(); - this.setState({ decryptedBlob: blob }); - } catch (e) { - this.setState({ error: e }); - console.warn("Unable to decrypt voice message", e); - return; // stop processing the audio file - } - } else { - try { - buffer = await media.downloadSource().then(r => r.blob()).then(r => r.arrayBuffer()); - } catch (e) { - this.setState({ error: e }); - console.warn("Unable to download voice message", e); - return; // stop processing the audio file - } - } - - const waveform = content?.["org.matrix.msc1767.audio"]?.waveform?.map(p => p / 1024); - - // We should have a buffer to work with now: let's set it up - const playback = new Playback(buffer, waveform); - this.setState({ playback }); - // Note: the RecordingPlayback component will handle preparing the Playback class for us. - } - - public componentWillUnmount() { - this.state.playback?.destroy(); - } + // A voice message is an audio file but rendered in a special way. public render() { if (this.state.error) { @@ -106,7 +50,7 @@ export default class MVoiceMessageBody extends React.PureComponent - + {/**/} ); } From ea7513fc16fd902d86069d45274f02dca2d292e8 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 15 Jul 2021 16:48:21 -0600 Subject: [PATCH 0454/1171] Convert MFileBody to TS and use media helper --- .../messages/{MFileBody.js => MFileBody.tsx} | 93 ++++++++++--------- 1 file changed, 47 insertions(+), 46 deletions(-) rename src/components/views/messages/{MFileBody.js => MFileBody.tsx} (86%) diff --git a/src/components/views/messages/MFileBody.js b/src/components/views/messages/MFileBody.tsx similarity index 86% rename from src/components/views/messages/MFileBody.js rename to src/components/views/messages/MFileBody.tsx index 9236c77e8d..f1f004ef21 100644 --- a/src/components/views/messages/MFileBody.js +++ b/src/components/views/messages/MFileBody.tsx @@ -25,6 +25,9 @@ import { replaceableComponent } from "../../../utils/replaceableComponent"; import { mediaFromContent } from "../../../customisations/Media"; import ErrorDialog from "../dialogs/ErrorDialog"; import { TileShape } from "../rooms/EventTile"; +import { IContent, MatrixEvent } from "matrix-js-sdk/src"; +import { MediaEventHelper } from "../../../utils/MediaEventHelper"; +import { IMediaEventContent } from "../../../customisations/models/IMediaEventContent"; let downloadIconUrl; // cached copy of the download.svg asset for the sandboxed iframe later on @@ -35,6 +38,7 @@ async function cacheDownloadIcon() { } // Cache the asset immediately +// noinspection JSIgnoredPromiseFromCall cacheDownloadIcon(); // User supplied content can contain scripts, we have to be careful that @@ -98,7 +102,7 @@ function computedStyle(element) { * @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) { +export function presentableTextForFile(content: IContent, withSize = true): string { let linkText = _t("Attachment"); if (content.body && content.body.length > 0) { // The content body should be the name of the file including a @@ -119,53 +123,56 @@ export function presentableTextForFile(content, withSize = true) { return linkText; } -@replaceableComponent("views.messages.MFileBody") -export default class MFileBody extends React.Component { - static propTypes = { - /* the MatrixEvent to show */ - mxEvent: PropTypes.object.isRequired, - /* already decrypted blob */ - decryptedBlob: PropTypes.object, - /* called when the download link iframe is shown */ - onHeightChanged: PropTypes.func, - /* the shape of the tile, used */ - tileShape: PropTypes.string, - /* whether or not to show the default placeholder for the file. Defaults to true. */ - showGenericPlaceholder: PropTypes.bool, - }; +interface IProps { + /* the MatrixEvent to show */ + mxEvent: MatrixEvent; + /* called when the download link iframe is shown */ + onHeightChanged: () => void; + /* the shape of the tile, used */ + tileShape: TileShape; + /* whether or not to show the default placeholder for the file. Defaults to true. */ + showGenericPlaceholder: boolean; + /* helper which contains the file access */ + mediaEventHelper: MediaEventHelper; +} +interface IState { + decryptedBlob?: Blob; +} + +@replaceableComponent("views.messages.MFileBody") +export default class MFileBody extends React.Component { static defaultProps = { showGenericPlaceholder: true, }; - constructor(props) { + private iframe: React.RefObject = createRef(); + private dummyLink: React.RefObject = createRef(); + private userDidClick = false; + + public constructor(props: IProps) { super(props); - this.state = { - decryptedBlob: (this.props.decryptedBlob ? this.props.decryptedBlob : null), - }; - - this._iframe = createRef(); - this._dummyLink = createRef(); + this.state = {}; } - _getContentUrl() { + private getContentUrl(): string { const media = mediaFromContent(this.props.mxEvent.getContent()); return media.srcHttp; } - componentDidUpdate(prevProps, prevState) { + public componentDidUpdate(prevProps, prevState) { if (this.props.onHeightChanged && !prevState.decryptedBlob && this.state.decryptedBlob) { this.props.onHeightChanged(); } } - render() { - const content = this.props.mxEvent.getContent(); + public render() { + const content = this.props.mxEvent.getContent(); const text = presentableTextForFile(content); - const isEncrypted = content.file !== undefined; + const isEncrypted = this.props.mediaEventHelper.media.isEncrypted; const fileName = content.body && content.body.length > 0 ? content.body : _t("Attachment"); - const contentUrl = this._getContentUrl(); + const contentUrl = this.getContentUrl(); const fileSize = content.info ? content.info.size : null; const fileType = content.info ? content.info.mimetype : "application/octet-stream"; @@ -182,29 +189,23 @@ export default class MFileBody extends React.Component { } if (isEncrypted) { - if (this.state.decryptedBlob === null) { + if (!this.state.decryptedBlob) { // Need to decrypt the attachment // Wait for the user to click on the link before downloading // and decrypting the attachment. - let decrypting = false; - const decrypt = (e) => { - if (decrypting) { - return false; - } - decrypting = true; - decryptFile(content.file).then((blob) => { + const decrypt = async () => { + try { + this.userDidClick = true; this.setState({ - decryptedBlob: blob, + decryptedBlob: await this.props.mediaEventHelper.sourceBlob.value, }); - }).catch((err) => { + } catch (err) { console.warn("Unable to decrypt attachment: ", err); Modal.createTrackedDialog('Error decrypting attachment', '', ErrorDialog, { title: _t("Error"), description: _t("Error decrypting attachment"), }); - }).finally(() => { - decrypting = false; - }); + } }; // This button should actually Download because usercontent/ will try to click itself @@ -226,7 +227,7 @@ export default class MFileBody extends React.Component { ev.target.contentWindow.postMessage({ imgSrc: downloadIconUrl, imgStyle: null, // it handles this internally for us. Useful if a downstream changes the icon. - style: computedStyle(this._dummyLink.current), + style: computedStyle(this.dummyLink.current), blob: this.state.decryptedBlob, // Set a download attribute for encrypted files so that the file // will have the correct name when the user tries to download it. @@ -234,7 +235,7 @@ export default class MFileBody extends React.Component { download: fileName, textContent: _t("Download %(text)s", { text: text }), // only auto-download if a user triggered this iframe explicitly - auto: !this.props.decryptedBlob, + auto: this.userDidClick, }, "*"); }; @@ -251,12 +252,12 @@ export default class MFileBody extends React.Component { * We'll use it to learn how the download link * would have been styled if it was rendered inline. */ } - +