diff --git a/README.md b/README.md index 1265c2bd77..c2e3737b81 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ matrix-react-sdk This is a react-based SDK for inserting a Matrix chat/voip client into a web page. This package provides the React components needed to build a Matrix web client -using React. It is not useable in isolation, and instead must must be used from +using React. It is not useable in isolation, and instead must be used from a 'skin'. A skin provides: * Customised implementations of presentation components. * Custom CSS @@ -82,7 +82,7 @@ practices that anyone working with the SDK needs to be be aware of and uphold: 'Stealing' styling information from other components (including parents) is not cool, as it breaks the independence of the components. - * CSS classes are named with an app-specific namespacing prefix to try to avoid + * CSS classes are named with an app-specific name-spacing prefix to try to avoid CSS collisions. The base skin shipped by Matrix.org with the matrix-react-sdk uses the naming prefix "mx_". A company called Yoyodyne Inc might use a prefix like "yy_" for its app-specific classes. @@ -107,7 +107,7 @@ practices that anyone working with the SDK needs to be be aware of and uphold: .mx_RoomTile {} in RoomList.css - only RoomTile.css is allowed to define its own CSS. Instead, say .mx_RoomList .mx_RoomTile {} to scope the override only to the context of RoomList views. N.B. overrides should be relatively - rare as in general CSS inheritence should be enough. + rare as in general CSS inheritance should be enough. * Components should render only within the bounding box of their outermost DOM element. Page-absolute positioning and negative CSS margins and similar are diff --git a/package.json b/package.json index 8688fe42e6..a1ebc6602d 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", @@ -75,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", @@ -83,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", @@ -100,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 233c781d7f..7a9ebfdf26 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"; @@ -57,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"; @@ -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/structures/_CustomRoomTagPanel.scss b/res/css/structures/_CustomRoomTagPanel.scss index 45961d7be1..1fb18ec41e 100644 --- a/res/css/structures/_CustomRoomTagPanel.scss +++ b/res/css/structures/_CustomRoomTagPanel.scss @@ -26,11 +26,16 @@ 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; + margin: 0 auto; width: 40px; + padding: 10px 0 9px 0; + position: relative; } .mx_CustomRoomTagPanel .mx_BaseAvatar_image { @@ -39,7 +44,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: 2px; // 10 [padding-top] - (56 - 40)/2 } diff --git a/res/css/structures/_TagPanel.scss b/res/css/structures/_TagPanel.scss index b03d36a592..dddd2e324c 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,39 @@ limitations under the License. // opacity: 1; } -.mx_TagPanel .mx_TagTile.mx_TagTile_selected .mx_TagTile_avatar .mx_BaseAvatar { - background-color: $accent-color; - border-radius: 40px; - - /* In case this is a "initial" avatar */ - display: block; +.mx_TagPanel .mx_TagTile_plus { + margin-bottom: 12px; height: 40px; width: 40px; + border-radius: 20px; + 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; + mask-repeat: no-repeat; + content: ''; + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + } } -.mx_TagPanel .mx_TagTile_selected .mx_BaseAvatar_image { - border: 3px solid $accent-color; - height: 40px; - width: 40px; - box-sizing: border-box; +.mx_TagPanel .mx_TagTile.mx_TagTile_selected::before { + content: ''; + height: 56px; + background-color: $accent-color; + width: 5px; + position: absolute; + left: -15px; + border-radius: 0 3px 3px 0; + top: -8px; // (56 - 40)/2 } .mx_TagPanel .mx_TagTile.mx_AccessibleButton:focus { 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/css/views/dialogs/_DMInviteDialog.scss b/res/css/views/dialogs/_DMInviteDialog.scss new file mode 100644 index 0000000000..1153ecb0d4 --- /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/css/views/dialogs/_RoomSettingsDialog.scss b/res/css/views/dialogs/_RoomSettingsDialog.scss index 08839d8493..aa66e97f9e 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'); } @@ -50,3 +55,17 @@ limitations under the License. mask-size: 36px; mask-position: center; } + +.mx_RoomSettingsDialog_BridgeList { + padding: 0; +} + +.mx_RoomSettingsDialog_BridgeList li { + list-style-type: none; + padding: 5px; + margin-bottom: 5px; + border-width: 1px 0px; + border-color: #dee1f3; + border-style: solid; +} + 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/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/res/css/views/rooms/_RoomTile.scss b/res/css/views/rooms/_RoomTile.scss index 1814919b61..e5c7948216 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. 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/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 + + + + + + + + + + + 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/res/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss index eadde4c672..a3515a9d99 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 { @@ -243,3 +246,13 @@ $breadcrumb-placeholder-bg-color: #272c35; } } } + +// diff highlight colors +// intentionally swapped to avoid inversion +.hljs-addition { + background: #fdd; +} + +.hljs-deletion { + background: #dfd; +} diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss index 0a3ef812b8..288fb3cadc 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 { @@ -338,3 +341,12 @@ $breadcrumb-placeholder-bg-color: #e8eef5; color: $accent-color; text-decoration: none; } + +// diff highlight colors +.hljs-addition { + background: #dfd; +} + +.hljs-deletion { + background: #fdd; +} 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/ 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/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; 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. 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/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(); 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/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()); 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}); } 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/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/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/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/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js b/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js index 78ff2a1698..25bc8cdfda 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'); @@ -125,6 +142,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent { } }, createSecretStorageKey: async () => this._keyInfo, + keyBackupInfo: this.state.backupInfo, }); this.setState({ phase: PHASE_DONE, @@ -250,6 +268,27 @@ export default class CreateSecretStorageDialog extends React.PureComponent { return this.state.zxcvbnResult && this.state.zxcvbnResult.score >= PASSWORD_MIN_SCORE; } + _renderPhaseMigrate() { + // 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. + // https://github.com/vector-im/riot-web/issues/11696 + 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 +488,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent { ; } - _renderBusyPhase(text) { + _renderBusyPhase() { const Spinner = sdk.getComponent('views.elements.Spinner'); return
@@ -488,6 +527,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 +566,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/autocomplete/EmojiProvider.js b/src/autocomplete/EmojiProvider.js index 1e39593022..9373ed662e 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,15 @@ 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.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 +96,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 +110,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/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/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/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..796840a625 100644 --- a/src/components/structures/LeftPanel.js +++ b/src/components/structures/LeftPanel.js @@ -19,12 +19,10 @@ 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'; import VectorConferenceHandler from '../../VectorConferenceHandler'; -import TagPanelButtons from './TagPanelButtons'; import SettingsStore from '../../settings/SettingsStore'; import {_t} from "../../languageHandler"; import Analytics from "../../Analytics"; @@ -39,10 +37,6 @@ const LeftPanel = createReactClass({ collapsed: PropTypes.bool.isRequired, }, - contextTypes: { - matrixClient: PropTypes.instanceOf(MatrixClient), - }, - getInitialState: function() { return { searchFilter: '', @@ -243,7 +237,6 @@ const LeftPanel = createReactClass({ tagPanelContainer = (
{ isCustomTagsEnabled ? : undefined } -
); } 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/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; diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 82a682f9ab..af3f4d2598 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. @@ -1466,7 +1456,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()) { @@ -1579,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 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..ff987a8f30 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); } @@ -190,7 +186,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 +205,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/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/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/components/structures/RoomView.js b/src/components/structures/RoomView.js index 739519a2b3..939f422a36 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -25,10 +25,8 @@ 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"; import { _t } from '../../languageHandler'; import {RoomPermalinkCreator} from '../../utils/permalinks/Permalinks'; @@ -55,6 +53,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 +65,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 +162,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); @@ -461,7 +439,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 +483,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); @@ -1953,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 = ( - - - -
- {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..c32da7193e 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)); } }, @@ -104,6 +103,7 @@ const TagPanel = createReactClass({ render() { const DNDTagTile = sdk.getComponent('elements.DNDTagTile'); const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); + const ActionButton = sdk.getComponent('elements.ActionButton'); const TintableSvg = sdk.getComponent('elements.TintableSvg'); const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper"); @@ -154,6 +154,13 @@ 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/avatars/BaseAvatar.js b/src/components/views/avatars/BaseAvatar.js index 82db78615e..59b509889b 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', @@ -38,10 +38,16 @@ 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: { - matrixClient: PropTypes.instanceOf(MatrixClient), + statics: { + contextType: MatrixClientContext, }, getDefaultProps: function() { @@ -59,12 +65,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) { @@ -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/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/context_menus/TopLeftMenu.js b/src/components/views/context_menus/TopLeftMenu.js index b9aabdc608..08a65e2f21 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,18 @@ export class TopLeftMenu extends React.Component {
    {homePageItem} {settingsItem} + {helpItem} {signInOutItem}
; } + openHelp = () => { + this.closeMenu(); + 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/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/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/dialogs/DMInviteDialog.js b/src/components/views/dialogs/DMInviteDialog.js new file mode 100644 index 0000000000..ff498e3e75 --- /dev/null +++ b/src/components/views/dialogs/DMInviteDialog.js @@ -0,0 +1,217 @@ +/* +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.PureComponent { + 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.PureComponent { + 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")} + + ); + } + + const tiles = toRender.map(r => ( + + )); + return ( +
+

{_t("Recent Conversations")}

+ {tiles} + {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/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/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/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/LogoutDialog.js b/src/components/views/dialogs/LogoutDialog.js index 47d4153494..ede03f13cc 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,10 +95,14 @@ 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"), + null, null, /* priority = */ false, /* static = */ true, ); } 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/dialogs/RoomSettingsDialog.js b/src/components/views/dialogs/RoomSettingsDialog.js index 740dc4d2c2..c31fe1992d 100644 --- a/src/components/views/dialogs/RoomSettingsDialog.js +++ b/src/components/views/dialogs/RoomSettingsDialog.js @@ -24,9 +24,11 @@ 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"; +import SettingsStore from "../../../settings/SettingsStore"; export default class RoomSettingsDialog extends React.Component { static propTypes = { @@ -52,6 +54,9 @@ 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; tabs.push(new Tab( _td("General"), @@ -73,6 +78,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", diff --git a/src/components/views/dialogs/keybackup/RestoreKeyBackupDialog.js b/src/components/views/dialogs/keybackup/RestoreKeyBackupDialog.js index 45168c381e..106d8cd6f8 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, @@ -73,7 +76,7 @@ export default class RestoreKeyBackupDialog extends React.PureComponent { onFinished: () => { this._loadBackupStatus(); }, - }, + }, null, /* priority = */ false, /* static = */ true, ); } @@ -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/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/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/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..0af772466b 100644 --- a/src/components/views/elements/Flair.js +++ b/src/components/views/elements/Flair.js @@ -18,9 +18,9 @@ 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"; class FlairAvatar extends React.Component { @@ -40,7 +40,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 +62,7 @@ FlairAvatar.propTypes = { }), }; -FlairAvatar.contextTypes = { - matrixClient: PropTypes.instanceOf(MatrixClient).isRequired, -}; +FlairAvatar.contextType = MatrixClientContext; export default class Flair extends React.Component { constructor() { @@ -92,7 +90,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 +132,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/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/Pill.js b/src/components/views/elements/Pill.js index b821276e92..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 @@ -127,7 +117,7 @@ const Pill = createReactClass({ } break; case Pill.TYPE_USER_MENTION: { - const localMember = nextProps.room ? nextProps.room.getMember(resourceId) : null; + const localMember = nextProps.room ? nextProps.room.getMember(resourceId) : undefined; member = localMember; if (!localMember) { member = new RoomMember(null, resourceId); @@ -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/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/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 e31c1c524e..0b81098124 100644 --- a/src/components/views/elements/TagTile.js +++ b/src/components/views/elements/TagTile.js @@ -20,17 +20,16 @@ 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'; 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"; +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 +45,8 @@ export default createReactClass({ tag: PropTypes.string, }, - contextTypes: { - matrixClient: PropTypes.instanceOf(MatrixClient).isRequired, + statics: { + contextType: MatrixClientContext, }, getInitialState() { @@ -83,7 +82,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; @@ -114,12 +113,10 @@ export default createReactClass({ }, onMouseOver: function() { - console.log("DEBUG onMouseOver"); this.setState({hover: true}); }, onMouseOut: function() { - console.log("DEBUG onMouseOut"); this.setState({hover: false}); }, @@ -142,12 +139,11 @@ 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; - const httpUrl = profile.avatarUrl ? this.context.matrixClient.mxcUrlToHttp( + const httpUrl = profile.avatarUrl ? this.context.mxcUrlToHttp( profile.avatarUrl, avatarHeight, avatarHeight, "crop", ) : null; @@ -166,9 +162,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 ?
@@ -186,14 +179,9 @@ export default createReactClass({ ); } + const AccessibleTooltipButton = sdk.getComponent("elements.AccessibleTooltipButton"); return - +
- { tip } { contextButton } { badgeElement }
-
+ { contextMenu }
; 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/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.")}
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..f3d7418a44 100644 --- a/src/components/views/groups/GroupTile.js +++ b/src/components/views/groups/GroupTile.js @@ -17,11 +17,11 @@ 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'; import FlairStore from '../../../stores/FlairStore'; +import MatrixClientContext from "../../../contexts/MatrixClientContext"; function nop() {} @@ -37,8 +37,8 @@ const GroupTile = createReactClass({ draggable: PropTypes.bool, }, - contextTypes: { - matrixClient: PropTypes.instanceOf(MatrixClient).isRequired, + statics: { + contextType: MatrixClientContext, }, getInitialState() { @@ -56,7 +56,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 +80,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..5c40c19b49 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(); @@ -88,7 +88,7 @@ const ReactButton = ({mxEvent, reactions, onFocusChange}) => { if (menuDisplayed) { const buttonRect = button.current.getBoundingClientRect(); const ReactionPicker = sdk.getComponent('emojipicker.ReactionPicker'); - contextMenu = + contextMenu = ; } @@ -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 65fc0bf66b..809fdcb6d7 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); @@ -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) => { @@ -129,17 +118,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 +139,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 +149,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 +160,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) { @@ -203,7 +208,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; @@ -313,14 +320,6 @@ const UserOptionsSection = withLegacyMatrixClient(({matrixClient: cli, member, i ); } - 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 (
@@ -332,11 +331,10 @@ const UserOptionsSection = withLegacyMatrixClient(({matrixClient: cli, member, i { insertPillButton } { inviteUserButton } { ignoreButton } - { unverifyButton }
); -}); +}; const _warnSelfDemote = async () => { const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); @@ -404,7 +402,12 @@ const useRoomPowerLevels = (cli, room) => { return powerLevels; }; -const RoomKickButton = withLegacyMatrixClient(({matrixClient: cli, member, startUpdating, stopUpdating}) => { +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( @@ -444,9 +447,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 +522,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 +580,209 @@ 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; - } + // 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"); + 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 +858,7 @@ function useRoomPermissions(cli, room, user) { return roomPermissions; } -const PowerLevelSection = withLegacyMatrixClient(({matrixClient: cli, user, room, roomPermissions, powerLevels}) => { +const PowerLevelSection = ({user, room, roomPermissions, powerLevels}) => { 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); @@ -953,7 +964,7 @@ const PowerLevelEditor = withLegacyMatrixClient(({matrixClient: cli, user, room, }); const [confirmed] = await finished; - if (confirmed) return; + if (!confirmed) return; } await _applyPowerChange(roomId, target, powerLevel, powerLevelEvent); } finally { @@ -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]); @@ -1260,11 +1272,20 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room 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 }
); @@ -1316,7 +1337,7 @@ const UserInfo = withLegacyMatrixClient(({matrixClient: cli, user, groupId, room
); -}); +}; UserInfo.propTypes = { user: PropTypes.oneOfType([ diff --git a/src/components/views/room_settings/AliasSettings.js b/src/components/views/room_settings/AliasSettings.js index daf5c6edc2..aa9d46c9d5 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,33 @@ 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 +74,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 +207,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 +258,7 @@ export default class AliasSettings extends React.Component { return (
{canonicalAliasSection} - {remoteAliasesSection}
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
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/EditMessageComposer.js b/src/components/views/rooms/EditMessageComposer.js index 34b2d92590..43f378a1e0 100644 --- a/src/components/views/rooms/EditMessageComposer.js +++ b/src/components/views/rooms/EditMessageComposer.js @@ -26,11 +26,11 @@ 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"; import {Key} from "../../../Keyboard"; +import MatrixClientContext from "../../../contexts/MatrixClientContext"; function _isReply(mxEvent) { const relatesTo = mxEvent.getContent()["m.relates_to"]; @@ -105,12 +105,10 @@ export default class EditMessageComposer extends React.Component { editState: PropTypes.instanceOf(EditorStateTransfer).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; @@ -124,7 +122,7 @@ export default class EditMessageComposer extends React.Component { }; _getRoom() { - return this.context.matrixClient.getRoom(this.props.editState.getEvent().getRoomId()); + return this.context.getRoom(this.props.editState.getEvent().getRoomId()); } _onKeyDown = (event) => { @@ -190,7 +188,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 +203,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 +230,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..b71771a916 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 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. @@ -24,17 +24,17 @@ 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'); 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 +222,8 @@ module.exports = createReactClass({ }; }, - contextTypes: { - matrixClient: PropTypes.instanceOf(MatrixClient).isRequired, + statics: { + contextType: MatrixClientContext, }, componentWillMount: function() { @@ -237,7 +237,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 +262,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 +291,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 +349,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; } @@ -442,15 +442,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 @@ -461,7 +452,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) { @@ -478,11 +469,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 @@ -490,11 +480,11 @@ module.exports = createReactClass({ if (this.state.verified) { return; // no icon for verified } else { - return (); + return (); } } - 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) { @@ -507,7 +497,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 @@ -741,7 +731,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 (
@@ -919,7 +909,6 @@ class E2ePadlock extends React.Component { static propTypes = { icon: PropTypes.string.isRequired, title: PropTypes.string.isRequired, - onClick: PropTypes.func, }; constructor() { @@ -930,10 +919,6 @@ class E2ePadlock extends React.Component { }; } - onClick = (e) => { - if (this.props.onClick) this.props.onClick(e); - }; - onHoverStart = () => { this.setState({hover: true}); }; 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(), 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/MemberList.js b/src/components/views/rooms/MemberList.js index 0805c0342c..de0a6fe478 100644 --- a/src/components/views/rooms/MemberList.js +++ b/src/components/views/rooms/MemberList.js @@ -32,6 +32,10 @@ const INITIAL_LOAD_NUM_MEMBERS = 30; const INITIAL_LOAD_NUM_INVITED = 5; const SHOW_MORE_INCREMENT = 100; +// 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', @@ -336,10 +340,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) { 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..595ec0681f 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$'); @@ -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'); @@ -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/components/views/rooms/ReadReceiptMarker.js b/src/components/views/rooms/ReadReceiptMarker.js index 27c5e8c20e..35d745ae5a 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 @@ -175,7 +178,7 @@ module.exports = createReactClass({ render: function() { const MemberAvatar = sdk.getComponent('avatars.MemberAvatar'); if (this.state.suppressDisplay) { - return
; + return
; } const style = { @@ -215,6 +218,7 @@ module.exports = createReactClass({ style={style} title={title} onClick={this.props.onClick} + inputRef={this._avatar} /> ); 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/RoomList.js b/src/components/views/rooms/RoomList.js index 80a03e7a73..35a5ca9e66 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'); @@ -589,10 +590,17 @@ module.exports = createReactClass({ _applySearchFilter: function(list, filter) { 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 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 - 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; + } + return room.name && utils.removeHiddenChars(room.name.toLowerCase()).toLowerCase().includes(fuzzyFilter); + }); }, _handleCollapsedState: function(key, collapsed) { @@ -628,7 +636,6 @@ module.exports = createReactClass({ const defaultProps = { collapsed: this.props.collapsed, isFiltered: !!this.props.searchFilter, - incomingCall: this.state.incomingCall, }; subListsProps.forEach((p) => { @@ -641,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; @@ -652,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 = ( {setupCaption} -

{ _t("Not now") } -

-

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

+
); diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index 817ada9706..241713c97d 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -68,11 +68,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 +366,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; + + const { room } = this.props; + const member = room.getMember(dmUserId); + if (member && member.membership === "join" && room.getJoinedMemberCount() === 2) { + const UserOnlineDot = sdk.getComponent('rooms.UserOnlineDot'); + dmOnline = ; + } } // The following labels are written in such a fashion to increase screen reader efficiency (speed). @@ -428,6 +433,7 @@ module.exports = createReactClass({ { label } { subtextLabel }
+ { dmOnline } { contextMenuButton } { badge }
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/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; diff --git a/src/components/views/rooms/UserOnlineDot.js b/src/components/views/rooms/UserOnlineDot.js new file mode 100644 index 0000000000..426dd1bf64 --- /dev/null +++ b/src/components/views/rooms/UserOnlineDot.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 UserOnlineDot = ({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; +}; + +UserOnlineDot.propTypes = { + userId: PropTypes.string.isRequired, +}; + +export default UserOnlineDot; 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/KeyBackupPanel.js b/src/components/views/settings/KeyBackupPanel.js index 559b1e0ba1..bfa96f277f 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. @@ -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) { @@ -128,36 +127,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 = () => { @@ -181,22 +168,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() { @@ -270,7 +246,7 @@ export default class KeyBackupPanel extends React.PureComponent { {sub} ; const verify = sub => - + {sub} ; const device = sub => {deviceName}; 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..3022885701 --- /dev/null +++ b/src/components/views/settings/tabs/room/BridgeSettingsTab.js @@ -0,0 +1,166 @@ +/* +Copyright 2019 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from 'react'; +import PropTypes from 'prop-types'; +import {_t} from "../../../../../languageHandler"; +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", + // m.bridge +]; + +export default class BridgeSettingsTab extends React.Component { + static propTypes = { + roomId: PropTypes.string.isRequired, + }; + + _renderBridgeCard(event, room) { + const content = event.getContent(); + if (!content || !content.channel || !content.protocol) { + return null; + } + 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 : protocolName; + + let creator = null; + if (content.creator) { + creator =

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

; + } + + const bot = (

{_t("This bridge is managed by .", {}, { + user: , + })}

); + let channelLink = channelName; + if (channel.external_url) { + channelLink = {channelName}; + } + + let networkLink = networkName; + if (network && network.external_url) { + networkLink = {networkName}; + } + + const chanAndNetworkInfo = ( + _t("Bridged into , on ", {}, { + channelLink, + networkLink, + 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", + ); + channelIcon = ; + } + + const heading = _t("Connected to on ", { }, { + channelIcon, + channelName, + networkName, + networkIcon, + }); + + return (
  • +
    +

    {heading}

    +

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

    +
    + {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")}
    +
    +

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

    +
      + { bridgeEvents.map((event) => this._renderBridgeCard(event, room)) } +
    +
    +
    + ); + } +} 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/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/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 9bccca14f4..5d32066fd8 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.", @@ -357,9 +358,10 @@ "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.", + "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)", + "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", @@ -762,6 +764,13 @@ "Room version:": "Room version:", "Developer options": "Developer options", "Open Devtools": "Open Devtools", + "This bridge was provisioned by ": "This bridge was provisioned by ", + "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", + "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", @@ -1114,7 +1123,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?", @@ -1124,8 +1132,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", @@ -1258,7 +1266,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", @@ -1432,6 +1439,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.", @@ -1624,6 +1636,7 @@ "Hide": "Hide", "Home": "Home", "Sign in": "Sign in", + "Help": "Help", "Reload": "Reload", "Take picture": "Take picture", "Remove for everyone": "Remove for everyone", @@ -1767,6 +1780,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", @@ -1935,6 +1949,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 +1976,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", 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(); diff --git a/src/settings/Settings.js b/src/settings/Settings.js index 82dd639819..1d24e81469 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: [], @@ -136,13 +142,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)"), @@ -156,6 +155,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, 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. 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 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/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( 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/components/views/elements/MemberEventListSummary-test.js b/test/components/views/elements/MemberEventListSummary-test.js index a31cbdebb5..906ba45711 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; 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; expect(result.props.children).toEqual([
    Expanded membership
    , 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(); diff --git a/test/test-utils.js b/test/test-utils.js index 64704fc610..5c8c7f8a10 100644 --- a/test/test-utils.js +++ b/test/test-utils.js @@ -2,13 +2,13 @@ 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'; 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 +291,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; 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"