From 0d537ecbb3e9cb59084e468fec0499b8ee6afe7a Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Mon, 2 Dec 2019 17:27:12 +0000 Subject: [PATCH 01/93] Add bridge info tab --- .../settings/tabs/room/BridgeSettingsTab.js | 128 ++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 src/components/views/settings/tabs/room/BridgeSettingsTab.js diff --git a/src/components/views/settings/tabs/room/BridgeSettingsTab.js b/src/components/views/settings/tabs/room/BridgeSettingsTab.js new file mode 100644 index 0000000000..732d7b2947 --- /dev/null +++ b/src/components/views/settings/tabs/room/BridgeSettingsTab.js @@ -0,0 +1,128 @@ +/* +Copyright 2019 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from 'react'; +import PropTypes from 'prop-types'; +import {_t} from "../../../../../languageHandler"; +import MatrixClientPeg from "../../../../../MatrixClientPeg"; +import Pill from "../../../elements/Pill"; +import {makeUserPermalink} from "../../../../../utils/permalinks/Permalinks"; + +const BRIDGE_EVENT_TYPES = [ + "uk.half-shot.bridge", + // m.bridge +]; + +export default class BridgeSettingsTab extends React.Component { + static propTypes = { + roomId: PropTypes.string.isRequired, + }; + + constructor() { + super(); + + this.state = { + }; + } + + componentWillMount() { + + } + + _renderBridgeCard(event, room) { + const content = event.getContent(); + if (!content || !content.channel || !content.protocol) { + return null; + } + const protocolName = content.protocol.displayname || content.protocol.id; + const channelName = content.channel.displayname || content.channel.id; + const networkName = content.network ? " on " + (content.network.displayname || content.network.id) : ""; + let status = null; + if (content.status === "active") { + status = (

Status: Active

); + } else if (content.status === "disabled") { + status = (

Status: Disabled

); + } + + let creator = null; + if (content.creator) { + creator = (

+ This bridge was provisioned by +

); + } + + const bot = (

+ The bridge is managed by the bot user.

+ ); + + const chanAndNetworkInfo = ( +

Bridged into {channelName}{networkName}, on {protocolName}

+ ); + + return (
  • +
    +

    {channelName}{networkName} ({protocolName})

    +
    + {status} + {creator} + {bot} + {chanAndNetworkInfo} +
    +
    +
  • ); + } + + static getBridgeStateEvents(roomId) { + const client = MatrixClientPeg.get(); + const roomState = (client.getRoom(roomId)).currentState; + + const bridgeEvents = Array.concat(...BRIDGE_EVENT_TYPES.map((typeName) => + Object.values(roomState.events[typeName] || {}), + )); + + return bridgeEvents; + } + + render() { + // This settings tab will only be invoked if the following function returns more + // than 0 events, so no validation is needed at this stage. + const bridgeEvents = BridgeSettingsTab.getBridgeStateEvents(this.props.roomId); + const client = MatrixClientPeg.get(); + const room = client.getRoom(this.props.roomId); + + return ( +
    +
    {_t("Bridge Info")}
    +
    +

    Below is a list of bridges connected to this room.

    +
      + { bridgeEvents.map((event) => this._renderBridgeCard(event, room)) } +
    +
    +
    + ); + } +} From 7c35b16f4d272b048e23ecd969e0f7aea5e44983 Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Mon, 2 Dec 2019 17:27:23 +0000 Subject: [PATCH 02/93] Add bridge tab button --- src/components/views/dialogs/RoomSettingsDialog.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/components/views/dialogs/RoomSettingsDialog.js b/src/components/views/dialogs/RoomSettingsDialog.js index 740dc4d2c2..b262a1f078 100644 --- a/src/components/views/dialogs/RoomSettingsDialog.js +++ b/src/components/views/dialogs/RoomSettingsDialog.js @@ -24,6 +24,7 @@ import RolesRoomSettingsTab from "../settings/tabs/room/RolesRoomSettingsTab"; import GeneralRoomSettingsTab from "../settings/tabs/room/GeneralRoomSettingsTab"; import SecurityRoomSettingsTab from "../settings/tabs/room/SecurityRoomSettingsTab"; import NotificationSettingsTab from "../settings/tabs/room/NotificationSettingsTab"; +import BridgeSettingsTab from "../settings/tabs/room/BridgeSettingsTab"; import sdk from "../../../index"; import MatrixClientPeg from "../../../MatrixClientPeg"; import dis from "../../../dispatcher"; @@ -52,6 +53,7 @@ export default class RoomSettingsDialog extends React.Component { _getTabs() { const tabs = []; + const shouldShowBridgeIcon = BridgeSettingsTab.getBridgeStateEvents(this.props.roomId).length > 0; tabs.push(new Tab( _td("General"), @@ -73,6 +75,15 @@ export default class RoomSettingsDialog extends React.Component { "mx_RoomSettingsDialog_rolesIcon", , )); + + if (shouldShowBridgeIcon) { + tabs.push(new Tab( + _td("Bridge Info"), + "mx_RoomSettingsDialog_bridgesIcon", + , + )); + } + tabs.push(new Tab( _td("Advanced"), "mx_RoomSettingsDialog_warningIcon", From 626ab17ed38e9c8c11dd9accb70d38be7439f1ac Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Mon, 2 Dec 2019 17:27:31 +0000 Subject: [PATCH 03/93] Styling --- .../views/dialogs/_RoomSettingsDialog.scss | 17 +++++++ res/img/feather-customised/bridge.svg | 50 +++++++++++++++++++ 2 files changed, 67 insertions(+) create mode 100644 res/img/feather-customised/bridge.svg diff --git a/res/css/views/dialogs/_RoomSettingsDialog.scss b/res/css/views/dialogs/_RoomSettingsDialog.scss index 723eb237ad..8e648e8881 100644 --- a/res/css/views/dialogs/_RoomSettingsDialog.scss +++ b/res/css/views/dialogs/_RoomSettingsDialog.scss @@ -29,6 +29,11 @@ limitations under the License. mask-image: url('$(res)/img/feather-customised/users-sm.svg'); } +.mx_RoomSettingsDialog_bridgesIcon::before { + // This icon is pants, please improve :) + mask-image: url('$(res)/img/feather-customised/bridge.svg'); +} + .mx_RoomSettingsDialog_warningIcon::before { mask-image: url('$(res)/img/feather-customised/warning-triangle.svg'); } @@ -42,3 +47,15 @@ limitations under the License. padding-left: 40px; padding-right: 80px; } + + +.mx_RoomSettingsDialog_BridgeList { + padding: 0; +} + +.mx_RoomSettingsDialog_BridgeList li { + list-style-type: none; + padding: 0; + margin: 0; + border-bottom: 1px solid $panel-divider-color; +} \ No newline at end of file diff --git a/res/img/feather-customised/bridge.svg b/res/img/feather-customised/bridge.svg new file mode 100644 index 0000000000..f8f3468155 --- /dev/null +++ b/res/img/feather-customised/bridge.svg @@ -0,0 +1,50 @@ + + + + + + + image/svg+xml + + + + + + + + + + + From 2bc6e2e3326250fb55ebf3b4662fd42602db1ecf Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Mon, 2 Dec 2019 17:27:38 +0000 Subject: [PATCH 04/93] Add the one string I bothered to i18n --- src/i18n/strings/en_EN.json | 1 + 1 file changed, 1 insertion(+) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 182c761c5f..74ac452bb1 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -725,6 +725,7 @@ "Room version:": "Room version:", "Developer options": "Developer options", "Open Devtools": "Open Devtools", + "Bridge Info": "Bridge Info", "Room Addresses": "Room Addresses", "Publish this room to the public in %(domain)s's room directory?": "Publish this room to the public in %(domain)s's room directory?", "URL Previews": "URL Previews", From 9f2ccdf913dce10170a7d2465a2f97ac0936d77e Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Wed, 4 Dec 2019 15:02:36 +0000 Subject: [PATCH 05/93] Add support for displaying avatars and links in bridge info --- .../settings/tabs/room/BridgeSettingsTab.js | 39 +++++++++++++++++-- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/src/components/views/settings/tabs/room/BridgeSettingsTab.js b/src/components/views/settings/tabs/room/BridgeSettingsTab.js index 732d7b2947..459f11277c 100644 --- a/src/components/views/settings/tabs/room/BridgeSettingsTab.js +++ b/src/components/views/settings/tabs/room/BridgeSettingsTab.js @@ -20,6 +20,8 @@ import {_t} from "../../../../../languageHandler"; import MatrixClientPeg from "../../../../../MatrixClientPeg"; import Pill from "../../../elements/Pill"; import {makeUserPermalink} from "../../../../../utils/permalinks/Permalinks"; +import BaseAvatar from "../../../avatars/BaseAvatar"; +import { ContentRepo } from "matrix-js-sdk"; const BRIDGE_EVENT_TYPES = [ "uk.half-shot.bridge", @@ -47,9 +49,10 @@ export default class BridgeSettingsTab extends React.Component { if (!content || !content.channel || !content.protocol) { return null; } + const { channel, network } = content; const protocolName = content.protocol.displayname || content.protocol.id; - const channelName = content.channel.displayname || content.channel.id; - const networkName = content.network ? " on " + (content.network.displayname || content.network.id) : ""; + const channelName = channel.displayname || channel.id; + const networkName = network ? network.displayname || network.id : ""; let status = null; if (content.status === "active") { status = (

    Status: Active

    ); @@ -78,13 +81,41 @@ export default class BridgeSettingsTab extends React.Component { /> bot user.

    ); + const channelLink = channel.external_url ? ({channelName}) : channelName; + const networkLink = network && network.external_url ? ({networkName}) + : networkName; + const chanAndNetworkInfo = ( -

    Bridged into {channelName}{networkName}, on {protocolName}

    +

    Bridged into {channelLink} {networkLink}, on {protocolName}

    ); + let networkIcon = null; + if (networkName && network.avatar) { + const avatarUrl = ContentRepo.getHttpUriForMxc( + MatrixClientPeg.get().getHomeserverUrl(), + network.avatar, 32, 32, "crop", + ); + networkIcon = ; + } + + let channelIcon = null; + if (channel.avatar) { + const avatarUrl = ContentRepo.getHttpUriForMxc( + MatrixClientPeg.get().getHomeserverUrl(), + channel.avatar, 32, 32, "crop", + ); + console.log(channel.avatar); + channelIcon = ; + } + return (
  • -

    {channelName}{networkName} ({protocolName})

    +

    {channelIcon} {channelName} {networkName ? ` on ${networkName}` : ""} {networkIcon}

    +

    Connected via {protocolName}

    {status} {creator} From ce21ce8bbea93c9a2b48b0c0c6f341f4360d375b Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Thu, 5 Dec 2019 00:28:29 +0000 Subject: [PATCH 06/93] Lint --- .../views/settings/tabs/room/BridgeSettingsTab.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/components/views/settings/tabs/room/BridgeSettingsTab.js b/src/components/views/settings/tabs/room/BridgeSettingsTab.js index 459f11277c..031e2651c3 100644 --- a/src/components/views/settings/tabs/room/BridgeSettingsTab.js +++ b/src/components/views/settings/tabs/room/BridgeSettingsTab.js @@ -80,10 +80,15 @@ export default class BridgeSettingsTab extends React.Component { shouldShowPillAvatar={true} /> bot user.

    ); + let channelLink = channelName; + if (channel.external_url) { + channelLink = {channelName}; + } - const channelLink = channel.external_url ? ({channelName}) : channelName; - const networkLink = network && network.external_url ? ({networkName}) - : networkName; + let networkLink = networkName; + if (network && network.external_url) { + networkLink = {networkName}; + } const chanAndNetworkInfo = (

    Bridged into {channelLink} {networkLink}, on {protocolName}

    From d9943754f7c936fae5643ae6c16205d99467d9f9 Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Mon, 9 Dec 2019 13:28:16 +0000 Subject: [PATCH 07/93] Remove `status` as it's no longer part of the MSC --- .../views/settings/tabs/room/BridgeSettingsTab.js | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/components/views/settings/tabs/room/BridgeSettingsTab.js b/src/components/views/settings/tabs/room/BridgeSettingsTab.js index 031e2651c3..a165a1db44 100644 --- a/src/components/views/settings/tabs/room/BridgeSettingsTab.js +++ b/src/components/views/settings/tabs/room/BridgeSettingsTab.js @@ -53,12 +53,6 @@ export default class BridgeSettingsTab extends React.Component { const protocolName = content.protocol.displayname || content.protocol.id; const channelName = channel.displayname || channel.id; const networkName = network ? network.displayname || network.id : ""; - let status = null; - if (content.status === "active") { - status = (

    Status: Active

    ); - } else if (content.status === "disabled") { - status = (

    Status: Disabled

    ); - } let creator = null; if (content.creator) { @@ -122,7 +116,6 @@ export default class BridgeSettingsTab extends React.Component {

    {channelIcon} {channelName} {networkName ? ` on ${networkName}` : ""} {networkIcon}

    Connected via {protocolName}

    - {status} {creator} {bot} {chanAndNetworkInfo} From 7ee5f7ba38e25cf5774e03008ba375c7d5e3f791 Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Mon, 9 Dec 2019 13:28:43 +0000 Subject: [PATCH 08/93] Add feature flag --- src/components/views/dialogs/RoomSettingsDialog.js | 4 +++- src/i18n/strings/en_EN.json | 3 ++- src/settings/Settings.js | 6 ++++++ 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/components/views/dialogs/RoomSettingsDialog.js b/src/components/views/dialogs/RoomSettingsDialog.js index b262a1f078..2952439076 100644 --- a/src/components/views/dialogs/RoomSettingsDialog.js +++ b/src/components/views/dialogs/RoomSettingsDialog.js @@ -28,6 +28,7 @@ import BridgeSettingsTab from "../settings/tabs/room/BridgeSettingsTab"; import sdk from "../../../index"; import MatrixClientPeg from "../../../MatrixClientPeg"; import dis from "../../../dispatcher"; +import SettingsStore from "../settings/SettingsStore"; export default class RoomSettingsDialog extends React.Component { static propTypes = { @@ -53,7 +54,8 @@ export default class RoomSettingsDialog extends React.Component { _getTabs() { const tabs = []; - const shouldShowBridgeIcon = BridgeSettingsTab.getBridgeStateEvents(this.props.roomId).length > 0; + const featureFlag = SettingsStore.isFeatureEnabled("feature_bridge_state"); + const shouldShowBridgeIcon = featureFlag && BridgeSettingsTab.getBridgeStateEvents(this.props.roomId).length > 0; tabs.push(new Tab( _td("General"), diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 74ac452bb1..14ba96fa4a 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1932,5 +1932,6 @@ "If you didn't remove the recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings.": "If you didn't remove the recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings.", "Failed to set direct chat tag": "Failed to set direct chat tag", "Failed to remove tag %(tagName)s from room": "Failed to remove tag %(tagName)s from room", - "Failed to add tag %(tagName)s to room": "Failed to add tag %(tagName)s to room" + "Failed to add tag %(tagName)s to room": "Failed to add tag %(tagName)s to room", + "Show info about bridges in room settings": "Show info about bridges in room settings" } diff --git a/src/settings/Settings.js b/src/settings/Settings.js index b02ab82400..94cc5b587d 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -154,6 +154,12 @@ export const SETTINGS = { displayName: _td("Enable local event indexing and E2EE search (requires restart)"), default: false, }, + "feature_bridge_state": { + isFeature: true, + supportedLevels: LEVELS_FEATURE, + displayName: _td("Show info about bridges in room settings"), + default: false, + }, "useCiderComposer": { displayName: _td("Use the new, faster, composer for writing messages"), supportedLevels: LEVELS_ACCOUNT_SETTINGS, From 6225e402ccdf7781cc6062c339265d63da455fc9 Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Mon, 9 Dec 2019 13:54:21 +0000 Subject: [PATCH 09/93] i18n'ed all over the plaace --- .../views/dialogs/RoomSettingsDialog.js | 2 +- .../settings/tabs/room/BridgeSettingsTab.js | 58 ++++++++++++------- src/i18n/strings/en_EN.json | 10 +++- 3 files changed, 45 insertions(+), 25 deletions(-) diff --git a/src/components/views/dialogs/RoomSettingsDialog.js b/src/components/views/dialogs/RoomSettingsDialog.js index 2952439076..9ac2b17f23 100644 --- a/src/components/views/dialogs/RoomSettingsDialog.js +++ b/src/components/views/dialogs/RoomSettingsDialog.js @@ -28,7 +28,7 @@ import BridgeSettingsTab from "../settings/tabs/room/BridgeSettingsTab"; import sdk from "../../../index"; import MatrixClientPeg from "../../../MatrixClientPeg"; import dis from "../../../dispatcher"; -import SettingsStore from "../settings/SettingsStore"; +import SettingsStore from "../../../settings/SettingsStore"; export default class RoomSettingsDialog extends React.Component { static propTypes = { diff --git a/src/components/views/settings/tabs/room/BridgeSettingsTab.js b/src/components/views/settings/tabs/room/BridgeSettingsTab.js index a165a1db44..82382e7828 100644 --- a/src/components/views/settings/tabs/room/BridgeSettingsTab.js +++ b/src/components/views/settings/tabs/room/BridgeSettingsTab.js @@ -52,28 +52,31 @@ export default class BridgeSettingsTab extends React.Component { const { channel, network } = content; const protocolName = content.protocol.displayname || content.protocol.id; const channelName = channel.displayname || channel.id; - const networkName = network ? network.displayname || network.id : ""; + const networkName = network ? network.displayname || network.id : protocolName; let creator = null; if (content.creator) { - creator = (

    - This bridge was provisioned by -

    ); + const pill = ; + creator = (

    { + _t("This bridge was provisioned by %(pill)s", { + pill, + }) + }

    ); } - const bot = (

    - The bridge is managed by the bot user.

    - ); + const bot = (

    {_t("This bridge is managed by the %(pill)s bot user.", { + pill: , + })}

    ); let channelLink = channelName; if (channel.external_url) { channelLink = {channelName}; @@ -85,7 +88,11 @@ export default class BridgeSettingsTab extends React.Component { } const chanAndNetworkInfo = ( -

    Bridged into {channelLink} {networkLink}, on {protocolName}

    + (_t("Bridged into %(channelLink)s %(networkLink)s, on %(protocolName)s", { + channelLink, + networkLink, + protocolName, + })) ); let networkIcon = null; @@ -111,14 +118,21 @@ export default class BridgeSettingsTab extends React.Component { url={ avatarUrl } />; } + const heading = _t("Connected to %(channelIcon)s %(channelName)s on %(networkIcon)s %(networkName)s", { + channelIcon, + channelName, + networkName, + networkIcon, + }); + return (
  • -

    {channelIcon} {channelName} {networkName ? ` on ${networkName}` : ""} {networkIcon}

    -

    Connected via {protocolName}

    +

    {heading}

    +

    {_t("Connected via %(protocolName)s", { protocolName })}

    {creator} {bot} - {chanAndNetworkInfo} +

    {chanAndNetworkInfo}

  • ); @@ -146,7 +160,7 @@ export default class BridgeSettingsTab extends React.Component {
    {_t("Bridge Info")}
    -

    Below is a list of bridges connected to this room.

    +

    { _t("Below is a list of bridges connected to this room.") }

      { bridgeEvents.map((event) => this._renderBridgeCard(event, room)) }
    diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 14ba96fa4a..b76310ec27 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -342,6 +342,7 @@ "Send verification requests in direct message, including a new verification UX in the member panel.": "Send verification requests in direct message, including a new verification UX in the member panel.", "Enable cross-signing to verify per-user instead of per-device (in development)": "Enable cross-signing to verify per-user instead of per-device (in development)", "Enable local event indexing and E2EE search (requires restart)": "Enable local event indexing and E2EE search (requires restart)", + "Show info about bridges in room settings": "Show info about bridges in room settings", "Use the new, faster, composer for writing messages": "Use the new, faster, composer for writing messages", "Enable Emoji suggestions while typing": "Enable Emoji suggestions while typing", "Use compact timeline layout": "Use compact timeline layout", @@ -725,7 +726,13 @@ "Room version:": "Room version:", "Developer options": "Developer options", "Open Devtools": "Open Devtools", + "This bridge was provisioned by %(pill)s": "This bridge was provisioned by %(pill)s", + "This bridge is managed by the %(pill)s bot user.": "This bridge is managed by the %(pill)s bot user.", + "Bridged into %(channelLink)s %(networkLink)s, on %(protocolName)s": "Bridged into %(channelLink)s %(networkLink)s, on %(protocolName)s", + "Connected to %(channelIcon)s %(channelName)s on %(networkIcon)s %(networkName)s": "Connected to %(channelIcon)s %(channelName)s on %(networkIcon)s %(networkName)s", + "Connected via %(protocolName)s": "Connected via %(protocolName)s", "Bridge Info": "Bridge Info", + "Below is a list of bridges connected to this room.": "Below is a list of bridges connected to this room.", "Room Addresses": "Room Addresses", "Publish this room to the public in %(domain)s's room directory?": "Publish this room to the public in %(domain)s's room directory?", "URL Previews": "URL Previews", @@ -1932,6 +1939,5 @@ "If you didn't remove the recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings.": "If you didn't remove the recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings.", "Failed to set direct chat tag": "Failed to set direct chat tag", "Failed to remove tag %(tagName)s from room": "Failed to remove tag %(tagName)s from room", - "Failed to add tag %(tagName)s to room": "Failed to add tag %(tagName)s to room", - "Show info about bridges in room settings": "Show info about bridges in room settings" + "Failed to add tag %(tagName)s to room": "Failed to add tag %(tagName)s to room" } From 2e8d66fa3635c6f7bd97be1362d4f5f86f42865b Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 17 Dec 2019 16:54:59 +0000 Subject: [PATCH 10/93] Null-guard member mention pills for rooms you have left (notif panel) --- src/components/views/elements/Pill.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/elements/Pill.js b/src/components/views/elements/Pill.js index 12830488b1..a065602f68 100644 --- a/src/components/views/elements/Pill.js +++ b/src/components/views/elements/Pill.js @@ -127,7 +127,7 @@ const Pill = createReactClass({ } break; case Pill.TYPE_USER_MENTION: { - const localMember = nextProps.room.getMember(resourceId); + const localMember = nextProps.room ? nextProps.room.getMember(resourceId) : undefined; member = localMember; if (!localMember) { member = new RoomMember(null, resourceId); From 937b32663c49c7cd69c211b83df52e56a61b352f Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 17 Dec 2019 17:26:12 +0000 Subject: [PATCH 11/93] Migrate away from Legacy React Contexts --- src/components/structures/EmbeddedPage.js | 8 +- src/components/structures/LeftPanel.js | 5 - src/components/structures/LoggedInView.js | 55 ++- src/components/structures/MatrixChat.js | 10 - src/components/structures/MyGroups.js | 9 +- src/components/structures/RightPanel.js | 18 +- src/components/structures/RoomDirectory.js | 11 +- src/components/structures/RoomView.js | 99 ++--- src/components/structures/TagPanel.js | 21 +- src/components/views/avatars/BaseAvatar.js | 10 +- .../avatars/MemberStatusMessageAvatar.js | 4 +- .../GroupInviteTileContextMenu.js | 4 +- .../context_menus/StatusMessageContextMenu.js | 4 +- .../views/context_menus/TagTileContextMenu.js | 8 +- src/components/views/dialogs/BaseDialog.js | 73 ++- .../views/dialogs/BugReportDialog.js | 4 +- .../views/dialogs/DeactivateAccountDialog.js | 4 +- .../views/dialogs/DevtoolsDialog.js | 131 +++--- .../views/dialogs/ReportEventDialog.js | 4 +- .../views/elements/EditableTextContainer.js | 6 +- src/components/views/elements/Flair.js | 13 +- src/components/views/elements/Pill.js | 34 +- src/components/views/elements/ReplyThread.js | 17 +- .../views/elements/SyntaxHighlight.js | 4 +- src/components/views/elements/TagTile.js | 10 +- .../views/groups/GroupInviteTile.js | 10 +- .../views/groups/GroupMemberInfo.js | 12 +- .../views/groups/GroupMemberTile.js | 8 +- src/components/views/groups/GroupRoomInfo.js | 8 +- src/components/views/groups/GroupRoomTile.js | 8 +- src/components/views/groups/GroupTile.js | 9 +- .../views/groups/GroupUserSettings.js | 9 +- src/components/views/messages/MFileBody.js | 10 +- src/components/views/messages/MImageBody.js | 20 +- .../views/messages/MessageActionBar.js | 10 +- .../views/messages/SenderProfile.js | 14 +- src/components/views/right_panel/UserInfo.js | 414 +++++++++--------- .../room_settings/RelatedGroupSettings.js | 11 +- .../views/rooms/BasicMessageComposer.js | 4 +- .../views/rooms/EditMessageComposer.js | 17 +- src/components/views/rooms/EventTile.js | 23 +- src/components/views/rooms/MemberInfo.js | 79 ++-- src/components/views/rooms/MessageComposer.js | 8 +- .../views/rooms/MessageComposerInput.js | 4 +- src/components/views/rooms/ReplyPreview.js | 4 +- .../views/rooms/SendMessageComposer.js | 16 +- .../views/rooms/SlateMessageComposer.js | 8 +- src/components/views/settings/DevicesPanel.js | 4 +- .../views/settings/DevicesPanelEntry.js | 4 +- .../tabs/room/GeneralRoomSettingsTab.js | 21 +- .../tabs/user/FlairUserSettingsTab.js | 17 - .../MatrixClientContext.js} | 18 +- src/contexts/RoomContext.js | 25 ++ .../structures/MessagePanel-test.js | 23 +- test/test-utils.js | 17 +- 55 files changed, 651 insertions(+), 750 deletions(-) rename src/{utils/withLegacyMatrixClient.js => contexts/MatrixClientContext.js} (51%) create mode 100644 src/contexts/RoomContext.js diff --git a/src/components/structures/EmbeddedPage.js b/src/components/structures/EmbeddedPage.js index ecc01a443d..63767255e2 100644 --- a/src/components/structures/EmbeddedPage.js +++ b/src/components/structures/EmbeddedPage.js @@ -26,8 +26,8 @@ import sanitizeHtml from 'sanitize-html'; import sdk from '../../index'; import dis from '../../dispatcher'; import MatrixClientPeg from '../../MatrixClientPeg'; -import { MatrixClient } from 'matrix-js-sdk'; import classnames from 'classnames'; +import MatrixClientContext from "../../contexts/MatrixClientContext"; export default class EmbeddedPage extends React.PureComponent { static propTypes = { @@ -39,9 +39,7 @@ export default class EmbeddedPage extends React.PureComponent { scrollbar: PropTypes.bool, }; - static contextTypes = { - matrixClient: PropTypes.instanceOf(MatrixClient), - }; + static contextType = MatrixClientContext; constructor(props) { super(props); @@ -104,7 +102,7 @@ export default class EmbeddedPage extends React.PureComponent { render() { // HACK: Workaround for the context's MatrixClient not updating. - const client = this.context.matrixClient || MatrixClientPeg.get(); + const client = this.context || MatrixClientPeg.get(); const isGuest = client ? client.isGuest() : true; const className = this.props.className; const classes = classnames({ diff --git a/src/components/structures/LeftPanel.js b/src/components/structures/LeftPanel.js index a0ad2b5c81..dd842be1ac 100644 --- a/src/components/structures/LeftPanel.js +++ b/src/components/structures/LeftPanel.js @@ -19,7 +19,6 @@ import React from 'react'; import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; import classNames from 'classnames'; -import { MatrixClient } from 'matrix-js-sdk'; import { Key } from '../../Keyboard'; import sdk from '../../index'; import dis from '../../dispatcher'; @@ -39,10 +38,6 @@ const LeftPanel = createReactClass({ collapsed: PropTypes.bool.isRequired, }, - contextTypes: { - matrixClient: PropTypes.instanceOf(MatrixClient), - }, - getInitialState: function() { return { searchFilter: '', diff --git a/src/components/structures/LoggedInView.js b/src/components/structures/LoggedInView.js index df2eebd7c9..7261af3bf0 100644 --- a/src/components/structures/LoggedInView.js +++ b/src/components/structures/LoggedInView.js @@ -38,6 +38,7 @@ import TagOrderActions from '../../actions/TagOrderActions'; import RoomListActions from '../../actions/RoomListActions'; import ResizeHandle from '../views/elements/ResizeHandle'; import {Resizer, CollapseDistributor} from '../../resizer'; +import MatrixClientContext from "../../contexts/MatrixClientContext"; // We need to fetch each pinned message individually (if we don't already have it) // so each pinned message may trigger a request. Limit the number per room for sanity. // NB. this is just for server notices rather than pinned messages in general. @@ -77,21 +78,6 @@ const LoggedInView = createReactClass({ // and lots and lots of other stuff. }, - childContextTypes: { - matrixClient: PropTypes.instanceOf(MatrixClient), - authCache: PropTypes.object, - }, - - getChildContext: function() { - return { - matrixClient: this._matrixClient, - authCache: { - auth: {}, - lastUpdate: 0, - }, - }; - }, - getInitialState: function() { return { // use compact timeline view @@ -631,21 +617,30 @@ const LoggedInView = createReactClass({ } return ( -
    - { topBar } - - -
    - - - { pageElement } -
    -
    -
    + +
    + { topBar } + + +
    + + + { pageElement } +
    +
    +
    +
    ); }, }); diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 82a682f9ab..d9ff16d2f8 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -150,16 +150,6 @@ export default createReactClass({ makeRegistrationUrl: PropTypes.func.isRequired, }, - childContextTypes: { - appConfig: PropTypes.object, - }, - - getChildContext: function() { - return { - appConfig: this.props.config, - }; - }, - getInitialState: function() { const s = { // the master view we are showing. diff --git a/src/components/structures/MyGroups.js b/src/components/structures/MyGroups.js index 63ae14ba09..d957e76dfb 100644 --- a/src/components/structures/MyGroups.js +++ b/src/components/structures/MyGroups.js @@ -17,12 +17,11 @@ limitations under the License. import React from 'react'; import createReactClass from 'create-react-class'; -import PropTypes from 'prop-types'; -import { MatrixClient } from 'matrix-js-sdk'; import sdk from '../../index'; import { _t } from '../../languageHandler'; import dis from '../../dispatcher'; import AccessibleButton from '../views/elements/AccessibleButton'; +import MatrixClientContext from "../../contexts/MatrixClientContext"; export default createReactClass({ displayName: 'MyGroups', @@ -34,8 +33,8 @@ export default createReactClass({ }; }, - contextTypes: { - matrixClient: PropTypes.instanceOf(MatrixClient).isRequired, + statics: { + contextType: MatrixClientContext, }, componentWillMount: function() { @@ -47,7 +46,7 @@ export default createReactClass({ }, _fetch: function() { - this.context.matrixClient.getJoinedGroups().then((result) => { + this.context.getJoinedGroups().then((result) => { this.setState({groups: result.groups, error: null}); }, (err) => { if (err.errcode === 'M_GUEST_ACCESS_FORBIDDEN') { diff --git a/src/components/structures/RightPanel.js b/src/components/structures/RightPanel.js index 1745c9d7dc..7bd3172faa 100644 --- a/src/components/structures/RightPanel.js +++ b/src/components/structures/RightPanel.js @@ -23,13 +23,13 @@ import PropTypes from 'prop-types'; import classNames from 'classnames'; import sdk from '../../index'; import dis from '../../dispatcher'; -import { MatrixClient } from 'matrix-js-sdk'; import RateLimitedFunc from '../../ratelimitedfunc'; import { showGroupInviteDialog, showGroupAddRoomDialog } from '../../GroupAddressPicker'; import GroupStore from '../../stores/GroupStore'; import SettingsStore from "../../settings/SettingsStore"; import {RIGHT_PANEL_PHASES, RIGHT_PANEL_PHASES_NO_ARGS} from "../../stores/RightPanelStorePhases"; import RightPanelStore from "../../stores/RightPanelStore"; +import MatrixClientContext from "../../contexts/MatrixClientContext"; export default class RightPanel extends React.Component { static get propTypes() { @@ -40,14 +40,10 @@ export default class RightPanel extends React.Component { }; } - static get contextTypes() { - return { - matrixClient: PropTypes.instanceOf(MatrixClient), - }; - } + static contextType = MatrixClientContext; - constructor(props, context) { - super(props, context); + constructor(props) { + super(props); this.state = { phase: this._getPhaseFromProps(), isUserPrivilegedInGroup: null, @@ -93,15 +89,15 @@ export default class RightPanel extends React.Component { componentWillMount() { this.dispatcherRef = dis.register(this.onAction); - const cli = this.context.matrixClient; + const cli = this.context; cli.on("RoomState.members", this.onRoomStateMember); this._initGroupStore(this.props.groupId); } componentWillUnmount() { dis.unregister(this.dispatcherRef); - if (this.context.matrixClient) { - this.context.matrixClient.removeListener("RoomState.members", this.onRoomStateMember); + if (this.context) { + this.context.removeListener("RoomState.members", this.onRoomStateMember); } this._unregisterGroupStore(this.props.groupId); } diff --git a/src/components/structures/RoomDirectory.js b/src/components/structures/RoomDirectory.js index 2c885f7eb2..cec016c3cf 100644 --- a/src/components/structures/RoomDirectory.js +++ b/src/components/structures/RoomDirectory.js @@ -30,6 +30,7 @@ import PropTypes from 'prop-types'; import { _t } from '../../languageHandler'; import { instanceForInstanceId, protocolNameForInstanceId } from '../../utils/DirectoryUtils'; import Analytics from '../../Analytics'; +import MatrixClientContext from "../../contexts/MatrixClientContext"; const MAX_NAME_LENGTH = 80; const MAX_TOPIC_LENGTH = 160; @@ -65,16 +66,6 @@ module.exports = createReactClass({ }; }, - childContextTypes: { - matrixClient: PropTypes.object, - }, - - getChildContext: function() { - return { - matrixClient: MatrixClientPeg.get(), - }; - }, - componentWillMount: function() { this._unmounted = false; this.nextBatch = null; diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 739519a2b3..8d59e42c3e 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -28,7 +28,6 @@ import createReactClass from 'create-react-class'; import ReactDOM from 'react-dom'; import PropTypes from 'prop-types'; import classNames from 'classnames'; -import {Room} from "matrix-js-sdk"; import { _t } from '../../languageHandler'; import {RoomPermalinkCreator} from '../../utils/permalinks/Permalinks'; @@ -55,6 +54,7 @@ import SettingsStore, {SettingLevel} from "../../settings/SettingsStore"; import WidgetUtils from '../../utils/WidgetUtils'; import AccessibleButton from "../views/elements/AccessibleButton"; import RightPanelStore from "../../stores/RightPanelStore"; +import RoomContext from "../../contexts/RoomContext"; const DEBUG = false; let debuglog = function() {}; @@ -66,12 +66,6 @@ if (DEBUG) { debuglog = console.log.bind(console); } -const RoomContext = PropTypes.shape({ - canReact: PropTypes.bool.isRequired, - canReply: PropTypes.bool.isRequired, - room: PropTypes.instanceOf(Room), -}); - module.exports = createReactClass({ displayName: 'RoomView', propTypes: { @@ -169,21 +163,6 @@ module.exports = createReactClass({ }; }, - childContextTypes: { - room: RoomContext, - }, - - getChildContext: function() { - const {canReact, canReply, room} = this.state; - return { - room: { - canReact, - canReply, - room, - }, - }; - }, - componentWillMount: function() { this.dispatcherRef = dis.register(this.onAction); MatrixClientPeg.get().on("Room", this.onRoom); @@ -1989,45 +1968,47 @@ module.exports = createReactClass({ : null; return ( -
    - - - -
    - {auxPanel} -
    - {topUnreadMessagesBar} - {jumpToBottom} - {messagePanel} - {searchResultsPanel} -
    -
    -
    -
    - {statusBar} + +
    + + + +
    + {auxPanel} +
    + {topUnreadMessagesBar} + {jumpToBottom} + {messagePanel} + {searchResultsPanel}
    +
    +
    +
    + {statusBar} +
    +
    + {previewBar} + {messageComposer}
    - {previewBar} - {messageComposer} -
    -
    -
    -
    + + +
    + ); }, }); diff --git a/src/components/structures/TagPanel.js b/src/components/structures/TagPanel.js index a758092dc8..081796cb85 100644 --- a/src/components/structures/TagPanel.js +++ b/src/components/structures/TagPanel.js @@ -16,8 +16,6 @@ limitations under the License. import React from 'react'; import createReactClass from 'create-react-class'; -import PropTypes from 'prop-types'; -import { MatrixClient } from 'matrix-js-sdk'; import TagOrderStore from '../../stores/TagOrderStore'; import GroupActions from '../../actions/GroupActions'; @@ -28,12 +26,13 @@ import { _t } from '../../languageHandler'; import { Droppable } from 'react-beautiful-dnd'; import classNames from 'classnames'; +import MatrixClientContext from "../../contexts/MatrixClientContext"; const TagPanel = createReactClass({ displayName: 'TagPanel', - contextTypes: { - matrixClient: PropTypes.instanceOf(MatrixClient), + statics: { + contextType: MatrixClientContext, }, getInitialState() { @@ -45,8 +44,8 @@ const TagPanel = createReactClass({ componentWillMount: function() { this.unmounted = false; - this.context.matrixClient.on("Group.myMembership", this._onGroupMyMembership); - this.context.matrixClient.on("sync", this._onClientSync); + this.context.on("Group.myMembership", this._onGroupMyMembership); + this.context.on("sync", this._onClientSync); this._tagOrderStoreToken = TagOrderStore.addListener(() => { if (this.unmounted) { @@ -58,13 +57,13 @@ const TagPanel = createReactClass({ }); }); // This could be done by anything with a matrix client - dis.dispatch(GroupActions.fetchJoinedGroups(this.context.matrixClient)); + dis.dispatch(GroupActions.fetchJoinedGroups(this.context)); }, componentWillUnmount() { this.unmounted = true; - this.context.matrixClient.removeListener("Group.myMembership", this._onGroupMyMembership); - this.context.matrixClient.removeListener("sync", this._onClientSync); + this.context.removeListener("Group.myMembership", this._onGroupMyMembership); + this.context.removeListener("sync", this._onClientSync); if (this._filterStoreToken) { this._filterStoreToken.remove(); } @@ -72,7 +71,7 @@ const TagPanel = createReactClass({ _onGroupMyMembership() { if (this.unmounted) return; - dis.dispatch(GroupActions.fetchJoinedGroups(this.context.matrixClient)); + dis.dispatch(GroupActions.fetchJoinedGroups(this.context)); }, _onClientSync(syncState, prevState) { @@ -81,7 +80,7 @@ const TagPanel = createReactClass({ const reconnected = syncState !== "ERROR" && prevState !== syncState; if (reconnected) { // Load joined groups - dis.dispatch(GroupActions.fetchJoinedGroups(this.context.matrixClient)); + dis.dispatch(GroupActions.fetchJoinedGroups(this.context)); } }, diff --git a/src/components/views/avatars/BaseAvatar.js b/src/components/views/avatars/BaseAvatar.js index 82db78615e..51375eb3fa 100644 --- a/src/components/views/avatars/BaseAvatar.js +++ b/src/components/views/avatars/BaseAvatar.js @@ -19,10 +19,10 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; import createReactClass from 'create-react-class'; -import { MatrixClient } from 'matrix-js-sdk'; import AvatarLogic from '../../../Avatar'; import SettingsStore from "../../../settings/SettingsStore"; import AccessibleButton from '../elements/AccessibleButton'; +import MatrixClientContext from "../../../contexts/MatrixClientContext"; module.exports = createReactClass({ displayName: 'BaseAvatar', @@ -40,8 +40,8 @@ module.exports = createReactClass({ defaultToInitialLetter: PropTypes.bool, // true to add default url }, - contextTypes: { - matrixClient: PropTypes.instanceOf(MatrixClient), + statics: { + contextType: MatrixClientContext, }, getDefaultProps: function() { @@ -59,12 +59,12 @@ module.exports = createReactClass({ componentDidMount() { this.unmounted = false; - this.context.matrixClient.on('sync', this.onClientSync); + this.context.on('sync', this.onClientSync); }, componentWillUnmount() { this.unmounted = true; - this.context.matrixClient.removeListener('sync', this.onClientSync); + this.context.removeListener('sync', this.onClientSync); }, componentWillReceiveProps: function(nextProps) { diff --git a/src/components/views/avatars/MemberStatusMessageAvatar.js b/src/components/views/avatars/MemberStatusMessageAvatar.js index ed73dd33b9..245d869419 100644 --- a/src/components/views/avatars/MemberStatusMessageAvatar.js +++ b/src/components/views/avatars/MemberStatusMessageAvatar.js @@ -38,8 +38,8 @@ export default class MemberStatusMessageAvatar extends React.Component { resizeMethod: 'crop', }; - constructor(props, context) { - super(props, context); + constructor(props) { + super(props); this.state = { hasStatus: this.hasStatus, diff --git a/src/components/views/context_menus/GroupInviteTileContextMenu.js b/src/components/views/context_menus/GroupInviteTileContextMenu.js index 3feffbc0d9..3c0fd081b4 100644 --- a/src/components/views/context_menus/GroupInviteTileContextMenu.js +++ b/src/components/views/context_menus/GroupInviteTileContextMenu.js @@ -31,8 +31,8 @@ export default class GroupInviteTileContextMenu extends React.Component { onFinished: PropTypes.func, }; - constructor(props, context) { - super(props, context); + constructor(props) { + super(props); this._onClickReject = this._onClickReject.bind(this); } diff --git a/src/components/views/context_menus/StatusMessageContextMenu.js b/src/components/views/context_menus/StatusMessageContextMenu.js index 441220c95e..31ba788ec7 100644 --- a/src/components/views/context_menus/StatusMessageContextMenu.js +++ b/src/components/views/context_menus/StatusMessageContextMenu.js @@ -27,8 +27,8 @@ export default class StatusMessageContextMenu extends React.Component { user: PropTypes.object, }; - constructor(props, context) { - super(props, context); + constructor(props) { + super(props); this.state = { message: this.comittedStatusMessage, diff --git a/src/components/views/context_menus/TagTileContextMenu.js b/src/components/views/context_menus/TagTileContextMenu.js index 1af0c9ae66..388d8aaf3d 100644 --- a/src/components/views/context_menus/TagTileContextMenu.js +++ b/src/components/views/context_menus/TagTileContextMenu.js @@ -17,12 +17,12 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; -import { MatrixClient } from 'matrix-js-sdk'; import { _t } from '../../../languageHandler'; import dis from '../../../dispatcher'; import TagOrderActions from '../../../actions/TagOrderActions'; import sdk from '../../../index'; import {MenuItem} from "../../structures/ContextMenu"; +import MatrixClientContext from "../../../contexts/MatrixClientContext"; export default class TagTileContextMenu extends React.Component { static propTypes = { @@ -31,9 +31,7 @@ export default class TagTileContextMenu extends React.Component { onFinished: PropTypes.func.isRequired, }; - static contextTypes = { - matrixClient: PropTypes.instanceOf(MatrixClient), - }; + static contextType = MatrixClientContext; constructor() { super(); @@ -51,7 +49,7 @@ export default class TagTileContextMenu extends React.Component { } _onRemoveClick() { - dis.dispatch(TagOrderActions.removeTag(this.context.matrixClient, this.props.tag)); + dis.dispatch(TagOrderActions.removeTag(this.context, this.props.tag)); this.props.onFinished(); } diff --git a/src/components/views/dialogs/BaseDialog.js b/src/components/views/dialogs/BaseDialog.js index 6ba0b322c4..a9f7fbf4b3 100644 --- a/src/components/views/dialogs/BaseDialog.js +++ b/src/components/views/dialogs/BaseDialog.js @@ -22,12 +22,11 @@ import FocusLock from 'react-focus-lock'; import PropTypes from 'prop-types'; import classNames from 'classnames'; -import { MatrixClient } from 'matrix-js-sdk'; - import { Key } from '../../../Keyboard'; import AccessibleButton from '../elements/AccessibleButton'; import MatrixClientPeg from '../../../MatrixClientPeg'; import { _t } from "../../../languageHandler"; +import MatrixClientContext from "../../../contexts/MatrixClientContext"; /** * Basic container for modal dialogs. @@ -84,16 +83,6 @@ export default createReactClass({ }; }, - childContextTypes: { - matrixClient: PropTypes.instanceOf(MatrixClient), - }, - - getChildContext: function() { - return { - matrixClient: this._matrixClient, - }; - }, - componentWillMount() { this._matrixClient = MatrixClientPeg.get(); }, @@ -122,36 +111,38 @@ export default createReactClass({ } return ( - -
    -
    - { this.props.title } + + +
    +
    + { this.props.title } +
    + { this.props.headerButton } + { cancelButton }
    - { this.props.headerButton } - { cancelButton } -
    - { this.props.children } - + { this.props.children } + + ); }, }); diff --git a/src/components/views/dialogs/BugReportDialog.js b/src/components/views/dialogs/BugReportDialog.js index a3c4ad96ee..91d2bb5213 100644 --- a/src/components/views/dialogs/BugReportDialog.js +++ b/src/components/views/dialogs/BugReportDialog.js @@ -25,8 +25,8 @@ import Modal from '../../../Modal'; import { _t } from '../../../languageHandler'; export default class BugReportDialog extends React.Component { - constructor(props, context) { - super(props, context); + constructor(props) { + super(props); this.state = { sendLogs: true, busy: false, diff --git a/src/components/views/dialogs/DeactivateAccountDialog.js b/src/components/views/dialogs/DeactivateAccountDialog.js index 703ecaf092..fc7669e1fe 100644 --- a/src/components/views/dialogs/DeactivateAccountDialog.js +++ b/src/components/views/dialogs/DeactivateAccountDialog.js @@ -25,8 +25,8 @@ import * as Lifecycle from '../../../Lifecycle'; import { _t } from '../../../languageHandler'; export default class DeactivateAccountDialog extends React.Component { - constructor(props, context) { - super(props, context); + constructor(props) { + super(props); this._onOk = this._onOk.bind(this); this._onCancel = this._onCancel.bind(this); diff --git a/src/components/views/dialogs/DevtoolsDialog.js b/src/components/views/dialogs/DevtoolsDialog.js index 9327e1e54e..c9ed71466d 100644 --- a/src/components/views/dialogs/DevtoolsDialog.js +++ b/src/components/views/dialogs/DevtoolsDialog.js @@ -16,23 +16,19 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; +import { Room } from "matrix-js-sdk"; + import sdk from '../../../index'; import SyntaxHighlight from '../elements/SyntaxHighlight'; import { _t } from '../../../languageHandler'; -import MatrixClientPeg from '../../../MatrixClientPeg'; import Field from "../elements/Field"; +import MatrixClientContext from "../../../contexts/MatrixClientContext"; -class DevtoolsComponent extends React.Component { - static contextTypes = { - roomId: PropTypes.string.isRequired, - }; -} - -class GenericEditor extends DevtoolsComponent { +class GenericEditor extends React.PureComponent { // static propTypes = {onBack: PropTypes.func.isRequired}; - constructor(props, context) { - super(props, context); + constructor(props) { + super(props); this._onChange = this._onChange.bind(this); this.onBack = this.onBack.bind(this); } @@ -67,12 +63,15 @@ class SendCustomEvent extends GenericEditor { static propTypes = { onBack: PropTypes.func.isRequired, + room: PropTypes.instanceOf(Room).isRequired, forceStateEvent: PropTypes.bool, inputs: PropTypes.object, }; - constructor(props, context) { - super(props, context); + static contextType = MatrixClientContext; + + constructor(props) { + super(props); this._send = this._send.bind(this); const {eventType, stateKey, evContent} = Object.assign({ @@ -91,11 +90,11 @@ class SendCustomEvent extends GenericEditor { } send(content) { - const cli = MatrixClientPeg.get(); + const cli = this.context; if (this.state.isStateEvent) { - return cli.sendStateEvent(this.context.roomId, this.state.eventType, content, this.state.stateKey); + return cli.sendStateEvent(this.props.room.roomId, this.state.eventType, content, this.state.stateKey); } else { - return cli.sendEvent(this.context.roomId, this.state.eventType, content); + return cli.sendEvent(this.props.room.roomId, this.state.eventType, content); } } @@ -154,13 +153,16 @@ class SendAccountData extends GenericEditor { static getLabel() { return _t('Send Account Data'); } static propTypes = { + room: PropTypes.instanceOf(Room).isRequired, isRoomAccountData: PropTypes.bool, forceMode: PropTypes.bool, inputs: PropTypes.object, }; - constructor(props, context) { - super(props, context); + static contextType = MatrixClientContext; + + constructor(props) { + super(props); this._send = this._send.bind(this); const {eventType, evContent} = Object.assign({ @@ -177,9 +179,9 @@ class SendAccountData extends GenericEditor { } send(content) { - const cli = MatrixClientPeg.get(); + const cli = this.context; if (this.state.isRoomAccountData) { - return cli.setRoomAccountData(this.context.roomId, this.state.eventType, content); + return cli.setRoomAccountData(this.props.room.roomId, this.state.eventType, content); } return cli.setAccountData(this.state.eventType, content); } @@ -234,7 +236,7 @@ class SendAccountData extends GenericEditor { const INITIAL_LOAD_TILES = 20; const LOAD_TILES_STEP_SIZE = 50; -class FilteredList extends React.Component { +class FilteredList extends React.PureComponent { static propTypes = { children: PropTypes.any, query: PropTypes.string, @@ -247,8 +249,8 @@ class FilteredList extends React.Component { return children.filter((child) => child.key.toLowerCase().includes(lcQuery)); } - constructor(props, context) { - super(props, context); + constructor(props) { + super(props); this.state = { filteredChildren: FilteredList.filterChildren(this.props.children, this.props.query), @@ -305,19 +307,20 @@ class FilteredList extends React.Component { } } -class RoomStateExplorer extends DevtoolsComponent { +class RoomStateExplorer extends React.PureComponent { static getLabel() { return _t('Explore Room State'); } - static propTypes = { onBack: PropTypes.func.isRequired, + room: PropTypes.instanceOf(Room).isRequired, }; - constructor(props, context) { - super(props, context); + static contextType = MatrixClientContext; - const room = MatrixClientPeg.get().getRoom(this.context.roomId); - this.roomStateEvents = room.currentState.events; + constructor(props) { + super(props); + + this.roomStateEvents = this.props.room.currentState.events; this.onBack = this.onBack.bind(this); this.editEv = this.editEv.bind(this); @@ -373,7 +376,7 @@ class RoomStateExplorer extends DevtoolsComponent { render() { if (this.state.event) { if (this.state.editing) { - return ; + return ; } return
    @@ -553,17 +562,20 @@ class AccountDataExplorer extends DevtoolsComponent { } } -class ServersInRoomList extends DevtoolsComponent { +class ServersInRoomList extends React.PureComponent { static getLabel() { return _t('View Servers in Room'); } static propTypes = { onBack: PropTypes.func.isRequired, + room: PropTypes.instanceOf(Room).isRequired, }; - constructor(props, context) { - super(props, context); + static contextType = MatrixClientContext; - const room = MatrixClientPeg.get().getRoom(this.context.roomId); + constructor(props) { + super(props); + + const room = this.props.room; const servers = new Set(); room.currentState.getStateEvents("m.room.member").forEach(ev => servers.add(ev.getSender().split(":")[1])); this.servers = Array.from(servers).map(s => @@ -602,19 +614,14 @@ const Entries = [ ServersInRoomList, ]; -export default class DevtoolsDialog extends React.Component { - static childContextTypes = { - roomId: PropTypes.string.isRequired, - // client: PropTypes.instanceOf(MatixClient), - }; - +export default class DevtoolsDialog extends React.PureComponent { static propTypes = { roomId: PropTypes.string.isRequired, onFinished: PropTypes.func.isRequired, }; - constructor(props, context) { - super(props, context); + constructor(props) { + super(props); this.onBack = this.onBack.bind(this); this.onCancel = this.onCancel.bind(this); @@ -627,10 +634,6 @@ export default class DevtoolsDialog extends React.Component { this._unmounted = true; } - getChildContext() { - return { roomId: this.props.roomId }; - } - _setMode(mode) { return () => { this.setState({ mode }); @@ -654,15 +657,17 @@ export default class DevtoolsDialog extends React.Component { let body; if (this.state.mode) { - body =
    -
    { this.state.mode.getLabel() }
    -
    Room ID: { this.props.roomId }
    -
    - -
    ; + body = + {(cli) => +
    { this.state.mode.getLabel() }
    +
    Room ID: { this.props.roomId }
    +
    + + } + ; } else { const classes = "mx_DevTools_RoomStateExplorer_button"; - body =
    + body =
    { _t('Toolbox') }
    Room ID: { this.props.roomId }
    @@ -679,7 +684,7 @@ export default class DevtoolsDialog extends React.Component {
    -
    ; +
    ; } const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); diff --git a/src/components/views/dialogs/ReportEventDialog.js b/src/components/views/dialogs/ReportEventDialog.js index 394e5ad47d..af140f6e18 100644 --- a/src/components/views/dialogs/ReportEventDialog.js +++ b/src/components/views/dialogs/ReportEventDialog.js @@ -30,8 +30,8 @@ export default class ReportEventDialog extends PureComponent { onFinished: PropTypes.func.isRequired, }; - constructor(props, context) { - super(props, context); + constructor(props) { + super(props); this.state = { reason: "", diff --git a/src/components/views/elements/EditableTextContainer.js b/src/components/views/elements/EditableTextContainer.js index 5cba98470c..3d656e6b79 100644 --- a/src/components/views/elements/EditableTextContainer.js +++ b/src/components/views/elements/EditableTextContainer.js @@ -25,13 +25,13 @@ import sdk from '../../../index'; * Parent components should supply an 'onSubmit' callback which returns a * promise; a spinner is shown until the promise resolves. * - * The parent can also supply a 'getIntialValue' callback, which works in a + * The parent can also supply a 'getInitialValue' callback, which works in a * similarly asynchronous way. If this is not provided, the initial value is * taken from the 'initialValue' property. */ export default class EditableTextContainer extends React.Component { - constructor(props, context) { - super(props, context); + constructor(props) { + super(props); this._unmounted = false; this.state = { diff --git a/src/components/views/elements/Flair.js b/src/components/views/elements/Flair.js index 0b9dabeae6..ef208bbea9 100644 --- a/src/components/views/elements/Flair.js +++ b/src/components/views/elements/Flair.js @@ -21,6 +21,7 @@ import PropTypes from 'prop-types'; import {MatrixClient} from 'matrix-js-sdk'; import FlairStore from '../../../stores/FlairStore'; import dis from '../../../dispatcher'; +import MatrixClientContext from "../../../contexts/MatrixClientContext"; class FlairAvatar extends React.Component { @@ -40,7 +41,7 @@ class FlairAvatar extends React.Component { } render() { - const httpUrl = this.context.matrixClient.mxcUrlToHttp( + const httpUrl = this.context.mxcUrlToHttp( this.props.groupProfile.avatarUrl, 16, 16, 'scale', false); const tooltip = this.props.groupProfile.name ? `${this.props.groupProfile.name} (${this.props.groupProfile.groupId})`: @@ -62,9 +63,7 @@ FlairAvatar.propTypes = { }), }; -FlairAvatar.contextTypes = { - matrixClient: PropTypes.instanceOf(MatrixClient).isRequired, -}; +FlairAvatar.contextType = MatrixClientContext; export default class Flair extends React.Component { constructor() { @@ -92,7 +91,7 @@ export default class Flair extends React.Component { for (const groupId of groups) { let groupProfile = null; try { - groupProfile = await FlairStore.getGroupProfileCached(this.context.matrixClient, groupId); + groupProfile = await FlairStore.getGroupProfileCached(this.context, groupId); } catch (err) { console.error('Could not get profile for group', groupId, err); } @@ -134,6 +133,4 @@ Flair.propTypes = { groups: PropTypes.arrayOf(PropTypes.string), }; -Flair.contextTypes = { - matrixClient: PropTypes.instanceOf(MatrixClient).isRequired, -}; +Flair.contextType = MatrixClientContext; diff --git a/src/components/views/elements/Pill.js b/src/components/views/elements/Pill.js index a065602f68..99005de03b 100644 --- a/src/components/views/elements/Pill.js +++ b/src/components/views/elements/Pill.js @@ -20,12 +20,13 @@ import createReactClass from 'create-react-class'; import sdk from '../../../index'; import dis from '../../../dispatcher'; import classNames from 'classnames'; -import { Room, RoomMember, MatrixClient } from 'matrix-js-sdk'; +import { Room, RoomMember } from 'matrix-js-sdk'; import PropTypes from 'prop-types'; import MatrixClientPeg from '../../../MatrixClientPeg'; import { getDisplayAliasForRoom } from '../../../Rooms'; import FlairStore from "../../../stores/FlairStore"; import {getPrimaryPermalinkEntity} from "../../../utils/permalinks/Permalinks"; +import MatrixClientContext from "../../../contexts/MatrixClientContext"; // For URLs of matrix.to links in the timeline which have been reformatted by // HttpUtils transformTags to relative links. This excludes event URLs (with `[^\/]*`) @@ -66,17 +67,6 @@ const Pill = createReactClass({ isSelected: PropTypes.bool, }, - - childContextTypes: { - matrixClient: PropTypes.instanceOf(MatrixClient), - }, - - getChildContext() { - return { - matrixClient: this._matrixClient, - }; - }, - getInitialState() { return { // ID/alias of the room/user @@ -276,15 +266,17 @@ const Pill = createReactClass({ }); if (this.state.pillType) { - return this.props.inMessage ? - - { avatar } - { linkText } - : - - { avatar } - { linkText } - ; + return + { this.props.inMessage ? + + { avatar } + { linkText } + : + + { avatar } + { linkText } + } + ; } else { // Deliberately render nothing if the URL isn't recognised return null; diff --git a/src/components/views/elements/ReplyThread.js b/src/components/views/elements/ReplyThread.js index 55fd028980..e7832efca7 100644 --- a/src/components/views/elements/ReplyThread.js +++ b/src/components/views/elements/ReplyThread.js @@ -21,10 +21,11 @@ import {_t} from '../../../languageHandler'; import PropTypes from 'prop-types'; import dis from '../../../dispatcher'; import {wantsDateSeparator} from '../../../DateUtils'; -import {MatrixEvent, MatrixClient} from 'matrix-js-sdk'; +import {MatrixEvent} from 'matrix-js-sdk'; import {makeUserPermalink, RoomPermalinkCreator} from "../../../utils/permalinks/Permalinks"; import SettingsStore from "../../../settings/SettingsStore"; import escapeHtml from "escape-html"; +import MatrixClientContext from "../../../contexts/MatrixClientContext"; // This component does no cycle detection, simply because the only way to make such a cycle would be to // craft event_id's, using a homeserver that generates predictable event IDs; even then the impact would @@ -38,12 +39,10 @@ export default class ReplyThread extends React.Component { permalinkCreator: PropTypes.instanceOf(RoomPermalinkCreator).isRequired, }; - static contextTypes = { - matrixClient: PropTypes.instanceOf(MatrixClient).isRequired, - }; + static contextType = MatrixClientContext; - constructor(props, context) { - super(props, context); + constructor(props) { + super(props); this.state = { // The loaded events to be rendered as linear-replies @@ -187,7 +186,7 @@ export default class ReplyThread extends React.Component { componentWillMount() { this.unmounted = false; - this.room = this.context.matrixClient.getRoom(this.props.parentEv.getRoomId()); + this.room = this.context.getRoom(this.props.parentEv.getRoomId()); this.room.on("Room.redaction", this.onRoomRedaction); // same event handler as Room.redaction as for both we just do forceUpdate this.room.on("Room.redactionCancelled", this.onRoomRedaction); @@ -259,7 +258,7 @@ export default class ReplyThread extends React.Component { try { // ask the client to fetch the event we want using the context API, only interface to do so is to ask // for a timeline with that event, but once it is loaded we can use findEventById to look up the ev map - await this.context.matrixClient.getEventTimeline(this.room.getUnfilteredTimelineSet(), eventId); + await this.context.getEventTimeline(this.room.getUnfilteredTimelineSet(), eventId); } catch (e) { // if it fails catch the error and return early, there's no point trying to find the event in this case. // Return null as it is falsey and thus should be treated as an error (as the event cannot be resolved). @@ -300,7 +299,7 @@ export default class ReplyThread extends React.Component { } else if (this.state.loadedEv) { const ev = this.state.loadedEv; const Pill = sdk.getComponent('elements.Pill'); - const room = this.context.matrixClient.getRoom(ev.getRoomId()); + const room = this.context.getRoom(ev.getRoomId()); header =
    { _t('In reply to ', {}, { diff --git a/src/components/views/elements/SyntaxHighlight.js b/src/components/views/elements/SyntaxHighlight.js index 82b5ae572c..bce65cf1a9 100644 --- a/src/components/views/elements/SyntaxHighlight.js +++ b/src/components/views/elements/SyntaxHighlight.js @@ -24,8 +24,8 @@ export default class SyntaxHighlight extends React.Component { children: PropTypes.node, }; - constructor(props, context) { - super(props, context); + constructor(props) { + super(props); this._ref = this._ref.bind(this); } diff --git a/src/components/views/elements/TagTile.js b/src/components/views/elements/TagTile.js index 767980f0a0..c57d973086 100644 --- a/src/components/views/elements/TagTile.js +++ b/src/components/views/elements/TagTile.js @@ -20,7 +20,6 @@ import React, {createRef} from 'react'; import PropTypes from 'prop-types'; import createReactClass from 'create-react-class'; import classNames from 'classnames'; -import { MatrixClient } from 'matrix-js-sdk'; import sdk from '../../../index'; import dis from '../../../dispatcher'; import {_t} from '../../../languageHandler'; @@ -31,6 +30,7 @@ import FlairStore from '../../../stores/FlairStore'; import GroupStore from '../../../stores/GroupStore'; import TagOrderStore from '../../../stores/TagOrderStore'; import {ContextMenu, ContextMenuButton, toRightOf} from "../../structures/ContextMenu"; +import MatrixClientContext from "../../../contexts/MatrixClientContext"; // A class for a child of TagPanel (possibly wrapped in a DNDTagTile) that represents // a thing to click on for the user to filter the visible rooms in the RoomList to: @@ -46,8 +46,8 @@ export default createReactClass({ tag: PropTypes.string, }, - contextTypes: { - matrixClient: PropTypes.instanceOf(MatrixClient).isRequired, + statics: { + contextType: MatrixClientContext, }, getInitialState() { @@ -81,7 +81,7 @@ export default createReactClass({ _onFlairStoreUpdated() { if (this.unmounted) return; FlairStore.getGroupProfileCached( - this.context.matrixClient, + this.context, this.props.tag, ).then((profile) => { if (this.unmounted) return; @@ -145,7 +145,7 @@ export default createReactClass({ const name = profile.name || this.props.tag; const avatarHeight = 40; - const httpUrl = profile.avatarUrl ? this.context.matrixClient.mxcUrlToHttp( + const httpUrl = profile.avatarUrl ? this.context.mxcUrlToHttp( profile.avatarUrl, avatarHeight, avatarHeight, "crop", ) : null; diff --git a/src/components/views/groups/GroupInviteTile.js b/src/components/views/groups/GroupInviteTile.js index a21b091145..c0d0d9eafe 100644 --- a/src/components/views/groups/GroupInviteTile.js +++ b/src/components/views/groups/GroupInviteTile.js @@ -19,13 +19,13 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; import createReactClass from 'create-react-class'; -import { MatrixClient } from 'matrix-js-sdk'; import sdk from '../../../index'; import dis from '../../../dispatcher'; import {_t} from '../../../languageHandler'; import classNames from 'classnames'; import MatrixClientPeg from "../../../MatrixClientPeg"; import {ContextMenu, ContextMenuButton, toRightOf} from "../../structures/ContextMenu"; +import MatrixClientContext from "../../../contexts/MatrixClientContext"; // XXX this class copies a lot from RoomTile.js export default createReactClass({ @@ -35,8 +35,8 @@ export default createReactClass({ group: PropTypes.object.isRequired, }, - contextTypes: { - matrixClient: PropTypes.instanceOf(MatrixClient), + statics: { + contextType: MatrixClientContext, }, getInitialState: function() { @@ -58,7 +58,7 @@ export default createReactClass({ onMouseEnter: function() { const state = {hover: true}; // Only allow non-guests to access the context menu - if (!this.context.matrixClient.isGuest()) { + if (!this.context.isGuest()) { state.badgeHover = true; } this.setState(state); @@ -118,7 +118,7 @@ export default createReactClass({ const groupName = this.props.group.name || this.props.group.groupId; const httpAvatarUrl = this.props.group.avatarUrl ? - this.context.matrixClient.mxcUrlToHttp(this.props.group.avatarUrl, 24, 24) : null; + this.context.mxcUrlToHttp(this.props.group.avatarUrl, 24, 24) : null; const av = ; diff --git a/src/components/views/groups/GroupMemberInfo.js b/src/components/views/groups/GroupMemberInfo.js index 3dac90fc35..eb90cdc0f8 100644 --- a/src/components/views/groups/GroupMemberInfo.js +++ b/src/components/views/groups/GroupMemberInfo.js @@ -18,7 +18,6 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; import createReactClass from 'create-react-class'; -import { MatrixClient } from 'matrix-js-sdk'; import dis from '../../../dispatcher'; import Modal from '../../../Modal'; import sdk from '../../../index'; @@ -26,12 +25,13 @@ import { _t } from '../../../languageHandler'; import { GroupMemberType } from '../../../groups'; import GroupStore from '../../../stores/GroupStore'; import AccessibleButton from '../elements/AccessibleButton'; +import MatrixClientContext from "../../../contexts/MatrixClientContext"; module.exports = createReactClass({ displayName: 'GroupMemberInfo', - contextTypes: { - matrixClient: PropTypes.instanceOf(MatrixClient), + statics: { + contextType: MatrixClientContext, }, propTypes: { @@ -85,7 +85,7 @@ module.exports = createReactClass({ _onKick: function() { const ConfirmUserActionDialog = sdk.getComponent("dialogs.ConfirmUserActionDialog"); Modal.createDialog(ConfirmUserActionDialog, { - matrixClient: this.context.matrixClient, + matrixClient: this.context, groupMember: this.props.groupMember, action: this.state.isUserInvited ? _t('Disinvite') : _t('Remove from community'), title: this.state.isUserInvited ? _t('Disinvite this user from community?') @@ -95,7 +95,7 @@ module.exports = createReactClass({ if (!proceed) return; this.setState({removingUser: true}); - this.context.matrixClient.removeUserFromGroup( + this.context.removeUserFromGroup( this.props.groupId, this.props.groupMember.userId, ).then(() => { // return to the user list @@ -171,7 +171,7 @@ module.exports = createReactClass({ const avatarUrl = this.props.groupMember.avatarUrl; let avatarElement; if (avatarUrl) { - const httpUrl = this.context.matrixClient.mxcUrlToHttp(avatarUrl, 800, 800); + const httpUrl = this.context.mxcUrlToHttp(avatarUrl, 800, 800); avatarElement = (
    ); diff --git a/src/components/views/groups/GroupMemberTile.js b/src/components/views/groups/GroupMemberTile.js index c4b41d23ce..7a9ba9289b 100644 --- a/src/components/views/groups/GroupMemberTile.js +++ b/src/components/views/groups/GroupMemberTile.js @@ -19,10 +19,10 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; import createReactClass from 'create-react-class'; -import { MatrixClient } from 'matrix-js-sdk'; import sdk from '../../../index'; import dis from '../../../dispatcher'; import { GroupMemberType } from '../../../groups'; +import MatrixClientContext from "../../../contexts/MatrixClientContext"; export default createReactClass({ displayName: 'GroupMemberTile', @@ -36,8 +36,8 @@ export default createReactClass({ return {}; }, - contextTypes: { - matrixClient: PropTypes.instanceOf(MatrixClient).isRequired, + statics: { + contextType: MatrixClientContext, }, onClick: function(e) { @@ -53,7 +53,7 @@ export default createReactClass({ const EntityTile = sdk.getComponent('rooms.EntityTile'); const name = this.props.member.displayname || this.props.member.userId; - const avatarUrl = this.context.matrixClient.mxcUrlToHttp( + const avatarUrl = this.context.mxcUrlToHttp( this.props.member.avatarUrl, 36, 36, 'crop', ); diff --git a/src/components/views/groups/GroupRoomInfo.js b/src/components/views/groups/GroupRoomInfo.js index f9f7324e23..d5b8759a67 100644 --- a/src/components/views/groups/GroupRoomInfo.js +++ b/src/components/views/groups/GroupRoomInfo.js @@ -17,18 +17,18 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; import createReactClass from 'create-react-class'; -import { MatrixClient } from 'matrix-js-sdk'; import dis from '../../../dispatcher'; import Modal from '../../../Modal'; import sdk from '../../../index'; import { _t } from '../../../languageHandler'; import GroupStore from '../../../stores/GroupStore'; +import MatrixClientContext from "../../../contexts/MatrixClientContext"; module.exports = createReactClass({ displayName: 'GroupRoomInfo', - contextTypes: { - matrixClient: PropTypes.instanceOf(MatrixClient), + statics: { + contextType: MatrixClientContext, }, propTypes: { @@ -206,7 +206,7 @@ module.exports = createReactClass({ const avatarUrl = this.state.groupRoom.avatarUrl; let avatarElement; if (avatarUrl) { - const httpUrl = this.context.matrixClient.mxcUrlToHttp(avatarUrl, 800, 800); + const httpUrl = this.context.mxcUrlToHttp(avatarUrl, 800, 800); avatarElement = (
    ); diff --git a/src/components/views/groups/GroupRoomTile.js b/src/components/views/groups/GroupRoomTile.js index ae325d4796..527e65d30c 100644 --- a/src/components/views/groups/GroupRoomTile.js +++ b/src/components/views/groups/GroupRoomTile.js @@ -17,10 +17,10 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; import createReactClass from 'create-react-class'; -import {MatrixClient} from 'matrix-js-sdk'; import sdk from '../../../index'; import dis from '../../../dispatcher'; import { GroupRoomType } from '../../../groups'; +import MatrixClientContext from "../../../contexts/MatrixClientContext"; const GroupRoomTile = createReactClass({ displayName: 'GroupRoomTile', @@ -41,7 +41,7 @@ const GroupRoomTile = createReactClass({ render: function() { const BaseAvatar = sdk.getComponent('avatars.BaseAvatar'); const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); - const avatarUrl = this.context.matrixClient.mxcUrlToHttp( + const avatarUrl = this.context.mxcUrlToHttp( this.props.groupRoom.avatarUrl, 36, 36, 'crop', ); @@ -66,9 +66,7 @@ const GroupRoomTile = createReactClass({ }, }); -GroupRoomTile.contextTypes = { - matrixClient: PropTypes.instanceOf(MatrixClient).isRequired, -}; +GroupRoomTile.contextType = MatrixClientContext; export default GroupRoomTile; diff --git a/src/components/views/groups/GroupTile.js b/src/components/views/groups/GroupTile.js index 3b64c10a1e..a6447cdf14 100644 --- a/src/components/views/groups/GroupTile.js +++ b/src/components/views/groups/GroupTile.js @@ -22,6 +22,7 @@ import { Draggable, Droppable } from 'react-beautiful-dnd'; import sdk from '../../../index'; import dis from '../../../dispatcher'; import FlairStore from '../../../stores/FlairStore'; +import MatrixClientContext from "../../../contexts/MatrixClientContext"; function nop() {} @@ -37,8 +38,8 @@ const GroupTile = createReactClass({ draggable: PropTypes.bool, }, - contextTypes: { - matrixClient: PropTypes.instanceOf(MatrixClient).isRequired, + statics: { + contextType: MatrixClientContext, }, getInitialState() { @@ -56,7 +57,7 @@ const GroupTile = createReactClass({ }, componentWillMount: function() { - FlairStore.getGroupProfileCached(this.context.matrixClient, this.props.groupId).then((profile) => { + FlairStore.getGroupProfileCached(this.context, this.props.groupId).then((profile) => { this.setState({profile}); }).catch((err) => { console.error('Error whilst getting cached profile for GroupTile', err); @@ -80,7 +81,7 @@ const GroupTile = createReactClass({ const descElement = this.props.showDescription ?
    { profile.shortDescription }
    :
    ; - const httpUrl = profile.avatarUrl ? this.context.matrixClient.mxcUrlToHttp( + const httpUrl = profile.avatarUrl ? this.context.mxcUrlToHttp( profile.avatarUrl, avatarHeight, avatarHeight, "crop") : null; let avatarElement = ( diff --git a/src/components/views/groups/GroupUserSettings.js b/src/components/views/groups/GroupUserSettings.js index 3cd5731b99..297c0fbd30 100644 --- a/src/components/views/groups/GroupUserSettings.js +++ b/src/components/views/groups/GroupUserSettings.js @@ -15,17 +15,16 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; import createReactClass from 'create-react-class'; import sdk from '../../../index'; -import { MatrixClient } from 'matrix-js-sdk'; import { _t } from '../../../languageHandler'; +import MatrixClientContext from "../../../contexts/MatrixClientContext"; export default createReactClass({ displayName: 'GroupUserSettings', - contextTypes: { - matrixClient: PropTypes.instanceOf(MatrixClient), + statics: { + contextType: MatrixClientContext, }, getInitialState() { @@ -36,7 +35,7 @@ export default createReactClass({ }, componentWillMount: function() { - this.context.matrixClient.getJoinedGroups().then((result) => { + this.context.getJoinedGroups().then((result) => { this.setState({groups: result.groups || [], error: null}); }, (err) => { console.error(err); diff --git a/src/components/views/messages/MFileBody.js b/src/components/views/messages/MFileBody.js index 552b1108d2..6045ec0571 100644 --- a/src/components/views/messages/MFileBody.js +++ b/src/components/views/messages/MFileBody.js @@ -26,6 +26,7 @@ import {decryptFile} from '../../../utils/DecryptFile'; import Tinter from '../../../Tinter'; import request from 'browser-request'; import Modal from '../../../Modal'; +import SdkConfig from "../../../SdkConfig"; // A cached tinted copy of require("../../../../res/img/download.svg") @@ -214,10 +215,6 @@ module.exports = createReactClass({ tileShape: PropTypes.string, }, - contextTypes: { - appConfig: PropTypes.object, - }, - /** * Extracts a human readable label for the file attachment to use as * link text. @@ -360,8 +357,9 @@ module.exports = createReactClass({ // If the attachment is encryped then put the link inside an iframe. let renderer_url = DEFAULT_CROSS_ORIGIN_RENDERER; - if (this.context.appConfig && this.context.appConfig.cross_origin_renderer_url) { - renderer_url = this.context.appConfig.cross_origin_renderer_url; + const appConfig = SdkConfig.get(); + if (appConfig && appConfig.cross_origin_renderer_url) { + renderer_url = appConfig.cross_origin_renderer_url; } renderer_url += "?origin=" + encodeURIComponent(window.location.origin); return ( diff --git a/src/components/views/messages/MImageBody.js b/src/components/views/messages/MImageBody.js index 427056203d..dbe6636c6b 100644 --- a/src/components/views/messages/MImageBody.js +++ b/src/components/views/messages/MImageBody.js @@ -18,7 +18,6 @@ limitations under the License. import React, {createRef} from 'react'; import PropTypes from 'prop-types'; -import { MatrixClient } from 'matrix-js-sdk'; import MFileBody from './MFileBody'; import Modal from '../../../Modal'; @@ -26,6 +25,7 @@ import sdk from '../../../index'; import { decryptFile } from '../../../utils/DecryptFile'; import { _t } from '../../../languageHandler'; import SettingsStore from "../../../settings/SettingsStore"; +import MatrixClientContext from "../../../contexts/MatrixClientContext"; export default class MImageBody extends React.Component { static propTypes = { @@ -39,9 +39,7 @@ export default class MImageBody extends React.Component { maxImageHeight: PropTypes.number, }; - static contextTypes = { - matrixClient: PropTypes.instanceOf(MatrixClient), - }; + static contextType = MatrixClientContext; constructor(props) { super(props); @@ -71,7 +69,7 @@ export default class MImageBody extends React.Component { componentWillMount() { this.unmounted = false; - this.context.matrixClient.on('sync', this.onClientSync); + this.context.on('sync', this.onClientSync); } // FIXME: factor this out and aplpy it to MVideoBody and MAudioBody too! @@ -174,7 +172,7 @@ export default class MImageBody extends React.Component { if (content.file !== undefined) { return this.state.decryptedUrl; } else { - return this.context.matrixClient.mxcUrlToHttp(content.url); + return this.context.mxcUrlToHttp(content.url); } } @@ -198,7 +196,7 @@ export default class MImageBody extends React.Component { // special case to return clientside sender-generated thumbnails for SVGs, if any, // given we deliberately don't thumbnail them serverside to prevent // billion lol attacks and similar - return this.context.matrixClient.mxcUrlToHttp( + return this.context.mxcUrlToHttp( content.info.thumbnail_url, thumbWidth, thumbHeight, @@ -221,7 +219,7 @@ export default class MImageBody extends React.Component { pixelRatio === 1.0 || (!info || !info.w || !info.h || !info.size) ) { - return this.context.matrixClient.mxcUrlToHttp(content.url, thumbWidth, thumbHeight); + return this.context.mxcUrlToHttp(content.url, thumbWidth, thumbHeight); } else { // we should only request thumbnails if the image is bigger than 800x600 // (or 1600x1200 on retina) otherwise the image in the timeline will just @@ -242,7 +240,7 @@ export default class MImageBody extends React.Component { // image is too large physically and bytewise to clutter our timeline so // we ask for a thumbnail, despite knowing that it will be max 800x600 // despite us being retina (as synapse doesn't do 1600x1200 thumbs yet). - return this.context.matrixClient.mxcUrlToHttp( + return this.context.mxcUrlToHttp( content.url, thumbWidth, thumbHeight, @@ -251,7 +249,7 @@ export default class MImageBody extends React.Component { // download the original image otherwise, so we can scale it client side // to take pixelRatio into account. // ( no width/height means we want the original image) - return this.context.matrixClient.mxcUrlToHttp( + return this.context.mxcUrlToHttp( content.url, ); } @@ -308,7 +306,7 @@ export default class MImageBody extends React.Component { componentWillUnmount() { this.unmounted = true; - this.context.matrixClient.removeListener('sync', this.onClientSync); + this.context.removeListener('sync', this.onClientSync); this._afterComponentWillUnmount(); if (this.state.decryptedUrl) { diff --git a/src/components/views/messages/MessageActionBar.js b/src/components/views/messages/MessageActionBar.js index 81e806cf62..29ade3cb3f 100644 --- a/src/components/views/messages/MessageActionBar.js +++ b/src/components/views/messages/MessageActionBar.js @@ -25,7 +25,7 @@ import dis from '../../../dispatcher'; import Modal from '../../../Modal'; import {aboveLeftOf, ContextMenu, ContextMenuButton, useContextMenu} from '../../structures/ContextMenu'; import { isContentActionable, canEditContent } from '../../../utils/EventUtils'; -import {RoomContext} from "../../structures/RoomView"; +import RoomContext from "../../../contexts/RoomContext"; const OptionsButton = ({mxEvent, getTile, getReplyThread, permalinkCreator, onFocusChange}) => { const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu(); @@ -117,9 +117,7 @@ export default class MessageActionBar extends React.PureComponent { onFocusChange: PropTypes.func, }; - static contextTypes = { - room: RoomContext, - }; + static contextType = RoomContext; componentDidMount() { this.props.mxEvent.on("Event.decrypted", this.onDecrypted); @@ -164,12 +162,12 @@ export default class MessageActionBar extends React.PureComponent { let editButton; if (isContentActionable(this.props.mxEvent)) { - if (this.context.room.canReact) { + if (this.context.canReact) { reactButton = ( ); } - if (this.context.room.canReply) { + if (this.context.canReply) { replyButton = { if (this.unmounted) return; this.setState({userGroups}); }); - this.context.matrixClient.on('RoomState.events', this.onRoomStateEvents); + this.context.on('RoomState.events', this.onRoomStateEvents); }, componentWillUnmount() { this.unmounted = true; - this.context.matrixClient.removeListener('RoomState.events', this.onRoomStateEvents); + this.context.removeListener('RoomState.events', this.onRoomStateEvents); }, onRoomStateEvents(event) { @@ -71,7 +71,7 @@ export default createReactClass({ _updateRelatedGroups() { if (this.unmounted) return; - const room = this.context.matrixClient.getRoom(this.props.mxEvent.getRoomId()); + const room = this.context.getRoom(this.props.mxEvent.getRoomId()); if (!room) return; const relatedGroupsEvent = room.currentState.getStateEvents('m.room.related_groups', ''); diff --git a/src/components/views/right_panel/UserInfo.js b/src/components/views/right_panel/UserInfo.js index d1d7aa0371..e952f8fad9 100644 --- a/src/components/views/right_panel/UserInfo.js +++ b/src/components/views/right_panel/UserInfo.js @@ -17,7 +17,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, {useCallback, useMemo, useState, useEffect} from 'react'; +import React, {useCallback, useMemo, useState, useEffect, useContext} from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; import {Group, RoomMember, User} from 'matrix-js-sdk'; @@ -37,9 +37,9 @@ import MultiInviter from "../../../utils/MultiInviter"; import GroupStore from "../../../stores/GroupStore"; import MatrixClientPeg from "../../../MatrixClientPeg"; import E2EIcon from "../rooms/E2EIcon"; -import withLegacyMatrixClient from "../../../utils/withLegacyMatrixClient"; import {useEventEmitter} from "../../../hooks/useEventEmitter"; import {textualPowerLevel} from '../../../Roles'; +import MatrixClientContext from "../../../contexts/MatrixClientContext"; const _disambiguateDevices = (devices) => { const names = Object.create(null); @@ -203,7 +203,9 @@ function DevicesSection({devices, userId, loading}) { ); } -const UserOptionsSection = withLegacyMatrixClient(({matrixClient: cli, member, isIgnored, canInvite, devices}) => { +const UserOptionsSection = ({member, isIgnored, canInvite, devices}) => { + const cli = useContext(MatrixClientContext); + let ignoreButton = null; let insertPillButton = null; let inviteUserButton = null; @@ -336,7 +338,7 @@ const UserOptionsSection = withLegacyMatrixClient(({matrixClient: cli, member, i
    ); -}); +}; const _warnSelfDemote = async () => { const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); @@ -404,7 +406,9 @@ const useRoomPowerLevels = (cli, room) => { return powerLevels; }; -const RoomKickButton = withLegacyMatrixClient(({matrixClient: cli, member, startUpdating, stopUpdating}) => { +const RoomKickButton = ({member, startUpdating, stopUpdating}) => { + const cli = useContext(MatrixClientContext); + const onKick = async () => { const ConfirmUserActionDialog = sdk.getComponent("dialogs.ConfirmUserActionDialog"); const {finished} = Modal.createTrackedDialog( @@ -444,9 +448,11 @@ const RoomKickButton = withLegacyMatrixClient(({matrixClient: cli, member, start return { kickLabel } ; -}); +}; + +const RedactMessagesButton = ({member}) => { + const cli = useContext(MatrixClientContext); -const RedactMessagesButton = withLegacyMatrixClient(({matrixClient: cli, member}) => { const onRedactAllMessages = async () => { const {roomId, userId} = member; const room = cli.getRoom(roomId); @@ -517,9 +523,11 @@ const RedactMessagesButton = withLegacyMatrixClient(({matrixClient: cli, member} return { _t("Remove recent messages") } ; -}); +}; + +const BanToggleButton = ({member, startUpdating, stopUpdating}) => { + const cli = useContext(MatrixClientContext); -const BanToggleButton = withLegacyMatrixClient(({matrixClient: cli, member, startUpdating, stopUpdating}) => { const onBanOrUnban = async () => { const ConfirmUserActionDialog = sdk.getComponent("dialogs.ConfirmUserActionDialog"); const {finished} = Modal.createTrackedDialog( @@ -573,207 +581,206 @@ const BanToggleButton = withLegacyMatrixClient(({matrixClient: cli, member, star return { label } ; -}); +}; -const MuteToggleButton = withLegacyMatrixClient( - ({matrixClient: cli, member, room, powerLevels, startUpdating, stopUpdating}) => { - const isMuted = _isMuted(member, powerLevels); - const onMuteToggle = async () => { - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - const roomId = member.roomId; - const target = member.userId; +const MuteToggleButton = ({member, room, powerLevels, startUpdating, stopUpdating}) => { + const cli = useContext(MatrixClientContext); - // if muting self, warn as it may be irreversible - if (target === cli.getUserId()) { - try { - if (!(await _warnSelfDemote())) return; - } catch (e) { - console.error("Failed to warn about self demotion: ", e); - return; - } + const isMuted = _isMuted(member, powerLevels); + const onMuteToggle = async () => { + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + const roomId = member.roomId; + const target = member.userId; + + // if muting self, warn as it may be irreversible + if (target === cli.getUserId()) { + try { + if (!(await _warnSelfDemote())) return; + } catch (e) { + console.error("Failed to warn about self demotion: ", e); + return; } + } - const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", ""); - if (!powerLevelEvent) return; + const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", ""); + if (!powerLevelEvent) return; - const powerLevels = powerLevelEvent.getContent(); - const levelToSend = ( - (powerLevels.events ? powerLevels.events["m.room.message"] : null) || - powerLevels.events_default - ); - let level; - if (isMuted) { // unmute - level = levelToSend; - } else { // mute - level = levelToSend - 1; - } - level = parseInt(level); + const powerLevels = powerLevelEvent.getContent(); + const levelToSend = ( + (powerLevels.events ? powerLevels.events["m.room.message"] : null) || + powerLevels.events_default + ); + let level; + if (isMuted) { // unmute + level = levelToSend; + } else { // mute + level = levelToSend - 1; + } + level = parseInt(level); - if (!isNaN(level)) { - startUpdating(); - cli.setPowerLevel(roomId, target, level, powerLevelEvent).then(() => { - // NO-OP; rely on the m.room.member event coming down else we could - // get out of sync if we force setState here! - console.log("Mute toggle success"); - }, function(err) { - console.error("Mute error: " + err); - Modal.createTrackedDialog('Failed to mute user', '', ErrorDialog, { - title: _t("Error"), - description: _t("Failed to mute user"), - }); - }).finally(() => { - stopUpdating(); + if (!isNaN(level)) { + startUpdating(); + cli.setPowerLevel(roomId, target, level, powerLevelEvent).then(() => { + // NO-OP; rely on the m.room.member event coming down else we could + // get out of sync if we force setState here! + console.log("Mute toggle success"); + }, function(err) { + console.error("Mute error: " + err); + Modal.createTrackedDialog('Failed to mute user', '', ErrorDialog, { + title: _t("Error"), + description: _t("Failed to mute user"), }); - } + }).finally(() => { + stopUpdating(); + }); + } + }; + + const classes = classNames("mx_UserInfo_field", { + mx_UserInfo_destructive: !isMuted, + }); + + const muteLabel = isMuted ? _t("Unmute") : _t("Mute"); + return + { muteLabel } + ; +}; + +const RoomAdminToolsContainer = ({room, children, member, startUpdating, stopUpdating, powerLevels}) => { + const cli = useContext(MatrixClientContext); + let kickButton; + let banButton; + let muteButton; + let redactButton; + + const editPowerLevel = ( + (powerLevels.events ? powerLevels.events["m.room.power_levels"] : null) || + powerLevels.state_default + ); + + const me = room.getMember(cli.getUserId()); + const isMe = me.userId === member.userId; + const canAffectUser = member.powerLevel < me.powerLevel || isMe; + + if (canAffectUser && me.powerLevel >= powerLevels.kick) { + kickButton = ; + } + if (me.powerLevel >= powerLevels.redact) { + redactButton = ( + + ); + } + if (canAffectUser && me.powerLevel >= powerLevels.ban) { + banButton = ; + } + if (canAffectUser && me.powerLevel >= editPowerLevel) { + muteButton = ( + + ); + } + + if (kickButton || banButton || muteButton || redactButton || children) { + return + { muteButton } + { kickButton } + { banButton } + { redactButton } + { children } + ; + } + + return
    ; +}; + +const GroupAdminToolsSection = ({children, groupId, groupMember, startUpdating, stopUpdating}) => { + const cli = useContext(MatrixClientContext); + + const [isPrivileged, setIsPrivileged] = useState(false); + const [isInvited, setIsInvited] = useState(false); + + // Listen to group store changes + useEffect(() => { + let unmounted = false; + + const onGroupStoreUpdated = () => { + if (unmounted) return; + setIsPrivileged(GroupStore.isUserPrivileged(groupId)); + setIsInvited(GroupStore.getGroupInvitedMembers(groupId).some( + (m) => m.userId === groupMember.userId, + )); }; - const classes = classNames("mx_UserInfo_field", { - mx_UserInfo_destructive: !isMuted, - }); + GroupStore.registerListener(groupId, onGroupStoreUpdated); + onGroupStoreUpdated(); + // Handle unmount + return () => { + unmounted = true; + GroupStore.unregisterListener(onGroupStoreUpdated); + }; + }, [groupId, groupMember.userId]); - const muteLabel = isMuted ? _t("Unmute") : _t("Mute"); - return - { muteLabel } - ; - }, -); + if (isPrivileged) { + const _onKick = async () => { + const ConfirmUserActionDialog = sdk.getComponent("dialogs.ConfirmUserActionDialog"); + const {finished} = Modal.createDialog(ConfirmUserActionDialog, { + matrixClient: cli, + groupMember, + action: isInvited ? _t('Disinvite') : _t('Remove from community'), + title: isInvited ? _t('Disinvite this user from community?') + : _t('Remove this user from community?'), + danger: true, + }); -const RoomAdminToolsContainer = withLegacyMatrixClient( - ({matrixClient: cli, room, children, member, startUpdating, stopUpdating, powerLevels}) => { - let kickButton; - let banButton; - let muteButton; - let redactButton; + const [proceed] = await finished; + if (!proceed) return; - const editPowerLevel = ( - (powerLevels.events ? powerLevels.events["m.room.power_levels"] : null) || - powerLevels.state_default + startUpdating(); + cli.removeUserFromGroup(groupId, groupMember.userId).then(() => { + // return to the user list + dis.dispatch({ + action: "view_user", + member: null, + }); + }).catch((e) => { + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + Modal.createTrackedDialog('Failed to remove user from group', '', ErrorDialog, { + title: _t('Error'), + description: isInvited ? + _t('Failed to withdraw invitation') : + _t('Failed to remove user from community'), + }); + console.log(e); + }).finally(() => { + stopUpdating(); + }); + }; + + const kickButton = ( + + { isInvited ? _t('Disinvite') : _t('Remove from community') } + ); - const me = room.getMember(cli.getUserId()); - const isMe = me.userId === member.userId; - const canAffectUser = member.powerLevel < me.powerLevel || isMe; + // No make/revoke admin API yet + /*const opLabel = this.state.isTargetMod ? _t("Revoke Moderator") : _t("Make Moderator"); + giveModButton = + {giveOpLabel} + ;*/ - if (canAffectUser && me.powerLevel >= powerLevels.kick) { - kickButton = ; - } - if (me.powerLevel >= powerLevels.redact) { - redactButton = ( - - ); - } - if (canAffectUser && me.powerLevel >= powerLevels.ban) { - banButton = ; - } - if (canAffectUser && me.powerLevel >= editPowerLevel) { - muteButton = ( - - ); - } + return + { kickButton } + { children } + ; + } - if (kickButton || banButton || muteButton || redactButton || children) { - return - { muteButton } - { kickButton } - { banButton } - { redactButton } - { children } - ; - } - - return
    ; - }, -); - -const GroupAdminToolsSection = withLegacyMatrixClient( - ({matrixClient: cli, children, groupId, groupMember, startUpdating, stopUpdating}) => { - const [isPrivileged, setIsPrivileged] = useState(false); - const [isInvited, setIsInvited] = useState(false); - - // Listen to group store changes - useEffect(() => { - let unmounted = false; - - const onGroupStoreUpdated = () => { - if (unmounted) return; - setIsPrivileged(GroupStore.isUserPrivileged(groupId)); - setIsInvited(GroupStore.getGroupInvitedMembers(groupId).some( - (m) => m.userId === groupMember.userId, - )); - }; - - GroupStore.registerListener(groupId, onGroupStoreUpdated); - onGroupStoreUpdated(); - // Handle unmount - return () => { - unmounted = true; - GroupStore.unregisterListener(onGroupStoreUpdated); - }; - }, [groupId, groupMember.userId]); - - if (isPrivileged) { - const _onKick = async () => { - const ConfirmUserActionDialog = sdk.getComponent("dialogs.ConfirmUserActionDialog"); - const {finished} = Modal.createDialog(ConfirmUserActionDialog, { - matrixClient: cli, - groupMember, - action: isInvited ? _t('Disinvite') : _t('Remove from community'), - title: isInvited ? _t('Disinvite this user from community?') - : _t('Remove this user from community?'), - danger: true, - }); - - const [proceed] = await finished; - if (!proceed) return; - - startUpdating(); - cli.removeUserFromGroup(groupId, groupMember.userId).then(() => { - // return to the user list - dis.dispatch({ - action: "view_user", - member: null, - }); - }).catch((e) => { - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - Modal.createTrackedDialog('Failed to remove user from group', '', ErrorDialog, { - title: _t('Error'), - description: isInvited ? - _t('Failed to withdraw invitation') : - _t('Failed to remove user from community'), - }); - console.log(e); - }).finally(() => { - stopUpdating(); - }); - }; - - const kickButton = ( - - { isInvited ? _t('Disinvite') : _t('Remove from community') } - - ); - - // No make/revoke admin API yet - /*const opLabel = this.state.isTargetMod ? _t("Revoke Moderator") : _t("Make Moderator"); - giveModButton = - {giveOpLabel} - ;*/ - - return - { kickButton } - { children } - ; - } - - return
    ; - }, -); + return
    ; +}; const GroupMember = PropTypes.shape({ userId: PropTypes.string.isRequired, @@ -849,7 +856,9 @@ function useRoomPermissions(cli, room, user) { return roomPermissions; } -const PowerLevelSection = withLegacyMatrixClient(({matrixClient: cli, user, room, roomPermissions, powerLevels}) => { +const PowerLevelSection = ({user, room, roomPermissions, powerLevels}) => { + const cli = useContext(MatrixClientContext); + const [isEditing, setEditing] = useState(false); if (room && user.roomId) { // is in room if (isEditing) { @@ -876,9 +885,11 @@ const PowerLevelSection = withLegacyMatrixClient(({matrixClient: cli, user, room } else { return null; } -}); +}; + +const PowerLevelEditor = ({user, room, roomPermissions, onFinished}) => { + const cli = useContext(MatrixClientContext); -const PowerLevelEditor = withLegacyMatrixClient(({matrixClient: cli, user, room, roomPermissions, onFinished}) => { const [isUpdating, setIsUpdating] = useState(false); const [selectedPowerLevel, setSelectedPowerLevel] = useState(parseInt(user.powerLevel, 10)); const [isDirty, setIsDirty] = useState(false); @@ -982,10 +993,11 @@ const PowerLevelEditor = withLegacyMatrixClient(({matrixClient: cli, user, room, {buttonOrSpinner}
    ); -}); +}; + +const UserInfo = ({user, groupId, roomId, onClose}) => { + const cli = useContext(MatrixClientContext); -// cli is injected by withLegacyMatrixClient -const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, roomId, onClose}) => { // Load room if we are given a room id and memoize it const room = useMemo(() => roomId ? cli.getRoom(roomId) : null, [cli, roomId]); @@ -1320,7 +1332,7 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room
    ); -}); +}; UserInfo.propTypes = { user: PropTypes.oneOfType([ diff --git a/src/components/views/room_settings/RelatedGroupSettings.js b/src/components/views/room_settings/RelatedGroupSettings.js index c30f446f41..20118f4f44 100644 --- a/src/components/views/room_settings/RelatedGroupSettings.js +++ b/src/components/views/room_settings/RelatedGroupSettings.js @@ -16,11 +16,12 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; -import {MatrixEvent, MatrixClient} from 'matrix-js-sdk'; +import {MatrixEvent} from 'matrix-js-sdk'; import sdk from '../../../index'; import { _t } from '../../../languageHandler'; import Modal from '../../../Modal'; import ErrorDialog from "../dialogs/ErrorDialog"; +import MatrixClientContext from "../../../contexts/MatrixClientContext"; const GROUP_ID_REGEX = /\+\S+:\S+/; @@ -31,9 +32,7 @@ export default class RelatedGroupSettings extends React.Component { relatedGroupsEvent: PropTypes.instanceOf(MatrixEvent), }; - static contextTypes = { - matrixClient: PropTypes.instanceOf(MatrixClient), - }; + static contextType = MatrixClientContext; static defaultProps = { canSetRelatedGroups: false, @@ -49,7 +48,7 @@ export default class RelatedGroupSettings extends React.Component { } updateGroups(newGroupsList) { - this.context.matrixClient.sendStateEvent(this.props.roomId, 'm.room.related_groups', { + this.context.sendStateEvent(this.props.roomId, 'm.room.related_groups', { groups: newGroupsList, }, '').catch((err) => { console.error(err); @@ -99,7 +98,7 @@ export default class RelatedGroupSettings extends React.Component { }; render() { - const localDomain = this.context.matrixClient.getDomain(); + const localDomain = this.context.getDomain(); const EditableItemList = sdk.getComponent('elements.EditableItemList'); return
    { @@ -190,7 +189,7 @@ export default class EditMessageComposer extends React.Component { if (this._isContentModified(newContent)) { const roomId = editedEvent.getRoomId(); this._cancelPreviousPendingEdit(); - this.context.matrixClient.sendMessage(roomId, editContent); + this.context.sendMessage(roomId, editContent); } // close the event editing and focus composer @@ -205,7 +204,7 @@ export default class EditMessageComposer extends React.Component { previousEdit.status === EventStatus.QUEUED || previousEdit.status === EventStatus.NOT_SENT )) { - this.context.matrixClient.cancelPendingEvent(previousEdit); + this.context.cancelPendingEvent(previousEdit); } } @@ -232,7 +231,7 @@ export default class EditMessageComposer extends React.Component { _createEditorModel() { const {editState} = this.props; const room = this._getRoom(); - const partCreator = new PartCreator(room, this.context.matrixClient); + const partCreator = new PartCreator(room, this.context); let parts; if (editState.hasEditorState()) { // if restoring state from a previous editor, diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index 988482df7f..784c4071aa 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -31,10 +31,11 @@ const TextForEvent = require('../../../TextForEvent'); import dis from '../../../dispatcher'; import SettingsStore from "../../../settings/SettingsStore"; -import {EventStatus, MatrixClient} from 'matrix-js-sdk'; +import {EventStatus} from 'matrix-js-sdk'; import {formatTime} from "../../../DateUtils"; import MatrixClientPeg from '../../../MatrixClientPeg'; import {ALL_RULE_TYPES} from "../../../mjolnir/BanList"; +import MatrixClientContext from "../../../contexts/MatrixClientContext"; const ObjectUtils = require('../../../ObjectUtils'); @@ -222,8 +223,8 @@ module.exports = createReactClass({ }; }, - contextTypes: { - matrixClient: PropTypes.instanceOf(MatrixClient).isRequired, + statics: { + contextType: MatrixClientContext, }, componentWillMount: function() { @@ -237,7 +238,7 @@ module.exports = createReactClass({ componentDidMount: function() { this._suppressReadReceiptAnimation = false; - const client = this.context.matrixClient; + const client = this.context; client.on("deviceVerificationChanged", this.onDeviceVerificationChanged); this.props.mxEvent.on("Event.decrypted", this._onDecrypted); if (this.props.showReactions) { @@ -262,7 +263,7 @@ module.exports = createReactClass({ }, componentWillUnmount: function() { - const client = this.context.matrixClient; + const client = this.context; client.removeListener("deviceVerificationChanged", this.onDeviceVerificationChanged); this.props.mxEvent.removeListener("Event.decrypted", this._onDecrypted); if (this.props.showReactions) { @@ -291,7 +292,7 @@ module.exports = createReactClass({ return; } - const verified = await this.context.matrixClient.isEventSenderVerified(mxEvent); + const verified = await this.context.isEventSenderVerified(mxEvent); this.setState({ verified: verified, }, () => { @@ -349,11 +350,11 @@ module.exports = createReactClass({ }, shouldHighlight: function() { - const actions = this.context.matrixClient.getPushActionsForEvent(this.props.mxEvent); + const actions = this.context.getPushActionsForEvent(this.props.mxEvent); if (!actions || !actions.tweaks) { return false; } // don't show self-highlights from another of our clients - if (this.props.mxEvent.getSender() === this.context.matrixClient.credentials.userId) { + if (this.props.mxEvent.getSender() === this.context.credentials.userId) { return false; } @@ -461,7 +462,7 @@ module.exports = createReactClass({ // Cancel any outgoing key request for this event and resend it. If a response // is received for the request with the required keys, the event could be // decrypted successfully. - this.context.matrixClient.cancelAndResendEventRoomKeyRequest(this.props.mxEvent); + this.context.cancelAndResendEventRoomKeyRequest(this.props.mxEvent); }, onPermalinkClicked: function(e) { @@ -494,7 +495,7 @@ module.exports = createReactClass({ } } - if (this.context.matrixClient.isRoomEncrypted(ev.getRoomId())) { + if (this.context.isRoomEncrypted(ev.getRoomId())) { // else if room is encrypted // and event is being encrypted or is not_sent (Unknown Devices/Network Error) if (ev.status === EventStatus.ENCRYPTING) { @@ -741,7 +742,7 @@ module.exports = createReactClass({ switch (this.props.tileShape) { case 'notif': { - const room = this.context.matrixClient.getRoom(this.props.mxEvent.getRoomId()); + const room = this.context.getRoom(this.props.mxEvent.getRoomId()); return (
    diff --git a/src/components/views/rooms/MemberInfo.js b/src/components/views/rooms/MemberInfo.js index 1a2c8e2212..cb8c5b8d49 100644 --- a/src/components/views/rooms/MemberInfo.js +++ b/src/components/views/rooms/MemberInfo.js @@ -31,7 +31,6 @@ import React from 'react'; import PropTypes from 'prop-types'; import createReactClass from 'create-react-class'; import classNames from 'classnames'; -import { MatrixClient } from 'matrix-js-sdk'; import dis from '../../../dispatcher'; import Modal from '../../../Modal'; import sdk from '../../../index'; @@ -48,7 +47,7 @@ import SettingsStore from "../../../settings/SettingsStore"; import E2EIcon from "./E2EIcon"; import AutoHideScrollbar from "../../structures/AutoHideScrollbar"; import MatrixClientPeg from "../../../MatrixClientPeg"; -import {EventTimeline} from "matrix-js-sdk"; +import MatrixClientContext from "../../../contexts/MatrixClientContext"; module.exports = createReactClass({ displayName: 'MemberInfo', @@ -76,13 +75,13 @@ module.exports = createReactClass({ }; }, - contextTypes: { - matrixClient: PropTypes.instanceOf(MatrixClient).isRequired, + statics: { + contextType: MatrixClientContext, }, componentWillMount: function() { this._cancelDeviceList = null; - const cli = this.context.matrixClient; + const cli = this.context; // only display the devices list if our client supports E2E this._enableDevices = cli.isCryptoEnabled(); @@ -112,7 +111,7 @@ module.exports = createReactClass({ }, componentWillUnmount: function() { - const client = this.context.matrixClient; + const client = this.context; if (client) { client.removeListener("deviceVerificationChanged", this.onDeviceVerificationChanged); client.removeListener("Room", this.onRoom); @@ -131,7 +130,7 @@ module.exports = createReactClass({ }, _checkIgnoreState: function() { - const isIgnoring = this.context.matrixClient.isUserIgnored(this.props.member.userId); + const isIgnoring = this.context.isUserIgnored(this.props.member.userId); this.setState({isIgnoring: isIgnoring}); }, @@ -163,7 +162,7 @@ module.exports = createReactClass({ // Promise.resolve to handle transition from static result to promise; can be removed // in future - Promise.resolve(this.context.matrixClient.getStoredDevicesForUser(userId)).then((devices) => { + Promise.resolve(this.context.getStoredDevicesForUser(userId)).then((devices) => { this.setState({ devices: devices, e2eStatus: this._getE2EStatus(devices), @@ -197,7 +196,7 @@ module.exports = createReactClass({ onRoomReceipt: function(receiptEvent, room) { // because if we read a notification, it will affect notification count // only bother updating if there's a receipt from us - if (findReadReceiptFromUserId(receiptEvent, this.context.matrixClient.credentials.userId)) { + if (findReadReceiptFromUserId(receiptEvent, this.context.credentials.userId)) { this.forceUpdate(); } }, @@ -242,7 +241,7 @@ module.exports = createReactClass({ let cancelled = false; this._cancelDeviceList = function() { cancelled = true; }; - const client = this.context.matrixClient; + const client = this.context; const self = this; client.downloadKeys([member.userId], true).then(() => { return client.getStoredDevicesForUser(member.userId); @@ -267,7 +266,7 @@ module.exports = createReactClass({ }, onIgnoreToggle: function() { - const ignoredUsers = this.context.matrixClient.getIgnoredUsers(); + const ignoredUsers = this.context.getIgnoredUsers(); if (this.state.isIgnoring) { const index = ignoredUsers.indexOf(this.props.member.userId); if (index !== -1) ignoredUsers.splice(index, 1); @@ -275,7 +274,7 @@ module.exports = createReactClass({ ignoredUsers.push(this.props.member.userId); } - this.context.matrixClient.setIgnoredUsers(ignoredUsers).then(() => { + this.context.setIgnoredUsers(ignoredUsers).then(() => { return this.setState({isIgnoring: !this.state.isIgnoring}); }); }, @@ -293,7 +292,7 @@ module.exports = createReactClass({ if (!proceed) return; this.setState({ updating: this.state.updating + 1 }); - this.context.matrixClient.kick( + this.context.kick( this.props.member.roomId, this.props.member.userId, reason || undefined, ).then(function() { @@ -329,11 +328,11 @@ module.exports = createReactClass({ this.setState({ updating: this.state.updating + 1 }); let promise; if (this.props.member.membership === 'ban') { - promise = this.context.matrixClient.unban( + promise = this.context.unban( this.props.member.roomId, this.props.member.userId, ); } else { - promise = this.context.matrixClient.ban( + promise = this.context.ban( this.props.member.roomId, this.props.member.userId, reason || undefined, ); @@ -360,7 +359,7 @@ module.exports = createReactClass({ onRedactAllMessages: async function() { const {roomId, userId} = this.props.member; - const room = this.context.matrixClient.getRoom(roomId); + const room = this.context.getRoom(roomId); if (!room) { return; } @@ -414,7 +413,7 @@ module.exports = createReactClass({ console.info(`Started redacting recent ${count} messages for ${user} in ${roomId}`); await Promise.all(eventsToRedact.map(async event => { try { - await this.context.matrixClient.redactEvent(roomId, event.getId()); + await this.context.redactEvent(roomId, event.getId()); } catch (err) { // log and swallow errors console.error("Could not redact", event.getId()); @@ -446,11 +445,11 @@ module.exports = createReactClass({ const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); const roomId = this.props.member.roomId; const target = this.props.member.userId; - const room = this.context.matrixClient.getRoom(roomId); + const room = this.context.getRoom(roomId); if (!room) return; // if muting self, warn as it may be irreversible - if (target === this.context.matrixClient.getUserId()) { + if (target === this.context.getUserId()) { try { if (!(await this._warnSelfDemote())) return; } catch (e) { @@ -478,7 +477,7 @@ module.exports = createReactClass({ if (!isNaN(level)) { this.setState({ updating: this.state.updating + 1 }); - this.context.matrixClient.setPowerLevel(roomId, target, level, powerLevelEvent).then( + this.context.setPowerLevel(roomId, target, level, powerLevelEvent).then( function() { // NO-OP; rely on the m.room.member event coming down else we could // get out of sync if we force setState here! @@ -500,13 +499,13 @@ module.exports = createReactClass({ const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); const roomId = this.props.member.roomId; const target = this.props.member.userId; - const room = this.context.matrixClient.getRoom(roomId); + const room = this.context.getRoom(roomId); if (!room) return; const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", ""); if (!powerLevelEvent) return; - const me = room.getMember(this.context.matrixClient.credentials.userId); + const me = room.getMember(this.context.credentials.userId); if (!me) return; const defaultLevel = powerLevelEvent.getContent().users_default; @@ -515,7 +514,7 @@ module.exports = createReactClass({ // toggle the level const newLevel = this.state.isTargetMod ? defaultLevel : modLevel; this.setState({ updating: this.state.updating + 1 }); - this.context.matrixClient.setPowerLevel(roomId, target, parseInt(newLevel), powerLevelEvent).then( + this.context.setPowerLevel(roomId, target, parseInt(newLevel), powerLevelEvent).then( function() { // NO-OP; rely on the m.room.member event coming down else we could // get out of sync if we force setState here! @@ -550,7 +549,7 @@ module.exports = createReactClass({ danger: true, onFinished: (accepted) => { if (!accepted) return; - this.context.matrixClient.deactivateSynapseUser(this.props.member.userId).catch(e => { + this.context.deactivateSynapseUser(this.props.member.userId).catch(e => { console.error("Failed to deactivate user"); console.error(e); @@ -566,7 +565,7 @@ module.exports = createReactClass({ _applyPowerChange: function(roomId, target, powerLevel, powerLevelEvent) { this.setState({ updating: this.state.updating + 1 }); - this.context.matrixClient.setPowerLevel(roomId, target, parseInt(powerLevel), powerLevelEvent).then( + this.context.setPowerLevel(roomId, target, parseInt(powerLevel), powerLevelEvent).then( function() { // NO-OP; rely on the m.room.member event coming down else we could // get out of sync if we force setState here! @@ -587,7 +586,7 @@ module.exports = createReactClass({ onPowerChange: async function(powerLevel) { const roomId = this.props.member.roomId; const target = this.props.member.userId; - const room = this.context.matrixClient.getRoom(roomId); + const room = this.context.getRoom(roomId); if (!room) return; const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", ""); @@ -598,7 +597,7 @@ module.exports = createReactClass({ return; } - const myUserId = this.context.matrixClient.getUserId(); + const myUserId = this.context.getUserId(); const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); // If we are changing our own PL it can only ever be decreasing, which we cannot reverse. @@ -650,9 +649,9 @@ module.exports = createReactClass({ _calculateOpsPermissions: async function(member) { let canDeactivate = false; - if (this.context.matrixClient) { + if (this.context) { try { - canDeactivate = await this.context.matrixClient.isSynapseAdministrator(); + canDeactivate = await this.context.isSynapseAdministrator(); } catch (e) { console.error(e); } @@ -665,13 +664,13 @@ module.exports = createReactClass({ }, muted: false, }; - const room = this.context.matrixClient.getRoom(member.roomId); + const room = this.context.getRoom(member.roomId); if (!room) return defaultPerms; const powerLevels = room.currentState.getStateEvents("m.room.power_levels", ""); if (!powerLevels) return defaultPerms; - const me = room.getMember(this.context.matrixClient.credentials.userId); + const me = room.getMember(this.context.credentials.userId); if (!me) return defaultPerms; const them = member; @@ -738,7 +737,7 @@ module.exports = createReactClass({ const avatarUrl = member.getMxcAvatarUrl(); if (!avatarUrl) return; - const httpUrl = this.context.matrixClient.mxcUrlToHttp(avatarUrl); + const httpUrl = this.context.mxcUrlToHttp(avatarUrl); const ImageView = sdk.getComponent("elements.ImageView"); const params = { src: httpUrl, @@ -797,7 +796,7 @@ module.exports = createReactClass({ }, _renderUserOptions: function() { - const cli = this.context.matrixClient; + const cli = this.context; const member = this.props.member; let ignoreButton = null; @@ -905,9 +904,9 @@ module.exports = createReactClass({ let synapseDeactivateButton; let spinner; - if (this.props.member.userId !== this.context.matrixClient.credentials.userId) { + if (this.props.member.userId !== this.context.credentials.userId) { // TODO: Immutable DMs replaces a lot of this - const dmRoomMap = new DMRoomMap(this.context.matrixClient); + const dmRoomMap = new DMRoomMap(this.context); // dmRooms will not include dmRooms that we have been invited into but did not join. // Because DMRoomMap runs off account_data[m.direct] which is only set on join of dm room. // XXX: we potentially want DMs we have been invited to, to also show up here :L @@ -918,7 +917,7 @@ module.exports = createReactClass({ const tiles = []; for (const roomId of dmRooms) { - const room = this.context.matrixClient.getRoom(roomId); + const room = this.context.getRoom(roomId); if (room) { const myMembership = room.getMyMembership(); // not a DM room if we have are not joined @@ -1064,12 +1063,12 @@ module.exports = createReactClass({ } } - const room = this.context.matrixClient.getRoom(this.props.member.roomId); + const room = this.context.getRoom(this.props.member.roomId); const powerLevelEvent = room ? room.currentState.getStateEvents("m.room.power_levels", "") : null; const powerLevelUsersDefault = powerLevelEvent ? powerLevelEvent.getContent().users_default : 0; const enablePresenceByHsUrl = SdkConfig.get()["enable_presence_by_hs_url"]; - const hsUrl = this.context.matrixClient.baseUrl; + const hsUrl = this.context.baseUrl; let showPresence = true; if (enablePresenceByHsUrl && enablePresenceByHsUrl[hsUrl] !== undefined) { showPresence = enablePresenceByHsUrl[hsUrl]; @@ -1108,7 +1107,7 @@ module.exports = createReactClass({
    ; - const isEncrypted = this.context.matrixClient.isRoomEncrypted(this.props.member.roomId); + const isEncrypted = this.context.isRoomEncrypted(this.props.member.roomId); if (this.state.e2eStatus && isEncrypted) { e2eIconElement = (); } @@ -1117,7 +1116,7 @@ module.exports = createReactClass({ const avatarUrl = this.props.member.getMxcAvatarUrl(); let avatarElement; if (avatarUrl) { - const httpUrl = this.context.matrixClient.mxcUrlToHttp(avatarUrl, 800, 800); + const httpUrl = this.context.mxcUrlToHttp(avatarUrl, 800, 800); avatarElement =
    ; diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js index 580e3b0d81..06e6834bec 100644 --- a/src/components/views/rooms/MessageComposer.js +++ b/src/components/views/rooms/MessageComposer.js @@ -107,8 +107,8 @@ class UploadButton extends React.Component { roomId: PropTypes.string.isRequired, } - constructor(props, context) { - super(props, context); + constructor(props) { + super(props); this.onUploadClick = this.onUploadClick.bind(this); this.onUploadFileInputChange = this.onUploadFileInputChange.bind(this); @@ -165,8 +165,8 @@ class UploadButton extends React.Component { } export default class MessageComposer extends React.Component { - constructor(props, context) { - super(props, context); + constructor(props) { + super(props); this.onInputStateChanged = this.onInputStateChanged.bind(this); this.onEvent = this.onEvent.bind(this); this._onRoomStateEvents = this._onRoomStateEvents.bind(this); diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index a5d8492d99..3d462647c8 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -141,8 +141,8 @@ export default class MessageComposerInput extends React.Component { autocomplete: Autocomplete; historyManager: SlateComposerHistoryManager; - constructor(props, context) { - super(props, context); + constructor(props) { + super(props); const isRichTextEnabled = SettingsStore.getValue('MessageComposerInput.isRichTextEnabled'); diff --git a/src/components/views/rooms/ReplyPreview.js b/src/components/views/rooms/ReplyPreview.js index caf8feeea2..af2ea640f5 100644 --- a/src/components/views/rooms/ReplyPreview.js +++ b/src/components/views/rooms/ReplyPreview.js @@ -35,8 +35,8 @@ export default class ReplyPreview extends React.Component { permalinkCreator: PropTypes.instanceOf(RoomPermalinkCreator).isRequired, }; - constructor(props, context) { - super(props, context); + constructor(props) { + super(props); this.unmounted = false; this.state = { diff --git a/src/components/views/rooms/SendMessageComposer.js b/src/components/views/rooms/SendMessageComposer.js index af25155588..af7177ebc7 100644 --- a/src/components/views/rooms/SendMessageComposer.js +++ b/src/components/views/rooms/SendMessageComposer.js @@ -26,7 +26,6 @@ import { unescapeMessage, } from '../../../editor/serialize'; import {CommandPartCreator} from '../../../editor/parts'; -import {MatrixClient} from 'matrix-js-sdk'; import BasicMessageComposer from "./BasicMessageComposer"; import ReplyPreview from "./ReplyPreview"; import RoomViewStore from '../../../stores/RoomViewStore'; @@ -40,6 +39,7 @@ import Modal from '../../../Modal'; import {_t, _td} from '../../../languageHandler'; import ContentMessages from '../../../ContentMessages'; import {Key} from "../../../Keyboard"; +import MatrixClientContext from "../../../contexts/MatrixClientContext"; function addReplyToMessageContent(content, repliedToEvent, permalinkCreator) { const replyContent = ReplyThread.makeReplyMixIn(repliedToEvent); @@ -89,12 +89,10 @@ export default class SendMessageComposer extends React.Component { permalinkCreator: PropTypes.object.isRequired, }; - static contextTypes = { - matrixClient: PropTypes.instanceOf(MatrixClient).isRequired, - }; + static contextType = MatrixClientContext; - constructor(props, context) { - super(props, context); + constructor(props) { + super(props); this.model = null; this._editorRef = null; this.currentlyComposedEditorState = null; @@ -245,7 +243,7 @@ export default class SendMessageComposer extends React.Component { const isReply = !!RoomViewStore.getQuotingEvent(); const {roomId} = this.props.room; const content = createMessageContent(this.model, this.props.permalinkCreator); - this.context.matrixClient.sendMessage(roomId, content); + this.context.sendMessage(roomId, content); if (isReply) { // Clear reply_to_event as we put the message into the queue // if the send fails, retry will handle resending. @@ -273,7 +271,7 @@ export default class SendMessageComposer extends React.Component { } componentWillMount() { - const partCreator = new CommandPartCreator(this.props.room, this.context.matrixClient); + const partCreator = new CommandPartCreator(this.props.room, this.context); const parts = this._restoreStoredEditorState(partCreator) || []; this.model = new EditorModel(parts, partCreator); this.dispatcherRef = dis.register(this.onAction); @@ -361,7 +359,7 @@ export default class SendMessageComposer extends React.Component { // from Finder) but more images copied from a different website // / word processor etc. ContentMessages.sharedInstance().sendContentListToRoom( - Array.from(clipboardData.files), this.props.room.roomId, this.context.matrixClient, + Array.from(clipboardData.files), this.props.room.roomId, this.context, ); } } diff --git a/src/components/views/rooms/SlateMessageComposer.js b/src/components/views/rooms/SlateMessageComposer.js index ebd9017d73..2b68e0d338 100644 --- a/src/components/views/rooms/SlateMessageComposer.js +++ b/src/components/views/rooms/SlateMessageComposer.js @@ -137,8 +137,8 @@ class UploadButton extends React.Component { static propTypes = { roomId: PropTypes.string.isRequired, } - constructor(props, context) { - super(props, context); + constructor(props) { + super(props); this.onUploadClick = this.onUploadClick.bind(this); this.onUploadFileInputChange = this.onUploadFileInputChange.bind(this); @@ -193,8 +193,8 @@ class UploadButton extends React.Component { } export default class SlateMessageComposer extends React.Component { - constructor(props, context) { - super(props, context); + constructor(props) { + super(props); this._onAutocompleteConfirm = this._onAutocompleteConfirm.bind(this); this.onToggleFormattingClicked = this.onToggleFormattingClicked.bind(this); this.onToggleMarkdownClicked = this.onToggleMarkdownClicked.bind(this); diff --git a/src/components/views/settings/DevicesPanel.js b/src/components/views/settings/DevicesPanel.js index cb5db10be4..cdde53b44b 100644 --- a/src/components/views/settings/DevicesPanel.js +++ b/src/components/views/settings/DevicesPanel.js @@ -25,8 +25,8 @@ import { _t } from '../../../languageHandler'; import Modal from '../../../Modal'; export default class DevicesPanel extends React.Component { - constructor(props, context) { - super(props, context); + constructor(props) { + super(props); this.state = { devices: undefined, diff --git a/src/components/views/settings/DevicesPanelEntry.js b/src/components/views/settings/DevicesPanelEntry.js index 98ba29471d..533c15976b 100644 --- a/src/components/views/settings/DevicesPanelEntry.js +++ b/src/components/views/settings/DevicesPanelEntry.js @@ -23,8 +23,8 @@ import MatrixClientPeg from '../../../MatrixClientPeg'; import {formatDate} from '../../../DateUtils'; export default class DevicesPanelEntry extends React.Component { - constructor(props, context) { - super(props, context); + constructor(props) { + super(props); this._unmounted = false; this.onDeviceToggled = this.onDeviceToggled.bind(this); diff --git a/src/components/views/settings/tabs/room/GeneralRoomSettingsTab.js b/src/components/views/settings/tabs/room/GeneralRoomSettingsTab.js index 5d707fcf16..2e718b0b69 100644 --- a/src/components/views/settings/tabs/room/GeneralRoomSettingsTab.js +++ b/src/components/views/settings/tabs/room/GeneralRoomSettingsTab.js @@ -18,22 +18,19 @@ import React from 'react'; import PropTypes from 'prop-types'; import {_t} from "../../../../../languageHandler"; import RoomProfileSettings from "../../../room_settings/RoomProfileSettings"; -import MatrixClientPeg from "../../../../../MatrixClientPeg"; import sdk from "../../../../.."; import AccessibleButton from "../../../elements/AccessibleButton"; -import {MatrixClient} from "matrix-js-sdk"; import dis from "../../../../../dispatcher"; import LabelledToggleSwitch from "../../../elements/LabelledToggleSwitch"; +import MatrixClientContext from "../../../../../contexts/MatrixClientContext"; export default class GeneralRoomSettingsTab extends React.Component { - static childContextTypes = { - matrixClient: PropTypes.instanceOf(MatrixClient), - }; - static propTypes = { roomId: PropTypes.string.isRequired, }; + static contextType = MatrixClientContext; + constructor() { super(); @@ -42,14 +39,8 @@ export default class GeneralRoomSettingsTab extends React.Component { }; } - getChildContext() { - return { - matrixClient: MatrixClientPeg.get(), - }; - } - componentWillMount() { - MatrixClientPeg.get().getRoomDirectoryVisibility(this.props.roomId).then((result => { + this.context.getRoomDirectoryVisibility(this.props.roomId).then((result => { this.setState({isRoomPublished: result.visibility === 'public'}); })); } @@ -59,7 +50,7 @@ export default class GeneralRoomSettingsTab extends React.Component { const newValue = !valueBefore; this.setState({isRoomPublished: newValue}); - MatrixClientPeg.get().setRoomDirectoryVisibility( + this.context.setRoomDirectoryVisibility( this.props.roomId, newValue ? 'public' : 'private', ).catch(() => { @@ -80,7 +71,7 @@ export default class GeneralRoomSettingsTab extends React.Component { const RelatedGroupSettings = sdk.getComponent("room_settings.RelatedGroupSettings"); const UrlPreviewSettings = sdk.getComponent("room_settings.UrlPreviewSettings"); - const client = MatrixClientPeg.get(); + const client = this.context; const room = client.getRoom(this.props.roomId); const canSetAliases = true; // Previously, we arbitrarily only allowed admins to do this diff --git a/src/components/views/settings/tabs/user/FlairUserSettingsTab.js b/src/components/views/settings/tabs/user/FlairUserSettingsTab.js index 0063a9a981..26e0033233 100644 --- a/src/components/views/settings/tabs/user/FlairUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/FlairUserSettingsTab.js @@ -17,25 +17,8 @@ limitations under the License. import React from 'react'; import {_t} from "../../../../../languageHandler"; import GroupUserSettings from "../../../groups/GroupUserSettings"; -import MatrixClientPeg from "../../../../../MatrixClientPeg"; -import PropTypes from "prop-types"; -import {MatrixClient} from "matrix-js-sdk"; export default class FlairUserSettingsTab extends React.Component { - static childContextTypes = { - matrixClient: PropTypes.instanceOf(MatrixClient), - }; - - constructor() { - super(); - } - - getChildContext() { - return { - matrixClient: MatrixClientPeg.get(), - }; - } - render() { return (
    diff --git a/src/utils/withLegacyMatrixClient.js b/src/contexts/MatrixClientContext.js similarity index 51% rename from src/utils/withLegacyMatrixClient.js rename to src/contexts/MatrixClientContext.js index af6a930a88..54a23ca132 100644 --- a/src/utils/withLegacyMatrixClient.js +++ b/src/contexts/MatrixClientContext.js @@ -14,18 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from "react"; -import PropTypes from "prop-types"; -import {MatrixClient} from "matrix-js-sdk"; +import { createContext } from "react"; -// Higher Order Component to allow use of legacy MatrixClient React Context -// in Functional Components which do not otherwise support legacy React Contexts -export default (Component) => class extends React.PureComponent { - static contextTypes = { - matrixClient: PropTypes.instanceOf(MatrixClient).isRequired, - }; - - render() { - return ; - } -}; +const MatrixClientContext = createContext(undefined); +MatrixClientContext.displayName = "MatrixClientContext"; +export default MatrixClientContext; diff --git a/src/contexts/RoomContext.js b/src/contexts/RoomContext.js new file mode 100644 index 0000000000..8613be195c --- /dev/null +++ b/src/contexts/RoomContext.js @@ -0,0 +1,25 @@ +/* +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 { createContext } from "react"; + +const RoomContext = createContext({ + canReact: undefined, + canReply: undefined, + room: undefined, +}); +RoomContext.displayName = "RoomContext"; +export default RoomContext; diff --git a/test/components/structures/MessagePanel-test.js b/test/components/structures/MessagePanel-test.js index 7c52512bc2..b7c7b4a396 100644 --- a/test/components/structures/MessagePanel-test.js +++ b/test/components/structures/MessagePanel-test.js @@ -36,27 +36,14 @@ const test_utils = require('test-utils'); const mockclock = require('mock-clock'); import Velocity from 'velocity-animate'; +import MatrixClientContext from "../../../src/contexts/MatrixClientContext"; +import RoomContext from "../../../src/contexts/RoomContext"; let client; const room = new Matrix.Room(); // wrap MessagePanel with a component which provides the MatrixClient in the context. const WrappedMessagePanel = createReactClass({ - childContextTypes: { - matrixClient: PropTypes.object, - room: PropTypes.object, - }, - - getChildContext: function() { - return { - matrixClient: client, - room: { - canReact: true, - canReply: true, - }, - }; - }, - getInitialState: function() { return { resizeNotifier: new EventEmitter(), @@ -64,7 +51,11 @@ const WrappedMessagePanel = createReactClass({ }, render: function() { - return ; + return + + + + ; }, }); diff --git a/test/test-utils.js b/test/test-utils.js index 64704fc610..047b727251 100644 --- a/test/test-utils.js +++ b/test/test-utils.js @@ -9,6 +9,7 @@ import jssdk from 'matrix-js-sdk'; import {makeType} from "../src/utils/TypeUtils"; import {ValidatedServerConfig} from "../src/utils/AutoDiscoveryUtils"; import ShallowRenderer from 'react-test-renderer/shallow'; +import MatrixClientContext from "../src/contexts/MatrixClientContext"; const MatrixEvent = jssdk.MatrixEvent; /** @@ -291,22 +292,16 @@ export function getDispatchForStore(store) { export function wrapInMatrixClientContext(WrappedComponent) { class Wrapper extends React.Component { - static childContextTypes = { - matrixClient: PropTypes.object, - } + constructor(props) { + super(props); - getChildContext() { - return { - matrixClient: this._matrixClient, - }; - } - - componentWillMount() { this._matrixClient = peg.get(); } render() { - return ; + return + + ; } } return Wrapper; From da4d72b3c455452d1b2782dd1957ee47b36c81d6 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 17 Dec 2019 17:34:03 +0000 Subject: [PATCH 12/93] delint --- src/components/views/elements/Flair.js | 1 - src/components/views/groups/GroupTile.js | 1 - src/components/views/right_panel/UserInfo.js | 2 -- src/components/views/rooms/EditMessageComposer.js | 1 - test/test-utils.js | 1 - 5 files changed, 6 deletions(-) diff --git a/src/components/views/elements/Flair.js b/src/components/views/elements/Flair.js index ef208bbea9..0af772466b 100644 --- a/src/components/views/elements/Flair.js +++ b/src/components/views/elements/Flair.js @@ -18,7 +18,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import {MatrixClient} from 'matrix-js-sdk'; import FlairStore from '../../../stores/FlairStore'; import dis from '../../../dispatcher'; import MatrixClientContext from "../../../contexts/MatrixClientContext"; diff --git a/src/components/views/groups/GroupTile.js b/src/components/views/groups/GroupTile.js index a6447cdf14..f3d7418a44 100644 --- a/src/components/views/groups/GroupTile.js +++ b/src/components/views/groups/GroupTile.js @@ -17,7 +17,6 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; import createReactClass from 'create-react-class'; -import {MatrixClient} from 'matrix-js-sdk'; import { Draggable, Droppable } from 'react-beautiful-dnd'; import sdk from '../../../index'; import dis from '../../../dispatcher'; diff --git a/src/components/views/right_panel/UserInfo.js b/src/components/views/right_panel/UserInfo.js index e952f8fad9..793a06bbaf 100644 --- a/src/components/views/right_panel/UserInfo.js +++ b/src/components/views/right_panel/UserInfo.js @@ -857,8 +857,6 @@ function useRoomPermissions(cli, room, user) { } const PowerLevelSection = ({user, room, roomPermissions, powerLevels}) => { - const cli = useContext(MatrixClientContext); - const [isEditing, setEditing] = useState(false); if (room && user.roomId) { // is in room if (isEditing) { diff --git a/src/components/views/rooms/EditMessageComposer.js b/src/components/views/rooms/EditMessageComposer.js index 94ef00183e..43f378a1e0 100644 --- a/src/components/views/rooms/EditMessageComposer.js +++ b/src/components/views/rooms/EditMessageComposer.js @@ -26,7 +26,6 @@ import {findEditableEvent} from '../../../utils/EventUtils'; import {parseEvent} from '../../../editor/deserialize'; import {PartCreator} from '../../../editor/parts'; import EditorStateTransfer from '../../../utils/EditorStateTransfer'; -import {MatrixClient} from 'matrix-js-sdk'; import classNames from 'classnames'; import {EventStatus} from 'matrix-js-sdk'; import BasicMessageComposer from "./BasicMessageComposer"; diff --git a/test/test-utils.js b/test/test-utils.js index 047b727251..5c8c7f8a10 100644 --- a/test/test-utils.js +++ b/test/test-utils.js @@ -2,7 +2,6 @@ import sinon from 'sinon'; import React from 'react'; -import PropTypes from 'prop-types'; import peg from '../src/MatrixClientPeg'; import dis from '../src/dispatcher'; import jssdk from 'matrix-js-sdk'; From f3ca4c0b7cf04d5d6d0a3bbf5b7204211bf1d65a Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 17 Dec 2019 17:54:19 +0000 Subject: [PATCH 13/93] fix tests --- .../views/elements/MemberEventListSummary-test.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/components/views/elements/MemberEventListSummary-test.js b/test/components/views/elements/MemberEventListSummary-test.js index a31cbdebb5..a25b2cb945 100644 --- a/test/components/views/elements/MemberEventListSummary-test.js +++ b/test/components/views/elements/MemberEventListSummary-test.js @@ -115,7 +115,8 @@ describe('MemberEventListSummary', function() { const renderer = new ShallowRenderer(); renderer.render(); - const result = renderer.getRenderOutput(); + const wrapper = renderer.getRenderOutput(); // matrix cli context wrapper + const result = wrapper.props.children[0]; expect(result.props.children).toEqual([
    Expanded membership
    , @@ -137,7 +138,8 @@ describe('MemberEventListSummary', function() { const renderer = new ShallowRenderer(); renderer.render(); - const result = renderer.getRenderOutput(); + const wrapper = renderer.getRenderOutput(); // matrix cli context wrapper + const result = wrapper.props.children[0]; expect(result.props.children).toEqual([
    Expanded membership
    , From 513ec30ef1fad097095fb65c0d0d29be3dcba5e3 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 17 Dec 2019 18:02:45 +0000 Subject: [PATCH 14/93] Fix tests v2 --- test/components/views/elements/MemberEventListSummary-test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/components/views/elements/MemberEventListSummary-test.js b/test/components/views/elements/MemberEventListSummary-test.js index a25b2cb945..906ba45711 100644 --- a/test/components/views/elements/MemberEventListSummary-test.js +++ b/test/components/views/elements/MemberEventListSummary-test.js @@ -116,7 +116,7 @@ describe('MemberEventListSummary', function() { const renderer = new ShallowRenderer(); renderer.render(); const wrapper = renderer.getRenderOutput(); // matrix cli context wrapper - const result = wrapper.props.children[0]; + const result = wrapper.props.children; expect(result.props.children).toEqual([
    Expanded membership
    , @@ -139,7 +139,7 @@ describe('MemberEventListSummary', function() { const renderer = new ShallowRenderer(); renderer.render(); const wrapper = renderer.getRenderOutput(); // matrix cli context wrapper - const result = wrapper.props.children[0]; + const result = wrapper.props.children; expect(result.props.children).toEqual([
    Expanded membership
    , From d35b01b63a5ed1f5d0da79f8872500ef6a7fedbc Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 18 Dec 2019 15:40:19 +0000 Subject: [PATCH 15/93] Get rid of stripped-emoji.json in favour of an in-memory single truth source --- package.json | 1 - scripts/emoji-data-strip.js | 30 ------- src/HtmlUtils.js | 23 +----- src/autocomplete/EmojiProvider.js | 36 ++++---- src/autocomplete/QueryMatcher.js | 1 + .../views/emojipicker/EmojiPicker.js | 46 +---------- .../views/emojipicker/QuickReactions.js | 8 +- .../views/rooms/BasicMessageComposer.js | 5 +- .../views/rooms/MessageComposerInput.js | 4 +- src/emoji.js | 82 +++++++++++++++++++ src/stripped-emoji.json | 1 - 11 files changed, 115 insertions(+), 122 deletions(-) delete mode 100644 scripts/emoji-data-strip.js create mode 100644 src/emoji.js delete mode 100644 src/stripped-emoji.json diff --git a/package.json b/package.json index 8688fe42e6..7ef14e6635 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,6 @@ "diff-i18n": "cp src/i18n/strings/en_EN.json src/i18n/strings/en_EN_orig.json && ./scripts/gen-i18n.js && node scripts/compare-file.js src/i18n/strings/en_EN_orig.json src/i18n/strings/en_EN.json", "build": "yarn reskindex && yarn start:init", "build:watch": "babel src -w --skip-initial-build -d lib --source-maps --copy-files", - "emoji-data-strip": "node scripts/emoji-data-strip.js", "start": "yarn start:init && yarn start:all", "start:all": "concurrently --kill-others-on-fail --prefix \"{time} [{name}]\" -n build,reskindex \"yarn build:watch\" \"yarn reskindex:watch\"", "start:init": "babel src -d lib --source-maps --copy-files", diff --git a/scripts/emoji-data-strip.js b/scripts/emoji-data-strip.js deleted file mode 100644 index 1c3738cab1..0000000000 --- a/scripts/emoji-data-strip.js +++ /dev/null @@ -1,30 +0,0 @@ -#!/usr/bin/env node - -// This generates src/stripped-emoji.json as used by the EmojiProvider autocomplete -// provider. - -const EMOJIBASE = require('emojibase-data/en/compact.json'); - -const fs = require('fs'); - -const output = EMOJIBASE.map( - (datum) => { - const newDatum = { - name: datum.annotation, - shortname: `:${datum.shortcodes[0]}:`, - category: datum.group, - emoji_order: datum.order, - }; - if (datum.shortcodes.length > 1) { - newDatum.aliases = datum.shortcodes.slice(1).map(s => `:${s}:`); - } - if (datum.emoticon) { - newDatum.aliases_ascii = [ datum.emoticon ]; - } - return newDatum; - } -); - -// Write to a file in src. Changes should be checked into git. This file is copied by -// babel using --copy-files -fs.writeFileSync('./src/stripped-emoji.json', JSON.stringify(output)); diff --git a/src/HtmlUtils.js b/src/HtmlUtils.js index 2b7384a5aa..7cdff26a21 100644 --- a/src/HtmlUtils.js +++ b/src/HtmlUtils.js @@ -32,9 +32,9 @@ import classNames from 'classnames'; import MatrixClientPeg from './MatrixClientPeg'; import url from 'url'; -import EMOJIBASE from 'emojibase-data/en/compact.json'; import EMOJIBASE_REGEX from 'emojibase-regex'; import {tryTransformPermalinkToLocalHref} from "./utils/permalinks/Permalinks"; +import {SHORTCODE_TO_EMOJI, getEmojiFromUnicode} from "./emoji"; linkifyMatrix(linkify); @@ -58,8 +58,6 @@ const COLOR_REGEX = /^#[0-9a-fA-F]{6}$/; const PERMITTED_URL_SCHEMES = ['http', 'https', 'ftp', 'mailto', 'magnet']; -const VARIATION_SELECTOR = String.fromCharCode(0xFE0F); - /* * Return true if the given string contains emoji * Uses a much, much simpler regex than emojibase's so will give false @@ -71,21 +69,6 @@ function mightContainEmoji(str) { return SURROGATE_PAIR_PATTERN.test(str) || SYMBOL_PATTERN.test(str); } -/** - * Find emoji data in emojibase by character. - * - * @param {String} char The emoji character - * @return {Object} The emoji data - */ -export function findEmojiData(char) { - // Check against both the char and the char with an empty variation selector - // appended because that's how emojibase stores its base emojis which have - // variations. - // See also https://github.com/vector-im/riot-web/issues/9785. - const emptyVariation = char + VARIATION_SELECTOR; - return EMOJIBASE.find(e => e.unicode === char || e.unicode === emptyVariation); -} - /** * Returns the shortcode for an emoji character. * @@ -93,7 +76,7 @@ export function findEmojiData(char) { * @return {String} The shortcode (such as :thumbup:) */ export function unicodeToShortcode(char) { - const data = findEmojiData(char); + const data = getEmojiFromUnicode(char); return (data && data.shortcodes ? `:${data.shortcodes[0]}:` : ''); } @@ -105,7 +88,7 @@ export function unicodeToShortcode(char) { */ export function shortcodeToUnicode(shortcode) { shortcode = shortcode.slice(1, shortcode.length - 1); - const data = EMOJIBASE.find(e => e.shortcodes && e.shortcodes.includes(shortcode)); + const data = SHORTCODE_TO_EMOJI.get(shortcode); return data ? data.unicode : null; } diff --git a/src/autocomplete/EmojiProvider.js b/src/autocomplete/EmojiProvider.js index 1e39593022..7e30c8ca6a 100644 --- a/src/autocomplete/EmojiProvider.js +++ b/src/autocomplete/EmojiProvider.js @@ -2,6 +2,7 @@ Copyright 2016 Aviral Dasgupta Copyright 2017 Vector Creations Ltd Copyright 2017, 2018 New Vector Ltd +Copyright 2019 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -28,7 +29,7 @@ import SettingsStore from "../settings/SettingsStore"; import { shortcodeToUnicode } from '../HtmlUtils'; import EMOTICON_REGEX from 'emojibase-regex/emoticon'; -import EmojiData from '../stripped-emoji.json'; +import EMOJIBASE from 'emojibase-data/en/compact.json'; const LIMIT = 20; @@ -38,19 +39,15 @@ const EMOJI_REGEX = new RegExp('(' + EMOTICON_REGEX.source + '|:[+-\\w]*:?)$', ' // XXX: it's very unclear why we bother with this generated emojidata file. // all it means is that we end up bloating the bundle with precomputed stuff // which would be trivial to calculate and cache on demand. -const EMOJI_SHORTNAMES = Object.keys(EmojiData).map((key) => EmojiData[key]).sort( - (a, b) => { - if (a.category === b.category) { - return a.emoji_order - b.emoji_order; - } - return a.category - b.category; - }, -).map((a, index) => { +const EMOJI_SHORTNAMES = EMOJIBASE.sort((a, b) => { + if (a.group === b.group) { + return a.order - b.order; + } + return a.group - b.group; +}).map((emoji, index) => { return { - name: a.name, - shortname: a.shortname, - aliases: a.aliases ? a.aliases.join(' ') : '', - aliases_ascii: a.aliases_ascii ? a.aliases_ascii.join(' ') : '', + emoji, + shortname: `:${emoji.shortcodes[0]}:`, // Include the index so that we can preserve the original order _orderBy: index, }; @@ -69,12 +66,16 @@ export default class EmojiProvider extends AutocompleteProvider { constructor() { super(EMOJI_REGEX); this.matcher = new QueryMatcher(EMOJI_SHORTNAMES, { - keys: ['aliases_ascii', 'shortname', 'aliases'], + keys: ['emoji.emoticon', 'shortname'], + funcs: [ + // (o) => `:${o.emoji.shortcodes[0]}:`, // shortname + (o) => o.emoji.shortcodes.length > 1 ? o.emoji.shortcodes.slice(1).map(s => `:${s}:`).join(" ") : "", // aliases + ], // For matching against ascii equivalents shouldMatchWordsOnly: false, }); this.nameMatcher = new QueryMatcher(EMOJI_SHORTNAMES, { - keys: ['name'], + keys: ['emoji.annotation'], // For removing punctuation shouldMatchWordsOnly: true, }); @@ -96,7 +97,7 @@ export default class EmojiProvider extends AutocompleteProvider { const sorters = []; // make sure that emoticons come first - sorters.push((c) => score(matchedString, c.aliases_ascii)); + sorters.push((c) => score(matchedString, c.emoji.emoticon || "")); // then sort by score (Infinity if matchedString not in shortname) sorters.push((c) => score(matchedString, c.shortname)); @@ -110,8 +111,7 @@ export default class EmojiProvider extends AutocompleteProvider { sorters.push((c) => c._orderBy); completions = _sortBy(_uniq(completions), sorters); - completions = completions.map((result) => { - const { shortname } = result; + completions = completions.map(({shortname}) => { const unicode = shortcodeToUnicode(shortname); return { completion: unicode, diff --git a/src/autocomplete/QueryMatcher.js b/src/autocomplete/QueryMatcher.js index a28d3003cf..ef1605e7a6 100644 --- a/src/autocomplete/QueryMatcher.js +++ b/src/autocomplete/QueryMatcher.js @@ -71,6 +71,7 @@ export default class QueryMatcher { } for (const keyValue of keyValues) { + if (!keyValue) continue; // skip falsy keyValues const key = stripDiacritics(keyValue).toLowerCase(); if (!this._items.has(key)) { this._items.set(key, []); diff --git a/src/components/views/emojipicker/EmojiPicker.js b/src/components/views/emojipicker/EmojiPicker.js index 0ec11c2b38..4d49b25100 100644 --- a/src/components/views/emojipicker/EmojiPicker.js +++ b/src/components/views/emojipicker/EmojiPicker.js @@ -16,54 +16,12 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; -import EMOJIBASE from 'emojibase-data/en/compact.json'; import sdk from '../../../index'; import { _t } from '../../../languageHandler'; import * as recent from './recent'; - -const EMOJIBASE_CATEGORY_IDS = [ - "people", // smileys - "people", // actually people - "control", // modifiers and such, not displayed in picker - "nature", - "foods", - "places", - "activity", - "objects", - "symbols", - "flags", -]; - -const DATA_BY_CATEGORY = { - "people": [], - "nature": [], - "foods": [], - "places": [], - "activity": [], - "objects": [], - "symbols": [], - "flags": [], -}; -const DATA_BY_EMOJI = {}; - -const VARIATION_SELECTOR = String.fromCharCode(0xFE0F); -EMOJIBASE.forEach(emoji => { - if (emoji.unicode.includes(VARIATION_SELECTOR)) { - // Clone data into variation-less version - emoji = Object.assign({}, emoji, { - unicode: emoji.unicode.replace(VARIATION_SELECTOR, ""), - }); - } - DATA_BY_EMOJI[emoji.unicode] = emoji; - const categoryId = EMOJIBASE_CATEGORY_IDS[emoji.group]; - if (DATA_BY_CATEGORY.hasOwnProperty(categoryId)) { - DATA_BY_CATEGORY[categoryId].push(emoji); - } - // This is used as the string to match the query against when filtering emojis. - emoji.filterString = `${emoji.annotation}\n${emoji.shortcodes.join('\n')}}\n${emoji.emoticon || ''}`.toLowerCase(); -}); +import {DATA_BY_CATEGORY, getEmojiFromUnicode} from "../../../emoji"; export const CATEGORY_HEADER_HEIGHT = 22; export const EMOJI_HEIGHT = 37; @@ -91,7 +49,7 @@ class EmojiPicker extends React.Component { // Convert recent emoji characters to emoji data, removing unknowns. this.recentlyUsed = recent.get() - .map(unicode => DATA_BY_EMOJI[unicode]) + .map(unicode => getEmojiFromUnicode(unicode)) .filter(data => !!data); this.memoizedDataByCategory = { recent: this.recentlyUsed, diff --git a/src/components/views/emojipicker/QuickReactions.js b/src/components/views/emojipicker/QuickReactions.js index 8444fb2d9c..e4419e9f3a 100644 --- a/src/components/views/emojipicker/QuickReactions.js +++ b/src/components/views/emojipicker/QuickReactions.js @@ -19,15 +19,15 @@ import PropTypes from 'prop-types'; import sdk from '../../../index'; import { _t } from '../../../languageHandler'; -import { findEmojiData } from '../../../HtmlUtils'; +import {getEmojiFromUnicode} from "../../../emoji"; +// We use the variation-selector Heart in Quick Reactions for some reason const QUICK_REACTIONS = ["👍", "👎", "😄", "🎉", "😕", "❤️", "🚀", "👀"].map(emoji => { - const data = findEmojiData(emoji); + const data = getEmojiFromUnicode(emoji); if (!data) { throw new Error(`Emoji ${emoji} doesn't exist in emojibase`); } - // Prefer our unicode value for quick reactions (which does not have - // variation selectors). + // Prefer our unicode value for quick reactions as we sometimes use variation selectors. return Object.assign({}, data, { unicode: emoji }); }); diff --git a/src/components/views/rooms/BasicMessageComposer.js b/src/components/views/rooms/BasicMessageComposer.js index c7659e89fb..b550644470 100644 --- a/src/components/views/rooms/BasicMessageComposer.js +++ b/src/components/views/rooms/BasicMessageComposer.js @@ -34,11 +34,11 @@ import {parsePlainTextMessage} from '../../../editor/deserialize'; import {renderModel} from '../../../editor/render'; import {Room} from 'matrix-js-sdk'; import TypingStore from "../../../stores/TypingStore"; -import EMOJIBASE from 'emojibase-data/en/compact.json'; import SettingsStore from "../../../settings/SettingsStore"; import EMOTICON_REGEX from 'emojibase-regex/emoticon'; import sdk from '../../../index'; import {Key} from "../../../Keyboard"; +import {EMOTICON_TO_EMOJI} from "../../../emoji"; const REGEX_EMOTICON_WHITESPACE = new RegExp('(?:^|\\s)(' + EMOTICON_REGEX.source + ')\\s$'); @@ -108,7 +108,8 @@ export default class BasicMessageEditor extends React.Component { const emoticonMatch = REGEX_EMOTICON_WHITESPACE.exec(range.text); if (emoticonMatch) { const query = emoticonMatch[1].toLowerCase().replace("-", ""); - const data = EMOJIBASE.find(e => e.emoticon ? e.emoticon.toLowerCase() === query : false); + const data = EMOTICON_TO_EMOJI.get(query); + if (data) { const {partCreator} = model; const hasPrecedingSpace = emoticonMatch[0][0] === " "; diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index a5d8492d99..dc8e8e439f 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -48,7 +48,6 @@ import Markdown from '../../../Markdown'; import MessageComposerStore from '../../../stores/MessageComposerStore'; import ContentMessages from '../../../ContentMessages'; -import EMOJIBASE from 'emojibase-data/en/compact.json'; import EMOTICON_REGEX from 'emojibase-regex/emoticon'; import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore"; @@ -61,6 +60,7 @@ import AccessibleButton from '../elements/AccessibleButton'; import {findEditableEvent} from '../../../utils/EventUtils'; import SlateComposerHistoryManager from "../../../SlateComposerHistoryManager"; import TypingStore from "../../../stores/TypingStore"; +import {EMOTICON_TO_EMOJI} from "../../../emoji"; const REGEX_EMOTICON_WHITESPACE = new RegExp('(?:^|\\s)(' + EMOTICON_REGEX.source + ')\\s$'); @@ -464,7 +464,7 @@ export default class MessageComposerInput extends React.Component { const emoticonMatch = REGEX_EMOTICON_WHITESPACE.exec(text.slice(0, currentStartOffset)); if (emoticonMatch) { const query = emoticonMatch[1].toLowerCase().replace("-", ""); - const data = EMOJIBASE.find(e => e.emoticon ? e.emoticon.toLowerCase() === query : false); + const data = EMOTICON_TO_EMOJI.get(query); // only perform replacement if we found a match, otherwise we would be not letting user type if (data) { diff --git a/src/emoji.js b/src/emoji.js new file mode 100644 index 0000000000..7b7a9c1bfe --- /dev/null +++ b/src/emoji.js @@ -0,0 +1,82 @@ +/* +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 EMOJIBASE from 'emojibase-data/en/compact.json'; + +export const VARIATION_SELECTOR = String.fromCharCode(0xFE0F); + +// The unicode is stored without the variant selector +const UNICODE_TO_EMOJI = new Map(); // not exported as gets for it are handled by getEmojiFromUnicode +export const EMOTICON_TO_EMOJI = new Map(); +export const SHORTCODE_TO_EMOJI = new Map(); + +export const getEmojiFromUnicode = unicode => UNICODE_TO_EMOJI.get(unicode.replace(VARIATION_SELECTOR, "")); + +const EMOJIBASE_GROUP_ID_TO_CATEGORY = [ + "people", // smileys + "people", // actually people + "control", // modifiers and such, not displayed in picker + "nature", + "foods", + "places", + "activity", + "objects", + "symbols", + "flags", +]; + +export const DATA_BY_CATEGORY = { + "people": [], + "nature": [], + "foods": [], + "places": [], + "activity": [], + "objects": [], + "symbols": [], + "flags": [], +}; + +// Store various mappings from unicode/emoticon/shortcode to the Emoji objects +EMOJIBASE.forEach(emoji => { + if (emoji.unicode.includes(VARIATION_SELECTOR)) { + // Clone data into variation-less version + emoji = Object.assign({}, emoji, { + unicode: emoji.unicode.replace(VARIATION_SELECTOR, ""), + }); + } + + const categoryId = EMOJIBASE_GROUP_ID_TO_CATEGORY[emoji.group]; + if (DATA_BY_CATEGORY.hasOwnProperty(categoryId)) { + DATA_BY_CATEGORY[categoryId].push(emoji); + } + // This is used as the string to match the query against when filtering emojis + emoji.filterString = `${emoji.annotation}\n${emoji.shortcodes.join('\n')}}\n${emoji.emoticon || ''}`.toLowerCase(); + + // Add mapping from unicode to Emoji object + UNICODE_TO_EMOJI.set(emoji.unicode, emoji); + + if (emoji.emoticon) { + // Add mapping from emoticon to Emoji object + EMOTICON_TO_EMOJI.set(emoji.emoticon, emoji); + } + + if (emoji.shortcodes) { + // Add mapping from each shortcode to Emoji object + emoji.shortcodes.forEach(shortcode => { + SHORTCODE_TO_EMOJI.set(shortcode, emoji); + }); + } +}); diff --git a/src/stripped-emoji.json b/src/stripped-emoji.json deleted file mode 100644 index 9d4308d24e..0000000000 --- a/src/stripped-emoji.json +++ /dev/null @@ -1 +0,0 @@ -[{"name":"grinning face","shortname":":gleeful:","category":0,"emoji_order":1},{"name":"grinning face with big eyes","shortname":":glad:","category":0,"emoji_order":2,"aliases":[":smile:"]},{"name":"grinning face with smiling eyes","shortname":":happy:","category":0,"emoji_order":3},{"name":"beaming face with smiling eyes","shortname":":blissful:","category":0,"emoji_order":4,"aliases":[":grin:"],"aliases_ascii":[":D"]},{"name":"grinning squinting face","shortname":":amused:","category":0,"emoji_order":5,"aliases":[":laugh:",":lol:"],"aliases_ascii":["xD"]},{"name":"grinning face with sweat","shortname":":embarassed:","category":0,"emoji_order":6},{"name":"rolling on the floor laughing","shortname":":entertained:","category":0,"emoji_order":7,"aliases":[":rofl:"],"aliases_ascii":[":'D"]},{"name":"face with tears of joy","shortname":":joyful:","category":0,"emoji_order":8,"aliases":[":haha:"],"aliases_ascii":[":')"]},{"name":"slightly smiling face","shortname":":pleased:","category":0,"emoji_order":9,"aliases_ascii":[":)"]},{"name":"upside-down face","shortname":":ecstatic:","category":0,"emoji_order":10,"aliases":[":upside_down:"]},{"name":"winking face","shortname":":coy:","category":0,"emoji_order":11,"aliases":[":wink:"],"aliases_ascii":[";)"]},{"name":"smiling face with smiling eyes","shortname":":blush:","category":0,"emoji_order":12,"aliases_ascii":[":>"]},{"name":"smiling face with halo","shortname":":innocent:","category":0,"emoji_order":13,"aliases":[":halo:"],"aliases_ascii":["o:)"]},{"name":"smiling face with hearts","shortname":":love:","category":0,"emoji_order":14},{"name":"smiling face with heart-eyes","shortname":":lovestruck:","category":0,"emoji_order":15},{"name":"star-struck","shortname":":starstruck:","category":0,"emoji_order":16},{"name":"face blowing a kiss","shortname":":flirty:","category":0,"emoji_order":17,"aliases_ascii":[":x"]},{"name":"kissing face","shortname":":kiss:","category":0,"emoji_order":18},{"name":"smiling face","shortname":":relaxed:","category":0,"emoji_order":20},{"name":"kissing face with closed eyes","shortname":":loving_kiss:","category":0,"emoji_order":21,"aliases_ascii":[":*"]},{"name":"kissing face with smiling eyes","shortname":":happy_kiss:","category":0,"emoji_order":22},{"name":"face savoring food","shortname":":yum:","category":0,"emoji_order":23,"aliases":[":savour:"]},{"name":"face with tongue","shortname":":playful:","category":0,"emoji_order":24,"aliases":[":tongue_out:"],"aliases_ascii":[":p"]},{"name":"winking face with tongue","shortname":":mischievous:","category":0,"emoji_order":25,"aliases_ascii":[";p"]},{"name":"zany face","shortname":":crazy:","category":0,"emoji_order":26},{"name":"squinting face with tongue","shortname":":facetious:","category":0,"emoji_order":27,"aliases":[":lmao:"],"aliases_ascii":["xp"]},{"name":"money-mouth face","shortname":":pretentious:","category":0,"emoji_order":28,"aliases":[":money_mouth:"]},{"name":"hugging face","shortname":":hugging:","category":0,"emoji_order":29},{"name":"face with hand over mouth","shortname":":gasp:","category":0,"emoji_order":30},{"name":"shushing face","shortname":":shushing:","category":0,"emoji_order":31},{"name":"thinking face","shortname":":curious:","category":0,"emoji_order":32,"aliases":[":thinking:"],"aliases_ascii":[":l"]},{"name":"zipper-mouth face","shortname":":silenced:","category":0,"emoji_order":33,"aliases":[":zipper_mouth:"],"aliases_ascii":[":z"]},{"name":"face with raised eyebrow","shortname":":contempt:","category":0,"emoji_order":34},{"name":"neutral face","shortname":":indifferent:","category":0,"emoji_order":35,"aliases":[":neutral:"],"aliases_ascii":[":|"]},{"name":"expressionless face","shortname":":apathetic:","category":0,"emoji_order":36,"aliases":[":expressionless:"]},{"name":"face without mouth","shortname":":vacant:","category":0,"emoji_order":37,"aliases":[":no_mouth:"],"aliases_ascii":[":#"]},{"name":"smirking face","shortname":":cocky:","category":0,"emoji_order":38,"aliases":[":smirk:"],"aliases_ascii":[":j"]},{"name":"unamused face","shortname":":unamused:","category":0,"emoji_order":39,"aliases_ascii":[":?"]},{"name":"face with rolling eyes","shortname":":disbelief:","category":0,"emoji_order":40},{"name":"grimacing face","shortname":":grimaced:","category":0,"emoji_order":41,"aliases_ascii":["8D"]},{"name":"lying face","shortname":":lying:","category":0,"emoji_order":42},{"name":"relieved face","shortname":":relieved:","category":0,"emoji_order":43},{"name":"pensive face","shortname":":pensive:","category":0,"emoji_order":44},{"name":"sleepy face","shortname":":sleepy:","category":0,"emoji_order":45},{"name":"drooling face","shortname":":drooling:","category":0,"emoji_order":46},{"name":"sleeping face","shortname":":exhausted:","category":0,"emoji_order":47,"aliases":[":sleeping:"]},{"name":"face with medical mask","shortname":":ill:","category":0,"emoji_order":48,"aliases":[":mask:"]},{"name":"face with thermometer","shortname":":sick:","category":0,"emoji_order":49},{"name":"face with head-bandage","shortname":":injured:","category":0,"emoji_order":50},{"name":"nauseated face","shortname":":nauseated:","category":0,"emoji_order":51,"aliases_ascii":["%("]},{"name":"face vomiting","shortname":":vomiting:","category":0,"emoji_order":52},{"name":"sneezing face","shortname":":sneezing:","category":0,"emoji_order":53},{"name":"hot face","shortname":":overheating:","category":0,"emoji_order":54},{"name":"cold face","shortname":":freezing:","category":0,"emoji_order":55},{"name":"woozy face","shortname":":woozy:","category":0,"emoji_order":56,"aliases_ascii":[":&"]},{"name":"dizzy face","shortname":":dizzy:","category":0,"emoji_order":57,"aliases_ascii":["xo"]},{"name":"exploding head","shortname":":shocked:","category":0,"emoji_order":58,"aliases":[":exploding_head:"]},{"name":"cowboy hat face","shortname":":cowboy:","category":0,"emoji_order":59},{"name":"partying face","shortname":":partying:","category":0,"emoji_order":60,"aliases":[":celebrating:"]},{"name":"smiling face with sunglasses","shortname":":confident:","category":0,"emoji_order":61,"aliases_ascii":["8)"]},{"name":"nerd face","shortname":":nerd:","category":0,"emoji_order":62,"aliases_ascii":[":B"]},{"name":"face with monocle","shortname":":monocle:","category":0,"emoji_order":63},{"name":"confused face","shortname":":confused:","category":0,"emoji_order":64,"aliases_ascii":[":/"]},{"name":"worried face","shortname":":worried:","category":0,"emoji_order":65},{"name":"slightly frowning face","shortname":":cheerless:","category":0,"emoji_order":66},{"name":"frowning face","shortname":":sad:","category":0,"emoji_order":68,"aliases":[":frowning:"],"aliases_ascii":[":("]},{"name":"face with open mouth","shortname":":surprised:","category":0,"emoji_order":69},{"name":"hushed face","shortname":":hushed:","category":0,"emoji_order":70},{"name":"astonished face","shortname":":astonished:","category":0,"emoji_order":71,"aliases_ascii":[":o"]},{"name":"flushed face","shortname":":flushed:","category":0,"emoji_order":72,"aliases_ascii":[":$"]},{"name":"pleading face","shortname":":pleading:","category":0,"emoji_order":73},{"name":"frowning face with open mouth","shortname":":bored:","category":0,"emoji_order":74},{"name":"anguished face","shortname":":anguished:","category":0,"emoji_order":75,"aliases":[":wtf:"],"aliases_ascii":[":s"]},{"name":"fearful face","shortname":":fearful:","category":0,"emoji_order":76},{"name":"anxious face with sweat","shortname":":cold_sweat:","category":0,"emoji_order":77,"aliases":[":anxious:",":frustrated:"]},{"name":"sad but relieved face","shortname":":hopeful:","category":0,"emoji_order":78},{"name":"crying face","shortname":":upset:","category":0,"emoji_order":79,"aliases":[":cry:"],"aliases_ascii":[":'("]},{"name":"loudly crying face","shortname":":distressed:","category":0,"emoji_order":80,"aliases":[":sob:"],"aliases_ascii":[":'o"]},{"name":"face screaming in fear","shortname":":frightened:","category":0,"emoji_order":81,"aliases":[":scream:"],"aliases_ascii":["Dx"]},{"name":"confounded face","shortname":":confounded:","category":0,"emoji_order":82,"aliases_ascii":["x("]},{"name":"persevering face","shortname":":persevered:","category":0,"emoji_order":83},{"name":"disappointed face","shortname":":disappointed:","category":0,"emoji_order":84},{"name":"downcast face with sweat","shortname":":shamed:","category":0,"emoji_order":85,"aliases_ascii":[":<"]},{"name":"weary face","shortname":":weary:","category":0,"emoji_order":86,"aliases_ascii":["D:"]},{"name":"tired face","shortname":":tired:","category":0,"emoji_order":87,"aliases_ascii":[":c"]},{"name":"yawning face","shortname":":yawn:","category":0,"emoji_order":88},{"name":"face with steam from nose","shortname":":annoyed:","category":0,"emoji_order":89,"aliases":[":hrmph:"]},{"name":"pouting face","shortname":":enraged:","category":0,"emoji_order":90,"aliases":[":pout:"],"aliases_ascii":[">:/"]},{"name":"angry face","shortname":":angry:","category":0,"emoji_order":91},{"name":"face with symbols on mouth","shortname":":censored:","category":0,"emoji_order":92,"aliases_ascii":[":@"]},{"name":"smiling face with horns","shortname":":imp:","category":0,"emoji_order":93,"aliases_ascii":[">:)"]},{"name":"angry face with horns","shortname":":angry_imp:","category":0,"emoji_order":94,"aliases_ascii":[">:("]},{"name":"skull","shortname":":skull:","category":0,"emoji_order":95},{"name":"skull and crossbones","shortname":":crossbones:","category":0,"emoji_order":97},{"name":"pile of poo","shortname":":poop:","category":0,"emoji_order":98},{"name":"clown face","shortname":":clown:","category":0,"emoji_order":99},{"name":"ogre","shortname":":ogre:","category":0,"emoji_order":100,"aliases_ascii":[">0)"]},{"name":"goblin","shortname":":goblin:","category":0,"emoji_order":101},{"name":"ghost","shortname":":ghost:","category":0,"emoji_order":102},{"name":"alien","shortname":":alien:","category":0,"emoji_order":103},{"name":"alien monster","shortname":":alien_monster:","category":0,"emoji_order":104,"aliases":[":space_invader:"]},{"name":"robot","shortname":":robot:","category":0,"emoji_order":105},{"name":"grinning cat","shortname":":smiling_cat:","category":0,"emoji_order":106},{"name":"grinning cat with smiling eyes","shortname":":grinning_cat:","category":0,"emoji_order":107},{"name":"cat with tears of joy","shortname":":joyful_cat:","category":0,"emoji_order":108},{"name":"smiling cat with heart-eyes","shortname":":lovestruck_cat:","category":0,"emoji_order":109},{"name":"cat with wry smile","shortname":":smirking_cat:","category":0,"emoji_order":110},{"name":"kissing cat","shortname":":kissing_cat:","category":0,"emoji_order":111,"aliases_ascii":[":3"]},{"name":"weary cat","shortname":":weary_cat:","category":0,"emoji_order":112},{"name":"crying cat","shortname":":crying_cat:","category":0,"emoji_order":113},{"name":"pouting cat","shortname":":pouting_cat:","category":0,"emoji_order":114},{"name":"see-no-evil monkey","shortname":":see_no_evil:","category":0,"emoji_order":115},{"name":"hear-no-evil monkey","shortname":":hear_no_evil:","category":0,"emoji_order":116},{"name":"speak-no-evil monkey","shortname":":speak_no_evil:","category":0,"emoji_order":117},{"name":"kiss mark","shortname":":kiss_lips:","category":0,"emoji_order":118},{"name":"love letter","shortname":":love_letter:","category":0,"emoji_order":119},{"name":"heart with arrow","shortname":":cupid:","category":0,"emoji_order":120},{"name":"heart with ribbon","shortname":":heart_ribbon:","category":0,"emoji_order":121},{"name":"sparkling heart","shortname":":sparkling_heart:","category":0,"emoji_order":122},{"name":"growing heart","shortname":":heartpulse:","category":0,"emoji_order":123},{"name":"beating heart","shortname":":heartbeat:","category":0,"emoji_order":124},{"name":"revolving hearts","shortname":":revolving_hearts:","category":0,"emoji_order":125},{"name":"two hearts","shortname":":two_hearts:","category":0,"emoji_order":126},{"name":"heart decoration","shortname":":heart_decoration:","category":0,"emoji_order":127},{"name":"heart exclamation","shortname":":heart_exclamation:","category":0,"emoji_order":129},{"name":"broken heart","shortname":":broken_heart:","category":0,"emoji_order":130,"aliases_ascii":[""]},{"name":"woman mage","shortname":":woman_mage:","category":1,"emoji_order":1375},{"name":"fairy","shortname":":fairy:","category":1,"emoji_order":1387},{"name":"man fairy","shortname":":man_fairy:","category":1,"emoji_order":1393},{"name":"woman fairy","shortname":":woman_fairy:","category":1,"emoji_order":1405},{"name":"vampire","shortname":":vampire:","category":1,"emoji_order":1417,"aliases_ascii":[":E"]},{"name":"man vampire","shortname":":man_vampire:","category":1,"emoji_order":1423},{"name":"woman vampire","shortname":":woman_vampire:","category":1,"emoji_order":1435},{"name":"merperson","shortname":":merperson:","category":1,"emoji_order":1447},{"name":"merman","shortname":":merman:","category":1,"emoji_order":1453},{"name":"mermaid","shortname":":mermaid:","category":1,"emoji_order":1465},{"name":"elf","shortname":":elf:","category":1,"emoji_order":1477},{"name":"man elf","shortname":":man_elf:","category":1,"emoji_order":1483},{"name":"woman elf","shortname":":woman_elf:","category":1,"emoji_order":1495},{"name":"genie","shortname":":genie:","category":1,"emoji_order":1507},{"name":"man genie","shortname":":man_genie:","category":1,"emoji_order":1508},{"name":"woman genie","shortname":":woman_genie:","category":1,"emoji_order":1510},{"name":"zombie","shortname":":zombie:","category":1,"emoji_order":1512,"aliases_ascii":["8#"]},{"name":"man zombie","shortname":":man_zombie:","category":1,"emoji_order":1513},{"name":"woman zombie","shortname":":woman_zombie:","category":1,"emoji_order":1515},{"name":"person getting massage","shortname":":person_getting_massage:","category":1,"emoji_order":1517},{"name":"man getting massage","shortname":":man_getting_face_massage:","category":1,"emoji_order":1523},{"name":"woman getting massage","shortname":":woman_getting_face_massage:","category":1,"emoji_order":1535},{"name":"person getting haircut","shortname":":person_getting_haircut:","category":1,"emoji_order":1547},{"name":"man getting haircut","shortname":":man_getting_haircut:","category":1,"emoji_order":1553},{"name":"woman getting haircut","shortname":":woman_getting_haircut:","category":1,"emoji_order":1565},{"name":"person walking","shortname":":person_walking:","category":1,"emoji_order":1577},{"name":"man walking","shortname":":man_walking:","category":1,"emoji_order":1583},{"name":"woman walking","shortname":":woman_walking:","category":1,"emoji_order":1595},{"name":"person standing","shortname":":person_standing:","category":1,"emoji_order":1607},{"name":"man standing","shortname":":man_standing:","category":1,"emoji_order":1613},{"name":"woman standing","shortname":":woman_standing:","category":1,"emoji_order":1625},{"name":"person kneeling","shortname":":person_kneeling:","category":1,"emoji_order":1637},{"name":"man kneeling","shortname":":man_kneeling:","category":1,"emoji_order":1643},{"name":"woman kneeling","shortname":":woman_kneeling:","category":1,"emoji_order":1655},{"name":"man with probing cane","shortname":":man_probing_cane:","category":1,"emoji_order":1667},{"name":"woman with probing cane","shortname":":woman_probing_cane:","category":1,"emoji_order":1673},{"name":"man in motorized wheelchair","shortname":":man_motor_wheelchair:","category":1,"emoji_order":1679},{"name":"woman in motorized wheelchair","shortname":":woman_motor_wheelchair:","category":1,"emoji_order":1685},{"name":"man in manual wheelchair","shortname":":man_wheelchair:","category":1,"emoji_order":1691},{"name":"woman in manual wheelchair","shortname":":woman_wheelchair:","category":1,"emoji_order":1697},{"name":"person running","shortname":":person_running:","category":1,"emoji_order":1703},{"name":"man running","shortname":":man_running:","category":1,"emoji_order":1709},{"name":"woman running","shortname":":woman_running:","category":1,"emoji_order":1721},{"name":"woman dancing","shortname":":dancer:","category":1,"emoji_order":1733,"aliases":[":woman_dancing:"]},{"name":"man dancing","shortname":":man_dancing:","category":1,"emoji_order":1739},{"name":"man in suit levitating","shortname":":levitate:","category":1,"emoji_order":1746},{"name":"people with bunny ears","shortname":":people_bunny_ears_partying:","category":1,"emoji_order":1752},{"name":"men with bunny ears","shortname":":men_bunny_ears_partying:","category":1,"emoji_order":1753},{"name":"women with bunny ears","shortname":":women_bunny_ears_partying:","category":1,"emoji_order":1755},{"name":"person in steamy room","shortname":":person_steamy_room:","category":1,"emoji_order":1757},{"name":"man in steamy room","shortname":":man_steamy_room:","category":1,"emoji_order":1763},{"name":"woman in steamy room","shortname":":woman_steamy_room:","category":1,"emoji_order":1775},{"name":"person climbing","shortname":":person_climbing:","category":1,"emoji_order":1787},{"name":"man climbing","shortname":":man_climbing:","category":1,"emoji_order":1793},{"name":"woman climbing","shortname":":woman_climbing:","category":1,"emoji_order":1805},{"name":"person fencing","shortname":":person_fencing:","category":1,"emoji_order":1817},{"name":"horse racing","shortname":":horse_racing:","category":1,"emoji_order":1818},{"name":"skier","shortname":":skier:","category":1,"emoji_order":1825},{"name":"snowboarder","shortname":":snowboarder:","category":1,"emoji_order":1826},{"name":"person golfing","shortname":":person_golfing:","category":1,"emoji_order":1833},{"name":"man golfing","shortname":":man_golfing:","category":1,"emoji_order":1839},{"name":"woman golfing","shortname":":woman_golfing:","category":1,"emoji_order":1853},{"name":"person surfing","shortname":":person_surfing:","category":1,"emoji_order":1867},{"name":"man surfing","shortname":":man_surfing:","category":1,"emoji_order":1873},{"name":"woman surfing","shortname":":woman_surfing:","category":1,"emoji_order":1885},{"name":"person rowing boat","shortname":":person_rowing_boat:","category":1,"emoji_order":1897},{"name":"man rowing boat","shortname":":man_rowing_boat:","category":1,"emoji_order":1903},{"name":"woman rowing boat","shortname":":woman_rowing_boat:","category":1,"emoji_order":1915},{"name":"person swimming","shortname":":person_swimming:","category":1,"emoji_order":1927},{"name":"man swimming","shortname":":man_swimming:","category":1,"emoji_order":1933},{"name":"woman swimming","shortname":":woman_swimming:","category":1,"emoji_order":1945},{"name":"person bouncing ball","shortname":":person_bouncing_ball:","category":1,"emoji_order":1958},{"name":"man bouncing ball","shortname":":man_bouncing_ball:","category":1,"emoji_order":1964},{"name":"woman bouncing ball","shortname":":woman_bouncing_ball:","category":1,"emoji_order":1978},{"name":"person lifting weights","shortname":":person_lifting_weights:","category":1,"emoji_order":1993},{"name":"man lifting weights","shortname":":man_lifting_weights:","category":1,"emoji_order":1999},{"name":"woman lifting weights","shortname":":woman_lifting_weights:","category":1,"emoji_order":2013},{"name":"person biking","shortname":":person_biking:","category":1,"emoji_order":2027},{"name":"man biking","shortname":":man_biking:","category":1,"emoji_order":2033},{"name":"woman biking","shortname":":woman_biking:","category":1,"emoji_order":2045},{"name":"person mountain biking","shortname":":person_mountain_biking:","category":1,"emoji_order":2057},{"name":"man mountain biking","shortname":":man_mountain_biking:","category":1,"emoji_order":2063},{"name":"woman mountain biking","shortname":":woman_mountain_biking:","category":1,"emoji_order":2075},{"name":"person cartwheeling","shortname":":person_cartwheel:","category":1,"emoji_order":2087},{"name":"man cartwheeling","shortname":":man_cartwheeling:","category":1,"emoji_order":2093},{"name":"woman cartwheeling","shortname":":woman_cartwheeling:","category":1,"emoji_order":2105},{"name":"people wrestling","shortname":":people_wrestling:","category":1,"emoji_order":2117},{"name":"men wrestling","shortname":":men_wrestling:","category":1,"emoji_order":2118},{"name":"women wrestling","shortname":":women_wrestling:","category":1,"emoji_order":2120},{"name":"person playing water polo","shortname":":person_water_polo:","category":1,"emoji_order":2122},{"name":"man playing water polo","shortname":":man_water_polo:","category":1,"emoji_order":2128},{"name":"woman playing water polo","shortname":":woman_water_polo:","category":1,"emoji_order":2140},{"name":"person playing handball","shortname":":person_handball:","category":1,"emoji_order":2152},{"name":"man playing handball","shortname":":man_handball:","category":1,"emoji_order":2158},{"name":"woman playing handball","shortname":":woman_handball:","category":1,"emoji_order":2170},{"name":"person juggling","shortname":":person_juggling:","category":1,"emoji_order":2182},{"name":"man juggling","shortname":":man_juggling:","category":1,"emoji_order":2188},{"name":"woman juggling","shortname":":woman_juggling:","category":1,"emoji_order":2200},{"name":"person in lotus position","shortname":":person_lotus_position:","category":1,"emoji_order":2212},{"name":"man in lotus position","shortname":":man_lotus_position:","category":1,"emoji_order":2218},{"name":"woman in lotus position","shortname":":woman_lotus_position:","category":1,"emoji_order":2230},{"name":"person taking bath","shortname":":bath:","category":1,"emoji_order":2242},{"name":"person in bed","shortname":":in_bed:","category":1,"emoji_order":2248},{"name":"people holding hands","shortname":":holding_hands_people:","category":1,"emoji_order":2254},{"name":"women holding hands","shortname":":holding_hands_ww:","category":1,"emoji_order":2270},{"name":"woman and man holding hands","shortname":":holding_hands_mw:","category":1,"emoji_order":2286,"aliases":[":holding_hands_wm:"]},{"name":"men holding hands","shortname":":holding_hands_mm:","category":1,"emoji_order":2312},{"name":"kiss","shortname":":couple:","category":1,"emoji_order":2328},{"name":"kiss: woman, man","shortname":":kiss_mw:","category":1,"emoji_order":2329,"aliases":[":kiss_wm:"]},{"name":"kiss: man, man","shortname":":kiss_mm:","category":1,"emoji_order":2331},{"name":"kiss: woman, woman","shortname":":kiss_ww:","category":1,"emoji_order":2333},{"name":"couple with heart","shortname":":couple_heart:","category":1,"emoji_order":2335},{"name":"couple with heart: woman, man","shortname":":couple_mw:","category":1,"emoji_order":2336,"aliases":[":couple_wm:"]},{"name":"couple with heart: man, man","shortname":":couple_mm:","category":1,"emoji_order":2338},{"name":"couple with heart: woman, woman","shortname":":couple_ww:","category":1,"emoji_order":2340},{"name":"family","shortname":":family:","category":1,"emoji_order":2342},{"name":"family: man, woman, boy","shortname":":family_mwb:","category":1,"emoji_order":2343},{"name":"family: man, woman, girl","shortname":":family_mwg:","category":1,"emoji_order":2344},{"name":"family: man, woman, girl, boy","shortname":":family_mwgb:","category":1,"emoji_order":2345},{"name":"family: man, woman, boy, boy","shortname":":family_mwbb:","category":1,"emoji_order":2346},{"name":"family: man, woman, girl, girl","shortname":":family_mwgg:","category":1,"emoji_order":2347},{"name":"family: man, man, boy","shortname":":family_mmb:","category":1,"emoji_order":2348},{"name":"family: man, man, girl","shortname":":family_mmg:","category":1,"emoji_order":2349},{"name":"family: man, man, girl, boy","shortname":":family_mmgb:","category":1,"emoji_order":2350},{"name":"family: man, man, boy, boy","shortname":":family_mmbb:","category":1,"emoji_order":2351},{"name":"family: man, man, girl, girl","shortname":":family_mmgg:","category":1,"emoji_order":2352},{"name":"family: woman, woman, boy","shortname":":family_wwb:","category":1,"emoji_order":2353},{"name":"family: woman, woman, girl","shortname":":family_wwg:","category":1,"emoji_order":2354},{"name":"family: woman, woman, girl, boy","shortname":":family_wwgb:","category":1,"emoji_order":2355},{"name":"family: woman, woman, boy, boy","shortname":":family_wwbb:","category":1,"emoji_order":2356},{"name":"family: woman, woman, girl, girl","shortname":":family_wwgg:","category":1,"emoji_order":2357},{"name":"family: man, boy","shortname":":family_mb:","category":1,"emoji_order":2358},{"name":"family: man, boy, boy","shortname":":family_mbb:","category":1,"emoji_order":2359},{"name":"family: man, girl","shortname":":family_mg:","category":1,"emoji_order":2360},{"name":"family: man, girl, boy","shortname":":family_mgb:","category":1,"emoji_order":2361},{"name":"family: man, girl, girl","shortname":":family_mgg:","category":1,"emoji_order":2362},{"name":"family: woman, boy","shortname":":family_wb:","category":1,"emoji_order":2363},{"name":"family: woman, boy, boy","shortname":":family_wbb:","category":1,"emoji_order":2364},{"name":"family: woman, girl","shortname":":family_wg:","category":1,"emoji_order":2365},{"name":"family: woman, girl, boy","shortname":":family_wgb:","category":1,"emoji_order":2366},{"name":"family: woman, girl, girl","shortname":":family_wgg:","category":1,"emoji_order":2367},{"name":"speaking head","shortname":":speaking_head:","category":1,"emoji_order":2369},{"name":"bust in silhouette","shortname":":bust_silhouette:","category":1,"emoji_order":2370},{"name":"busts in silhouette","shortname":":busts_silhouette:","category":1,"emoji_order":2371},{"name":"footprints","shortname":":footprints:","category":1,"emoji_order":2372},{"name":"light skin tone","shortname":":tone_light:","category":2,"emoji_order":2373,"aliases":[":tone1:"]},{"name":"medium-light skin tone","shortname":":tone_medium_light:","category":2,"emoji_order":2374,"aliases":[":tone2:"]},{"name":"medium skin tone","shortname":":tone_medium:","category":2,"emoji_order":2375,"aliases":[":tone3:"]},{"name":"medium-dark skin tone","shortname":":tone_medium_dark:","category":2,"emoji_order":2376,"aliases":[":tone4:"]},{"name":"dark skin tone","shortname":":tone_dark:","category":2,"emoji_order":2377,"aliases":[":tone5:"]},{"name":"red hair","shortname":":red_hair:","category":2,"emoji_order":2378},{"name":"curly hair","shortname":":curly_hair:","category":2,"emoji_order":2379},{"name":"white hair","shortname":":white_hair:","category":2,"emoji_order":2380},{"name":"bald","shortname":":bald:","category":2,"emoji_order":2381},{"name":"monkey face","shortname":":monkey_face:","category":3,"emoji_order":2382},{"name":"monkey","shortname":":monkey:","category":3,"emoji_order":2383},{"name":"gorilla","shortname":":gorilla:","category":3,"emoji_order":2384},{"name":"orangutan","shortname":":orangutan:","category":3,"emoji_order":2385},{"name":"dog face","shortname":":dog_face:","category":3,"emoji_order":2386},{"name":"dog","shortname":":dog:","category":3,"emoji_order":2387},{"name":"guide dog","shortname":":guide_dog:","category":3,"emoji_order":2388},{"name":"service dog","shortname":":service_dog:","category":3,"emoji_order":2389},{"name":"poodle","shortname":":poodle:","category":3,"emoji_order":2390},{"name":"wolf","shortname":":wolf_face:","category":3,"emoji_order":2391},{"name":"fox","shortname":":fox_face:","category":3,"emoji_order":2392},{"name":"raccoon","shortname":":raccoon:","category":3,"emoji_order":2393},{"name":"cat face","shortname":":cat_face:","category":3,"emoji_order":2394},{"name":"cat","shortname":":cat:","category":3,"emoji_order":2395},{"name":"lion","shortname":":lion_face:","category":3,"emoji_order":2396},{"name":"tiger face","shortname":":tiger_face:","category":3,"emoji_order":2397},{"name":"tiger","shortname":":tiger:","category":3,"emoji_order":2398},{"name":"leopard","shortname":":leopard:","category":3,"emoji_order":2399},{"name":"horse face","shortname":":horse_face:","category":3,"emoji_order":2400},{"name":"horse","shortname":":horse:","category":3,"emoji_order":2401},{"name":"unicorn","shortname":":unicorn_face:","category":3,"emoji_order":2402},{"name":"zebra","shortname":":zebra:","category":3,"emoji_order":2403},{"name":"deer","shortname":":deer:","category":3,"emoji_order":2404},{"name":"cow face","shortname":":cow_face:","category":3,"emoji_order":2405},{"name":"ox","shortname":":ox:","category":3,"emoji_order":2406},{"name":"water buffalo","shortname":":water_buffalo:","category":3,"emoji_order":2407},{"name":"cow","shortname":":cow:","category":3,"emoji_order":2408},{"name":"pig face","shortname":":pig_face:","category":3,"emoji_order":2409},{"name":"pig","shortname":":pig:","category":3,"emoji_order":2410},{"name":"boar","shortname":":boar:","category":3,"emoji_order":2411},{"name":"pig nose","shortname":":pig_nose:","category":3,"emoji_order":2412},{"name":"ram","shortname":":ram:","category":3,"emoji_order":2413},{"name":"ewe","shortname":":sheep:","category":3,"emoji_order":2414},{"name":"goat","shortname":":goat:","category":3,"emoji_order":2415},{"name":"camel","shortname":":camel:","category":3,"emoji_order":2416},{"name":"two-hump camel","shortname":":two_hump_camel:","category":3,"emoji_order":2417},{"name":"llama","shortname":":llama:","category":3,"emoji_order":2418},{"name":"giraffe","shortname":":giraffe:","category":3,"emoji_order":2419},{"name":"elephant","shortname":":elephant:","category":3,"emoji_order":2420},{"name":"rhinoceros","shortname":":rhino:","category":3,"emoji_order":2421},{"name":"hippopotamus","shortname":":hippo:","category":3,"emoji_order":2422},{"name":"mouse face","shortname":":mouse_face:","category":3,"emoji_order":2423},{"name":"mouse","shortname":":mouse:","category":3,"emoji_order":2424},{"name":"rat","shortname":":rat:","category":3,"emoji_order":2425},{"name":"hamster","shortname":":hamster_face:","category":3,"emoji_order":2426},{"name":"rabbit face","shortname":":rabbit_face:","category":3,"emoji_order":2427},{"name":"rabbit","shortname":":rabbit:","category":3,"emoji_order":2428},{"name":"chipmunk","shortname":":chipmunk:","category":3,"emoji_order":2430},{"name":"hedgehog","shortname":":hedgehog:","category":3,"emoji_order":2431},{"name":"bat","shortname":":bat:","category":3,"emoji_order":2432},{"name":"bear","shortname":":bear_face:","category":3,"emoji_order":2433},{"name":"koala","shortname":":koala_face:","category":3,"emoji_order":2434},{"name":"panda","shortname":":panda_face:","category":3,"emoji_order":2435},{"name":"sloth","shortname":":sloth:","category":3,"emoji_order":2436},{"name":"otter","shortname":":otter:","category":3,"emoji_order":2437},{"name":"skunk","shortname":":skunk:","category":3,"emoji_order":2438},{"name":"kangaroo","shortname":":kangaroo:","category":3,"emoji_order":2439},{"name":"badger","shortname":":badger:","category":3,"emoji_order":2440},{"name":"paw prints","shortname":":feet:","category":3,"emoji_order":2441},{"name":"turkey","shortname":":turkey:","category":3,"emoji_order":2442},{"name":"chicken","shortname":":chicken:","category":3,"emoji_order":2443},{"name":"rooster","shortname":":rooster:","category":3,"emoji_order":2444},{"name":"hatching chick","shortname":":hatching_chick:","category":3,"emoji_order":2445},{"name":"baby chick","shortname":":baby_chick:","category":3,"emoji_order":2446},{"name":"front-facing baby chick","shortname":":hatched_chick:","category":3,"emoji_order":2447},{"name":"bird","shortname":":bird:","category":3,"emoji_order":2448},{"name":"penguin","shortname":":penguin:","category":3,"emoji_order":2449},{"name":"dove","shortname":":dove:","category":3,"emoji_order":2451},{"name":"eagle","shortname":":eagle:","category":3,"emoji_order":2452},{"name":"duck","shortname":":duck:","category":3,"emoji_order":2453},{"name":"swan","shortname":":swan:","category":3,"emoji_order":2454},{"name":"owl","shortname":":owl:","category":3,"emoji_order":2455},{"name":"flamingo","shortname":":flamingo:","category":3,"emoji_order":2456},{"name":"peacock","shortname":":peacock:","category":3,"emoji_order":2457},{"name":"parrot","shortname":":parrot:","category":3,"emoji_order":2458},{"name":"frog","shortname":":frog_face:","category":3,"emoji_order":2459},{"name":"crocodile","shortname":":crocodile:","category":3,"emoji_order":2460},{"name":"turtle","shortname":":turtle:","category":3,"emoji_order":2461},{"name":"lizard","shortname":":lizard:","category":3,"emoji_order":2462},{"name":"snake","shortname":":snake:","category":3,"emoji_order":2463},{"name":"dragon face","shortname":":dragon_face:","category":3,"emoji_order":2464},{"name":"dragon","shortname":":dragon:","category":3,"emoji_order":2465},{"name":"sauropod","shortname":":sauropod:","category":3,"emoji_order":2466},{"name":"T-Rex","shortname":":trex:","category":3,"emoji_order":2467},{"name":"spouting whale","shortname":":spouting_whale:","category":3,"emoji_order":2468},{"name":"whale","shortname":":whale:","category":3,"emoji_order":2469},{"name":"dolphin","shortname":":dolphin:","category":3,"emoji_order":2470},{"name":"fish","shortname":":fish:","category":3,"emoji_order":2471},{"name":"tropical fish","shortname":":tropical_fish:","category":3,"emoji_order":2472},{"name":"blowfish","shortname":":blowfish:","category":3,"emoji_order":2473},{"name":"shark","shortname":":shark:","category":3,"emoji_order":2474},{"name":"octopus","shortname":":octopus:","category":3,"emoji_order":2475},{"name":"spiral shell","shortname":":shell:","category":3,"emoji_order":2476},{"name":"snail","shortname":":snail:","category":3,"emoji_order":2477},{"name":"butterfly","shortname":":butterfly:","category":3,"emoji_order":2478},{"name":"bug","shortname":":bug:","category":3,"emoji_order":2479},{"name":"ant","shortname":":ant:","category":3,"emoji_order":2480},{"name":"honeybee","shortname":":bee:","category":3,"emoji_order":2481},{"name":"lady beetle","shortname":":beetle:","category":3,"emoji_order":2482},{"name":"cricket","shortname":":cricket:","category":3,"emoji_order":2483},{"name":"spider","shortname":":spider:","category":3,"emoji_order":2485},{"name":"spider web","shortname":":spider_web:","category":3,"emoji_order":2487},{"name":"scorpion","shortname":":scorpion:","category":3,"emoji_order":2488},{"name":"mosquito","shortname":":mosquito:","category":3,"emoji_order":2489},{"name":"microbe","shortname":":microbe:","category":3,"emoji_order":2490,"aliases":[":germ:"]},{"name":"bouquet","shortname":":bouquet:","category":3,"emoji_order":2491},{"name":"cherry blossom","shortname":":cherry_blossom:","category":3,"emoji_order":2492},{"name":"white flower","shortname":":white_flower:","category":3,"emoji_order":2493},{"name":"rosette","shortname":":rosette:","category":3,"emoji_order":2495},{"name":"rose","shortname":":rose:","category":3,"emoji_order":2496},{"name":"wilted flower","shortname":":wilted_rose:","category":3,"emoji_order":2497},{"name":"hibiscus","shortname":":hibiscus:","category":3,"emoji_order":2498},{"name":"sunflower","shortname":":sunflower:","category":3,"emoji_order":2499},{"name":"blossom","shortname":":blossom:","category":3,"emoji_order":2500},{"name":"tulip","shortname":":tulip:","category":3,"emoji_order":2501},{"name":"seedling","shortname":":seedling:","category":3,"emoji_order":2502},{"name":"evergreen tree","shortname":":evergreen_tree:","category":3,"emoji_order":2503},{"name":"deciduous tree","shortname":":deciduous_tree:","category":3,"emoji_order":2504},{"name":"palm tree","shortname":":palm_tree:","category":3,"emoji_order":2505},{"name":"cactus","shortname":":cactus:","category":3,"emoji_order":2506},{"name":"sheaf of rice","shortname":":ear_of_rice:","category":3,"emoji_order":2507},{"name":"herb","shortname":":herb:","category":3,"emoji_order":2508},{"name":"shamrock","shortname":":shamrock:","category":3,"emoji_order":2510},{"name":"four leaf clover","shortname":":four_leaf_clover:","category":3,"emoji_order":2511},{"name":"maple leaf","shortname":":maple_leaf:","category":3,"emoji_order":2512},{"name":"fallen leaf","shortname":":fallen_leaf:","category":3,"emoji_order":2513},{"name":"leaf fluttering in wind","shortname":":leaves:","category":3,"emoji_order":2514},{"name":"grapes","shortname":":grapes:","category":4,"emoji_order":2515},{"name":"melon","shortname":":melon:","category":4,"emoji_order":2516},{"name":"watermelon","shortname":":watermelon:","category":4,"emoji_order":2517},{"name":"tangerine","shortname":":tangerine:","category":4,"emoji_order":2518},{"name":"lemon","shortname":":lemon:","category":4,"emoji_order":2519},{"name":"banana","shortname":":banana:","category":4,"emoji_order":2520},{"name":"pineapple","shortname":":pineapple:","category":4,"emoji_order":2521},{"name":"mango","shortname":":mango:","category":4,"emoji_order":2522},{"name":"red apple","shortname":":apple:","category":4,"emoji_order":2523},{"name":"green apple","shortname":":green_apple:","category":4,"emoji_order":2524},{"name":"pear","shortname":":pear:","category":4,"emoji_order":2525},{"name":"peach","shortname":":peach:","category":4,"emoji_order":2526},{"name":"cherries","shortname":":cherries:","category":4,"emoji_order":2527},{"name":"strawberry","shortname":":strawberry:","category":4,"emoji_order":2528},{"name":"kiwi fruit","shortname":":kiwi:","category":4,"emoji_order":2529},{"name":"tomato","shortname":":tomato:","category":4,"emoji_order":2530},{"name":"coconut","shortname":":coconut:","category":4,"emoji_order":2531},{"name":"avocado","shortname":":avocado:","category":4,"emoji_order":2532},{"name":"eggplant","shortname":":eggplant:","category":4,"emoji_order":2533},{"name":"potato","shortname":":potato:","category":4,"emoji_order":2534},{"name":"carrot","shortname":":carrot:","category":4,"emoji_order":2535},{"name":"ear of corn","shortname":":corn:","category":4,"emoji_order":2536},{"name":"hot pepper","shortname":":hot_pepper:","category":4,"emoji_order":2538},{"name":"cucumber","shortname":":cucumber:","category":4,"emoji_order":2539},{"name":"leafy green","shortname":":leafy_green:","category":4,"emoji_order":2540},{"name":"broccoli","shortname":":broccoli:","category":4,"emoji_order":2541},{"name":"garlic","shortname":":garlic:","category":4,"emoji_order":2542},{"name":"onion","shortname":":onion:","category":4,"emoji_order":2543},{"name":"mushroom","shortname":":mushroom:","category":4,"emoji_order":2544},{"name":"peanuts","shortname":":peanuts:","category":4,"emoji_order":2545},{"name":"chestnut","shortname":":chestnut:","category":4,"emoji_order":2546},{"name":"bread","shortname":":bread:","category":4,"emoji_order":2547},{"name":"croissant","shortname":":croissant:","category":4,"emoji_order":2548},{"name":"baguette bread","shortname":":french_bread:","category":4,"emoji_order":2549},{"name":"pretzel","shortname":":pretzel:","category":4,"emoji_order":2550},{"name":"bagel","shortname":":bagel:","category":4,"emoji_order":2551},{"name":"pancakes","shortname":":pancakes:","category":4,"emoji_order":2552},{"name":"waffle","shortname":":waffle:","category":4,"emoji_order":2553},{"name":"cheese wedge","shortname":":cheese:","category":4,"emoji_order":2554},{"name":"meat on bone","shortname":":meat_on_bone:","category":4,"emoji_order":2555},{"name":"poultry leg","shortname":":poultry_leg:","category":4,"emoji_order":2556},{"name":"cut of meat","shortname":":cut_of_meat:","category":4,"emoji_order":2557},{"name":"bacon","shortname":":bacon:","category":4,"emoji_order":2558},{"name":"hamburger","shortname":":hamburger:","category":4,"emoji_order":2559},{"name":"french fries","shortname":":fries:","category":4,"emoji_order":2560},{"name":"pizza","shortname":":pizza:","category":4,"emoji_order":2561},{"name":"hot dog","shortname":":hotdog:","category":4,"emoji_order":2562},{"name":"sandwich","shortname":":sandwich:","category":4,"emoji_order":2563},{"name":"taco","shortname":":taco:","category":4,"emoji_order":2564},{"name":"burrito","shortname":":burrito:","category":4,"emoji_order":2565},{"name":"stuffed flatbread","shortname":":stuffed_flatbread:","category":4,"emoji_order":2566},{"name":"falafel","shortname":":falafel:","category":4,"emoji_order":2567},{"name":"egg","shortname":":egg:","category":4,"emoji_order":2568},{"name":"cooking","shortname":":cooking:","category":4,"emoji_order":2569},{"name":"shallow pan of food","shortname":":shallow_pan_of_food:","category":4,"emoji_order":2570},{"name":"pot of food","shortname":":stew:","category":4,"emoji_order":2571},{"name":"bowl with spoon","shortname":":bowl_spoon:","category":4,"emoji_order":2572},{"name":"green salad","shortname":":salad:","category":4,"emoji_order":2573},{"name":"popcorn","shortname":":popcorn:","category":4,"emoji_order":2574},{"name":"butter","shortname":":butter:","category":4,"emoji_order":2575},{"name":"salt","shortname":":salt:","category":4,"emoji_order":2576},{"name":"canned food","shortname":":canned_food:","category":4,"emoji_order":2577},{"name":"bento box","shortname":":bento:","category":4,"emoji_order":2578},{"name":"rice cracker","shortname":":rice_cracker:","category":4,"emoji_order":2579},{"name":"rice ball","shortname":":rice_ball:","category":4,"emoji_order":2580},{"name":"cooked rice","shortname":":rice:","category":4,"emoji_order":2581},{"name":"curry rice","shortname":":curry:","category":4,"emoji_order":2582},{"name":"steaming bowl","shortname":":ramen:","category":4,"emoji_order":2583},{"name":"spaghetti","shortname":":spaghetti:","category":4,"emoji_order":2584},{"name":"roasted sweet potato","shortname":":sweet_potato:","category":4,"emoji_order":2585},{"name":"oden","shortname":":oden:","category":4,"emoji_order":2586},{"name":"sushi","shortname":":sushi:","category":4,"emoji_order":2587},{"name":"fried shrimp","shortname":":fried_shrimp:","category":4,"emoji_order":2588},{"name":"fish cake with swirl","shortname":":fish_cake:","category":4,"emoji_order":2589},{"name":"moon cake","shortname":":moon_cake:","category":4,"emoji_order":2590},{"name":"dango","shortname":":dango:","category":4,"emoji_order":2591},{"name":"dumpling","shortname":":dumpling:","category":4,"emoji_order":2592},{"name":"fortune cookie","shortname":":fortune_cookie:","category":4,"emoji_order":2593},{"name":"takeout box","shortname":":takeout_box:","category":4,"emoji_order":2594},{"name":"crab","shortname":":crab:","category":4,"emoji_order":2595},{"name":"lobster","shortname":":lobster:","category":4,"emoji_order":2596},{"name":"shrimp","shortname":":shrimp:","category":4,"emoji_order":2597},{"name":"squid","shortname":":squid:","category":4,"emoji_order":2598},{"name":"oyster","shortname":":oyster:","category":4,"emoji_order":2599},{"name":"soft ice cream","shortname":":icecream:","category":4,"emoji_order":2600},{"name":"shaved ice","shortname":":shaved_ice:","category":4,"emoji_order":2601},{"name":"ice cream","shortname":":ice_cream:","category":4,"emoji_order":2602},{"name":"doughnut","shortname":":doughnut:","category":4,"emoji_order":2603},{"name":"cookie","shortname":":cookie:","category":4,"emoji_order":2604},{"name":"birthday cake","shortname":":birthday:","category":4,"emoji_order":2605},{"name":"shortcake","shortname":":cake:","category":4,"emoji_order":2606},{"name":"cupcake","shortname":":cupcake:","category":4,"emoji_order":2607},{"name":"pie","shortname":":pie:","category":4,"emoji_order":2608},{"name":"chocolate bar","shortname":":chocolate_bar:","category":4,"emoji_order":2609},{"name":"candy","shortname":":candy:","category":4,"emoji_order":2610},{"name":"lollipop","shortname":":lollipop:","category":4,"emoji_order":2611},{"name":"custard","shortname":":custard:","category":4,"emoji_order":2612},{"name":"honey pot","shortname":":honey_pot:","category":4,"emoji_order":2613},{"name":"baby bottle","shortname":":baby_bottle:","category":4,"emoji_order":2614},{"name":"glass of milk","shortname":":milk:","category":4,"emoji_order":2615},{"name":"hot beverage","shortname":":coffee:","category":4,"emoji_order":2616},{"name":"teacup without handle","shortname":":tea:","category":4,"emoji_order":2617},{"name":"sake","shortname":":sake:","category":4,"emoji_order":2618},{"name":"bottle with popping cork","shortname":":champagne:","category":4,"emoji_order":2619},{"name":"wine glass","shortname":":wine_glass:","category":4,"emoji_order":2620},{"name":"cocktail glass","shortname":":cocktail:","category":4,"emoji_order":2621},{"name":"tropical drink","shortname":":tropical_drink:","category":4,"emoji_order":2622},{"name":"beer mug","shortname":":beer:","category":4,"emoji_order":2623},{"name":"clinking beer mugs","shortname":":beers:","category":4,"emoji_order":2624},{"name":"clinking glasses","shortname":":champagne_glass:","category":4,"emoji_order":2625},{"name":"tumbler glass","shortname":":tumbler_glass:","category":4,"emoji_order":2626},{"name":"cup with straw","shortname":":cup_straw:","category":4,"emoji_order":2627},{"name":"beverage box","shortname":":beverage_box:","category":4,"emoji_order":2628,"aliases":[":juice_box:"]},{"name":"mate","shortname":":mate:","category":4,"emoji_order":2629,"aliases":[":yerba_mate:"]},{"name":"ice","shortname":":ice:","category":4,"emoji_order":2630},{"name":"chopsticks","shortname":":chopsticks:","category":4,"emoji_order":2631},{"name":"fork and knife with plate","shortname":":fork_knife_plate:","category":4,"emoji_order":2633},{"name":"fork and knife","shortname":":utensils:","category":4,"emoji_order":2634},{"name":"spoon","shortname":":spoon:","category":4,"emoji_order":2635},{"name":"kitchen knife","shortname":":knife:","category":4,"emoji_order":2636},{"name":"amphora","shortname":":amphora:","category":4,"emoji_order":2637},{"name":"globe showing Europe-Africa","shortname":":earth_africa:","category":5,"emoji_order":2638},{"name":"globe showing Americas","shortname":":earth_americas:","category":5,"emoji_order":2639},{"name":"globe showing Asia-Australia","shortname":":earth_asia:","category":5,"emoji_order":2640},{"name":"globe with meridians","shortname":":globe:","category":5,"emoji_order":2641},{"name":"world map","shortname":":map:","category":5,"emoji_order":2643},{"name":"map of Japan","shortname":":japan:","category":5,"emoji_order":2644},{"name":"compass","shortname":":compass:","category":5,"emoji_order":2645},{"name":"snow-capped mountain","shortname":":snowy_mountain:","category":5,"emoji_order":2647},{"name":"mountain","shortname":":mountain:","category":5,"emoji_order":2649},{"name":"volcano","shortname":":volcano:","category":5,"emoji_order":2650},{"name":"mount fuji","shortname":":mount_fuji:","category":5,"emoji_order":2651},{"name":"camping","shortname":":camping:","category":5,"emoji_order":2653},{"name":"beach with umbrella","shortname":":beach:","category":5,"emoji_order":2655},{"name":"desert","shortname":":desert:","category":5,"emoji_order":2657},{"name":"desert island","shortname":":island:","category":5,"emoji_order":2659},{"name":"national park","shortname":":park:","category":5,"emoji_order":2661},{"name":"stadium","shortname":":stadium:","category":5,"emoji_order":2663},{"name":"classical building","shortname":":classical_building:","category":5,"emoji_order":2665},{"name":"building construction","shortname":":construction_site:","category":5,"emoji_order":2667},{"name":"brick","shortname":":brick:","category":5,"emoji_order":2668},{"name":"houses","shortname":":homes:","category":5,"emoji_order":2670},{"name":"derelict house","shortname":":house_abandoned:","category":5,"emoji_order":2672},{"name":"house","shortname":":house:","category":5,"emoji_order":2673},{"name":"house with garden","shortname":":house_garden:","category":5,"emoji_order":2674},{"name":"office building","shortname":":office:","category":5,"emoji_order":2675},{"name":"Japanese post office","shortname":":ja_post_office:","category":5,"emoji_order":2676},{"name":"post office","shortname":":post_office:","category":5,"emoji_order":2677},{"name":"hospital","shortname":":hospital:","category":5,"emoji_order":2678},{"name":"bank","shortname":":bank:","category":5,"emoji_order":2679},{"name":"hotel","shortname":":hotel:","category":5,"emoji_order":2680},{"name":"love hotel","shortname":":love_hotel:","category":5,"emoji_order":2681},{"name":"convenience store","shortname":":convenience_store:","category":5,"emoji_order":2682},{"name":"school","shortname":":school:","category":5,"emoji_order":2683},{"name":"department store","shortname":":department_store:","category":5,"emoji_order":2684},{"name":"factory","shortname":":factory:","category":5,"emoji_order":2685},{"name":"Japanese castle","shortname":":japanese_castle:","category":5,"emoji_order":2686},{"name":"castle","shortname":":castle:","category":5,"emoji_order":2687,"aliases":[":european_castle:"]},{"name":"wedding","shortname":":wedding:","category":5,"emoji_order":2688},{"name":"Tokyo tower","shortname":":tokyo_tower:","category":5,"emoji_order":2689},{"name":"Statue of Liberty","shortname":":statue_of_liberty:","category":5,"emoji_order":2690},{"name":"church","shortname":":church:","category":5,"emoji_order":2691},{"name":"mosque","shortname":":mosque:","category":5,"emoji_order":2692},{"name":"hindu temple","shortname":":hindu_temple:","category":5,"emoji_order":2693},{"name":"synagogue","shortname":":synagogue:","category":5,"emoji_order":2694},{"name":"shinto shrine","shortname":":shinto_shrine:","category":5,"emoji_order":2696},{"name":"kaaba","shortname":":kaaba:","category":5,"emoji_order":2697},{"name":"fountain","shortname":":fountain:","category":5,"emoji_order":2698},{"name":"tent","shortname":":tent:","category":5,"emoji_order":2699},{"name":"foggy","shortname":":foggy:","category":5,"emoji_order":2700},{"name":"night with stars","shortname":":night_stars:","category":5,"emoji_order":2701},{"name":"cityscape","shortname":":cityscape:","category":5,"emoji_order":2703},{"name":"sunrise over mountains","shortname":":sunrise_over_mountains:","category":5,"emoji_order":2704},{"name":"sunrise","shortname":":sunrise:","category":5,"emoji_order":2705},{"name":"cityscape at dusk","shortname":":dusk:","category":5,"emoji_order":2706},{"name":"sunset","shortname":":sunset:","category":5,"emoji_order":2707},{"name":"bridge at night","shortname":":bridge_at_night:","category":5,"emoji_order":2708},{"name":"hot springs","shortname":":hotsprings:","category":5,"emoji_order":2710},{"name":"carousel horse","shortname":":carousel_horse:","category":5,"emoji_order":2711},{"name":"ferris wheel","shortname":":ferris_wheel:","category":5,"emoji_order":2712},{"name":"roller coaster","shortname":":roller_coaster:","category":5,"emoji_order":2713},{"name":"barber pole","shortname":":barber:","category":5,"emoji_order":2714},{"name":"circus tent","shortname":":circus_tent:","category":5,"emoji_order":2715},{"name":"locomotive","shortname":":steam_locomotive:","category":5,"emoji_order":2716},{"name":"railway car","shortname":":railway_car:","category":5,"emoji_order":2717},{"name":"high-speed train","shortname":":bullettrain_side:","category":5,"emoji_order":2718},{"name":"bullet train","shortname":":bullettrain:","category":5,"emoji_order":2719},{"name":"train","shortname":":train:","category":5,"emoji_order":2720},{"name":"metro","shortname":":metro:","category":5,"emoji_order":2721},{"name":"light rail","shortname":":light_rail:","category":5,"emoji_order":2722},{"name":"station","shortname":":station:","category":5,"emoji_order":2723},{"name":"tram","shortname":":tram:","category":5,"emoji_order":2724},{"name":"monorail","shortname":":monorail:","category":5,"emoji_order":2725},{"name":"mountain railway","shortname":":mountain_railway:","category":5,"emoji_order":2726},{"name":"tram car","shortname":":tram_car:","category":5,"emoji_order":2727},{"name":"bus","shortname":":bus:","category":5,"emoji_order":2728},{"name":"oncoming bus","shortname":":oncoming_bus:","category":5,"emoji_order":2729},{"name":"trolleybus","shortname":":trolleybus:","category":5,"emoji_order":2730},{"name":"minibus","shortname":":minibus:","category":5,"emoji_order":2731},{"name":"ambulance","shortname":":ambulance:","category":5,"emoji_order":2732},{"name":"fire engine","shortname":":fire_engine:","category":5,"emoji_order":2733},{"name":"police car","shortname":":police_car:","category":5,"emoji_order":2734},{"name":"oncoming police car","shortname":":oncoming_police_car:","category":5,"emoji_order":2735},{"name":"taxi","shortname":":taxi:","category":5,"emoji_order":2736},{"name":"oncoming taxi","shortname":":oncoming_taxi:","category":5,"emoji_order":2737},{"name":"automobile","shortname":":red_car:","category":5,"emoji_order":2738},{"name":"oncoming automobile","shortname":":oncoming_automobile:","category":5,"emoji_order":2739},{"name":"sport utility vehicle","shortname":":blue_car:","category":5,"emoji_order":2740},{"name":"delivery truck","shortname":":truck:","category":5,"emoji_order":2741},{"name":"articulated lorry","shortname":":lorry:","category":5,"emoji_order":2742},{"name":"tractor","shortname":":tractor:","category":5,"emoji_order":2743},{"name":"racing car","shortname":":race_car:","category":5,"emoji_order":2745},{"name":"motorcycle","shortname":":motorcycle:","category":5,"emoji_order":2747},{"name":"motor scooter","shortname":":motor_scooter:","category":5,"emoji_order":2748},{"name":"manual wheelchair","shortname":":wheelchair:","category":5,"emoji_order":2749},{"name":"motorized wheelchair","shortname":":motor_wheelchair:","category":5,"emoji_order":2750},{"name":"auto rickshaw","shortname":":auto_rickshaw:","category":5,"emoji_order":2751},{"name":"bicycle","shortname":":bike:","category":5,"emoji_order":2752},{"name":"kick scooter","shortname":":scooter:","category":5,"emoji_order":2753},{"name":"skateboard","shortname":":skateboard:","category":5,"emoji_order":2754},{"name":"bus stop","shortname":":bus_stop:","category":5,"emoji_order":2755},{"name":"motorway","shortname":":motorway:","category":5,"emoji_order":2757},{"name":"railway track","shortname":":railway_track:","category":5,"emoji_order":2759},{"name":"oil drum","shortname":":oil_drum:","category":5,"emoji_order":2761},{"name":"fuel pump","shortname":":fuel_pump:","category":5,"emoji_order":2762},{"name":"police car light","shortname":":rotating_light:","category":5,"emoji_order":2763,"aliases":[":police_light:"]},{"name":"horizontal traffic light","shortname":":traffic_light:","category":5,"emoji_order":2764},{"name":"vertical traffic light","shortname":":vertical_traffic_light:","category":5,"emoji_order":2765},{"name":"stop sign","shortname":":stop_sign:","category":5,"emoji_order":2766,"aliases":[":octagonal_sign:"]},{"name":"construction","shortname":":construction:","category":5,"emoji_order":2767},{"name":"anchor","shortname":":anchor:","category":5,"emoji_order":2768},{"name":"sailboat","shortname":":sailboat:","category":5,"emoji_order":2769},{"name":"canoe","shortname":":canoe:","category":5,"emoji_order":2770},{"name":"speedboat","shortname":":speedboat:","category":5,"emoji_order":2771},{"name":"passenger ship","shortname":":cruise_ship:","category":5,"emoji_order":2773},{"name":"ferry","shortname":":ferry:","category":5,"emoji_order":2775},{"name":"motor boat","shortname":":motorboat:","category":5,"emoji_order":2777},{"name":"ship","shortname":":ship:","category":5,"emoji_order":2778},{"name":"airplane","shortname":":airplane:","category":5,"emoji_order":2780},{"name":"small airplane","shortname":":small_airplane:","category":5,"emoji_order":2782},{"name":"airplane departure","shortname":":airplane_departure:","category":5,"emoji_order":2783},{"name":"airplane arrival","shortname":":airplane_arriving:","category":5,"emoji_order":2784},{"name":"parachute","shortname":":parachute:","category":5,"emoji_order":2785},{"name":"seat","shortname":":seat:","category":5,"emoji_order":2786},{"name":"helicopter","shortname":":helicopter:","category":5,"emoji_order":2787},{"name":"suspension railway","shortname":":suspension_railway:","category":5,"emoji_order":2788},{"name":"mountain cableway","shortname":":mountain_cableway:","category":5,"emoji_order":2789},{"name":"aerial tramway","shortname":":aerial_tramway:","category":5,"emoji_order":2790},{"name":"satellite","shortname":":satellite:","category":5,"emoji_order":2792},{"name":"rocket","shortname":":rocket:","category":5,"emoji_order":2793},{"name":"flying saucer","shortname":":flying_saucer:","category":5,"emoji_order":2794},{"name":"bellhop bell","shortname":":bellhop:","category":5,"emoji_order":2796},{"name":"luggage","shortname":":luggage:","category":5,"emoji_order":2797},{"name":"hourglass done","shortname":":hourglass:","category":5,"emoji_order":2798},{"name":"hourglass not done","shortname":":hourglass_flowing:","category":5,"emoji_order":2799},{"name":"watch","shortname":":watch:","category":5,"emoji_order":2800},{"name":"alarm clock","shortname":":alarm_clock:","category":5,"emoji_order":2801},{"name":"stopwatch","shortname":":stopwatch:","category":5,"emoji_order":2803},{"name":"timer clock","shortname":":timer:","category":5,"emoji_order":2805},{"name":"mantelpiece clock","shortname":":clock:","category":5,"emoji_order":2807},{"name":"twelve o’clock","shortname":":clock12:","category":5,"emoji_order":2808},{"name":"twelve-thirty","shortname":":clock1230:","category":5,"emoji_order":2809},{"name":"one o’clock","shortname":":clock1:","category":5,"emoji_order":2810},{"name":"one-thirty","shortname":":clock130:","category":5,"emoji_order":2811},{"name":"two o’clock","shortname":":clock2:","category":5,"emoji_order":2812},{"name":"two-thirty","shortname":":clock230:","category":5,"emoji_order":2813},{"name":"three o’clock","shortname":":clock3:","category":5,"emoji_order":2814},{"name":"three-thirty","shortname":":clock330:","category":5,"emoji_order":2815},{"name":"four o’clock","shortname":":clock4:","category":5,"emoji_order":2816},{"name":"four-thirty","shortname":":clock430:","category":5,"emoji_order":2817},{"name":"five o’clock","shortname":":clock5:","category":5,"emoji_order":2818},{"name":"five-thirty","shortname":":clock530:","category":5,"emoji_order":2819},{"name":"six o’clock","shortname":":clock6:","category":5,"emoji_order":2820},{"name":"six-thirty","shortname":":clock630:","category":5,"emoji_order":2821},{"name":"seven o’clock","shortname":":clock7:","category":5,"emoji_order":2822},{"name":"seven-thirty","shortname":":clock730:","category":5,"emoji_order":2823},{"name":"eight o’clock","shortname":":clock8:","category":5,"emoji_order":2824},{"name":"eight-thirty","shortname":":clock830:","category":5,"emoji_order":2825},{"name":"nine o’clock","shortname":":clock9:","category":5,"emoji_order":2826},{"name":"nine-thirty","shortname":":clock930:","category":5,"emoji_order":2827},{"name":"ten o’clock","shortname":":clock10:","category":5,"emoji_order":2828},{"name":"ten-thirty","shortname":":clock1030:","category":5,"emoji_order":2829},{"name":"eleven o’clock","shortname":":clock11:","category":5,"emoji_order":2830},{"name":"eleven-thirty","shortname":":clock1130:","category":5,"emoji_order":2831},{"name":"new moon","shortname":":new_moon:","category":5,"emoji_order":2832},{"name":"waxing crescent moon","shortname":":waxing_crescent_moon:","category":5,"emoji_order":2833},{"name":"first quarter moon","shortname":":first_quarter_moon:","category":5,"emoji_order":2834},{"name":"waxing gibbous moon","shortname":":waxing_gibbous_moon:","category":5,"emoji_order":2835},{"name":"full moon","shortname":":full_moon:","category":5,"emoji_order":2836},{"name":"waning gibbous moon","shortname":":waning_gibbous_moon:","category":5,"emoji_order":2837},{"name":"last quarter moon","shortname":":last_quarter_moon:","category":5,"emoji_order":2838},{"name":"waning crescent moon","shortname":":waning_crescent_moon:","category":5,"emoji_order":2839},{"name":"crescent moon","shortname":":crescent_moon:","category":5,"emoji_order":2840},{"name":"new moon face","shortname":":new_moon_face:","category":5,"emoji_order":2841},{"name":"first quarter moon face","shortname":":first_quarter_moon_face:","category":5,"emoji_order":2842},{"name":"last quarter moon face","shortname":":last_quarter_moon_face:","category":5,"emoji_order":2843},{"name":"thermometer","shortname":":thermometer:","category":5,"emoji_order":2845},{"name":"sun","shortname":":sun:","category":5,"emoji_order":2847},{"name":"full moon face","shortname":":full_moon_face:","category":5,"emoji_order":2848},{"name":"sun with face","shortname":":sun_face:","category":5,"emoji_order":2849},{"name":"ringed planet","shortname":":ringed_planet:","category":5,"emoji_order":2850,"aliases":[":saturn:"]},{"name":"star","shortname":":star:","category":5,"emoji_order":2851},{"name":"glowing star","shortname":":star2:","category":5,"emoji_order":2852,"aliases":[":glowing_star:"]},{"name":"shooting star","shortname":":star3:","category":5,"emoji_order":2853,"aliases":[":shooting_star:"]},{"name":"milky way","shortname":":milky_way:","category":5,"emoji_order":2854},{"name":"cloud","shortname":":cloud:","category":5,"emoji_order":2856},{"name":"sun behind cloud","shortname":":partly_sunny:","category":5,"emoji_order":2857},{"name":"cloud with lightning and rain","shortname":":storm:","category":5,"emoji_order":2859},{"name":"sun behind small cloud","shortname":":overcast:","category":5,"emoji_order":2861},{"name":"sun behind large cloud","shortname":":cloudy:","category":5,"emoji_order":2863},{"name":"sun behind rain cloud","shortname":":sunshower:","category":5,"emoji_order":2865},{"name":"cloud with rain","shortname":":rain:","category":5,"emoji_order":2867},{"name":"cloud with snow","shortname":":snow:","category":5,"emoji_order":2869},{"name":"cloud with lightning","shortname":":lightning:","category":5,"emoji_order":2871},{"name":"tornado","shortname":":tornado:","category":5,"emoji_order":2873},{"name":"fog","shortname":":fog:","category":5,"emoji_order":2875},{"name":"wind face","shortname":":wind_face:","category":5,"emoji_order":2877},{"name":"cyclone","shortname":":cyclone:","category":5,"emoji_order":2878},{"name":"rainbow","shortname":":rainbow:","category":5,"emoji_order":2879},{"name":"closed umbrella","shortname":":closed_umbrella:","category":5,"emoji_order":2880},{"name":"umbrella","shortname":":umbrella:","category":5,"emoji_order":2882},{"name":"umbrella with rain drops","shortname":":umbrella_rain:","category":5,"emoji_order":2883},{"name":"umbrella on ground","shortname":":beach_umbrella:","category":5,"emoji_order":2885},{"name":"high voltage","shortname":":zap:","category":5,"emoji_order":2886,"aliases":[":high_voltage:"]},{"name":"snowflake","shortname":":snowflake:","category":5,"emoji_order":2888},{"name":"snowman","shortname":":snowy_snowman:","category":5,"emoji_order":2890},{"name":"snowman without snow","shortname":":snowman:","category":5,"emoji_order":2891},{"name":"comet","shortname":":comet:","category":5,"emoji_order":2893},{"name":"fire","shortname":":fire:","category":5,"emoji_order":2894},{"name":"droplet","shortname":":droplet:","category":5,"emoji_order":2895},{"name":"water wave","shortname":":ocean:","category":5,"emoji_order":2896},{"name":"jack-o-lantern","shortname":":jack_o_lantern:","category":6,"emoji_order":2897},{"name":"Christmas tree","shortname":":christmas_tree:","category":6,"emoji_order":2898,"aliases":[":xmas_tree:"]},{"name":"fireworks","shortname":":fireworks:","category":6,"emoji_order":2899},{"name":"sparkler","shortname":":sparkler:","category":6,"emoji_order":2900},{"name":"firecracker","shortname":":firecracker:","category":6,"emoji_order":2901},{"name":"sparkles","shortname":":sparkles:","category":6,"emoji_order":2902},{"name":"balloon","shortname":":balloon:","category":6,"emoji_order":2903},{"name":"party popper","shortname":":tada:","category":6,"emoji_order":2904,"aliases":[":party:"]},{"name":"confetti ball","shortname":":confetti_ball:","category":6,"emoji_order":2905},{"name":"tanabata tree","shortname":":tanabata_tree:","category":6,"emoji_order":2906},{"name":"pine decoration","shortname":":bamboo:","category":6,"emoji_order":2907,"aliases":[":pine_decor:"]},{"name":"Japanese dolls","shortname":":dolls:","category":6,"emoji_order":2908},{"name":"carp streamer","shortname":":carp_streamer:","category":6,"emoji_order":2909},{"name":"wind chime","shortname":":wind_chime:","category":6,"emoji_order":2910},{"name":"moon viewing ceremony","shortname":":moon_ceremony:","category":6,"emoji_order":2911,"aliases":[":rice_scene:"]},{"name":"red envelope","shortname":":red_envelope:","category":6,"emoji_order":2912},{"name":"ribbon","shortname":":ribbon:","category":6,"emoji_order":2913},{"name":"wrapped gift","shortname":":gift:","category":6,"emoji_order":2914},{"name":"reminder ribbon","shortname":":reminder_ribbon:","category":6,"emoji_order":2916},{"name":"admission tickets","shortname":":tickets:","category":6,"emoji_order":2918,"aliases":[":admission:"]},{"name":"ticket","shortname":":ticket:","category":6,"emoji_order":2919},{"name":"military medal","shortname":":military_medal:","category":6,"emoji_order":2921},{"name":"trophy","shortname":":trophy:","category":6,"emoji_order":2922},{"name":"sports medal","shortname":":medal:","category":6,"emoji_order":2923},{"name":"1st place medal","shortname":":first_place:","category":6,"emoji_order":2924},{"name":"2nd place medal","shortname":":second_place:","category":6,"emoji_order":2925},{"name":"3rd place medal","shortname":":third_place:","category":6,"emoji_order":2926},{"name":"soccer ball","shortname":":soccer:","category":6,"emoji_order":2927},{"name":"baseball","shortname":":baseball:","category":6,"emoji_order":2928},{"name":"softball","shortname":":softball:","category":6,"emoji_order":2929},{"name":"basketball","shortname":":basketball:","category":6,"emoji_order":2930},{"name":"volleyball","shortname":":volleyball:","category":6,"emoji_order":2931},{"name":"american football","shortname":":football:","category":6,"emoji_order":2932},{"name":"rugby football","shortname":":rugby:","category":6,"emoji_order":2933},{"name":"tennis","shortname":":tennis:","category":6,"emoji_order":2934},{"name":"flying disc","shortname":":flying_disc:","category":6,"emoji_order":2935},{"name":"bowling","shortname":":bowling:","category":6,"emoji_order":2936},{"name":"cricket game","shortname":":cricket_game:","category":6,"emoji_order":2937},{"name":"field hockey","shortname":":field_hockey:","category":6,"emoji_order":2938},{"name":"ice hockey","shortname":":hockey:","category":6,"emoji_order":2939},{"name":"lacrosse","shortname":":lacrosse:","category":6,"emoji_order":2940},{"name":"ping pong","shortname":":ping_pong:","category":6,"emoji_order":2941},{"name":"badminton","shortname":":badminton:","category":6,"emoji_order":2942},{"name":"boxing glove","shortname":":boxing_glove:","category":6,"emoji_order":2943},{"name":"martial arts uniform","shortname":":gi:","category":6,"emoji_order":2944,"aliases":[":martial_arts_uniform:"]},{"name":"goal net","shortname":":goal:","category":6,"emoji_order":2945},{"name":"flag in hole","shortname":":golf:","category":6,"emoji_order":2946},{"name":"ice skate","shortname":":ice_skate:","category":6,"emoji_order":2948},{"name":"fishing pole","shortname":":fishing_pole:","category":6,"emoji_order":2949},{"name":"diving mask","shortname":":diving_mask:","category":6,"emoji_order":2950,"aliases":[":scuba_mask:"]},{"name":"running shirt","shortname":":running_shirt:","category":6,"emoji_order":2951},{"name":"skis","shortname":":ski:","category":6,"emoji_order":2952},{"name":"sled","shortname":":sled:","category":6,"emoji_order":2953},{"name":"curling stone","shortname":":curling_stone:","category":6,"emoji_order":2954},{"name":"direct hit","shortname":":dart:","category":6,"emoji_order":2955},{"name":"yo-yo","shortname":":yoyo:","category":6,"emoji_order":2956},{"name":"kite","shortname":":kite:","category":6,"emoji_order":2957},{"name":"pool 8 ball","shortname":":8ball:","category":6,"emoji_order":2958},{"name":"crystal ball","shortname":":crystal_ball:","category":6,"emoji_order":2959},{"name":"nazar amulet","shortname":":nazar_amulet:","category":6,"emoji_order":2960},{"name":"video game","shortname":":video_game:","category":6,"emoji_order":2961},{"name":"joystick","shortname":":joystick:","category":6,"emoji_order":2963},{"name":"slot machine","shortname":":slot_machine:","category":6,"emoji_order":2964},{"name":"game die","shortname":":game_die:","category":6,"emoji_order":2965},{"name":"puzzle piece","shortname":":jigsaw:","category":6,"emoji_order":2966,"aliases":[":puzzle_piece:"]},{"name":"teddy bear","shortname":":teddy_bear:","category":6,"emoji_order":2967},{"name":"spade suit","shortname":":spades:","category":6,"emoji_order":2969},{"name":"heart suit","shortname":":hearts:","category":6,"emoji_order":2971},{"name":"diamond suit","shortname":":diamonds:","category":6,"emoji_order":2973},{"name":"club suit","shortname":":clubs:","category":6,"emoji_order":2975},{"name":"chess pawn","shortname":":chess_pawn:","category":6,"emoji_order":2977},{"name":"joker","shortname":":black_joker:","category":6,"emoji_order":2978},{"name":"mahjong red dragon","shortname":":mahjong:","category":6,"emoji_order":2979},{"name":"flower playing cards","shortname":":flower_cards:","category":6,"emoji_order":2980},{"name":"performing arts","shortname":":performing_arts:","category":6,"emoji_order":2981},{"name":"framed picture","shortname":":frame_photo:","category":6,"emoji_order":2983},{"name":"artist palette","shortname":":art:","category":6,"emoji_order":2984,"aliases":[":palette:"]},{"name":"thread","shortname":":spool:","category":6,"emoji_order":2985},{"name":"yarn","shortname":":yarn:","category":6,"emoji_order":2986},{"name":"glasses","shortname":":glasses:","category":7,"emoji_order":2987},{"name":"sunglasses","shortname":":sunglasses:","category":7,"emoji_order":2989},{"name":"goggles","shortname":":goggles:","category":7,"emoji_order":2990},{"name":"lab coat","shortname":":lab_coat:","category":7,"emoji_order":2991},{"name":"safety vest","shortname":":safety_vest:","category":7,"emoji_order":2992},{"name":"necktie","shortname":":necktie:","category":7,"emoji_order":2993,"aliases":[":tie:"]},{"name":"t-shirt","shortname":":shirt:","category":7,"emoji_order":2994},{"name":"jeans","shortname":":jeans:","category":7,"emoji_order":2995},{"name":"scarf","shortname":":scarf:","category":7,"emoji_order":2996},{"name":"gloves","shortname":":gloves:","category":7,"emoji_order":2997},{"name":"coat","shortname":":coat:","category":7,"emoji_order":2998},{"name":"socks","shortname":":socks:","category":7,"emoji_order":2999},{"name":"dress","shortname":":dress:","category":7,"emoji_order":3000},{"name":"kimono","shortname":":kimono:","category":7,"emoji_order":3001},{"name":"sari","shortname":":sari:","category":7,"emoji_order":3002},{"name":"one-piece swimsuit","shortname":":one_piece_swimsuit:","category":7,"emoji_order":3003},{"name":"briefs","shortname":":briefs:","category":7,"emoji_order":3004},{"name":"shorts","shortname":":shorts:","category":7,"emoji_order":3005},{"name":"bikini","shortname":":bikini:","category":7,"emoji_order":3006},{"name":"woman’s clothes","shortname":":blouse:","category":7,"emoji_order":3007,"aliases":[":womans_clothes:"]},{"name":"purse","shortname":":purse:","category":7,"emoji_order":3008},{"name":"handbag","shortname":":handbag:","category":7,"emoji_order":3009},{"name":"clutch bag","shortname":":pouch:","category":7,"emoji_order":3010,"aliases":[":clutch_bag:"]},{"name":"shopping bags","shortname":":shopping_bags:","category":7,"emoji_order":3012},{"name":"backpack","shortname":":backpack:","category":7,"emoji_order":3013},{"name":"man’s shoe","shortname":":dress_shoe:","category":7,"emoji_order":3014,"aliases":[":mans_shoe:"]},{"name":"running shoe","shortname":":sneaker:","category":7,"emoji_order":3015,"aliases":[":athletic_shoe:"]},{"name":"hiking boot","shortname":":hiking_boot:","category":7,"emoji_order":3016},{"name":"flat shoe","shortname":":flat_shoe:","category":7,"emoji_order":3017},{"name":"high-heeled shoe","shortname":":high_heel:","category":7,"emoji_order":3018},{"name":"woman’s sandal","shortname":":womans_sandal:","category":7,"emoji_order":3019},{"name":"ballet shoes","shortname":":ballet_shoes:","category":7,"emoji_order":3020},{"name":"woman’s boot","shortname":":womans_boot:","category":7,"emoji_order":3021},{"name":"crown","shortname":":crown:","category":7,"emoji_order":3022},{"name":"woman’s hat","shortname":":womans_hat:","category":7,"emoji_order":3023},{"name":"top hat","shortname":":top_hat:","category":7,"emoji_order":3024},{"name":"graduation cap","shortname":":graduation_cap:","category":7,"emoji_order":3025},{"name":"billed cap","shortname":":billed_cap:","category":7,"emoji_order":3026},{"name":"rescue worker’s helmet","shortname":":helmet_cross:","category":7,"emoji_order":3028},{"name":"prayer beads","shortname":":prayer_beads:","category":7,"emoji_order":3029},{"name":"lipstick","shortname":":lipstick:","category":7,"emoji_order":3030},{"name":"ring","shortname":":ring:","category":7,"emoji_order":3031},{"name":"gem stone","shortname":":gem:","category":7,"emoji_order":3032},{"name":"muted speaker","shortname":":mute:","category":7,"emoji_order":3033,"aliases":[":no_sound:"]},{"name":"speaker low volume","shortname":":speaker:","category":7,"emoji_order":3034,"aliases":[":low_sound:"]},{"name":"speaker medium volume","shortname":":sound:","category":7,"emoji_order":3035},{"name":"speaker high volume","shortname":":loud_sound:","category":7,"emoji_order":3036},{"name":"loudspeaker","shortname":":loudspeaker:","category":7,"emoji_order":3037},{"name":"megaphone","shortname":":megaphone:","category":7,"emoji_order":3038},{"name":"postal horn","shortname":":postal_horn:","category":7,"emoji_order":3039},{"name":"bell","shortname":":bell:","category":7,"emoji_order":3040},{"name":"bell with slash","shortname":":no_bell:","category":7,"emoji_order":3041},{"name":"musical score","shortname":":musical_score:","category":7,"emoji_order":3042},{"name":"musical note","shortname":":musical_note:","category":7,"emoji_order":3043},{"name":"musical notes","shortname":":musical_notes:","category":7,"emoji_order":3044},{"name":"studio microphone","shortname":":studio_microphone:","category":7,"emoji_order":3046},{"name":"level slider","shortname":":level_slider:","category":7,"emoji_order":3048},{"name":"control knobs","shortname":":control_knobs:","category":7,"emoji_order":3050},{"name":"microphone","shortname":":microphone:","category":7,"emoji_order":3051},{"name":"headphone","shortname":":headphones:","category":7,"emoji_order":3052},{"name":"radio","shortname":":radio:","category":7,"emoji_order":3053},{"name":"saxophone","shortname":":saxophone:","category":7,"emoji_order":3054},{"name":"guitar","shortname":":guitar:","category":7,"emoji_order":3055},{"name":"musical keyboard","shortname":":musical_keyboard:","category":7,"emoji_order":3056},{"name":"trumpet","shortname":":trumpet:","category":7,"emoji_order":3057},{"name":"violin","shortname":":violin:","category":7,"emoji_order":3058},{"name":"banjo","shortname":":banjo:","category":7,"emoji_order":3059},{"name":"drum","shortname":":drum:","category":7,"emoji_order":3060},{"name":"mobile phone","shortname":":mobile:","category":7,"emoji_order":3061,"aliases":[":iphone:",":android:"]},{"name":"mobile phone with arrow","shortname":":mobile_calling:","category":7,"emoji_order":3062},{"name":"telephone","shortname":":telephone:","category":7,"emoji_order":3064},{"name":"telephone receiver","shortname":":telephone_receiver:","category":7,"emoji_order":3065},{"name":"pager","shortname":":pager:","category":7,"emoji_order":3066},{"name":"fax machine","shortname":":fax:","category":7,"emoji_order":3067},{"name":"battery","shortname":":battery:","category":7,"emoji_order":3068},{"name":"electric plug","shortname":":electric_plug:","category":7,"emoji_order":3069},{"name":"laptop computer","shortname":":laptop:","category":7,"emoji_order":3070},{"name":"desktop computer","shortname":":desktop:","category":7,"emoji_order":3072,"aliases":[":computer:"]},{"name":"printer","shortname":":printer:","category":7,"emoji_order":3074},{"name":"keyboard","shortname":":keyboard:","category":7,"emoji_order":3076},{"name":"computer mouse","shortname":":computer_mouse:","category":7,"emoji_order":3078},{"name":"trackball","shortname":":trackball:","category":7,"emoji_order":3080},{"name":"computer disk","shortname":":minidisc:","category":7,"emoji_order":3081},{"name":"floppy disk","shortname":":floppy_disk:","category":7,"emoji_order":3082},{"name":"optical disk","shortname":":cd:","category":7,"emoji_order":3083,"aliases":[":disk:"]},{"name":"dvd","shortname":":dvd:","category":7,"emoji_order":3084},{"name":"abacus","shortname":":abacus:","category":7,"emoji_order":3085},{"name":"movie camera","shortname":":movie_camera:","category":7,"emoji_order":3086},{"name":"film frames","shortname":":film_frames:","category":7,"emoji_order":3088},{"name":"film projector","shortname":":projector:","category":7,"emoji_order":3090},{"name":"clapper board","shortname":":clapper:","category":7,"emoji_order":3091},{"name":"television","shortname":":tv:","category":7,"emoji_order":3092},{"name":"camera","shortname":":camera:","category":7,"emoji_order":3093},{"name":"camera with flash","shortname":":camera_flash:","category":7,"emoji_order":3094},{"name":"video camera","shortname":":video_camera:","category":7,"emoji_order":3095},{"name":"videocassette","shortname":":vhs:","category":7,"emoji_order":3096},{"name":"magnifying glass tilted left","shortname":":mag:","category":7,"emoji_order":3097},{"name":"magnifying glass tilted right","shortname":":mag_right:","category":7,"emoji_order":3098},{"name":"candle","shortname":":candle:","category":7,"emoji_order":3100},{"name":"light bulb","shortname":":bulb:","category":7,"emoji_order":3101,"aliases":[":light_bulb:"]},{"name":"flashlight","shortname":":flashlight:","category":7,"emoji_order":3102},{"name":"red paper lantern","shortname":":red_lantern:","category":7,"emoji_order":3103},{"name":"diya lamp","shortname":":diya_lamp:","category":7,"emoji_order":3104},{"name":"notebook with decorative cover","shortname":":decorative_notebook:","category":7,"emoji_order":3105},{"name":"closed book","shortname":":closed_book:","category":7,"emoji_order":3106},{"name":"open book","shortname":":book:","category":7,"emoji_order":3107},{"name":"green book","shortname":":green_book:","category":7,"emoji_order":3108},{"name":"blue book","shortname":":blue_book:","category":7,"emoji_order":3109},{"name":"orange book","shortname":":orange_book:","category":7,"emoji_order":3110},{"name":"books","shortname":":books:","category":7,"emoji_order":3111},{"name":"notebook","shortname":":notebook:","category":7,"emoji_order":3112},{"name":"ledger","shortname":":ledger:","category":7,"emoji_order":3113},{"name":"page with curl","shortname":":page_curl:","category":7,"emoji_order":3114},{"name":"scroll","shortname":":scroll:","category":7,"emoji_order":3115},{"name":"page facing up","shortname":":page_facing_up:","category":7,"emoji_order":3116},{"name":"newspaper","shortname":":newspaper:","category":7,"emoji_order":3117},{"name":"rolled-up newspaper","shortname":":rolled_newspaper:","category":7,"emoji_order":3119},{"name":"bookmark tabs","shortname":":bookmark_tabs:","category":7,"emoji_order":3120},{"name":"bookmark","shortname":":bookmark:","category":7,"emoji_order":3121},{"name":"label","shortname":":label:","category":7,"emoji_order":3123},{"name":"money bag","shortname":":moneybag:","category":7,"emoji_order":3124},{"name":"yen banknote","shortname":":yen:","category":7,"emoji_order":3125},{"name":"dollar banknote","shortname":":dollar:","category":7,"emoji_order":3126},{"name":"euro banknote","shortname":":euro:","category":7,"emoji_order":3127},{"name":"pound banknote","shortname":":pound:","category":7,"emoji_order":3128},{"name":"money with wings","shortname":":money_wings:","category":7,"emoji_order":3129},{"name":"credit card","shortname":":credit_card:","category":7,"emoji_order":3130},{"name":"receipt","shortname":":receipt:","category":7,"emoji_order":3131},{"name":"chart increasing with yen","shortname":":ja_chart:","category":7,"emoji_order":3132},{"name":"currency exchange","shortname":":currency_exchange:","category":7,"emoji_order":3133},{"name":"heavy dollar sign","shortname":":dollar_sign:","category":7,"emoji_order":3134},{"name":"envelope","shortname":":envelope:","category":7,"emoji_order":3136},{"name":"e-mail","shortname":":email:","category":7,"emoji_order":3137},{"name":"incoming envelope","shortname":":incoming_envelope:","category":7,"emoji_order":3138},{"name":"envelope with arrow","shortname":":envelope_arrow:","category":7,"emoji_order":3139},{"name":"outbox tray","shortname":":outbox_tray:","category":7,"emoji_order":3140},{"name":"inbox tray","shortname":":inbox_tray:","category":7,"emoji_order":3141},{"name":"package","shortname":":package:","category":7,"emoji_order":3142},{"name":"closed mailbox with raised flag","shortname":":mailbox:","category":7,"emoji_order":3143},{"name":"closed mailbox with lowered flag","shortname":":mailbox_closed:","category":7,"emoji_order":3144},{"name":"open mailbox with raised flag","shortname":":mailbox_mail:","category":7,"emoji_order":3145},{"name":"open mailbox with lowered flag","shortname":":mailbox_no_mail:","category":7,"emoji_order":3146},{"name":"postbox","shortname":":postbox:","category":7,"emoji_order":3147},{"name":"ballot box with ballot","shortname":":ballot_box:","category":7,"emoji_order":3149},{"name":"pencil","shortname":":pencil:","category":7,"emoji_order":3151},{"name":"black nib","shortname":":black_nib:","category":7,"emoji_order":3153},{"name":"fountain pen","shortname":":fountain_pen:","category":7,"emoji_order":3155},{"name":"pen","shortname":":pen:","category":7,"emoji_order":3157},{"name":"paintbrush","shortname":":paintbrush:","category":7,"emoji_order":3159},{"name":"crayon","shortname":":crayon:","category":7,"emoji_order":3161},{"name":"memo","shortname":":memo:","category":7,"emoji_order":3162},{"name":"briefcase","shortname":":briefcase:","category":7,"emoji_order":3163},{"name":"file folder","shortname":":file_folder:","category":7,"emoji_order":3164},{"name":"open file folder","shortname":":open_file_folder:","category":7,"emoji_order":3165},{"name":"card index dividers","shortname":":dividers:","category":7,"emoji_order":3167},{"name":"calendar","shortname":":date:","category":7,"emoji_order":3168,"aliases":[":calendar:"]},{"name":"tear-off calendar","shortname":":torn_calendar:","category":7,"emoji_order":3169},{"name":"spiral notepad","shortname":":notepad_spiral:","category":7,"emoji_order":3171},{"name":"spiral calendar","shortname":":calendar_spiral:","category":7,"emoji_order":3173},{"name":"card index","shortname":":card_index:","category":7,"emoji_order":3174},{"name":"chart increasing","shortname":":chart_up:","category":7,"emoji_order":3175},{"name":"chart decreasing","shortname":":chart_down:","category":7,"emoji_order":3176},{"name":"bar chart","shortname":":bar_chart:","category":7,"emoji_order":3177},{"name":"clipboard","shortname":":clipboard:","category":7,"emoji_order":3178},{"name":"pushpin","shortname":":pushpin:","category":7,"emoji_order":3179},{"name":"round pushpin","shortname":":round_pushpin:","category":7,"emoji_order":3180},{"name":"paperclip","shortname":":paperclip:","category":7,"emoji_order":3181},{"name":"linked paperclips","shortname":":paperclips:","category":7,"emoji_order":3183},{"name":"straight ruler","shortname":":straight_ruler:","category":7,"emoji_order":3184},{"name":"triangular ruler","shortname":":triangular_ruler:","category":7,"emoji_order":3185},{"name":"scissors","shortname":":scissors:","category":7,"emoji_order":3187},{"name":"card file box","shortname":":card_box:","category":7,"emoji_order":3189},{"name":"file cabinet","shortname":":file_cabinet:","category":7,"emoji_order":3191},{"name":"wastebasket","shortname":":trashcan:","category":7,"emoji_order":3193,"aliases":[":wastebasket:"]},{"name":"locked","shortname":":lock:","category":7,"emoji_order":3194},{"name":"unlocked","shortname":":unlock:","category":7,"emoji_order":3195},{"name":"locked with pen","shortname":":locked_pen:","category":7,"emoji_order":3196},{"name":"locked with key","shortname":":locked_key:","category":7,"emoji_order":3197},{"name":"key","shortname":":key:","category":7,"emoji_order":3198},{"name":"old key","shortname":":old_key:","category":7,"emoji_order":3200},{"name":"hammer","shortname":":hammer:","category":7,"emoji_order":3201},{"name":"axe","shortname":":axe:","category":7,"emoji_order":3202},{"name":"pick","shortname":":pick:","category":7,"emoji_order":3204},{"name":"hammer and pick","shortname":":hammer_pick:","category":7,"emoji_order":3206},{"name":"hammer and wrench","shortname":":tools:","category":7,"emoji_order":3208,"aliases":[":hammer_wrench:"]},{"name":"dagger","shortname":":dagger:","category":7,"emoji_order":3210},{"name":"crossed swords","shortname":":crossed_swords:","category":7,"emoji_order":3212},{"name":"pistol","shortname":":gun:","category":7,"emoji_order":3213,"aliases":[":pistol:"]},{"name":"bow and arrow","shortname":":bow:","category":7,"emoji_order":3214},{"name":"shield","shortname":":shield:","category":7,"emoji_order":3216},{"name":"wrench","shortname":":wrench:","category":7,"emoji_order":3217},{"name":"nut and bolt","shortname":":nut_and_bolt:","category":7,"emoji_order":3218},{"name":"gear","shortname":":gear:","category":7,"emoji_order":3220},{"name":"clamp","shortname":":clamp:","category":7,"emoji_order":3222,"aliases":[":compression:"]},{"name":"balance scale","shortname":":scales:","category":7,"emoji_order":3224},{"name":"probing cane","shortname":":probing_cane:","category":7,"emoji_order":3225},{"name":"link","shortname":":link:","category":7,"emoji_order":3226},{"name":"chains","shortname":":chains:","category":7,"emoji_order":3228},{"name":"toolbox","shortname":":toolbox:","category":7,"emoji_order":3229},{"name":"magnet","shortname":":magnet:","category":7,"emoji_order":3230},{"name":"alembic","shortname":":alembic:","category":7,"emoji_order":3232},{"name":"test tube","shortname":":test_tube:","category":7,"emoji_order":3233},{"name":"petri dish","shortname":":petri_dish:","category":7,"emoji_order":3234},{"name":"dna","shortname":":dna:","category":7,"emoji_order":3235,"aliases":[":double_helix:"]},{"name":"microscope","shortname":":microscope:","category":7,"emoji_order":3236},{"name":"telescope","shortname":":telescope:","category":7,"emoji_order":3237},{"name":"satellite antenna","shortname":":satellite_antenna:","category":7,"emoji_order":3238},{"name":"syringe","shortname":":syringe:","category":7,"emoji_order":3239},{"name":"drop of blood","shortname":":blood_drop:","category":7,"emoji_order":3240},{"name":"pill","shortname":":pill:","category":7,"emoji_order":3241},{"name":"adhesive bandage","shortname":":bandaid:","category":7,"emoji_order":3242,"aliases":[":adhesive_bandage:"]},{"name":"stethoscope","shortname":":stethoscope:","category":7,"emoji_order":3243},{"name":"door","shortname":":door:","category":7,"emoji_order":3244},{"name":"bed","shortname":":bed:","category":7,"emoji_order":3246},{"name":"couch and lamp","shortname":":couch:","category":7,"emoji_order":3248},{"name":"chair","shortname":":chair:","category":7,"emoji_order":3249},{"name":"toilet","shortname":":toilet:","category":7,"emoji_order":3250},{"name":"shower","shortname":":shower:","category":7,"emoji_order":3251},{"name":"bathtub","shortname":":bathtub:","category":7,"emoji_order":3252},{"name":"razor","shortname":":razor:","category":7,"emoji_order":3253},{"name":"lotion bottle","shortname":":lotion:","category":7,"emoji_order":3254},{"name":"safety pin","shortname":":safety_pin:","category":7,"emoji_order":3255},{"name":"broom","shortname":":broom:","category":7,"emoji_order":3256},{"name":"basket","shortname":":basket:","category":7,"emoji_order":3257},{"name":"roll of paper","shortname":":toilet_paper:","category":7,"emoji_order":3258},{"name":"soap","shortname":":soap:","category":7,"emoji_order":3259},{"name":"sponge","shortname":":sponge:","category":7,"emoji_order":3260},{"name":"fire extinguisher","shortname":":fire_extinguisher:","category":7,"emoji_order":3261},{"name":"shopping cart","shortname":":shopping_cart:","category":7,"emoji_order":3262},{"name":"cigarette","shortname":":cigarette:","category":7,"emoji_order":3263,"aliases":[":smoking:"]},{"name":"coffin","shortname":":coffin:","category":7,"emoji_order":3265},{"name":"funeral urn","shortname":":urn:","category":7,"emoji_order":3267},{"name":"moai","shortname":":moai:","category":7,"emoji_order":3268},{"name":"ATM sign","shortname":":atm:","category":8,"emoji_order":3269},{"name":"litter in bin sign","shortname":":litter_bin:","category":8,"emoji_order":3270},{"name":"potable water","shortname":":potable_water:","category":8,"emoji_order":3271},{"name":"wheelchair symbol","shortname":":handicapped:","category":8,"emoji_order":3272},{"name":"men’s room","shortname":":mens:","category":8,"emoji_order":3273},{"name":"women’s room","shortname":":womens:","category":8,"emoji_order":3274},{"name":"restroom","shortname":":restroom:","category":8,"emoji_order":3275,"aliases":[":bathroom:"]},{"name":"baby symbol","shortname":":baby_symbol:","category":8,"emoji_order":3276},{"name":"water closet","shortname":":wc:","category":8,"emoji_order":3277},{"name":"passport control","shortname":":passport_control:","category":8,"emoji_order":3278},{"name":"customs","shortname":":customs:","category":8,"emoji_order":3279},{"name":"baggage claim","shortname":":baggage_claim:","category":8,"emoji_order":3280},{"name":"left luggage","shortname":":left_luggage:","category":8,"emoji_order":3281},{"name":"warning","shortname":":warning:","category":8,"emoji_order":3283},{"name":"children crossing","shortname":":children_crossing:","category":8,"emoji_order":3284},{"name":"no entry","shortname":":no_entry:","category":8,"emoji_order":3285},{"name":"prohibited","shortname":":no_entry_sign:","category":8,"emoji_order":3286},{"name":"no bicycles","shortname":":no_bicycles:","category":8,"emoji_order":3287},{"name":"no smoking","shortname":":no_smoking:","category":8,"emoji_order":3288},{"name":"no littering","shortname":":do_not_litter:","category":8,"emoji_order":3289},{"name":"non-potable water","shortname":":non_potable_water:","category":8,"emoji_order":3290},{"name":"no pedestrians","shortname":":no_pedestrians:","category":8,"emoji_order":3291},{"name":"no mobile phones","shortname":":no_mobile_phones:","category":8,"emoji_order":3292},{"name":"no one under eighteen","shortname":":underage:","category":8,"emoji_order":3293},{"name":"radioactive","shortname":":radioactive:","category":8,"emoji_order":3295},{"name":"biohazard","shortname":":biohazard:","category":8,"emoji_order":3297},{"name":"up arrow","shortname":":arrow_up:","category":8,"emoji_order":3299},{"name":"up-right arrow","shortname":":arrow_upper_right:","category":8,"emoji_order":3301},{"name":"right arrow","shortname":":arrow_right:","category":8,"emoji_order":3303},{"name":"down-right arrow","shortname":":arrow_lower_right:","category":8,"emoji_order":3305},{"name":"down arrow","shortname":":arrow_down:","category":8,"emoji_order":3307},{"name":"down-left arrow","shortname":":arrow_lower_left:","category":8,"emoji_order":3309},{"name":"left arrow","shortname":":arrow_left:","category":8,"emoji_order":3311},{"name":"up-left arrow","shortname":":arrow_upper_left:","category":8,"emoji_order":3313},{"name":"up-down arrow","shortname":":arrow_up_down:","category":8,"emoji_order":3315},{"name":"left-right arrow","shortname":":arrow_left_right:","category":8,"emoji_order":3317},{"name":"right arrow curving left","shortname":":arrow_left_hook:","category":8,"emoji_order":3319},{"name":"left arrow curving right","shortname":":arrow_right_hook:","category":8,"emoji_order":3321},{"name":"right arrow curving up","shortname":":arrow_heading_up:","category":8,"emoji_order":3323},{"name":"right arrow curving down","shortname":":arrow_heading_down:","category":8,"emoji_order":3325},{"name":"clockwise vertical arrows","shortname":":clockwise:","category":8,"emoji_order":3326},{"name":"counterclockwise arrows button","shortname":":counter_clockwise:","category":8,"emoji_order":3327},{"name":"BACK arrow","shortname":":back:","category":8,"emoji_order":3328},{"name":"END arrow","shortname":":end:","category":8,"emoji_order":3329},{"name":"ON! arrow","shortname":":on:","category":8,"emoji_order":3330},{"name":"SOON arrow","shortname":":soon:","category":8,"emoji_order":3331},{"name":"TOP arrow","shortname":":top:","category":8,"emoji_order":3332},{"name":"place of worship","shortname":":place_of_worship:","category":8,"emoji_order":3333},{"name":"atom symbol","shortname":":atom:","category":8,"emoji_order":3335},{"name":"om","shortname":":om_symbol:","category":8,"emoji_order":3337},{"name":"star of David","shortname":":star_of_david:","category":8,"emoji_order":3339},{"name":"wheel of dharma","shortname":":wheel_of_dharma:","category":8,"emoji_order":3341},{"name":"yin yang","shortname":":yin_yang:","category":8,"emoji_order":3343},{"name":"latin cross","shortname":":cross:","category":8,"emoji_order":3345},{"name":"orthodox cross","shortname":":orthodox_cross:","category":8,"emoji_order":3347},{"name":"star and crescent","shortname":":star_and_crescent:","category":8,"emoji_order":3349},{"name":"peace symbol","shortname":":peace:","category":8,"emoji_order":3351},{"name":"menorah","shortname":":menorah:","category":8,"emoji_order":3352},{"name":"dotted six-pointed star","shortname":":six_pointed_star:","category":8,"emoji_order":3353},{"name":"Aries","shortname":":aries:","category":8,"emoji_order":3354},{"name":"Taurus","shortname":":taurus:","category":8,"emoji_order":3355},{"name":"Gemini","shortname":":gemini:","category":8,"emoji_order":3356},{"name":"Cancer","shortname":":cancer:","category":8,"emoji_order":3357},{"name":"Leo","shortname":":leo:","category":8,"emoji_order":3358},{"name":"Virgo","shortname":":virgo:","category":8,"emoji_order":3359},{"name":"Libra","shortname":":libra:","category":8,"emoji_order":3360},{"name":"Scorpio","shortname":":scorpius:","category":8,"emoji_order":3361},{"name":"Sagittarius","shortname":":sagittarius:","category":8,"emoji_order":3362},{"name":"Capricorn","shortname":":capricorn:","category":8,"emoji_order":3363},{"name":"Aquarius","shortname":":aquarius:","category":8,"emoji_order":3364},{"name":"Pisces","shortname":":pisces:","category":8,"emoji_order":3365},{"name":"Ophiuchus","shortname":":ophiuchus:","category":8,"emoji_order":3366},{"name":"shuffle tracks button","shortname":":shuffle:","category":8,"emoji_order":3367},{"name":"repeat button","shortname":":repeat:","category":8,"emoji_order":3368},{"name":"repeat single button","shortname":":repeat_single:","category":8,"emoji_order":3369},{"name":"play button","shortname":":play:","category":8,"emoji_order":3371},{"name":"fast-forward button","shortname":":fast_forward:","category":8,"emoji_order":3372},{"name":"next track button","shortname":":next_track:","category":8,"emoji_order":3374},{"name":"play or pause button","shortname":":play_pause:","category":8,"emoji_order":3376},{"name":"reverse button","shortname":":reverse:","category":8,"emoji_order":3378},{"name":"fast reverse button","shortname":":rewind:","category":8,"emoji_order":3379},{"name":"last track button","shortname":":previous_track:","category":8,"emoji_order":3381},{"name":"upwards button","shortname":":up_button:","category":8,"emoji_order":3382},{"name":"fast up button","shortname":":fast_up_button:","category":8,"emoji_order":3383},{"name":"downwards button","shortname":":down_button:","category":8,"emoji_order":3384},{"name":"fast down button","shortname":":fast_down_button:","category":8,"emoji_order":3385},{"name":"pause button","shortname":":pause:","category":8,"emoji_order":3387},{"name":"stop button","shortname":":stop:","category":8,"emoji_order":3389},{"name":"record button","shortname":":record:","category":8,"emoji_order":3391},{"name":"eject button","shortname":":eject:","category":8,"emoji_order":3393},{"name":"cinema","shortname":":cinema:","category":8,"emoji_order":3394},{"name":"dim button","shortname":":dim:","category":8,"emoji_order":3395,"aliases":[":low_brightness:"]},{"name":"bright button","shortname":":bright:","category":8,"emoji_order":3396,"aliases":[":high_brightness:"]},{"name":"antenna bars","shortname":":signal_strength:","category":8,"emoji_order":3397,"aliases":[":antenna_bars:"]},{"name":"vibration mode","shortname":":vibration_mode:","category":8,"emoji_order":3398},{"name":"mobile phone off","shortname":":mobile_phone_off:","category":8,"emoji_order":3399},{"name":"female sign","shortname":":female:","category":8,"emoji_order":3401,"aliases":[":female_sign:"]},{"name":"male sign","shortname":":male:","category":8,"emoji_order":3403,"aliases":[":male_sign:"]},{"name":"medical symbol","shortname":":medical:","category":8,"emoji_order":3405},{"name":"infinity","shortname":":infinity:","category":8,"emoji_order":3407},{"name":"recycling symbol","shortname":":recycle:","category":8,"emoji_order":3409},{"name":"fleur-de-lis","shortname":":fleur-de-lis:","category":8,"emoji_order":3411},{"name":"trident emblem","shortname":":trident:","category":8,"emoji_order":3412},{"name":"name badge","shortname":":name_badge:","category":8,"emoji_order":3413},{"name":"Japanese symbol for beginner","shortname":":ja_beginner:","category":8,"emoji_order":3414},{"name":"hollow red circle","shortname":":o:","category":8,"emoji_order":3415},{"name":"check mark button","shortname":":white_check_mark:","category":8,"emoji_order":3416},{"name":"check box with check","shortname":":checked_ballot:","category":8,"emoji_order":3418},{"name":"check mark","shortname":":check_mark:","category":8,"emoji_order":3420},{"name":"multiplication sign","shortname":":multiplication:","category":8,"emoji_order":3422},{"name":"cross mark","shortname":":x:","category":8,"emoji_order":3423,"aliases":[":cross_mark:"]},{"name":"cross mark button","shortname":":cross_mark_button:","category":8,"emoji_order":3424},{"name":"plus sign","shortname":":plus:","category":8,"emoji_order":3425},{"name":"minus sign","shortname":":minus:","category":8,"emoji_order":3426},{"name":"division sign","shortname":":division:","category":8,"emoji_order":3427},{"name":"curly loop","shortname":":curly_loop:","category":8,"emoji_order":3428},{"name":"double curly loop","shortname":":double_curly_loop:","category":8,"emoji_order":3429},{"name":"part alternation mark","shortname":":part_alternation_mark:","category":8,"emoji_order":3431},{"name":"eight-spoked asterisk","shortname":":eight_spoked_asterisk:","category":8,"emoji_order":3433},{"name":"eight-pointed star","shortname":":eight_pointed_star:","category":8,"emoji_order":3435},{"name":"sparkle","shortname":":sparkle:","category":8,"emoji_order":3437},{"name":"double exclamation mark","shortname":":bangbang:","category":8,"emoji_order":3439,"aliases":[":double_exclamation:"]},{"name":"exclamation question mark","shortname":":interrobang:","category":8,"emoji_order":3441,"aliases":[":exclamation_question:"]},{"name":"question mark","shortname":":question:","category":8,"emoji_order":3442},{"name":"white question mark","shortname":":white_question:","category":8,"emoji_order":3443},{"name":"white exclamation mark","shortname":":white_exclamation:","category":8,"emoji_order":3444},{"name":"exclamation mark","shortname":":exclamation:","category":8,"emoji_order":3445},{"name":"wavy dash","shortname":":wavy_dash:","category":8,"emoji_order":3447},{"name":"copyright","shortname":":copyright:","category":8,"emoji_order":3449},{"name":"registered","shortname":":registered:","category":8,"emoji_order":3451},{"name":"trade mark","shortname":":tm:","category":8,"emoji_order":3453},{"name":"keycap: #","shortname":":hash:","category":8,"emoji_order":3454},{"name":"keycap: *","shortname":":asterisk:","category":8,"emoji_order":3456},{"name":"keycap: 0","shortname":":zero:","category":8,"emoji_order":3458},{"name":"keycap: 1","shortname":":one:","category":8,"emoji_order":3460},{"name":"keycap: 2","shortname":":two:","category":8,"emoji_order":3462},{"name":"keycap: 3","shortname":":three:","category":8,"emoji_order":3464},{"name":"keycap: 4","shortname":":four:","category":8,"emoji_order":3466},{"name":"keycap: 5","shortname":":five:","category":8,"emoji_order":3468},{"name":"keycap: 6","shortname":":six:","category":8,"emoji_order":3470},{"name":"keycap: 7","shortname":":seven:","category":8,"emoji_order":3472},{"name":"keycap: 8","shortname":":eight:","category":8,"emoji_order":3474},{"name":"keycap: 9","shortname":":nine:","category":8,"emoji_order":3476},{"name":"keycap: 10","shortname":":ten:","category":8,"emoji_order":3478},{"name":"input latin uppercase","shortname":":upper_abcd:","category":8,"emoji_order":3479},{"name":"input latin lowercase","shortname":":abcd:","category":8,"emoji_order":3480},{"name":"input numbers","shortname":":1234:","category":8,"emoji_order":3481},{"name":"input symbols","shortname":":symbols:","category":8,"emoji_order":3482},{"name":"input latin letters","shortname":":abc:","category":8,"emoji_order":3483},{"name":"A button (blood type)","shortname":":a_blood:","category":8,"emoji_order":3485},{"name":"AB button (blood type)","shortname":":ab_blood:","category":8,"emoji_order":3486},{"name":"B button (blood type)","shortname":":b_blood:","category":8,"emoji_order":3488},{"name":"CL button","shortname":":cl:","category":8,"emoji_order":3489},{"name":"COOL button","shortname":":cool:","category":8,"emoji_order":3490},{"name":"FREE button","shortname":":free:","category":8,"emoji_order":3491},{"name":"information","shortname":":info:","category":8,"emoji_order":3493},{"name":"ID button","shortname":":id:","category":8,"emoji_order":3494},{"name":"circled M","shortname":":m:","category":8,"emoji_order":3496},{"name":"NEW button","shortname":":new:","category":8,"emoji_order":3497},{"name":"NG button","shortname":":ng:","category":8,"emoji_order":3498},{"name":"O button (blood type)","shortname":":o_blood:","category":8,"emoji_order":3500},{"name":"OK button","shortname":":ok:","category":8,"emoji_order":3501},{"name":"P button","shortname":":p:","category":8,"emoji_order":3503},{"name":"SOS button","shortname":":sos:","category":8,"emoji_order":3504},{"name":"UP! button","shortname":":up:","category":8,"emoji_order":3505},{"name":"VS button","shortname":":vs:","category":8,"emoji_order":3506},{"name":"Japanese “here” button","shortname":":ja_here:","category":8,"emoji_order":3507,"aliases":[":koko:"]},{"name":"Japanese “service charge” button","shortname":":ja_service_charge:","category":8,"emoji_order":3509},{"name":"Japanese “monthly amount” button","shortname":":ja_monthly_amount:","category":8,"emoji_order":3511},{"name":"Japanese “not free of charge” button","shortname":":ja_not_free_of_carge:","category":8,"emoji_order":3512},{"name":"Japanese “reserved” button","shortname":":ja_reserved:","category":8,"emoji_order":3513},{"name":"Japanese “bargain” button","shortname":":ja_bargain:","category":8,"emoji_order":3514},{"name":"Japanese “discount” button","shortname":":ja_discount:","category":8,"emoji_order":3515},{"name":"Japanese “free of charge” button","shortname":":ja_free_of_charge:","category":8,"emoji_order":3516},{"name":"Japanese “prohibited” button","shortname":":ja_prohibited:","category":8,"emoji_order":3517},{"name":"Japanese “acceptable” button","shortname":":ja_acceptable:","category":8,"emoji_order":3518},{"name":"Japanese “application” button","shortname":":ja_application:","category":8,"emoji_order":3519},{"name":"Japanese “passing grade” button","shortname":":ja_passing_grade:","category":8,"emoji_order":3520},{"name":"Japanese “vacancy” button","shortname":":ja_vacancy:","category":8,"emoji_order":3521},{"name":"Japanese “congratulations” button","shortname":":ja_congratulations:","category":8,"emoji_order":3523},{"name":"Japanese “secret” button","shortname":":ja_secret:","category":8,"emoji_order":3525},{"name":"Japanese “open for business” button","shortname":":ja_open_for_business:","category":8,"emoji_order":3526},{"name":"Japanese “no vacancy” button","shortname":":ja_no_vacancy:","category":8,"emoji_order":3527},{"name":"red circle","shortname":":red_circle:","category":8,"emoji_order":3528},{"name":"orange circle","shortname":":orange_circle:","category":8,"emoji_order":3529},{"name":"yellow circle","shortname":":yellow_circle:","category":8,"emoji_order":3530},{"name":"green circle","shortname":":green_circle:","category":8,"emoji_order":3531},{"name":"blue circle","shortname":":blue_circle:","category":8,"emoji_order":3532},{"name":"purple circle","shortname":":purple_circle:","category":8,"emoji_order":3533},{"name":"brown circle","shortname":":brown_circle:","category":8,"emoji_order":3534},{"name":"black circle","shortname":":black_circle:","category":8,"emoji_order":3535},{"name":"white circle","shortname":":white_circle:","category":8,"emoji_order":3536},{"name":"red square","shortname":":red_square:","category":8,"emoji_order":3537},{"name":"orange square","shortname":":orange_square:","category":8,"emoji_order":3538},{"name":"yellow square","shortname":":yellow_square:","category":8,"emoji_order":3539},{"name":"green square","shortname":":green_square:","category":8,"emoji_order":3540},{"name":"blue square","shortname":":blue_square:","category":8,"emoji_order":3541},{"name":"purple square","shortname":":purple_square:","category":8,"emoji_order":3542},{"name":"brown square","shortname":":brown_square:","category":8,"emoji_order":3543},{"name":"black large square","shortname":":large_black_square:","category":8,"emoji_order":3544},{"name":"white large square","shortname":":large_white_square:","category":8,"emoji_order":3545},{"name":"black medium square","shortname":":medium_black_square:","category":8,"emoji_order":3547},{"name":"white medium square","shortname":":medium_white_square:","category":8,"emoji_order":3549},{"name":"black medium-small square","shortname":":medium_small_black_square:","category":8,"emoji_order":3550},{"name":"white medium-small square","shortname":":medium_small_white_square:","category":8,"emoji_order":3551},{"name":"black small square","shortname":":small_black_square:","category":8,"emoji_order":3553},{"name":"white small square","shortname":":small_white_square:","category":8,"emoji_order":3555},{"name":"large orange diamond","shortname":":large_orange_diamond:","category":8,"emoji_order":3556},{"name":"large blue diamond","shortname":":large_blue_diamond:","category":8,"emoji_order":3557},{"name":"small orange diamond","shortname":":small_orange_diamond:","category":8,"emoji_order":3558},{"name":"small blue diamond","shortname":":small_blue_diamond:","category":8,"emoji_order":3559},{"name":"red triangle pointed up","shortname":":up_red_triangle:","category":8,"emoji_order":3560},{"name":"red triangle pointed down","shortname":":down_red_triangle:","category":8,"emoji_order":3561},{"name":"diamond with a dot","shortname":":diamond_dot:","category":8,"emoji_order":3562},{"name":"radio button","shortname":":radio_button:","category":8,"emoji_order":3563},{"name":"white square button","shortname":":white_square_button:","category":8,"emoji_order":3564},{"name":"black square button","shortname":":black_square_button:","category":8,"emoji_order":3565},{"name":"chequered flag","shortname":":checkered_flag:","category":9,"emoji_order":3566},{"name":"triangular flag","shortname":":triangle_flag:","category":9,"emoji_order":3567},{"name":"crossed flags","shortname":":crossed_flags:","category":9,"emoji_order":3568},{"name":"black flag","shortname":":black_flag:","category":9,"emoji_order":3569},{"name":"white flag","shortname":":white_flag:","category":9,"emoji_order":3571},{"name":"rainbow flag","shortname":":rainbow_flag:","category":9,"emoji_order":3572},{"name":"pirate flag","shortname":":pirate_flag:","category":9,"emoji_order":3574,"aliases":[":jolly_roger:"]},{"name":"flag: Ascension Island","shortname":":flag_ac:","category":9,"emoji_order":3576},{"name":"flag: Andorra","shortname":":flag_ad:","category":9,"emoji_order":3577},{"name":"flag: United Arab Emirates","shortname":":flag_ae:","category":9,"emoji_order":3578},{"name":"flag: Afghanistan","shortname":":flag_af:","category":9,"emoji_order":3579},{"name":"flag: Antigua & Barbuda","shortname":":flag_ag:","category":9,"emoji_order":3580},{"name":"flag: Anguilla","shortname":":flag_ai:","category":9,"emoji_order":3581},{"name":"flag: Albania","shortname":":flag_al:","category":9,"emoji_order":3582},{"name":"flag: Armenia","shortname":":flag_am:","category":9,"emoji_order":3583},{"name":"flag: Angola","shortname":":flag_ao:","category":9,"emoji_order":3584},{"name":"flag: Antarctica","shortname":":flag_aq:","category":9,"emoji_order":3585},{"name":"flag: Argentina","shortname":":flag_ar:","category":9,"emoji_order":3586},{"name":"flag: American Samoa","shortname":":flag_as:","category":9,"emoji_order":3587},{"name":"flag: Austria","shortname":":flag_at:","category":9,"emoji_order":3588},{"name":"flag: Australia","shortname":":flag_au:","category":9,"emoji_order":3589},{"name":"flag: Aruba","shortname":":flag_aw:","category":9,"emoji_order":3590},{"name":"flag: Åland Islands","shortname":":flag_ax:","category":9,"emoji_order":3591},{"name":"flag: Azerbaijan","shortname":":flag_az:","category":9,"emoji_order":3592},{"name":"flag: Bosnia & Herzegovina","shortname":":flag_ba:","category":9,"emoji_order":3593},{"name":"flag: Barbados","shortname":":flag_bb:","category":9,"emoji_order":3594},{"name":"flag: Bangladesh","shortname":":flag_bd:","category":9,"emoji_order":3595},{"name":"flag: Belgium","shortname":":flag_be:","category":9,"emoji_order":3596},{"name":"flag: Burkina Faso","shortname":":flag_bf:","category":9,"emoji_order":3597},{"name":"flag: Bulgaria","shortname":":flag_bg:","category":9,"emoji_order":3598},{"name":"flag: Bahrain","shortname":":flag_bh:","category":9,"emoji_order":3599},{"name":"flag: Burundi","shortname":":flag_bi:","category":9,"emoji_order":3600},{"name":"flag: Benin","shortname":":flag_bj:","category":9,"emoji_order":3601},{"name":"flag: St. Barthélemy","shortname":":flag_bl:","category":9,"emoji_order":3602},{"name":"flag: Bermuda","shortname":":flag_bm:","category":9,"emoji_order":3603},{"name":"flag: Brunei","shortname":":flag_bn:","category":9,"emoji_order":3604},{"name":"flag: Bolivia","shortname":":flag_bo:","category":9,"emoji_order":3605},{"name":"flag: Caribbean Netherlands","shortname":":flag_bq:","category":9,"emoji_order":3606},{"name":"flag: Brazil","shortname":":flag_br:","category":9,"emoji_order":3607},{"name":"flag: Bahamas","shortname":":flag_bs:","category":9,"emoji_order":3608},{"name":"flag: Bhutan","shortname":":flag_bt:","category":9,"emoji_order":3609},{"name":"flag: Bouvet Island","shortname":":flag_bv:","category":9,"emoji_order":3610},{"name":"flag: Botswana","shortname":":flag_bw:","category":9,"emoji_order":3611},{"name":"flag: Belarus","shortname":":flag_by:","category":9,"emoji_order":3612},{"name":"flag: Belize","shortname":":flag_bz:","category":9,"emoji_order":3613},{"name":"flag: Canada","shortname":":flag_ca:","category":9,"emoji_order":3614},{"name":"flag: Cocos (Keeling) Islands","shortname":":flag_cc:","category":9,"emoji_order":3615},{"name":"flag: Congo - Kinshasa","shortname":":flag_cd:","category":9,"emoji_order":3616},{"name":"flag: Central African Republic","shortname":":flag_cf:","category":9,"emoji_order":3617},{"name":"flag: Congo - Brazzaville","shortname":":flag_cg:","category":9,"emoji_order":3618},{"name":"flag: Switzerland","shortname":":flag_ch:","category":9,"emoji_order":3619},{"name":"flag: Côte d’Ivoire","shortname":":flag_ci:","category":9,"emoji_order":3620},{"name":"flag: Cook Islands","shortname":":flag_ck:","category":9,"emoji_order":3621},{"name":"flag: Chile","shortname":":flag_cl:","category":9,"emoji_order":3622},{"name":"flag: Cameroon","shortname":":flag_cm:","category":9,"emoji_order":3623},{"name":"flag: China","shortname":":flag_cn:","category":9,"emoji_order":3624},{"name":"flag: Colombia","shortname":":flag_co:","category":9,"emoji_order":3625},{"name":"flag: Clipperton Island","shortname":":flag_cp:","category":9,"emoji_order":3626},{"name":"flag: Costa Rica","shortname":":flag_cr:","category":9,"emoji_order":3627},{"name":"flag: Cuba","shortname":":flag_cu:","category":9,"emoji_order":3628},{"name":"flag: Cape Verde","shortname":":flag_cv:","category":9,"emoji_order":3629},{"name":"flag: Curaçao","shortname":":flag_cw:","category":9,"emoji_order":3630},{"name":"flag: Christmas Island","shortname":":flag_cx:","category":9,"emoji_order":3631},{"name":"flag: Cyprus","shortname":":flag_cy:","category":9,"emoji_order":3632},{"name":"flag: Czechia","shortname":":flag_cz:","category":9,"emoji_order":3633},{"name":"flag: Germany","shortname":":flag_de:","category":9,"emoji_order":3634},{"name":"flag: Diego Garcia","shortname":":flag_dg:","category":9,"emoji_order":3635},{"name":"flag: Djibouti","shortname":":flag_dj:","category":9,"emoji_order":3636},{"name":"flag: Denmark","shortname":":flag_dk:","category":9,"emoji_order":3637},{"name":"flag: Dominica","shortname":":flag_dm:","category":9,"emoji_order":3638},{"name":"flag: Dominican Republic","shortname":":flag_do:","category":9,"emoji_order":3639},{"name":"flag: Algeria","shortname":":flag_dz:","category":9,"emoji_order":3640},{"name":"flag: Ceuta & Melilla","shortname":":flag_ea:","category":9,"emoji_order":3641},{"name":"flag: Ecuador","shortname":":flag_ec:","category":9,"emoji_order":3642},{"name":"flag: Estonia","shortname":":flag_ee:","category":9,"emoji_order":3643},{"name":"flag: Egypt","shortname":":flag_eg:","category":9,"emoji_order":3644},{"name":"flag: Western Sahara","shortname":":flag_eh:","category":9,"emoji_order":3645},{"name":"flag: Eritrea","shortname":":flag_er:","category":9,"emoji_order":3646},{"name":"flag: Spain","shortname":":flag_es:","category":9,"emoji_order":3647},{"name":"flag: Ethiopia","shortname":":flag_et:","category":9,"emoji_order":3648},{"name":"flag: European Union","shortname":":flag_eu:","category":9,"emoji_order":3649},{"name":"flag: Finland","shortname":":flag_fi:","category":9,"emoji_order":3650},{"name":"flag: Fiji","shortname":":flag_fj:","category":9,"emoji_order":3651},{"name":"flag: Falkland Islands","shortname":":flag_fk:","category":9,"emoji_order":3652},{"name":"flag: Micronesia","shortname":":flag_fm:","category":9,"emoji_order":3653},{"name":"flag: Faroe Islands","shortname":":flag_fo:","category":9,"emoji_order":3654},{"name":"flag: France","shortname":":flag_fr:","category":9,"emoji_order":3655},{"name":"flag: Gabon","shortname":":flag_ga:","category":9,"emoji_order":3656},{"name":"flag: United Kingdom","shortname":":flag_gb:","category":9,"emoji_order":3657},{"name":"flag: Grenada","shortname":":flag_gd:","category":9,"emoji_order":3658},{"name":"flag: Georgia","shortname":":flag_ge:","category":9,"emoji_order":3659},{"name":"flag: French Guiana","shortname":":flag_gf:","category":9,"emoji_order":3660},{"name":"flag: Guernsey","shortname":":flag_gg:","category":9,"emoji_order":3661},{"name":"flag: Ghana","shortname":":flag_gh:","category":9,"emoji_order":3662},{"name":"flag: Gibraltar","shortname":":flag_gi:","category":9,"emoji_order":3663},{"name":"flag: Greenland","shortname":":flag_gl:","category":9,"emoji_order":3664},{"name":"flag: Gambia","shortname":":flag_gm:","category":9,"emoji_order":3665},{"name":"flag: Guinea","shortname":":flag_gn:","category":9,"emoji_order":3666},{"name":"flag: Guadeloupe","shortname":":flag_gp:","category":9,"emoji_order":3667},{"name":"flag: Equatorial Guinea","shortname":":flag_gq:","category":9,"emoji_order":3668},{"name":"flag: Greece","shortname":":flag_gr:","category":9,"emoji_order":3669},{"name":"flag: South Georgia & South Sandwich Islands","shortname":":flag_gs:","category":9,"emoji_order":3670},{"name":"flag: Guatemala","shortname":":flag_gt:","category":9,"emoji_order":3671},{"name":"flag: Guam","shortname":":flag_gu:","category":9,"emoji_order":3672},{"name":"flag: Guinea-Bissau","shortname":":flag_gw:","category":9,"emoji_order":3673},{"name":"flag: Guyana","shortname":":flag_gy:","category":9,"emoji_order":3674},{"name":"flag: Hong Kong SAR China","shortname":":flag_hk:","category":9,"emoji_order":3675},{"name":"flag: Heard & McDonald Islands","shortname":":flag_hm:","category":9,"emoji_order":3676},{"name":"flag: Honduras","shortname":":flag_hn:","category":9,"emoji_order":3677},{"name":"flag: Croatia","shortname":":flag_hr:","category":9,"emoji_order":3678},{"name":"flag: Haiti","shortname":":flag_ht:","category":9,"emoji_order":3679},{"name":"flag: Hungary","shortname":":flag_hu:","category":9,"emoji_order":3680},{"name":"flag: Canary Islands","shortname":":flag_ic:","category":9,"emoji_order":3681},{"name":"flag: Indonesia","shortname":":flag_id:","category":9,"emoji_order":3682},{"name":"flag: Ireland","shortname":":flag_ie:","category":9,"emoji_order":3683},{"name":"flag: Israel","shortname":":flag_il:","category":9,"emoji_order":3684},{"name":"flag: Isle of Man","shortname":":flag_im:","category":9,"emoji_order":3685},{"name":"flag: India","shortname":":flag_in:","category":9,"emoji_order":3686},{"name":"flag: British Indian Ocean Territory","shortname":":flag_io:","category":9,"emoji_order":3687},{"name":"flag: Iraq","shortname":":flag_iq:","category":9,"emoji_order":3688},{"name":"flag: Iran","shortname":":flag_ir:","category":9,"emoji_order":3689},{"name":"flag: Iceland","shortname":":flag_is:","category":9,"emoji_order":3690},{"name":"flag: Italy","shortname":":flag_it:","category":9,"emoji_order":3691},{"name":"flag: Jersey","shortname":":flag_je:","category":9,"emoji_order":3692},{"name":"flag: Jamaica","shortname":":flag_jm:","category":9,"emoji_order":3693},{"name":"flag: Jordan","shortname":":flag_jo:","category":9,"emoji_order":3694},{"name":"flag: Japan","shortname":":flag_jp:","category":9,"emoji_order":3695},{"name":"flag: Kenya","shortname":":flag_ke:","category":9,"emoji_order":3696},{"name":"flag: Kyrgyzstan","shortname":":flag_kg:","category":9,"emoji_order":3697},{"name":"flag: Cambodia","shortname":":flag_kh:","category":9,"emoji_order":3698},{"name":"flag: Kiribati","shortname":":flag_ki:","category":9,"emoji_order":3699},{"name":"flag: Comoros","shortname":":flag_km:","category":9,"emoji_order":3700},{"name":"flag: St. Kitts & Nevis","shortname":":flag_kn:","category":9,"emoji_order":3701},{"name":"flag: North Korea","shortname":":flag_kp:","category":9,"emoji_order":3702},{"name":"flag: South Korea","shortname":":flag_kr:","category":9,"emoji_order":3703},{"name":"flag: Kuwait","shortname":":flag_kw:","category":9,"emoji_order":3704},{"name":"flag: Cayman Islands","shortname":":flag_ky:","category":9,"emoji_order":3705},{"name":"flag: Kazakhstan","shortname":":flag_kz:","category":9,"emoji_order":3706},{"name":"flag: Laos","shortname":":flag_la:","category":9,"emoji_order":3707},{"name":"flag: Lebanon","shortname":":flag_lb:","category":9,"emoji_order":3708},{"name":"flag: St. Lucia","shortname":":flag_lc:","category":9,"emoji_order":3709},{"name":"flag: Liechtenstein","shortname":":flag_li:","category":9,"emoji_order":3710},{"name":"flag: Sri Lanka","shortname":":flag_lk:","category":9,"emoji_order":3711},{"name":"flag: Liberia","shortname":":flag_lr:","category":9,"emoji_order":3712},{"name":"flag: Lesotho","shortname":":flag_ls:","category":9,"emoji_order":3713},{"name":"flag: Lithuania","shortname":":flag_lt:","category":9,"emoji_order":3714},{"name":"flag: Luxembourg","shortname":":flag_lu:","category":9,"emoji_order":3715},{"name":"flag: Latvia","shortname":":flag_lv:","category":9,"emoji_order":3716},{"name":"flag: Libya","shortname":":flag_ly:","category":9,"emoji_order":3717},{"name":"flag: Morocco","shortname":":flag_ma:","category":9,"emoji_order":3718},{"name":"flag: Monaco","shortname":":flag_mc:","category":9,"emoji_order":3719},{"name":"flag: Moldova","shortname":":flag_md:","category":9,"emoji_order":3720},{"name":"flag: Montenegro","shortname":":flag_me:","category":9,"emoji_order":3721},{"name":"flag: St. Martin","shortname":":flag_mf:","category":9,"emoji_order":3722},{"name":"flag: Madagascar","shortname":":flag_mg:","category":9,"emoji_order":3723},{"name":"flag: Marshall Islands","shortname":":flag_mh:","category":9,"emoji_order":3724},{"name":"flag: North Macedonia","shortname":":flag_mk:","category":9,"emoji_order":3725},{"name":"flag: Mali","shortname":":flag_ml:","category":9,"emoji_order":3726},{"name":"flag: Myanmar (Burma)","shortname":":flag_mm:","category":9,"emoji_order":3727},{"name":"flag: Mongolia","shortname":":flag_mn:","category":9,"emoji_order":3728},{"name":"flag: Macao SAR China","shortname":":flag_mo:","category":9,"emoji_order":3729},{"name":"flag: Northern Mariana Islands","shortname":":flag_mp:","category":9,"emoji_order":3730},{"name":"flag: Martinique","shortname":":flag_mq:","category":9,"emoji_order":3731},{"name":"flag: Mauritania","shortname":":flag_mr:","category":9,"emoji_order":3732},{"name":"flag: Montserrat","shortname":":flag_ms:","category":9,"emoji_order":3733},{"name":"flag: Malta","shortname":":flag_mt:","category":9,"emoji_order":3734},{"name":"flag: Mauritius","shortname":":flag_mu:","category":9,"emoji_order":3735},{"name":"flag: Maldives","shortname":":flag_mv:","category":9,"emoji_order":3736},{"name":"flag: Malawi","shortname":":flag_mw:","category":9,"emoji_order":3737},{"name":"flag: Mexico","shortname":":flag_mx:","category":9,"emoji_order":3738},{"name":"flag: Malaysia","shortname":":flag_my:","category":9,"emoji_order":3739},{"name":"flag: Mozambique","shortname":":flag_mz:","category":9,"emoji_order":3740},{"name":"flag: Namibia","shortname":":flag_na:","category":9,"emoji_order":3741},{"name":"flag: New Caledonia","shortname":":flag_nc:","category":9,"emoji_order":3742},{"name":"flag: Niger","shortname":":flag_ne:","category":9,"emoji_order":3743},{"name":"flag: Norfolk Island","shortname":":flag_nf:","category":9,"emoji_order":3744},{"name":"flag: Nigeria","shortname":":flag_ng:","category":9,"emoji_order":3745},{"name":"flag: Nicaragua","shortname":":flag_ni:","category":9,"emoji_order":3746},{"name":"flag: Netherlands","shortname":":flag_nl:","category":9,"emoji_order":3747},{"name":"flag: Norway","shortname":":flag_no:","category":9,"emoji_order":3748},{"name":"flag: Nepal","shortname":":flag_np:","category":9,"emoji_order":3749},{"name":"flag: Nauru","shortname":":flag_nr:","category":9,"emoji_order":3750},{"name":"flag: Niue","shortname":":flag_nu:","category":9,"emoji_order":3751},{"name":"flag: New Zealand","shortname":":flag_nz:","category":9,"emoji_order":3752},{"name":"flag: Oman","shortname":":flag_om:","category":9,"emoji_order":3753},{"name":"flag: Panama","shortname":":flag_pa:","category":9,"emoji_order":3754},{"name":"flag: Peru","shortname":":flag_pe:","category":9,"emoji_order":3755},{"name":"flag: French Polynesia","shortname":":flag_pf:","category":9,"emoji_order":3756},{"name":"flag: Papua New Guinea","shortname":":flag_pg:","category":9,"emoji_order":3757},{"name":"flag: Philippines","shortname":":flag_ph:","category":9,"emoji_order":3758},{"name":"flag: Pakistan","shortname":":flag_pk:","category":9,"emoji_order":3759},{"name":"flag: Poland","shortname":":flag_pl:","category":9,"emoji_order":3760},{"name":"flag: St. Pierre & Miquelon","shortname":":flag_pm:","category":9,"emoji_order":3761},{"name":"flag: Pitcairn Islands","shortname":":flag_pn:","category":9,"emoji_order":3762},{"name":"flag: Puerto Rico","shortname":":flag_pr:","category":9,"emoji_order":3763},{"name":"flag: Palestinian Territories","shortname":":flag_ps:","category":9,"emoji_order":3764},{"name":"flag: Portugal","shortname":":flag_pt:","category":9,"emoji_order":3765},{"name":"flag: Palau","shortname":":flag_pw:","category":9,"emoji_order":3766},{"name":"flag: Paraguay","shortname":":flag_py:","category":9,"emoji_order":3767},{"name":"flag: Qatar","shortname":":flag_qa:","category":9,"emoji_order":3768},{"name":"flag: Réunion","shortname":":flag_re:","category":9,"emoji_order":3769},{"name":"flag: Romania","shortname":":flag_ro:","category":9,"emoji_order":3770},{"name":"flag: Serbia","shortname":":flag_rs:","category":9,"emoji_order":3771},{"name":"flag: Russia","shortname":":flag_ru:","category":9,"emoji_order":3772},{"name":"flag: Rwanda","shortname":":flag_rw:","category":9,"emoji_order":3773},{"name":"flag: Saudi Arabia","shortname":":flag_sa:","category":9,"emoji_order":3774},{"name":"flag: Solomon Islands","shortname":":flag_sb:","category":9,"emoji_order":3775},{"name":"flag: Seychelles","shortname":":flag_sc:","category":9,"emoji_order":3776},{"name":"flag: Sudan","shortname":":flag_sd:","category":9,"emoji_order":3777},{"name":"flag: Sweden","shortname":":flag_se:","category":9,"emoji_order":3778},{"name":"flag: Singapore","shortname":":flag_sg:","category":9,"emoji_order":3779},{"name":"flag: St. Helena","shortname":":flag_sh:","category":9,"emoji_order":3780},{"name":"flag: Slovenia","shortname":":flag_si:","category":9,"emoji_order":3781},{"name":"flag: Svalbard & Jan Mayen","shortname":":flag_sj:","category":9,"emoji_order":3782},{"name":"flag: Slovakia","shortname":":flag_sk:","category":9,"emoji_order":3783},{"name":"flag: Sierra Leone","shortname":":flag_sl:","category":9,"emoji_order":3784},{"name":"flag: San Marino","shortname":":flag_sm:","category":9,"emoji_order":3785},{"name":"flag: Senegal","shortname":":flag_sn:","category":9,"emoji_order":3786},{"name":"flag: Somalia","shortname":":flag_so:","category":9,"emoji_order":3787},{"name":"flag: Suriname","shortname":":flag_sr:","category":9,"emoji_order":3788},{"name":"flag: South Sudan","shortname":":flag_ss:","category":9,"emoji_order":3789},{"name":"flag: São Tomé & Príncipe","shortname":":flag_st:","category":9,"emoji_order":3790},{"name":"flag: El Salvador","shortname":":flag_sv:","category":9,"emoji_order":3791},{"name":"flag: Sint Maarten","shortname":":flag_sx:","category":9,"emoji_order":3792},{"name":"flag: Syria","shortname":":flag_sy:","category":9,"emoji_order":3793},{"name":"flag: Eswatini","shortname":":flag_sz:","category":9,"emoji_order":3794},{"name":"flag: Tristan da Cunha","shortname":":flag_ta:","category":9,"emoji_order":3795},{"name":"flag: Turks & Caicos Islands","shortname":":flag_tc:","category":9,"emoji_order":3796},{"name":"flag: Chad","shortname":":flag_td:","category":9,"emoji_order":3797},{"name":"flag: French Southern Territories","shortname":":flag_tf:","category":9,"emoji_order":3798},{"name":"flag: Togo","shortname":":flag_tg:","category":9,"emoji_order":3799},{"name":"flag: Thailand","shortname":":flag_th:","category":9,"emoji_order":3800},{"name":"flag: Tajikistan","shortname":":flag_tj:","category":9,"emoji_order":3801},{"name":"flag: Tokelau","shortname":":flag_tk:","category":9,"emoji_order":3802},{"name":"flag: Timor-Leste","shortname":":flag_tl:","category":9,"emoji_order":3803},{"name":"flag: Turkmenistan","shortname":":flag_tm:","category":9,"emoji_order":3804},{"name":"flag: Tunisia","shortname":":flag_tn:","category":9,"emoji_order":3805},{"name":"flag: Tonga","shortname":":flag_to:","category":9,"emoji_order":3806},{"name":"flag: Turkey","shortname":":flag_tr:","category":9,"emoji_order":3807},{"name":"flag: Trinidad & Tobago","shortname":":flag_tt:","category":9,"emoji_order":3808},{"name":"flag: Tuvalu","shortname":":flag_tv:","category":9,"emoji_order":3809},{"name":"flag: Taiwan","shortname":":flag_tw:","category":9,"emoji_order":3810},{"name":"flag: Tanzania","shortname":":flag_tz:","category":9,"emoji_order":3811},{"name":"flag: Ukraine","shortname":":flag_ua:","category":9,"emoji_order":3812},{"name":"flag: Uganda","shortname":":flag_ug:","category":9,"emoji_order":3813},{"name":"flag: U.S. Outlying Islands","shortname":":flag_um:","category":9,"emoji_order":3814},{"name":"flag: United Nations","shortname":":flag_un:","category":9,"emoji_order":3815},{"name":"flag: United States","shortname":":flag_us:","category":9,"emoji_order":3816,"aliases":[":usa:"]},{"name":"flag: Uruguay","shortname":":flag_uy:","category":9,"emoji_order":3817},{"name":"flag: Uzbekistan","shortname":":flag_uz:","category":9,"emoji_order":3818},{"name":"flag: Vatican City","shortname":":flag_va:","category":9,"emoji_order":3819},{"name":"flag: St. Vincent & Grenadines","shortname":":flag_vc:","category":9,"emoji_order":3820},{"name":"flag: Venezuela","shortname":":flag_ve:","category":9,"emoji_order":3821},{"name":"flag: British Virgin Islands","shortname":":flag_vg:","category":9,"emoji_order":3822},{"name":"flag: U.S. Virgin Islands","shortname":":flag_vi:","category":9,"emoji_order":3823},{"name":"flag: Vietnam","shortname":":flag_vn:","category":9,"emoji_order":3824},{"name":"flag: Vanuatu","shortname":":flag_vu:","category":9,"emoji_order":3825},{"name":"flag: Wallis & Futuna","shortname":":flag_wf:","category":9,"emoji_order":3826},{"name":"flag: Samoa","shortname":":flag_ws:","category":9,"emoji_order":3827},{"name":"flag: Kosovo","shortname":":flag_xk:","category":9,"emoji_order":3828},{"name":"flag: Yemen","shortname":":flag_ye:","category":9,"emoji_order":3829},{"name":"flag: Mayotte","shortname":":flag_yt:","category":9,"emoji_order":3830},{"name":"flag: South Africa","shortname":":flag_za:","category":9,"emoji_order":3831},{"name":"flag: Zambia","shortname":":flag_zm:","category":9,"emoji_order":3832},{"name":"flag: Zimbabwe","shortname":":flag_zw:","category":9,"emoji_order":3833},{"name":"flag: England","shortname":":flag_gbeng:","category":9,"emoji_order":3834,"aliases":[":england:"]},{"name":"flag: Scotland","shortname":":flag_gbsct:","category":9,"emoji_order":3835,"aliases":[":scotland:"]},{"name":"flag: Wales","shortname":":flag_gbwls:","category":9,"emoji_order":3836,"aliases":[":wales:"]}] \ No newline at end of file From a1df87a37579ff948302a1387e2ec160409c9dea Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 19 Dec 2019 07:23:05 +0000 Subject: [PATCH 16/93] Make EmojiPicker an unmanaged Context Menu as it is too complex to be managed --- src/components/structures/ContextMenu.js | 20 +++++++++++-------- .../views/messages/MessageActionBar.js | 2 +- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/src/components/structures/ContextMenu.js b/src/components/structures/ContextMenu.js index e861e3d45f..662972ee37 100644 --- a/src/components/structures/ContextMenu.js +++ b/src/components/structures/ContextMenu.js @@ -71,12 +71,12 @@ export class ContextMenu extends React.Component { // on resize callback windowResize: PropTypes.func, - catchTab: PropTypes.bool, // whether to close the ContextMenu on TAB (default=true) + managed: PropTypes.bool, // whether this context menu should be focus managed. If false it must handle itself }; static defaultProps = { hasBackground: true, - catchTab: true, + managed: true, }; constructor() { @@ -186,15 +186,19 @@ export class ContextMenu extends React.Component { }; _onKeyDown = (ev) => { + if (!this.props.managed) { + if (ev.key === Key.ESCAPE) { + this.props.onFinished(); + ev.stopPropagation(); + ev.preventDefault(); + } + return; + } + let handled = true; switch (ev.key) { case Key.TAB: - if (!this.props.catchTab) { - handled = false; - break; - } - // fallthrough case Key.ESCAPE: this.props.onFinished(); break; @@ -321,7 +325,7 @@ export class ContextMenu extends React.Component { return (
    -
    +
    { chevron } { props.children }
    diff --git a/src/components/views/messages/MessageActionBar.js b/src/components/views/messages/MessageActionBar.js index 81e806cf62..52d7a74632 100644 --- a/src/components/views/messages/MessageActionBar.js +++ b/src/components/views/messages/MessageActionBar.js @@ -88,7 +88,7 @@ const ReactButton = ({mxEvent, reactions, onFocusChange}) => { if (menuDisplayed) { const buttonRect = button.current.getBoundingClientRect(); const ReactionPicker = sdk.getComponent('emojipicker.ReactionPicker'); - contextMenu = + contextMenu = ; } From 6851ad04b6864ceb721b8185f9db083e0503b33b Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 19 Dec 2019 11:26:20 +0000 Subject: [PATCH 17/93] Migrate key backups to SSSS If there's a key backup set up when we bootstrap SSSS, use its key for SSSS and add the key backup key as a passthrough secret. Requires https://github.com/matrix-org/matrix-js-sdk/pull/1128 Fixes https://github.com/vector-im/riot-web/issues/11210 --- .../CreateSecretStorageDialog.js | 64 ++++++++++++++++--- src/i18n/strings/en_EN.json | 2 + 2 files changed, 57 insertions(+), 9 deletions(-) diff --git a/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js b/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js index 78ff2a1698..f47baa9f4e 100644 --- a/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js +++ b/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js @@ -23,13 +23,15 @@ import FileSaver from 'file-saver'; import { _t } from '../../../../languageHandler'; import Modal from '../../../../Modal'; -const PHASE_PASSPHRASE = 0; -const PHASE_PASSPHRASE_CONFIRM = 1; -const PHASE_SHOWKEY = 2; -const PHASE_KEEPITSAFE = 3; -const PHASE_STORING = 4; -const PHASE_DONE = 5; -const PHASE_OPTOUT_CONFIRM = 6; +const PHASE_LOADING = 0; +const PHASE_MIGRATE = 1; +const PHASE_PASSPHRASE = 2; +const PHASE_PASSPHRASE_CONFIRM = 3; +const PHASE_SHOWKEY = 4; +const PHASE_KEEPITSAFE = 5; +const PHASE_STORING = 6; +const PHASE_DONE = 7; +const PHASE_OPTOUT_CONFIRM = 8; const PASSWORD_MIN_SCORE = 4; // So secure, many characters, much complex, wow, etc, etc. const PASSPHRASE_FEEDBACK_DELAY = 500; // How long after keystroke to offer passphrase feedback, ms. @@ -58,7 +60,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent { this._setZxcvbnResultTimeout = null; this.state = { - phase: PHASE_PASSPHRASE, + phase: PHASE_LOADING, passPhrase: '', passPhraseConfirm: '', copied: false, @@ -66,6 +68,8 @@ export default class CreateSecretStorageDialog extends React.PureComponent { zxcvbnResult: null, setPassPhrase: false, }; + + this._fetchBackupInfo(); } componentWillUnmount() { @@ -74,10 +78,23 @@ export default class CreateSecretStorageDialog extends React.PureComponent { } } + async _fetchBackupInfo() { + const backupInfo = await MatrixClientPeg.get().getKeyBackupVersion(); + + this.setState({ + phase: backupInfo ? PHASE_MIGRATE: PHASE_PASSPHRASE, + backupInfo, + }); + } + _collectRecoveryKeyNode = (n) => { this._recoveryKeyNode = n; } + _onMigrateNextClick = () => { + this._bootstrapSecretStorage(); + } + _onCopyClick = () => { selectText(this._recoveryKeyNode); const successful = document.execCommand('copy'); @@ -105,6 +122,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent { this.setState({ phase: PHASE_STORING, error: null, + keyBackupInfo: this.state.backupInfo, }); const cli = MatrixClientPeg.get(); try { @@ -250,6 +268,26 @@ export default class CreateSecretStorageDialog extends React.PureComponent { return this.state.zxcvbnResult && this.state.zxcvbnResult.score >= PASSWORD_MIN_SCORE; } + _renderPhaseMigrate() { + // This is a temporary screen so people who have the labs flag turned on and + // click the button are aware they're making a change to their account. + // Once we're confident enough in this (and it's supported enough) we can do + // it automatically. + const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); + return
    +

    {_t( + "Secret Storage will be set up using your existing key backup details." + + "Your secret storage passphrase and recovery key will be the same as " + + " they were for your key backup", + )}

    + +
    ; + } + _renderPhasePassPhrase() { const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); @@ -449,7 +487,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
    ; } - _renderBusyPhase(text) { + _renderBusyPhase() { const Spinner = sdk.getComponent('views.elements.Spinner'); return
    @@ -488,6 +526,8 @@ export default class CreateSecretStorageDialog extends React.PureComponent { _titleForPhase(phase) { switch (phase) { + case PHASE_MIGRATE: + return _t('Migrate from Key Backup'); case PHASE_PASSPHRASE: return _t('Secure your encrypted messages with a passphrase'); case PHASE_PASSPHRASE_CONFIRM: @@ -525,6 +565,12 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
    ; } else { switch (this.state.phase) { + case PHASE_LOADING: + content = this._renderBusyPhase(); + break; + case PHASE_MIGRATE: + content = this._renderPhaseMigrate(); + break; case PHASE_PASSPHRASE: content = this._renderPhasePassPhrase(); break; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 9bccca14f4..14cfbac987 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1935,6 +1935,7 @@ "The export file will be protected with a passphrase. You should enter the passphrase here, to decrypt the file.": "The export file will be protected with a passphrase. You should enter the passphrase here, to decrypt the file.", "File to import": "File to import", "Import": "Import", + "Secret Storage will be set up using your existing key backup details.Your secret storage passphrase and recovery key will be the same as they were for your key backup": "Secret Storage will be set up using your existing key backup details.Your secret storage passphrase and recovery key will be the same as they were for your key backup", "Great! This passphrase looks strong enough.": "Great! This passphrase looks strong enough.", "Warning: You should only set up secret storage from a trusted computer.": "Warning: You should only set up secret storage from a trusted computer.", "We'll use secret storage to optionally store an encrypted copy of your cross-signing identity for verifying other devices and message keys on our server. Protect your access to encrypted messages with a passphrase to keep it secure.": "We'll use secret storage to optionally store an encrypted copy of your cross-signing identity for verifying other devices and message keys on our server. Protect your access to encrypted messages with a passphrase to keep it secure.", @@ -1961,6 +1962,7 @@ "Your access to encrypted messages is now protected.": "Your access to encrypted messages is now protected.", "Without setting up secret storage, you won't be able to restore your access to encrypted messages or your cross-signing identity for verifying other devices if you log out or use another device.": "Without setting up secret storage, you won't be able to restore your access to encrypted messages or your cross-signing identity for verifying other devices if you log out or use another device.", "Set up secret storage": "Set up secret storage", + "Migrate from Key Backup": "Migrate from Key Backup", "Secure your encrypted messages with a passphrase": "Secure your encrypted messages with a passphrase", "Confirm your passphrase": "Confirm your passphrase", "Recovery key": "Recovery key", From 3678e64f5d10aff9eaad2da93b735411a93ea3a0 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 19 Dec 2019 13:25:43 +0000 Subject: [PATCH 18/93] Internationalise M_TOO_LARGE error from Synapse --- src/components/structures/RoomStatusBar.js | 4 ++-- src/i18n/strings/en_EN.json | 1 + src/utils/ErrorUtils.js | 6 ++++++ 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/components/structures/RoomStatusBar.js b/src/components/structures/RoomStatusBar.js index b0aa4cb59b..574d3b7d1e 100644 --- a/src/components/structures/RoomStatusBar.js +++ b/src/components/structures/RoomStatusBar.js @@ -25,7 +25,7 @@ import MatrixClientPeg from '../../MatrixClientPeg'; import Resend from '../../Resend'; import * as cryptodevices from '../../cryptodevices'; import dis from '../../dispatcher'; -import { messageForResourceLimitError } from '../../utils/ErrorUtils'; +import {messageForResourceLimitError, messageForSendError} from '../../utils/ErrorUtils'; const STATUS_BAR_HIDDEN = 0; const STATUS_BAR_EXPANDED = 1; @@ -272,7 +272,7 @@ module.exports = createReactClass({ unsentMessages[0].error.data && unsentMessages[0].error.data.error ) { - title = unsentMessages[0].error.data.error; + title = messageForSendError(unsentMessages[0].error.data) || unsentMessages[0].error.data.error; } else { title = _t('%(count)s of your messages have not been sent.', { count: unsentMessages.length }); } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 9bccca14f4..1dbd94f64f 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -301,6 +301,7 @@ "No homeserver URL provided": "No homeserver URL provided", "Unexpected error resolving homeserver configuration": "Unexpected error resolving homeserver configuration", "Unexpected error resolving identity server configuration": "Unexpected error resolving identity server configuration", + "The message you are trying to send is too large.": "The message you are trying to send is too large.", "This homeserver has hit its Monthly Active User limit.": "This homeserver has hit its Monthly Active User limit.", "This homeserver has exceeded one of its resource limits.": "This homeserver has exceeded one of its resource limits.", "Please contact your service administrator to continue using the service.": "Please contact your service administrator to continue using the service.", diff --git a/src/utils/ErrorUtils.js b/src/utils/ErrorUtils.js index 4a56d64ef1..51b130bdb6 100644 --- a/src/utils/ErrorUtils.js +++ b/src/utils/ErrorUtils.js @@ -49,6 +49,12 @@ export function messageForResourceLimitError(limitType, adminContact, strings, e } } +export function messageForSendError(errorData) { + if (errorData.errcode === "M_TOO_LARGE") { + return _t("The message you are trying to send is too large."); + } +} + export function messageForSyncError(err) { if (err.errcode === 'M_RESOURCE_LIMIT_EXCEEDED') { const limitError = messageForResourceLimitError( From 6c1944359ece104736f64f1051d525ba9f45ea28 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 19 Dec 2019 14:29:46 +0000 Subject: [PATCH 19/93] Add RoomTile online indicator to DMs --- res/css/views/rooms/_RoomTile.scss | 9 ++++ src/components/views/rooms/RoomTile.js | 17 ++++--- .../views/rooms/RoomTileOnlineDot.js | 48 +++++++++++++++++++ 3 files changed, 68 insertions(+), 6 deletions(-) create mode 100644 src/components/views/rooms/RoomTileOnlineDot.js diff --git a/res/css/views/rooms/_RoomTile.scss b/res/css/views/rooms/_RoomTile.scss index 1814919b61..2b181f366e 100644 --- a/res/css/views/rooms/_RoomTile.scss +++ b/res/css/views/rooms/_RoomTile.scss @@ -1,5 +1,6 @@ /* Copyright 2015, 2016 OpenMarket Ltd +Copyright 2019 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -61,6 +62,14 @@ limitations under the License. min-width: 0; } +.mx_RoomTile_online_dot { + border-radius: 50%; + background-color: $accent-color; + height: 5px; + width: 5px; + display: inline-block; +} + .mx_RoomTile_subtext { display: inline-block; font-size: 11px; diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index 817ada9706..797aa7b87a 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -32,6 +32,7 @@ import ActiveRoomObserver from '../../../ActiveRoomObserver'; import RoomViewStore from '../../../stores/RoomViewStore'; import SettingsStore from "../../../settings/SettingsStore"; import {_t} from "../../../languageHandler"; +import RoomTileOnlineDot from "./RoomTileOnlineDot"; module.exports = createReactClass({ displayName: 'RoomTile', @@ -68,11 +69,6 @@ module.exports = createReactClass({ }); }, - _isDirectMessageRoom: function(roomId) { - const dmRooms = DMRoomMap.shared().getUserIdForRoomId(roomId); - return Boolean(dmRooms); - }, - _shouldShowStatusMessage() { if (!SettingsStore.isFeatureEnabled("feature_custom_status")) { return false; @@ -371,8 +367,11 @@ module.exports = createReactClass({ let ariaLabel = name; + const dmUserId = DMRoomMap.shared().getUserIdForRoomId(this.props.room.roomId); + let dmIndicator; - if (this._isDirectMessageRoom(this.props.room.roomId)) { + let dmOnline; + if (dmUserId) { dmIndicator = dm; + + if (this.props.room.getMember(dmUserId).membership === "join") { + const RoomTileOnlineDot = sdk.getComponent('rooms.RoomTileOnlineDot'); + dmOnline = ; + } } // The following labels are written in such a fashion to increase screen reader efficiency (speed). @@ -428,6 +432,7 @@ module.exports = createReactClass({ { label } { subtextLabel }
    + { dmOnline } { contextMenuButton } { badge }
    diff --git a/src/components/views/rooms/RoomTileOnlineDot.js b/src/components/views/rooms/RoomTileOnlineDot.js new file mode 100644 index 0000000000..a882aec613 --- /dev/null +++ b/src/components/views/rooms/RoomTileOnlineDot.js @@ -0,0 +1,48 @@ +/* +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, {useContext, useEffect, useMemo, useState, useCallback} from "react"; +import PropTypes from "prop-types"; + +import {useEventEmitter} from "../../../hooks/useEventEmitter"; +import MatrixClientContext from "../../../contexts/MatrixClientContext"; + +const RoomTileOnlineDot = ({userId}) => { + const cli = useContext(MatrixClientContext); + const user = useMemo(() => cli.getUser(userId), [cli, userId]); + + const [isOnline, setIsOnline] = useState(false); + + // Recheck if the user or client changes + useEffect(() => { + setIsOnline(user && (user.currentlyActive || user.presence === "online")); + }, [cli, user]); + // Recheck also if we receive a User.currentlyActive event + const currentlyActiveHandler = useCallback((ev) => { + const content = ev.getContent(); + setIsOnline(content.currently_active || content.presence === "online"); + }, []); + useEventEmitter(user, "User.currentlyActive", currentlyActiveHandler); + useEventEmitter(user, "User.presence", currentlyActiveHandler); + + return isOnline ? : null; +}; + +RoomTileOnlineDot.propTypes = { + userId: PropTypes.string.isRequired, +}; + +export default RoomTileOnlineDot; From db069b960207f3c8af5b6320a0bc5709b06354cb Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 19 Dec 2019 14:33:46 +0000 Subject: [PATCH 20/93] delint --- src/components/views/rooms/RoomTile.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index 797aa7b87a..cd5af61862 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -32,7 +32,6 @@ import ActiveRoomObserver from '../../../ActiveRoomObserver'; import RoomViewStore from '../../../stores/RoomViewStore'; import SettingsStore from "../../../settings/SettingsStore"; import {_t} from "../../../languageHandler"; -import RoomTileOnlineDot from "./RoomTileOnlineDot"; module.exports = createReactClass({ displayName: 'RoomTile', From 3d8422c88afe85a59eb9759a8bfb25bafb5ffd22 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 19 Dec 2019 15:23:32 +0000 Subject: [PATCH 21/93] Combine cross signing and verification over DM feature flags This means we can just make the new member info panel support cross-signing exclusively rather than having to try & make it temporarily support both --- src/components/structures/MatrixChat.js | 2 +- src/components/structures/RightPanel.js | 4 ++-- src/components/views/dialogs/DeviceVerifyDialog.js | 2 +- src/settings/Settings.js | 7 ------- 4 files changed, 4 insertions(+), 11 deletions(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 82a682f9ab..8cee3e9def 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -1466,7 +1466,7 @@ export default createReactClass({ } }); - if (SettingsStore.isFeatureEnabled("feature_dm_verification")) { + if (SettingsStore.isFeatureEnabled("feature_cross_signing")) { cli.on("crypto.verification.request", request => { let requestObserver; if (request.event.getRoomId()) { diff --git a/src/components/structures/RightPanel.js b/src/components/structures/RightPanel.js index 1745c9d7dc..2f91c5419d 100644 --- a/src/components/structures/RightPanel.js +++ b/src/components/structures/RightPanel.js @@ -190,7 +190,7 @@ export default class RightPanel extends React.Component { } else if (this.state.phase === RIGHT_PANEL_PHASES.GroupRoomList) { panel = ; } else if (this.state.phase === RIGHT_PANEL_PHASES.RoomMemberInfo) { - if (SettingsStore.isFeatureEnabled("feature_dm_verification")) { + if (SettingsStore.isFeatureEnabled("feature_cross_signing")) { const onClose = () => { dis.dispatch({ action: "view_user", @@ -209,7 +209,7 @@ export default class RightPanel extends React.Component { } else if (this.state.phase === RIGHT_PANEL_PHASES.Room3pidMemberInfo) { panel = ; } else if (this.state.phase === RIGHT_PANEL_PHASES.GroupMemberInfo) { - if (SettingsStore.isFeatureEnabled("feature_dm_verification")) { + if (SettingsStore.isFeatureEnabled("feature_cross_signing")) { const onClose = () => { dis.dispatch({ action: "view_user", diff --git a/src/components/views/dialogs/DeviceVerifyDialog.js b/src/components/views/dialogs/DeviceVerifyDialog.js index 0e191cc192..6408245452 100644 --- a/src/components/views/dialogs/DeviceVerifyDialog.js +++ b/src/components/views/dialogs/DeviceVerifyDialog.js @@ -97,7 +97,7 @@ export default class DeviceVerifyDialog extends React.Component { const client = MatrixClientPeg.get(); const verifyingOwnDevice = this.props.userId === client.getUserId(); try { - if (!verifyingOwnDevice && SettingsStore.getValue("feature_dm_verification")) { + if (!verifyingOwnDevice && SettingsStore.getValue("feature_cross_signing")) { const roomId = await ensureDMExistsAndOpen(this.props.userId); // throws upon cancellation before having started this._verifier = await client.requestVerificationDM( diff --git a/src/settings/Settings.js b/src/settings/Settings.js index 82dd639819..f1299a9045 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -136,13 +136,6 @@ export const SETTINGS = { supportedLevels: ['account'], default: null, }, - "feature_dm_verification": { - isFeature: true, - displayName: _td("Send verification requests in direct message," + - " including a new verification UX in the member panel."), - supportedLevels: LEVELS_FEATURE, - default: false, - }, "feature_cross_signing": { isFeature: true, displayName: _td("Enable cross-signing to verify per-user instead of per-device (in development)"), From 26980e2ad542bcf290640bee3af50d89b585b303 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 19 Dec 2019 15:26:04 +0000 Subject: [PATCH 22/93] apply unhomoglyph when filtering room list to fuzzify it --- src/components/views/rooms/RoomList.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index 80a03e7a73..fb1643b8dd 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -22,6 +22,7 @@ import React from "react"; import ReactDOM from "react-dom"; import createReactClass from 'create-react-class'; import PropTypes from 'prop-types'; +import utils from "matrix-js-sdk/lib/utils"; import { _t } from '../../../languageHandler'; const MatrixClientPeg = require("../../../MatrixClientPeg"); const CallHandler = require('../../../CallHandler'); @@ -588,11 +589,17 @@ module.exports = createReactClass({ _applySearchFilter: function(list, filter) { if (filter === "") return list; + const fuzzyFilter = utils.removeHiddenChars(filter).toLowerCase(); const lcFilter = filter.toLowerCase(); // case insensitive if room name includes filter, // or if starts with `#` and one of room's aliases starts with filter - return list.filter((room) => (room.name && room.name.toLowerCase().includes(lcFilter)) || - (filter[0] === '#' && room.getAliases().some((alias) => alias.toLowerCase().startsWith(lcFilter)))); + return list.filter((room) => { + if (filter[0] === "#" && room.getAliases().some((alias) => alias.toLowerCase().startsWith(lcFilter))) { + return true; + } + const lcRoomName = room.name ? utils.removeHiddenChars(room.name).toLowerCase() : ""; + return lcRoomName.includes(fuzzyFilter); + }); }, _handleCollapsedState: function(key, collapsed) { From c5e7594fe9869ab89bf3a9d3444d7418798dbe02 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 19 Dec 2019 15:27:29 +0000 Subject: [PATCH 23/93] i18n --- src/i18n/strings/en_EN.json | 1 - 1 file changed, 1 deletion(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 9bccca14f4..c959be6263 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -357,7 +357,6 @@ "Render simple counters in room header": "Render simple counters in room header", "Multiple integration managers": "Multiple integration managers", "Try out new ways to ignore people (experimental)": "Try out new ways to ignore people (experimental)", - "Send verification requests in direct message, including a new verification UX in the member panel.": "Send verification requests in direct message, including a new verification UX in the member panel.", "Enable cross-signing to verify per-user instead of per-device (in development)": "Enable cross-signing to verify per-user instead of per-device (in development)", "Enable local event indexing and E2EE search (requires restart)": "Enable local event indexing and E2EE search (requires restart)", "Use the new, faster, composer for writing messages": "Use the new, faster, composer for writing messages", From e54429680be928e27815bf11761ea68950147b0b Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 19 Dec 2019 15:28:11 +0000 Subject: [PATCH 24/93] tidy up --- src/components/views/rooms/RoomList.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index fb1643b8dd..152ce5105e 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -597,8 +597,7 @@ module.exports = createReactClass({ if (filter[0] === "#" && room.getAliases().some((alias) => alias.toLowerCase().startsWith(lcFilter))) { return true; } - const lcRoomName = room.name ? utils.removeHiddenChars(room.name).toLowerCase() : ""; - return lcRoomName.includes(fuzzyFilter); + return room.name ? utils.removeHiddenChars(room.name).toLowerCase().includes(fuzzyFilter) : false; }); }, From 1c31fd34130e4aa8e0a224f36863efe3ae744e74 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 19 Dec 2019 16:54:30 +0000 Subject: [PATCH 25/93] Apply new design to highlighted tags and add toggle mechanic --- res/css/structures/_TagPanel.scss | 24 +++++++++--------------- src/stores/TagOrderStore.js | 9 +++++++-- 2 files changed, 16 insertions(+), 17 deletions(-) diff --git a/res/css/structures/_TagPanel.scss b/res/css/structures/_TagPanel.scss index b03d36a592..8efd0796f5 100644 --- a/res/css/structures/_TagPanel.scss +++ b/res/css/structures/_TagPanel.scss @@ -68,7 +68,7 @@ limitations under the License. } .mx_TagPanel .mx_TagPanel_tagTileContainer > div { height: 40px; - padding: 5px 0 4px 0; + padding: 10px 0 9px 0; } .mx_TagPanel .mx_TagTile { @@ -82,21 +82,15 @@ limitations under the License. // opacity: 1; } -.mx_TagPanel .mx_TagTile.mx_TagTile_selected .mx_TagTile_avatar .mx_BaseAvatar { +.mx_TagPanel .mx_TagTile.mx_TagTile_selected::before { + content: ''; + height: 56px; background-color: $accent-color; - border-radius: 40px; - - /* In case this is a "initial" avatar */ - display: block; - height: 40px; - width: 40px; -} - -.mx_TagPanel .mx_TagTile_selected .mx_BaseAvatar_image { - border: 3px solid $accent-color; - height: 40px; - width: 40px; - box-sizing: border-box; + width: 5px; + position: absolute; + left: -15px; + border-radius: 0 2px 2px 0; + top: -8px; // (56 - 40)/2 } .mx_TagPanel .mx_TagTile.mx_AccessibleButton:focus { diff --git a/src/stores/TagOrderStore.js b/src/stores/TagOrderStore.js index 48a8817270..973d27f4e7 100644 --- a/src/stores/TagOrderStore.js +++ b/src/stores/TagOrderStore.js @@ -141,8 +141,13 @@ class TagOrderStore extends Store { newTags = [...this._state.selectedTags, payload.tag]; } } else { - // Select individual tag - newTags = [payload.tag]; + if (this._state.selectedTags.length === 1 && this._state.selectedTags.includes(payload.tag)) { + // Existing (only) selected tag is being normally clicked again, clear tags + newTags = []; + } else { + // Select individual tag + newTags = [payload.tag]; + } } // Only set the anchor tag if the tag was previously unselected, otherwise // the next range starts with an unselected tag. From a25bc9434390de9ac8a076179fc518ab8e8a4f01 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 19 Dec 2019 17:22:02 +0000 Subject: [PATCH 26/93] remove unused, commented line of code --- src/autocomplete/EmojiProvider.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/autocomplete/EmojiProvider.js b/src/autocomplete/EmojiProvider.js index 7e30c8ca6a..9373ed662e 100644 --- a/src/autocomplete/EmojiProvider.js +++ b/src/autocomplete/EmojiProvider.js @@ -68,7 +68,6 @@ export default class EmojiProvider extends AutocompleteProvider { this.matcher = new QueryMatcher(EMOJI_SHORTNAMES, { keys: ['emoji.emoticon', 'shortname'], funcs: [ - // (o) => `:${o.emoji.shortcodes[0]}:`, // shortname (o) => o.emoji.shortcodes.length > 1 ? o.emoji.shortcodes.slice(1).map(s => `:${s}:`).join(" ") : "", // aliases ], // For matching against ascii equivalents From 48b166f4511e8e99475295485b091fa65e23f829 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 19 Dec 2019 17:28:32 +0000 Subject: [PATCH 27/93] Pass the key backup into the right thing --- .../views/dialogs/secretstorage/CreateSecretStorageDialog.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js b/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js index f47baa9f4e..f8f782057b 100644 --- a/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js +++ b/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js @@ -122,7 +122,6 @@ export default class CreateSecretStorageDialog extends React.PureComponent { this.setState({ phase: PHASE_STORING, error: null, - keyBackupInfo: this.state.backupInfo, }); const cli = MatrixClientPeg.get(); try { @@ -143,6 +142,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent { } }, createSecretStorageKey: async () => this._keyInfo, + keyBackupInfo: this.state.backupInfo, }); this.setState({ phase: PHASE_DONE, From 822762f01434cfb5dabf92d36e6eef575dd954f8 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 19 Dec 2019 19:49:36 +0000 Subject: [PATCH 28/93] add todo --- .../views/dialogs/secretstorage/CreateSecretStorageDialog.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js b/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js index f8f782057b..289cb3265b 100644 --- a/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js +++ b/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js @@ -269,7 +269,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent { } _renderPhaseMigrate() { - // This is a temporary screen so people who have the labs flag turned on and + // TODO: This is a temporary screen so people who have the labs flag turned on and // click the button are aware they're making a change to their account. // Once we're confident enough in this (and it's supported enough) we can do // it automatically. From b98058fc3c4b30db3795dc555f55a50ff55512bd Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 19 Dec 2019 19:54:44 +0000 Subject: [PATCH 29/93] Add bug for removing temporary srceen --- .../views/dialogs/secretstorage/CreateSecretStorageDialog.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js b/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js index 289cb3265b..25bc8cdfda 100644 --- a/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js +++ b/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js @@ -273,6 +273,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent { // click the button are aware they're making a change to their account. // Once we're confident enough in this (and it's supported enough) we can do // it automatically. + // https://github.com/vector-im/riot-web/issues/11696 const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); return

    {_t( From b2249d056117cb0c3f1891f476c0547ee5fcbfc3 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 19 Dec 2019 20:09:05 +0000 Subject: [PATCH 30/93] Initial rejig --- res/css/_components.scss | 1 - res/css/structures/_TagPanel.scss | 27 ++++++++- res/css/structures/_TagPanelButtons.scss | 56 ------------------ res/css/views/context_menus/_TopLeftMenu.scss | 4 ++ res/img/feather-customised/plus.svg | 4 ++ src/components/structures/LeftPanel.js | 1 - src/components/structures/TagPanel.js | 2 + src/components/structures/TagPanelButtons.js | 59 ------------------- .../views/context_menus/TopLeftMenu.js | 13 ++++ .../views/elements/AccessibleTooltipButton.js | 3 +- src/components/views/elements/GroupsButton.js | 37 ------------ src/components/views/elements/TagTile.js | 18 +----- src/i18n/strings/en_EN.json | 3 +- 13 files changed, 56 insertions(+), 172 deletions(-) delete mode 100644 res/css/structures/_TagPanelButtons.scss create mode 100644 res/img/feather-customised/plus.svg delete mode 100644 src/components/structures/TagPanelButtons.js delete mode 100644 src/components/views/elements/GroupsButton.js diff --git a/res/css/_components.scss b/res/css/_components.scss index 233c781d7f..20395550ab 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -24,7 +24,6 @@ @import "./structures/_SearchBox.scss"; @import "./structures/_TabbedView.scss"; @import "./structures/_TagPanel.scss"; -@import "./structures/_TagPanelButtons.scss"; @import "./structures/_ToastContainer.scss"; @import "./structures/_TopLeftMenuButton.scss"; @import "./structures/_UploadBar.scss"; diff --git a/res/css/structures/_TagPanel.scss b/res/css/structures/_TagPanel.scss index 8efd0796f5..307925335f 100644 --- a/res/css/structures/_TagPanel.scss +++ b/res/css/structures/_TagPanel.scss @@ -82,6 +82,31 @@ limitations under the License. // opacity: 1; } +.mx_TagPanel .mx_TagTile_plus { + margin-bottom: 12px; + height: 40px; + width: 40px; + border-radius: 20px; + background-color: $button-primary-fg-color; + opacity: 0.11; + position: relative; + /* overwrite mx_RoleButton inline-block */ + display: block !important; + + &::before { + mask-image: url('$(res)/img/feather-customised/plus.svg'); + mask-position: center; + background-color: $tagpanel-bg-color; + mask-repeat: no-repeat; + content: ''; + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + } +} + .mx_TagPanel .mx_TagTile.mx_TagTile_selected::before { content: ''; height: 56px; @@ -89,7 +114,7 @@ limitations under the License. width: 5px; position: absolute; left: -15px; - border-radius: 0 2px 2px 0; + border-radius: 0 3px 3px 0; top: -8px; // (56 - 40)/2 } diff --git a/res/css/structures/_TagPanelButtons.scss b/res/css/structures/_TagPanelButtons.scss deleted file mode 100644 index 70fea92959..0000000000 --- a/res/css/structures/_TagPanelButtons.scss +++ /dev/null @@ -1,56 +0,0 @@ -/* -Copyright 2019 New Vector Ltd. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -.mx_TagPanelButtons { - background-color: $tagpanel-bg-color; - display: flex; - flex-direction: column; - align-items: center; - justify-content: space-between; - padding: 17px 0 3px 0; -} - -.mx_TagPanelButtons > .mx_GroupsButton::before { - mask: url('$(res)/img/feather-customised/users.svg'); - mask-position: center 11px; -} - -.mx_TagPanelButtons > .mx_TagPanelButtons_report::before { - mask: url('$(res)/img/feather-customised/life-buoy.svg'); - mask-position: center 9px; -} - -.mx_TagPanelButtons > .mx_AccessibleButton { - margin-bottom: 12px; - height: 40px; - width: 40px; - border-radius: 20px; - background-color: $tagpanel-button-color; - position: relative; - /* overwrite mx_RoleButton inline-block */ - display: block !important; - - &::before { - background-color: $tagpanel-bg-color; - mask-repeat: no-repeat; - content: ''; - position: absolute; - top: 0; - bottom: 0; - left: 0; - right: 0; - } -} diff --git a/res/css/views/context_menus/_TopLeftMenu.scss b/res/css/views/context_menus/_TopLeftMenu.scss index d17d683e7e..ed0d0106bc 100644 --- a/res/css/views/context_menus/_TopLeftMenu.scss +++ b/res/css/views/context_menus/_TopLeftMenu.scss @@ -53,6 +53,10 @@ limitations under the License. mask-image: url('$(res)/img/feather-customised/home.svg'); } + .mx_TopLeftMenu_icon_help::after { + mask-image: url('$(res)/img/feather-customised/life-buoy.svg'); + } + .mx_TopLeftMenu_icon_settings::after { mask-image: url('$(res)/img/feather-customised/settings.svg'); } diff --git a/res/img/feather-customised/plus.svg b/res/img/feather-customised/plus.svg new file mode 100644 index 0000000000..c747253139 --- /dev/null +++ b/res/img/feather-customised/plus.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/components/structures/LeftPanel.js b/src/components/structures/LeftPanel.js index a0ad2b5c81..f733888db9 100644 --- a/src/components/structures/LeftPanel.js +++ b/src/components/structures/LeftPanel.js @@ -243,7 +243,6 @@ const LeftPanel = createReactClass({ tagPanelContainer = (

    { isCustomTagsEnabled ? : undefined } -
    ); } diff --git a/src/components/structures/TagPanel.js b/src/components/structures/TagPanel.js index a758092dc8..6410af174e 100644 --- a/src/components/structures/TagPanel.js +++ b/src/components/structures/TagPanel.js @@ -104,6 +104,7 @@ const TagPanel = createReactClass({ render() { const DNDTagTile = sdk.getComponent('elements.DNDTagTile'); const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); + const AccessibleTooltipButton = sdk.getComponent('elements.AccessibleTooltipButton'); const TintableSvg = sdk.getComponent('elements.TintableSvg'); const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper"); @@ -154,6 +155,7 @@ const TagPanel = createReactClass({ ref={provided.innerRef} > { tags } +
    { provided.placeholder }
    ) } diff --git a/src/components/structures/TagPanelButtons.js b/src/components/structures/TagPanelButtons.js deleted file mode 100644 index 7255e12307..0000000000 --- a/src/components/structures/TagPanelButtons.js +++ /dev/null @@ -1,59 +0,0 @@ -/* -Copyright 2019 New Vector Ltd. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import React from 'react'; -import createReactClass from 'create-react-class'; -import sdk from '../../index'; -import dis from '../../dispatcher'; -import Modal from '../../Modal'; -import { _t } from '../../languageHandler'; - -const TagPanelButtons = createReactClass({ - displayName: 'TagPanelButtons', - - - componentDidMount: function() { - this._dispatcherRef = dis.register(this._onAction); - }, - - componentWillUnmount() { - if (this._dispatcherRef) { - dis.unregister(this._dispatcherRef); - this._dispatcherRef = null; - } - }, - - _onAction(payload) { - if (payload.action === "show_redesign_feedback_dialog") { - const RedesignFeedbackDialog = - sdk.getComponent("views.dialogs.RedesignFeedbackDialog"); - Modal.createTrackedDialog('Report bugs & give feedback', '', RedesignFeedbackDialog); - } - }, - - render() { - const GroupsButton = sdk.getComponent('elements.GroupsButton'); - const ActionButton = sdk.getComponent("elements.ActionButton"); - - return (
    - - -
    ); - }, -}); -export default TagPanelButtons; diff --git a/src/components/views/context_menus/TopLeftMenu.js b/src/components/views/context_menus/TopLeftMenu.js index b9aabdc608..db388a657d 100644 --- a/src/components/views/context_menus/TopLeftMenu.js +++ b/src/components/views/context_menus/TopLeftMenu.js @@ -25,6 +25,7 @@ import SdkConfig from '../../../SdkConfig'; import { getHostingLink } from '../../../utils/HostingLink'; import MatrixClientPeg from '../../../MatrixClientPeg'; import {MenuItem} from "../../structures/ContextMenu"; +import sdk from "../../../index"; export class TopLeftMenu extends React.Component { static propTypes = { @@ -100,6 +101,12 @@ export class TopLeftMenu extends React.Component { ); } + const helpItem = ( + + {_t("Help")} + + ); + const settingsItem = ( {_t("Settings")} @@ -115,11 +122,17 @@ export class TopLeftMenu extends React.Component {
      {homePageItem} {settingsItem} + {helpItem} {signInOutItem}
    ; } + openHelp() { + const RedesignFeedbackDialog = sdk.getComponent("views.dialogs.RedesignFeedbackDialog"); + Modal.createTrackedDialog('Report bugs & give feedback', '', RedesignFeedbackDialog); + } + viewHomePage() { dis.dispatch({action: 'view_home_page'}); this.closeMenu(); diff --git a/src/components/views/elements/AccessibleTooltipButton.js b/src/components/views/elements/AccessibleTooltipButton.js index c824ea4025..4f2b1f1a96 100644 --- a/src/components/views/elements/AccessibleTooltipButton.js +++ b/src/components/views/elements/AccessibleTooltipButton.js @@ -48,7 +48,7 @@ export default class AccessibleTooltipButton extends React.PureComponent { const Tooltip = sdk.getComponent("elements.Tooltip"); const AccessibleButton = sdk.getComponent("elements.AccessibleButton"); - const {title, ...props} = this.props; + const {title, children, ...props} = this.props; const tip = this.state.hover ? :
    ; return ( + { children } { tip } ); diff --git a/src/components/views/elements/GroupsButton.js b/src/components/views/elements/GroupsButton.js deleted file mode 100644 index 7b15e96424..0000000000 --- a/src/components/views/elements/GroupsButton.js +++ /dev/null @@ -1,37 +0,0 @@ -/* -Copyright 2017 New Vector Ltd - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import React from 'react'; -import sdk from '../../../index'; -import PropTypes from 'prop-types'; -import { _t } from '../../../languageHandler'; - -const GroupsButton = function(props) { - const ActionButton = sdk.getComponent('elements.ActionButton'); - return ( - - ); -}; - -GroupsButton.propTypes = { - size: PropTypes.string, -}; - -export default GroupsButton; diff --git a/src/components/views/elements/TagTile.js b/src/components/views/elements/TagTile.js index 767980f0a0..f3dbc5dd21 100644 --- a/src/components/views/elements/TagTile.js +++ b/src/components/views/elements/TagTile.js @@ -112,12 +112,10 @@ export default createReactClass({ }, onMouseOver: function() { - console.log("DEBUG onMouseOver"); this.setState({hover: true}); }, onMouseOut: function() { - console.log("DEBUG onMouseOut"); this.setState({hover: false}); }, @@ -140,7 +138,6 @@ export default createReactClass({ render: function() { const BaseAvatar = sdk.getComponent('avatars.BaseAvatar'); - const Tooltip = sdk.getComponent('elements.Tooltip'); const profile = this.state.profile || {}; const name = profile.name || this.props.tag; const avatarHeight = 40; @@ -164,9 +161,6 @@ export default createReactClass({ badgeElement = (
    {FormattingUtils.formatCount(badge.count)}
    ); } - const tip = this.state.hover ? - : -
    ; // FIXME: this ought to use AccessibleButton for a11y but that causes onMouseOut/onMouseOver to fire too much const contextButton = this.state.hover || this.state.menuDisplayed ?
    @@ -184,14 +178,9 @@ export default createReactClass({ ); } + const AccessibleTooltipButton = sdk.getComponent("elements.AccessibleTooltipButton"); return - +
    - { tip } { contextButton } { badgeElement }
    -
    + { contextMenu }
    ; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 1dbd94f64f..57479bc70e 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1259,7 +1259,6 @@ "Please create a new issue on GitHub so that we can investigate this bug.": "Please create a new issue on GitHub so that we can investigate this bug.", "collapse": "collapse", "expand": "expand", - "Communities": "Communities", "You cannot delete this image. (%(code)s)": "You cannot delete this image. (%(code)s)", "Uploaded on %(date)s by %(user)s": "Uploaded on %(date)s by %(user)s", "Rotate Left": "Rotate Left", @@ -1625,6 +1624,7 @@ "Hide": "Hide", "Home": "Home", "Sign in": "Sign in", + "Help": "Help", "Reload": "Reload", "Take picture": "Take picture", "Remove for everyone": "Remove for everyone", @@ -1768,6 +1768,7 @@ "Did you know: you can use communities to filter your Riot.im experience!": "Did you know: you can use communities to filter your Riot.im experience!", "To set up a filter, drag a community avatar over to the filter panel on the far left hand side of the screen. You can click on an avatar in the filter panel at any time to see only the rooms and people associated with that community.": "To set up a filter, drag a community avatar over to the filter panel on the far left hand side of the screen. You can click on an avatar in the filter panel at any time to see only the rooms and people associated with that community.", "Error whilst fetching joined communities": "Error whilst fetching joined communities", + "Communities": "Communities", "Create a new community": "Create a new community", "Create a community to group together users and rooms! Build a custom homepage to mark out your space in the Matrix universe.": "Create a community to group together users and rooms! Build a custom homepage to mark out your space in the Matrix universe.", "You have no visible notifications": "You have no visible notifications", From d9ea9b4ad3c1b59e1872e8123974b839ce6a04fd Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 19 Dec 2019 20:53:34 +0000 Subject: [PATCH 31/93] Iterate to match design on Zeplin --- res/css/structures/_CustomRoomTagPanel.scss | 16 +++++++-- res/css/structures/_TagPanel.scss | 4 +-- .../structures/CustomRoomTagPanel.js | 33 ++++--------------- src/components/structures/TagPanel.js | 10 ++++-- .../views/context_menus/TopLeftMenu.js | 1 + 5 files changed, 30 insertions(+), 34 deletions(-) diff --git a/res/css/structures/_CustomRoomTagPanel.scss b/res/css/structures/_CustomRoomTagPanel.scss index 45961d7be1..278c7c3d0c 100644 --- a/res/css/structures/_CustomRoomTagPanel.scss +++ b/res/css/structures/_CustomRoomTagPanel.scss @@ -26,11 +26,15 @@ limitations under the License. .mx_CustomRoomTagPanel_scroller { max-height: inherit; + display: flex; + flex-direction: column; + align-items: center; } .mx_CustomRoomTagPanel .mx_AccessibleButton { margin: 9px auto; width: 40px; + position: relative; } .mx_CustomRoomTagPanel .mx_BaseAvatar_image { @@ -39,7 +43,13 @@ limitations under the License. height: 40px; } -.mx_CustomRoomTagPanel .mx_AccessibleButton.CustomRoomTagPanel_tileSelected .mx_BaseAvatar_image { - border: 3px solid $warning-color; - border-radius: 40px; +.mx_CustomRoomTagPanel .mx_AccessibleButton.CustomRoomTagPanel_tileSelected::before { + content: ''; + height: 56px; + background-color: $accent-color-alt; + width: 5px; + position: absolute; + left: -15px; + border-radius: 0 3px 3px 0; + top: -8px; // (56 - 40)/2 } diff --git a/res/css/structures/_TagPanel.scss b/res/css/structures/_TagPanel.scss index 307925335f..e2eda433ba 100644 --- a/res/css/structures/_TagPanel.scss +++ b/res/css/structures/_TagPanel.scss @@ -87,7 +87,7 @@ limitations under the License. height: 40px; width: 40px; border-radius: 20px; - background-color: $button-primary-fg-color; + background-color: $tagpanel-button-color; opacity: 0.11; position: relative; /* overwrite mx_RoleButton inline-block */ @@ -96,7 +96,7 @@ limitations under the License. &::before { mask-image: url('$(res)/img/feather-customised/plus.svg'); mask-position: center; - background-color: $tagpanel-bg-color; + background-color: $button-primary-fg-color; mask-repeat: no-repeat; content: ''; position: absolute; diff --git a/src/components/structures/CustomRoomTagPanel.js b/src/components/structures/CustomRoomTagPanel.js index ee69d800ed..f1b548d72f 100644 --- a/src/components/structures/CustomRoomTagPanel.js +++ b/src/components/structures/CustomRoomTagPanel.js @@ -61,30 +61,13 @@ class CustomRoomTagPanel extends React.Component { } class CustomRoomTagTile extends React.Component { - constructor(props) { - super(props); - this.state = {hover: false}; - this.onClick = this.onClick.bind(this); - this.onMouseOut = this.onMouseOut.bind(this); - this.onMouseOver = this.onMouseOver.bind(this); - } - - onMouseOver() { - this.setState({hover: true}); - } - - onMouseOut() { - this.setState({hover: false}); - } - - onClick() { + onClick = () => { dis.dispatch({action: 'select_custom_room_tag', tag: this.props.tag.name}); - } + }; render() { const BaseAvatar = sdk.getComponent('avatars.BaseAvatar'); - const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); - const Tooltip = sdk.getComponent('elements.Tooltip'); + const AccessibleTooltipButton = sdk.getComponent('elements.AccessibleTooltipButton'); const tag = this.props.tag; const avatarHeight = 40; @@ -102,12 +85,9 @@ class CustomRoomTagTile extends React.Component { badgeElement = (
    {FormattingUtils.formatCount(badge.count)}
    ); } - const tip = (this.state.hover ? - : -
    ); return ( - -
    + +
    { badgeElement } - { tip }
    - +
    ); } } diff --git a/src/components/structures/TagPanel.js b/src/components/structures/TagPanel.js index 6410af174e..0e05dbd785 100644 --- a/src/components/structures/TagPanel.js +++ b/src/components/structures/TagPanel.js @@ -104,7 +104,7 @@ const TagPanel = createReactClass({ render() { const DNDTagTile = sdk.getComponent('elements.DNDTagTile'); const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); - const AccessibleTooltipButton = sdk.getComponent('elements.AccessibleTooltipButton'); + const ActionButton = sdk.getComponent('elements.ActionButton'); const TintableSvg = sdk.getComponent('elements.TintableSvg'); const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper"); @@ -155,7 +155,13 @@ const TagPanel = createReactClass({ ref={provided.innerRef} > { tags } -
    +
    + +
    { provided.placeholder }
    ) } diff --git a/src/components/views/context_menus/TopLeftMenu.js b/src/components/views/context_menus/TopLeftMenu.js index db388a657d..7a7b124919 100644 --- a/src/components/views/context_menus/TopLeftMenu.js +++ b/src/components/views/context_menus/TopLeftMenu.js @@ -129,6 +129,7 @@ export class TopLeftMenu extends React.Component { } openHelp() { + this.closeMenu(); const RedesignFeedbackDialog = sdk.getComponent("views.dialogs.RedesignFeedbackDialog"); Modal.createTrackedDialog('Report bugs & give feedback', '', RedesignFeedbackDialog); } From 3a36d61fab829640cb1aed82230d1565cab29c10 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 19 Dec 2019 21:10:22 +0000 Subject: [PATCH 32/93] delint --- src/components/structures/LeftPanel.js | 1 - src/components/views/elements/TagTile.js | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/components/structures/LeftPanel.js b/src/components/structures/LeftPanel.js index f733888db9..57ba58d6fe 100644 --- a/src/components/structures/LeftPanel.js +++ b/src/components/structures/LeftPanel.js @@ -24,7 +24,6 @@ import { Key } from '../../Keyboard'; import sdk from '../../index'; import dis from '../../dispatcher'; import VectorConferenceHandler from '../../VectorConferenceHandler'; -import TagPanelButtons from './TagPanelButtons'; import SettingsStore from '../../settings/SettingsStore'; import {_t} from "../../languageHandler"; import Analytics from "../../Analytics"; diff --git a/src/components/views/elements/TagTile.js b/src/components/views/elements/TagTile.js index f3dbc5dd21..5464d69609 100644 --- a/src/components/views/elements/TagTile.js +++ b/src/components/views/elements/TagTile.js @@ -23,14 +23,13 @@ import classNames from 'classnames'; import { MatrixClient } from 'matrix-js-sdk'; import sdk from '../../../index'; import dis from '../../../dispatcher'; -import {_t} from '../../../languageHandler'; import { isOnlyCtrlOrCmdIgnoreShiftKeyEvent } from '../../../Keyboard'; import * as FormattingUtils from '../../../utils/FormattingUtils'; import FlairStore from '../../../stores/FlairStore'; import GroupStore from '../../../stores/GroupStore'; import TagOrderStore from '../../../stores/TagOrderStore'; -import {ContextMenu, ContextMenuButton, toRightOf} from "../../structures/ContextMenu"; +import {ContextMenu, toRightOf} from "../../structures/ContextMenu"; // A class for a child of TagPanel (possibly wrapped in a DNDTagTile) that represents // a thing to click on for the user to filter the visible rooms in the RoomList to: From b998e6ffe88df4936d8c172fdf70d50b8315cff3 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 20 Dec 2019 11:31:11 +0000 Subject: [PATCH 33/93] Switch to using checkDeviceTrust In the UserInfo panel. This means we now use cross-signing verifications in the UserInfoPanel so we can see our cross-signing verifications working! Lots more to do here: the remaining device.isVerified() calls in UserInfoPanel are where it needs to be switched to verifying users rather than devices, and of course we need to replace all the calls to device.isVerified() with checkDeviceTrust everywhere else. --- src/components/views/right_panel/UserInfo.js | 32 +++++++++++++++----- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/src/components/views/right_panel/UserInfo.js b/src/components/views/right_panel/UserInfo.js index da82fb2bdf..0ad2cc4dfb 100644 --- a/src/components/views/right_panel/UserInfo.js +++ b/src/components/views/right_panel/UserInfo.js @@ -129,17 +129,20 @@ function verifyDevice(userId, device) { } function DeviceItem({userId, device}) { + const cli = useContext(MatrixClientContext); + const deviceTrust = cli.checkDeviceTrust(userId, device.deviceId); + const classes = classNames("mx_UserInfo_device", { - mx_UserInfo_device_verified: device.isVerified(), - mx_UserInfo_device_unverified: !device.isVerified(), + mx_UserInfo_device_verified: deviceTrust.isVerified(), + mx_UserInfo_device_unverified: !deviceTrust.isVerified(), }); const iconClasses = classNames("mx_E2EIcon", { - mx_E2EIcon_verified: device.isVerified(), - mx_E2EIcon_warning: !device.isVerified(), + mx_E2EIcon_verified: deviceTrust.isVerified(), + mx_E2EIcon_warning: !deviceTrust.isVerified(), }); const onDeviceClick = () => { - if (!device.isVerified()) { + if (!deviceTrust.isVerified()) { verifyDevice(userId, device); } }; @@ -147,7 +150,7 @@ function DeviceItem({userId, device}) { const deviceName = device.ambiguous ? (device.getDisplayName() ? device.getDisplayName() : "") + " (" + device.deviceId + ")" : device.getDisplayName(); - const trustedLabel = device.isVerified() ? _t("Trusted") : _t("Not trusted"); + const trustedLabel = deviceTrust.isVerified() ? _t("Trusted") : _t("Not trusted"); return (
    {deviceName}
    @@ -157,6 +160,7 @@ function DeviceItem({userId, device}) { function DevicesSection({devices, userId, loading}) { const Spinner = sdk.getComponent("elements.Spinner"); + const cli = useContext(MatrixClientContext); const [isExpanded, setExpanded] = useState(false); @@ -167,9 +171,21 @@ function DevicesSection({devices, userId, loading}) { if (devices === null) { return _t("Unable to load device list"); } + const deviceTrusts = devices.map(d => cli.checkDeviceTrust(userId, d.deviceId)); - const unverifiedDevices = devices.filter(d => !d.isVerified()); - const verifiedDevices = devices.filter(d => d.isVerified()); + const unverifiedDevices = []; + const verifiedDevices = []; + + for (let i = 0; i < devices.length; ++i) { + const device = devices[i]; + const deviceTrust = deviceTrusts[i]; + + if (deviceTrust.isVerified()) { + verifiedDevices.push(device); + } else { + unverifiedDevices.push(device); + } + } let expandButton; if (verifiedDevices.length) { From a928b33f274adf93cfb8b97a826776f4f0b5934e Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 20 Dec 2019 16:51:50 +0000 Subject: [PATCH 34/93] Don't show the 'verify' button if the user is verified Which is more in keeping with the designs (and we can do this now that the new user info panel is only enabled with cross signing). --- src/components/views/right_panel/UserInfo.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/components/views/right_panel/UserInfo.js b/src/components/views/right_panel/UserInfo.js index 0ad2cc4dfb..4dd71700ad 100644 --- a/src/components/views/right_panel/UserInfo.js +++ b/src/components/views/right_panel/UserInfo.js @@ -1286,11 +1286,20 @@ const UserInfo = ({user, groupId, roomId, onClose}) => { const devicesSection = isRoomEncrypted ? () : null; + + const userVerified = cli.checkUserTrust(user.userId).isVerified(); + let verifyButton; + if (!userVerified) { + verifyButton = verifyDevice(user.userId, null)}> + {_t("Verify")} + ; + } + const securitySection = (

    { _t("Security") }

    { text }

    - verifyDevice(user.userId, null)}>{_t("Verify")} + {verifyButton} { devicesSection }
    ); From 376bf7e2138c2eb62199211ea37a4915a6505d5c Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 20 Dec 2019 16:56:38 +0000 Subject: [PATCH 35/93] 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 313bc93edd..ec1797cafd 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1124,8 +1124,8 @@ "This client does not support end-to-end encryption.": "This client does not support end-to-end encryption.", "Messages in this room are not end-to-end encrypted.": "Messages in this room are not end-to-end encrypted.", "Messages in this room are end-to-end encrypted.": "Messages in this room are end-to-end encrypted.", - "Security": "Security", "Verify": "Verify", + "Security": "Security", "Sunday": "Sunday", "Monday": "Monday", "Tuesday": "Tuesday", From e936f7eb095fc03d53b512d081db8aa37b2a87ad Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Sat, 21 Dec 2019 11:13:37 +0000 Subject: [PATCH 36/93] Fix room list filtering weird case sensitivity Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/rooms/RoomList.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index 152ce5105e..2fd4fef5b0 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -589,15 +589,17 @@ module.exports = createReactClass({ _applySearchFilter: function(list, filter) { if (filter === "") return list; - const fuzzyFilter = utils.removeHiddenChars(filter).toLowerCase(); const lcFilter = filter.toLowerCase(); + // apply toLowerCase before and after removeHiddenChars because different rules get applied + // e.g M -> M but m -> n, yet some unicode homoglyphs come out as uppsercase, e.g 𝚮 -> H + const fuzzyFilter = utils.removeHiddenChars(lcFilter).toLowerCase(); // case insensitive if room name includes filter, // or if starts with `#` and one of room's aliases starts with filter return list.filter((room) => { if (filter[0] === "#" && room.getAliases().some((alias) => alias.toLowerCase().startsWith(lcFilter))) { return true; } - return room.name ? utils.removeHiddenChars(room.name).toLowerCase().includes(fuzzyFilter) : false; + return room.name && utils.removeHiddenChars(room.name.toLowerCase()).toLowerCase().includes(fuzzyFilter); }); }, From 24a1017d6d246e388d23910630edb89f1164248f Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Sat, 21 Dec 2019 20:26:32 +0000 Subject: [PATCH 37/93] Fix typo in comment --- src/components/views/rooms/RoomList.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index 2fd4fef5b0..cb88861509 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -591,7 +591,7 @@ module.exports = createReactClass({ if (filter === "") return list; const lcFilter = filter.toLowerCase(); // apply toLowerCase before and after removeHiddenChars because different rules get applied - // e.g M -> M but m -> n, yet some unicode homoglyphs come out as uppsercase, e.g 𝚮 -> H + // e.g M -> M but m -> n, yet some unicode homoglyphs come out as uppercase, e.g 𝚮 -> H const fuzzyFilter = utils.removeHiddenChars(lcFilter).toLowerCase(); // case insensitive if room name includes filter, // or if starts with `#` and one of room's aliases starts with filter From afcbb218faf8a63fddce5ac19e8f90990e9fca2a Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 23 Dec 2019 11:31:30 +0000 Subject: [PATCH 38/93] match padding on CustomRoomTagPanel and fix colour of Communities btn --- res/css/structures/_CustomRoomTagPanel.scss | 3 ++- res/css/structures/_TagPanel.scss | 5 ++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/res/css/structures/_CustomRoomTagPanel.scss b/res/css/structures/_CustomRoomTagPanel.scss index 278c7c3d0c..32ad20132d 100644 --- a/res/css/structures/_CustomRoomTagPanel.scss +++ b/res/css/structures/_CustomRoomTagPanel.scss @@ -32,8 +32,9 @@ limitations under the License. } .mx_CustomRoomTagPanel .mx_AccessibleButton { - margin: 9px auto; + margin: 0 auto; width: 40px; + padding: 10px 0 9px 0; position: relative; } diff --git a/res/css/structures/_TagPanel.scss b/res/css/structures/_TagPanel.scss index e2eda433ba..dddd2e324c 100644 --- a/res/css/structures/_TagPanel.scss +++ b/res/css/structures/_TagPanel.scss @@ -87,16 +87,15 @@ limitations under the License. height: 40px; width: 40px; border-radius: 20px; - background-color: $tagpanel-button-color; - opacity: 0.11; + background-color: $roomheader-addroom-bg-color; position: relative; /* overwrite mx_RoleButton inline-block */ display: block !important; &::before { + background-color: $roomheader-addroom-fg-color; mask-image: url('$(res)/img/feather-customised/plus.svg'); mask-position: center; - background-color: $button-primary-fg-color; mask-repeat: no-repeat; content: ''; position: absolute; From 84eb72b0e065148395cad871745f138405931025 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 23 Dec 2019 11:38:52 +0000 Subject: [PATCH 39/93] fix alignment --- res/css/structures/_CustomRoomTagPanel.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/css/structures/_CustomRoomTagPanel.scss b/res/css/structures/_CustomRoomTagPanel.scss index 32ad20132d..1fb18ec41e 100644 --- a/res/css/structures/_CustomRoomTagPanel.scss +++ b/res/css/structures/_CustomRoomTagPanel.scss @@ -52,5 +52,5 @@ limitations under the License. position: absolute; left: -15px; border-radius: 0 3px 3px 0; - top: -8px; // (56 - 40)/2 + top: 2px; // 10 [padding-top] - (56 - 40)/2 } From 17f58499857d71dd1ac854292643111013b02467 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 23 Dec 2019 12:24:49 +0000 Subject: [PATCH 40/93] stop using ReactDOM.findDOMNode in componentWillUnmount, use refs --- src/components/structures/RoomView.js | 5 ++--- src/components/views/avatars/BaseAvatar.js | 21 ++++++++++++++----- .../views/rooms/ReadReceiptMarker.js | 12 +++++++---- 3 files changed, 26 insertions(+), 12 deletions(-) diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 739519a2b3..29ffc24009 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -25,7 +25,6 @@ import shouldHideEvent from '../../shouldHideEvent'; import React, {createRef} from 'react'; import createReactClass from 'create-react-class'; -import ReactDOM from 'react-dom'; import PropTypes from 'prop-types'; import classNames from 'classnames'; import {Room} from "matrix-js-sdk"; @@ -461,7 +460,7 @@ module.exports = createReactClass({ componentDidUpdate: function() { if (this._roomView.current) { - const roomView = ReactDOM.findDOMNode(this._roomView.current); + const roomView = this._roomView.current; if (!roomView.ondrop) { roomView.addEventListener('drop', this.onDrop); roomView.addEventListener('dragover', this.onDragOver); @@ -505,7 +504,7 @@ module.exports = createReactClass({ // is really just for hygiene - we're going to be // deleted anyway, so it doesn't matter if the event listeners // don't get cleaned up. - const roomView = ReactDOM.findDOMNode(this._roomView.current); + const roomView = this._roomView.current; roomView.removeEventListener('drop', this.onDrop); roomView.removeEventListener('dragover', this.onDragOver); roomView.removeEventListener('dragleave', this.onDragLeaveOrEnd); diff --git a/src/components/views/avatars/BaseAvatar.js b/src/components/views/avatars/BaseAvatar.js index 82db78615e..d7e30bc6f1 100644 --- a/src/components/views/avatars/BaseAvatar.js +++ b/src/components/views/avatars/BaseAvatar.js @@ -38,6 +38,12 @@ module.exports = createReactClass({ // XXX resizeMethod not actually used. resizeMethod: PropTypes.string, defaultToInitialLetter: PropTypes.bool, // true to add default url + inputRef: PropTypes.oneOfType([ + // Either a function + PropTypes.func, + // Or the instance of a DOM native element + PropTypes.shape({ current: PropTypes.instanceOf(Element) }), + ]), }, contextTypes: { @@ -148,7 +154,7 @@ module.exports = createReactClass({ const { name, idName, title, url, urls, width, height, resizeMethod, - defaultToInitialLetter, onClick, + defaultToInitialLetter, onClick, inputRef, ...otherProps } = this.props; @@ -171,7 +177,7 @@ module.exports = createReactClass({ if (onClick != null) { return ( { textNode } { imgNode } @@ -179,7 +185,7 @@ module.exports = createReactClass({ ); } else { return ( - + { textNode } { imgNode } @@ -188,21 +194,26 @@ module.exports = createReactClass({ } if (onClick != null) { return ( - ); } else { return ( - ); } diff --git a/src/components/views/rooms/ReadReceiptMarker.js b/src/components/views/rooms/ReadReceiptMarker.js index 27c5e8c20e..8f28d96f9d 100644 --- a/src/components/views/rooms/ReadReceiptMarker.js +++ b/src/components/views/rooms/ReadReceiptMarker.js @@ -14,8 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; -import ReactDOM from 'react-dom'; +import React, {createRef} from 'react'; import PropTypes from 'prop-types'; import createReactClass from 'create-react-class'; @@ -90,6 +89,10 @@ module.exports = createReactClass({ }; }, + UNSAFE_componentWillMount: function() { + this._avatar = createRef(); + }, + componentWillUnmount: function() { // before we remove the rr, store its location in the map, so that if // it reappears, it can be animated from the right place. @@ -105,7 +108,7 @@ module.exports = createReactClass({ return; } - const avatarNode = ReactDOM.findDOMNode(this); + const avatarNode = this._avatar.current; rrInfo.top = avatarNode.offsetTop; rrInfo.left = avatarNode.offsetLeft; rrInfo.parent = avatarNode.offsetParent; @@ -125,7 +128,7 @@ module.exports = createReactClass({ oldTop = oldInfo.top + oldInfo.parent.getBoundingClientRect().top; } - const newElement = ReactDOM.findDOMNode(this); + const newElement = this._avatar.current; let startTopOffset; if (!newElement.offsetParent) { // this seems to happen sometimes for reasons I don't understand @@ -215,6 +218,7 @@ module.exports = createReactClass({ style={style} title={title} onClick={this.props.onClick} + inputRef={this._avatar} /> ); From 207045e979ed9916f68c0021b37a544aa93ffa1c Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 23 Dec 2019 12:54:31 +0000 Subject: [PATCH 41/93] fix ReadReceiptMarker ref --- src/components/views/rooms/ReadReceiptMarker.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/ReadReceiptMarker.js b/src/components/views/rooms/ReadReceiptMarker.js index 8f28d96f9d..35d745ae5a 100644 --- a/src/components/views/rooms/ReadReceiptMarker.js +++ b/src/components/views/rooms/ReadReceiptMarker.js @@ -178,7 +178,7 @@ module.exports = createReactClass({ render: function() { const MemberAvatar = sdk.getComponent('avatars.MemberAvatar'); if (this.state.suppressDisplay) { - return
    ; + return
    ; } const style = { From 8018097e56be3c2d428a53eace899584c4a7e3a5 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 23 Dec 2019 14:13:56 +0000 Subject: [PATCH 42/93] Add alt="" to presentational images --- src/components/views/globals/MatrixToolbar.js | 2 +- src/components/views/globals/NewVersionBar.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/globals/MatrixToolbar.js b/src/components/views/globals/MatrixToolbar.js index aabf0810f8..1e496741ad 100644 --- a/src/components/views/globals/MatrixToolbar.js +++ b/src/components/views/globals/MatrixToolbar.js @@ -34,7 +34,7 @@ module.exports = createReactClass({ render: function() { return (
    - +
    { _t('You are not receiving desktop notifications') } { _t('Enable them now') }
    diff --git a/src/components/views/globals/NewVersionBar.js b/src/components/views/globals/NewVersionBar.js index abb9334242..f6bd029969 100644 --- a/src/components/views/globals/NewVersionBar.js +++ b/src/components/views/globals/NewVersionBar.js @@ -97,7 +97,7 @@ export default createReactClass({ } return (
    - +
    {_t("A new version of Riot is available.")}
    From e55219570144af0b6244138d2ba6553b88fc2837 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 23 Dec 2019 17:57:53 +0000 Subject: [PATCH 43/93] Fix duplicate Incoming Call prompt on Community Invite sublist --- src/components/views/rooms/RoomList.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index cb88861509..35a5ca9e66 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -636,7 +636,6 @@ module.exports = createReactClass({ const defaultProps = { collapsed: this.props.collapsed, isFiltered: !!this.props.searchFilter, - incomingCall: this.state.incomingCall, }; subListsProps.forEach((p) => { @@ -649,7 +648,7 @@ module.exports = createReactClass({ })); return subListsProps.reduce((components, props, i) => { - props = Object.assign({}, defaultProps, props); + props = {...defaultProps, ...props}; const isLast = i === subListsProps.length - 1; const len = props.list.length + (props.extraTiles ? props.extraTiles.length : 0); const {key, label, onHeaderClick, ...otherProps} = props; @@ -660,12 +659,12 @@ module.exports = createReactClass({ onHeaderClick(collapsed); } }; - let startAsHidden = props.startAsHidden || this.collapsedState[chosenKey]; + const startAsHidden = props.startAsHidden || this.collapsedState[chosenKey]; this._layoutSections.push({ id: chosenKey, count: len, }); - let subList = ( Date: Mon, 23 Dec 2019 18:20:59 +0000 Subject: [PATCH 44/93] UserInfo hide kick/mute buttons if they make no sense --- src/components/views/right_panel/UserInfo.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/components/views/right_panel/UserInfo.js b/src/components/views/right_panel/UserInfo.js index 4dd71700ad..316c8d3d0e 100644 --- a/src/components/views/right_panel/UserInfo.js +++ b/src/components/views/right_panel/UserInfo.js @@ -425,6 +425,9 @@ const useRoomPowerLevels = (cli, room) => { const RoomKickButton = ({member, startUpdating, stopUpdating}) => { const cli = useContext(MatrixClientContext); + // check if user can be kicked/disinvited + if (member.membership !== "invite" && member.membership !== "join") return null; + const onKick = async () => { const ConfirmUserActionDialog = sdk.getComponent("dialogs.ConfirmUserActionDialog"); const {finished} = Modal.createTrackedDialog( @@ -602,6 +605,9 @@ const BanToggleButton = ({member, startUpdating, stopUpdating}) => { const MuteToggleButton = ({member, room, powerLevels, startUpdating, stopUpdating}) => { const cli = useContext(MatrixClientContext); + // Don't show the mute/unmute option if the user is not in the room + if (member.membership !== "join") return null; + const isMuted = _isMuted(member, powerLevels); const onMuteToggle = async () => { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); From 25d864c39dddc78eed6b8aa09621965e28fa3b73 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 23 Dec 2019 19:29:43 +0000 Subject: [PATCH 45/93] Fix wrong scope binding on openHelp for TopLeftMenu --- src/components/views/context_menus/TopLeftMenu.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/context_menus/TopLeftMenu.js b/src/components/views/context_menus/TopLeftMenu.js index 7a7b124919..08a65e2f21 100644 --- a/src/components/views/context_menus/TopLeftMenu.js +++ b/src/components/views/context_menus/TopLeftMenu.js @@ -128,11 +128,11 @@ export class TopLeftMenu extends React.Component {
    ; } - openHelp() { + openHelp = () => { this.closeMenu(); const RedesignFeedbackDialog = sdk.getComponent("views.dialogs.RedesignFeedbackDialog"); Modal.createTrackedDialog('Report bugs & give feedback', '', RedesignFeedbackDialog); - } + }; viewHomePage() { dis.dispatch({action: 'view_home_page'}); From 92ea1157bec969006882e87f38f8d9c35a7b5ce1 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 23 Dec 2019 19:26:59 -0700 Subject: [PATCH 46/93] Reintroduce working resizer code for right panel Fixes https://github.com/vector-im/riot-web/issues/11674 This re-introduces and adapted version of what was there before, but fixed for the new collapsed logic: https://github.com/matrix-org/matrix-react-sdk/pull/3703/files#diff-633a0248e235d7446a8868a9145efce2L77-L93 We no longer have a collapsedRhs variable and only set the panel when it is opened, so we can accurately track expanded/collapsed state through presence of a panel. --- src/components/structures/MainSplit.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/components/structures/MainSplit.js b/src/components/structures/MainSplit.js index bd7bfd8780..772be358cf 100644 --- a/src/components/structures/MainSplit.js +++ b/src/components/structures/MainSplit.js @@ -74,6 +74,21 @@ export default class MainSplit extends React.Component { } } + componentDidUpdate(prevProps) { + const wasPanelSet = this.props.panel && !prevProps.panel; + const wasPanelCleared = !this.props.panel && prevProps.panel; + + if (this.resizeContainer && wasPanelSet) { + // The resizer can only be created when **both** expanded and the panel is + // set. Once both are true, the container ref will mount, which is required + // for the resizer to work. + this._createResizer(); + } else if (this.resizer && wasPanelCleared) { + this.resizer.detach(); + this.resizer = null; + } + } + render() { const bodyView = React.Children.only(this.props.children); const panelView = this.props.panel; From a9cae90a520be50608ef6aee60fce911c66cfbbe Mon Sep 17 00:00:00 2001 From: Pankaj Kumar Singh <34129569+ps0305@users.noreply.github.com> Date: Thu, 26 Dec 2019 23:03:04 +0530 Subject: [PATCH 47/93] Signed-off-by Pankaj Singh Date: Thu, 26 Dec 2019 18:04:58 +0000 Subject: [PATCH 48/93] Fix UserInfo promote user inverted buttons --- src/components/views/right_panel/UserInfo.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/right_panel/UserInfo.js b/src/components/views/right_panel/UserInfo.js index 316c8d3d0e..208c5e8906 100644 --- a/src/components/views/right_panel/UserInfo.js +++ b/src/components/views/right_panel/UserInfo.js @@ -984,7 +984,7 @@ const PowerLevelEditor = ({user, room, roomPermissions, onFinished}) => { }); const [confirmed] = await finished; - if (confirmed) return; + if (!confirmed) return; } await _applyPowerChange(roomId, target, powerLevel, powerLevelEvent); } finally { From 22fe0add3c01215f2525e36f567196b6f3d0e8b2 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 26 Dec 2019 18:10:52 +0000 Subject: [PATCH 49/93] Make UserOnlineDot more generic --- res/css/_components.scss | 1 + res/css/views/rooms/_RoomTile.scss | 8 ------- res/css/views/rooms/_UserOnlineDot.scss | 23 +++++++++++++++++++ src/components/views/rooms/RoomTile.js | 4 ++-- ...{RoomTileOnlineDot.js => UserOnlineDot.js} | 8 +++---- 5 files changed, 30 insertions(+), 14 deletions(-) create mode 100644 res/css/views/rooms/_UserOnlineDot.scss rename src/components/views/rooms/{RoomTileOnlineDot.js => UserOnlineDot.js} (89%) diff --git a/res/css/_components.scss b/res/css/_components.scss index 233c781d7f..03c2663af8 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -174,6 +174,7 @@ @import "./views/rooms/_SendMessageComposer.scss"; @import "./views/rooms/_Stickers.scss"; @import "./views/rooms/_TopUnreadMessagesBar.scss"; +@import "./views/rooms/_UserOnlineDot.scss"; @import "./views/rooms/_WhoIsTypingTile.scss"; @import "./views/settings/_AvatarSetting.scss"; @import "./views/settings/_CrossSigningPanel.scss"; diff --git a/res/css/views/rooms/_RoomTile.scss b/res/css/views/rooms/_RoomTile.scss index 2b181f366e..e5c7948216 100644 --- a/res/css/views/rooms/_RoomTile.scss +++ b/res/css/views/rooms/_RoomTile.scss @@ -62,14 +62,6 @@ limitations under the License. min-width: 0; } -.mx_RoomTile_online_dot { - border-radius: 50%; - background-color: $accent-color; - height: 5px; - width: 5px; - display: inline-block; -} - .mx_RoomTile_subtext { display: inline-block; font-size: 11px; diff --git a/res/css/views/rooms/_UserOnlineDot.scss b/res/css/views/rooms/_UserOnlineDot.scss new file mode 100644 index 0000000000..339e5cc48a --- /dev/null +++ b/res/css/views/rooms/_UserOnlineDot.scss @@ -0,0 +1,23 @@ +/* +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_UserOnlineDot { + border-radius: 50%; + background-color: $accent-color; + height: 5px; + width: 5px; + display: inline-block; +} diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index cd5af61862..3f45b5b342 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -380,8 +380,8 @@ module.exports = createReactClass({ />; if (this.props.room.getMember(dmUserId).membership === "join") { - const RoomTileOnlineDot = sdk.getComponent('rooms.RoomTileOnlineDot'); - dmOnline = ; + const UserOnlineDot = sdk.getComponent('rooms.UserOnlineDot'); + dmOnline = ; } } diff --git a/src/components/views/rooms/RoomTileOnlineDot.js b/src/components/views/rooms/UserOnlineDot.js similarity index 89% rename from src/components/views/rooms/RoomTileOnlineDot.js rename to src/components/views/rooms/UserOnlineDot.js index a882aec613..426dd1bf64 100644 --- a/src/components/views/rooms/RoomTileOnlineDot.js +++ b/src/components/views/rooms/UserOnlineDot.js @@ -20,7 +20,7 @@ import PropTypes from "prop-types"; import {useEventEmitter} from "../../../hooks/useEventEmitter"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; -const RoomTileOnlineDot = ({userId}) => { +const UserOnlineDot = ({userId}) => { const cli = useContext(MatrixClientContext); const user = useMemo(() => cli.getUser(userId), [cli, userId]); @@ -38,11 +38,11 @@ const RoomTileOnlineDot = ({userId}) => { useEventEmitter(user, "User.currentlyActive", currentlyActiveHandler); useEventEmitter(user, "User.presence", currentlyActiveHandler); - return isOnline ? : null; + return isOnline ? : null; }; -RoomTileOnlineDot.propTypes = { +UserOnlineDot.propTypes = { userId: PropTypes.string.isRequired, }; -export default RoomTileOnlineDot; +export default UserOnlineDot; From e95b67e101399c6d6b5b74e11f396e8489e6799a Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 26 Dec 2019 18:15:08 +0000 Subject: [PATCH 50/93] Only show it if exactly 2 members, until we get Canonical DMs --- src/components/views/rooms/RoomTile.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index 3f45b5b342..ecf2de394b 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -379,7 +379,8 @@ module.exports = createReactClass({ alt="dm" />; - if (this.props.room.getMember(dmUserId).membership === "join") { + const { room } = this.props; + if (room.getMember(dmUserId).membership === "join" && room.getJoinedMemberCount() === 2) { const UserOnlineDot = sdk.getComponent('rooms.UserOnlineDot'); dmOnline = ; } From ebf7eb698dd548766ec8dfb3f76438dbb13b1f64 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 26 Dec 2019 18:52:57 +0000 Subject: [PATCH 51/93] Turn RoomAliasField into properly controlled and use in RoomSettings --- .../views/dialogs/CreateRoomDialog.js | 2 +- .../views/elements/RoomAliasField.js | 5 +++- .../views/room_settings/AliasSettings.js | 28 +++++++++++++++++-- 3 files changed, 30 insertions(+), 5 deletions(-) diff --git a/src/components/views/dialogs/CreateRoomDialog.js b/src/components/views/dialogs/CreateRoomDialog.js index 6a73d22708..5ddebb1119 100644 --- a/src/components/views/dialogs/CreateRoomDialog.js +++ b/src/components/views/dialogs/CreateRoomDialog.js @@ -173,7 +173,7 @@ export default createReactClass({ const domain = MatrixClientPeg.get().getDomain(); aliasField = (
    - this._aliasFieldRef = ref} onChange={this.onAliasChange} domain={domain} /> + this._aliasFieldRef = ref} onChange={this.onAliasChange} domain={domain} value={this.state.alias} />
    ); } else { diff --git a/src/components/views/elements/RoomAliasField.js b/src/components/views/elements/RoomAliasField.js index 03f4000e59..7054dfcce2 100644 --- a/src/components/views/elements/RoomAliasField.js +++ b/src/components/views/elements/RoomAliasField.js @@ -20,11 +20,13 @@ import sdk from '../../../index'; import withValidation from './Validation'; import MatrixClientPeg from '../../../MatrixClientPeg'; +// Controlled form component wrapping Field for inputting a room alias scoped to a given domain export default class RoomAliasField extends React.PureComponent { static propTypes = { id: PropTypes.string.isRequired, domain: PropTypes.string.isRequired, onChange: PropTypes.func, + value: PropTypes.string.isRequired, }; constructor(props) { @@ -53,6 +55,7 @@ export default class RoomAliasField extends React.PureComponent { onValidate={this._onValidate} placeholder={_t("e.g. my-room")} onChange={this._onChange} + value={this.props.value.substring(1, this.props.value.length - this.props.domain.length - 1)} maxLength={maxlength} /> ); } @@ -61,7 +64,7 @@ export default class RoomAliasField extends React.PureComponent { if (this.props.onChange) { this.props.onChange(this._asFullAlias(ev.target.value)); } - } + }; _onValidate = async (fieldState) => { const result = await this._validationRules(fieldState); diff --git a/src/components/views/room_settings/AliasSettings.js b/src/components/views/room_settings/AliasSettings.js index daf5c6edc2..946bf0d791 100644 --- a/src/components/views/room_settings/AliasSettings.js +++ b/src/components/views/room_settings/AliasSettings.js @@ -15,6 +15,8 @@ See the License for the specific language governing permissions and limitations under the License. */ +import EditableItemList from "../elements/EditableItemList"; + const React = require('react'); import PropTypes from 'prop-types'; const MatrixClientPeg = require('../../../MatrixClientPeg'); @@ -22,8 +24,29 @@ const sdk = require("../../../index"); import { _t } from '../../../languageHandler'; import Field from "../elements/Field"; import ErrorDialog from "../dialogs/ErrorDialog"; +import AccessibleButton from "../elements/AccessibleButton"; const Modal = require("../../../Modal"); +class EditableAliasesList extends EditableItemList { + _renderNewItemField() { + const RoomAliasField = sdk.getComponent('views.elements.RoomAliasField'); + const onChange = (alias) => this._onNewItemChanged({target: {value: alias}}); + return ( +
    + + + {_t("Add")} + + + ); + } +} + export default class AliasSettings extends React.Component { static propTypes = { roomId: PropTypes.string.isRequired, @@ -47,7 +70,6 @@ export default class AliasSettings extends React.Component { remoteDomains: [], // [ domain.com, foobar.com ] canonicalAlias: null, // #canonical:domain.com updatingCanonicalAlias: false, - newItem: "", }; const localDomain = MatrixClientPeg.get().getDomain(); @@ -181,7 +203,6 @@ export default class AliasSettings extends React.Component { }; render() { - const EditableItemList = sdk.getComponent("elements.EditableItemList"); const localDomain = MatrixClientPeg.get().getDomain(); let found = false; @@ -233,7 +254,7 @@ export default class AliasSettings extends React.Component { return (
    {canonicalAliasSection} - {remoteAliasesSection}
    From e320f64ba1ed9d6f99138885bcbd68fe2f402e85 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 26 Dec 2019 19:27:04 +0000 Subject: [PATCH 52/93] fuzzy-sort MemberList --- src/components/views/rooms/MemberList.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/components/views/rooms/MemberList.js b/src/components/views/rooms/MemberList.js index 0805c0342c..b874c378d4 100644 --- a/src/components/views/rooms/MemberList.js +++ b/src/components/views/rooms/MemberList.js @@ -32,6 +32,9 @@ const INITIAL_LOAD_NUM_MEMBERS = 30; const INITIAL_LOAD_NUM_INVITED = 5; const SHOW_MORE_INCREMENT = 100; +// Regex applied to member names before applying sort, to fuzzy it a little +const SORT_REGEX = /[.?!,;:\-()[\]{}'"&@]+/g; + module.exports = createReactClass({ displayName: 'MemberList', @@ -336,10 +339,13 @@ module.exports = createReactClass({ } // Fourth by name (alphabetical) - const nameA = memberA.name[0] === '@' ? memberA.name.substr(1) : memberA.name; - const nameB = memberB.name[0] === '@' ? memberB.name.substr(1) : memberB.name; + const nameA = (memberA.name[0] === '@' ? memberA.name.substr(1) : memberA.name).replace(SORT_REGEX, ""); + const nameB = (memberB.name[0] === '@' ? memberB.name.substr(1) : memberB.name).replace(SORT_REGEX, ""); // console.log(`Comparing userA_name=${nameA} against userB_name=${nameB} - returning`); - return nameA.localeCompare(nameB); + return nameA.localeCompare(nameB, { + ignorePunctuation: true, + sensitivity: "base", + }); }, onSearchQueryChanged: function(searchQuery) { From ab122889795b5aa864dda67ac1a9b4d99e4555be Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 26 Dec 2019 19:29:25 +0000 Subject: [PATCH 53/93] Add more punctuation to regex --- src/components/views/rooms/MemberList.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/MemberList.js b/src/components/views/rooms/MemberList.js index b874c378d4..bbb1390611 100644 --- a/src/components/views/rooms/MemberList.js +++ b/src/components/views/rooms/MemberList.js @@ -33,7 +33,7 @@ const INITIAL_LOAD_NUM_INVITED = 5; const SHOW_MORE_INCREMENT = 100; // Regex applied to member names before applying sort, to fuzzy it a little -const SORT_REGEX = /[.?!,;:\-()[\]{}'"&@]+/g; +const SORT_REGEX = /[.?!,;:\-()[\]{}'"&@#\\/+_=]+/g; module.exports = createReactClass({ displayName: 'MemberList', From 06230e01e3805cdcb418d4cc452628f4f73a8317 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 26 Dec 2019 19:36:16 +0000 Subject: [PATCH 54/93] Fix End-to-End tests for RoomSettings interactivity --- test/end-to-end-tests/src/usecases/room-settings.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/end-to-end-tests/src/usecases/room-settings.js b/test/end-to-end-tests/src/usecases/room-settings.js index 7655d2d066..5b425f14b7 100644 --- a/test/end-to-end-tests/src/usecases/room-settings.js +++ b/test/end-to-end-tests/src/usecases/room-settings.js @@ -52,7 +52,7 @@ module.exports = async function changeRoomSettings(session, settings) { if (settings.alias) { session.log.step(`sets alias to ${settings.alias}`); const aliasField = await session.query(".mx_RoomSettingsDialog .mx_AliasSettings input[type=text]"); - await session.replaceInputText(aliasField, settings.alias); + await session.replaceInputText(aliasField, settings.alias.substring(1, settings.alias.lastIndexOf(":"))); const addButton = await session.query(".mx_RoomSettingsDialog .mx_AliasSettings .mx_AccessibleButton"); await addButton.click(); session.log.done(); From ed24f19a3f24f6c23cb3dd04aae6c1c6a287a9d2 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 26 Dec 2019 20:12:50 +0000 Subject: [PATCH 55/93] Fix stick picker chevron offset calculation --- src/components/views/rooms/Stickerpicker.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/rooms/Stickerpicker.js b/src/components/views/rooms/Stickerpicker.js index 24f256e706..4fabb75ed6 100644 --- a/src/components/views/rooms/Stickerpicker.js +++ b/src/components/views/rooms/Stickerpicker.js @@ -315,8 +315,8 @@ export default class Stickerpicker extends React.Component { // Offset the chevron location, which is relative to the left of the context menu // (10 = offset when context menu would not be displayed off viewport) - // (8 = value required in practice (possibly 10 - 2 where the 2 = context menu borders) - const stickerPickerChevronOffset = Math.max(10, 8 + window.pageXOffset + buttonRect.left - x); + // (2 = context menu borders) + const stickerPickerChevronOffset = Math.max(10, 2 + window.pageXOffset + buttonRect.left - x); const y = (buttonRect.top + (buttonRect.height / 2) + window.pageYOffset) - 19; From e9ebfa100fa639444c283222e658f8f64bb06c61 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 26 Dec 2019 20:32:25 +0000 Subject: [PATCH 56/93] Do not show Top Unread Messages Bar and Jump to bottom button if searching --- src/components/structures/RoomView.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 21d5a8f354..939f422a36 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -1931,7 +1931,8 @@ module.exports = createReactClass({ />); let topUnreadMessagesBar = null; - if (this.state.showTopUnreadMessagesBar) { + // Do not show TopUnreadMessagesBar if we have search results showing, it makes no sense + if (this.state.showTopUnreadMessagesBar && !this.state.searchResults) { const TopUnreadMessagesBar = sdk.getComponent('rooms.TopUnreadMessagesBar'); topUnreadMessagesBar = (); } let jumpToBottom; - if (!this.state.atEndOfLiveTimeline) { + // Do not show JumpToBottomButton if we have search results showing, it makes no sense + if (!this.state.atEndOfLiveTimeline && !this.state.searchResults) { const JumpToBottomButton = sdk.getComponent('rooms.JumpToBottomButton'); jumpToBottom = ( Date: Fri, 27 Dec 2019 13:59:57 +0000 Subject: [PATCH 57/93] Serialize file uploads into room to match confirmation dialog order --- src/ContentMessages.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/ContentMessages.js b/src/ContentMessages.js index 6908a6a18e..0ce349f348 100644 --- a/src/ContentMessages.js +++ b/src/ContentMessages.js @@ -422,6 +422,9 @@ export default class ContentMessages { const UploadConfirmDialog = sdk.getComponent("dialogs.UploadConfirmDialog"); let uploadAll = false; + // Promise to complete before sending next file into room, used for synchronisation of file-sending + // to match the order the files were specified in + let promBefore = Promise.resolve(); for (let i = 0; i < okFiles.length; ++i) { const file = okFiles[i]; if (!uploadAll) { @@ -440,11 +443,11 @@ export default class ContentMessages { }); if (!shouldContinue) break; } - this._sendContentToRoom(file, roomId, matrixClient); + promBefore = this._sendContentToRoom(file, roomId, matrixClient, promBefore); } } - _sendContentToRoom(file, roomId, matrixClient) { + _sendContentToRoom(file, roomId, matrixClient, promBefore) { const content = { body: file.name || 'Attachment', info: { @@ -517,7 +520,10 @@ export default class ContentMessages { content.file = result.file; content.url = result.url; }); - }).then(function(url) { + }).then((url) => { + // Await previous message being sent into the room + return promBefore; + }).then(function() { return matrixClient.sendMessage(roomId, content); }, function(err) { error = err; From f9e386adaa289e871a6da26ecaf12fc92fea0473 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 27 Dec 2019 17:04:14 +0000 Subject: [PATCH 58/93] Improve SORT_REGEX --- src/components/views/rooms/MemberList.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/views/rooms/MemberList.js b/src/components/views/rooms/MemberList.js index bbb1390611..de0a6fe478 100644 --- a/src/components/views/rooms/MemberList.js +++ b/src/components/views/rooms/MemberList.js @@ -32,8 +32,9 @@ const INITIAL_LOAD_NUM_MEMBERS = 30; const INITIAL_LOAD_NUM_INVITED = 5; const SHOW_MORE_INCREMENT = 100; -// Regex applied to member names before applying sort, to fuzzy it a little -const SORT_REGEX = /[.?!,;:\-()[\]{}'"&@#\\/+_=]+/g; +// Regex applied to filter our punctuation in member names before applying sort, to fuzzy it a little +// matches all ASCII punctuation: !"#$%&'()*+,-./:;<=>?@[\]^_`{|}~ +const SORT_REGEX = /[\x21-\x2F\x3A-\x40\x5B-\x60\x7B-\x7E]+/g; module.exports = createReactClass({ displayName: 'MemberList', From aa990462cec34b47349d3ef6ba542aed17470c33 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 27 Dec 2019 17:05:51 +0000 Subject: [PATCH 59/93] delint --- src/components/views/room_settings/AliasSettings.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/components/views/room_settings/AliasSettings.js b/src/components/views/room_settings/AliasSettings.js index 946bf0d791..aa9d46c9d5 100644 --- a/src/components/views/room_settings/AliasSettings.js +++ b/src/components/views/room_settings/AliasSettings.js @@ -32,15 +32,19 @@ class EditableAliasesList extends EditableItemList { const RoomAliasField = sdk.getComponent('views.elements.RoomAliasField'); const onChange = (alias) => this._onNewItemChanged({target: {value: alias}}); return ( -
    + - {_t("Add")} + { _t("Add") } ); From 98571d706f16d36fd3402e765fd9c1b61c640824 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 27 Dec 2019 11:31:15 -0700 Subject: [PATCH 60/93] Fix NPE when searching for rooms Regressed by https://github.com/matrix-org/matrix-react-sdk/pull/3751 --- src/components/views/rooms/RoomTile.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index ecf2de394b..241713c97d 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -380,7 +380,8 @@ module.exports = createReactClass({ />; const { room } = this.props; - if (room.getMember(dmUserId).membership === "join" && room.getJoinedMemberCount() === 2) { + const member = room.getMember(dmUserId); + if (member && member.membership === "join" && room.getJoinedMemberCount() === 2) { const UserOnlineDot = sdk.getComponent('rooms.UserOnlineDot'); dmOnline = ; } From d0b8565f544a6cb72af58054679f5471514ffe28 Mon Sep 17 00:00:00 2001 From: Justin Sleep Date: Sat, 28 Dec 2019 23:52:57 -0600 Subject: [PATCH 61/93] Fix inverted diff line highlighting in dark theme Signed-off-by: Justin Sleep --- res/themes/dark/css/_dark.scss | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/res/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss index eadde4c672..d1d0e333a0 100644 --- a/res/themes/dark/css/_dark.scss +++ b/res/themes/dark/css/_dark.scss @@ -243,3 +243,12 @@ $breadcrumb-placeholder-bg-color: #272c35; } } } + +// Fixes diff color inversion by swapping add / del colors +.hljs-addition { + background: #fdd; +} + +.hljs-deletion { + background: #dfd; +} From 9eed423994b546dc01ecec75d4f8fae0df35d28b Mon Sep 17 00:00:00 2001 From: j Date: Tue, 24 Dec 2019 15:06:50 +0100 Subject: [PATCH 62/93] support channel names with slash in name/alias Signed-off-by: Jan Gerber --- src/components/structures/MatrixChat.js | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index fad57f5d52..af3f4d2598 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -1569,9 +1569,17 @@ export default createReactClass({ action: 'start_post_registration', }); } else if (screen.indexOf('room/') == 0) { - const segments = screen.substring(5).split('/'); - const roomString = segments[0]; - let eventId = segments.splice(1).join("/"); // empty string if no event id given + // Rooms can have the following formats: + // #room_alias:domain or !opaque_id:domain + const room = screen.substring(5); + const domainOffset = room.indexOf(':') + 1; // 0 in case room does not contain a : + let eventOffset = room.length; + // room aliases can contain slashes only look for slash after domain + if (room.substring(domainOffset).indexOf('/') > -1) { + eventOffset = domainOffset + room.substring(domainOffset).indexOf('/'); + } + const roomString = room.substring(0, eventOffset); + let eventId = room.substring(eventOffset + 1); // empty string if no event id given // Previously we pulled the eventID from the segments in such a way // where if there was no eventId then we'd get undefined. However, we From b1c28870878ee0e5fb6f634bf8a4b5ef030b3c12 Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Mon, 30 Dec 2019 16:08:24 +0000 Subject: [PATCH 63/93] line length --- src/components/views/dialogs/RoomSettingsDialog.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/views/dialogs/RoomSettingsDialog.js b/src/components/views/dialogs/RoomSettingsDialog.js index 9ac2b17f23..e01319e3bd 100644 --- a/src/components/views/dialogs/RoomSettingsDialog.js +++ b/src/components/views/dialogs/RoomSettingsDialog.js @@ -55,7 +55,8 @@ export default class RoomSettingsDialog extends React.Component { _getTabs() { const tabs = []; const featureFlag = SettingsStore.isFeatureEnabled("feature_bridge_state"); - const shouldShowBridgeIcon = featureFlag && BridgeSettingsTab.getBridgeStateEvents(this.props.roomId).length > 0; + const shouldShowBridgeIcon = featureFlag && + BridgeSettingsTab.getBridgeStateEvents(this.props.roomId).length > 0; tabs.push(new Tab( _td("General"), From fb94be4abdb713bb23b8b22461500b90f682ec92 Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Mon, 30 Dec 2019 16:11:59 +0000 Subject: [PATCH 64/93] No trailing space --- src/components/views/dialogs/RoomSettingsDialog.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/dialogs/RoomSettingsDialog.js b/src/components/views/dialogs/RoomSettingsDialog.js index e01319e3bd..c31fe1992d 100644 --- a/src/components/views/dialogs/RoomSettingsDialog.js +++ b/src/components/views/dialogs/RoomSettingsDialog.js @@ -55,7 +55,7 @@ export default class RoomSettingsDialog extends React.Component { _getTabs() { const tabs = []; const featureFlag = SettingsStore.isFeatureEnabled("feature_bridge_state"); - const shouldShowBridgeIcon = featureFlag && + const shouldShowBridgeIcon = featureFlag && BridgeSettingsTab.getBridgeStateEvents(this.props.roomId).length > 0; tabs.push(new Tab( From fa9fd97a4e5b2a7e94d0185650402e214a649909 Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Thu, 2 Jan 2020 09:41:50 +0000 Subject: [PATCH 65/93] missing } --- res/css/views/dialogs/_RoomSettingsDialog.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/res/css/views/dialogs/_RoomSettingsDialog.scss b/res/css/views/dialogs/_RoomSettingsDialog.scss index b7641a4ad0..4b13684d9f 100644 --- a/res/css/views/dialogs/_RoomSettingsDialog.scss +++ b/res/css/views/dialogs/_RoomSettingsDialog.scss @@ -54,6 +54,7 @@ limitations under the License. mask-repeat: no-repeat; mask-size: 36px; mask-position: center; +} .mx_RoomSettingsDialog_BridgeList { padding: 0; From ffba19bd613796090bdc899f5cb4b40e96bbeb47 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 2 Jan 2020 16:52:25 +0000 Subject: [PATCH 66/93] Remove E2eIcon onClick It displayed the Encrypted Event Info dialog, but this full of super advanced debug information and base64 strings that no normal users should ever have to see. It's still accessible via the comtext menu (ie. the same place as 'View Source'). --- res/css/views/rooms/_EventTile.scss | 2 +- src/components/views/rooms/EventTile.js | 23 ++++------------------- 2 files changed, 5 insertions(+), 20 deletions(-) diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index 5359992f84..fbac1e932a 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -1,5 +1,6 @@ /* Copyright 2015, 2016 OpenMarket 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. @@ -353,7 +354,6 @@ div.mx_EventTile_notSent.mx_EventTile_redacted .mx_UnknownBody { left: 46px; width: 15px; height: 15px; - cursor: pointer; display: block; bottom: 0; right: 0; diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index 784c4071aa..e7696de841 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -1,7 +1,7 @@ /* Copyright 2015, 2016 OpenMarket Ltd Copyright 2017 New Vector Ltd -Copyright 2019 The Matrix.org Foundation C.I.C. +Copyright 2019, 2020 The Matrix.org Foundation C.I.C. Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> Licensed under the Apache License, Version 2.0 (the "License"); @@ -443,15 +443,6 @@ module.exports = createReactClass({ }); }, - onCryptoClick: function(e) { - const event = this.props.mxEvent; - - Modal.createTrackedDialogAsync('Encrypted Event Dialog', '', - import('../../../async-components/views/dialogs/EncryptedEventDialog'), - {event}, - ); - }, - onRequestKeysClick: function() { this.setState({ // Indicate in the UI that the keys have been requested (this is expected to @@ -479,11 +470,10 @@ module.exports = createReactClass({ _renderE2EPadlock: function() { const ev = this.props.mxEvent; - const props = {onClick: this.onCryptoClick}; // event could not be decrypted if (ev.getContent().msgtype === 'm.bad.encrypted') { - return ; + return ; } // event is encrypted, display padlock corresponding to whether or not it is verified @@ -491,7 +481,7 @@ module.exports = createReactClass({ if (this.state.verified) { return; // no icon for verified } else { - return (); + return (); } } @@ -508,7 +498,7 @@ module.exports = createReactClass({ return; // we expect this to be unencrypted } // if the event is not encrypted, but it's an e2e room, show the open padlock - return ; + return ; } // no padlock needed @@ -920,7 +910,6 @@ class E2ePadlock extends React.Component { static propTypes = { icon: PropTypes.string.isRequired, title: PropTypes.string.isRequired, - onClick: PropTypes.func, }; constructor() { @@ -931,10 +920,6 @@ class E2ePadlock extends React.Component { }; } - onClick = (e) => { - if (this.props.onClick) this.props.onClick(e); - }; - onHoverStart = () => { this.setState({hover: true}); }; From 39777620a3d299943ed7d8f24fb34ff9263aa231 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 2 Jan 2020 16:58:00 +0000 Subject: [PATCH 67/93] order copyright lines by date --- src/components/views/rooms/EventTile.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index e7696de841..d8ca5ef7cd 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -1,8 +1,8 @@ /* Copyright 2015, 2016 OpenMarket Ltd Copyright 2017 New Vector Ltd -Copyright 2019, 2020 The Matrix.org Foundation C.I.C. Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> +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. From 89ac476281763cea379fea4f23ed60a72ae4e031 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 2 Jan 2020 16:59:46 +0000 Subject: [PATCH 68/93] Unused import --- src/components/views/rooms/EventTile.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index d8ca5ef7cd..b71771a916 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -24,7 +24,6 @@ import PropTypes from 'prop-types'; import createReactClass from 'create-react-class'; const classNames = require("classnames"); import { _t, _td } from '../../../languageHandler'; -const Modal = require('../../../Modal'); const sdk = require('../../../index'); const TextForEvent = require('../../../TextForEvent'); From 4de0f7257a1846ac75d56bd75bc9941927c55a2b Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 2 Jan 2020 17:40:18 -0700 Subject: [PATCH 69/93] Initial implementation of FTUE user lists design This covers the "recents" section and rough design exclusively. It is known that the Field does nothing and that there's a bunch of missing functionality - this is to be iterated upon in future PRs. Labs flag is to aide development and should be removed in a very near future PR. Also, this is focusing on DMs and not user lists in general because I misinterpreted the scope. I'll fix this in a future PR and instead make this the best DM invite dialog it can be. Closes https://github.com/vector-im/riot-web/issues/11197 --- package.json | 3 +- res/css/_components.scss | 1 + res/css/views/dialogs/_DMInviteDialog.scss | 81 +++++++ res/themes/dark/css/_dark.scss | 3 + res/themes/light/css/_light.scss | 3 + src/RoomInvite.js | 15 +- .../views/dialogs/DMInviteDialog.js | 212 ++++++++++++++++++ src/i18n/strings/en_EN.json | 6 + src/settings/Settings.js | 6 + src/utils/DMRoomMap.js | 9 + yarn.lock | 5 + 11 files changed, 342 insertions(+), 2 deletions(-) create mode 100644 res/css/views/dialogs/_DMInviteDialog.scss create mode 100644 src/components/views/dialogs/DMInviteDialog.js diff --git a/package.json b/package.json index 7ef14e6635..a1ebc6602d 100644 --- a/package.json +++ b/package.json @@ -74,7 +74,6 @@ "file-saver": "^1.3.3", "filesize": "3.5.6", "flux": "2.1.1", - "react-focus-lock": "^2.2.1", "focus-visible": "^5.0.2", "fuse.js": "^2.2.0", "gemini-scrollbar": "github:matrix-org/gemini-scrollbar#91e1e566", @@ -82,6 +81,7 @@ "glob": "^5.0.14", "glob-to-regexp": "^0.4.1", "highlight.js": "^9.15.8", + "humanize": "^0.0.9", "is-ip": "^2.0.0", "isomorphic-fetch": "^2.2.1", "linkifyjs": "^2.1.6", @@ -99,6 +99,7 @@ "react-addons-css-transition-group": "15.6.2", "react-beautiful-dnd": "^4.0.1", "react-dom": "^16.9.0", + "react-focus-lock": "^2.2.1", "react-gemini-scrollbar": "github:matrix-org/react-gemini-scrollbar#9cf17f63b7c0b0ec5f31df27da0f82f7238dc594", "resize-observer-polyfill": "^1.5.0", "sanitize-html": "^1.18.4", diff --git a/res/css/_components.scss b/res/css/_components.scss index 7b8ca77739..7a9ebfdf26 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -56,6 +56,7 @@ @import "./views/dialogs/_ConfirmUserActionDialog.scss"; @import "./views/dialogs/_CreateGroupDialog.scss"; @import "./views/dialogs/_CreateRoomDialog.scss"; +@import "./views/dialogs/_DMInviteDialog.scss"; @import "./views/dialogs/_DeactivateAccountDialog.scss"; @import "./views/dialogs/_DeviceVerifyDialog.scss"; @import "./views/dialogs/_DevtoolsDialog.scss"; diff --git a/res/css/views/dialogs/_DMInviteDialog.scss b/res/css/views/dialogs/_DMInviteDialog.scss new file mode 100644 index 0000000000..50e9b0a15f --- /dev/null +++ b/res/css/views/dialogs/_DMInviteDialog.scss @@ -0,0 +1,81 @@ +/* +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_DMInviteDialog_addressBar { + display: flex; + flex-direction: row; + + .mx_DMInviteDialog_editor { + flex: 1; + width: 100%; // Needed to make the Field inside grow + } + + .mx_Field { + margin: 0; + } + + .mx_DMInviteDialog_goButton { + width: 48px; + margin-left: 10px; + } +} + +.mx_DMInviteDialog_section { + padding-bottom: 10px; + + h3 { + font-size: 12px; + color: $muted-fg-color; + font-weight: bold; + text-transform: uppercase; + } +} + +.mx_DMInviteDialog_roomTile { + cursor: pointer; + padding: 5px 10px; + + &:hover { + background-color: $user-tile-hover-bg-color; + border-radius: 4px; + } + + * { + vertical-align: middle; + } + + .mx_DMInviteDialog_roomTile_name { + font-weight: 600; + font-size: 14px; + color: $primary-fg-color; + margin-left: 7px; + } + + .mx_DMInviteDialog_roomTile_userId { + font-size: 12px; + color: $muted-fg-color; + margin-left: 7px; + } + + .mx_DMInviteDialog_roomTile_time { + text-align: right; + font-size: 12px; + color: $muted-fg-color; + float: right; + line-height: 36px; // Height of the avatar to keep the time vertically aligned + } +} + diff --git a/res/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss index eadde4c672..d28efbb11f 100644 --- a/res/themes/dark/css/_dark.scss +++ b/res/themes/dark/css/_dark.scss @@ -16,6 +16,7 @@ $room-highlight-color: #343a46; // typical text (dark-on-white in light skin) $primary-fg-color: $text-primary-color; $primary-bg-color: $bg-color; +$muted-fg-color: $header-panel-text-primary-color; // used for dialog box text $light-fg-color: $header-panel-text-secondary-color; @@ -172,6 +173,8 @@ $interactive-tooltip-fg-color: #ffffff; $breadcrumb-placeholder-bg-color: #272c35; +$user-tile-hover-bg-color: $header-panel-bg-color; + // ***** Mixins! ***** @define-mixin mx_DialogButton { diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss index 0a3ef812b8..ac9cb261d3 100644 --- a/res/themes/light/css/_light.scss +++ b/res/themes/light/css/_light.scss @@ -21,6 +21,7 @@ $header-panel-bg-color: #f3f8fd; // typical text (dark-on-white in light skin) $primary-fg-color: #2e2f32; $primary-bg-color: #ffffff; +$muted-fg-color: #61708b; // Commonly used in headings and relevant alt text // used for dialog box text $light-fg-color: #747474; @@ -293,6 +294,8 @@ $interactive-tooltip-fg-color: #ffffff; $breadcrumb-placeholder-bg-color: #e8eef5; +$user-tile-hover-bg-color: $header-panel-bg-color; + // ***** Mixins! ***** @define-mixin mx_DialogButton { diff --git a/src/RoomInvite.js b/src/RoomInvite.js index 48baad5d9f..ba9fe1f541 100644 --- a/src/RoomInvite.js +++ b/src/RoomInvite.js @@ -25,6 +25,7 @@ import sdk from './'; import dis from './dispatcher'; import DMRoomMap from './utils/DMRoomMap'; import { _t } from './languageHandler'; +import SettingsStore from "./settings/SettingsStore"; /** * Invites multiple addresses to a room @@ -41,6 +42,18 @@ function inviteMultipleToRoom(roomId, addrs) { } export function showStartChatInviteDialog() { + if (SettingsStore.isFeatureEnabled("feature_ftue_dms")) { + const DMInviteDialog = sdk.getComponent("dialogs.DMInviteDialog"); + Modal.createTrackedDialog('Start DM', '', DMInviteDialog, { + onFinished: (inviteIds) => { + // TODO: Replace _onStartDmFinished with less hacks + if (inviteIds.length > 0) _onStartDmFinished(true, inviteIds.map(i => ({address: i}))); + // else ignore and just do nothing + }, + }, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true); + return; + } + const AddressPickerDialog = sdk.getComponent("dialogs.AddressPickerDialog"); Modal.createTrackedDialog('Start a chat', '', AddressPickerDialog, { @@ -99,7 +112,7 @@ export function isValid3pidInvite(event) { return true; } -// TODO: Immutable DMs replaces this +// TODO: Canonical DMs replaces this function _onStartDmFinished(shouldInvite, addrs) { if (!shouldInvite) return; diff --git a/src/components/views/dialogs/DMInviteDialog.js b/src/components/views/dialogs/DMInviteDialog.js new file mode 100644 index 0000000000..5b67112c14 --- /dev/null +++ b/src/components/views/dialogs/DMInviteDialog.js @@ -0,0 +1,212 @@ +/* +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. +*/ + +import React from 'react'; +import PropTypes from 'prop-types'; +import {_t} from "../../../languageHandler"; +import sdk from "../../../index"; +import MatrixClientPeg from "../../../MatrixClientPeg"; +import {makeUserPermalink} from "../../../utils/permalinks/Permalinks"; +import DMRoomMap from "../../../utils/DMRoomMap"; +import {RoomMember} from "matrix-js-sdk/lib/matrix"; +import * as humanize from "humanize"; + +// TODO: [TravisR] Make this generic for all kinds of invites + +const INITIAL_ROOMS_SHOWN = 3; // Number of rooms to show at first +const INCREMENT_ROOMS_SHOWN = 5; // Number of rooms to add when 'show more' is clicked + +class DMRoomTile extends React.Component { + static propTypes = { + member: PropTypes.object.isRequired, + lastActiveTs: PropTypes.number, + onToggle: PropTypes.func.isRequired, + }; + + constructor() { + super(); + } + + _onClick = (e) => { + // Stop the browser from highlighting text + e.preventDefault(); + e.stopPropagation(); + + this.props.onToggle(this.props.member.userId); + }; + + render() { + const MemberAvatar = sdk.getComponent("views.avatars.MemberAvatar"); + + let timestamp = null; + if (this.props.lastActiveTs) { + // TODO: [TravisR] Figure out how to i18n this + // `humanize` wants seconds for a timestamp, so divide by 1000 + const humanTs = humanize.relativeTime(this.props.lastActiveTs / 1000); + timestamp = {humanTs}; + } + + return ( +
    + + {this.props.member.name} + {this.props.member.userId} + {timestamp} +
    + ); + } +} + +export default class DMInviteDialog extends React.Component { + static propTypes = { + // Takes an array of user IDs/emails to invite. + onFinished: PropTypes.func.isRequired, + }; + + constructor() { + super(); + + this.state = { + targets: [], // string[] of mxids/email addresses + filterText: "", + recents: this._buildRecents(), + numRecentsShown: INITIAL_ROOMS_SHOWN, + }; + } + + _buildRecents(): {userId: string, user: RoomMember, lastActive: number} { + const rooms = DMRoomMap.shared().getUniqueRoomsWithIndividuals(); + const recents = []; + for (const userId in rooms) { + const room = rooms[userId]; + const member = room.getMember(userId); + if (!member) continue; // just skip people who don't have memberships for some reason + + const lastEventTs = room.timeline && room.timeline.length ? room.timeline[room.timeline.length - 1].getTs() : 0; + if (!lastEventTs) continue; // something weird is going on with this room + + recents.push({userId, user: member, lastActive: lastEventTs}); + } + + // Sort the recents by last active to save us time later + recents.sort((a, b) => b.lastActive - a.lastActive); + + return recents; + } + + _startDm = () => { + this.props.onFinished(this.state.targets); + }; + + _cancel = () => { + this.props.onFinished([]); + }; + + _updateFilter = (e) => { + this.setState({filterText: e.target.value}); + }; + + _showMoreRecents = () => { + this.setState({numRecentsShown: this.state.numRecentsShown + INCREMENT_ROOMS_SHOWN}); + }; + + _toggleMember = (userId) => { + const targets = this.state.targets.map(t => t); // cheap clone for mutation + const idx = targets.indexOf(userId); + if (idx >= 0) targets.splice(idx, 1); + else targets.push(userId); + this.setState({targets}); + }; + + _renderRecents() { + if (!this.state.recents || this.state.recents.length === 0) return null; + + // .slice() will return an incomplete array but won't error on us if we go too far + const toRender = this.state.recents.slice(0, this.state.numRecentsShown); + const hasMore = toRender.length < this.state.recents.length; + + const AccessibleButton = sdk.getComponent("elements.AccessibleButton"); + let showMore = null; + if (hasMore) { + showMore = ( + + {_t("Show more")} + + ) + } + + return ( +
    +

    {_t("Recent Conversations")}

    + {toRender.map(r => )} + {showMore} +
    + ) + } + + render() { + const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); + const Field = sdk.getComponent("elements.Field"); + const AccessibleButton = sdk.getComponent("elements.AccessibleButton"); + + // Dev note: The use of Field is temporary/incomplete pending https://github.com/vector-im/riot-web/issues/11197 + // For now, we just list who the targets are. + const editor = ( +
    + +
    + ); + const targets = this.state.targets.map(t =>
    {t}
    ); + + const userId = MatrixClientPeg.get().getUserId(); + return ( + +
    +

    + {_t( + "If you can't find someone, ask them for their username, or share your " + + "username (%(userId)s) or profile link.", + {userId}, + {a: (sub) => {sub}}, + )} +

    + {targets} +
    + {editor} + + {_t("Go")} + +
    + {this._renderRecents()} +
    +
    + ); + } +} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 6979759cd2..18c3d76e12 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -358,6 +358,7 @@ "Render simple counters in room header": "Render simple counters in room header", "Multiple integration managers": "Multiple integration managers", "Try out new ways to ignore people (experimental)": "Try out new ways to ignore people (experimental)", + "New DM invite dialog (under development)": "New DM invite dialog (under development)", "Enable cross-signing to verify per-user instead of per-device (in development)": "Enable cross-signing to verify per-user instead of per-device (in development)", "Enable local event indexing and E2EE search (requires restart)": "Enable local event indexing and E2EE search (requires restart)", "Use the new, faster, composer for writing messages": "Use the new, faster, composer for writing messages", @@ -1431,6 +1432,11 @@ "View Servers in Room": "View Servers in Room", "Toolbox": "Toolbox", "Developer Tools": "Developer Tools", + "Show more": "Show more", + "Recent Conversations": "Recent Conversations", + "Direct Messages": "Direct Messages", + "If you can't find someone, ask them for their username, or share your username (%(userId)s) or profile link.": "If you can't find someone, ask them for their username, or share your username (%(userId)s) or profile link.", + "Go": "Go", "An error has occurred.": "An error has occurred.", "Verify this user to mark them as trusted. Trusting users gives you extra peace of mind when using end-to-end encrypted messages.": "Verify this user to mark them as trusted. Trusting users gives you extra peace of mind when using end-to-end encrypted messages.", "Verifying this user will mark their device as trusted, and also mark your device as trusted to them.": "Verifying this user will mark their device as trusted, and also mark your device as trusted to them.", diff --git a/src/settings/Settings.js b/src/settings/Settings.js index f1299a9045..d606528ca3 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -128,6 +128,12 @@ export const SETTINGS = { supportedLevels: LEVELS_FEATURE, default: false, }, + "feature_ftue_dms": { + isFeature: true, + displayName: _td("New DM invite dialog (under development)"), + supportedLevels: LEVELS_FEATURE, + default: false, + }, "mjolnirRooms": { supportedLevels: ['account'], default: [], diff --git a/src/utils/DMRoomMap.js b/src/utils/DMRoomMap.js index af65b6f001..498c073e0e 100644 --- a/src/utils/DMRoomMap.js +++ b/src/utils/DMRoomMap.js @@ -1,5 +1,6 @@ /* Copyright 2016 OpenMarket Ltd +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. @@ -16,6 +17,7 @@ limitations under the License. import MatrixClientPeg from '../MatrixClientPeg'; import _uniq from 'lodash/uniq'; +import {Room} from "matrix-js-sdk/lib/matrix"; /** * Class that takes a Matrix Client and flips the m.direct map @@ -144,6 +146,13 @@ export default class DMRoomMap { return this.roomToUser[roomId]; } + getUniqueRoomsWithIndividuals(): {[userId: string]: Room} { + return Object.keys(this.roomToUser) + .map(r => ({userId: this.getUserIdForRoomId(r), room: this.matrixClient.getRoom(r)})) + .filter(r => r.userId && r.room && r.room.getInvitedAndJoinedMemberCount() === 2) + .reduce((obj, r) => (obj[r.userId] = r.room) && obj, {}); + } + _getUserToRooms() { if (!this.userToRooms) { const userToRooms = this.mDirectEvent; diff --git a/yarn.lock b/yarn.lock index b8b877ab62..fc2b9e04c4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4089,6 +4089,11 @@ humanize-ms@^1.2.1: dependencies: ms "^2.0.0" +humanize@^0.0.9: + version "0.0.9" + resolved "https://registry.yarnpkg.com/humanize/-/humanize-0.0.9.tgz#1994ffaecdfe9c441ed2bdac7452b7bb4c9e41a4" + integrity sha1-GZT/rs3+nEQe0r2sdFK3u0yeQaQ= + iconv-lite@0.4.24, iconv-lite@^0.4.24, iconv-lite@^0.4.4, iconv-lite@~0.4.13: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" From 3488eaba3cbed69f552d6a66d4e383b31768217a Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 2 Jan 2020 17:44:19 -0700 Subject: [PATCH 70/93] Appease the linter --- src/components/views/dialogs/DMInviteDialog.js | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/components/views/dialogs/DMInviteDialog.js b/src/components/views/dialogs/DMInviteDialog.js index 5b67112c14..6456ef7083 100644 --- a/src/components/views/dialogs/DMInviteDialog.js +++ b/src/components/views/dialogs/DMInviteDialog.js @@ -95,7 +95,9 @@ export default class DMInviteDialog extends React.Component { const member = room.getMember(userId); if (!member) continue; // just skip people who don't have memberships for some reason - const lastEventTs = room.timeline && room.timeline.length ? room.timeline[room.timeline.length - 1].getTs() : 0; + const lastEventTs = room.timeline && room.timeline.length + ? room.timeline[room.timeline.length - 1].getTs() + : 0; if (!lastEventTs) continue; // something weird is going on with this room recents.push({userId, user: member, lastActive: lastEventTs}); @@ -145,16 +147,19 @@ export default class DMInviteDialog extends React.Component { {_t("Show more")} - ) + ); } + const tiles = toRender.map(r => ( + + )); return (

    {_t("Recent Conversations")}

    - {toRender.map(r => )} + {tiles} {showMore}
    - ) + ); } render() { @@ -170,7 +175,7 @@ export default class DMInviteDialog extends React.Component { id="inviteTargets" value={this.state.filterText} onChange={this._updateFilter} - placeholder="TODO: Implement filtering/searching (https://github.com/vector-im/riot-web/issues/11199)" + placeholder="TODO: Implement filtering/searching (vector-im/riot-web#11199)" />
    ); From 6f1525c1f341c1dd7fc64727f6a4f22971df4ebc Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 2 Jan 2020 17:47:26 -0700 Subject: [PATCH 71/93] Appease the scss linter --- res/css/views/dialogs/_DMInviteDialog.scss | 94 +++++++++++----------- 1 file changed, 47 insertions(+), 47 deletions(-) diff --git a/res/css/views/dialogs/_DMInviteDialog.scss b/res/css/views/dialogs/_DMInviteDialog.scss index 50e9b0a15f..1153ecb0d4 100644 --- a/res/css/views/dialogs/_DMInviteDialog.scss +++ b/res/css/views/dialogs/_DMInviteDialog.scss @@ -15,67 +15,67 @@ limitations under the License. */ .mx_DMInviteDialog_addressBar { - display: flex; - flex-direction: row; + display: flex; + flex-direction: row; - .mx_DMInviteDialog_editor { - flex: 1; - width: 100%; // Needed to make the Field inside grow - } + .mx_DMInviteDialog_editor { + flex: 1; + width: 100%; // Needed to make the Field inside grow + } - .mx_Field { - margin: 0; - } + .mx_Field { + margin: 0; + } - .mx_DMInviteDialog_goButton { - width: 48px; - margin-left: 10px; - } + .mx_DMInviteDialog_goButton { + width: 48px; + margin-left: 10px; + } } .mx_DMInviteDialog_section { - padding-bottom: 10px; + padding-bottom: 10px; - h3 { - font-size: 12px; - color: $muted-fg-color; - font-weight: bold; - text-transform: uppercase; - } + h3 { + font-size: 12px; + color: $muted-fg-color; + font-weight: bold; + text-transform: uppercase; + } } .mx_DMInviteDialog_roomTile { - cursor: pointer; - padding: 5px 10px; + cursor: pointer; + padding: 5px 10px; - &:hover { - background-color: $user-tile-hover-bg-color; - border-radius: 4px; - } + &:hover { + background-color: $user-tile-hover-bg-color; + border-radius: 4px; + } - * { - vertical-align: middle; - } + * { + vertical-align: middle; + } - .mx_DMInviteDialog_roomTile_name { - font-weight: 600; - font-size: 14px; - color: $primary-fg-color; - margin-left: 7px; - } + .mx_DMInviteDialog_roomTile_name { + font-weight: 600; + font-size: 14px; + color: $primary-fg-color; + margin-left: 7px; + } - .mx_DMInviteDialog_roomTile_userId { - font-size: 12px; - color: $muted-fg-color; - margin-left: 7px; - } + .mx_DMInviteDialog_roomTile_userId { + font-size: 12px; + color: $muted-fg-color; + margin-left: 7px; + } - .mx_DMInviteDialog_roomTile_time { - text-align: right; - font-size: 12px; - color: $muted-fg-color; - float: right; - line-height: 36px; // Height of the avatar to keep the time vertically aligned - } + .mx_DMInviteDialog_roomTile_time { + text-align: right; + font-size: 12px; + color: $muted-fg-color; + float: right; + line-height: 36px; // Height of the avatar to keep the time vertically aligned + } } From 557669b08e3734bf1e0e6fa871f17d76968af0fb Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 3 Jan 2020 11:12:55 +0000 Subject: [PATCH 72/93] Don't crash if a keyshare request is removed ...during the time the dialog is displayed. Fixes https://github.com/vector-im/riot-web/issues/11745 (hopefully) --- src/KeyRequestHandler.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/KeyRequestHandler.js b/src/KeyRequestHandler.js index c3de7988b2..4ee258da53 100644 --- a/src/KeyRequestHandler.js +++ b/src/KeyRequestHandler.js @@ -111,6 +111,12 @@ export default class KeyRequestHandler { this._currentUser = null; this._currentDevice = null; + if (!this._pendingKeyRequests[userId] || !this._pendingKeyRequests[userId][deviceId]) { + // request was removed in the time the dialog was displayed + this._processNextRequest(); + return; + } + if (r) { for (const req of this._pendingKeyRequests[userId][deviceId]) { req.share(); From c2723176e403dcb16a11e57a40db5e800e0fc653 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 3 Jan 2020 12:08:35 +0000 Subject: [PATCH 73/93] Convert /verify to checkDeviceTrust Also de-promiseify the code a bit --- src/SlashCommands.js | 86 ++++++++++++++++++++++---------------------- 1 file changed, 42 insertions(+), 44 deletions(-) diff --git a/src/SlashCommands.js b/src/SlashCommands.js index a9c015fdaf..21fa4a134e 100644 --- a/src/SlashCommands.js +++ b/src/SlashCommands.js @@ -780,54 +780,52 @@ export const CommandMap = { const deviceId = matches[2]; const fingerprint = matches[3]; - return success( - // Promise.resolve to handle transition from static result to promise; can be removed - // in future - Promise.resolve(cli.getStoredDevice(userId, deviceId)).then((device) => { - if (!device) { - throw new Error(_t('Unknown (user, device) pair:') + ` (${userId}, ${deviceId})`); - } + return success((async () => { + const device = await cli.getStoredDevice(userId, deviceId); + if (!device) { + throw new Error(_t('Unknown (user, device) pair:') + ` (${userId}, ${deviceId})`); + } + const deviceTrust = await cli.checkDeviceTrust(userId, deviceId); - if (device.isVerified()) { - if (device.getFingerprint() === fingerprint) { - throw new Error(_t('Device already verified!')); - } else { - throw new Error(_t('WARNING: Device already verified, but keys do NOT MATCH!')); - } + if (deviceTrust.isVerified()) { + if (device.getFingerprint() === fingerprint) { + throw new Error(_t('Device already verified!')); + } else { + throw new Error(_t('WARNING: Device already verified, but keys do NOT MATCH!')); } + } - if (device.getFingerprint() !== fingerprint) { - const fprint = device.getFingerprint(); - throw new Error( - _t('WARNING: KEY VERIFICATION FAILED! The signing key for %(userId)s and device' + - ' %(deviceId)s is "%(fprint)s" which does not match the provided key ' + - '"%(fingerprint)s". This could mean your communications are being intercepted!', - { - fprint, - userId, - deviceId, - fingerprint, - })); - } + if (device.getFingerprint() !== fingerprint) { + const fprint = device.getFingerprint(); + throw new Error( + _t('WARNING: KEY VERIFICATION FAILED! The signing key for %(userId)s and device' + + ' %(deviceId)s is "%(fprint)s" which does not match the provided key ' + + '"%(fingerprint)s". This could mean your communications are being intercepted!', + { + fprint, + userId, + deviceId, + fingerprint, + })); + } - return cli.setDeviceVerified(userId, deviceId, true); - }).then(() => { - // Tell the user we verified everything - const InfoDialog = sdk.getComponent('dialogs.InfoDialog'); - Modal.createTrackedDialog('Slash Commands', 'Verified key', InfoDialog, { - title: _t('Verified key'), - description:
    -

    - { - _t('The signing key you provided matches the signing key you received ' + - 'from %(userId)s\'s device %(deviceId)s. Device marked as verified.', - {userId, deviceId}) - } -

    -
    , - }); - }), - ); + await cli.setDeviceVerified(userId, deviceId, true); + + // Tell the user we verified everything + const InfoDialog = sdk.getComponent('dialogs.InfoDialog'); + Modal.createTrackedDialog('Slash Commands', 'Verified key', InfoDialog, { + title: _t('Verified key'), + description:
    +

    + { + _t('The signing key you provided matches the signing key you received ' + + 'from %(userId)s\'s device %(deviceId)s. Device marked as verified.', + {userId, deviceId}) + } +

    +
    , + }); + })()); } } return reject(this.getUsage()); From 99559c5121e5759b6a6801b66db5d13a6f558388 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Fri, 3 Jan 2020 13:33:32 +0000 Subject: [PATCH 74/93] Update backup restore paths for SSSS This updates all the various key backup entry points to ensure they use a flow that supports backups stored under secret storage. --- .../keybackup/NewRecoveryMethodDialog.js | 11 +++-- src/components/views/dialogs/LogoutDialog.js | 6 ++- .../keybackup/RestoreKeyBackupDialog.js | 47 +++++++++++++++++-- .../views/rooms/RoomRecoveryReminder.js | 6 ++- .../views/settings/KeyBackupPanel.js | 23 +++------ 5 files changed, 66 insertions(+), 27 deletions(-) diff --git a/src/async-components/views/dialogs/keybackup/NewRecoveryMethodDialog.js b/src/async-components/views/dialogs/keybackup/NewRecoveryMethodDialog.js index 28281af771..147f109113 100644 --- a/src/async-components/views/dialogs/keybackup/NewRecoveryMethodDialog.js +++ b/src/async-components/views/dialogs/keybackup/NewRecoveryMethodDialog.js @@ -1,5 +1,6 @@ /* -Copyright 2018-2019 New Vector Ltd +Copyright 2018, 2019 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. @@ -40,9 +41,11 @@ export default class NewRecoveryMethodDialog extends React.PureComponent { onSetupClick = async () => { const RestoreKeyBackupDialog = sdk.getComponent('dialogs.keybackup.RestoreKeyBackupDialog'); - Modal.createTrackedDialog('Restore Backup', '', RestoreKeyBackupDialog, { - onFinished: this.props.onFinished, - }); + Modal.createTrackedDialog( + 'Restore Backup', '', RestoreKeyBackupDialog, { + onFinished: this.props.onFinished, + }, null, /* priority = */ false, /* static = */ true, + ); } render() { diff --git a/src/components/views/dialogs/LogoutDialog.js b/src/components/views/dialogs/LogoutDialog.js index 47d4153494..6e4f950830 100644 --- a/src/components/views/dialogs/LogoutDialog.js +++ b/src/components/views/dialogs/LogoutDialog.js @@ -1,5 +1,6 @@ /* Copyright 2018, 2019 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. @@ -94,7 +95,10 @@ export default class LogoutDialog extends React.Component { // verified, so restore the backup which will give us the keys from it and // allow us to trust it (ie. upload keys to it) const RestoreKeyBackupDialog = sdk.getComponent('dialogs.keybackup.RestoreKeyBackupDialog'); - Modal.createTrackedDialog('Restore Backup', '', RestoreKeyBackupDialog, {}); + Modal.createTrackedDialog( + 'Restore Backup', '', RestoreKeyBackupDialog, null, null, + /* priority = */ false, /* static = */ true, + ); } else { Modal.createTrackedDialogAsync("Key Backup", "Key Backup", import("../../../async-components/views/dialogs/keybackup/CreateKeyBackupDialog"), diff --git a/src/components/views/dialogs/keybackup/RestoreKeyBackupDialog.js b/src/components/views/dialogs/keybackup/RestoreKeyBackupDialog.js index 45168c381e..2881cc920c 100644 --- a/src/components/views/dialogs/keybackup/RestoreKeyBackupDialog.js +++ b/src/components/views/dialogs/keybackup/RestoreKeyBackupDialog.js @@ -1,5 +1,6 @@ /* Copyright 2018, 2019 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,17 +16,18 @@ limitations under the License. */ import React from 'react'; +import { MatrixClient } from 'matrix-js-sdk'; + import sdk from '../../../../index'; import MatrixClientPeg from '../../../../MatrixClientPeg'; import Modal from '../../../../Modal'; - -import { MatrixClient } from 'matrix-js-sdk'; - import { _t } from '../../../../languageHandler'; import {Key} from "../../../../Keyboard"; +import { accessSecretStorage } from '../../../../CrossSigningManager'; const RESTORE_TYPE_PASSPHRASE = 0; const RESTORE_TYPE_RECOVERYKEY = 1; +const RESTORE_TYPE_SECRET_STORAGE = 2; /* * Dialog for restoring e2e keys from a backup and the user's recovery key @@ -35,6 +37,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent { super(props); this.state = { backupInfo: null, + backupKeyStored: null, loading: false, loadError: null, restoreError: null, @@ -148,6 +151,32 @@ export default class RestoreKeyBackupDialog extends React.PureComponent { } } + async _restoreWithSecretStorage() { + this.setState({ + loading: true, + restoreError: null, + restoreType: RESTORE_TYPE_SECRET_STORAGE, + }); + try { + // `accessSecretStorage` may prompt for storage access as needed. + const recoverInfo = await accessSecretStorage(async () => { + return MatrixClientPeg.get().restoreKeyBackupWithSecretStorage( + this.state.backupInfo, + ); + }); + this.setState({ + loading: false, + recoverInfo, + }); + } catch (e) { + console.log("Error restoring backup", e); + this.setState({ + restoreError: e, + loading: false, + }); + } + } + async _loadBackupStatus() { this.setState({ loading: true, @@ -155,10 +184,20 @@ export default class RestoreKeyBackupDialog extends React.PureComponent { }); try { const backupInfo = await MatrixClientPeg.get().getKeyBackupVersion(); + const backupKeyStored = await MatrixClientPeg.get().isKeyBackupKeyStored(); + this.setState({ + backupInfo, + backupKeyStored, + }); + + // If the backup key is stored, we can proceed directly to restore. + if (backupKeyStored) { + return this._restoreWithSecretStorage(); + } + this.setState({ loadError: null, loading: false, - backupInfo, }); } catch (e) { console.log("Error loading backup status", e); diff --git a/src/components/views/rooms/RoomRecoveryReminder.js b/src/components/views/rooms/RoomRecoveryReminder.js index 6b7366bc4f..495364bf4c 100644 --- a/src/components/views/rooms/RoomRecoveryReminder.js +++ b/src/components/views/rooms/RoomRecoveryReminder.js @@ -1,5 +1,6 @@ /* Copyright 2018, 2019 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. @@ -70,7 +71,10 @@ export default class RoomRecoveryReminder extends React.PureComponent { // verified, so restore the backup which will give us the keys from it and // allow us to trust it (ie. upload keys to it) const RestoreKeyBackupDialog = sdk.getComponent('dialogs.keybackup.RestoreKeyBackupDialog'); - Modal.createTrackedDialog('Restore Backup', '', RestoreKeyBackupDialog, {}); + Modal.createTrackedDialog( + 'Restore Backup', '', RestoreKeyBackupDialog, null, null, + /* priority = */ false, /* static = */ true, + ); } else { Modal.createTrackedDialogAsync("Key Backup", "Key Backup", import("../../../async-components/views/dialogs/keybackup/CreateKeyBackupDialog"), diff --git a/src/components/views/settings/KeyBackupPanel.js b/src/components/views/settings/KeyBackupPanel.js index 559b1e0ba1..55bfadba88 100644 --- a/src/components/views/settings/KeyBackupPanel.js +++ b/src/components/views/settings/KeyBackupPanel.js @@ -1,6 +1,6 @@ /* Copyright 2018 New Vector Ltd -Copyright 2019 The Matrix.org Foundation C.I.C. +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. @@ -181,22 +181,11 @@ export default class KeyBackupPanel extends React.PureComponent { } _restoreBackup = async () => { - // Use legacy path if backup key not stored in secret storage - if (!this.state.backupKeyStored) { - const RestoreKeyBackupDialog = sdk.getComponent('dialogs.keybackup.RestoreKeyBackupDialog'); - Modal.createTrackedDialog('Restore Backup', '', RestoreKeyBackupDialog); - return; - } - - try { - await accessSecretStorage(async () => { - await MatrixClientPeg.get().restoreKeyBackupWithSecretStorage( - this.state.backupInfo, - ); - }); - } catch (e) { - console.log("Error restoring backup", e); - } + const RestoreKeyBackupDialog = sdk.getComponent('dialogs.keybackup.RestoreKeyBackupDialog'); + Modal.createTrackedDialog( + 'Restore Backup', '', RestoreKeyBackupDialog, null, null, + /* priority = */ false, /* static = */ true, + ); } render() { From 43e4f2dcc0f5d970bd4b4021a713b58358e213f0 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 3 Jan 2020 13:34:43 +0000 Subject: [PATCH 75/93] Use deviceTrust when displaying key backup trust status Requires https://github.com/matrix-org/matrix-js-sdk/pull/1138 --- src/components/views/settings/KeyBackupPanel.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/settings/KeyBackupPanel.js b/src/components/views/settings/KeyBackupPanel.js index 559b1e0ba1..9f20288fff 100644 --- a/src/components/views/settings/KeyBackupPanel.js +++ b/src/components/views/settings/KeyBackupPanel.js @@ -270,7 +270,7 @@ export default class KeyBackupPanel extends React.PureComponent { {sub} ; const verify = sub => - + {sub} ; const device = sub => {deviceName}; From d5a82a5fc2e9057ec94873e7c5f595ab4c79d569 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Fri, 3 Jan 2020 13:45:52 +0000 Subject: [PATCH 76/93] Finish sentence in accessSecretStorage docs --- src/CrossSigningManager.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CrossSigningManager.js b/src/CrossSigningManager.js index ab0a22e4d5..f3953b1897 100644 --- a/src/CrossSigningManager.js +++ b/src/CrossSigningManager.js @@ -97,7 +97,7 @@ export const crossSigningCallbacks = { * * Additionally, the secret storage keys are cached during the scope of this function * to ensure the user is prompted only once for their secret storage - * passphrase. The cache is then + * passphrase. The cache is then cleared once the provided function completes. * * @param {Function} [func] An operation to perform once secret storage has been * bootstrapped. Optional. From 4211ec5063d1e531ffe2938dc2ea0b6088253bf4 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Fri, 3 Jan 2020 13:51:42 +0000 Subject: [PATCH 77/93] Fix DOM structure in RoomRecoveryReminder Buttons (which end up as
    s) aren't allowed inside

    s. --- res/css/views/rooms/_RoomRecoveryReminder.scss | 1 + src/components/views/rooms/RoomRecoveryReminder.js | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/res/css/views/rooms/_RoomRecoveryReminder.scss b/res/css/views/rooms/_RoomRecoveryReminder.scss index 68e2bf861e..85d42ca4b4 100644 --- a/res/css/views/rooms/_RoomRecoveryReminder.scss +++ b/res/css/views/rooms/_RoomRecoveryReminder.scss @@ -40,4 +40,5 @@ limitations under the License. .mx_RoomRecoveryReminder_secondary { font-size: 90%; + margin-top: 1em; } diff --git a/src/components/views/rooms/RoomRecoveryReminder.js b/src/components/views/rooms/RoomRecoveryReminder.js index 495364bf4c..8554c804c0 100644 --- a/src/components/views/rooms/RoomRecoveryReminder.js +++ b/src/components/views/rooms/RoomRecoveryReminder.js @@ -154,14 +154,14 @@ export default class RoomRecoveryReminder extends React.PureComponent { onClick={this.onSetupClick}> {setupCaption} -

    { _t("Not now") } -

    -

    + { _t("Don't ask me again") } -

    +
    ); From 668479d94a477976b4113906b21cce56f01458ac Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Fri, 3 Jan 2020 13:57:59 +0000 Subject: [PATCH 78/93] Tweak as per git review --- .../settings/tabs/room/BridgeSettingsTab.js | 69 +++++++++---------- src/i18n/strings/en_EN.json | 8 +-- 2 files changed, 35 insertions(+), 42 deletions(-) diff --git a/src/components/views/settings/tabs/room/BridgeSettingsTab.js b/src/components/views/settings/tabs/room/BridgeSettingsTab.js index 82382e7828..9e6bdf8958 100644 --- a/src/components/views/settings/tabs/room/BridgeSettingsTab.js +++ b/src/components/views/settings/tabs/room/BridgeSettingsTab.js @@ -1,5 +1,5 @@ /* -Copyright 2019 New Vector Ltd +Copyright 2019 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -33,17 +33,6 @@ export default class BridgeSettingsTab extends React.Component { roomId: PropTypes.string.isRequired, }; - constructor() { - super(); - - this.state = { - }; - } - - componentWillMount() { - - } - _renderBridgeCard(event, room) { const content = event.getContent(); if (!content || !content.channel || !content.protocol) { @@ -56,21 +45,18 @@ export default class BridgeSettingsTab extends React.Component { let creator = null; if (content.creator) { - const pill = ; - creator = (

    { - _t("This bridge was provisioned by %(pill)s", { - pill, - }) - }

    ); + creator =

    { _t("This bridge was provisioned by ", {}, { + user: , + })}

    ; } - const bot = (

    {_t("This bridge is managed by the %(pill)s bot user.", { - pill: {_t("This bridge is managed by the bot user.", {}, { + user: ); let channelLink = channelName; if (channel.external_url) { - channelLink = {channelName}; + channelLink = {channelName}; } let networkLink = networkName; if (network && network.external_url) { - networkLink = {networkName}; + networkLink = {networkName}; } const chanAndNetworkInfo = ( - (_t("Bridged into %(channelLink)s %(networkLink)s, on %(protocolName)s", { + _t("Bridged into , on ", {}, { channelLink, networkLink, protocolName, - })) + }) ); let networkIcon = null; @@ -101,9 +87,13 @@ export default class BridgeSettingsTab extends React.Component { MatrixClientPeg.get().getHomeserverUrl(), network.avatar, 32, 32, "crop", ); - networkIcon = ; + networkIcon = ; } let channelIcon = null; @@ -112,13 +102,16 @@ export default class BridgeSettingsTab extends React.Component { MatrixClientPeg.get().getHomeserverUrl(), channel.avatar, 32, 32, "crop", ); - console.log(channel.avatar); - channelIcon = ; + channelIcon = ; } - const heading = _t("Connected to %(channelIcon)s %(channelName)s on %(networkIcon)s %(networkName)s", { + const heading = _t("Connected to on ", { }, { channelIcon, channelName, networkName, @@ -127,7 +120,7 @@ export default class BridgeSettingsTab extends React.Component { return (

  • -

    {heading}

    +

    {heading}

    {_t("Connected via %(protocolName)s", { protocolName })}

    {creator} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 6fcfd2eb95..1f66891857 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -763,10 +763,10 @@ "Room version:": "Room version:", "Developer options": "Developer options", "Open Devtools": "Open Devtools", - "This bridge was provisioned by %(pill)s": "This bridge was provisioned by %(pill)s", - "This bridge is managed by the %(pill)s bot user.": "This bridge is managed by the %(pill)s bot user.", - "Bridged into %(channelLink)s %(networkLink)s, on %(protocolName)s": "Bridged into %(channelLink)s %(networkLink)s, on %(protocolName)s", - "Connected to %(channelIcon)s %(channelName)s on %(networkIcon)s %(networkName)s": "Connected to %(channelIcon)s %(channelName)s on %(networkIcon)s %(networkName)s", + "This bridge was provisioned by ": "This bridge was provisioned by ", + "This bridge is managed by the bot user.": "This bridge is managed by the bot user.", + "Bridged into , on ": "Bridged into , on ", + "Connected to on ": "Connected to on ", "Connected via %(protocolName)s": "Connected via %(protocolName)s", "Bridge Info": "Bridge Info", "Below is a list of bridges connected to this room.": "Below is a list of bridges connected to this room.", From 831053de112921eaffafac5d94933e0e6dcc6a88 Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Fri, 3 Jan 2020 13:58:07 +0000 Subject: [PATCH 79/93] Make bridge info cards more obvious --- res/css/views/dialogs/_RoomSettingsDialog.scss | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/res/css/views/dialogs/_RoomSettingsDialog.scss b/res/css/views/dialogs/_RoomSettingsDialog.scss index 4b13684d9f..aa66e97f9e 100644 --- a/res/css/views/dialogs/_RoomSettingsDialog.scss +++ b/res/css/views/dialogs/_RoomSettingsDialog.scss @@ -62,8 +62,10 @@ limitations under the License. .mx_RoomSettingsDialog_BridgeList li { list-style-type: none; - padding: 0; - margin: 0; - border-bottom: 1px solid $panel-divider-color; + padding: 5px; + margin-bottom: 5px; + border-width: 1px 0px; + border-color: #dee1f3; + border-style: solid; } From 50e19ba43d64e168e670e4657153f9359477bcb6 Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Fri, 3 Jan 2020 14:04:59 +0000 Subject: [PATCH 80/93] User may not be a bot, therefore do not imply it. --- src/components/views/settings/tabs/room/BridgeSettingsTab.js | 2 +- src/i18n/strings/en_EN.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/settings/tabs/room/BridgeSettingsTab.js b/src/components/views/settings/tabs/room/BridgeSettingsTab.js index 9e6bdf8958..c3c85ec31c 100644 --- a/src/components/views/settings/tabs/room/BridgeSettingsTab.js +++ b/src/components/views/settings/tabs/room/BridgeSettingsTab.js @@ -55,7 +55,7 @@ export default class BridgeSettingsTab extends React.Component { })}

    ; } - const bot = (

    {_t("This bridge is managed by the bot user.", {}, { + const bot = (

    {_t("This bridge is managed by .", {}, { user: ": "This bridge was provisioned by ", - "This bridge is managed by the bot user.": "This bridge is managed by the bot user.", + "This bridge is managed by .": "This bridge is managed by .", "Bridged into , on ": "Bridged into , on ", "Connected to on ": "Connected to on ", "Connected via %(protocolName)s": "Connected via %(protocolName)s", From 5897c8ca7ff5c20ef9f027097c6dae3735d52887 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 3 Jan 2020 15:00:51 +0000 Subject: [PATCH 81/93] Remove 'unverify' from UserInfoPanel It's not in the designs and it's not a thing we can do with cross-signing (at least not at the moment). --- src/components/views/right_panel/UserInfo.js | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/src/components/views/right_panel/UserInfo.js b/src/components/views/right_panel/UserInfo.js index 208c5e8906..809fdcb6d7 100644 --- a/src/components/views/right_panel/UserInfo.js +++ b/src/components/views/right_panel/UserInfo.js @@ -74,17 +74,6 @@ const _getE2EStatus = (cli, userId, devices) => { return "warning"; }; -async function unverifyUser(matrixClient, userId) { - const devices = await matrixClient.getStoredDevicesForUser(userId); - for (const device of devices) { - if (device.isVerified()) { - matrixClient.setDeviceVerified( - userId, device.deviceId, false, - ); - } - } -} - function openDMForUser(matrixClient, userId) { const dmRooms = DMRoomMap.shared().getDMRoomsForUserId(userId); const lastActiveRoom = dmRooms.reduce((lastActiveRoom, roomId) => { @@ -331,14 +320,6 @@ const UserOptionsSection = ({member, isIgnored, canInvite, devices}) => { ); } - let unverifyButton; - if (devices && devices.some(device => device.isVerified())) { - unverifyButton = ( - unverifyUser(cli, member.userId)} className="mx_UserInfo_field mx_UserInfo_destructive"> - { _t('Unverify user') } - - ); - } return (

    @@ -350,7 +331,6 @@ const UserOptionsSection = ({member, isIgnored, canInvite, devices}) => { { insertPillButton } { inviteUserButton } { ignoreButton } - { unverifyButton }
    ); From 5faae1d2f2a0425997c08d5f03b9be80eb136152 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 3 Jan 2020 15:05:41 +0000 Subject: [PATCH 82/93] i18n --- src/i18n/strings/en_EN.json | 1 - 1 file changed, 1 deletion(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 6979759cd2..2065454525 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1114,7 +1114,6 @@ "%(count)s verified sessions|other": "%(count)s verified sessions", "%(count)s verified sessions|one": "1 verified session", "Direct message": "Direct message", - "Unverify user": "Unverify user", "Remove from community": "Remove from community", "Disinvite this user from community?": "Disinvite this user from community?", "Remove this user from community?": "Remove this user from community?", From 2970a9faaffde5f4eb22b33d15d481c9e83501ec Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 3 Jan 2020 15:16:02 +0000 Subject: [PATCH 83/93] Don't fail if logs exists and is an empty dir --- scripts/ci/end-to-end-tests.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/ci/end-to-end-tests.sh b/scripts/ci/end-to-end-tests.sh index ae88ef70c7..a592888292 100755 --- a/scripts/ci/end-to-end-tests.sh +++ b/scripts/ci/end-to-end-tests.sh @@ -36,7 +36,8 @@ echo "--- Install synapse & other dependencies" ./install.sh # install static webserver to server symlinked local copy of riot ./riot/install-webserver.sh -mkdir logs || rm -r logs/* +rm -r logs || true +mkdir logs echo "+++ Running end-to-end tests" TESTS_STARTED=1 ./run.sh --no-sandbox --log-directory logs/ From b8683462e8f0bee0654defe92883e36ef99cef44 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Fri, 3 Jan 2020 15:34:03 +0000 Subject: [PATCH 84/93] Update backup creation paths for SSSS This updates the various backup creation entry points to ensure they support creating with secret storage if the feature flag is enabled. --- .../keybackup/CreateKeyBackupDialog.js | 53 ++++++++++++++++--- .../keybackup/RecoveryMethodRemovedDialog.js | 2 + src/components/views/dialogs/LogoutDialog.js | 1 + .../keybackup/RestoreKeyBackupDialog.js | 2 +- .../views/rooms/RoomRecoveryReminder.js | 1 + .../views/settings/KeyBackupPanel.js | 34 ++++-------- 6 files changed, 61 insertions(+), 32 deletions(-) diff --git a/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js b/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js index 3fac00c1b3..19720e077a 100644 --- a/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js +++ b/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js @@ -1,6 +1,6 @@ /* Copyright 2018, 2019 New Vector Ltd -Copyright 2019 The Matrix.org Foundation C.I.C. +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. @@ -17,11 +17,14 @@ limitations under the License. import React from 'react'; import FileSaver from 'file-saver'; +import PropTypes from 'prop-types'; import sdk from '../../../../index'; import MatrixClientPeg from '../../../../MatrixClientPeg'; import { scorePassword } from '../../../../utils/PasswordScorer'; import { _t } from '../../../../languageHandler'; +import { accessSecretStorage } from '../../../../CrossSigningManager'; +import SettingsStore from '../../../../../lib/settings/SettingsStore'; const PHASE_PASSPHRASE = 0; const PHASE_PASSPHRASE_CONFIRM = 1; @@ -49,10 +52,20 @@ function selectText(target) { * on the server. */ export default class CreateKeyBackupDialog extends React.PureComponent { + static propTypes = { + secureSecretStorage: PropTypes.bool, + onFinished: PropTypes.func.isRequired, + } + constructor(props) { super(props); + this._recoveryKeyNode = null; + this._keyBackupInfo = null; + this._setZxcvbnResultTimeout = null; + this.state = { + secureSecretStorage: props.secureSecretStorage, phase: PHASE_PASSPHRASE, passPhrase: '', passPhraseConfirm: '', @@ -61,12 +74,25 @@ export default class CreateKeyBackupDialog extends React.PureComponent { zxcvbnResult: null, setPassPhrase: false, }; + + if (this.state.secureSecretStorage === undefined) { + this.state.secureSecretStorage = + SettingsStore.isFeatureEnabled("feature_cross_signing"); + } + + // If we're using secret storage, skip ahead to the backing up step, as + // `accessSecretStorage` will handle passphrases as needed. + if (this.state.secureSecretStorage) { + this.state.phase = PHASE_BACKINGUP; + } } - componentWillMount() { - this._recoveryKeyNode = null; - this._keyBackupInfo = null; - this._setZxcvbnResultTimeout = null; + componentDidMount() { + // If we're using secret storage, skip ahead to the backing up step, as + // `accessSecretStorage` will handle passphrases as needed. + if (this.state.secureSecretStorage) { + this._createBackup(); + } } componentWillUnmount() { @@ -103,15 +129,26 @@ export default class CreateKeyBackupDialog extends React.PureComponent { } _createBackup = async () => { + const { secureSecretStorage } = this.state; this.setState({ phase: PHASE_BACKINGUP, error: null, }); let info; try { - info = await MatrixClientPeg.get().createKeyBackupVersion( - this._keyBackupInfo, - ); + if (secureSecretStorage) { + await accessSecretStorage(async () => { + info = await MatrixClientPeg.get().prepareKeyBackupVersion( + null /* random key */, + { secureSecretStorage: true }, + ); + info = await MatrixClientPeg.get().createKeyBackupVersion(info); + }); + } else { + info = await MatrixClientPeg.get().createKeyBackupVersion( + this._keyBackupInfo, + ); + } await MatrixClientPeg.get().scheduleAllGroupSessionsForBackup(); this.setState({ phase: PHASE_DONE, diff --git a/src/async-components/views/dialogs/keybackup/RecoveryMethodRemovedDialog.js b/src/async-components/views/dialogs/keybackup/RecoveryMethodRemovedDialog.js index 1975fbe6d6..4383908e23 100644 --- a/src/async-components/views/dialogs/keybackup/RecoveryMethodRemovedDialog.js +++ b/src/async-components/views/dialogs/keybackup/RecoveryMethodRemovedDialog.js @@ -1,5 +1,6 @@ /* Copyright 2019 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. @@ -35,6 +36,7 @@ export default class RecoveryMethodRemovedDialog extends React.PureComponent { this.props.onFinished(); Modal.createTrackedDialogAsync("Key Backup", "Key Backup", import("./CreateKeyBackupDialog"), + null, null, /* priority = */ false, /* static = */ true, ); } diff --git a/src/components/views/dialogs/LogoutDialog.js b/src/components/views/dialogs/LogoutDialog.js index 6e4f950830..ede03f13cc 100644 --- a/src/components/views/dialogs/LogoutDialog.js +++ b/src/components/views/dialogs/LogoutDialog.js @@ -102,6 +102,7 @@ export default class LogoutDialog extends React.Component { } else { Modal.createTrackedDialogAsync("Key Backup", "Key Backup", import("../../../async-components/views/dialogs/keybackup/CreateKeyBackupDialog"), + null, null, /* priority = */ false, /* static = */ true, ); } diff --git a/src/components/views/dialogs/keybackup/RestoreKeyBackupDialog.js b/src/components/views/dialogs/keybackup/RestoreKeyBackupDialog.js index 2881cc920c..106d8cd6f8 100644 --- a/src/components/views/dialogs/keybackup/RestoreKeyBackupDialog.js +++ b/src/components/views/dialogs/keybackup/RestoreKeyBackupDialog.js @@ -76,7 +76,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent { onFinished: () => { this._loadBackupStatus(); }, - }, + }, null, /* priority = */ false, /* static = */ true, ); } diff --git a/src/components/views/rooms/RoomRecoveryReminder.js b/src/components/views/rooms/RoomRecoveryReminder.js index 8554c804c0..aa8134d680 100644 --- a/src/components/views/rooms/RoomRecoveryReminder.js +++ b/src/components/views/rooms/RoomRecoveryReminder.js @@ -78,6 +78,7 @@ export default class RoomRecoveryReminder extends React.PureComponent { } else { Modal.createTrackedDialogAsync("Key Backup", "Key Backup", import("../../../async-components/views/dialogs/keybackup/CreateKeyBackupDialog"), + null, null, /* priority = */ false, /* static = */ true, ); } } diff --git a/src/components/views/settings/KeyBackupPanel.js b/src/components/views/settings/KeyBackupPanel.js index 55bfadba88..765dd16717 100644 --- a/src/components/views/settings/KeyBackupPanel.js +++ b/src/components/views/settings/KeyBackupPanel.js @@ -128,36 +128,24 @@ export default class KeyBackupPanel extends React.PureComponent { Modal.createTrackedDialogAsync('Key Backup', 'Key Backup', import('../../../async-components/views/dialogs/keybackup/CreateKeyBackupDialog'), { + secureSecretStorage: false, onFinished: () => { this._loadBackupStatus(); }, - }, + }, null, /* priority = */ false, /* static = */ true, ); } _startNewBackupWithSecureSecretStorage = async () => { - const cli = MatrixClientPeg.get(); - let info; - try { - await accessSecretStorage(async () => { - info = await cli.prepareKeyBackupVersion( - null /* random key */, - { secureSecretStorage: true }, - ); - info = await cli.createKeyBackupVersion(info); - }); - await MatrixClientPeg.get().scheduleAllGroupSessionsForBackup(); - this._loadBackupStatus(); - } catch (e) { - console.error("Error creating key backup", e); - // TODO: If creating a version succeeds, but backup fails, should we - // delete the version, disable backup, or do nothing? If we just - // disable without deleting, we'll enable on next app reload since - // it is trusted. - if (info && info.version) { - MatrixClientPeg.get().deleteKeyBackupVersion(info.version); - } - } + Modal.createTrackedDialogAsync('Key Backup', 'Key Backup', + import('../../../async-components/views/dialogs/keybackup/CreateKeyBackupDialog'), + { + secureSecretStorage: true, + onFinished: () => { + this._loadBackupStatus(); + }, + }, null, /* priority = */ false, /* static = */ true, + ); } _deleteBackup = () => { From 2125bcf5a67d37142615b839d5c67cd0e77c082f Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 3 Jan 2020 15:38:59 +0000 Subject: [PATCH 85/93] Comment remaining non-cross-signing-compliant components Fixes https://github.com/vector-im/riot-web/issues/11748 --- src/async-components/views/dialogs/EncryptedEventDialog.js | 3 +++ src/components/views/elements/DeviceVerifyButtons.js | 2 ++ src/components/views/rooms/MemberDeviceInfo.js | 2 ++ 3 files changed, 7 insertions(+) diff --git a/src/async-components/views/dialogs/EncryptedEventDialog.js b/src/async-components/views/dialogs/EncryptedEventDialog.js index 15bb1e046b..ea3c109e05 100644 --- a/src/async-components/views/dialogs/EncryptedEventDialog.js +++ b/src/async-components/views/dialogs/EncryptedEventDialog.js @@ -23,6 +23,9 @@ import { _t } from '../../../languageHandler'; const sdk = require('../../../index'); const MatrixClientPeg = require("../../../MatrixClientPeg"); +// XXX: This component is not cross-signing aware. +// https://github.com/vector-im/riot-web/issues/11752 tracks either updating this +// component or taking it out to pasture. module.exports = createReactClass({ displayName: 'EncryptedEventDialog', diff --git a/src/components/views/elements/DeviceVerifyButtons.js b/src/components/views/elements/DeviceVerifyButtons.js index bb08f8b234..14b4ad1760 100644 --- a/src/components/views/elements/DeviceVerifyButtons.js +++ b/src/components/views/elements/DeviceVerifyButtons.js @@ -22,6 +22,8 @@ import sdk from '../../../index'; import Modal from '../../../Modal'; import { _t } from '../../../languageHandler'; +// XXX: This component is *not* cross-signing aware. Once everything is +// cross-signing, this component should just go away. export default createReactClass({ displayName: 'DeviceVerifyButtons', diff --git a/src/components/views/rooms/MemberDeviceInfo.js b/src/components/views/rooms/MemberDeviceInfo.js index ff88c6f6e6..ba90850b35 100644 --- a/src/components/views/rooms/MemberDeviceInfo.js +++ b/src/components/views/rooms/MemberDeviceInfo.js @@ -23,6 +23,8 @@ import classNames from 'classnames'; export default class MemberDeviceInfo extends React.Component { render() { const DeviceVerifyButtons = sdk.getComponent('elements.DeviceVerifyButtons'); + // XXX: These checks are not cross-signing aware but this component is only used + // from the old, pre-cross-signing memberinfopanel const iconClasses = classNames({ mx_MemberDeviceInfo_icon: true, mx_MemberDeviceInfo_icon_blacklisted: this.props.device.isBlocked(), From e12ed04da8cf2914f002a599ec0d9e5ca079434d Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Fri, 3 Jan 2020 15:59:14 +0000 Subject: [PATCH 86/93] Remove unused import --- src/components/views/settings/KeyBackupPanel.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/views/settings/KeyBackupPanel.js b/src/components/views/settings/KeyBackupPanel.js index 765dd16717..30d0416968 100644 --- a/src/components/views/settings/KeyBackupPanel.js +++ b/src/components/views/settings/KeyBackupPanel.js @@ -22,7 +22,6 @@ import MatrixClientPeg from '../../../MatrixClientPeg'; import { _t } from '../../../languageHandler'; import Modal from '../../../Modal'; import SettingsStore from '../../../../lib/settings/SettingsStore'; -import { accessSecretStorage } from '../../../CrossSigningManager'; export default class KeyBackupPanel extends React.PureComponent { constructor(props) { From 752482964a4c5ce98825011c4797bf4c878b58d5 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 3 Jan 2020 10:24:07 -0700 Subject: [PATCH 87/93] Purify the components --- src/components/views/dialogs/DMInviteDialog.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/dialogs/DMInviteDialog.js b/src/components/views/dialogs/DMInviteDialog.js index 6456ef7083..ff498e3e75 100644 --- a/src/components/views/dialogs/DMInviteDialog.js +++ b/src/components/views/dialogs/DMInviteDialog.js @@ -29,7 +29,7 @@ import * as humanize from "humanize"; const INITIAL_ROOMS_SHOWN = 3; // Number of rooms to show at first const INCREMENT_ROOMS_SHOWN = 5; // Number of rooms to add when 'show more' is clicked -class DMRoomTile extends React.Component { +class DMRoomTile extends React.PureComponent { static propTypes = { member: PropTypes.object.isRequired, lastActiveTs: PropTypes.number, @@ -70,7 +70,7 @@ class DMRoomTile extends React.Component { } } -export default class DMInviteDialog extends React.Component { +export default class DMInviteDialog extends React.PureComponent { static propTypes = { // Takes an array of user IDs/emails to invite. onFinished: PropTypes.func.isRequired, From 2ccc8caa6974ed3787436815aea95e5fc38a5205 Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Sun, 5 Jan 2020 15:50:06 +0000 Subject: [PATCH 88/93] Fix indent --- .../settings/tabs/room/BridgeSettingsTab.js | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/components/views/settings/tabs/room/BridgeSettingsTab.js b/src/components/views/settings/tabs/room/BridgeSettingsTab.js index c3c85ec31c..5536a2eb06 100644 --- a/src/components/views/settings/tabs/room/BridgeSettingsTab.js +++ b/src/components/views/settings/tabs/room/BridgeSettingsTab.js @@ -88,12 +88,12 @@ export default class BridgeSettingsTab extends React.Component { network.avatar, 32, 32, "crop", ); networkIcon = ; + width={32} + height={32} + resizeMethod='crop' + name={ networkName } + idName={ networkName } + url={ avatarUrl } />; } let channelIcon = null; @@ -103,12 +103,12 @@ export default class BridgeSettingsTab extends React.Component { channel.avatar, 32, 32, "crop", ); channelIcon = ; + width={32} + height={32} + resizeMethod='crop' + name={ networkName } + idName={ networkName } + url={ avatarUrl } />; } const heading = _t("Connected to on ", { }, { From 814c0aa4c2469fa71c2372609d85c4185a4320ca Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Sun, 5 Jan 2020 20:52:54 +0000 Subject: [PATCH 89/93] Send enabled_labs over rageshake as comma delimited list Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/rageshake/submit-rageshake.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/rageshake/submit-rageshake.js b/src/rageshake/submit-rageshake.js index 457958eb82..44f1039016 100644 --- a/src/rageshake/submit-rageshake.js +++ b/src/rageshake/submit-rageshake.js @@ -27,6 +27,7 @@ import rageshake from './rageshake'; // polyfill textencoder if necessary import * as TextEncodingUtf8 from 'text-encoding-utf-8'; +import SettingsStore from "../settings/SettingsStore"; let TextEncoder = window.TextEncoder; if (!TextEncoder) { TextEncoder = TextEncodingUtf8.TextEncoder; @@ -85,6 +86,12 @@ export default async function sendBugReport(bugReportEndpoint, opts) { body.append('label', opts.label); } + // add labs options + const enabledLabs = SettingsStore.getLabsFeatures().filter(SettingsStore.isFeatureEnabled); + if (enabledLabs.length) { + body.append('enabled_labs', enabledLabs.join(', ')); + } + if (opts.sendLogs) { progressCallback(_t("Collecting logs")); const logs = await rageshake.getLogsForReport(); From 7f0ed05ee1dc5e03ff718986ba2040621bc41b71 Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Sun, 5 Jan 2020 23:32:49 +0000 Subject: [PATCH 90/93] Update BridgeSettingsTab.js --- .../settings/tabs/room/BridgeSettingsTab.js | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/src/components/views/settings/tabs/room/BridgeSettingsTab.js b/src/components/views/settings/tabs/room/BridgeSettingsTab.js index 5536a2eb06..3022885701 100644 --- a/src/components/views/settings/tabs/room/BridgeSettingsTab.js +++ b/src/components/views/settings/tabs/room/BridgeSettingsTab.js @@ -88,12 +88,13 @@ export default class BridgeSettingsTab extends React.Component { network.avatar, 32, 32, "crop", ); networkIcon = ; + width={32} + height={32} + resizeMethod='crop' + name={ networkName } + idName={ networkName } + url={ avatarUrl } + />; } let channelIcon = null; @@ -103,12 +104,13 @@ export default class BridgeSettingsTab extends React.Component { channel.avatar, 32, 32, "crop", ); channelIcon = ; + width={32} + height={32} + resizeMethod='crop' + name={ networkName } + idName={ networkName } + url={ avatarUrl } + />; } const heading = _t("Connected to on ", { }, { From dcdf68d7e1cb9f9eb4f18c627fe0029d7c57db67 Mon Sep 17 00:00:00 2001 From: Justin Sleep Date: Sun, 5 Jan 2020 17:35:05 -0600 Subject: [PATCH 91/93] Explicitly define diff colors in light theme Signed-off-by: Justin Sleep --- res/themes/dark/css/_dark.scss | 2 +- res/themes/light/css/_light.scss | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/res/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss index d1d0e333a0..212513347d 100644 --- a/res/themes/dark/css/_dark.scss +++ b/res/themes/dark/css/_dark.scss @@ -244,7 +244,7 @@ $breadcrumb-placeholder-bg-color: #272c35; } } -// Fixes diff color inversion by swapping add / del colors +// diff highlight colors .hljs-addition { background: #fdd; } diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss index 0a3ef812b8..f4fc459596 100644 --- a/res/themes/light/css/_light.scss +++ b/res/themes/light/css/_light.scss @@ -338,3 +338,12 @@ $breadcrumb-placeholder-bg-color: #e8eef5; color: $accent-color; text-decoration: none; } + +// diff highlight colors +.hljs-addition { + background: #dfd; +} + +.hljs-deletion { + background: #fdd; +} From 8ec8a7c01534818178259b0708681e9b3f43b50a Mon Sep 17 00:00:00 2001 From: Justin Sleep Date: Sun, 5 Jan 2020 17:58:07 -0600 Subject: [PATCH 92/93] Comment rationale for swapping dark theme colors Signed-off-by: Justin Sleep --- res/themes/dark/css/_dark.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/res/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss index 212513347d..0c47943301 100644 --- a/res/themes/dark/css/_dark.scss +++ b/res/themes/dark/css/_dark.scss @@ -245,6 +245,7 @@ $breadcrumb-placeholder-bg-color: #272c35; } // diff highlight colors +// intentionally swapped to avoid inversion .hljs-addition { background: #fdd; } From 98ede6437e8d0132bec7ebe61a0dd3cc375c2729 Mon Sep 17 00:00:00 2001 From: Aaron Raimist Date: Sun, 5 Jan 2020 23:04:42 -0600 Subject: [PATCH 93/93] Use display name when pinned messages are changed Signed-off-by: Aaron Raimist --- src/TextForEvent.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/TextForEvent.js b/src/TextForEvent.js index c3c8396e26..2e79ff2044 100644 --- a/src/TextForEvent.js +++ b/src/TextForEvent.js @@ -473,7 +473,7 @@ function textForPowerEvent(event) { } function textForPinnedEvent(event) { - const senderName = event.getSender(); + const senderName = event.sender ? event.sender.name : event.getSender(); return _t("%(senderName)s changed the pinned messages for the room.", {senderName}); }