From cfb9d0f2d45ace5ad80862e038c0f366668bf50f Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 3 Nov 2020 16:06:45 +0000 Subject: [PATCH 01/62] Disallow sending of empty messages --- src/components/views/rooms/SendMessageComposer.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/components/views/rooms/SendMessageComposer.js b/src/components/views/rooms/SendMessageComposer.js index 9438cceef5..78b1dd85db 100644 --- a/src/components/views/rooms/SendMessageComposer.js +++ b/src/components/views/rooms/SendMessageComposer.js @@ -308,6 +308,9 @@ export default class SendMessageComposer extends React.Component { const startTime = CountlyAnalytics.getTimestamp(); const {roomId} = this.props.room; const content = createMessageContent(this.model, this.props.permalinkCreator, replyToEvent); + // don't bother sending an empty message + if (!content.body.trim()) return; + const prom = this.context.sendMessage(roomId, content); if (replyToEvent) { // Clear reply_to_event as we put the message into the queue From 1b55eb5f44d0216900fe125dc5ef6511388e1eb6 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Wed, 4 Nov 2020 14:10:56 +0000 Subject: [PATCH 02/62] Upgrade matrix-js-sdk to 9.1.0-rc.1 --- package.json | 2 +- yarn.lock | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 9689892e24..5d47d81ca4 100644 --- a/package.json +++ b/package.json @@ -78,7 +78,7 @@ "is-ip": "^2.0.0", "linkifyjs": "^2.1.9", "lodash": "^4.17.19", - "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop", + "matrix-js-sdk": "9.1.0-rc.1", "matrix-widget-api": "^0.1.0-beta.5", "minimist": "^1.2.5", "pako": "^1.0.11", diff --git a/yarn.lock b/yarn.lock index 4d4152c659..04ceb32110 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6505,9 +6505,10 @@ mathml-tag-names@^2.0.1: resolved "https://registry.yarnpkg.com/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz#4ddadd67308e780cf16a47685878ee27b736a0a3" integrity sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg== -"matrix-js-sdk@github:matrix-org/matrix-js-sdk#develop": - version "9.0.1" - resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/f8863d5c2488a31a093f5a1b11761d7ac3829470" +matrix-js-sdk@9.1.0-rc.1: + version "9.1.0-rc.1" + resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-9.1.0-rc.1.tgz#cbdad0d272152a27bb32fe74e02ad069717d2554" + integrity sha512-Y0ftnH3HtMaNPFyfYqbrhS6aZkdQxxeluErFmOmpMTRGuMiIGrqhdWP8Z/MCZMRP4NPAh/dN2eW0vgqH9ij52A== dependencies: "@babel/runtime" "^7.11.2" another-json "^0.2.0" From 6752e1f8d752235300781ba054754799c8cd2c70 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Wed, 4 Nov 2020 14:16:13 +0000 Subject: [PATCH 03/62] Prepare changelog for v3.8.0-rc.1 --- CHANGELOG.md | 70 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f5372ae5d..035225b18a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,73 @@ +Changes in [3.8.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.8.0-rc.1) (2020-11-04) +============================================================================================================= +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.7.1...v3.8.0-rc.1) + + * Upgrade JS SDK to 9.1.0-rc.1 + * Log when saving profile + [\#5394](https://github.com/matrix-org/matrix-react-sdk/pull/5394) + * Translations update from Weblate + [\#5395](https://github.com/matrix-org/matrix-react-sdk/pull/5395) + * Hide prompt to add email for notifications if 3pid ui feature is off + [\#5392](https://github.com/matrix-org/matrix-react-sdk/pull/5392) + * Fix room list message preview copy for hangup events + [\#5388](https://github.com/matrix-org/matrix-react-sdk/pull/5388) + * Track UISIs as Countly Events + [\#5382](https://github.com/matrix-org/matrix-react-sdk/pull/5382) + * Don't let users accidentally redact ACL events + [\#5384](https://github.com/matrix-org/matrix-react-sdk/pull/5384) + * Two more easy files to remove from eslintignore + [\#5378](https://github.com/matrix-org/matrix-react-sdk/pull/5378) + * Fix Widget OpenID Permissions for realsies + [\#5381](https://github.com/matrix-org/matrix-react-sdk/pull/5381) + * Fix regression with OpenID permissions on widgets + [\#5380](https://github.com/matrix-org/matrix-react-sdk/pull/5380) + * Fix room directory events happening in the wrong order for Funnels + [\#5379](https://github.com/matrix-org/matrix-react-sdk/pull/5379) + * Remove a couple more files from eslintignore + [\#5377](https://github.com/matrix-org/matrix-react-sdk/pull/5377) + * Fix countly method bindings and errors + [\#5376](https://github.com/matrix-org/matrix-react-sdk/pull/5376) + * Fix a bunch of silly lint errors + [\#5375](https://github.com/matrix-org/matrix-react-sdk/pull/5375) + * Typescript: ImageUtils + [\#5374](https://github.com/matrix-org/matrix-react-sdk/pull/5374) + * Convert AuxPanel to TypeScript + [\#5373](https://github.com/matrix-org/matrix-react-sdk/pull/5373) + * Only pass metrics if they exist otherwise Countly will be unhappy! + [\#5372](https://github.com/matrix-org/matrix-react-sdk/pull/5372) + * Fix CountlyAnalytics NPE on MatrixClientPeg + [\#5370](https://github.com/matrix-org/matrix-react-sdk/pull/5370) + * fix CountlyAnalytics canEnable on wrong target + [\#5369](https://github.com/matrix-org/matrix-react-sdk/pull/5369) + * Initial Countly work + [\#5365](https://github.com/matrix-org/matrix-react-sdk/pull/5365) + * Fix videos not playing in non-encrypted rooms + [\#5368](https://github.com/matrix-org/matrix-react-sdk/pull/5368) + * Fix custom tag layout which regressed in #5309 + [\#5367](https://github.com/matrix-org/matrix-react-sdk/pull/5367) + * Watch replyToEvent at RoomView to prevent races + [\#5360](https://github.com/matrix-org/matrix-react-sdk/pull/5360) + * Add a UI Feature flag for room history settings + [\#5362](https://github.com/matrix-org/matrix-react-sdk/pull/5362) + * Hide inline images when preference disabled + [\#5361](https://github.com/matrix-org/matrix-react-sdk/pull/5361) + * Fix React warning by moving handler to each button + [\#5359](https://github.com/matrix-org/matrix-react-sdk/pull/5359) + * Do not preload encrypted videos|images unless autoplay or thumbnailing is on + [\#5352](https://github.com/matrix-org/matrix-react-sdk/pull/5352) + * Fix theme variable passed to Jitsi + [\#5357](https://github.com/matrix-org/matrix-react-sdk/pull/5357) + * docs: added comment explanation + [\#5349](https://github.com/matrix-org/matrix-react-sdk/pull/5349) + * Modal Widgets - MSC2790 + [\#5252](https://github.com/matrix-org/matrix-react-sdk/pull/5252) + * Widgets fixes + [\#5350](https://github.com/matrix-org/matrix-react-sdk/pull/5350) + * Fix User Menu avatar colouring being based on wrong string + [\#5348](https://github.com/matrix-org/matrix-react-sdk/pull/5348) + * Support 'answered elsewhere' + [\#5345](https://github.com/matrix-org/matrix-react-sdk/pull/5345) + Changes in [3.7.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.7.1) (2020-10-28) =================================================================================================== [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.7.0...v3.7.1) From 82574703833ef28a9fbd5efcff9e0d0d3e0691c1 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Wed, 4 Nov 2020 14:16:13 +0000 Subject: [PATCH 04/62] v3.8.0-rc.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5d47d81ca4..a4beaa696b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "3.7.1", + "version": "3.8.0-rc.1", "description": "SDK for matrix.org using React", "author": "matrix.org", "repository": { From 333b13802f066486aac3fe4dab3ebb7772e53d39 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 4 Nov 2020 15:32:21 +0000 Subject: [PATCH 05/62] Support thirdparty lookup for phone numbers As per MSC2845. Hidden behind /query for now, until we hook it up to a dial pad. --- src/SlashCommands.tsx | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/SlashCommands.tsx b/src/SlashCommands.tsx index e94cf7a37c..5a44e4058b 100644 --- a/src/SlashCommands.tsx +++ b/src/SlashCommands.tsx @@ -1001,14 +1001,29 @@ export const Commands = [ description: _td("Opens chat with the given user"), args: "", runFn: function(roomId, userId) { - if (!userId || !userId.startsWith("@") || !userId.includes(":")) { + // easter-egg for now: look up phone numbers through the thirdparty API + // (very dumb phone number detection...) + const isPhoneNumber = userId && /^\+?[0123456789]+$/.test(userId); + if (!userId || (!userId.startsWith("@") || !userId.includes(":")) && !isPhoneNumber) { return reject(this.getUsage()); } return success((async () => { + if (isPhoneNumber) { + const results = await MatrixClientPeg.get().getThirdpartyUser('im.vector.protocol.pstn', { + 'm.id.phone': userId, + }); + if (!results || results.length === 0 || !results[0].userid) { + throw new Error("Unable to find Matrix ID for phone number"); + } + userId = results[0].userid; + } + + const roomId = await ensureDMExists(MatrixClientPeg.get(), userId); + dis.dispatch({ action: 'view_room', - room_id: await ensureDMExists(MatrixClientPeg.get(), userId), + room_id: roomId, }); })()); }, From ff25a9b45de5e5125bfb6552ff7470df8327acd1 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 4 Nov 2020 17:00:07 +0000 Subject: [PATCH 06/62] Consolidate all EventTile bubble stuff into its own component and use it for the room continuation plinth --- res/css/_components.scss | 1 + res/css/views/messages/_CreateEvent.scss | 34 ++++------ res/css/views/messages/_EventTileBubble.scss | 63 +++++++++++++++++++ .../views/messages/_MJitsiWidgetEvent.scss | 33 ---------- .../views/messages/_common_CryptoEvent.scss | 48 +++----------- res/css/views/rooms/_EventTile.scss | 9 --- .../views/messages/EncryptionEvent.js | 45 ++++++------- .../views/messages/EventTileBubble.tsx | 34 ++++++++++ .../views/messages/MJitsiWidgetEvent.tsx | 42 +++++-------- .../messages/MKeyVerificationConclusion.js | 13 ++-- .../views/messages/MKeyVerificationRequest.js | 25 ++++---- src/components/views/messages/RoomCreate.js | 20 +++--- src/components/views/rooms/EventTile.js | 1 + 13 files changed, 182 insertions(+), 186 deletions(-) create mode 100644 res/css/views/messages/_EventTileBubble.scss create mode 100644 src/components/views/messages/EventTileBubble.tsx diff --git a/res/css/_components.scss b/res/css/_components.scss index 37d0e0d286..af3589a415 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -139,6 +139,7 @@ @import "./views/groups/_GroupUserSettings.scss"; @import "./views/messages/_CreateEvent.scss"; @import "./views/messages/_DateSeparator.scss"; +@import "./views/messages/_EventTileBubble.scss"; @import "./views/messages/_MEmoteBody.scss"; @import "./views/messages/_MFileBody.scss"; @import "./views/messages/_MImageBody.scss"; diff --git a/res/css/views/messages/_CreateEvent.scss b/res/css/views/messages/_CreateEvent.scss index d45645863f..1f75fb9693 100644 --- a/res/css/views/messages/_CreateEvent.scss +++ b/res/css/views/messages/_CreateEvent.scss @@ -1,5 +1,5 @@ /* -Copyright 2018 New Vector Ltd +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. @@ -15,25 +15,17 @@ limitations under the License. */ .mx_CreateEvent { - background-color: $info-plinth-bg-color; - padding-left: 20px; - padding-right: 20px; - padding-top: 10px; - padding-bottom: 10px; -} + // override default EventTileBubble styling + padding-left: 80px !important; -.mx_CreateEvent_image { - float: left; - margin-right: 20px; - width: 72px; - height: 34px; - - background-color: $primary-fg-color; - mask: url('$(res)/img/room-continuation.svg'); - mask-repeat: no-repeat; - mask-position: center; -} - -.mx_CreateEvent_header { - font-weight: bold; + &::before { + background-color: $primary-fg-color; + mask-image: url('$(res)/img/room-continuation.svg'); + mask-repeat: no-repeat; + mask-position: center; + mask-size: 100%; + width: 72px !important; + height: 34px !important; + left: -64px !important; + } } diff --git a/res/css/views/messages/_EventTileBubble.scss b/res/css/views/messages/_EventTileBubble.scss new file mode 100644 index 0000000000..99feb03ef9 --- /dev/null +++ b/res/css/views/messages/_EventTileBubble.scss @@ -0,0 +1,63 @@ +/* +Copyright 2019, 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. +*/ + +.mx_EventTileBubble { + background-color: $dark-panel-bg-color; + padding: 10px; + border-radius: 8px; + margin: 10px auto; + max-width: 75%; + box-sizing: border-box; + display: grid; + grid-template-columns: 24px minmax(0, 1fr) min-content; + + &::before, &::after { + position: relative; + grid-column: 1; + grid-row: 1 / 3; + width: 16px; + height: 16px; + content: ""; + top: 0; + bottom: 0; + left: 0; + right: 0; + mask-repeat: no-repeat; + mask-position: center; + mask-size: contain; + margin-top: 4px; + } + + .mx_EventTileBubble_title, .mx_EventTileBubble_subtitle { + overflow-wrap: break-word; + } + + .mx_EventTileBubble_title { + font-weight: 600; + font-size: $font-15px; + grid-column: 2; + grid-row: 1; + } + + .mx_EventTileBubble_subtitle { + grid-column: 2; + grid-row: 2; + } + + .mx_EventTileBubble_subtitle { + font-size: $font-12px; + } +} diff --git a/res/css/views/messages/_MJitsiWidgetEvent.scss b/res/css/views/messages/_MJitsiWidgetEvent.scss index 3e51e89744..bea8651543 100644 --- a/res/css/views/messages/_MJitsiWidgetEvent.scss +++ b/res/css/views/messages/_MJitsiWidgetEvent.scss @@ -15,41 +15,8 @@ limitations under the License. */ .mx_MJitsiWidgetEvent { - display: grid; - grid-template-columns: 24px minmax(0, 1fr) min-content; - &::before { - grid-column: 1; - grid-row: 1 / 3; - width: 16px; - height: 16px; - content: ""; - top: 0; - bottom: 0; - left: 0; - right: 0; - mask-repeat: no-repeat; - mask-position: center; - mask-size: contain; background-color: $composer-e2e-icon-color; // XXX: Variable abuse - margin-top: 4px; mask-image: url('$(res)/img/element-icons/call/video-call.svg'); } - - .mx_MJitsiWidgetEvent_title { - font-weight: 600; - font-size: $font-15px; - grid-column: 2; - grid-row: 1; - } - - .mx_MJitsiWidgetEvent_subtitle { - grid-column: 2; - grid-row: 2; - } - - .mx_MJitsiWidgetEvent_title, - .mx_MJitsiWidgetEvent_subtitle { - overflow-wrap: break-word; - } } diff --git a/res/css/views/messages/_common_CryptoEvent.scss b/res/css/views/messages/_common_CryptoEvent.scss index 09c78ae5b4..4faa4b594f 100644 --- a/res/css/views/messages/_common_CryptoEvent.scss +++ b/res/css/views/messages/_common_CryptoEvent.scss @@ -15,28 +15,6 @@ limitations under the License. */ .mx_cryptoEvent { - display: grid; - grid-template-columns: 24px minmax(0, 1fr) min-content; - - &.mx_cryptoEvent_icon::before, - &.mx_cryptoEvent_icon::after { - grid-column: 1; - grid-row: 1 / 3; - width: 16px; - height: 16px; - content: ""; - top: 0; - bottom: 0; - left: 0; - right: 0; - mask-repeat: no-repeat; - mask-position: center; - mask-size: contain; - mask-image: url('$(res)/img/e2e/normal.svg'); - background-color: $composer-e2e-icon-color; - margin-top: 4px; - } - // white infill for the transparency &.mx_cryptoEvent_icon::before { background-color: #ffffff; @@ -46,6 +24,11 @@ limitations under the License. mask-size: 90%; } + &.mx_cryptoEvent_icon::after { + mask-image: url('$(res)/img/e2e/normal.svg'); + background-color: $composer-e2e-icon-color; + } + &.mx_cryptoEvent_icon_verified::after { mask-image: url("$(res)/img/e2e/verified.svg"); background-color: $accent-color; @@ -56,25 +39,6 @@ limitations under the License. background-color: $notice-primary-color; } - .mx_cryptoEvent_title, .mx_cryptoEvent_subtitle, .mx_cryptoEvent_state { - overflow-wrap: break-word; - } - - .mx_cryptoEvent_title { - font-weight: 600; - font-size: $font-15px; - grid-column: 2; - grid-row: 1; - } - - .mx_cryptoEvent_subtitle { - grid-column: 2; - grid-row: 2; - } - - .mx_cryptoEvent_state, .mx_cryptoEvent_subtitle { - font-size: $font-12px; - } .mx_cryptoEvent_state, .mx_cryptoEvent_buttons { grid-column: 3; @@ -92,5 +56,7 @@ limitations under the License. margin: auto 0; text-align: center; color: $notice-secondary-color; + overflow-wrap: break-word; + font-size: $font-12px; } } diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index 3b9a491db5..18eb581776 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -25,15 +25,6 @@ $left-gutter: 64px; position: relative; } -.mx_EventTile_bubble { - background-color: $dark-panel-bg-color; - padding: 10px; - border-radius: 5px; - margin: 10px auto; - max-width: 75%; - box-sizing: border-box; -} - .mx_EventTile.mx_EventTile_info { padding-top: 0px; } diff --git a/src/components/views/messages/EncryptionEvent.js b/src/components/views/messages/EncryptionEvent.js index a9ce10d202..ee304b4fa3 100644 --- a/src/components/views/messages/EncryptionEvent.js +++ b/src/components/views/messages/EncryptionEvent.js @@ -18,42 +18,35 @@ import React from 'react'; import PropTypes from 'prop-types'; import { _t } from '../../../languageHandler'; import { MatrixClientPeg } from '../../../MatrixClientPeg'; +import EventTileBubble from "./EventTileBubble"; export default class EncryptionEvent extends React.Component { render() { const {mxEvent} = this.props; - let body; - let classes = "mx_EventTile_bubble mx_cryptoEvent mx_cryptoEvent_icon"; const isRoomEncrypted = MatrixClientPeg.get().isRoomEncrypted(mxEvent.getRoomId()); if (mxEvent.getContent().algorithm === 'm.megolm.v1.aes-sha2' && isRoomEncrypted) { - body =
-
{_t("Encryption enabled")}
-
- {_t( - "Messages in this room are end-to-end encrypted. " + - "Learn more & verify this user in their user profile.", - )} -
-
; + return ; } else if (isRoomEncrypted) { - body =
-
{_t("Encryption enabled")}
-
- {_t("Ignored attempt to disable encryption")} -
-
; - } else { - body =
-
{_t("Encryption not enabled")}
-
{_t("The encryption used by this room isn't supported.")}
-
; - classes += " mx_cryptoEvent_icon_warning"; + return ; } - return (
- {body} -
); + return ; } } diff --git a/src/components/views/messages/EventTileBubble.tsx b/src/components/views/messages/EventTileBubble.tsx new file mode 100644 index 0000000000..8a2cf0f01c --- /dev/null +++ b/src/components/views/messages/EventTileBubble.tsx @@ -0,0 +1,34 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React, {ReactNode} from "react"; +import classNames from "classnames"; + +interface IProps { + className: string; + title: string; + subtitle?: ReactNode; +} + +const EventTileBubble: React.FC = ({ className, title, subtitle, children }) => { + return
+
{ title }
+ { subtitle &&
{ subtitle }
} + { children } +
; +}; + +export default EventTileBubble; diff --git a/src/components/views/messages/MJitsiWidgetEvent.tsx b/src/components/views/messages/MJitsiWidgetEvent.tsx index 3d191209f9..82aa32d3b7 100644 --- a/src/components/views/messages/MJitsiWidgetEvent.tsx +++ b/src/components/views/messages/MJitsiWidgetEvent.tsx @@ -18,6 +18,7 @@ import React from 'react'; import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { _t } from "../../../languageHandler"; import WidgetStore from "../../../stores/WidgetStore"; +import EventTileBubble from "./EventTileBubble"; interface IProps { mxEvent: MatrixEvent; @@ -40,37 +41,24 @@ export default class MJitsiWidgetEvent extends React.PureComponent { if (!url) { // removed - return ( -
-
- {_t('Video conference ended by %(senderName)s', {senderName})} -
-
- ); + return ; } else if (prevUrl) { // modified - return ( -
-
- {_t('Video conference updated by %(senderName)s', {senderName})} -
-
- {joinCopy} -
-
- ); + return ; } else { // assume added - return ( -
-
- {_t("Video conference started by %(senderName)s", {senderName})} -
-
- {joinCopy} -
-
- ); + return ; } } } diff --git a/src/components/views/messages/MKeyVerificationConclusion.js b/src/components/views/messages/MKeyVerificationConclusion.js index ececfc60ed..880299d29d 100644 --- a/src/components/views/messages/MKeyVerificationConclusion.js +++ b/src/components/views/messages/MKeyVerificationConclusion.js @@ -21,6 +21,7 @@ import {MatrixClientPeg} from '../../../MatrixClientPeg'; import { _t } from '../../../languageHandler'; import {getNameForEventRoom, userLabelForEventRoom} from '../../../utils/KeyVerificationStateObserver'; +import EventTileBubble from "./EventTileBubble"; export default class MKeyVerificationConclusion extends React.Component { constructor(props) { @@ -115,14 +116,14 @@ export default class MKeyVerificationConclusion extends React.Component { } if (title) { - const subtitle = userLabelForEventRoom(request.otherUserId, mxEvent.getRoomId()); - const classes = classNames("mx_EventTile_bubble", "mx_cryptoEvent", "mx_cryptoEvent_icon", { + const classes = classNames("mx_cryptoEvent mx_cryptoEvent_icon", { mx_cryptoEvent_icon_verified: request.done, }); - return (
-
{title}
-
{subtitle}
-
); + return ; } return null; diff --git a/src/components/views/messages/MKeyVerificationRequest.js b/src/components/views/messages/MKeyVerificationRequest.js index 01a5c2663e..d9594091c5 100644 --- a/src/components/views/messages/MKeyVerificationRequest.js +++ b/src/components/views/messages/MKeyVerificationRequest.js @@ -24,6 +24,7 @@ import {getNameForEventRoom, userLabelForEventRoom} import dis from "../../../dispatcher/dispatcher"; import {RightPanelPhases} from "../../../stores/RightPanelStorePhases"; import {Action} from "../../../dispatcher/actions"; +import EventTileBubble from "./EventTileBubble"; export default class MKeyVerificationRequest extends React.Component { constructor(props) { @@ -146,10 +147,8 @@ export default class MKeyVerificationRequest extends React.Component { if (!request.initiatedByMe) { const name = getNameForEventRoom(request.requestingUserId, mxEvent.getRoomId()); - title = (
{ - _t("%(name)s wants to verify", {name})}
); - subtitle = (
{ - userLabelForEventRoom(request.requestingUserId, mxEvent.getRoomId())}
); + title = _t("%(name)s wants to verify", {name}); + subtitle = userLabelForEventRoom(request.requestingUserId, mxEvent.getRoomId()); if (request.canAccept) { stateNode = (
@@ -157,18 +156,18 @@ export default class MKeyVerificationRequest extends React.Component {
); } } else { // request sent by us - title = (
{ - _t("You sent a verification request")}
); - subtitle = (
{ - userLabelForEventRoom(request.receivingUserId, mxEvent.getRoomId())}
); + title = _t("You sent a verification request"); + subtitle = userLabelForEventRoom(request.receivingUserId, mxEvent.getRoomId()); } if (title) { - return (
- {title} - {subtitle} - {stateNode} -
); + return + { stateNode } + ; } return null; } diff --git a/src/components/views/messages/RoomCreate.js b/src/components/views/messages/RoomCreate.js index 6098b1217e..479592aa42 100644 --- a/src/components/views/messages/RoomCreate.js +++ b/src/components/views/messages/RoomCreate.js @@ -22,6 +22,7 @@ import dis from '../../../dispatcher/dispatcher'; import { RoomPermalinkCreator } from '../../../utils/permalinks/Permalinks'; import { _t } from '../../../languageHandler'; import {MatrixClientPeg} from '../../../MatrixClientPeg'; +import EventTileBubble from "./EventTileBubble"; export default class RoomCreate extends React.Component { static propTypes = { @@ -51,17 +52,16 @@ export default class RoomCreate extends React.Component { const permalinkCreator = new RoomPermalinkCreator(prevRoom, predecessor['room_id']); permalinkCreator.load(); const predecessorPermalink = permalinkCreator.forEvent(predecessor['event_id']); - return
-
-
- {_t("This room is a continuation of another conversation.")} -
- + const link = ( + {_t("Click here to see older messages.")} -
; + ); + + return ; } } diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index 48ab6831d9..9060ddb02b 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -647,6 +647,7 @@ export default class EventTile extends React.Component { // Info messages are basically information about commands processed on a room const isBubbleMessage = eventType.startsWith("m.key.verification") || (eventType === "m.room.message" && msgtype && msgtype.startsWith("m.key.verification")) || + (eventType === "m.room.create") || (eventType === "m.room.encryption") || (tileHandler === "messages.MJitsiWidgetEvent"); let isInfoMessage = ( From 4e2d9c28f5347ac184492cf6ec6ac5433e492ba2 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 4 Nov 2020 17:21:25 +0000 Subject: [PATCH 07/62] Convert EncryptionEvent to Typescript --- .../views/messages/EncryptionEvent.js | 56 ------------------- .../views/messages/EncryptionEvent.tsx | 54 ++++++++++++++++++ 2 files changed, 54 insertions(+), 56 deletions(-) delete mode 100644 src/components/views/messages/EncryptionEvent.js create mode 100644 src/components/views/messages/EncryptionEvent.tsx diff --git a/src/components/views/messages/EncryptionEvent.js b/src/components/views/messages/EncryptionEvent.js deleted file mode 100644 index ee304b4fa3..0000000000 --- a/src/components/views/messages/EncryptionEvent.js +++ /dev/null @@ -1,56 +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. -*/ - -import React from 'react'; -import PropTypes from 'prop-types'; -import { _t } from '../../../languageHandler'; -import { MatrixClientPeg } from '../../../MatrixClientPeg'; -import EventTileBubble from "./EventTileBubble"; - -export default class EncryptionEvent extends React.Component { - render() { - const {mxEvent} = this.props; - - const isRoomEncrypted = MatrixClientPeg.get().isRoomEncrypted(mxEvent.getRoomId()); - if (mxEvent.getContent().algorithm === 'm.megolm.v1.aes-sha2' && isRoomEncrypted) { - return ; - } else if (isRoomEncrypted) { - return ; - } - - return ; - } -} - -EncryptionEvent.propTypes = { - /* the MatrixEvent to show */ - mxEvent: PropTypes.object.isRequired, -}; diff --git a/src/components/views/messages/EncryptionEvent.tsx b/src/components/views/messages/EncryptionEvent.tsx new file mode 100644 index 0000000000..9a6af9446f --- /dev/null +++ b/src/components/views/messages/EncryptionEvent.tsx @@ -0,0 +1,54 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from 'react'; +import {MatrixEvent} from "matrix-js-sdk/src/models/event"; + +import { _t } from '../../../languageHandler'; +import { MatrixClientPeg } from '../../../MatrixClientPeg'; +import EventTileBubble from "./EventTileBubble"; + +interface IProps { + mxEvent: MatrixEvent; +} + +const EncryptionEvent: React.FC = ({mxEvent}) => { + const isRoomEncrypted = MatrixClientPeg.get().isRoomEncrypted(mxEvent.getRoomId()); + if (mxEvent.getContent().algorithm === 'm.megolm.v1.aes-sha2' && isRoomEncrypted) { + return ; + } else if (isRoomEncrypted) { + return ; + } + + return ; +} + +export default EncryptionEvent; From af71b0735dfc0f28d15040514f60ebac22cb7084 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 4 Nov 2020 17:24:03 +0000 Subject: [PATCH 08/62] Change icon for the Create Event bubble tile --- res/css/views/messages/_CreateEvent.scss | 11 +---------- res/img/element-icons/chat-bubbles.svg | 11 +++++++++++ res/img/room-continuation.svg | 6 ------ 3 files changed, 12 insertions(+), 16 deletions(-) create mode 100644 res/img/element-icons/chat-bubbles.svg delete mode 100644 res/img/room-continuation.svg diff --git a/res/css/views/messages/_CreateEvent.scss b/res/css/views/messages/_CreateEvent.scss index 1f75fb9693..1181a80d10 100644 --- a/res/css/views/messages/_CreateEvent.scss +++ b/res/css/views/messages/_CreateEvent.scss @@ -15,17 +15,8 @@ limitations under the License. */ .mx_CreateEvent { - // override default EventTileBubble styling - padding-left: 80px !important; - &::before { background-color: $primary-fg-color; - mask-image: url('$(res)/img/room-continuation.svg'); - mask-repeat: no-repeat; - mask-position: center; - mask-size: 100%; - width: 72px !important; - height: 34px !important; - left: -64px !important; + mask-image: url('$(res)/img/element-icons/chat-bubbles.svg'); } } diff --git a/res/img/element-icons/chat-bubbles.svg b/res/img/element-icons/chat-bubbles.svg new file mode 100644 index 0000000000..ac9db61f29 --- /dev/null +++ b/res/img/element-icons/chat-bubbles.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/res/img/room-continuation.svg b/res/img/room-continuation.svg deleted file mode 100644 index dc7e15462a..0000000000 --- a/res/img/room-continuation.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - From 2fdfe9346f962abdb350ae0731f8ba90ad428370 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 4 Nov 2020 17:50:59 +0000 Subject: [PATCH 09/62] Update copy on the Encryption Enabled bubble tile and room creation ELS to cater better in general but specifically for DMs --- src/components/structures/MessagePanel.js | 14 ++++++++--- .../views/messages/EncryptionEvent.tsx | 25 ++++++++++++++----- src/i18n/strings/en_EN.json | 6 +++-- 3 files changed, 34 insertions(+), 11 deletions(-) diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index e2e3592536..9cf3eaaa9d 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -30,6 +30,7 @@ import {_t} from "../../languageHandler"; import {haveTileForEvent} from "../views/rooms/EventTile"; import {textForEvent} from "../../TextForEvent"; import IRCTimelineProfileResizer from "../views/elements/IRCTimelineProfileResizer"; +import DMRoomMap from "../../utils/DMRoomMap"; const CONTINUATION_MAX_INTERVAL = 5 * 60 * 1000; // 5 minutes const continuedTypes = ['m.sticker', 'm.room.message']; @@ -952,15 +953,22 @@ class CreationGrouper { }).reduce((a, b) => a.concat(b), []); // Get sender profile from the latest event in the summary as the m.room.create doesn't contain one const ev = this.events[this.events.length - 1]; + + let summaryText; + const creator = ev.sender ? ev.sender.name : ev.getSender(); + if (DMRoomMap.shared().getUserIdForRoomId(ev.getRoomId())) { + summaryText = _t("%(creator)s created this DM.", { creator }); + } else { + summaryText = _t("%(creator)s created and configured the room.", { creator }); + } + ret.push( { eventTiles } , diff --git a/src/components/views/messages/EncryptionEvent.tsx b/src/components/views/messages/EncryptionEvent.tsx index 9a6af9446f..7416cc3c9a 100644 --- a/src/components/views/messages/EncryptionEvent.tsx +++ b/src/components/views/messages/EncryptionEvent.tsx @@ -14,27 +14,40 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; +import React, {useContext} from 'react'; import {MatrixEvent} from "matrix-js-sdk/src/models/event"; import { _t } from '../../../languageHandler'; import { MatrixClientPeg } from '../../../MatrixClientPeg'; import EventTileBubble from "./EventTileBubble"; +import MatrixClientContext from "../../../contexts/MatrixClientContext"; +import DMRoomMap from "../../../utils/DMRoomMap"; interface IProps { mxEvent: MatrixEvent; } const EncryptionEvent: React.FC = ({mxEvent}) => { - const isRoomEncrypted = MatrixClientPeg.get().isRoomEncrypted(mxEvent.getRoomId()); + const cli = useContext(MatrixClientContext); + const roomId = mxEvent.getRoomId(); + const isRoomEncrypted = MatrixClientPeg.get().isRoomEncrypted(roomId); + if (mxEvent.getContent().algorithm === 'm.megolm.v1.aes-sha2' && isRoomEncrypted) { + let subtitle: string; + const dmPartner = DMRoomMap.shared().getUserIdForRoomId(roomId); + if (dmPartner) { + const displayName = cli?.getRoom(roomId)?.getMember(dmPartner)?.rawDisplayName || dmPartner; + subtitle = _t("Messages here are end-to-end encrypted. " + + "Verify %(displayName)s in their profile - tap on their avatar.", { displayName }); + } else { + subtitle = _t("Messages in this room are end-to-end encrypted. " + + "When people join, you can verify them in their profile, just tap on their avatar."); + } + return ; } else if (isRoomEncrypted) { return ": "%(senderDisplayName)s changed the room avatar to ", - "This room is a continuation of another conversation.": "This room is a continuation of another conversation.", "Click here to see older messages.": "Click here to see older messages.", + "This room is a continuation of another conversation.": "This room is a continuation of another conversation.", "Copied!": "Copied!", "Failed to copy": "Failed to copy", "Add an Integration": "Add an Integration", @@ -2079,6 +2080,7 @@ "Data from an older version of %(brand)s has been detected. This will have caused end-to-end cryptography to malfunction in the older version. End-to-end encrypted messages exchanged recently whilst using the older version may not be decryptable in this version. This may also cause messages exchanged with this version to fail. If you experience problems, log out and back in again. To retain message history, export and re-import your keys.": "Data from an older version of %(brand)s has been detected. This will have caused end-to-end cryptography to malfunction in the older version. End-to-end encrypted messages exchanged recently whilst using the older version may not be decryptable in this version. This may also cause messages exchanged with this version to fail. If you experience problems, log out and back in again. To retain message history, export and re-import your keys.", "Self-verification request": "Self-verification request", "Logout": "Logout", + "%(creator)s created this DM.": "%(creator)s created this DM.", "%(creator)s created and configured the room.": "%(creator)s created and configured the room.", "Your Communities": "Your Communities", "Did you know: you can use communities to filter your %(brand)s experience!": "Did you know: you can use communities to filter your %(brand)s experience!", From 2594ff8e803b5eb6d9125ddad50698172bde0eee Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 5 Nov 2020 15:39:22 +0000 Subject: [PATCH 10/62] Remove old isAlone checker --- res/css/structures/_RoomStatusBar.scss | 10 ----- src/components/structures/RoomStatusBar.js | 30 +------------- src/components/structures/RoomView.tsx | 46 +++------------------- 3 files changed, 7 insertions(+), 79 deletions(-) diff --git a/res/css/structures/_RoomStatusBar.scss b/res/css/structures/_RoomStatusBar.scss index cd4390ee5c..2d5359c0eb 100644 --- a/res/css/structures/_RoomStatusBar.scss +++ b/res/css/structures/_RoomStatusBar.scss @@ -153,16 +153,6 @@ limitations under the License. display: block; } -.mx_RoomStatusBar_isAlone { - height: 50px; - line-height: $font-50px; - - color: $primary-fg-color; - opacity: 0.5; - overflow-y: hidden; - display: block; -} - .mx_MatrixChat_useCompactLayout { .mx_RoomStatusBar { min-height: 40px; diff --git a/src/components/structures/RoomStatusBar.js b/src/components/structures/RoomStatusBar.js index e390be6979..e6d2985073 100644 --- a/src/components/structures/RoomStatusBar.js +++ b/src/components/structures/RoomStatusBar.js @@ -41,9 +41,6 @@ export default class RoomStatusBar extends React.Component { static propTypes = { // the room this statusbar is representing. room: PropTypes.object.isRequired, - // This is true when the user is alone in the room, but has also sent a message. - // Used to suggest to the user to invite someone - sentMessageAndIsAlone: PropTypes.bool, // The active call in the room, if any (means we show the call bar // along with the status of the call) @@ -68,10 +65,6 @@ export default class RoomStatusBar extends React.Component { // 'you are alone' bar onInviteClick: PropTypes.func, - // callback for when the user clicks on the 'stop warning me' button in the - // 'you are alone' bar - onStopWarningClick: PropTypes.func, - // callback for when we do something that changes the size of the // status bar. This is used to trigger a re-layout in the parent // component. @@ -159,10 +152,7 @@ export default class RoomStatusBar extends React.Component { // changed - so we use '0' to indicate normal size, and other values to // indicate other sizes. _getSize() { - if (this._shouldShowConnectionError() || - this._showCallBar() || - this.props.sentMessageAndIsAlone - ) { + if (this._shouldShowConnectionError() || this._showCallBar()) { return STATUS_BAR_EXPANDED; } else if (this.state.unsentMessages.length > 0) { return STATUS_BAR_EXPANDED_LARGE; @@ -325,24 +315,6 @@ export default class RoomStatusBar extends React.Component { ); } - // If you're alone in the room, and have sent a message, suggest to invite someone - if (this.props.sentMessageAndIsAlone && !this.props.isPeeking) { - return ( -
- { _t("There's no one else here! Would you like to invite others " + - "or stop warning about the empty room?", - {}, - { - 'inviteText': (sub) => - { sub }, - 'nowarnText': (sub) => - { sub }, - }, - ) } -
- ); - } - return null; } diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index 0cb4a5d305..8618e930d5 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -150,7 +150,6 @@ export interface IState { guestsCanJoin: boolean; canPeek: boolean; showApps: boolean; - isAlone: boolean; isPeeking: boolean; showingPinned: boolean; showReadReceipts: boolean; @@ -223,7 +222,6 @@ export default class RoomView extends React.Component { guestsCanJoin: false, canPeek: false, showApps: false, - isAlone: false, isPeeking: false, showingPinned: false, showReadReceipts: true, @@ -705,9 +703,8 @@ export default class RoomView extends React.Component { private onAction = payload => { switch (payload.action) { - case 'message_send_failed': case 'message_sent': - this.checkIfAlone(this.state.room); + this.checkDesktopNotifications(); break; case 'post_sticker_message': this.injectSticker( @@ -1025,36 +1022,15 @@ export default class RoomView extends React.Component { } // rate limited because a power level change will emit an event for every member in the room. - private updateRoomMembers = rateLimitedFunc((dueToMember) => { + private updateRoomMembers = rateLimitedFunc(() => { this.updateDMState(); - - let memberCountInfluence = 0; - if (dueToMember && dueToMember.membership === "invite" && this.state.room.getInvitedMemberCount() === 0) { - // A member got invited, but the room hasn't detected that change yet. Influence the member - // count by 1 to counteract this. - memberCountInfluence = 1; - } - this.checkIfAlone(this.state.room, memberCountInfluence); - this.updateE2EStatus(this.state.room); }, 500); - private checkIfAlone(room: Room, countInfluence?: number) { - let warnedAboutLonelyRoom = false; - if (localStorage) { - warnedAboutLonelyRoom = Boolean(localStorage.getItem('mx_user_alone_warned_' + this.state.room.roomId)); - } - if (warnedAboutLonelyRoom) { - if (this.state.isAlone) this.setState({isAlone: false}); - return; - } - - let joinedOrInvitedMemberCount = room.getJoinedMemberCount() + room.getInvitedMemberCount(); - if (countInfluence) joinedOrInvitedMemberCount += countInfluence; - this.setState({isAlone: joinedOrInvitedMemberCount === 1}); - - // if they are not alone additionally prompt the user about notifications so they don't miss replies - if (joinedOrInvitedMemberCount > 1 && Notifier.shouldShowPrompt()) { + private checkDesktopNotifications() { + const memberCount = this.state.room.getJoinedMemberCount() + this.state.room.getInvitedMemberCount(); + // if they are not alone prompt the user about notifications so they don't miss replies + if (memberCount > 1 && Notifier.shouldShowPrompt()) { showNotificationsToast(true); } } @@ -1091,14 +1067,6 @@ export default class RoomView extends React.Component { action: 'view_invite', roomId: this.state.room.roomId, }); - this.setState({isAlone: false}); // there's a good chance they'll invite someone - }; - - private onStopAloneWarningClick = () => { - if (localStorage) { - localStorage.setItem('mx_user_alone_warned_' + this.state.room.roomId, String(true)); - } - this.setState({isAlone: false}); }; private onJoinButtonClicked = () => { @@ -1797,12 +1765,10 @@ export default class RoomView extends React.Component { isStatusAreaExpanded = this.state.statusBarVisible; statusBar = ; From 0a42853a2534b410807933d5d529bd178eff93ad Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 5 Nov 2020 15:39:37 +0000 Subject: [PATCH 11/62] Fix centering of bubble event tile --- res/css/views/rooms/_EventTile.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index 18eb581776..80ede1152d 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -122,9 +122,9 @@ $left-gutter: 64px; grid-template-columns: 1fr 100px; .mx_EventTile_line { - margin-right: 0px; + margin-right: 0; grid-column: 1 / 3; - padding: 0; + padding: 0 !important; } .mx_EventTile_msgOption { From 6aeea3e38e0ba6f8b6a2467b293f69148ee7c2e4 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 5 Nov 2020 15:42:45 +0000 Subject: [PATCH 12/62] Extract MiniAvatarUploader into a reusable component --- res/css/_components.scss | 1 + res/css/structures/_HomePage.scss | 36 +------- .../views/elements/_MiniAvatarUploader.scss | 56 ++++++++++++ src/components/structures/HomePage.tsx | 60 +++---------- .../views/elements/MiniAvatarUploader.tsx | 90 +++++++++++++++++++ 5 files changed, 159 insertions(+), 84 deletions(-) create mode 100644 res/css/views/elements/_MiniAvatarUploader.scss create mode 100644 src/components/views/elements/MiniAvatarUploader.tsx diff --git a/res/css/_components.scss b/res/css/_components.scss index af3589a415..860b0f126e 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -115,6 +115,7 @@ @import "./views/elements/_InfoTooltip.scss"; @import "./views/elements/_InlineSpinner.scss"; @import "./views/elements/_ManageIntegsButton.scss"; +@import "./views/elements/_MiniAvatarUploader.scss"; @import "./views/elements/_PowerSelector.scss"; @import "./views/elements/_ProgressBar.scss"; @import "./views/elements/_QRCode.scss"; diff --git a/res/css/structures/_HomePage.scss b/res/css/structures/_HomePage.scss index 2077582a7d..45aa34d3b5 100644 --- a/res/css/structures/_HomePage.scss +++ b/res/css/structures/_HomePage.scss @@ -50,42 +50,8 @@ limitations under the License. color: $muted-fg-color; } - .mx_HomePage_userAvatar { - position: relative; - width: min-content; + .mx_MiniAvatarUploader { margin: 0 auto; - - &::before, &::after { - content: ''; - position: absolute; - - height: 26px; - width: 26px; - - right: -6px; - bottom: -6px; - } - - &::before { - background-color: $primary-bg-color; - border-radius: 50%; - z-index: 1; - } - - &::after { - background-color: $secondary-fg-color; - mask-position: center; - mask-repeat: no-repeat; - mask-image: url('$(res)/img/element-icons/camera.svg'); - mask-size: 16px; - z-index: 2; - } - - &.mx_HomePage_userAvatar_busy::after { - background: url("$(res)/img/spinner.gif") no-repeat center; - background-size: 80%; - mask: unset; - } } .mx_HomePage_default_buttons { diff --git a/res/css/views/elements/_MiniAvatarUploader.scss b/res/css/views/elements/_MiniAvatarUploader.scss new file mode 100644 index 0000000000..2502977331 --- /dev/null +++ b/res/css/views/elements/_MiniAvatarUploader.scss @@ -0,0 +1,56 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +.mx_MiniAvatarUploader { + position: relative; + width: min-content; + + &::before, &::after { + content: ''; + position: absolute; + + height: 26px; + width: 26px; + + right: -6px; + bottom: -6px; + } + + &::before { + background-color: $primary-bg-color; + border-radius: 50%; + z-index: 1; + } + + &::after { + background-color: $secondary-fg-color; + mask-position: center; + mask-repeat: no-repeat; + mask-image: url('$(res)/img/element-icons/camera.svg'); + mask-size: 16px; + z-index: 2; + } + + &.mx_MiniAvatarUploader_busy::after { + background: url("$(res)/img/spinner.gif") no-repeat center; + background-size: 80%; + mask: unset; + } +} + +.mx_MiniAvatarUploader_input { + display: none; +} diff --git a/src/components/structures/HomePage.tsx b/src/components/structures/HomePage.tsx index 8058ddad93..d11944e470 100644 --- a/src/components/structures/HomePage.tsx +++ b/src/components/structures/HomePage.tsx @@ -15,7 +15,7 @@ limitations under the License. */ import * as React from "react"; -import {useContext, useRef, useState} from "react"; +import {useContext, useState} from "react"; import AutoHideScrollbar from './AutoHideScrollbar'; import {getHomePageUrl} from "../../utils/pages"; @@ -24,16 +24,13 @@ import SdkConfig from "../../SdkConfig"; import * as sdk from "../../index"; import dis from "../../dispatcher/dispatcher"; import {Action} from "../../dispatcher/actions"; -import {Transition} from "react-transition-group"; import BaseAvatar from "../views/avatars/BaseAvatar"; import {OwnProfileStore} from "../../stores/OwnProfileStore"; import AccessibleButton from "../views/elements/AccessibleButton"; -import Tooltip from "../views/elements/Tooltip"; import {UPDATE_EVENT} from "../../stores/AsyncStore"; import {useEventEmitter} from "../../hooks/useEventEmitter"; import MatrixClientContext from "../../contexts/MatrixClientContext"; -import classNames from "classnames"; -import {ENTERING} from "react-transition-group/Transition"; +import MiniAvatarUploader, {AVATAR_SIZE} from "../views/elements/MiniAvatarUploader"; const onClickSendDm = () => dis.dispatch({action: 'view_create_chat'}); const onClickExplore = () => dis.fire(Action.ViewRoomDirectory); @@ -43,11 +40,9 @@ interface IProps { justRegistered?: boolean; } -const avatarSize = 52; - const getOwnProfile = (userId: string) => ({ displayName: OwnProfileStore.instance.displayName || userId, - avatarUrl: OwnProfileStore.instance.getHttpAvatarUrl(avatarSize), + avatarUrl: OwnProfileStore.instance.getHttpAvatarUrl(AVATAR_SIZE), }); const UserWelcomeTop = () => { @@ -57,56 +52,23 @@ const UserWelcomeTop = () => { useEventEmitter(OwnProfileStore.instance, UPDATE_EVENT, () => { setOwnProfile(getOwnProfile(userId)); }); - const [busy, setBusy] = useState(false); - - const uploadRef = useRef(); return
- { - if (!ev.target.files?.length) return; - setBusy(true); - const file = ev.target.files[0]; - const uri = await cli.uploadContent(file); - await cli.setAvatarUrl(uri); - setBusy(false); - }} - accept="image/*" - /> - - { - uploadRef.current.click(); - }} + cli.setAvatarUrl(url)} > - - - {state => ( - - )} - - +

{ _t("Welcome %(name)s", { name: ownProfile.displayName }) }

{ _t("Now, let's help you get started") }

diff --git a/src/components/views/elements/MiniAvatarUploader.tsx b/src/components/views/elements/MiniAvatarUploader.tsx new file mode 100644 index 0000000000..903826c3e6 --- /dev/null +++ b/src/components/views/elements/MiniAvatarUploader.tsx @@ -0,0 +1,90 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React, {useContext, useRef, useState} from 'react'; +import classNames from 'classnames'; + +import AccessibleButton from "./AccessibleButton"; +import Tooltip from './Tooltip'; +import MatrixClientContext from "../../../contexts/MatrixClientContext"; +import {useTimeout} from "../../../hooks/useTimeout"; + +export const AVATAR_SIZE = 52; + +interface IProps { + hasAvatar: boolean; + noAvatarLabel?: string; + hasAvatarLabel?: string; + setAvatarUrl(url: string): Promise; +} + +const MiniAvatarUploader: React.FC = ({ hasAvatar, hasAvatarLabel, noAvatarLabel, setAvatarUrl, children }) => { + const cli = useContext(MatrixClientContext); + const [busy, setBusy] = useState(false); + const [hover, setHover] = useState(false); + const [show, setShow] = useState(false); + + useTimeout(() => { + setShow(true); + }, 3_000); // show after 3 seconds + useTimeout(() => { + setShow(false); + }, 13_000); // hide after being shown for 10 seconds + + const uploadRef = useRef(); + + const label = hasAvatar || busy ? hasAvatarLabel : noAvatarLabel; + + return + { + if (!ev.target.files?.length) return; + setBusy(true); + const file = ev.target.files[0]; + const uri = await cli.uploadContent(file); + await setAvatarUrl(uri); + setBusy(false); + }} + accept="image/*" + /> + + { + uploadRef.current.click(); + }} + onMouseOver={() => setHover(true)} + onMouseLeave={() => setHover(false)} + > + { children } + + + + ; +}; + +export default MiniAvatarUploader; From 664079a12735f0bbeb36593d2da754dbdfda76f6 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 5 Nov 2020 15:50:42 +0000 Subject: [PATCH 13/62] Use MiniAvatarUploader for the new NewRoomIntro component --- res/css/_components.scss | 1 + res/css/views/messages/_CreateEvent.scss | 2 +- res/css/views/rooms/_NewRoomIntro.scss | 67 ++++++++++ src/components/structures/MessagePanel.js | 6 +- src/components/views/avatars/RoomAvatar.tsx | 5 +- src/components/views/rooms/NewRoomIntro.tsx | 131 ++++++++++++++++++++ 6 files changed, 208 insertions(+), 4 deletions(-) create mode 100644 res/css/views/rooms/_NewRoomIntro.scss create mode 100644 src/components/views/rooms/NewRoomIntro.tsx diff --git a/res/css/_components.scss b/res/css/_components.scss index 860b0f126e..62b5031a31 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -184,6 +184,7 @@ @import "./views/rooms/_MemberList.scss"; @import "./views/rooms/_MessageComposer.scss"; @import "./views/rooms/_MessageComposerFormatBar.scss"; +@import "./views/rooms/_NewRoomIntro.scss"; @import "./views/rooms/_NotificationBadge.scss"; @import "./views/rooms/_PinnedEventTile.scss"; @import "./views/rooms/_PinnedEventsPanel.scss"; diff --git a/res/css/views/messages/_CreateEvent.scss b/res/css/views/messages/_CreateEvent.scss index 1181a80d10..a61a261ff1 100644 --- a/res/css/views/messages/_CreateEvent.scss +++ b/res/css/views/messages/_CreateEvent.scss @@ -16,7 +16,7 @@ limitations under the License. .mx_CreateEvent { &::before { - background-color: $primary-fg-color; + background-color: $composer-e2e-icon-color; mask-image: url('$(res)/img/element-icons/chat-bubbles.svg'); } } diff --git a/res/css/views/rooms/_NewRoomIntro.scss b/res/css/views/rooms/_NewRoomIntro.scss new file mode 100644 index 0000000000..af72a0dd69 --- /dev/null +++ b/res/css/views/rooms/_NewRoomIntro.scss @@ -0,0 +1,67 @@ +/* +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. +*/ + +.mx_NewRoomIntro { + margin: 80px 0 48px 64px; + + .mx_MiniAvatarUploader_hasAvatar:not(.mx_MiniAvatarUploader_busy):not(:hover) { + &::before, &::after { + content: unset; + } + } + + .mx_AccessibleButton_kind_link { + padding: 0; + font-size: inherit; + } + + .mx_NewRoomIntro_buttons { + margin-top: 28px; + + .mx_AccessibleButton { + line-height: $font-24px; + + &::before { + content: ''; + display: inline-block; + background-color: $button-fg-color; + mask-position: center; + mask-repeat: no-repeat; + mask-size: 20px; + width: 20px; + height: 20px; + margin-right: 5px; + vertical-align: text-bottom; + } + } + + .mx_NewRoomIntro_inviteButton::before { + mask-image: url('$(res)/img/element-icons/room/invite.svg'); + } + } + + > h2 { + margin-top: 24px; + font-size: $font-24px; + font-weight: 600; + } + + > p { + margin: 0; + font-size: $font-15px; + color: $secondary-fg-color; + } +} diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index 9cf3eaaa9d..512ffb6c07 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -31,6 +31,7 @@ import {haveTileForEvent} from "../views/rooms/EventTile"; import {textForEvent} from "../../TextForEvent"; import IRCTimelineProfileResizer from "../views/elements/IRCTimelineProfileResizer"; import DMRoomMap from "../../utils/DMRoomMap"; +import NewRoomIntro from "../views/rooms/NewRoomIntro"; const CONTINUATION_MAX_INTERVAL = 5 * 60 * 1000; // 5 minutes const continuedTypes = ['m.sticker', 'm.room.message']; @@ -955,13 +956,16 @@ class CreationGrouper { const ev = this.events[this.events.length - 1]; let summaryText; + const roomId = ev.getRoomId(); const creator = ev.sender ? ev.sender.name : ev.getSender(); - if (DMRoomMap.shared().getUserIdForRoomId(ev.getRoomId())) { + if (DMRoomMap.shared().getUserIdForRoomId(roomId)) { summaryText = _t("%(creator)s created this DM.", { creator }); } else { summaryText = _t("%(creator)s created and configured the room.", { creator }); } + ret.push(); + ret.push( { }; public render() { - const {room, oobData, viewAvatarOnClick, ...otherProps} = this.props; + const {room, oobData, viewAvatarOnClick, onClick, ...otherProps} = this.props; const roomName = room ? room.name : oobData.name; @@ -139,7 +140,7 @@ export default class RoomAvatar extends React.Component { name={roomName} idName={room ? room.roomId : null} urls={this.state.urls} - onClick={viewAvatarOnClick && this.state.urls[0] ? this.onRoomAvatarClick : null} + onClick={viewAvatarOnClick && this.state.urls[0] ? this.onRoomAvatarClick : onClick} /> ); } diff --git a/src/components/views/rooms/NewRoomIntro.tsx b/src/components/views/rooms/NewRoomIntro.tsx new file mode 100644 index 0000000000..27404eef12 --- /dev/null +++ b/src/components/views/rooms/NewRoomIntro.tsx @@ -0,0 +1,131 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React, {useContext} from "react"; +import {EventType} from "matrix-js-sdk/src/@types/event"; + +import MatrixClientContext from "../../../contexts/MatrixClientContext"; +import RoomContext from "../../../contexts/RoomContext"; +import DMRoomMap from "../../../utils/DMRoomMap"; +import {_t} from "../../../languageHandler"; +import AccessibleButton from "../elements/AccessibleButton"; +import MiniAvatarUploader, {AVATAR_SIZE} from "../elements/MiniAvatarUploader"; +import RoomAvatar from "../avatars/RoomAvatar"; +import defaultDispatcher from "../../../dispatcher/dispatcher"; +import {ViewUserPayload} from "../../../dispatcher/payloads/ViewUserPayload"; +import {Action} from "../../../dispatcher/actions"; +import dis from "../../../dispatcher/dispatcher"; + +const NewRoomIntro = () => { + const cli = useContext(MatrixClientContext); + const {room, roomId} = useContext(RoomContext); + + const dmPartner = DMRoomMap.shared().getUserIdForRoomId(roomId); + let body; + if (dmPartner) { + let caption; + if ((room.getJoinedMemberCount() + room.getInvitedMemberCount()) === 2) { + caption = _t("Only the two of you are in this conversation, unless either of you invites anyone to join."); + } + + const member = room?.getMember(dmPartner); + const displayName = member?.rawDisplayName || dmPartner; + body = + { + defaultDispatcher.dispatch({ + action: Action.ViewUser, + // XXX: We should be using a real member object and not assuming what the receiver wants. + member: member || {userId: dmPartner}, + }); + }} /> + +

{ room.name }

+ +

{_t("This is the beginning of your direct message history with .", {}, { + displayName: () => { displayName }, + })}

+ { caption &&

{ caption }

} +
; + } else { + const topic = room.currentState.getStateEvents(EventType.RoomTopic, "")?.getContent()?.topic; + const canAddTopic = room.currentState.maySendStateEvent(EventType.RoomTopic, cli.getUserId()); + + const onTopicClick = () => { + dis.dispatch({ + action: "open_room_settings", + room_id: roomId, + }, true); + // focus the topic field to help the user find it as it'll gain an outline + setImmediate(() => { + window.document.getElementById("profileTopic").focus(); + }); + }; + + let topicText; + if (canAddTopic && topic) { + topicText = _t("Topic: %(topic)s (edit)", { topic }, { + a: sub => { sub }, + }); + } else if (topic) { + topicText = _t("Topic: %(topic)s ", { topic }); + } else if (canAddTopic) { + topicText = _t("Add a topic to help people know what it is about.", {}, { + a: sub => { sub }, + }); + } + + const creator = room.currentState.getStateEvents(EventType.RoomCreate, "")?.getSender(); + const creatorName = room?.getMember(creator)?.rawDisplayName || creator; + + let createdText; + if (creator === cli.getUserId()) { + createdText = _t("You created this room."); + } else { + createdText = _t("%(displayName)s created this room.", { + displayName: creatorName, + }); + } + + const avatarUrl = room.currentState.getStateEvents(EventType.RoomAvatar, "")?.getContent()?.url; + body = + cli.sendStateEvent(roomId, EventType.RoomAvatar, { url }, '')} + > + + + +

{ room.name }

+ +

{createdText} {_t("This is the start of .", {}, { + roomName: () => { room.name }, + })}

+

{topicText}

+
+ + {_t("Invite to this room")} + +
+
; + } + + return
+ { body } +
; +}; + +export default NewRoomIntro; From c269577037ca209266c2e95cd8d182e4932f86d6 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 5 Nov 2020 15:51:36 +0000 Subject: [PATCH 14/62] i18n --- src/i18n/strings/en_EN.json | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 8b976c144e..271b288c11 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1083,6 +1083,15 @@ "Strikethrough": "Strikethrough", "Code block": "Code block", "Quote": "Quote", + "Only the two of you are in this conversation, unless either of you invites anyone to join.": "Only the two of you are in this conversation, unless either of you invites anyone to join.", + "This is the beginning of your direct message history with .": "This is the beginning of your direct message history with .", + "Topic: %(topic)s (edit)": "Topic: %(topic)s (edit)", + "Topic: %(topic)s ": "Topic: %(topic)s ", + "Add a topic to help people know what it is about.": "Add a topic to help people know what it is about.", + "You created this room.": "You created this room.", + "%(displayName)s created this room.": "%(displayName)s created this room.", + "Add a photo, so people can easily spot your room.": "Add a photo, so people can easily spot your room.", + "This is the start of .": "This is the start of .", "No pinned messages.": "No pinned messages.", "Loading...": "Loading...", "Pinned Messages": "Pinned Messages", @@ -2126,7 +2135,6 @@ "Starting microphone...": "Starting microphone...", "Connectivity to the server has been lost.": "Connectivity to the server has been lost.", "Sent messages will be stored until your connection has returned.": "Sent messages will be stored until your connection has returned.", - "There's no one else here! Would you like to invite others or stop warning about the empty room?": "There's no one else here! Would you like to invite others or stop warning about the empty room?", "You seem to be uploading files, are you sure you want to quit?": "You seem to be uploading files, are you sure you want to quit?", "You seem to be in a call, are you sure you want to quit?": "You seem to be in a call, are you sure you want to quit?", "Search failed": "Search failed", From 81f1e1d8d713da3264722a58ba534fd430d4c859 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 5 Nov 2020 15:54:21 +0000 Subject: [PATCH 15/62] delint --- res/css/views/messages/_EventTileBubble.scss | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/res/css/views/messages/_EventTileBubble.scss b/res/css/views/messages/_EventTileBubble.scss index 99feb03ef9..e0f5d521cb 100644 --- a/res/css/views/messages/_EventTileBubble.scss +++ b/res/css/views/messages/_EventTileBubble.scss @@ -53,11 +53,8 @@ limitations under the License. } .mx_EventTileBubble_subtitle { + font-size: $font-12px; grid-column: 2; grid-row: 2; } - - .mx_EventTileBubble_subtitle { - font-size: $font-12px; - } } From 602232a524d0b2b17fd878b3949e999a1634ec86 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 5 Nov 2020 16:08:36 +0000 Subject: [PATCH 16/62] Update res/css/views/messages/_CreateEvent.scss Co-authored-by: Travis Ralston --- res/css/views/messages/_CreateEvent.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/css/views/messages/_CreateEvent.scss b/res/css/views/messages/_CreateEvent.scss index a61a261ff1..cb2bf841dd 100644 --- a/res/css/views/messages/_CreateEvent.scss +++ b/res/css/views/messages/_CreateEvent.scss @@ -1,5 +1,5 @@ /* -Copyright 2020 The Matrix.org Foundation C.I.C. +Copyright 2018, 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. From 54e41b5f32e37e43686f827a05f232905d03ed2d Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 5 Nov 2020 16:27:41 +0000 Subject: [PATCH 17/62] fix tests --- src/components/views/messages/EncryptionEvent.tsx | 7 ++++--- src/components/views/messages/EventTileBubble.tsx | 8 ++++---- src/contexts/RoomContext.ts | 1 - test/components/structures/MessagePanel-test.js | 3 +++ test/test-utils.js | 1 + 5 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/components/views/messages/EncryptionEvent.tsx b/src/components/views/messages/EncryptionEvent.tsx index 7416cc3c9a..3af9c463c9 100644 --- a/src/components/views/messages/EncryptionEvent.tsx +++ b/src/components/views/messages/EncryptionEvent.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, {useContext} from 'react'; +import React, {forwardRef, useContext} from 'react'; import {MatrixEvent} from "matrix-js-sdk/src/models/event"; import { _t } from '../../../languageHandler'; @@ -27,7 +27,7 @@ interface IProps { mxEvent: MatrixEvent; } -const EncryptionEvent: React.FC = ({mxEvent}) => { +const EncryptionEvent = forwardRef(({mxEvent}, ref) => { const cli = useContext(MatrixClientContext); const roomId = mxEvent.getRoomId(); const isRoomEncrypted = MatrixClientPeg.get().isRoomEncrypted(roomId); @@ -61,7 +61,8 @@ const EncryptionEvent: React.FC = ({mxEvent}) => { className="mx_cryptoEvent mx_cryptoEvent_icon mx_cryptoEvent_icon_warning" title={_t("Encryption not enabled")} subtitle={_t("The encryption used by this room isn't supported.")} + ref={ref} />; -} +}); export default EncryptionEvent; diff --git a/src/components/views/messages/EventTileBubble.tsx b/src/components/views/messages/EventTileBubble.tsx index 8a2cf0f01c..f797a97a3d 100644 --- a/src/components/views/messages/EventTileBubble.tsx +++ b/src/components/views/messages/EventTileBubble.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, {ReactNode} from "react"; +import React, {forwardRef, ReactNode} from "react"; import classNames from "classnames"; interface IProps { @@ -23,12 +23,12 @@ interface IProps { subtitle?: ReactNode; } -const EventTileBubble: React.FC = ({ className, title, subtitle, children }) => { - return
+const EventTileBubble = forwardRef(({ className, title, subtitle, children }, ref) => { + return
{ title }
{ subtitle &&
{ subtitle }
} { children }
; -}; +}); export default EventTileBubble; diff --git a/src/contexts/RoomContext.ts b/src/contexts/RoomContext.ts index e8eb0c23b4..082dcc4e6b 100644 --- a/src/contexts/RoomContext.ts +++ b/src/contexts/RoomContext.ts @@ -29,7 +29,6 @@ const RoomContext = createContext({ guestsCanJoin: false, canPeek: false, showApps: false, - isAlone: false, isPeeking: false, showingPinned: false, showReadReceipts: true, diff --git a/test/components/structures/MessagePanel-test.js b/test/components/structures/MessagePanel-test.js index 235ae94010..0bcdfcd5e5 100644 --- a/test/components/structures/MessagePanel-test.js +++ b/test/components/structures/MessagePanel-test.js @@ -38,6 +38,7 @@ import { configure, mount } from "enzyme"; import Velocity from 'velocity-animate'; import MatrixClientContext from "../../../src/contexts/MatrixClientContext"; import RoomContext from "../../../src/contexts/RoomContext"; +import DMRoomMap from "../../../src/utils/DMRoomMap"; configure({ adapter: new Adapter() }); @@ -79,6 +80,8 @@ describe('MessagePanel', function() { // complete without this even if we mock the clock and tick it // what should be the correct amount of time). Velocity.mock = true; + + DMRoomMap.makeShared(); }); afterEach(function() { diff --git a/test/test-utils.js b/test/test-utils.js index d006eee823..c8e623b1d9 100644 --- a/test/test-utils.js +++ b/test/test-utils.js @@ -242,6 +242,7 @@ export function mkStubRoom(roomId = null) { setBlacklistUnverifiedDevices: jest.fn(), on: jest.fn(), removeListener: jest.fn(), + getDMInviter: jest.fn(), }; } From ae2d9941ff26c4f8dc55124dbf1237c879332b67 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 5 Nov 2020 16:54:25 +0000 Subject: [PATCH 18/62] fix more tests --- src/Skinner.js | 4 ++-- test/components/structures/MessagePanel-test.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Skinner.js b/src/Skinner.js index 87c5a7be7f..d17bc1782a 100644 --- a/src/Skinner.js +++ b/src/Skinner.js @@ -50,8 +50,8 @@ class Skinner { return null; } - // components have to be functions. - const validType = typeof comp === 'function'; + // components have to be functions or forwardRef objects with a render function. + const validType = typeof comp === 'function' || comp.render; if (!validType) { throw new Error(`Not a valid component: ${name} (type = ${typeof(comp)}).`); } diff --git a/test/components/structures/MessagePanel-test.js b/test/components/structures/MessagePanel-test.js index 0bcdfcd5e5..0cb7e2cc71 100644 --- a/test/components/structures/MessagePanel-test.js +++ b/test/components/structures/MessagePanel-test.js @@ -53,7 +53,7 @@ class WrappedMessagePanel extends React.Component { render() { return - + ; From 4997676f5d472462b0ddbabafb7aafb1044ea7e3 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 5 Nov 2020 16:57:51 +0000 Subject: [PATCH 19/62] fix last remaining broken test --- test/components/structures/MessagePanel-test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/components/structures/MessagePanel-test.js b/test/components/structures/MessagePanel-test.js index 0cb7e2cc71..f40f8c5187 100644 --- a/test/components/structures/MessagePanel-test.js +++ b/test/components/structures/MessagePanel-test.js @@ -436,8 +436,8 @@ describe('MessagePanel', function() { const rm = res.find('.mx_RoomView_myReadMarker_container').getDOMNode(); const rows = res.find('.mx_RoomView_MessageList').children(); - expect(rows.length).toEqual(6); - expect(rm.previousSibling).toEqual(rows.at(4).getDOMNode()); + expect(rows.length).toEqual(7); // 6 events + the NewRoomIntro + expect(rm.previousSibling).toEqual(rows.at(5).getDOMNode()); // read marker should be hidden given props and at the last event expect(isReadMarkerVisible(rm)).toBeFalsy(); From 252484fcd9c9d435c41ea7f6832af04ee94a4277 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 5 Nov 2020 17:01:20 +0000 Subject: [PATCH 20/62] add key to make React happier --- src/components/structures/MessagePanel.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index 512ffb6c07..375545f819 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -964,7 +964,7 @@ class CreationGrouper { summaryText = _t("%(creator)s created and configured the room.", { creator }); } - ret.push(); + ret.push(); ret.push( Date: Mon, 9 Nov 2020 01:07:15 +0000 Subject: [PATCH 21/62] hide some validation tooltips if fields are valid. fixes https://github.com/vector-im/element-web/issues/9683 --- src/components/views/auth/RegistrationForm.js | 3 +++ src/components/views/elements/Validation.tsx | 7 +++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/components/views/auth/RegistrationForm.js b/src/components/views/auth/RegistrationForm.js index db7d1df994..419443984a 100644 --- a/src/components/views/auth/RegistrationForm.js +++ b/src/components/views/auth/RegistrationForm.js @@ -250,6 +250,7 @@ export default class RegistrationForm extends React.Component { validateEmailRules = withValidation({ description: () => _t("Use an email address to recover your account"), + hideDescriptionIfValid: true, rules: [ { key: "required", @@ -326,6 +327,7 @@ export default class RegistrationForm extends React.Component { validatePhoneNumberRules = withValidation({ description: () => _t("Other users can invite you to rooms using your contact details"), + hideDescriptionIfValid: true, rules: [ { key: "required", @@ -356,6 +358,7 @@ export default class RegistrationForm extends React.Component { validateUsernameRules = withValidation({ description: () => _t("Use lowercase letters, numbers, dashes and underscores only"), + hideDescriptionIfValid: true, rules: [ { key: "required", diff --git a/src/components/views/elements/Validation.tsx b/src/components/views/elements/Validation.tsx index 55e5714719..667b8e99aa 100644 --- a/src/components/views/elements/Validation.tsx +++ b/src/components/views/elements/Validation.tsx @@ -33,6 +33,7 @@ interface IRule { interface IArgs { rules: IRule[]; description(this: T, derivedData: D): React.ReactChild; + hideDescriptionIfValid: Boolean; deriveData?(data: Data): Promise; } @@ -54,6 +55,8 @@ export interface IValidationResult { * @param {Function} description * Function that returns a string summary of the kind of value that will * meet the validation rules. Shown at the top of the validation feedback. + * @param {Boolean} hideDescriptionIfValid + * If true, don't show the description if the validation passes validation. * @param {Function} deriveData * Optional function that returns a Promise to an object of generic type D. * The result of this Promise is passed to rule methods `skip`, `test`, `valid`, and `invalid`. @@ -71,7 +74,7 @@ export interface IValidationResult { * A validation function that takes in the current input value and returns * the overall validity and a feedback UI that can be rendered for more detail. */ -export default function withValidation({ description, deriveData, rules }: IArgs) { +export default function withValidation({ description, hideDescriptionIfValid, deriveData, rules }: IArgs) { return async function onValidate({ value, focused, allowEmpty = true }: IFieldState): Promise { if (!value && allowEmpty) { return { @@ -156,7 +159,7 @@ export default function withValidation({ description, d } let summary; - if (description) { + if (description && (details || !hideDescriptionIfValid)) { // We're setting `this` to whichever component holds the validation // function. That allows rules to access the state of the component. const content = description.call(this, derivedData); From 2cac5f05a1c56ce1a18c4f1fe6e6a789f3696e24 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Mon, 9 Nov 2020 01:16:38 +0000 Subject: [PATCH 22/62] lint --- src/components/views/elements/Validation.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/views/elements/Validation.tsx b/src/components/views/elements/Validation.tsx index 667b8e99aa..7a8e47dfb6 100644 --- a/src/components/views/elements/Validation.tsx +++ b/src/components/views/elements/Validation.tsx @@ -74,7 +74,9 @@ export interface IValidationResult { * A validation function that takes in the current input value and returns * the overall validity and a feedback UI that can be rendered for more detail. */ -export default function withValidation({ description, hideDescriptionIfValid, deriveData, rules }: IArgs) { +export default function withValidation({ + description, hideDescriptionIfValid, deriveData, rules +}: IArgs) { return async function onValidate({ value, focused, allowEmpty = true }: IFieldState): Promise { if (!value && allowEmpty) { return { From ef09ff661593aa52e0b151cf0e7af87142bc8b8e Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Mon, 9 Nov 2020 01:16:58 +0000 Subject: [PATCH 23/62] lint again --- src/components/views/elements/Validation.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/elements/Validation.tsx b/src/components/views/elements/Validation.tsx index 7a8e47dfb6..35bf3a0456 100644 --- a/src/components/views/elements/Validation.tsx +++ b/src/components/views/elements/Validation.tsx @@ -75,7 +75,7 @@ export interface IValidationResult { * the overall validity and a feedback UI that can be rendered for more detail. */ export default function withValidation({ - description, hideDescriptionIfValid, deriveData, rules + description, hideDescriptionIfValid, deriveData, rules, }: IArgs) { return async function onValidate({ value, focused, allowEmpty = true }: IFieldState): Promise { if (!value && allowEmpty) { From 99fb62cc681e4c006a26d6831ee0f75cab8ce7a5 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Mon, 9 Nov 2020 01:37:17 +0000 Subject: [PATCH 24/62] fix lint yet again --- src/components/views/elements/Validation.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/elements/Validation.tsx b/src/components/views/elements/Validation.tsx index 35bf3a0456..c45a745918 100644 --- a/src/components/views/elements/Validation.tsx +++ b/src/components/views/elements/Validation.tsx @@ -33,7 +33,7 @@ interface IRule { interface IArgs { rules: IRule[]; description(this: T, derivedData: D): React.ReactChild; - hideDescriptionIfValid: Boolean; + hideDescriptionIfValid?: Boolean; deriveData?(data: Data): Promise; } From d121f8a35787897fb2fe25facf86183c12e847b8 Mon Sep 17 00:00:00 2001 From: Aaron Raimist Date: Sun, 8 Nov 2020 22:33:12 -0600 Subject: [PATCH 25/62] Allow country names to be translated Signed-off-by: Aaron Raimist --- src/components/views/auth/CountryDropdown.js | 2 +- src/i18n/strings/en_EN.json | 249 +++++++++ src/phonenumber.ts | 500 ++++++++++--------- 3 files changed, 501 insertions(+), 250 deletions(-) diff --git a/src/components/views/auth/CountryDropdown.js b/src/components/views/auth/CountryDropdown.js index 37b1967c48..3296b574a4 100644 --- a/src/components/views/auth/CountryDropdown.js +++ b/src/components/views/auth/CountryDropdown.js @@ -123,7 +123,7 @@ export default class CountryDropdown extends React.Component { const options = displayedCountries.map((country) => { return
{ this._flagImgForIso2(country.iso2) } - { country.name } (+{ country.prefix }) + { _t(country.name) } (+{ country.prefix })
; }); diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index a56e22e5fc..78340447f3 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -117,6 +117,255 @@ "Unable to enable Notifications": "Unable to enable Notifications", "This email address was not found": "This email address was not found", "Your email address does not appear to be associated with a Matrix ID on this Homeserver.": "Your email address does not appear to be associated with a Matrix ID on this Homeserver.", + "United Kingdom": "United Kingdom", + "United States": "United States", + "Afghanistan": "Afghanistan", + "Åland Islands": "Åland Islands", + "Albania": "Albania", + "Algeria": "Algeria", + "American Samoa": "American Samoa", + "Andorra": "Andorra", + "Angola": "Angola", + "Anguilla": "Anguilla", + "Antarctica": "Antarctica", + "Antigua & Barbuda": "Antigua & Barbuda", + "Argentina": "Argentina", + "Armenia": "Armenia", + "Aruba": "Aruba", + "Australia": "Australia", + "Austria": "Austria", + "Azerbaijan": "Azerbaijan", + "Bahamas": "Bahamas", + "Bahrain": "Bahrain", + "Bangladesh": "Bangladesh", + "Barbados": "Barbados", + "Belarus": "Belarus", + "Belgium": "Belgium", + "Belize": "Belize", + "Benin": "Benin", + "Bermuda": "Bermuda", + "Bhutan": "Bhutan", + "Bolivia": "Bolivia", + "Bosnia": "Bosnia", + "Botswana": "Botswana", + "Bouvet Island": "Bouvet Island", + "Brazil": "Brazil", + "British Indian Ocean Territory": "British Indian Ocean Territory", + "British Virgin Islands": "British Virgin Islands", + "Brunei": "Brunei", + "Bulgaria": "Bulgaria", + "Burkina Faso": "Burkina Faso", + "Burundi": "Burundi", + "Cambodia": "Cambodia", + "Cameroon": "Cameroon", + "Canada": "Canada", + "Cape Verde": "Cape Verde", + "Caribbean Netherlands": "Caribbean Netherlands", + "Cayman Islands": "Cayman Islands", + "Central African Republic": "Central African Republic", + "Chad": "Chad", + "Chile": "Chile", + "China": "China", + "Christmas Island": "Christmas Island", + "Cocos (Keeling) Islands": "Cocos (Keeling) Islands", + "Colombia": "Colombia", + "Comoros": "Comoros", + "Congo - Brazzaville": "Congo - Brazzaville", + "Congo - Kinshasa": "Congo - Kinshasa", + "Cook Islands": "Cook Islands", + "Costa Rica": "Costa Rica", + "Croatia": "Croatia", + "Cuba": "Cuba", + "Curaçao": "Curaçao", + "Cyprus": "Cyprus", + "Czech Republic": "Czech Republic", + "Côte d’Ivoire": "Côte d’Ivoire", + "Denmark": "Denmark", + "Djibouti": "Djibouti", + "Dominica": "Dominica", + "Dominican Republic": "Dominican Republic", + "Ecuador": "Ecuador", + "Egypt": "Egypt", + "El Salvador": "El Salvador", + "Equatorial Guinea": "Equatorial Guinea", + "Eritrea": "Eritrea", + "Estonia": "Estonia", + "Ethiopia": "Ethiopia", + "Falkland Islands": "Falkland Islands", + "Faroe Islands": "Faroe Islands", + "Fiji": "Fiji", + "Finland": "Finland", + "France": "France", + "French Guiana": "French Guiana", + "French Polynesia": "French Polynesia", + "French Southern Territories": "French Southern Territories", + "Gabon": "Gabon", + "Gambia": "Gambia", + "Georgia": "Georgia", + "Germany": "Germany", + "Ghana": "Ghana", + "Gibraltar": "Gibraltar", + "Greece": "Greece", + "Greenland": "Greenland", + "Grenada": "Grenada", + "Guadeloupe": "Guadeloupe", + "Guam": "Guam", + "Guatemala": "Guatemala", + "Guernsey": "Guernsey", + "Guinea": "Guinea", + "Guinea-Bissau": "Guinea-Bissau", + "Guyana": "Guyana", + "Haiti": "Haiti", + "Heard & McDonald Islands": "Heard & McDonald Islands", + "Honduras": "Honduras", + "Hong Kong": "Hong Kong", + "Hungary": "Hungary", + "Iceland": "Iceland", + "India": "India", + "Indonesia": "Indonesia", + "Iran": "Iran", + "Iraq": "Iraq", + "Ireland": "Ireland", + "Isle of Man": "Isle of Man", + "Israel": "Israel", + "Italy": "Italy", + "Jamaica": "Jamaica", + "Japan": "Japan", + "Jersey": "Jersey", + "Jordan": "Jordan", + "Kazakhstan": "Kazakhstan", + "Kenya": "Kenya", + "Kiribati": "Kiribati", + "Kosovo": "Kosovo", + "Kuwait": "Kuwait", + "Kyrgyzstan": "Kyrgyzstan", + "Laos": "Laos", + "Latvia": "Latvia", + "Lebanon": "Lebanon", + "Lesotho": "Lesotho", + "Liberia": "Liberia", + "Libya": "Libya", + "Liechtenstein": "Liechtenstein", + "Lithuania": "Lithuania", + "Luxembourg": "Luxembourg", + "Macau": "Macau", + "Macedonia": "Macedonia", + "Madagascar": "Madagascar", + "Malawi": "Malawi", + "Malaysia": "Malaysia", + "Maldives": "Maldives", + "Mali": "Mali", + "Malta": "Malta", + "Marshall Islands": "Marshall Islands", + "Martinique": "Martinique", + "Mauritania": "Mauritania", + "Mauritius": "Mauritius", + "Mayotte": "Mayotte", + "Mexico": "Mexico", + "Micronesia": "Micronesia", + "Moldova": "Moldova", + "Monaco": "Monaco", + "Mongolia": "Mongolia", + "Montenegro": "Montenegro", + "Montserrat": "Montserrat", + "Morocco": "Morocco", + "Mozambique": "Mozambique", + "Myanmar": "Myanmar", + "Namibia": "Namibia", + "Nauru": "Nauru", + "Nepal": "Nepal", + "Netherlands": "Netherlands", + "New Caledonia": "New Caledonia", + "New Zealand": "New Zealand", + "Nicaragua": "Nicaragua", + "Niger": "Niger", + "Nigeria": "Nigeria", + "Niue": "Niue", + "Norfolk Island": "Norfolk Island", + "North Korea": "North Korea", + "Northern Mariana Islands": "Northern Mariana Islands", + "Norway": "Norway", + "Oman": "Oman", + "Pakistan": "Pakistan", + "Palau": "Palau", + "Palestine": "Palestine", + "Panama": "Panama", + "Papua New Guinea": "Papua New Guinea", + "Paraguay": "Paraguay", + "Peru": "Peru", + "Philippines": "Philippines", + "Pitcairn Islands": "Pitcairn Islands", + "Poland": "Poland", + "Portugal": "Portugal", + "Puerto Rico": "Puerto Rico", + "Qatar": "Qatar", + "Romania": "Romania", + "Russia": "Russia", + "Rwanda": "Rwanda", + "Réunion": "Réunion", + "Samoa": "Samoa", + "San Marino": "San Marino", + "Saudi Arabia": "Saudi Arabia", + "Senegal": "Senegal", + "Serbia": "Serbia", + "Seychelles": "Seychelles", + "Sierra Leone": "Sierra Leone", + "Singapore": "Singapore", + "Sint Maarten": "Sint Maarten", + "Slovakia": "Slovakia", + "Slovenia": "Slovenia", + "Solomon Islands": "Solomon Islands", + "Somalia": "Somalia", + "South Africa": "South Africa", + "South Georgia & South Sandwich Islands": "South Georgia & South Sandwich Islands", + "South Korea": "South Korea", + "South Sudan": "South Sudan", + "Spain": "Spain", + "Sri Lanka": "Sri Lanka", + "St. Barthélemy": "St. Barthélemy", + "St. Helena": "St. Helena", + "St. Kitts & Nevis": "St. Kitts & Nevis", + "St. Lucia": "St. Lucia", + "St. Martin": "St. Martin", + "St. Pierre & Miquelon": "St. Pierre & Miquelon", + "St. Vincent & Grenadines": "St. Vincent & Grenadines", + "Sudan": "Sudan", + "Suriname": "Suriname", + "Svalbard & Jan Mayen": "Svalbard & Jan Mayen", + "Swaziland": "Swaziland", + "Sweden": "Sweden", + "Switzerland": "Switzerland", + "Syria": "Syria", + "São Tomé & Príncipe": "São Tomé & Príncipe", + "Taiwan": "Taiwan", + "Tajikistan": "Tajikistan", + "Tanzania": "Tanzania", + "Thailand": "Thailand", + "Timor-Leste": "Timor-Leste", + "Togo": "Togo", + "Tokelau": "Tokelau", + "Tonga": "Tonga", + "Trinidad & Tobago": "Trinidad & Tobago", + "Tunisia": "Tunisia", + "Turkey": "Turkey", + "Turkmenistan": "Turkmenistan", + "Turks & Caicos Islands": "Turks & Caicos Islands", + "Tuvalu": "Tuvalu", + "U.S. Virgin Islands": "U.S. Virgin Islands", + "Uganda": "Uganda", + "Ukraine": "Ukraine", + "United Arab Emirates": "United Arab Emirates", + "Uruguay": "Uruguay", + "Uzbekistan": "Uzbekistan", + "Vanuatu": "Vanuatu", + "Vatican City": "Vatican City", + "Venezuela": "Venezuela", + "Vietnam": "Vietnam", + "Wallis & Futuna": "Wallis & Futuna", + "Western Sahara": "Western Sahara", + "Yemen": "Yemen", + "Zambia": "Zambia", + "Zimbabwe": "Zimbabwe", "Sign In or Create Account": "Sign In or Create Account", "Use your account or create a new one to continue.": "Use your account or create a new one to continue.", "Create Account": "Create Account", diff --git a/src/phonenumber.ts b/src/phonenumber.ts index 6dd44a3c12..ea008cf2f0 100644 --- a/src/phonenumber.ts +++ b/src/phonenumber.ts @@ -14,6 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ +import { _td } from './languageHandler'; + const PHONE_NUMBER_REGEXP = /^[0-9 -.]+$/; /* @@ -43,1247 +45,1247 @@ export const getEmojiFlag = (countryCode: string) => { export const COUNTRIES = [ { "iso2": "GB", - "name": "United Kingdom", + "name": _td("United Kingdom"), "prefix": "44", }, { "iso2": "US", - "name": "United States", + "name": _td("United States"), "prefix": "1", }, { "iso2": "AF", - "name": "Afghanistan", + "name": _td("Afghanistan"), "prefix": "93", }, { "iso2": "AX", - "name": "\u00c5land Islands", + "name": _td("\u00c5land Islands"), "prefix": "358", }, { "iso2": "AL", - "name": "Albania", + "name": _td("Albania"), "prefix": "355", }, { "iso2": "DZ", - "name": "Algeria", + "name": _td("Algeria"), "prefix": "213", }, { "iso2": "AS", - "name": "American Samoa", + "name": _td("American Samoa"), "prefix": "1", }, { "iso2": "AD", - "name": "Andorra", + "name": _td("Andorra"), "prefix": "376", }, { "iso2": "AO", - "name": "Angola", + "name": _td("Angola"), "prefix": "244", }, { "iso2": "AI", - "name": "Anguilla", + "name": _td("Anguilla"), "prefix": "1", }, { "iso2": "AQ", - "name": "Antarctica", + "name": _td("Antarctica"), "prefix": "672", }, { "iso2": "AG", - "name": "Antigua & Barbuda", + "name": _td("Antigua & Barbuda"), "prefix": "1", }, { "iso2": "AR", - "name": "Argentina", + "name": _td("Argentina"), "prefix": "54", }, { "iso2": "AM", - "name": "Armenia", + "name": _td("Armenia"), "prefix": "374", }, { "iso2": "AW", - "name": "Aruba", + "name": _td("Aruba"), "prefix": "297", }, { "iso2": "AU", - "name": "Australia", + "name": _td("Australia"), "prefix": "61", }, { "iso2": "AT", - "name": "Austria", + "name": _td("Austria"), "prefix": "43", }, { "iso2": "AZ", - "name": "Azerbaijan", + "name": _td("Azerbaijan"), "prefix": "994", }, { "iso2": "BS", - "name": "Bahamas", + "name": _td("Bahamas"), "prefix": "1", }, { "iso2": "BH", - "name": "Bahrain", + "name": _td("Bahrain"), "prefix": "973", }, { "iso2": "BD", - "name": "Bangladesh", + "name": _td("Bangladesh"), "prefix": "880", }, { "iso2": "BB", - "name": "Barbados", + "name": _td("Barbados"), "prefix": "1", }, { "iso2": "BY", - "name": "Belarus", + "name": _td("Belarus"), "prefix": "375", }, { "iso2": "BE", - "name": "Belgium", + "name": _td("Belgium"), "prefix": "32", }, { "iso2": "BZ", - "name": "Belize", + "name": _td("Belize"), "prefix": "501", }, { "iso2": "BJ", - "name": "Benin", + "name": _td("Benin"), "prefix": "229", }, { "iso2": "BM", - "name": "Bermuda", + "name": _td("Bermuda"), "prefix": "1", }, { "iso2": "BT", - "name": "Bhutan", + "name": _td("Bhutan"), "prefix": "975", }, { "iso2": "BO", - "name": "Bolivia", + "name": _td("Bolivia"), "prefix": "591", }, { "iso2": "BA", - "name": "Bosnia", + "name": _td("Bosnia"), "prefix": "387", }, { "iso2": "BW", - "name": "Botswana", + "name": _td("Botswana"), "prefix": "267", }, { "iso2": "BV", - "name": "Bouvet Island", + "name": _td("Bouvet Island"), "prefix": "47", }, { "iso2": "BR", - "name": "Brazil", + "name": _td("Brazil"), "prefix": "55", }, { "iso2": "IO", - "name": "British Indian Ocean Territory", + "name": _td("British Indian Ocean Territory"), "prefix": "246", }, { "iso2": "VG", - "name": "British Virgin Islands", + "name": _td("British Virgin Islands"), "prefix": "1", }, { "iso2": "BN", - "name": "Brunei", + "name": _td("Brunei"), "prefix": "673", }, { "iso2": "BG", - "name": "Bulgaria", + "name": _td("Bulgaria"), "prefix": "359", }, { "iso2": "BF", - "name": "Burkina Faso", + "name": _td("Burkina Faso"), "prefix": "226", }, { "iso2": "BI", - "name": "Burundi", + "name": _td("Burundi"), "prefix": "257", }, { "iso2": "KH", - "name": "Cambodia", + "name": _td("Cambodia"), "prefix": "855", }, { "iso2": "CM", - "name": "Cameroon", + "name": _td("Cameroon"), "prefix": "237", }, { "iso2": "CA", - "name": "Canada", + "name": _td("Canada"), "prefix": "1", }, { "iso2": "CV", - "name": "Cape Verde", + "name": _td("Cape Verde"), "prefix": "238", }, { "iso2": "BQ", - "name": "Caribbean Netherlands", + "name": _td("Caribbean Netherlands"), "prefix": "599", }, { "iso2": "KY", - "name": "Cayman Islands", + "name": _td("Cayman Islands"), "prefix": "1", }, { "iso2": "CF", - "name": "Central African Republic", + "name": _td("Central African Republic"), "prefix": "236", }, { "iso2": "TD", - "name": "Chad", + "name": _td("Chad"), "prefix": "235", }, { "iso2": "CL", - "name": "Chile", + "name": _td("Chile"), "prefix": "56", }, { "iso2": "CN", - "name": "China", + "name": _td("China"), "prefix": "86", }, { "iso2": "CX", - "name": "Christmas Island", + "name": _td("Christmas Island"), "prefix": "61", }, { "iso2": "CC", - "name": "Cocos (Keeling) Islands", + "name": _td("Cocos (Keeling) Islands"), "prefix": "61", }, { "iso2": "CO", - "name": "Colombia", + "name": _td("Colombia"), "prefix": "57", }, { "iso2": "KM", - "name": "Comoros", + "name": _td("Comoros"), "prefix": "269", }, { "iso2": "CG", - "name": "Congo - Brazzaville", + "name": _td("Congo - Brazzaville"), "prefix": "242", }, { "iso2": "CD", - "name": "Congo - Kinshasa", + "name": _td("Congo - Kinshasa"), "prefix": "243", }, { "iso2": "CK", - "name": "Cook Islands", + "name": _td("Cook Islands"), "prefix": "682", }, { "iso2": "CR", - "name": "Costa Rica", + "name": _td("Costa Rica"), "prefix": "506", }, { "iso2": "HR", - "name": "Croatia", + "name": _td("Croatia"), "prefix": "385", }, { "iso2": "CU", - "name": "Cuba", + "name": _td("Cuba"), "prefix": "53", }, { "iso2": "CW", - "name": "Cura\u00e7ao", + "name": _td("Cura\u00e7ao"), "prefix": "599", }, { "iso2": "CY", - "name": "Cyprus", + "name": _td("Cyprus"), "prefix": "357", }, { "iso2": "CZ", - "name": "Czech Republic", + "name": _td("Czech Republic"), "prefix": "420", }, { "iso2": "CI", - "name": "C\u00f4te d\u2019Ivoire", + "name": _td("C\u00f4te d\u2019Ivoire"), "prefix": "225", }, { "iso2": "DK", - "name": "Denmark", + "name": _td("Denmark"), "prefix": "45", }, { "iso2": "DJ", - "name": "Djibouti", + "name": _td("Djibouti"), "prefix": "253", }, { "iso2": "DM", - "name": "Dominica", + "name": _td("Dominica"), "prefix": "1", }, { "iso2": "DO", - "name": "Dominican Republic", + "name": _td("Dominican Republic"), "prefix": "1", }, { "iso2": "EC", - "name": "Ecuador", + "name": _td("Ecuador"), "prefix": "593", }, { "iso2": "EG", - "name": "Egypt", + "name": _td("Egypt"), "prefix": "20", }, { "iso2": "SV", - "name": "El Salvador", + "name": _td("El Salvador"), "prefix": "503", }, { "iso2": "GQ", - "name": "Equatorial Guinea", + "name": _td("Equatorial Guinea"), "prefix": "240", }, { "iso2": "ER", - "name": "Eritrea", + "name": _td("Eritrea"), "prefix": "291", }, { "iso2": "EE", - "name": "Estonia", + "name": _td("Estonia"), "prefix": "372", }, { "iso2": "ET", - "name": "Ethiopia", + "name": _td("Ethiopia"), "prefix": "251", }, { "iso2": "FK", - "name": "Falkland Islands", + "name": _td("Falkland Islands"), "prefix": "500", }, { "iso2": "FO", - "name": "Faroe Islands", + "name": _td("Faroe Islands"), "prefix": "298", }, { "iso2": "FJ", - "name": "Fiji", + "name": _td("Fiji"), "prefix": "679", }, { "iso2": "FI", - "name": "Finland", + "name": _td("Finland"), "prefix": "358", }, { "iso2": "FR", - "name": "France", + "name": _td("France"), "prefix": "33", }, { "iso2": "GF", - "name": "French Guiana", + "name": _td("French Guiana"), "prefix": "594", }, { "iso2": "PF", - "name": "French Polynesia", + "name": _td("French Polynesia"), "prefix": "689", }, { "iso2": "TF", - "name": "French Southern Territories", + "name": _td("French Southern Territories"), "prefix": "262", }, { "iso2": "GA", - "name": "Gabon", + "name": _td("Gabon"), "prefix": "241", }, { "iso2": "GM", - "name": "Gambia", + "name": _td("Gambia"), "prefix": "220", }, { "iso2": "GE", - "name": "Georgia", + "name": _td("Georgia"), "prefix": "995", }, { "iso2": "DE", - "name": "Germany", + "name": _td("Germany"), "prefix": "49", }, { "iso2": "GH", - "name": "Ghana", + "name": _td("Ghana"), "prefix": "233", }, { "iso2": "GI", - "name": "Gibraltar", + "name": _td("Gibraltar"), "prefix": "350", }, { "iso2": "GR", - "name": "Greece", + "name": _td("Greece"), "prefix": "30", }, { "iso2": "GL", - "name": "Greenland", + "name": _td("Greenland"), "prefix": "299", }, { "iso2": "GD", - "name": "Grenada", + "name": _td("Grenada"), "prefix": "1", }, { "iso2": "GP", - "name": "Guadeloupe", + "name": _td("Guadeloupe"), "prefix": "590", }, { "iso2": "GU", - "name": "Guam", + "name": _td("Guam"), "prefix": "1", }, { "iso2": "GT", - "name": "Guatemala", + "name": _td("Guatemala"), "prefix": "502", }, { "iso2": "GG", - "name": "Guernsey", + "name": _td("Guernsey"), "prefix": "44", }, { "iso2": "GN", - "name": "Guinea", + "name": _td("Guinea"), "prefix": "224", }, { "iso2": "GW", - "name": "Guinea-Bissau", + "name": _td("Guinea-Bissau"), "prefix": "245", }, { "iso2": "GY", - "name": "Guyana", + "name": _td("Guyana"), "prefix": "592", }, { "iso2": "HT", - "name": "Haiti", + "name": _td("Haiti"), "prefix": "509", }, { "iso2": "HM", - "name": "Heard & McDonald Islands", + "name": _td("Heard & McDonald Islands"), "prefix": "672", }, { "iso2": "HN", - "name": "Honduras", + "name": _td("Honduras"), "prefix": "504", }, { "iso2": "HK", - "name": "Hong Kong", + "name": _td("Hong Kong"), "prefix": "852", }, { "iso2": "HU", - "name": "Hungary", + "name": _td("Hungary"), "prefix": "36", }, { "iso2": "IS", - "name": "Iceland", + "name": _td("Iceland"), "prefix": "354", }, { "iso2": "IN", - "name": "India", + "name": _td("India"), "prefix": "91", }, { "iso2": "ID", - "name": "Indonesia", + "name": _td("Indonesia"), "prefix": "62", }, { "iso2": "IR", - "name": "Iran", + "name": _td("Iran"), "prefix": "98", }, { "iso2": "IQ", - "name": "Iraq", + "name": _td("Iraq"), "prefix": "964", }, { "iso2": "IE", - "name": "Ireland", + "name": _td("Ireland"), "prefix": "353", }, { "iso2": "IM", - "name": "Isle of Man", + "name": _td("Isle of Man"), "prefix": "44", }, { "iso2": "IL", - "name": "Israel", + "name": _td("Israel"), "prefix": "972", }, { "iso2": "IT", - "name": "Italy", + "name": _td("Italy"), "prefix": "39", }, { "iso2": "JM", - "name": "Jamaica", + "name": _td("Jamaica"), "prefix": "1", }, { "iso2": "JP", - "name": "Japan", + "name": _td("Japan"), "prefix": "81", }, { "iso2": "JE", - "name": "Jersey", + "name": _td("Jersey"), "prefix": "44", }, { "iso2": "JO", - "name": "Jordan", + "name": _td("Jordan"), "prefix": "962", }, { "iso2": "KZ", - "name": "Kazakhstan", + "name": _td("Kazakhstan"), "prefix": "7", }, { "iso2": "KE", - "name": "Kenya", + "name": _td("Kenya"), "prefix": "254", }, { "iso2": "KI", - "name": "Kiribati", + "name": _td("Kiribati"), "prefix": "686", }, { "iso2": "XK", - "name": "Kosovo", + "name": _td("Kosovo"), "prefix": "383", }, { "iso2": "KW", - "name": "Kuwait", + "name": _td("Kuwait"), "prefix": "965", }, { "iso2": "KG", - "name": "Kyrgyzstan", + "name": _td("Kyrgyzstan"), "prefix": "996", }, { "iso2": "LA", - "name": "Laos", + "name": _td("Laos"), "prefix": "856", }, { "iso2": "LV", - "name": "Latvia", + "name": _td("Latvia"), "prefix": "371", }, { "iso2": "LB", - "name": "Lebanon", + "name": _td("Lebanon"), "prefix": "961", }, { "iso2": "LS", - "name": "Lesotho", + "name": _td("Lesotho"), "prefix": "266", }, { "iso2": "LR", - "name": "Liberia", + "name": _td("Liberia"), "prefix": "231", }, { "iso2": "LY", - "name": "Libya", + "name": _td("Libya"), "prefix": "218", }, { "iso2": "LI", - "name": "Liechtenstein", + "name": _td("Liechtenstein"), "prefix": "423", }, { "iso2": "LT", - "name": "Lithuania", + "name": _td("Lithuania"), "prefix": "370", }, { "iso2": "LU", - "name": "Luxembourg", + "name": _td("Luxembourg"), "prefix": "352", }, { "iso2": "MO", - "name": "Macau", + "name": _td("Macau"), "prefix": "853", }, { "iso2": "MK", - "name": "Macedonia", + "name": _td("Macedonia"), "prefix": "389", }, { "iso2": "MG", - "name": "Madagascar", + "name": _td("Madagascar"), "prefix": "261", }, { "iso2": "MW", - "name": "Malawi", + "name": _td("Malawi"), "prefix": "265", }, { "iso2": "MY", - "name": "Malaysia", + "name": _td("Malaysia"), "prefix": "60", }, { "iso2": "MV", - "name": "Maldives", + "name": _td("Maldives"), "prefix": "960", }, { "iso2": "ML", - "name": "Mali", + "name": _td("Mali"), "prefix": "223", }, { "iso2": "MT", - "name": "Malta", + "name": _td("Malta"), "prefix": "356", }, { "iso2": "MH", - "name": "Marshall Islands", + "name": _td("Marshall Islands"), "prefix": "692", }, { "iso2": "MQ", - "name": "Martinique", + "name": _td("Martinique"), "prefix": "596", }, { "iso2": "MR", - "name": "Mauritania", + "name": _td("Mauritania"), "prefix": "222", }, { "iso2": "MU", - "name": "Mauritius", + "name": _td("Mauritius"), "prefix": "230", }, { "iso2": "YT", - "name": "Mayotte", + "name": _td("Mayotte"), "prefix": "262", }, { "iso2": "MX", - "name": "Mexico", + "name": _td("Mexico"), "prefix": "52", }, { "iso2": "FM", - "name": "Micronesia", + "name": _td("Micronesia"), "prefix": "691", }, { "iso2": "MD", - "name": "Moldova", + "name": _td("Moldova"), "prefix": "373", }, { "iso2": "MC", - "name": "Monaco", + "name": _td("Monaco"), "prefix": "377", }, { "iso2": "MN", - "name": "Mongolia", + "name": _td("Mongolia"), "prefix": "976", }, { "iso2": "ME", - "name": "Montenegro", + "name": _td("Montenegro"), "prefix": "382", }, { "iso2": "MS", - "name": "Montserrat", + "name": _td("Montserrat"), "prefix": "1", }, { "iso2": "MA", - "name": "Morocco", + "name": _td("Morocco"), "prefix": "212", }, { "iso2": "MZ", - "name": "Mozambique", + "name": _td("Mozambique"), "prefix": "258", }, { "iso2": "MM", - "name": "Myanmar", + "name": _td("Myanmar"), "prefix": "95", }, { "iso2": "NA", - "name": "Namibia", + "name": _td("Namibia"), "prefix": "264", }, { "iso2": "NR", - "name": "Nauru", + "name": _td("Nauru"), "prefix": "674", }, { "iso2": "NP", - "name": "Nepal", + "name": _td("Nepal"), "prefix": "977", }, { "iso2": "NL", - "name": "Netherlands", + "name": _td("Netherlands"), "prefix": "31", }, { "iso2": "NC", - "name": "New Caledonia", + "name": _td("New Caledonia"), "prefix": "687", }, { "iso2": "NZ", - "name": "New Zealand", + "name": _td("New Zealand"), "prefix": "64", }, { "iso2": "NI", - "name": "Nicaragua", + "name": _td("Nicaragua"), "prefix": "505", }, { "iso2": "NE", - "name": "Niger", + "name": _td("Niger"), "prefix": "227", }, { "iso2": "NG", - "name": "Nigeria", + "name": _td("Nigeria"), "prefix": "234", }, { "iso2": "NU", - "name": "Niue", + "name": _td("Niue"), "prefix": "683", }, { "iso2": "NF", - "name": "Norfolk Island", + "name": _td("Norfolk Island"), "prefix": "672", }, { "iso2": "KP", - "name": "North Korea", + "name": _td("North Korea"), "prefix": "850", }, { "iso2": "MP", - "name": "Northern Mariana Islands", + "name": _td("Northern Mariana Islands"), "prefix": "1", }, { "iso2": "NO", - "name": "Norway", + "name": _td("Norway"), "prefix": "47", }, { "iso2": "OM", - "name": "Oman", + "name": _td("Oman"), "prefix": "968", }, { "iso2": "PK", - "name": "Pakistan", + "name": _td("Pakistan"), "prefix": "92", }, { "iso2": "PW", - "name": "Palau", + "name": _td("Palau"), "prefix": "680", }, { "iso2": "PS", - "name": "Palestine", + "name": _td("Palestine"), "prefix": "970", }, { "iso2": "PA", - "name": "Panama", + "name": _td("Panama"), "prefix": "507", }, { "iso2": "PG", - "name": "Papua New Guinea", + "name": _td("Papua New Guinea"), "prefix": "675", }, { "iso2": "PY", - "name": "Paraguay", + "name": _td("Paraguay"), "prefix": "595", }, { "iso2": "PE", - "name": "Peru", + "name": _td("Peru"), "prefix": "51", }, { "iso2": "PH", - "name": "Philippines", + "name": _td("Philippines"), "prefix": "63", }, { "iso2": "PN", - "name": "Pitcairn Islands", + "name": _td("Pitcairn Islands"), "prefix": "870", }, { "iso2": "PL", - "name": "Poland", + "name": _td("Poland"), "prefix": "48", }, { "iso2": "PT", - "name": "Portugal", + "name": _td("Portugal"), "prefix": "351", }, { "iso2": "PR", - "name": "Puerto Rico", + "name": _td("Puerto Rico"), "prefix": "1", }, { "iso2": "QA", - "name": "Qatar", + "name": _td("Qatar"), "prefix": "974", }, { "iso2": "RO", - "name": "Romania", + "name": _td("Romania"), "prefix": "40", }, { "iso2": "RU", - "name": "Russia", + "name": _td("Russia"), "prefix": "7", }, { "iso2": "RW", - "name": "Rwanda", + "name": _td("Rwanda"), "prefix": "250", }, { "iso2": "RE", - "name": "R\u00e9union", + "name": _td("R\u00e9union"), "prefix": "262", }, { "iso2": "WS", - "name": "Samoa", + "name": _td("Samoa"), "prefix": "685", }, { "iso2": "SM", - "name": "San Marino", + "name": _td("San Marino"), "prefix": "378", }, { "iso2": "SA", - "name": "Saudi Arabia", + "name": _td("Saudi Arabia"), "prefix": "966", }, { "iso2": "SN", - "name": "Senegal", + "name": _td("Senegal"), "prefix": "221", }, { "iso2": "RS", - "name": "Serbia", + "name": _td("Serbia"), "prefix": "381 p", }, { "iso2": "SC", - "name": "Seychelles", + "name": _td("Seychelles"), "prefix": "248", }, { "iso2": "SL", - "name": "Sierra Leone", + "name": _td("Sierra Leone"), "prefix": "232", }, { "iso2": "SG", - "name": "Singapore", + "name": _td("Singapore"), "prefix": "65", }, { "iso2": "SX", - "name": "Sint Maarten", + "name": _td("Sint Maarten"), "prefix": "1", }, { "iso2": "SK", - "name": "Slovakia", + "name": _td("Slovakia"), "prefix": "421", }, { "iso2": "SI", - "name": "Slovenia", + "name": _td("Slovenia"), "prefix": "386", }, { "iso2": "SB", - "name": "Solomon Islands", + "name": _td("Solomon Islands"), "prefix": "677", }, { "iso2": "SO", - "name": "Somalia", + "name": _td("Somalia"), "prefix": "252", }, { "iso2": "ZA", - "name": "South Africa", + "name": _td("South Africa"), "prefix": "27", }, { "iso2": "GS", - "name": "South Georgia & South Sandwich Islands", + "name": _td("South Georgia & South Sandwich Islands"), "prefix": "500", }, { "iso2": "KR", - "name": "South Korea", + "name": _td("South Korea"), "prefix": "82", }, { "iso2": "SS", - "name": "South Sudan", + "name": _td("South Sudan"), "prefix": "211", }, { "iso2": "ES", - "name": "Spain", + "name": _td("Spain"), "prefix": "34", }, { "iso2": "LK", - "name": "Sri Lanka", + "name": _td("Sri Lanka"), "prefix": "94", }, { "iso2": "BL", - "name": "St. Barth\u00e9lemy", + "name": _td("St. Barth\u00e9lemy"), "prefix": "590", }, { "iso2": "SH", - "name": "St. Helena", + "name": _td("St. Helena"), "prefix": "290 n", }, { "iso2": "KN", - "name": "St. Kitts & Nevis", + "name": _td("St. Kitts & Nevis"), "prefix": "1", }, { "iso2": "LC", - "name": "St. Lucia", + "name": _td("St. Lucia"), "prefix": "1", }, { "iso2": "MF", - "name": "St. Martin", + "name": _td("St. Martin"), "prefix": "590", }, { "iso2": "PM", - "name": "St. Pierre & Miquelon", + "name": _td("St. Pierre & Miquelon"), "prefix": "508", }, { "iso2": "VC", - "name": "St. Vincent & Grenadines", + "name": _td("St. Vincent & Grenadines"), "prefix": "1", }, { "iso2": "SD", - "name": "Sudan", + "name": _td("Sudan"), "prefix": "249", }, { "iso2": "SR", - "name": "Suriname", + "name": _td("Suriname"), "prefix": "597", }, { "iso2": "SJ", - "name": "Svalbard & Jan Mayen", + "name": _td("Svalbard & Jan Mayen"), "prefix": "47", }, { "iso2": "SZ", - "name": "Swaziland", + "name": _td("Swaziland"), "prefix": "268", }, { "iso2": "SE", - "name": "Sweden", + "name": _td("Sweden"), "prefix": "46", }, { "iso2": "CH", - "name": "Switzerland", + "name": _td("Switzerland"), "prefix": "41", }, { "iso2": "SY", - "name": "Syria", + "name": _td("Syria"), "prefix": "963", }, { "iso2": "ST", - "name": "S\u00e3o Tom\u00e9 & Pr\u00edncipe", + "name": _td("S\u00e3o Tom\u00e9 & Pr\u00edncipe"), "prefix": "239", }, { "iso2": "TW", - "name": "Taiwan", + "name": _td("Taiwan"), "prefix": "886", }, { "iso2": "TJ", - "name": "Tajikistan", + "name": _td("Tajikistan"), "prefix": "992", }, { "iso2": "TZ", - "name": "Tanzania", + "name": _td("Tanzania"), "prefix": "255", }, { "iso2": "TH", - "name": "Thailand", + "name": _td("Thailand"), "prefix": "66", }, { "iso2": "TL", - "name": "Timor-Leste", + "name": _td("Timor-Leste"), "prefix": "670", }, { "iso2": "TG", - "name": "Togo", + "name": _td("Togo"), "prefix": "228", }, { "iso2": "TK", - "name": "Tokelau", + "name": _td("Tokelau"), "prefix": "690", }, { "iso2": "TO", - "name": "Tonga", + "name": _td("Tonga"), "prefix": "676", }, { "iso2": "TT", - "name": "Trinidad & Tobago", + "name": _td("Trinidad & Tobago"), "prefix": "1", }, { "iso2": "TN", - "name": "Tunisia", + "name": _td("Tunisia"), "prefix": "216", }, { "iso2": "TR", - "name": "Turkey", + "name": _td("Turkey"), "prefix": "90", }, { "iso2": "TM", - "name": "Turkmenistan", + "name": _td("Turkmenistan"), "prefix": "993", }, { "iso2": "TC", - "name": "Turks & Caicos Islands", + "name": _td("Turks & Caicos Islands"), "prefix": "1", }, { "iso2": "TV", - "name": "Tuvalu", + "name": _td("Tuvalu"), "prefix": "688", }, { "iso2": "VI", - "name": "U.S. Virgin Islands", + "name": _td("U.S. Virgin Islands"), "prefix": "1", }, { "iso2": "UG", - "name": "Uganda", + "name": _td("Uganda"), "prefix": "256", }, { "iso2": "UA", - "name": "Ukraine", + "name": _td("Ukraine"), "prefix": "380", }, { "iso2": "AE", - "name": "United Arab Emirates", + "name": _td("United Arab Emirates"), "prefix": "971", }, { "iso2": "UY", - "name": "Uruguay", + "name": _td("Uruguay"), "prefix": "598", }, { "iso2": "UZ", - "name": "Uzbekistan", + "name": _td("Uzbekistan"), "prefix": "998", }, { "iso2": "VU", - "name": "Vanuatu", + "name": _td("Vanuatu"), "prefix": "678", }, { "iso2": "VA", - "name": "Vatican City", + "name": _td("Vatican City"), "prefix": "39", }, { "iso2": "VE", - "name": "Venezuela", + "name": _td("Venezuela"), "prefix": "58", }, { "iso2": "VN", - "name": "Vietnam", + "name": _td("Vietnam"), "prefix": "84", }, { "iso2": "WF", - "name": "Wallis & Futuna", + "name": _td("Wallis & Futuna"), "prefix": "681", }, { "iso2": "EH", - "name": "Western Sahara", + "name": _td("Western Sahara"), "prefix": "212", }, { "iso2": "YE", - "name": "Yemen", + "name": _td("Yemen"), "prefix": "967", }, { "iso2": "ZM", - "name": "Zambia", + "name": _td("Zambia"), "prefix": "260", }, { "iso2": "ZW", - "name": "Zimbabwe", + "name": _td("Zimbabwe"), "prefix": "263", }, ]; From 462be2b6a04112e11814bd219c988085bd88bfb1 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 9 Nov 2020 13:45:33 +0000 Subject: [PATCH 26/62] Close context menu when user clicks the Home button --- src/components/structures/UserMenu.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/structures/UserMenu.tsx b/src/components/structures/UserMenu.tsx index 4847d41fa8..ed5e0c2e55 100644 --- a/src/components/structures/UserMenu.tsx +++ b/src/components/structures/UserMenu.tsx @@ -203,6 +203,7 @@ export default class UserMenu extends React.Component { ev.stopPropagation(); defaultDispatcher.dispatch({action: 'view_home_page'}); + this.setState({contextMenuPosition: null}); // also close the menu }; private onCommunitySettingsClick = (ev: ButtonEvent) => { From 27d276715e73822f4f44a84e1191bd9fcf5c4649 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 9 Nov 2020 13:53:28 +0000 Subject: [PATCH 27/62] Fix Skeleton UI showing up when not intended. --- src/components/views/rooms/RoomList.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/RoomList.tsx b/src/components/views/rooms/RoomList.tsx index 62fd70e1ec..b099beee08 100644 --- a/src/components/views/rooms/RoomList.tsx +++ b/src/components/views/rooms/RoomList.tsx @@ -332,7 +332,7 @@ export default class RoomList extends React.PureComponent { return p; }, [] as TagID[]); - const showSkeleton = tagOrder.every(tag => !this.state.sublists[tag]?.length); + const showSkeleton = Object.values(RoomListStore.instance.unfilteredLists).every(list => !list?.length); for (const orderedTagId of tagOrder) { const orderedRooms = this.state.sublists[orderedTagId] || []; From 36ef9ec341cb352b1e0a04ed45c954324c21690c Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 9 Nov 2020 14:21:45 +0000 Subject: [PATCH 28/62] Update styling of the Analytics toast to try and decrease number of users blindly pressing No --- res/css/_components.scss | 1 + src/components/structures/ToastContainer.tsx | 4 ++-- src/i18n/strings/en_EN.json | 3 +-- src/stores/ToastStore.ts | 1 + src/toasts/AnalyticsToast.tsx | 3 ++- 5 files changed, 7 insertions(+), 5 deletions(-) diff --git a/res/css/_components.scss b/res/css/_components.scss index 37d0e0d286..ae4b4b23fe 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -225,6 +225,7 @@ @import "./views/settings/tabs/user/_SecurityUserSettingsTab.scss"; @import "./views/settings/tabs/user/_VoiceUserSettingsTab.scss"; @import "./views/terms/_InlineTermsAgreement.scss"; +@import "./views/toasts/_AnalyticsToast.scss"; @import "./views/toasts/_NonUrgentEchoFailureToast.scss"; @import "./views/verification/_VerificationShowSas.scss"; @import "./views/voip/_CallContainer.scss"; diff --git a/src/components/structures/ToastContainer.tsx b/src/components/structures/ToastContainer.tsx index 84473031fa..513cca82c3 100644 --- a/src/components/structures/ToastContainer.tsx +++ b/src/components/structures/ToastContainer.tsx @@ -55,11 +55,11 @@ export default class ToastContainer extends React.Component<{}, IState> { let toast; if (totalCount !== 0) { const topToast = this.state.toasts[0]; - const {title, icon, key, component, props} = topToast; + const {title, icon, key, component, className, props} = topToast; const toastClasses = classNames("mx_Toast_toast", { "mx_Toast_hasIcon": icon, [`mx_Toast_icon_${icon}`]: icon, - }); + }, className); let countIndicator; if (isStacked || this.state.countSeen > 0) { diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index a56e22e5fc..bf07193acf 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -397,7 +397,7 @@ "Unknown App": "Unknown App", "Help us improve %(brand)s": "Help us improve %(brand)s", "Send anonymous usage data which helps us improve %(brand)s. This will use a cookie.": "Send anonymous usage data which helps us improve %(brand)s. This will use a cookie.", - "I want to help": "I want to help", + "Yes": "Yes", "No": "No", "Review where you’re logged in": "Review where you’re logged in", "Verify all your sessions to ensure your account & messages are safe": "Verify all your sessions to ensure your account & messages are safe", @@ -1355,7 +1355,6 @@ "Verify by emoji": "Verify by emoji", "Almost there! Is your other session showing the same shield?": "Almost there! Is your other session showing the same shield?", "Almost there! Is %(displayName)s showing the same shield?": "Almost there! Is %(displayName)s showing the same shield?", - "Yes": "Yes", "Verify all users in a room to ensure it's secure.": "Verify all users in a room to ensure it's secure.", "In encrypted rooms, verify all users to ensure it’s secure.": "In encrypted rooms, verify all users to ensure it’s secure.", "You've successfully verified your device!": "You've successfully verified your device!", diff --git a/src/stores/ToastStore.ts b/src/stores/ToastStore.ts index 038aebc7c9..850c3cb026 100644 --- a/src/stores/ToastStore.ts +++ b/src/stores/ToastStore.ts @@ -25,6 +25,7 @@ export interface IToast { title: string; icon?: string; component: C; + className?: string; props?: Omit, "toastKey">; // toastKey is injected by ToastContainer } diff --git a/src/toasts/AnalyticsToast.tsx b/src/toasts/AnalyticsToast.tsx index e0eda5fa48..5a7737b1a6 100644 --- a/src/toasts/AnalyticsToast.tsx +++ b/src/toasts/AnalyticsToast.tsx @@ -64,12 +64,13 @@ export const showToast = (policyUrl?: string) => { ) : sub, }, ), - acceptLabel: _t("I want to help"), + acceptLabel: _t("Yes"), onAccept, rejectLabel: _t("No"), onReject, }, component: GenericToast, + className: "mx_AnalyticsToast", priority: 10, }); }; From 5dd0766d35ff658dac984479bdbe93f12acb1d6c Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 9 Nov 2020 14:36:12 +0000 Subject: [PATCH 29/62] Add missing scss file --- res/css/views/toasts/_AnalyticsToast.scss | 27 +++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 res/css/views/toasts/_AnalyticsToast.scss diff --git a/res/css/views/toasts/_AnalyticsToast.scss b/res/css/views/toasts/_AnalyticsToast.scss new file mode 100644 index 0000000000..fdbe7f1c76 --- /dev/null +++ b/res/css/views/toasts/_AnalyticsToast.scss @@ -0,0 +1,27 @@ +/* +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. +*/ + +.mx_AnalyticsToast { + .mx_AccessibleButton_kind_danger { + background: none; + color: $accent-color; + } + + .mx_AccessibleButton_kind_primary { + background: $accent-color; + color: #ffffff; + } +} From 5bed02b6e82ecc5e4b63649975d1a2c469f1477b Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 9 Nov 2020 15:50:35 +0000 Subject: [PATCH 30/62] Skip e2ee warn logout prompt if user has no megolm sessions to lose --- src/components/structures/UserMenu.tsx | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/components/structures/UserMenu.tsx b/src/components/structures/UserMenu.tsx index 4847d41fa8..54e1ba811a 100644 --- a/src/components/structures/UserMenu.tsx +++ b/src/components/structures/UserMenu.tsx @@ -190,11 +190,18 @@ export default class UserMenu extends React.Component { this.setState({contextMenuPosition: null}); // also close the menu }; - private onSignOutClick = (ev: ButtonEvent) => { + private onSignOutClick = async (ev: ButtonEvent) => { ev.preventDefault(); ev.stopPropagation(); - Modal.createTrackedDialog('Logout from LeftPanel', '', LogoutDialog); + const cli = MatrixClientPeg.get(); + if (!cli || !cli.isCryptoEnabled() || !(await cli.exportRoomKeys())?.length) { + // log out without user prompt if they have no local megolm sessions + dis.dispatch({action: 'logout'}); + } else { + Modal.createTrackedDialog('Logout from LeftPanel', '', LogoutDialog); + } + this.setState({contextMenuPosition: null}); // also close the menu }; From 6f3cdadcdb09843136438da19b017a97bb1fac97 Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Mon, 9 Nov 2020 16:26:36 +0000 Subject: [PATCH 31/62] Upgrade matrix-js-sdk to 9.1.0 --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index a4beaa696b..022aea69f2 100644 --- a/package.json +++ b/package.json @@ -78,7 +78,7 @@ "is-ip": "^2.0.0", "linkifyjs": "^2.1.9", "lodash": "^4.17.19", - "matrix-js-sdk": "9.1.0-rc.1", + "matrix-js-sdk": "9.1.0", "matrix-widget-api": "^0.1.0-beta.5", "minimist": "^1.2.5", "pako": "^1.0.11", diff --git a/yarn.lock b/yarn.lock index 04ceb32110..2f87826697 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6505,10 +6505,10 @@ mathml-tag-names@^2.0.1: resolved "https://registry.yarnpkg.com/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz#4ddadd67308e780cf16a47685878ee27b736a0a3" integrity sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg== -matrix-js-sdk@9.1.0-rc.1: - version "9.1.0-rc.1" - resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-9.1.0-rc.1.tgz#cbdad0d272152a27bb32fe74e02ad069717d2554" - integrity sha512-Y0ftnH3HtMaNPFyfYqbrhS6aZkdQxxeluErFmOmpMTRGuMiIGrqhdWP8Z/MCZMRP4NPAh/dN2eW0vgqH9ij52A== +matrix-js-sdk@9.1.0: + version "9.1.0" + resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-9.1.0.tgz#51a3240f6545c0ae42ca0db0c579d17476b0ca94" + integrity sha512-4swrPb93qF2lid9mqzCDPMIvUlgS/MIUVxxD0EfNQoapdekFtnf7pMimYs+lMpRk3VDfa01FEsUu1k0q8O+Utw== dependencies: "@babel/runtime" "^7.11.2" another-json "^0.2.0" From e8d18b8b119154a51cc5f7c8fb3ee4027fc50ffc Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Mon, 9 Nov 2020 16:31:47 +0000 Subject: [PATCH 32/62] Prepare changelog for v3.8.0 --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 035225b18a..13c5ba81ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +Changes in [3.8.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.8.0) (2020-11-09) +=================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.8.0-rc.1...v3.8.0) + + * Upgrade JS SDK to 9.1.0 + Changes in [3.8.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.8.0-rc.1) (2020-11-04) ============================================================================================================= [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.7.1...v3.8.0-rc.1) From 8edbc7c654c06f0c38cc8b25661dd702ec4273db Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Mon, 9 Nov 2020 16:31:48 +0000 Subject: [PATCH 33/62] v3.8.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 022aea69f2..6fd1489017 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "3.8.0-rc.1", + "version": "3.8.0", "description": "SDK for matrix.org using React", "author": "matrix.org", "repository": { From 46e01196f0f4e0f291716de669d5aaad6b270c7d Mon Sep 17 00:00:00 2001 From: RiotRobot Date: Mon, 9 Nov 2020 16:33:48 +0000 Subject: [PATCH 34/62] Reset matrix-js-sdk back to develop branch --- package.json | 2 +- yarn.lock | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 6fd1489017..048edea3fd 100644 --- a/package.json +++ b/package.json @@ -78,7 +78,7 @@ "is-ip": "^2.0.0", "linkifyjs": "^2.1.9", "lodash": "^4.17.19", - "matrix-js-sdk": "9.1.0", + "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop", "matrix-widget-api": "^0.1.0-beta.5", "minimist": "^1.2.5", "pako": "^1.0.11", diff --git a/yarn.lock b/yarn.lock index 2f87826697..23a32280c1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6505,10 +6505,9 @@ mathml-tag-names@^2.0.1: resolved "https://registry.yarnpkg.com/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz#4ddadd67308e780cf16a47685878ee27b736a0a3" integrity sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg== -matrix-js-sdk@9.1.0: +"matrix-js-sdk@github:matrix-org/matrix-js-sdk#develop": version "9.1.0" - resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-9.1.0.tgz#51a3240f6545c0ae42ca0db0c579d17476b0ca94" - integrity sha512-4swrPb93qF2lid9mqzCDPMIvUlgS/MIUVxxD0EfNQoapdekFtnf7pMimYs+lMpRk3VDfa01FEsUu1k0q8O+Utw== + resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/5ac00e346593f29f324b3af8e322928a6e1c427a" dependencies: "@babel/runtime" "^7.11.2" another-json "^0.2.0" From a5d827c44608a41f2cc9cc39213e852059505493 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 10 Nov 2020 01:22:30 +0000 Subject: [PATCH 35/62] Update src/components/views/rooms/RoomList.tsx Co-authored-by: Travis Ralston --- src/components/views/rooms/RoomList.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/views/rooms/RoomList.tsx b/src/components/views/rooms/RoomList.tsx index b099beee08..d952c137cd 100644 --- a/src/components/views/rooms/RoomList.tsx +++ b/src/components/views/rooms/RoomList.tsx @@ -332,6 +332,7 @@ export default class RoomList extends React.PureComponent { return p; }, [] as TagID[]); + // show a skeleton UI if the user is in no rooms const showSkeleton = Object.values(RoomListStore.instance.unfilteredLists).every(list => !list?.length); for (const orderedTagId of tagOrder) { From 65b8f27b260210bd7cb305ca03f8f274a05b3d1e Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 9 Nov 2020 21:14:20 -0700 Subject: [PATCH 36/62] Disable buttons when required by MSC2790 --- .../views/dialogs/ModalWidgetDialog.tsx | 36 ++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/src/components/views/dialogs/ModalWidgetDialog.tsx b/src/components/views/dialogs/ModalWidgetDialog.tsx index 6ce3230a7a..c8a736e8a6 100644 --- a/src/components/views/dialogs/ModalWidgetDialog.tsx +++ b/src/components/views/dialogs/ModalWidgetDialog.tsx @@ -23,6 +23,11 @@ import { IModalWidgetCloseRequest, IModalWidgetOpenRequestData, IModalWidgetReturnData, + ISetModalButtonEnabledActionRequest, + IWidgetApiAcknowledgeResponseData, + IWidgetApiErrorResponseData, + BuiltInModalButtonID, + ModalButtonID, ModalButtonKind, Widget, WidgetApiFromWidgetAction, @@ -31,6 +36,7 @@ import {StopGapWidgetDriver} from "../../../stores/widgets/StopGapWidgetDriver"; import {MatrixClientPeg} from "../../../MatrixClientPeg"; import RoomViewStore from "../../../stores/RoomViewStore"; import {OwnProfileStore} from "../../../stores/OwnProfileStore"; +import { arrayFastClone } from "../../../utils/arrays"; interface IProps { widgetDefinition: IModalWidgetOpenRequestData; @@ -40,15 +46,19 @@ interface IProps { interface IState { messaging?: ClientWidgetApi; + disabledButtonIds: ModalButtonID[]; } const MAX_BUTTONS = 3; export default class ModalWidgetDialog extends React.PureComponent { private readonly widget: Widget; + private readonly possibleButtons: ModalButtonID[]; private appFrame: React.RefObject = React.createRef(); - state: IState = {}; + state: IState = { + disabledButtonIds: [], + }; constructor(props) { super(props); @@ -58,6 +68,7 @@ export default class ModalWidgetDialog extends React.PureComponent b.id); } public componentDidMount() { @@ -79,12 +90,35 @@ export default class ModalWidgetDialog extends React.PureComponent { this.state.messaging.once("ready", this.onReady); this.state.messaging.on(`action:${WidgetApiFromWidgetAction.CloseModalWidget}`, this.onWidgetClose); + this.state.messaging.on(`action:${WidgetApiFromWidgetAction.SetModalButtonEnabled}`, this.onButtonEnableToggle); }; private onWidgetClose = (ev: CustomEvent) => { this.props.onFinished(true, ev.detail.data); } + private onButtonEnableToggle = (ev: CustomEvent) => { + ev.preventDefault(); + const isClose = ev.detail.data.button === BuiltInModalButtonID.Close; + if (isClose || !this.possibleButtons.includes(ev.detail.data.button)) { + return this.state.messaging.transport.reply(ev.detail, { + error: {message: "Invalid button"}, + } as IWidgetApiErrorResponseData); + } + + let buttonIds: ModalButtonID[]; + if (ev.detail.data.enabled) { + buttonIds = arrayFastClone(this.state.disabledButtonIds).filter(i => i !== ev.detail.data.button); + } else { + // use a set to swap the operation to avoid memory leaky arrays. + const tempSet = new Set(this.state.disabledButtonIds); + tempSet.add(ev.detail.data.button); + buttonIds = Array.from(tempSet); + } + this.setState({disabledButtonIds: buttonIds}); + this.state.messaging.transport.reply(ev.detail, {} as IWidgetApiAcknowledgeResponseData); + }; + public render() { const templated = this.widget.getCompleteUrl({ currentRoomId: RoomViewStore.getRoomId(), From 80bb091cc597aced3354cc5496ec676a97b1eba2 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 10 Nov 2020 10:00:55 +0000 Subject: [PATCH 37/62] Iterate PR --- res/css/views/rooms/_EventTile.scss | 1 + src/components/views/elements/MiniAvatarUploader.tsx | 6 +++--- src/components/views/rooms/EventTile.js | 11 ++++++----- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index 80ede1152d..429ac7ed4b 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -124,6 +124,7 @@ $left-gutter: 64px; .mx_EventTile_line { margin-right: 0; grid-column: 1 / 3; + // override default padding of mx_EventTile_line so that we can be centered padding: 0 !important; } diff --git a/src/components/views/elements/MiniAvatarUploader.tsx b/src/components/views/elements/MiniAvatarUploader.tsx index 903826c3e6..b5e117b42a 100644 --- a/src/components/views/elements/MiniAvatarUploader.tsx +++ b/src/components/views/elements/MiniAvatarUploader.tsx @@ -39,14 +39,14 @@ const MiniAvatarUploader: React.FC = ({ hasAvatar, hasAvatarLabel, noAva useTimeout(() => { setShow(true); - }, 3_000); // show after 3 seconds + }, 3000); // show after 3 seconds useTimeout(() => { setShow(false); - }, 13_000); // hide after being shown for 10 seconds + }, 13000); // hide after being shown for 10 seconds const uploadRef = useRef(); - const label = hasAvatar || busy ? hasAvatarLabel : noAvatarLabel; + const label = (hasAvatar || busy) ? hasAvatarLabel : noAvatarLabel; return Date: Tue, 10 Nov 2020 12:03:50 +0000 Subject: [PATCH 38/62] Change how we expose Role in User Info and hide in DMs --- src/components/views/right_panel/UserInfo.tsx | 134 ++++++------------ src/i18n/strings/en_EN.json | 2 +- 2 files changed, 48 insertions(+), 88 deletions(-) diff --git a/src/components/views/right_panel/UserInfo.tsx b/src/components/views/right_panel/UserInfo.tsx index f8be6327bf..66b689ddb9 100644 --- a/src/components/views/right_panel/UserInfo.tsx +++ b/src/components/views/right_panel/UserInfo.tsx @@ -51,7 +51,6 @@ import BaseCard from "./BaseCard"; import {E2EStatus} from "../../../utils/ShieldUtils"; import ImageView from "../elements/ImageView"; import Spinner from "../elements/Spinner"; -import IconButton from "../elements/IconButton"; import PowerSelector from "../elements/PowerSelector"; import MemberAvatar from "../avatars/MemberAvatar"; import PresenceLabel from "../rooms/PresenceLabel"; @@ -1028,24 +1027,15 @@ const PowerLevelSection: React.FC<{ roomPermissions: IRoomPermissions; powerLevels: IPowerLevelsContent; }> = ({user, room, roomPermissions, powerLevels}) => { - const [isEditing, setEditing] = useState(false); - if (isEditing) { - return ( setEditing(false)} />); + if (roomPermissions.canEdit) { + return (); } else { const powerLevelUsersDefault = powerLevels.users_default || 0; const powerLevel = parseInt(user.powerLevel, 10); - const modifyButton = roomPermissions.canEdit ? - ( setEditing(true)} />) : null; const role = textualPowerLevel(powerLevel, powerLevelUsersDefault); - const label = _t("%(role)s in %(roomName)s", - {role, roomName: room.name}, - {strong: label => {label}}, - ); return (
-
{label}{modifyButton}
+
{role}
); } @@ -1055,20 +1045,15 @@ const PowerLevelEditor: React.FC<{ user: User; room: Room; roomPermissions: IRoomPermissions; - onFinished(): void; -}> = ({user, room, roomPermissions, onFinished}) => { +}> = ({user, room, roomPermissions}) => { const cli = useContext(MatrixClientContext); - const [isUpdating, setIsUpdating] = useState(false); const [selectedPowerLevel, setSelectedPowerLevel] = useState(parseInt(user.powerLevel, 10)); - const [isDirty, setIsDirty] = useState(false); - const onPowerChange = useCallback((powerLevel) => { - setIsDirty(true); - setSelectedPowerLevel(parseInt(powerLevel, 10)); - }, [setSelectedPowerLevel, setIsDirty]); + const onPowerChange = useCallback(async (powerLevelStr: string) => { + const powerLevel = parseInt(powerLevelStr, 10); + setSelectedPowerLevel(powerLevel); - const changePowerLevel = useCallback(async () => { - const _applyPowerChange = (roomId, target, powerLevel, powerLevelEvent) => { + const applyPowerChange = (roomId, target, powerLevel, powerLevelEvent) => { return cli.setPowerLevel(roomId, target, parseInt(powerLevel), powerLevelEvent).then( function() { // NO-OP; rely on the m.room.member event coming down else we could @@ -1084,64 +1069,42 @@ const PowerLevelEditor: React.FC<{ ); }; - try { - if (!isDirty) { - return; - } + const roomId = user.roomId; + const target = user.userId; - setIsUpdating(true); + const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", ""); + if (!powerLevelEvent) return; - const powerLevel = selectedPowerLevel; + const myUserId = cli.getUserId(); + const myPower = powerLevelEvent.getContent().users[myUserId]; + if (myPower && parseInt(myPower) === powerLevel) { + const {finished} = Modal.createTrackedDialog('Promote to PL100 Warning', '', QuestionDialog, { + title: _t("Warning!"), + description: +
+ { _t("You will not be able to undo this change as you are promoting the user " + + "to have the same power level as yourself.") }
+ { _t("Are you sure?") } +
, + button: _t("Continue"), + }); - const roomId = user.roomId; - const target = user.userId; - - const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", ""); - if (!powerLevelEvent) return; - - if (!powerLevelEvent.getContent().users) { - _applyPowerChange(roomId, target, powerLevel, powerLevelEvent); - return; - } - - const myUserId = cli.getUserId(); + const [confirmed] = await finished; + if (!confirmed) return; + } else if (myUserId === target) { // If we are changing our own PL it can only ever be decreasing, which we cannot reverse. - if (myUserId === target) { - try { - if (!(await warnSelfDemote())) return; - } catch (e) { - console.error("Failed to warn about self demotion: ", e); - } - await _applyPowerChange(roomId, target, powerLevel, powerLevelEvent); - return; + try { + if (!(await warnSelfDemote())) return; + } catch (e) { + console.error("Failed to warn about self demotion: ", e); } - - const myPower = powerLevelEvent.getContent().users[myUserId]; - if (parseInt(myPower) === powerLevel) { - const {finished} = Modal.createTrackedDialog('Promote to PL100 Warning', '', QuestionDialog, { - title: _t("Warning!"), - description: -
- { _t("You will not be able to undo this change as you are promoting the user " + - "to have the same power level as yourself.") }
- { _t("Are you sure?") } -
, - button: _t("Continue"), - }); - - const [confirmed] = await finished; - if (!confirmed) return; - } - await _applyPowerChange(roomId, target, powerLevel, powerLevelEvent); - } finally { - onFinished(); } - }, [user.roomId, user.userId, cli, selectedPowerLevel, isDirty, setIsUpdating, onFinished, room]); + + await applyPowerChange(roomId, target, powerLevel, powerLevelEvent); + }, [user.roomId, user.userId, cli, room]); const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", ""); const powerLevelUsersDefault = powerLevelEvent ? powerLevelEvent.getContent().users_default : 0; - const buttonOrSpinner = isUpdating ? : - ; return (
@@ -1151,9 +1114,7 @@ const PowerLevelEditor: React.FC<{ maxValue={roomPermissions.modifyLevelMax} usersDefault={powerLevelUsersDefault} onChange={onPowerChange} - disabled={isUpdating} /> - {buttonOrSpinner}
); }; @@ -1343,13 +1304,17 @@ const BasicUserInfo: React.FC<{ } let memberDetails; - if (room && member.roomId) { - memberDetails = ; + // hide the Roles section for DMs as it doesn't make sense there + if (room && member.roomId && !DMRoomMap.shared().getUserIdForRoomId(member.roomId)) { + memberDetails =
+

{ _t("Role") }

+ +
; } // only display the devices list if our client supports E2E @@ -1419,12 +1384,7 @@ const BasicUserInfo: React.FC<{ ); return - { memberDetails && -
-
- { memberDetails } -
-
} + { memberDetails } { securitySection } %(role)s in %(roomName)s": "%(role)s in %(roomName)s", "Failed to change power level": "Failed to change power level", "You will not be able to undo this change as you are promoting the user to have the same power level as yourself.": "You will not be able to undo this change as you are promoting the user to have the same power level as yourself.", "Are you sure?": "Are you sure?", @@ -1597,6 +1596,7 @@ "Deactivating this user will log them out and prevent them from logging back in. Additionally, they will leave all the rooms they are in. This action cannot be reversed. Are you sure you want to deactivate this user?": "Deactivating this user will log them out and prevent them from logging back in. Additionally, they will leave all the rooms they are in. This action cannot be reversed. Are you sure you want to deactivate this user?", "Deactivate user": "Deactivate user", "Failed to deactivate user": "Failed to deactivate user", + "Role": "Role", "This client does not support end-to-end encryption.": "This client does not support end-to-end encryption.", "Security": "Security", "The session you are trying to verify doesn't support scanning a QR code or emoji verification, which is what %(brand)s supports. Try with a different client.": "The session you are trying to verify doesn't support scanning a QR code or emoji verification, which is what %(brand)s supports. Try with a different client.", From 5fb86f74d7fe15dec4b0ec38ae37484d40889a06 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 10 Nov 2020 12:06:56 +0000 Subject: [PATCH 39/62] Remove unused IconButton --- res/css/_components.scss | 1 - res/css/views/elements/_IconButton.scss | 55 --------------------- res/css/views/right_panel/_UserInfo.scss | 14 ------ src/components/views/elements/IconButton.js | 34 ------------- 4 files changed, 104 deletions(-) delete mode 100644 res/css/views/elements/_IconButton.scss delete mode 100644 src/components/views/elements/IconButton.js diff --git a/res/css/_components.scss b/res/css/_components.scss index 0b46df9bd8..ca4db2cdcb 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -110,7 +110,6 @@ @import "./views/elements/_EventListSummary.scss"; @import "./views/elements/_Field.scss"; @import "./views/elements/_FormButton.scss"; -@import "./views/elements/_IconButton.scss"; @import "./views/elements/_ImageView.scss"; @import "./views/elements/_InfoTooltip.scss"; @import "./views/elements/_InlineSpinner.scss"; diff --git a/res/css/views/elements/_IconButton.scss b/res/css/views/elements/_IconButton.scss deleted file mode 100644 index d8ebbeb65e..0000000000 --- a/res/css/views/elements/_IconButton.scss +++ /dev/null @@ -1,55 +0,0 @@ -/* -Copyright 2019 The Matrix.org Foundation C.I.C. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -.mx_IconButton { - width: 32px; - height: 32px; - border-radius: 100%; - background-color: $accent-bg-color; - // don't shrink or grow if in a flex container - flex: 0 0 auto; - - &.mx_AccessibleButton_disabled { - background-color: none; - - &::before { - background-color: lightgrey; - } - } - - &:hover { - opacity: 90%; - } - - &::before { - content: ""; - display: block; - width: 100%; - height: 100%; - mask-repeat: no-repeat; - mask-position: center; - mask-size: 55%; - background-color: $accent-color; - } - - &.mx_IconButton_icon_check::before { - mask-image: url('$(res)/img/feather-customised/check.svg'); - } - - &.mx_IconButton_icon_edit::before { - mask-image: url('$(res)/img/feather-customised/edit.svg'); - } -} diff --git a/res/css/views/right_panel/_UserInfo.scss b/res/css/views/right_panel/_UserInfo.scss index f20c9b7868..87420ae4e7 100644 --- a/res/css/views/right_panel/_UserInfo.scss +++ b/res/css/views/right_panel/_UserInfo.scss @@ -173,26 +173,12 @@ limitations under the License. margin: 6px 0; - .mx_IconButton, .mx_Spinner { - margin-left: 20px; - width: 16px; - height: 16px; - - &::before { - mask-size: 80%; - } - } - .mx_UserInfo_roleDescription { display: flex; justify-content: center; align-items: center; // try to make it the same height as the dropdown margin: 11px 0 12px 0; - - .mx_IconButton { - margin-left: 6px; - } } .mx_Field { diff --git a/src/components/views/elements/IconButton.js b/src/components/views/elements/IconButton.js deleted file mode 100644 index ef7b4a8399..0000000000 --- a/src/components/views/elements/IconButton.js +++ /dev/null @@ -1,34 +0,0 @@ -/* -Copyright 2019 The Matrix.org Foundation C.I.C. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import React from 'react'; -import PropTypes from 'prop-types'; -import AccessibleButton from "./AccessibleButton"; - -export default function IconButton(props) { - const {icon, className, ...restProps} = props; - - let newClassName = (className || "") + " mx_IconButton"; - newClassName = newClassName + " mx_IconButton_icon_" + icon; - - const allProps = Object.assign({}, restProps, {className: newClassName}); - - return React.createElement(AccessibleButton, allProps); -} - -IconButton.propTypes = Object.assign({ - icon: PropTypes.string, -}, AccessibleButton.propTypes); From dc304d4ad83fdad44a1de36a1cacbf4fa0419298 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 10 Nov 2020 15:55:07 +0000 Subject: [PATCH 40/62] Fix drag drop file to upload for Safari --- src/components/structures/RoomView.tsx | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index 1c2bf3a000..c19272fb6a 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -1147,16 +1147,9 @@ export default class RoomView extends React.Component { ev.dataTransfer.dropEffect = 'none'; - const items = [...ev.dataTransfer.items]; - if (items.length >= 1) { - const isDraggingFiles = items.every(function(item) { - return item.kind == 'file'; - }); - - if (isDraggingFiles) { - this.setState({ draggingFile: true }); - ev.dataTransfer.dropEffect = 'copy'; - } + if (ev.dataTransfer.types.includes("Files") || ev.dataTransfer.types.includes("application/x-moz-file")) { + this.setState({ draggingFile: true }); + ev.dataTransfer.dropEffect = 'copy'; } }; From 36aa80fb64ce8b281fcffae98d2eba282da03798 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Tue, 10 Nov 2020 18:29:28 +0000 Subject: [PATCH 41/62] Update src/components/views/elements/Validation.tsx Co-authored-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/elements/Validation.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/elements/Validation.tsx b/src/components/views/elements/Validation.tsx index c45a745918..31f7c866b1 100644 --- a/src/components/views/elements/Validation.tsx +++ b/src/components/views/elements/Validation.tsx @@ -33,7 +33,7 @@ interface IRule { interface IArgs { rules: IRule[]; description(this: T, derivedData: D): React.ReactChild; - hideDescriptionIfValid?: Boolean; + hideDescriptionIfValid?: boolean; deriveData?(data: Data): Promise; } From 979faf23e85bdf1c2d4ec05cbdff2428a1d4e30e Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 11 Nov 2020 16:49:51 +0000 Subject: [PATCH 42/62] Fix poorly i18n'd string --- src/components/views/settings/EventIndexPanel.js | 12 +++++++----- src/i18n/strings/en_EN.json | 5 ++--- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/components/views/settings/EventIndexPanel.js b/src/components/views/settings/EventIndexPanel.js index 8598a2a966..1fe18cb207 100644 --- a/src/components/views/settings/EventIndexPanel.js +++ b/src/components/views/settings/EventIndexPanel.js @@ -129,11 +129,13 @@ export default class EventIndexPanel extends React.Component { eventIndexingSettings = (
- {_t( "Securely cache encrypted messages locally for them " + - "to appear in search results, using ") - } {formatBytes(this.state.eventIndexSize, 0)} - {_t( " to store messages from ")} - {formatCountLong(this.state.roomCount)} {_t("rooms.")} + {_t("Securely cache encrypted messages locally for them " + + "to appear in search results, using %(size)s to store messages from %(count)s rooms.", + { + size: formatBytes(this.state.eventIndexSize, 0), + count: formatCountLong(this.state.roomCount), + }, + )}
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 830d3cdee4..110c51163f 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -935,9 +935,8 @@ "Failed to set display name": "Failed to set display name", "Encryption": "Encryption", "Individually verify each session used by a user to mark it as trusted, not trusting cross-signed devices.": "Individually verify each session used by a user to mark it as trusted, not trusting cross-signed devices.", - "Securely cache encrypted messages locally for them to appear in search results, using ": "Securely cache encrypted messages locally for them to appear in search results, using ", - " to store messages from ": " to store messages from ", - "rooms.": "rooms.", + "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(count)s rooms.|one": "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(count)s room.", + "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(count)s rooms.|other": "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(count)s rooms.", "Manage": "Manage", "Securely cache encrypted messages locally for them to appear in search results.": "Securely cache encrypted messages locally for them to appear in search results.", "%(brand)s is missing some components required for securely caching encrypted messages locally. If you'd like to experiment with this feature, build a custom %(brand)s Desktop with search components added.": "%(brand)s is missing some components required for securely caching encrypted messages locally. If you'd like to experiment with this feature, build a custom %(brand)s Desktop with search components added.", From 71e2536d02363f1d1469f03b5cc8d222d33fb5d6 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 11 Nov 2020 16:51:56 +0000 Subject: [PATCH 43/62] sort file i18n --- src/i18n/strings/en_EN.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 110c51163f..c8c7eece57 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -935,8 +935,8 @@ "Failed to set display name": "Failed to set display name", "Encryption": "Encryption", "Individually verify each session used by a user to mark it as trusted, not trusting cross-signed devices.": "Individually verify each session used by a user to mark it as trusted, not trusting cross-signed devices.", - "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(count)s rooms.|one": "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(count)s room.", "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(count)s rooms.|other": "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(count)s rooms.", + "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(count)s rooms.|one": "Securely cache encrypted messages locally for them to appear in search results, using %(size)s to store messages from %(count)s room.", "Manage": "Manage", "Securely cache encrypted messages locally for them to appear in search results.": "Securely cache encrypted messages locally for them to appear in search results.", "%(brand)s is missing some components required for securely caching encrypted messages locally. If you'd like to experiment with this feature, build a custom %(brand)s Desktop with search components added.": "%(brand)s is missing some components required for securely caching encrypted messages locally. If you'd like to experiment with this feature, build a custom %(brand)s Desktop with search components added.", From b2366c38ba27e826b2401963013c4ae56e108af4 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 11 Nov 2020 16:54:11 +0000 Subject: [PATCH 44/62] Fix the feedback not closing without feedback/countly --- src/components/views/dialogs/FeedbackDialog.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/dialogs/FeedbackDialog.js b/src/components/views/dialogs/FeedbackDialog.js index 2515377709..cbe26af6cc 100644 --- a/src/components/views/dialogs/FeedbackDialog.js +++ b/src/components/views/dialogs/FeedbackDialog.js @@ -48,8 +48,8 @@ export default (props) => { title: _t('Feedback sent'), description: _t('Thank you!'), }); - props.onFinished(); } + props.onFinished(); }; const brand = SdkConfig.get().brand; From 1a2bd55c099391092de573f92cea48f270957354 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 11 Nov 2020 15:17:34 +0000 Subject: [PATCH 45/62] Fix New Room Intro invite to this room button --- src/components/views/rooms/NewRoomIntro.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/components/views/rooms/NewRoomIntro.tsx b/src/components/views/rooms/NewRoomIntro.tsx index 27404eef12..be4ecaffb3 100644 --- a/src/components/views/rooms/NewRoomIntro.tsx +++ b/src/components/views/rooms/NewRoomIntro.tsx @@ -99,6 +99,10 @@ const NewRoomIntro = () => { }); } + const onInviteClick = () => { + dis.dispatch({ action: "view_invite", roomId }); + }; + const avatarUrl = room.currentState.getStateEvents(EventType.RoomAvatar, "")?.getContent()?.url; body = { })}

{topicText}

- + {_t("Invite to this room")}
From d5c399dfd9f5265230f44fb92fd124a12fdbc5ce Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 11 Nov 2020 12:56:20 +0000 Subject: [PATCH 46/62] Fix Left Panel layout being wrong when filtering with 0 rooms --- src/components/structures/LeftPanel.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/components/structures/LeftPanel.tsx b/src/components/structures/LeftPanel.tsx index 4445ff3ff8..52d461edcc 100644 --- a/src/components/structures/LeftPanel.tsx +++ b/src/components/structures/LeftPanel.tsx @@ -46,6 +46,7 @@ interface IProps { } interface IState { + isFiltering: boolean; showBreadcrumbs: boolean; showGroupFilterPanel: boolean; } @@ -70,6 +71,7 @@ export default class LeftPanel extends React.Component { super(props); this.state = { + isFiltering: !!RoomListStore.instance.getFirstNameFilterCondition(), showBreadcrumbs: BreadcrumbsStore.instance.visible, showGroupFilterPanel: SettingsStore.getValue('TagPanel.enableTagPanel'), }; @@ -102,9 +104,10 @@ export default class LeftPanel extends React.Component { }; private onBreadcrumbsUpdate = () => { - const newVal = BreadcrumbsStore.instance.visible; - if (newVal !== this.state.showBreadcrumbs) { - this.setState({showBreadcrumbs: newVal}); + const showBreadcrumbs = BreadcrumbsStore.instance.visible; + const isFiltering = !!RoomListStore.instance.getFirstNameFilterCondition(); + if (showBreadcrumbs !== this.state.showBreadcrumbs || isFiltering !== this.state.isFiltering) { + this.setState({showBreadcrumbs, isFiltering}); // Update the sticky headers too as the breadcrumbs will be popping in or out. if (!this.listContainerRef.current) return; // ignore: no headers to sticky From 187901004d0a97818b95f2a184cb731f9b5bd2e8 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 11 Nov 2020 13:01:40 +0000 Subject: [PATCH 47/62] Fix the Join rooms prompt not showing up due to missing updates --- src/components/structures/LeftPanel.tsx | 9 +++------ src/components/views/rooms/RoomList.tsx | 9 ++++++--- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/components/structures/LeftPanel.tsx b/src/components/structures/LeftPanel.tsx index 52d461edcc..4445ff3ff8 100644 --- a/src/components/structures/LeftPanel.tsx +++ b/src/components/structures/LeftPanel.tsx @@ -46,7 +46,6 @@ interface IProps { } interface IState { - isFiltering: boolean; showBreadcrumbs: boolean; showGroupFilterPanel: boolean; } @@ -71,7 +70,6 @@ export default class LeftPanel extends React.Component { super(props); this.state = { - isFiltering: !!RoomListStore.instance.getFirstNameFilterCondition(), showBreadcrumbs: BreadcrumbsStore.instance.visible, showGroupFilterPanel: SettingsStore.getValue('TagPanel.enableTagPanel'), }; @@ -104,10 +102,9 @@ export default class LeftPanel extends React.Component { }; private onBreadcrumbsUpdate = () => { - const showBreadcrumbs = BreadcrumbsStore.instance.visible; - const isFiltering = !!RoomListStore.instance.getFirstNameFilterCondition(); - if (showBreadcrumbs !== this.state.showBreadcrumbs || isFiltering !== this.state.isFiltering) { - this.setState({showBreadcrumbs, isFiltering}); + const newVal = BreadcrumbsStore.instance.visible; + if (newVal !== this.state.showBreadcrumbs) { + this.setState({showBreadcrumbs: newVal}); // Update the sticky headers too as the breadcrumbs will be popping in or out. if (!this.listContainerRef.current) return; // ignore: no headers to sticky diff --git a/src/components/views/rooms/RoomList.tsx b/src/components/views/rooms/RoomList.tsx index d952c137cd..3e9a8ac102 100644 --- a/src/components/views/rooms/RoomList.tsx +++ b/src/components/views/rooms/RoomList.tsx @@ -58,6 +58,7 @@ interface IProps { interface IState { sublists: ITagMap; + isNameFiltering: boolean; } const TAG_ORDER: TagID[] = [ @@ -183,6 +184,7 @@ export default class RoomList extends React.PureComponent { this.state = { sublists: {}, + isNameFiltering: !!RoomListStore.instance.getFirstNameFilterCondition(), }; this.dispatcherRef = defaultDispatcher.register(this.onAction); @@ -253,7 +255,8 @@ export default class RoomList extends React.PureComponent { return CustomRoomTagStore.getTags()[t]; }); - let doUpdate = arrayHasDiff(previousListIds, newListIds); + const isNameFiltering = !!RoomListStore.instance.getFirstNameFilterCondition(); + let doUpdate = this.state.isNameFiltering !== isNameFiltering || arrayHasDiff(previousListIds, newListIds); if (!doUpdate) { // so we didn't have the visible sublists change, but did the contents of those // sublists change significantly enough to break the sticky headers? Probably, so @@ -275,7 +278,7 @@ export default class RoomList extends React.PureComponent { const newSublists = objectWithOnly(newLists, newListIds); const sublists = objectShallowClone(newSublists, (k, v) => arrayFastClone(v)); - this.setState({sublists}, () => { + this.setState({sublists, isNameFiltering}, () => { this.props.onResize(); }); } @@ -370,7 +373,7 @@ export default class RoomList extends React.PureComponent { public render() { let explorePrompt: JSX.Element; if (!this.props.isMinimized) { - if (RoomListStore.instance.getFirstNameFilterCondition()) { + if (this.state.isNameFiltering) { explorePrompt =
{_t("Can't see what you’re looking for?")}
From d3fee540c5b8223b80301791e9bb38ac19dd68ca Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 11 Nov 2020 13:08:09 +0000 Subject: [PATCH 48/62] Update Room List filter copy --- src/components/structures/RoomSearch.tsx | 4 ++-- src/i18n/strings/en_EN.json | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/components/structures/RoomSearch.tsx b/src/components/structures/RoomSearch.tsx index 526aecddd7..a64e40bc65 100644 --- a/src/components/structures/RoomSearch.tsx +++ b/src/components/structures/RoomSearch.tsx @@ -148,7 +148,7 @@ export default class RoomSearch extends React.PureComponent { onBlur={this.onBlur} onChange={this.onChange} onKeyDown={this.onKeyDown} - placeholder={_t("Search")} + placeholder={_t("Filter")} autoComplete="off" /> ); @@ -164,7 +164,7 @@ export default class RoomSearch extends React.PureComponent { if (this.props.isMinimized) { icon = ( diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 830d3cdee4..701bee457f 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2372,8 +2372,9 @@ "Find a room… (e.g. %(exampleRoom)s)": "Find a room… (e.g. %(exampleRoom)s)", "If you can't find the room you're looking for, ask for an invite or Create a new room.": "If you can't find the room you're looking for, ask for an invite or Create a new room.", "Explore rooms in %(communityName)s": "Explore rooms in %(communityName)s", + "Filter": "Filter", "Clear filter": "Clear filter", - "Search rooms": "Search rooms", + "Filter rooms and people": "Filter rooms and people", "You can't send any messages until you review and agree to our terms and conditions.": "You can't send any messages until you review and agree to our terms and conditions.", "Your message wasn't sent because this homeserver has hit its Monthly Active User Limit. Please contact your service administrator to continue using the service.": "Your message wasn't sent because this homeserver has hit its Monthly Active User Limit. Please contact your service administrator to continue using the service.", "Your message wasn't sent because this homeserver has exceeded a resource limit. Please contact your service administrator to continue using the service.": "Your message wasn't sent because this homeserver has exceeded a resource limit. Please contact your service administrator to continue using the service.", From a481f3bdf1d6be30538185fda44718acb9d3863b Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 11 Nov 2020 13:21:20 +0000 Subject: [PATCH 49/62] Iterate the filtering prompt --- res/css/views/rooms/_RoomList.scss | 9 ++++++- src/components/views/rooms/RoomList.tsx | 35 ++++++++++++++++++++++--- src/i18n/strings/en_EN.json | 1 + 3 files changed, 40 insertions(+), 5 deletions(-) diff --git a/res/css/views/rooms/_RoomList.scss b/res/css/views/rooms/_RoomList.scss index 78e7307bc0..6ea99585d2 100644 --- a/res/css/views/rooms/_RoomList.scss +++ b/res/css/views/rooms/_RoomList.scss @@ -33,7 +33,6 @@ limitations under the License. div:first-child { font-weight: $font-semi-bold; - margin-bottom: 8px; } .mx_AccessibleButton { @@ -41,6 +40,7 @@ limitations under the License. position: relative; padding: 0 0 0 24px; font-size: inherit; + margin-top: 8px; &::before { content: ''; @@ -53,6 +53,13 @@ limitations under the License. mask-position: center; mask-size: contain; mask-repeat: no-repeat; + } + + &.mx_RoomList_explorePrompt_startChat::before { + mask-image: url('$(res)/img/element-icons/feedback.svg'); + } + + &.mx_RoomList_explorePrompt_explore::before { mask-image: url('$(res)/img/element-icons/roomlist/explore.svg'); } } diff --git a/src/components/views/rooms/RoomList.tsx b/src/components/views/rooms/RoomList.tsx index 3e9a8ac102..de54fabc53 100644 --- a/src/components/views/rooms/RoomList.tsx +++ b/src/components/views/rooms/RoomList.tsx @@ -284,6 +284,10 @@ export default class RoomList extends React.PureComponent { } }; + private onStartChat = () => { + dis.dispatch({action: "view_create_chat"}); + }; + private onExplore = () => { dis.fire(Action.ViewRoomDirectory); }; @@ -335,8 +339,9 @@ export default class RoomList extends React.PureComponent { return p; }, [] as TagID[]); - // show a skeleton UI if the user is in no rooms - const showSkeleton = Object.values(RoomListStore.instance.unfilteredLists).every(list => !list?.length); + // show a skeleton UI if the user is in no rooms and they are not filtering + const showSkeleton = !this.state.isNameFiltering && + Object.values(RoomListStore.instance.unfilteredLists).every(list => !list?.length); for (const orderedTagId of tagOrder) { const orderedRooms = this.state.sublists[orderedTagId] || []; @@ -376,7 +381,18 @@ export default class RoomList extends React.PureComponent { if (this.state.isNameFiltering) { explorePrompt =
{_t("Can't see what you’re looking for?")}
- + + {_t("Start a new chat")} + + {_t("Explore all public rooms")}
; @@ -388,7 +404,18 @@ export default class RoomList extends React.PureComponent { if (unfilteredRooms.length < 1 && unfilteredHistorical < 1) { explorePrompt =
{_t("Use the + to make a new room or explore existing ones below")}
- + + {_t("Start a new chat")} + + {_t("Explore all public rooms")}
; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 701bee457f..3bfa962216 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1391,6 +1391,7 @@ "Historical": "Historical", "Custom Tag": "Custom Tag", "Can't see what you’re looking for?": "Can't see what you’re looking for?", + "Start a new chat": "Start a new chat", "Explore all public rooms": "Explore all public rooms", "Use the + to make a new room or explore existing ones below": "Use the + to make a new room or explore existing ones below", "%(count)s results|other": "%(count)s results", From d0513406ee1d78ca82f144a8e72f59a23050965a Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 11 Nov 2020 13:36:17 +0000 Subject: [PATCH 50/62] Pass filter text when clicking explore/dm prompt --- src/RoomInvite.js | 4 +- src/components/structures/MatrixChat.tsx | 7 +- src/components/structures/RoomDirectory.js | 4 +- src/components/views/dialogs/InviteDialog.js | 230 ++++++++++-------- .../views/elements/DirectorySearchBox.js | 30 ++- src/components/views/rooms/RoomList.tsx | 6 +- 6 files changed, 153 insertions(+), 128 deletions(-) diff --git a/src/RoomInvite.js b/src/RoomInvite.js index 7eb7f5dbb2..06d3fb04e8 100644 --- a/src/RoomInvite.js +++ b/src/RoomInvite.js @@ -40,11 +40,11 @@ export function inviteMultipleToRoom(roomId, addrs) { return inviter.invite(addrs).then(states => Promise.resolve({states, inviter})); } -export function showStartChatInviteDialog() { +export function showStartChatInviteDialog(initialText) { // This dialog handles the room creation internally - we don't need to worry about it. const InviteDialog = sdk.getComponent("dialogs.InviteDialog"); Modal.createTrackedDialog( - 'Start DM', '', InviteDialog, {kind: KIND_DM}, + 'Start DM', '', InviteDialog, {kind: KIND_DM, initialText}, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true, ); } diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index 22cd73eff7..b2c94e4a8b 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -653,8 +653,9 @@ export default class MatrixChat extends React.PureComponent { } case Action.ViewRoomDirectory: { const RoomDirectory = sdk.getComponent("structures.RoomDirectory"); - Modal.createTrackedDialog('Room directory', '', RoomDirectory, {}, - 'mx_RoomDirectory_dialogWrapper', false, true); + Modal.createTrackedDialog('Room directory', '', RoomDirectory, { + initialText: payload.initialText, + }, 'mx_RoomDirectory_dialogWrapper', false, true); // View the welcome or home page if we need something to look at this.viewSomethingBehindModal(); @@ -677,7 +678,7 @@ export default class MatrixChat extends React.PureComponent { this.chatCreateOrReuse(payload.user_id); break; case 'view_create_chat': - showStartChatInviteDialog(); + showStartChatInviteDialog(payload.initialText || ""); break; case 'view_invite': showRoomInviteDialog(payload.roomId); diff --git a/src/components/structures/RoomDirectory.js b/src/components/structures/RoomDirectory.js index ece70e3a8f..e3323b05fa 100644 --- a/src/components/structures/RoomDirectory.js +++ b/src/components/structures/RoomDirectory.js @@ -44,6 +44,7 @@ function track(action) { export default class RoomDirectory extends React.Component { static propTypes = { + initialText: PropTypes.string, onFinished: PropTypes.func.isRequired, }; @@ -61,7 +62,7 @@ export default class RoomDirectory extends React.Component { error: null, instanceId: undefined, roomServer: MatrixClientPeg.getHomeserverName(), - filterString: null, + filterString: this.props.initialText || "", selectedCommunityId: SettingsStore.getValue("feature_communities_v2_prototypes") ? selectedCommunityId : null, @@ -686,6 +687,7 @@ export default class RoomDirectory extends React.Component { onJoinClick={this.onJoinFromSearchClick} placeholder={placeholder} showJoinButton={showJoinButton} + initialText={this.props.initialText} /> {dropdown}
; diff --git a/src/components/views/dialogs/InviteDialog.js b/src/components/views/dialogs/InviteDialog.js index 99878569d3..9b7c5803a1 100644 --- a/src/components/views/dialogs/InviteDialog.js +++ b/src/components/views/dialogs/InviteDialog.js @@ -308,10 +308,14 @@ export default class InviteDialog extends React.PureComponent { // The room ID this dialog is for. Only required for KIND_INVITE. roomId: PropTypes.string, + + // Initial value to populate the filter with + initialText: PropTypes.string, }; static defaultProps = { kind: KIND_DM, + initialText: "", }; _debounceTimer: number = null; @@ -338,7 +342,7 @@ export default class InviteDialog extends React.PureComponent { this.state = { targets: [], // array of Member objects (see interface above) - filterText: "", + filterText: this.props.initialText, recents: InviteDialog.buildRecents(alreadyInvited), numRecentsShown: INITIAL_ROOMS_SHOWN, suggestions: this._buildSuggestions(alreadyInvited), @@ -356,6 +360,12 @@ export default class InviteDialog extends React.PureComponent { this._editorRef = createRef(); } + componentDidMount() { + if (this.props.initialText) { + this._updateSuggestions(this.props.initialText); + } + } + static buildRecents(excludedTargetIds: Set): {userId: string, user: RoomMember, lastActive: number} { const rooms = DMRoomMap.shared().getUniqueRoomsWithIndividuals(); // map of userId => js-sdk Room @@ -687,6 +697,115 @@ export default class InviteDialog extends React.PureComponent { } }; + _updateSuggestions = async (term) => { + MatrixClientPeg.get().searchUserDirectory({term}).then(async r => { + if (term !== this.state.filterText) { + // Discard the results - we were probably too slow on the server-side to make + // these results useful. This is a race we want to avoid because we could overwrite + // more accurate results. + return; + } + + if (!r.results) r.results = []; + + // While we're here, try and autocomplete a search result for the mxid itself + // if there's no matches (and the input looks like a mxid). + if (term[0] === '@' && term.indexOf(':') > 1) { + try { + const profile = await MatrixClientPeg.get().getProfileInfo(term); + if (profile) { + // If we have a profile, we have enough information to assume that + // the mxid can be invited - add it to the list. We stick it at the + // top so it is most obviously presented to the user. + r.results.splice(0, 0, { + user_id: term, + display_name: profile['displayname'], + avatar_url: profile['avatar_url'], + }); + } + } catch (e) { + console.warn("Non-fatal error trying to make an invite for a user ID"); + console.warn(e); + + // Add a result anyways, just without a profile. We stick it at the + // top so it is most obviously presented to the user. + r.results.splice(0, 0, { + user_id: term, + display_name: term, + avatar_url: null, + }); + } + } + + this.setState({ + serverResultsMixin: r.results.map(u => ({ + userId: u.user_id, + user: new DirectoryMember(u), + })), + }); + }).catch(e => { + console.error("Error searching user directory:"); + console.error(e); + this.setState({serverResultsMixin: []}); // clear results because it's moderately fatal + }); + + // Whenever we search the directory, also try to search the identity server. It's + // all debounced the same anyways. + if (!this.state.canUseIdentityServer) { + // The user doesn't have an identity server set - warn them of that. + this.setState({tryingIdentityServer: true}); + return; + } + if (term.indexOf('@') > 0 && Email.looksValid(term) && SettingsStore.getValue(UIFeature.IdentityServer)) { + // Start off by suggesting the plain email while we try and resolve it + // to a real account. + this.setState({ + // per above: the userId is a lie here - it's just a regular identifier + threepidResultsMixin: [{user: new ThreepidMember(term), userId: term}], + }); + try { + const authClient = new IdentityAuthClient(); + const token = await authClient.getAccessToken(); + if (term !== this.state.filterText) return; // abandon hope + + const lookup = await MatrixClientPeg.get().lookupThreePid( + 'email', + term, + undefined, // callback + token, + ); + if (term !== this.state.filterText) return; // abandon hope + + if (!lookup || !lookup.mxid) { + // We weren't able to find anyone - we're already suggesting the plain email + // as an alternative, so do nothing. + return; + } + + // We append the user suggestion to give the user an option to click + // the email anyways, and so we don't cause things to jump around. In + // theory, the user would see the user pop up and think "ah yes, that + // person!" + const profile = await MatrixClientPeg.get().getProfileInfo(lookup.mxid); + if (term !== this.state.filterText || !profile) return; // abandon hope + this.setState({ + threepidResultsMixin: [...this.state.threepidResultsMixin, { + user: new DirectoryMember({ + user_id: lookup.mxid, + display_name: profile.displayname, + avatar_url: profile.avatar_url, + }), + userId: lookup.mxid, + }], + }); + } catch (e) { + console.error("Error searching identity server:"); + console.error(e); + this.setState({threepidResultsMixin: []}); // clear results because it's moderately fatal + } + } + }; + _updateFilter = (e) => { const term = e.target.value; this.setState({filterText: term}); @@ -697,113 +816,8 @@ export default class InviteDialog extends React.PureComponent { if (this._debounceTimer) { clearTimeout(this._debounceTimer); } - this._debounceTimer = setTimeout(async () => { - MatrixClientPeg.get().searchUserDirectory({term}).then(async r => { - if (term !== this.state.filterText) { - // Discard the results - we were probably too slow on the server-side to make - // these results useful. This is a race we want to avoid because we could overwrite - // more accurate results. - return; - } - - if (!r.results) r.results = []; - - // While we're here, try and autocomplete a search result for the mxid itself - // if there's no matches (and the input looks like a mxid). - if (term[0] === '@' && term.indexOf(':') > 1) { - try { - const profile = await MatrixClientPeg.get().getProfileInfo(term); - if (profile) { - // If we have a profile, we have enough information to assume that - // the mxid can be invited - add it to the list. We stick it at the - // top so it is most obviously presented to the user. - r.results.splice(0, 0, { - user_id: term, - display_name: profile['displayname'], - avatar_url: profile['avatar_url'], - }); - } - } catch (e) { - console.warn("Non-fatal error trying to make an invite for a user ID"); - console.warn(e); - - // Add a result anyways, just without a profile. We stick it at the - // top so it is most obviously presented to the user. - r.results.splice(0, 0, { - user_id: term, - display_name: term, - avatar_url: null, - }); - } - } - - this.setState({ - serverResultsMixin: r.results.map(u => ({ - userId: u.user_id, - user: new DirectoryMember(u), - })), - }); - }).catch(e => { - console.error("Error searching user directory:"); - console.error(e); - this.setState({serverResultsMixin: []}); // clear results because it's moderately fatal - }); - - // Whenever we search the directory, also try to search the identity server. It's - // all debounced the same anyways. - if (!this.state.canUseIdentityServer) { - // The user doesn't have an identity server set - warn them of that. - this.setState({tryingIdentityServer: true}); - return; - } - if (term.indexOf('@') > 0 && Email.looksValid(term) && SettingsStore.getValue(UIFeature.IdentityServer)) { - // Start off by suggesting the plain email while we try and resolve it - // to a real account. - this.setState({ - // per above: the userId is a lie here - it's just a regular identifier - threepidResultsMixin: [{user: new ThreepidMember(term), userId: term}], - }); - try { - const authClient = new IdentityAuthClient(); - const token = await authClient.getAccessToken(); - if (term !== this.state.filterText) return; // abandon hope - - const lookup = await MatrixClientPeg.get().lookupThreePid( - 'email', - term, - undefined, // callback - token, - ); - if (term !== this.state.filterText) return; // abandon hope - - if (!lookup || !lookup.mxid) { - // We weren't able to find anyone - we're already suggesting the plain email - // as an alternative, so do nothing. - return; - } - - // We append the user suggestion to give the user an option to click - // the email anyways, and so we don't cause things to jump around. In - // theory, the user would see the user pop up and think "ah yes, that - // person!" - const profile = await MatrixClientPeg.get().getProfileInfo(lookup.mxid); - if (term !== this.state.filterText || !profile) return; // abandon hope - this.setState({ - threepidResultsMixin: [...this.state.threepidResultsMixin, { - user: new DirectoryMember({ - user_id: lookup.mxid, - display_name: profile.displayname, - avatar_url: profile.avatar_url, - }), - userId: lookup.mxid, - }], - }); - } catch (e) { - console.error("Error searching identity server:"); - console.error(e); - this.setState({threepidResultsMixin: []}); // clear results because it's moderately fatal - } - } + this._debounceTimer = setTimeout(() => { + this._updateSuggestions(term); }, 150); // 150ms debounce (human reaction time + some) }; diff --git a/src/components/views/elements/DirectorySearchBox.js b/src/components/views/elements/DirectorySearchBox.js index c2e8e4fd68..644b69417b 100644 --- a/src/components/views/elements/DirectorySearchBox.js +++ b/src/components/views/elements/DirectorySearchBox.js @@ -20,8 +20,8 @@ import * as sdk from '../../../index'; import { _t } from '../../../languageHandler'; export default class DirectorySearchBox extends React.Component { - constructor() { - super(); + constructor(props) { + super(props); this._collectInput = this._collectInput.bind(this); this._onClearClick = this._onClearClick.bind(this); this._onChange = this._onChange.bind(this); @@ -31,7 +31,7 @@ export default class DirectorySearchBox extends React.Component { this.input = null; this.state = { - value: '', + value: this.props.initialText || '', }; } @@ -90,15 +90,20 @@ export default class DirectorySearchBox extends React.Component { } return
- - { joinButton } - -
; + + { joinButton } + +
; } } @@ -109,4 +114,5 @@ DirectorySearchBox.propTypes = { onJoinClick: PropTypes.func, placeholder: PropTypes.string, showJoinButton: PropTypes.bool, + initialText: PropTypes.string, }; diff --git a/src/components/views/rooms/RoomList.tsx b/src/components/views/rooms/RoomList.tsx index de54fabc53..6e677f2b01 100644 --- a/src/components/views/rooms/RoomList.tsx +++ b/src/components/views/rooms/RoomList.tsx @@ -285,11 +285,13 @@ export default class RoomList extends React.PureComponent { }; private onStartChat = () => { - dis.dispatch({action: "view_create_chat"}); + const initialText = RoomListStore.instance.getFirstNameFilterCondition()?.search; + dis.dispatch({ action: "view_create_chat", initialText }); }; private onExplore = () => { - dis.fire(Action.ViewRoomDirectory); + const initialText = RoomListStore.instance.getFirstNameFilterCondition()?.search; + dis.dispatch({ action: Action.ViewRoomDirectory, initialText }); }; private renderCommunityInvites(): TemporaryTile[] { From 0bee4bd72bbdde493a0eed3a55d07944e219897d Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 11 Nov 2020 13:45:50 +0000 Subject: [PATCH 51/62] Update `Confirm` password placeholder --- src/components/views/auth/RegistrationForm.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/auth/RegistrationForm.js b/src/components/views/auth/RegistrationForm.js index 419443984a..5245d1a921 100644 --- a/src/components/views/auth/RegistrationForm.js +++ b/src/components/views/auth/RegistrationForm.js @@ -461,7 +461,7 @@ export default class RegistrationForm extends React.Component { ref={field => this[FIELD_PASSWORD_CONFIRM] = field} type="password" autoComplete="new-password" - label={_t("Confirm")} + label={_t("Confirm password")} value={this.state.passwordConfirm} onChange={this.onPasswordConfirmChange} onValidate={this.onPasswordConfirmValidate} From b3ccabbe6bfdfae580862307c6739b06652f6118 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 11 Nov 2020 14:00:40 +0000 Subject: [PATCH 52/62] Clear recaptcha error on reattempts --- src/components/views/auth/CaptchaForm.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/components/views/auth/CaptchaForm.js b/src/components/views/auth/CaptchaForm.js index 5cce93f0b8..e2d7d594fa 100644 --- a/src/components/views/auth/CaptchaForm.js +++ b/src/components/views/auth/CaptchaForm.js @@ -102,6 +102,10 @@ export default class CaptchaForm extends React.Component { console.log("Loaded recaptcha script."); try { this._renderRecaptcha(DIV_ID); + // clear error if re-rendered + this.setState({ + errorText: null, + }); CountlyAnalytics.instance.track("onboarding_grecaptcha_loaded"); } catch (e) { this.setState({ From edb5e10506cd79349adff8e7d16dc8810b311abd Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 11 Nov 2020 15:07:57 +0000 Subject: [PATCH 53/62] Iterate registration to simplify it based on usertesting --- .../auth/_InteractiveAuthEntryComponents.scss | 29 +++++++++ res/img/element-icons/email-prompt.svg | 13 ++++ .../structures/auth/Registration.js | 63 ++++++++++++------- .../auth/InteractiveAuthEntryComponents.js | 8 +-- src/components/views/auth/RegistrationForm.js | 32 ---------- 5 files changed, 86 insertions(+), 59 deletions(-) create mode 100644 res/img/element-icons/email-prompt.svg diff --git a/res/css/views/auth/_InteractiveAuthEntryComponents.scss b/res/css/views/auth/_InteractiveAuthEntryComponents.scss index 05cddf2c48..0a5ac9b2bc 100644 --- a/res/css/views/auth/_InteractiveAuthEntryComponents.scss +++ b/res/css/views/auth/_InteractiveAuthEntryComponents.scss @@ -14,6 +14,35 @@ See the License for the specific language governing permissions and limitations under the License. */ +.mx_InteractiveAuthEntryComponents_emailWrapper { + padding-right: 60px; + position: relative; + margin-top: 32px; + margin-bottom: 32px; + + &::before, &::after { + position: absolute; + width: 116px; + height: 116px; + content: ""; + right: -10px; + } + + &::before { + background-color: rgba(244, 246, 250, 0.91); + border-radius: 50%; + top: -20px; + } + + &::after { + background-image: url('$(res)/img/element-icons/email-prompt.svg'); + background-repeat: no-repeat; + background-position: center; + background-size: contain; + top: -25px; + } +} + .mx_InteractiveAuthEntryComponents_msisdnWrapper { text-align: center; } diff --git a/res/img/element-icons/email-prompt.svg b/res/img/element-icons/email-prompt.svg new file mode 100644 index 0000000000..19b8f82449 --- /dev/null +++ b/res/img/element-icons/email-prompt.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/src/components/structures/auth/Registration.js b/src/components/structures/auth/Registration.js index 630e04da9c..777d57344d 100644 --- a/src/components/structures/auth/Registration.js +++ b/src/components/structures/auth/Registration.js @@ -502,17 +502,9 @@ export default class Registration extends React.Component { return null; } - // If we're on a different phase, we only show the server type selector, - // which is always shown if we allow custom URLs at all. - // (if there's a fatal server error, we need to show the full server - // config as the user may need to change servers to resolve the error). - if (PHASES_ENABLED && this.state.phase !== PHASE_SERVER_DETAILS && !this.state.serverErrorIsFatal) { - return
- -
; + // Hide the server picker once the user is doing UI Auth unless encountered a fatal server error + if (this.state.phase !== PHASE_SERVER_DETAILS && this.state.doingUIAuth && !this.state.serverErrorIsFatal) { + return null; } const serverDetailsProps = {}; @@ -582,17 +574,6 @@ export default class Registration extends React.Component {
; } else if (this.state.flows.length) { - let onEditServerDetailsClick = null; - // If custom URLs are allowed and we haven't selected the Free server type, wire - // up the server details edit link. - if ( - PHASES_ENABLED && - !SdkConfig.get()['disable_custom_urls'] && - this.state.serverType !== ServerType.FREE - ) { - onEditServerDetailsClick = this.onEditServerDetailsClick; - } - return ; } else { + let yourMatrixAccountText = _t('Create your Matrix account on %(serverName)s', { + serverName: this.props.serverConfig.hsName, + }); + if (this.props.serverConfig.hsNameIsDifferent) { + const TextWithTooltip = sdk.getComponent("elements.TextWithTooltip"); + + yourMatrixAccountText = _t('Create your Matrix account on ', {}, { + 'underlinedServerName': () => { + return ; + }, + }); + } + + // If custom URLs are allowed, user is not doing UIA flows and they haven't selected the Free server type, + // wire up the server details edit link. + let editLink = null; + if (PHASES_ENABLED && + !SdkConfig.get()['disable_custom_urls'] && + this.state.serverType !== ServerType.FREE && + !this.state.doingUIAuth + ) { + editLink = ( + + {_t('Change')} + + ); + } + body =

{ _t('Create your account') }

{ errorText } { serverDeadSection } { this.renderServerComponent() } +

+ {yourMatrixAccountText} + {editLink} +

{ this.renderRegisterComponent() } { goBack } { signIn } diff --git a/src/components/views/auth/InteractiveAuthEntryComponents.js b/src/components/views/auth/InteractiveAuthEntryComponents.js index f49e6959fb..6628ca7120 100644 --- a/src/components/views/auth/InteractiveAuthEntryComponents.js +++ b/src/components/views/auth/InteractiveAuthEntryComponents.js @@ -421,12 +421,12 @@ export class EmailIdentityAuthEntry extends React.Component { return ; } else { return ( -
-

{ _t("An email has been sent to %(emailAddress)s", - { emailAddress: (sub) => { this.props.inputs.emailAddress } }, +

+

{ _t("A confirmation email has been sent to %(emailAddress)s", + { emailAddress: (sub) => { this.props.inputs.emailAddress } }, ) }

-

{ _t("Please check your email to continue registration.") }

+

{ _t("Open the link in the email to continue registration.") }

); } diff --git a/src/components/views/auth/RegistrationForm.js b/src/components/views/auth/RegistrationForm.js index 5245d1a921..70c1017427 100644 --- a/src/components/views/auth/RegistrationForm.js +++ b/src/components/views/auth/RegistrationForm.js @@ -51,7 +51,6 @@ export default class RegistrationForm extends React.Component { defaultUsername: PropTypes.string, defaultPassword: PropTypes.string, onRegisterClick: PropTypes.func.isRequired, // onRegisterClick(Object) => ?Promise - onEditServerDetailsClick: PropTypes.func, flows: PropTypes.arrayOf(PropTypes.object).isRequired, serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired, canSubmit: PropTypes.bool, @@ -513,33 +512,6 @@ export default class RegistrationForm extends React.Component { } render() { - let yourMatrixAccountText = _t('Create your Matrix account on %(serverName)s', { - serverName: this.props.serverConfig.hsName, - }); - if (this.props.serverConfig.hsNameIsDifferent) { - const TextWithTooltip = sdk.getComponent("elements.TextWithTooltip"); - - yourMatrixAccountText = _t('Create your Matrix account on ', {}, { - 'underlinedServerName': () => { - return ; - }, - }); - } - - let editLink = null; - if (this.props.onEditServerDetailsClick) { - editLink = - {_t('Change')} - ; - } - const registerButton = ( ); @@ -575,10 +547,6 @@ export default class RegistrationForm extends React.Component { return (
-

- {yourMatrixAccountText} - {editLink} -

{this.renderUsername()} From a2958f8f99e9b142cac482d888d6f9fcc0bc2e53 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 11 Nov 2020 16:45:46 +0000 Subject: [PATCH 54/62] i18n --- src/i18n/strings/en_EN.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 3bfa962216..d9bdd82c42 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2211,8 +2211,8 @@ "Missing captcha public key in homeserver configuration. Please report this to your homeserver administrator.": "Missing captcha public key in homeserver configuration. Please report this to your homeserver administrator.", "Please review and accept all of the homeserver's policies": "Please review and accept all of the homeserver's policies", "Please review and accept the policies of this homeserver:": "Please review and accept the policies of this homeserver:", - "An email has been sent to %(emailAddress)s": "An email has been sent to %(emailAddress)s", - "Please check your email to continue registration.": "Please check your email to continue registration.", + "A confirmation email has been sent to %(emailAddress)s": "A confirmation email has been sent to %(emailAddress)s", + "Open the link in the email to continue registration.": "Open the link in the email to continue registration.", "Token incorrect": "Token incorrect", "A text message has been sent to %(msisdn)s": "A text message has been sent to %(msisdn)s", "Please enter the code it contains:": "Please enter the code it contains:", @@ -2249,8 +2249,6 @@ "Enter username": "Enter username", "Email (optional)": "Email (optional)", "Phone (optional)": "Phone (optional)", - "Create your Matrix account on %(serverName)s": "Create your Matrix account on %(serverName)s", - "Create your Matrix account on ": "Create your Matrix account on ", "Register": "Register", "Set an email for account recovery. Use email or phone to optionally be discoverable by existing contacts.": "Set an email for account recovery. Use email or phone to optionally be discoverable by existing contacts.", "Set an email for account recovery. Use email to optionally be discoverable by existing contacts.": "Set an email for account recovery. Use email to optionally be discoverable by existing contacts.", @@ -2474,6 +2472,8 @@ "Log in to your new account.": "Log in to your new account.", "You can now close this window or log in to your new account.": "You can now close this window or log in to your new account.", "Registration Successful": "Registration Successful", + "Create your Matrix account on %(serverName)s": "Create your Matrix account on %(serverName)s", + "Create your Matrix account on ": "Create your Matrix account on ", "Create your account": "Create your account", "Use Recovery Key or Passphrase": "Use Recovery Key or Passphrase", "Use Recovery Key": "Use Recovery Key", From 0911007c77e35efbc0fd00712ffcdcfcaf025034 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 11 Nov 2020 17:12:27 +0000 Subject: [PATCH 55/62] fix issue with server selector introduced in this PR --- src/components/structures/auth/Registration.js | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/components/structures/auth/Registration.js b/src/components/structures/auth/Registration.js index 777d57344d..80bf3b72cd 100644 --- a/src/components/structures/auth/Registration.js +++ b/src/components/structures/auth/Registration.js @@ -507,6 +507,19 @@ export default class Registration extends React.Component { return null; } + // If we're on a different phase, we only show the server type selector, + // which is always shown if we allow custom URLs at all. + // (if there's a fatal server error, we need to show the full server + // config as the user may need to change servers to resolve the error). + if (PHASES_ENABLED && this.state.phase !== PHASE_SERVER_DETAILS && !this.state.serverErrorIsFatal) { + return
+ +
; + } + const serverDetailsProps = {}; if (PHASES_ENABLED) { serverDetailsProps.onAfterSubmit = this.onServerDetailsNextPhaseClick; @@ -704,10 +717,10 @@ export default class Registration extends React.Component { { errorText } { serverDeadSection } { this.renderServerComponent() } -

+ { this.state.phase !== PHASE_SERVER_DETAILS &&

{yourMatrixAccountText} {editLink} -

+ } { this.renderRegisterComponent() } { goBack } { signIn } From 2adc578d285c2b81671e3b7ffa334b1135cdc446 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 11 Nov 2020 17:31:30 +0000 Subject: [PATCH 56/62] mark the onClick prop required on AccessibleButton --- src/components/views/elements/AccessibleButton.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/elements/AccessibleButton.tsx b/src/components/views/elements/AccessibleButton.tsx index ae822204df..e634057a21 100644 --- a/src/components/views/elements/AccessibleButton.tsx +++ b/src/components/views/elements/AccessibleButton.tsx @@ -39,7 +39,7 @@ interface IProps extends React.InputHTMLAttributes { tabIndex?: number; disabled?: boolean; className?: string; - onClick?(e?: ButtonEvent): void; + onClick(e?: ButtonEvent): void; } interface IAccessibleButtonProps extends React.InputHTMLAttributes { From f1121c1b6e2024afc216459b06d0f9793de11a30 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 12 Nov 2020 11:09:08 +0000 Subject: [PATCH 57/62] Fix vertical centering of the Homepage and button layout --- res/css/structures/_HomePage.scss | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/res/css/structures/_HomePage.scss b/res/css/structures/_HomePage.scss index 45aa34d3b5..9f72213d1a 100644 --- a/res/css/structures/_HomePage.scss +++ b/res/css/structures/_HomePage.scss @@ -26,9 +26,10 @@ limitations under the License. .mx_HomePage_default { text-align: center; + display: flex; .mx_HomePage_default_wrapper { - padding: 25vh 0 12px; + margin: auto; } img { @@ -55,7 +56,7 @@ limitations under the License. } .mx_HomePage_default_buttons { - margin: 80px auto 0; + margin: 60px auto 0; width: fit-content; .mx_AccessibleButton { @@ -63,7 +64,7 @@ limitations under the License. width: 160px; height: 132px; - margin: 0 20px; + margin: 20px; position: relative; display: inline-block; border-radius: 8px; From 4bcb9323550b4ea242432cdcdc897c61c461b282 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 12 Nov 2020 11:33:44 -0700 Subject: [PATCH 58/62] Update widget-api --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 048edea3fd..620f854be4 100644 --- a/package.json +++ b/package.json @@ -79,7 +79,7 @@ "linkifyjs": "^2.1.9", "lodash": "^4.17.19", "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop", - "matrix-widget-api": "^0.1.0-beta.5", + "matrix-widget-api": "^0.1.0-beta.7", "minimist": "^1.2.5", "pako": "^1.0.11", "parse5": "^5.1.1", diff --git a/yarn.lock b/yarn.lock index 23a32280c1..a7b12ad8da 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6532,10 +6532,10 @@ matrix-react-test-utils@^0.2.2: resolved "https://registry.yarnpkg.com/matrix-react-test-utils/-/matrix-react-test-utils-0.2.2.tgz#c87144d3b910c7edc544a6699d13c7c2bf02f853" integrity sha512-49+7gfV6smvBIVbeloql+37IeWMTD+fiywalwCqk8Dnz53zAFjKSltB3rmWHso1uecLtQEcPtCijfhzcLXAxTQ== -matrix-widget-api@^0.1.0-beta.5: - version "0.1.0-beta.5" - resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-0.1.0-beta.5.tgz#dd7f24a177aa590d812bd4e92e2c3ac225c5557e" - integrity sha512-J3GBJtVMFuEM/EWFylc0IlkPjdgmWxrkGYPaZ0LSmxp+OlNJxYfnWPR6F6HveW+Z8C1i0vq+BTueofSqKv2zDg== +matrix-widget-api@^0.1.0-beta.7: + version "0.1.0-beta.7" + resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-0.1.0-beta.7.tgz#36f7ed968fe526b25f95bd643b9ff6e53f718083" + integrity sha512-aUyOoCgLs6+6Wxug19npJVMwZl7WCgj2EmXs0mw24bz5zOp5coRyuRvwA1gX0Rqh+AuW4vNcjC+iwwJxuvOQvg== dependencies: events "^3.2.0" From 1b6e47c01b8596b1c77c116301f27f0cad9ff213 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 12 Nov 2020 11:52:47 -0700 Subject: [PATCH 59/62] Update widget-api again --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 620f854be4..a015728256 100644 --- a/package.json +++ b/package.json @@ -79,7 +79,7 @@ "linkifyjs": "^2.1.9", "lodash": "^4.17.19", "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop", - "matrix-widget-api": "^0.1.0-beta.7", + "matrix-widget-api": "^0.1.0-beta.8", "minimist": "^1.2.5", "pako": "^1.0.11", "parse5": "^5.1.1", diff --git a/yarn.lock b/yarn.lock index a7b12ad8da..3c163b7c2f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6532,10 +6532,10 @@ matrix-react-test-utils@^0.2.2: resolved "https://registry.yarnpkg.com/matrix-react-test-utils/-/matrix-react-test-utils-0.2.2.tgz#c87144d3b910c7edc544a6699d13c7c2bf02f853" integrity sha512-49+7gfV6smvBIVbeloql+37IeWMTD+fiywalwCqk8Dnz53zAFjKSltB3rmWHso1uecLtQEcPtCijfhzcLXAxTQ== -matrix-widget-api@^0.1.0-beta.7: - version "0.1.0-beta.7" - resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-0.1.0-beta.7.tgz#36f7ed968fe526b25f95bd643b9ff6e53f718083" - integrity sha512-aUyOoCgLs6+6Wxug19npJVMwZl7WCgj2EmXs0mw24bz5zOp5coRyuRvwA1gX0Rqh+AuW4vNcjC+iwwJxuvOQvg== +matrix-widget-api@^0.1.0-beta.8: + version "0.1.0-beta.8" + resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-0.1.0-beta.8.tgz#17e85c03c46353373890b869b1fd46162bdb0026" + integrity sha512-sWqyWs0RQqny/BimZUOxUd9BTJBzQmJlJ1i3lsSh1JBygV+aK5xQsONL97fc4i6/nwQPK72uCVDF+HwTtkpAbQ== dependencies: events "^3.2.0" From 75888fb7b63d03809a3263a74148bf837f1a27dc Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 13 Nov 2020 00:16:58 +0000 Subject: [PATCH 60/62] Fix BaseAvatar sometimes messing up and duplicating the url --- src/components/views/avatars/BaseAvatar.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/views/avatars/BaseAvatar.tsx b/src/components/views/avatars/BaseAvatar.tsx index 245c50576a..799a559263 100644 --- a/src/components/views/avatars/BaseAvatar.tsx +++ b/src/components/views/avatars/BaseAvatar.tsx @@ -51,7 +51,8 @@ const calculateUrls = (url, urls) => { _urls = urls || []; if (url) { - _urls.unshift(url); // put in urls[0] + // copy urls and put url first + _urls = [url, ..._urls]; } } From 91b1c8b8173b2e83b97602ac6a0eb5fa587cb86a Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 13 Nov 2020 15:19:34 -0700 Subject: [PATCH 61/62] Fix DM logic to always pick a more reliable DM room Fixes https://github.com/vector-im/element-web/issues/15605 --- src/components/views/dialogs/InviteDialog.js | 10 ++++++++-- src/components/views/right_panel/UserInfo.tsx | 14 ++----------- src/createRoom.ts | 20 +++++++++++++------ src/utils/membership.ts | 5 +++++ 4 files changed, 29 insertions(+), 20 deletions(-) diff --git a/src/components/views/dialogs/InviteDialog.js b/src/components/views/dialogs/InviteDialog.js index 99878569d3..e29ef143f2 100644 --- a/src/components/views/dialogs/InviteDialog.js +++ b/src/components/views/dialogs/InviteDialog.js @@ -31,7 +31,7 @@ import dis from "../../../dispatcher/dispatcher"; import IdentityAuthClient from "../../../IdentityAuthClient"; import Modal from "../../../Modal"; import {humanizeTime} from "../../../utils/humanize"; -import createRoom, {canEncryptToAllUsers, privateShouldBeEncrypted} from "../../../createRoom"; +import createRoom, {canEncryptToAllUsers, findDMForUser, privateShouldBeEncrypted} from "../../../createRoom"; import {inviteMultipleToRoom, showCommunityInviteDialog} from "../../../RoomInvite"; import {Key} from "../../../Keyboard"; import {Action} from "../../../dispatcher/actions"; @@ -41,6 +41,7 @@ import {CommunityPrototypeStore} from "../../../stores/CommunityPrototypeStore"; import SettingsStore from "../../../settings/SettingsStore"; import {UIFeature} from "../../../settings/UIFeature"; import CountlyAnalytics from "../../../CountlyAnalytics"; +import {Room} from "matrix-js-sdk/src/models/room"; // we have a number of types defined from the Matrix spec which can't reasonably be altered here. /* eslint-disable camelcase */ @@ -575,7 +576,12 @@ export default class InviteDialog extends React.PureComponent { const targetIds = targets.map(t => t.userId); // Check if there is already a DM with these people and reuse it if possible. - const existingRoom = DMRoomMap.shared().getDMRoomForIdentifiers(targetIds); + let existingRoom: Room; + if (targetIds.length === 1) { + existingRoom = findDMForUser(MatrixClientPeg.get(), targetIds[0]); + } else { + existingRoom = DMRoomMap.shared().getDMRoomForIdentifiers(targetIds); + } if (existingRoom) { dis.dispatch({ action: 'view_room', diff --git a/src/components/views/right_panel/UserInfo.tsx b/src/components/views/right_panel/UserInfo.tsx index 66b689ddb9..cdb4c43b09 100644 --- a/src/components/views/right_panel/UserInfo.tsx +++ b/src/components/views/right_panel/UserInfo.tsx @@ -28,7 +28,7 @@ import {EventTimeline} from 'matrix-js-sdk/src/models/event-timeline'; import dis from '../../../dispatcher/dispatcher'; import Modal from '../../../Modal'; import {_t} from '../../../languageHandler'; -import createRoom, {privateShouldBeEncrypted} from '../../../createRoom'; +import createRoom, { findDMForUser, privateShouldBeEncrypted } from '../../../createRoom'; import DMRoomMap from '../../../utils/DMRoomMap'; import AccessibleButton from '../elements/AccessibleButton'; import SdkConfig from '../../../SdkConfig'; @@ -105,17 +105,7 @@ export const getE2EStatus = (cli: MatrixClient, userId: string, devices: IDevice }; async function openDMForUser(matrixClient: MatrixClient, userId: string) { - const dmRooms = DMRoomMap.shared().getDMRoomsForUserId(userId); - const lastActiveRoom = dmRooms.reduce((lastActiveRoom, roomId) => { - const room = matrixClient.getRoom(roomId); - if (!room || room.getMyMembership() === "leave") { - return lastActiveRoom; - } - if (!lastActiveRoom || lastActiveRoom.getLastActiveTimestamp() < room.getLastActiveTimestamp()) { - return room; - } - return lastActiveRoom; - }, null); + const lastActiveRoom = findDMForUser(matrixClient, userId); if (lastActiveRoom) { dis.dispatch({ diff --git a/src/createRoom.ts b/src/createRoom.ts index a42fcc5e7b..699df0d799 100644 --- a/src/createRoom.ts +++ b/src/createRoom.ts @@ -15,20 +15,21 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {MatrixClient} from "matrix-js-sdk/src/client"; -import {Room} from "matrix-js-sdk/src/models/room"; +import { MatrixClient } from "matrix-js-sdk/src/client"; +import { Room } from "matrix-js-sdk/src/models/room"; -import {MatrixClientPeg} from './MatrixClientPeg'; +import { MatrixClientPeg } from './MatrixClientPeg'; import Modal from './Modal'; import * as sdk from './index'; import { _t } from './languageHandler'; import dis from "./dispatcher/dispatcher"; import * as Rooms from "./Rooms"; import DMRoomMap from "./utils/DMRoomMap"; -import {getAddressType} from "./UserAddress"; +import { getAddressType } from "./UserAddress"; import { getE2EEWellKnown } from "./utils/WellKnownUtils"; import GroupStore from "./stores/GroupStore"; import CountlyAnalytics from "./CountlyAnalytics"; +import { isJoinedOrNearlyJoined } from "./utils/membership"; // we define a number of interfaces which take their names from the js-sdk /* eslint-disable camelcase */ @@ -236,9 +237,16 @@ export function findDMForUser(client: MatrixClient, userId: string): Room { const roomIds = DMRoomMap.shared().getDMRoomsForUserId(userId); const rooms = roomIds.map(id => client.getRoom(id)); const suitableDMRooms = rooms.filter(r => { + // Validate that we are joined and the other person is also joined. We'll also make sure + // that the room also looks like a DM (until we have canonical DMs to tell us). For now, + // a DM is a room of two people that contains those two people exactly. This does mean + // that bots, assistants, etc will ruin a room's DM-ness, though this is a problem for + // canonical DMs to solve. if (r && r.getMyMembership() === "join") { - const member = r.getMember(userId); - return member && (member.membership === "invite" || member.membership === "join"); + const members = r.currentState.getMembers(); + const joinedMembers = members.filter(m => isJoinedOrNearlyJoined(m.membership)); + const otherMember = joinedMembers.find(m => m.userId === userId); + return otherMember && joinedMembers.length === 2; } return false; }).sort((r1, r2) => { diff --git a/src/utils/membership.ts b/src/utils/membership.ts index 68ac958490..696bd57880 100644 --- a/src/utils/membership.ts +++ b/src/utils/membership.ts @@ -78,6 +78,11 @@ export function getEffectiveMembership(membership: string): EffectiveMembership } } +export function isJoinedOrNearlyJoined(membership: string): boolean { + const effective = getEffectiveMembership(membership); + return effective === EffectiveMembership.Join || effective === EffectiveMembership.Invite; +} + export async function leaveRoomBehaviour(roomId: string) { let leavingAllVersions = true; const history = await MatrixClientPeg.get().getRoomUpgradeHistory(roomId); From 3ee25dadd46fb4b38b5fa582a52c1d656e9b1c66 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 18 Nov 2020 13:09:43 +0000 Subject: [PATCH 62/62] Shrink new room intro top margin to half for encryption bubble tile --- res/css/views/rooms/_NewRoomIntro.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/css/views/rooms/_NewRoomIntro.scss b/res/css/views/rooms/_NewRoomIntro.scss index af72a0dd69..4322ba341c 100644 --- a/res/css/views/rooms/_NewRoomIntro.scss +++ b/res/css/views/rooms/_NewRoomIntro.scss @@ -15,7 +15,7 @@ limitations under the License. */ .mx_NewRoomIntro { - margin: 80px 0 48px 64px; + margin: 40px 0 48px 64px; .mx_MiniAvatarUploader_hasAvatar:not(.mx_MiniAvatarUploader_busy):not(:hover) { &::before, &::after {