diff --git a/.eslintignore.errorfiles b/.eslintignore.errorfiles index ffe0ade5cc..3636a5e563 100644 --- a/.eslintignore.errorfiles +++ b/.eslintignore.errorfiles @@ -4,7 +4,6 @@ src/component-index.js src/components/structures/BottomLeftMenu.js src/components/structures/CreateRoom.js src/components/structures/MessagePanel.js -src/components/structures/NotificationPanel.js src/components/structures/RoomDirectory.js src/components/structures/RoomStatusBar.js src/components/structures/RoomView.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 245d0c7e60..4119af8136 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,161 @@ +Changes in [1.2.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v1.2.0) (2019-05-29) +=================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v1.2.0-rc.1...v1.2.0) + + * COLR font check fixes for release + [\#3041](https://github.com/matrix-org/matrix-react-sdk/pull/3041) + * Revert "Make the timeline less noisy for screen readers (mk II) #3019" for + release + [\#3036](https://github.com/matrix-org/matrix-react-sdk/pull/3036) + * Override font for usercontent download link for release + [\#3037](https://github.com/matrix-org/matrix-react-sdk/pull/3037) + +Changes in [1.2.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v1.2.0-rc.1) (2019-05-23) +============================================================================================================= +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v1.1.2...v1.2.0-rc.1) + + * Update from Weblate + [\#3023](https://github.com/matrix-org/matrix-react-sdk/pull/3023) + * Use the correct line-height for bold emoji + [\#3022](https://github.com/matrix-org/matrix-react-sdk/pull/3022) + * Make the timeline less noisy for screen readers (mk II) + [\#3019](https://github.com/matrix-org/matrix-react-sdk/pull/3019) + * Label message edit field as such for screen readers + [\#3020](https://github.com/matrix-org/matrix-react-sdk/pull/3020) + * Move checkmark to the front of key backup message + [\#3014](https://github.com/matrix-org/matrix-react-sdk/pull/3014) + * Revert "Make the timeline less noisy for screen readers" + [\#3017](https://github.com/matrix-org/matrix-react-sdk/pull/3017) + * Translate scroll movement if the deltaX is the same as the threshold + [\#3016](https://github.com/matrix-org/matrix-react-sdk/pull/3016) + * Make the timeline less noisy for screen readers + [\#3007](https://github.com/matrix-org/matrix-react-sdk/pull/3007) + * Windows emoji tweaks + [\#3015](https://github.com/matrix-org/matrix-react-sdk/pull/3015) + * Message editing: update link previews after editing + [\#3004](https://github.com/matrix-org/matrix-react-sdk/pull/3004) + * js-sdk interactive auth now sends email token + [\#3010](https://github.com/matrix-org/matrix-react-sdk/pull/3010) + * remove SBIX font and fallback to native emoji + [\#3011](https://github.com/matrix-org/matrix-react-sdk/pull/3011) + * Update from Weblate + [\#3012](https://github.com/matrix-org/matrix-react-sdk/pull/3012) + * load twemoji dynamically as colr or sbix; fix monospace + [\#3008](https://github.com/matrix-org/matrix-react-sdk/pull/3008) + * Guard against null rooms in `onEventDecrypted` + [\#3009](https://github.com/matrix-org/matrix-react-sdk/pull/3009) + * Only show reactions in main message timeline + [\#3005](https://github.com/matrix-org/matrix-react-sdk/pull/3005) + * Add voice labels for quick add room buttons + [\#3006](https://github.com/matrix-org/matrix-react-sdk/pull/3006) + * Update TopLeftMenu for accessibility: Keyboard shortcut, reduced screen + reader noise + [\#2994](https://github.com/matrix-org/matrix-react-sdk/pull/2994) + * Remove reacted with text when shortcode missing + [\#3003](https://github.com/matrix-org/matrix-react-sdk/pull/3003) + * Fixup: also change editor margin when last event and buttons are not + overlaying + [\#3002](https://github.com/matrix-org/matrix-react-sdk/pull/3002) + * Message editing: render avatars for pills in the editor + [\#2997](https://github.com/matrix-org/matrix-react-sdk/pull/2997) + * Replace emojione with twemoji + emojibase + [\#2995](https://github.com/matrix-org/matrix-react-sdk/pull/2995) + * Hide WhoIsTyping component if the MessagePanel is shaped e.g file grid + [\#3000](https://github.com/matrix-org/matrix-react-sdk/pull/3000) + * Close copy tooltip in edge cases correctly + [\#2999](https://github.com/matrix-org/matrix-react-sdk/pull/2999) + * Limit reaction sender tooltip to 6 people + [\#2998](https://github.com/matrix-org/matrix-react-sdk/pull/2998) + * Message editing: apply design + [\#2996](https://github.com/matrix-org/matrix-react-sdk/pull/2996) + * Add debug feature to show hidden events in timeline + [\#2993](https://github.com/matrix-org/matrix-react-sdk/pull/2993) + * Mute screen readers over reactions + [\#2986](https://github.com/matrix-org/matrix-react-sdk/pull/2986) + * Fix not being able to edit already edited messages + [\#2992](https://github.com/matrix-org/matrix-react-sdk/pull/2992) + * Add a basic tooltip showing who reacted + [\#2991](https://github.com/matrix-org/matrix-react-sdk/pull/2991) + * Message editing: show (edited) marker on edited messages, with tooltip + [\#2990](https://github.com/matrix-org/matrix-react-sdk/pull/2990) + * Update from Weblate + [\#2989](https://github.com/matrix-org/matrix-react-sdk/pull/2989) + * Message editing: only allow editing of text messages + [\#2988](https://github.com/matrix-org/matrix-react-sdk/pull/2988) + * Message editing: shift+enter for newline, enter to send + [\#2987](https://github.com/matrix-org/matrix-react-sdk/pull/2987) + * Apply Flex voodoo for devtools send event dialog + [\#2985](https://github.com/matrix-org/matrix-react-sdk/pull/2985) + * Fix some source strings noticed as incorrect by translators + [\#2984](https://github.com/matrix-org/matrix-react-sdk/pull/2984) + * Message editing: fix some bugs in cursor behaviour + [\#2983](https://github.com/matrix-org/matrix-react-sdk/pull/2983) + * Message editing: local echo & back-pagination + [\#2982](https://github.com/matrix-org/matrix-react-sdk/pull/2982) + * Listen for removed relations + [\#2981](https://github.com/matrix-org/matrix-react-sdk/pull/2981) + * Update from Weblate + [\#2980](https://github.com/matrix-org/matrix-react-sdk/pull/2980) + * Use `getRelation` helper + [\#2977](https://github.com/matrix-org/matrix-react-sdk/pull/2977) + * Add tooltips to rotate and close buttons in ImageView (#9686) + [\#2979](https://github.com/matrix-org/matrix-react-sdk/pull/2979) + * Message editing: smaller fixes + [\#2978](https://github.com/matrix-org/matrix-react-sdk/pull/2978) + * Message editing: adjust to js-sdk changes of marking original event as + replaced + [\#2973](https://github.com/matrix-org/matrix-react-sdk/pull/2973) + * Fix Single Sign-on + [\#2974](https://github.com/matrix-org/matrix-react-sdk/pull/2974) + * Initial support for editing messages + [\#2952](https://github.com/matrix-org/matrix-react-sdk/pull/2952) + * Check permission to invite before showing invite buttons/disable them + [\#2957](https://github.com/matrix-org/matrix-react-sdk/pull/2957) + * Support a backup room ID in PermalinkCreator + [\#2963](https://github.com/matrix-org/matrix-react-sdk/pull/2963) + * Always thumbnail for GIFs + [\#2962](https://github.com/matrix-org/matrix-react-sdk/pull/2962) + * Fix registration with email + [\#2967](https://github.com/matrix-org/matrix-react-sdk/pull/2967) + * Add configuration flag to disable minimum password requirements + [\#2947](https://github.com/matrix-org/matrix-react-sdk/pull/2947) + * Send and undo reaction events + [\#2954](https://github.com/matrix-org/matrix-react-sdk/pull/2954) + * Fix bug where email was not required where it shouldn't have been + [\#2961](https://github.com/matrix-org/matrix-react-sdk/pull/2961) + * add /rainbow and /rainbowme Slash Commands + [\#2958](https://github.com/matrix-org/matrix-react-sdk/pull/2958) + * Fix invite via MemberInfo + [\#2956](https://github.com/matrix-org/matrix-react-sdk/pull/2956) + * Close Room Settings upon Leave Room + [\#2955](https://github.com/matrix-org/matrix-react-sdk/pull/2955) + * Command to change avatar for a single room, including upload of mxc res + [\#2953](https://github.com/matrix-org/matrix-react-sdk/pull/2953) + * Add View Servers in Room to Devtools + [\#2804](https://github.com/matrix-org/matrix-react-sdk/pull/2804) + * Update 'Rooms' import RoomView.js file + [\#2951](https://github.com/matrix-org/matrix-react-sdk/pull/2951) + * Extract `ReactionDimension` out of `MessageActionBar` + [\#2950](https://github.com/matrix-org/matrix-react-sdk/pull/2950) + * Always default to the registration form + [\#2942](https://github.com/matrix-org/matrix-react-sdk/pull/2942) + * Check for `room` in all `Room.timeline*` handlers + [\#2945](https://github.com/matrix-org/matrix-react-sdk/pull/2945) + * Remove the karma junit reporter + [\#2944](https://github.com/matrix-org/matrix-react-sdk/pull/2944) + * yarn upgrade + [\#2943](https://github.com/matrix-org/matrix-react-sdk/pull/2943) + * Support changing options for .m.rule.tombstone push rule + [\#2798](https://github.com/matrix-org/matrix-react-sdk/pull/2798) + * Remove timeline explosion rageshake prompt + [\#2939](https://github.com/matrix-org/matrix-react-sdk/pull/2939) + * Add existing reactions below message + [\#2940](https://github.com/matrix-org/matrix-react-sdk/pull/2940) + * Fix lint errors in TimelinePanel + [\#2938](https://github.com/matrix-org/matrix-react-sdk/pull/2938) + * Add primary reactions to action bar + [\#2937](https://github.com/matrix-org/matrix-react-sdk/pull/2937) + Changes in [1.1.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v1.1.2) (2019-05-15) =================================================================================================== [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v1.1.1...v1.1.2) diff --git a/package.json b/package.json index 9c55ff43c8..c409188125 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "1.1.2", + "version": "1.2.0", "description": "SDK for matrix.org using React", "author": "matrix.org", "repository": { @@ -65,7 +65,8 @@ "classnames": "^2.1.2", "commonmark": "^0.28.1", "counterpart": "^0.18.0", - "emojione": "2.2.7", + "emojibase-data": "^4.0.0", + "emojibase-regex": "^3.0.0", "file-saver": "^1.3.3", "filesize": "3.5.6", "flux": "2.1.1", @@ -80,7 +81,7 @@ "linkifyjs": "^2.1.6", "lodash": "^4.13.1", "lolex": "2.3.2", - "matrix-js-sdk": "1.1.0", + "matrix-js-sdk": "1.2.0", "optimist": "^0.6.1", "pako": "^1.0.5", "png-chunks-extract": "^1.0.0", diff --git a/res/css/_common.scss b/res/css/_common.scss index d3cf1921e0..973103899d 100644 --- a/res/css/_common.scss +++ b/res/css/_common.scss @@ -32,6 +32,11 @@ body { margin: 0px; } +pre, code { + font-family: $monospace-font-family; + font-size: 100% !important; +} + .error, .warning { color: $warning-color; } @@ -110,6 +115,14 @@ textarea { color: $primary-fg-color; } +// This is used to hide the standard outline added by browsers for +// accessible (focusable) components. Not intended for buttons, but +// should be used on things like focusable containers where the outline +// is usually not helping anyone. +.mx_HiddenFocusable { + outline: none; +} + // .mx_textinput is a container for a text input // + some other controls like buttons, ... // it has the appearance of a text box so the controls @@ -445,15 +458,6 @@ textarea { background-color: $primary-bg-color; } -.mx_emojione { - height: 1em; - vertical-align: middle; -} - -.mx_emojione_selected { - background-color: $accent-color; -} - ::-moz-selection { background-color: $accent-color; color: $selection-fg-color; @@ -557,4 +561,3 @@ textarea { .mx_Username_color8 { color: $username-variant8-color; } - diff --git a/res/css/_components.scss b/res/css/_components.scss index 2e0c91bd8c..2a91f08ee4 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -98,6 +98,7 @@ @import "./views/elements/_RoleButton.scss"; @import "./views/elements/_Spinner.scss"; @import "./views/elements/_SyntaxHighlight.scss"; +@import "./views/elements/_TextWithTooltip.scss"; @import "./views/elements/_ToggleSwitch.scss"; @import "./views/elements/_ToolTipButton.scss"; @import "./views/elements/_Tooltip.scss"; @@ -119,10 +120,12 @@ @import "./views/messages/_ReactionDimension.scss"; @import "./views/messages/_ReactionsRow.scss"; @import "./views/messages/_ReactionsRowButton.scss"; +@import "./views/messages/_ReactionsRowButtonTooltip.scss"; @import "./views/messages/_RoomAvatarEvent.scss"; @import "./views/messages/_SenderProfile.scss"; @import "./views/messages/_TextualEvent.scss"; @import "./views/messages/_UnknownBody.scss"; +@import "./views/messages/_ViewSourceEvent.scss"; @import "./views/room_settings/_AliasSettings.scss"; @import "./views/room_settings/_ColorSettings.scss"; @import "./views/rooms/_AppsDrawer.scss"; diff --git a/res/css/structures/auth/_Login.scss b/res/css/structures/auth/_Login.scss index 2cf6276557..4eff5c33e4 100644 --- a/res/css/structures/auth/_Login.scss +++ b/res/css/structures/auth/_Login.scss @@ -79,3 +79,7 @@ limitations under the License. .mx_Login_type_dropdown { min-width: 200px; } + +.mx_Login_underlinedServerName { + border-bottom: 1px dashed $accent-color; +} diff --git a/res/css/views/auth/_ServerConfig.scss b/res/css/views/auth/_ServerConfig.scss index 79ad9e8238..fe96da2019 100644 --- a/res/css/views/auth/_ServerConfig.scss +++ b/res/css/views/auth/_ServerConfig.scss @@ -35,3 +35,8 @@ limitations under the License. .mx_ServerConfig_help:link { opacity: 0.8; } + +.mx_ServerConfig_error { + display: block; + color: $warning-color; +} diff --git a/res/css/views/dialogs/_DevtoolsDialog.scss b/res/css/views/dialogs/_DevtoolsDialog.scss index 2f01f3ecc6..1f5d36b57a 100644 --- a/res/css/views/dialogs/_DevtoolsDialog.scss +++ b/res/css/views/dialogs/_DevtoolsDialog.scss @@ -82,8 +82,13 @@ limitations under the License. display: inline-block; } -.mx_DevTools_content .mx_Field_input + .mx_Field_input { - margin-left: 42px; +.mx_DevTools_eventTypeStateKeyGroup { + display: flex; + flex-wrap: wrap; +} + +.mx_DevTools_content .mx_Field_input:first-of-type { + margin-right: 42px; } .mx_DevTools_tgl { diff --git a/res/css/views/elements/_MessageEditor.scss b/res/css/views/elements/_MessageEditor.scss index ec6d903753..e721b267fa 100644 --- a/res/css/views/elements/_MessageEditor.scss +++ b/res/css/views/elements/_MessageEditor.scss @@ -16,40 +16,61 @@ limitations under the License. .mx_MessageEditor { border-radius: 4px; - background-color: $header-panel-bg-color; - padding: 11px 13px 7px 56px; + padding: 3px; + // this is to try not make the text move but still have some + // padding around and in the editor. + // Actual values from fiddling around in inspector + margin: -7px -10px -5px -10px; + overflow: visible !important; // override mx_EventTile_content .mx_MessageEditor_editor { border-radius: 4px; - border: solid 1px #e9edf1; - background-color: #ffffff; - padding: 10px; + border: solid 1px $primary-hairline-color; + background-color: $primary-bg-color; + padding: 3px 6px; white-space: pre-wrap; word-wrap: break-word; outline: none; + max-height: 200px; + overflow-x: auto; - span { - display: inline-block; - padding: 0 5px; - border-radius: 4px; - color: white; - } + span.mx_UserPill, span.mx_RoomPill { + padding-left: 21px; + position: relative; - span.user-pill, span.room-pill { - border-radius: 16px; - display: inline-block; - color: $primary-fg-color; - background-color: $other-user-pill-bg-color; - padding-left: 5px; - padding-right: 5px; + // avatar psuedo element + &::before { + position: absolute; + left: 2px; + top: 2px; + content: var(--avatar-letter); + width: 16px; + height: 16px; + background: var(--avatar-background), $avatar-bg-color; + color: $avatar-initial-color; + background-repeat: no-repeat; + background-size: 16px; + border-radius: 8px; + text-align: center; + font-weight: normal; + line-height: 16px; + font-size: 10.4px; + } } } .mx_MessageEditor_buttons { display: flex; flex-direction: row; - justify-content: end; - padding: 5px 0; + justify-content: flex-end; + padding: 5px; + position: absolute; + left: 0; + background: $header-panel-bg-color; + z-index: 100; + right: 0; + margin: 0 -110px 0 0; + padding-right: 147px; .mx_AccessibleButton { margin-left: 5px; @@ -62,3 +83,8 @@ limitations under the License. height: 0; } } + +.mx_EventTile_last .mx_MessageEditor_buttons { + position: static; + margin-right: -147px; +} diff --git a/res/css/views/elements/_TextWithTooltip.scss b/res/css/views/elements/_TextWithTooltip.scss new file mode 100644 index 0000000000..5b34e7a3be --- /dev/null +++ b/res/css/views/elements/_TextWithTooltip.scss @@ -0,0 +1,19 @@ +/* +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_TextWithTooltip_tooltip { + display: none; +} \ No newline at end of file diff --git a/res/css/views/elements/_Tooltip.scss b/res/css/views/elements/_Tooltip.scss index 43ddf6dde5..3a6b6fb936 100644 --- a/res/css/views/elements/_Tooltip.scss +++ b/res/css/views/elements/_Tooltip.scss @@ -74,3 +74,19 @@ limitations under the License. animation: mx_fadeout 0.1s forwards; } } + +.mx_Tooltip_timeline { + box-shadow: none; + background-color: $tooltip-timeline-bg-color; + color: $tooltip-timeline-fg-color; + text-align: center; + border: none; + border-radius: 3px; + font-size: 14px; + line-height: 1.2; + padding: 6px 8px; + + .mx_Tooltip_chevron::after { + border-right-color: $tooltip-timeline-bg-color; + } +} diff --git a/res/css/views/messages/_ReactionsRowButtonTooltip.scss b/res/css/views/messages/_ReactionsRowButtonTooltip.scss new file mode 100644 index 0000000000..cf4219fcec --- /dev/null +++ b/res/css/views/messages/_ReactionsRowButtonTooltip.scss @@ -0,0 +1,19 @@ +/* +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_ReactionsRowButtonTooltip_reactedWith { + opacity: 0.7; +} diff --git a/res/css/views/messages/_ViewSourceEvent.scss b/res/css/views/messages/_ViewSourceEvent.scss new file mode 100644 index 0000000000..a15924e759 --- /dev/null +++ b/res/css/views/messages/_ViewSourceEvent.scss @@ -0,0 +1,50 @@ +/* +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_EventTile_content.mx_ViewSourceEvent { + display: flex; + opacity: 0.6; + font-size: 12px; + + pre, code { + flex: 1; + } + + pre { + line-height: 1.2; + margin: 3.5px 0; + } + + .mx_ViewSourceEvent_toggle { + width: 12px; + mask-repeat: no-repeat; + mask-position: 0 center; + mask-size: auto 12px; + visibility: hidden; + background-color: $accent-color; + mask-image: url('$(res)/img/feather-customised/widget/maximise.svg'); + } + + &.mx_ViewSourceEvent_expanded .mx_ViewSourceEvent_toggle { + mask-position: 0 bottom; + margin-bottom: 7px; + mask-image: url('$(res)/img/feather-customised/widget/minimise.svg'); + } + + &:hover .mx_ViewSourceEvent_toggle { + visibility: visible; + } +} diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index f4c12bb734..7d8a58f7a8 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -40,7 +40,16 @@ limitations under the License. } .mx_EventTile_continuation { - padding-top: 0px ! important; + padding-top: 0px !important; + + &.mx_EventTile_isEditing { + padding-top: 5px !important; + margin-top: -5px; + } +} + +.mx_EventTile_isEditing { + background-color: $header-panel-bg-color; } .mx_EventTile .mx_SenderProfile { @@ -72,6 +81,10 @@ limitations under the License. } } +.mx_EventTile_isEditing .mx_MessageTimestamp { + visibility: hidden !important; +} + .mx_EventTile .mx_MessageTimestamp { display: block; visibility: hidden; @@ -108,7 +121,7 @@ limitations under the License. /* HACK to override line-height which is already marked important elsewhere */ .mx_EventTile_bigEmoji.mx_EventTile_bigEmoji { font-size: 48px ! important; - line-height: 48px ! important; + line-height: 57px ! important; } /* this is used for the tile for the event which is selected via the URL. @@ -149,8 +162,7 @@ limitations under the License. } .mx_EventTile_sending .mx_UserPill, -.mx_EventTile_sending .mx_RoomPill, -.mx_EventTile_sending .mx_emojione { +.mx_EventTile_sending .mx_RoomPill { opacity: 0.5; } @@ -377,6 +389,14 @@ limitations under the License. left: 41px; } +.mx_EventTile_content .mx_EventTile_edited { + user-select: none; + font-size: 12px; + color: $roomtopic-color; + display: inline-block; + margin-left: 9px; +} + /* Various markdown overrides */ .mx_EventTile_content .markdown-body { @@ -404,6 +424,7 @@ limitations under the License. .mx_EventTile_content .markdown-body { pre, code { + font-family: $monospace-font-family ! important; // deliberate constants as we're behind an invert filter color: #333; } diff --git a/res/fonts/Fira_Mono/FiraMono-Bold.ttf b/res/fonts/Fira_Mono/FiraMono-Bold.ttf deleted file mode 100755 index 4b8b1cfbcb..0000000000 Binary files a/res/fonts/Fira_Mono/FiraMono-Bold.ttf and /dev/null differ diff --git a/res/fonts/Fira_Mono/FiraMono-Regular.ttf b/res/fonts/Fira_Mono/FiraMono-Regular.ttf deleted file mode 100755 index 5238c09eda..0000000000 Binary files a/res/fonts/Fira_Mono/FiraMono-Regular.ttf and /dev/null differ diff --git a/res/fonts/Fira_Mono/OFL.txt b/res/fonts/Fira_Mono/OFL.txt deleted file mode 100755 index ba853c049e..0000000000 --- a/res/fonts/Fira_Mono/OFL.txt +++ /dev/null @@ -1,92 +0,0 @@ -Copyright (c) 2012-2013, The Mozilla Corporation and Telefonica S.A. -This Font Software is licensed under the SIL Open Font License, Version 1.1. -This license is copied below, and is also available with a FAQ at: -http://scripts.sil.org/OFL - - ------------------------------------------------------------ -SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 ------------------------------------------------------------ - -PREAMBLE -The goals of the Open Font License (OFL) are to stimulate worldwide -development of collaborative font projects, to support the font creation -efforts of academic and linguistic communities, and to provide a free and -open framework in which fonts may be shared and improved in partnership -with others. - -The OFL allows the licensed fonts to be used, studied, modified and -redistributed freely as long as they are not sold by themselves. The -fonts, including any derivative works, can be bundled, embedded, -redistributed and/or sold with any software provided that any reserved -names are not used by derivative works. The fonts and derivatives, -however, cannot be released under any other type of license. The -requirement for fonts to remain under this license does not apply -to any document created using the fonts or their derivatives. - -DEFINITIONS -"Font Software" refers to the set of files released by the Copyright -Holder(s) under this license and clearly marked as such. This may -include source files, build scripts and documentation. - -"Reserved Font Name" refers to any names specified as such after the -copyright statement(s). - -"Original Version" refers to the collection of Font Software components as -distributed by the Copyright Holder(s). - -"Modified Version" refers to any derivative made by adding to, deleting, -or substituting -- in part or in whole -- any of the components of the -Original Version, by changing formats or by porting the Font Software to a -new environment. - -"Author" refers to any designer, engineer, programmer, technical -writer or other person who contributed to the Font Software. - -PERMISSION & CONDITIONS -Permission is hereby granted, free of charge, to any person obtaining -a copy of the Font Software, to use, study, copy, merge, embed, modify, -redistribute, and sell modified and unmodified copies of the Font -Software, subject to the following conditions: - -1) Neither the Font Software nor any of its individual components, -in Original or Modified Versions, may be sold by itself. - -2) Original or Modified Versions of the Font Software may be bundled, -redistributed and/or sold with any software, provided that each copy -contains the above copyright notice and this license. These can be -included either as stand-alone text files, human-readable headers or -in the appropriate machine-readable metadata fields within text or -binary files as long as those fields can be easily viewed by the user. - -3) No Modified Version of the Font Software may use the Reserved Font -Name(s) unless explicit written permission is granted by the corresponding -Copyright Holder. This restriction only applies to the primary font name as -presented to the users. - -4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font -Software shall not be used to promote, endorse or advertise any -Modified Version, except to acknowledge the contribution(s) of the -Copyright Holder(s) and the Author(s) or with their explicit written -permission. - -5) The Font Software, modified or unmodified, in part or in whole, -must be distributed entirely under this license, and must not be -distributed under any other license. The requirement for fonts to -remain under this license does not apply to any document created -using the Font Software. - -TERMINATION -This license becomes null and void if any of the above conditions are -not met. - -DISCLAIMER -THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT -OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE -COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL -DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM -OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/res/fonts/Inconsolata/QldKNThLqRwH-OJ1UHjlKGlX5qhExfHwNJU.woff2 b/res/fonts/Inconsolata/QldKNThLqRwH-OJ1UHjlKGlX5qhExfHwNJU.woff2 new file mode 100644 index 0000000000..880f06af78 Binary files /dev/null and b/res/fonts/Inconsolata/QldKNThLqRwH-OJ1UHjlKGlX5qhExfHwNJU.woff2 differ diff --git a/res/fonts/Inconsolata/QldKNThLqRwH-OJ1UHjlKGlZ5qhExfHw.woff2 b/res/fonts/Inconsolata/QldKNThLqRwH-OJ1UHjlKGlZ5qhExfHw.woff2 new file mode 100644 index 0000000000..9fe96559d1 Binary files /dev/null and b/res/fonts/Inconsolata/QldKNThLqRwH-OJ1UHjlKGlZ5qhExfHw.woff2 differ diff --git a/res/fonts/Inconsolata/QldXNThLqRwH-OJ1UHjlKGHiw71n5_zaDpwm80E.woff2 b/res/fonts/Inconsolata/QldXNThLqRwH-OJ1UHjlKGHiw71n5_zaDpwm80E.woff2 new file mode 100644 index 0000000000..cd79590d99 Binary files /dev/null and b/res/fonts/Inconsolata/QldXNThLqRwH-OJ1UHjlKGHiw71n5_zaDpwm80E.woff2 differ diff --git a/res/fonts/Inconsolata/QldXNThLqRwH-OJ1UHjlKGHiw71p5_zaDpwm.woff2 b/res/fonts/Inconsolata/QldXNThLqRwH-OJ1UHjlKGHiw71p5_zaDpwm.woff2 new file mode 100644 index 0000000000..cf26d38db4 Binary files /dev/null and b/res/fonts/Inconsolata/QldXNThLqRwH-OJ1UHjlKGHiw71p5_zaDpwm.woff2 differ diff --git a/res/fonts/Twemoji_Mozilla/TwemojiMozilla-colr.woff2 b/res/fonts/Twemoji_Mozilla/TwemojiMozilla-colr.woff2 new file mode 100644 index 0000000000..70f9b03c4d Binary files /dev/null and b/res/fonts/Twemoji_Mozilla/TwemojiMozilla-colr.woff2 differ diff --git a/res/img/edit.svg b/res/img/edit.svg index 95bd44f606..9674b31690 100644 --- a/res/img/edit.svg +++ b/res/img/edit.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/res/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss index 592b1a1887..bdccf71540 100644 --- a/res/themes/dark/css/_dark.scss +++ b/res/themes/dark/css/_dark.scss @@ -157,6 +157,9 @@ $reaction-row-button-hover-border-color: $header-panel-text-primary-color; $reaction-row-button-selected-bg-color: #1f6954; $reaction-row-button-selected-border-color: $accent-color; +$tooltip-timeline-bg-color: $tagpanel-bg-color; +$tooltip-timeline-fg-color: #ffffff; + // ***** Mixins! ***** @define-mixin mx_DialogButton { diff --git a/res/themes/light/css/_fonts.scss b/res/themes/light/css/_fonts.scss index ac15847e44..1bc9b5a4a3 100644 --- a/res/themes/light/css/_fonts.scss +++ b/res/themes/light/css/_fonts.scss @@ -15,39 +15,70 @@ /* the 'src' links are relative to the bundle.css, which is in a subdirectory. */ @font-face { - font-family: 'Nunito'; - font-style: normal; - font-weight: 400; - src: url('$(res)/fonts/Nunito/Nunito-Regular.ttf') format('truetype'); -} -@font-face { - font-family: 'Nunito'; - font-style: normal; - font-weight: 600; - src: url('$(res)/fonts/Nunito/Nunito-SemiBold.ttf') format('truetype'); -} -@font-face { - font-family: 'Nunito'; - font-style: normal; - font-weight: 700; - src: url('$(res)/fonts/Nunito/Nunito-Bold.ttf') format('truetype'); -} - -/* - * Fira Mono - * Used for monospace copy, i.e. code - */ - -@font-face { - font-family: 'Fira Mono'; - src: url('$(res)/fonts/Fira_Mono/FiraMono-Regular.ttf') format('truetype'); + font-family: 'Nunito'; + font-style: normal; font-weight: 400; + src: url('$(res)/fonts/Nunito/Nunito-Regular.ttf') format('truetype'); +} +@font-face { + font-family: 'Nunito'; font-style: normal; + font-weight: 600; + src: url('$(res)/fonts/Nunito/Nunito-SemiBold.ttf') format('truetype'); +} +@font-face { + font-family: 'Nunito'; + font-style: normal; + font-weight: 700; + src: url('$(res)/fonts/Nunito/Nunito-Bold.ttf') format('truetype'); } +/* latin-ext */ @font-face { - font-family: 'Fira Mono'; - src: url('$(res)/fonts/Fira_Mono/FiraMono-Bold.ttf') format('truetype'); - font-weight: 700; + font-family: 'Inconsolata'; font-style: normal; + font-weight: 400; + src: local('Inconsolata Regular'), local('Inconsolata-Regular'), url('$(res)/fonts/Inconsolata/QldKNThLqRwH-OJ1UHjlKGlX5qhExfHwNJU.woff2') format('woff2'); + unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; } +/* latin */ +@font-face { + font-family: 'Inconsolata'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: local('Inconsolata Regular'), local('Inconsolata-Regular'), url('$(res)/fonts/Inconsolata/QldKNThLqRwH-OJ1UHjlKGlZ5qhExfHw.woff2') format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} +/* latin-ext */ +@font-face { + font-family: 'Inconsolata'; + font-style: normal; + font-weight: 700; + font-display: swap; + src: local('Inconsolata Bold'), local('Inconsolata-Bold'), url('$(res)/fonts/Inconsolata/QldXNThLqRwH-OJ1UHjlKGHiw71n5_zaDpwm80E.woff2') format('woff2'); + unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +/* latin */ +@font-face { + font-family: 'Inconsolata'; + font-style: normal; + font-weight: 700; + font-display: swap; + src: local('Inconsolata Bold'), local('Inconsolata-Bold'), url('$(res)/fonts/Inconsolata/QldXNThLqRwH-OJ1UHjlKGHiw71p5_zaDpwm.woff2') format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} + +/* a COLR/CPAL version of Twemoji used for consistent cross-browser emoji + * taken from https://github.com/mozilla/twemoji-colr + * using the fix from https://github.com/mozilla/twemoji-colr/issues/50 to + * work on macOS + */ +/* +// except we now load it dynamically via FontManager to handle browsers +// which can't render COLR/CPAL still +@font-face { + font-family: "Twemoji Mozilla"; + src: url('$(res)/fonts/Twemoji_Mozilla/TwemojiMozilla.woff2') format('woff2'); +} +*/ \ No newline at end of file diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss index fc15170b87..712d905b43 100644 --- a/res/themes/light/css/_light.scss +++ b/res/themes/light/css/_light.scss @@ -1,10 +1,13 @@ // XXX: check this? /* Nunito lacks combining diacritics, so these will fall through to the next font. Helevetica's diacritics however do not combine - nicely with Open Sans (on OSX, at least) and result in a huge - horizontal mess. Arial empirically gets it right, hence prioritising - Arial here. */ -$font-family: 'Nunito', Arial, Helvetica, Sans-Serif; + nicely (on OSX, at least) and result in a huge horizontal mess. + Arial empirically gets it right, hence prioritising Arial here. */ +/* We fall through to Twemoji for emoji rather than falling through + to native Emoji fonts (if any) to ensure cross-browser consistency */ +$font-family: Nunito, Twemoji, 'Apple Color Emoji', 'Segoe UI Emoji', 'Noto Color Emoji', Arial, Helvetica, Sans-Serif; + +$monospace-font-family: Inconsolata, Twemoji, 'Apple Color Emoji', 'Segoe UI Emoji', 'Noto Color Emoji', Courier, monospace; // unified palette // try to use these colors when possible @@ -265,6 +268,9 @@ $reaction-row-button-hover-border-color: $focus-bg-color; $reaction-row-button-selected-bg-color: #e9fff9; $reaction-row-button-selected-border-color: $accent-color; +$tooltip-timeline-bg-color: $tagpanel-bg-color; +$tooltip-timeline-fg-color: #ffffff; + // ***** Mixins! ***** @define-mixin mx_DialogButton { diff --git a/scripts/emoji-data-strip.js b/scripts/emoji-data-strip.js index 42bf2ac2de..1c3738cab1 100644 --- a/scripts/emoji-data-strip.js +++ b/scripts/emoji-data-strip.js @@ -1,28 +1,29 @@ #!/usr/bin/env node -const EMOJI_DATA = require('emojione/emoji.json'); -const EMOJI_SUPPORTED = Object.keys(require('emojione').emojioneList); + +// 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 = Object.keys(EMOJI_DATA).map( - (key) => { - const datum = EMOJI_DATA[key]; +const output = EMOJIBASE.map( + (datum) => { const newDatum = { - name: datum.name, - shortname: datum.shortname, - category: datum.category, - emoji_order: datum.emoji_order, + name: datum.annotation, + shortname: `:${datum.shortcodes[0]}:`, + category: datum.group, + emoji_order: datum.order, }; - if (datum.aliases.length > 0) { - newDatum.aliases = datum.aliases; + if (datum.shortcodes.length > 1) { + newDatum.aliases = datum.shortcodes.slice(1).map(s => `:${s}:`); } - if (datum.aliases_ascii.length > 0) { - newDatum.aliases_ascii = datum.aliases_ascii; + if (datum.emoticon) { + newDatum.aliases_ascii = [ datum.emoticon ]; } return newDatum; } -).filter((datum) => { - return EMOJI_SUPPORTED.includes(datum.shortname); -}); +); // Write to a file in src. Changes should be checked into git. This file is copied by // babel using --copy-files diff --git a/src/Avatar.js b/src/Avatar.js index 99b558fa93..2e15874b4e 100644 --- a/src/Avatar.js +++ b/src/Avatar.js @@ -17,6 +17,7 @@ limitations under the License. 'use strict'; import {ContentRepo} from 'matrix-js-sdk'; import MatrixClientPeg from './MatrixClientPeg'; +import DMRoomMap from './utils/DMRoomMap'; module.exports = { avatarUrlForMember: function(member, width, height, resizeMethod) { @@ -58,4 +59,71 @@ module.exports = { } return require('../res/img/' + images[total % images.length] + '.png'); }, + + /** + * returns the first (non-sigil) character of 'name', + * converted to uppercase + * @param {string} name + * @return {string} the first letter + */ + getInitialLetter(name) { + if (name.length < 1) { + return undefined; + } + + let idx = 0; + const initial = name[0]; + if ((initial === '@' || initial === '#' || initial === '+') && name[1]) { + idx++; + } + + // string.codePointAt(0) would do this, but that isn't supported by + // some browsers (notably PhantomJS). + let chars = 1; + const first = name.charCodeAt(idx); + + // check if it’s the start of a surrogate pair + if (first >= 0xD800 && first <= 0xDBFF && name[idx+1]) { + const second = name.charCodeAt(idx+1); + if (second >= 0xDC00 && second <= 0xDFFF) { + chars++; + } + } + + const firstChar = name.substring(idx, idx+chars); + return firstChar.toUpperCase(); + }, + + avatarUrlForRoom(room, width, height, resizeMethod) { + const explicitRoomAvatar = room.getAvatarUrl( + MatrixClientPeg.get().getHomeserverUrl(), + width, + height, + resizeMethod, + false, + ); + if (explicitRoomAvatar) { + return explicitRoomAvatar; + } + + let otherMember = null; + const otherUserId = DMRoomMap.shared().getUserIdForRoomId(room.roomId); + if (otherUserId) { + otherMember = room.getMember(otherUserId); + } else { + // if the room is not marked as a 1:1, but only has max 2 members + // then still try to show any avatar (pref. other member) + otherMember = room.getAvatarFallbackMember(); + } + if (otherMember) { + return otherMember.getAvatarUrl( + MatrixClientPeg.get().getHomeserverUrl(), + width, + height, + resizeMethod, + false, + ); + } + return null; + }, }; diff --git a/src/CallHandler.js b/src/CallHandler.js index acdc3e5122..e47209eebe 100644 --- a/src/CallHandler.js +++ b/src/CallHandler.js @@ -361,7 +361,7 @@ async function _startCallApp(roomId, type) { Modal.createTrackedDialog('Could not connect to the integration server', '', ErrorDialog, { title: _t('Could not connect to the integration server'), - description: _t('A conference call could not be started because the intgrations server is not available'), + description: _t('A conference call could not be started because the integrations server is not available'), }); return; } diff --git a/src/ComposerHistoryManager.js b/src/ComposerHistoryManager.js deleted file mode 100644 index ecf773f2e7..0000000000 --- a/src/ComposerHistoryManager.js +++ /dev/null @@ -1,86 +0,0 @@ -//@flow -/* -Copyright 2017 Aviral Dasgupta - -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 { Value } from 'slate'; - -import _clamp from 'lodash/clamp'; - -type MessageFormat = 'rich' | 'markdown'; - -class HistoryItem { - // We store history items in their native format to ensure history is accurate - // and then convert them if our RTE has subsequently changed format. - value: Value; - format: MessageFormat = 'rich'; - - constructor(value: ?Value, format: ?MessageFormat) { - this.value = value; - this.format = format; - } - - static fromJSON(obj: Object): HistoryItem { - return new HistoryItem( - Value.fromJSON(obj.value), - obj.format, - ); - } - - toJSON(): Object { - return { - value: this.value.toJSON(), - format: this.format, - }; - } -} - -export default class ComposerHistoryManager { - history: Array = []; - prefix: string; - lastIndex: number = 0; // used for indexing the storage - currentIndex: number = 0; // used for indexing the loaded validated history Array - - constructor(roomId: string, prefix: string = 'mx_composer_history_') { - this.prefix = prefix + roomId; - - // TODO: Performance issues? - let item; - for (; item = sessionStorage.getItem(`${this.prefix}[${this.currentIndex}]`); this.currentIndex++) { - try { - this.history.push( - HistoryItem.fromJSON(JSON.parse(item)), - ); - } catch (e) { - console.warn("Throwing away unserialisable history", e); - } - } - this.lastIndex = this.currentIndex; - // reset currentIndex to account for any unserialisable history - this.currentIndex = this.history.length; - } - - save(value: Value, format: MessageFormat) { - const item = new HistoryItem(value, format); - this.history.push(item); - this.currentIndex = this.history.length; - sessionStorage.setItem(`${this.prefix}[${this.lastIndex++}]`, JSON.stringify(item.toJSON())); - } - - getItem(offset: number): ?HistoryItem { - this.currentIndex = _clamp(this.currentIndex + offset, 0, this.history.length - 1); - return this.history[this.currentIndex]; - } -} diff --git a/src/GroupAddressPicker.js b/src/GroupAddressPicker.js index 532ee23c25..cd5ecc790d 100644 --- a/src/GroupAddressPicker.js +++ b/src/GroupAddressPicker.js @@ -38,7 +38,7 @@ export function showGroupInviteDialog(groupId) { Modal.createTrackedDialog('Group Invite', '', AddressPickerDialog, { title: _t("Invite new community members"), description: description, - placeholder: _t("Name or matrix ID"), + placeholder: _t("Name or Matrix ID"), button: _t("Invite to Community"), validAddressTypes: ['mx-user-id'], onFinished: (success, addrs) => { diff --git a/src/HtmlUtils.js b/src/HtmlUtils.js index d9d8bac93b..a8bbce10dc 100644 --- a/src/HtmlUtils.js +++ b/src/HtmlUtils.js @@ -27,22 +27,18 @@ import linkifyMatrix from './linkify-matrix'; import _linkifyElement from 'linkifyjs/element'; import _linkifyString from 'linkifyjs/string'; import escape from 'lodash/escape'; -import emojione from 'emojione'; import classNames from 'classnames'; import MatrixClientPeg from './MatrixClientPeg'; import url from 'url'; -linkifyMatrix(linkify); +import EMOJIBASE from 'emojibase-data/en/compact.json'; +import EMOJIBASE_REGEX from 'emojibase-regex'; -emojione.imagePathSVG = 'emojione/svg/'; -// Store PNG path for displaying many flags at once (for increased performance over SVG) -emojione.imagePathPNG = 'emojione/png/'; -// Use SVGs for emojis -emojione.imageType = 'svg'; +linkifyMatrix(linkify); // Anything outside the basic multilingual plane will be a surrogate pair const SURROGATE_PAIR_PATTERN = /([\ud800-\udbff])([\udc00-\udfff])/; -// And there a bunch more symbol characters that emojione has within the +// And there a bunch more symbol characters that emojibase has within the // BMP, so this includes the ranges from 'letterlike symbols' to // 'miscellaneous symbols and arrows' which should catch all of them // (with plenty of false positives, but that's OK) @@ -54,15 +50,15 @@ const ZWJ_REGEX = new RegExp("\u200D|\u2003", "g"); // Regex pattern for whitespace characters const WHITESPACE_REGEX = new RegExp("\\s", "g"); -// And this is emojione's complete regex -const EMOJI_REGEX = new RegExp(emojione.unicodeRegexp+"+", "gi"); +const BIGEMOJI_REGEX = new RegExp(`^(${EMOJIBASE_REGEX.source})+$`, 'i'); + const COLOR_REGEX = /^#[0-9a-fA-F]{6}$/; const PERMITTED_URL_SCHEMES = ['http', 'https', 'ftp', 'mailto', 'magnet']; /* * Return true if the given string contains emoji - * Uses a much, much simpler regex than emojione's so will give false + * Uses a much, much simpler regex than emojibase's so will give false * positives, but useful for fast-path testing strings to see if they * need emojification. * unicodeToImage uses this function. @@ -71,62 +67,27 @@ export function containsEmoji(str) { return SURROGATE_PAIR_PATTERN.test(str) || SYMBOL_PATTERN.test(str); } -/* modified from https://github.com/Ranks/emojione/blob/master/lib/js/emojione.js - * because we want to include emoji shortnames in title text +/** + * Returns the shortcode for an emoji character. + * + * @param {String} char The emoji character + * @return {String} The shortcode (such as :thumbup:) */ -function unicodeToImage(str, addAlt) { - if (addAlt === undefined) addAlt = true; - - let replaceWith; let unicode; let short; let fname; - const mappedUnicode = emojione.mapUnicodeToShort(); - - str = str.replace(emojione.regUnicode, function(unicodeChar) { - if ( (typeof unicodeChar === 'undefined') || (unicodeChar === '') || (!(unicodeChar in emojione.jsEscapeMap)) ) { - // if the unicodeChar doesnt exist just return the entire match - return unicodeChar; - } else { - // get the unicode codepoint from the actual char - unicode = emojione.jsEscapeMap[unicodeChar]; - - short = mappedUnicode[unicode]; - fname = emojione.emojioneList[short].fname; - - // depending on the settings, we'll either add the native unicode as the alt tag, otherwise the shortname - const title = mappedUnicode[unicode]; - - if (addAlt) { - const alt = (emojione.unicodeAlt) ? emojione.convert(unicode.toUpperCase()) : mappedUnicode[unicode]; - replaceWith = `${alt}`; - } else { - replaceWith = ``; - } - return replaceWith; - } - }); - - return str; +export function unicodeToShortcode(char) { + const data = EMOJIBASE.find(e => e.unicode === char); + return (data && data.shortcodes ? `:${data.shortcodes[0]}:` : ''); } /** - * Given one or more unicode characters (represented by unicode - * character number), return an image node with the corresponding - * emoji. + * Returns the unicode character for an emoji shortcode * - * @param alt {string} String to use for the image alt text - * @param useSvg {boolean} Whether to use SVG image src. If False, PNG will be used. - * @param unicode {integer} One or more integers representing unicode characters - * @returns A img node with the corresponding emoji + * @param {String} shortcode The shortcode (such as :thumbup:) + * @return {String} The emoji character; null if none exists */ -export function charactersToImageNode(alt, useSvg, ...unicode) { - const fileName = unicode.map((u) => { - return u.toString(16); - }).join('-'); - const path = useSvg ? emojione.imagePathSVG : emojione.imagePathPNG; - const fileType = useSvg ? 'svg' : 'png'; - return {alt}; +export function shortcodeToUnicode(shortcode) { + shortcode = shortcode.slice(1, shortcode.length - 1); + const data = EMOJIBASE.find(e => e.shortcodes && e.shortcodes.includes(shortcode)); + return data ? data.unicode : null; } export function processHtmlForSending(html: string): string { @@ -433,13 +394,10 @@ class TextHighlighter extends BaseHighlighter { * opts.disableBigEmoji: optional argument to disable the big emoji class. * opts.stripReplyFallback: optional argument specifying the event is a reply and so fallback needs removing * opts.returnString: return an HTML string rather than JSX elements - * opts.emojiOne: optional param to do emojiOne (default true) * opts.forComposerQuote: optional param to lessen the url rewriting done by sanitization, for quoting into composer */ export function bodyToHtml(content, highlights, opts={}) { const isHtmlMessage = content.format === "org.matrix.custom.html" && content.formatted_body; - - const doEmojiOne = opts.emojiOne === undefined ? true : opts.emojiOne; let bodyHasEmoji = false; let sanitizeParams = sanitizeHtmlParams; @@ -470,28 +428,12 @@ export function bodyToHtml(content, highlights, opts={}) { if (opts.stripReplyFallback && formattedBody) formattedBody = ReplyThread.stripHTMLReply(formattedBody); strippedBody = opts.stripReplyFallback ? ReplyThread.stripPlainReply(content.body) : content.body; - if (doEmojiOne) { - bodyHasEmoji = containsEmoji(isHtmlMessage ? formattedBody : content.body); - } + bodyHasEmoji = containsEmoji(isHtmlMessage ? formattedBody : content.body); // Only generate safeBody if the message was sent as org.matrix.custom.html if (isHtmlMessage) { isDisplayedWithHtml = true; safeBody = sanitizeHtml(formattedBody, sanitizeParams); - } else { - // ... or if there are emoji, which we insert as HTML alongside the - // escaped plaintext body. - if (bodyHasEmoji) { - isDisplayedWithHtml = true; - safeBody = sanitizeHtml(escape(strippedBody), sanitizeParams); - } - } - - // An HTML message with emoji - // or a plaintext message with emoji that was escaped and sanitized into - // HTML. - if (bodyHasEmoji) { - safeBody = unicodeToImage(safeBody); } } finally { delete sanitizeParams.textFilter; @@ -503,7 +445,6 @@ export function bodyToHtml(content, highlights, opts={}) { let emojiBody = false; if (!opts.disableBigEmoji && bodyHasEmoji) { - EMOJI_REGEX.lastIndex = 0; let contentBodyTrimmed = strippedBody !== undefined ? strippedBody.trim() : ''; // Ignore spaces in body text. Emojis with spaces in between should @@ -515,29 +456,25 @@ export function bodyToHtml(content, highlights, opts={}) { // presented as large. contentBodyTrimmed = contentBodyTrimmed.replace(ZWJ_REGEX, ''); - const match = EMOJI_REGEX.exec(contentBodyTrimmed); - emojiBody = match && match[0] && match[0].length === contentBodyTrimmed.length + const match = BIGEMOJI_REGEX.exec(contentBodyTrimmed); + emojiBody = match && match[0] && match[0].length === contentBodyTrimmed.length && // Prevent user pills expanding for users with only emoji in // their username - && (content.formatted_body == undefined - || !content.formatted_body.includes("https://matrix.to/")); + ( + content.formatted_body == undefined || + !content.formatted_body.includes("https://matrix.to/") + ); } const className = classNames({ 'mx_EventTile_body': true, 'mx_EventTile_bigEmoji': emojiBody, - 'markdown-body': isHtmlMessage, + 'markdown-body': isHtmlMessage && !emojiBody, }); return isDisplayedWithHtml ? - : - { strippedBody }; -} - -export function emojifyText(text, addAlt) { - return { - __html: unicodeToImage(escape(text), addAlt), - }; + : + { strippedBody }; } /** diff --git a/src/Lifecycle.js b/src/Lifecycle.js index a7f90f847d..b8494ce9f5 100644 --- a/src/Lifecycle.js +++ b/src/Lifecycle.js @@ -33,6 +33,7 @@ import ActiveWidgetStore from './stores/ActiveWidgetStore'; import PlatformPeg from "./PlatformPeg"; import { sendLoginRequest } from "./Login"; import * as StorageManager from './utils/StorageManager'; +import SettingsStore from "./settings/SettingsStore"; /** * Called at startup, to attempt to build a logged-in Matrix session. It tries @@ -499,7 +500,9 @@ async function startMatrixClient() { Notifier.start(); UserActivity.sharedInstance().start(); - Presence.start(); + if (!SettingsStore.getValue("lowBandwidth")) { + Presence.start(); + } DMRoomMap.makeShared().start(); ActiveWidgetStore.start(); diff --git a/src/MatrixClientPeg.js b/src/MatrixClientPeg.js index 82bd273846..574f05bf85 100644 --- a/src/MatrixClientPeg.js +++ b/src/MatrixClientPeg.js @@ -119,7 +119,7 @@ class MatrixClientPeg { // try to initialise e2e on the new client try { // check that we have a version of the js-sdk which includes initCrypto - if (this.matrixClient.initCrypto) { + if (!SettingsStore.getValue("lowBandwidth") && this.matrixClient.initCrypto) { await this.matrixClient.initCrypto(); StorageManager.setCryptoInitialised(true); } @@ -188,8 +188,7 @@ class MatrixClientPeg { timelineSupport: true, forceTURN: !SettingsStore.getValue('webRtcAllowPeerToPeer', false), verificationMethods: [verificationMethods.SAS], - unstableClientRelationAggregation: aggregateRelations, - unstableClientRelationReplacements: enableEdits, + unstableClientRelationAggregation: aggregateRelations || enableEdits, }; this.matrixClient = createMatrixClient(opts); diff --git a/src/Notifier.js b/src/Notifier.js index 8c62a9e822..7410fe316a 100644 --- a/src/Notifier.js +++ b/src/Notifier.js @@ -85,7 +85,11 @@ const Notifier = { msg = ''; } - const avatarUrl = ev.sender ? Avatar.avatarUrlForMember(ev.sender, 40, 40, 'crop') : null; + let avatarUrl = null; + if (ev.sender && !SettingsStore.getValue("lowBandwidth")) { + avatarUrl = Avatar.avatarUrlForMember(ev.sender, 40, 40, 'crop'); + } + const notif = plaf.displayNotification(title, msg, avatarUrl, room); // if displayNotification returns non-null, the platform supports diff --git a/src/RichText.js b/src/RichText.js deleted file mode 100644 index 3e8f834da6..0000000000 --- a/src/RichText.js +++ /dev/null @@ -1,40 +0,0 @@ -/* -Copyright 2015 - 2017 OpenMarket Ltd -Copyright 2017 Vector Creations Ltd -Copyright 2018 New Vector Ltd - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import * as emojione from 'emojione'; - - -export function unicodeToEmojiUri(str) { - const mappedUnicode = emojione.mapUnicodeToShort(); - - // remove any zero width joiners/spaces used in conjugate emojis as the emojione URIs don't contain them - return str.replace(emojione.regUnicode, function(unicodeChar) { - if ((typeof unicodeChar === 'undefined') || (unicodeChar === '') || (!(unicodeChar in emojione.jsEscapeMap))) { - // if the unicodeChar doesn't exist just return the entire match - return unicodeChar; - } else { - // get the unicode codepoint from the actual char - const unicode = emojione.jsEscapeMap[unicodeChar]; - - const short = mappedUnicode[unicode]; - const fname = emojione.emojioneList[short].fname; - - return emojione.imagePathSVG+fname+'.svg'+emojione.cacheBustParam; - } - }); -} diff --git a/src/RoomInvite.js b/src/RoomInvite.js index b808b935a6..34b9635780 100644 --- a/src/RoomInvite.js +++ b/src/RoomInvite.js @@ -45,7 +45,7 @@ export function showStartChatInviteDialog() { Modal.createTrackedDialog('Start a chat', '', AddressPickerDialog, { title: _t('Start a chat'), description: _t("Who would you like to communicate with?"), - placeholder: _t("Email, name or matrix ID"), + placeholder: _t("Email, name or Matrix ID"), validAddressTypes: ['mx-user-id', 'email'], button: _t("Start Chat"), onFinished: _onStartChatFinished, @@ -58,7 +58,7 @@ export function showRoomInviteDialog(roomId) { title: _t('Invite new room members'), description: _t('Who would you like to add to this room?'), button: _t('Send Invites'), - placeholder: _t("Email, name or matrix ID"), + placeholder: _t("Email, name or Matrix ID"), onFinished: (shouldInvite, addrs) => { _onRoomInviteFinished(roomId, shouldInvite, addrs); }, diff --git a/src/SlashCommands.js b/src/SlashCommands.js index 55107db899..f25bc9af07 100644 --- a/src/SlashCommands.js +++ b/src/SlashCommands.js @@ -518,7 +518,7 @@ export const CommandMap = { unban: new Command({ name: 'unban', args: '', - description: _td('Unbans user with given id'), + description: _td('Unbans user with given ID'), runFn: function(roomId, args) { if (args) { const matches = args.match(/^(\S+)$/); diff --git a/src/autocomplete/EmojiProvider.js b/src/autocomplete/EmojiProvider.js index 704cdbd55d..8afcba6ab0 100644 --- a/src/autocomplete/EmojiProvider.js +++ b/src/autocomplete/EmojiProvider.js @@ -19,47 +19,31 @@ limitations under the License. import React from 'react'; import { _t } from '../languageHandler'; import AutocompleteProvider from './AutocompleteProvider'; -import {shortnameToUnicode, asciiRegexp, unicodeRegexp} from 'emojione'; import QueryMatcher from './QueryMatcher'; -import sdk from '../index'; import {PillCompletion} from './Components'; import type {Completion, SelectionRange} from './Autocompleter'; import _uniq from 'lodash/uniq'; import _sortBy from 'lodash/sortBy'; import SettingsStore from "../settings/SettingsStore"; +import { shortcodeToUnicode } from '../HtmlUtils'; +import EMOTICON_REGEX from 'emojibase-regex/emoticon'; import EmojiData from '../stripped-emoji.json'; const LIMIT = 20; -const CATEGORY_ORDER = [ - 'people', - 'food', - 'objects', - 'activity', - 'nature', - 'travel', - 'flags', - 'regional', - 'symbols', - 'modifier', -]; -// Match for ":wink:" or ascii-style ";-)" provided by emojione -// (^|\s|(emojiUnicode)) to make sure we're either at the start of the string or there's a -// whitespace character or an emoji before the emoji. The reason for unicodeRegexp is -// that we need to support inputting multiple emoji with no space between them. -const EMOJI_REGEX = new RegExp('(?:^|\\s|' + unicodeRegexp + ')(' + asciiRegexp + '|:[+-\\w]*:?)$', 'g'); - -// We also need to match the non-zero-length prefixes to remove them from the final match, -// and update the range so that we don't replace the whitespace or the previous emoji. -const MATCH_PREFIX_REGEX = new RegExp('(\\s|' + unicodeRegexp + ')'); +// Match for ascii-style ";-)" emoticons or ":wink:" shortcodes provided by emojibase +const EMOJI_REGEX = new RegExp('(' + EMOTICON_REGEX.source + '|:[+-\\w]*:?)$', 'g'); +// 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 CATEGORY_ORDER.indexOf(a.category) - CATEGORY_ORDER.indexOf(b.category); + return a.category - b.category; }, ).map((a, index) => { return { @@ -101,26 +85,20 @@ export default class EmojiProvider extends AutocompleteProvider { return []; // don't give any suggestions if the user doesn't want them } - const EmojiText = sdk.getComponent('views.elements.EmojiText'); - let completions = []; const {command, range} = this.getCurrentCommand(query, selection); if (command) { - let matchedString = command[0]; - - // Remove prefix of any length (single whitespace or unicode emoji) - const prefixMatch = MATCH_PREFIX_REGEX.exec(matchedString); - if (prefixMatch) { - matchedString = matchedString.slice(prefixMatch[0].length); - range.start += prefixMatch[0].length; - } + const matchedString = command[0]; completions = this.matcher.match(matchedString); // Do second match with shouldMatchWordsOnly in order to match against 'name' completions = completions.concat(this.nameMatcher.match(matchedString)); const sorters = []; - // First, sort by score (Infinity if matchedString not in shortname) + // make sure that emoticons come first + sorters.push((c) => score(matchedString, c.aliases_ascii)); + + // then sort by score (Infinity if matchedString not in shortname) sorters.push((c) => score(matchedString, c.shortname)); // If the matchedString is not empty, sort by length of shortname. Example: // matchedString = ":bookmark" @@ -133,12 +111,12 @@ export default class EmojiProvider extends AutocompleteProvider { completions = _sortBy(_uniq(completions), sorters); completions = completions.map((result) => { - const {shortname} = result; - const unicode = shortnameToUnicode(shortname); + const { shortname } = result; + const unicode = shortcodeToUnicode(shortname); return { completion: unicode, component: ( - { unicode }} /> + { unicode }} /> ), range, }; diff --git a/src/components/structures/GroupView.js b/src/components/structures/GroupView.js index dcbe212267..cdfbe26fea 100644 --- a/src/components/structures/GroupView.js +++ b/src/components/structures/GroupView.js @@ -265,7 +265,7 @@ const RoleUserList = React.createClass({ Modal.createTrackedDialog('Add Users to Group Summary', '', AddressPickerDialog, { title: _t('Add users to the community summary'), description: _t("Who would you like to add to this summary?"), - placeholder: _t("Name or matrix ID"), + placeholder: _t("Name or Matrix ID"), button: _t("Add to summary"), validAddressTypes: ['mx-user-id'], groupId: this.props.groupId, diff --git a/src/components/structures/IndicatorScrollbar.js b/src/components/structures/IndicatorScrollbar.js index a615717104..a62206b32b 100644 --- a/src/components/structures/IndicatorScrollbar.js +++ b/src/components/structures/IndicatorScrollbar.js @@ -129,7 +129,7 @@ export default class IndicatorScrollbar extends React.Component { // the harshness of the scroll behaviour. Should be a value between 0 and 1. const yRetention = 1.0; - if (Math.abs(e.deltaX) < xyThreshold) { + if (Math.abs(e.deltaX) <= xyThreshold) { // noinspection JSSuspiciousNameCombination this._scrollElement.scrollLeft += e.deltaY * yRetention; } diff --git a/src/components/structures/InteractiveAuth.js b/src/components/structures/InteractiveAuth.js index 89ff4d43a3..c893057022 100644 --- a/src/components/structures/InteractiveAuth.js +++ b/src/components/structures/InteractiveAuth.js @@ -1,5 +1,6 @@ /* Copyright 2017 Vector Creations 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. @@ -60,7 +61,7 @@ export default React.createClass({ inputs: PropTypes.object, // As js-sdk interactive-auth - makeRegistrationUrl: PropTypes.func, + requestEmailToken: PropTypes.func, sessionId: PropTypes.string, clientSecret: PropTypes.string, emailSid: PropTypes.string, @@ -96,6 +97,7 @@ export default React.createClass({ sessionId: this.props.sessionId, clientSecret: this.props.clientSecret, emailSid: this.props.emailSid, + requestEmailToken: this.props.requestEmailToken, }); this._authLogic.attemptAuth().then((result) => { @@ -202,7 +204,6 @@ export default React.createClass({ stageState={this.state.stageState} fail={this._onAuthStageFailed} setEmailSid={this._setEmailSid} - makeRegistrationUrl={this.props.makeRegistrationUrl} showContinue={!this.props.continueIsManaged} /> ); diff --git a/src/components/structures/LoggedInView.js b/src/components/structures/LoggedInView.js index 4771c6f487..7a5c6e3006 100644 --- a/src/components/structures/LoggedInView.js +++ b/src/components/structures/LoggedInView.js @@ -24,6 +24,7 @@ import { DragDropContext } from 'react-beautiful-dnd'; import { KeyCode, isOnlyCtrlOrCmdKeyEvent } from '../../Keyboard'; import PageTypes from '../../PageTypes'; import CallMediaHandler from '../../CallMediaHandler'; +import { fixupColorFonts } from '../../utils/FontManager'; import sdk from '../../index'; import dis from '../../dispatcher'; import sessionStore from '../../stores/SessionStore'; @@ -118,6 +119,8 @@ const LoggedInView = React.createClass({ this._matrixClient.on("accountData", this.onAccountData); this._matrixClient.on("sync", this.onSync); this._matrixClient.on("RoomState.events", this.onRoomStateEvents); + + fixupColorFonts(); }, componentDidUpdate(prevProps) { @@ -322,6 +325,18 @@ const LoggedInView = React.createClass({ handled = true; } break; + case KeyCode.KEY_I: + // Ideally this would be CTRL+P for "Profile", but that's + // taken by the print dialog. CTRL+I for "Information" + // will have to do. + + if (ctrlCmdOnly) { + dis.dispatch({ + action: 'toggle_top_left_menu', + }); + handled = true; + } + break; } if (handled) { diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 0b52cfa1bc..ac328f8387 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -50,8 +50,7 @@ import SettingsStore, {SettingLevel} from "../../settings/SettingsStore"; import { startAnyRegistrationFlow } from "../../Registration.js"; import { messageForSyncError } from '../../utils/ErrorUtils'; import ResizeNotifier from "../../utils/ResizeNotifier"; - -const AutoDiscovery = Matrix.AutoDiscovery; +import {ValidatedServerConfig} from "../../utils/AutoDiscoveryUtils"; // Disable warnings for now: we use deprecated bluebird functions // and need to migrate, but they spam the console with warnings. @@ -109,6 +108,7 @@ export default React.createClass({ propTypes: { config: PropTypes.object, + serverConfig: PropTypes.instanceOf(ValidatedServerConfig), ConferenceHandler: PropTypes.any, onNewScreen: PropTypes.func, registrationUrl: PropTypes.string, @@ -181,16 +181,8 @@ export default React.createClass({ // Parameters used in the registration dance with the IS register_client_secret: null, register_session_id: null, - register_hs_url: null, - register_is_url: null, register_id_sid: null, - // Parameters used for setting up the authentication views - defaultServerName: this.props.config.default_server_name, - defaultHsUrl: this.props.config.default_hs_url, - defaultIsUrl: this.props.config.default_is_url, - defaultServerDiscoveryError: null, - // When showing Modal dialogs we need to set aria-hidden on the root app element // and disable it when there are no dialogs hideToSRUsers: false, @@ -211,42 +203,19 @@ export default React.createClass({ }; }, - getDefaultServerName: function() { - return this.state.defaultServerName; - }, - - getCurrentHsUrl: function() { - if (this.state.register_hs_url) { - return this.state.register_hs_url; - } else if (MatrixClientPeg.get()) { - return MatrixClientPeg.get().getHomeserverUrl(); - } else { - return this.getDefaultHsUrl(); - } - }, - - getDefaultHsUrl(defaultToMatrixDotOrg) { - defaultToMatrixDotOrg = typeof(defaultToMatrixDotOrg) !== 'boolean' ? true : defaultToMatrixDotOrg; - if (!this.state.defaultHsUrl && defaultToMatrixDotOrg) return "https://matrix.org"; - return this.state.defaultHsUrl; - }, - getFallbackHsUrl: function() { - return this.props.config.fallback_hs_url; - }, - - getCurrentIsUrl: function() { - if (this.state.register_is_url) { - return this.state.register_is_url; - } else if (MatrixClientPeg.get()) { - return MatrixClientPeg.get().getIdentityServerUrl(); + if (this.props.serverConfig && this.props.serverConfig.isDefault) { + return this.props.config.fallback_hs_url; } else { - return this.getDefaultIsUrl(); + return null; } }, - getDefaultIsUrl() { - return this.state.defaultIsUrl || "https://vector.im"; + getServerProperties() { + let props = this.state.serverConfig; + if (!props) props = this.props.serverConfig; // for unit tests + if (!props) props = SdkConfig.get()["validated_server_config"]; + return {serverConfig: props}; }, componentWillMount: function() { @@ -260,40 +229,6 @@ export default React.createClass({ MatrixClientPeg.opts.initialSyncLimit = this.props.config.sync_timeline_limit; } - // Set up the default URLs (async) - if (this.getDefaultServerName() && !this.getDefaultHsUrl(false)) { - this.setState({loadingDefaultHomeserver: true}); - this._tryDiscoverDefaultHomeserver(this.getDefaultServerName()); - } else if (this.getDefaultServerName() && this.getDefaultHsUrl(false)) { - // Ideally we would somehow only communicate this to the server admins, but - // given this is at login time we can't really do much besides hope that people - // will check their settings. - this.setState({ - defaultServerName: null, // To un-hide any secrets people might be keeping - defaultServerDiscoveryError: _t( - "Invalid configuration: Cannot supply a default homeserver URL and " + - "a default server name", - ), - }); - } - - // Set a default HS with query param `hs_url` - const paramHs = this.props.startingFragmentQueryParams.hs_url; - if (paramHs) { - console.log('Setting register_hs_url ', paramHs); - this.setState({ - register_hs_url: paramHs, - }); - } - // Set a default IS with query param `is_url` - const paramIs = this.props.startingFragmentQueryParams.is_url; - if (paramIs) { - console.log('Setting register_is_url ', paramIs); - this.setState({ - register_is_url: paramIs, - }); - } - // a thing to call showScreen with once login completes. this is kept // outside this.state because updating it should never trigger a // rerender. @@ -374,8 +309,8 @@ export default React.createClass({ return Lifecycle.loadSession({ fragmentQueryParams: this.props.startingFragmentQueryParams, enableGuest: this.props.enableGuest, - guestHsUrl: this.getCurrentHsUrl(), - guestIsUrl: this.getCurrentIsUrl(), + guestHsUrl: this.getServerProperties().serverConfig.hsUrl, + guestIsUrl: this.getServerProperties().serverConfig.isUrl, defaultDeviceDisplayName: this.props.defaultDeviceDisplayName, }); }).then((loadedSession) => { @@ -1827,44 +1762,7 @@ export default React.createClass({ }, onServerConfigChange(config) { - const newState = {}; - if (config.hsUrl) { - newState.register_hs_url = config.hsUrl; - } - if (config.isUrl) { - newState.register_is_url = config.isUrl; - } - this.setState(newState); - }, - - _tryDiscoverDefaultHomeserver: async function(serverName) { - try { - const discovery = await AutoDiscovery.findClientConfig(serverName); - const state = discovery["m.homeserver"].state; - if (state !== AutoDiscovery.SUCCESS) { - console.error("Failed to discover homeserver on startup:", discovery); - this.setState({ - defaultServerDiscoveryError: discovery["m.homeserver"].error, - loadingDefaultHomeserver: false, - }); - } else { - const hsUrl = discovery["m.homeserver"].base_url; - const isUrl = discovery["m.identity_server"].state === AutoDiscovery.SUCCESS - ? discovery["m.identity_server"].base_url - : "https://vector.im"; - this.setState({ - defaultHsUrl: hsUrl, - defaultIsUrl: isUrl, - loadingDefaultHomeserver: false, - }); - } - } catch (e) { - console.error(e); - this.setState({ - defaultServerDiscoveryError: _t("Unknown error discovering homeserver"), - loadingDefaultHomeserver: false, - }); - } + this.setState({serverConfig: config}); }, _makeRegistrationUrl: function(params) { @@ -1883,8 +1781,7 @@ export default React.createClass({ if ( this.state.view === VIEWS.LOADING || - this.state.view === VIEWS.LOGGING_IN || - this.state.loadingDefaultHomeserver + this.state.view === VIEWS.LOGGING_IN ) { const Spinner = sdk.getComponent('elements.Spinner'); return ( @@ -1962,18 +1859,13 @@ export default React.createClass({ sessionId={this.state.register_session_id} idSid={this.state.register_id_sid} email={this.props.startingFragmentQueryParams.email} - defaultServerName={this.getDefaultServerName()} - defaultServerDiscoveryError={this.state.defaultServerDiscoveryError} - defaultHsUrl={this.getDefaultHsUrl()} - defaultIsUrl={this.getDefaultIsUrl()} brand={this.props.config.brand} - customHsUrl={this.getCurrentHsUrl()} - customIsUrl={this.getCurrentIsUrl()} makeRegistrationUrl={this._makeRegistrationUrl} onLoggedIn={this.onRegistered} onLoginClick={this.onLoginClick} onServerConfigChange={this.onServerConfigChange} - /> + {...this.getServerProperties()} + /> ); } @@ -1982,14 +1874,11 @@ export default React.createClass({ const ForgotPassword = sdk.getComponent('structures.auth.ForgotPassword'); return ( + onLoginClick={this.onLoginClick} + onServerConfigChange={this.onServerConfigChange} + {...this.getServerProperties()} + /> ); } @@ -1999,16 +1888,11 @@ export default React.createClass({ ); } diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index 6f21bb6951..657b01c663 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -24,6 +24,7 @@ import {wantsDateSeparator} from '../../DateUtils'; import sdk from '../../index'; import MatrixClientPeg from '../../MatrixClientPeg'; +import SettingsStore from '../../settings/SettingsStore'; const CONTINUATION_MAX_INTERVAL = 5 * 60 * 1000; // 5 minutes const continuedTypes = ['m.sticker', 'm.room.message']; @@ -95,6 +96,9 @@ module.exports = React.createClass({ // helper function to access relations for an event getRelationsForEvent: PropTypes.func, + + // whether to show reactions for an event + showReactions: PropTypes.bool, }, componentWillMount: function() { @@ -230,6 +234,13 @@ module.exports = React.createClass({ } }, + scrollToEventIfNeeded: function(eventId) { + const node = this.eventNodes[eventId]; + if (node) { + node.scrollIntoView({block: "nearest", behavior: "instant"}); + } + }, + /* check the scroll state and send out pagination requests if necessary. */ checkFillState: function() { @@ -248,6 +259,10 @@ module.exports = React.createClass({ return false; // ignored = no show (only happens if the ignore happens after an event was received) } + if (SettingsStore.getValue("showHiddenEventsInTimeline")) { + return true; + } + const EventTile = sdk.getComponent('rooms.EventTile'); if (!EventTile.haveTileForEvent(mxEv)) { return false; // no tile = no show @@ -450,14 +465,10 @@ module.exports = React.createClass({ _getTilesForEvent: function(prevEvent, mxEv, last) { const EventTile = sdk.getComponent('rooms.EventTile'); - const MessageEditor = sdk.getComponent('elements.MessageEditor'); const DateSeparator = sdk.getComponent('messages.DateSeparator'); const ret = []; - if (this.props.editEvent && this.props.editEvent.getId() === mxEv.getId()) { - return []; - } - + const isEditing = this.props.editEvent && this.props.editEvent.getId() === mxEv.getId(); // is this a continuation of the previous message? let continuation = false; @@ -527,18 +538,20 @@ module.exports = React.createClass({ continuation={continuation} isRedacted={mxEv.isRedacted()} replacingEventId={mxEv.replacingEventId()} + isEditing={isEditing} onHeightChanged={this._onHeightChanged} readReceipts={readReceipts} readReceiptMap={this._readReceiptMap} showUrlPreview={this.props.showUrlPreview} checkUnmounting={this._isUnmounting} - eventSendStatus={mxEv.status} + eventSendStatus={mxEv.replacementOrOwnStatus()} tileShape={this.props.tileShape} isTwelveHour={this.props.isTwelveHour} permalinkCreator={this.props.permalinkCreator} last={last} isSelectedEvent={highlight} getRelationsForEvent={this.props.getRelationsForEvent} + showReactions={this.props.showReactions} /> , ); @@ -714,7 +727,7 @@ module.exports = React.createClass({ ); let whoIsTyping; - if (this.props.room) { + if (this.props.room && !this.props.tileShape) { whoIsTyping = ( diff --git a/src/components/structures/RoomStatusBar.js b/src/components/structures/RoomStatusBar.js index 1df0581b89..7ef080e235 100644 --- a/src/components/structures/RoomStatusBar.js +++ b/src/components/structures/RoomStatusBar.js @@ -304,8 +304,6 @@ module.exports = React.createClass({ // return suitable content for the main (text) part of the status bar. _getContent: function() { - const EmojiText = sdk.getComponent('elements.EmojiText'); - if (this._shouldShowConnectionError()) { return (
diff --git a/src/components/structures/RoomSubList.js b/src/components/structures/RoomSubList.js index d094d88c24..9ca9d3261d 100644 --- a/src/components/structures/RoomSubList.js +++ b/src/components/structures/RoomSubList.js @@ -29,6 +29,7 @@ import { Group } from 'matrix-js-sdk'; import PropTypes from 'prop-types'; import RoomTile from "../views/rooms/RoomTile"; import LazyRenderList from "../views/elements/LazyRenderList"; +import {_t} from "../../languageHandler"; // turn this on for drop & drag console debugging galore const debug = false; @@ -42,6 +43,7 @@ const RoomSubList = React.createClass({ list: PropTypes.arrayOf(PropTypes.object).isRequired, label: PropTypes.string.isRequired, tagName: PropTypes.string, + addRoomLabel: PropTypes.string, order: PropTypes.string.isRequired, @@ -232,7 +234,11 @@ const RoomSubList = React.createClass({ let addRoomButton; if (this.props.onAddRoom) { addRoomButton = ( - + ); } diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 5f4dc984d0..7c0710a18d 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -1832,6 +1832,7 @@ module.exports = React.createClass({ membersLoaded={this.state.membersLoaded} permalinkCreator={this._getPermalinkCreatorForRoom(this.state.room)} resizeNotifier={this.props.resizeNotifier} + showReactions={true} />); let topUnreadMessagesBar = null; diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index 3e79ea7f18..c939d31f94 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -106,6 +106,9 @@ const TimelinePanel = React.createClass({ // placeholder text to use if the timeline is empty empty: PropTypes.string, + + // whether to show reactions for an event + showReactions: PropTypes.bool, }, statics: { @@ -204,11 +207,11 @@ const TimelinePanel = React.createClass({ MatrixClientPeg.get().on("Room.timeline", this.onRoomTimeline); MatrixClientPeg.get().on("Room.timelineReset", this.onRoomTimelineReset); MatrixClientPeg.get().on("Room.redaction", this.onRoomRedaction); - MatrixClientPeg.get().on("Room.replaceEvent", this.onRoomReplaceEvent); MatrixClientPeg.get().on("Room.receipt", this.onRoomReceipt); MatrixClientPeg.get().on("Room.localEchoUpdated", this.onLocalEchoUpdated); MatrixClientPeg.get().on("Room.accountData", this.onAccountData); MatrixClientPeg.get().on("Event.decrypted", this.onEventDecrypted); + MatrixClientPeg.get().on("Event.replaced", this.onEventReplaced); MatrixClientPeg.get().on("sync", this.onSync); this._initTimeline(this.props); @@ -283,11 +286,11 @@ const TimelinePanel = React.createClass({ client.removeListener("Room.timeline", this.onRoomTimeline); client.removeListener("Room.timelineReset", this.onRoomTimelineReset); client.removeListener("Room.redaction", this.onRoomRedaction); - client.removeListener("Room.replaceEvent", this.onRoomReplaceEvent); client.removeListener("Room.receipt", this.onRoomReceipt); client.removeListener("Room.localEchoUpdated", this.onLocalEchoUpdated); client.removeListener("Room.accountData", this.onAccountData); client.removeListener("Event.decrypted", this.onEventDecrypted); + client.removeListener("Event.replaced", this.onEventReplaced); client.removeListener("sync", this.onSync); } }, @@ -405,7 +408,13 @@ const TimelinePanel = React.createClass({ this.forceUpdate(); } if (payload.action === "edit_event") { - this.setState({editEvent: payload.event}); + this.setState({editEvent: payload.event}, () => { + if (payload.event && this.refs.messagePanel) { + this.refs.messagePanel.scrollToEventIfNeeded( + payload.event.getId(), + ); + } + }); } }, @@ -507,7 +516,7 @@ const TimelinePanel = React.createClass({ this.forceUpdate(); }, - onRoomReplaceEvent: function(replacedEvent, room) { + onEventReplaced: function(replacedEvent, room) { if (this.unmounted) return; // ignore events for other rooms @@ -553,6 +562,9 @@ const TimelinePanel = React.createClass({ }, onEventDecrypted: function(ev) { + // Can be null for the notification timeline, etc. + if (!this.props.timelineSet.room) return; + // Need to update as we don't display event tiles for events that // haven't yet been decrypted. The event will have just been updated // in place so we just need to re-render. @@ -601,6 +613,8 @@ const TimelinePanel = React.createClass({ }, sendReadReceipt: function() { + if (SettingsStore.getValue("lowBandwidth")) return; + if (!this.refs.messagePanel) return; if (!this.props.manageReadReceipts) return; // This happens on user_activity_end which is delayed, and it's @@ -1261,6 +1275,7 @@ const TimelinePanel = React.createClass({ resizeNotifier={this.props.resizeNotifier} getRelationsForEvent={this.getRelationsForEvent} editEvent={this.state.editEvent} + showReactions={this.props.showReactions} /> ); }, diff --git a/src/components/structures/TopLeftMenuButton.js b/src/components/structures/TopLeftMenuButton.js index b68d3a95a0..f745a7f7bc 100644 --- a/src/components/structures/TopLeftMenuButton.js +++ b/src/components/structures/TopLeftMenuButton.js @@ -1,5 +1,6 @@ /* Copyright 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. @@ -23,6 +24,8 @@ import BaseAvatar from '../views/avatars/BaseAvatar'; import MatrixClientPeg from '../../MatrixClientPeg'; import Avatar from '../../Avatar'; import { _t } from '../../languageHandler'; +import dis from "../../dispatcher"; +import {focusCapturedRef} from "../../utils/Accessibility"; const AVATAR_SIZE = 28; @@ -37,6 +40,7 @@ export default class TopLeftMenuButton extends React.Component { super(); this.state = { menuDisplayed: false, + menuFunctions: null, // should be { close: fn } profileInfo: null, }; @@ -59,6 +63,8 @@ export default class TopLeftMenuButton extends React.Component { } async componentDidMount() { + this._dispatcherRef = dis.register(this.onAction); + try { const profileInfo = await this._getProfileInfo(); this.setState({profileInfo}); @@ -68,6 +74,17 @@ export default class TopLeftMenuButton extends React.Component { } } + componentWillUnmount() { + dis.unregister(this._dispatcherRef); + } + + onAction = (payload) => { + // For accessibility + if (payload.action === "toggle_top_left_menu") { + if (this._buttonRef) this._buttonRef.click(); + } + }; + _getDisplayName() { if (MatrixClientPeg.get().isGuest()) { return _t("Guest"); @@ -88,7 +105,13 @@ export default class TopLeftMenuButton extends React.Component { } return ( - + this._buttonRef = r} + aria-label={_t("Your profile")} + > { nameElement } - + ); } @@ -107,20 +130,26 @@ export default class TopLeftMenuButton extends React.Component { e.preventDefault(); e.stopPropagation(); + if (this.state.menuDisplayed && this.state.menuFunctions) { + this.state.menuFunctions.close(); + return; + } + const elementRect = e.currentTarget.getBoundingClientRect(); const x = elementRect.left; const y = elementRect.top + elementRect.height; - ContextualMenu.createMenu(TopLeftMenu, { + const menuFunctions = ContextualMenu.createMenu(TopLeftMenu, { chevronFace: "none", left: x, top: y, userId: MatrixClientPeg.get().getUserId(), displayName: this._getDisplayName(), + containerRef: focusCapturedRef, // Focus the TopLeftMenu on first render onFinished: () => { - this.setState({ menuDisplayed: false }); + this.setState({ menuDisplayed: false, menuFunctions: null }); }, }); - this.setState({ menuDisplayed: true }); + this.setState({ menuDisplayed: true, menuFunctions }); } } diff --git a/src/components/structures/auth/ForgotPassword.js b/src/components/structures/auth/ForgotPassword.js index 46071f0a9c..2ea39ad657 100644 --- a/src/components/structures/auth/ForgotPassword.js +++ b/src/components/structures/auth/ForgotPassword.js @@ -21,8 +21,8 @@ import { _t } from '../../../languageHandler'; import sdk from '../../../index'; import Modal from "../../../Modal"; import SdkConfig from "../../../SdkConfig"; - import PasswordReset from "../../../PasswordReset"; +import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils"; // Phases // Show controls to configure server details @@ -40,28 +40,14 @@ module.exports = React.createClass({ displayName: 'ForgotPassword', propTypes: { - // The default server name to use when the user hasn't specified - // one. If set, `defaultHsUrl` and `defaultHsUrl` were derived for this - // via `.well-known` discovery. The server name is used instead of the - // HS URL when talking about "your account". - defaultServerName: PropTypes.string, - // An error passed along from higher up explaining that something - // went wrong when finding the defaultHsUrl. - defaultServerDiscoveryError: PropTypes.string, - - defaultHsUrl: PropTypes.string, - defaultIsUrl: PropTypes.string, - customHsUrl: PropTypes.string, - customIsUrl: PropTypes.string, - + serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired, + onServerConfigChange: PropTypes.func.isRequired, onLoginClick: PropTypes.func, onComplete: PropTypes.func.isRequired, }, getInitialState: function() { return { - enteredHsUrl: this.props.customHsUrl || this.props.defaultHsUrl, - enteredIsUrl: this.props.customIsUrl || this.props.defaultIsUrl, phase: PHASE_FORGOT, email: "", password: "", @@ -70,11 +56,11 @@ module.exports = React.createClass({ }; }, - submitPasswordReset: function(hsUrl, identityUrl, email, password) { + submitPasswordReset: function(email, password) { this.setState({ phase: PHASE_SENDING_EMAIL, }); - this.reset = new PasswordReset(hsUrl, identityUrl); + this.reset = new PasswordReset(this.props.serverConfig.hsUrl, this.props.serverConfig.isUrl); this.reset.resetPassword(email, password).done(() => { this.setState({ phase: PHASE_EMAIL_SENT, @@ -103,13 +89,6 @@ module.exports = React.createClass({ onSubmitForm: function(ev) { ev.preventDefault(); - // Don't allow the user to register if there's a discovery error - // Without this, the user could end up registering on the wrong homeserver. - if (this.props.defaultServerDiscoveryError) { - this.setState({errorText: this.props.defaultServerDiscoveryError}); - return; - } - if (!this.state.email) { this.showErrorDialog(_t('The email address linked to your account must be entered.')); } else if (!this.state.password || !this.state.password2) { @@ -132,10 +111,7 @@ module.exports = React.createClass({ button: _t('Continue'), onFinished: (confirmed) => { if (confirmed) { - this.submitPasswordReset( - this.state.enteredHsUrl, this.state.enteredIsUrl, - this.state.email, this.state.password, - ); + this.submitPasswordReset(this.state.email, this.state.password); } }, }); @@ -148,19 +124,7 @@ module.exports = React.createClass({ }); }, - onServerConfigChange: function(config) { - const newState = {}; - if (config.hsUrl !== undefined) { - newState.enteredHsUrl = config.hsUrl; - } - if (config.isUrl !== undefined) { - newState.enteredIsUrl = config.isUrl; - } - this.setState(newState); - }, - - onServerDetailsNextPhaseClick(ev) { - ev.stopPropagation(); + async onServerDetailsNextPhaseClick() { this.setState({ phase: PHASE_FORGOT, }); @@ -190,26 +154,19 @@ module.exports = React.createClass({ renderServerDetails() { const ServerConfig = sdk.getComponent("auth.ServerConfig"); - const AccessibleButton = sdk.getComponent("elements.AccessibleButton"); if (SdkConfig.get()['disable_custom_urls']) { return null; } - return
- - - {_t("Next")} - -
; + return ; }, renderForgot() { @@ -221,25 +178,22 @@ module.exports = React.createClass({ errorText =
{ err }
; } - let yourMatrixAccountText = _t('Your Matrix account'); - if (this.state.enteredHsUrl === this.props.defaultHsUrl && this.props.defaultServerName) { - yourMatrixAccountText = _t('Your Matrix account on %(serverName)s', { - serverName: this.props.defaultServerName, + let yourMatrixAccountText = _t('Your Matrix account on %(serverName)s', { + serverName: this.props.serverConfig.hsName, + }); + if (this.props.serverConfig.hsNameIsDifferent) { + const TextWithTooltip = sdk.getComponent("elements.TextWithTooltip"); + + yourMatrixAccountText = _t('Your Matrix account on ', {}, { + 'underlinedServerName': () => { + return ; + }, }); - } else { - try { - const parsedHsUrl = new URL(this.state.enteredHsUrl); - yourMatrixAccountText = _t('Your Matrix account on %(serverName)s', { - serverName: parsedHsUrl.hostname, - }); - } catch (e) { - errorText =
{_t( - "The homeserver URL %(hsUrl)s doesn't seem to be valid URL. Please " + - "enter a valid URL including the protocol prefix.", - { - hsUrl: this.state.enteredHsUrl, - })}
; - } } // If custom URLs are allowed, wire up the server details edit link. diff --git a/src/components/structures/auth/Login.js b/src/components/structures/auth/Login.js index 2940346a4f..aed8e9fca0 100644 --- a/src/components/structures/auth/Login.js +++ b/src/components/structures/auth/Login.js @@ -20,12 +20,12 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; -import { _t, _td } from '../../../languageHandler'; +import {_t, _td} from '../../../languageHandler'; import sdk from '../../../index'; import Login from '../../../Login'; import SdkConfig from '../../../SdkConfig'; import { messageForResourceLimitError } from '../../../utils/ErrorUtils'; -import { AutoDiscovery } from "matrix-js-sdk"; +import AutoDiscoveryUtils, {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils"; // For validating phone numbers without country codes const PHONE_NUMBER_REGEX = /^[0-9()\-\s]*$/; @@ -59,19 +59,14 @@ module.exports = React.createClass({ propTypes: { onLoggedIn: PropTypes.func.isRequired, - // The default server name to use when the user hasn't specified - // one. If set, `defaultHsUrl` and `defaultHsUrl` were derived for this - // via `.well-known` discovery. The server name is used instead of the - // HS URL when talking about where to "sign in to". - defaultServerName: PropTypes.string, // An error passed along from higher up explaining that something - // went wrong when finding the defaultHsUrl. - defaultServerDiscoveryError: PropTypes.string, + // went wrong. May be replaced with a different error within the + // Login component. + errorText: PropTypes.string, + + // If true, the component will consider itself busy. + busy: PropTypes.bool, - customHsUrl: PropTypes.string, - customIsUrl: PropTypes.string, - defaultHsUrl: PropTypes.string, - defaultIsUrl: PropTypes.string, // Secondary HS which we try to log into if the user is using // the default HS but login fails. Useful for migrating to a // different homeserver without confusing users. @@ -79,12 +74,13 @@ module.exports = React.createClass({ defaultDeviceDisplayName: PropTypes.string, - // login shouldn't know or care how registration is done. + // login shouldn't know or care how registration, password recovery, + // etc is done. onRegisterClick: PropTypes.func.isRequired, - - // login shouldn't care how password recovery is done. onForgotPasswordClick: PropTypes.func, onServerConfigChange: PropTypes.func.isRequired, + + serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired, }, getInitialState: function() { @@ -93,9 +89,6 @@ module.exports = React.createClass({ errorText: null, loginIncorrect: false, - enteredHsUrl: this.props.customHsUrl || this.props.defaultHsUrl, - enteredIsUrl: this.props.customIsUrl || this.props.defaultIsUrl, - // used for preserving form values when changing homeserver username: "", phoneCountry: null, @@ -105,10 +98,6 @@ module.exports = React.createClass({ phase: PHASE_LOGIN, // The current login flow, such as password, SSO, etc. currentFlow: "m.login.password", - - // .well-known discovery - discoveryError: "", - findingHomeserver: false, }; }, @@ -132,6 +121,14 @@ module.exports = React.createClass({ this._unmounted = true; }, + componentWillReceiveProps(newProps) { + if (newProps.serverConfig.hsUrl === this.props.serverConfig.hsUrl && + newProps.serverConfig.isUrl === this.props.serverConfig.isUrl) return; + + // Ensure that we end up actually logging in to the right place + this._initLoginLogic(newProps.serverConfig.hsUrl, newProps.serverConfig.isUrl); + }, + onPasswordLoginError: function(errorText) { this.setState({ errorText, @@ -139,10 +136,17 @@ module.exports = React.createClass({ }); }, + isBusy: function() { + return this.state.busy || this.props.busy; + }, + + hasError: function() { + return this.state.errorText || this.props.errorText; + }, + onPasswordLogin: function(username, phoneCountry, phoneNumber, password) { - // Prevent people from submitting their password when homeserver - // discovery went wrong - if (this.state.discoveryError || this.props.defaultServerDiscoveryError) return; + // Prevent people from submitting their password when something isn't right. + if (this.isBusy() || this.hasError()) return; this.setState({ busy: true, @@ -164,7 +168,7 @@ module.exports = React.createClass({ const usingEmail = username.indexOf("@") > 0; if (error.httpStatus === 400 && usingEmail) { errorText = _t('This homeserver does not support login using email address.'); - } else if (error.errcode == 'M_RESOURCE_LIMIT_EXCEEDED') { + } else if (error.errcode === 'M_RESOURCE_LIMIT_EXCEEDED') { const errorTop = messageForResourceLimitError( error.data.limit_type, error.data.admin_contact, { @@ -194,11 +198,10 @@ module.exports = React.createClass({
{ _t('Incorrect username and/or password.') }
- { _t('Please note you are logging into the %(hs)s server, not matrix.org.', - { - hs: this.props.defaultHsUrl.replace(/^https?:\/\//, ''), - }) - } + {_t( + 'Please note you are logging into the %(hs)s server, not matrix.org.', + {hs: this.props.serverConfig.hsName}, + )}
); @@ -232,21 +235,26 @@ module.exports = React.createClass({ this.setState({ username: username }); }, - onUsernameBlur: function(username) { + onUsernameBlur: async function(username) { + const doWellknownLookup = username[0] === "@"; this.setState({ username: username, - discoveryError: null, + busy: doWellknownLookup, // unset later by the result of onServerConfigChange + errorText: null, }); - if (username[0] === "@") { + if (doWellknownLookup) { const serverName = username.split(':').slice(1).join(':'); try { - // we have to append 'https://' to make the URL constructor happy - // otherwise we get things like 'protocol: matrix.org, pathname: 8448' - const url = new URL("https://" + serverName); - this._tryWellKnownDiscovery(url.hostname); + const result = await AutoDiscoveryUtils.validateServerName(serverName); + this.props.onServerConfigChange(result); } catch (e) { console.error("Problem parsing URL or unhandled error doing .well-known discovery:", e); - this.setState({discoveryError: _t("Failed to perform homeserver discovery")}); + + let message = _t("Failed to perform homeserver discovery"); + if (e.translatedMessage) { + message = e.translatedMessage; + } + this.setState({errorText: message, busy: false}); } } }, @@ -274,32 +282,13 @@ module.exports = React.createClass({ } }, - onServerConfigChange: function(config) { - const self = this; - const newState = { - errorText: null, // reset err messages - }; - if (config.hsUrl !== undefined) { - newState.enteredHsUrl = config.hsUrl; - } - if (config.isUrl !== undefined) { - newState.enteredIsUrl = config.isUrl; - } - - this.props.onServerConfigChange(config); - this.setState(newState, function() { - self._initLoginLogic(config.hsUrl || null, config.isUrl); - }); - }, - onRegisterClick: function(ev) { ev.preventDefault(); ev.stopPropagation(); this.props.onRegisterClick(); }, - onServerDetailsNextPhaseClick(ev) { - ev.stopPropagation(); + async onServerDetailsNextPhaseClick() { this.setState({ phase: PHASE_LOGIN, }); @@ -313,64 +302,13 @@ module.exports = React.createClass({ }); }, - _tryWellKnownDiscovery: async function(serverName) { - if (!serverName.trim()) { - // Nothing to discover - this.setState({ - discoveryError: "", - findingHomeserver: false, - }); - return; - } - - this.setState({findingHomeserver: true}); - try { - const discovery = await AutoDiscovery.findClientConfig(serverName); - - const state = discovery["m.homeserver"].state; - if (state !== AutoDiscovery.SUCCESS && state !== AutoDiscovery.PROMPT) { - this.setState({ - discoveryError: discovery["m.homeserver"].error, - findingHomeserver: false, - }); - } else if (state === AutoDiscovery.PROMPT) { - this.setState({ - discoveryError: "", - findingHomeserver: false, - }); - } else if (state === AutoDiscovery.SUCCESS) { - this.setState({ - discoveryError: "", - findingHomeserver: false, - }); - this.onServerConfigChange({ - hsUrl: discovery["m.homeserver"].base_url, - isUrl: discovery["m.identity_server"].state === AutoDiscovery.SUCCESS - ? discovery["m.identity_server"].base_url - : "", - }); - } else { - console.warn("Unknown state for m.homeserver in discovery response: ", discovery); - this.setState({ - discoveryError: _t("Unknown failure discovering homeserver"), - findingHomeserver: false, - }); - } - } catch (e) { - console.error(e); - this.setState({ - findingHomeserver: false, - discoveryError: _t("Unknown error discovering homeserver"), - }); - } - }, - _initLoginLogic: function(hsUrl, isUrl) { const self = this; - hsUrl = hsUrl || this.state.enteredHsUrl; - isUrl = isUrl || this.state.enteredIsUrl; + hsUrl = hsUrl || this.props.serverConfig.hsUrl; + isUrl = isUrl || this.props.serverConfig.isUrl; - const fallbackHsUrl = hsUrl === this.props.defaultHsUrl ? this.props.fallbackHsUrl : null; + // TODO: TravisR - Only use this if the homeserver is the default homeserver + const fallbackHsUrl = this.props.fallbackHsUrl; const loginLogic = new Login(hsUrl, isUrl, fallbackHsUrl, { defaultDeviceDisplayName: this.props.defaultDeviceDisplayName, @@ -378,8 +316,6 @@ module.exports = React.createClass({ this._loginLogic = loginLogic; this.setState({ - enteredHsUrl: hsUrl, - enteredIsUrl: isUrl, busy: true, loginIncorrect: false, }); @@ -445,8 +381,8 @@ module.exports = React.createClass({ if (err.cors === 'rejected') { if (window.location.protocol === 'https:' && - (this.state.enteredHsUrl.startsWith("http:") || - !this.state.enteredHsUrl.startsWith("http")) + (this.props.serverConfig.hsUrl.startsWith("http:") || + !this.props.serverConfig.hsUrl.startsWith("http")) ) { errorText = { _t("Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. " + @@ -469,9 +405,9 @@ module.exports = React.createClass({ "is not blocking requests.", {}, { 'a': (sub) => { - return { sub }; + return + { sub } + ; }, }, ) } @@ -484,7 +420,6 @@ module.exports = React.createClass({ renderServerComponent() { const ServerConfig = sdk.getComponent("auth.ServerConfig"); - const AccessibleButton = sdk.getComponent("elements.AccessibleButton"); if (SdkConfig.get()['disable_custom_urls']) { return null; @@ -494,28 +429,19 @@ module.exports = React.createClass({ return null; } - const serverDetails = ; - - let nextButton = null; + const serverDetailsProps = {}; if (PHASES_ENABLED) { - nextButton = - {_t("Next")} - ; + serverDetailsProps.onAfterSubmit = this.onServerDetailsNextPhaseClick; + serverDetailsProps.submitText = _t("Next"); + serverDetailsProps.submitClass = "mx_Login_submit"; } - return
- {serverDetails} - {nextButton} -
; + return ; }, renderLoginComponentForStep() { @@ -547,13 +473,6 @@ module.exports = React.createClass({ onEditServerDetailsClick = this.onEditServerDetailsClick; } - // If the current HS URL is the default HS URL, then we can label it - // with the default HS name (if it exists). - let hsName; - if (this.state.enteredHsUrl === this.props.defaultHsUrl) { - hsName = this.props.defaultServerName; - } - return ( + serverConfig={this.props.serverConfig} + disableSubmit={this.isBusy()} + /> ); }, @@ -595,9 +513,9 @@ module.exports = React.createClass({ const AuthPage = sdk.getComponent("auth.AuthPage"); const AuthHeader = sdk.getComponent("auth.AuthHeader"); const AuthBody = sdk.getComponent("auth.AuthBody"); - const loader = this.state.busy ?
: null; + const loader = this.isBusy() ?
: null; - const errorText = this.props.defaultServerDiscoveryError || this.state.discoveryError || this.state.errorText; + const errorText = this.state.errorText || this.props.errorText; let errorTextSection; if (errorText) { diff --git a/src/components/structures/auth/Registration.js b/src/components/structures/auth/Registration.js index 708118bb22..6e4f076091 100644 --- a/src/components/structures/auth/Registration.js +++ b/src/components/structures/auth/Registration.js @@ -2,6 +2,7 @@ Copyright 2015, 2016 OpenMarket Ltd Copyright 2017 Vector Creations Ltd Copyright 2018, 2019 New Vector Ltd +Copyright 2019 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -17,16 +18,15 @@ limitations under the License. */ import Matrix from 'matrix-js-sdk'; - import Promise from 'bluebird'; import React from 'react'; import PropTypes from 'prop-types'; - import sdk from '../../../index'; import { _t, _td } from '../../../languageHandler'; import SdkConfig from '../../../SdkConfig'; import { messageForResourceLimitError } from '../../../utils/ErrorUtils'; import * as ServerType from '../../views/auth/ServerTypeSelector'; +import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils"; // Phases // Show controls to configure server details @@ -46,18 +46,7 @@ module.exports = React.createClass({ sessionId: PropTypes.string, makeRegistrationUrl: PropTypes.func.isRequired, idSid: PropTypes.string, - // The default server name to use when the user hasn't specified - // one. If set, `defaultHsUrl` and `defaultHsUrl` were derived for this - // via `.well-known` discovery. The server name is used instead of the - // HS URL when talking about "your account". - defaultServerName: PropTypes.string, - // An error passed along from higher up explaining that something - // went wrong when finding the defaultHsUrl. - defaultServerDiscoveryError: PropTypes.string, - customHsUrl: PropTypes.string, - customIsUrl: PropTypes.string, - defaultHsUrl: PropTypes.string, - defaultIsUrl: PropTypes.string, + serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired, brand: PropTypes.string, email: PropTypes.string, // registration shouldn't know or care how login is done. @@ -66,7 +55,7 @@ module.exports = React.createClass({ }, getInitialState: function() { - const serverType = ServerType.getTypeFromHsUrl(this.props.customHsUrl); + const serverType = ServerType.getTypeFromServerConfig(this.props.serverConfig); return { busy: false, @@ -87,8 +76,6 @@ module.exports = React.createClass({ // straight back into UI auth doingUIAuth: Boolean(this.props.sessionId), serverType, - hsUrl: this.props.customHsUrl, - isUrl: this.props.customIsUrl, // Phase of the overall registration dialog. phase: PHASE_REGISTRATION, flows: null, @@ -100,18 +87,22 @@ module.exports = React.createClass({ this._replaceClient(); }, - onServerConfigChange: function(config) { - const newState = {}; - if (config.hsUrl !== undefined) { - newState.hsUrl = config.hsUrl; + componentWillReceiveProps(newProps) { + if (newProps.serverConfig.hsUrl === this.props.serverConfig.hsUrl && + newProps.serverConfig.isUrl === this.props.serverConfig.isUrl) return; + + this._replaceClient(newProps.serverConfig); + + // Handle cases where the user enters "https://matrix.org" for their server + // from the advanced option - we should default to FREE at that point. + const serverType = ServerType.getTypeFromServerConfig(newProps.serverConfig); + if (serverType !== this.state.serverType) { + // Reset the phase to default phase for the server type. + this.setState({ + serverType, + phase: this.getDefaultPhaseForServerType(serverType), + }); } - if (config.isUrl !== undefined) { - newState.isUrl = config.isUrl; - } - this.props.onServerConfigChange(config); - this.setState(newState, () => { - this._replaceClient(); - }); }, getDefaultPhaseForServerType(type) { @@ -136,19 +127,17 @@ module.exports = React.createClass({ // the new type. switch (type) { case ServerType.FREE: { - const { hsUrl, isUrl } = ServerType.TYPES.FREE; - this.onServerConfigChange({ - hsUrl, - isUrl, - }); + const { serverConfig } = ServerType.TYPES.FREE; + this.props.onServerConfigChange(serverConfig); break; } case ServerType.PREMIUM: + // We can accept whatever server config was the default here as this essentially + // acts as a slightly different "custom server"/ADVANCED option. + break; case ServerType.ADVANCED: - this.onServerConfigChange({ - hsUrl: this.props.defaultHsUrl, - isUrl: this.props.defaultIsUrl, - }); + // Use the default config from the config + this.props.onServerConfigChange(SdkConfig.get()["validated_server_config"]); break; } @@ -158,13 +147,15 @@ module.exports = React.createClass({ }); }, - _replaceClient: async function() { + _replaceClient: async function(serverConfig) { this.setState({ errorText: null, }); + if (!serverConfig) serverConfig = this.props.serverConfig; + const {hsUrl, isUrl} = serverConfig; this._matrixClient = Matrix.createClient({ - baseUrl: this.state.hsUrl, - idBaseUrl: this.state.isUrl, + baseUrl: hsUrl, + idBaseUrl: isUrl, }); try { await this._makeRegisterRequest({}); @@ -189,12 +180,6 @@ module.exports = React.createClass({ }, onFormSubmit: function(formVals) { - // Don't allow the user to register if there's a discovery error - // Without this, the user could end up registering on the wrong homeserver. - if (this.props.defaultServerDiscoveryError) { - this.setState({errorText: this.props.defaultServerDiscoveryError}); - return; - } this.setState({ errorText: "", busy: true, @@ -203,11 +188,25 @@ module.exports = React.createClass({ }); }, + _requestEmailToken: function(emailAddress, clientSecret, sendAttempt, sessionId) { + return this._matrixClient.requestRegisterEmailToken( + emailAddress, + clientSecret, + sendAttempt, + this.props.makeRegistrationUrl({ + client_secret: clientSecret, + hs_url: this._matrixClient.getHomeserverUrl(), + is_url: this._matrixClient.getIdentityServerUrl(), + session_id: sessionId, + }), + ); + }, + _onUIAuthFinished: async function(success, response, extra) { if (!success) { let msg = response.message || response.toString(); // can we give a better error message? - if (response.errcode == 'M_RESOURCE_LIMIT_EXCEEDED') { + if (response.errcode === 'M_RESOURCE_LIMIT_EXCEEDED') { const errorTop = messageForResourceLimitError( response.data.limit_type, response.data.admin_contact, { @@ -302,8 +301,7 @@ module.exports = React.createClass({ }); }, - onServerDetailsNextPhaseClick(ev) { - ev.stopPropagation(); + async onServerDetailsNextPhaseClick() { this.setState({ phase: PHASE_REGISTRATION, }); @@ -348,7 +346,6 @@ module.exports = React.createClass({ const ServerTypeSelector = sdk.getComponent("auth.ServerTypeSelector"); const ServerConfig = sdk.getComponent("auth.ServerConfig"); const ModularServerConfig = sdk.getComponent("auth.ModularServerConfig"); - const AccessibleButton = sdk.getComponent("elements.AccessibleButton"); if (SdkConfig.get()['disable_custom_urls']) { return null; @@ -365,47 +362,41 @@ module.exports = React.createClass({
; } + const serverDetailsProps = {}; + if (PHASES_ENABLED) { + serverDetailsProps.onAfterSubmit = this.onServerDetailsNextPhaseClick; + serverDetailsProps.submitText = _t("Next"); + serverDetailsProps.submitClass = "mx_Login_submit"; + } + let serverDetails = null; switch (this.state.serverType) { case ServerType.FREE: break; case ServerType.PREMIUM: serverDetails = ; break; case ServerType.ADVANCED: serverDetails = ; break; } - let nextButton = null; - if (PHASES_ENABLED) { - nextButton = - {_t("Next")} - ; - } - return
{serverDetails} - {nextButton}
; }, @@ -424,7 +415,7 @@ module.exports = React.createClass({ makeRequest={this._makeRegisterRequest} onAuthFinished={this._onUIAuthFinished} inputs={this._getUIAuthInputs()} - makeRegistrationUrl={this.props.makeRegistrationUrl} + requestEmailToken={this._requestEmailToken} sessionId={this.props.sessionId} clientSecret={this.props.clientSecret} emailSid={this.props.idSid} @@ -446,13 +437,6 @@ module.exports = React.createClass({ onEditServerDetailsClick = this.onEditServerDetailsClick; } - // If the current HS URL is the default HS URL, then we can label it - // with the default HS name (if it exists). - let hsName; - if (this.state.hsUrl === this.props.defaultHsUrl) { - hsName = this.props.defaultServerName; - } - return ; } }, @@ -474,7 +457,7 @@ module.exports = React.createClass({ const AuthPage = sdk.getComponent('auth.AuthPage'); let errorText; - const err = this.state.errorText || this.props.defaultServerDiscoveryError; + const err = this.state.errorText; if (err) { errorText =
{ err }
; } diff --git a/src/components/views/auth/InteractiveAuthEntryComponents.js b/src/components/views/auth/InteractiveAuthEntryComponents.js index 4851222e49..b3687db2bd 100644 --- a/src/components/views/auth/InteractiveAuthEntryComponents.js +++ b/src/components/views/auth/InteractiveAuthEntryComponents.js @@ -1,6 +1,7 @@ /* Copyright 2016 OpenMarket Ltd Copyright 2017 Vector Creations 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. @@ -57,7 +58,6 @@ import SettingsStore from "../../../settings/SettingsStore"; * session to be failed and the process to go back to the start. * setEmailSid: m.login.email.identity only: a function to be called with the * email sid after a token is requested. - * makeRegistrationUrl A function that makes a registration URL * * Each component may also provide the following functions (beyond the standard React ones): * focus: set the input focus appropriately in the form. @@ -365,7 +365,6 @@ export const EmailIdentityAuthEntry = React.createClass({ stageState: PropTypes.object.isRequired, fail: PropTypes.func.isRequired, setEmailSid: PropTypes.func.isRequired, - makeRegistrationUrl: PropTypes.func.isRequired, }, getInitialState: function() { @@ -374,38 +373,6 @@ export const EmailIdentityAuthEntry = React.createClass({ }; }, - componentWillMount: function() { - if (this.props.stageState.emailSid === null) { - this.setState({requestingToken: true}); - this._requestEmailToken().catch((e) => { - this.props.fail(e); - }).finally(() => { - this.setState({requestingToken: false}); - }).done(); - } - }, - - /* - * Requests a verification token by email. - */ - _requestEmailToken: function() { - const nextLink = this.props.makeRegistrationUrl({ - client_secret: this.props.clientSecret, - hs_url: this.props.matrixClient.getHomeserverUrl(), - is_url: this.props.matrixClient.getIdentityServerUrl(), - session_id: this.props.authSessionId, - }); - - return this.props.matrixClient.requestRegisterEmailToken( - this.props.inputs.emailAddress, - this.props.clientSecret, - 1, // TODO: Multiple send attempts? - nextLink, - ).then((result) => { - this.props.setEmailSid(result.sid); - }); - }, - render: function() { if (this.state.requestingToken) { const Loader = sdk.getComponent("elements.Spinner"); diff --git a/src/components/views/auth/ModularServerConfig.js b/src/components/views/auth/ModularServerConfig.js index 9c6c4b01bf..5a3bc23596 100644 --- a/src/components/views/auth/ModularServerConfig.js +++ b/src/components/views/auth/ModularServerConfig.js @@ -18,9 +18,15 @@ import React from 'react'; import PropTypes from 'prop-types'; import sdk from '../../../index'; import { _t } from '../../../languageHandler'; +import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils"; +import SdkConfig from "../../../SdkConfig"; +import AutoDiscoveryUtils from "../../../utils/AutoDiscoveryUtils"; +import * as ServerType from '../../views/auth/ServerTypeSelector'; const MODULAR_URL = 'https://modular.im/?utm_source=riot-web&utm_medium=web&utm_campaign=riot-web-authentication'; +// TODO: TravisR - Can this extend ServerConfig for most things? + /* * Configure the Modular server name. * @@ -31,65 +37,107 @@ export default class ModularServerConfig extends React.PureComponent { static propTypes = { onServerConfigChange: PropTypes.func, - // default URLs are defined in config.json (or the hardcoded defaults) - // they are used if the user has not overridden them with a custom URL. - // In other words, if the custom URL is blank, the default is used. - defaultHsUrl: PropTypes.string, // e.g. https://matrix.org - - // This component always uses the default IS URL and doesn't allow it - // to be changed. We still receive it as a prop here to simplify - // consumers by still passing the IS URL via onServerConfigChange. - defaultIsUrl: PropTypes.string, // e.g. https://vector.im - - // custom URLs are explicitly provided by the user and override the - // default URLs. The user enters them via the component's input fields, - // which is reflected on these properties whenever on..UrlChanged fires. - // They are persisted in localStorage by MatrixClientPeg, and so can - // override the default URLs when the component initially loads. - customHsUrl: PropTypes.string, + // The current configuration that the user is expecting to change. + serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired, delayTimeMs: PropTypes.number, // time to wait before invoking onChanged - } + + // Called after the component calls onServerConfigChange + onAfterSubmit: PropTypes.func, + + // Optional text for the submit button. If falsey, no button will be shown. + submitText: PropTypes.string, + + // Optional class for the submit button. Only applies if the submit button + // is to be rendered. + submitClass: PropTypes.string, + }; static defaultProps = { onServerConfigChange: function() {}, customHsUrl: "", delayTimeMs: 0, - } + }; constructor(props) { super(props); this.state = { - hsUrl: props.customHsUrl, + busy: false, + errorText: "", + hsUrl: props.serverConfig.hsUrl, + isUrl: props.serverConfig.isUrl, }; } componentWillReceiveProps(newProps) { - if (newProps.customHsUrl === this.state.hsUrl) return; + if (newProps.serverConfig.hsUrl === this.state.hsUrl && + newProps.serverConfig.isUrl === this.state.isUrl) return; + + this.validateAndApplyServer(newProps.serverConfig.hsUrl, newProps.serverConfig.isUrl); + } + + async validateAndApplyServer(hsUrl, isUrl) { + // Always try and use the defaults first + const defaultConfig: ValidatedServerConfig = SdkConfig.get()["validated_server_config"]; + if (defaultConfig.hsUrl === hsUrl && defaultConfig.isUrl === isUrl) { + this.setState({busy: false, errorText: ""}); + this.props.onServerConfigChange(defaultConfig); + return defaultConfig; + } this.setState({ - hsUrl: newProps.customHsUrl, - }); - this.props.onServerConfigChange({ - hsUrl: newProps.customHsUrl, - isUrl: this.props.defaultIsUrl, + hsUrl, + isUrl, + busy: true, + errorText: "", }); + + try { + const result = await AutoDiscoveryUtils.validateServerConfigWithStaticUrls(hsUrl, isUrl); + this.setState({busy: false, errorText: ""}); + this.props.onServerConfigChange(result); + return result; + } catch (e) { + console.error(e); + let message = _t("Unable to validate homeserver/identity server"); + if (e.translatedMessage) { + message = e.translatedMessage; + } + this.setState({ + busy: false, + errorText: message, + }); + } + } + + async validateServer() { + // TODO: Do we want to support .well-known lookups here? + // If for some reason someone enters "matrix.org" for a URL, we could do a lookup to + // find their homeserver without demanding they use "https://matrix.org" + return this.validateAndApplyServer(this.state.hsUrl, ServerType.TYPES.PREMIUM.identityServerUrl); } onHomeserverBlur = (ev) => { this._hsTimeoutId = this._waitThenInvoke(this._hsTimeoutId, () => { - this.props.onServerConfigChange({ - hsUrl: this.state.hsUrl, - isUrl: this.props.defaultIsUrl, - }); + this.validateServer(); }); - } + }; onHomeserverChange = (ev) => { const hsUrl = ev.target.value; this.setState({ hsUrl }); - } + }; + + onSubmit = async (ev) => { + ev.preventDefault(); + ev.stopPropagation(); + await this.validateServer(); + + if (this.props.onAfterSubmit) { + this.props.onAfterSubmit(); + } + }; _waitThenInvoke(existingTimeoutId, fn) { if (existingTimeoutId) { @@ -100,6 +148,16 @@ export default class ModularServerConfig extends React.PureComponent { render() { const Field = sdk.getComponent('elements.Field'); + const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); + + const submitButton = this.props.submitText + ? {this.props.submitText} + : null; return (
@@ -113,15 +171,18 @@ export default class ModularServerConfig extends React.PureComponent { , }, )} -
- -
+
+
+ +
+ {submitButton} +
); } diff --git a/src/components/views/auth/PasswordLogin.js b/src/components/views/auth/PasswordLogin.js index ed3afede2f..b7dd4a4e83 100644 --- a/src/components/views/auth/PasswordLogin.js +++ b/src/components/views/auth/PasswordLogin.js @@ -1,6 +1,7 @@ /* Copyright 2015, 2016 OpenMarket Ltd Copyright 2017 Vector Creations Ltd +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. @@ -21,11 +22,29 @@ import classNames from 'classnames'; import sdk from '../../../index'; import { _t } from '../../../languageHandler'; import SdkConfig from '../../../SdkConfig'; +import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils"; /** * A pure UI component which displays a username/password form. */ -class PasswordLogin extends React.Component { +export default class PasswordLogin extends React.Component { + static propTypes = { + onSubmit: PropTypes.func.isRequired, // fn(username, password) + onError: PropTypes.func, + onForgotPasswordClick: PropTypes.func, // fn() + initialUsername: PropTypes.string, + initialPhoneCountry: PropTypes.string, + initialPhoneNumber: PropTypes.string, + initialPassword: PropTypes.string, + onUsernameChanged: PropTypes.func, + onPhoneCountryChanged: PropTypes.func, + onPhoneNumberChanged: PropTypes.func, + onPasswordChanged: PropTypes.func, + loginIncorrect: PropTypes.bool, + disableSubmit: PropTypes.bool, + serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired, + }; + static defaultProps = { onError: function() {}, onEditServerDetailsClick: null, @@ -40,13 +59,12 @@ class PasswordLogin extends React.Component { initialPhoneNumber: "", initialPassword: "", loginIncorrect: false, - // This is optional and only set if we used a server name to determine - // the HS URL via `.well-known` discovery. The server name is used - // instead of the HS URL when talking about where to "sign in to". - hsName: null, - hsUrl: "", disableSubmit: false, - } + }; + + static LOGIN_FIELD_EMAIL = "login_field_email"; + static LOGIN_FIELD_MXID = "login_field_mxid"; + static LOGIN_FIELD_PHONE = "login_field_phone"; constructor(props) { super(props); @@ -193,10 +211,7 @@ class PasswordLogin extends React.Component { name="username" // make it a little easier for browser's remember-password key="username_input" type="text" - label={SdkConfig.get().disable_custom_urls ? - _t("Username on %(hs)s", { - hs: this.props.hsUrl.replace(/^https?:\/\//, ''), - }) : _t("Username")} + label={_t("Username")} value={this.state.username} onChange={this.onUsernameChanged} onBlur={this.onUsernameBlur} @@ -258,20 +273,22 @@ class PasswordLogin extends React.Component {
; } - let signInToText = _t('Sign in to your Matrix account'); - if (this.props.hsName) { - signInToText = _t('Sign in to your Matrix account on %(serverName)s', { - serverName: this.props.hsName, + let signInToText = _t('Sign in to your Matrix account on %(serverName)s', { + serverName: this.props.serverConfig.hsName, + }); + if (this.props.serverConfig.hsNameIsDifferent) { + const TextWithTooltip = sdk.getComponent("elements.TextWithTooltip"); + + signInToText = _t('Sign in to your Matrix account on ', {}, { + 'underlinedServerName': () => { + return ; + }, }); - } else { - try { - const parsedHsUrl = new URL(this.props.hsUrl); - signInToText = _t('Sign in to your Matrix account on %(serverName)s', { - serverName: parsedHsUrl.hostname, - }); - } catch (e) { - // ignore - } } let editLink = null; @@ -353,27 +370,3 @@ class PasswordLogin extends React.Component { ); } } - -PasswordLogin.LOGIN_FIELD_EMAIL = "login_field_email"; -PasswordLogin.LOGIN_FIELD_MXID = "login_field_mxid"; -PasswordLogin.LOGIN_FIELD_PHONE = "login_field_phone"; - -PasswordLogin.propTypes = { - onSubmit: PropTypes.func.isRequired, // fn(username, password) - onError: PropTypes.func, - onForgotPasswordClick: PropTypes.func, // fn() - initialUsername: PropTypes.string, - initialPhoneCountry: PropTypes.string, - initialPhoneNumber: PropTypes.string, - initialPassword: PropTypes.string, - onUsernameChanged: PropTypes.func, - onPhoneCountryChanged: PropTypes.func, - onPhoneNumberChanged: PropTypes.func, - onPasswordChanged: PropTypes.func, - loginIncorrect: PropTypes.bool, - hsName: PropTypes.string, - hsUrl: PropTypes.string, - disableSubmit: PropTypes.bool, -}; - -module.exports = PasswordLogin; diff --git a/src/components/views/auth/RegistrationForm.js b/src/components/views/auth/RegistrationForm.js index 33df42be15..b1af6ea42c 100644 --- a/src/components/views/auth/RegistrationForm.js +++ b/src/components/views/auth/RegistrationForm.js @@ -26,6 +26,7 @@ import { _t } from '../../../languageHandler'; import SdkConfig from '../../../SdkConfig'; import { SAFE_LOCALPART_REGEX } from '../../../Registration'; import withValidation from '../elements/Validation'; +import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils"; const FIELD_EMAIL = 'field_email'; const FIELD_PHONE_NUMBER = 'field_phone_number'; @@ -51,11 +52,7 @@ module.exports = React.createClass({ onRegisterClick: PropTypes.func.isRequired, // onRegisterClick(Object) => ?Promise onEditServerDetailsClick: PropTypes.func, flows: PropTypes.arrayOf(PropTypes.object).isRequired, - // This is optional and only set if we used a server name to determine - // the HS URL via `.well-known` discovery. The server name is used - // instead of the HS URL when talking about "your account". - hsName: PropTypes.string, - hsUrl: PropTypes.string, + serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired, }, getDefaultProps: function() { @@ -515,20 +512,22 @@ module.exports = React.createClass({ }, render: function() { - let yourMatrixAccountText = _t('Create your Matrix account'); - if (this.props.hsName) { - yourMatrixAccountText = _t('Create your Matrix account on %(serverName)s', { - serverName: this.props.hsName, + let yourMatrixAccountText = _t('Create your Matrix account on %(serverName)s', { + serverName: this.props.serverConfig.hsName, + }); + if (this.props.serverConfig.hsNameIsDifferent) { + const TextWithTooltip = sdk.getComponent("elements.TextWithTooltip"); + + yourMatrixAccountText = _t('Create your Matrix account on ', {}, { + 'underlinedServerName': () => { + return ; + }, }); - } else { - try { - const parsedHsUrl = new URL(this.props.hsUrl); - yourMatrixAccountText = _t('Create your Matrix account on %(serverName)s', { - serverName: parsedHsUrl.hostname, - }); - } catch (e) { - // ignore - } } let editLink = null; diff --git a/src/components/views/auth/ServerConfig.js b/src/components/views/auth/ServerConfig.js index cb0e0dc38e..3967f49f18 100644 --- a/src/components/views/auth/ServerConfig.js +++ b/src/components/views/auth/ServerConfig.js @@ -20,6 +20,9 @@ import PropTypes from 'prop-types'; import Modal from '../../../Modal'; import sdk from '../../../index'; import { _t } from '../../../languageHandler'; +import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils"; +import AutoDiscoveryUtils from "../../../utils/AutoDiscoveryUtils"; +import SdkConfig from "../../../SdkConfig"; /* * A pure UI component which displays the HS and IS to use. @@ -27,82 +30,119 @@ import { _t } from '../../../languageHandler'; export default class ServerConfig extends React.PureComponent { static propTypes = { - onServerConfigChange: PropTypes.func, + onServerConfigChange: PropTypes.func.isRequired, - // default URLs are defined in config.json (or the hardcoded defaults) - // they are used if the user has not overridden them with a custom URL. - // In other words, if the custom URL is blank, the default is used. - defaultHsUrl: PropTypes.string, // e.g. https://matrix.org - defaultIsUrl: PropTypes.string, // e.g. https://vector.im - - // custom URLs are explicitly provided by the user and override the - // default URLs. The user enters them via the component's input fields, - // which is reflected on these properties whenever on..UrlChanged fires. - // They are persisted in localStorage by MatrixClientPeg, and so can - // override the default URLs when the component initially loads. - customHsUrl: PropTypes.string, - customIsUrl: PropTypes.string, + // The current configuration that the user is expecting to change. + serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired, delayTimeMs: PropTypes.number, // time to wait before invoking onChanged - } + + // Called after the component calls onServerConfigChange + onAfterSubmit: PropTypes.func, + + // Optional text for the submit button. If falsey, no button will be shown. + submitText: PropTypes.string, + + // Optional class for the submit button. Only applies if the submit button + // is to be rendered. + submitClass: PropTypes.string, + }; static defaultProps = { onServerConfigChange: function() {}, - customHsUrl: "", - customIsUrl: "", delayTimeMs: 0, - } + }; constructor(props) { super(props); this.state = { - hsUrl: props.customHsUrl, - isUrl: props.customIsUrl, + busy: false, + errorText: "", + hsUrl: props.serverConfig.hsUrl, + isUrl: props.serverConfig.isUrl, }; } componentWillReceiveProps(newProps) { - if (newProps.customHsUrl === this.state.hsUrl && - newProps.customIsUrl === this.state.isUrl) return; + if (newProps.serverConfig.hsUrl === this.state.hsUrl && + newProps.serverConfig.isUrl === this.state.isUrl) return; + + this.validateAndApplyServer(newProps.serverConfig.hsUrl, newProps.serverConfig.isUrl); + } + + async validateServer() { + // TODO: Do we want to support .well-known lookups here? + // If for some reason someone enters "matrix.org" for a URL, we could do a lookup to + // find their homeserver without demanding they use "https://matrix.org" + return this.validateAndApplyServer(this.state.hsUrl, this.state.isUrl); + } + + async validateAndApplyServer(hsUrl, isUrl) { + // Always try and use the defaults first + const defaultConfig: ValidatedServerConfig = SdkConfig.get()["validated_server_config"]; + if (defaultConfig.hsUrl === hsUrl && defaultConfig.isUrl === isUrl) { + this.setState({busy: false, errorText: ""}); + this.props.onServerConfigChange(defaultConfig); + return defaultConfig; + } this.setState({ - hsUrl: newProps.customHsUrl, - isUrl: newProps.customIsUrl, - }); - this.props.onServerConfigChange({ - hsUrl: newProps.customHsUrl, - isUrl: newProps.customIsUrl, + hsUrl, + isUrl, + busy: true, + errorText: "", }); + + try { + const result = await AutoDiscoveryUtils.validateServerConfigWithStaticUrls(hsUrl, isUrl); + this.setState({busy: false, errorText: ""}); + this.props.onServerConfigChange(result); + return result; + } catch (e) { + console.error(e); + let message = _t("Unable to validate homeserver/identity server"); + if (e.translatedMessage) { + message = e.translatedMessage; + } + this.setState({ + busy: false, + errorText: message, + }); + } } onHomeserverBlur = (ev) => { this._hsTimeoutId = this._waitThenInvoke(this._hsTimeoutId, () => { - this.props.onServerConfigChange({ - hsUrl: this.state.hsUrl, - isUrl: this.state.isUrl, - }); + this.validateServer(); }); - } + }; onHomeserverChange = (ev) => { const hsUrl = ev.target.value; this.setState({ hsUrl }); - } + }; onIdentityServerBlur = (ev) => { this._isTimeoutId = this._waitThenInvoke(this._isTimeoutId, () => { - this.props.onServerConfigChange({ - hsUrl: this.state.hsUrl, - isUrl: this.state.isUrl, - }); + this.validateServer(); }); - } + }; onIdentityServerChange = (ev) => { const isUrl = ev.target.value; this.setState({ isUrl }); - } + }; + + onSubmit = async (ev) => { + ev.preventDefault(); + ev.stopPropagation(); + await this.validateServer(); + + if (this.props.onAfterSubmit) { + this.props.onAfterSubmit(); + } + }; _waitThenInvoke(existingTimeoutId, fn) { if (existingTimeoutId) { @@ -114,10 +154,24 @@ export default class ServerConfig extends React.PureComponent { showHelpPopup = () => { const CustomServerDialog = sdk.getComponent('auth.CustomServerDialog'); Modal.createTrackedDialog('Custom Server Dialog', '', CustomServerDialog); - } + }; render() { const Field = sdk.getComponent('elements.Field'); + const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); + + const errorText = this.state.errorText + ? {this.state.errorText} + : null; + + const submitButton = this.props.submitText + ? {this.props.submitText} + : null; return (
@@ -127,22 +181,28 @@ export default class ServerConfig extends React.PureComponent { { sub } , })} -
- - -
+ {errorText} +
+
+ + +
+ {submitButton} +
); } diff --git a/src/components/views/auth/ServerTypeSelector.js b/src/components/views/auth/ServerTypeSelector.js index 71d13da421..602de72f3f 100644 --- a/src/components/views/auth/ServerTypeSelector.js +++ b/src/components/views/auth/ServerTypeSelector.js @@ -19,6 +19,8 @@ import PropTypes from 'prop-types'; import { _t } from '../../../languageHandler'; import sdk from '../../../index'; import classnames from 'classnames'; +import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils"; +import {makeType} from "../../../utils/TypeUtils"; const MODULAR_URL = 'https://modular.im/?utm_source=riot-web&utm_medium=web&utm_campaign=riot-web-authentication'; @@ -32,8 +34,13 @@ export const TYPES = { label: () => _t('Free'), logo: () => , description: () => _t('Join millions for free on the largest public server'), - hsUrl: 'https://matrix.org', - isUrl: 'https://vector.im', + serverConfig: makeType(ValidatedServerConfig, { + hsUrl: "https://matrix.org", + hsName: "matrix.org", + hsNameIsDifferent: false, + isUrl: "https://vector.im", + identityEnabled: true, + }), }, PREMIUM: { id: PREMIUM, @@ -44,6 +51,7 @@ export const TYPES = { {sub} , }), + identityServerUrl: "https://vector.im", }, ADVANCED: { id: ADVANCED, @@ -56,10 +64,11 @@ export const TYPES = { }, }; -export function getTypeFromHsUrl(hsUrl) { +export function getTypeFromServerConfig(config) { + const {hsUrl} = config; if (!hsUrl) { return null; - } else if (hsUrl === TYPES.FREE.hsUrl) { + } else if (hsUrl === TYPES.FREE.serverConfig.hsUrl) { return FREE; } else if (new URL(hsUrl).hostname.endsWith('.modular.im')) { // This is an unlikely case to reach, as Modular defaults to hiding the @@ -76,7 +85,7 @@ export default class ServerTypeSelector extends React.PureComponent { selected: PropTypes.string, // Handler called when the selected type changes. onChange: PropTypes.func.isRequired, - } + }; constructor(props) { super(props); @@ -106,7 +115,7 @@ export default class ServerTypeSelector extends React.PureComponent { e.stopPropagation(); const type = e.currentTarget.dataset.id; this.updateSelectedType(type); - } + }; render() { const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); diff --git a/src/components/views/avatars/BaseAvatar.js b/src/components/views/avatars/BaseAvatar.js index 47de7c9dc4..80f5c43d0c 100644 --- a/src/components/views/avatars/BaseAvatar.js +++ b/src/components/views/avatars/BaseAvatar.js @@ -20,6 +20,7 @@ import PropTypes from 'prop-types'; import { MatrixClient } from 'matrix-js-sdk'; import AvatarLogic from '../../../Avatar'; import sdk from '../../../index'; +import SettingsStore from "../../../settings/SettingsStore"; import AccessibleButton from '../elements/AccessibleButton'; module.exports = React.createClass({ @@ -104,9 +105,13 @@ module.exports = React.createClass({ // work out the full set of urls to try to load. This is formed like so: // imageUrls: [ props.url, props.urls, default image ] - const urls = props.urls || []; - if (props.url) { - urls.unshift(props.url); // put in urls[0] + let urls = []; + if (!SettingsStore.getValue("lowBandwidth")) { + urls = props.urls || []; + + if (props.url) { + urls.unshift(props.url); // put in urls[0] + } } let defaultImageUrl = null; @@ -133,40 +138,7 @@ module.exports = React.createClass({ } }, - /** - * returns the first (non-sigil) character of 'name', - * converted to uppercase - */ - _getInitialLetter: function(name) { - if (name.length < 1) { - return undefined; - } - - let idx = 0; - const initial = name[0]; - if ((initial === '@' || initial === '#' || initial === '+') && name[1]) { - idx++; - } - - // string.codePointAt(0) would do this, but that isn't supported by - // some browsers (notably PhantomJS). - let chars = 1; - const first = name.charCodeAt(idx); - - // check if it’s the start of a surrogate pair - if (first >= 0xD800 && first <= 0xDBFF && name[idx+1]) { - const second = name.charCodeAt(idx+1); - if (second >= 0xDC00 && second <= 0xDFFF) { - chars++; - } - } - - const firstChar = name.substring(idx, idx+chars); - return firstChar.toUpperCase(); - }, - render: function() { - const EmojiText = sdk.getComponent('elements.EmojiText'); const imageUrl = this.state.imageUrls[this.state.urlsIndex]; const { @@ -176,20 +148,20 @@ module.exports = React.createClass({ } = this.props; if (imageUrl === this.state.defaultImageUrl) { - const initialLetter = this._getInitialLetter(name); + const initialLetter = AvatarLogic.getInitialLetter(name); const textNode = ( - + ); const imgNode = ( + width={width} height={height} aria-hidden="true" /> ); if (onClick != null) { return ( diff --git a/src/components/views/avatars/RoomAvatar.js b/src/components/views/avatars/RoomAvatar.js index 38f088238f..557a4d8dbf 100644 --- a/src/components/views/avatars/RoomAvatar.js +++ b/src/components/views/avatars/RoomAvatar.js @@ -19,7 +19,7 @@ import {ContentRepo} from "matrix-js-sdk"; import MatrixClientPeg from "../../../MatrixClientPeg"; import Modal from '../../../Modal'; import sdk from "../../../index"; -import DMRoomMap from '../../../utils/DMRoomMap'; +import Avatar from '../../../Avatar'; module.exports = React.createClass({ displayName: 'RoomAvatar', @@ -89,7 +89,6 @@ module.exports = React.createClass({ props.resizeMethod, ), // highest priority this.getRoomAvatarUrl(props), - this.getOneToOneAvatar(props), // lowest priority ].filter(function(url) { return (url != null && url != ""); }); @@ -98,41 +97,14 @@ module.exports = React.createClass({ getRoomAvatarUrl: function(props) { if (!props.room) return null; - return props.room.getAvatarUrl( - MatrixClientPeg.get().getHomeserverUrl(), + return Avatar.avatarUrlForRoom( + props.room, Math.floor(props.width * window.devicePixelRatio), Math.floor(props.height * window.devicePixelRatio), props.resizeMethod, - false, ); }, - getOneToOneAvatar: function(props) { - const room = props.room; - if (!room) { - return null; - } - let otherMember = null; - const otherUserId = DMRoomMap.shared().getUserIdForRoomId(room.roomId); - if (otherUserId) { - otherMember = room.getMember(otherUserId); - } else { - // if the room is not marked as a 1:1, but only has max 2 members - // then still try to show any avatar (pref. other member) - otherMember = room.getAvatarFallbackMember(); - } - if (otherMember) { - return otherMember.getAvatarUrl( - MatrixClientPeg.get().getHomeserverUrl(), - Math.floor(props.width * window.devicePixelRatio), - Math.floor(props.height * window.devicePixelRatio), - props.resizeMethod, - false, - ); - } - return null; - }, - onRoomAvatarClick: function() { const avatarUrl = this.props.room.getAvatarUrl( MatrixClientPeg.get().getHomeserverUrl(), diff --git a/src/components/views/context_menus/TopLeftMenu.js b/src/components/views/context_menus/TopLeftMenu.js index 278c879404..09e9142201 100644 --- a/src/components/views/context_menus/TopLeftMenu.js +++ b/src/components/views/context_menus/TopLeftMenu.js @@ -1,5 +1,6 @@ /* Copyright 2018, 2019 New Vector Ltd +Copyright 2019 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -29,6 +30,10 @@ export class TopLeftMenu extends React.Component { displayName: PropTypes.string.isRequired, userId: PropTypes.string.isRequired, onFinished: PropTypes.func, + + // Optional function to collect a reference to the container + // of this component directly. + containerRef: PropTypes.func, }; constructor() { @@ -61,44 +66,48 @@ export class TopLeftMenu extends React.Component { {_t( "Upgrade to your own domain", {}, { - a: sub => {sub}, + a: sub => {sub}, }, )} - + ; } - let homePageSection = null; + let homePageItem = null; if (this.hasHomePage()) { - homePageSection =
    -
  • {_t("Home")}
  • -
; + homePageItem =
  • + {_t("Home")} +
  • ; } - let signInOutSection; + let signInOutItem; if (isGuest) { - signInOutSection =
      -
    • {_t("Sign in")}
    • -
    ; + signInOutItem =
  • + {_t("Sign in")} +
  • ; } else { - signInOutSection =
      -
    • {_t("Sign out")}
    • -
    ; + signInOutItem =
  • + {_t("Sign out")} +
  • ; } - return
    -
    + const settingsItem =
  • + {_t("Settings")} +
  • ; + + return
    +
    {this.props.displayName}
    -
    {this.props.userId}
    +
    {this.props.userId}
    {hostingSignup}
    - {homePageSection}
      -
    • {_t("Settings")}
    • + {homePageItem} + {settingsItem} + {signInOutItem}
    - {signInOutSection}
    ; } diff --git a/src/components/views/dialogs/DevtoolsDialog.js b/src/components/views/dialogs/DevtoolsDialog.js index 0835c41bb9..9327e1e54e 100644 --- a/src/components/views/dialogs/DevtoolsDialog.js +++ b/src/components/views/dialogs/DevtoolsDialog.js @@ -128,8 +128,10 @@ class SendCustomEvent extends GenericEditor { return
    - { this.textInput('eventType', _t('Event Type')) } - { this.state.isStateEvent && this.textInput('stateKey', _t('State Key')) } +
    + { this.textInput('eventType', _t('Event Type')) } + { this.state.isStateEvent && this.textInput('stateKey', _t('State Key')) } +

    diff --git a/src/components/views/dialogs/ShareDialog.js b/src/components/views/dialogs/ShareDialog.js index 13d8e99258..bd6746a1e5 100644 --- a/src/components/views/dialogs/ShareDialog.js +++ b/src/components/views/dialogs/ShareDialog.js @@ -114,7 +114,8 @@ export default class ShareDialog extends React.Component { top: y, message: successful ? _t('Copied!') : _t('Failed to copy'), }, false); - e.target.onmouseleave = close; + // Drop a reference to this close handler for componentWillUnmount + this.closeCopiedTooltip = e.target.onmouseleave = close; } onLinkSpecificEventCheckboxClick() { @@ -131,6 +132,12 @@ export default class ShareDialog extends React.Component { } } + componentWillUnmount() { + // if the Copied tooltip is open then get rid of it, there are ways to close the modal which wouldn't close + // the tooltip otherwise, such as pressing Escape or clicking X really quickly + if (this.closeCopiedTooltip) this.closeCopiedTooltip(); + } + render() { let title; let matrixToUrl; diff --git a/src/components/views/dialogs/UserSettingsDialog.js b/src/components/views/dialogs/UserSettingsDialog.js index 1c4509eee5..a24c18c8b2 100644 --- a/src/components/views/dialogs/UserSettingsDialog.js +++ b/src/components/views/dialogs/UserSettingsDialog.js @@ -28,6 +28,7 @@ import VoiceUserSettingsTab from "../settings/tabs/user/VoiceUserSettingsTab"; import HelpUserSettingsTab from "../settings/tabs/user/HelpUserSettingsTab"; import FlairUserSettingsTab from "../settings/tabs/user/FlairUserSettingsTab"; import sdk from "../../../index"; +import SdkConfig from "../../../SdkConfig"; export default class UserSettingsDialog extends React.Component { static propTypes = { @@ -67,7 +68,7 @@ export default class UserSettingsDialog extends React.Component { "mx_UserSettingsDialog_securityIcon", , )); - if (SettingsStore.getLabsFeatures().length > 0) { + if (SdkConfig.get()['showLabsSettings'] || SettingsStore.getLabsFeatures().length > 0) { tabs.push(new Tab( _td("Labs"), "mx_UserSettingsDialog_labsIcon", diff --git a/src/components/views/elements/AccessibleButton.js b/src/components/views/elements/AccessibleButton.js index 1c39ba4f49..06c440c54e 100644 --- a/src/components/views/elements/AccessibleButton.js +++ b/src/components/views/elements/AccessibleButton.js @@ -63,6 +63,10 @@ export default function AccessibleButton(props) { }; } + // Pass through the ref - used for keyboard shortcut access to some buttons + restProps.ref = restProps.inputRef; + delete restProps.inputRef; + restProps.tabIndex = restProps.tabIndex || "0"; restProps.role = "button"; restProps.className = (restProps.className ? restProps.className + " " : "") + @@ -89,6 +93,7 @@ export default function AccessibleButton(props) { */ AccessibleButton.propTypes = { children: PropTypes.node, + inputRef: PropTypes.func, element: PropTypes.string, onClick: PropTypes.func.isRequired, diff --git a/src/components/views/elements/EmojiText.js b/src/components/views/elements/EmojiText.js deleted file mode 100644 index b7f3e45321..0000000000 --- a/src/components/views/elements/EmojiText.js +++ /dev/null @@ -1,43 +0,0 @@ -/* - Copyright 2016 Aviral Dasgupta - 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 PropTypes from 'prop-types'; -import {emojifyText, containsEmoji} from '../../../HtmlUtils'; - -export default function EmojiText(props) { - const {element, children, addAlt, ...restProps} = props; - - // fast path: simple regex to detect strings that don't contain - // emoji and just return them - if (containsEmoji(children)) { - restProps.dangerouslySetInnerHTML = emojifyText(children, addAlt); - return React.createElement(element, restProps); - } else { - return React.createElement(element, restProps, children); - } -} - -EmojiText.propTypes = { - element: PropTypes.string, - children: PropTypes.string.isRequired, -}; - -EmojiText.defaultProps = { - element: 'span', - addAlt: true, -}; diff --git a/src/components/views/elements/ImageView.js b/src/components/views/elements/ImageView.js index 2adc0df57f..9a47bce553 100644 --- a/src/components/views/elements/ImageView.js +++ b/src/components/views/elements/ImageView.js @@ -195,13 +195,13 @@ export default class ImageView extends React.Component {
    - + { - + { - + {
    diff --git a/src/components/views/elements/MemberEventListSummary.js b/src/components/views/elements/MemberEventListSummary.js index 3c58f90a2b..0065fb208f 100644 --- a/src/components/views/elements/MemberEventListSummary.js +++ b/src/components/views/elements/MemberEventListSummary.js @@ -1,5 +1,6 @@ /* Copyright 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. @@ -13,11 +14,13 @@ 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 sdk from '../../../index'; -const MemberAvatar = require('../avatars/MemberAvatar.js'); +import MemberAvatar from '../avatars/MemberAvatar'; import { _t } from '../../../languageHandler'; +import { formatCommaSeparatedList } from '../../../utils/FormattingUtils'; module.exports = React.createClass({ displayName: 'MemberEventListSummary', @@ -105,7 +108,7 @@ module.exports = React.createClass({ ); }); - const desc = this._renderCommaSeparatedList(descs); + const desc = formatCommaSeparatedList(descs); return _t('%(nameList)s %(transitionList)s', { nameList: nameList, transitionList: desc }); }); @@ -114,13 +117,9 @@ module.exports = React.createClass({ return null; } - const EmojiText = sdk.getComponent('elements.EmojiText'); - return ( - - { summaries.join(", ") } - + { summaries.join(", ") } ); }, @@ -132,7 +131,7 @@ module.exports = React.createClass({ * included before "and [n] others". */ _renderNameList: function(users) { - return this._renderCommaSeparatedList(users, this.props.summaryLength); + return formatCommaSeparatedList(users, this.props.summaryLength); }, /** @@ -283,35 +282,6 @@ module.exports = React.createClass({ return res; }, - /** - * Constructs a written English string representing `items`, with an optional limit on - * the number of items included in the result. If specified and if the length of - *`items` is greater than the limit, the string "and n others" will be appended onto - * the result. - * If `items` is empty, returns the empty string. If there is only one item, return - * it. - * @param {string[]} items the items to construct a string from. - * @param {number?} itemLimit the number by which to limit the list. - * @returns {string} a string constructed by joining `items` with a comma between each - * item, but with the last item appended as " and [lastItem]". - */ - _renderCommaSeparatedList(items, itemLimit) { - const remaining = itemLimit === undefined ? 0 : Math.max( - items.length - itemLimit, 0, - ); - if (items.length === 0) { - return ""; - } else if (items.length === 1) { - return items[0]; - } else if (remaining > 0) { - items = items.slice(0, itemLimit); - return _t("%(items)s and %(count)s others", { items: items.join(', '), count: remaining } ); - } else { - const lastItem = items.pop(); - return _t("%(items)s and %(lastItem)s", { items: items.join(', '), lastItem: lastItem }); - } - }, - _renderAvatars: function(roomMembers) { const avatars = roomMembers.slice(0, this.props.avatarsMaxLength).map((m) => { return ( diff --git a/src/components/views/elements/MessageEditor.js b/src/components/views/elements/MessageEditor.js index a2005feace..ed86bcb0a3 100644 --- a/src/components/views/elements/MessageEditor.js +++ b/src/components/views/elements/MessageEditor.js @@ -1,5 +1,6 @@ /* Copyright 2019 New Vector Ltd +Copyright 2019 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -21,12 +22,14 @@ import dis from '../../../dispatcher'; import EditorModel from '../../../editor/model'; import {setCaretPosition} from '../../../editor/caret'; import {getCaretOffsetAndText} from '../../../editor/dom'; -import {htmlSerialize, textSerialize, requiresHtml} from '../../../editor/serialize'; +import {htmlSerializeIfNeeded, textSerialize} from '../../../editor/serialize'; +import {findEditableEvent} from '../../../utils/EventUtils'; import {parseEvent} from '../../../editor/deserialize'; import Autocomplete from '../rooms/Autocomplete'; import {PartCreator} from '../../../editor/parts'; import {renderModel} from '../../../editor/render'; import {MatrixEvent, MatrixClient} from 'matrix-js-sdk'; +import classNames from 'classnames'; export default class MessageEditor extends React.Component { static propTypes = { @@ -40,22 +43,28 @@ export default class MessageEditor extends React.Component { constructor(props, context) { super(props, context); + const room = this._getRoom(); const partCreator = new PartCreator( () => this._autocompleteRef, query => this.setState({query}), + room, ); this.model = new EditorModel( - parseEvent(this.props.event), + parseEvent(this.props.event, room), partCreator, this._updateEditorState, ); - const room = this.context.matrixClient.getRoom(this.props.event.getRoomId()); this.state = { autoComplete: null, room, }; this._editorRef = null; this._autocompleteRef = null; + this._hasModifications = false; + } + + _getRoom() { + return this.context.matrixClient.getRoom(this.props.event.getRoomId()); } _updateEditorState = (caret) => { @@ -71,48 +80,99 @@ export default class MessageEditor extends React.Component { } _onInput = (event) => { + this._hasModifications = true; const sel = document.getSelection(); const {caret, text} = getCaretOffsetAndText(this._editorRef, sel); this.model.update(text, event.inputType, caret); } + _isCaretAtStart() { + const {caret} = getCaretOffsetAndText(this._editorRef, document.getSelection()); + return caret.offset === 0; + } + + _isCaretAtEnd() { + const {caret, text} = getCaretOffsetAndText(this._editorRef, document.getSelection()); + return caret.offset === text.length; + } + _onKeyDown = (event) => { + // insert newline on Shift+Enter + if (event.shiftKey && event.key === "Enter") { + event.preventDefault(); // just in case the browser does support this + document.execCommand("insertHTML", undefined, "\n"); + return; + } + // autocomplete or enter to send below shouldn't have any modifier keys pressed. if (event.metaKey || event.altKey || event.shiftKey) { return; } - if (!this.model.autoComplete) { - return; + if (this.model.autoComplete) { + const autoComplete = this.model.autoComplete; + switch (event.key) { + case "Enter": + autoComplete.onEnter(event); break; + case "ArrowUp": + autoComplete.onUpArrow(event); break; + case "ArrowDown": + autoComplete.onDownArrow(event); break; + case "Tab": + autoComplete.onTab(event); break; + case "Escape": + autoComplete.onEscape(event); break; + default: + return; // don't preventDefault on anything else + } + event.preventDefault(); + } else if (event.key === "Enter") { + this._sendEdit(); + event.preventDefault(); + } else if (event.key === "Escape") { + this._cancelEdit(); + } else if (event.key === "ArrowUp") { + if (this._hasModifications || !this._isCaretAtStart()) { + return; + } + const previousEvent = findEditableEvent(this._getRoom(), false, this.props.event.getId()); + if (previousEvent) { + dis.dispatch({action: 'edit_event', event: previousEvent}); + event.preventDefault(); + } + } else if (event.key === "ArrowDown") { + if (this._hasModifications || !this._isCaretAtEnd()) { + return; + } + const nextEvent = findEditableEvent(this._getRoom(), true, this.props.event.getId()); + if (nextEvent) { + dis.dispatch({action: 'edit_event', event: nextEvent}); + } else { + dis.dispatch({action: 'edit_event', event: null}); + dis.dispatch({action: 'focus_composer'}); + } + event.preventDefault(); } - const autoComplete = this.model.autoComplete; - switch (event.key) { - case "Enter": - autoComplete.onEnter(event); break; - case "ArrowUp": - autoComplete.onUpArrow(event); break; - case "ArrowDown": - autoComplete.onDownArrow(event); break; - case "Tab": - autoComplete.onTab(event); break; - case "Escape": - autoComplete.onEscape(event); break; - default: - return; // don't preventDefault on anything else - } - event.preventDefault(); } - _onCancelClicked = () => { + _cancelEdit = () => { dis.dispatch({action: "edit_event", event: null}); + dis.dispatch({action: 'focus_composer'}); } - _onSaveClicked = () => { + _sendEdit = () => { const newContent = { "msgtype": "m.text", "body": textSerialize(this.model), }; - if (requiresHtml(this.model)) { + const contentBody = { + msgtype: newContent.msgtype, + body: ` * ${newContent.body}`, + }; + const formattedBody = htmlSerializeIfNeeded(this.model); + if (formattedBody) { newContent.format = "org.matrix.custom.html"; - newContent.formatted_body = htmlSerialize(this.model); + newContent.formatted_body = formattedBody; + contentBody.format = newContent.format; + contentBody.formatted_body = ` * ${newContent.formatted_body}`; } const content = Object.assign({ "m.new_content": newContent, @@ -120,12 +180,13 @@ export default class MessageEditor extends React.Component { "rel_type": "m.replace", "event_id": this.props.event.getId(), }, - }, newContent); + }, contentBody); const roomId = this.props.event.getRoomId(); this.context.matrixClient.sendMessage(roomId, content); dis.dispatch({action: "edit_event", event: null}); + dis.dispatch({action: 'focus_composer'}); } _onAutoCompleteConfirm = (completion) => { @@ -138,6 +199,8 @@ export default class MessageEditor extends React.Component { componentDidMount() { this._updateEditorState(); + setCaretPosition(this._editorRef, this.model, this.model.getPositionAtEnd()); + this._editorRef.focus(); } render() { @@ -157,7 +220,7 @@ export default class MessageEditor extends React.Component {
    ; } const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); - return
    + return
    { autoComplete }
    this._editorRef = ref} + aria-label={_t("Edit message")} >
    - {_t("Cancel")} - {_t("Save")} + {_t("Cancel")} + {_t("Save")}
    ; } diff --git a/src/components/views/elements/ReplyThread.js b/src/components/views/elements/ReplyThread.js index f3cd6e144d..ab7b1abb1c 100644 --- a/src/components/views/elements/ReplyThread.js +++ b/src/components/views/elements/ReplyThread.js @@ -63,6 +63,11 @@ export default class ReplyThread extends React.Component { static getParentEventId(ev) { if (!ev || ev.isRedacted()) return; + // XXX: For newer relations (annotations, replacements, etc.), we now + // have a `getRelation` helper on the event, and you might assume it + // could be used here for replies as well... However, the helper + // currently assumes the relation has a `rel_type`, which older replies + // do not, so this block is left as-is for now. const mRelatesTo = ev.getWireContent()['m.relates_to']; if (mRelatesTo && mRelatesTo['m.in_reply_to']) { const mInReplyTo = mRelatesTo['m.in_reply_to']; diff --git a/src/components/views/elements/TextWithTooltip.js b/src/components/views/elements/TextWithTooltip.js new file mode 100644 index 0000000000..61c3a2125a --- /dev/null +++ b/src/components/views/elements/TextWithTooltip.js @@ -0,0 +1,56 @@ +/* + Copyright 2019 New Vector Ltd. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import React from 'react'; +import PropTypes from 'prop-types'; +import sdk from '../../../index'; + +export default class TextWithTooltip extends React.Component { + static propTypes = { + class: PropTypes.string, + tooltip: PropTypes.string.isRequired, + }; + + constructor() { + super(); + + this.state = { + hover: false, + }; + } + + onMouseOver = () => { + this.setState({hover: true}); + }; + + onMouseOut = () => { + this.setState({hover: false}); + }; + + render() { + const Tooltip = sdk.getComponent("elements.Tooltip"); + + return ( + + {this.props.children} + + + ); + } +} diff --git a/src/components/views/elements/Tooltip.js b/src/components/views/elements/Tooltip.js index 1cc82978ed..1d6b54f413 100644 --- a/src/components/views/elements/Tooltip.js +++ b/src/components/views/elements/Tooltip.js @@ -79,6 +79,10 @@ module.exports = React.createClass({ let offset = 0; if (parentBox.height > MIN_TOOLTIP_HEIGHT) { offset = Math.floor((parentBox.height - MIN_TOOLTIP_HEIGHT) / 2); + } else { + // The tooltip is larger than the parent height: figure out what offset + // we need so that we're still centered. + offset = Math.floor(parentBox.height - MIN_TOOLTIP_HEIGHT); } style.top = (parentBox.top - 2) + window.pageYOffset + offset; style.left = 6 + parentBox.right + window.pageXOffset; diff --git a/src/components/views/groups/GroupInviteTile.js b/src/components/views/groups/GroupInviteTile.js index 8482bce593..843bb29055 100644 --- a/src/components/views/groups/GroupInviteTile.js +++ b/src/components/views/groups/GroupInviteTile.js @@ -117,7 +117,6 @@ export default React.createClass({ render: function() { const BaseAvatar = sdk.getComponent('avatars.BaseAvatar'); - const EmojiText = sdk.getComponent('elements.EmojiText'); const groupName = this.props.group.name || this.props.group.groupId; const httpAvatarUrl = this.props.group.avatarUrl ? @@ -129,9 +128,9 @@ export default React.createClass({ 'mx_RoomTile_badgeShown': this.state.badgeHover || this.state.menuDisplayed, }); - const label = + const label =
    { groupName } - ; +
    ; const badgeEllipsis = this.state.badgeHover || this.state.menuDisplayed; const badgeClasses = classNames('mx_RoomTile_badge mx_RoomTile_highlight', { diff --git a/src/components/views/groups/GroupMemberInfo.js b/src/components/views/groups/GroupMemberInfo.js index a25d4271ed..34a7e139fd 100644 --- a/src/components/views/groups/GroupMemberInfo.js +++ b/src/components/views/groups/GroupMemberInfo.js @@ -180,7 +180,6 @@ module.exports = React.createClass({ this.props.groupMember.displayname || this.props.groupMember.userId ); - const EmojiText = sdk.getComponent('elements.EmojiText'); const GeminiScrollbarWrapper = sdk.getComponent('elements.GeminiScrollbarWrapper'); return (
    @@ -189,7 +188,7 @@ module.exports = React.createClass({ { avatarElement } - { groupMemberName } +

    { groupMemberName }

    diff --git a/src/components/views/groups/GroupRoomInfo.js b/src/components/views/groups/GroupRoomInfo.js index df1803fa11..db060218d4 100644 --- a/src/components/views/groups/GroupRoomInfo.js +++ b/src/components/views/groups/GroupRoomInfo.js @@ -149,7 +149,6 @@ module.exports = React.createClass({ }, render: function() { - const EmojiText = sdk.getComponent('elements.EmojiText'); const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); const InlineSpinner = sdk.getComponent('elements.InlineSpinner'); const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper"); @@ -221,7 +220,7 @@ module.exports = React.createClass({ { avatarElement } - { groupRoomName } +

    { groupRoomName }

    diff --git a/src/components/views/messages/MFileBody.js b/src/components/views/messages/MFileBody.js index 7e107c1ad8..d7452632e1 100644 --- a/src/components/views/messages/MFileBody.js +++ b/src/components/views/messages/MFileBody.js @@ -145,6 +145,7 @@ function remoteRender(event) { a.target = data.target; a.download = data.download; a.style = data.style; + a.style.fontFamily = "Arial, Helvetica, Sans-Serif"; a.href = window.URL.createObjectURL(data.blob); a.appendChild(img); a.appendChild(document.createTextNode(data.textContent)); diff --git a/src/components/views/messages/MImageBody.js b/src/components/views/messages/MImageBody.js index 2f12022140..78af5da727 100644 --- a/src/components/views/messages/MImageBody.js +++ b/src/components/views/messages/MImageBody.js @@ -172,8 +172,8 @@ export default class MImageBody extends React.Component { // thumbnail resolution will be unnecessarily reduced. // custom timeline widths seems preferable. const pixelRatio = window.devicePixelRatio; - const thumbWidth = 800 * pixelRatio; - const thumbHeight = 600 * pixelRatio; + const thumbWidth = Math.round(800 * pixelRatio); + const thumbHeight = Math.round(600 * pixelRatio); const content = this.props.mxEvent.getContent(); if (content.file !== undefined) { diff --git a/src/components/views/messages/MessageActionBar.js b/src/components/views/messages/MessageActionBar.js index fe6c22ab1e..84474710cd 100644 --- a/src/components/views/messages/MessageActionBar.js +++ b/src/components/views/messages/MessageActionBar.js @@ -23,7 +23,7 @@ import dis from '../../../dispatcher'; import Modal from '../../../Modal'; import { createMenu } from '../../structures/ContextualMenu'; import SettingsStore from '../../../settings/SettingsStore'; -import { isContentActionable } from '../../../utils/EventUtils'; +import { isContentActionable, canEditContent } from '../../../utils/EventUtils'; export default class MessageActionBar extends React.PureComponent { static propTypes = { @@ -148,12 +148,12 @@ export default class MessageActionBar extends React.PureComponent { title={_t("Reply")} onClick={this.onReplyClick} />; - if (this.isEditingEnabled()) { - editButton = ; - } + } + if (this.isEditingEnabled() && canEditContent(this.props.mxEvent)) { + editButton = ; } return
    diff --git a/src/components/views/messages/MessageEvent.js b/src/components/views/messages/MessageEvent.js index 357da1cd10..8c90ec5a46 100644 --- a/src/components/views/messages/MessageEvent.js +++ b/src/components/views/messages/MessageEvent.js @@ -90,6 +90,7 @@ module.exports = React.createClass({ tileShape={this.props.tileShape} maxImageHeight={this.props.maxImageHeight} replacingEventId={this.props.replacingEventId} + isEditing={this.props.isEditing} onHeightChanged={this.props.onHeightChanged} />; }, }); diff --git a/src/components/views/messages/ReactionDimension.js b/src/components/views/messages/ReactionDimension.js index a0cf5a86ec..de33ad1a57 100644 --- a/src/components/views/messages/ReactionDimension.js +++ b/src/components/views/messages/ReactionDimension.js @@ -37,6 +37,7 @@ export default class ReactionDimension extends React.PureComponent { if (props.reactions) { props.reactions.on("Relations.add", this.onReactionsChange); + props.reactions.on("Relations.remove", this.onReactionsChange); props.reactions.on("Relations.redaction", this.onReactionsChange); } } @@ -44,6 +45,7 @@ export default class ReactionDimension extends React.PureComponent { componentDidUpdate(prevProps) { if (prevProps.reactions !== this.props.reactions) { this.props.reactions.on("Relations.add", this.onReactionsChange); + this.props.reactions.on("Relations.remove", this.onReactionsChange); this.props.reactions.on("Relations.redaction", this.onReactionsChange); this.onReactionsChange(); } @@ -55,6 +57,10 @@ export default class ReactionDimension extends React.PureComponent { "Relations.add", this.onReactionsChange, ); + this.props.reactions.removeListener( + "Relations.remove", + this.onReactionsChange, + ); this.props.reactions.removeListener( "Relations.redaction", this.onReactionsChange, @@ -82,7 +88,7 @@ export default class ReactionDimension extends React.PureComponent { if (mxEvent.isRedacted()) { return false; } - return mxEvent.getContent()["m.relates_to"].key === option; + return mxEvent.getRelation().key === option; }); if (!reactionForOption) { continue; @@ -107,7 +113,11 @@ export default class ReactionDimension extends React.PureComponent { return null; } const userId = MatrixClientPeg.get().getUserId(); - return reactions.getAnnotationsBySender()[userId]; + const myReactions = reactions.getAnnotationsBySender()[userId]; + if (!myReactions) { + return null; + } + return [...myReactions.values()]; } onOptionClick = (ev) => { @@ -158,6 +168,7 @@ export default class ReactionDimension extends React.PureComponent { return {items} ; diff --git a/src/components/views/messages/ReactionsRow.js b/src/components/views/messages/ReactionsRow.js index ffb81e1a38..d3bf6a2035 100644 --- a/src/components/views/messages/ReactionsRow.js +++ b/src/components/views/messages/ReactionsRow.js @@ -34,6 +34,7 @@ export default class ReactionsRow extends React.PureComponent { if (props.reactions) { props.reactions.on("Relations.add", this.onReactionsChange); + props.reactions.on("Relations.remove", this.onReactionsChange); props.reactions.on("Relations.redaction", this.onReactionsChange); } @@ -45,6 +46,7 @@ export default class ReactionsRow extends React.PureComponent { componentDidUpdate(prevProps) { if (prevProps.reactions !== this.props.reactions) { this.props.reactions.on("Relations.add", this.onReactionsChange); + this.props.reactions.on("Relations.remove", this.onReactionsChange); this.props.reactions.on("Relations.redaction", this.onReactionsChange); this.onReactionsChange(); } @@ -56,6 +58,10 @@ export default class ReactionsRow extends React.PureComponent { "Relations.add", this.onReactionsChange, ); + this.props.reactions.removeListener( + "Relations.remove", + this.onReactionsChange, + ); this.props.reactions.removeListener( "Relations.redaction", this.onReactionsChange, @@ -80,7 +86,11 @@ export default class ReactionsRow extends React.PureComponent { return null; } const userId = MatrixClientPeg.get().getUserId(); - return reactions.getAnnotationsBySender()[userId]; + const myReactions = reactions.getAnnotationsBySender()[userId]; + if (!myReactions) { + return null; + } + return [...myReactions.values()]; } render() { @@ -101,13 +111,13 @@ export default class ReactionsRow extends React.PureComponent { if (mxEvent.isRedacted()) { return false; } - return mxEvent.getContent()["m.relates_to"].key === content; + return mxEvent.getRelation().key === content; }); return ; }); diff --git a/src/components/views/messages/ReactionsRowButton.js b/src/components/views/messages/ReactionsRowButton.js index 721147cdb8..19cae27b87 100644 --- a/src/components/views/messages/ReactionsRowButton.js +++ b/src/components/views/messages/ReactionsRowButton.js @@ -19,17 +19,28 @@ import PropTypes from 'prop-types'; import classNames from 'classnames'; import MatrixClientPeg from '../../../MatrixClientPeg'; +import sdk from '../../../index'; export default class ReactionsRowButton extends React.PureComponent { static propTypes = { // The event we're displaying reactions for mxEvent: PropTypes.object.isRequired, + // The reaction content / key / emoji content: PropTypes.string.isRequired, - count: PropTypes.number.isRequired, + // A Set of Martix reaction events for this key + reactionEvents: PropTypes.object.isRequired, // A possible Matrix event if the current user has voted for this type myReactionEvent: PropTypes.object, } + constructor(props) { + super(props); + + this.state = { + tooltipVisible: false, + }; + } + onClick = (ev) => { const { mxEvent, myReactionEvent, content } = this.props; if (myReactionEvent) { @@ -48,18 +59,53 @@ export default class ReactionsRowButton extends React.PureComponent { } }; + onMouseOver = () => { + this.setState({ + // To avoid littering the DOM with a tooltip for every reaction, + // only render it on first use. + tooltipRendered: true, + tooltipVisible: true, + }); + } + + onMouseOut = () => { + this.setState({ + tooltipVisible: false, + }); + } + render() { - const { content, count, myReactionEvent } = this.props; + const ReactionsRowButtonTooltip = + sdk.getComponent('messages.ReactionsRowButtonTooltip'); + const { content, reactionEvents, myReactionEvent } = this.props; + + const count = reactionEvents.size; + if (!count) { + return null; + } const classes = classNames({ mx_ReactionsRowButton: true, mx_ReactionsRowButton_selected: !!myReactionEvent, }); + let tooltip; + if (this.state.tooltipRendered) { + tooltip = ; + } + return {content} {count} + {tooltip} ; } } diff --git a/src/components/views/messages/ReactionsRowButtonTooltip.js b/src/components/views/messages/ReactionsRowButtonTooltip.js new file mode 100644 index 0000000000..b70724d516 --- /dev/null +++ b/src/components/views/messages/ReactionsRowButtonTooltip.js @@ -0,0 +1,84 @@ +/* +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 MatrixClientPeg from '../../../MatrixClientPeg'; +import sdk from '../../../index'; +import { unicodeToShortcode } from '../../../HtmlUtils'; +import { _t } from '../../../languageHandler'; +import { formatCommaSeparatedList } from '../../../utils/FormattingUtils'; + +export default class ReactionsRowButtonTooltip extends React.PureComponent { + static propTypes = { + // The event we're displaying reactions for + mxEvent: PropTypes.object.isRequired, + // The reaction content / key / emoji + content: PropTypes.string.isRequired, + // A Set of Martix reaction events for this key + reactionEvents: PropTypes.object.isRequired, + visible: PropTypes.bool.isRequired, + } + + render() { + const Tooltip = sdk.getComponent('elements.Tooltip'); + const { content, reactionEvents, mxEvent, visible } = this.props; + + const room = MatrixClientPeg.get().getRoom(mxEvent.getRoomId()); + let tooltipLabel; + if (room) { + const senders = []; + for (const reactionEvent of reactionEvents) { + const { name } = room.getMember(reactionEvent.getSender()); + senders.push(name); + } + const shortName = unicodeToShortcode(content); + tooltipLabel =
    {_t( + "reacted with %(shortName)s", + { + shortName, + }, + { + reactors: () => { + return
    + {formatCommaSeparatedList(senders, 6)} +
    ; + }, + reactedWith: (sub) => { + if (!shortName) { + return null; + } + return
    + {sub} +
    ; + }, + }, + )}
    ; + } + + let tooltip; + if (tooltipLabel) { + tooltip = ; + } + + return tooltip; + } +} diff --git a/src/components/views/messages/SenderProfile.js b/src/components/views/messages/SenderProfile.js index 75898736f1..637a56727f 100644 --- a/src/components/views/messages/SenderProfile.js +++ b/src/components/views/messages/SenderProfile.js @@ -19,7 +19,6 @@ import React from 'react'; import PropTypes from 'prop-types'; import {MatrixClient} from 'matrix-js-sdk'; -import sdk from '../../../index'; import Flair from '../elements/Flair.js'; import FlairStore from '../../../stores/FlairStore'; import { _t } from '../../../languageHandler'; @@ -95,7 +94,6 @@ export default React.createClass({ }, render() { - const EmojiText = sdk.getComponent('elements.EmojiText'); const {mxEvent} = this.props; const colorClass = getUserNameColorClass(mxEvent.getSender()); const name = mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender(); @@ -117,7 +115,7 @@ export default React.createClass({ />; } - const nameElem = { name || '' }; + const nameElem = name || ''; // Name + flair const nameFlair = diff --git a/src/components/views/messages/TextualBody.js b/src/components/views/messages/TextualBody.js index deb3c5cc0f..1fc16d6a53 100644 --- a/src/components/views/messages/TextualBody.js +++ b/src/components/views/messages/TextualBody.js @@ -1,6 +1,7 @@ /* Copyright 2015, 2016 OpenMarket Ltd Copyright 2017 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. @@ -22,6 +23,7 @@ import ReactDOM from 'react-dom'; import PropTypes from 'prop-types'; import highlight from 'highlight.js'; import * as HtmlUtils from '../../../HtmlUtils'; +import {formatDate} from '../../../DateUtils'; import sdk from '../../../index'; import ScalarAuthClient from '../../../ScalarAuthClient'; import Modal from '../../../Modal'; @@ -88,7 +90,12 @@ module.exports = React.createClass({ componentDidMount: function() { this._unmounted = false; + if (!this.props.isEditing) { + this._applyFormatting(); + } + }, + _applyFormatting() { // pillifyLinks BEFORE linkifyElement because plain room/user URLs in the composer // are still sent as plaintext URLs. If these are ever pillified in the composer, // we should be pillify them here by doing the linkifying BEFORE the pillifying. @@ -123,8 +130,14 @@ module.exports = React.createClass({ } }, - componentDidUpdate: function() { - this.calculateUrlPreview(); + componentDidUpdate: function(prevProps) { + if (!this.props.isEditing) { + const stoppedEditing = prevProps.isEditing && !this.props.isEditing; + const messageWasEdited = prevProps.replacingEventId !== this.props.replacingEventId; + if (messageWasEdited || stoppedEditing) { + this._applyFormatting(); + } + } }, componentWillUnmount: function() { @@ -140,14 +153,16 @@ module.exports = React.createClass({ nextProps.replacingEventId !== this.props.replacingEventId || nextProps.highlightLink !== this.props.highlightLink || nextProps.showUrlPreview !== this.props.showUrlPreview || + nextProps.isEditing !== this.props.isEditing || nextState.links !== this.state.links || + nextState.editedMarkerHovered !== this.state.editedMarkerHovered || nextState.widgetHidden !== this.state.widgetHidden); }, calculateUrlPreview: function() { //console.log("calculateUrlPreview: ShowUrlPreview for %s is %s", this.props.mxEvent.getId(), this.props.showUrlPreview); - if (this.props.showUrlPreview && !this.state.links.length) { + if (this.props.showUrlPreview) { let links = this.findLinks(this.refs.content.children); if (links.length) { // de-dup the links (but preserve ordering) @@ -425,8 +440,39 @@ module.exports = React.createClass({ }); }, + _onMouseEnterEditedMarker: function() { + this.setState({editedMarkerHovered: true}); + }, + + _onMouseLeaveEditedMarker: function() { + this.setState({editedMarkerHovered: false}); + }, + + _renderEditedMarker: function() { + let editedTooltip; + if (this.state.editedMarkerHovered) { + const Tooltip = sdk.getComponent('elements.Tooltip'); + const editEvent = this.props.mxEvent.replacingEvent(); + const date = editEvent && formatDate(editEvent.getDate()); + editedTooltip = ; + } + return ( +
    {editedTooltip}{`(${_t("edited")})`}
    + ); + }, + render: function() { - const EmojiText = sdk.getComponent('elements.EmojiText'); + if (this.props.isEditing) { + const MessageEditor = sdk.getComponent('elements.MessageEditor'); + return ; + } const mxEvent = this.props.mxEvent; const content = mxEvent.getContent(); @@ -436,6 +482,9 @@ module.exports = React.createClass({ // Part of Replies fallback support stripReplyFallback: stripReply, }); + if (this.props.replacingEventId) { + body = [body, this._renderEditedMarker()]; + } if (this.props.highlightLink) { body = { body }; @@ -462,12 +511,12 @@ module.exports = React.createClass({ return ( *  - { name } - +   { body } { widgets } diff --git a/src/components/views/messages/TextualEvent.js b/src/components/views/messages/TextualEvent.js index 6c87000615..a46e6cf8ec 100644 --- a/src/components/views/messages/TextualEvent.js +++ b/src/components/views/messages/TextualEvent.js @@ -20,7 +20,6 @@ const React = require('react'); import PropTypes from 'prop-types'; const TextForEvent = require('../../../TextForEvent'); -import sdk from '../../../index'; module.exports = React.createClass({ displayName: 'TextualEvent', @@ -31,11 +30,10 @@ module.exports = React.createClass({ }, render: function() { - const EmojiText = sdk.getComponent('elements.EmojiText'); const text = TextForEvent.textForEvent(this.props.mxEvent); if (text == null || text.length === 0) return null; return ( - { text } +
    { text }
    ); }, }); diff --git a/src/components/views/messages/ViewSourceEvent.js b/src/components/views/messages/ViewSourceEvent.js new file mode 100644 index 0000000000..62cf45fb6e --- /dev/null +++ b/src/components/views/messages/ViewSourceEvent.js @@ -0,0 +1,67 @@ +/* +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 classNames from 'classnames'; + +export default class ViewSourceEvent extends React.PureComponent { + static propTypes = { + /* the MatrixEvent to show */ + mxEvent: PropTypes.object.isRequired, + }; + + constructor(props) { + super(props); + + this.state = { + expanded: false, + }; + } + + onToggle = (ev) => { + ev.preventDefault(); + const { expanded } = this.state; + this.setState({ + expanded: !expanded, + }); + } + + render() { + const { mxEvent } = this.props; + const { expanded } = this.state; + + let content; + if (expanded) { + content =
    {JSON.stringify(mxEvent, null, 4)}
    ; + } else { + content = {`{ "type": ${mxEvent.getType()} }`}; + } + + const classes = classNames("mx_ViewSourceEvent mx_EventTile_content", { + mx_ViewSourceEvent_expanded: expanded, + }); + + return + {content} + + ; + } +} diff --git a/src/components/views/rooms/Autocomplete.js b/src/components/views/rooms/Autocomplete.js index a19a4eaad0..466deeba28 100644 --- a/src/components/views/rooms/Autocomplete.js +++ b/src/components/views/rooms/Autocomplete.js @@ -256,8 +256,6 @@ export default class Autocomplete extends React.Component { } render() { - const EmojiText = sdk.getComponent('views.elements.EmojiText'); - let position = 1; const renderedCompletions = this.state.completions.map((completionResult, i) => { const completions = completionResult.completions.map((completion, i) => { @@ -282,7 +280,7 @@ export default class Autocomplete extends React.Component { return completions.length > 0 ? (
    - { completionResult.provider.getName() } +
    { completionResult.provider.getName() }
    { completionResult.provider.renderCompletions(completions) }
    ) : null; diff --git a/src/components/views/rooms/EntityTile.js b/src/components/views/rooms/EntityTile.js index d2ae6217b7..bfeeced339 100644 --- a/src/components/views/rooms/EntityTile.js +++ b/src/components/views/rooms/EntityTile.js @@ -111,7 +111,6 @@ const EntityTile = React.createClass({ let nameEl; const {name} = this.props; - const EmojiText = sdk.getComponent('elements.EmojiText'); if (!this.props.suppressOnHover) { const activeAgo = this.props.presenceLastActiveAgo ? (Date.now() - (this.props.presenceLastTs - this.props.presenceLastActiveAgo)) : -1; @@ -128,24 +127,24 @@ const EntityTile = React.createClass({ } nameEl = (
    - +
    { name } - +
    {presenceLabel}
    ); } else if (this.props.subtextLabel) { nameEl = (
    - +
    {name} - +
    {this.props.subtextLabel}
    ); } else { nameEl = ( - { name } +
    { name }
    ); } diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index f38e3c3946..78e98a8133 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -160,8 +160,11 @@ module.exports = withMatrixClient(React.createClass({ // show twelve hour timestamps isTwelveHour: PropTypes.bool, - // helper function to access relations for an event + // helper function to access relations for this event getRelationsForEvent: PropTypes.func, + + // whether to show reactions for this event + showReactions: PropTypes.bool, }, getDefaultProps: function() { @@ -198,7 +201,7 @@ module.exports = withMatrixClient(React.createClass({ const client = this.props.matrixClient; client.on("deviceVerificationChanged", this.onDeviceVerificationChanged); this.props.mxEvent.on("Event.decrypted", this._onDecrypted); - if (SettingsStore.isFeatureEnabled("feature_reactions")) { + if (this.props.showReactions && SettingsStore.isFeatureEnabled("feature_reactions")) { this.props.mxEvent.on("Event.relationsCreated", this._onReactionsCreated); } }, @@ -223,7 +226,7 @@ module.exports = withMatrixClient(React.createClass({ const client = this.props.matrixClient; client.removeListener("deviceVerificationChanged", this.onDeviceVerificationChanged); this.props.mxEvent.removeListener("Event.decrypted", this._onDecrypted); - if (SettingsStore.isFeatureEnabled("feature_reactions")) { + if (this.props.showReactions && SettingsStore.isFeatureEnabled("feature_reactions")) { this.props.mxEvent.removeListener("Event.relationsCreated", this._onReactionsCreated); } }, @@ -485,6 +488,7 @@ module.exports = withMatrixClient(React.createClass({ getReactions() { if ( + !this.props.showReactions || !this.props.getRelationsForEvent || !SettingsStore.isFeatureEnabled("feature_reactions") ) { @@ -520,7 +524,10 @@ module.exports = withMatrixClient(React.createClass({ eventType !== 'm.room.message' && eventType !== 'm.sticker' && eventType != 'm.room.create' ); - const tileHandler = getHandlerTile(this.props.mxEvent); + let tileHandler = getHandlerTile(this.props.mxEvent); + if (!tileHandler && SettingsStore.getValue("showHiddenEventsInTimeline")) { + tileHandler = "messages.ViewSourceEvent"; + } // This shouldn't happen: the caller should check we support this type // before trying to instantiate us if (!tileHandler) { @@ -540,6 +547,7 @@ module.exports = withMatrixClient(React.createClass({ const classes = classNames({ mx_EventTile: true, + mx_EventTile_isEditing: this.props.isEditing, mx_EventTile_info: isInfoMessage, mx_EventTile_12hr: this.props.isTwelveHour, mx_EventTile_encrypting: this.props.eventSendStatus === 'encrypting', @@ -617,14 +625,14 @@ module.exports = withMatrixClient(React.createClass({ } const MessageActionBar = sdk.getComponent('messages.MessageActionBar'); - const actionBar = ; + /> : undefined; const timestamp = this.props.mxEvent.getTs() ? : null; @@ -674,14 +682,13 @@ module.exports = withMatrixClient(React.createClass({ switch (this.props.tileShape) { case 'notif': { - const EmojiText = sdk.getComponent('elements.EmojiText'); const room = this.props.matrixClient.getRoom(this.props.mxEvent.getRoomId()); return (
    { avatar } @@ -780,6 +787,7 @@ module.exports = withMatrixClient(React.createClass({ { - // The avatar goes after the event tile as it's absolutly positioned to be over the + // The avatar goes after the event tile as it's absolutely positioned to be over the // event tile line, so needs to be later in the DOM so it appears on top (this avoids // the need for further z-indexing chaos) } diff --git a/src/components/views/rooms/MemberInfo.js b/src/components/views/rooms/MemberInfo.js index b9eea2b455..3c098b3d7a 100644 --- a/src/components/views/rooms/MemberInfo.js +++ b/src/components/views/rooms/MemberInfo.js @@ -978,7 +978,6 @@ module.exports = withMatrixClient(React.createClass({ } const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper"); - const EmojiText = sdk.getComponent('elements.EmojiText'); let backButton; if (this.props.member.roomId) { @@ -993,7 +992,7 @@ module.exports = withMatrixClient(React.createClass({
    { backButton } { e2eIconElement } - { memberName } +

    { memberName }

    { avatarElement }
    diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index e54ddd6787..e1a7f559f8 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -40,21 +40,18 @@ import Analytics from '../../../Analytics'; import dis from '../../../dispatcher'; -import * as RichText from '../../../RichText'; import * as HtmlUtils from '../../../HtmlUtils'; import Autocomplete from './Autocomplete'; import {Completion} from "../../../autocomplete/Autocompleter"; import Markdown from '../../../Markdown'; -import ComposerHistoryManager from '../../../ComposerHistoryManager'; import MessageComposerStore from '../../../stores/MessageComposerStore'; import ContentMessages from '../../../ContentMessages'; import {MATRIXTO_URL_PATTERN} from '../../../linkify-matrix'; -import { - asciiRegexp, unicodeRegexp, shortnameToUnicode, - asciiList, mapUnicodeToShort, toShort, -} from 'emojione'; +import EMOJIBASE from 'emojibase-data/en/compact.json'; +import EMOTICON_REGEX from 'emojibase-regex/emoticon'; + import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore"; import {makeUserPermalink} from "../../../matrix-to"; import ReplyPreview from "./ReplyPreview"; @@ -62,10 +59,9 @@ import RoomViewStore from '../../../stores/RoomViewStore'; import ReplyThread from "../elements/ReplyThread"; import {ContentHelpers} from 'matrix-js-sdk'; import AccessibleButton from '../elements/AccessibleButton'; +import {findEditableEvent} from '../../../utils/EventUtils'; -const EMOJI_UNICODE_TO_SHORTNAME = mapUnicodeToShort(); -const REGEX_EMOJI_WHITESPACE = new RegExp('(?:^|\\s)(' + asciiRegexp + ')\\s$'); -const EMOJI_REGEX = new RegExp(unicodeRegexp, 'g'); +const REGEX_EMOTICON_WHITESPACE = new RegExp('(?:^|\\s)(' + EMOTICON_REGEX.source + ')\\s$'); const TYPING_USER_TIMEOUT = 10000; const TYPING_SERVER_TIMEOUT = 30000; @@ -144,7 +140,6 @@ export default class MessageComposerInput extends React.Component { client: MatrixClient; autocomplete: Autocomplete; - historyManager: ComposerHistoryManager; constructor(props, context) { super(props, context); @@ -273,9 +268,8 @@ export default class MessageComposerInput extends React.Component { case 'emoji': // XXX: apparently you can't return plain strings from serializer rules // until https://github.com/ianstormtaylor/slate/pull/1854 is merged. - // So instead we temporarily wrap emoji from RTE in an arbitrary tag - // (). would be nicer, but in practice it causes CSS issues. - return { obj.data.get('emojiUnicode') }; + // So instead we temporarily wrap emoji from RTE in a span. + return { obj.data.get('emojiUnicode') }; } return this.renderNode({ node: obj, @@ -335,7 +329,6 @@ export default class MessageComposerInput extends React.Component { componentWillMount() { this.dispatcherRef = dis.register(this.onAction); - this.historyManager = new ComposerHistoryManager(this.props.room.roomId, 'mx_slate_composer_history_'); } componentWillUnmount() { @@ -375,7 +368,6 @@ export default class MessageComposerInput extends React.Component { const html = HtmlUtils.bodyToHtml(payload.event.getContent(), null, { forComposerQuote: true, returnString: true, - emojiOne: false, }); const fragment = this.html.deserialize(html); // FIXME: do we want to put in a permalink to the original quote here? @@ -483,6 +475,7 @@ export default class MessageComposerInput extends React.Component { sendTyping(isTyping) { if (!SettingsStore.getValue('sendTypingNotifications')) return; + if (SettingsStore.getValue('lowBandwidth')) return; MatrixClientPeg.get().sendTyping( this.props.room.roomId, this.isTyping, TYPING_SERVER_TIMEOUT, @@ -538,17 +531,15 @@ export default class MessageComposerInput extends React.Component { // Automatic replacement of plaintext emoji to Unicode emoji if (SettingsStore.getValue('MessageComposerInput.autoReplaceEmoji')) { // The first matched group includes just the matched plaintext emoji - const emojiMatch = REGEX_EMOJI_WHITESPACE.exec(text.slice(0, currentStartOffset)); - if (emojiMatch) { - // plaintext -> hex unicode - const emojiUc = asciiList[emojiMatch[1]]; - // hex unicode -> shortname -> actual unicode - const unicodeEmoji = shortnameToUnicode(EMOJI_UNICODE_TO_SHORTNAME[emojiUc]); + const emoticonMatch = REGEX_EMOTICON_WHITESPACE.exec(text.slice(0, currentStartOffset)); + if (emoticonMatch) { + const data = EMOJIBASE.find(e => e.emoticon === emoticonMatch[1]); + const unicodeEmoji = data ? data.unicode : ''; const range = Range.create({ anchor: { key: editorState.startText.key, - offset: currentStartOffset - emojiMatch[1].length - 1, + offset: currentStartOffset - emoticonMatch[1].length - 1, }, focus: { key: editorState.startText.key, @@ -561,54 +552,6 @@ export default class MessageComposerInput extends React.Component { } } - // emojioneify any emoji - let foundEmoji; - do { - foundEmoji = false; - - for (const node of editorState.document.getTexts()) { - if (node.text !== '' && HtmlUtils.containsEmoji(node.text)) { - let match; - EMOJI_REGEX.lastIndex = 0; - while ((match = EMOJI_REGEX.exec(node.text)) !== null) { - const range = Range.create({ - anchor: { - key: node.key, - offset: match.index, - }, - focus: { - key: node.key, - offset: match.index + match[0].length, - }, - }); - const inline = Inline.create({ - type: 'emoji', - data: { emojiUnicode: match[0] }, - }); - change = change.insertInlineAtRange(range, inline); - editorState = change.value; - - // if we replaced an emoji, start again looking for more - // emoji in the new editor state since doing the replacement - // will change the node structure & offsets so we can't compute - // insertion ranges from node.key / match.index anymore. - foundEmoji = true; - break; - } - } - } - } while (foundEmoji); - - // work around weird bug where inserting emoji via the macOS - // emoji picker can leave the selection stuck in the emoji's - // child text. This seems to happen due to selection getting - // moved in the normalisation phase after calculating these changes - if (editorState.selection.anchor.key && - editorState.document.getParent(editorState.selection.anchor.key).type === 'emoji') { - change = change.moveToStartOfNextText(); - editorState = change.value; - } - if (this.props.onInputStateChanged && editorState.blocks.size > 0) { let blockType = editorState.blocks.first().type; // console.log("onInputStateChanged; current block type is " + blockType + " and marks are " + editorState.activeMarks); @@ -1046,6 +989,12 @@ export default class MessageComposerInput extends React.Component { return change.insertText('\n'); } + if (this.autocomplete.countCompletions() > 0) { + this.autocomplete.hide(); + ev.preventDefault(); + return true; + } + const editorState = this.state.editorState; const lastBlock = editorState.blocks.last(); @@ -1087,7 +1036,6 @@ export default class MessageComposerInput extends React.Component { if (cmd) { if (!cmd.error) { - this.historyManager.save(editorState, this.state.isRichTextEnabled ? 'rich' : 'markdown'); this.setState({ editorState: this.createEditorState(), }, ()=>{ @@ -1165,11 +1113,6 @@ export default class MessageComposerInput extends React.Component { let sendHtmlFn = ContentHelpers.makeHtmlMessage; let sendTextFn = ContentHelpers.makeTextMessage; - this.historyManager.save( - editorState, - this.state.isRichTextEnabled ? 'rich' : 'markdown', - ); - if (commandText && commandText.startsWith('/me')) { if (replyingToEv) { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); @@ -1244,14 +1187,16 @@ export default class MessageComposerInput extends React.Component { // and we must be at the edge of the document (up=start, down=end) if (up) { if (!selection.anchor.isAtStartOfNode(document)) return; - } else { - if (!selection.anchor.isAtEndOfNode(document)) return; - } - const selected = this.selectHistory(up); - if (selected) { - // We're selecting history, so prevent the key event from doing anything else - e.preventDefault(); + const editEvent = findEditableEvent(this.props.room, false); + if (editEvent) { + // We're selecting history, so prevent the key event from doing anything else + e.preventDefault(); + dis.dispatch({ + action: 'edit_event', + event: editEvent, + }); + } } } else { this.moveAutocompleteSelection(up); @@ -1259,54 +1204,6 @@ export default class MessageComposerInput extends React.Component { } }; - selectHistory = async (up) => { - const delta = up ? -1 : 1; - - // True if we are not currently selecting history, but composing a message - if (this.historyManager.currentIndex === this.historyManager.history.length) { - // We can't go any further - there isn't any more history, so nop. - if (!up) { - return; - } - this.setState({ - currentlyComposedEditorState: this.state.editorState, - }); - } else if (this.historyManager.currentIndex + delta === this.historyManager.history.length) { - // True when we return to the message being composed currently - this.setState({ - editorState: this.state.currentlyComposedEditorState, - }); - this.historyManager.currentIndex = this.historyManager.history.length; - return; - } - - let editorState; - const historyItem = this.historyManager.getItem(delta); - if (!historyItem) return; - - if (historyItem.format === 'rich' && !this.state.isRichTextEnabled) { - editorState = this.richToMdEditorState(historyItem.value); - } else if (historyItem.format === 'markdown' && this.state.isRichTextEnabled) { - editorState = this.mdToRichEditorState(historyItem.value); - } else { - editorState = historyItem.value; - } - - // Move selection to the end of the selected history - const change = editorState.change().moveToEndOfNode(editorState.document); - - // We don't call this.onChange(change) now, as fixups on stuff like emoji - // should already have been done and persisted in the history. - editorState = change.value; - - this.suppressAutoComplete = true; - - this.setState({ editorState }, ()=>{ - this._editor.focus(); - }); - return true; - }; - onTab = async (e) => { this.setState({ someCompletions: null, @@ -1475,17 +1372,7 @@ export default class MessageComposerInput extends React.Component { } case 'emoji': { const { data } = node; - const emojiUnicode = data.get('emojiUnicode'); - const uri = RichText.unicodeToEmojiUri(emojiUnicode); - const shortname = toShort(emojiUnicode); - const className = classNames('mx_emojione', { - mx_emojione_selected: isSelected, - }); - const style = {}; - if (props.selected) style.border = '1px solid blue'; - return {; + return data.get('emojiUnicode'); } } }; diff --git a/src/components/views/rooms/ReplyPreview.js b/src/components/views/rooms/ReplyPreview.js index 7599c5c308..3b7874a875 100644 --- a/src/components/views/rooms/ReplyPreview.js +++ b/src/components/views/rooms/ReplyPreview.js @@ -66,13 +66,12 @@ export default class ReplyPreview extends React.Component { if (!this.state.event) return null; const EventTile = sdk.getComponent('rooms.EventTile'); - const EmojiText = sdk.getComponent('views.elements.EmojiText'); return
    - +
    { '💬 ' + _t('Replying') } - +
    diff --git a/src/components/views/rooms/RoomHeader.js b/src/components/views/rooms/RoomHeader.js index 435b41f828..a40746dd04 100644 --- a/src/components/views/rooms/RoomHeader.js +++ b/src/components/views/rooms/RoomHeader.js @@ -147,7 +147,6 @@ module.exports = React.createClass({ render: function() { const RoomAvatar = sdk.getComponent("avatars.RoomAvatar"); - const EmojiText = sdk.getComponent('elements.EmojiText'); let searchStatus = null; let cancelButton = null; @@ -191,10 +190,10 @@ module.exports = React.createClass({ roomName = this.props.room.name; } - const emojiTextClasses = classNames('mx_RoomHeader_nametext', { mx_RoomHeader_settingsHint: settingsHint }); + const textClasses = classNames('mx_RoomHeader_nametext', { mx_RoomHeader_settingsHint: settingsHint }); const name =
    - { roomName } +
    { roomName }
    { searchStatus }
    ; diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index 33b97964f6..ef7d0ed5fb 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -750,6 +750,7 @@ module.exports = React.createClass({ order: "recent", incomingCall: incomingCallIfTaggedAs('im.vector.fake.direct'), onAddRoom: () => {dis.dispatch({action: 'view_create_chat'})}, + addRoomLabel: _t("Start chat"), }, { list: this.state.lists['im.vector.fake.recent'], diff --git a/src/components/views/rooms/RoomTile.js b/src/components/views/rooms/RoomTile.js index 93b4a59fca..1c98b97559 100644 --- a/src/components/views/rooms/RoomTile.js +++ b/src/components/views/rooms/RoomTile.js @@ -342,7 +342,6 @@ module.exports = React.createClass({ badge =
    { badgeContent }
    ; } - const EmojiText = sdk.getComponent('elements.EmojiText'); let label; let subtextLabel; let tooltip; @@ -354,14 +353,7 @@ module.exports = React.createClass({ }); subtextLabel = subtext ? { subtext } : null; - - if (this.state.selected) { - const nameSelected = { name }; - - label =
    { nameSelected }
    ; - } else { - label = { name }; - } + label =
    { name }
    ; } else if (this.state.hover) { const Tooltip = sdk.getComponent("elements.Tooltip"); tooltip = ; diff --git a/src/components/views/rooms/WhoIsTypingTile.js b/src/components/views/rooms/WhoIsTypingTile.js index eb5e14876d..c639b205d8 100644 --- a/src/components/views/rooms/WhoIsTypingTile.js +++ b/src/components/views/rooms/WhoIsTypingTile.js @@ -17,7 +17,6 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; -import sdk from '../../../index'; import WhoIsTyping from '../../../WhoIsTyping'; import Timer from '../../../utils/Timer'; import MatrixClientPeg from '../../../MatrixClientPeg'; @@ -212,15 +211,13 @@ module.exports = React.createClass({ return (
    ); } - const EmojiText = sdk.getComponent('elements.EmojiText'); - return (
  • { this._renderTypingIndicatorAvatars(usersTyping, this.props.whoIsTypingLimit) }
    - { typingString } + { typingString }
  • ); diff --git a/src/components/views/settings/KeyBackupPanel.js b/src/components/views/settings/KeyBackupPanel.js index 2ba05a0e6b..ec1e52a90c 100644 --- a/src/components/views/settings/KeyBackupPanel.js +++ b/src/components/views/settings/KeyBackupPanel.js @@ -174,14 +174,13 @@ export default class KeyBackupPanel extends React.PureComponent { } else if (this.state.loading) { return ; } else if (this.state.backupInfo) { - const EmojiText = sdk.getComponent('elements.EmojiText'); let clientBackupStatus; let restoreButtonCaption = _t("Restore from Backup"); if (MatrixClientPeg.get().getKeyBackupEnabled()) { clientBackupStatus =

    {encryptedMessageAreEncrypted}

    -

    {_t("This device is backing up your keys. ")}

    +

    ✅ {_t("This device is backing up your keys. ")}

    ; } else { clientBackupStatus =
    diff --git a/src/components/views/settings/tabs/user/LabsUserSettingsTab.js b/src/components/views/settings/tabs/user/LabsUserSettingsTab.js index c2e62044a3..23b5e516cd 100644 --- a/src/components/views/settings/tabs/user/LabsUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/LabsUserSettingsTab.js @@ -52,6 +52,8 @@ export default class LabsUserSettingsTab extends React.Component {
    {flags} + +
    ); diff --git a/src/components/views/verification/VerificationShowSas.js b/src/components/views/verification/VerificationShowSas.js index a2531800e5..e7846a0199 100644 --- a/src/components/views/verification/VerificationShowSas.js +++ b/src/components/views/verification/VerificationShowSas.js @@ -36,7 +36,6 @@ export default class VerificationShowSas extends React.Component { render() { const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); - const EmojiText = sdk.getComponent('views.elements.EmojiText'); let sasDisplay; let sasCaption; @@ -44,7 +43,7 @@ export default class VerificationShowSas extends React.Component { const emojiBlocks = this.props.sas.emoji.map( (emoji, i) =>
    - {emoji[0]} + { emoji[0] }
    {_t(capFirst(emoji[1]))} diff --git a/src/editor/autocomplete.js b/src/editor/autocomplete.js index d2f73b1dff..ceaf18c444 100644 --- a/src/editor/autocomplete.js +++ b/src/editor/autocomplete.js @@ -1,5 +1,6 @@ /* Copyright 2019 New Vector Ltd +Copyright 2019 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -17,11 +18,12 @@ limitations under the License. import {UserPillPart, RoomPillPart, PlainPart} from "./parts"; export default class AutocompleteWrapperModel { - constructor(updateCallback, getAutocompleterComponent, updateQuery) { + constructor(updateCallback, getAutocompleterComponent, updateQuery, room) { this._updateCallback = updateCallback; this._getAutocompleterComponent = getAutocompleterComponent; this._updateQuery = updateQuery; this._query = null; + this._room = room; } onEscape(e) { @@ -37,8 +39,24 @@ export default class AutocompleteWrapperModel { this._updateCallback({close: true}); } - onTab() { - //forceCompletion here? + async onTab(e) { + const acComponent = this._getAutocompleterComponent(); + + if (acComponent.state.completionList.length === 0) { + // Force completions to show for the text currently entered + await acComponent.forceComplete(); + // Select the first item by moving "down" + await acComponent.onDownArrow(); + } else { + if (e.shiftKey) { + await acComponent.onUpArrow(); + } else { + await acComponent.onDownArrow(); + } + } + this._updateCallback({ + close: true, + }); } onUpArrow() { @@ -83,7 +101,8 @@ export default class AutocompleteWrapperModel { case "@": { const displayName = completion.completion; const userId = completion.completionId; - return new UserPillPart(userId, displayName); + const member = this._room.getMember(userId); + return new UserPillPart(userId, displayName, member); } case "#": { const displayAlias = completion.completionId; diff --git a/src/editor/caret.js b/src/editor/caret.js index 3a784aa8eb..f93e9604d5 100644 --- a/src/editor/caret.js +++ b/src/editor/caret.js @@ -1,5 +1,6 @@ /* Copyright 2019 New Vector Ltd +Copyright 2019 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -19,13 +20,23 @@ export function setCaretPosition(editor, model, caretPosition) { sel.removeAllRanges(); const range = document.createRange(); const {parts} = model; + const {index} = caretPosition; + let {offset} = caretPosition; let lineIndex = 0; let nodeIndex = -1; - for (let i = 0; i <= caretPosition.index; ++i) { + for (let i = 0; i <= index; ++i) { const part = parts[i]; if (part && part.type === "newline") { - lineIndex += 1; - nodeIndex = -1; + if (i < index) { + lineIndex += 1; + nodeIndex = -1; + } else { + // if index points at a newline part, + // put the caret at the end of the previous part + // so it stays on the same line + const prevPart = parts[i - 1]; + offset = prevPart ? prevPart.text.length : 0; + } } else { nodeIndex += 1; } @@ -33,14 +44,11 @@ export function setCaretPosition(editor, model, caretPosition) { let focusNode; const lineNode = editor.childNodes[lineIndex]; if (lineNode) { - if (lineNode.childNodes.length === 0 && caretPosition.offset === 0) { + focusNode = lineNode.childNodes[nodeIndex]; + if (!focusNode) { focusNode = lineNode; - } else { - focusNode = lineNode.childNodes[nodeIndex]; - - if (focusNode && focusNode.nodeType === Node.ELEMENT_NODE) { - focusNode = focusNode.childNodes[0]; - } + } else if (focusNode.nodeType === Node.ELEMENT_NODE) { + focusNode = focusNode.childNodes[0]; } } // node not found, set caret at end @@ -49,7 +57,7 @@ export function setCaretPosition(editor, model, caretPosition) { range.collapse(false); } else { // make sure we have a text node - range.setStart(focusNode, caretPosition.offset); + range.setStart(focusNode, offset); range.collapse(true); } sel.addRange(range); diff --git a/src/editor/deserialize.js b/src/editor/deserialize.js index a7f28badb1..64b219c2a9 100644 --- a/src/editor/deserialize.js +++ b/src/editor/deserialize.js @@ -1,5 +1,6 @@ /* Copyright 2019 New Vector Ltd +Copyright 2019 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -16,48 +17,198 @@ limitations under the License. import { MATRIXTO_URL_PATTERN } from '../linkify-matrix'; import { PlainPart, UserPillPart, RoomPillPart, NewlinePart } from "./parts"; +import { walkDOMDepthFirst } from "./dom"; -function parseHtmlMessage(html) { - const REGEX_MATRIXTO = new RegExp(MATRIXTO_URL_PATTERN); - // no nodes from parsing here should be inserted in the document, - // as scripts in event handlers, etc would be executed then. - // we're only taking text, so that is fine - const nodes = Array.from(new DOMParser().parseFromString(html, "text/html").body.childNodes); - const parts = nodes.map(n => { - switch (n.nodeType) { - case Node.TEXT_NODE: - return new PlainPart(n.nodeValue); - case Node.ELEMENT_NODE: - switch (n.nodeName) { - case "MX-REPLY": - return null; - case "A": { - const {href} = n; - const pillMatch = REGEX_MATRIXTO.exec(href) || []; - const resourceId = pillMatch[1]; // The room/user ID - const prefix = pillMatch[2]; // The first character of prefix - switch (prefix) { - case "@": return new UserPillPart(resourceId, n.textContent); - case "#": return new RoomPillPart(resourceId, n.textContent); - default: return new PlainPart(n.textContent); - } - } - case "BR": - return new NewlinePart("\n"); - default: - return new PlainPart(n.textContent); - } - default: - return null; +const REGEX_MATRIXTO = new RegExp(MATRIXTO_URL_PATTERN); + +function parseLink(a, room) { + const {href} = a; + const pillMatch = REGEX_MATRIXTO.exec(href) || []; + const resourceId = pillMatch[1]; // The room/user ID + const prefix = pillMatch[2]; // The first character of prefix + switch (prefix) { + case "@": + return new UserPillPart( + resourceId, + a.textContent, + room.getMember(resourceId), + ); + case "#": + return new RoomPillPart(resourceId); + default: { + if (href === a.textContent) { + return new PlainPart(a.textContent); + } else { + return new PlainPart(`[${a.textContent}](${href})`); + } } - }).filter(p => !!p); + } +} + +function parseCodeBlock(n) { + const parts = []; + const preLines = ("```\n" + n.textContent + "```").split("\n"); + preLines.forEach((l, i) => { + parts.push(new PlainPart(l)); + if (i < preLines.length - 1) { + parts.push(new NewlinePart("\n")); + } + }); return parts; } -export function parseEvent(event) { +function parseElement(n, room) { + switch (n.nodeName) { + case "A": + return parseLink(n, room); + case "BR": + return new NewlinePart("\n"); + case "EM": + return new PlainPart(`*${n.textContent}*`); + case "STRONG": + return new PlainPart(`**${n.textContent}**`); + case "PRE": + return parseCodeBlock(n); + case "CODE": + return new PlainPart(`\`${n.textContent}\``); + case "DEL": + return new PlainPart(`${n.textContent}`); + case "LI": + if (n.parentElement.nodeName === "OL") { + return new PlainPart(` 1. `); + } else { + return new PlainPart(` - `); + } + default: + // don't textify block nodes we'll decend into + if (!checkDecendInto(n)) { + return new PlainPart(n.textContent); + } + } +} + +function checkDecendInto(node) { + switch (node.nodeName) { + case "PRE": + // a code block is textified in parseCodeBlock + // as we don't want to preserve markup in it, + // so no need to decend into it + return false; + default: + return checkBlockNode(node); + } +} + +function checkBlockNode(node) { + switch (node.nodeName) { + case "PRE": + case "BLOCKQUOTE": + case "DIV": + case "P": + case "UL": + case "OL": + case "LI": + return true; + default: + return false; + } +} + +function checkIgnored(n) { + if (n.nodeType === Node.TEXT_NODE) { + // riot adds \n text nodes in a lot of places, + // which should be ignored + return n.nodeValue === "\n"; + } else if (n.nodeType === Node.ELEMENT_NODE) { + return n.nodeName === "MX-REPLY"; + } + return true; +} + +function prefixQuoteLines(isFirstNode, parts) { + const PREFIX = "> "; + // a newline (to append a > to) wouldn't be added to parts for the first line + // if there was no content before the BLOCKQUOTE, so handle that + if (isFirstNode) { + parts.splice(0, 0, new PlainPart(PREFIX)); + } + for (let i = 0; i < parts.length; i += 1) { + if (parts[i].type === "newline") { + parts.splice(i + 1, 0, new PlainPart(PREFIX)); + i += 1; + } + } +} + +function parseHtmlMessage(html, room) { + // no nodes from parsing here should be inserted in the document, + // as scripts in event handlers, etc would be executed then. + // we're only taking text, so that is fine + const rootNode = new DOMParser().parseFromString(html, "text/html").body; + const parts = []; + let lastNode; + let inQuote = false; + + function onNodeEnter(n) { + if (checkIgnored(n)) { + return false; + } + if (n.nodeName === "BLOCKQUOTE") { + inQuote = true; + } + + const newParts = []; + if (lastNode && (checkBlockNode(lastNode) || checkBlockNode(n))) { + newParts.push(new NewlinePart("\n")); + } + + if (n.nodeType === Node.TEXT_NODE) { + newParts.push(new PlainPart(n.nodeValue)); + } else if (n.nodeType === Node.ELEMENT_NODE) { + const parseResult = parseElement(n, room); + if (parseResult) { + if (Array.isArray(parseResult)) { + newParts.push(...parseResult); + } else { + newParts.push(parseResult); + } + } + } + + if (newParts.length && inQuote) { + const isFirstPart = parts.length === 0; + prefixQuoteLines(isFirstPart, newParts); + } + + parts.push(...newParts); + + // extra newline after quote, only if there something behind it... + if (lastNode && lastNode.nodeName === "BLOCKQUOTE") { + parts.push(new NewlinePart("\n")); + } + lastNode = null; + return checkDecendInto(n); + } + + function onNodeLeave(n) { + if (checkIgnored(n)) { + return; + } + if (n.nodeName === "BLOCKQUOTE") { + inQuote = false; + } + lastNode = n; + } + + walkDOMDepthFirst(rootNode, onNodeEnter, onNodeLeave); + + return parts; +} + +export function parseEvent(event, room) { const content = event.getContent(); if (content.format === "org.matrix.custom.html") { - return parseHtmlMessage(content.formatted_body || ""); + return parseHtmlMessage(content.formatted_body || "", room); } else { const body = content.body || ""; const lines = body.split("\n"); diff --git a/src/editor/diff.js b/src/editor/diff.js index 6dc8b746e4..2c82e22793 100644 --- a/src/editor/diff.js +++ b/src/editor/diff.js @@ -1,5 +1,6 @@ /* Copyright 2019 New Vector Ltd +Copyright 2019 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/editor/dom.js b/src/editor/dom.js index 0899fd25b3..3ef1df24c3 100644 --- a/src/editor/dom.js +++ b/src/editor/dom.js @@ -1,5 +1,6 @@ /* Copyright 2019 New Vector Ltd +Copyright 2019 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,22 +15,22 @@ See the License for the specific language governing permissions and limitations under the License. */ -function walkDOMDepthFirst(editor, enterNodeCallback, leaveNodeCallback) { - let node = editor.firstChild; - while (node && node !== editor) { - enterNodeCallback(node); - if (node.firstChild) { +export function walkDOMDepthFirst(rootNode, enterNodeCallback, leaveNodeCallback) { + let node = rootNode.firstChild; + while (node && node !== rootNode) { + const shouldDecend = enterNodeCallback(node); + if (shouldDecend && node.firstChild) { node = node.firstChild; } else if (node.nextSibling) { node = node.nextSibling; } else { - while (!node.nextSibling && node !== editor) { + while (!node.nextSibling && node !== rootNode) { node = node.parentElement; - if (node !== editor) { + if (node !== rootNode) { leaveNodeCallback(node); } } - if (node !== editor) { + if (node !== rootNode) { node = node.nextSibling; } } @@ -61,6 +62,7 @@ export function getCaretOffsetAndText(editor, sel) { } text += nodeText; } + return true; } function leaveNodeCallback(node) { diff --git a/src/editor/model.js b/src/editor/model.js index 85dd425b0e..fb6b417530 100644 --- a/src/editor/model.js +++ b/src/editor/model.js @@ -1,5 +1,6 @@ /* Copyright 2019 New Vector Ltd +Copyright 2019 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -61,6 +62,16 @@ export default class EditorModel { return null; } + getPositionAtEnd() { + if (this._parts.length) { + const index = this._parts.length - 1; + const part = this._parts[index]; + return new DocumentPosition(index, part.text.length); + } else { + return new DocumentPosition(0, 0); + } + } + serializeParts() { return this._parts.map(({type, text}) => {return {type, text};}); } @@ -88,7 +99,8 @@ export default class EditorModel { } this._mergeAdjacentParts(); const caretOffset = diff.at - removedOffsetDecrease + addedLen; - const newPosition = this._positionForOffset(caretOffset, true); + let newPosition = this._positionForOffset(caretOffset, true); + newPosition = newPosition.skipUneditableParts(this._parts); this._setActivePart(newPosition); this._updateCallback(newPosition); } @@ -172,21 +184,26 @@ export default class EditorModel { // part might be undefined here let part = this._parts[index]; const amount = Math.min(len, part.text.length - offset); - if (part.canEdit) { - const replaceWith = part.remove(offset, amount); - if (typeof replaceWith === "string") { - this._replacePart(index, this._partCreator.createDefaultPart(replaceWith)); - } - part = this._parts[index]; - // remove empty part - if (!part.text.length) { - this._removePart(index); + // don't allow 0 amount deletions + if (amount) { + if (part.canEdit) { + const replaceWith = part.remove(offset, amount); + if (typeof replaceWith === "string") { + this._replacePart(index, this._partCreator.createDefaultPart(replaceWith)); + } + part = this._parts[index]; + // remove empty part + if (!part.text.length) { + this._removePart(index); + } else { + index += 1; + } } else { - index += 1; + removedOffsetDecrease += offset; + this._removePart(index); } } else { - removedOffsetDecrease += offset; - this._removePart(index); + index += 1; } len -= amount; offset = 0; @@ -215,8 +232,9 @@ export default class EditorModel { index += 1; this._insertPart(index, splitPart); } - } else { - // not-editable, insert str after this part + } else if (offset !== 0) { + // not-editable part, caret is not at start, + // so insert str after this part addLen += part.text.length - offset; index += 1; } @@ -261,4 +279,13 @@ class DocumentPosition { get offset() { return this._offset; } + + skipUneditableParts(parts) { + const part = parts[this.index]; + if (part && !part.canEdit) { + return new DocumentPosition(this.index + 1, 0); + } else { + return this; + } + } } diff --git a/src/editor/parts.js b/src/editor/parts.js index a20b857fee..be3080db12 100644 --- a/src/editor/parts.js +++ b/src/editor/parts.js @@ -1,5 +1,6 @@ /* Copyright 2019 New Vector Ltd +Copyright 2019 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -15,6 +16,8 @@ limitations under the License. */ import AutocompleteWrapperModel from "./autocomplete"; +import Avatar from "../Avatar"; +import MatrixClientPeg from "../MatrixClientPeg"; class BasePart { constructor(text = "") { @@ -57,7 +60,7 @@ class BasePart { appendUntilRejected(str) { for (let i = 0; i < str.length; ++i) { const chr = str.charAt(i); - if (!this.acceptsInsertion(chr)) { + if (!this.acceptsInsertion(chr, i)) { this._text = this._text + str.substr(0, i); return str.substr(i); } @@ -150,21 +153,21 @@ class PillPart extends BasePart { toDOMNode() { const container = document.createElement("span"); - container.className = this.type; + container.className = this.className; container.appendChild(document.createTextNode(this.text)); + this.setAvatar(container); return container; } updateDOMNode(node) { const textNode = node.childNodes[0]; if (textNode.textContent !== this.text) { - // console.log("changing pill text from", textNode.textContent, "to", this.text); textNode.textContent = this.text; } - if (node.className !== this.type) { - // console.log("turning", node.className, "into", this.type); - node.className = this.type; + if (node.className !== this.className) { + node.className = this.className; } + this.setAvatar(node); } canUpdateDOMNode(node) { @@ -174,14 +177,28 @@ class PillPart extends BasePart { node.childNodes[0].nodeType === Node.TEXT_NODE; } + // helper method for subclasses + _setAvatarVars(node, avatarUrl, initialLetter) { + const avatarBackground = `url('${avatarUrl}')`; + const avatarLetter = `'${initialLetter}'`; + // check if the value is changing, + // otherwise the avatars flicker on every keystroke while updating. + if (node.style.getPropertyValue("--avatar-background") !== avatarBackground) { + node.style.setProperty("--avatar-background", avatarBackground); + } + if (node.style.getPropertyValue("--avatar-letter") !== avatarLetter) { + node.style.setProperty("--avatar-letter", avatarLetter); + } + } + get canEdit() { return false; } } export class NewlinePart extends BasePart { - acceptsInsertion(chr) { - return this.text.length === 0 && chr === "\n"; + acceptsInsertion(chr, i) { + return (this.text.length + i) === 0 && chr === "\n"; } acceptsRemoval(position, chr) { @@ -205,22 +222,84 @@ export class NewlinePart extends BasePart { get type() { return "newline"; } + + // this makes the cursor skip this part when it is inserted + // rather than trying to append to it, which is what we want. + // As a newline can also be only one character, it makes sense + // as it can only be one character long. This caused #9741. + get canEdit() { + return false; + } } export class RoomPillPart extends PillPart { constructor(displayAlias) { super(displayAlias, displayAlias); + this._room = this._findRoomByAlias(displayAlias); + } + + _findRoomByAlias(alias) { + const client = MatrixClientPeg.get(); + if (alias[0] === '#') { + return client.getRooms().find((r) => { + return r.getAliases().includes(alias); + }); + } else { + return client.getRoom(alias); + } + } + + setAvatar(node) { + let initialLetter = ""; + let avatarUrl = Avatar.avatarUrlForRoom(this._room, 16 * window.devicePixelRatio, 16 * window.devicePixelRatio); + if (!avatarUrl) { + initialLetter = Avatar.getInitialLetter(this._room.name); + avatarUrl = `../../${Avatar.defaultAvatarUrlForString(this._room.roomId)}`; + } + this._setAvatarVars(node, avatarUrl, initialLetter); } get type() { return "room-pill"; } + + get className() { + return "mx_RoomPill mx_Pill"; + } } export class UserPillPart extends PillPart { + constructor(userId, displayName, member) { + super(userId, displayName); + this._member = member; + } + + setAvatar(node) { + const name = this._member.name || this._member.userId; + const defaultAvatarUrl = Avatar.defaultAvatarUrlForString(this._member.userId); + let avatarUrl = Avatar.avatarUrlForMember( + this._member, + 16 * window.devicePixelRatio, + 16 * window.devicePixelRatio); + let initialLetter = ""; + if (avatarUrl === defaultAvatarUrl) { + // the url from defaultAvatarUrlForString is meant to go in an img element, + // which has the base of the document. we're using it in css, + // which has the base of the theme css file, two levels deeper than the document, + // so go up to the level of the document. + avatarUrl = `../../${avatarUrl}`; + initialLetter = Avatar.getInitialLetter(name); + } + this._setAvatarVars(node, avatarUrl, initialLetter); + } + get type() { return "user-pill"; } + + get className() { + return "mx_UserPill mx_Pill"; + } } @@ -234,8 +313,16 @@ export class PillCandidatePart extends PlainPart { return this._autoCompleteCreator(updateCallback); } - acceptsInsertion(chr) { - return true; + acceptsInsertion(chr, i) { + if ((this.text.length + i) === 0) { + return true; + } else { + return super.acceptsInsertion(chr, i); + } + } + + merge() { + return false; } acceptsRemoval(position, chr) { @@ -248,9 +335,14 @@ export class PillCandidatePart extends PlainPart { } export class PartCreator { - constructor(getAutocompleterComponent, updateQuery) { + constructor(getAutocompleterComponent, updateQuery, room) { this._autoCompleteCreator = (updateCallback) => { - return new AutocompleteWrapperModel(updateCallback, getAutocompleterComponent, updateQuery); + return new AutocompleteWrapperModel( + updateCallback, + getAutocompleterComponent, + updateQuery, + room, + ); }; } diff --git a/src/editor/render.js b/src/editor/render.js index abc5d42fa1..58ef0eaee1 100644 --- a/src/editor/render.js +++ b/src/editor/render.js @@ -1,5 +1,6 @@ /* Copyright 2019 New Vector Ltd +Copyright 2019 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -60,12 +61,13 @@ export function renderModel(editor, model) { let foundBR = false; let partNode = lineContainer.firstChild; while (partNode) { + const nextNode = partNode.nextSibling; if (!foundBR && partNode.tagName === "BR") { foundBR = true; } else { lineContainer.removeChild(partNode); } - partNode = partNode.nextSibling; + partNode = nextNode; } if (!foundBR) { lineContainer.appendChild(document.createElement("br")); diff --git a/src/editor/serialize.js b/src/editor/serialize.js index 57cc79b375..73fbbe5d01 100644 --- a/src/editor/serialize.js +++ b/src/editor/serialize.js @@ -1,18 +1,45 @@ -export function htmlSerialize(model) { +/* +Copyright 2019 New Vector Ltd +Copyright 2019 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +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 Markdown from '../Markdown'; + +export function mdSerialize(model) { return model.parts.reduce((html, part) => { switch (part.type) { case "newline": - return html + "
    "; + return html + "\n"; case "plain": case "pill-candidate": return html + part.text; case "room-pill": case "user-pill": - return html + `${part.text}`; + return html + `[${part.text}](https://matrix.to/#/${part.resourceId})`; } }, ""); } +export function htmlSerializeIfNeeded(model) { + const md = mdSerialize(model); + const parser = new Markdown(md); + if (!parser.isPlainText()) { + return parser.toHTML(); + } +} + export function textSerialize(model) { return model.parts.reduce((text, part) => { switch (part.type) { diff --git a/src/i18n/strings/bg.json b/src/i18n/strings/bg.json index aec953b40b..4e273c27e8 100644 --- a/src/i18n/strings/bg.json +++ b/src/i18n/strings/bg.json @@ -1821,5 +1821,67 @@ "Want more than a community? Get your own server": "Искате повече от общност? Сдобийте се със собствен сървър", "You are logged in to another account": "Влезли сте в друг акаунт", "Thank you for verifying your email! The account you're logged into here (%(sessionUserId)s) appears to be different from the account you've verified an email for (%(verifiedUserId)s). If you would like to log in to %(verifiedUserId2)s, please log out first.": "Благодарим, че потвърждавате имейла си! Акаунтът, с които сте влезли тук (%(sessionUserId)s) изглежда е различен от акаунтът за който потвърждавате имейл адреса (%(verifiedUserId)s). Ако искате да влезете в акаунт %(verifiedUserId2)s, моля първо излезте.", - "Changing your password will reset any end-to-end encryption keys on all of your devices, making encrypted chat history unreadable. Set up Key Backup or export your room keys from another device before resetting your password.": "Промяната на паролата Ви, ще анулира всички ключове за шифроване от-край-до-край по всички Ваши устройства, правейки историята на чата нечетима. Настройте резервно копие на ключовете или експортирайте ключовете от друго устройство преди да промените паролата си." + "Changing your password will reset any end-to-end encryption keys on all of your devices, making encrypted chat history unreadable. Set up Key Backup or export your room keys from another device before resetting your password.": "Промяната на паролата Ви, ще анулира всички ключове за шифроване от-край-до-край по всички Ваши устройства, правейки историята на чата нечетима. Настройте резервно копие на ключовете или експортирайте ключовете от друго устройство преди да промените паролата си.", + "Whether or not you're using the 'breadcrumbs' feature (avatars above the room list)": "Дали използвате 'breadcrumbs' функцията (аватари над списъка със стаи)", + "Replying With Files": "Отговаряне с файлове", + "At this time it is not possible to reply with a file. Would you like to upload this file without replying?": "Все още не е възможно да отговорите с файл. Искате ли да качите файла без той да бъде отговор?", + "The file '%(fileName)s' failed to upload.": "Файлът '%(fileName)s' не можа да бъде качен.", + "Room upgrade confirmation": "Потвърждение на обновяването на стаята", + "Upgrading a room can be destructive and isn't always necessary.": "Обновяването на стаята може да бъде деструктивно и не винаги е задължително.", + "Room upgrades are usually recommended when a room version is considered unstable. Unstable room versions might have bugs, missing features, or security vulnerabilities.": "Обновяването на стаи обикновено се препоръчва за стаи с версии считащи се за нестабилни. Нестабилните версии може да имат бъгове, липсващи функции или проблеми със сигурността.", + "Room upgrades usually only affect server-side processing of the room. If you're having problems with your Riot client, please file an issue with .": "Обновяванията на стаи обикновено повлияват само сървърната обработка. Ако имате проблем с Riot, моля съобщете за него със .", + "Warning: Upgrading a room will not automatically migrate room members to the new version of the room. We'll post a link to the new room in the old version of the room - room members will have to click this link to join the new room.": "Внимание: Обновяването на стаята няма автоматично да прехвърли членовете в новата версия на стаята. Ще изпратим съобщение в старата стая с връзка към новата - членовете на стаята ще трябва да кликнат на връзката за да влязат в новата стая.", + "Upgrade": "Обнови", + "Adds a custom widget by URL to the room": "Добавя собствено приспособление от URL в стаята", + "Please supply a https:// or http:// widget URL": "Моля, укажете https:// или http:// адрес на приспособление", + "You cannot modify widgets in this room.": "Не можете да модифицирате приспособления в тази стая.", + "%(senderName)s revoked the invitation for %(targetDisplayName)s to join the room.": "%(senderName)s премахна покана към %(targetDisplayName)s за присъединяване в стаята.", + "Show recent room avatars above the room list": "Показвай аватари на скоро-използваните стаи над списъка със стаи", + "Enable desktop notifications for this device": "Включи известия на работния плот за това устройство", + "Enable audible notifications for this device": "Включи звукови уведомления за това устройство", + "Upgrade this room to the recommended room version": "Обнови тази стая до препоръчаната версия на стаята", + "This room is running room version , which this homeserver has marked as unstable.": "Тази стая използва версия на стая , която сървърът счита за нестабилна.", + "Upgrading this room will shut down the current instance of the room and create an upgraded room with the same name.": "Обновяването на тази стая ще изключи текущата стая и ще създаде обновена стая със същото име.", + "Failed to revoke invite": "Неуспешно оттегляне на поканата", + "Could not revoke the invite. The server may be experiencing a temporary problem or you do not have sufficient permissions to revoke the invite.": "Поканата не можа да бъде оттеглена. Или има временен проблем със сървъра, или нямате достатъчно права за да оттеглите поканата.", + "Revoke invite": "Оттегли поканата", + "Invited by %(sender)s": "Поканен от %(sender)s", + "Maximize apps": "Максимизирай приложенията", + "Rotate counter-clockwise": "Завърти обратно на часовниковата стрелка", + "Rotate clockwise": "Завърти по часовниковата стрелка", + "GitHub issue": "GitHub проблем", + "Notes": "Бележки", + "If there is additional context that would help in analysing the issue, such as what you were doing at the time, room IDs, user IDs, etc., please include those things here.": "Моля включете допълнителни сведения, които ще помогнат за анализиране на проблема, като например: какво правихте когато възникна проблема, идентификатори на стаи, идентификатори на потребители и т.н.", + "Sign out and remove encryption keys?": "Излизане и премахване на ключовете за шифроване?", + "To help us prevent this in future, please send us logs.": "За да ни помогнете да предотвратим това в бъдеще, моля изпратете логове.", + "Missing session data": "Липсват данни за сесията", + "Some session data, including encrypted message keys, is missing. Sign out and sign in to fix this, restoring keys from backup.": "Липсват данни за сесията, като например ключове за шифровани съобщения. За да поправите това, излезте и влезте отново, възстановявайки ключовете от резервно копие.", + "Your browser likely removed this data when running low on disk space.": "Най-вероятно браузърът Ви е премахнал тези данни поради липса на дисково пространство.", + "Upload files (%(current)s of %(total)s)": "Качване на файлове (%(current)s от %(total)s)", + "Upload files": "Качи файлове", + "Upload": "Качи", + "This file is too large to upload. The file size limit is %(limit)s but this file is %(sizeOfThisFile)s.": "Файлът е прекалено голям за да се качи. Максималният допустим размер е %(limit)s, докато този файл е %(sizeOfThisFile)s.", + "These files are too large to upload. The file size limit is %(limit)s.": "Тези файлове са прекалено големи за да се качат. Максималният допустим размер е %(limit)s.", + "Some files are too large to be uploaded. The file size limit is %(limit)s.": "Някои файлове са прекалено големи за да се качат. Максималният допустим размер е %(limit)s.", + "Upload %(count)s other files|other": "Качи %(count)s други файла", + "Upload %(count)s other files|one": "Качи %(count)s друг файл", + "Cancel All": "Откажи всички", + "Upload Error": "Грешка при качване", + "A widget would like to verify your identity": "Приспособление иска да потвърди идентичността Ви", + "A widget located at %(widgetUrl)s would like to verify your identity. By allowing this, the widget will be able to verify your user ID, but not perform actions as you.": "Приспособлението от адрес %(widgetUrl)s иска да потвърди идентичността Ви. Ако позволите това, приспособлението ще може да потвърди потребителския Ви идентификатор, без да може да извършва действия с него.", + "Remember my selection for this widget": "Запомни избора ми за това приспособление", + "Deny": "Откажи", + "Riot failed to get the protocol list from the homeserver. The homeserver may be too old to support third party networks.": "Riot не успя да вземе списъка с протоколи от сървъра. Този сървър може да е прекалено стар за да поддържа чужди мрежи.", + "Riot failed to get the public room list.": "Riot не успя да вземе списъка с публични стаи.", + "The homeserver may be unavailable or overloaded.": "Сървърът може да не е наличен или претоварен.", + "You have %(count)s unread notifications in a prior version of this room.|other": "Имате %(count)s непрочетени известия в предишна версия на тази стая.", + "You have %(count)s unread notifications in a prior version of this room.|one": "Имате %(count)s непрочетено известие в предишна версия на тази стая.", + "A conference call could not be started because the integrations server is not available": "Не може да бъде започнат конферентен разговор, защото сървърът за интеграции не е достъпен", + "The server does not support the room version specified.": "Сървърът не поддържа указаната версия на стая.", + "Name or Matrix ID": "Име или Matrix идентификатор", + "Email, name or Matrix ID": "Имейл, име или Matrix идентификатор", + "Please confirm that you'd like to go forward with upgrading this room from to .": "Потвърдете, че искате да обновите стаята от до .", + "Changes your avatar in this current room only": "Променя снимката Ви само в тази стая", + "Unbans user with given ID": "Премахва блокирането на потребител с даден идентификатор", + "Sends the given message coloured as a rainbow": "Изпраща текущото съобщение оцветено като дъга" } diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json index 3cfdb9c733..a21dae887a 100644 --- a/src/i18n/strings/de_DE.json +++ b/src/i18n/strings/de_DE.json @@ -1428,7 +1428,7 @@ "General failure": "Allgemeiner Fehler", "Failed to perform homeserver discovery": "Fehler beim Aufspüren des Heimservers", "Unknown failure discovering homeserver": "Unbekannter Fehler beim Aufspüren des Heimservers", - "Great! This passphrase looks strong enough.": "Gut! Diese Passphrase sieht start genug aus.", + "Great! This passphrase looks strong enough.": "Gut! Diese Passphrase sieht stark genug aus.", "Secure your encrypted message history with a Recovery Passphrase.": "Sichere deine sichere Nachrichtenhistorie mit einer Wiederherstellungspassphrase.", "If you don't want encrypted message history to be available on other devices, .": "Wenn du deine verschlüsselte Nachrichtenhistorie nicht auf anderen Geräten verfügbar haben möchtest, .", "Or, if you don't want to create a Recovery Passphrase, skip this step and .": "Oder, wenn du keine Wiederherstellungspassphrase erzeugen möchtest, überspringe diesen Schritt und .", @@ -1807,5 +1807,6 @@ "Thank you for verifying your email! The account you're logged into here (%(sessionUserId)s) appears to be different from the account you've verified an email for (%(verifiedUserId)s). If you would like to log in to %(verifiedUserId2)s, please log out first.": "Danke für das Verifizieren deiner E-Mail! Das Konto, mit dem du angemeldet bist (%(sessionUserId)s) scheint ein anderes zu sein als das wofür die die E-Mail verifizierst (%(verifiedUserId)s). Wenn du dich als %(verifiedUserId2)s anmelden willst, melde dich zuerst ab.", "Could not load user profile": "Konnte Nutzerprofil nicht laden", "Your Matrix account": "Dein Matrixkonto", - "Your Matrix account on %(serverName)s": "Dein Matrixkonto auf %(serverName)s" + "Your Matrix account on %(serverName)s": "Dein Matrixkonto auf %(serverName)s", + "Show recent room avatars above the room list": "Zeige die letzten Avatare über der Raumliste an (neu laden um Änderungen zu übernehmen)" } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 58719e216a..9341f92f75 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -35,7 +35,7 @@ "You cannot place VoIP calls in this browser.": "You cannot place VoIP calls in this browser.", "You cannot place a call with yourself.": "You cannot place a call with yourself.", "Could not connect to the integration server": "Could not connect to the integration server", - "A conference call could not be started because the intgrations server is not available": "A conference call could not be started because the intgrations server is not available", + "A conference call could not be started because the integrations server is not available": "A conference call could not be started because the integrations server is not available", "Call in Progress": "Call in Progress", "A call is currently being placed!": "A call is currently being placed!", "A call is already in progress!": "A call is already in progress!", @@ -80,7 +80,7 @@ "Who would you like to add to this community?": "Who would you like to add to this community?", "Warning: any person you add to a community will be publicly visible to anyone who knows the community ID": "Warning: any person you add to a community will be publicly visible to anyone who knows the community ID", "Invite new community members": "Invite new community members", - "Name or matrix ID": "Name or matrix ID", + "Name or Matrix ID": "Name or Matrix ID", "Invite to Community": "Invite to Community", "Which rooms would you like to add to this community?": "Which rooms would you like to add to this community?", "Show these rooms to non-members on the community page and room list?": "Show these rooms to non-members on the community page and room list?", @@ -109,7 +109,7 @@ "Admin": "Admin", "Start a chat": "Start a chat", "Who would you like to communicate with?": "Who would you like to communicate with?", - "Email, name or matrix ID": "Email, name or matrix ID", + "Email, name or Matrix ID": "Email, name or Matrix ID", "Start Chat": "Start Chat", "Invite new room members": "Invite new room members", "Who would you like to add to this room?": "Who would you like to add to this room?", @@ -157,7 +157,7 @@ "Unrecognised room alias:": "Unrecognised room alias:", "Kicks user with given id": "Kicks user with given id", "Bans user with given id": "Bans user with given id", - "Unbans user with given id": "Unbans user with given id", + "Unbans user with given ID": "Unbans user with given ID", "Ignores a user, hiding their messages from you": "Ignores a user, hiding their messages from you", "Ignored user": "Ignored user", "You are now ignoring %(userId)s": "You are now ignoring %(userId)s", @@ -249,10 +249,15 @@ "%(names)s and %(count)s others are typing …|other": "%(names)s and %(count)s others are typing …", "%(names)s and %(count)s others are typing …|one": "%(names)s and one other is typing …", "%(names)s and %(lastPerson)s are typing …": "%(names)s and %(lastPerson)s are typing …", + "No homeserver URL provided": "No homeserver URL provided", + "Unexpected error resolving homeserver configuration": "Unexpected error resolving homeserver configuration", "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.", "Unable to connect to Homeserver. Retrying...": "Unable to connect to Homeserver. Retrying...", + "%(items)s and %(count)s others|other": "%(items)s and %(count)s others", + "%(items)s and %(count)s others|one": "%(items)s and one other", + "%(items)s and %(lastItem)s": "%(items)s and %(lastItem)s", "Your browser does not support the required cryptography extensions": "Your browser does not support the required cryptography extensions", "Not a valid Riot keyfile": "Not a valid Riot keyfile", "Authentication check failed: incorrect password?": "Authentication check failed: incorrect password?", @@ -334,6 +339,8 @@ "Prompt before sending invites to potentially invalid matrix IDs": "Prompt before sending invites to potentially invalid matrix IDs", "Show developer tools": "Show developer tools", "Order rooms in the room list by most important first instead of most recent": "Order rooms in the room list by most important first instead of most recent", + "Show hidden events in timeline": "Show hidden events in timeline", + "Low bandwidth mode": "Low bandwidth mode", "Collecting app version information": "Collecting app version information", "Collecting logs": "Collecting logs", "Uploading report": "Uploading report", @@ -800,6 +807,7 @@ "Invites": "Invites", "Favourites": "Favourites", "People": "People", + "Start chat": "Start chat", "Rooms": "Rooms", "Low priority": "Low priority", "Historical": "Historical", @@ -914,6 +922,7 @@ "Invalid file%(extra)s": "Invalid file%(extra)s", "Error decrypting image": "Error decrypting image", "Error decrypting video": "Error decrypting video", + "reacted with %(shortName)s": "reacted with %(shortName)s", "%(senderDisplayName)s changed the avatar for %(roomName)s": "%(senderDisplayName)s changed the avatar for %(roomName)s", "%(senderDisplayName)s removed the room avatar.": "%(senderDisplayName)s removed the room avatar.", "%(senderDisplayName)s changed the room avatar to ": "%(senderDisplayName)s changed the room avatar to ", @@ -923,6 +932,8 @@ "Failed to copy": "Failed to copy", "Add an Integration": "Add an Integration", "You are about to be taken to a third-party site so you can authenticate your account for use with %(integrationsUrl)s. Do you wish to continue?": "You are about to be taken to a third-party site so you can authenticate your account for use with %(integrationsUrl)s. Do you wish to continue?", + "Edited at %(date)s": "Edited at %(date)s", + "edited": "edited", "Removed or unknown message type": "Removed or unknown message type", "Message removed by %(userId)s": "Message removed by %(userId)s", "Message removed": "Message removed", @@ -993,7 +1004,9 @@ "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", "Rotate counter-clockwise": "Rotate counter-clockwise", + "Rotate Right": "Rotate Right", "Rotate clockwise": "Rotate clockwise", "Download this file": "Download this file", "Integrations Error": "Integrations Error", @@ -1047,17 +1060,14 @@ "%(severalUsers)schanged their avatar %(count)s times|one": "%(severalUsers)schanged their avatar", "%(oneUser)schanged their avatar %(count)s times|other": "%(oneUser)schanged their avatar %(count)s times", "%(oneUser)schanged their avatar %(count)s times|one": "%(oneUser)schanged their avatar", - "%(items)s and %(count)s others|other": "%(items)s and %(count)s others", - "%(items)s and %(count)s others|one": "%(items)s and one other", - "%(items)s and %(lastItem)s": "%(items)s and %(lastItem)s", "collapse": "collapse", "expand": "expand", + "Edit message": "Edit message", "Power level": "Power level", "Custom level": "Custom level", "Unable to load event that was replied to, it either does not exist or you do not have permission to view it.": "Unable to load event that was replied to, it either does not exist or you do not have permission to view it.", "In reply to ": "In reply to ", "Room directory": "Room directory", - "Start chat": "Start chat", "And %(count)s more...|other": "And %(count)s more...", "ex. @bob:example.com": "ex. @bob:example.com", "Add User": "Add User", @@ -1318,6 +1328,7 @@ "Code": "Code", "Submit": "Submit", "Start authentication": "Start authentication", + "Unable to validate homeserver/identity server": "Unable to validate homeserver/identity server", "Your Modular server": "Your Modular server", "Enter the location of your Modular homeserver. It may use your own domain name or be a subdomain of modular.im.": "Enter the location of your Modular homeserver. It may use your own domain name or be a subdomain of modular.im.", "Server Name": "Server Name", @@ -1326,12 +1337,11 @@ "The phone number field must not be blank.": "The phone number field must not be blank.", "The password field must not be blank.": "The password field must not be blank.", "Email": "Email", - "Username on %(hs)s": "Username on %(hs)s", "Username": "Username", "Phone": "Phone", "Not sure of your password? Set a new one": "Not sure of your password? Set a new one", - "Sign in to your Matrix account": "Sign in to your Matrix account", "Sign in to your Matrix account on %(serverName)s": "Sign in to your Matrix account on %(serverName)s", + "Sign in to your Matrix account on ": "Sign in to your Matrix account on ", "Change": "Change", "Sign in with": "Sign in with", "If you don't specify an email address, you won't be able to reset your password. Are you sure?": "If you don't specify an email address, you won't be able to reset your password. Are you sure?", @@ -1352,8 +1362,8 @@ "Email (optional)": "Email (optional)", "Confirm": "Confirm", "Phone (optional)": "Phone (optional)", - "Create your Matrix account": "Create your Matrix account", "Create your Matrix account on %(serverName)s": "Create your Matrix account on %(serverName)s", + "Create your Matrix account on ": "Create your Matrix account on ", "Use an email address to recover your account.": "Use an email address to recover your account.", "Other users can invite you to rooms using your contact details.": "Other users can invite you to rooms using your contact details.", "Other servers": "Other servers", @@ -1420,7 +1430,6 @@ "This homeserver does not support communities": "This homeserver does not support communities", "Failed to load %(groupId)s": "Failed to load %(groupId)s", "Filter room names": "Filter room names", - "Invalid configuration: Cannot supply a default homeserver URL and a default server name": "Invalid configuration: Cannot supply a default homeserver URL and a default server name", "Failed to reject invitation": "Failed to reject invitation", "This room is not public. You will not be able to rejoin without an invite.": "This room is not public. You will not be able to rejoin without an invite.", "Are you sure you want to leave the room '%(roomName)s'?": "Are you sure you want to leave the room '%(roomName)s'?", @@ -1436,7 +1445,6 @@ "Data from an older version of Riot has been detected. This will have caused end-to-end cryptography to malfunction in the older version. End-to-end encrypted messages exchanged recently whilst using the older version may not be decryptable in this version. This may also cause messages exchanged with this version to fail. If you experience problems, log out and back in again. To retain message history, export and re-import your keys.": "Data from an older version of Riot has been detected. This will have caused end-to-end cryptography to malfunction in the older version. End-to-end encrypted messages exchanged recently whilst using the older version may not be decryptable in this version. This may also cause messages exchanged with this version to fail. If you experience problems, log out and back in again. To retain message history, export and re-import your keys.", "You are logged in to another account": "You are logged in to another account", "Thank you for verifying your email! The account you're logged into here (%(sessionUserId)s) appears to be different from the account you've verified an email for (%(verifiedUserId)s). If you would like to log in to %(verifiedUserId2)s, please log out first.": "Thank you for verifying your email! The account you're logged into here (%(sessionUserId)s) appears to be different from the account you've verified an email for (%(verifiedUserId)s). If you would like to log in to %(verifiedUserId2)s, please log out first.", - "Unknown error discovering homeserver": "Unknown error discovering homeserver", "Logout": "Logout", "Your Communities": "Your Communities", "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!", @@ -1475,6 +1483,7 @@ "Sent messages will be stored until your connection has returned.": "Sent messages will be stored until your connection has returned.", "Active call": "Active call", "There's no one else here! Would you like to invite others or stop warning about the empty room?": "There's no one else here! Would you like to invite others or stop warning about the empty room?", + "Add room": "Add room", "You seem to be uploading files, are you sure you want to quit?": "You seem to be uploading files, are you sure you want to quit?", "You seem to be in a call, are you sure you want to quit?": "You seem to be in a call, are you sure you want to quit?", "Search failed": "Search failed", @@ -1495,6 +1504,7 @@ "Tried to load a specific point in this room's timeline, but was unable to find it.": "Tried to load a specific point in this room's timeline, but was unable to find it.", "Failed to load timeline position": "Failed to load timeline position", "Guest": "Guest", + "Your profile": "Your profile", "Uploading %(filename)s and %(count)s others|other": "Uploading %(filename)s and %(count)s others", "Uploading %(filename)s and %(count)s others|zero": "Uploading %(filename)s", "Uploading %(filename)s and %(count)s others|one": "Uploading %(filename)s and %(count)s other", @@ -1504,9 +1514,8 @@ "A new password must be entered.": "A new password must be entered.", "New passwords must match each other.": "New passwords must match each other.", "Changing your password will reset any end-to-end encryption keys on all of your devices, making encrypted chat history unreadable. Set up Key Backup or export your room keys from another device before resetting your password.": "Changing your password will reset any end-to-end encryption keys on all of your devices, making encrypted chat history unreadable. Set up Key Backup or export your room keys from another device before resetting your password.", - "Your Matrix account": "Your Matrix account", "Your Matrix account on %(serverName)s": "Your Matrix account on %(serverName)s", - "The homeserver URL %(hsUrl)s doesn't seem to be valid URL. Please enter a valid URL including the protocol prefix.": "The homeserver URL %(hsUrl)s doesn't seem to be valid URL. Please enter a valid URL including the protocol prefix.", + "Your Matrix account on ": "Your Matrix account on ", "A verification email will be sent to your inbox to confirm setting your new password.": "A verification email will be sent to your inbox to confirm setting your new password.", "Send Reset Email": "Send Reset Email", "Sign in instead": "Sign in instead", @@ -1530,7 +1539,6 @@ "Please note you are logging into the %(hs)s server, not matrix.org.": "Please note you are logging into the %(hs)s server, not matrix.org.", "Failed to perform homeserver discovery": "Failed to perform homeserver discovery", "The phone number entered looks invalid": "The phone number entered looks invalid", - "Unknown failure discovering homeserver": "Unknown failure discovering homeserver", "This homeserver doesn't offer any login flows which are supported by this client.": "This homeserver doesn't offer any login flows which are supported by this client.", "Error: Problem communicating with the given homeserver.": "Error: Problem communicating with the given homeserver.", "Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. Either use HTTPS or enable unsafe scripts.": "Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. Either use HTTPS or enable unsafe scripts.", diff --git a/src/i18n/strings/en_US.json b/src/i18n/strings/en_US.json index 1c6f782db4..8829b1a421 100644 --- a/src/i18n/strings/en_US.json +++ b/src/i18n/strings/en_US.json @@ -887,5 +887,8 @@ "%(oldDisplayName)s changed their display name to %(displayName)s.": "%(oldDisplayName)s changed their display name to %(displayName)s.", "Spanner": "Wrench", "Aeroplane": "Airplane", - "Cat": "Cat" + "Cat": "Cat", + "Sends the given message coloured as a rainbow": "Sends the given message colored as a rainbow", + "Sends the given emote coloured as a rainbow": "Sends the given emote colored as a rainbow", + "Unrecognised address": "Unrecognized address" } diff --git a/src/i18n/strings/eo.json b/src/i18n/strings/eo.json index bc19f3ba06..3177d5e52a 100644 --- a/src/i18n/strings/eo.json +++ b/src/i18n/strings/eo.json @@ -1392,5 +1392,112 @@ "Okay": "Bone", "Success!": "Sukceso!", "Retry": "Reprovi", - "Set up": "Agordi" + "Set up": "Agordi", + "A conference call could not be started because the integrations server is not available": "Grupa voko ne povis komenciĝi, ĉar la kuniga servilo estas neatingebla", + "Replying With Files": "Respondado kun dosieroj", + "At this time it is not possible to reply with a file. Would you like to upload this file without replying?": "Nun ne eblas respondi kun dosiero. Ĉu vi volas alŝuti la dosieron sen respondo?", + "The file '%(fileName)s' failed to upload.": "Malsukcesis alŝuti dosieron «%(fileName)s».", + "The server does not support the room version specified.": "La servilo ne subtenas la donitan ĉambran version.", + "Name or Matrix ID": "Nomo aŭ Matrix-identigilo", + "Email, name or Matrix ID": "Retpoŝtadreso, nomo, aŭ Matrix-identigilo", + "Upgrades a room to a new version": "Gradaltigas ĉambron al nova versio", + "Room upgrade confirmation": "Konfirmo de ĉambra gradaltigo", + "Upgrading a room can be destructive and isn't always necessary.": "Gradaltigo de ĉambro povas esti detrua kaj ne estas ĉiam necesa.", + "Room upgrades are usually recommended when a room version is considered unstable. Unstable room versions might have bugs, missing features, or security vulnerabilities.": "Gradaltigoj de ĉambroj estas kutime rekomendataj kiam ĉambra versio estas opiniata malstabila. Malstabilaj ĉambraj versioj povas kunhavi erarojn, mankojn de funkcioj, aŭ malsekuraĵojn.", + "Room upgrades usually only affect server-side processing of the room. If you're having problems with your Riot client, please file an issue with .": "Ĉambraj gradaltigoj efikas nur sur servil-flanka funkciado de la ĉambro. Se vi havas problemon kun via kliento (Riot), bonvolu raparti problemon per .", + "Warning: Upgrading a room will not automatically migrate room members to the new version of the room. We'll post a link to the new room in the old version of the room - room members will have to click this link to join the new room.": "Averto: Gradaltigo de ĉambro ne transmetos ĉiujn ĉambranojn al la nova versio de la ĉambro. Ni afiŝos ligilon al la nova ĉambro en la malnova versio de la ĉambro – ĉambranoj devos tien klaki por aliĝi al la nova ĉambro.", + "Please confirm that you'd like to go forward with upgrading this room from to .": "Bonvolu konfirmi, ke vi certe volas gradaltigi ĉi tiun ĉambron de al .", + "Upgrade": "Gradaltigi", + "Changes your display nickname in the current room only": "Ŝanĝas vian vidigan nomon nur en la nuna ĉambro", + "Changes your avatar in this current room only": "Ŝanĝas vian profilbildon nur en la nuna ĉambro", + "Gets or sets the room topic": "Ekhavas aŭ agordas la temon de la ĉambro", + "This room has no topic.": "Ĉi tiu ĉambro ne havas temon.", + "Sets the room name": "Agordas nomon de la ĉambro", + "Sends the given message coloured as a rainbow": "Sendas la mesaĝon ĉielarke kolorigitan", + "Sends the given emote coloured as a rainbow": "Sendas la mienon ĉielarke kolorigitan", + "%(senderDisplayName)s upgraded this room.": "%(senderDisplayName)s gradaltigis ĉi tiun ĉambron.", + "%(senderDisplayName)s made the room public to whoever knows the link.": "%(senderDisplayName)s publikigis la ĉambron al kiu ajn konas la ligilon.", + "%(senderDisplayName)s made the room invite only.": "%(senderDisplayName)s necesigis invitojn por aliĝoj al la ĉambro.", + "%(senderDisplayName)s changed the join rule to %(rule)s": "%(senderDisplayName)s ŝanĝis la aliĝan regulon al %(rule)s", + "%(senderDisplayName)s has allowed guests to join the room.": "%(senderDisplayName)s permesis al gastoj aliĝi al la ĉambro.", + "%(senderDisplayName)s has prevented guests from joining the room.": "%(senderDisplayName)s malpermesis al gastoj aliĝi al la ĉambro.", + "Unbans user with given ID": "Malforbaras uzanton kun la donita identigilo", + "Please supply a https:// or http:// widget URL": "Bonvolu doni URL-on de fenestraĵo kun https:// aŭ http://", + "%(senderDisplayName)s changed guest access to %(rule)s": "%(senderDisplayName)s ŝanĝis aliron de gastoj al %(rule)s", + "%(displayName)s is typing …": "%(displayName)s tajpas…", + "%(names)s and %(count)s others are typing …|other": "%(names)s kaj %(count)s aliaj tajpas…", + "%(names)s and %(count)s others are typing …|one": "%(names)s kaj unu alia tajpas…", + "%(names)s and %(lastPerson)s are typing …": "%(names)s kaj %(lastPerson)s tajpas…", + "Unrecognised address": "Nerekonita adreso", + "User %(userId)s is already in the room": "Uzanto %(userId)s jam enas la ĉambron", + "User %(user_id)s may or may not exist": "Uzanto %(user_id)s eble ne ekzistas", + "The user must be unbanned before they can be invited.": "Necesas malforbari ĉi tiun uzanton antaŭ ol ĝin inviti.", + "The user's homeserver does not support the version of the room.": "Hejmservilo de ĉi tiu uzanto ne subtenas la version de la ĉambro.", + "No need for symbols, digits, or uppercase letters": "Ne necesas simboloj, cirefoj, aŭ majuskloj", + "Render simple counters in room header": "Bildigi simplajn kalkulilojn en la ĉapo de la fenestro", + "Edit messages after they have been sent (refresh to apply changes)": "Redakti mesaĝojn senditajn (aktualigu por apliki ŝanĝojn)", + "React to messages with emoji (refresh to apply changes)": "Reagi al mesaĝoj per bildsignoj (aktualigu por apliki ŝanĝojn)", + "Enable Emoji suggestions while typing": "Ŝalti proponojn de bildsignoj dum tajpado", + "Show a placeholder for removed messages": "Meti kovrilon anstataŭ forigitajn mesaĝojn", + "Show join/leave messages (invites/kicks/bans unaffected)": "Montri mesaĝojn pri aliĝo/foriro (neteme pri invitoj/forpeloj/forbaroj)", + "Show avatar changes": "Montri ŝanĝojn de profilbildoj", + "Show display name changes": "Montri ŝanĝojn de vidigaj nomoj", + "Show read receipts sent by other users": "Montri legokonfirmojn senditajn de aliaj uzantoj", + "Show avatars in user and room mentions": "Montri profilbildojn en mencioj de uzantoj kaj ĉambroj", + "Enable big emoji in chat": "Ŝalti grandajn bildsignojn en babilejo", + "Send typing notifications": "Sendi sciigojn pri tajpado", + "Allow Peer-to-Peer for 1:1 calls": "Permesi samtavolan teĥnikon por duopaj vokoj", + "Prompt before sending invites to potentially invalid matrix IDs": "Averti antaŭ ol sendi invitojn al eble nevalidaj Matrix-identigiloj", + "Order rooms in the room list by most important first instead of most recent": "Ordigi ĉambrojn en listo de ĉambroj laŭ graveco anstataŭ freŝeco", + "Messages containing my username": "Mesaĝoj kun mia salutnomo", + "When rooms are upgraded": "Kiam ĉambroj gradaltiĝas", + "The other party cancelled the verification.": "La alia kontrolano nuligis la kontrolon.", + "You've successfully verified this user.": "Vi sukcese kontrolis ĉi tiun uzanton.", + "Secure messages with this user are end-to-end encrypted and not able to be read by third parties.": "Sekuraj mesaĝoj kun ĉi tiu uzanto estas tutvoje ĉirfitaj kaj nelegeblaj al ceteruloj.", + "Verify this user by confirming the following emoji appear on their screen.": "Kontrolu ĉi tiun uzanton per konfirmo, ke la jenaj bildsignoj aperis sur ĝia ekrano.", + "Verify this user by confirming the following number appears on their screen.": "Kontrolu ĉu tiun uzanton per konfirmo, ke la jena numero aperis sur ĝia ekrano.", + "Unable to find a supported verification method.": "Ne povas trovi subtenatan metodon de kontrolo.", + "For maximum security, we recommend you do this in person or use another trusted means of communication.": "Por la plej bona sekureco, ni rekomendas fari ĉi tion persone, aŭ per alia, fidata komunikilo.", + "Santa": "Kristnaska viro", + "Thumbs up": "Dikfingro supren", + "Paperclip": "Paperkuntenilo", + "Pin": "Pinglo", + "Your homeserver does not support device management.": "Via hejmservilo ne subtenas administradon de aparatoj.", + "We've sent you an email to verify your address. Please follow the instructions there and then click the button below.": "Ni sendis al vi retleteron por konfirmi vian adreson. Bonvolu sekvi la tieajn intrukciojn kaj poste klaki al la butono sube.", + "Are you sure? You will lose your encrypted messages if your keys are not backed up properly.": "Ĉu vi certas? Vi perdos ĉiujn viajn ĉifritajn mesaĝojn, se viaj ŝlosiloj ne estas savkopiitaj.", + "Encrypted messages are secured with end-to-end encryption. Only you and the recipient(s) have the keys to read these messages.": "Ĉifritaj mesaĝoj estas sekurigitaj per tutvoja ĉifrado. Nur vi kaj la ricevonto(j) havas la ŝlosilojn necesajn por legado.", + "This device is backing up your keys. ": "Ĉi tiu aparato savkopias viajn ŝlosilojn. ", + "Custom user status messages": "Propraj uzantoaj statmesaĝoj", + "Group & filter rooms by custom tags (refresh to apply changes)": "Grupigi kaj filtri ĉambrojn per propraj etikedoj (aktualigu por ŝanĝojn apliki)", + "Restore from Backup": "Rehavi el savkopio", + "This device is not backing up your keys, but you do have an existing backup you can restore from and add to going forward.": "Tiu ĉi aparato ne ne savkopias viajn ŝlosilojn, sed vi jam havas savkopion, kiun vi povas restarigi, kaj aldonadi al ĝi de nun.", + "Backing up %(sessionsRemaining)s keys...": "Savkopianta %(sessionsRemaining)s ŝlosilojn…", + "All keys backed up": "Ĉiuj ŝlosiloj estas savkopiitaj", + "Backup has a signature from unknown device with ID %(deviceId)s.": "Savkopio havas subskribon de nekonata aparato kun la identigilo %(deviceId)s.", + "Backup has a valid signature from this device": "Savkopio havas validan subskribon de ĉi tiu aparato", + "Backup has an invalid signature from this device": "Savkopio havas nevalidan subskribon de tiu ĉi aparato", + "Backup has a valid signature from verified device ": "Savkopio havas validan subskribon de kontrolita aparato ", + "Backup has a valid signature from unverified device ": "Savkopio havas validan subskribon de nekontrolita aparato ", + "Backup has an invalid signature from verified device ": "Savkopio havas nevalidan subskribon de kontrolita aparato ", + "Backup has an invalid signature from unverified device ": "Savkopio havas nevalidan subskribon de nekontrolita aparato ", + "Backup is not signed by any of your devices": "Savkopio estas subskribita de neniu el viaj aparatoj", + "This backup is trusted because it has been restored on this device": "Ĉi tiu savkopio estas fidata ĉar ĝi estis restarigita por ĉi tiu aparato", + "Backup version: ": "Versio de savkopio: ", + "Algorithm: ": "Algoritmo: ", + "Your keys are not being backed up from this device.": "Viaj ŝlosiloj ne estas savkopiataj de ĉi tiu aparato.", + "Back up your keys before signing out to avoid losing them.": "Savkopiu viajn ŝlosilojn antaŭ adiaŭo, por ilin ne perdi.", + "Add an email address to configure email notifications": "Aldonu retpoŝtadreson por agordi retpoŝtajn sciigojn", + "Enable desktop notifications for this device": "Ŝalti labortablajn sciigojn por ĉi tiu aparato", + "Enable audible notifications for this device": "Ŝalti sonajn sciigojn por ĉi tiu aparato", + "Unable to verify phone number.": "Ne povas kontroli telefonnumeron.", + "Verification code": "Kontrola kodo", + "Deactivating your account is a permanent action - be careful!": "Malaktivigo de via konto estas nemalfarebla – atentu!", + "If you've submitted a bug via GitHub, debug logs can help us track down the problem. Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.": "Se vi raportis eraron per GitHub, senerariga protokolo povas helpi al ni spuri la problemon. Senerariga protokolo povas enhavi datumojn pri uzo de la aplikaĵo, inkluzive vian salutnomon, identigilojn aŭ nomojn de la vizititaj ĉambroj aŭ grupoj, kaj salutnomojn de aliaj uzantoj. Ili ne enhavas mesaĝojn.", + "Close button should minimize window to tray": "Ferma butono devas nur plejetigi la fenestron", + "Accept all %(invitedRooms)s invites": "Akcepti ĉiujn %(invitedRooms)s invitojn", + "Missing media permissions, click the button below to request.": "Mankas aŭdovidaj permesoj; klaku al la suba butono por peti.", + "Request media permissions": "Peti aŭdovidajn permesojn", + "Audio Output": "Sona eligo", + "this room": "ĉi tiu ĉambro", + "View older messages in %(roomName)s.": "Montri pli malnovajn mesaĝojn en %(roomName)s." } diff --git a/src/i18n/strings/es.json b/src/i18n/strings/es.json index c9af3bde0f..d512d91646 100644 --- a/src/i18n/strings/es.json +++ b/src/i18n/strings/es.json @@ -603,7 +603,7 @@ "Waiting for response from server": "Esperando una respuesta del servidor", "Leave": "Salir", "Uploaded on %(date)s by %(user)s": "Subido el %(date)s por %(user)s", - "Send Custom Event": "Enviar Evento Personalizado", + "Send Custom Event": "Enviar evento personalizado", "All notifications are currently disabled for all targets.": "Las notificaciones están deshabilitadas para todos los objetivos.", "Failed to send logs: ": "Error al enviar registros: ", "delete the alias.": "eliminar el alias.", @@ -669,7 +669,7 @@ "All messages (noisy)": "Todos los mensajes (ruidoso)", "Enable them now": "Habilitarlos ahora", "Messages containing my user name": "Mensajes que contienen mi nombre de usuario", - "Toolbox": "Caja de Herramientas", + "Toolbox": "Caja de herramientas", "Collecting logs": "Recolectando registros", "more": "más", "GitHub issue link:": "Enlace de incidencia en GitHub:", @@ -728,7 +728,7 @@ "Failed to change settings": "Error al cambiar los ajustes", "View Community": "Ver la comunidad", "%(count)s Members|one": "%(count)s miembro", - "Developer Tools": "Herramientas de Desarrollo", + "Developer Tools": "Herramientas de desarrollo", "View Source": "Ver Fuente", "Event Content": "Contenido del Evento", "Unable to fetch notification target list": "No se puede obtener la lista de objetivos de notificación", @@ -1285,7 +1285,7 @@ "Language and region": "Idioma y región", "You can also set a custom identity server, but you won't be able to invite users by email address, or be invited by email address yourself.": "También puedes definir un servidor de identidad personalizado, pero no podrás invitar a usuarios o ser inivitado usando direcciones de correo.", "The file '%(fileName)s' exceeds this homeserver's size limit for uploads": "El fichero %(fileName)s supera el tamaño límite del servidor para subidas", - "Unable to load! Check your network connectivity and try again.": "No es posible cargar! Comprueba tu conexión de red e inténtalo de nuevo.", + "Unable to load! Check your network connectivity and try again.": "¡No es posible cargar! Comprueba tu conexión de red e inténtalo de nuevo.", "Failed to invite users to the room:": "Fallo al invitar usuarios a la sala:", "Upgrades a room to a new version": "Actualiza una sala a una nueva versión", "%(senderDisplayName)s upgraded this room.": "%(senderDisplayName)s actualizó esta sala.", diff --git a/src/i18n/strings/eu.json b/src/i18n/strings/eu.json index 0f49dd152e..c4cb32f713 100644 --- a/src/i18n/strings/eu.json +++ b/src/i18n/strings/eu.json @@ -1824,5 +1824,6 @@ "Riot failed to get the public room list.": "Riot-ek ezin izan du du gelen zerrenda publikoa eskuratu.", "The homeserver may be unavailable or overloaded.": "Hasiera-zerbitzaria eskuraezin edo kargatuegia egon daiteke.", "You have %(count)s unread notifications in a prior version of this room.|other": "Irakurri gabeko %(count)s jakinarazpen dituzu gela honen aurreko bertsio batean.", - "You have %(count)s unread notifications in a prior version of this room.|one": "Irakurri gabeko %(count)s jakinarazpen duzu gela honen aurreko bertsio batean." + "You have %(count)s unread notifications in a prior version of this room.|one": "Irakurri gabeko %(count)s jakinarazpen duzu gela honen aurreko bertsio batean.", + "Replying With Files": "Fitxategiekin erantzutea" } diff --git a/src/i18n/strings/fi.json b/src/i18n/strings/fi.json index 76448ddcb2..e5970f70ca 100644 --- a/src/i18n/strings/fi.json +++ b/src/i18n/strings/fi.json @@ -37,7 +37,7 @@ "Default Device": "Oletuslaite", "Microphone": "Mikrofoni", "Camera": "Kamera", - "Advanced": "Kehittyneet", + "Advanced": "Edistynyt", "Algorithm": "Algoritmi", "Hide removed messages": "Piilota poistetut viestit", "Always show message timestamps": "Näytä aina viestien aikaleimat", @@ -70,7 +70,7 @@ "and %(count)s others...|one": "ja yksi muu...", "Ban": "Anna porttikielto", "Banned users": "Porttikiellon saanneet käyttäjät", - "Bans user with given id": "Antaa porttikiellon käyttäjälle jolla on annettu tunniste", + "Bans user with given id": "Antaa porttikiellon tunnuksen mukaiselle käyttäjälle", "Bulk Options": "Bulkkiasetukset", "Changes your display nickname": "Muuttaa näyttönimesi", "Changes colour scheme of current room": "Muuttaa tamänhetkisen huoneen väritystä", @@ -175,7 +175,7 @@ "Incoming call from %(name)s": "Saapuva puhelu käyttäjältä %(name)s", "Incoming video call from %(name)s": "Saapuva videopuhelu käyttäjältä %(name)s", "Incoming voice call from %(name)s": "Saapuva äänipuhelu käyttäjältä %(name)s", - "Incorrect username and/or password.": "Virheellinen käyttäjänimi ja/tai salasana.", + "Incorrect username and/or password.": "Virheellinen käyttäjätunnus ja/tai salasana.", "Incorrect verification code": "Virheellinen varmennuskoodi", "Integrations Error": "Integraatiovirhe", "Interface Language": "Käyttöliittymän kieli", @@ -185,13 +185,13 @@ "Invite new room members": "Kutsu lisää jäseniä huoneeseen", "Invited": "Kutsuttu", "Invites": "Kutsut", - "Invites user with given id to current room": "Kutsuu annetun käyttäjätunnisteen mukaisen käyttäjän huoneeseen", + "Invites user with given id to current room": "Kutsuu tunnuksen mukaisen käyttäjän huoneeseen", "Sign in with": "Tunnistus", "Join Room": "Liity huoneeseen", "Joins room with given alias": "Liittyy huoneeseen jolla on annettu alias", "Jump to first unread message.": "Hyppää ensimmäiseen lukemattomaan viestiin.", "Kick": "Poista huoneesta", - "Kicks user with given id": "Poistaa käyttäjätunnisteen mukaisen käyttäjän huoneesta", + "Kicks user with given id": "Poistaa tunnuksen mukaisen käyttäjän huoneesta", "Labs": "Laboratorio", "Last seen": "Viimeksi nähty", "Leave room": "Poistu huoneesta", @@ -217,8 +217,8 @@ "not specified": "ei määritetty", "(not supported by this browser)": "(ei tuettu tässä selaimessa)", "": "", - "AM": "AM", - "PM": "PM", + "AM": "ap.", + "PM": "ip.", "NOT verified": "EI varmennettu", "NOTE: Apps are not end-to-end encrypted": "Huom: Ohjelmat eivät käytä osapuolten välistä salausta", "No display name": "Ei näyttönimeä", @@ -328,7 +328,7 @@ "Turn Markdown off": "Ota Markdown pois käytöstä", "Turn Markdown on": "Ota Markdown käyttöön", "%(senderName)s turned on end-to-end encryption (algorithm %(algorithm)s).": "%(senderName)s otti osapuolten välisen salauksen käyttöön (algoritmi %(algorithm)s).", - "Username invalid: %(errMessage)s": "Virheellinen käyttäjänimi: %(errMessage)s", + "Username invalid: %(errMessage)s": "Käyttäjätunnus ei kelpaa: %(errMessage)s", "Users": "Käyttäjät", "Verification": "Varmennus", "verified": "varmennettu", @@ -370,13 +370,13 @@ "Your password has been reset": "Salasanasi on palautettu", "You should not yet trust it to secure data": "Sinun ei vielä kannata luottaa siihen turvataksesi dataa", "Your home server does not support device management.": "Kotipalvelimesi ei tue laitteiden hallintaa.", - "Sun": "Su", - "Mon": "Ma", - "Tue": "Ti", - "Wed": "Ke", - "Thu": "To", - "Fri": "Pe", - "Sat": "La", + "Sun": "su", + "Mon": "ma", + "Tue": "ti", + "Wed": "ke", + "Thu": "to", + "Fri": "pe", + "Sat": "la", "Set a display name:": "Aseta näyttönimi:", "This server does not support authentication with a phone number.": "Tämä palvelin ei tue autentikointia puhelinnumeron avulla.", "Missing password.": "Salasana puuttuu.", @@ -426,7 +426,7 @@ "Start new chat": "Aloita uusi keskustelu", "Failed to invite": "Kutsu epäonnistui", "Failed to invite user": "Käyttäjän kutsuminen epäonnistui", - "Failed to invite the following users to the %(roomName)s room:": "Seuraavian käyttäjien kutsuminen huoneeseen %(roomName)s epäonnistui:", + "Failed to invite the following users to the %(roomName)s room:": "Seuraavien käyttäjien kutsuminen huoneeseen %(roomName)s epäonnistui:", "Confirm Removal": "Varmista poistaminen", "Unknown error": "Tuntematon virhe", "Incorrect password": "Virheellinen salasana", @@ -469,7 +469,7 @@ "Riot does not have permission to send you notifications - please check your browser settings": "Riotilla ei ole oikeuksia lähettää sinulle ilmoituksia. Ole hyvä ja tarkista selaimen asetukset", "Riot was not given permission to send notifications - please try again": "Riot ei saannut lupaa lähettää ilmoituksia. Ole hyvä ja yritä uudelleen", "Room %(roomId)s not visible": "Huone %(roomId)s ei ole näkyvissä", - "%(roomName)s does not exist.": "%(roomName)s ei ole olemassa.", + "%(roomName)s does not exist.": "Huonetta %(roomName)s ei ole olemassa.", "%(roomName)s is not accessible at this time.": "%(roomName)s ei ole saatavilla tällä hetkellä.", "Seen by %(userName)s at %(dateTime)s": "Käyttäjän %(userName)s näkemä %(dateTime)s", "Send Reset Email": "Lähetä salasanan palautusviesti", @@ -502,7 +502,7 @@ "Usage": "Käyttö", "Use compact timeline layout": "Käytä tiivistä aikajanaa", "Use with caution": "Käytä varoen", - "User ID": "Käyttäjätunniste", + "User ID": "Käyttäjätunnus", "User Interface": "Käyttöliittymä", "User name": "Käyttäjänimi", "%(senderDisplayName)s changed the topic to \"%(topic)s\".": "%(senderDisplayName)s asetti aiheeksi \"%(topic)s\".", @@ -520,7 +520,7 @@ "Server unavailable, overloaded, or something else went wrong.": "Palvelin on saavuttamattomissa, ylikuormitettu tai jotain muuta meni vikaan.", "The email address linked to your account must be entered.": "Sinun pitää syöttää tiliisi liitetty sähköpostiosoite.", "The visibility of existing history will be unchanged": "Olemassaolevan viestihistorian näkyvyys ei muutu", - "To get started, please pick a username!": "Valitse käyttäjänimi aloittaaksesi!", + "To get started, please pick a username!": "Aloita valitsemalla käyttäjätunnus!", "To use it, just wait for autocomplete results to load and tab through them.": "Käyttääksesi sitä odota vain automaattitäydennyksiä ja selaa niiden läpi.", "To reset your password, enter the email address linked to your account": "Syötä tiliisi liitetty sähköpostiosoite uudelleenalustaaksesi salasanasi", "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.": "Aikajanan tietty hetki yritettiin ladata, mutta sinulla ei ole oikeutta nähdä kyseistä viestiä.", @@ -538,18 +538,18 @@ "Your password was successfully changed. You will not receive push notifications on other devices until you log back in to them": "Salasanan muuttaminen onnistui. Et saa push-ilmoituksia muilla laitteilla ennen kuin kirjaudut niihin sisään", "You seem to be in a call, are you sure you want to quit?": "Sinulla näyttää olevan puhelu kesken. Haluatko varmasti lopettaa?", "You seem to be uploading files, are you sure you want to quit?": "Näytät lataavan tiedostoja. Oletko varma että haluat lopettaa?", - "Jan": "tammikuu", - "Feb": "helmikuu", - "Mar": "maaliskuu", - "Apr": "huhtikuu", - "May": "toukokuu", - "Jun": "kesäkuu", - "Jul": "heinäkuu", - "Aug": "elokuu", - "Sep": "syyskuu", - "Oct": "lokakuu", - "Nov": "marraskuu", - "Dec": "joulukuu", + "Jan": "tammi", + "Feb": "helmi", + "Mar": "maalis", + "Apr": "huhti", + "May": "touko", + "Jun": "kesä", + "Jul": "heinä", + "Aug": "elo", + "Sep": "syys", + "Oct": "loka", + "Nov": "marras", + "Dec": "joulu", "User names may only contain letters, numbers, dots, hyphens and underscores.": "Käyttäjänimet voivat sisältää vain kirjaimia, numeroita, pisteitä, viivoja ja alaviivoja.", "To continue, please enter your password.": "Ole hyvä ja syötä salasanasi jatkaaksesi.", "Verifies a user, device, and pubkey tuple": "Varmentaa käyttäjän, laitteen ja julkisen avaimen kolmikon", @@ -580,8 +580,8 @@ "Start chatting": "Aloita keskustelu", "Start Chatting": "Aloita keskustelu", "Click on the button below to start chatting!": "Paina nappia alla aloittaaksesi keskustelu!", - "Username available": "Käyttäjänimi saatavissa", - "Username not available": "Käyttäjänimi ei ole saatavissa", + "Username available": "Käyttäjätunnus saatavilla", + "Username not available": "Käyttäjätunnus ei ole saatavissa", "Something went wrong!": "Jokin meni vikaan!", "This will be your account name on the homeserver, or you can pick a different server.": "Tästä tulee tilisi nimi -kotipalvelimella, tai voit valita toisen palvelimen.", "If you already have a Matrix account you can log in instead.": "Jos sinulla on jo Matrix-tili voit kirjautua.", @@ -715,7 +715,7 @@ "An email has been sent to %(emailAddress)s": "Sähköpostia lähetetty osoitteeseen %(emailAddress)s", "Please check your email to continue registration.": "Ole hyvä ja tarkista sähköpostisi jatkaaksesi.", "A text message has been sent to %(msisdn)s": "Tekstiviesti lähetetty numeroon %(msisdn)s", - "Username on %(hs)s": "Käyttäjänimi palvelimella %(hs)s", + "Username on %(hs)s": "Käyttäjätunnus palvelimella %(hs)s", "Custom server": "Muu palvelin", "Remove from community": "Poista yhteisöstä", "Disinvite this user from community?": "Peruuta tämän käyttäjän kutsu yhteisöön?", @@ -736,8 +736,8 @@ "%(nameList)s %(transitionList)s": "%(nameList)s %(transitionList)s", "were unbanned %(count)s times|other": "porttikiellot poistettiin %(count)s kertaa", "And %(count)s more...|other": "Ja %(count)s lisää...", - "Matrix ID": "Matrix ID", - "Matrix Room ID": "Matrix huonetunniste", + "Matrix ID": "Matrix-tunnus", + "Matrix Room ID": "Matrix-huonetunnus", "email address": "sähköpostiosoite", "Try using one of the following valid address types: %(validTypesList)s.": "Kokeile käyttää yhtä näistä kelvollisista osoitetyypeistä: %(validTypesList)s.", "You have entered an invalid address.": "Olet syöttänyt virheellisen sähköpostiosoitteen.", @@ -796,7 +796,7 @@ "Please note you are logging into the %(hs)s server, not matrix.org.": "Huomaa että olet kirjautumassa palvelimelle %(hs)s, etkä palvelimelle matrix.org.", "Sign in to get started": "Kirjaudu aloittaksesi", "Upload an avatar:": "Lataa profiilikuva:", - "Deops user with given id": "Poistaa annetun tunnisteen omaavalta käyttäjältä ylläpito-oikeudet", + "Deops user with given id": "Poistaa tunnuksen mukaiselta käyttäjältä ylläpito-oikeudet", "Ignores a user, hiding their messages from you": "Jättää käyttäjän huomioimatta, jotta hänen viestejään ei näytetä sinulle", "Stops ignoring a user, showing their messages going forward": "Lopettaa käyttäjän huomiotta jättämisen, jotta hänen viestinsä näytetään sinulle", "Notify the whole room": "Ilmoita koko huoneelle", @@ -810,8 +810,8 @@ "Answer": "Vastaa", "Call Timeout": "Puhelun aikakatkaisu", "%(weekDayName)s %(time)s": "%(weekDayName)s %(time)s", - "%(weekDayName)s, %(monthName)s %(day)s %(time)s": "%(weekDayName)s, %(monthName)s %(day)s %(time)s", - "%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s %(time)s": "%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s %(time)s", + "%(weekDayName)s, %(monthName)s %(day)s %(time)s": "%(weekDayName)s %(day)s. %(monthName)s %(time)s", + "%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s %(time)s": "%(weekDayName)s %(day)s. %(monthName)s %(fullYear)s %(time)s", "Ignored user": "Estetyt käyttäjät", "Unignored user": "Sallitut käyttäjät", "%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s": "%(userId)s tasolta %(fromPowerLevel)s tasolle %(toPowerLevel)s", @@ -943,7 +943,7 @@ "Send Custom Event": "Lähetä mukautettu tapahtuma", "Advanced notification settings": "Lisäasetukset ilmoituksille", "delete the alias.": "poista alias.", - "To return to your account in future you need to set a password": "Voidaksesi tulevaisuudessa palata tilillesi sinun pitää asettaa salasana", + "To return to your account in future you need to set a password": "Jotta voit jatkossa palata tilillesi, sinun pitää asettaa salasana", "Forget": "Unohda", "#example": "#esimerkki", "Hide panel": "Piilota paneeli", @@ -1345,12 +1345,12 @@ "We encountered an error trying to restore your previous session.": "Törmäsimme ongelmaan yrittäessämme palauttaa edellistä istuntoasi.", "If you have previously used a more recent version of Riot, your session may be incompatible with this version. Close this window and return to the more recent version.": "Jos olet aikaisemmin käyttänyt uudempaa versiota Riotista, istuntosi voi olla epäyhteensopiva tämän version kanssa. Sulje tämä ikkuna ja yritä uudemman version kanssa.", "The platform you're on": "Alusta, jolla olet", - "Whether or not you're logged in (we don't record your username)": "Riippumatta siitä oletko kirjautunut sisään (emme tallenna käyttäjätunnustasi)", - "Whether or not you're using the Richtext mode of the Rich Text Editor": "Riippumatta siitä, että käytätkö muotoillun tekstin tilaa muotoilueditorissa", + "Whether or not you're logged in (we don't record your username)": "Riippumatta siitä, oletko kirjautunut sisään (emme tallenna käyttäjätunnustasi)", + "Whether or not you're using the Richtext mode of the Rich Text Editor": "Riippumatta siitä, käytätkö muotoillun tekstin tilaa muotoilueditorissa", "Your User Agent": "Selaintunnisteesi", "The information being sent to us to help make Riot.im better includes:": "Tietoihin, jota lähetetään Riot.im:ään palvelun parantamiseksi, sisältyy:", - "Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "Niissä kohdissa, missä tämä sivu sisältää yksilöivää tietoa, kuten huoneen, käyttäjän tai ryhmän ID:n, kyseinen tieto poistetaan ennen tiedon lähetystä palvelimelle.", - "A conference call could not be started because the intgrations server is not available": "Konferenssipuhelua ei pystytty aloittamaan, koska integraatiopalvelin ei ole käytettävissä", + "Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "Kohdissa, joissa tämä sivu sisältää yksilöivää tietoa, kuten huoneen, käyttäjän tai ryhmän tunnuksen, kyseinen tieto poistetaan ennen palvelimelle lähettämistä.", + "A conference call could not be started because the intgrations server is not available": "Konferenssipuhelua ei voitu aloittaa, koska integraatiopalvelin ei ole käytettävissä", "A call is currently being placed!": "Puhelua ollaan aloittamassa!", "A call is already in progress!": "Puhelu on jo meneillään!", "Permission Required": "Lisäoikeuksia tarvitaan", @@ -1406,7 +1406,7 @@ "Enable Community Filter Panel": "Ota käyttöön yhteisön suodatinpaneeli", "Allow Peer-to-Peer for 1:1 calls": "Salli vertaisten väliset yhteydet kahdenkeskisissä puheluissa", "Prompt before sending invites to potentially invalid matrix IDs": "Kysy varmistus ennen kutsujen lähettämistä mahdollisesti epäkelpoihin Matrix ID:hin", - "Messages containing my username": "Viestit, jotka sisältävät käyttäjänimeni", + "Messages containing my username": "Viestit, jotka sisältävät käyttäjätunnukseni", "Messages containing @room": "Viestit, jotka sisältävät sanan ”@room”", "Secure messages with this user are end-to-end encrypted and not able to be read by third parties.": "Turvalliset viestit tämän käyttäjän kanssa ovat salattuja päästä päähän, eivätkä kolmannet osapuolet voi lukea niitä.", "Thumbs up": "Peukut ylös", @@ -1438,7 +1438,7 @@ "For help with using Riot, click here.": "Saadaksesi apua Riotin käyttämisessä, klikkaa tästä.", "For help with using Riot, click here or start a chat with our bot using the button below.": "Saadaksesi apua Riotin käytössä, klikkaa tästä tai aloita keskustelu bottimme kanssa alla olevasta painikkeesta.", "Bug reporting": "Virheiden raportointi", - "If you've submitted a bug via GitHub, debug logs can help us track down the problem. Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.": "Jos olet ilmoittanut virheestä Githubin kautta, debug-lokit voivat auttaa meitä ongelman jäljittämisessä. Debug-lokit sisältävät ohjelman käyttödataa sisältäen käyttäjätunnuksen, vierailemiesi huoneiden tai ryhmien ID:t tai aliakset ja muiden käyttäjien käyttäjätunnukset. Debug-lokit eivät sisällä viestejä.", + "If you've submitted a bug via GitHub, debug logs can help us track down the problem. Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.": "Jos olet ilmoittanut virheestä Githubin kautta, debug-lokit voivat auttaa meitä ongelman jäljittämisessä. Debug-lokit sisältävät sovelluksen käyttödataa sisältäen käyttäjätunnuksen, vierailemiesi huoneiden tai ryhmien tunnukset tai aliakset ja muiden käyttäjien käyttäjätunnukset. Debug-lokit eivät sisällä viestejä.", "Autocomplete delay (ms)": "Automaattisen täydennyksen viive (ms)", "To link to this room, please add an alias.": "Lisää alias linkittääksesi tähän huoneeseen.", "Ignored users": "Hiljennetyt käyttäjät", @@ -1465,7 +1465,7 @@ "For maximum security, we recommend you do this in person or use another trusted means of communication.": "Parhaan turvallisuuden takaamiseksi suosittelemme, että teet tämän kasvotusten tai muun luotetun viestintäkeinon avulla.", "Scissors": "Sakset", "Which officially provided instance you are using, if any": "Mitä virallisesti saatavilla olevaa instanssia käytät, jos mitään", - "%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s": "%(weekDayName)s, %(day)s %(monthName)s %(fullYear)s", + "%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s": "%(weekDayName)s %(day)s. %(monthName)s %(fullYear)s", "Missing roomId.": "roomId puuttuu.", "Forces the current outbound group session in an encrypted room to be discarded": "Pakottaa hylkäämään nykyisen ulospäin suuntautuvan ryhmäistunnon salatussa huoneessa", "%(senderDisplayName)s enabled flair for %(groups)s in this room.": "%(senderDisplayName)s otti käyttöön tyylin ryhmille %(groups)s tässä huoneessa.", @@ -1558,7 +1558,7 @@ "Unable to load event that was replied to, it either does not exist or you do not have permission to view it.": "Tapahtuman, johon oli vastattu, lataaminen epäonnistui. Se joko ei ole olemassa tai sinulla ei ole oikeutta katsoa sitä.", "The following users may not exist": "Seuraavat käyttäjät eivät välttämättä ole olemassa", "Unable to find profiles for the Matrix IDs listed below - would you like to invite them anyway?": "Alla luetelluille Matrix ID:ille ei löytynyt profiileja. Haluaisitko kutsua ne siitä huolimatta?", - "Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.": "Debug-lokit sisältävät ohjelman käyttödataa, kuten käyttäjätunnuksesi, huoneiden ja ryhmien ID:t tai aliakset, joissa olet vieraillut sekä muiden käyttäjien käyttäjätunnukset. Ne eivät sisällä viestejä.", + "Debug logs contain application usage data including your username, the IDs or aliases of the rooms or groups you have visited and the usernames of other users. They do not contain messages.": "Debug-lokit sisältävät sovelluksen käyttödataa, kuten käyttäjätunnuksesi, vierailemiesi huoneiden ja ryhmien tunnukset tai aliakset, sekä muiden käyttäjien käyttäjätunnukset. Ne eivät sisällä viestejä.", "Before submitting logs, you must create a GitHub issue to describe your problem.": "Ennen lokien lähettämistä sinun täytyy luoda Githubiin issue (kysymys/ongelma), joka sisältää kuvauksen ongelmastasi.", "What GitHub issue are these logs for?": "Mihin Github-issueen nämä lokit liittyvät?", "Notes:": "Huomiot:", @@ -1566,7 +1566,7 @@ "Community IDs cannot be empty.": "Yhteisön ID:t eivät voi olla tyhjänä.", "To avoid losing your chat history, you must export your room keys before logging out. You will need to go back to the newer version of Riot to do this": "Jotta et menetä keskusteluhistoriaasi, sinun täytyy tallentaa huoneen avaimet ennen kuin kirjaudut ulos. Joudut käyttämään uudempaa Riotin versiota tätä varten", "You've previously used a newer version of Riot on %(host)s. To use this version again with end to end encryption, you will need to sign out and back in again. ": "Olet aikaisemmin käyttänyt uudempaa Riotin versiota koneella %(host)s. Jotta voit käyttää tätä versiota osapuolten välisellä salauksella, sinun täytyy kirjautua ulos ja kirjautua takaisin sisään. ", - "This will make your account permanently unusable. You will not be able to log in, and no one will be able to re-register the same user ID. This will cause your account to leave all rooms it is participating in, and it will remove your account details from your identity server. This action is irreversible.": "Tämä tekee tilistäsi lopullisesti käyttökelvottoman. Et voi kirjautua sisään, eikä kukaan voi rekisteröidä tunnusta samalla käyttäjä-ID:llä. Tunnuksesi poistuu kaikista huoneista, joihin se on liittynyt, ja tilisi tiedot poistetaan identiteettipalvelimelta. Tämä toimenpide on lopullinen eikä sitä voi kumota.", + "This will make your account permanently unusable. You will not be able to log in, and no one will be able to re-register the same user ID. This will cause your account to leave all rooms it is participating in, and it will remove your account details from your identity server. This action is irreversible.": "Tämä tekee tilistäsi lopullisesti käyttökelvottoman. Et voi kirjautua sisään, eikä kukaan voi rekisteröidä samaa käyttäjätunnusta. Tilisi poistuu kaikista huoneista, joihin se on liittynyt, ja tilisi tiedot poistetaan identiteettipalvelimeltasi. Tämä toimenpidettä ei voi kumota.", "Deactivating your account does not by default cause us to forget messages you have sent. If you would like us to forget your messages, please tick the box below.": "Tilisi poistaminen käytöstä ei oletuksena saa meitä unohtamaan lähettämiäsi viestejä. Jos haluaisit meidän unohtavan viestisi, rastita alla oleva ruutu.", "Message visibility in Matrix is similar to email. Our forgetting your messages means that messages you have sent will not be shared with any new or unregistered users, but registered users who already have access to these messages will still have access to their copy.": "Viestien näkyvyys Matrixissa on samantapainen kuin sähköpostissa. Vaikka se, että unohdamme viestisi, tarkoittaa, ettei viestejäsi jaeta enää uusille tai rekisteröitymättömille käyttäjille, käyttäjät, jotka ovat jo saaneet viestisi pystyvät lukemaan jatkossakin omaa kopiotaan viesteistäsi.", "Please forget all messages I have sent when my account is deactivated (Warning: this will cause future users to see an incomplete view of conversations)": "Unohda kaikki viestit, jotka olen lähettänyt, kun tilini on poistettu käytöstä (b>Varoitus:
    tästä seuraa, että tulevat käyttäjät näkevät epätäydellisen version keskusteluista)", @@ -1627,7 +1627,7 @@ "Use an email address to recover your account. Other users can invite you to rooms using your contact details.": "Käytä sähköpostia tilisi palauttamiseen. Muut käyttäjät voivat kutsua sinut huoneisiin yhteystiedoillasi.", "Other servers": "Muut palvelimet", "Enter custom server URLs What does this mean?": "Syötä mukautettujen palvelinten osoitteet. Mitä tämä tarkoittaa?", - "Free": "Vapaa", + "Free": "Ilmainen", "Join millions for free on the largest public server": "Liity ilmaiseksi miljoonien joukkoon suurimmalla julkisella palvelimella", "Premium": "Premium", "Premium hosting for organisations Learn more": "Premium-ylläpitoa organisaatioille. Lue lisää", @@ -1713,7 +1713,7 @@ "Send debug logs and reload Riot": "Lähetä debug-lokit ja päivitä Riot", "Reload Riot without sending logs": "Päivitä Riot lähettämättä lokeja", "A widget would like to verify your identity": "Sovelma haluaisi vahvistaa identiteettisi", - "A widget located at %(widgetUrl)s would like to verify your identity. By allowing this, the widget will be able to verify your user ID, but not perform actions as you.": "Sovelma osoitteessa %(widgetUrl)s haluaisi vahvistaa identiteettisi. Jos sallit tämän, sovelma pystyy vahvistamaan käyttäjä-ID:si, mutta ei voi toimia nimelläsi.", + "A widget located at %(widgetUrl)s would like to verify your identity. By allowing this, the widget will be able to verify your user ID, but not perform actions as you.": "Sovelma osoitteessa %(widgetUrl)s haluaisi todentaa henkilöllisyytesi. Jos sallit tämän, sovelma pystyy todentamaan käyttäjätunnuksesi, muttei voi toimia nimissäsi.", "Remember my selection for this widget": "Muista valintani tälle sovelmalle", "Deny": "Kiellä", "Data from an older version of Riot has been detected. This will have caused end-to-end cryptography to malfunction in the older version. End-to-end encrypted messages exchanged recently whilst using the older version may not be decryptable in this version. This may also cause messages exchanged with this version to fail. If you experience problems, log out and back in again. To retain message history, export and re-import your keys.": "Tunnistimme dataa, joka on lähtöisin vanhasta Riotin versiosta. Tämä aiheuttaa toimintahäiriöitä osapuolten välisessä salauksessa vanhassa versiossa. Viestejä, jotka ovat salattu osapuolten välisellä salauksella, ei välttämättä voida purkaa tällä versiolla. Tämä voi myös aiheuttaa epäonnistumisia viestien välityksessä tämän version kanssa. Jos kohtaat ongelmia, kirjaudu ulos ja takaisin sisään. Säilyttääksesi viestihistoriasi, tallenna salausavaimesi ja tuo ne takaisin kirjauduttuasi takaisin sisälle.", @@ -1760,5 +1760,95 @@ "Recovery Method Removed": "Palautustapa poistettu", "This device has detected that your recovery passphrase and key for Secure Messages have been removed.": "Tämä laite on huomannut, että palautuksen salalauseesi ja avaimesi salatuille viesteille on poistettu.", "If you did this accidentally, you can setup Secure Messages on this device which will re-encrypt this device's message history with a new recovery method.": "Jos teit tämän vahingossa, voit ottaa käyttöön salatut viestit tälle laitteelle, joka uudelleensalaa tämän laitteen keskusteluhistorian uudella palautustavalla.", - "If you didn't remove the recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings.": "Jos et poistanut palautustapaa, hyökkääjä saattaa yrittää käyttää tiliäsi. Vaihda tilisi salasana ja aseta uusi palautustapa asetuksissa välittömästi." + "If you didn't remove the recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings.": "Jos et poistanut palautustapaa, hyökkääjä saattaa yrittää käyttää tiliäsi. Vaihda tilisi salasana ja aseta uusi palautustapa asetuksissa välittömästi.", + "Whether or not you're using the 'breadcrumbs' feature (avatars above the room list)": "Riippumatta siitä, käytätkö 'leivänmuruja' (kuvia huonelistan yläpuolella)", + "Replying With Files": "Tiedostoilla vastaaminen", + "At this time it is not possible to reply with a file. Would you like to upload this file without replying?": "Tiedostolla vastaaminen ei onnistu tällä kertaa. Haluatko ladata tiedoston vastaamatta?", + "The file '%(fileName)s' failed to upload.": "Tiedoston '%(fileName)s' lataaminen ei onnistunut.", + "The server does not support the room version specified.": "Palvelin ei tue määritettyä huoneversiota.", + "Please confirm that you'd like to go forward with upgrading this room from to .": "Vahvista, että haluat päivittää huoneen versiosta versioon .", + "Changes your avatar in this current room only": "Vaihtaa kuvasi vain nykyisessä huoneessa", + "Sends the given message coloured as a rainbow": "Lähettää viestin sateenkaaren väreissä", + "Sends the given emote coloured as a rainbow": "Lähettää emoten sateenkaaren väreissä", + "The user's homeserver does not support the version of the room.": "Käyttäjän kotipalvelin ei tue huoneen versiota.", + "Show recent room avatars above the room list": "Näytä viimeaikaiset huoneen kuvat huoneluettelon yläpuolella", + "Edit messages after they have been sent (refresh to apply changes)": "Muokkaa viestejä niiden lähettämisen jälkeen (päivitä saattaaksesi muutokset voimaan)", + "React to messages with emoji (refresh to apply changes)": "Reagoi viesteihin emojeilla (päivitä saattaaksesi muutokset voimaan)", + "This device is not backing up your keys, but you do have an existing backup you can restore from and add to going forward.": "Tämä laite ei varmuuskopioi avaimiasi, mutta sinulla on olemassa varmuuskopio palauttamista ja lisäämistä varten.", + "Backup has an invalid signature from this device": "Varmuuskopiossa on epäkelpo allekirjoitus tältä laitteelta", + "this room": "tämä huone", + "View older messages in %(roomName)s.": "Näytä vanhemmat viestit huoneessa %(roomName)s.", + "Joining room …": "Liitytään huoneeseen …", + "Loading …": "Latataan …", + "Join the conversation with an account": "Liity keskusteluun tilin avulla", + "Sign Up": "Rekisteröidy", + "Sign In": "Kirjaudu", + "Reason: %(reason)s": "Syy: %(reason)s", + "Forget this room": "Unohda tämä huone", + "Re-join": "Liity uudelleen", + "You were banned from %(roomName)s by %(memberName)s": "%(memberName)s antoi sinulle porttikiellon huoneeseen %(roomName)s", + "Something went wrong with your invite to %(roomName)s": "Jotain meni vikaan kutsussasi huoneeseen %(roomName)s", + "%(errcode)s was returned while trying to valide your invite. You could try to pass this information on to a room admin.": "Kutsusi validointi palautti virhekoodin %(errcode)s. Voit koettaa välittää tiedon huoneen ylläpitäjälle.", + "You can only join it with a working invite.": "Voit liittyä siihen vain toimivalla kutsulla.", + "You can still join it because this is a public room.": "Voit silti liittyä siihen, koska huone on julkinen.", + "Join the discussion": "Liity keskusteluun", + "Try to join anyway": "Yritä silti liittyä", + "This invite to %(roomName)s wasn't sent to your account": "Kutsua huoneeseen %(roomName)s ei lähetetty tilillesi", + "Sign in with a different account, ask for another invite, or add the e-mail address %(email)s to this account.": "Kirjaudu eri tilillä, pyydä uutta kutsua tai lisää sähköpostiosoite %(email)s tähän tiliin.", + "Do you want to chat with %(user)s?": "Haluatko keskustella käyttäjän %(user)s kanssa?", + "Do you want to join %(roomName)s?": "Haluatko liittyä huoneeseen %(roomName)s?", + " invited you": " kutsui sinut", + "You're previewing %(roomName)s. Want to join it?": "Esikatselet huonetta %(roomName)s. Haluatko liittyä siihen?", + "%(roomName)s can't be previewed. Do you want to join it?": "Huonetta %(roomName)s ei voi esikatsella. Haluatko liittyä siihen?", + "This room doesn't exist. Are you sure you're at the right place?": "Tätä huonetta ei ole olemassa. Oletko varma, että olet oikeassa paikassa?", + "This room has already been upgraded.": "Tämä huone on jo päivitetty.", + "Rotate Left": "Kierrä vasempaan", + "Rotate counter-clockwise": "Kierrä vastapäivään", + "Rotate Right": "Kierrä oikeaan", + "Rotate clockwise": "Kierrä myötäpäivään", + "View Servers in Room": "Näytä huoneessa olevat palvelimet", + "Sign out and remove encryption keys?": "Kirjaudu ulos ja poista salausavaimet?", + "Missing session data": "Istunnon dataa puuttuu", + "Some session data, including encrypted message keys, is missing. Sign out and sign in to fix this, restoring keys from backup.": "Istunnon dataa, mukaanlukien salausavaimia, puuttuu. Kirjaudu ulos ja sisään, jolloin avaimet palautetaan varmuuskopiosta.", + "Your browser likely removed this data when running low on disk space.": "Selaimesi luultavasti poisti tämän datan, kun levytila oli vähissä.", + "Upload files (%(current)s of %(total)s)": "Lataa tiedostot (%(current)s / %(total)s)", + "Upload files": "Lataa tiedostot", + "These files are too large to upload. The file size limit is %(limit)s.": "Tiedostot ovat liian isoja ladattaviksi. Tiedoston kokoraja on %(limit)s.", + "Some files are too large to be uploaded. The file size limit is %(limit)s.": "Osa tiedostoista on liian isoja ladattaviksi. Tiedoston kokoraja on %(limit)s.", + "Upload %(count)s other files|other": "Lataa %(count)s muuta tiedostoa", + "Upload %(count)s other files|one": "Lataa %(count)s muu tiedosto", + "Cancel All": "Peruuta kaikki", + "Upload Error": "Latausvirhe", + "Use an email address to recover your account": "Palauta tilisi sähköpostiosoitteen avulla", + "Enter email address (required on this homeserver)": "Syötä sähköpostiosoite (vaaditaan tällä kotipalvelimella)", + "Doesn't look like a valid email address": "Ei näytä kelvolliselta sähköpostiosoitteelta", + "Enter password": "Syötä salasana", + "Password is allowed, but unsafe": "Salasana on sallittu, mutta turvaton", + "Nice, strong password!": "Hyvä, vahva salasana!", + "Passwords don't match": "Salasanat eivät täsmää", + "Other users can invite you to rooms using your contact details": "Muut voivat kutsua sinut huoneisiin yhteystietojesi avulla", + "Enter phone number (required on this homeserver)": "Syötä puhelinnumero (vaaditaan tällä kotipalvelimella)", + "Doesn't look like a valid phone number": "Ei näytä kelvolliselta puhelinnumerolta", + "Use letters, numbers, dashes and underscores only": "Käytä vain kirjaimia, numeroita, viivoja ja alaviivoja", + "Enter username": "Syötä käyttäjätunnus", + "Some characters not allowed": "Osaa merkeistä ei sallita", + "Use an email address to recover your account.": "Palauta tilisi sähköpostiosoitteen avulla.", + "Other users can invite you to rooms using your contact details.": "Muut käyttäjät voivat kutsua sinut huoneisiin yhteystietojesi avulla.", + "Error loading Riot": "Virhe Riotin lataamisessa", + "If this is unexpected, please contact your system administrator or technical support representative.": "Jos et odottanut tätä, ota yhteyttä järjestelmänvalvojaan tai tekniseen tukeen.", + "Homeserver URL does not appear to be a valid Matrix homeserver": "Kotipalvelimen osoite ei näytä olevan kelvollinen Matrix-kotipalvelin", + "Identity server URL does not appear to be a valid identity server": "Identiteettipalvelimen osoite ei näytä olevan kelvollinen identiteettipalvelin", + "A conference call could not be started because the integrations server is not available": "Konferenssipuhelua ei voitu aloittaa, koska integraatiopalvelin ei ole käytettävissä", + "When rooms are upgraded": "Kun huoneet päivitetään", + "Connect this device to key backup before signing out to avoid losing any keys that may only be on this device.": "Yhdistä tämä laite avainten varmuuskopiointiin ennen kuin kirjaudut ulos, jotta et menetä mahdollisia vain tällä laitteella olevia avaimia.", + "Rejecting invite …": "Hylätään kutsua …", + "You were kicked from %(roomName)s by %(memberName)s": "%(memberName)s poisti sinut huoneesta %(roomName)s", + "Edited at %(date)s.": "Muokattu %(date)s.", + "edited": "muokattu", + "To help us prevent this in future, please send us logs.": "Voit auttaa meitä estämään tämän toistumisen lähettämällä meille lokeja.", + "Name or Matrix ID": "Nimi tai Matrix-tunnus", + "Email, name or Matrix ID": "Sähköposti, nimi tai Matrix-tunnus", + "Edited at %(date)s": "Muokattu %(date)s", + "This file is too large to upload. The file size limit is %(limit)s but this file is %(sizeOfThisFile)s.": "Tiedosto on liian iso ladattavaksi. Tiedostojen kokoraja on %(limit)s mutta tämä tiedosto on %(sizeOfThisFile)s.", + "Unbans user with given ID": "Poistaa porttikiellon tunnuksen mukaiselta käyttäjältä" } diff --git a/src/i18n/strings/fr.json b/src/i18n/strings/fr.json index 8afaeef2e6..84a2e93c1b 100644 --- a/src/i18n/strings/fr.json +++ b/src/i18n/strings/fr.json @@ -198,7 +198,7 @@ "No devices with registered encryption keys": "Pas d’appareil avec des clés de chiffrement enregistrées", "No more results": "Fin des résultats", "No results": "Pas de résultat", - "unknown error code": "Code d'erreur inconnu", + "unknown error code": "code d’erreur inconnu", "OK": "OK", "Once encryption is enabled for a room it cannot be turned off again (for now)": "Une fois le chiffrement activé dans un salon il ne peut pas être désactivé (pour le moment)", "Only people who have been invited": "Seules les personnes ayant été invitées", @@ -313,7 +313,7 @@ "Unknown room %(roomId)s": "Salon inconnu %(roomId)s", "Unmute": "Activer le son", "Upload avatar": "Télécharger une photo de profil", - "Upload Failed": "Erreur lors de l'envoi", + "Upload Failed": "Échec de l’envoi", "Upload Files": "Télécharger les fichiers", "Upload file": "Envoyer un fichier", "Usage": "Utilisation", @@ -422,9 +422,9 @@ "You must join the room to see its files": "Vous devez rejoindre le salon pour voir ses fichiers", "Reject all %(invitedRooms)s invites": "Rejeter la totalité des %(invitedRooms)s invitations", "Start new chat": "Démarrer une nouvelle discussion", - "Failed to invite": "Echec de l'invitation", - "Failed to invite user": "Echec lors de l'invitation de l'utilisateur", - "Failed to invite the following users to the %(roomName)s room:": "Echec lors de l’invitation des utilisateurs suivants dans le salon %(roomName)s :", + "Failed to invite": "Échec de l’invitation", + "Failed to invite user": "Échec lors de l'invitation de l'utilisateur", + "Failed to invite the following users to the %(roomName)s room:": "Échec de l’invitation des utilisateurs suivants dans le salon %(roomName)s :", "Confirm Removal": "Confirmer la suppression", "Are you sure you wish to remove (delete) this event? Note that if you delete a room name or topic change, it could undo the change.": "Voulez-vous vraiment supprimer cet événement ? Notez que si vous supprimez le changement du nom ou du sujet d’un salon, il est possible que ce changement soit annulé.", "Unknown error": "Erreur inconnue", @@ -641,7 +641,7 @@ "Revoke widget access": "Révoquer les accès du widget", "Sets the room topic": "Défini le sujet du salon", "To get started, please pick a username!": "Pour commencer, choisissez un nom d'utilisateur !", - "Unable to create widget.": "Impossible de créer un widget.", + "Unable to create widget.": "Impossible de créer le widget.", "Unbans user with given id": "Révoque le bannissement de l'utilisateur à partir de son identifiant", "You are not in this room.": "Vous n'êtes pas dans ce salon.", "You do not have permission to do that in this room.": "Vous n'avez pas la permission d'effectuer cette action dans ce salon.", @@ -716,7 +716,7 @@ "%(names)s and %(count)s others are typing|other": "%(names)s et %(count)s autres écrivent", "Jump to read receipt": "Aller à l'accusé de lecture", "World readable": "Lisible publiquement", - "Guests can join": "Les invités peuvent rejoindre le salon", + "Guests can join": "Accessible aux invités", "To invite users into the room, you must be a": "Pour inviter des utilisateurs dans le salon, vous devez être un(e)", "To configure the room, you must be a": "Pour configurer le salon, vous devez être un(e)", "To kick users, you must be a": "Pour expulser des utilisateurs, vous devez être", @@ -1019,7 +1019,7 @@ "Cancel Sending": "Annuler l'envoi", "This Room": "Ce salon", "The Home Server may be too old to support third party networks": "Le serveur d'accueil semble trop ancien pour supporter des réseaux tiers", - "Noisy": "Bruyant", + "Noisy": "Sonore", "Room not found": "Salon non trouvé", "Messages containing my display name": "Messages contenant mon nom affiché", "Messages in one-to-one chats": "Messages dans les discussions directes", @@ -1490,7 +1490,7 @@ "The other party cancelled the verification.": "L'autre personne a annulé la vérification.", "Verified!": "Vérifié !", "You've successfully verified this user.": "Vous avez vérifié cet utilisateur avec succès.", - "Secure messages with this user are end-to-end encrypted and not able to be read by third parties.": "Les messages sécurisés avec cet utilisateur sont chiffrés de bout en bout et ne peuvent être lus par personne d'autre.", + "Secure messages with this user are end-to-end encrypted and not able to be read by third parties.": "Les messages sécurisés avec cet utilisateur sont chiffrés de bout en bout et ne peuvent être lus par d’autres personnes.", "Got It": "Compris", "Verify this user by confirming the following number appears on their screen.": "Vérifier cet utilisateur en confirmant que le nombre suivant apparaît sur leur écran.", "For maximum security, we reccommend you do this in person or use another trusted means of communication.": "Pour une sécurité maximale, nous recommandons que vous fassiez cela en personne ou que vous utilisiez un autre moyen sûr de communication.", @@ -1569,7 +1569,7 @@ "Begin Verifying": "Commencer la vérification", "Waiting for partner to accept...": "Nous attendons que le partenaire accepte…", "Use two-way text verification": "Utiliser la vérification textuelle bidirectionnelle", - "Verify this user to mark them as trusted. Trusting users gives you extra peace of mind when using end-to-end encrypted messages.": "Vérifier cet utilisateur pour que ce soit un utilisateur de confiance. Faire confiance aux utilisateurs vous apporte la tranquillité d'esprit quand vous utilisez des messages chiffrés de bout en bout.", + "Verify this user to mark them as trusted. Trusting users gives you extra peace of mind when using end-to-end encrypted messages.": "Vérifier cet utilisateur pour que ce soit un utilisateur de confiance. Faire confiance aux utilisateurs vous apporte une tranquillité d’esprit quand vous utilisez des messages chiffrés de bout en bout.", "Verifying this user will mark their device as trusted, and also mark your device as trusted to them.": "En vérifiant cet utilisateur, son appareil sera marqué comme appareil de confiance, et le vôtre sera aussi marqué comme appareil de confiance pour lui.", "Waiting for partner to confirm...": "Nous attendons que le partenaire confirme…", "Incoming Verification Request": "Demande de vérification entrante", @@ -1867,5 +1867,112 @@ "Riot failed to get the public room list.": "Riot n’a pas pu récupérer la liste des salons publics.", "The homeserver may be unavailable or overloaded.": "Le serveur d’accueil est peut-être indisponible ou surchargé.", "You have %(count)s unread notifications in a prior version of this room.|other": "Vous avez %(count)s notifications non lues dans une version précédente de ce salon.", - "You have %(count)s unread notifications in a prior version of this room.|one": "Vous avez %(count)s notification non lue dans une version précédente de ce salon." + "You have %(count)s unread notifications in a prior version of this room.|one": "Vous avez %(count)s notification non lue dans une version précédente de ce salon.", + "Whether or not you're using the 'breadcrumbs' feature (avatars above the room list)": "Si vous utilisez ou non la fonction « fil d’ariane » (les avatars au-dessus de la liste des salons)", + "Replying With Files": "Répondre avec des fichiers", + "At this time it is not possible to reply with a file. Would you like to upload this file without replying?": "Pour le moment, il n’est pas possible de répondre avec un fichier. Souhaitez-vous envoyer ce fichier sans répondre ?", + "The file '%(fileName)s' failed to upload.": "Le fichier « %(fileName)s » n’a pas pu être envoyé.", + "Show recent room avatars above the room list": "Afficher les avatars des salons récents au-dessus de la liste des salons", + "Rotate counter-clockwise": "Pivoter dans le sens inverse des aiguilles d’une montre", + "Rotate clockwise": "Pivoter dans le sens des aiguilles d’une montre", + "GitHub issue": "Rapport GitHub", + "Notes": "Notes", + "If there is additional context that would help in analysing the issue, such as what you were doing at the time, room IDs, user IDs, etc., please include those things here.": "S’il y a des informations supplémentaires qui pourraient nous aider à analyser le problème, comme ce que vous faisiez, l’identifiant du salon ou des utilisateurs etc, veuillez les préciser ici.", + "Sign out and remove encryption keys?": "Se déconnecter et supprimer les clés de chiffrement ?", + "To help us prevent this in future, please send us logs.": "Pour nous aider à éviter cela dans le futur, veuillez nous envoyer les journaux.", + "Missing session data": "Données de la session manquantes", + "Some session data, including encrypted message keys, is missing. Sign out and sign in to fix this, restoring keys from backup.": "Des données de la session, dont les clés des messages chiffrés, sont manquantes. Déconnectez-vous et reconnectez-vous pour régler ce problème, en restaurant les clés depuis la sauvegarde.", + "Your browser likely removed this data when running low on disk space.": "Votre navigateur a sûrement supprimé ces données car il restait peu d’espace sur le disque.", + "Upload files (%(current)s of %(total)s)": "Envoi des fichiers (%(current)s sur %(total)s)", + "Upload files": "Envoyer les fichiers", + "Upload": "Envoyer", + "This file is too large to upload. The file size limit is %(limit)s but this file is %(sizeOfThisFile)s.": "Le fichier est trop lourd pour être envoyé. La taille limite est de %(limit)s mais la taille de ce fichier est de %(sizeOfThisFile)s.", + "These files are too large to upload. The file size limit is %(limit)s.": "Ces fichiers sont trop lourds pour être envoyés. La taille limite des fichiers est de %(limit)s.", + "Some files are too large to be uploaded. The file size limit is %(limit)s.": "Certains fichiers sont trop lourds pour être envoyés. La taille limite des fichiers est de %(limit)s.", + "Upload %(count)s other files|other": "Envoyer %(count)s autres fichiers", + "Upload %(count)s other files|one": "Envoyer %(count)s autre fichier", + "Cancel All": "Tout annuler", + "Upload Error": "Erreur d’envoi", + "A conference call could not be started because the integrations server is not available": "L’appel en téléconférence n’a pas pu être lancé car les intégrations du serveur ne sont pas disponibles", + "The server does not support the room version specified.": "Le serveur ne prend pas en charge la version de salon spécifiée.", + "Name or Matrix ID": "Nom ou identifiant Matrix", + "Email, name or Matrix ID": "E-mail, nom ou identifiant Matrix", + "Please confirm that you'd like to go forward with upgrading this room from to .": "Veuillez confirmer la mise à niveau de ce salon de à .", + "Changes your avatar in this current room only": "Change votre avatar seulement dans le salon actuel", + "Unbans user with given ID": "Révoque le bannissement de l’utilisateur ayant l’identifiant fourni", + "Sends the given message coloured as a rainbow": "Envoie le message coloré aux couleurs de l’arc-en-ciel", + "Sends the given emote coloured as a rainbow": "Envoie l’émoji coloré aux couleurs de l’arc-en-ciel", + "The user's homeserver does not support the version of the room.": "Le serveur d’accueil de l’utilisateur ne prend pas en charge la version de ce salon.", + "Edit messages after they have been sent (refresh to apply changes)": "Éditer les messages après leur envoi (actualiser pour appliquer les changements)", + "React to messages with emoji (refresh to apply changes)": "Réagir aux messages avec des émojis (actualiser pour appliquer les changements)", + "When rooms are upgraded": "Quand les salons sont mis à niveau", + "This device is not backing up your keys, but you do have an existing backup you can restore from and add to going forward.": "Cet appareil ne sauvegarde pas vos clés, mais vous avez une sauvegarde existante que vous pouvez restaurer et joindre.", + "Connect this device to key backup before signing out to avoid losing any keys that may only be on this device.": "Connecter cet appareil à la sauvegarde de clés avant de vous déconnecter pour éviter de perdre des clés qui pourraient n’être présentes que sur cet appareil.", + "Connect this device to Key Backup": "Connecter cet appareil à la sauvegarde de clés", + "Backup has an invalid signature from this device": "La sauvegarde a une signature invalide depuis cet appareil", + "this room": "ce salon", + "View older messages in %(roomName)s.": "Voir les messages plus anciens dans %(roomName)s.", + "Joining room …": "Inscription au salon…", + "Loading …": "Chargement…", + "Rejecting invite …": "Rejet de l’invitation…", + "Join the conversation with an account": "Rejoindre la conversation avec un compte", + "Sign Up": "S’inscrire", + "Sign In": "Se connecter", + "You were kicked from %(roomName)s by %(memberName)s": "Vous avez été expulsé(e) de %(roomName)s par %(memberName)s", + "Reason: %(reason)s": "Motif : %(reason)s", + "Forget this room": "Oublier ce salon", + "Re-join": "Revenir", + "You were banned from %(roomName)s by %(memberName)s": "Vous avez été banni(e) de %(roomName)s par %(memberName)s", + "Something went wrong with your invite to %(roomName)s": "Une erreur est survenue avec votre invitation à %(roomName)s", + "%(errcode)s was returned while trying to valide your invite. You could try to pass this information on to a room admin.": "%(errcode)s a été retourné en essayant de valider votre invitation. Vous pouvez essayer de transmettre cette information à l’administrateur du salon.", + "You can only join it with a working invite.": "Vous ne pouvez le rejoindre qu’avec une invitation fonctionnelle.", + "You can still join it because this is a public room.": "Vous pouvez quand même le rejoindre car c’est un salon public.", + "Join the discussion": "Rejoindre la discussion", + "Try to join anyway": "Essayer de le rejoindre quand même", + "This invite to %(roomName)s wasn't sent to your account": "Cette invitation à %(roomName)s n’a pas été envoyée à votre compte", + "Sign in with a different account, ask for another invite, or add the e-mail address %(email)s to this account.": "Connectez-vous avec un autre compte, demandez une autre invitation ou ajoutez l’adresse e-mail %(email)s à ce compte.", + "Do you want to chat with %(user)s?": "Voulez-vous discuter avec %(user)s ?", + "Do you want to join %(roomName)s?": "Voulez-vous rejoindre %(roomName)s ?", + " invited you": " vous a invité(e)", + "You're previewing %(roomName)s. Want to join it?": "Ceci est un aperçu de %(roomName)s. Voulez-vous le rejoindre ?", + "%(roomName)s can't be previewed. Do you want to join it?": "Vous ne pouvez pas avoir d’aperçu de %(roomName)s. Voulez-vous le rejoindre ?", + "This room doesn't exist. Are you sure you're at the right place?": "Ce salon n’existe pas. Êtes-vous vraiment au bon endroit ?", + "Try again later, or ask a room admin to check if you have access.": "Réessayez plus tard ou demandez à l’administrateur du salon si vous y avez accès.", + "%(errcode)s was returned while trying to access the room. If you think you're seeing this message in error, please submit a bug report.": "%(errcode)s a été retourné en essayant d’accéder au salon. Si vous pensez que vous ne devriez pas voir ce message, veuillez soumettre un rapport d’anomalie.", + "This room has already been upgraded.": "Ce salon a déjà été mis à niveau.", + "Agree or Disagree": "Accepter ou refuser", + "Like or Dislike": "Aimer ou ne pas aimer", + "reacted with %(shortName)s": "ont réagi avec %(shortName)s", + "Edited at %(date)s.": "Édité à %(date)s.", + "edited": "édité", + "Rotate Left": "Tourner à gauche", + "Rotate Right": "Tourner à droite", + "View Servers in Room": "Voir les serveurs dans le salon", + "Use an email address to recover your account": "Utiliser une adresse e-mail pour récupérer votre compte", + "Enter email address (required on this homeserver)": "Saisir l’adresse e-mail (obligatoire sur ce serveur d’accueil)", + "Doesn't look like a valid email address": "Cela ne ressemble pas a une adresse e-mail valide", + "Enter password": "Saisir le mot de passe", + "Password is allowed, but unsafe": "Ce mot de passe est autorisé, mais peu sûr", + "Nice, strong password!": "Bien joué, un mot de passe robuste !", + "Passwords don't match": "Les mots de passe ne correspondent pas", + "Other users can invite you to rooms using your contact details": "D’autres utilisateurs peuvent vous inviter à des salons grâce à vos informations de contact", + "Enter phone number (required on this homeserver)": "Saisir le numéro de téléphone (obligatoire sur ce serveur d’accueil)", + "Doesn't look like a valid phone number": "Cela ne ressemble pas à un numéro de téléphone valide", + "Use letters, numbers, dashes and underscores only": "Utilisez uniquement des lettres, chiffres, traits d’union et tirets bas", + "Enter username": "Saisir le nom d’utilisateur", + "Some characters not allowed": "Certains caractères ne sont pas autorisés", + "Use an email address to recover your account.": "Utilisez une adresse e-mail pour récupérer votre compte.", + "Other users can invite you to rooms using your contact details.": "D’autre utilisateurs peuvent vous inviter à des salons en utilisant vos informations de contact.", + "Error loading Riot": "Erreur lors du chargement de Riot", + "If this is unexpected, please contact your system administrator or technical support representative.": "Si cela est inattendu, veuillez contacter votre administrateur système ou un représentant du support technique.", + "Failed to get autodiscovery configuration from server": "Échec de la découverte automatique de la configuration depuis le serveur", + "Invalid base_url for m.homeserver": "base_url pour m.homeserver non valide", + "Homeserver URL does not appear to be a valid Matrix homeserver": "L’URL du serveur d’accueil ne semble pas être un serveur d’accueil Matrix valide", + "Invalid base_url for m.identity_server": "base_url pour m.identity_server non valide", + "Identity server URL does not appear to be a valid identity server": "L’URL du serveur d’identité ne semble pas être un serveur d’identité valide", + "Edited at %(date)s": "Édité à %(date)s", + "Show hidden events in timeline": "Afficher les évènements cachés dans l’historique", + "Your profile": "Votre profil", + "Add room": "Ajouter un salon", + "Edit message": "Éditer le message" } diff --git a/src/i18n/strings/hi.json b/src/i18n/strings/hi.json index 162ddf08a4..7ff6c6a7cf 100644 --- a/src/i18n/strings/hi.json +++ b/src/i18n/strings/hi.json @@ -772,5 +772,25 @@ "To ban users, you must be a": "उपयोगकर्ताओं को प्रतिबंधित करने के लिए, आपको होना चाहिए", "To remove other users' messages, you must be a": "अन्य उपयोगकर्ताओं के संदेशों को हटाने के लिए, आपको होना चाहिए", "To notify everyone in the room, you must be a": "कमरे में सभी को सूचित करने के लिए, आपको होना चाहिए", - "No users have specific privileges in this room": "इस कमरे में किसी भी उपयोगकर्ता के विशेष विशेषाधिकार नहीं हैं" + "No users have specific privileges in this room": "इस कमरे में किसी भी उपयोगकर्ता के विशेष विशेषाधिकार नहीं हैं", + "Whether or not you're using the 'breadcrumbs' feature (avatars above the room list)": "आप 'ब्रेडक्रंब' सुविधा का उपयोग कर रहे हैं या नहीं (कमरे की सूची के ऊपर अवतार)", + "Replying With Files": "फाइलों के साथ उत्तर", + "At this time it is not possible to reply with a file. Would you like to upload this file without replying?": "इस समय किसी फ़ाइल के साथ उत्तर देना संभव नहीं है। क्या आप इस फ़ाइल को बिना उत्तर दिए अपलोड करना चाहेंगे?", + "The file '%(fileName)s' failed to upload.": "फ़ाइल '%(fileName)s' अपलोड करने में विफल रही।", + "Prepends ¯\\_(ツ)_/¯ to a plain-text message": "एक सादे पाठ संदेश के लिए ¯\\_(ツ)_/¯ प्रस्तुत करता है", + "Room upgrade confirmation": "रूम के उन्नयन की पुष्टि", + "Upgrading a room can be destructive and isn't always necessary.": "एक कमरे को अपग्रेड करना विनाशकारी हो सकता है और हमेशा आवश्यक नहीं होता है।", + "Room upgrades are usually recommended when a room version is considered unstable. Unstable room versions might have bugs, missing features, or security vulnerabilities.": "एक कमरे के संस्करण को अस्थिर माना जाता है, तो आमतौर पर कमरे के उन्नयन की सिफारिश की जाती है। अस्थिर कमरे के संस्करणों में बग, लापता विशेषताएं या सुरक्षा कमजोरियां हो सकती हैं।", + "Room upgrades usually only affect server-side processing of the room. If you're having problems with your Riot client, please file an issue with .": "रूम का उन्नयन आमतौर पर केवल रूम के सर्वर-साइड को प्रभावित करता है। यदि आपको अपने रायट क्लाइंट के साथ समस्या हो रही है, तो कृपया के साथ एक समस्या दर्ज करें।", + "Warning: Upgrading a room will not automatically migrate room members to the new version of the room. We'll post a link to the new room in the old version of the room - room members will have to click this link to join the new room.": "चेतावनी: किसी कमरे को अपग्रेड करना कमरे के सदस्यों को कमरे के नए संस्करण में स्वचालित रूप से माइग्रेट नहीं करना है। हम कमरे के पुराने संस्करण में नए कमरे के लिए एक लिंक पोस्ट करेंगे। नए कमरे में शामिल होने के लिए कमरे के सदस्यों को इस लिंक पर क्लिक करना होगा।", + "Upgrade": "अपग्रेड", + "Adds a custom widget by URL to the room": "रूम में URL द्वारा एक कस्टम विजेट जोड़ता है", + "Please supply a https:// or http:// widget URL": "कृपया एक https:// या http:// विजेट URL की आपूर्ति करें", + "You cannot modify widgets in this room.": "आप इस रूम में विजेट्स को संशोधित नहीं कर सकते।", + "%(senderName)s revoked the invitation for %(targetDisplayName)s to join the room.": "%(senderName)s ने कमरे में शामिल होने के लिए %(targetDisplayName)s के निमंत्रण को रद्द कर दिया।", + "User %(userId)s is already in the room": "उपयोगकर्ता %(userId)s पहले से ही रूम में है", + "The user must be unbanned before they can be invited.": "उपयोगकर्ता को आमंत्रित करने से पहले उन्हें प्रतिबंधित किया जाना चाहिए।", + "Show recent room avatars above the room list": "रूम की सूची के ऊपर हाल के अवतारों को दिखाएं", + "Enable desktop notifications for this device": "इस उपकरण के लिए डेस्कटॉप सूचनाएं सक्षम करें", + "Enable audible notifications for this device": "इस उपकरण के लिए श्रव्य सूचनाएँ सक्षम करें" } diff --git a/src/i18n/strings/hu.json b/src/i18n/strings/hu.json index e7f0164779..45f219f1f7 100644 --- a/src/i18n/strings/hu.json +++ b/src/i18n/strings/hu.json @@ -961,7 +961,7 @@ "Failed to add tag %(tagName)s to room": "Nem sikerült hozzáadni a szobához ezt: %(tagName)s", "Clear filter": "Szűrő törlése", "Disable Community Filter Panel": "Közösség keresési panel tiltása", - "Did you know: you can use communities to filter your Riot.im experience!": "Tudtad, hogy a Riot.im élmény fokozásához használhatsz közösségeket?", + "Did you know: you can use communities to filter your Riot.im experience!": "Tudtad, hogy a Riot.im élmény fokozásához használhatsz közösségeket!", "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.": "A szűrő beállításához húzd a közösség avatarját a szűrő panel fölé a képernyő bal szélén. A szűrő panelen az avatarra kattintva bármikor leszűrheted azokat a szobákat és embereket akik a megadott közösséghez tartoznak.", "Your key share request has been sent - please check your other devices for key share requests.": "A kulcs megosztási kérést elküldtük - ellenőrizd a többi eszközödön a kulcs megosztási kéréseket.", "Key share requests are sent to your other devices automatically. If you rejected or dismissed the key share request on your other devices, click here to request the keys for this session again.": "A kulcs megosztási kérelem automatikusan el lett küldve a többi eszközödre. Ha elutasítottad vagy törölted a kérést a másik eszközön ide kattintva újra kérheted a kulcsokat.", @@ -1175,8 +1175,8 @@ "Please forget all messages I have sent when my account is deactivated (Warning: this will cause future users to see an incomplete view of conversations)": "Kérlek töröld az összes általam küldött üzenetet amikor a fiókomat felfüggesztem (Figyelem: ez azt eredményezheti, hogy a jövőbeni felhasználók csak részleges beszélgetést látnak majd)", "e.g. %(exampleValue)s": "pl. %(exampleValue)s", "Reload widget": "Kisalkalmazás újratöltése", - "Please help improve Riot.im by sending anonymous usage data. This will use a cookie (please see our Cookie Policy).": "Kérlek segíts javítani a Riot.im-et azzal, hogy anonim felhasználási adatokat küldesz. Ez szütit (cookie) fog használni (lásd a sütire vonatkozó szabályozásunkat).", - "Please help improve Riot.im by sending anonymous usage data. This will use a cookie.": "Kérlek segíts javítani a Riot.im-et azzal, hogy anonim felhasználási adatokat küldesz. Ez szütit (cookie) fog használni.", + "Please help improve Riot.im by sending anonymous usage data. This will use a cookie (please see our Cookie Policy).": "Kérlek segíts javítani a Riot.im-et azzal, hogy anonim felhasználási adatokat küldesz. Ez sütit (cookie) fog használni (lásd a sütire vonatkozó szabályozásunkat).", + "Please help improve Riot.im by sending anonymous usage data. This will use a cookie.": "Kérlek segíts javítani a Riot.im-et azzal, hogy anonim felhasználási adatokat küldesz. Ez sütit (cookie) fog használni.", "Yes, I want to help!": "Igen, segítek!", "Can't leave Server Notices room": "Nem lehet elhagyni a Szerver Üzenetek szobát", "This room is used for important messages from the Homeserver, so you cannot leave it.": "Ez a szoba fontos szerverüzenetek közlésére jött létre, nem tudsz kilépni belőle.", @@ -1867,5 +1867,111 @@ "Riot failed to get the public room list.": "Riotnak nem sikerült beszereznie a nyilvános szoba listát.", "The homeserver may be unavailable or overloaded.": "A Matrix szerver elérhetetlen vagy túlterhelt.", "You have %(count)s unread notifications in a prior version of this room.|other": "%(count)s olvasatlan értesítésed van a régi verziójú szobában.", - "You have %(count)s unread notifications in a prior version of this room.|one": "%(count)s olvasatlan értesítésed van a régi verziójú szobában." + "You have %(count)s unread notifications in a prior version of this room.|one": "%(count)s olvasatlan értesítésed van a régi verziójú szobában.", + "Whether or not you're using the 'breadcrumbs' feature (avatars above the room list)": "Használsz vagy nem „morzsákat” (profilképek a szobalista felett)", + "Replying With Files": "Válasz fájlokkal", + "At this time it is not possible to reply with a file. Would you like to upload this file without replying?": "Egyenlőre nem lehet fájlal válaszolni. Szeretnéd feltölteni a fájlt úgy, hogy az nem egy válasz lesz?", + "The file '%(fileName)s' failed to upload.": "A %(fileName)s fájlt nem sikerült feltölteni.", + "Show recent room avatars above the room list": "A legfrissebb szoba profilképét mutassa a szoba lista felett", + "Rotate counter-clockwise": "Óramutató járásával ellentétesen fordít", + "Rotate clockwise": "Óramutató járásával megegyező irányba fordít", + "GitHub issue": "GitHub hibajegy", + "Notes": "Megjegyzések", + "If there is additional context that would help in analysing the issue, such as what you were doing at the time, room IDs, user IDs, etc., please include those things here.": "Ha a hiba felderítésében további adat is segítséget adhat, mint az, hogy mit csináltál éppen, mi a szoba-, felhasználó azonosítója, stb... itt add meg.", + "Sign out and remove encryption keys?": "Kilépés és a titkosítási kulcsok törlése?", + "To help us prevent this in future, please send us logs.": "Segíts abban, hogy ez később ne fordulhasson elő, kérlek küld el a naplókat.", + "Missing session data": "A kapcsolati adat hiányzik", + "Some session data, including encrypted message keys, is missing. Sign out and sign in to fix this, restoring keys from backup.": "Néhány kapcsolati adat hiányzik, beleértve a titkosított üzenetek kulcsait. Lépj ki és jelentkezz vissza a hiba javításához és állítsd vissza mentésből a kulcsokat.", + "Your browser likely removed this data when running low on disk space.": "A böngésző valószínűleg törölte ezeket az adatokat amikor lecsökkent a szabad lemezterület.", + "Upload files (%(current)s of %(total)s)": "Fájlok feltöltése (%(current)s / %(total)s)", + "Upload files": "Fájlok feltöltése", + "Upload": "Feltöltés", + "This file is too large to upload. The file size limit is %(limit)s but this file is %(sizeOfThisFile)s.": "Ez a fájl túl nagy, hogy fel lehessen tölteni. A fájl méret korlát %(limit)s de a fájl %(sizeOfThisFile)s méretű.", + "These files are too large to upload. The file size limit is %(limit)s.": "A fájl túl nagy a feltöltéshez. A fájlméret korlát %(limit)s.", + "Some files are too large to be uploaded. The file size limit is %(limit)s.": "Néhány fájl túl nagy, hogy fel lehessen tölteni. A fájlméret korlát %(limit)s.", + "Upload %(count)s other files|other": "Feltölt %(count)s másik fájlt", + "Upload %(count)s other files|one": "Feltölt %(count)s másik fájlt", + "Cancel All": "Mindent megszakít", + "Upload Error": "Feltöltési hiba", + "The server does not support the room version specified.": "A szerver nem támogatja a megadott szoba verziót.", + "Please confirm that you'd like to go forward with upgrading this room from to .": "Kérlek erősítsd meg, hogy a szobát frissíted a verzióról verzióra.", + "Changes your avatar in this current room only": "A profilképedet csak ebben a szobában változtatja meg", + "Sends the given message coloured as a rainbow": "A megadott üzenetet szivárvány színben küldi el", + "Sends the given emote coloured as a rainbow": "A megadott hangulatjelet szivárvány színben küldi el", + "The user's homeserver does not support the version of the room.": "A felhasználó matrix szervere nem támogatja a megadott szoba verziót.", + "Edit messages after they have been sent (refresh to apply changes)": "Üzenet szerkesztése küldés után (újratöltés szükséges)", + "React to messages with emoji (refresh to apply changes)": "Reagálj az üzenetre emoji-val (újratöltés szükséges)", + "When rooms are upgraded": "Ha a szobák frissültek", + "This device is not backing up your keys, but you do have an existing backup you can restore from and add to going forward.": "Ez az eszköz nem menti el a kulcsaidat, de létezik mentés amit visszaállíthatsz és folytathatod.", + "Connect this device to key backup before signing out to avoid losing any keys that may only be on this device.": "Csatlakozz ezzel az eszközzel a kulcs mentéshez kilépés előtt, hogy ne veszíts el kulcsot ami esetleg csak ezen az eszközön van meg.", + "Connect this device to Key Backup": "Csatlakozz ezzel az eszközzel a Kulcs Mentéshez", + "Backup has an invalid signature from this device": "A mentés érvénytelen aláírással rendelkezik erről az eszközről", + "this room": "ez a szoba", + "View older messages in %(roomName)s.": "Régebbi üzenetek megjelenítése itt: %(roomName)s.", + "Joining room …": "Szobához csatlakozás …", + "Loading …": "Betöltés …", + "Rejecting invite …": "Meghívó elutasítása …", + "Join the conversation with an account": "Beszélgetéshez csatlakozás felhasználói fiókkal", + "Sign Up": "Fiók készítés", + "Sign In": "Bejelentkezés", + "You were kicked from %(roomName)s by %(memberName)s": "Téged kirúgott %(memberName)s ebből a szobából: %(roomName)s", + "Reason: %(reason)s": "Ok: %(reason)s", + "Forget this room": "Szoba elfelejtése", + "Re-join": "Újra-csatlakozás", + "You were banned from %(roomName)s by %(memberName)s": "Téged kitiltott %(memberName)s ebből a szobából: %(roomName)s", + "Something went wrong with your invite to %(roomName)s": "A meghívóddal ebbe a szobába: %(roomName)s valami baj történt", + "%(errcode)s was returned while trying to valide your invite. You could try to pass this information on to a room admin.": "A meghívód ellenőrzése során az alábbi hibakódot kaptuk: %(errcode)s. Megpróbálhatod ezt az információt átadni a szoba adminisztrátorának.", + "You can only join it with a working invite.": "Csak érvényes meghívóval tudsz csatlakozni.", + "You can still join it because this is a public room.": "Mivel a szoba nyilvános megpróbálhatsz csatlakozni.", + "Join the discussion": "Beszélgetéshez csatlakozás", + "Try to join anyway": "Mindennek ellenére próbálj csatlakozni", + "This invite to %(roomName)s wasn't sent to your account": "Ezt a meghívót ide: %(roomName)s nem a te fiókodnak küldték", + "Sign in with a different account, ask for another invite, or add the e-mail address %(email)s to this account.": "Jelentkezz be más fiókkal, kérj másik meghívót vagy add hozzá a fiókodhoz ezt az e-mail címet: %(email)s.", + "Do you want to chat with %(user)s?": "%(user)s felhasználóval szeretnél beszélgetni?", + "Do you want to join %(roomName)s?": "%(roomName)s szobába szeretnél belépni?", + " invited you": " meghívott", + "You're previewing %(roomName)s. Want to join it?": "%(roomName)s szoba előnézetét látod. Belépsz?", + "%(roomName)s can't be previewed. Do you want to join it?": "%(roomName)s szobának nincs előnézete. Be szeretnél lépni?", + "This room doesn't exist. Are you sure you're at the right place?": "Ez a szoba nem létezik. Biztos, hogy jó helyen vagy?", + "Try again later, or ask a room admin to check if you have access.": "Próbálkozz később vagy kérd meg a szoba adminisztrátorát, hogy nézze meg van-e hozzáférésed.", + "%(errcode)s was returned while trying to access the room. If you think you're seeing this message in error, please submit a bug report.": "Amikor a szobát próbáltuk elérni ezt a hibaüzenetet kaptuk: %(errcode)s. Ha úgy gondolod, hogy ez egy hiba légy szívesnyiss egy hibajegyet.", + "This room has already been upgraded.": "Ez a szoba már frissült.", + "Agree or Disagree": "Egyetért vagy Ellentmond", + "Like or Dislike": "Kedveli vagy Nem kedveli", + "Rotate Left": "Balra forgat", + "Rotate Right": "Jobbra forgat", + "View Servers in Room": "Szerverek megjelenítése a szobában", + "Use an email address to recover your account": "A felhasználói fiók visszaszerzése e-mail címmel", + "Enter email address (required on this homeserver)": "E-mail cím megadása (ezen a matrix szerveren kötelező)", + "Doesn't look like a valid email address": "Az e-mail cím nem tűnik érvényesnek", + "Enter password": "Jelszó megadása", + "Password is allowed, but unsafe": "A jelszó engedélyezett, de nem biztonságos", + "Nice, strong password!": "Szép, erős jelszó!", + "Passwords don't match": "A jelszavak nem egyeznek meg", + "Other users can invite you to rooms using your contact details": "Mások meghívhatnak a szobákba a kapcsolatoknál megadott adataiddal", + "Enter phone number (required on this homeserver)": "Telefonszám megadása (ennél a matrix szervernél kötelező)", + "Doesn't look like a valid phone number": "Ez a telefonszám nem tűnik érvényesnek", + "Use letters, numbers, dashes and underscores only": "Csak betűket, számokat, kötőjelet és aláhúzást használj", + "Enter username": "Felhasználói név megadása", + "Some characters not allowed": "Néhány karakter nem engedélyezett", + "Use an email address to recover your account.": "A felhasználói fiókod visszaszerzéséhez használd az e-mail címet.", + "Other users can invite you to rooms using your contact details.": "Mások meghívhatnak a szobákba a kapcsolatoknál megadott adataid alapján.", + "Error loading Riot": "A Riot betöltésénél hiba", + "If this is unexpected, please contact your system administrator or technical support representative.": "Ha ez váratlanul ért, kérlek vedd fel a kapcsolatot a rendszer adminisztrátorával vagy a technikai segítséggel.", + "Failed to get autodiscovery configuration from server": "A szerverről nem sikerült beszerezni az automatikus felderítés beállításait", + "Invalid base_url for m.homeserver": "Hibás base_url az m.homeserver -hez", + "Homeserver URL does not appear to be a valid Matrix homeserver": "A matrix URL nem tűnik érvényesnek", + "Invalid base_url for m.identity_server": "Érvénytelen base_url az m.identity_server -hez", + "Identity server URL does not appear to be a valid identity server": "Az Azonosító szerver URL nem tűnik érvényesnek", + "A conference call could not be started because the integrations server is not available": "A konferencia hívást nem lehet elkezdeni mert az integrációs szerver nem érhető el", + "Name or Matrix ID": "Név vagy Matrix azon.", + "Email, name or Matrix ID": "E-mail, név vagy Matrix azon.", + "Unbans user with given ID": "Visszaengedi a megadott azonosítójú felhasználót", + "reacted with %(shortName)s": "ezzel reagált: %(shortName)s", + "Edited at %(date)s.": "Szerkesztve ekkor: %(date)s.", + "edited": "szerkesztve", + "Edited at %(date)s": "Szerkesztve: %(date)s", + "Show hidden events in timeline": "Rejtett események megmutatása az idővonalon", + "Add room": "Szoba hozzáadása", + "Your profile": "Profilod" } diff --git a/src/i18n/strings/it.json b/src/i18n/strings/it.json index cef7c32772..b0903cf0a5 100644 --- a/src/i18n/strings/it.json +++ b/src/i18n/strings/it.json @@ -1822,5 +1822,32 @@ "Riot failed to get the public room list.": "Riot non è riuscito ad ottenere l'elenco di stanze pubbliche.", "The homeserver may be unavailable or overloaded.": "L'homeserver potrebbe non essere disponibile o sovraccarico.", "You have %(count)s unread notifications in a prior version of this room.|other": "Hai %(count)s notifiche non lette in una versione precedente di questa stanza.", - "You have %(count)s unread notifications in a prior version of this room.|one": "Hai %(count)s notifiche non lette in una versione precedente di questa stanza." + "You have %(count)s unread notifications in a prior version of this room.|one": "Hai %(count)s notifiche non lette in una versione precedente di questa stanza.", + "Whether or not you're using the 'breadcrumbs' feature (avatars above the room list)": "Che tu stia usando o meno la funzione 'breadcrumbs' (avatar sopra l'elenco delle stanze)", + "A conference call could not be started because the integrations server is not available": "Non è stato possibile avviare una conferenza perchè il server di integrazione non è disponibile", + "Replying With Files": "Risposta con dei file", + "At this time it is not possible to reply with a file. Would you like to upload this file without replying?": "Al momento non è possibile rispondere con un file. Vorresti inviare questo file senza rispondere?", + "The file '%(fileName)s' failed to upload.": "Invio del file '%(fileName)s' fallito.", + "The server does not support the room version specified.": "Il server non supporta la versione di stanza specificata.", + "Name or Matrix ID": "Nome o ID Matrix", + "Email, name or Matrix ID": "Email, nome o ID Matrix", + "Please confirm that you'd like to go forward with upgrading this room from to .": "Conferma di volere continuare nell'aggiornamento di questa stanza dalla alla .", + "Changes your avatar in this current room only": "Cambia il tuo avatar solo nella stanza attuale", + "Unbans user with given ID": "Riammette l'utente con l'ID dato", + "Sends the given message coloured as a rainbow": "Invia il messaggio dato colorato come un arcobaleno", + "Sends the given emote coloured as a rainbow": "Invia l'emoticon dato colorato come un arcobaleno", + "The user's homeserver does not support the version of the room.": "L'homeserver dell'utente non supporta la versione della stanza.", + "Show recent room avatars above the room list": "Mostra gli avatar recenti della stanza sopra l'elenco stanze", + "Edit messages after they have been sent (refresh to apply changes)": "Modifica i messaggi dopo l'invio (ricarica per applicare le modifiche)", + "React to messages with emoji (refresh to apply changes)": "Reagisci ai messaggi con emoji (ricarica per applicare le modifiche)", + "Show hidden events in timeline": "Mostra eventi nascosti nella timeline", + "When rooms are upgraded": "Quando le stanze vengono aggiornate", + "This device is not backing up your keys, but you do have an existing backup you can restore from and add to going forward.": "Questo dispositivo non sta facendo backup delle tue chiavi, ma hai un backup esistente che puoi ripristinare e aggiungere da adesso in poi.", + "Connect this device to key backup before signing out to avoid losing any keys that may only be on this device.": "Connetti questo dispositivo al backup chiavi prima di disconnetterti per evitare di perdere chiavi che potrebbero essere solo in questo dispositivo.", + "Connect this device to Key Backup": "Connetti questo dispositibo al Backup Chiavi", + "Backup has an invalid signature from this device": "Il backup ha un firma non valida da questo dispositivo", + "this room": "questa stanza", + "View older messages in %(roomName)s.": "Vedi messaggi più vecchi in %(roomName)s.", + "Joining room …": "Ingresso nella stanza …", + "Loading …": "Caricamento …" } diff --git a/src/i18n/strings/lv.json b/src/i18n/strings/lv.json index 09182ed776..efd66b8f0e 100644 --- a/src/i18n/strings/lv.json +++ b/src/i18n/strings/lv.json @@ -1126,5 +1126,10 @@ "Collapse panel": "Sakļaut (saritināt) paneli", "With your current browser, the look and feel of the application may be completely incorrect, and some or all features may not function. If you want to try it anyway you can continue, but you are on your own in terms of any issues you may encounter!": "Tavā pašreizējā pārlūkā aplikācijas izskats un uzvedība var būt pilnīgi neatbilstoša, kā arī dažas no visām funkcijām var nedarboties. Ja vēlies turpināt izmantot šo pārlūku, Tu vari arī turpināt, apzinoties, ka šajā gadījumā esi viens/a ar iespējamo problēmu!", "Checking for an update...": "Lūkojos pēc aktualizācijas...", - "There are advanced notifications which are not shown here": "Pastāv papildus paziņojumi, kuri šeit netiek rādīti" + "There are advanced notifications which are not shown here": "Pastāv papildus paziņojumi, kuri šeit netiek rādīti", + "e.g. %(exampleValue)s": "piemēram %(exampleValue)s", + "e.g. ": "piemēram ", + "Your device resolution": "Tavas iekārtas izšķirtspēja", + "Sign In": "Ienākt", + "You can also set a custom identity server, but you won't be able to invite users by email address, or be invited by email address yourself.": "Varat arī iestatīt pielāgotu identitātes serveri, bet jūs nevarēsiet uzaicināt lietotājus izmantojot e-pasta adresi, kā arī tikt uzaicināts pēc e-pasta adreses." } diff --git a/src/i18n/strings/nl.json b/src/i18n/strings/nl.json index 2b4e50a138..2b39f7939f 100644 --- a/src/i18n/strings/nl.json +++ b/src/i18n/strings/nl.json @@ -1771,5 +1771,35 @@ "Riot failed to get the public room list.": "Riot kon de lijst met openbare gesprekken niet verkrijgen.", "The homeserver may be unavailable or overloaded.": "De thuisserver is mogelijk onbereikbaar of overbelast.", "You have %(count)s unread notifications in a prior version of this room.|other": "U heeft %(count)s ongelezen meldingen in een voorgaande versie van dit gesprek.", - "You have %(count)s unread notifications in a prior version of this room.|one": "U heeft %(count)s ongelezen melding in een voorgaande versie van dit gesprek." + "You have %(count)s unread notifications in a prior version of this room.|one": "U heeft %(count)s ongelezen melding in een voorgaande versie van dit gesprek.", + "Whether or not you're using the 'breadcrumbs' feature (avatars above the room list)": "Of u de 'broodkruimels'-functie al dan niet gebruikt (avatars boven de gesprekslijst)", + "Replying With Files": "Beantwoorden met bestanden", + "At this time it is not possible to reply with a file. Would you like to upload this file without replying?": "Momenteel is het niet mogelijk om met een bestand te antwoorden. Wilt u dit bestand uploaden zonder te antwoorden?", + "The file '%(fileName)s' failed to upload.": "Het bestand ‘%(fileName)s’ kon niet geüpload worden.", + "Show recent room avatars above the room list": "Recente gespreksavatars weergeven boven de gesprekslijst", + "Rotate counter-clockwise": "Tegen de klok in draaien", + "Rotate clockwise": "Met de klok mee draaien", + "GitHub issue": "GitHub-melding", + "Notes": "Opmerkingen", + "If there is additional context that would help in analysing the issue, such as what you were doing at the time, room IDs, user IDs, etc., please include those things here.": "Indien er extra context zou kunnen helpen om het probleem te analyseren, zoals wat u aan het doen was, relevante gespreks-ID’s, gebruikers-ID’s, enz., gelieve deze informatie dan hier mee te geven.", + "Sign out and remove encryption keys?": "Afmelden en versleutelingssleutels verwijderen?", + "To help us prevent this in future, please send us logs.": "Gelieve ons logboeken te sturen om dit in de toekomst te helpen voorkomen.", + "Missing session data": "Sessiegegevens ontbreken", + "Some session data, including encrypted message keys, is missing. Sign out and sign in to fix this, restoring keys from backup.": "Sommige sessiegegevens, inclusief sleutels voor versleutelde berichten, ontbreken. Meld u af en weer aan om dit op te lossen, en herstel de sleutels uit de back-up.", + "Your browser likely removed this data when running low on disk space.": "Uw browser heeft deze gegevens mogelijk verwijderd toen de beschikbare opslagruimte vol was.", + "Upload files (%(current)s of %(total)s)": "Bestanden worden geüpload (%(current)s van %(total)s)", + "Upload files": "Bestanden uploaden", + "Upload": "Uploaden", + "This file is too large to upload. The file size limit is %(limit)s but this file is %(sizeOfThisFile)s.": "Dit bestand is te groot om te uploaden. De bestandsgroottelimiet is %(limit)s, maar dit bestand is %(sizeOfThisFile)s.", + "These files are too large to upload. The file size limit is %(limit)s.": "Deze bestanden zijn te groot om te uploaden. De bestandsgroottelimiet is %(limit)s.", + "Some files are too large to be uploaded. The file size limit is %(limit)s.": "Sommige bestanden zijn te groot om te uploaden. De bestandsgroottelimiet is %(limit)s.", + "Upload %(count)s other files|other": "%(count)s overige bestanden uploaden", + "Upload %(count)s other files|one": "%(count)s overig bestand uploaden", + "Cancel All": "Alles annuleren", + "Upload Error": "Uploadfout", + "A conference call could not be started because the integrations server is not available": "Daar de integratieserver onbereikbaar is kon het groepsaudiogesprek niet gestart worden.", + "The server does not support the room version specified.": "De server ondersteunt deze versie van gesprekken niet.", + "Name or Matrix ID": "Naam of Matrix-ID", + "Email, name or Matrix ID": "E-mail, naam, of matrix-ID", + "Please confirm that you'd like to go forward with upgrading this room from to .": "Bevestig dat u dit gesprek van wilt opwaarderen naar ." } diff --git a/src/i18n/strings/pl.json b/src/i18n/strings/pl.json index 840595c5f7..2e709ed636 100644 --- a/src/i18n/strings/pl.json +++ b/src/i18n/strings/pl.json @@ -128,7 +128,7 @@ "Are you sure you want to reject the invitation?": "Czy na pewno chcesz odrzucić zaproszenie?", "Are you sure you want to upload the following files?": "Czy na pewno chcesz przesłać następujące pliki?", "Autoplay GIFs and videos": "Automatycznie odtwarzaj GIFy i filmiki", - "%(senderName)s banned %(targetName)s.": "%(senderName)s zbanował %(targetName)s.", + "%(senderName)s banned %(targetName)s.": "%(senderName)s zbanował(a) %(targetName)s.", "Ban": "Zbanuj", "Bans user with given id": "Blokuje użytkownika o podanym ID", "Blacklisted": "Umieszczono na czarnej liście", @@ -456,7 +456,7 @@ "Unable to create widget.": "Nie można utworzyć widżetu.", "Unable to remove contact information": "Nie można usunąć informacji kontaktowych", "Unable to verify email address.": "Weryfikacja adresu e-mail nie powiodła się.", - "%(senderName)s unbanned %(targetName)s.": "%(senderName)s odblokował/a %(targetName)s.", + "%(senderName)s unbanned %(targetName)s.": "%(senderName)s odblokował(a) %(targetName)s.", "Unable to capture screen": "Nie można zrobić zrzutu ekranu", "Unable to enable Notifications": "Nie można włączyć powiadomień", "Unable to load device list": "Nie można załadować listy urządzeń", @@ -1337,5 +1337,109 @@ "Show read receipts": "Wyświetl potwierdzenia odczytu", "Send typing notifications": "Wyślij powiadomienia o pisaniu", "I don't want my encrypted messages": "Nie chcę moich zaszyfrowanych wiadomości", - "You'll lose access to your encrypted messages": "Utracisz dostęp do zaszyfrowanych wiadomości" + "You'll lose access to your encrypted messages": "Utracisz dostęp do zaszyfrowanych wiadomości", + "Verified!": "Zweryfikowano!", + "Dog": "Pies", + "Cat": "Kot", + "Lion": "Lew", + "Horse": "Koń", + "Unicorn": "Jednorożec", + "Pig": "Świnia", + "Elephant": "Słoń", + "Rabbit": "Królik", + "Panda": "Panda", + "Rooster": "Kogut", + "Penguin": "Pingwin", + "Turtle": "Żółw", + "Fish": "Ryba", + "Octopus": "Ośmiornica", + "Butterfly": "Motyl", + "Flower": "Kwiat", + "Tree": "Drzewo", + "Cactus": "Kaktus", + "Mushroom": "Grzyb", + "Moon": "Księżyc", + "Cloud": "Chmura", + "Fire": "Ogień", + "Banana": "Banan", + "Apple": "Jabłko", + "Strawberry": "Truskawka", + "Corn": "Kukurydza", + "Pizza": "Pizza", + "Cake": "Ciasto", + "Heart": "Serce", + "Robot": "Robot", + "Hat": "Kapelusz", + "Glasses": "Okulary", + "Umbrella": "Parasol", + "Hourglass": "Klepsydra", + "Clock": "Zegar", + "Light bulb": "Żarówka", + "Book": "Książka", + "Pencil": "Ołówek", + "Paperclip": "Spinacz", + "Scissors": "Nożyczki", + "Padlock": "Kłódka", + "Key": "Klucz", + "Telephone": "Telefon", + "Flag": "Flaga", + "Train": "Pociąg", + "Bicycle": "Rower", + "Aeroplane": "Samolot", + "Rocket": "Rakieta", + "Trophy": "Trofeum", + "Guitar": "Gitara", + "Trumpet": "Trąbka", + "Bell": "Dzwonek", + "Anchor": "Kotwica", + "Headphones": "Słuchawki", + "Folder": "Folder", + "For maximum security, we recommend you do this in person or use another trusted means of communication.": "W celu zapewnienia maksymalnego bezpieczeństwa zalecamy, abyś zrobił to osobiście lub skorzystał z innego zaufanego środka komunikacji.", + "Phone Number": "Numer telefonu", + "Display Name": "Wyświetlana nazwa", + "Set a new account password...": "Ustaw nowe hasło do konta…", + "Email addresses": "Adresy E-mail", + "Phone numbers": "Numery telefonów", + "Language and region": "Język i region", + "Theme": "Motyw", + "Account management": "Zarządzanie kontem", + "Bug reporting": "Zgłaszanie błędów", + "Versions": "Wersje", + "Preferences": "Preferencje", + "Timeline": "Oś czasu", + "Room list": "Lista pokoi", + "Security & Privacy": "Bezpieczeństwo i prywatność", + "Room Addresses": "Adresy pokoju", + "Change room avatar": "Zmień awatar pokoju", + "Change room name": "Zmień nazwę pokoju", + "Change permissions": "Zmieniać uprawnienia", + "Change topic": "Zmieniać temat", + "Default role": "Domyślna rola", + "Send messages": "Wysyłanie wiadomości", + "Change settings": "Zmieniać ustawienia", + "Remove messages": "Usuwanie wiadomości", + "Notify everyone": "Powiadamianie wszystkich", + "Roles & Permissions": "Role i uprawnienia", + "Encryption": "Szyfrowanie", + "Join the conversation with an account": "Przyłącz się do rozmowy przy użyciu konta", + "Sign Up": "Zarejestruj się", + "Join the discussion": "Dołącz do dyskusji", + "%(roomName)s can't be previewed. Do you want to join it?": "%(roomName)s nie może być wyświetlony. Chcesz do niego dołączyć?", + "Main address": "Główny adres", + "Room avatar": "Awatar pokoju", + "Upload room avatar": "Prześlij awatar pokoju", + "Room Name": "Nazwa pokoju", + "Room Topic": "Temat pokoju", + "Power level": "Poziom uprawnień", + "Verify by comparing a short text string.": "Weryfikuj, porównując krótki ciąg tekstu.", + "Begin Verifying": "Rozpocznij weryfikację", + "Waiting for partner to accept...": "Czekanie, aż partner zaakceptuje…", + "Room Settings - %(roomName)s": "Ustawienia pokoju - %(roomName)s", + "Doesn't look like a valid phone number": "To nie wygląda na poprawny numer telefonu", + "Globe": "Ziemia", + "Smiley": "Uśmiech", + "Spanner": "Klucz francuski", + "Santa": "Mikołaj", + "Gift": "Prezent", + "Hammer": "Młotek" } diff --git a/src/i18n/strings/sl.json b/src/i18n/strings/sl.json index 0967ef424b..01da138193 100644 --- a/src/i18n/strings/sl.json +++ b/src/i18n/strings/sl.json @@ -1 +1,14 @@ -{} +{ + "This email address is already in use": "Ta e-poštni naslov je že v uporabi", + "This phone number is already in use": "Ta telefonska številka je že v uporabi", + "Failed to verify email address: make sure you clicked the link in the email": "E-poštnega naslova ni bilo mogoče preveriti: preverite, ali ste kliknili povezavo v e-poštnem sporočilu", + "The platform you're on": "Vaša platforma", + "The version of Riot.im": "Različica Riot.im", + "Dismiss": "Opusti", + "Chat with Riot Bot": "Klepetajte z Riot Botom", + "Sign In": "Prijava", + "powered by Matrix": "poganja Matrix", + "Custom Server Options": "Možnosti strežnika po meri", + "You can also set a custom identity server, but you won't be able to invite users by email address, or be invited by email address yourself.": "Nastavite lahko tudi strežnik za identiteto po meri, vendar ne boste mogli povabiti uporabnikov prek e-pošte, prav tako pa vas ne bodo mogli povabiti drugi.", + "Your language of choice": "Vaš jezik po izbiri" +} diff --git a/src/i18n/strings/sv.json b/src/i18n/strings/sv.json index cb60d31e48..15673d23e4 100644 --- a/src/i18n/strings/sv.json +++ b/src/i18n/strings/sv.json @@ -380,7 +380,7 @@ "Online": "Online", "Unnamed room": "Namnlöst rum", "World readable": "Alla kan läsa", - "Guests can join": "Gäster kan bli medlem i rummet", + "Guests can join": "Gäster kan gå med i rummet", "No rooms to show": "Inga fler rum att visa", "This phone number is already in use": "Detta telefonnummer används redan", "The version of Riot.im": "Versionen av Riot.im", @@ -1607,5 +1607,59 @@ "Unable to load backup status": "Det går inte att ladda backupstatus", "Guest": "Gäst", "Could not load user profile": "Kunde inte ladda användarprofil", - "Changing your password will reset any end-to-end encryption keys on all of your devices, making encrypted chat history unreadable. Set up Key Backup or export your room keys from another device before resetting your password.": "Om du ändrar ditt lösenord återställs eventuella krypteringsnycklar på alla dina enheter, vilket gör att krypterad chatthistorik inte kan läsas. Aktivera nyckelsäkerhetskopiering eller exportera dina rumsnycklar från en annan enhet innan du återställer ditt lösenord." + "Changing your password will reset any end-to-end encryption keys on all of your devices, making encrypted chat history unreadable. Set up Key Backup or export your room keys from another device before resetting your password.": "Om du ändrar ditt lösenord återställs eventuella krypteringsnycklar på alla dina enheter, vilket gör att krypterad chatthistorik inte kan läsas. Aktivera nyckelsäkerhetskopiering eller exportera dina rumsnycklar från en annan enhet innan du återställer ditt lösenord.", + "Whether or not you're using the 'breadcrumbs' feature (avatars above the room list)": "Om du använder 'breadcrumbs' eller inte (avatarer ovanför rumslistan)", + "Replying With Files": "Svarar med filer", + "At this time it is not possible to reply with a file. Would you like to upload this file without replying?": "Just nu är det inte möjligt att svara med en fil. Vill du ladda upp filen utan att svara?", + "The file '%(fileName)s' failed to upload.": "Filen '%(fileName)s' kunde inte laddas upp.", + "Room upgrade confirmation": "Bekräfta rumsuppgradering", + "Upgrading a room can be destructive and isn't always necessary.": "Uppgradering av ett rum kan vara destruktivt och är inte alltid nödvändigt.", + "Room upgrades are usually recommended when a room version is considered unstable. Unstable room versions might have bugs, missing features, or security vulnerabilities.": "Rumsuppgraderingar rekommenderas vanligtvis när en rumversion anses vara instabil. Instabila rumsversioner kan ha fel, sakna funktioner eller ha säkerhetsproblem.", + "Room upgrades usually only affect server-side processing of the room. If you're having problems with your Riot client, please file an issue with .": "", + "Upgrade": "Uppgradera", + "Close button should minimize window to tray": "Stängknappen ska minimera fönstret till systemfältet", + "Composer": "Meddelandefält", + "Key backup": "Nyckelsäkerhetskopiering", + "Never lose encrypted messages": "Förlora aldrig krypterade meddelanden", + "Securely back up your keys to avoid losing them. Learn more.": "Säkerhetskopiera dina nycklar på ett säkert sätt för att undvika att förlora dem. Läs mer.", + "Failed to load group members": "Det gick inte att ladda gruppmedlemmar", + "Maximize apps": "Maximera appar", + "Join": "Gå med", + "Rotate counter-clockwise": "Rotera moturs", + "Rotate clockwise": "Rotera medurs", + "Power level": "Behörighetsnivå", + "Unable to find profiles for the Matrix IDs listed below - would you like to invite them anyway?": "Det gick inte att hitta profiler för de Matrix-IDn som anges nedan - vill du bjuda in dem ändå?", + "GitHub issue": "GitHub-ärende", + "Notes": "Noteringar", + "You've previously used Riot on %(host)s with lazy loading of members enabled. In this version lazy loading is disabled. As the local cache is not compatible between these two settings, Riot needs to resync your account.": "Du har tidigare använt Riot på %(host)s med lazy loading av medlemmar aktiverat. I den här versionen är lazy loading inaktiverat. Eftersom den lokala cachen inte är kompatibel mellan dessa två inställningar behöver Riot synkronisera om ditt konto.", + "If the other version of Riot is still open in another tab, please close it as using Riot on the same host with both lazy loading enabled and disabled simultaneously will cause issues.": "Om den andra versionen av Riot fortfarande är öppen i en annan flik, stäng den eftersom användning av Riot på samma värd med både lazy loading aktiverad och inaktiverad samtidigt kommer att orsaka problem.", + "Incompatible local cache": "Inkompatibel lokal cache", + "Clear cache and resync": "Töm cache och synkronisera om", + "Riot now uses 3-5x less memory, by only loading information about other users when needed. Please wait whilst we resynchronise with the server!": "Riot använder nu 3-5 gånger mindre minne, genom att bara ladda information om andra användare när det behövs. Vänta medan vi återsynkroniserar med servern!", + "I don't want my encrypted messages": "Jag vill inte ha mina krypterade meddelanden", + "Manually export keys": "Exportera nycklar manuellt", + "You'll lose access to your encrypted messages": "Du kommer att förlora åtkomst till dina krypterade meddelanden", + "Are you sure you want to sign out?": "Är du säker på att du vill logga ut?", + "If you run into any bugs or have feedback you'd like to share, please let us know on GitHub.": "Om du stöter på några fel eller har feedback du vill dela, vänligen meddela oss på GitHub.", + "To help avoid duplicate issues, please view existing issues first (and add a +1) or create a new issue if you can't find it.": "För att undvika dubbla ärenden, vänligen granska befintliga ärenden först (och lägg till +1) eller skapa ett nytt ärende om du inte hittar det.", + "Report bugs & give feedback": "Rapportera fel och ge feedback", + "Go back": "Gå tillbaka", + "Room Settings - %(roomName)s": "Rumsinställningar - %(roomName)s", + "Sign out and remove encryption keys?": "Logga ut och ta bort krypteringsnycklar?", + "A username can only contain lower case letters, numbers and '=_-./'": "Ett användarnamn får endast innehålla små bokstäver, siffror och '=_-./'", + "Checking...": "Kontrollerar...", + "To help us prevent this in future, please send us logs.": "För att hjälpa oss att förhindra detta i framtiden, vänligen skicka oss loggar.", + "Missing session data": "Sessionsdata saknas", + "Some session data, including encrypted message keys, is missing. Sign out and sign in to fix this, restoring keys from backup.": "Vissa sessionsdata, inklusive krypteringsnycklar för meddelanden, saknas. Logga ut och logga in för att åtgärda detta genom återställning av nycklarna från säkerhetskopia.", + "Your browser likely removed this data when running low on disk space.": "Din webbläsare har troligen tagit bort dessa data när det blev ont om diskutrymme.", + "Upload files (%(current)s of %(total)s)": "Ladda upp filer (%(current)s av %(total)s)", + "Upload files": "Ladda upp filer", + "Upload": "Ladda upp", + "This file is too large to upload. The file size limit is %(limit)s but this file is %(sizeOfThisFile)s.": "Den här filen är för stor för att ladda upp. Filstorleksgränsen är %(limit)s men den här filen är %(sizeOfThisFile)s.", + "These files are too large to upload. The file size limit is %(limit)s.": "Dessa filer är för stora för att laddas upp. Filstorleksgränsen är %(limit)s.", + "Some files are too large to be uploaded. The file size limit is %(limit)s.": "Vissa filer är för stora för att laddas upp. Filstorleksgränsen är %(limit)s.", + "Upload %(count)s other files|other": "Ladda upp %(count)s andra filer", + "Upload %(count)s other files|one": "Ladda upp %(count)s annan fil", + "Cancel All": "Avbryt alla", + "Upload Error": "Uppladdningsfel" } diff --git a/src/i18n/strings/zh_Hant.json b/src/i18n/strings/zh_Hant.json index bb3e1fd84a..a50182149a 100644 --- a/src/i18n/strings/zh_Hant.json +++ b/src/i18n/strings/zh_Hant.json @@ -1862,5 +1862,30 @@ "Riot failed to get the public room list.": "Riot 取得公開聊天室清單失敗。", "The homeserver may be unavailable or overloaded.": "家伺服器似乎不可用或超載。", "You have %(count)s unread notifications in a prior version of this room.|other": "您在此聊天室的先前版本有 %(count)s 個未讀的通知。", - "You have %(count)s unread notifications in a prior version of this room.|one": "您在此聊天室的先前版本有 %(count)s 個未讀的通知。" + "You have %(count)s unread notifications in a prior version of this room.|one": "您在此聊天室的先前版本有 %(count)s 個未讀的通知。", + "Whether or not you're using the 'breadcrumbs' feature (avatars above the room list)": "不論您是否使用「麵包屑」功能(大頭貼在聊天室清單上)", + "Replying With Files": "以檔案回覆", + "At this time it is not possible to reply with a file. Would you like to upload this file without replying?": "此時無法使用檔案回覆。您想要上傳此檔案而不回覆嗎?", + "The file '%(fileName)s' failed to upload.": "檔案「%(fileName)s」上傳失敗。", + "Show recent room avatars above the room list": "在聊天室清單上顯示聊天室大頭貼", + "Rotate counter-clockwise": "逆時針旋轉", + "Rotate clockwise": "順時針旋轉", + "GitHub issue": "GitHub 議題", + "Notes": "註記", + "If there is additional context that would help in analysing the issue, such as what you were doing at the time, room IDs, user IDs, etc., please include those things here.": "如果有其他有助於釐清問題的情境,如您當時正在做什麼,聊天室 ID、使用者 ID 等等,請在這裡加入這些資訊。", + "Sign out and remove encryption keys?": "登出並移除加密金鑰?", + "To help us prevent this in future, please send us logs.": "要協助我們讓這個問題不再發生,請將紀錄檔傳送給我們。", + "Missing session data": "遺失工作階段資料", + "Some session data, including encrypted message keys, is missing. Sign out and sign in to fix this, restoring keys from backup.": "某些工作階段資料遺失了,其中包含加密訊息金鑰。登出再登入並從備份中復原金鑰可以修復這個問題。", + "Your browser likely removed this data when running low on disk space.": "當硬碟空間不足時,您的瀏覽器可能會移除這些資料。", + "Upload files (%(current)s of %(total)s)": "上傳檔案 (%(total)s 中的 %(current)s)", + "Upload files": "上傳檔案", + "Upload": "上傳", + "This file is too large to upload. The file size limit is %(limit)s but this file is %(sizeOfThisFile)s.": "這個檔案太大了,所以沒辦法上傳。檔案大小限制為 %(limit)s 但這個檔案大小是 %(sizeOfThisFile)s。", + "These files are too large to upload. The file size limit is %(limit)s.": "這些檔案太大了,所以沒辦法上傳。檔案大小限制為 %(limit)s。", + "Some files are too large to be uploaded. The file size limit is %(limit)s.": "某些檔案太大了,所以沒辦法上傳。檔案大小限制為 %(limit)s。", + "Upload %(count)s other files|other": "上傳 %(count)s 個其他檔案", + "Upload %(count)s other files|one": "上傳 %(count)s 個其他檔案", + "Cancel All": "取消全部", + "Upload Error": "上傳錯誤" } diff --git a/src/languageHandler.js b/src/languageHandler.js index 854ac079bc..267d62a7bb 100644 --- a/src/languageHandler.js +++ b/src/languageHandler.js @@ -32,6 +32,18 @@ counterpart.setSeparator('|'); // Fall back to English counterpart.setFallbackLocale('en'); +/** + * Helper function to create an error which has an English message + * with a translatedMessage property for use by the consumer. + * @param {string} message Message to translate. + * @returns {Error} The constructed error. + */ +export function newTranslatableError(message) { + const error = new Error(message); + error.translatedMessage = _t(message); + return error; +} + // Function which only purpose is to mark that a string is translatable // Does not actually do anything. It's helpful for automatic extraction of translatable strings export function _td(s) { @@ -125,20 +137,25 @@ export function _t(text, variables, tags) { * @return a React component if any non-strings were used in substitutions, otherwise a string */ export function substitute(text, variables, tags) { - const regexpMapping = {}; + let result = text; if (variables !== undefined) { + const regexpMapping = {}; for (const variable in variables) { regexpMapping[`%\\(${variable}\\)s`] = variables[variable]; } + result = replaceByRegexes(result, regexpMapping); } if (tags !== undefined) { + const regexpMapping = {}; for (const tag in tags) { regexpMapping[`(<${tag}>(.*?)<\\/${tag}>|<${tag}>|<${tag}\\s*\\/>)`] = tags[tag]; } + result = replaceByRegexes(result, regexpMapping); } - return replaceByRegexes(text, regexpMapping); + + return result; } /* diff --git a/src/settings/Settings.js b/src/settings/Settings.js index 0efc10233f..1b2096b5d3 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -23,6 +23,7 @@ import { } from "./controllers/NotificationControllers"; import CustomStatusController from "./controllers/CustomStatusController"; import ThemeController from './controllers/ThemeController'; +import LowBandwidthController from "./controllers/LowBandwidthController"; // These are just a bunch of helper arrays to avoid copy/pasting a bunch of times const LEVELS_ROOM_SETTINGS = ['device', 'room-device', 'room-account', 'account', 'config']; @@ -377,4 +378,15 @@ export const SETTINGS = { displayName: _td('Order rooms in the room list by most important first instead of most recent'), default: true, }, + "showHiddenEventsInTimeline": { + displayName: _td("Show hidden events in timeline"), + supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, + default: false, + }, + "lowBandwidth": { + supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG, + displayName: _td('Low bandwidth mode'), + default: false, + controller: new LowBandwidthController(), + }, }; diff --git a/src/settings/controllers/LowBandwidthController.js b/src/settings/controllers/LowBandwidthController.js new file mode 100644 index 0000000000..c7796a425a --- /dev/null +++ b/src/settings/controllers/LowBandwidthController.js @@ -0,0 +1,24 @@ +/* +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 SettingController from "./SettingController"; +import PlatformPeg from "../../PlatformPeg"; + +export default class LowBandwidthController extends SettingController { + onChange(level, roomId, newValue) { + PlatformPeg.get().reload(); + } +} diff --git a/src/stripped-emoji.json b/src/stripped-emoji.json index 16b772ae51..a9debd7f95 100644 --- a/src/stripped-emoji.json +++ b/src/stripped-emoji.json @@ -1 +1 @@ -[{"name":"hundred points symbol","shortname":":100:","category":"symbols","emoji_order":"2119"},{"name":"input symbol for numbers","shortname":":1234:","category":"symbols","emoji_order":"2122"},{"name":"grinning face","shortname":":grinning:","category":"people","emoji_order":"1"},{"name":"grinning face with smiling eyes","shortname":":grin:","category":"people","emoji_order":"2"},{"name":"face with tears of joy","shortname":":joy:","category":"people","emoji_order":"3","aliases_ascii":[":')",":'-)"]},{"name":"rolling on the floor laughing","shortname":":rofl:","category":"people","emoji_order":"4","aliases":[":rolling_on_the_floor_laughing:"]},{"name":"smiling face with open mouth","shortname":":smiley:","category":"people","emoji_order":"5","aliases_ascii":[":D",":-D","=D"]},{"name":"smiling face with open mouth and smiling eyes","shortname":":smile:","category":"people","emoji_order":"6"},{"name":"smiling face with open mouth and cold sweat","shortname":":sweat_smile:","category":"people","emoji_order":"7","aliases_ascii":["':)","':-)","'=)","':D","':-D","'=D"]},{"name":"smiling face with open mouth and tightly-closed eyes","shortname":":laughing:","category":"people","emoji_order":"8","aliases":[":satisfied:"],"aliases_ascii":[">:)",">;)",">:-)",">=)"]},{"name":"winking face","shortname":":wink:","category":"people","emoji_order":"9","aliases_ascii":[";)",";-)","*-)","*)",";-]",";]",";D",";^)"]},{"name":"smiling face with smiling eyes","shortname":":blush:","category":"people","emoji_order":"10"},{"name":"face savouring delicious food","shortname":":yum:","category":"people","emoji_order":"11"},{"name":"smiling face with sunglasses","shortname":":sunglasses:","category":"people","emoji_order":"12","aliases_ascii":["B-)","B)","8)","8-)","B-D","8-D"]},{"name":"smiling face with heart-shaped eyes","shortname":":heart_eyes:","category":"people","emoji_order":"13"},{"name":"face throwing a kiss","shortname":":kissing_heart:","category":"people","emoji_order":"14","aliases_ascii":[":*",":-*","=*",":^*"]},{"name":"kissing face","shortname":":kissing:","category":"people","emoji_order":"15"},{"name":"kissing face with smiling eyes","shortname":":kissing_smiling_eyes:","category":"people","emoji_order":"16"},{"name":"kissing face with closed eyes","shortname":":kissing_closed_eyes:","category":"people","emoji_order":"17"},{"name":"white smiling face","shortname":":relaxed:","category":"people","emoji_order":"18"},{"name":"slightly smiling face","shortname":":slight_smile:","category":"people","emoji_order":"19","aliases":[":slightly_smiling_face:"],"aliases_ascii":[":)",":-)","=]","=)",":]"]},{"name":"hugging face","shortname":":hugging:","category":"people","emoji_order":"20","aliases":[":hugging_face:"]},{"name":"thinking face","shortname":":thinking:","category":"people","emoji_order":"21","aliases":[":thinking_face:"]},{"name":"neutral face","shortname":":neutral_face:","category":"people","emoji_order":"22"},{"name":"expressionless face","shortname":":expressionless:","category":"people","emoji_order":"23","aliases_ascii":["-_-","-__-","-___-"]},{"name":"face without mouth","shortname":":no_mouth:","category":"people","emoji_order":"24","aliases_ascii":[":-X",":X",":-#",":#","=X","=x",":x",":-x","=#"]},{"name":"face with rolling eyes","shortname":":rolling_eyes:","category":"people","emoji_order":"25","aliases":[":face_with_rolling_eyes:"]},{"name":"smirking face","shortname":":smirk:","category":"people","emoji_order":"26"},{"name":"persevering face","shortname":":persevere:","category":"people","emoji_order":"27","aliases_ascii":[">.<"]},{"name":"disappointed but relieved face","shortname":":disappointed_relieved:","category":"people","emoji_order":"28"},{"name":"face with open mouth","shortname":":open_mouth:","category":"people","emoji_order":"29","aliases_ascii":[":-O",":O",":-o",":o","O_O",">:O"]},{"name":"zipper-mouth face","shortname":":zipper_mouth:","category":"people","emoji_order":"30","aliases":[":zipper_mouth_face:"]},{"name":"hushed face","shortname":":hushed:","category":"people","emoji_order":"31"},{"name":"sleepy face","shortname":":sleepy:","category":"people","emoji_order":"32"},{"name":"tired face","shortname":":tired_face:","category":"people","emoji_order":"33"},{"name":"sleeping face","shortname":":sleeping:","category":"people","emoji_order":"34"},{"name":"relieved face","shortname":":relieved:","category":"people","emoji_order":"35"},{"name":"nerd face","shortname":":nerd:","category":"people","emoji_order":"36","aliases":[":nerd_face:"]},{"name":"face with stuck-out tongue","shortname":":stuck_out_tongue:","category":"people","emoji_order":"37","aliases_ascii":[":P",":-P","=P",":-p",":p","=p",":-Þ",":Þ",":þ",":-þ",":-b",":b","d:"]},{"name":"face with stuck-out tongue and winking eye","shortname":":stuck_out_tongue_winking_eye:","category":"people","emoji_order":"38","aliases_ascii":[">:P","X-P","x-p"]},{"name":"face with stuck-out tongue and tightly-closed eyes","shortname":":stuck_out_tongue_closed_eyes:","category":"people","emoji_order":"39"},{"name":"drooling face","shortname":":drooling_face:","category":"people","emoji_order":"40","aliases":[":drool:"]},{"name":"unamused face","shortname":":unamused:","category":"people","emoji_order":"41"},{"name":"face with cold sweat","shortname":":sweat:","category":"people","emoji_order":"42","aliases_ascii":["':(","':-(","'=("]},{"name":"pensive face","shortname":":pensive:","category":"people","emoji_order":"43"},{"name":"confused face","shortname":":confused:","category":"people","emoji_order":"44","aliases_ascii":[">:\\",">:/",":-/",":-.",":/",":\\","=/","=\\",":L","=L"]},{"name":"upside-down face","shortname":":upside_down:","category":"people","emoji_order":"45","aliases":[":upside_down_face:"]},{"name":"money-mouth face","shortname":":money_mouth:","category":"people","emoji_order":"46","aliases":[":money_mouth_face:"]},{"name":"astonished face","shortname":":astonished:","category":"people","emoji_order":"47"},{"name":"white frowning face","shortname":":frowning2:","category":"people","emoji_order":"48","aliases":[":white_frowning_face:"]},{"name":"slightly frowning face","shortname":":slight_frown:","category":"people","emoji_order":"49","aliases":[":slightly_frowning_face:"]},{"name":"confounded face","shortname":":confounded:","category":"people","emoji_order":"50"},{"name":"disappointed face","shortname":":disappointed:","category":"people","emoji_order":"51","aliases_ascii":[">:[",":-(",":(",":-[",":[","=("]},{"name":"worried face","shortname":":worried:","category":"people","emoji_order":"52"},{"name":"face with look of triumph","shortname":":triumph:","category":"people","emoji_order":"53"},{"name":"crying face","shortname":":cry:","category":"people","emoji_order":"54","aliases_ascii":[":'(",":'-(",";(",";-("]},{"name":"loudly crying face","shortname":":sob:","category":"people","emoji_order":"55"},{"name":"frowning face with open mouth","shortname":":frowning:","category":"people","emoji_order":"56"},{"name":"anguished face","shortname":":anguished:","category":"people","emoji_order":"57"},{"name":"fearful face","shortname":":fearful:","category":"people","emoji_order":"58","aliases_ascii":["D:"]},{"name":"weary face","shortname":":weary:","category":"people","emoji_order":"59"},{"name":"grimacing face","shortname":":grimacing:","category":"people","emoji_order":"60"},{"name":"face with open mouth and cold sweat","shortname":":cold_sweat:","category":"people","emoji_order":"61"},{"name":"face screaming in fear","shortname":":scream:","category":"people","emoji_order":"62"},{"name":"flushed face","shortname":":flushed:","category":"people","emoji_order":"63","aliases_ascii":[":$","=$"]},{"name":"dizzy face","shortname":":dizzy_face:","category":"people","emoji_order":"64","aliases_ascii":["#-)","#)","%-)","%)","X)","X-)"]},{"name":"pouting face","shortname":":rage:","category":"people","emoji_order":"65"},{"name":"angry face","shortname":":angry:","category":"people","emoji_order":"66","aliases_ascii":[">:(",">:-(",":@"]},{"name":"smiling face with halo","shortname":":innocent:","category":"people","emoji_order":"67","aliases_ascii":["O:-)","0:-3","0:3","0:-)","0:)","0;^)","O:)","O;-)","O=)","0;-)","O:-3","O:3"]},{"name":"face with cowboy hat","shortname":":cowboy:","category":"people","emoji_order":"68","aliases":[":face_with_cowboy_hat:"]},{"name":"clown face","shortname":":clown:","category":"people","emoji_order":"69","aliases":[":clown_face:"]},{"name":"lying face","shortname":":lying_face:","category":"people","emoji_order":"70","aliases":[":liar:"]},{"name":"face with medical mask","shortname":":mask:","category":"people","emoji_order":"71"},{"name":"face with thermometer","shortname":":thermometer_face:","category":"people","emoji_order":"72","aliases":[":face_with_thermometer:"]},{"name":"face with head-bandage","shortname":":head_bandage:","category":"people","emoji_order":"73","aliases":[":face_with_head_bandage:"]},{"name":"nauseated face","shortname":":nauseated_face:","category":"people","emoji_order":"74","aliases":[":sick:"]},{"name":"sneezing face","shortname":":sneezing_face:","category":"people","emoji_order":"75","aliases":[":sneeze:"]},{"name":"smiling face with horns","shortname":":smiling_imp:","category":"people","emoji_order":"76"},{"name":"imp","shortname":":imp:","category":"people","emoji_order":"77"},{"name":"japanese ogre","shortname":":japanese_ogre:","category":"people","emoji_order":"78"},{"name":"japanese goblin","shortname":":japanese_goblin:","category":"people","emoji_order":"79"},{"name":"skull","shortname":":skull:","category":"people","emoji_order":"80","aliases":[":skeleton:"]},{"name":"skull and crossbones","shortname":":skull_crossbones:","category":"objects","emoji_order":"81","aliases":[":skull_and_crossbones:"]},{"name":"ghost","shortname":":ghost:","category":"people","emoji_order":"82"},{"name":"extraterrestrial alien","shortname":":alien:","category":"people","emoji_order":"83"},{"name":"alien monster","shortname":":space_invader:","category":"activity","emoji_order":"84"},{"name":"robot face","shortname":":robot:","category":"people","emoji_order":"85","aliases":[":robot_face:"]},{"name":"pile of poo","shortname":":poop:","category":"people","emoji_order":"86","aliases":[":shit:",":hankey:",":poo:"]},{"name":"smiling cat face with open mouth","shortname":":smiley_cat:","category":"people","emoji_order":"87"},{"name":"grinning cat face with smiling eyes","shortname":":smile_cat:","category":"people","emoji_order":"88"},{"name":"cat face with tears of joy","shortname":":joy_cat:","category":"people","emoji_order":"89"},{"name":"smiling cat face with heart-shaped eyes","shortname":":heart_eyes_cat:","category":"people","emoji_order":"90"},{"name":"cat face with wry smile","shortname":":smirk_cat:","category":"people","emoji_order":"91"},{"name":"kissing cat face with closed eyes","shortname":":kissing_cat:","category":"people","emoji_order":"92"},{"name":"weary cat face","shortname":":scream_cat:","category":"people","emoji_order":"93"},{"name":"crying cat face","shortname":":crying_cat_face:","category":"people","emoji_order":"94"},{"name":"pouting cat face","shortname":":pouting_cat:","category":"people","emoji_order":"95"},{"name":"see-no-evil monkey","shortname":":see_no_evil:","category":"nature","emoji_order":"96"},{"name":"hear-no-evil monkey","shortname":":hear_no_evil:","category":"nature","emoji_order":"97"},{"name":"speak-no-evil monkey","shortname":":speak_no_evil:","category":"nature","emoji_order":"98"},{"name":"boy","shortname":":boy:","category":"people","emoji_order":"99"},{"name":"boy tone 1","shortname":":boy_tone1:","category":"people","emoji_order":"100"},{"name":"boy tone 2","shortname":":boy_tone2:","category":"people","emoji_order":"101"},{"name":"boy tone 3","shortname":":boy_tone3:","category":"people","emoji_order":"102"},{"name":"boy tone 4","shortname":":boy_tone4:","category":"people","emoji_order":"103"},{"name":"boy tone 5","shortname":":boy_tone5:","category":"people","emoji_order":"104"},{"name":"girl","shortname":":girl:","category":"people","emoji_order":"105"},{"name":"girl tone 1","shortname":":girl_tone1:","category":"people","emoji_order":"106"},{"name":"girl tone 2","shortname":":girl_tone2:","category":"people","emoji_order":"107"},{"name":"girl tone 3","shortname":":girl_tone3:","category":"people","emoji_order":"108"},{"name":"girl tone 4","shortname":":girl_tone4:","category":"people","emoji_order":"109"},{"name":"girl tone 5","shortname":":girl_tone5:","category":"people","emoji_order":"110"},{"name":"man","shortname":":man:","category":"people","emoji_order":"111"},{"name":"man tone 1","shortname":":man_tone1:","category":"people","emoji_order":"112"},{"name":"man tone 2","shortname":":man_tone2:","category":"people","emoji_order":"113"},{"name":"man tone 3","shortname":":man_tone3:","category":"people","emoji_order":"114"},{"name":"man tone 4","shortname":":man_tone4:","category":"people","emoji_order":"115"},{"name":"man tone 5","shortname":":man_tone5:","category":"people","emoji_order":"116"},{"name":"woman","shortname":":woman:","category":"people","emoji_order":"117"},{"name":"woman tone 1","shortname":":woman_tone1:","category":"people","emoji_order":"118"},{"name":"woman tone 2","shortname":":woman_tone2:","category":"people","emoji_order":"119"},{"name":"woman tone 3","shortname":":woman_tone3:","category":"people","emoji_order":"120"},{"name":"woman tone 4","shortname":":woman_tone4:","category":"people","emoji_order":"121"},{"name":"woman tone 5","shortname":":woman_tone5:","category":"people","emoji_order":"122"},{"name":"older man","shortname":":older_man:","category":"people","emoji_order":"123"},{"name":"older man tone 1","shortname":":older_man_tone1:","category":"people","emoji_order":"124"},{"name":"older man tone 2","shortname":":older_man_tone2:","category":"people","emoji_order":"125"},{"name":"older man tone 3","shortname":":older_man_tone3:","category":"people","emoji_order":"126"},{"name":"older man tone 4","shortname":":older_man_tone4:","category":"people","emoji_order":"127"},{"name":"older man tone 5","shortname":":older_man_tone5:","category":"people","emoji_order":"128"},{"name":"older woman","shortname":":older_woman:","category":"people","emoji_order":"129","aliases":[":grandma:"]},{"name":"older woman tone 1","shortname":":older_woman_tone1:","category":"people","emoji_order":"130","aliases":[":grandma_tone1:"]},{"name":"older woman tone 2","shortname":":older_woman_tone2:","category":"people","emoji_order":"131","aliases":[":grandma_tone2:"]},{"name":"older woman tone 3","shortname":":older_woman_tone3:","category":"people","emoji_order":"132","aliases":[":grandma_tone3:"]},{"name":"older woman tone 4","shortname":":older_woman_tone4:","category":"people","emoji_order":"133","aliases":[":grandma_tone4:"]},{"name":"older woman tone 5","shortname":":older_woman_tone5:","category":"people","emoji_order":"134","aliases":[":grandma_tone5:"]},{"name":"baby","shortname":":baby:","category":"people","emoji_order":"135"},{"name":"baby tone 1","shortname":":baby_tone1:","category":"people","emoji_order":"136"},{"name":"baby tone 2","shortname":":baby_tone2:","category":"people","emoji_order":"137"},{"name":"baby tone 3","shortname":":baby_tone3:","category":"people","emoji_order":"138"},{"name":"baby tone 4","shortname":":baby_tone4:","category":"people","emoji_order":"139"},{"name":"baby tone 5","shortname":":baby_tone5:","category":"people","emoji_order":"140"},{"name":"baby angel","shortname":":angel:","category":"people","emoji_order":"141"},{"name":"baby angel tone 1","shortname":":angel_tone1:","category":"people","emoji_order":"142"},{"name":"baby angel tone 2","shortname":":angel_tone2:","category":"people","emoji_order":"143"},{"name":"baby angel tone 3","shortname":":angel_tone3:","category":"people","emoji_order":"144"},{"name":"baby angel tone 4","shortname":":angel_tone4:","category":"people","emoji_order":"145"},{"name":"baby angel tone 5","shortname":":angel_tone5:","category":"people","emoji_order":"146"},{"name":"police officer","shortname":":cop:","category":"people","emoji_order":"339"},{"name":"police officer tone 1","shortname":":cop_tone1:","category":"people","emoji_order":"340"},{"name":"police officer tone 2","shortname":":cop_tone2:","category":"people","emoji_order":"341"},{"name":"police officer tone 3","shortname":":cop_tone3:","category":"people","emoji_order":"342"},{"name":"police officer tone 4","shortname":":cop_tone4:","category":"people","emoji_order":"343"},{"name":"police officer tone 5","shortname":":cop_tone5:","category":"people","emoji_order":"344"},{"name":"sleuth or spy","shortname":":spy:","category":"people","emoji_order":"357","aliases":[":sleuth_or_spy:"]},{"name":"sleuth or spy tone 1","shortname":":spy_tone1:","category":"people","emoji_order":"358","aliases":[":sleuth_or_spy_tone1:"]},{"name":"sleuth or spy tone 2","shortname":":spy_tone2:","category":"people","emoji_order":"359","aliases":[":sleuth_or_spy_tone2:"]},{"name":"sleuth or spy tone 3","shortname":":spy_tone3:","category":"people","emoji_order":"360","aliases":[":sleuth_or_spy_tone3:"]},{"name":"sleuth or spy tone 4","shortname":":spy_tone4:","category":"people","emoji_order":"361","aliases":[":sleuth_or_spy_tone4:"]},{"name":"sleuth or spy tone 5","shortname":":spy_tone5:","category":"people","emoji_order":"362","aliases":[":sleuth_or_spy_tone5:"]},{"name":"guardsman","shortname":":guardsman:","category":"people","emoji_order":"375"},{"name":"guardsman tone 1","shortname":":guardsman_tone1:","category":"people","emoji_order":"376"},{"name":"guardsman tone 2","shortname":":guardsman_tone2:","category":"people","emoji_order":"377"},{"name":"guardsman tone 3","shortname":":guardsman_tone3:","category":"people","emoji_order":"378"},{"name":"guardsman tone 4","shortname":":guardsman_tone4:","category":"people","emoji_order":"379"},{"name":"guardsman tone 5","shortname":":guardsman_tone5:","category":"people","emoji_order":"380"},{"name":"construction worker","shortname":":construction_worker:","category":"people","emoji_order":"393"},{"name":"construction worker tone 1","shortname":":construction_worker_tone1:","category":"people","emoji_order":"394"},{"name":"construction worker tone 2","shortname":":construction_worker_tone2:","category":"people","emoji_order":"395"},{"name":"construction worker tone 3","shortname":":construction_worker_tone3:","category":"people","emoji_order":"396"},{"name":"construction worker tone 4","shortname":":construction_worker_tone4:","category":"people","emoji_order":"397"},{"name":"construction worker tone 5","shortname":":construction_worker_tone5:","category":"people","emoji_order":"398"},{"name":"man with turban","shortname":":man_with_turban:","category":"people","emoji_order":"411"},{"name":"man with turban tone 1","shortname":":man_with_turban_tone1:","category":"people","emoji_order":"412"},{"name":"man with turban tone 2","shortname":":man_with_turban_tone2:","category":"people","emoji_order":"413"},{"name":"man with turban tone 3","shortname":":man_with_turban_tone3:","category":"people","emoji_order":"414"},{"name":"man with turban tone 4","shortname":":man_with_turban_tone4:","category":"people","emoji_order":"415"},{"name":"man with turban tone 5","shortname":":man_with_turban_tone5:","category":"people","emoji_order":"416"},{"name":"person with blond hair","shortname":":person_with_blond_hair:","category":"people","emoji_order":"429"},{"name":"person with blond hair tone 1","shortname":":person_with_blond_hair_tone1:","category":"people","emoji_order":"430"},{"name":"person with blond hair tone 2","shortname":":person_with_blond_hair_tone2:","category":"people","emoji_order":"431"},{"name":"person with blond hair tone 3","shortname":":person_with_blond_hair_tone3:","category":"people","emoji_order":"432"},{"name":"person with blond hair tone 4","shortname":":person_with_blond_hair_tone4:","category":"people","emoji_order":"433"},{"name":"person with blond hair tone 5","shortname":":person_with_blond_hair_tone5:","category":"people","emoji_order":"434"},{"name":"father christmas","shortname":":santa:","category":"people","emoji_order":"447"},{"name":"father christmas tone 1","shortname":":santa_tone1:","category":"people","emoji_order":"448"},{"name":"father christmas tone 2","shortname":":santa_tone2:","category":"people","emoji_order":"449"},{"name":"father christmas tone 3","shortname":":santa_tone3:","category":"people","emoji_order":"450"},{"name":"father christmas tone 4","shortname":":santa_tone4:","category":"people","emoji_order":"451"},{"name":"father christmas tone 5","shortname":":santa_tone5:","category":"people","emoji_order":"452"},{"name":"mother christmas","shortname":":mrs_claus:","category":"people","emoji_order":"453","aliases":[":mother_christmas:"]},{"name":"mother christmas tone 1","shortname":":mrs_claus_tone1:","category":"people","emoji_order":"454","aliases":[":mother_christmas_tone1:"]},{"name":"mother christmas tone 2","shortname":":mrs_claus_tone2:","category":"people","emoji_order":"455","aliases":[":mother_christmas_tone2:"]},{"name":"mother christmas tone 3","shortname":":mrs_claus_tone3:","category":"people","emoji_order":"456","aliases":[":mother_christmas_tone3:"]},{"name":"mother christmas tone 4","shortname":":mrs_claus_tone4:","category":"people","emoji_order":"457","aliases":[":mother_christmas_tone4:"]},{"name":"mother christmas tone 5","shortname":":mrs_claus_tone5:","category":"people","emoji_order":"458","aliases":[":mother_christmas_tone5:"]},{"name":"princess","shortname":":princess:","category":"people","emoji_order":"459"},{"name":"princess tone 1","shortname":":princess_tone1:","category":"people","emoji_order":"460"},{"name":"princess tone 2","shortname":":princess_tone2:","category":"people","emoji_order":"461"},{"name":"princess tone 3","shortname":":princess_tone3:","category":"people","emoji_order":"462"},{"name":"princess tone 4","shortname":":princess_tone4:","category":"people","emoji_order":"463"},{"name":"princess tone 5","shortname":":princess_tone5:","category":"people","emoji_order":"464"},{"name":"prince","shortname":":prince:","category":"people","emoji_order":"465"},{"name":"prince tone 1","shortname":":prince_tone1:","category":"people","emoji_order":"466"},{"name":"prince tone 2","shortname":":prince_tone2:","category":"people","emoji_order":"467"},{"name":"prince tone 3","shortname":":prince_tone3:","category":"people","emoji_order":"468"},{"name":"prince tone 4","shortname":":prince_tone4:","category":"people","emoji_order":"469"},{"name":"prince tone 5","shortname":":prince_tone5:","category":"people","emoji_order":"470"},{"name":"bride with veil","shortname":":bride_with_veil:","category":"people","emoji_order":"471"},{"name":"bride with veil tone 1","shortname":":bride_with_veil_tone1:","category":"people","emoji_order":"472"},{"name":"bride with veil tone 2","shortname":":bride_with_veil_tone2:","category":"people","emoji_order":"473"},{"name":"bride with veil tone 3","shortname":":bride_with_veil_tone3:","category":"people","emoji_order":"474"},{"name":"bride with veil tone 4","shortname":":bride_with_veil_tone4:","category":"people","emoji_order":"475"},{"name":"bride with veil tone 5","shortname":":bride_with_veil_tone5:","category":"people","emoji_order":"476"},{"name":"man in tuxedo","shortname":":man_in_tuxedo:","category":"people","emoji_order":"477"},{"name":"man in tuxedo tone 1","shortname":":man_in_tuxedo_tone1:","category":"people","emoji_order":"478","aliases":[":tuxedo_tone1:"]},{"name":"man in tuxedo tone 2","shortname":":man_in_tuxedo_tone2:","category":"people","emoji_order":"479","aliases":[":tuxedo_tone2:"]},{"name":"man in tuxedo tone 3","shortname":":man_in_tuxedo_tone3:","category":"people","emoji_order":"480","aliases":[":tuxedo_tone3:"]},{"name":"man in tuxedo tone 4","shortname":":man_in_tuxedo_tone4:","category":"people","emoji_order":"481","aliases":[":tuxedo_tone4:"]},{"name":"man in tuxedo tone 5","shortname":":man_in_tuxedo_tone5:","category":"people","emoji_order":"482","aliases":[":tuxedo_tone5:"]},{"name":"pregnant woman","shortname":":pregnant_woman:","category":"people","emoji_order":"483","aliases":[":expecting_woman:"]},{"name":"pregnant woman tone 1","shortname":":pregnant_woman_tone1:","category":"people","emoji_order":"484","aliases":[":expecting_woman_tone1:"]},{"name":"pregnant woman tone 2","shortname":":pregnant_woman_tone2:","category":"people","emoji_order":"485","aliases":[":expecting_woman_tone2:"]},{"name":"pregnant woman tone 3","shortname":":pregnant_woman_tone3:","category":"people","emoji_order":"486","aliases":[":expecting_woman_tone3:"]},{"name":"pregnant woman tone 4","shortname":":pregnant_woman_tone4:","category":"people","emoji_order":"487","aliases":[":expecting_woman_tone4:"]},{"name":"pregnant woman tone 5","shortname":":pregnant_woman_tone5:","category":"people","emoji_order":"488","aliases":[":expecting_woman_tone5:"]},{"name":"man with gua pi mao","shortname":":man_with_gua_pi_mao:","category":"people","emoji_order":"489"},{"name":"man with gua pi mao tone 1","shortname":":man_with_gua_pi_mao_tone1:","category":"people","emoji_order":"490"},{"name":"man with gua pi mao tone 2","shortname":":man_with_gua_pi_mao_tone2:","category":"people","emoji_order":"491"},{"name":"man with gua pi mao tone 3","shortname":":man_with_gua_pi_mao_tone3:","category":"people","emoji_order":"492"},{"name":"man with gua pi mao tone 4","shortname":":man_with_gua_pi_mao_tone4:","category":"people","emoji_order":"493"},{"name":"man with gua pi mao tone 5","shortname":":man_with_gua_pi_mao_tone5:","category":"people","emoji_order":"494"},{"name":"person frowning","shortname":":person_frowning:","category":"people","emoji_order":"495"},{"name":"person frowning tone 1","shortname":":person_frowning_tone1:","category":"people","emoji_order":"496"},{"name":"person frowning tone 2","shortname":":person_frowning_tone2:","category":"people","emoji_order":"497"},{"name":"person frowning tone 3","shortname":":person_frowning_tone3:","category":"people","emoji_order":"498"},{"name":"person frowning tone 4","shortname":":person_frowning_tone4:","category":"people","emoji_order":"499"},{"name":"person frowning tone 5","shortname":":person_frowning_tone5:","category":"people","emoji_order":"500"},{"name":"person with pouting face","shortname":":person_with_pouting_face:","category":"people","emoji_order":"513"},{"name":"person with pouting face tone1","shortname":":person_with_pouting_face_tone1:","category":"people","emoji_order":"514"},{"name":"person with pouting face tone2","shortname":":person_with_pouting_face_tone2:","category":"people","emoji_order":"515"},{"name":"person with pouting face tone3","shortname":":person_with_pouting_face_tone3:","category":"people","emoji_order":"516"},{"name":"person with pouting face tone4","shortname":":person_with_pouting_face_tone4:","category":"people","emoji_order":"517"},{"name":"person with pouting face tone5","shortname":":person_with_pouting_face_tone5:","category":"people","emoji_order":"518"},{"name":"face with no good gesture","shortname":":no_good:","category":"people","emoji_order":"531"},{"name":"face with no good gesture tone 1","shortname":":no_good_tone1:","category":"people","emoji_order":"532"},{"name":"face with no good gesture tone 2","shortname":":no_good_tone2:","category":"people","emoji_order":"533"},{"name":"face with no good gesture tone 3","shortname":":no_good_tone3:","category":"people","emoji_order":"534"},{"name":"face with no good gesture tone 4","shortname":":no_good_tone4:","category":"people","emoji_order":"535"},{"name":"face with no good gesture tone 5","shortname":":no_good_tone5:","category":"people","emoji_order":"536"},{"name":"face with ok gesture","shortname":":ok_woman:","category":"people","emoji_order":"549","aliases_ascii":["*\\0/*","\\0/","*\\O/*","\\O/"]},{"name":"face with ok gesture tone1","shortname":":ok_woman_tone1:","category":"people","emoji_order":"550"},{"name":"face with ok gesture tone2","shortname":":ok_woman_tone2:","category":"people","emoji_order":"551"},{"name":"face with ok gesture tone3","shortname":":ok_woman_tone3:","category":"people","emoji_order":"552"},{"name":"face with ok gesture tone4","shortname":":ok_woman_tone4:","category":"people","emoji_order":"553"},{"name":"face with ok gesture tone5","shortname":":ok_woman_tone5:","category":"people","emoji_order":"554"},{"name":"information desk person","shortname":":information_desk_person:","category":"people","emoji_order":"567"},{"name":"information desk person tone 1","shortname":":information_desk_person_tone1:","category":"people","emoji_order":"568"},{"name":"information desk person tone 2","shortname":":information_desk_person_tone2:","category":"people","emoji_order":"569"},{"name":"information desk person tone 3","shortname":":information_desk_person_tone3:","category":"people","emoji_order":"570"},{"name":"information desk person tone 4","shortname":":information_desk_person_tone4:","category":"people","emoji_order":"571"},{"name":"information desk person tone 5","shortname":":information_desk_person_tone5:","category":"people","emoji_order":"572"},{"name":"happy person raising one hand","shortname":":raising_hand:","category":"people","emoji_order":"585"},{"name":"happy person raising one hand tone1","shortname":":raising_hand_tone1:","category":"people","emoji_order":"586"},{"name":"happy person raising one hand tone2","shortname":":raising_hand_tone2:","category":"people","emoji_order":"587"},{"name":"happy person raising one hand tone3","shortname":":raising_hand_tone3:","category":"people","emoji_order":"588"},{"name":"happy person raising one hand tone4","shortname":":raising_hand_tone4:","category":"people","emoji_order":"589"},{"name":"happy person raising one hand tone5","shortname":":raising_hand_tone5:","category":"people","emoji_order":"590"},{"name":"person bowing deeply","shortname":":bow:","category":"people","emoji_order":"603"},{"name":"person bowing deeply tone 1","shortname":":bow_tone1:","category":"people","emoji_order":"604"},{"name":"person bowing deeply tone 2","shortname":":bow_tone2:","category":"people","emoji_order":"605"},{"name":"person bowing deeply tone 3","shortname":":bow_tone3:","category":"people","emoji_order":"606"},{"name":"person bowing deeply tone 4","shortname":":bow_tone4:","category":"people","emoji_order":"607"},{"name":"person bowing deeply tone 5","shortname":":bow_tone5:","category":"people","emoji_order":"608"},{"name":"face palm","shortname":":face_palm:","category":"people","emoji_order":"621","aliases":[":facepalm:"]},{"name":"face palm tone 1","shortname":":face_palm_tone1:","category":"people","emoji_order":"622","aliases":[":facepalm_tone1:"]},{"name":"face palm tone 2","shortname":":face_palm_tone2:","category":"people","emoji_order":"623","aliases":[":facepalm_tone2:"]},{"name":"face palm tone 3","shortname":":face_palm_tone3:","category":"people","emoji_order":"624","aliases":[":facepalm_tone3:"]},{"name":"face palm tone 4","shortname":":face_palm_tone4:","category":"people","emoji_order":"625","aliases":[":facepalm_tone4:"]},{"name":"face palm tone 5","shortname":":face_palm_tone5:","category":"people","emoji_order":"626","aliases":[":facepalm_tone5:"]},{"name":"shrug","shortname":":shrug:","category":"people","emoji_order":"639"},{"name":"shrug tone 1","shortname":":shrug_tone1:","category":"people","emoji_order":"640"},{"name":"shrug tone 2","shortname":":shrug_tone2:","category":"people","emoji_order":"641"},{"name":"shrug tone 3","shortname":":shrug_tone3:","category":"people","emoji_order":"642"},{"name":"shrug tone 4","shortname":":shrug_tone4:","category":"people","emoji_order":"643"},{"name":"shrug tone 5","shortname":":shrug_tone5:","category":"people","emoji_order":"644"},{"name":"face massage","shortname":":massage:","category":"people","emoji_order":"657"},{"name":"face massage tone 1","shortname":":massage_tone1:","category":"people","emoji_order":"658"},{"name":"face massage tone 2","shortname":":massage_tone2:","category":"people","emoji_order":"659"},{"name":"face massage tone 3","shortname":":massage_tone3:","category":"people","emoji_order":"660"},{"name":"face massage tone 4","shortname":":massage_tone4:","category":"people","emoji_order":"661"},{"name":"face massage tone 5","shortname":":massage_tone5:","category":"people","emoji_order":"662"},{"name":"haircut","shortname":":haircut:","category":"people","emoji_order":"675"},{"name":"haircut tone 1","shortname":":haircut_tone1:","category":"people","emoji_order":"676"},{"name":"haircut tone 2","shortname":":haircut_tone2:","category":"people","emoji_order":"677"},{"name":"haircut tone 3","shortname":":haircut_tone3:","category":"people","emoji_order":"678"},{"name":"haircut tone 4","shortname":":haircut_tone4:","category":"people","emoji_order":"679"},{"name":"haircut tone 5","shortname":":haircut_tone5:","category":"people","emoji_order":"680"},{"name":"pedestrian","shortname":":walking:","category":"people","emoji_order":"693"},{"name":"pedestrian tone 1","shortname":":walking_tone1:","category":"people","emoji_order":"694"},{"name":"pedestrian tone 2","shortname":":walking_tone2:","category":"people","emoji_order":"695"},{"name":"pedestrian tone 3","shortname":":walking_tone3:","category":"people","emoji_order":"696"},{"name":"pedestrian tone 4","shortname":":walking_tone4:","category":"people","emoji_order":"697"},{"name":"pedestrian tone 5","shortname":":walking_tone5:","category":"people","emoji_order":"698"},{"name":"runner","shortname":":runner:","category":"people","emoji_order":"711"},{"name":"runner tone 1","shortname":":runner_tone1:","category":"people","emoji_order":"712"},{"name":"runner tone 2","shortname":":runner_tone2:","category":"people","emoji_order":"713"},{"name":"runner tone 3","shortname":":runner_tone3:","category":"people","emoji_order":"714"},{"name":"runner tone 4","shortname":":runner_tone4:","category":"people","emoji_order":"715"},{"name":"runner tone 5","shortname":":runner_tone5:","category":"people","emoji_order":"716"},{"name":"dancer","shortname":":dancer:","category":"people","emoji_order":"729"},{"name":"dancer tone 1","shortname":":dancer_tone1:","category":"people","emoji_order":"730"},{"name":"dancer tone 2","shortname":":dancer_tone2:","category":"people","emoji_order":"731"},{"name":"dancer tone 3","shortname":":dancer_tone3:","category":"people","emoji_order":"732"},{"name":"dancer tone 4","shortname":":dancer_tone4:","category":"people","emoji_order":"733"},{"name":"dancer tone 5","shortname":":dancer_tone5:","category":"people","emoji_order":"734"},{"name":"man dancing","shortname":":man_dancing:","category":"people","emoji_order":"735","aliases":[":male_dancer:"]},{"name":"man dancing tone 1","shortname":":man_dancing_tone1:","category":"people","emoji_order":"736","aliases":[":male_dancer_tone1:"]},{"name":"man dancing tone 2","shortname":":man_dancing_tone2:","category":"people","emoji_order":"737","aliases":[":male_dancer_tone2:"]},{"name":"man dancing tone 3","shortname":":man_dancing_tone3:","category":"people","emoji_order":"738","aliases":[":male_dancer_tone3:"]},{"name":"man dancing tone 4","shortname":":man_dancing_tone4:","category":"people","emoji_order":"739","aliases":[":male_dancer_tone4:"]},{"name":"man dancing tone 5","shortname":":man_dancing_tone5:","category":"people","emoji_order":"740","aliases":[":male_dancer_tone5:"]},{"name":"woman with bunny ears","shortname":":dancers:","category":"people","emoji_order":"741"},{"name":"man in business suit levitating","shortname":":levitate:","category":"activity","emoji_order":"759","aliases":[":man_in_business_suit_levitating:"]},{"name":"speaking head in silhouette","shortname":":speaking_head:","category":"people","emoji_order":"765","aliases":[":speaking_head_in_silhouette:"]},{"name":"bust in silhouette","shortname":":bust_in_silhouette:","category":"people","emoji_order":"766"},{"name":"busts in silhouette","shortname":":busts_in_silhouette:","category":"people","emoji_order":"767"},{"name":"fencer","shortname":":fencer:","category":"activity","emoji_order":"768","aliases":[":fencing:"]},{"name":"horse racing","shortname":":horse_racing:","category":"activity","emoji_order":"769"},{"name":"horse racing tone 1","shortname":":horse_racing_tone1:","category":"activity","emoji_order":"770"},{"name":"horse racing tone 2","shortname":":horse_racing_tone2:","category":"activity","emoji_order":"771"},{"name":"horse racing tone 3","shortname":":horse_racing_tone3:","category":"activity","emoji_order":"772"},{"name":"horse racing tone 4","shortname":":horse_racing_tone4:","category":"activity","emoji_order":"773"},{"name":"horse racing tone 5","shortname":":horse_racing_tone5:","category":"activity","emoji_order":"774"},{"name":"skier","shortname":":skier:","category":"activity","emoji_order":"775"},{"name":"snowboarder","shortname":":snowboarder:","category":"activity","emoji_order":"776"},{"name":"golfer","shortname":":golfer:","category":"activity","emoji_order":"782"},{"name":"surfer","shortname":":surfer:","category":"activity","emoji_order":"800"},{"name":"surfer tone 1","shortname":":surfer_tone1:","category":"activity","emoji_order":"801"},{"name":"surfer tone 2","shortname":":surfer_tone2:","category":"activity","emoji_order":"802"},{"name":"surfer tone 3","shortname":":surfer_tone3:","category":"activity","emoji_order":"803"},{"name":"surfer tone 4","shortname":":surfer_tone4:","category":"activity","emoji_order":"804"},{"name":"surfer tone 5","shortname":":surfer_tone5:","category":"activity","emoji_order":"805"},{"name":"rowboat","shortname":":rowboat:","category":"activity","emoji_order":"818"},{"name":"rowboat tone 1","shortname":":rowboat_tone1:","category":"activity","emoji_order":"819"},{"name":"rowboat tone 2","shortname":":rowboat_tone2:","category":"activity","emoji_order":"820"},{"name":"rowboat tone 3","shortname":":rowboat_tone3:","category":"activity","emoji_order":"821"},{"name":"rowboat tone 4","shortname":":rowboat_tone4:","category":"activity","emoji_order":"822"},{"name":"rowboat tone 5","shortname":":rowboat_tone5:","category":"activity","emoji_order":"823"},{"name":"swimmer","shortname":":swimmer:","category":"activity","emoji_order":"836"},{"name":"swimmer tone 1","shortname":":swimmer_tone1:","category":"activity","emoji_order":"837"},{"name":"swimmer tone 2","shortname":":swimmer_tone2:","category":"activity","emoji_order":"838"},{"name":"swimmer tone 3","shortname":":swimmer_tone3:","category":"activity","emoji_order":"839"},{"name":"swimmer tone 4","shortname":":swimmer_tone4:","category":"activity","emoji_order":"840"},{"name":"swimmer tone 5","shortname":":swimmer_tone5:","category":"activity","emoji_order":"841"},{"name":"person with ball","shortname":":basketball_player:","category":"activity","emoji_order":"854","aliases":[":person_with_ball:"]},{"name":"person with ball tone 1","shortname":":basketball_player_tone1:","category":"activity","emoji_order":"855","aliases":[":person_with_ball_tone1:"]},{"name":"person with ball tone 2","shortname":":basketball_player_tone2:","category":"activity","emoji_order":"856","aliases":[":person_with_ball_tone2:"]},{"name":"person with ball tone 3","shortname":":basketball_player_tone3:","category":"activity","emoji_order":"857","aliases":[":person_with_ball_tone3:"]},{"name":"person with ball tone 4","shortname":":basketball_player_tone4:","category":"activity","emoji_order":"858","aliases":[":person_with_ball_tone4:"]},{"name":"person with ball tone 5","shortname":":basketball_player_tone5:","category":"activity","emoji_order":"859","aliases":[":person_with_ball_tone5:"]},{"name":"weight lifter","shortname":":lifter:","category":"activity","emoji_order":"872","aliases":[":weight_lifter:"]},{"name":"weight lifter tone 1","shortname":":lifter_tone1:","category":"activity","emoji_order":"873","aliases":[":weight_lifter_tone1:"]},{"name":"weight lifter tone 2","shortname":":lifter_tone2:","category":"activity","emoji_order":"874","aliases":[":weight_lifter_tone2:"]},{"name":"weight lifter tone 3","shortname":":lifter_tone3:","category":"activity","emoji_order":"875","aliases":[":weight_lifter_tone3:"]},{"name":"weight lifter tone 4","shortname":":lifter_tone4:","category":"activity","emoji_order":"876","aliases":[":weight_lifter_tone4:"]},{"name":"weight lifter tone 5","shortname":":lifter_tone5:","category":"activity","emoji_order":"877","aliases":[":weight_lifter_tone5:"]},{"name":"bicyclist","shortname":":bicyclist:","category":"activity","emoji_order":"890"},{"name":"bicyclist tone 1","shortname":":bicyclist_tone1:","category":"activity","emoji_order":"891"},{"name":"bicyclist tone 2","shortname":":bicyclist_tone2:","category":"activity","emoji_order":"892"},{"name":"bicyclist tone 3","shortname":":bicyclist_tone3:","category":"activity","emoji_order":"893"},{"name":"bicyclist tone 4","shortname":":bicyclist_tone4:","category":"activity","emoji_order":"894"},{"name":"bicyclist tone 5","shortname":":bicyclist_tone5:","category":"activity","emoji_order":"895"},{"name":"mountain bicyclist","shortname":":mountain_bicyclist:","category":"activity","emoji_order":"908"},{"name":"mountain bicyclist tone 1","shortname":":mountain_bicyclist_tone1:","category":"activity","emoji_order":"909"},{"name":"mountain bicyclist tone 2","shortname":":mountain_bicyclist_tone2:","category":"activity","emoji_order":"910"},{"name":"mountain bicyclist tone 3","shortname":":mountain_bicyclist_tone3:","category":"activity","emoji_order":"911"},{"name":"mountain bicyclist tone 4","shortname":":mountain_bicyclist_tone4:","category":"activity","emoji_order":"912"},{"name":"mountain bicyclist tone 5","shortname":":mountain_bicyclist_tone5:","category":"activity","emoji_order":"913"},{"name":"racing car","shortname":":race_car:","category":"travel","emoji_order":"926","aliases":[":racing_car:"]},{"name":"racing motorcycle","shortname":":motorcycle:","category":"travel","emoji_order":"927","aliases":[":racing_motorcycle:"]},{"name":"person doing cartwheel","shortname":":cartwheel:","category":"activity","emoji_order":"928","aliases":[":person_doing_cartwheel:"]},{"name":"person doing cartwheel tone 1","shortname":":cartwheel_tone1:","category":"activity","emoji_order":"929","aliases":[":person_doing_cartwheel_tone1:"]},{"name":"person doing cartwheel tone 2","shortname":":cartwheel_tone2:","category":"activity","emoji_order":"930","aliases":[":person_doing_cartwheel_tone2:"]},{"name":"person doing cartwheel tone 3","shortname":":cartwheel_tone3:","category":"activity","emoji_order":"931","aliases":[":person_doing_cartwheel_tone3:"]},{"name":"person doing cartwheel tone 4","shortname":":cartwheel_tone4:","category":"activity","emoji_order":"932","aliases":[":person_doing_cartwheel_tone4:"]},{"name":"person doing cartwheel tone 5","shortname":":cartwheel_tone5:","category":"activity","emoji_order":"933","aliases":[":person_doing_cartwheel_tone5:"]},{"name":"wrestlers","shortname":":wrestlers:","category":"activity","emoji_order":"946","aliases":[":wrestling:"]},{"name":"wrestlers tone 1","shortname":":wrestlers_tone1:","category":"activity","emoji_order":"947","aliases":[":wrestling_tone1:"]},{"name":"wrestlers tone 2","shortname":":wrestlers_tone2:","category":"activity","emoji_order":"948","aliases":[":wrestling_tone2:"]},{"name":"wrestlers tone 3","shortname":":wrestlers_tone3:","category":"activity","emoji_order":"949","aliases":[":wrestling_tone3:"]},{"name":"wrestlers tone 4","shortname":":wrestlers_tone4:","category":"activity","emoji_order":"950","aliases":[":wrestling_tone4:"]},{"name":"wrestlers tone 5","shortname":":wrestlers_tone5:","category":"activity","emoji_order":"951","aliases":[":wrestling_tone5:"]},{"name":"water polo","shortname":":water_polo:","category":"activity","emoji_order":"964"},{"name":"water polo tone 1","shortname":":water_polo_tone1:","category":"activity","emoji_order":"965"},{"name":"water polo tone 2","shortname":":water_polo_tone2:","category":"activity","emoji_order":"966"},{"name":"water polo tone 3","shortname":":water_polo_tone3:","category":"activity","emoji_order":"967"},{"name":"water polo tone 4","shortname":":water_polo_tone4:","category":"activity","emoji_order":"968"},{"name":"water polo tone 5","shortname":":water_polo_tone5:","category":"activity","emoji_order":"969"},{"name":"handball","shortname":":handball:","category":"activity","emoji_order":"982"},{"name":"handball tone 1","shortname":":handball_tone1:","category":"activity","emoji_order":"983"},{"name":"handball tone 2","shortname":":handball_tone2:","category":"activity","emoji_order":"984"},{"name":"handball tone 3","shortname":":handball_tone3:","category":"activity","emoji_order":"985"},{"name":"handball tone 4","shortname":":handball_tone4:","category":"activity","emoji_order":"986"},{"name":"handball tone 5","shortname":":handball_tone5:","category":"activity","emoji_order":"987"},{"name":"juggling","shortname":":juggling:","category":"activity","emoji_order":"1000","aliases":[":juggler:"]},{"name":"juggling tone 1","shortname":":juggling_tone1:","category":"activity","emoji_order":"1001","aliases":[":juggler_tone1:"]},{"name":"juggling tone 2","shortname":":juggling_tone2:","category":"activity","emoji_order":"1002","aliases":[":juggler_tone2:"]},{"name":"juggling tone 3","shortname":":juggling_tone3:","category":"activity","emoji_order":"1003","aliases":[":juggler_tone3:"]},{"name":"juggling tone 4","shortname":":juggling_tone4:","category":"activity","emoji_order":"1004","aliases":[":juggler_tone4:"]},{"name":"juggling tone 5","shortname":":juggling_tone5:","category":"activity","emoji_order":"1005","aliases":[":juggler_tone5:"]},{"name":"man and woman holding hands","shortname":":couple:","category":"people","emoji_order":"1018"},{"name":"two men holding hands","shortname":":two_men_holding_hands:","category":"people","emoji_order":"1024"},{"name":"two women holding hands","shortname":":two_women_holding_hands:","category":"people","emoji_order":"1030"},{"name":"kiss","shortname":":couplekiss:","category":"people","emoji_order":"1036"},{"name":"kiss (man,man)","shortname":":kiss_mm:","category":"people","emoji_order":"1038","aliases":[":couplekiss_mm:"]},{"name":"kiss (woman,woman)","shortname":":kiss_ww:","category":"people","emoji_order":"1039","aliases":[":couplekiss_ww:"]},{"name":"couple with heart","shortname":":couple_with_heart:","category":"people","emoji_order":"1040"},{"name":"couple (man,man)","shortname":":couple_mm:","category":"people","emoji_order":"1042","aliases":[":couple_with_heart_mm:"]},{"name":"couple (woman,woman)","shortname":":couple_ww:","category":"people","emoji_order":"1043","aliases":[":couple_with_heart_ww:"]},{"name":"family","shortname":":family:","category":"people","emoji_order":"1044"},{"name":"family (man,woman,girl)","shortname":":family_mwg:","category":"people","emoji_order":"1051"},{"name":"family (man,woman,girl,boy)","shortname":":family_mwgb:","category":"people","emoji_order":"1052"},{"name":"family (man,woman,boy,boy)","shortname":":family_mwbb:","category":"people","emoji_order":"1053"},{"name":"family (man,woman,girl,girl)","shortname":":family_mwgg:","category":"people","emoji_order":"1054"},{"name":"family (man,man,boy)","shortname":":family_mmb:","category":"people","emoji_order":"1055"},{"name":"family (man,man,girl)","shortname":":family_mmg:","category":"people","emoji_order":"1056"},{"name":"family (man,man,girl,boy)","shortname":":family_mmgb:","category":"people","emoji_order":"1057"},{"name":"family (man,man,boy,boy)","shortname":":family_mmbb:","category":"people","emoji_order":"1058"},{"name":"family (man,man,girl,girl)","shortname":":family_mmgg:","category":"people","emoji_order":"1059"},{"name":"family (woman,woman,boy)","shortname":":family_wwb:","category":"people","emoji_order":"1060"},{"name":"family (woman,woman,girl)","shortname":":family_wwg:","category":"people","emoji_order":"1061"},{"name":"family (woman,woman,girl,boy)","shortname":":family_wwgb:","category":"people","emoji_order":"1062"},{"name":"family (woman,woman,boy,boy)","shortname":":family_wwbb:","category":"people","emoji_order":"1063"},{"name":"family (woman,woman,girl,girl)","shortname":":family_wwgg:","category":"people","emoji_order":"1064"},{"name":"emoji modifier Fitzpatrick type-1-2","shortname":":tone1:","category":"modifier","emoji_order":"1075"},{"name":"emoji modifier Fitzpatrick type-3","shortname":":tone2:","category":"modifier","emoji_order":"1076"},{"name":"emoji modifier Fitzpatrick type-4","shortname":":tone3:","category":"modifier","emoji_order":"1077"},{"name":"emoji modifier Fitzpatrick type-5","shortname":":tone4:","category":"modifier","emoji_order":"1078"},{"name":"emoji modifier Fitzpatrick type-6","shortname":":tone5:","category":"modifier","emoji_order":"1079"},{"name":"flexed biceps","shortname":":muscle:","category":"people","emoji_order":"1080"},{"name":"flexed biceps tone 1","shortname":":muscle_tone1:","category":"people","emoji_order":"1081"},{"name":"flexed biceps tone 2","shortname":":muscle_tone2:","category":"people","emoji_order":"1082"},{"name":"flexed biceps tone 3","shortname":":muscle_tone3:","category":"people","emoji_order":"1083"},{"name":"flexed biceps tone 4","shortname":":muscle_tone4:","category":"people","emoji_order":"1084"},{"name":"flexed biceps tone 5","shortname":":muscle_tone5:","category":"people","emoji_order":"1085"},{"name":"selfie","shortname":":selfie:","category":"people","emoji_order":"1086"},{"name":"selfie tone 1","shortname":":selfie_tone1:","category":"people","emoji_order":"1087"},{"name":"selfie tone 2","shortname":":selfie_tone2:","category":"people","emoji_order":"1088"},{"name":"selfie tone 3","shortname":":selfie_tone3:","category":"people","emoji_order":"1089"},{"name":"selfie tone 4","shortname":":selfie_tone4:","category":"people","emoji_order":"1090"},{"name":"selfie tone 5","shortname":":selfie_tone5:","category":"people","emoji_order":"1091"},{"name":"white left pointing backhand index","shortname":":point_left:","category":"people","emoji_order":"1092"},{"name":"white left pointing backhand index tone 1","shortname":":point_left_tone1:","category":"people","emoji_order":"1093"},{"name":"white left pointing backhand index tone 2","shortname":":point_left_tone2:","category":"people","emoji_order":"1094"},{"name":"white left pointing backhand index tone 3","shortname":":point_left_tone3:","category":"people","emoji_order":"1095"},{"name":"white left pointing backhand index tone 4","shortname":":point_left_tone4:","category":"people","emoji_order":"1096"},{"name":"white left pointing backhand index tone 5","shortname":":point_left_tone5:","category":"people","emoji_order":"1097"},{"name":"white right pointing backhand index","shortname":":point_right:","category":"people","emoji_order":"1098"},{"name":"white right pointing backhand index tone 1","shortname":":point_right_tone1:","category":"people","emoji_order":"1099"},{"name":"white right pointing backhand index tone 2","shortname":":point_right_tone2:","category":"people","emoji_order":"1100"},{"name":"white right pointing backhand index tone 3","shortname":":point_right_tone3:","category":"people","emoji_order":"1101"},{"name":"white right pointing backhand index tone 4","shortname":":point_right_tone4:","category":"people","emoji_order":"1102"},{"name":"white right pointing backhand index tone 5","shortname":":point_right_tone5:","category":"people","emoji_order":"1103"},{"name":"white up pointing index","shortname":":point_up:","category":"people","emoji_order":"1104"},{"name":"white up pointing index tone 1","shortname":":point_up_tone1:","category":"people","emoji_order":"1105"},{"name":"white up pointing index tone 2","shortname":":point_up_tone2:","category":"people","emoji_order":"1106"},{"name":"white up pointing index tone 3","shortname":":point_up_tone3:","category":"people","emoji_order":"1107"},{"name":"white up pointing index tone 4","shortname":":point_up_tone4:","category":"people","emoji_order":"1108"},{"name":"white up pointing index tone 5","shortname":":point_up_tone5:","category":"people","emoji_order":"1109"},{"name":"white up pointing backhand index","shortname":":point_up_2:","category":"people","emoji_order":"1110"},{"name":"white up pointing backhand index tone 1","shortname":":point_up_2_tone1:","category":"people","emoji_order":"1111"},{"name":"white up pointing backhand index tone 2","shortname":":point_up_2_tone2:","category":"people","emoji_order":"1112"},{"name":"white up pointing backhand index tone 3","shortname":":point_up_2_tone3:","category":"people","emoji_order":"1113"},{"name":"white up pointing backhand index tone 4","shortname":":point_up_2_tone4:","category":"people","emoji_order":"1114"},{"name":"white up pointing backhand index tone 5","shortname":":point_up_2_tone5:","category":"people","emoji_order":"1115"},{"name":"reversed hand with middle finger extended","shortname":":middle_finger:","category":"people","emoji_order":"1116","aliases":[":reversed_hand_with_middle_finger_extended:"]},{"name":"reversed hand with middle finger extended tone 1","shortname":":middle_finger_tone1:","category":"people","emoji_order":"1117","aliases":[":reversed_hand_with_middle_finger_extended_tone1:"]},{"name":"reversed hand with middle finger extended tone 2","shortname":":middle_finger_tone2:","category":"people","emoji_order":"1118","aliases":[":reversed_hand_with_middle_finger_extended_tone2:"]},{"name":"reversed hand with middle finger extended tone 3","shortname":":middle_finger_tone3:","category":"people","emoji_order":"1119","aliases":[":reversed_hand_with_middle_finger_extended_tone3:"]},{"name":"reversed hand with middle finger extended tone 4","shortname":":middle_finger_tone4:","category":"people","emoji_order":"1120","aliases":[":reversed_hand_with_middle_finger_extended_tone4:"]},{"name":"reversed hand with middle finger extended tone 5","shortname":":middle_finger_tone5:","category":"people","emoji_order":"1121","aliases":[":reversed_hand_with_middle_finger_extended_tone5:"]},{"name":"white down pointing backhand index","shortname":":point_down:","category":"people","emoji_order":"1122"},{"name":"white down pointing backhand index tone 1","shortname":":point_down_tone1:","category":"people","emoji_order":"1123"},{"name":"white down pointing backhand index tone 2","shortname":":point_down_tone2:","category":"people","emoji_order":"1124"},{"name":"white down pointing backhand index tone 3","shortname":":point_down_tone3:","category":"people","emoji_order":"1125"},{"name":"white down pointing backhand index tone 4","shortname":":point_down_tone4:","category":"people","emoji_order":"1126"},{"name":"white down pointing backhand index tone 5","shortname":":point_down_tone5:","category":"people","emoji_order":"1127"},{"name":"victory hand","shortname":":v:","category":"people","emoji_order":"1128"},{"name":"victory hand tone 1","shortname":":v_tone1:","category":"people","emoji_order":"1129"},{"name":"victory hand tone 2","shortname":":v_tone2:","category":"people","emoji_order":"1130"},{"name":"victory hand tone 3","shortname":":v_tone3:","category":"people","emoji_order":"1131"},{"name":"victory hand tone 4","shortname":":v_tone4:","category":"people","emoji_order":"1132"},{"name":"victory hand tone 5","shortname":":v_tone5:","category":"people","emoji_order":"1133"},{"name":"hand with first and index finger crossed","shortname":":fingers_crossed:","category":"people","emoji_order":"1134","aliases":[":hand_with_index_and_middle_finger_crossed:"]},{"name":"hand with index and middle fingers crossed tone 1","shortname":":fingers_crossed_tone1:","category":"people","emoji_order":"1135","aliases":[":hand_with_index_and_middle_fingers_crossed_tone1:"]},{"name":"hand with index and middle fingers crossed tone 2","shortname":":fingers_crossed_tone2:","category":"people","emoji_order":"1136","aliases":[":hand_with_index_and_middle_fingers_crossed_tone2:"]},{"name":"hand with index and middle fingers crossed tone 3","shortname":":fingers_crossed_tone3:","category":"people","emoji_order":"1137","aliases":[":hand_with_index_and_middle_fingers_crossed_tone3:"]},{"name":"hand with index and middle fingers crossed tone 4","shortname":":fingers_crossed_tone4:","category":"people","emoji_order":"1138","aliases":[":hand_with_index_and_middle_fingers_crossed_tone4:"]},{"name":"hand with index and middle fingers crossed tone 5","shortname":":fingers_crossed_tone5:","category":"people","emoji_order":"1139","aliases":[":hand_with_index_and_middle_fingers_crossed_tone5:"]},{"name":"raised hand with part between middle and ring fingers","shortname":":vulcan:","category":"people","emoji_order":"1140","aliases":[":raised_hand_with_part_between_middle_and_ring_fingers:"]},{"name":"raised hand with part between middle and ring fingers tone 1","shortname":":vulcan_tone1:","category":"people","emoji_order":"1141","aliases":[":raised_hand_with_part_between_middle_and_ring_fingers_tone1:"]},{"name":"raised hand with part between middle and ring fingers tone 2","shortname":":vulcan_tone2:","category":"people","emoji_order":"1142","aliases":[":raised_hand_with_part_between_middle_and_ring_fingers_tone2:"]},{"name":"raised hand with part between middle and ring fingers tone 3","shortname":":vulcan_tone3:","category":"people","emoji_order":"1143","aliases":[":raised_hand_with_part_between_middle_and_ring_fingers_tone3:"]},{"name":"raised hand with part between middle and ring fingers tone 4","shortname":":vulcan_tone4:","category":"people","emoji_order":"1144","aliases":[":raised_hand_with_part_between_middle_and_ring_fingers_tone4:"]},{"name":"raised hand with part between middle and ring fingers tone 5","shortname":":vulcan_tone5:","category":"people","emoji_order":"1145","aliases":[":raised_hand_with_part_between_middle_and_ring_fingers_tone5:"]},{"name":"sign of the horns","shortname":":metal:","category":"people","emoji_order":"1146","aliases":[":sign_of_the_horns:"]},{"name":"sign of the horns tone 1","shortname":":metal_tone1:","category":"people","emoji_order":"1147","aliases":[":sign_of_the_horns_tone1:"]},{"name":"sign of the horns tone 2","shortname":":metal_tone2:","category":"people","emoji_order":"1148","aliases":[":sign_of_the_horns_tone2:"]},{"name":"sign of the horns tone 3","shortname":":metal_tone3:","category":"people","emoji_order":"1149","aliases":[":sign_of_the_horns_tone3:"]},{"name":"sign of the horns tone 4","shortname":":metal_tone4:","category":"people","emoji_order":"1150","aliases":[":sign_of_the_horns_tone4:"]},{"name":"sign of the horns tone 5","shortname":":metal_tone5:","category":"people","emoji_order":"1151","aliases":[":sign_of_the_horns_tone5:"]},{"name":"call me hand","shortname":":call_me:","category":"people","emoji_order":"1152","aliases":[":call_me_hand:"]},{"name":"call me hand tone 1","shortname":":call_me_tone1:","category":"people","emoji_order":"1153","aliases":[":call_me_hand_tone1:"]},{"name":"call me hand tone 2","shortname":":call_me_tone2:","category":"people","emoji_order":"1154","aliases":[":call_me_hand_tone2:"]},{"name":"call me hand tone 3","shortname":":call_me_tone3:","category":"people","emoji_order":"1155","aliases":[":call_me_hand_tone3:"]},{"name":"call me hand tone 4","shortname":":call_me_tone4:","category":"people","emoji_order":"1156","aliases":[":call_me_hand_tone4:"]},{"name":"call me hand tone 5","shortname":":call_me_tone5:","category":"people","emoji_order":"1157","aliases":[":call_me_hand_tone5:"]},{"name":"raised hand with fingers splayed","shortname":":hand_splayed:","category":"people","emoji_order":"1158","aliases":[":raised_hand_with_fingers_splayed:"]},{"name":"raised hand with fingers splayed tone 1","shortname":":hand_splayed_tone1:","category":"people","emoji_order":"1159","aliases":[":raised_hand_with_fingers_splayed_tone1:"]},{"name":"raised hand with fingers splayed tone 2","shortname":":hand_splayed_tone2:","category":"people","emoji_order":"1160","aliases":[":raised_hand_with_fingers_splayed_tone2:"]},{"name":"raised hand with fingers splayed tone 3","shortname":":hand_splayed_tone3:","category":"people","emoji_order":"1161","aliases":[":raised_hand_with_fingers_splayed_tone3:"]},{"name":"raised hand with fingers splayed tone 4","shortname":":hand_splayed_tone4:","category":"people","emoji_order":"1162","aliases":[":raised_hand_with_fingers_splayed_tone4:"]},{"name":"raised hand with fingers splayed tone 5","shortname":":hand_splayed_tone5:","category":"people","emoji_order":"1163","aliases":[":raised_hand_with_fingers_splayed_tone5:"]},{"name":"raised hand","shortname":":raised_hand:","category":"people","emoji_order":"1164"},{"name":"raised hand tone 1","shortname":":raised_hand_tone1:","category":"people","emoji_order":"1165"},{"name":"raised hand tone 2","shortname":":raised_hand_tone2:","category":"people","emoji_order":"1166"},{"name":"raised hand tone 3","shortname":":raised_hand_tone3:","category":"people","emoji_order":"1167"},{"name":"raised hand tone 4","shortname":":raised_hand_tone4:","category":"people","emoji_order":"1168"},{"name":"raised hand tone 5","shortname":":raised_hand_tone5:","category":"people","emoji_order":"1169"},{"name":"ok hand sign","shortname":":ok_hand:","category":"people","emoji_order":"1170"},{"name":"ok hand sign tone 1","shortname":":ok_hand_tone1:","category":"people","emoji_order":"1171"},{"name":"ok hand sign tone 2","shortname":":ok_hand_tone2:","category":"people","emoji_order":"1172"},{"name":"ok hand sign tone 3","shortname":":ok_hand_tone3:","category":"people","emoji_order":"1173"},{"name":"ok hand sign tone 4","shortname":":ok_hand_tone4:","category":"people","emoji_order":"1174"},{"name":"ok hand sign tone 5","shortname":":ok_hand_tone5:","category":"people","emoji_order":"1175"},{"name":"thumbs up sign","shortname":":thumbsup:","category":"people","emoji_order":"1176","aliases":[":+1:",":thumbup:"]},{"name":"thumbs up sign tone 1","shortname":":thumbsup_tone1:","category":"people","emoji_order":"1177","aliases":[":+1_tone1:",":thumbup_tone1:"]},{"name":"thumbs up sign tone 2","shortname":":thumbsup_tone2:","category":"people","emoji_order":"1178","aliases":[":+1_tone2:",":thumbup_tone2:"]},{"name":"thumbs up sign tone 3","shortname":":thumbsup_tone3:","category":"people","emoji_order":"1179","aliases":[":+1_tone3:",":thumbup_tone3:"]},{"name":"thumbs up sign tone 4","shortname":":thumbsup_tone4:","category":"people","emoji_order":"1180","aliases":[":+1_tone4:",":thumbup_tone4:"]},{"name":"thumbs up sign tone 5","shortname":":thumbsup_tone5:","category":"people","emoji_order":"1181","aliases":[":+1_tone5:",":thumbup_tone5:"]},{"name":"thumbs down sign","shortname":":thumbsdown:","category":"people","emoji_order":"1182","aliases":[":-1:",":thumbdown:"]},{"name":"thumbs down sign tone 1","shortname":":thumbsdown_tone1:","category":"people","emoji_order":"1183","aliases":[":-1_tone1:",":thumbdown_tone1:"]},{"name":"thumbs down sign tone 2","shortname":":thumbsdown_tone2:","category":"people","emoji_order":"1184","aliases":[":-1_tone2:",":thumbdown_tone2:"]},{"name":"thumbs down sign tone 3","shortname":":thumbsdown_tone3:","category":"people","emoji_order":"1185","aliases":[":-1_tone3:",":thumbdown_tone3:"]},{"name":"thumbs down sign tone 4","shortname":":thumbsdown_tone4:","category":"people","emoji_order":"1186","aliases":[":-1_tone4:",":thumbdown_tone4:"]},{"name":"thumbs down sign tone 5","shortname":":thumbsdown_tone5:","category":"people","emoji_order":"1187","aliases":[":-1_tone5:",":thumbdown_tone5:"]},{"name":"raised fist","shortname":":fist:","category":"people","emoji_order":"1188"},{"name":"raised fist tone 1","shortname":":fist_tone1:","category":"people","emoji_order":"1189"},{"name":"raised fist tone 2","shortname":":fist_tone2:","category":"people","emoji_order":"1190"},{"name":"raised fist tone 3","shortname":":fist_tone3:","category":"people","emoji_order":"1191"},{"name":"raised fist tone 4","shortname":":fist_tone4:","category":"people","emoji_order":"1192"},{"name":"raised fist tone 5","shortname":":fist_tone5:","category":"people","emoji_order":"1193"},{"name":"fisted hand sign","shortname":":punch:","category":"people","emoji_order":"1194"},{"name":"fisted hand sign tone 1","shortname":":punch_tone1:","category":"people","emoji_order":"1195"},{"name":"fisted hand sign tone 2","shortname":":punch_tone2:","category":"people","emoji_order":"1196"},{"name":"fisted hand sign tone 3","shortname":":punch_tone3:","category":"people","emoji_order":"1197"},{"name":"fisted hand sign tone 4","shortname":":punch_tone4:","category":"people","emoji_order":"1198"},{"name":"fisted hand sign tone 5","shortname":":punch_tone5:","category":"people","emoji_order":"1199"},{"name":"left-facing fist","shortname":":left_facing_fist:","category":"people","emoji_order":"1200","aliases":[":left_fist:"]},{"name":"left facing fist tone 1","shortname":":left_facing_fist_tone1:","category":"people","emoji_order":"1201","aliases":[":left_fist_tone1:"]},{"name":"left facing fist tone 2","shortname":":left_facing_fist_tone2:","category":"people","emoji_order":"1202","aliases":[":left_fist_tone2:"]},{"name":"left facing fist tone 3","shortname":":left_facing_fist_tone3:","category":"people","emoji_order":"1203","aliases":[":left_fist_tone3:"]},{"name":"left facing fist tone 4","shortname":":left_facing_fist_tone4:","category":"people","emoji_order":"1204","aliases":[":left_fist_tone4:"]},{"name":"left facing fist tone 5","shortname":":left_facing_fist_tone5:","category":"people","emoji_order":"1205","aliases":[":left_fist_tone5:"]},{"name":"right-facing fist","shortname":":right_facing_fist:","category":"people","emoji_order":"1206","aliases":[":right_fist:"]},{"name":"right facing fist tone 1","shortname":":right_facing_fist_tone1:","category":"people","emoji_order":"1207","aliases":[":right_fist_tone1:"]},{"name":"right facing fist tone 2","shortname":":right_facing_fist_tone2:","category":"people","emoji_order":"1208","aliases":[":right_fist_tone2:"]},{"name":"right facing fist tone 3","shortname":":right_facing_fist_tone3:","category":"people","emoji_order":"1209","aliases":[":right_fist_tone3:"]},{"name":"right facing fist tone 4","shortname":":right_facing_fist_tone4:","category":"people","emoji_order":"1210","aliases":[":right_fist_tone4:"]},{"name":"right facing fist tone 5","shortname":":right_facing_fist_tone5:","category":"people","emoji_order":"1211","aliases":[":right_fist_tone5:"]},{"name":"raised back of hand","shortname":":raised_back_of_hand:","category":"people","emoji_order":"1212","aliases":[":back_of_hand:"]},{"name":"raised back of hand tone 1","shortname":":raised_back_of_hand_tone1:","category":"people","emoji_order":"1213","aliases":[":back_of_hand_tone1:"]},{"name":"raised back of hand tone 2","shortname":":raised_back_of_hand_tone2:","category":"people","emoji_order":"1214","aliases":[":back_of_hand_tone2:"]},{"name":"raised back of hand tone 3","shortname":":raised_back_of_hand_tone3:","category":"people","emoji_order":"1215","aliases":[":back_of_hand_tone3:"]},{"name":"raised back of hand tone 4","shortname":":raised_back_of_hand_tone4:","category":"people","emoji_order":"1216","aliases":[":back_of_hand_tone4:"]},{"name":"raised back of hand tone 5","shortname":":raised_back_of_hand_tone5:","category":"people","emoji_order":"1217","aliases":[":back_of_hand_tone5:"]},{"name":"waving hand sign","shortname":":wave:","category":"people","emoji_order":"1218"},{"name":"waving hand sign tone 1","shortname":":wave_tone1:","category":"people","emoji_order":"1219"},{"name":"waving hand sign tone 2","shortname":":wave_tone2:","category":"people","emoji_order":"1220"},{"name":"waving hand sign tone 3","shortname":":wave_tone3:","category":"people","emoji_order":"1221"},{"name":"waving hand sign tone 4","shortname":":wave_tone4:","category":"people","emoji_order":"1222"},{"name":"waving hand sign tone 5","shortname":":wave_tone5:","category":"people","emoji_order":"1223"},{"name":"clapping hands sign","shortname":":clap:","category":"people","emoji_order":"1224"},{"name":"clapping hands sign tone 1","shortname":":clap_tone1:","category":"people","emoji_order":"1225"},{"name":"clapping hands sign tone 2","shortname":":clap_tone2:","category":"people","emoji_order":"1226"},{"name":"clapping hands sign tone 3","shortname":":clap_tone3:","category":"people","emoji_order":"1227"},{"name":"clapping hands sign tone 4","shortname":":clap_tone4:","category":"people","emoji_order":"1228"},{"name":"clapping hands sign tone 5","shortname":":clap_tone5:","category":"people","emoji_order":"1229"},{"name":"writing hand","shortname":":writing_hand:","category":"people","emoji_order":"1230"},{"name":"writing hand tone 1","shortname":":writing_hand_tone1:","category":"people","emoji_order":"1231"},{"name":"writing hand tone 2","shortname":":writing_hand_tone2:","category":"people","emoji_order":"1232"},{"name":"writing hand tone 3","shortname":":writing_hand_tone3:","category":"people","emoji_order":"1233"},{"name":"writing hand tone 4","shortname":":writing_hand_tone4:","category":"people","emoji_order":"1234"},{"name":"writing hand tone 5","shortname":":writing_hand_tone5:","category":"people","emoji_order":"1235"},{"name":"open hands sign","shortname":":open_hands:","category":"people","emoji_order":"1236"},{"name":"open hands sign tone 1","shortname":":open_hands_tone1:","category":"people","emoji_order":"1237"},{"name":"open hands sign tone 2","shortname":":open_hands_tone2:","category":"people","emoji_order":"1238"},{"name":"open hands sign tone 3","shortname":":open_hands_tone3:","category":"people","emoji_order":"1239"},{"name":"open hands sign tone 4","shortname":":open_hands_tone4:","category":"people","emoji_order":"1240"},{"name":"open hands sign tone 5","shortname":":open_hands_tone5:","category":"people","emoji_order":"1241"},{"name":"person raising both hands in celebration","shortname":":raised_hands:","category":"people","emoji_order":"1242"},{"name":"person raising both hands in celebration tone 1","shortname":":raised_hands_tone1:","category":"people","emoji_order":"1243"},{"name":"person raising both hands in celebration tone 2","shortname":":raised_hands_tone2:","category":"people","emoji_order":"1244"},{"name":"person raising both hands in celebration tone 3","shortname":":raised_hands_tone3:","category":"people","emoji_order":"1245"},{"name":"person raising both hands in celebration tone 4","shortname":":raised_hands_tone4:","category":"people","emoji_order":"1246"},{"name":"person raising both hands in celebration tone 5","shortname":":raised_hands_tone5:","category":"people","emoji_order":"1247"},{"name":"person with folded hands","shortname":":pray:","category":"people","emoji_order":"1248"},{"name":"person with folded hands tone 1","shortname":":pray_tone1:","category":"people","emoji_order":"1249"},{"name":"person with folded hands tone 2","shortname":":pray_tone2:","category":"people","emoji_order":"1250"},{"name":"person with folded hands tone 3","shortname":":pray_tone3:","category":"people","emoji_order":"1251"},{"name":"person with folded hands tone 4","shortname":":pray_tone4:","category":"people","emoji_order":"1252"},{"name":"person with folded hands tone 5","shortname":":pray_tone5:","category":"people","emoji_order":"1253"},{"name":"handshake","shortname":":handshake:","category":"people","emoji_order":"1254","aliases":[":shaking_hands:"]},{"name":"handshake tone 1","shortname":":handshake_tone1:","category":"people","emoji_order":"1255","aliases":[":shaking_hands_tone1:"]},{"name":"handshake tone 2","shortname":":handshake_tone2:","category":"people","emoji_order":"1256","aliases":[":shaking_hands_tone2:"]},{"name":"handshake tone 3","shortname":":handshake_tone3:","category":"people","emoji_order":"1257","aliases":[":shaking_hands_tone3:"]},{"name":"handshake tone 4","shortname":":handshake_tone4:","category":"people","emoji_order":"1258","aliases":[":shaking_hands_tone4:"]},{"name":"handshake tone 5","shortname":":handshake_tone5:","category":"people","emoji_order":"1259","aliases":[":shaking_hands_tone5:"]},{"name":"nail polish","shortname":":nail_care:","category":"people","emoji_order":"1260"},{"name":"nail polish tone 1","shortname":":nail_care_tone1:","category":"people","emoji_order":"1261"},{"name":"nail polish tone 2","shortname":":nail_care_tone2:","category":"people","emoji_order":"1262"},{"name":"nail polish tone 3","shortname":":nail_care_tone3:","category":"people","emoji_order":"1263"},{"name":"nail polish tone 4","shortname":":nail_care_tone4:","category":"people","emoji_order":"1264"},{"name":"nail polish tone 5","shortname":":nail_care_tone5:","category":"people","emoji_order":"1265"},{"name":"ear","shortname":":ear:","category":"people","emoji_order":"1266"},{"name":"ear tone 1","shortname":":ear_tone1:","category":"people","emoji_order":"1267"},{"name":"ear tone 2","shortname":":ear_tone2:","category":"people","emoji_order":"1268"},{"name":"ear tone 3","shortname":":ear_tone3:","category":"people","emoji_order":"1269"},{"name":"ear tone 4","shortname":":ear_tone4:","category":"people","emoji_order":"1270"},{"name":"ear tone 5","shortname":":ear_tone5:","category":"people","emoji_order":"1271"},{"name":"nose","shortname":":nose:","category":"people","emoji_order":"1272"},{"name":"nose tone 1","shortname":":nose_tone1:","category":"people","emoji_order":"1273"},{"name":"nose tone 2","shortname":":nose_tone2:","category":"people","emoji_order":"1274"},{"name":"nose tone 3","shortname":":nose_tone3:","category":"people","emoji_order":"1275"},{"name":"nose tone 4","shortname":":nose_tone4:","category":"people","emoji_order":"1276"},{"name":"nose tone 5","shortname":":nose_tone5:","category":"people","emoji_order":"1277"},{"name":"footprints","shortname":":footprints:","category":"people","emoji_order":"1278"},{"name":"eyes","shortname":":eyes:","category":"people","emoji_order":"1279"},{"name":"eye","shortname":":eye:","category":"people","emoji_order":"1280"},{"name":"eye in speech bubble","shortname":":eye_in_speech_bubble:","category":"symbols","emoji_order":"1281"},{"name":"tongue","shortname":":tongue:","category":"people","emoji_order":"1282"},{"name":"mouth","shortname":":lips:","category":"people","emoji_order":"1283"},{"name":"kiss mark","shortname":":kiss:","category":"people","emoji_order":"1284"},{"name":"heart with arrow","shortname":":cupid:","category":"symbols","emoji_order":"1285"},{"name":"heavy black heart","shortname":":heart:","category":"symbols","emoji_order":"1286","aliases_ascii":["<3"]},{"name":"beating heart","shortname":":heartbeat:","category":"symbols","emoji_order":"1287"},{"name":"broken heart","shortname":":broken_heart:","category":"symbols","emoji_order":"1288","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":":frustrated:","category":0,"emoji_order":77},{"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/Accessibility.js b/src/utils/Accessibility.js new file mode 100644 index 0000000000..f4909f971b --- /dev/null +++ b/src/utils/Accessibility.js @@ -0,0 +1,28 @@ +/* +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. +*/ + +/** + * Automatically focuses the captured reference when receiving a non-null + * object. Useful in scenarios where componentDidMount does not have a + * useful reference to an element, but one needs to focus the element on + * first render. Example usage: ref={focusCapturedRef} + * @param {function} ref The React reference to focus on, if not null + */ +export function focusCapturedRef(ref) { + if (ref) { + ref.focus(); + } +} diff --git a/src/utils/AutoDiscoveryUtils.js b/src/utils/AutoDiscoveryUtils.js new file mode 100644 index 0000000000..0850039344 --- /dev/null +++ b/src/utils/AutoDiscoveryUtils.js @@ -0,0 +1,107 @@ +/* +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 {AutoDiscovery} from "matrix-js-sdk"; +import {_td, newTranslatableError} from "../languageHandler"; +import {makeType} from "./TypeUtils"; +import SdkConfig from "../SdkConfig"; + +export class ValidatedServerConfig { + hsUrl: string; + hsName: string; + hsNameIsDifferent: string; + + isUrl: string; + identityEnabled: boolean; + + isDefault: boolean; +} + +export default class AutoDiscoveryUtils { + static async validateServerConfigWithStaticUrls(homeserverUrl: string, identityUrl: string): ValidatedServerConfig { + if (!homeserverUrl) { + throw newTranslatableError(_td("No homeserver URL provided")); + } + + const wellknownConfig = { + "m.homeserver": { + base_url: homeserverUrl, + }, + "m.identity_server": { + base_url: identityUrl, + }, + }; + + const result = await AutoDiscovery.fromDiscoveryConfig(wellknownConfig); + + const url = new URL(homeserverUrl); + const serverName = url.hostname; + + return AutoDiscoveryUtils.buildValidatedConfigFromDiscovery(serverName, result); + } + + static async validateServerName(serverName: string): ValidatedServerConfig { + const result = await AutoDiscovery.findClientConfig(serverName); + return AutoDiscoveryUtils.buildValidatedConfigFromDiscovery(serverName, result); + } + + static buildValidatedConfigFromDiscovery(serverName: string, discoveryResult): ValidatedServerConfig { + if (!discoveryResult || !discoveryResult["m.homeserver"]) { + // This shouldn't happen without major misconfiguration, so we'll log a bit of information + // in the log so we can find this bit of codee but otherwise tell teh user "it broke". + console.error("Ended up in a state of not knowing which homeserver to connect to."); + throw newTranslatableError(_td("Unexpected error resolving homeserver configuration")); + } + + const hsResult = discoveryResult['m.homeserver']; + if (hsResult.state !== AutoDiscovery.SUCCESS) { + if (AutoDiscovery.ALL_ERRORS.indexOf(hsResult.error) !== -1) { + throw newTranslatableError(hsResult.error); + } + throw newTranslatableError(_td("Unexpected error resolving homeserver configuration")); + } + + const isResult = discoveryResult['m.identity_server']; + let preferredIdentityUrl = "https://vector.im"; + if (isResult && isResult.state === AutoDiscovery.SUCCESS) { + preferredIdentityUrl = isResult["base_url"]; + } else if (isResult && isResult.state !== AutoDiscovery.PROMPT) { + console.error("Error determining preferred identity server URL:", isResult); + throw newTranslatableError(_td("Unexpected error resolving homeserver configuration")); + } + + const preferredHomeserverUrl = hsResult["base_url"]; + let preferredHomeserverName = serverName ? serverName : hsResult["server_name"]; + + const url = new URL(preferredHomeserverUrl); + if (!preferredHomeserverName) preferredHomeserverName = url.hostname; + + // It should have been set by now, so check it + if (!preferredHomeserverName) { + console.error("Failed to parse homeserver name from homeserver URL"); + throw newTranslatableError(_td("Unexpected error resolving homeserver configuration")); + } + + return makeType(ValidatedServerConfig, { + hsUrl: preferredHomeserverUrl, + hsName: preferredHomeserverName, + hsNameIsDifferent: url.hostname !== preferredHomeserverName, + isUrl: preferredIdentityUrl, + identityEnabled: !SdkConfig.get()['disable_identity_server'], + isDefault: false, + }); + } +} diff --git a/src/utils/EventUtils.js b/src/utils/EventUtils.js index 911257f95c..ff20a68e3c 100644 --- a/src/utils/EventUtils.js +++ b/src/utils/EventUtils.js @@ -15,7 +15,8 @@ limitations under the License. */ import { EventStatus } from 'matrix-js-sdk'; - +import MatrixClientPeg from '../MatrixClientPeg'; +import shouldHideEvent from "../shouldHideEvent"; /** * Returns whether an event should allow actions like reply, reactions, edit, etc. * which effectively checks whether it's a regular message that has been sent and that we @@ -43,3 +44,47 @@ export function isContentActionable(mxEvent) { return false; } + +export function canEditContent(mxEvent) { + return isContentActionable(mxEvent) && + mxEvent.getOriginalContent().msgtype === "m.text" && + mxEvent.getSender() === MatrixClientPeg.get().getUserId(); +} + +export function canEditOwnEvent(mxEvent) { + // for now we only allow editing + // your own events. So this just call through + // In the future though, moderators will be able to + // edit other people's messages as well but we don't + // want findEditableEvent to return other people's events + // hence this method. + return canEditContent(mxEvent); +} + +const MAX_JUMP_DISTANCE = 100; +export function findEditableEvent(room, isForward, fromEventId = undefined) { + const liveTimeline = room.getLiveTimeline(); + const events = liveTimeline.getEvents(); + const maxIdx = events.length - 1; + const inc = isForward ? 1 : -1; + const beginIdx = isForward ? 0 : maxIdx; + let endIdx = isForward ? maxIdx : 0; + if (!fromEventId) { + endIdx = Math.min(Math.max(0, beginIdx + (inc * MAX_JUMP_DISTANCE)), maxIdx); + } + let foundFromEventId = !fromEventId; + for (let i = beginIdx; i !== (endIdx + inc); i += inc) { + const e = events[i]; + // find start event first + if (!foundFromEventId && e.getId() === fromEventId) { + foundFromEventId = true; + // don't look further than MAX_JUMP_DISTANCE events from `fromEventId` + // to not iterate potentially 1000nds of events on key up/down + endIdx = Math.min(Math.max(0, i + (inc * MAX_JUMP_DISTANCE)), maxIdx); + } else if (foundFromEventId && !shouldHideEvent(e) && canEditOwnEvent(e)) { + // otherwise look for editable event + return e; + } + } +} + diff --git a/src/utils/FontManager.js b/src/utils/FontManager.js new file mode 100644 index 0000000000..796018a497 --- /dev/null +++ b/src/utils/FontManager.js @@ -0,0 +1,120 @@ +/* +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. +*/ + +/* + * Based on... + * ChromaCheck 1.16 + * author Roel Nieskens, https://pixelambacht.nl + * MIT license + */ + +function safariVersionCheck(ua) { + console.log("Browser is Safari - checking version for COLR support"); + try { + const safariVersionMatch = ua.match(/Mac OS X ([\d|_]+).*Version\/([\d|.]+).*Safari/); + if (safariVersionMatch) { + const macOSVersionStr = safariVersionMatch[1]; + const safariVersionStr = safariVersionMatch[2]; + const macOSVersion = macOSVersionStr.split("_").map(n => parseInt(n, 10)); + const safariVersion = safariVersionStr.split(".").map(n => parseInt(n, 10)); + const colrFontSupported = macOSVersion[0] >= 10 && macOSVersion[1] >= 14 && safariVersion[0] >= 12; + // https://www.colorfonts.wtf/ states safari supports COLR fonts from this version on + console.log(`COLR support on Safari requires macOS 10.14 and Safari 12, ` + + `detected Safari ${safariVersionStr} on macOS ${macOSVersionStr}, ` + + `COLR supported: ${colrFontSupported}`); + return colrFontSupported; + } + } catch (err) { + console.error("Error in Safari COLR version check", err); + } + console.warn("Couldn't determine Safari version to check COLR font support, assuming no."); + return false; +} + +async function isColrFontSupported() { + console.log("Checking for COLR support"); + + const {userAgent} = navigator; + // Firefox has supported COLR fonts since version 26 + // but doesn't support the check below without + // "Extract canvas data" permissions + // when content blocking is enabled. + if (userAgent.includes("Firefox")) { + console.log("Browser is Firefox - assuming COLR is supported"); + return true; + } + // Safari doesn't wait for the font to load (if it doesn't have it in cache) + // to emit the load event on the image, so there is no way to not make the check + // reliable. Instead sniff the version. + // Excluding "Chrome", as it's user agent unhelpfully also contains Safari... + if (!userAgent.includes("Chrome") && userAgent.includes("Safari")) { + return safariVersionCheck(userAgent); + } + + try { + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d'); + const img = new Image(); + // eslint-disable-next-line + const fontCOLR = 'd09GRgABAAAAAAKAAAwAAAAAAowAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABDT0xSAAACVAAAABYAAAAYAAIAJUNQQUwAAAJsAAAAEgAAABLJAAAQT1MvMgAAAYAAAAA6AAAAYBfxJ0pjbWFwAAABxAAAACcAAAAsAAzpM2dseWYAAAH0AAAAGgAAABoNIh0kaGVhZAAAARwAAAAvAAAANgxLumdoaGVhAAABTAAAABUAAAAkCAEEAmhtdHgAAAG8AAAABgAAAAYEAAAAbG9jYQAAAewAAAAGAAAABgANAABtYXhwAAABZAAAABsAAAAgAg4AHW5hbWUAAAIQAAAAOAAAAD4C5wsecG9zdAAAAkgAAAAMAAAAIAADAAB4AWNgZGAAYQ5+qdB4fpuvDNIsDCBwaQGTAIi+VlscBaJZGMDiHAxMIAoAtjIF/QB4AWNgZGBgYQACOAkUQQWMAAGRABAAAAB4AWNgZGBgYGJgAdMMUJILJMQgAWICAAH3AC4AeAFjYGFhYJzAwMrAwDST6QwDA0M/hGZ8zWDMyMmAChgFkDgKQMBw4CXDSwYWEBdIYgAFBgYA/8sIdAAABAAAAAAAAAB4AWNgYGBkYAZiBgYeBhYGBSDNAoRA/kuG//8hpDgjWJ4BAFVMBiYAAAAAAAANAAAAAQAAAAAEAAQAAAMAABEhESEEAPwABAD8AAAAeAEtxgUNgAAAAMHHIQTShTlOAty9/4bf7AARCwlBNhBw4L/43qXjYGUmf19TMuLcj/BJL3XfBg54AWNgZsALAAB9AAR4AWNgYGAEYj4gFgGygGwICQACOwAoAAAAAAABAAEAAQAAAA4AAAAAyP8AAA=='; + const svg = ` + + + + + + `; + canvas.width = 20; + canvas.height = 100; + + img.src = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svg); + + console.log("Waiting for COLR SVG to load"); + await new Promise(resolve => img.onload = resolve); + console.log("Drawing canvas to detect COLR support"); + context.drawImage(img, 0, 0); + const colrFontSupported = (context.getImageData(10, 10, 1, 1).data[0] === 200); + console.log("Canvas check revealed COLR is supported? " + colrFontSupported); + return colrFontSupported; + } catch (e) { + console.error("Couldn't load COLR font", e); + return false; + } +} + +let colrFontCheckStarted = false; +export async function fixupColorFonts() { + if (colrFontCheckStarted) { + return; + } + colrFontCheckStarted = true; + + if (await isColrFontSupported()) { + const path = `url('${require("../../res/fonts/Twemoji_Mozilla/TwemojiMozilla-colr.woff2")}')`; + document.fonts.add(new FontFace("Twemoji", path, {})); + // For at least Chrome on Windows 10, we have to explictly add extra + // weights for the emoji to appear in bold messages, etc. + document.fonts.add(new FontFace("Twemoji", path, { weight: 600 })); + document.fonts.add(new FontFace("Twemoji", path, { weight: 700 })); + } + // if not supported, the browser will fall back to one of the native fonts specified. +} + diff --git a/src/utils/FormattingUtils.js b/src/utils/FormattingUtils.js index b461d22079..1fd7d00feb 100644 --- a/src/utils/FormattingUtils.js +++ b/src/utils/FormattingUtils.js @@ -1,5 +1,6 @@ /* Copyright 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. @@ -14,6 +15,8 @@ See the License for the specific language governing permissions and limitations under the License. */ +import { _t } from '../languageHandler'; + /** * formats numbers to fit into ~3 characters, suitable for badge counts * e.g: 999, 9.9K, 99K, 0.9M, 9.9M, 99M, 0.9B, 9.9B @@ -63,3 +66,31 @@ export function getUserNameColorClass(userId) { const colorNumber = (hashCode(userId) % 8) + 1; return `mx_Username_color${colorNumber}`; } + +/** + * Constructs a written English string representing `items`, with an optional + * limit on the number of items included in the result. If specified and if the + * length of `items` is greater than the limit, the string "and n others" will + * be appended onto the result. If `items` is empty, returns the empty string. + * If there is only one item, return it. + * @param {string[]} items the items to construct a string from. + * @param {number?} itemLimit the number by which to limit the list. + * @returns {string} a string constructed by joining `items` with a comma + * between each item, but with the last item appended as " and [lastItem]". + */ +export function formatCommaSeparatedList(items, itemLimit) { + const remaining = itemLimit === undefined ? 0 : Math.max( + items.length - itemLimit, 0, + ); + if (items.length === 0) { + return ""; + } else if (items.length === 1) { + return items[0]; + } else if (remaining > 0) { + items = items.slice(0, itemLimit); + return _t("%(items)s and %(count)s others", { items: items.join(', '), count: remaining } ); + } else { + const lastItem = items.pop(); + return _t("%(items)s and %(lastItem)s", { items: items.join(', '), lastItem: lastItem }); + } +} diff --git a/src/utils/TypeUtils.js b/src/utils/TypeUtils.js new file mode 100644 index 0000000000..abdd0eb2a0 --- /dev/null +++ b/src/utils/TypeUtils.js @@ -0,0 +1,30 @@ +/* +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. +*/ + +/** + * Creates a class of a given type using the objects defined. This + * is a stopgap function while we don't have TypeScript interfaces. + * In future, we'd define the `type` as an interface and just cast + * it instead of cheating like we are here. + * @param {Type} Type The type of class to construct. + * @param {*} opts The options (properties) to set on the object. + * @returns {*} The created object. + */ +export function makeType(Type: any, opts: any) { + const c = new Type(); + Object.assign(c, opts); + return c; +} diff --git a/test/components/structures/auth/Login-test.js b/test/components/structures/auth/Login-test.js index ec95243a56..74451b922f 100644 --- a/test/components/structures/auth/Login-test.js +++ b/test/components/structures/auth/Login-test.js @@ -22,6 +22,7 @@ import ReactTestUtils from 'react-dom/test-utils'; import sdk from 'matrix-react-sdk'; import SdkConfig from '../../../../src/SdkConfig'; import * as TestUtils from '../../../test-utils'; +import {mkServerConfig} from "../../../test-utils"; const Login = sdk.getComponent( 'structures.auth.Login', @@ -44,8 +45,7 @@ describe('Login', function() { function render() { return ReactDOM.render( {}} onRegisterClick={() => {}} onServerConfigChange={() => {}} diff --git a/test/components/structures/auth/Registration-test.js b/test/components/structures/auth/Registration-test.js index a10201d465..6914ed71d7 100644 --- a/test/components/structures/auth/Registration-test.js +++ b/test/components/structures/auth/Registration-test.js @@ -22,6 +22,7 @@ import ReactTestUtils from 'react-dom/test-utils'; import sdk from 'matrix-react-sdk'; import SdkConfig from '../../../../src/SdkConfig'; import * as TestUtils from '../../../test-utils'; +import {mkServerConfig} from "../../../test-utils"; const Registration = sdk.getComponent( 'structures.auth.Registration', @@ -44,8 +45,7 @@ describe('Registration', function() { function render() { return ReactDOM.render( {}} onLoggedIn={() => {}} onLoginClick={() => {}} diff --git a/test/test-utils.js b/test/test-utils.js index f4f00effbb..54705434e2 100644 --- a/test/test-utils.js +++ b/test/test-utils.js @@ -7,6 +7,8 @@ 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"; const MatrixEvent = jssdk.MatrixEvent; /** @@ -260,6 +262,16 @@ export function mkStubRoom(roomId = null) { }; } +export function mkServerConfig(hsUrl, isUrl) { + return makeType(ValidatedServerConfig, { + hsUrl, + hsName: "TEST_ENVIRONMENT", + hsNameIsDifferent: false, // yes, we lie + isUrl, + identityEnabled: true, + }); +} + export function getDispatchForStore(store) { // Mock the dispatcher by gut-wrenching. Stores can only __emitChange whilst a // dispatcher `_isDispatching` is true. diff --git a/yarn.lock b/yarn.lock index 22072971eb..fa5e6c660e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1450,13 +1450,6 @@ balanced-match@^1.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= -base-x@3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/base-x/-/base-x-3.0.4.tgz#94c1788736da065edb1d68808869e357c977fa77" - integrity sha512-UYOadoSIkEI/VrRGSG6qp93rp2WdokiAiNYDfGW5qURAY8GiAQkvMbwNNSDYiVJopqv4gCna7xqf4rrNGp+5AA== - dependencies: - safe-buffer "^5.0.1" - base-x@^3.0.2: version "3.0.5" resolved "https://registry.yarnpkg.com/base-x/-/base-x-3.0.5.tgz#d3ada59afed05b921ab581ec3112e6444ba0795a" @@ -2576,10 +2569,15 @@ emoji-regex@^7.0.1: resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156" integrity sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA== -emojione@2.2.7: - version "2.2.7" - resolved "https://registry.yarnpkg.com/emojione/-/emojione-2.2.7.tgz#46457cf6b9b2f8da13ae8a2e4e547de06ee15e96" - integrity sha1-RkV89rmy+NoTroouTlR94G7hXpY= +emojibase-data@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/emojibase-data/-/emojibase-data-4.0.0.tgz#3feb3e5bb5e5f2b6373b183b0f038c60889a9e29" + integrity sha512-Yi4A1IxB7iZ+09Wqr2BEpHSQfugc5I8G+wckDOhCia0F7oOdErf/85jCwbpRQy7xtBbvlyS3xQrYedSeQot5Og== + +emojibase-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/emojibase-regex/-/emojibase-regex-3.0.0.tgz#fc7a17aa20584df5a73619f06ac236b8a5bb452d" + integrity sha512-iNDkbtn8UxKTxjIlvHLqfXovZaIulnuuyo2/emU+ZlPr2OmzxGfiDI+iIQ3WWqdlA6lgjrPNWgpbytt4lnJYrg== emojis-list@^2.0.0: version "2.1.0" @@ -4879,14 +4877,13 @@ mathml-tag-names@^2.0.1: resolved "https://registry.yarnpkg.com/mathml-tag-names/-/mathml-tag-names-2.1.0.tgz#490b70e062ee24636536e3d9481e333733d00f2c" integrity sha512-3Zs9P/0zzwTob2pdgT0CHZuMbnSUSp8MB1bddfm+HDmnFWHGT4jvEZRf+2RuPoa+cjdn/z25SEt5gFTqdhvJAg== -matrix-js-sdk@1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-1.1.0.tgz#f0db49de2aa0b0d50c1fe49da538beb4926ce59c" - integrity sha512-ECoMN6DkwPdKiMa/jSoMkSDngFCo6x7oH84rLd1NtD7lBPl3Ejj6ARa0iIELE7u0OUO6J0FzdWh7Hd0ZnVTmww== +matrix-js-sdk@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-1.2.0.tgz#1e18f7bead2b31f1b6886299f3198f545eb7d19e" + integrity sha512-XqxIm30/ePPQxMpoSZ6560nVZN296E7DI4twGKspII+Pvc0u7p+UHTh3OXwTtPOj3eVo0ptKv3kn/a5SmYzXmQ== dependencies: another-json "^0.2.0" babel-runtime "^6.26.0" - base-x "3.0.4" bluebird "^3.5.0" browser-request "^0.3.3" bs58 "^4.0.1"