diff --git a/.eslintignore.errorfiles b/.eslintignore.errorfiles index ffd492d491..55eaf75e4b 100644 --- a/.eslintignore.errorfiles +++ b/.eslintignore.errorfiles @@ -1,6 +1,5 @@ # autogenerated file: run scripts/generate-eslint-error-ignore-file to update. -src/AddThreepid.js src/async-components/views/dialogs/EncryptedEventDialog.js src/autocomplete/AutocompleteProvider.js src/autocomplete/Autocompleter.js @@ -9,8 +8,6 @@ src/autocomplete/DuckDuckGoProvider.js src/autocomplete/EmojiProvider.js src/autocomplete/RoomProvider.js src/autocomplete/UserProvider.js -src/Avatar.js -src/BasePlatform.js src/CallHandler.js src/component-index.js src/components/structures/ContextualMenu.js @@ -96,7 +93,6 @@ src/components/views/rooms/MessageComposerInput.js src/components/views/rooms/MessageComposerInputOld.js src/components/views/rooms/PresenceLabel.js src/components/views/rooms/ReadReceiptMarker.js -src/components/views/rooms/RoomHeader.js src/components/views/rooms/RoomList.js src/components/views/rooms/RoomNameEditor.js src/components/views/rooms/RoomPreviewBar.js @@ -115,16 +111,7 @@ src/components/views/settings/ChangePassword.js src/components/views/settings/DevicesPanel.js src/components/views/settings/DevicesPanelEntry.js src/components/views/settings/EnableNotificationsButton.js -src/components/views/voip/CallView.js -src/components/views/voip/IncomingCallBox.js -src/components/views/voip/VideoFeed.js -src/components/views/voip/VideoView.js src/ContentMessages.js -src/createRoom.js -src/DateUtils.js -src/email.js -src/Entities.js -src/extend.js src/HtmlUtils.js src/ImageUtils.js src/Invite.js @@ -135,30 +122,20 @@ src/Markdown.js src/MatrixClientPeg.js src/Modal.js src/Notifier.js -src/ObjectUtils.js -src/PasswordReset.js src/PlatformPeg.js src/Presence.js src/ratelimitedfunc.js -src/Resend.js src/RichText.js src/Roles.js -src/RoomListSorter.js -src/RoomNotifs.js src/Rooms.js src/ScalarAuthClient.js src/ScalarMessaging.js -src/SdkConfig.js -src/Skinner.js -src/SlashCommands.js -src/stores/LifecycleStore.js src/TabComplete.js src/TabCompleteEntries.js src/TextForEvent.js src/Tinter.js src/UiEffects.js src/Unread.js -src/UserActivity.js src/utils/DecryptFile.js src/utils/DMRoomMap.js src/utils/FormattingUtils.js diff --git a/.travis-test-riot.sh b/.travis-test-riot.sh index 4296c72e6c..87200871a5 100755 --- a/.travis-test-riot.sh +++ b/.travis-test-riot.sh @@ -22,8 +22,11 @@ git checkout "$curbranch" || git checkout develop mkdir node_modules npm install -(cd node_modules/matrix-js-sdk && npm install) +# use the version of js-sdk we just used in the react-sdk tests +rm -r node_modules/matrix-js-sdk +ln -s "$REACT_SDK_DIR/node_modules/matrix-js-sdk" node_modules/matrix-js-sdk +# ... and, of course, the version of react-sdk we just built rm -r node_modules/matrix-react-sdk ln -s "$REACT_SDK_DIR" node_modules/matrix-react-sdk diff --git a/.travis.yml b/.travis.yml index 918cec696b..4137d754bf 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,15 @@ +# we need trusty for the chrome addon +dist: trusty + +# we don't need sudo, so can run in a container, which makes startup much +# quicker. +sudo: false + language: node_js node_js: - node # Latest stable version of nodejs. +addons: + chrome: stable install: - npm install - (cd node_modules/matrix-js-sdk && npm install) diff --git a/karma.conf.js b/karma.conf.js index d544248332..d8a6c25cc6 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -116,11 +116,25 @@ module.exports = function (config) { browsers: [ 'Chrome', //'PhantomJS', + //'ChromeHeadless', ], + customLaunchers: { + 'ChromeHeadless': { + base: 'Chrome', + flags: [ + // See https://chromium.googlesource.com/chromium/src/+/lkgr/headless/README.md + '--headless', + '--disable-gpu', + // Without a remote debugging port, Google Chrome exits immediately. + '--remote-debugging-port=9222', + ], + } + }, + // Continuous Integration mode // if true, Karma captures browsers, runs the tests and exits - singleRun: true, + // singleRun: false, // Concurrency level // how many browser should be started simultaneous diff --git a/package.json b/package.json index 8d638a5928..888fd9e32a 100644 --- a/package.json +++ b/package.json @@ -33,15 +33,16 @@ "scripts": { "reskindex": "node scripts/reskindex.js -h header", "reskindex:watch": "node scripts/reskindex.js -h header -w", - "build": "npm run reskindex && babel src -d lib --source-maps", - "build:watch": "babel src -w -d lib --source-maps", + "build": "npm run reskindex && babel src -d lib --source-maps --copy-files", + "build:watch": "babel src -w -d lib --source-maps --copy-files", + "emoji-data-strip": "node scripts/emoji-data-strip.js", "start": "parallelshell \"npm run build:watch\" \"npm run reskindex:watch\"", "lint": "eslint src/", "lintall": "eslint src/ test/", "clean": "rimraf lib", "prepublish": "npm run build && git rev-parse HEAD > git-revision.txt", - "test": "karma start $KARMAFLAGS --browsers PhantomJS", - "test-multi": "karma start $KARMAFLAGS --single-run=false" + "test": "karma start $KARMAFLAGS --single-run=true --browsers ChromeHeadless", + "test-multi": "karma start $KARMAFLAGS" }, "dependencies": { "babel-runtime": "^6.11.6", @@ -74,6 +75,7 @@ "react-gemini-scrollbar": "matrix-org/react-gemini-scrollbar#5e97aef", "sanitize-html": "^1.11.1", "text-encoding-utf-8": "^1.0.1", + "url": "^0.11.0", "velocity-vector": "vector-im/velocity#059e3b2", "whatwg-fetch": "^1.0.0" }, @@ -105,12 +107,10 @@ "karma-cli": "^0.1.2", "karma-junit-reporter": "^0.4.1", "karma-mocha": "^0.2.2", - "karma-phantomjs-launcher": "^1.0.0", "karma-sourcemap-loader": "^0.3.7", "karma-webpack": "^1.7.0", "mocha": "^2.4.5", "parallelshell": "^1.2.0", - "phantomjs-prebuilt": "^2.1.7", "react-addons-test-utils": "^15.4.0", "require-json": "0.0.1", "rimraf": "^2.4.3", diff --git a/scripts/emoji-data-strip.js b/scripts/emoji-data-strip.js new file mode 100644 index 0000000000..40156471fe --- /dev/null +++ b/scripts/emoji-data-strip.js @@ -0,0 +1,26 @@ +#!/usr/bin/env node +const EMOJI_DATA = require('emojione/emoji.json'); +const EMOJI_SUPPORTED = Object.keys(require('emojione').emojioneList); +const fs = require('fs'); + +const output = Object.keys(EMOJI_DATA).map( + (key) => { + const datum = EMOJI_DATA[key]; + const newDatum = { + name: datum.name, + shortname: datum.shortname, + category: datum.category, + emoji_order: datum.emoji_order, + }; + if (datum.aliases_ascii.length > 0) { + newDatum.aliases_ascii = datum.aliases_ascii; + } + 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 +fs.writeFileSync('./src/stripped-emoji.json', JSON.stringify(output)); diff --git a/src/AddThreepid.js b/src/AddThreepid.js index 8be7a19b13..337e38d867 100644 --- a/src/AddThreepid.js +++ b/src/AddThreepid.js @@ -15,7 +15,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -var MatrixClientPeg = require("./MatrixClientPeg"); +import MatrixClientPeg from './MatrixClientPeg'; import { _t } from './languageHandler'; /** @@ -44,7 +44,7 @@ class AddThreepid { this.sessionId = res.sid; return res; }, function(err) { - if (err.errcode == 'M_THREEPID_IN_USE') { + if (err.errcode === 'M_THREEPID_IN_USE') { err.message = _t('This email address is already in use'); } else if (err.httpStatus) { err.message = err.message + ` (Status ${err.httpStatus})`; @@ -69,7 +69,7 @@ class AddThreepid { this.sessionId = res.sid; return res; }, function(err) { - if (err.errcode == 'M_THREEPID_IN_USE') { + if (err.errcode === 'M_THREEPID_IN_USE') { err.message = _t('This phone number is already in use'); } else if (err.httpStatus) { err.message = err.message + ` (Status ${err.httpStatus})`; @@ -85,16 +85,15 @@ class AddThreepid { * the request failed. */ checkEmailLinkClicked() { - var identityServerDomain = MatrixClientPeg.get().idBaseUrl.split("://")[1]; + const identityServerDomain = MatrixClientPeg.get().idBaseUrl.split("://")[1]; return MatrixClientPeg.get().addThreePid({ sid: this.sessionId, client_secret: this.clientSecret, - id_server: identityServerDomain + id_server: identityServerDomain, }, this.bind).catch(function(err) { if (err.httpStatus === 401) { err.message = _t('Failed to verify email address: make sure you clicked the link in the email'); - } - else if (err.httpStatus) { + } else if (err.httpStatus) { err.message += ` (Status ${err.httpStatus})`; } throw err; @@ -104,6 +103,7 @@ class AddThreepid { /** * Takes a phone number verification code as entered by the user and validates * it with the ID server, then if successful, adds the phone number. + * @param {string} token phone number verification code as entered by the user * @return {Promise} Resolves if the phone number was added. Rejects with an object * with a "message" property which contains a human-readable message detailing why * the request failed. @@ -119,7 +119,7 @@ class AddThreepid { return MatrixClientPeg.get().addThreePid({ sid: this.sessionId, client_secret: this.clientSecret, - id_server: identityServerDomain + id_server: identityServerDomain, }, this.bind); }); } diff --git a/src/Avatar.js b/src/Avatar.js index c0127d49af..d41a3f6a79 100644 --- a/src/Avatar.js +++ b/src/Avatar.js @@ -15,18 +15,18 @@ limitations under the License. */ 'use strict'; -var ContentRepo = require("matrix-js-sdk").ContentRepo; -var MatrixClientPeg = require('./MatrixClientPeg'); +import {ContentRepo} from 'matrix-js-sdk'; +import MatrixClientPeg from './MatrixClientPeg'; module.exports = { avatarUrlForMember: function(member, width, height, resizeMethod) { - var url = member.getAvatarUrl( + let url = member.getAvatarUrl( MatrixClientPeg.get().getHomeserverUrl(), Math.floor(width * window.devicePixelRatio), Math.floor(height * window.devicePixelRatio), resizeMethod, false, - false + false, ); if (!url) { // member can be null here currently since on invites, the JS SDK @@ -38,11 +38,11 @@ module.exports = { }, avatarUrlForUser: function(user, width, height, resizeMethod) { - var url = ContentRepo.getHttpUriForMxc( + const url = ContentRepo.getHttpUriForMxc( MatrixClientPeg.get().getHomeserverUrl(), user.avatarUrl, Math.floor(width * window.devicePixelRatio), Math.floor(height * window.devicePixelRatio), - resizeMethod + resizeMethod, ); if (!url || url.length === 0) { return null; @@ -51,11 +51,11 @@ module.exports = { }, defaultAvatarUrlForString: function(s) { - var images = ['76cfa6', '50e2c2', 'f4c371']; - var total = 0; - for (var i = 0; i < s.length; ++i) { + const images = ['76cfa6', '50e2c2', 'f4c371']; + let total = 0; + for (let i = 0; i < s.length; ++i) { total += s.charCodeAt(i); } return 'img/' + images[total % images.length] + '.png'; - } + }, }; diff --git a/src/BasePlatform.js b/src/BasePlatform.js index a920479823..5f8772c7aa 100644 --- a/src/BasePlatform.js +++ b/src/BasePlatform.js @@ -57,6 +57,7 @@ export default class BasePlatform { /** * Returns true if the platform supports displaying * notifications, otherwise false. + * @returns {boolean} whether the platform supports displaying notifications */ supportsNotifications(): boolean { return false; @@ -65,6 +66,7 @@ export default class BasePlatform { /** * Returns true if the application currently has permission * to display notifications. Otherwise false. + * @returns {boolean} whether the application has permission to display notifications */ maySendNotifications(): boolean { return false; diff --git a/src/ComposerHistoryManager.js b/src/ComposerHistoryManager.js index 3e19a78bfe..1ae836574b 100644 --- a/src/ComposerHistoryManager.js +++ b/src/ComposerHistoryManager.js @@ -52,21 +52,19 @@ export default class ComposerHistoryManager { history: Array = []; prefix: string; lastIndex: number = 0; - currentIndex: number = -1; + currentIndex: number = 0; constructor(roomId: string, prefix: string = 'mx_composer_history_') { this.prefix = prefix + roomId; // TODO: Performance issues? - for(; sessionStorage.getItem(`${this.prefix}[${this.lastIndex}]`); this.lastIndex++, this.currentIndex++) { + let item; + for(; item = sessionStorage.getItem(`${this.prefix}[${this.currentIndex}]`); this.currentIndex++) { this.history.push( - Object.assign( - new HistoryItem(), - JSON.parse(sessionStorage.getItem(`${this.prefix}[${this.lastIndex}]`)), - ), + Object.assign(new HistoryItem(), JSON.parse(item)), ); } - this.currentIndex--; + this.lastIndex = this.currentIndex; } addItem(message: string, format: MessageFormat) { diff --git a/src/DateUtils.js b/src/DateUtils.js index 545d92dd3b..78eef57eae 100644 --- a/src/DateUtils.js +++ b/src/DateUtils.js @@ -54,24 +54,25 @@ function pad(n) { function twelveHourTime(date) { let hours = date.getHours() % 12; const minutes = pad(date.getMinutes()); - const ampm = date.getHours() >= 12 ? 'PM' : 'AM'; - hours = pad(hours ? hours : 12); + const ampm = date.getHours() >= 12 ? _t('PM') : _t('AM'); + hours = hours ? hours : 12; // convert 0 -> 12 return `${hours}:${minutes}${ampm}`; } module.exports = { formatDate: function(date, showTwelveHour=false) { - var now = new Date(); + const now = new Date(); const days = getDaysArray(); const months = getMonthsArray(); if (date.toDateString() === now.toDateString()) { return this.formatTime(date); - } - else if (now.getTime() - date.getTime() < 6 * 24 * 60 * 60 * 1000) { + } else if (now.getTime() - date.getTime() < 6 * 24 * 60 * 60 * 1000) { // TODO: use standard date localize function provided in counterpart - return _t('%(weekDayName)s %(time)s', {weekDayName: days[date.getDay()], time: this.formatTime(date, showTwelveHour)}); - } - else if (now.getFullYear() === date.getFullYear()) { + return _t('%(weekDayName)s %(time)s', { + weekDayName: days[date.getDay()], + time: this.formatTime(date, showTwelveHour), + }); + } else if (now.getFullYear() === date.getFullYear()) { // TODO: use standard date localize function provided in counterpart return _t('%(weekDayName)s, %(monthName)s %(day)s %(time)s', { weekDayName: days[date.getDay()], diff --git a/src/Entities.js b/src/Entities.js index 7c3909f36f..21abd9c473 100644 --- a/src/Entities.js +++ b/src/Entities.js @@ -14,8 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -var React = require('react'); -var sdk = require('./index'); +import sdk from './index'; function isMatch(query, name, uid) { query = query.toLowerCase(); @@ -33,8 +32,8 @@ function isMatch(query, name, uid) { } // split spaces in name and try matching constituent parts - var parts = name.split(" "); - for (var i = 0; i < parts.length; i++) { + const parts = name.split(" "); + for (let i = 0; i < parts.length; i++) { if (parts[i].indexOf(query) === 0) { return true; } @@ -67,7 +66,7 @@ class Entity { class MemberEntity extends Entity { getJsx() { - var MemberTile = sdk.getComponent("rooms.MemberTile"); + const MemberTile = sdk.getComponent("rooms.MemberTile"); return ( ); @@ -84,6 +83,7 @@ class UserEntity extends Entity { super(model); this.showInviteButton = Boolean(showInviteButton); this.inviteFn = inviteFn; + this.onClick = this.onClick.bind(this); } onClick() { @@ -93,15 +93,15 @@ class UserEntity extends Entity { } getJsx() { - var UserTile = sdk.getComponent("rooms.UserTile"); + const UserTile = sdk.getComponent("rooms.UserTile"); return ( + showInviteButton={this.showInviteButton} onClick={this.onClick} /> ); } matches(queryString) { - var name = this.model.displayName || this.model.userId; + const name = this.model.displayName || this.model.userId; return isMatch(queryString, name, this.model.userId); } } @@ -109,7 +109,7 @@ class UserEntity extends Entity { module.exports = { newEntity: function(jsx, matchFn) { - var entity = new Entity(); + const entity = new Entity(); entity.getJsx = function() { return jsx; }; @@ -137,5 +137,5 @@ module.exports = { return users.map(function(u) { return new UserEntity(u, showInviteButton, inviteFn); }); - } + }, }; diff --git a/src/HtmlUtils.js b/src/HtmlUtils.js index 5c1c2881e5..8c7e397fce 100644 --- a/src/HtmlUtils.js +++ b/src/HtmlUtils.js @@ -84,7 +84,7 @@ export function charactersToImageNode(alt, useSvg, ...unicode) { } -export function stripParagraphs(html: string): string { +export function processHtmlForSending(html: string): string { const contentDiv = document.createElement('div'); contentDiv.innerHTML = html; @@ -93,10 +93,21 @@ export function stripParagraphs(html: string): string { } let contentHTML = ""; - for (let i=0; i'; + contentHTML += element.innerHTML; + // Don't add a
for the last

+ if (i !== contentDiv.children.length - 1) { + contentHTML += '
'; + } + } else if (element.tagName.toLowerCase() === 'pre') { + // Replace "
\n" with "\n" within `

` tags because the 
is + // redundant. This is a workaround for a bug in draft-js-export-html: + // https://github.com/sstur/draft-js-export-html/issues/62 + contentHTML += '
' +
+                element.innerHTML.replace(/
\n/g, '\n').trim() + + '
'; } else { const temp = document.createElement('div'); temp.appendChild(element.cloneNode(true)); @@ -134,6 +145,7 @@ const sanitizeHtmlParams = { // would make sense if we did img: ['src'], ol: ['start'], + code: ['class'], // We don't actually allow all classes, we filter them in transformTags }, // Lots of these won't come up by default because we don't allow them selfClosing: ['img', 'br', 'hr', 'area', 'base', 'basefont', 'input', 'link', 'meta'], @@ -175,6 +187,19 @@ const sanitizeHtmlParams = { attribs.rel = 'noopener'; // https://mathiasbynens.github.io/rel-noopener/ return { tagName: tagName, attribs : attribs }; }, + 'code': function(tagName, attribs) { + if (typeof attribs.class !== 'undefined') { + // Filter out all classes other than ones starting with language- for syntax highlighting. + let classes = attribs.class.split(/\s+/).filter(function(cl) { + return cl.startsWith('language-'); + }); + attribs.class = classes.join(' '); + } + return { + tagName: tagName, + attribs: attribs, + }; + }, '*': function(tagName, attribs) { // Delete any style previously assigned, style is an allowedTag for font and span // because attributes are stripped after transforming diff --git a/src/KeyCode.js b/src/KeyCode.js index 28aafc00cb..90c2caeb0e 100644 --- a/src/KeyCode.js +++ b/src/KeyCode.js @@ -30,7 +30,30 @@ module.exports = { RIGHT: 39, DOWN: 40, DELETE: 46, + KEY_A: 65, + KEY_B: 66, + KEY_C: 67, KEY_D: 68, KEY_E: 69, + KEY_F: 70, + KEY_G: 71, + KEY_H: 72, + KEY_I: 73, + KEY_J: 74, + KEY_K: 75, + KEY_L: 76, KEY_M: 77, + KEY_N: 78, + KEY_O: 79, + KEY_P: 80, + KEY_Q: 81, + KEY_R: 82, + KEY_S: 83, + KEY_T: 84, + KEY_U: 85, + KEY_V: 86, + KEY_W: 87, + KEY_X: 88, + KEY_Y: 89, + KEY_Z: 90, }; diff --git a/src/Lifecycle.js b/src/Lifecycle.js index 06f5d9ef00..f64e2b3858 100644 --- a/src/Lifecycle.js +++ b/src/Lifecycle.js @@ -419,6 +419,8 @@ export function logout() { * listen for events while a session is logged in. */ function startMatrixClient() { + console.log(`Lifecycle: Starting MatrixClient`); + // dispatch this before starting the matrix client: it's used // to add listeners for the 'sync' event so otherwise we'd have // a race condition (and we need to dispatch synchronously for this diff --git a/src/Markdown.js b/src/Markdown.js index 4a46ce4f24..5730e42a09 100644 --- a/src/Markdown.js +++ b/src/Markdown.js @@ -17,7 +17,7 @@ limitations under the License. import commonmark from 'commonmark'; import escape from 'lodash/escape'; -const ALLOWED_HTML_TAGS = ['del']; +const ALLOWED_HTML_TAGS = ['del', 'u']; // These types of node are definitely text const TEXT_NODES = ['text', 'softbreak', 'linebreak', 'paragraph', 'document']; diff --git a/src/MatrixClientPeg.js b/src/MatrixClientPeg.js index 0676e4600f..b31cf7511e 100644 --- a/src/MatrixClientPeg.js +++ b/src/MatrixClientPeg.js @@ -77,22 +77,26 @@ class MatrixClientPeg { this._createClient(creds); } - start() { + async start() { const opts = utils.deepCopy(this.opts); // the react sdk doesn't work without this, so don't allow opts.pendingEventOrdering = "detached"; - let promise = this.matrixClient.store.startup(); - // log any errors when starting up the database (if one exists) - promise.catch((err) => { + try { + let promise = this.matrixClient.store.startup(); + console.log(`MatrixClientPeg: waiting for MatrixClient store to initialise`); + await promise; + } catch(err) { + // log any errors when starting up the database (if one exists) console.error(`Error starting matrixclient store: ${err}`); - }); + } // regardless of errors, start the client. If we did error out, we'll // just end up doing a full initial /sync. - promise.finally(() => { - this.get().startClient(opts); - }); + + console.log(`MatrixClientPeg: really starting MatrixClient`); + this.get().startClient(opts); + console.log(`MatrixClientPeg: MatrixClient started`); } getCredentials(): MatrixClientCreds { diff --git a/src/ObjectUtils.js b/src/ObjectUtils.js index 5fac588a4f..07d8b465af 100644 --- a/src/ObjectUtils.js +++ b/src/ObjectUtils.js @@ -23,8 +23,8 @@ limitations under the License. * { key: $KEY, val: $VALUE, place: "add|del" } */ module.exports.getKeyValueArrayDiffs = function(before, after) { - var results = []; - var delta = {}; + const results = []; + const delta = {}; Object.keys(before).forEach(function(beforeKey) { delta[beforeKey] = delta[beforeKey] || 0; // init to 0 initially delta[beforeKey]--; // keys present in the past have -ve values @@ -46,9 +46,9 @@ module.exports.getKeyValueArrayDiffs = function(before, after) { results.push({ place: "del", key: muxedKey, val: beforeVal }); }); break; - case 0: // A mix of added/removed keys + case 0: {// A mix of added/removed keys // compare old & new vals - var itemDelta = {}; + const itemDelta = {}; before[muxedKey].forEach(function(beforeVal) { itemDelta[beforeVal] = itemDelta[beforeVal] || 0; itemDelta[beforeVal]--; @@ -68,9 +68,9 @@ module.exports.getKeyValueArrayDiffs = function(before, after) { } }); break; + } default: - console.error("Calculated key delta of " + delta[muxedKey] + - " - this should never happen!"); + console.error("Calculated key delta of " + delta[muxedKey] + " - this should never happen!"); break; } }); @@ -79,8 +79,10 @@ module.exports.getKeyValueArrayDiffs = function(before, after) { }; /** - * Shallow-compare two objects for equality: each key and value must be - * identical + * Shallow-compare two objects for equality: each key and value must be identical + * @param {Object} objA First object to compare against the second + * @param {Object} objB Second object to compare against the first + * @return {boolean} whether the two objects have same key=values */ module.exports.shallowEqual = function(objA, objB) { if (objA === objB) { @@ -92,15 +94,15 @@ module.exports.shallowEqual = function(objA, objB) { return false; } - var keysA = Object.keys(objA); - var keysB = Object.keys(objB); + const keysA = Object.keys(objA); + const keysB = Object.keys(objB); if (keysA.length !== keysB.length) { return false; } - for (var i = 0; i < keysA.length; i++) { - var key = keysA[i]; + for (let i = 0; i < keysA.length; i++) { + const key = keysA[i]; if (!objB.hasOwnProperty(key) || objA[key] !== objB[key]) { return false; } diff --git a/src/PasswordReset.js b/src/PasswordReset.js index 0739ca0a24..71fc4f6b31 100644 --- a/src/PasswordReset.js +++ b/src/PasswordReset.js @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -var Matrix = require("matrix-js-sdk"); +import * as Matrix from 'matrix-js-sdk'; import { _t } from './languageHandler'; /** @@ -34,7 +34,7 @@ class PasswordReset { constructor(homeserverUrl, identityUrl) { this.client = Matrix.createClient({ baseUrl: homeserverUrl, - idBaseUrl: identityUrl + idBaseUrl: identityUrl, }); this.clientSecret = this.client.generateClientSecret(); this.identityServerDomain = identityUrl.split("://")[1]; @@ -53,7 +53,7 @@ class PasswordReset { this.sessionId = res.sid; return res; }, function(err) { - if (err.errcode == 'M_THREEPID_NOT_FOUND') { + if (err.errcode === 'M_THREEPID_NOT_FOUND') { err.message = _t('This email address was not found'); } else if (err.httpStatus) { err.message = err.message + ` (Status ${err.httpStatus})`; @@ -75,16 +75,15 @@ class PasswordReset { threepid_creds: { sid: this.sessionId, client_secret: this.clientSecret, - id_server: this.identityServerDomain - } + id_server: this.identityServerDomain, + }, }, this.password).catch(function(err) { if (err.httpStatus === 401) { err.message = _t('Failed to verify email address: make sure you clicked the link in the email'); - } - else if (err.httpStatus === 404) { - err.message = _t('Your email address does not appear to be associated with a Matrix ID on this Homeserver.'); - } - else if (err.httpStatus) { + } else if (err.httpStatus === 404) { + err.message = + _t('Your email address does not appear to be associated with a Matrix ID on this Homeserver.'); + } else if (err.httpStatus) { err.message += ` (Status ${err.httpStatus})`; } throw err; diff --git a/src/Resend.js b/src/Resend.js index bbd980ea7f..1fee5854ea 100644 --- a/src/Resend.js +++ b/src/Resend.js @@ -14,10 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ -var MatrixClientPeg = require('./MatrixClientPeg'); -var dis = require('./dispatcher'); -var sdk = require('./index'); -var Modal = require('./Modal'); +import MatrixClientPeg from './MatrixClientPeg'; +import dis from './dispatcher'; import { EventStatus } from 'matrix-js-sdk'; module.exports = { @@ -37,12 +35,10 @@ module.exports = { }, resend: function(event) { const room = MatrixClientPeg.get().getRoom(event.getRoomId()); - MatrixClientPeg.get().resendEvent( - event, room - ).done(function(res) { + MatrixClientPeg.get().resendEvent(event, room).done(function(res) { dis.dispatch({ action: 'message_sent', - event: event + event: event, }); }, function(err) { // XXX: temporary logging to try to diagnose @@ -58,7 +54,7 @@ module.exports = { dis.dispatch({ action: 'message_send_failed', - event: event + event: event, }); }); }, @@ -66,7 +62,7 @@ module.exports = { MatrixClientPeg.get().cancelPendingEvent(event); dis.dispatch({ action: 'message_send_cancelled', - event: event + event: event, }); }, }; diff --git a/src/Roles.js b/src/Roles.js index 8c1f711bbe..83d8192c67 100644 --- a/src/Roles.js +++ b/src/Roles.js @@ -19,7 +19,7 @@ export function levelRoleMap() { return { undefined: _t('Default'), 0: _t('User'), - 50: _t('Moderator'), + 50: _t('Moderator'), 100: _t('Admin'), }; } diff --git a/src/RoomListSorter.js b/src/RoomListSorter.js index 7a43c1891e..c06cc60c97 100644 --- a/src/RoomListSorter.js +++ b/src/RoomListSorter.js @@ -19,8 +19,7 @@ limitations under the License. function tsOfNewestEvent(room) { if (room.timeline.length) { return room.timeline[room.timeline.length - 1].getTs(); - } - else { + } else { return Number.MAX_SAFE_INTEGER; } } @@ -32,5 +31,5 @@ function mostRecentActivityFirst(roomList) { } module.exports = { - mostRecentActivityFirst: mostRecentActivityFirst + mostRecentActivityFirst, }; diff --git a/src/RoomNotifs.js b/src/RoomNotifs.js index 7cb7d4b9de..88b6e56c7f 100644 --- a/src/RoomNotifs.js +++ b/src/RoomNotifs.js @@ -52,7 +52,7 @@ export function getRoomNotifsState(roomId) { } export function setRoomNotifsState(roomId, newState) { - if (newState == MUTE) { + if (newState === MUTE) { return setRoomNotifsStateMuted(roomId); } else { return setRoomNotifsStateUnmuted(roomId, newState); @@ -80,11 +80,11 @@ function setRoomNotifsStateMuted(roomId) { kind: 'event_match', key: 'room_id', pattern: roomId, - } + }, ], actions: [ 'dont_notify', - ] + ], })); return q.all(promises); @@ -99,16 +99,16 @@ function setRoomNotifsStateUnmuted(roomId, newState) { promises.push(cli.deletePushRule('global', 'override', overrideMuteRule.rule_id)); } - if (newState == 'all_messages') { + if (newState === 'all_messages') { const roomRule = cli.getRoomPushRule('global', roomId); if (roomRule) { promises.push(cli.deletePushRule('global', 'room', roomRule.rule_id)); } - } else if (newState == 'mentions_only') { + } else if (newState === 'mentions_only') { promises.push(cli.addPushRule('global', 'room', roomId, { actions: [ 'dont_notify', - ] + ], })); // https://matrix.org/jira/browse/SPEC-400 promises.push(cli.setPushRuleEnabled('global', 'room', roomId, true)); @@ -119,8 +119,8 @@ function setRoomNotifsStateUnmuted(roomId, newState) { { set_tweak: 'sound', value: 'default', - } - ] + }, + ], })); // https://matrix.org/jira/browse/SPEC-400 promises.push(cli.setPushRuleEnabled('global', 'room', roomId, true)); @@ -145,20 +145,10 @@ function isRuleForRoom(roomId, rule) { return false; } const cond = rule.conditions[0]; - if ( - cond.kind == 'event_match' && - cond.key == 'room_id' && - cond.pattern == roomId - ) { - return true; - } - return false; + return (cond.kind === 'event_match' && cond.key === 'room_id' && cond.pattern === roomId); } function isMuteRule(rule) { - return ( - rule.actions.length == 1 && - rule.actions[0] == 'dont_notify' - ); + return (rule.actions.length === 1 && rule.actions[0] === 'dont_notify'); } diff --git a/src/ScalarMessaging.js b/src/ScalarMessaging.js index c1b975e8e8..1104458f22 100644 --- a/src/ScalarMessaging.js +++ b/src/ScalarMessaging.js @@ -1,5 +1,6 @@ /* Copyright 2016 OpenMarket Ltd +Copyright 2017 Vector Creations Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -109,6 +110,76 @@ Example: response: 78 } +set_widget +---------- +Set a new widget in the room. Clobbers based on the ID. + +Request: + - `room_id` (String) is the room to set the widget in. + - `widget_id` (String) is the ID of the widget to add (or replace if it already exists). + It can be an arbitrary UTF8 string and is purely for distinguishing between widgets. + - `url` (String) is the URL that clients should load in an iframe to run the widget. + All widgets must have a valid URL. If the URL is `null` (not `undefined`), the + widget will be removed from the room. + - `type` (String) is the type of widget, which is provided as a hint for matrix clients so they + can configure/lay out the widget in different ways. All widgets must have a type. + - `name` (String) is an optional human-readable string about the widget. + - `data` (Object) is some optional data about the widget, and can contain arbitrary key/value pairs. +Response: +{ + success: true +} +Example: +{ + action: "set_widget", + room_id: "!foo:bar", + widget_id: "abc123", + url: "http://widget.url", + type: "example", + response: { + success: true + } +} + +get_widgets +----------- +Get a list of all widgets in the room. The response is the `content` field +of the state event. + +Request: + - `room_id` (String) is the room to get the widgets in. +Response: +{ + $widget_id: { + type: "example", + url: "http://widget.url", + name: "Example Widget", + data: { + key: "val" + } + }, + $widget_id: { ... } +} +Example: +{ + action: "get_widgets", + room_id: "!foo:bar", + widget_id: "abc123", + url: "http://widget.url", + type: "example", + response: { + $widget_id: { + type: "example", + url: "http://widget.url", + name: "Example Widget", + data: { + key: "val" + } + }, + $widget_id: { ... } + } +} + membership_state AND bot_options -------------------------------- @@ -191,6 +262,84 @@ function inviteUser(event, roomId, userId) { }); } +function setWidget(event, roomId) { + const widgetId = event.data.widget_id; + const widgetType = event.data.type; + const widgetUrl = event.data.url; + const widgetName = event.data.name; // optional + const widgetData = event.data.data; // optional + + const client = MatrixClientPeg.get(); + if (!client) { + sendError(event, _t('You need to be logged in.')); + return; + } + + // both adding/removing widgets need these checks + if (!widgetId || widgetUrl === undefined) { + sendError(event, _t("Unable to create widget."), new Error("Missing required widget fields.")); + return; + } + + if (widgetUrl !== null) { // if url is null it is being deleted, don't need to check name/type/etc + // check types of fields + if (widgetName !== undefined && typeof widgetName !== 'string') { + sendError(event, _t("Unable to create widget."), new Error("Optional field 'name' must be a string.")); + return; + } + if (widgetData !== undefined && !(widgetData instanceof Object)) { + sendError(event, _t("Unable to create widget."), new Error("Optional field 'data' must be an Object.")); + return; + } + if (typeof widgetType !== 'string') { + sendError(event, _t("Unable to create widget."), new Error("Field 'type' must be a string.")); + return; + } + if (typeof widgetUrl !== 'string') { + sendError(event, _t("Unable to create widget."), new Error("Field 'url' must be a string or null.")); + return; + } + } + + // TODO: same dance we do for power levels. It'd be nice if the JS SDK had helper methods to do this. + client.getStateEvent(roomId, "im.vector.modular.widgets", "").then((widgets) => { + if (widgetUrl === null) { + delete widgets[widgetId]; + } + else { + widgets[widgetId] = { + type: widgetType, + url: widgetUrl, + name: widgetName, + data: widgetData, + }; + } + return client.sendStateEvent(roomId, "im.vector.modular.widgets", widgets); + }, (err) => { + if (err.errcode === "M_NOT_FOUND") { + return client.sendStateEvent(roomId, "im.vector.modular.widgets", { + [widgetId]: { + type: widgetType, + url: widgetUrl, + name: widgetName, + data: widgetData, + } + }); + } + throw err; + }).done(() => { + sendResponse(event, { + success: true, + }); + }, (err) => { + sendError(event, _t('Failed to send request.'), err); + }); +} + +function getWidgets(event, roomId) { + returnStateEvent(event, roomId, "im.vector.modular.widgets", ""); +} + function setPlumbingState(event, roomId, status) { if (typeof status !== 'string') { throw new Error('Plumbing state status should be a string'); @@ -367,7 +516,7 @@ const onMessage = function(event) { return; } - // Getting join rules does not require userId + // These APIs don't require userId if (event.data.action === "join_rules_state") { getJoinRules(event, roomId); return; @@ -377,6 +526,12 @@ const onMessage = function(event) { } else if (event.data.action === "get_membership_count") { getMembershipCount(event, roomId); return; + } else if (event.data.action === "set_widget") { + setWidget(event, roomId); + return; + } else if (event.data.action === "get_widgets") { + getWidgets(event, roomId); + return; } if (!userId) { @@ -409,12 +564,27 @@ const onMessage = function(event) { }); }; +let listenerCount = 0; module.exports = { startListening: function() { - window.addEventListener("message", onMessage, false); + if (listenerCount === 0) { + window.addEventListener("message", onMessage, false); + } + listenerCount += 1; }, stopListening: function() { - window.removeEventListener("message", onMessage); + listenerCount -= 1; + if (listenerCount === 0) { + window.removeEventListener("message", onMessage); + } + if (listenerCount < 0) { + // Make an error so we get a stack trace + const e = new Error( + "ScalarMessaging: mismatched startListening / stopListening detected." + + " Negative count" + ); + console.error(e); + } }, }; diff --git a/src/SdkConfig.js b/src/SdkConfig.js index 8d8e93a889..48ebf011f2 100644 --- a/src/SdkConfig.js +++ b/src/SdkConfig.js @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -var DEFAULTS = { +const DEFAULTS = { // URL to a page we show in an iframe to configure integrations integrations_ui_url: "https://scalar.vector.im/", // Base URL to the REST interface of the integrations server @@ -30,8 +30,8 @@ class SdkConfig { } static put(cfg) { - var defaultKeys = Object.keys(DEFAULTS); - for (var i = 0; i < defaultKeys.length; ++i) { + const defaultKeys = Object.keys(DEFAULTS); + for (let i = 0; i < defaultKeys.length; ++i) { if (cfg[defaultKeys[i]] === undefined) { cfg[defaultKeys[i]] = DEFAULTS[defaultKeys[i]]; } diff --git a/src/Skinner.js b/src/Skinner.js index 0688c9fc26..f47572ba01 100644 --- a/src/Skinner.js +++ b/src/Skinner.js @@ -51,19 +51,18 @@ class Skinner { if (this.components !== null) { throw new Error( "Attempted to load a skin while a skin is already loaded"+ - "If you want to change the active skin, call resetSkin first" - ); + "If you want to change the active skin, call resetSkin first"); } this.components = {}; - var compKeys = Object.keys(skinObject.components); - for (var i = 0; i < compKeys.length; ++i) { - var comp = skinObject.components[compKeys[i]]; + const compKeys = Object.keys(skinObject.components); + for (let i = 0; i < compKeys.length; ++i) { + const comp = skinObject.components[compKeys[i]]; this.addComponent(compKeys[i], comp); } } addComponent(name, comp) { - var slot = name; + let slot = name; if (comp.replaces !== undefined) { if (comp.replaces.indexOf('.') > -1) { slot = comp.replaces; diff --git a/src/SlashCommands.js b/src/SlashCommands.js index 185ea504ac..b1cd59f3a9 100644 --- a/src/SlashCommands.js +++ b/src/SlashCommands.js @@ -186,7 +186,7 @@ const commands = { if (targetRoomId) { break; } } if (!targetRoomId) { - return reject(_t("Unrecognised room alias:") + ' ' + roomAlias); + return reject(_t("Unrecognised room alias:") + ' ' + roomAlias); } } } @@ -344,8 +344,7 @@ const commands = { _t('WARNING: KEY VERIFICATION FAILED! The signing key for %(userId)s and device' + ' %(deviceId)s is "%(fprint)s" which does not match the provided key' + ' "%(fingerprint)s". This could mean your communications are being intercepted!', - {deviceId: deviceId, fprint: fprint, userId: userId, fingerprint: fingerprint}) - ); + {deviceId: deviceId, fprint: fprint, userId: userId, fingerprint: fingerprint})); } } } diff --git a/src/UserActivity.js b/src/UserActivity.js index 1ae272f5df..b6fae38ed5 100644 --- a/src/UserActivity.js +++ b/src/UserActivity.js @@ -14,10 +14,10 @@ See the License for the specific language governing permissions and limitations under the License. */ -var dis = require("./dispatcher"); +import dis from './dispatcher'; -var MIN_DISPATCH_INTERVAL_MS = 500; -var CURRENTLY_ACTIVE_THRESHOLD_MS = 2000; +const MIN_DISPATCH_INTERVAL_MS = 500; +const CURRENTLY_ACTIVE_THRESHOLD_MS = 2000; /** * This class watches for user activity (moving the mouse or pressing a key) @@ -58,16 +58,15 @@ class UserActivity { /** * Return true if there has been user activity very recently * (ie. within a few seconds) + * @returns {boolean} true if user is currently/very recently active */ userCurrentlyActive() { return this.lastActivityAtTs > new Date().getTime() - CURRENTLY_ACTIVE_THRESHOLD_MS; } _onUserActivity(event) { - if (event.screenX && event.type == "mousemove") { - if (event.screenX === this.lastScreenX && - event.screenY === this.lastScreenY) - { + if (event.screenX && event.type === "mousemove") { + if (event.screenX === this.lastScreenX && event.screenY === this.lastScreenY) { // mouse hasn't actually moved return; } @@ -79,28 +78,24 @@ class UserActivity { if (this.lastDispatchAtTs < this.lastActivityAtTs - MIN_DISPATCH_INTERVAL_MS) { this.lastDispatchAtTs = this.lastActivityAtTs; dis.dispatch({ - action: 'user_activity' + action: 'user_activity', }); if (!this.activityEndTimer) { - this.activityEndTimer = setTimeout( - this._onActivityEndTimer.bind(this), MIN_DISPATCH_INTERVAL_MS - ); + this.activityEndTimer = setTimeout(this._onActivityEndTimer.bind(this), MIN_DISPATCH_INTERVAL_MS); } } } _onActivityEndTimer() { - var now = new Date().getTime(); - var targetTime = this.lastActivityAtTs + MIN_DISPATCH_INTERVAL_MS; + const now = new Date().getTime(); + const targetTime = this.lastActivityAtTs + MIN_DISPATCH_INTERVAL_MS; if (now >= targetTime) { dis.dispatch({ - action: 'user_activity_end' + action: 'user_activity_end', }); this.activityEndTimer = undefined; } else { - this.activityEndTimer = setTimeout( - this._onActivityEndTimer.bind(this), targetTime - now - ); + this.activityEndTimer = setTimeout(this._onActivityEndTimer.bind(this), targetTime - now); } } } diff --git a/src/UserSettingsStore.js b/src/UserSettingsStore.js index 84d85e7565..009fdabb53 100644 --- a/src/UserSettingsStore.js +++ b/src/UserSettingsStore.js @@ -30,11 +30,17 @@ export default { id: 'rich_text_editor', default: false, }, + { + name: "-", + id: 'matrix_apps', + default: false, + }, ], // horrible but it works. The locality makes this somewhat more palatable. doTranslations: function() { this.LABS_FEATURES[0].name = _t("New Composer & Autocomplete"); + this.LABS_FEATURES[1].name = _t("Matrix Apps"); }, loadProfileInfo: function() { diff --git a/src/autocomplete/CommandProvider.js b/src/autocomplete/CommandProvider.js index 9ae3a7badb..6f2f68b121 100644 --- a/src/autocomplete/CommandProvider.js +++ b/src/autocomplete/CommandProvider.js @@ -21,6 +21,7 @@ import AutocompleteProvider from './AutocompleteProvider'; import FuzzyMatcher from './FuzzyMatcher'; import {TextualCompletion} from './Components'; +// TODO merge this with the factory mechanics of SlashCommands? // Warning: Since the description string will be translated in _t(result.description), all these strings below must be in i18n/strings/en_EN.json file const COMMANDS = [ { @@ -28,11 +29,6 @@ const COMMANDS = [ args: '', description: 'Displays action', }, - { - command: '/part', - args: '[#alias:domain]', - description: 'Leave room', - }, { command: '/ban', args: ' [reason]', @@ -43,6 +39,11 @@ const COMMANDS = [ args: '', description: 'Unbans user with given id', }, + { + command: '/op', + args: ' []', + description: 'Define the power level of a user', + }, { command: '/deop', args: '', @@ -58,6 +59,16 @@ const COMMANDS = [ args: '', description: 'Joins room with given alias', }, + { + command: '/part', + args: '[]', + description: 'Leave room', + }, + { + command: '/topic', + args: '', + description: 'Sets the room topic', + }, { command: '/kick', args: ' [reason]', @@ -74,10 +85,16 @@ const COMMANDS = [ description: 'Searches DuckDuckGo for results', }, { - command: '/op', - args: ' []', - description: 'Define the power level of a user', + command: '/tint', + args: ' []', + description: 'Changes colour scheme of current room', }, + { + command: '/verify', + args: ' ', + description: 'Verifies a user, device, and pubkey tuple', + }, + // Omitting `/markdown` as it only seems to apply to OldComposer ]; const COMMAND_RE = /(^\/\w*)/g; diff --git a/src/autocomplete/EmojiProvider.js b/src/autocomplete/EmojiProvider.js index 416855ca09..35e9cc7b68 100644 --- a/src/autocomplete/EmojiProvider.js +++ b/src/autocomplete/EmojiProvider.js @@ -18,16 +18,42 @@ limitations under the License. import React from 'react'; import { _t } from '../languageHandler'; import AutocompleteProvider from './AutocompleteProvider'; -import {emojioneList, shortnameToImage, shortnameToUnicode} from 'emojione'; +import {emojioneList, shortnameToImage, shortnameToUnicode, asciiRegexp} from 'emojione'; import FuzzyMatcher from './FuzzyMatcher'; import sdk from '../index'; import {PillCompletion} from './Components'; import type {SelectionRange, Completion} from './Autocompleter'; -const EMOJI_REGEX = /:\w*:?/g; -const EMOJI_SHORTNAMES = Object.keys(emojioneList).map(shortname => { +import EmojiData from '../stripped-emoji.json'; + +const LIMIT = 20; +const CATEGORY_ORDER = [ + 'people', + 'food', + 'objects', + 'activity', + 'nature', + 'travel', + 'flags', + 'symbols', + 'unicode9', + 'modifier', +]; + +// Match for ":wink:" or ascii-style ";-)" provided by emojione +const EMOJI_REGEX = new RegExp('(' + asciiRegexp + '|:\\w*:?)$', 'g'); +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); + }, +).map((a) => { return { - shortname, + name: a.name, + shortname: a.shortname, + aliases_ascii: a.aliases_ascii ? a.aliases_ascii.join(' ') : '', }; }); @@ -37,7 +63,9 @@ export default class EmojiProvider extends AutocompleteProvider { constructor() { super(EMOJI_REGEX); this.matcher = new FuzzyMatcher(EMOJI_SHORTNAMES, { - keys: 'shortname', + keys: ['aliases_ascii', 'shortname', 'name'], + // For matching against ascii equivalents + shouldMatchWordsOnly: false, }); } @@ -57,7 +85,7 @@ export default class EmojiProvider extends AutocompleteProvider { ), range, }; - }).slice(0, 8); + }).slice(0, LIMIT); } return completions; } @@ -73,7 +101,7 @@ export default class EmojiProvider extends AutocompleteProvider { } renderCompletions(completions: [React.Component]): ?React.Component { - return
+ return
{completions}
; } diff --git a/src/autocomplete/QueryMatcher.js b/src/autocomplete/QueryMatcher.js index 01fc251318..07398e7a5f 100644 --- a/src/autocomplete/QueryMatcher.js +++ b/src/autocomplete/QueryMatcher.js @@ -63,6 +63,18 @@ export default class QueryMatcher { this.options = options; this.keys = options.keys; this.setObjects(objects); + + // By default, we remove any non-alphanumeric characters ([^A-Za-z0-9_]) from the + // query and the value being queried before matching + if (this.options.shouldMatchWordsOnly === undefined) { + this.options.shouldMatchWordsOnly = true; + } + + // By default, match anywhere in the string being searched. If enabled, only return + // matches that are prefixed with the query. + if (this.options.shouldMatchPrefix === undefined) { + this.options.shouldMatchPrefix = false; + } } setObjects(objects: Array) { @@ -70,10 +82,31 @@ export default class QueryMatcher { } match(query: String): Array { - query = query.toLowerCase().replace(/[^\w]/g, ''); - const results = _sortedUniq(_sortBy(_flatMap(this.keyMap.keys, (key) => { - return key.toLowerCase().replace(/[^\w]/g, '').indexOf(query) >= 0 ? this.keyMap.objectMap[key] : []; - }), (candidate) => this.keyMap.priorityMap.get(candidate))); - return results; + query = query.toLowerCase(); + if (this.options.shouldMatchWordsOnly) { + query = query.replace(/[^\w]/g, ''); + } + if (query.length === 0) { + return []; + } + const results = []; + this.keyMap.keys.forEach((key) => { + let resultKey = key.toLowerCase(); + if (this.options.shouldMatchWordsOnly) { + resultKey = resultKey.replace(/[^\w]/g, ''); + } + const index = resultKey.indexOf(query); + if (index !== -1 && (!this.options.shouldMatchPrefix || index === 0)) { + results.push({key, index}); + } + }); + + return _sortedUniq(_flatMap(_sortBy(results, (candidate) => { + return candidate.index; + }).map((candidate) => { + // return an array of objects (those given to setObjects) that have the given + // key as a property. + return this.keyMap.objectMap[candidate.key]; + }))); } } diff --git a/src/autocomplete/RoomProvider.js b/src/autocomplete/RoomProvider.js index a001f381ee..bf8495a90e 100644 --- a/src/autocomplete/RoomProvider.js +++ b/src/autocomplete/RoomProvider.js @@ -78,7 +78,7 @@ export default class RoomProvider extends AutocompleteProvider { } renderCompletions(completions: [React.Component]): ?React.Component { - return
+ return
{completions}
; } diff --git a/src/autocomplete/UserProvider.js b/src/autocomplete/UserProvider.js index 4e0c0f5ea7..26ec15e124 100644 --- a/src/autocomplete/UserProvider.js +++ b/src/autocomplete/UserProvider.js @@ -37,10 +37,11 @@ export default class UserProvider extends AutocompleteProvider { constructor() { super(USER_REGEX, { - keys: ['name', 'userId'], + keys: ['name'], }); this.matcher = new FuzzyMatcher([], { - keys: ['name', 'userId'], + keys: ['name'], + shouldMatchPrefix: true, }); } @@ -50,7 +51,7 @@ export default class UserProvider extends AutocompleteProvider { let completions = []; let {command, range} = this.getCurrentCommand(query, selection, force); if (command) { - completions = this.matcher.match(command[0]).map(user => { + completions = this.matcher.match(command[0]).slice(0, 4).map((user) => { let displayName = (user.name || user.userId || '').replace(' (IRC)', ''); // FIXME when groups are done let completion = displayName; if (range.start === 0) { @@ -68,7 +69,7 @@ export default class UserProvider extends AutocompleteProvider { ), range, }; - }).slice(0, 4); + }); } return completions; } @@ -90,7 +91,9 @@ export default class UserProvider extends AutocompleteProvider { if (member.userId !== currentUserId) return true; }); - this.users = _sortBy(this.users, (user) => 1E20 - lastSpoken[user.userId] || 1E20); + this.users = _sortBy(this.users, (completion) => + 1E20 - lastSpoken[completion.user.userId] || 1E20, + ); this.matcher.setObjects(this.users); } @@ -98,9 +101,10 @@ export default class UserProvider extends AutocompleteProvider { onUserSpoke(user: RoomMember) { if(user.userId === MatrixClientPeg.get().credentials.userId) return; - // Probably unsafe to compare by reference here? - _pull(this.users, user); - this.users.splice(0, 0, user); + this.users = this.users.splice( + this.users.findIndex((user2) => user2.userId === user.userId), 1); + this.users = [user, ...this.users]; + this.matcher.setObjects(this.users); } @@ -112,7 +116,7 @@ export default class UserProvider extends AutocompleteProvider { } renderCompletions(completions: [React.Component]): ?React.Component { - return
+ return
{completions}
; } diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 67b523bfaf..ffb0c1243c 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -47,13 +47,12 @@ import UserProvider from '../../autocomplete/UserProvider'; import RoomViewStore from '../../stores/RoomViewStore'; -var DEBUG = false; +let DEBUG = false; +let debuglog = function() {}; if (DEBUG) { // using bind means that we get to keep useful line numbers in the console - var debuglog = console.log.bind(console); -} else { - var debuglog = function() {}; + debuglog = console.log.bind(console); } module.exports = React.createClass({ @@ -113,6 +112,7 @@ module.exports = React.createClass({ callState: null, guestsCanJoin: false, canPeek: false, + showApps: false, // error object, as from the matrix client/server API // If we failed to load information about the room, @@ -236,6 +236,7 @@ module.exports = React.createClass({ if (room) { this.setState({ unsentMessageError: this._getUnsentMessageError(room), + showApps: this._shouldShowApps(room), }); this._onRoomLoaded(room); } @@ -273,6 +274,11 @@ module.exports = React.createClass({ } }, + _shouldShowApps: function(room) { + const appsStateEvents = room.currentState.getStateEvents('im.vector.modular.widgets', ''); + return appsStateEvents && Object.keys(appsStateEvents.getContent()).length > 0; + }, + componentDidMount: function() { var call = this._getCallForRoom(); var callState = call ? call.call_state : "ended"; @@ -453,9 +459,14 @@ module.exports = React.createClass({ this._updateConfCallNotification(); this.setState({ - callState: callState + callState: callState, }); + break; + case 'appsDrawer': + this.setState({ + showApps: payload.show, + }); break; } }, @@ -1604,11 +1615,13 @@ module.exports = React.createClass({ var auxPanel = ( + onResize={this.onChildResize} + showApps={this.state.showApps && !this.state.editingRoomSettings} > { aux } ); @@ -1621,8 +1634,14 @@ module.exports = React.createClass({ if (canSpeak) { messageComposer = ; + room={this.state.room} + onResize={this.onChildResize} + uploadFile={this.uploadFile} + callState={this.state.callState} + tabComplete={this.tabComplete} + opacity={ this.props.opacity } + showApps={ this.state.showApps } + />; } // TODO: Why aren't we storing the term/scope/count in this format diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index ef574d2ed6..bfbb9831b0 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -93,6 +93,10 @@ const SETTINGS_LABELS = [ id: 'disableMarkdown', label: 'Disable markdown formatting', }, + { + id: 'enableSyntaxHighlightLanguageDetection', + label: 'Enable automatic language detection for syntax highlighting', + }, /* { id: 'useFixedWidthFont', diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js new file mode 100644 index 0000000000..9fed0e7d5b --- /dev/null +++ b/src/components/views/elements/AppTile.js @@ -0,0 +1,161 @@ +/* +Copyright 2017 Vector Creations 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. +*/ + +'use strict'; + +import React from 'react'; +import MatrixClientPeg from '../../../MatrixClientPeg'; +import ScalarAuthClient from '../../../ScalarAuthClient'; +import SdkConfig from '../../../SdkConfig'; +import { _t } from '../../../languageHandler'; + +import url from 'url'; + +export default React.createClass({ + displayName: 'AppTile', + + propTypes: { + id: React.PropTypes.string.isRequired, + url: React.PropTypes.string.isRequired, + name: React.PropTypes.string.isRequired, + room: React.PropTypes.object.isRequired, + }, + + getDefaultProps: function() { + return { + url: "", + }; + }, + + getInitialState: function() { + return { + loading: false, + widgetUrl: this.props.url, + error: null, + }; + }, + + // Returns true if props.url is a scalar URL, typically https://scalar.vector.im/api + isScalarUrl: function() { + const scalarUrl = SdkConfig.get().integrations_rest_url; + return scalarUrl && this.props.url.startsWith(scalarUrl); + }, + + componentWillMount: function() { + if (!this.isScalarUrl()) { + return; + } + // Fetch the token before loading the iframe as we need to mangle the URL + this.setState({ + loading: true, + }); + this._scalarClient = new ScalarAuthClient(); + this._scalarClient.getScalarToken().done((token) => { + // Append scalar_token as a query param + const u = url.parse(this.props.url); + if (!u.search) { + u.search = "?scalar_token=" + encodeURIComponent(token); + } else { + u.search += "&scalar_token=" + encodeURIComponent(token); + } + + this.setState({ + error: null, + widgetUrl: u.format(), + loading: false, + }); + }, (err) => { + this.setState({ + error: err.message, + loading: false, + }); + }); + }, + + _onEditClick: function() { + console.log("Edit widget %s", this.props.id); + }, + + _onDeleteClick: function() { + console.log("Delete widget %s", this.props.id); + const appsStateEvents = this.props.room.currentState.getStateEvents('im.vector.modular.widgets', ''); + if (!appsStateEvents) { + return; + } + const appsStateEvent = appsStateEvents.getContent(); + if (appsStateEvent[this.props.id]) { + delete appsStateEvent[this.props.id]; + MatrixClientPeg.get().sendStateEvent( + this.props.room.roomId, + 'im.vector.modular.widgets', + appsStateEvent, + '', + ).then(() => { + console.log('Deleted widget'); + }, (e) => { + console.error('Failed to delete widget', e); + }); + } + }, + + formatAppTileName: function() { + let appTileName = "No name"; + if(this.props.name && this.props.name.trim()) { + appTileName = this.props.name.trim(); + appTileName = appTileName[0].toUpperCase() + appTileName.slice(1).toLowerCase(); + } + return appTileName; + }, + + render: function() { + let appTileBody; + if (this.state.loading) { + appTileBody = ( +
Loading...
+ ); + } else { + appTileBody = ( +
+ +
+ ); + } + return ( +
+
+ {this.formatAppTileName()} + + {/* Edit widget */} + {/* Edit */} + + {/* Delete widget */} + {_t("Cancel")} + +
+ {appTileBody} +
+ ); + }, +}); diff --git a/src/components/views/messages/MVideoBody.js b/src/components/views/messages/MVideoBody.js index 46d8366592..f31b94df80 100644 --- a/src/components/views/messages/MVideoBody.js +++ b/src/components/views/messages/MVideoBody.js @@ -79,7 +79,7 @@ module.exports = React.createClass({ const content = this.props.mxEvent.getContent(); if (content.file !== undefined) { return this.state.decryptedThumbnailUrl; - } else if (content.info.thumbnail_url) { + } else if (content.info && content.info.thumbnail_url) { return MatrixClientPeg.get().mxcUrlToHttp(content.info.thumbnail_url); } else { return null; diff --git a/src/components/views/messages/TextualBody.js b/src/components/views/messages/TextualBody.js index d5a1977cdd..2c50a94a6a 100644 --- a/src/components/views/messages/TextualBody.js +++ b/src/components/views/messages/TextualBody.js @@ -29,6 +29,7 @@ import Modal from '../../../Modal'; import SdkConfig from '../../../SdkConfig'; import dis from '../../../dispatcher'; import { _t } from '../../../languageHandler'; +import UserSettingsStore from "../../../UserSettingsStore"; linkifyMatrix(linkify); @@ -90,7 +91,18 @@ module.exports = React.createClass({ setTimeout(() => { if (this._unmounted) return; for (let i = 0; i < blocks.length; i++) { - highlight.highlightBlock(blocks[i]); + if (UserSettingsStore.getSyncedSetting("enableSyntaxHighlightLanguageDetection", false)) { + highlight.highlightBlock(blocks[i]) + } else { + // Only syntax highlight if there's a class starting with language- + let classes = blocks[i].className.split(/\s+/).filter(function (cl) { + return cl.startsWith('language-'); + }); + + if (classes.length != 0) { + highlight.highlightBlock(blocks[i]); + } + } } }, 10); } @@ -131,9 +143,15 @@ module.exports = React.createClass({ if (this.props.showUrlPreview && !this.state.links.length) { var links = this.findLinks(this.refs.content.children); if (links.length) { - this.setState({ links: links.map((link)=>{ - return link.getAttribute("href"); - })}); + // de-dup the links (but preserve ordering) + const seen = new Set(); + links = links.filter((link) => { + if (seen.has(link)) return false; + seen.add(link); + return true; + }); + + this.setState({ links: links }); // lazy-load the hidden state of the preview widget from localstorage if (global.localStorage) { @@ -146,12 +164,13 @@ module.exports = React.createClass({ findLinks: function(nodes) { var links = []; + for (var i = 0; i < nodes.length; i++) { var node = nodes[i]; if (node.tagName === "A" && node.getAttribute("href")) { if (this.isLinkPreviewable(node)) { - links.push(node); + links.push(node.getAttribute("href")); } } else if (node.tagName === "PRE" || node.tagName === "CODE" || diff --git a/src/components/views/rooms/AppsDrawer.js b/src/components/views/rooms/AppsDrawer.js new file mode 100644 index 0000000000..b535b148ac --- /dev/null +++ b/src/components/views/rooms/AppsDrawer.js @@ -0,0 +1,218 @@ +/* +Copyright 2017 Vector Creations 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. +*/ + +'use strict'; + +import React from 'react'; +import MatrixClientPeg from '../../../MatrixClientPeg'; +import AppTile from '../elements/AppTile'; +import Modal from '../../../Modal'; +import dis from '../../../dispatcher'; +import sdk from '../../../index'; +import SdkConfig from '../../../SdkConfig'; +import ScalarAuthClient from '../../../ScalarAuthClient'; +import ScalarMessaging from '../../../ScalarMessaging'; +import { _t } from '../../../languageHandler'; + + +module.exports = React.createClass({ + displayName: 'AppsDrawer', + + propTypes: { + room: React.PropTypes.object.isRequired, + }, + + getInitialState: function() { + return { + apps: this._getApps(), + }; + }, + + componentWillMount: function() { + ScalarMessaging.startListening(); + MatrixClientPeg.get().on("RoomState.events", this.onRoomStateEvents); + }, + + componentDidMount: function() { + this.scalarClient = null; + if (SdkConfig.get().integrations_ui_url && SdkConfig.get().integrations_rest_url) { + this.scalarClient = new ScalarAuthClient(); + this.scalarClient.connect().done(() => { + this.forceUpdate(); + if (this.state.apps && this.state.apps.length < 1) { + this.onClickAddWidget(); + } + // TODO -- Handle Scalar errors + // }, + // (err) => { + // this.setState({ + // scalar_error: err, + // }); + }); + } + }, + + componentWillUnmount: function() { + ScalarMessaging.stopListening(); + if (MatrixClientPeg.get()) { + MatrixClientPeg.get().removeListener("RoomState.events", this.onRoomStateEvents); + } + }, + + /** + * Encodes a URI according to a set of template variables. Variables will be + * passed through encodeURIComponent. + * @param {string} pathTemplate The path with template variables e.g. '/foo/$bar'. + * @param {Object} variables The key/value pairs to replace the template + * variables with. E.g. { "$bar": "baz" }. + * @return {string} The result of replacing all template variables e.g. '/foo/baz'. + */ + encodeUri: function(pathTemplate, variables) { + for (const key in variables) { + if (!variables.hasOwnProperty(key)) { + continue; + } + pathTemplate = pathTemplate.replace( + key, encodeURIComponent(variables[key]), + ); + } + return pathTemplate; + }, + + _initAppConfig: function(appId, app) { + const user = MatrixClientPeg.get().getUser(this.props.userId); + const params = { + '$matrix_user_id': this.props.userId, + '$matrix_room_id': this.props.room.roomId, + '$matrix_display_name': user ? user.displayName : this.props.userId, + '$matrix_avatar_url': user ? MatrixClientPeg.get().mxcUrlToHttp(user.avatarUrl) : '', + }; + + if(app.data) { + Object.keys(app.data).forEach((key) => { + params['$' + key] = app.data[key]; + }); + } + + app.id = appId; + app.name = app.name || app.type; + app.url = this.encodeUri(app.url, params); + + // switch(app.type) { + // case 'etherpad': + // app.queryParams = '?userName=' + this.props.userId + + // '&padId=' + this.props.room.roomId; + // break; + // case 'jitsi': { + // + // app.queryParams = '?confId=' + app.data.confId + + // '&displayName=' + encodeURIComponent(user.displayName) + + // '&avatarUrl=' + encodeURIComponent(MatrixClientPeg.get().mxcUrlToHttp(user.avatarUrl)) + + // '&email=' + encodeURIComponent(this.props.userId) + + // '&isAudioConf=' + app.data.isAudioConf; + // + // break; + // } + // case 'vrdemo': + // app.queryParams = '?roomAlias=' + encodeURIComponent(app.data.roomAlias); + // break; + // } + + return app; + }, + + onRoomStateEvents: function(ev, state) { + if (ev.getRoomId() !== this.props.room.roomId || ev.getType() !== 'im.vector.modular.widgets') { + return; + } + this._updateApps(); + }, + + _getApps: function() { + const appsStateEvents = this.props.room.currentState.getStateEvents('im.vector.modular.widgets', ''); + if (!appsStateEvents) { + return []; + } + const appsStateEvent = appsStateEvents.getContent(); + if (Object.keys(appsStateEvent).length < 1) { + return []; + } + + return Object.keys(appsStateEvent).map((appId) => { + return this._initAppConfig(appId, appsStateEvent[appId]); + }); + }, + + _updateApps: function() { + const apps = this._getApps(); + if (apps.length < 1) { + dis.dispatch({ + action: 'appsDrawer', + show: false, + }); + } + this.setState({ + apps: apps, + }); + }, + + onClickAddWidget: function(e) { + if (e) { + e.preventDefault(); + } + + const IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager"); + const src = (this.scalarClient !== null && this.scalarClient.hasCredentials()) ? + this.scalarClient.getScalarInterfaceUrlForRoom(this.props.room.roomId) : + null; + Modal.createDialog(IntegrationsManager, { + src: src, + }, "mx_IntegrationsManager"); + }, + + render: function() { + const apps = this.state.apps.map( + (app, index, arr) => { + return ; + }); + + const addWidget = this.state.apps && this.state.apps.length < 2 && + (
+ [+] {_t('Add a widget')} +
); + + return ( +
+
+ {apps} +
+ {addWidget} +
+ ); + }, +}); diff --git a/src/components/views/rooms/Autocomplete.js b/src/components/views/rooms/Autocomplete.js index d591e4f6c2..807e93cc0b 100644 --- a/src/components/views/rooms/Autocomplete.js +++ b/src/components/views/rooms/Autocomplete.js @@ -40,25 +40,51 @@ export default class Autocomplete extends React.Component { }; } - async componentWillReceiveProps(props, state) { - if (props.query === this.props.query) { - return null; - } - - return await this.complete(props.query, props.selection); - } - - async complete(query, selection) { - let forceComplete = this.state.forceComplete; - const completionPromise = getCompletions(query, selection, forceComplete); - this.completionPromise = completionPromise; - const completions = await this.completionPromise; - - // There's a newer completion request, so ignore results. - if (completionPromise !== this.completionPromise) { + componentWillReceiveProps(newProps, state) { + // Query hasn't changed so don't try to complete it + if (newProps.query === this.props.query) { return; } + this.complete(newProps.query, newProps.selection); + } + + complete(query, selection) { + if (this.debounceCompletionsRequest) { + clearTimeout(this.debounceCompletionsRequest); + } + if (query === "") { + this.setState({ + // Clear displayed completions + completions: [], + completionList: [], + // Reset selected completion + selectionOffset: COMPOSER_SELECTED, + // Hide the autocomplete box + hide: true, + }); + return Q(null); + } + let autocompleteDelay = UserSettingsStore.getLocalSetting('autocompleteDelay', 200); + + // Don't debounce if we are already showing completions + if (this.state.completions.length > 0 || this.state.forceComplete) { + autocompleteDelay = 0; + } + + const deferred = Q.defer(); + this.debounceCompletionsRequest = setTimeout(() => { + getCompletions( + query, selection, this.state.forceComplete, + ).then((completions) => { + this.processCompletions(completions); + deferred.resolve(); + }); + }, autocompleteDelay); + return deferred.promise; + } + + processCompletions(completions) { const completionList = flatMap(completions, (provider) => provider.completions); // Reset selection when completion list becomes empty. @@ -88,23 +114,13 @@ export default class Autocomplete extends React.Component { hide = false; } - const autocompleteDelay = UserSettingsStore.getSyncedSetting('autocompleteDelay', 200); - - // We had no completions before, but do now, so we should apply our display delay here - if (this.state.completionList.length === 0 && completionList.length > 0 && - !forceComplete && autocompleteDelay > 0) { - await Q.delay(autocompleteDelay); - } - - // Force complete is turned off each time since we can't edit the query in that case - forceComplete = false; - this.setState({ completions, completionList, selectionOffset, hide, - forceComplete, + // Force complete is turned off each time since we can't edit the query in that case + forceComplete: false, }); } @@ -161,7 +177,7 @@ export default class Autocomplete extends React.Component { hide: false, }, () => { this.complete(this.props.query, this.props.selection).then(() => { - done.resolve(); + done.resolve(this.countCompletions()); }); }); return done.promise; diff --git a/src/components/views/rooms/AuxPanel.js b/src/components/views/rooms/AuxPanel.js index acb9c76aa0..a50743a25d 100644 --- a/src/components/views/rooms/AuxPanel.js +++ b/src/components/views/rooms/AuxPanel.js @@ -19,7 +19,9 @@ import MatrixClientPeg from "../../../MatrixClientPeg"; import sdk from '../../../index'; import dis from "../../../dispatcher"; import ObjectUtils from '../../../ObjectUtils'; -import { _t, _tJsx} from '../../../languageHandler'; +import AppsDrawer from './AppsDrawer'; +import { _t, _tJsx} from '../../../languageHandler'; +import UserSettingsStore from '../../../UserSettingsStore'; module.exports = React.createClass({ @@ -28,6 +30,8 @@ module.exports = React.createClass({ propTypes: { // js-sdk room object room: React.PropTypes.object.isRequired, + userId: React.PropTypes.string.isRequired, + showApps: React.PropTypes.bool, // Conference Handler implementation conferenceHandler: React.PropTypes.object, @@ -70,10 +74,10 @@ module.exports = React.createClass({ }, render: function() { - var CallView = sdk.getComponent("voip.CallView"); - var TintableSvg = sdk.getComponent("elements.TintableSvg"); + const CallView = sdk.getComponent("voip.CallView"); + const TintableSvg = sdk.getComponent("elements.TintableSvg"); - var fileDropTarget = null; + let fileDropTarget = null; if (this.props.draggingFile) { fileDropTarget = (
@@ -87,14 +91,13 @@ module.exports = React.createClass({ ); } - var conferenceCallNotification = null; + let conferenceCallNotification = null; if (this.props.displayConfCallNotification) { let supportedText = ''; let joinNode; if (!MatrixClientPeg.get().supportsVoip()) { supportedText = _t(" (unsupported)"); - } - else { + } else { joinNode = ( {_tJsx( "Join as voice or video.", @@ -105,7 +108,6 @@ module.exports = React.createClass({ ] )} ); - } // XXX: the translation here isn't great: appending ' (unsupported)' is likely to not make sense in many languages, // but there are translations for this in the languages we do have so I'm leaving it for now. @@ -118,7 +120,7 @@ module.exports = React.createClass({ ); } - var callView = ( + const callView = ( ); + let appsDrawer = null; + if(UserSettingsStore.isFeatureEnabled('matrix_apps') && this.props.showApps) { + appsDrawer = ; + } + return (
+ { appsDrawer } { fileDropTarget } { callView } { conferenceCallNotification } diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js index 434685e8e1..27d5e11119 100644 --- a/src/components/views/rooms/MessageComposer.js +++ b/src/components/views/rooms/MessageComposer.js @@ -13,16 +13,14 @@ 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. */ -var React = require('react'); +import React from 'react'; import { _t } from '../../../languageHandler'; -var CallHandler = require('../../../CallHandler'); -var MatrixClientPeg = require('../../../MatrixClientPeg'); -var Modal = require('../../../Modal'); -var sdk = require('../../../index'); -var dis = require('../../../dispatcher'); +import CallHandler from '../../../CallHandler'; +import MatrixClientPeg from '../../../MatrixClientPeg'; +import Modal from '../../../Modal'; +import sdk from '../../../index'; +import dis from '../../../dispatcher'; import Autocomplete from './Autocomplete'; -import classNames from 'classnames'; - import UserSettingsStore from '../../../UserSettingsStore'; @@ -32,6 +30,8 @@ export default class MessageComposer extends React.Component { this.onCallClick = this.onCallClick.bind(this); this.onHangupClick = this.onHangupClick.bind(this); this.onUploadClick = this.onUploadClick.bind(this); + this.onShowAppsClick = this.onShowAppsClick.bind(this); + this.onHideAppsClick = this.onHideAppsClick.bind(this); this.onUploadFileSelected = this.onUploadFileSelected.bind(this); this.uploadFiles = this.uploadFiles.bind(this); this.onVoiceCallClick = this.onVoiceCallClick.bind(this); @@ -57,7 +57,6 @@ export default class MessageComposer extends React.Component { }, showFormatting: UserSettingsStore.getSyncedSetting('MessageComposer.showFormatting', false), }; - } componentDidMount() { @@ -127,7 +126,7 @@ export default class MessageComposer extends React.Component { if(shouldUpload) { // MessageComposer shouldn't have to rely on its parent passing in a callback to upload a file if (files) { - for(var i=0; i console.log('Sent state'), (e) => console.error(e)); + // } + // } + onCallClick(ev) { + // NOTE -- Will be replaced by Jitsi code (currently commented) dis.dispatch({ action: 'place_call', type: ev.shiftKey ? "screensharing" : "video", room_id: this.props.room.roomId, }); + // this._startCallApp(false); } onVoiceCallClick(ev) { + // NOTE -- Will be replaced by Jitsi code (currently commented) dis.dispatch({ action: 'place_call', - type: 'voice', + type: "voice", room_id: this.props.room.roomId, }); + // this._startCallApp(true); + } + + onShowAppsClick(ev) { + dis.dispatch({ + action: 'appsDrawer', + show: true, + }); + } + + onHideAppsClick(ev) { + dis.dispatch({ + action: 'appsDrawer', + show: false, + }); } onInputContentChanged(content: string, selection: {start: number, end: number}) { @@ -216,19 +263,19 @@ export default class MessageComposer extends React.Component { } render() { - var me = this.props.room.getMember(MatrixClientPeg.get().credentials.userId); - var uploadInputStyle = {display: 'none'}; - var MemberAvatar = sdk.getComponent('avatars.MemberAvatar'); - var TintableSvg = sdk.getComponent("elements.TintableSvg"); - var MessageComposerInput = sdk.getComponent("rooms.MessageComposerInput" + + const me = this.props.room.getMember(MatrixClientPeg.get().credentials.userId); + const uploadInputStyle = {display: 'none'}; + const MemberAvatar = sdk.getComponent('avatars.MemberAvatar'); + const TintableSvg = sdk.getComponent("elements.TintableSvg"); + const MessageComposerInput = sdk.getComponent("rooms.MessageComposerInput" + (UserSettingsStore.isFeatureEnabled('rich_text_editor') ? "" : "Old")); - var controls = []; + const controls = []; controls.push(
-
+
, ); let e2eImg, e2eTitle, e2eClass; @@ -247,16 +294,15 @@ export default class MessageComposer extends React.Component { controls.push( {e2eTitle} + />, ); - var callButton, videoCallButton, hangupButton; + let callButton, videoCallButton, hangupButton, showAppsButton, hideAppsButton; if (this.props.callState && this.props.callState !== 'ended') { hangupButton =
{
; - } - else { + } else { callButton =
@@ -267,14 +313,29 @@ export default class MessageComposer extends React.Component {
; } - var canSendMessages = this.props.room.currentState.maySendMessage( + // Apps + if (UserSettingsStore.isFeatureEnabled('matrix_apps')) { + if (this.props.showApps) { + hideAppsButton = +
+ +
; + } else { + showAppsButton = +
+ +
; + } + } + + const canSendMessages = this.props.room.currentState.maySendMessage( MatrixClientPeg.get().credentials.userId); if (canSendMessages) { // This also currently includes the call buttons. Really we should // check separately for whether we can call, but this is slightly // complex because of conference calls. - var uploadButton = ( + const uploadButton = (
@@ -300,7 +361,7 @@ export default class MessageComposer extends React.Component { controls.push( this.messageComposerInput = c} + ref={(c) => this.messageComposerInput = c} key="controls_input" onResize={this.props.onResize} room={this.props.room} @@ -316,13 +377,15 @@ export default class MessageComposer extends React.Component { uploadButton, hangupButton, callButton, - videoCallButton + videoCallButton, + showAppsButton, + hideAppsButton, ); } else { controls.push(
{ _t('You do not have permission to post to this room') } -
+
, ); } @@ -340,18 +403,14 @@ export default class MessageComposer extends React.Component { const {style, blockType} = this.state.inputState; const formatButtons = ["bold", "italic", "strike", "underline", "code", "quote", "bullet", "numbullet"].map( - name => { + (name) => { const active = style.includes(name) || blockType === name; const suffix = active ? '-o-n' : ''; const onFormatButtonClicked = this.onFormatButtonClicked.bind(this, name); - const disabled = !this.state.inputState.isRichtextEnabled && 'underline' === name; - const className = classNames("mx_MessageComposer_format_button", { - mx_MessageComposer_format_button_disabled: disabled, - mx_filterFlipColor: true, - }); + const className = 'mx_MessageComposer_format_button mx_filterFlipColor'; return ; @@ -403,5 +462,8 @@ MessageComposer.propTypes = { uploadFile: React.PropTypes.func.isRequired, // opacity for dynamic UI fading effects - opacity: React.PropTypes.number + opacity: React.PropTypes.number, + + // string representing the current room app drawer state + showApps: React.PropTypes.bool, }; diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index 5ea92d18ce..3465b2ad14 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -43,6 +43,8 @@ import Markdown from '../../../Markdown'; import ComposerHistoryManager from '../../../ComposerHistoryManager'; import {onSendMessageFailed} from './MessageComposerInputOld'; +import MessageComposerStore from '../../../stores/MessageComposerStore'; + const TYPING_USER_TIMEOUT = 10000, TYPING_SERVER_TIMEOUT = 30000; const ZWS_CODE = 8203; @@ -87,6 +89,13 @@ export default class MessageComposerInput extends React.Component { return 'toggle-mode'; } + // Allow opening of dev tools. getDefaultKeyBinding would be 'italic' for KEY_I + if (e.keyCode === KeyCode.KEY_I && e.shiftKey && e.ctrlKey) { + // When null is returned, draft-js will NOT preventDefault, allowing dev tools + // to be toggled when the editor is focussed + return null; + } + return getDefaultKeyBinding(e); } @@ -114,6 +123,7 @@ export default class MessageComposerInput extends React.Component { this.onEscape = this.onEscape.bind(this); this.setDisplayedCompletion = this.setDisplayedCompletion.bind(this); this.onMarkdownToggleClicked = this.onMarkdownToggleClicked.bind(this); + this.onTextPasted = this.onTextPasted.bind(this); const isRichtextEnabled = UserSettingsStore.getSyncedSetting('MessageComposerInput.isRichTextEnabled', false); @@ -122,15 +132,21 @@ export default class MessageComposerInput extends React.Component { isRichtextEnabled, // the currently displayed editor state (note: this is always what is modified on input) - editorState: null, + editorState: this.createEditorState( + isRichtextEnabled, + MessageComposerStore.getContentState(this.props.room.roomId), + ), // the original editor state, before we started tabbing through completions originalEditorState: null, - }; - // bit of a hack, but we need to do this here since createEditorState needs isRichtextEnabled - /* eslint react/no-direct-mutation-state:0 */ - this.state.editorState = this.createEditorState(); + // the virtual state "above" the history stack, the message currently being composed that + // we want to persist whilst browsing history + currentlyComposedEditorState: null, + + // whether there were any completions + someCompletions: null, + }; this.client = MatrixClientPeg.get(); } @@ -217,7 +233,8 @@ export default class MessageComposerInput extends React.Component { if (this.state.isRichtextEnabled) { contentState = Modifier.setBlockType(contentState, startSelection, 'blockquote'); } - const editorState = EditorState.push(this.state.editorState, contentState, 'insert-characters'); + let editorState = EditorState.push(this.state.editorState, contentState, 'insert-characters'); + editorState = EditorState.moveSelectionToEnd(editorState); this.onEditorContentChanged(editorState); editor.focus(); } @@ -323,6 +340,14 @@ export default class MessageComposerInput extends React.Component { this.onFinishedTyping(); } + // Record the editor state for this room so that it can be retrieved after + // switching to another room and back + dis.dispatch({ + action: 'content_state', + room_id: this.props.room.roomId, + content_state: state.editorState.getCurrentContent(), + }); + if (!state.hasOwnProperty('originalEditorState')) { state.originalEditorState = null; } @@ -390,26 +415,59 @@ export default class MessageComposerInput extends React.Component { }); } } else { - let contentState = this.state.editorState.getCurrentContent(), - selection = this.state.editorState.getSelection(); + let contentState = this.state.editorState.getCurrentContent(); const modifyFn = { 'bold': (text) => `**${text}**`, 'italic': (text) => `*${text}*`, - 'underline': (text) => `_${text}_`, // there's actually no valid underline in Markdown, but *shrug* + 'underline': (text) => `${text}`, 'strike': (text) => `${text}`, - 'code-block': (text) => `\`\`\`\n${text}\n\`\`\``, - 'blockquote': (text) => text.split('\n').map((line) => `> ${line}\n`).join(''), + 'code-block': (text) => `\`\`\`\n${text}\n\`\`\`\n`, + 'blockquote': (text) => text.split('\n').map((line) => `> ${line}\n`).join('') + '\n', 'unordered-list-item': (text) => text.split('\n').map((line) => `\n- ${line}`).join(''), 'ordered-list-item': (text) => text.split('\n').map((line, i) => `\n${i + 1}. ${line}`).join(''), }[command]; + const selectionAfterOffset = { + 'bold': -2, + 'italic': -1, + 'underline': -4, + 'strike': -6, + 'code-block': -5, + 'blockquote': -2, + }[command]; + + // Returns a function that collapses a selectionState to its end and moves it by offset + const collapseAndOffsetSelection = (selectionState, offset) => { + const key = selectionState.getEndKey(); + return new SelectionState({ + anchorKey: key, anchorOffset: offset, + focusKey: key, focusOffset: offset, + }); + }; + if (modifyFn) { + const previousSelection = this.state.editorState.getSelection(); + const newContentState = RichText.modifyText(contentState, previousSelection, modifyFn); newState = EditorState.push( this.state.editorState, - RichText.modifyText(contentState, selection, modifyFn), + newContentState, 'insert-characters', ); + + let newSelection = newContentState.getSelectionAfter(); + // If the selection range is 0, move the cursor inside the formatted body + if (previousSelection.getStartOffset() === previousSelection.getEndOffset() && + previousSelection.getStartKey() === previousSelection.getEndKey() && + selectionAfterOffset !== undefined + ) { + const selectedBlock = newContentState.getBlockForKey(previousSelection.getAnchorKey()); + const blockLength = selectedBlock.getText().length; + const newOffset = blockLength + selectionAfterOffset; + newSelection = collapseAndOffsetSelection(newSelection, newOffset); + } + + newState = EditorState.forceSelection(newState, newSelection); } } @@ -425,6 +483,28 @@ export default class MessageComposerInput extends React.Component { return false; } + onTextPasted(text: string, html?: string) { + const currentSelection = this.state.editorState.getSelection(); + const currentContent = this.state.editorState.getCurrentContent(); + + let contentState = null; + if (html && this.state.isRichtextEnabled) { + contentState = Modifier.replaceWithFragment( + currentContent, + currentSelection, + RichText.htmlToContentState(html).getBlockMap(), + ); + } else { + contentState = Modifier.replaceText(currentContent, currentSelection, text); + } + + let newEditorState = EditorState.push(this.state.editorState, contentState, 'insert-characters'); + + newEditorState = EditorState.forceSelection(newEditorState, contentState.getSelectionAfter()); + this.onEditorContentChanged(newEditorState); + return true; + } + handleReturn(ev) { if (ev.shiftKey) { this.onEditorContentChanged(RichUtils.insertSoftNewline(this.state.editorState)); @@ -476,9 +556,30 @@ export default class MessageComposerInput extends React.Component { } if (this.state.isRichtextEnabled) { - contentHTML = HtmlUtils.stripParagraphs( - RichText.contentStateToHTML(contentState), - ); + // We should only send HTML if any block is styled or contains inline style + let shouldSendHTML = false; + const blocks = contentState.getBlocksAsArray(); + if (blocks.some((block) => block.getType() !== 'unstyled')) { + shouldSendHTML = true; + } else { + const characterLists = blocks.map((block) => block.getCharacterList()); + // For each block of characters, determine if any inline styles are applied + // and if yes, send HTML + characterLists.forEach((characters) => { + const numberOfStylesForCharacters = characters.map( + (character) => character.getStyle().toArray().length, + ).toArray(); + // If any character has more than 0 inline styles applied, send HTML + if (numberOfStylesForCharacters.some((styles) => styles > 0)) { + shouldSendHTML = true; + } + }); + } + if (shouldSendHTML) { + contentHTML = HtmlUtils.processHtmlForSending( + RichText.contentStateToHTML(contentState), + ); + } } else { const md = new Markdown(contentText); if (md.isPlainText()) { @@ -491,6 +592,16 @@ export default class MessageComposerInput extends React.Component { let sendHtmlFn = this.client.sendHtmlMessage; let sendTextFn = this.client.sendTextMessage; + if (this.state.isRichtextEnabled) { + this.historyManager.addItem( + contentHTML ? contentHTML : contentText, + contentHTML ? 'html' : 'markdown', + ); + } else { + // Always store MD input as input history + this.historyManager.addItem(contentText, 'markdown'); + } + if (contentText.startsWith('/me')) { contentText = contentText.substring(4); // bit of a hack, but the alternative would be quite complicated @@ -499,10 +610,6 @@ export default class MessageComposerInput extends React.Component { sendTextFn = this.client.sendEmoteMessage; } - this.historyManager.addItem( - this.state.isRichtextEnabled ? contentHTML : contentState.getPlainText(), - this.state.isRichtextEnabled ? 'html' : 'markdown'); - let sendMessagePromise; if (contentHTML) { sendMessagePromise = sendHtmlFn.call( @@ -525,49 +632,117 @@ export default class MessageComposerInput extends React.Component { this.autocomplete.hide(); return true; + } + + onUpArrow = (e) => { + this.onVerticalArrow(e, true); }; - onUpArrow = async (e) => { - const completion = this.autocomplete.onUpArrow(); - if (completion == null) { - const newContent = this.historyManager.getItem(-1, this.state.isRichtextEnabled ? 'html' : 'markdown'); - if (!newContent) return false; - const editorState = EditorState.push(this.state.editorState, - newContent, - 'insert-characters'); - this.setState({editorState}); - return true; + onDownArrow = (e) => { + this.onVerticalArrow(e, false); + }; + + onVerticalArrow = (e, up) => { + if (e.ctrlKey || e.shiftKey || e.altKey || e.metaKey) { + return; } - e.preventDefault(); - return await this.setDisplayedCompletion(completion); - }; - onDownArrow = async (e) => { - const completion = this.autocomplete.onDownArrow(); - if (completion == null) { - const newContent = this.historyManager.getItem(+1, this.state.isRichtextEnabled ? 'html' : 'markdown'); - if (!newContent) return false; - const editorState = EditorState.push(this.state.editorState, - newContent, - 'insert-characters'); - this.setState({editorState}); - return true; - } - e.preventDefault(); - return await this.setDisplayedCompletion(completion); - }; - - // tab and shift-tab are mapped to down and up arrow respectively - onTab = async (e) => { - e.preventDefault(); // we *never* want tab's default to happen, but we do want up/down sometimes + // Select history only if we are not currently auto-completing if (this.autocomplete.state.completionList.length === 0) { - await this.autocomplete.forceComplete(); - this.onDownArrow(e); + // Don't go back in history if we're in the middle of a multi-line message + const selection = this.state.editorState.getSelection(); + const blockKey = selection.getStartKey(); + const firstBlock = this.state.editorState.getCurrentContent().getFirstBlock(); + const lastBlock = this.state.editorState.getCurrentContent().getLastBlock(); + + let canMoveUp = false; + let canMoveDown = false; + if (blockKey === firstBlock.getKey()) { + canMoveUp = selection.getStartOffset() === selection.getEndOffset() && + selection.getStartOffset() === 0; + } + + if (blockKey === lastBlock.getKey()) { + canMoveDown = selection.getStartOffset() === selection.getEndOffset() && + selection.getStartOffset() === lastBlock.getText().length; + } + + if ((up && !canMoveUp) || (!up && !canMoveDown)) return; + + const selected = this.selectHistory(up); + if (selected) { + // We're selecting history, so prevent the key event from doing anything else + e.preventDefault(); + } } else { - await (e.shiftKey ? this.onUpArrow : this.onDownArrow)(e); + this.moveAutocompleteSelection(up); } }; + 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; + } + + const newContent = this.historyManager.getItem(delta, this.state.isRichtextEnabled ? 'html' : 'markdown'); + if (!newContent) return false; + let editorState = EditorState.push( + this.state.editorState, + newContent, + 'insert-characters', + ); + + // Move selection to the end of the selected history + let newSelection = SelectionState.createEmpty(newContent.getLastBlock().getKey()); + newSelection = newSelection.merge({ + focusOffset: newContent.getLastBlock().getLength(), + anchorOffset: newContent.getLastBlock().getLength(), + }); + editorState = EditorState.forceSelection(editorState, newSelection); + + this.setState({editorState}); + return true; + }; + + onTab = async (e) => { + this.setState({ + someCompletions: null, + }); + e.preventDefault(); + if (this.autocomplete.state.completionList.length === 0) { + // Force completions to show for the text currently entered + const completionCount = await this.autocomplete.forceComplete(); + this.setState({ + someCompletions: completionCount > 0, + }); + // Select the first item by moving "down" + await this.moveAutocompleteSelection(false); + } else { + await this.moveAutocompleteSelection(e.shiftKey); + } + }; + + moveAutocompleteSelection = (up) => { + const completion = up ? this.autocomplete.onUpArrow() : this.autocomplete.onDownArrow(); + return this.setDisplayedCompletion(completion); + }; + onEscape = async (e) => { e.preventDefault(); if (this.autocomplete) { @@ -676,6 +851,7 @@ export default class MessageComposerInput extends React.Component { const className = classNames('mx_MessageComposer_input', { mx_MessageComposer_input_empty: hidePlaceholder, + mx_MessageComposer_input_error: this.state.someCompletions === false, }); const content = activeEditorState.getCurrentContent(); @@ -706,6 +882,7 @@ export default class MessageComposerInput extends React.Component { keyBindingFn={MessageComposerInput.getKeyBinding} handleKeyCommand={this.handleKeyCommand} handleReturn={this.handleReturn} + handlePastedText={this.onTextPasted} handlePastedFiles={this.props.onFilesPasted} stripPastedStyles={!this.state.isRichtextEnabled} onTab={this.onTab} diff --git a/src/components/views/rooms/RoomHeader.js b/src/components/views/rooms/RoomHeader.js index 19010d8a10..85aedadf64 100644 --- a/src/components/views/rooms/RoomHeader.js +++ b/src/components/views/rooms/RoomHeader.js @@ -16,18 +16,18 @@ limitations under the License. 'use strict'; -var React = require('react'); -var classNames = require('classnames'); -var sdk = require('../../../index'); +import React from 'react'; +import classNames from 'classnames'; +import sdk from '../../../index'; import { _t } from '../../../languageHandler'; -var MatrixClientPeg = require('../../../MatrixClientPeg'); -var Modal = require("../../../Modal"); -var dis = require("../../../dispatcher"); -var rate_limited_func = require('../../../ratelimitedfunc'); +import MatrixClientPeg from '../../../MatrixClientPeg'; +import Modal from "../../../Modal"; +import dis from "../../../dispatcher"; +import RateLimitedFunc from '../../../ratelimitedfunc'; -var linkify = require('linkifyjs'); -var linkifyElement = require('linkifyjs/element'); -var linkifyMatrix = require('../../../linkify-matrix'); +import * as linkify from 'linkifyjs'; +import linkifyElement from 'linkifyjs/element'; +import linkifyMatrix from '../../../linkify-matrix'; import AccessibleButton from '../elements/AccessibleButton'; import {CancelButton} from './SimpleRoomHeader'; @@ -58,7 +58,7 @@ module.exports = React.createClass({ }, componentDidMount: function() { - var cli = MatrixClientPeg.get(); + const cli = MatrixClientPeg.get(); cli.on("RoomState.events", this._onRoomStateEvents); // When a room name occurs, RoomState.events is fired *before* @@ -79,14 +79,14 @@ module.exports = React.createClass({ if (this.props.room) { this.props.room.removeListener("Room.name", this._onRoomNameChange); } - var cli = MatrixClientPeg.get(); + const cli = MatrixClientPeg.get(); if (cli) { cli.removeListener("RoomState.events", this._onRoomStateEvents); } }, _onRoomStateEvents: function(event, state) { - if (!this.props.room || event.getRoomId() != this.props.room.roomId) { + if (!this.props.room || event.getRoomId() !== this.props.room.roomId) { return; } @@ -94,7 +94,8 @@ module.exports = React.createClass({ this._rateLimitedUpdate(); }, - _rateLimitedUpdate: new rate_limited_func(function() { + _rateLimitedUpdate: new RateLimitedFunc(function() { + /* eslint-disable babel/no-invalid-this */ this.forceUpdate(); }, 500), @@ -109,15 +110,14 @@ module.exports = React.createClass({ }, onAvatarSelected: function(ev) { - var self = this; - var changeAvatar = this.refs.changeAvatar; + const changeAvatar = this.refs.changeAvatar; if (!changeAvatar) { console.error("No ChangeAvatar found to upload image to!"); return; } changeAvatar.onFileSelected(ev).catch(function(err) { - var errMsg = (typeof err === "string") ? err : (err.error || ""); - var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + const errMsg = (typeof err === "string") ? err : (err.error || ""); + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); console.error("Failed to set avatar: " + errMsg); Modal.createDialog(ErrorDialog, { title: _t("Error"), @@ -133,10 +133,10 @@ module.exports = React.createClass({ /** * After editing the settings, get the new name for the room * - * Returns undefined if we didn't let the user edit the room name + * @return {?string} newName or undefined if we didn't let the user edit the room name */ getEditedName: function() { - var newName; + let newName; if (this.refs.nameEditor) { newName = this.refs.nameEditor.getRoomName(); } @@ -146,10 +146,10 @@ module.exports = React.createClass({ /** * After editing the settings, get the new topic for the room * - * Returns undefined if we didn't let the user edit the room topic + * @return {?string} newTopic or undefined if we didn't let the user edit the room topic */ getEditedTopic: function() { - var newTopic; + let newTopic; if (this.refs.topicEditor) { newTopic = this.refs.topicEditor.getTopic(); } @@ -157,38 +157,31 @@ module.exports = React.createClass({ }, render: function() { - var RoomAvatar = sdk.getComponent("avatars.RoomAvatar"); - var ChangeAvatar = sdk.getComponent("settings.ChangeAvatar"); - var TintableSvg = sdk.getComponent("elements.TintableSvg"); + const RoomAvatar = sdk.getComponent("avatars.RoomAvatar"); + const ChangeAvatar = sdk.getComponent("settings.ChangeAvatar"); + const TintableSvg = sdk.getComponent("elements.TintableSvg"); const EmojiText = sdk.getComponent('elements.EmojiText'); - var header; - var name = null; - var searchStatus = null; - var topic_el = null; - var cancel_button = null; - var spinner = null; - var save_button = null; - var settings_button = null; + let name = null; + let searchStatus = null; + let topicElement = null; + let cancelButton = null; + let spinner = null; + let saveButton = null; + let settingsButton = null; + + let canSetRoomName; + let canSetRoomAvatar; + let canSetRoomTopic; if (this.props.editing) { - // calculate permissions. XXX: this should be done on mount or something - var user_id = MatrixClientPeg.get().credentials.userId; + const userId = MatrixClientPeg.get().credentials.userId; - var can_set_room_name = this.props.room.currentState.maySendStateEvent( - 'm.room.name', user_id - ); - var can_set_room_avatar = this.props.room.currentState.maySendStateEvent( - 'm.room.avatar', user_id - ); - var can_set_room_topic = this.props.room.currentState.maySendStateEvent( - 'm.room.topic', user_id - ); - var can_set_room_name = this.props.room.currentState.maySendStateEvent( - 'm.room.name', user_id - ); + canSetRoomName = this.props.room.currentState.maySendStateEvent('m.room.name', userId); + canSetRoomAvatar = this.props.room.currentState.maySendStateEvent('m.room.avatar', userId); + canSetRoomTopic = this.props.room.currentState.maySendStateEvent('m.room.topic', userId); - save_button = ( + saveButton = ( {_t("Save")} @@ -196,39 +189,41 @@ module.exports = React.createClass({ } if (this.props.onCancelClick) { - cancel_button = ; + cancelButton = ; } if (this.props.saving) { - var Spinner = sdk.getComponent("elements.Spinner"); + const Spinner = sdk.getComponent("elements.Spinner"); spinner =
; } - if (can_set_room_name) { - var RoomNameEditor = sdk.getComponent("rooms.RoomNameEditor"); + if (canSetRoomName) { + const RoomNameEditor = sdk.getComponent("rooms.RoomNameEditor"); name = ; - } - else { - var searchStatus; + } else { // don't display the search count until the search completes and // gives us a valid (possibly zero) searchCount. - if (this.props.searchInfo && this.props.searchInfo.searchCount !== undefined && this.props.searchInfo.searchCount !== null) { - searchStatus =
 { _t("(~%(count)s results)", { count: this.props.searchInfo.searchCount }) }
; + if (this.props.searchInfo && + this.props.searchInfo.searchCount !== undefined && + this.props.searchInfo.searchCount !== null) { + searchStatus =
  + { _t("(~%(count)s results)", { count: this.props.searchInfo.searchCount }) } +
; } // XXX: this is a bit inefficient - we could just compare room.name for 'Empty room'... - var settingsHint = false; - var members = this.props.room ? this.props.room.getJoinedMembers() : undefined; + let settingsHint = false; + const members = this.props.room ? this.props.room.getJoinedMembers() : undefined; if (members) { if (members.length === 1 && members[0].userId === MatrixClientPeg.get().credentials.userId) { - var name = this.props.room.currentState.getStateEvents('m.room.name', ''); - if (!name || !name.getContent().name) { + const nameEvent = this.props.room.currentState.getStateEvents('m.room.name', ''); + if (!nameEvent || !nameEvent.getContent().name) { settingsHint = true; } } } - var roomName = _t("Join Room"); + let roomName = _t("Join Room"); if (this.props.oobData && this.props.oobData.name) { roomName = this.props.oobData.name; } else if (this.props.room) { @@ -243,24 +238,25 @@ module.exports = React.createClass({
; } - if (can_set_room_topic) { - var RoomTopicEditor = sdk.getComponent("rooms.RoomTopicEditor"); - topic_el = ; + if (canSetRoomTopic) { + const RoomTopicEditor = sdk.getComponent("rooms.RoomTopicEditor"); + topicElement = ; } else { - var topic; + let topic; if (this.props.room) { - var ev = this.props.room.currentState.getStateEvents('m.room.topic', ''); + const ev = this.props.room.currentState.getStateEvents('m.room.topic', ''); if (ev) { topic = ev.getContent().topic; } } if (topic) { - topic_el =
{ topic }
; + topicElement = +
{ topic }
; } } - var roomAvatar = null; - if (can_set_room_avatar) { + let roomAvatar = null; + if (canSetRoomAvatar) { roomAvatar = (
@@ -276,8 +272,7 @@ module.exports = React.createClass({
); - } - else if (this.props.room || (this.props.oobData && this.props.oobData.name)) { + } else if (this.props.room || (this.props.oobData && this.props.oobData.name)) { roomAvatar = (
@@ -285,9 +280,8 @@ module.exports = React.createClass({ ); } - var settings_button; if (this.props.onSettingsClick) { - settings_button = + settingsButton = ; @@ -301,61 +295,58 @@ module.exports = React.createClass({ //
; // } - var forget_button; + let forgetButton; if (this.props.onForgetClick) { - forget_button = + forgetButton = ; } - let search_button; + let searchButton; if (this.props.onSearchClick && this.props.inRoom) { - search_button = + searchButton = ; } - var rightPanel_buttons; + let rightPanelButtons; if (this.props.collapsedRhs) { - rightPanel_buttons = + rightPanelButtons = ; } - var right_row; + let rightRow; if (!this.props.editing) { - right_row = + rightRow =
- { settings_button } - { forget_button } - { search_button } - { rightPanel_buttons } + { settingsButton } + { forgetButton } + { searchButton } + { rightPanelButtons }
; } - header = -
-
-
- { roomAvatar } -
-
- { name } - { topic_el } -
-
- {spinner} - {save_button} - {cancel_button} - {right_row} -
; - return (
- { header } +
+
+
+ { roomAvatar } +
+
+ { name } + { topicElement } +
+
+ {spinner} + {saveButton} + {cancelButton} + {rightRow} +
); }, diff --git a/src/components/views/rooms/RoomSettings.js b/src/components/views/rooms/RoomSettings.js index 171af4764b..d255670a52 100644 --- a/src/components/views/rooms/RoomSettings.js +++ b/src/components/views/rooms/RoomSettings.js @@ -39,6 +39,7 @@ function parseIntWithDefault(val, def) { const BannedUser = React.createClass({ propTypes: { + canUnban: React.PropTypes.bool, member: React.PropTypes.object.isRequired, // js-sdk RoomMember reason: React.PropTypes.string, }, @@ -67,13 +68,17 @@ const BannedUser = React.createClass({ }, render: function() { + let unbanButton; + + if (this.props.canUnban) { + unbanButton = + { _t('Unban') } + ; + } + return (
  • - - { _t('Unban') } - + { unbanButton } {this.props.member.name} {this.props.member.userId} {this.props.reason ? " " +_t('Reason') + ": " + this.props.reason : ""}
  • @@ -667,6 +672,7 @@ module.exports = React.createClass({ const banned = this.props.room.getMembersWithMembership("ban"); let bannedUsersSection; if (banned.length) { + const canBanUsers = current_user_level >= ban_level; bannedUsersSection =

    { _t('Banned users') }

    @@ -674,7 +680,7 @@ module.exports = React.createClass({ {banned.map(function(member) { const banEvent = member.events.member.getContent(); return ( - + ); })} diff --git a/src/components/views/voip/CallView.js b/src/components/views/voip/CallView.js index b53794637f..e669f7e0a6 100644 --- a/src/components/views/voip/CallView.js +++ b/src/components/views/voip/CallView.js @@ -13,11 +13,11 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ -var React = require("react"); -var dis = require("../../../dispatcher"); -var CallHandler = require("../../../CallHandler"); -var sdk = require('../../../index'); -var MatrixClientPeg = require("../../../MatrixClientPeg"); +import React from 'react'; +import dis from '../../../dispatcher'; +import CallHandler from '../../../CallHandler'; +import sdk from '../../../index'; +import MatrixClientPeg from '../../../MatrixClientPeg'; import { _t } from '../../../languageHandler'; module.exports = React.createClass({ @@ -73,10 +73,10 @@ module.exports = React.createClass({ }, showCall: function() { - var call; + let call; if (this.props.room) { - var roomId = this.props.room.roomId; + const roomId = this.props.room.roomId; call = CallHandler.getCallForRoom(roomId) || (this.props.ConferenceHandler ? this.props.ConferenceHandler.getConferenceCallForRoom(roomId) : @@ -86,9 +86,7 @@ module.exports = React.createClass({ if (this.call) { this.setState({ call: call }); } - - } - else { + } else { call = CallHandler.getAnyActiveCall(); this.setState({ call: call }); } @@ -109,8 +107,7 @@ module.exports = React.createClass({ call.confUserId ? "none" : "block" ); this.getVideoView().getRemoteVideoElement().style.display = "block"; - } - else { + } else { this.getVideoView().getLocalVideoElement().style.display = "none"; this.getVideoView().getRemoteVideoElement().style.display = "none"; dis.dispatch({action: 'video_fullscreen', fullscreen: false}); @@ -126,11 +123,11 @@ module.exports = React.createClass({ }, render: function() { - var VideoView = sdk.getComponent('voip.VideoView'); + const VideoView = sdk.getComponent('voip.VideoView'); - var voice; + let voice; if (this.state.call && this.state.call.type === "voice" && this.props.showVoice) { - var callRoom = MatrixClientPeg.get().getRoom(this.state.call.roomId); + const callRoom = MatrixClientPeg.get().getRoom(this.state.call.roomId); voice = (
    {_t("Active call (%(roomName)s)", {roomName: callRoom.name})} @@ -147,6 +144,6 @@ module.exports = React.createClass({ { voice }
    ); - } + }, }); diff --git a/src/components/views/voip/IncomingCallBox.js b/src/components/views/voip/IncomingCallBox.js index 1b806fc5b3..c5934b74dc 100644 --- a/src/components/views/voip/IncomingCallBox.js +++ b/src/components/views/voip/IncomingCallBox.js @@ -13,10 +13,9 @@ 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. */ -var React = require('react'); -var MatrixClientPeg = require('../../../MatrixClientPeg'); -var dis = require("../../../dispatcher"); -var CallHandler = require("../../../CallHandler"); +import React from 'react'; +import MatrixClientPeg from '../../../MatrixClientPeg'; +import dis from '../../../dispatcher'; import { _t } from '../../../languageHandler'; module.exports = React.createClass({ @@ -29,34 +28,32 @@ module.exports = React.createClass({ onAnswerClick: function() { dis.dispatch({ action: 'answer', - room_id: this.props.incomingCall.roomId + room_id: this.props.incomingCall.roomId, }); }, onRejectClick: function() { dis.dispatch({ action: 'hangup', - room_id: this.props.incomingCall.roomId + room_id: this.props.incomingCall.roomId, }); }, render: function() { - var room = null; + let room = null; if (this.props.incomingCall) { room = MatrixClientPeg.get().getRoom(this.props.incomingCall.roomId); } - var caller = room ? room.name : _t("unknown caller"); + const caller = room ? room.name : _t("unknown caller"); let incomingCallText = null; if (this.props.incomingCall) { if (this.props.incomingCall.type === "voice") { incomingCallText = _t("Incoming voice call from %(name)s", {name: caller}); - } - else if (this.props.incomingCall.type === "video") { + } else if (this.props.incomingCall.type === "video") { incomingCallText = _t("Incoming video call from %(name)s", {name: caller}); - } - else { + } else { incomingCallText = _t("Incoming call from %(name)s", {name: caller}); } } @@ -81,6 +78,6 @@ module.exports = React.createClass({
    ); - } + }, }); diff --git a/src/components/views/voip/VideoFeed.js b/src/components/views/voip/VideoFeed.js index 0b8d0b20fc..953dbc866f 100644 --- a/src/components/views/voip/VideoFeed.js +++ b/src/components/views/voip/VideoFeed.js @@ -16,7 +16,7 @@ limitations under the License. 'use strict'; -var React = require('react'); +import React from 'react'; module.exports = React.createClass({ displayName: 'VideoFeed', diff --git a/src/components/views/voip/VideoView.js b/src/components/views/voip/VideoView.js index ea37579237..6ebf2078c1 100644 --- a/src/components/views/voip/VideoView.js +++ b/src/components/views/voip/VideoView.js @@ -16,11 +16,11 @@ limitations under the License. 'use strict'; -var React = require('react'); -var ReactDOM = require('react-dom'); +import React from 'react'; +import ReactDOM from 'react-dom'; -var sdk = require('../../../index'); -var dis = require('../../../dispatcher'); +import sdk from '../../../index'; +import dis from '../../../dispatcher'; module.exports = React.createClass({ displayName: 'VideoView', @@ -53,9 +53,10 @@ module.exports = React.createClass({ // this needs to be somewhere at the top of the DOM which // always exists to avoid audio interruptions. // Might as well just use DOM. - var remoteAudioElement = document.getElementById("remoteAudio"); + const remoteAudioElement = document.getElementById("remoteAudio"); if (!remoteAudioElement) { - console.error("Failed to find remoteAudio element - cannot play audio! You need to add an
    ); - } + }, }); diff --git a/src/createRoom.js b/src/createRoom.js index ce83f31c27..916405776d 100644 --- a/src/createRoom.js +++ b/src/createRoom.js @@ -14,24 +14,24 @@ See the License for the specific language governing permissions and limitations under the License. */ -var MatrixClientPeg = require('./MatrixClientPeg'); -var Modal = require('./Modal'); -var sdk = require('./index'); +import MatrixClientPeg from './MatrixClientPeg'; +import Modal from './Modal'; +import sdk from './index'; import { _t } from './languageHandler'; -var dis = require("./dispatcher"); -var Rooms = require("./Rooms"); +import dis from "./dispatcher"; +import * as Rooms from "./Rooms"; -var q = require('q'); +import q from 'q'; /** * Create a new room, and switch to it. * - * Returns a promise which resolves to the room id, or null if the - * action was aborted or failed. - * * @param {object=} opts parameters for creating the room * @param {string=} opts.dmUserId If specified, make this a DM room for this user and invite them * @param {object=} opts.createOpts set of options to pass to createRoom call. + * + * @returns {Promise} which resolves to the room id, or null if the + * action was aborted or failed. */ function createRoom(opts) { opts = opts || {}; @@ -69,16 +69,22 @@ function createRoom(opts) { createOpts.initial_state = createOpts.initial_state || [ { content: { - guest_access: 'can_join' + guest_access: 'can_join', }, type: 'm.room.guest_access', state_key: '', - } + }, ]; const modal = Modal.createDialog(Loader, null, 'mx_Dialog_spinner'); let roomId; + if (opts.andView) { + // We will possibly have a successful join, indicate as such + dis.dispatch({ + action: 'will_join', + }); + } return client.createRoom(createOpts).finally(function() { modal.close(); }).then(function(res) { @@ -98,10 +104,16 @@ function createRoom(opts) { action: 'view_room', room_id: roomId, should_peek: false, + // Creating a room will have joined us to the room + joined: true, }); } return roomId; }, function(err) { + // We also failed to join the room (this sets joining to false in RoomViewStore) + dis.dispatch({ + action: 'join_room_error', + }); console.error("Failed to create room " + roomId + " " + err); Modal.createDialog(ErrorDialog, { title: _t("Failure to create room"), diff --git a/src/email.js b/src/email.js index c4375079d7..3fd535c849 100644 --- a/src/email.js +++ b/src/email.js @@ -14,10 +14,10 @@ See the License for the specific language governing permissions and limitations under the License. */ -var EMAIL_ADDRESS_REGEX = /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i; +const EMAIL_ADDRESS_REGEX = /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i; module.exports = { looksValid: function(email) { return EMAIL_ADDRESS_REGEX.test(email); - } + }, }; diff --git a/src/extend.js b/src/extend.js index cc3c33b2e7..4b3f028a94 100644 --- a/src/extend.js +++ b/src/extend.js @@ -17,7 +17,7 @@ limitations under the License. 'use strict'; module.exports = function(dest, src) { - for (var i in src) { + for (const i in src) { if (src.hasOwnProperty(i)) { dest[i] = src[i]; } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 27a377e8b7..15ae37c94e 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1,929 +1,938 @@ { - "af":"Afrikaans", - "ar-ae":"Arabic (U.A.E.)", - "ar-bh":"Arabic (Bahrain)", - "ar-dz":"Arabic (Algeria)", - "ar-eg":"Arabic (Egypt)", - "ar-iq":"Arabic (Iraq)", - "ar-jo":"Arabic (Jordan)", - "ar-kw":"Arabic (Kuwait)", - "ar-lb":"Arabic (Lebanon)", - "ar-ly":"Arabic (Libya)", - "ar-ma":"Arabic (Morocco)", - "ar-om":"Arabic (Oman)", - "ar-qa":"Arabic (Qatar)", - "ar-sa":"Arabic (Saudi Arabia)", - "ar-sy":"Arabic (Syria)", - "ar-tn":"Arabic (Tunisia)", - "ar-ye":"Arabic (Yemen)", - "be":"Belarusian", - "bg":"Bulgarian", - "ca":"Catalan", - "cs":"Czech", - "da":"Danish", - "de-at":"German (Austria)", - "de-ch":"German (Switzerland)", - "de":"German", - "de-li":"German (Liechtenstein)", - "de-lu":"German (Luxembourg)", - "el":"Greek", - "en-au":"English (Australia)", - "en-bz":"English (Belize)", - "en-ca":"English (Canada)", - "en":"English", - "en-gb":"English (United Kingdom)", - "en-ie":"English (Ireland)", - "en-jm":"English (Jamaica)", - "en-nz":"English (New Zealand)", - "en-tt":"English (Trinidad)", - "en-us":"English (United States)", - "en-za":"English (South Africa)", - "es-ar":"Spanish (Argentina)", - "es-bo":"Spanish (Bolivia)", - "es-cl":"Spanish (Chile)", - "es-co":"Spanish (Colombia)", - "es-cr":"Spanish (Costa Rica)", - "es-do":"Spanish (Dominican Republic)", - "es-ec":"Spanish (Ecuador)", - "es-gt":"Spanish (Guatemala)", - "es-hn":"Spanish (Honduras)", - "es-mx":"Spanish (Mexico)", - "es-ni":"Spanish (Nicaragua)", - "es-pa":"Spanish (Panama)", - "es-pe":"Spanish (Peru)", - "es-pr":"Spanish (Puerto Rico)", - "es-py":"Spanish (Paraguay)", - "es":"Spanish (Spain)", - "es-sv":"Spanish (El Salvador)", - "es-uy":"Spanish (Uruguay)", - "es-ve":"Spanish (Venezuela)", - "et":"Estonian", - "eu":"Basque (Basque)", - "fa":"Farsi", - "fi":"Finnish", - "fo":"Faeroese", - "fr-be":"French (Belgium)", - "fr-ca":"French (Canada)", - "fr-ch":"French (Switzerland)", - "fr":"French", - "fr-lu":"French (Luxembourg)", - "ga":"Irish", - "gd":"Gaelic (Scotland)", - "he":"Hebrew", - "hi":"Hindi", - "hr":"Croatian", - "hu":"Hungarian", - "id":"Indonesian", - "is":"Icelandic", - "it-ch":"Italian (Switzerland)", - "it":"Italian", - "ja":"Japanese", - "ji":"Yiddish", - "ko":"Korean", - "lt":"Lithuanian", - "lv":"Latvian", - "mk":"Macedonian (FYROM)", - "ms":"Malaysian", - "mt":"Maltese", - "nl-be":"Dutch (Belgium)", - "nl":"Dutch", - "no":"Norwegian", - "pl":"Polish", - "pt-br":"Brazilian Portuguese", - "pt":"Portuguese", - "rm":"Rhaeto-Romanic", - "ro-mo":"Romanian (Republic of Moldova)", - "ro":"Romanian", - "ru-mo":"Russian (Republic of Moldova)", - "ru":"Russian", - "sb":"Sorbian", - "sk":"Slovak", - "sl":"Slovenian", - "sq":"Albanian", - "sr":"Serbian", - "sv-fi":"Swedish (Finland)", - "sv":"Swedish", - "sx":"Sutu", - "sz":"Sami (Lappish)", - "th":"Thai", - "tn":"Tswana", - "tr":"Turkish", - "ts":"Tsonga", - "uk":"Ukrainian", - "ur":"Urdu", - "ve":"Venda", - "vi":"Vietnamese", - "xh":"Xhosa", - "zh-cn":"Chinese (PRC)", - "zh-hk":"Chinese (Hong Kong SAR)", - "zh-sg":"Chinese (Singapore)", - "zh-tw":"Chinese (Taiwan)", - "zu":"Zulu", - "a room": "a room", - "A text message has been sent to +%(msisdn)s. Please enter the verification code it contains": "A text message has been sent to +%(msisdn)s. Please enter the verification code it contains", - "Accept": "Accept", - "%(targetName)s accepted an invitation.": "%(targetName)s accepted an invitation.", - "%(targetName)s accepted the invitation for %(displayName)s.": "%(targetName)s accepted the invitation for %(displayName)s.", - "Account": "Account", - "Access Token:": "Access Token:", - "Active call (%(roomName)s)": "Active call (%(roomName)s)", - "Add": "Add", - "Add a topic": "Add a topic", - "Add email address": "Add email address", - "Add phone number": "Add phone number", - "Admin": "Admin", - "Admin tools": "Admin tools", - "And %(count)s more...": "And %(count)s more...", - "VoIP": "VoIP", - "Missing Media Permissions, click here to request.": "Missing Media Permissions, click here to request.", - "No Microphones detected": "No Microphones detected", - "No Webcams detected": "No Webcams detected", - "No media permissions": "No media permissions", - "You may need to manually permit Riot to access your microphone/webcam": "You may need to manually permit Riot to access your microphone/webcam", - "Default Device": "Default Device", - "Microphone": "Microphone", - "Camera": "Camera", - "Advanced": "Advanced", - "Algorithm": "Algorithm", - "Hide removed messages": "Hide removed messages", - "Always show message timestamps": "Always show message timestamps", - "Authentication": "Authentication", - "Alias (optional)": "Alias (optional)", - "all room members": "all room members", - "all room members, from the point they are invited": "all room members, from the point they are invited", - "all room members, from the point they joined": "all room members, from the point they joined", - "and": "and", - "%(items)s and %(remaining)s others": "%(items)s and %(remaining)s others", - "%(items)s and one other": "%(items)s and one other", - "%(items)s and %(lastItem)s": "%(items)s and %(lastItem)s", - "and %(overflowCount)s others...": "and %(overflowCount)s others...", - "and one other...": "and one other...", - "%(names)s and %(lastPerson)s are typing": "%(names)s and %(lastPerson)s are typing", - "%(names)s and one other are typing": "%(names)s and one other are typing", - "%(names)s and %(count)s others are typing": "%(names)s and %(count)s others are typing", - "An email has been sent to": "An email has been sent to", - "A new password must be entered.": "A new password must be entered.", - "%(senderName)s answered the call.": "%(senderName)s answered the call.", - "anyone": "anyone", - "An error has occurred.": "An error has occurred.", - "Anyone": "Anyone", - "Anyone who knows the room's link, apart from guests": "Anyone who knows the room's link, apart from guests", - "Anyone who knows the room's link, including guests": "Anyone who knows the room's link, including guests", - "Are you sure?": "Are you sure?", - "Are you sure you want to leave the room '%(roomName)s'?": "Are you sure you want to leave the room '%(roomName)s'?", - "Are you sure you want to reject the invitation?": "Are you sure you want to reject the invitation?", - "Are you sure you want to upload the following files?": "Are you sure you want to upload the following files?", - "Attachment": "Attachment", - "Autoplay GIFs and videos": "Autoplay GIFs and videos", - "%(senderName)s banned %(targetName)s.": "%(senderName)s banned %(targetName)s.", - "Ban": "Ban", - "Banned users": "Banned users", - "Bans user with given id": "Bans user with given id", - "Blacklisted": "Blacklisted", - "Bug Report": "Bug Report", - "Bulk Options": "Bulk Options", - "Call Timeout": "Call Timeout", - "Can't connect to homeserver - please check your connectivity, ensure your homeserver's SSL certificate is trusted, and that a browser extension is not blocking requests.": "Can't connect to homeserver - please check your connectivity, ensure your homeserver's SSL certificate is trusted, and that a browser extension is not blocking requests.", - "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.", - "Can't load user settings": "Can't load user settings", - "Change Password": "Change Password", - "%(senderName)s changed their display name from %(oldDisplayName)s to %(displayName)s.": "%(senderName)s changed their display name from %(oldDisplayName)s to %(displayName)s.", - "%(senderName)s changed their profile picture.": "%(senderName)s changed their profile picture.", - "%(senderName)s changed the power level of %(powerLevelDiffText)s.": "%(senderName)s changed the power level of %(powerLevelDiffText)s.", - "%(senderDisplayName)s changed the room name to %(roomName)s.": "%(senderDisplayName)s changed the room name to %(roomName)s.", - "%(senderDisplayName)s removed the room name.": "%(senderDisplayName)s removed the room name.", - "%(senderDisplayName)s changed the topic to \"%(topic)s\".": "%(senderDisplayName)s changed the topic to \"%(topic)s\".", - "Changes to who can read history will only apply to future messages in this room": "Changes to who can read history will only apply to future messages in this room", - "Changes your display nickname": "Changes your display nickname", - "changing room on a RoomView is not supported": "changing room on a RoomView is not supported", - "Changing password will currently reset any end-to-end encryption keys on all devices, making encrypted chat history unreadable, unless you first export your room keys and re-import them afterwards. In future this will be improved.": "Changing password will currently reset any end-to-end encryption keys on all devices, making encrypted chat history unreadable, unless you first export your room keys and re-import them afterwards. In future this will be improved.", - "Claimed Ed25519 fingerprint key": "Claimed Ed25519 fingerprint key", - "Clear Cache and Reload": "Clear Cache and Reload", - "Clear Cache": "Clear Cache", - "Click here to join the discussion!": "Click here to join the discussion!", - "Click here to fix": "Click here to fix", - "Click to mute audio": "Click to mute audio", - "Click to mute video": "Click to mute video", - "click to reveal": "click to reveal", - "Click to unmute video": "Click to unmute video", - "Click to unmute audio": "Click to unmute audio", - "Close": "Close", - "Command error": "Command error", - "Commands": "Commands", - "Conference call failed.": "Conference call failed.", - "Conference calling is in development and may not be reliable.": "Conference calling is in development and may not be reliable.", - "Conference calls are not supported in encrypted rooms": "Conference calls are not supported in encrypted rooms", - "Conference calls are not supported in this client": "Conference calls are not supported in this client", - "Confirm password": "Confirm password", - "Confirm your new password": "Confirm your new password", - "Continue": "Continue", - "Could not connect to the integration server": "Could not connect to the integration server", - "%(count)s new messages": { - "one": "%(count)s new message", - "other": "%(count)s new messages" - }, - "Create a new chat or reuse an existing one": "Create a new chat or reuse an existing one", - "Create an account": "Create an account", - "Create Room": "Create Room", - "Cryptography": "Cryptography", - "Current password": "Current password", - "Curve25519 identity key": "Curve25519 identity key", - "Custom": "Custom", - "Custom level": "Custom level", - "/ddg is not a command": "/ddg is not a command", - "Deactivate Account": "Deactivate Account", - "Deactivate my account": "Deactivate my account", - "Decline": "Decline", - "Decrypt %(text)s": "Decrypt %(text)s", - "Decryption error": "Decryption error", - "Delete": "Delete", - "demote": "demote", - "Deops user with given id": "Deops user with given id", - "Default": "Default", - "Define the power level of a user": "Define the power level of a user", - "Device already verified!": "Device already verified!", - "Device ID": "Device ID", - "Device ID:": "Device ID:", - "device id: ": "device id: ", - "Device key:": "Device key:", - "Devices": "Devices", - "Devices will not yet be able to decrypt history from before they joined the room": "Devices will not yet be able to decrypt history from before they joined the room", - "Direct Chat": "Direct Chat", - "Direct chats": "Direct chats", - "Disable Notifications": "Disable Notifications", - "disabled": "disabled", - "Disable inline URL previews by default": "Disable inline URL previews by default", - "Disable markdown formatting": "Disable markdown formatting", - "Disinvite": "Disinvite", - "Display name": "Display name", - "Displays action": "Displays action", - "Don't send typing notifications": "Don't send typing notifications", - "Download %(text)s": "Download %(text)s", - "Drop File Here": "Drop File Here", - "Drop here %(toAction)s": "Drop here %(toAction)s", - "Drop here to tag %(section)s": "Drop here to tag %(section)s", - "Ed25519 fingerprint": "Ed25519 fingerprint", - "Email": "Email", - "Email address": "Email address", - "Email address (optional)": "Email address (optional)", - "Email, name or matrix ID": "Email, name or matrix ID", - "Emoji": "Emoji", - "Enable encryption": "Enable encryption", - "Enable Notifications": "Enable Notifications", - "enabled": "enabled", - "Encrypted by a verified device": "Encrypted by a verified device", - "Encrypted by an unverified device": "Encrypted by an unverified device", - "Encrypted messages will not be visible on clients that do not yet implement encryption": "Encrypted messages will not be visible on clients that do not yet implement encryption", - "Encrypted room": "Encrypted room", - "Encryption is enabled in this room": "Encryption is enabled in this room", - "Encryption is not enabled in this room": "Encryption is not enabled in this room", - "%(senderName)s ended the call.": "%(senderName)s ended the call.", - "End-to-end encryption information": "End-to-end encryption information", - "End-to-end encryption is in beta and may not be reliable": "End-to-end encryption is in beta and may not be reliable", - "Enter Code": "Enter Code", - "Enter passphrase": "Enter passphrase", - "Error": "Error", - "Error decrypting attachment": "Error decrypting attachment", - "Error: Problem communicating with the given homeserver.": "Error: Problem communicating with the given homeserver.", - "Event information": "Event information", - "Existing Call": "Existing Call", - "Export": "Export", - "Export E2E room keys": "Export E2E room keys", - "Failed to ban user": "Failed to ban user", - "Failed to change password. Is your password correct?": "Failed to change password. Is your password correct?", - "Failed to change power level": "Failed to change power level", - "Failed to delete device": "Failed to delete device", - "Failed to fetch avatar URL": "Failed to fetch avatar URL", - "Failed to forget room %(errCode)s": "Failed to forget room %(errCode)s", - "Failed to join room": "Failed to join room", - "Failed to join the room": "Failed to join the room", - "Failed to kick": "Failed to kick", - "Failed to leave room": "Failed to leave room", - "Failed to load timeline position": "Failed to load timeline position", - "Failed to lookup current room": "Failed to lookup current room", - "Failed to mute user": "Failed to mute user", - "Failed to register as guest:": "Failed to register as guest:", - "Failed to reject invite": "Failed to reject invite", - "Failed to reject invitation": "Failed to reject invitation", - "Failed to save settings": "Failed to save settings", - "Failed to send email": "Failed to send email", - "Failed to send request.": "Failed to send request.", - "Failed to set avatar.": "Failed to set avatar.", - "Failed to set display name": "Failed to set display name", - "Failed to set up conference call": "Failed to set up conference call", - "Failed to toggle moderator status": "Failed to toggle moderator status", - "Failed to unban": "Failed to unban", - "Failed to upload file": "Failed to upload file", - "Failed to upload profile picture!": "Failed to upload profile picture!", - "Failed to verify email address: make sure you clicked the link in the email": "Failed to verify email address: make sure you clicked the link in the email", - "Failure to create room": "Failure to create room", - "Favourite": "Favourite", - "favourite": "favourite", - "Favourites": "Favourites", - "Fill screen": "Fill screen", - "Filter room members": "Filter room members", - "Forget room": "Forget room", - "Forgot your password?": "Forgot your password?", - "For security, this session has been signed out. Please sign in again.": "For security, this session has been signed out. Please sign in again.", - "For security, logging out will delete any end-to-end encryption keys from this browser. If you want to be able to decrypt your conversation history from future Riot sessions, please export your room keys for safe-keeping.": "For security, logging out will delete any end-to-end encryption keys from this browser. If you want to be able to decrypt your conversation history from future Riot sessions, please export your room keys for safe-keeping.", - "Found a bug?": "Found a bug?", - "%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s": "%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s", - "Guest access is disabled on this Home Server.": "Guest access is disabled on this Home Server.", - "Guests can't set avatars. Please register.": "Guests can't set avatars. Please register.", - "Guest users can't create new rooms. Please register to create room and start a chat.": "Guest users can't create new rooms. Please register to create room and start a chat.", - "Guest users can't upload files. Please register to upload.": "Guest users can't upload files. Please register to upload.", - "Guests can't use labs features. Please register.": "Guests can't use labs features. Please register.", - "Guests cannot join this room even if explicitly invited.": "Guests cannot join this room even if explicitly invited.", - "had": "had", - "Hangup": "Hangup", - "Hide read receipts": "Hide read receipts", - "Hide Text Formatting Toolbar": "Hide Text Formatting Toolbar", - "Historical": "Historical", - "Home": "Home", - "Homeserver is": "Homeserver is", - "Identity Server is": "Identity Server is", - "I have verified my email address": "I have verified my email address", - "Import": "Import", - "Import E2E room keys": "Import E2E room keys", - "Incoming call from %(name)s": "Incoming call from %(name)s", - "Incoming video call from %(name)s": "Incoming video call from %(name)s", - "Incoming voice call from %(name)s": "Incoming voice call from %(name)s", - "Incorrect username and/or password.": "Incorrect username and/or password.", - "Incorrect verification code": "Incorrect verification code", - "Interface Language": "Interface Language", - "Invalid alias format": "Invalid alias format", - "Invalid address format": "Invalid address format", - "Invalid Email Address": "Invalid Email Address", - "Invalid file%(extra)s": "Invalid file%(extra)s", - "%(senderName)s invited %(targetName)s.": "%(senderName)s invited %(targetName)s.", - "Invite new room members": "Invite new room members", - "Invited": "Invited", - "Invites": "Invites", - "Invites user with given id to current room": "Invites user with given id to current room", - "'%(alias)s' is not a valid format for an address": "'%(alias)s' is not a valid format for an address", - "'%(alias)s' is not a valid format for an alias": "'%(alias)s' is not a valid format for an alias", - "%(displayName)s is typing": "%(displayName)s is typing", - "Sign in with": "Sign in with", - "Join as voice or video.": "Join as voice or video.", - "Join Room": "Join Room", - "joined and left": "joined and left", - "joined": "joined", - "%(targetName)s joined the room.": "%(targetName)s joined the room.", - "Joins room with given alias": "Joins room with given alias", - "Jump to first unread message.": "Jump to first unread message.", - "%(senderName)s kicked %(targetName)s.": "%(senderName)s kicked %(targetName)s.", - "Kick": "Kick", - "Kicks user with given id": "Kicks user with given id", - "Labs": "Labs", - "Last seen": "Last seen", - "Leave room": "Leave room", - "left and rejoined": "left and rejoined", - "left": "left", - "%(targetName)s left the room.": "%(targetName)s left the room.", - "Level:": "Level:", - "List this room in %(domain)s's room directory?": "List this room in %(domain)s's room directory?", - "Local addresses for this room:": "Local addresses for this room:", - "Logged in as:": "Logged in as:", - "Login as guest": "Login as guest", - "Logout": "Logout", - "Low priority": "Low priority", - "%(senderName)s made future room history visible to": "%(senderName)s made future room history visible to", - "Manage Integrations": "Manage Integrations", - "Markdown is disabled": "Markdown is disabled", - "Markdown is enabled": "Markdown is enabled", - "matrix-react-sdk version:": "matrix-react-sdk version:", - "Members only": "Members only", - "Message not sent due to unknown devices being present": "Message not sent due to unknown devices being present", - "Missing room_id in request": "Missing room_id in request", - "Missing user_id in request": "Missing user_id in request", - "Mobile phone number": "Mobile phone number", - "Mobile phone number (optional)": "Mobile phone number (optional)", - "Moderator": "Moderator", - "Must be viewing a room": "Must be viewing a room", - "Mute": "Mute", - "my Matrix ID": "my Matrix ID", - "Name": "Name", - "Never send encrypted messages to unverified devices from this device": "Never send encrypted messages to unverified devices from this device", - "Never send encrypted messages to unverified devices in this room": "Never send encrypted messages to unverified devices in this room", - "Never send encrypted messages to unverified devices in this room from this device": "Never send encrypted messages to unverified devices in this room from this device", - "New address (e.g. #foo:%(localDomain)s)": "New address (e.g. #foo:%(localDomain)s)", - "New Composer & Autocomplete": "New Composer & Autocomplete", - "New password": "New password", - "New passwords don't match": "New passwords don't match", - "New passwords must match each other.": "New passwords must match each other.", - "none": "none", - "not set": "not set", - "not specified": "not specified", - "Notifications": "Notifications", - "(not supported by this browser)": "(not supported by this browser)", - "": "", - "NOT verified": "NOT verified", - "No devices with registered encryption keys": "No devices with registered encryption keys", - "No display name": "No display name", - "No more results": "No more results", - "No results": "No results", - "No users have specific privileges in this room": "No users have specific privileges in this room", - "OK": "OK", - "olm version:": "olm version:", - "Once encryption is enabled for a room it cannot be turned off again (for now)": "Once encryption is enabled for a room it cannot be turned off again (for now)", - "Once you've followed the link it contains, click below": "Once you've followed the link it contains, click below", - "Only people who have been invited": "Only people who have been invited", - "Operation failed": "Operation failed", - "Otherwise, click here to send a bug report.": "Otherwise, click here to send a bug report.", - "Password": "Password", - "Password:": "Password:", - "Passwords can't be empty": "Passwords can't be empty", - "People": "People", - "Permissions": "Permissions", - "Phone": "Phone", - "%(senderName)s placed a %(callType)s call.": "%(senderName)s placed a %(callType)s call.", - "Please check your email and click on the link it contains. Once this is done, click continue.": "Please check your email and click on the link it contains. Once this is done, click continue.", - "Please Register": "Please Register", - "Power level must be positive integer.": "Power level must be positive integer.", - "Press": "Press", - "Press to start a chat with someone": "Press to start a chat with someone", - "Privacy warning": "Privacy warning", - "Private Chat": "Private Chat", - "Privileged Users": "Privileged Users", - "Profile": "Profile", - "Public Chat": "Public Chat", - "Reason": "Reason", - "Reason: %(reasonText)s": "Reason: %(reasonText)s", - "Revoke Moderator": "Revoke Moderator", - "Refer a friend to Riot:": "Refer a friend to Riot:", - "Register": "Register", - "rejected": "rejected", - "%(targetName)s rejected the invitation.": "%(targetName)s rejected the invitation.", - "Reject invitation": "Reject invitation", - "Rejoin": "Rejoin", - "Remote addresses for this room:": "Remote addresses for this room:", - "Remove Contact Information?": "Remove Contact Information?", - "%(senderName)s removed their display name (%(oldDisplayName)s).": "%(senderName)s removed their display name (%(oldDisplayName)s).", - "%(senderName)s removed their profile picture.": "%(senderName)s removed their profile picture.", - "Remove": "Remove", - "Remove %(threePid)s?": "Remove %(threePid)s?", - "%(senderName)s requested a VoIP conference.": "%(senderName)s requested a VoIP conference.", - "Report it": "Report it", - "Resetting password will currently reset any end-to-end encryption keys on all devices, making encrypted chat history unreadable, unless you first export your room keys and re-import them afterwards. In future this will be improved.": "Resetting password will currently reset any end-to-end encryption keys on all devices, making encrypted chat history unreadable, unless you first export your room keys and re-import them afterwards. In future this will be improved.", - "restore": "restore", - "Results from DuckDuckGo": "Results from DuckDuckGo", - "Return to app": "Return to app", - "Return to login screen": "Return to login screen", - "Riot does not have permission to send you notifications - please check your browser settings": "Riot does not have permission to send you notifications - please check your browser settings", - "Riot was not given permission to send notifications - please try again": "Riot was not given permission to send notifications - please try again", - "riot-web version:": "riot-web version:", - "Room %(roomId)s not visible": "Room %(roomId)s not visible", - "Room Colour": "Room Colour", - "Room contains unknown devices": "Room contains unknown devices", - "Room name (optional)": "Room name (optional)", - "%(roomName)s does not exist.": "%(roomName)s does not exist.", - "%(roomName)s is not accessible at this time.": "%(roomName)s is not accessible at this time.", - "Rooms": "Rooms", - "Save": "Save", - "Scroll to bottom of page": "Scroll to bottom of page", - "Scroll to unread messages": "Scroll to unread messages", - "Search": "Search", - "Search failed": "Search failed", - "Searches DuckDuckGo for results": "Searches DuckDuckGo for results", - "Searching known users": "Searching known users", - "Seen by %(userName)s at %(dateTime)s": "Seen by %(userName)s at %(dateTime)s", - "Send a message (unencrypted)": "Send a message (unencrypted)", - "Send an encrypted message": "Send an encrypted message", - "Send anyway": "Send anyway", - "Sender device information": "Sender device information", - "Send Invites": "Send Invites", - "Send Reset Email": "Send Reset Email", - "sent an image": "sent an image", - "%(senderDisplayName)s sent an image.": "%(senderDisplayName)s sent an image.", - "%(senderName)s sent an invitation to %(targetDisplayName)s to join the room.": "%(senderName)s sent an invitation to %(targetDisplayName)s to join the room.", - "sent a video": "sent a video", - "Server error": "Server error", - "Server may be unavailable or overloaded": "Server may be unavailable or overloaded", - "Server may be unavailable, overloaded, or search timed out :(": "Server may be unavailable, overloaded, or search timed out :(", - "Server may be unavailable, overloaded, or the file too big": "Server may be unavailable, overloaded, or the file too big", - "Server may be unavailable, overloaded, or you hit a bug.": "Server may be unavailable, overloaded, or you hit a bug.", - "Server unavailable, overloaded, or something else went wrong.": "Server unavailable, overloaded, or something else went wrong.", - "Session ID": "Session ID", - "%(senderName)s set a profile picture.": "%(senderName)s set a profile picture.", - "%(senderName)s set their display name to %(displayName)s.": "%(senderName)s set their display name to %(displayName)s.", - "Set": "Set", - "Settings": "Settings", - "Show panel": "Show panel", - "Show Text Formatting Toolbar": "Show Text Formatting Toolbar", - "Show timestamps in 12 hour format (e.g. 2:30pm)": "Show timestamps in 12 hour format (e.g. 2:30pm)", - "Signed Out": "Signed Out", - "Sign in": "Sign in", - "Sign out": "Sign out", - "since the point in time of selecting this option": "since the point in time of selecting this option", - "since they joined": "since they joined", - "since they were invited": "since they were invited", - "Some of your messages have not been sent.": "Some of your messages have not been sent.", - "Someone": "Someone", - "Sorry, this homeserver is using a login which is not recognised ": "Sorry, this homeserver is using a login which is not recognised ", - "Start a chat": "Start a chat", - "Start authentication": "Start authentication", - "Start Chat": "Start Chat", - "Submit": "Submit", - "Success": "Success", - "tag as %(tagName)s": "tag as %(tagName)s", - "tag direct chat": "tag direct chat", - "Tagged as: ": "Tagged as: ", - "The default role for new room members is": "The default role for new room members is", - "The main address for this room is": "The main address for this room is", - "The phone number entered looks invalid": "The phone number entered looks invalid", - "The signing key you provided matches the signing key you received from %(userId)s's device %(deviceId)s. Device marked as verified.": "The signing key you provided matches the signing key you received from %(userId)s's device %(deviceId)s. Device marked as verified.", - "This action cannot be performed by a guest user. Please register to be able to do this.": "This action cannot be performed by a guest user. Please register to be able to do this.", - "This email address is already in use": "This email address is already in use", - "This email address was not found": "This email address was not found", - "%(actionVerb)s this person?": "%(actionVerb)s this person?", - "The email address linked to your account must be entered.": "The email address linked to your account must be entered.", - "The file '%(fileName)s' exceeds this home server's size limit for uploads": "The file '%(fileName)s' exceeds this home server's size limit for uploads", - "The file '%(fileName)s' failed to upload": "The file '%(fileName)s' failed to upload", - "The remote side failed to pick up": "The remote side failed to pick up", - "This Home Server does not support login using email address.": "This Home Server does not support login using email address.", - "This invitation was sent to an email address which is not associated with this account:": "This invitation was sent to an email address which is not associated with this account:", - "There was a problem logging in.": "There was a problem logging in.", - "This room has no local addresses": "This room has no local addresses", - "This room is not recognised.": "This room is not recognised.", - "These are experimental features that may break in unexpected ways": "These are experimental features that may break in unexpected ways", - "The visibility of existing history will be unchanged": "The visibility of existing history will be unchanged", - "This doesn't appear to be a valid email address": "This doesn't appear to be a valid email address", - "This is a preview of this room. Room interactions have been disabled": "This is a preview of this room. Room interactions have been disabled", - "This phone number is already in use": "This phone number is already in use", - "This room": "This room", - "This room is not accessible by remote Matrix servers": "This room is not accessible by remote Matrix servers", - "This room's internal ID is": "This room's internal ID is", - "times": "times", - "To ban users": "To ban users", - "to browse the directory": "to browse the directory", - "To configure the room": "To configure the room", - "to demote": "to demote", - "to favourite": "to favourite", - "To invite users into the room": "To invite users into the room", - "To kick users": "To kick users", - "To link to a room it must have an address.": "To link to a room it must have an address.", - "to make a room or": "to make a room or", - "To remove other users' messages": "To remove other users' messages", - "To reset your password, enter the email address linked to your account": "To reset your password, enter the email address linked to your account", - "to restore": "to restore", - "To send events of type": "To send events of type", - "To send messages": "To send messages", - "to start a chat with someone": "to start a chat with someone", - "to tag as %(tagName)s": "to tag as %(tagName)s", - "to tag direct chat": "to tag direct chat", - "To use it, just wait for autocomplete results to load and tab through them.": "To use it, just wait for autocomplete results to load and tab through them.", - "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.": "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.", - "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.", - "Turn Markdown off": "Turn Markdown off", - "Turn Markdown on": "Turn Markdown on", - "%(senderName)s turned on end-to-end encryption (algorithm %(algorithm)s).": "%(senderName)s turned on end-to-end encryption (algorithm %(algorithm)s).", - "Unable to add email address": "Unable to add email address", - "Unable to remove contact information": "Unable to remove contact information", - "Unable to restore previous session": "Unable to restore previous session", - "Unable to verify email address.": "Unable to verify email address.", - "Unban": "Unban", - "Unbans user with given id": "Unbans user with given id", - "%(senderName)s unbanned %(targetName)s.": "%(senderName)s unbanned %(targetName)s.", - "Unable to ascertain that the address this invite was sent to matches one associated with your account.": "Unable to ascertain that the address this invite was sent to matches one associated with your account.", - "Unable to capture screen": "Unable to capture screen", - "Unable to enable Notifications": "Unable to enable Notifications", - "Unable to load device list": "Unable to load device list", - "Undecryptable": "Undecryptable", - "Unencrypted room": "Unencrypted room", - "unencrypted": "unencrypted", - "Unencrypted message": "Unencrypted message", - "unknown caller": "unknown caller", - "Unknown command": "Unknown command", - "unknown device": "unknown device", - "unknown error code": "unknown error code", - "Unknown room %(roomId)s": "Unknown room %(roomId)s", - "Unknown (user, device) pair:": "Unknown (user, device) pair:", - "unknown": "unknown", - "Unmute": "Unmute", - "Unnamed Room": "Unnamed Room", - "Unrecognised command:": "Unrecognised command:", - "Unrecognised room alias:": "Unrecognised room alias:", - "Unverified": "Unverified", - "Uploading %(filename)s and %(count)s others": { - "zero": "Uploading %(filename)s", - "one": "Uploading %(filename)s and %(count)s other", - "other": "Uploading %(filename)s and %(count)s others" - }, - "uploaded a file": "uploaded a file", - "Upload avatar": "Upload avatar", - "Upload Failed": "Upload Failed", - "Upload Files": "Upload Files", - "Upload file": "Upload file", - "Upload new:": "Upload new:", - "Usage": "Usage", - "Use compact timeline layout": "Use compact timeline layout", - "Use with caution": "Use with caution", - "User ID": "User ID", - "User Interface": "User Interface", - "%(user)s is a": "%(user)s is a", - "User name": "User name", - "%(userName)s (power %(powerLevelNumber)s)": "%(userName)s (power %(powerLevelNumber)s)", - "Username invalid: %(errMessage)s": "Username invalid: %(errMessage)s", - "Users": "Users", - "User": "User", - "Verification Pending": "Verification Pending", - "Verification": "Verification", - "verified": "verified", - "Verified": "Verified", - "Verified key": "Verified key", - "Video call": "Video call", - "Voice call": "Voice call", - "VoIP conference finished.": "VoIP conference finished.", - "VoIP conference started.": "VoIP conference started.", - "VoIP is unsupported": "VoIP is unsupported", - "(could not connect media)": "(could not connect media)", - "(no answer)": "(no answer)", - "(unknown failure: %(reason)s)": "(unknown failure: %(reason)s)", - "(warning: cannot be disabled again!)": "(warning: cannot be disabled again!)", - "Warning!": "Warning!", - "WARNING: Device already verified, but keys do NOT MATCH!": "WARNING: Device already verified, but keys do NOT MATCH!", - "WARNING: KEY VERIFICATION FAILED! The signing key for %(userId)s and device %(deviceId)s is \"%(fprint)s\" which does not match the provided key \"%(fingerprint)s\". This could mean your communications are being intercepted!": "WARNING: KEY VERIFICATION FAILED! The signing key for %(userId)s and device %(deviceId)s is \"%(fprint)s\" which does not match the provided key \"%(fingerprint)s\". This could mean your communications are being intercepted!", - "Who can access this room?": "Who can access this room?", - "Who can read history?": "Who can read history?", - "Who would you like to add to this room?": "Who would you like to add to this room?", - "Who would you like to communicate with?": "Who would you like to communicate with?", - "%(senderName)s withdrew %(targetName)s's invitation.": "%(senderName)s withdrew %(targetName)s's invitation.", - "Would you like to accept or decline this invitation?": "Would you like to accept or decline this invitation?", - "You already have existing direct chats with this user:": "You already have existing direct chats with this user:", - "You are already in a call.": "You are already in a call.", - "You're not in any rooms yet! Press to make a room or to browse the directory": "You're not in any rooms yet! Press to make a room or to browse the directory", - "You are trying to access %(roomName)s.": "You are trying to access %(roomName)s.", - "You cannot place a call with yourself.": "You cannot place a call with yourself.", - "You cannot place VoIP calls in this browser.": "You cannot place VoIP calls in this browser.", - "You do not have permission to post to this room": "You do not have permission to post to this room", - "You have been banned from %(roomName)s by %(userName)s.": "You have been banned from %(roomName)s by %(userName)s.", - "You have been invited to join this room by %(inviterName)s": "You have been invited to join this room by %(inviterName)s", - "You have been kicked from %(roomName)s by %(userName)s.": "You have been kicked from %(roomName)s by %(userName)s.", - "You have been logged out of all devices and will no longer receive push notifications. To re-enable notifications, sign in again on each device": "You have been logged out of all devices and will no longer receive push notifications. To re-enable notifications, sign in again on each device", - "You have disabled URL previews by default.": "You have disabled URL previews by default.", - "You have enabled URL previews by default.": "You have enabled URL previews by default.", - "You have entered an invalid contact. Try using their Matrix ID or email address.": "You have entered an invalid contact. Try using their Matrix ID or email address.", - "You have no visible notifications": "You have no visible notifications", - "You may wish to login with a different account, or add this email to this account.": "You may wish to login with a different account, or add this email to this account.", - "you must be a": "you must be a", - "You must register to use this functionality": "You must register to use this functionality", - "You need to be able to invite users to do that.": "You need to be able to invite users to do that.", - "You need to be logged in.": "You need to be logged in.", - "You need to enter a user name.": "You need to enter a user name.", - "Your email address does not appear to be associated with a Matrix ID on this Homeserver.": "Your email address does not appear to be associated with a Matrix ID on this Homeserver.", - "Your password has been reset": "Your password has been reset", - "Your password was successfully changed. You will not receive push notifications on other devices until you log back in to them": "Your password was successfully changed. You will not receive push notifications on other devices until you log back in to them", - "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?", - "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 should not yet trust it to secure data": "You should not yet trust it to secure data", - "You will not be able to undo this change as you are promoting the user to have the same power level as yourself.": "You will not be able to undo this change as you are promoting the user to have the same power level as yourself.", - "Your home server does not support device management.": "Your home server does not support device management.", - "Sun": "Sun", - "Mon": "Mon", - "Tue": "Tue", - "Wed": "Wed", - "Thu": "Thu", - "Fri": "Fri", - "Sat": "Sat", - "Jan": "Jan", - "Feb": "Feb", - "Mar": "Mar", - "Apr": "Apr", - "May": "May", - "Jun": "Jun", - "Jul": "Jul", - "Aug": "Aug", - "Sep": "Sep", - "Oct": "Oct", - "Nov": "Nov", - "Dec": "Dec", - "%(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 %(time)s": "%(weekDayName)s %(time)s", - "Set a display name:": "Set a display name:", - "Set a Display Name": "Set a Display Name", - "Upload an avatar:": "Upload an avatar:", - "This server does not support authentication with a phone number.": "This server does not support authentication with a phone number.", - "Missing password.": "Missing password.", - "Passwords don't match.": "Passwords don't match.", - "Password too short (min %(MIN_PASSWORD_LENGTH)s).": "Password too short (min %(MIN_PASSWORD_LENGTH)s).", - "This doesn't look like a valid email address.": "This doesn't look like a valid email address.", - "This doesn't look like a valid phone number.": "This doesn't look like a valid phone number.", - "User names may only contain letters, numbers, dots, hyphens and underscores.": "User names may only contain letters, numbers, dots, hyphens and underscores.", - "An unknown error occurred.": "An unknown error occurred.", - "I already have an account": "I already have an account", - "An error occurred: %(error_string)s": "An error occurred: %(error_string)s", - "Topic": "Topic", - "Make Moderator": "Make Moderator", - "Make this room private": "Make this room private", - "Share message history with new users": "Share message history with new users", - "Encrypt room": "Encrypt room", - "There are no visible files in this room": "There are no visible files in this room", - "Room": "Room", - "Connectivity to the server has been lost.": "Connectivity to the server has been lost.", - "Sent messages will be stored until your connection has returned.": "Sent messages will be stored until your connection has returned.", - "Auto-complete": "Auto-complete", - "Resend all or cancel all now. You can also select individual messages to resend or cancel.": "Resend all or cancel all now. You can also select individual messages to resend or cancel.", - "(~%(count)s results)": { - "one": "(~%(count)s result)", - "other": "(~%(count)s results)" - }, - "Cancel": "Cancel", - "or": "or", - "Active call": "Active call", - "Monday": "Monday", - "Tuesday": "Tuesday", - "Wednesday": "Wednesday", - "Thursday": "Thursday", - "Friday": "Friday", - "Saturday": "Saturday", - "Sunday": "Sunday", - "bold": "bold", - "italic": "italic", - "strike": "strike", - "underline": "underline", - "code":"code", - "quote":"quote", - "bullet":"bullet", - "numbullet":"numbullet", - "%(severalUsers)sjoined %(repeats)s times": "%(severalUsers)sjoined %(repeats)s times", - "%(oneUser)sjoined %(repeats)s times": "%(oneUser)sjoined %(repeats)s times", - "%(severalUsers)sjoined": "%(severalUsers)sjoined", - "%(oneUser)sjoined": "%(oneUser)sjoined", - "%(severalUsers)sleft %(repeats)s times": "%(severalUsers)sleft %(repeats)s times", - "%(oneUser)sleft %(repeats)s times": "%(oneUser)sleft %(repeats)s times", - "%(severalUsers)sleft": "%(severalUsers)sleft", - "%(oneUser)sleft": "%(oneUser)sleft", - "%(severalUsers)sjoined and left %(repeats)s times": "%(severalUsers)sjoined and left %(repeats)s times", - "%(oneUser)sjoined and left %(repeats)s times": "%(oneUser)sjoined and left %(repeats)s times", - "%(severalUsers)sjoined and left": "%(severalUsers)sjoined and left", - "%(oneUser)sjoined and left": "%(oneUser)sjoined and left", - "%(severalUsers)sleft and rejoined %(repeats)s times": "%(severalUsers)sleft and rejoined %(repeats)s times", - "%(oneUser)sleft and rejoined %(repeats)s times": "%(oneUser)sleft and rejoined %(repeats)s times", - "%(severalUsers)sleft and rejoined": "%(severalUsers)sleft and rejoined", - "%(oneUser)sleft and rejoined": "%(oneUser)sleft and rejoined", - "%(severalUsers)srejected their invitations %(repeats)s times": "%(severalUsers)srejected their invitations %(repeats)s times", - "%(oneUser)srejected their invitation %(repeats)s times": "%(oneUser)srejected their invitation %(repeats)s times", - "%(severalUsers)srejected their invitations": "%(severalUsers)srejected their invitations", - "%(oneUser)srejected their invitation": "%(oneUser)srejected their invitation", - "%(severalUsers)shad their invitations withdrawn %(repeats)s times": "%(severalUsers)shad their invitations withdrawn %(repeats)s times", - "%(oneUser)shad their invitation withdrawn %(repeats)s times": "%(oneUser)shad their invitation withdrawn %(repeats)s times", - "%(severalUsers)shad their invitations withdrawn": "%(severalUsers)shad their invitations withdrawn", - "%(oneUser)shad their invitation withdrawn": "%(oneUser)shad their invitation withdrawn", - "were invited %(repeats)s times": "were invited %(repeats)s times", - "was invited %(repeats)s times": "was invited %(repeats)s times", - "were invited": "were invited", - "was invited": "was invited", - "were banned %(repeats)s times": "were banned %(repeats)s times", - "was banned %(repeats)s times": "was banned %(repeats)s times", - "were banned": "were banned", - "was banned": "was banned", - "were unbanned %(repeats)s times": "were unbanned %(repeats)s times", - "was unbanned %(repeats)s times": "was unbanned %(repeats)s times", - "were unbanned": "were unbanned", - "was unbanned": "was unbanned", - "were kicked %(repeats)s times": "were kicked %(repeats)s times", - "was kicked %(repeats)s times": "was kicked %(repeats)s times", - "were kicked": "were kicked", - "was kicked": "was kicked", - "%(severalUsers)schanged their name %(repeats)s times": "%(severalUsers)schanged their name %(repeats)s times", - "%(oneUser)schanged their name %(repeats)s times": "%(oneUser)schanged their name %(repeats)s times", - "%(severalUsers)schanged their name": "%(severalUsers)schanged their name", - "%(oneUser)schanged their name": "%(oneUser)schanged their name", - "%(severalUsers)schanged their avatar %(repeats)s times": "%(severalUsers)schanged their avatar %(repeats)s times", - "%(oneUser)schanged their avatar %(repeats)s times": "%(oneUser)schanged their avatar %(repeats)s times", - "%(severalUsers)schanged their avatar": "%(severalUsers)schanged their avatar", - "%(oneUser)schanged their avatar": "%(oneUser)schanged their avatar", - "Please select the destination room for this message": "Please select the destination room for this message", - "Create new room": "Create new room", - "Welcome page": "Welcome page", - "Room directory": "Room directory", - "Start chat": "Start chat", - "New Password": "New Password", - "Start automatically after system login": "Start automatically after system login", - "Desktop specific": "Desktop specific", - "Analytics": "Analytics", - "Opt out of analytics": "Opt out of analytics", - "Options": "Options", - "Riot collects anonymous analytics to allow us to improve the application.": "Riot collects anonymous analytics to allow us to improve the application.", - "Passphrases must match": "Passphrases must match", - "Passphrase must not be empty": "Passphrase must not be empty", - "Export room keys": "Export room keys", - "Confirm passphrase": "Confirm passphrase", - "Import room keys": "Import room keys", - "File to import": "File to import", - "This process allows you to export the keys for messages you have received in encrypted rooms to a local file. You will then be able to import the file into another Matrix client in the future, so that client will also be able to decrypt these messages.": "This process allows you to export the keys for messages you have received in encrypted rooms to a local file. You will then be able to import the file into another Matrix client in the future, so that client will also be able to decrypt these messages.", - "The exported file will allow anyone who can read it to decrypt any encrypted messages that you can see, so you should be careful to keep it secure. To help with this, you should enter a passphrase below, which will be used to encrypt the exported data. It will only be possible to import the data by using the same passphrase.": "The exported file will allow anyone who can read it to decrypt any encrypted messages that you can see, so you should be careful to keep it secure. To help with this, you should enter a passphrase below, which will be used to encrypt the exported data. It will only be possible to import the data by using the same passphrase.", - "This process allows you to import encryption keys that you had previously exported from another Matrix client. You will then be able to decrypt any messages that the other client could decrypt.": "This process allows you to import encryption keys that you had previously exported from another Matrix client. You will then be able to decrypt any messages that the other client could decrypt.", - "The export file will be protected with a passphrase. You should enter the passphrase here, to decrypt the file.": "The export file will be protected with a passphrase. You should enter the passphrase here, to decrypt the file.", - "You must join the room to see its files": "You must join the room to see its files", - "Reject all %(invitedRooms)s invites": "Reject all %(invitedRooms)s invites", - "Start new chat": "Start new chat", - "Guest users can't invite users. Please register.": "Guest users can't invite users. Please register.", - "Failed to invite": "Failed to invite", - "Failed to invite user": "Failed to invite user", - "Failed to invite the following users to the %(roomName)s room:": "Failed to invite the following users to the %(roomName)s room:", - "Confirm Removal": "Confirm Removal", - "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.": "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.", - "Unknown error": "Unknown error", - "Incorrect password": "Incorrect password", - "This will make your account permanently unusable. You will not be able to re-register the same user ID.": "This will make your account permanently unusable. You will not be able to re-register the same user ID.", - "This action is irreversible.": "This action is irreversible.", - "To continue, please enter your password.": "To continue, please enter your password.", - "To verify that this device can be trusted, please contact its owner using some other means (e.g. in person or a phone call) and ask them whether the key they see in their User Settings for this device matches the key below:": "To verify that this device can be trusted, please contact its owner using some other means (e.g. in person or a phone call) and ask them whether the key they see in their User Settings for this device matches the key below:", - "Device name": "Device name", - "Device Name": "Device Name", - "Device key": "Device key", - "If it matches, press the verify button below. If it doesn't, then someone else is intercepting this device and you probably want to press the blacklist button instead.": "If it matches, press the verify button below. If it doesn't, then someone else is intercepting this device and you probably want to press the blacklist button instead.", - "In future this verification process will be more sophisticated.": "In future this verification process will be more sophisticated.", - "Verify device": "Verify device", - "I verify that the keys match": "I verify that the keys match", - "We encountered an error trying to restore your previous session. If you continue, you will need to log in again, and encrypted chat history will be unreadable.": "We encountered an error trying to restore your previous session. If you continue, you will need to log in again, and encrypted chat history will be unreadable.", - "Unable to restore session": "Unable to restore session", - "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.": "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.", - "Continue anyway": "Continue anyway", - "Your display name is how you'll appear to others when you speak in rooms. What would you like it to be?": "Your display name is how you'll appear to others when you speak in rooms. What would you like it to be?", - "You are currently blacklisting unverified devices; to send messages to these devices you must verify them.": "You are currently blacklisting unverified devices; to send messages to these devices you must verify them.", - "We recommend you go through the verification process for each device to confirm they belong to their legitimate owner, but you can resend the message without verifying if you prefer.": "We recommend you go through the verification process for each device to confirm they belong to their legitimate owner, but you can resend the message without verifying if you prefer.", - "\"%(RoomName)s\" contains devices that you haven't seen before.": "\"%(RoomName)s\" contains devices that you haven't seen before.", - "Unknown devices": "Unknown devices", - "Unknown Address": "Unknown Address", - "Unblacklist": "Unblacklist", - "Blacklist": "Blacklist", - "Unverify": "Unverify", - "Verify...": "Verify...", - "ex. @bob:example.com": "ex. @bob:example.com", - "Add User": "Add User", - "This Home Server would like to make sure you are not a robot": "This Home Server would like to make sure you are not a robot", - "Sign in with CAS": "Sign in with CAS", - "Custom Server Options": "Custom Server Options", - "You can use the custom server options to sign into other Matrix servers by specifying a different Home server URL.": "You can use the custom server options to sign into other Matrix servers by specifying a different Home server URL.", - "This allows you to use this app with an existing Matrix account on a different home server.": "This allows you to use this app with an existing Matrix account on a different home server.", - "You can also set a custom identity server but this will typically prevent interaction with users based on email address.": "You can also set a custom identity server but this will typically prevent interaction with users based on email address.", - "Dismiss": "Dismiss", - "Please check your email to continue registration.": "Please check your email to continue registration.", - "Token incorrect": "Token incorrect", - "A text message has been sent to": "A text message has been sent to", - "Please enter the code it contains:": "Please enter the code it contains:", - "powered by Matrix": "powered by Matrix", - "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?", - "You are registering with %(SelectedTeamName)s": "You are registering with %(SelectedTeamName)s", - "Default server": "Default server", - "Custom server": "Custom server", - "Home server URL": "Home server URL", - "Identity server URL": "Identity server URL", - "What does this mean?": "What does this mean?", - "Error decrypting audio": "Error decrypting audio", - "Error decrypting image": "Error decrypting image", - "Image '%(Body)s' cannot be displayed.": "Image '%(Body)s' cannot be displayed.", - "This image cannot be displayed.": "This image cannot be displayed.", - "Error decrypting video": "Error decrypting video", - "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?", - "Removed or unknown message type": "Removed or unknown message type", - "Disable URL previews by default for participants in this room": "Disable URL previews by default for participants in this room", - "Disable URL previews for this room (affects only you)": "Disable URL previews for this room (affects only you)", - "URL previews are %(globalDisableUrlPreview)s by default for participants in this room.": "URL previews are %(globalDisableUrlPreview)s by default for participants in this room.", - "URL Previews": "URL Previews", - "Enable URL previews for this room (affects only you)": "Enable URL previews for this room (affects only you)", - "Drop file here to upload": "Drop file here to upload", - " (unsupported)": " (unsupported)", - "Ongoing conference call%(supportedText)s.": "Ongoing conference call%(supportedText)s.", - "for %(amount)ss": "for %(amount)ss", - "for %(amount)sm": "for %(amount)sm", - "for %(amount)sh": "for %(amount)sh", - "for %(amount)sd": "for %(amount)sd", - "Online": "Online", - "Idle": "Idle", - "Offline": "Offline", - "Updates": "Updates", - "Check for update": "Check for update", - "Start chatting": "Start chatting", - "Start Chatting": "Start Chatting", - "Click on the button below to start chatting!": "Click on the button below to start chatting!", - "$senderDisplayName changed the room avatar to ": "$senderDisplayName changed the room avatar to ", - "%(senderDisplayName)s removed the room avatar.": "%(senderDisplayName)s removed the room avatar.", - "%(senderDisplayName)s changed the avatar for %(roomName)s": "%(senderDisplayName)s changed the avatar for %(roomName)s", - "Username available": "Username available", - "Username not available": "Username not available", - "Something went wrong!": "Something went wrong!", - "This will be your account name on the homeserver, or you can pick a different server.": "This will be your account name on the homeserver, or you can pick a different server.", - "If you already have a Matrix account you can log in instead.": "If you already have a Matrix account you can log in instead.", - "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?", - "Disable Peer-to-Peer for 1:1 calls": "Disable Peer-to-Peer for 1:1 calls", - "Do you want to set an email address?": "Do you want to set an email address?", - "This will allow you to reset your password and receive notifications.": "This will allow you to reset your password and receive notifications.", - "To return to your account in future you need to set a password": "To return to your account in future you need to set a password", - "Skip":"Skip", - "Start verification": "Start verification", - "Share without verifying": "Share without verifying", - "Ignore request": "Ignore request", - "You added a new device '%(displayName)s', which is requesting encryption keys.": "You added a new device '%(displayName)s', which is requesting encryption keys.", - "Your unverified device '%(displayName)s' is requesting encryption keys.": "Your unverified device '%(displayName)s' is requesting encryption keys.", - "Encryption key request": "Encryption key request", - "Autocomplete Delay (ms):": "Autocomplete Delay (ms):", - "This Home server does not support groups": "This Home server does not support groups", - "Loading device info...": "Loading device info..." + "Add a widget": "Add a widget", + "af": "Afrikaans", + "ar-ae": "Arabic (U.A.E.)", + "ar-bh": "Arabic (Bahrain)", + "ar-dz": "Arabic (Algeria)", + "ar-eg": "Arabic (Egypt)", + "ar-iq": "Arabic (Iraq)", + "ar-jo": "Arabic (Jordan)", + "ar-kw": "Arabic (Kuwait)", + "ar-lb": "Arabic (Lebanon)", + "ar-ly": "Arabic (Libya)", + "ar-ma": "Arabic (Morocco)", + "ar-om": "Arabic (Oman)", + "ar-qa": "Arabic (Qatar)", + "ar-sa": "Arabic (Saudi Arabia)", + "ar-sy": "Arabic (Syria)", + "ar-tn": "Arabic (Tunisia)", + "ar-ye": "Arabic (Yemen)", + "be": "Belarusian", + "bg": "Bulgarian", + "ca": "Catalan", + "cs": "Czech", + "da": "Danish", + "de-at": "German (Austria)", + "de-ch": "German (Switzerland)", + "de": "German", + "de-li": "German (Liechtenstein)", + "de-lu": "German (Luxembourg)", + "el": "Greek", + "en-au": "English (Australia)", + "en-bz": "English (Belize)", + "en-ca": "English (Canada)", + "en": "English", + "en-gb": "English (United Kingdom)", + "en-ie": "English (Ireland)", + "en-jm": "English (Jamaica)", + "en-nz": "English (New Zealand)", + "en-tt": "English (Trinidad)", + "en-us": "English (United States)", + "en-za": "English (South Africa)", + "es-ar": "Spanish (Argentina)", + "es-bo": "Spanish (Bolivia)", + "es-cl": "Spanish (Chile)", + "es-co": "Spanish (Colombia)", + "es-cr": "Spanish (Costa Rica)", + "es-do": "Spanish (Dominican Republic)", + "es-ec": "Spanish (Ecuador)", + "es-gt": "Spanish (Guatemala)", + "es-hn": "Spanish (Honduras)", + "es-mx": "Spanish (Mexico)", + "es-ni": "Spanish (Nicaragua)", + "es-pa": "Spanish (Panama)", + "es-pe": "Spanish (Peru)", + "es-pr": "Spanish (Puerto Rico)", + "es-py": "Spanish (Paraguay)", + "es": "Spanish (Spain)", + "es-sv": "Spanish (El Salvador)", + "es-uy": "Spanish (Uruguay)", + "es-ve": "Spanish (Venezuela)", + "et": "Estonian", + "eu": "Basque (Basque)", + "fa": "Farsi", + "fi": "Finnish", + "fo": "Faeroese", + "fr-be": "French (Belgium)", + "fr-ca": "French (Canada)", + "fr-ch": "French (Switzerland)", + "fr": "French", + "fr-lu": "French (Luxembourg)", + "ga": "Irish", + "gd": "Gaelic (Scotland)", + "he": "Hebrew", + "hi": "Hindi", + "hr": "Croatian", + "hu": "Hungarian", + "id": "Indonesian", + "is": "Icelandic", + "it-ch": "Italian (Switzerland)", + "it": "Italian", + "ja": "Japanese", + "ji": "Yiddish", + "ko": "Korean", + "lt": "Lithuanian", + "lv": "Latvian", + "mk": "Macedonian (FYROM)", + "ms": "Malaysian", + "mt": "Maltese", + "nl-be": "Dutch (Belgium)", + "nl": "Dutch", + "no": "Norwegian", + "pl": "Polish", + "pt-br": "Brazilian Portuguese", + "pt": "Portuguese", + "rm": "Rhaeto-Romanic", + "ro-mo": "Romanian (Republic of Moldova)", + "ro": "Romanian", + "ru-mo": "Russian (Republic of Moldova)", + "ru": "Russian", + "sb": "Sorbian", + "sk": "Slovak", + "sl": "Slovenian", + "sq": "Albanian", + "sr": "Serbian", + "sv-fi": "Swedish (Finland)", + "sv": "Swedish", + "sx": "Sutu", + "sz": "Sami (Lappish)", + "th": "Thai", + "tn": "Tswana", + "tr": "Turkish", + "ts": "Tsonga", + "uk": "Ukrainian", + "ur": "Urdu", + "ve": "Venda", + "vi": "Vietnamese", + "xh": "Xhosa", + "zh-cn": "Chinese (PRC)", + "zh-hk": "Chinese (Hong Kong SAR)", + "zh-sg": "Chinese (Singapore)", + "zh-tw": "Chinese (Taiwan)", + "zu": "Zulu", + "a room": "a room", + "A text message has been sent to +%(msisdn)s. Please enter the verification code it contains": "A text message has been sent to +%(msisdn)s. Please enter the verification code it contains", + "Accept": "Accept", + "%(targetName)s accepted an invitation.": "%(targetName)s accepted an invitation.", + "%(targetName)s accepted the invitation for %(displayName)s.": "%(targetName)s accepted the invitation for %(displayName)s.", + "Account": "Account", + "Access Token:": "Access Token:", + "Active call (%(roomName)s)": "Active call (%(roomName)s)", + "Add": "Add", + "Add a topic": "Add a topic", + "Add email address": "Add email address", + "Add phone number": "Add phone number", + "Admin": "Admin", + "Admin tools": "Admin tools", + "And %(count)s more...": "And %(count)s more...", + "VoIP": "VoIP", + "Missing Media Permissions, click here to request.": "Missing Media Permissions, click here to request.", + "No Microphones detected": "No Microphones detected", + "No Webcams detected": "No Webcams detected", + "No media permissions": "No media permissions", + "You may need to manually permit Riot to access your microphone/webcam": "You may need to manually permit Riot to access your microphone/webcam", + "Default Device": "Default Device", + "Microphone": "Microphone", + "Camera": "Camera", + "Advanced": "Advanced", + "Algorithm": "Algorithm", + "Hide removed messages": "Hide removed messages", + "Always show message timestamps": "Always show message timestamps", + "Authentication": "Authentication", + "Alias (optional)": "Alias (optional)", + "all room members": "all room members", + "all room members, from the point they are invited": "all room members, from the point they are invited", + "all room members, from the point they joined": "all room members, from the point they joined", + "and": "and", + "%(items)s and %(remaining)s others": "%(items)s and %(remaining)s others", + "%(items)s and one other": "%(items)s and one other", + "%(items)s and %(lastItem)s": "%(items)s and %(lastItem)s", + "and %(overflowCount)s others...": "and %(overflowCount)s others...", + "and one other...": "and one other...", + "%(names)s and %(lastPerson)s are typing": "%(names)s and %(lastPerson)s are typing", + "%(names)s and one other are typing": "%(names)s and one other are typing", + "%(names)s and %(count)s others are typing": "%(names)s and %(count)s others are typing", + "An email has been sent to": "An email has been sent to", + "A new password must be entered.": "A new password must be entered.", + "%(senderName)s answered the call.": "%(senderName)s answered the call.", + "anyone": "anyone", + "An error has occurred.": "An error has occurred.", + "Anyone": "Anyone", + "Anyone who knows the room's link, apart from guests": "Anyone who knows the room's link, apart from guests", + "Anyone who knows the room's link, including guests": "Anyone who knows the room's link, including guests", + "Are you sure?": "Are you sure?", + "Are you sure you want to leave the room '%(roomName)s'?": "Are you sure you want to leave the room '%(roomName)s'?", + "Are you sure you want to reject the invitation?": "Are you sure you want to reject the invitation?", + "Are you sure you want to upload the following files?": "Are you sure you want to upload the following files?", + "Attachment": "Attachment", + "Autoplay GIFs and videos": "Autoplay GIFs and videos", + "%(senderName)s banned %(targetName)s.": "%(senderName)s banned %(targetName)s.", + "Ban": "Ban", + "Banned users": "Banned users", + "Bans user with given id": "Bans user with given id", + "Blacklisted": "Blacklisted", + "Bug Report": "Bug Report", + "Bulk Options": "Bulk Options", + "Call Timeout": "Call Timeout", + "Can't connect to homeserver - please check your connectivity, ensure your homeserver's SSL certificate is trusted, and that a browser extension is not blocking requests.": "Can't connect to homeserver - please check your connectivity, ensure your homeserver's SSL certificate is trusted, and that a browser extension is not blocking requests.", + "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.", + "Can't load user settings": "Can't load user settings", + "Change Password": "Change Password", + "%(senderName)s changed their display name from %(oldDisplayName)s to %(displayName)s.": "%(senderName)s changed their display name from %(oldDisplayName)s to %(displayName)s.", + "%(senderName)s changed their profile picture.": "%(senderName)s changed their profile picture.", + "%(senderName)s changed the power level of %(powerLevelDiffText)s.": "%(senderName)s changed the power level of %(powerLevelDiffText)s.", + "%(senderDisplayName)s changed the room name to %(roomName)s.": "%(senderDisplayName)s changed the room name to %(roomName)s.", + "%(senderDisplayName)s removed the room name.": "%(senderDisplayName)s removed the room name.", + "%(senderDisplayName)s changed the topic to \"%(topic)s\".": "%(senderDisplayName)s changed the topic to \"%(topic)s\".", + "Changes to who can read history will only apply to future messages in this room": "Changes to who can read history will only apply to future messages in this room", + "Changes your display nickname": "Changes your display nickname", + "Changes colour scheme of current room": "Changes colour scheme of current room", + "changing room on a RoomView is not supported": "changing room on a RoomView is not supported", + "Changing password will currently reset any end-to-end encryption keys on all devices, making encrypted chat history unreadable, unless you first export your room keys and re-import them afterwards. In future this will be improved.": "Changing password will currently reset any end-to-end encryption keys on all devices, making encrypted chat history unreadable, unless you first export your room keys and re-import them afterwards. In future this will be improved.", + "Claimed Ed25519 fingerprint key": "Claimed Ed25519 fingerprint key", + "Clear Cache and Reload": "Clear Cache and Reload", + "Clear Cache": "Clear Cache", + "Click here to join the discussion!": "Click here to join the discussion!", + "Click here to fix": "Click here to fix", + "Click to mute audio": "Click to mute audio", + "Click to mute video": "Click to mute video", + "click to reveal": "click to reveal", + "Click to unmute video": "Click to unmute video", + "Click to unmute audio": "Click to unmute audio", + "Close": "Close", + "Command error": "Command error", + "Commands": "Commands", + "Conference call failed.": "Conference call failed.", + "Conference calling is in development and may not be reliable.": "Conference calling is in development and may not be reliable.", + "Conference calls are not supported in encrypted rooms": "Conference calls are not supported in encrypted rooms", + "Conference calls are not supported in this client": "Conference calls are not supported in this client", + "Confirm password": "Confirm password", + "Confirm your new password": "Confirm your new password", + "Continue": "Continue", + "Could not connect to the integration server": "Could not connect to the integration server", + "%(count)s new messages": { + "one": "%(count)s new message", + "other": "%(count)s new messages" + }, + "Create a new chat or reuse an existing one": "Create a new chat or reuse an existing one", + "Create an account": "Create an account", + "Create Room": "Create Room", + "Cryptography": "Cryptography", + "Current password": "Current password", + "Curve25519 identity key": "Curve25519 identity key", + "Custom": "Custom", + "Custom level": "Custom level", + "/ddg is not a command": "/ddg is not a command", + "Deactivate Account": "Deactivate Account", + "Deactivate my account": "Deactivate my account", + "Decline": "Decline", + "Decrypt %(text)s": "Decrypt %(text)s", + "Decryption error": "Decryption error", + "Delete": "Delete", + "demote": "demote", + "Deops user with given id": "Deops user with given id", + "Default": "Default", + "Define the power level of a user": "Define the power level of a user", + "Device already verified!": "Device already verified!", + "Device ID": "Device ID", + "Device ID:": "Device ID:", + "device id: ": "device id: ", + "Device key:": "Device key:", + "Devices": "Devices", + "Devices will not yet be able to decrypt history from before they joined the room": "Devices will not yet be able to decrypt history from before they joined the room", + "Direct Chat": "Direct Chat", + "Direct chats": "Direct chats", + "Disable Notifications": "Disable Notifications", + "disabled": "disabled", + "Disable inline URL previews by default": "Disable inline URL previews by default", + "Disable markdown formatting": "Disable markdown formatting", + "Disinvite": "Disinvite", + "Display name": "Display name", + "Displays action": "Displays action", + "Don't send typing notifications": "Don't send typing notifications", + "Download %(text)s": "Download %(text)s", + "Drop File Here": "Drop File Here", + "Drop here %(toAction)s": "Drop here %(toAction)s", + "Drop here to tag %(section)s": "Drop here to tag %(section)s", + "Ed25519 fingerprint": "Ed25519 fingerprint", + "Email": "Email", + "Email address": "Email address", + "Email address (optional)": "Email address (optional)", + "Email, name or matrix ID": "Email, name or matrix ID", + "Emoji": "Emoji", + "Enable automatic language detection for syntax highlighting": "Enable automatic language detection for syntax highlighting", + "Enable encryption": "Enable encryption", + "Enable Notifications": "Enable Notifications", + "enabled": "enabled", + "Encrypted by a verified device": "Encrypted by a verified device", + "Encrypted by an unverified device": "Encrypted by an unverified device", + "Encrypted messages will not be visible on clients that do not yet implement encryption": "Encrypted messages will not be visible on clients that do not yet implement encryption", + "Encrypted room": "Encrypted room", + "Encryption is enabled in this room": "Encryption is enabled in this room", + "Encryption is not enabled in this room": "Encryption is not enabled in this room", + "%(senderName)s ended the call.": "%(senderName)s ended the call.", + "End-to-end encryption information": "End-to-end encryption information", + "End-to-end encryption is in beta and may not be reliable": "End-to-end encryption is in beta and may not be reliable", + "Enter Code": "Enter Code", + "Enter passphrase": "Enter passphrase", + "Error": "Error", + "Error decrypting attachment": "Error decrypting attachment", + "Error: Problem communicating with the given homeserver.": "Error: Problem communicating with the given homeserver.", + "Event information": "Event information", + "Existing Call": "Existing Call", + "Export": "Export", + "Export E2E room keys": "Export E2E room keys", + "Failed to ban user": "Failed to ban user", + "Failed to change password. Is your password correct?": "Failed to change password. Is your password correct?", + "Failed to change power level": "Failed to change power level", + "Failed to delete device": "Failed to delete device", + "Failed to fetch avatar URL": "Failed to fetch avatar URL", + "Failed to forget room %(errCode)s": "Failed to forget room %(errCode)s", + "Failed to join room": "Failed to join room", + "Failed to join the room": "Failed to join the room", + "Failed to kick": "Failed to kick", + "Failed to leave room": "Failed to leave room", + "Failed to load timeline position": "Failed to load timeline position", + "Failed to lookup current room": "Failed to lookup current room", + "Failed to mute user": "Failed to mute user", + "Failed to register as guest:": "Failed to register as guest:", + "Failed to reject invite": "Failed to reject invite", + "Failed to reject invitation": "Failed to reject invitation", + "Failed to save settings": "Failed to save settings", + "Failed to send email": "Failed to send email", + "Failed to send request.": "Failed to send request.", + "Failed to set avatar.": "Failed to set avatar.", + "Failed to set display name": "Failed to set display name", + "Failed to set up conference call": "Failed to set up conference call", + "Failed to toggle moderator status": "Failed to toggle moderator status", + "Failed to unban": "Failed to unban", + "Failed to upload file": "Failed to upload file", + "Failed to upload profile picture!": "Failed to upload profile picture!", + "Failed to verify email address: make sure you clicked the link in the email": "Failed to verify email address: make sure you clicked the link in the email", + "Failure to create room": "Failure to create room", + "Favourite": "Favourite", + "favourite": "favourite", + "Favourites": "Favourites", + "Fill screen": "Fill screen", + "Filter room members": "Filter room members", + "Forget room": "Forget room", + "Forgot your password?": "Forgot your password?", + "For security, this session has been signed out. Please sign in again.": "For security, this session has been signed out. Please sign in again.", + "For security, logging out will delete any end-to-end encryption keys from this browser. If you want to be able to decrypt your conversation history from future Riot sessions, please export your room keys for safe-keeping.": "For security, logging out will delete any end-to-end encryption keys from this browser. If you want to be able to decrypt your conversation history from future Riot sessions, please export your room keys for safe-keeping.", + "Found a bug?": "Found a bug?", + "%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s": "%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s", + "Guest access is disabled on this Home Server.": "Guest access is disabled on this Home Server.", + "Guests can't set avatars. Please register.": "Guests can't set avatars. Please register.", + "Guest users can't create new rooms. Please register to create room and start a chat.": "Guest users can't create new rooms. Please register to create room and start a chat.", + "Guest users can't upload files. Please register to upload.": "Guest users can't upload files. Please register to upload.", + "Guests can't use labs features. Please register.": "Guests can't use labs features. Please register.", + "Guests cannot join this room even if explicitly invited.": "Guests cannot join this room even if explicitly invited.", + "had": "had", + "Hangup": "Hangup", + "Hide Apps": "Hide Apps", + "Hide read receipts": "Hide read receipts", + "Hide Text Formatting Toolbar": "Hide Text Formatting Toolbar", + "Historical": "Historical", + "Home": "Home", + "Homeserver is": "Homeserver is", + "Identity Server is": "Identity Server is", + "I have verified my email address": "I have verified my email address", + "Import": "Import", + "Import E2E room keys": "Import E2E room keys", + "Incoming call from %(name)s": "Incoming call from %(name)s", + "Incoming video call from %(name)s": "Incoming video call from %(name)s", + "Incoming voice call from %(name)s": "Incoming voice call from %(name)s", + "Incorrect username and/or password.": "Incorrect username and/or password.", + "Incorrect verification code": "Incorrect verification code", + "Interface Language": "Interface Language", + "Invalid alias format": "Invalid alias format", + "Invalid address format": "Invalid address format", + "Invalid Email Address": "Invalid Email Address", + "Invalid file%(extra)s": "Invalid file%(extra)s", + "%(senderName)s invited %(targetName)s.": "%(senderName)s invited %(targetName)s.", + "Invite new room members": "Invite new room members", + "Invited": "Invited", + "Invites": "Invites", + "Invites user with given id to current room": "Invites user with given id to current room", + "'%(alias)s' is not a valid format for an address": "'%(alias)s' is not a valid format for an address", + "'%(alias)s' is not a valid format for an alias": "'%(alias)s' is not a valid format for an alias", + "%(displayName)s is typing": "%(displayName)s is typing", + "Sign in with": "Sign in with", + "Join as voice or video.": "Join as voice or video.", + "Join Room": "Join Room", + "joined and left": "joined and left", + "joined": "joined", + "%(targetName)s joined the room.": "%(targetName)s joined the room.", + "Joins room with given alias": "Joins room with given alias", + "Jump to first unread message.": "Jump to first unread message.", + "%(senderName)s kicked %(targetName)s.": "%(senderName)s kicked %(targetName)s.", + "Kick": "Kick", + "Kicks user with given id": "Kicks user with given id", + "Labs": "Labs", + "Last seen": "Last seen", + "Leave room": "Leave room", + "left and rejoined": "left and rejoined", + "left": "left", + "%(targetName)s left the room.": "%(targetName)s left the room.", + "Level:": "Level:", + "List this room in %(domain)s's room directory?": "List this room in %(domain)s's room directory?", + "Local addresses for this room:": "Local addresses for this room:", + "Logged in as:": "Logged in as:", + "Login as guest": "Login as guest", + "Logout": "Logout", + "Low priority": "Low priority", + "%(senderName)s made future room history visible to": "%(senderName)s made future room history visible to", + "Manage Integrations": "Manage Integrations", + "Markdown is disabled": "Markdown is disabled", + "Markdown is enabled": "Markdown is enabled", + "matrix-react-sdk version:": "matrix-react-sdk version:", + "Matrix Apps": "Matrix Apps", + "Members only": "Members only", + "Message not sent due to unknown devices being present": "Message not sent due to unknown devices being present", + "Missing room_id in request": "Missing room_id in request", + "Missing user_id in request": "Missing user_id in request", + "Mobile phone number": "Mobile phone number", + "Mobile phone number (optional)": "Mobile phone number (optional)", + "Moderator": "Moderator", + "Must be viewing a room": "Must be viewing a room", + "Mute": "Mute", + "my Matrix ID": "my Matrix ID", + "Name": "Name", + "Never send encrypted messages to unverified devices from this device": "Never send encrypted messages to unverified devices from this device", + "Never send encrypted messages to unverified devices in this room": "Never send encrypted messages to unverified devices in this room", + "Never send encrypted messages to unverified devices in this room from this device": "Never send encrypted messages to unverified devices in this room from this device", + "New address (e.g. #foo:%(localDomain)s)": "New address (e.g. #foo:%(localDomain)s)", + "New Composer & Autocomplete": "New Composer & Autocomplete", + "New password": "New password", + "New passwords don't match": "New passwords don't match", + "New passwords must match each other.": "New passwords must match each other.", + "none": "none", + "not set": "not set", + "not specified": "not specified", + "Notifications": "Notifications", + "(not supported by this browser)": "(not supported by this browser)", + "": "", + "AM": "AM", + "PM": "PM", + "NOT verified": "NOT verified", + "No devices with registered encryption keys": "No devices with registered encryption keys", + "No display name": "No display name", + "No more results": "No more results", + "No results": "No results", + "No users have specific privileges in this room": "No users have specific privileges in this room", + "OK": "OK", + "olm version:": "olm version:", + "Once encryption is enabled for a room it cannot be turned off again (for now)": "Once encryption is enabled for a room it cannot be turned off again (for now)", + "Once you've followed the link it contains, click below": "Once you've followed the link it contains, click below", + "Only people who have been invited": "Only people who have been invited", + "Operation failed": "Operation failed", + "Otherwise, click here to send a bug report.": "Otherwise, click here to send a bug report.", + "Password": "Password", + "Password:": "Password:", + "Passwords can't be empty": "Passwords can't be empty", + "People": "People", + "Permissions": "Permissions", + "Phone": "Phone", + "%(senderName)s placed a %(callType)s call.": "%(senderName)s placed a %(callType)s call.", + "Please check your email and click on the link it contains. Once this is done, click continue.": "Please check your email and click on the link it contains. Once this is done, click continue.", + "Please Register": "Please Register", + "Power level must be positive integer.": "Power level must be positive integer.", + "Press": "Press", + "Press to start a chat with someone": "Press to start a chat with someone", + "Privacy warning": "Privacy warning", + "Private Chat": "Private Chat", + "Privileged Users": "Privileged Users", + "Profile": "Profile", + "Public Chat": "Public Chat", + "Reason": "Reason", + "Reason: %(reasonText)s": "Reason: %(reasonText)s", + "Revoke Moderator": "Revoke Moderator", + "Refer a friend to Riot:": "Refer a friend to Riot:", + "Register": "Register", + "rejected": "rejected", + "%(targetName)s rejected the invitation.": "%(targetName)s rejected the invitation.", + "Reject invitation": "Reject invitation", + "Rejoin": "Rejoin", + "Remote addresses for this room:": "Remote addresses for this room:", + "Remove Contact Information?": "Remove Contact Information?", + "%(senderName)s removed their display name (%(oldDisplayName)s).": "%(senderName)s removed their display name (%(oldDisplayName)s).", + "%(senderName)s removed their profile picture.": "%(senderName)s removed their profile picture.", + "Remove": "Remove", + "Remove %(threePid)s?": "Remove %(threePid)s?", + "%(senderName)s requested a VoIP conference.": "%(senderName)s requested a VoIP conference.", + "Report it": "Report it", + "Resetting password will currently reset any end-to-end encryption keys on all devices, making encrypted chat history unreadable, unless you first export your room keys and re-import them afterwards. In future this will be improved.": "Resetting password will currently reset any end-to-end encryption keys on all devices, making encrypted chat history unreadable, unless you first export your room keys and re-import them afterwards. In future this will be improved.", + "restore": "restore", + "Results from DuckDuckGo": "Results from DuckDuckGo", + "Return to app": "Return to app", + "Return to login screen": "Return to login screen", + "Riot does not have permission to send you notifications - please check your browser settings": "Riot does not have permission to send you notifications - please check your browser settings", + "Riot was not given permission to send notifications - please try again": "Riot was not given permission to send notifications - please try again", + "riot-web version:": "riot-web version:", + "Room %(roomId)s not visible": "Room %(roomId)s not visible", + "Room Colour": "Room Colour", + "Room contains unknown devices": "Room contains unknown devices", + "Room name (optional)": "Room name (optional)", + "%(roomName)s does not exist.": "%(roomName)s does not exist.", + "%(roomName)s is not accessible at this time.": "%(roomName)s is not accessible at this time.", + "Rooms": "Rooms", + "Save": "Save", + "Scroll to bottom of page": "Scroll to bottom of page", + "Scroll to unread messages": "Scroll to unread messages", + "Search": "Search", + "Search failed": "Search failed", + "Searches DuckDuckGo for results": "Searches DuckDuckGo for results", + "Searching known users": "Searching known users", + "Seen by %(userName)s at %(dateTime)s": "Seen by %(userName)s at %(dateTime)s", + "Send a message (unencrypted)": "Send a message (unencrypted)", + "Send an encrypted message": "Send an encrypted message", + "Send anyway": "Send anyway", + "Sender device information": "Sender device information", + "Send Invites": "Send Invites", + "Send Reset Email": "Send Reset Email", + "sent an image": "sent an image", + "%(senderDisplayName)s sent an image.": "%(senderDisplayName)s sent an image.", + "%(senderName)s sent an invitation to %(targetDisplayName)s to join the room.": "%(senderName)s sent an invitation to %(targetDisplayName)s to join the room.", + "sent a video": "sent a video", + "Server error": "Server error", + "Server may be unavailable or overloaded": "Server may be unavailable or overloaded", + "Server may be unavailable, overloaded, or search timed out :(": "Server may be unavailable, overloaded, or search timed out :(", + "Server may be unavailable, overloaded, or the file too big": "Server may be unavailable, overloaded, or the file too big", + "Server may be unavailable, overloaded, or you hit a bug.": "Server may be unavailable, overloaded, or you hit a bug.", + "Server unavailable, overloaded, or something else went wrong.": "Server unavailable, overloaded, or something else went wrong.", + "Session ID": "Session ID", + "%(senderName)s set a profile picture.": "%(senderName)s set a profile picture.", + "%(senderName)s set their display name to %(displayName)s.": "%(senderName)s set their display name to %(displayName)s.", + "Set": "Set", + "Settings": "Settings", + "Sets the room topic": "Sets the room topic", + "Show Apps": "Show Apps", + "Show panel": "Show panel", + "Show Text Formatting Toolbar": "Show Text Formatting Toolbar", + "Show timestamps in 12 hour format (e.g. 2:30pm)": "Show timestamps in 12 hour format (e.g. 2:30pm)", + "Signed Out": "Signed Out", + "Sign in": "Sign in", + "Sign out": "Sign out", + "since the point in time of selecting this option": "since the point in time of selecting this option", + "since they joined": "since they joined", + "since they were invited": "since they were invited", + "Some of your messages have not been sent.": "Some of your messages have not been sent.", + "Someone": "Someone", + "Sorry, this homeserver is using a login which is not recognised ": "Sorry, this homeserver is using a login which is not recognised ", + "Start a chat": "Start a chat", + "Start authentication": "Start authentication", + "Start Chat": "Start Chat", + "Submit": "Submit", + "Success": "Success", + "tag as %(tagName)s": "tag as %(tagName)s", + "tag direct chat": "tag direct chat", + "Tagged as: ": "Tagged as: ", + "The default role for new room members is": "The default role for new room members is", + "The main address for this room is": "The main address for this room is", + "The phone number entered looks invalid": "The phone number entered looks invalid", + "The signing key you provided matches the signing key you received from %(userId)s's device %(deviceId)s. Device marked as verified.": "The signing key you provided matches the signing key you received from %(userId)s's device %(deviceId)s. Device marked as verified.", + "This action cannot be performed by a guest user. Please register to be able to do this.": "This action cannot be performed by a guest user. Please register to be able to do this.", + "This email address is already in use": "This email address is already in use", + "This email address was not found": "This email address was not found", + "%(actionVerb)s this person?": "%(actionVerb)s this person?", + "The email address linked to your account must be entered.": "The email address linked to your account must be entered.", + "The file '%(fileName)s' exceeds this home server's size limit for uploads": "The file '%(fileName)s' exceeds this home server's size limit for uploads", + "The file '%(fileName)s' failed to upload": "The file '%(fileName)s' failed to upload", + "The remote side failed to pick up": "The remote side failed to pick up", + "This Home Server does not support login using email address.": "This Home Server does not support login using email address.", + "This invitation was sent to an email address which is not associated with this account:": "This invitation was sent to an email address which is not associated with this account:", + "There was a problem logging in.": "There was a problem logging in.", + "This room has no local addresses": "This room has no local addresses", + "This room is not recognised.": "This room is not recognised.", + "These are experimental features that may break in unexpected ways": "These are experimental features that may break in unexpected ways", + "The visibility of existing history will be unchanged": "The visibility of existing history will be unchanged", + "This doesn't appear to be a valid email address": "This doesn't appear to be a valid email address", + "This is a preview of this room. Room interactions have been disabled": "This is a preview of this room. Room interactions have been disabled", + "This phone number is already in use": "This phone number is already in use", + "This room": "This room", + "This room is not accessible by remote Matrix servers": "This room is not accessible by remote Matrix servers", + "This room's internal ID is": "This room's internal ID is", + "times": "times", + "To ban users": "To ban users", + "to browse the directory": "to browse the directory", + "To configure the room": "To configure the room", + "to demote": "to demote", + "to favourite": "to favourite", + "To invite users into the room": "To invite users into the room", + "To kick users": "To kick users", + "To link to a room it must have an address.": "To link to a room it must have an address.", + "to make a room or": "to make a room or", + "To remove other users' messages": "To remove other users' messages", + "To reset your password, enter the email address linked to your account": "To reset your password, enter the email address linked to your account", + "to restore": "to restore", + "To send events of type": "To send events of type", + "To send messages": "To send messages", + "to start a chat with someone": "to start a chat with someone", + "to tag as %(tagName)s": "to tag as %(tagName)s", + "to tag direct chat": "to tag direct chat", + "To use it, just wait for autocomplete results to load and tab through them.": "To use it, just wait for autocomplete results to load and tab through them.", + "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.": "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.", + "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.", + "Turn Markdown off": "Turn Markdown off", + "Turn Markdown on": "Turn Markdown on", + "%(senderName)s turned on end-to-end encryption (algorithm %(algorithm)s).": "%(senderName)s turned on end-to-end encryption (algorithm %(algorithm)s).", + "Unable to add email address": "Unable to add email address", + "Unable to create widget.": "Unable to create widget.", + "Unable to remove contact information": "Unable to remove contact information", + "Unable to restore previous session": "Unable to restore previous session", + "Unable to verify email address.": "Unable to verify email address.", + "Unban": "Unban", + "Unbans user with given id": "Unbans user with given id", + "%(senderName)s unbanned %(targetName)s.": "%(senderName)s unbanned %(targetName)s.", + "Unable to ascertain that the address this invite was sent to matches one associated with your account.": "Unable to ascertain that the address this invite was sent to matches one associated with your account.", + "Unable to capture screen": "Unable to capture screen", + "Unable to enable Notifications": "Unable to enable Notifications", + "Unable to load device list": "Unable to load device list", + "Undecryptable": "Undecryptable", + "Unencrypted room": "Unencrypted room", + "unencrypted": "unencrypted", + "Unencrypted message": "Unencrypted message", + "unknown caller": "unknown caller", + "Unknown command": "Unknown command", + "unknown device": "unknown device", + "unknown error code": "unknown error code", + "Unknown room %(roomId)s": "Unknown room %(roomId)s", + "Unknown (user, device) pair:": "Unknown (user, device) pair:", + "unknown": "unknown", + "Unmute": "Unmute", + "Unnamed Room": "Unnamed Room", + "Unrecognised command:": "Unrecognised command:", + "Unrecognised room alias:": "Unrecognised room alias:", + "Unverified": "Unverified", + "Uploading %(filename)s and %(count)s others": { + "zero": "Uploading %(filename)s", + "one": "Uploading %(filename)s and %(count)s other", + "other": "Uploading %(filename)s and %(count)s others" + }, + "uploaded a file": "uploaded a file", + "Upload avatar": "Upload avatar", + "Upload Failed": "Upload Failed", + "Upload Files": "Upload Files", + "Upload file": "Upload file", + "Upload new:": "Upload new:", + "Usage": "Usage", + "Use compact timeline layout": "Use compact timeline layout", + "Use with caution": "Use with caution", + "User ID": "User ID", + "User Interface": "User Interface", + "%(user)s is a": "%(user)s is a", + "User name": "User name", + "%(userName)s (power %(powerLevelNumber)s)": "%(userName)s (power %(powerLevelNumber)s)", + "Username invalid: %(errMessage)s": "Username invalid: %(errMessage)s", + "Users": "Users", + "User": "User", + "Verification Pending": "Verification Pending", + "Verification": "Verification", + "verified": "verified", + "Verified": "Verified", + "Verified key": "Verified key", + "Video call": "Video call", + "Voice call": "Voice call", + "VoIP conference finished.": "VoIP conference finished.", + "VoIP conference started.": "VoIP conference started.", + "VoIP is unsupported": "VoIP is unsupported", + "(could not connect media)": "(could not connect media)", + "(no answer)": "(no answer)", + "(unknown failure: %(reason)s)": "(unknown failure: %(reason)s)", + "(warning: cannot be disabled again!)": "(warning: cannot be disabled again!)", + "Warning!": "Warning!", + "WARNING: Device already verified, but keys do NOT MATCH!": "WARNING: Device already verified, but keys do NOT MATCH!", + "WARNING: KEY VERIFICATION FAILED! The signing key for %(userId)s and device %(deviceId)s is \"%(fprint)s\" which does not match the provided key \"%(fingerprint)s\". This could mean your communications are being intercepted!": "WARNING: KEY VERIFICATION FAILED! The signing key for %(userId)s and device %(deviceId)s is \"%(fprint)s\" which does not match the provided key \"%(fingerprint)s\". This could mean your communications are being intercepted!", + "Who can access this room?": "Who can access this room?", + "Who can read history?": "Who can read history?", + "Who would you like to add to this room?": "Who would you like to add to this room?", + "Who would you like to communicate with?": "Who would you like to communicate with?", + "%(senderName)s withdrew %(targetName)s's invitation.": "%(senderName)s withdrew %(targetName)s's invitation.", + "Would you like to accept or decline this invitation?": "Would you like to accept or decline this invitation?", + "You already have existing direct chats with this user:": "You already have existing direct chats with this user:", + "You are already in a call.": "You are already in a call.", + "You're not in any rooms yet! Press to make a room or to browse the directory": "You're not in any rooms yet! Press to make a room or to browse the directory", + "You are trying to access %(roomName)s.": "You are trying to access %(roomName)s.", + "You cannot place a call with yourself.": "You cannot place a call with yourself.", + "You cannot place VoIP calls in this browser.": "You cannot place VoIP calls in this browser.", + "You do not have permission to post to this room": "You do not have permission to post to this room", + "You have been banned from %(roomName)s by %(userName)s.": "You have been banned from %(roomName)s by %(userName)s.", + "You have been invited to join this room by %(inviterName)s": "You have been invited to join this room by %(inviterName)s", + "You have been kicked from %(roomName)s by %(userName)s.": "You have been kicked from %(roomName)s by %(userName)s.", + "You have been logged out of all devices and will no longer receive push notifications. To re-enable notifications, sign in again on each device": "You have been logged out of all devices and will no longer receive push notifications. To re-enable notifications, sign in again on each device", + "You have disabled URL previews by default.": "You have disabled URL previews by default.", + "You have enabled URL previews by default.": "You have enabled URL previews by default.", + "You have entered an invalid contact. Try using their Matrix ID or email address.": "You have entered an invalid contact. Try using their Matrix ID or email address.", + "You have no visible notifications": "You have no visible notifications", + "You may wish to login with a different account, or add this email to this account.": "You may wish to login with a different account, or add this email to this account.", + "you must be a": "you must be a", + "You must register to use this functionality": "You must register to use this functionality", + "You need to be able to invite users to do that.": "You need to be able to invite users to do that.", + "You need to be logged in.": "You need to be logged in.", + "You need to enter a user name.": "You need to enter a user name.", + "Your email address does not appear to be associated with a Matrix ID on this Homeserver.": "Your email address does not appear to be associated with a Matrix ID on this Homeserver.", + "Your password has been reset": "Your password has been reset", + "Your password was successfully changed. You will not receive push notifications on other devices until you log back in to them": "Your password was successfully changed. You will not receive push notifications on other devices until you log back in to them", + "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?", + "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 should not yet trust it to secure data": "You should not yet trust it to secure data", + "You will not be able to undo this change as you are promoting the user to have the same power level as yourself.": "You will not be able to undo this change as you are promoting the user to have the same power level as yourself.", + "Your home server does not support device management.": "Your home server does not support device management.", + "Sun": "Sun", + "Mon": "Mon", + "Tue": "Tue", + "Wed": "Wed", + "Thu": "Thu", + "Fri": "Fri", + "Sat": "Sat", + "Jan": "Jan", + "Feb": "Feb", + "Mar": "Mar", + "Apr": "Apr", + "May": "May", + "Jun": "Jun", + "Jul": "Jul", + "Aug": "Aug", + "Sep": "Sep", + "Oct": "Oct", + "Nov": "Nov", + "Dec": "Dec", + "%(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 %(time)s": "%(weekDayName)s %(time)s", + "Set a display name:": "Set a display name:", + "Set a Display Name": "Set a Display Name", + "Upload an avatar:": "Upload an avatar:", + "This server does not support authentication with a phone number.": "This server does not support authentication with a phone number.", + "Missing password.": "Missing password.", + "Passwords don't match.": "Passwords don't match.", + "Password too short (min %(MIN_PASSWORD_LENGTH)s).": "Password too short (min %(MIN_PASSWORD_LENGTH)s).", + "This doesn't look like a valid email address.": "This doesn't look like a valid email address.", + "This doesn't look like a valid phone number.": "This doesn't look like a valid phone number.", + "User names may only contain letters, numbers, dots, hyphens and underscores.": "User names may only contain letters, numbers, dots, hyphens and underscores.", + "An unknown error occurred.": "An unknown error occurred.", + "I already have an account": "I already have an account", + "An error occurred: %(error_string)s": "An error occurred: %(error_string)s", + "Topic": "Topic", + "Make Moderator": "Make Moderator", + "Make this room private": "Make this room private", + "Share message history with new users": "Share message history with new users", + "Encrypt room": "Encrypt room", + "There are no visible files in this room": "There are no visible files in this room", + "Room": "Room", + "Connectivity to the server has been lost.": "Connectivity to the server has been lost.", + "Sent messages will be stored until your connection has returned.": "Sent messages will be stored until your connection has returned.", + "Auto-complete": "Auto-complete", + "Resend all or cancel all now. You can also select individual messages to resend or cancel.": "Resend all or cancel all now. You can also select individual messages to resend or cancel.", + "(~%(count)s results)": { + "one": "(~%(count)s result)", + "other": "(~%(count)s results)" + }, + "Cancel": "Cancel", + "or": "or", + "Active call": "Active call", + "Monday": "Monday", + "Tuesday": "Tuesday", + "Wednesday": "Wednesday", + "Thursday": "Thursday", + "Friday": "Friday", + "Saturday": "Saturday", + "Sunday": "Sunday", + "bold": "bold", + "italic": "italic", + "strike": "strike", + "underline": "underline", + "code": "code", + "quote": "quote", + "bullet": "bullet", + "numbullet": "numbullet", + "%(severalUsers)sjoined %(repeats)s times": "%(severalUsers)sjoined %(repeats)s times", + "%(oneUser)sjoined %(repeats)s times": "%(oneUser)sjoined %(repeats)s times", + "%(severalUsers)sjoined": "%(severalUsers)sjoined", + "%(oneUser)sjoined": "%(oneUser)sjoined", + "%(severalUsers)sleft %(repeats)s times": "%(severalUsers)sleft %(repeats)s times", + "%(oneUser)sleft %(repeats)s times": "%(oneUser)sleft %(repeats)s times", + "%(severalUsers)sleft": "%(severalUsers)sleft", + "%(oneUser)sleft": "%(oneUser)sleft", + "%(severalUsers)sjoined and left %(repeats)s times": "%(severalUsers)sjoined and left %(repeats)s times", + "%(oneUser)sjoined and left %(repeats)s times": "%(oneUser)sjoined and left %(repeats)s times", + "%(severalUsers)sjoined and left": "%(severalUsers)sjoined and left", + "%(oneUser)sjoined and left": "%(oneUser)sjoined and left", + "%(severalUsers)sleft and rejoined %(repeats)s times": "%(severalUsers)sleft and rejoined %(repeats)s times", + "%(oneUser)sleft and rejoined %(repeats)s times": "%(oneUser)sleft and rejoined %(repeats)s times", + "%(severalUsers)sleft and rejoined": "%(severalUsers)sleft and rejoined", + "%(oneUser)sleft and rejoined": "%(oneUser)sleft and rejoined", + "%(severalUsers)srejected their invitations %(repeats)s times": "%(severalUsers)srejected their invitations %(repeats)s times", + "%(oneUser)srejected their invitation %(repeats)s times": "%(oneUser)srejected their invitation %(repeats)s times", + "%(severalUsers)srejected their invitations": "%(severalUsers)srejected their invitations", + "%(oneUser)srejected their invitation": "%(oneUser)srejected their invitation", + "%(severalUsers)shad their invitations withdrawn %(repeats)s times": "%(severalUsers)shad their invitations withdrawn %(repeats)s times", + "%(oneUser)shad their invitation withdrawn %(repeats)s times": "%(oneUser)shad their invitation withdrawn %(repeats)s times", + "%(severalUsers)shad their invitations withdrawn": "%(severalUsers)shad their invitations withdrawn", + "%(oneUser)shad their invitation withdrawn": "%(oneUser)shad their invitation withdrawn", + "were invited %(repeats)s times": "were invited %(repeats)s times", + "was invited %(repeats)s times": "was invited %(repeats)s times", + "were invited": "were invited", + "was invited": "was invited", + "were banned %(repeats)s times": "were banned %(repeats)s times", + "was banned %(repeats)s times": "was banned %(repeats)s times", + "were banned": "were banned", + "was banned": "was banned", + "were unbanned %(repeats)s times": "were unbanned %(repeats)s times", + "was unbanned %(repeats)s times": "was unbanned %(repeats)s times", + "were unbanned": "were unbanned", + "was unbanned": "was unbanned", + "were kicked %(repeats)s times": "were kicked %(repeats)s times", + "was kicked %(repeats)s times": "was kicked %(repeats)s times", + "were kicked": "were kicked", + "was kicked": "was kicked", + "%(severalUsers)schanged their name %(repeats)s times": "%(severalUsers)schanged their name %(repeats)s times", + "%(oneUser)schanged their name %(repeats)s times": "%(oneUser)schanged their name %(repeats)s times", + "%(severalUsers)schanged their name": "%(severalUsers)schanged their name", + "%(oneUser)schanged their name": "%(oneUser)schanged their name", + "%(severalUsers)schanged their avatar %(repeats)s times": "%(severalUsers)schanged their avatar %(repeats)s times", + "%(oneUser)schanged their avatar %(repeats)s times": "%(oneUser)schanged their avatar %(repeats)s times", + "%(severalUsers)schanged their avatar": "%(severalUsers)schanged their avatar", + "%(oneUser)schanged their avatar": "%(oneUser)schanged their avatar", + "Please select the destination room for this message": "Please select the destination room for this message", + "Create new room": "Create new room", + "Welcome page": "Welcome page", + "Room directory": "Room directory", + "Start chat": "Start chat", + "New Password": "New Password", + "Start automatically after system login": "Start automatically after system login", + "Desktop specific": "Desktop specific", + "Analytics": "Analytics", + "Opt out of analytics": "Opt out of analytics", + "Options": "Options", + "Riot collects anonymous analytics to allow us to improve the application.": "Riot collects anonymous analytics to allow us to improve the application.", + "Passphrases must match": "Passphrases must match", + "Passphrase must not be empty": "Passphrase must not be empty", + "Export room keys": "Export room keys", + "Confirm passphrase": "Confirm passphrase", + "Import room keys": "Import room keys", + "File to import": "File to import", + "This process allows you to export the keys for messages you have received in encrypted rooms to a local file. You will then be able to import the file into another Matrix client in the future, so that client will also be able to decrypt these messages.": "This process allows you to export the keys for messages you have received in encrypted rooms to a local file. You will then be able to import the file into another Matrix client in the future, so that client will also be able to decrypt these messages.", + "The exported file will allow anyone who can read it to decrypt any encrypted messages that you can see, so you should be careful to keep it secure. To help with this, you should enter a passphrase below, which will be used to encrypt the exported data. It will only be possible to import the data by using the same passphrase.": "The exported file will allow anyone who can read it to decrypt any encrypted messages that you can see, so you should be careful to keep it secure. To help with this, you should enter a passphrase below, which will be used to encrypt the exported data. It will only be possible to import the data by using the same passphrase.", + "This process allows you to import encryption keys that you had previously exported from another Matrix client. You will then be able to decrypt any messages that the other client could decrypt.": "This process allows you to import encryption keys that you had previously exported from another Matrix client. You will then be able to decrypt any messages that the other client could decrypt.", + "The export file will be protected with a passphrase. You should enter the passphrase here, to decrypt the file.": "The export file will be protected with a passphrase. You should enter the passphrase here, to decrypt the file.", + "You must join the room to see its files": "You must join the room to see its files", + "Reject all %(invitedRooms)s invites": "Reject all %(invitedRooms)s invites", + "Start new chat": "Start new chat", + "Guest users can't invite users. Please register.": "Guest users can't invite users. Please register.", + "Failed to invite": "Failed to invite", + "Failed to invite user": "Failed to invite user", + "Failed to invite the following users to the %(roomName)s room:": "Failed to invite the following users to the %(roomName)s room:", + "Confirm Removal": "Confirm Removal", + "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.": "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.", + "Unknown error": "Unknown error", + "Incorrect password": "Incorrect password", + "This will make your account permanently unusable. You will not be able to re-register the same user ID.": "This will make your account permanently unusable. You will not be able to re-register the same user ID.", + "This action is irreversible.": "This action is irreversible.", + "To continue, please enter your password.": "To continue, please enter your password.", + "To verify that this device can be trusted, please contact its owner using some other means (e.g. in person or a phone call) and ask them whether the key they see in their User Settings for this device matches the key below:": "To verify that this device can be trusted, please contact its owner using some other means (e.g. in person or a phone call) and ask them whether the key they see in their User Settings for this device matches the key below:", + "Device name": "Device name", + "Device Name": "Device Name", + "Device key": "Device key", + "If it matches, press the verify button below. If it doesn't, then someone else is intercepting this device and you probably want to press the blacklist button instead.": "If it matches, press the verify button below. If it doesn't, then someone else is intercepting this device and you probably want to press the blacklist button instead.", + "In future this verification process will be more sophisticated.": "In future this verification process will be more sophisticated.", + "Verify device": "Verify device", + "Verifies a user, device, and pubkey tuple": "Verifies a user, device, and pubkey tuple", + "I verify that the keys match": "I verify that the keys match", + "We encountered an error trying to restore your previous session. If you continue, you will need to log in again, and encrypted chat history will be unreadable.": "We encountered an error trying to restore your previous session. If you continue, you will need to log in again, and encrypted chat history will be unreadable.", + "Unable to restore session": "Unable to restore session", + "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.": "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.", + "Continue anyway": "Continue anyway", + "Your display name is how you'll appear to others when you speak in rooms. What would you like it to be?": "Your display name is how you'll appear to others when you speak in rooms. What would you like it to be?", + "You are currently blacklisting unverified devices; to send messages to these devices you must verify them.": "You are currently blacklisting unverified devices; to send messages to these devices you must verify them.", + "We recommend you go through the verification process for each device to confirm they belong to their legitimate owner, but you can resend the message without verifying if you prefer.": "We recommend you go through the verification process for each device to confirm they belong to their legitimate owner, but you can resend the message without verifying if you prefer.", + "\"%(RoomName)s\" contains devices that you haven't seen before.": "\"%(RoomName)s\" contains devices that you haven't seen before.", + "Unknown devices": "Unknown devices", + "Unknown Address": "Unknown Address", + "Unblacklist": "Unblacklist", + "Blacklist": "Blacklist", + "Unverify": "Unverify", + "Verify...": "Verify...", + "ex. @bob:example.com": "ex. @bob:example.com", + "Add User": "Add User", + "This Home Server would like to make sure you are not a robot": "This Home Server would like to make sure you are not a robot", + "Sign in with CAS": "Sign in with CAS", + "Custom Server Options": "Custom Server Options", + "You can use the custom server options to sign into other Matrix servers by specifying a different Home server URL.": "You can use the custom server options to sign into other Matrix servers by specifying a different Home server URL.", + "This allows you to use this app with an existing Matrix account on a different home server.": "This allows you to use this app with an existing Matrix account on a different home server.", + "You can also set a custom identity server but this will typically prevent interaction with users based on email address.": "You can also set a custom identity server but this will typically prevent interaction with users based on email address.", + "Dismiss": "Dismiss", + "Please check your email to continue registration.": "Please check your email to continue registration.", + "Token incorrect": "Token incorrect", + "A text message has been sent to": "A text message has been sent to", + "Please enter the code it contains:": "Please enter the code it contains:", + "powered by Matrix": "powered by Matrix", + "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?", + "You are registering with %(SelectedTeamName)s": "You are registering with %(SelectedTeamName)s", + "Default server": "Default server", + "Custom server": "Custom server", + "Home server URL": "Home server URL", + "Identity server URL": "Identity server URL", + "What does this mean?": "What does this mean?", + "Error decrypting audio": "Error decrypting audio", + "Error decrypting image": "Error decrypting image", + "Image '%(Body)s' cannot be displayed.": "Image '%(Body)s' cannot be displayed.", + "This image cannot be displayed.": "This image cannot be displayed.", + "Error decrypting video": "Error decrypting video", + "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?", + "Removed or unknown message type": "Removed or unknown message type", + "Disable URL previews by default for participants in this room": "Disable URL previews by default for participants in this room", + "Disable URL previews for this room (affects only you)": "Disable URL previews for this room (affects only you)", + "URL previews are %(globalDisableUrlPreview)s by default for participants in this room.": "URL previews are %(globalDisableUrlPreview)s by default for participants in this room.", + "URL Previews": "URL Previews", + "Enable URL previews for this room (affects only you)": "Enable URL previews for this room (affects only you)", + "Drop file here to upload": "Drop file here to upload", + " (unsupported)": " (unsupported)", + "Ongoing conference call%(supportedText)s.": "Ongoing conference call%(supportedText)s.", + "for %(amount)ss": "for %(amount)ss", + "for %(amount)sm": "for %(amount)sm", + "for %(amount)sh": "for %(amount)sh", + "for %(amount)sd": "for %(amount)sd", + "Online": "Online", + "Idle": "Idle", + "Offline": "Offline", + "Updates": "Updates", + "Check for update": "Check for update", + "Start chatting": "Start chatting", + "Start Chatting": "Start Chatting", + "Click on the button below to start chatting!": "Click on the button below to start chatting!", + "$senderDisplayName changed the room avatar to ": "$senderDisplayName changed the room avatar to ", + "%(senderDisplayName)s removed the room avatar.": "%(senderDisplayName)s removed the room avatar.", + "%(senderDisplayName)s changed the avatar for %(roomName)s": "%(senderDisplayName)s changed the avatar for %(roomName)s", + "Username available": "Username available", + "Username not available": "Username not available", + "Something went wrong!": "Something went wrong!", + "This will be your account name on the homeserver, or you can pick a different server.": "This will be your account name on the homeserver, or you can pick a different server.", + "If you already have a Matrix account you can log in instead.": "If you already have a Matrix account you can log in instead.", + "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?", + "Disable Peer-to-Peer for 1:1 calls": "Disable Peer-to-Peer for 1:1 calls", + "Do you want to set an email address?": "Do you want to set an email address?", + "This will allow you to reset your password and receive notifications.": "This will allow you to reset your password and receive notifications.", + "To return to your account in future you need to set a password": "To return to your account in future you need to set a password", + "Skip": "Skip", + "Start verification": "Start verification", + "Share without verifying": "Share without verifying", + "Ignore request": "Ignore request", + "You added a new device '%(displayName)s', which is requesting encryption keys.": "You added a new device '%(displayName)s', which is requesting encryption keys.", + "Your unverified device '%(displayName)s' is requesting encryption keys.": "Your unverified device '%(displayName)s' is requesting encryption keys.", + "Encryption key request": "Encryption key request", + "Autocomplete Delay (ms):": "Autocomplete Delay (ms):" } diff --git a/src/i18n/strings/en_US.json b/src/i18n/strings/en_US.json index e060add84f..6e94be47dc 100644 --- a/src/i18n/strings/en_US.json +++ b/src/i18n/strings/en_US.json @@ -1,4 +1,5 @@ { + "Add a widget": "Add a widget", "af": "Afrikaans", "ar-ae": "Arabic (U.A.E.)", "ar-bh": "Arabic (Bahrain)", @@ -119,6 +120,8 @@ "zh-sg": "Chinese (Singapore)", "zh-tw": "Chinese (Taiwan)", "zu": "Zulu", + "AM": "AM", + "PM": "PM", "A text message has been sent to +%(msisdn)s. Please enter the verification code it contains": "A text message has been sent to +%(msisdn)s. Please enter the verification code it contains", "accept": "accept", "%(targetName)s accepted an invitation.": "%(targetName)s accepted an invitation.", @@ -311,6 +314,7 @@ "Guests cannot join this room even if explicitly invited.": "Guests cannot join this room even if explicitly invited.", "had": "had", "Hangup": "Hangup", + "Hide Apps": "Hide Apps", "Hide read receipts": "Hide read receipts", "Hide Text Formatting Toolbar": "Hide Text Formatting Toolbar", "Historical": "Historical", @@ -362,6 +366,7 @@ "Markdown is disabled": "Markdown is disabled", "Markdown is enabled": "Markdown is enabled", "matrix-react-sdk version:": "matrix-react-sdk version:", + "Matrix Apps": "Matrix Apps", "Members only": "Members only", "Message not sent due to unknown devices being present": "Message not sent due to unknown devices being present", "Missing room_id in request": "Missing room_id in request", @@ -464,6 +469,7 @@ "%(senderName)s set a profile picture.": "%(senderName)s set a profile picture.", "%(senderName)s set their display name to %(displayName)s.": "%(senderName)s set their display name to %(displayName)s.", "Settings": "Settings", + "Show Apps": "Show Apps", "Show panel": "Show panel", "Show timestamps in 12 hour format (e.g. 2:30pm)": "Show timestamps in 12 hour format (e.g. 2:30pm)", "Signed Out": "Signed Out", diff --git a/src/stores/LifecycleStore.js b/src/stores/LifecycleStore.js index d954ef16b6..0d76f06e72 100644 --- a/src/stores/LifecycleStore.js +++ b/src/stores/LifecycleStore.js @@ -50,7 +50,7 @@ class LifecycleStore extends Store { deferred_action: null, }); break; - case 'sync_state': + case 'sync_state': { if (payload.state !== 'PREPARED') { break; } @@ -61,6 +61,7 @@ class LifecycleStore extends Store { }); dis.dispatch(deferredAction); break; + } case 'on_logged_out': this.reset(); break; diff --git a/src/stores/MessageComposerStore.js b/src/stores/MessageComposerStore.js new file mode 100644 index 0000000000..d02bcf953f --- /dev/null +++ b/src/stores/MessageComposerStore.js @@ -0,0 +1,77 @@ +/* +Copyright 2017 Vector Creations 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 dis from '../dispatcher'; +import {Store} from 'flux/utils'; +import {convertToRaw, convertFromRaw} from 'draft-js'; + +const INITIAL_STATE = { + editorStateMap: localStorage.getItem('content_state') ? + JSON.parse(localStorage.getItem('content_state')) : {}, +}; + +/** + * A class for storing application state to do with the message composer. This is a simple + * flux store that listens for actions and updates its state accordingly, informing any + * listeners (views) of state changes. + */ +class MessageComposerStore extends Store { + constructor() { + super(dis); + + // Initialise state + this._state = Object.assign({}, INITIAL_STATE); + } + + _setState(newState) { + this._state = Object.assign(this._state, newState); + this.__emitChange(); + } + + __onDispatch(payload) { + switch (payload.action) { + case 'content_state': + this._contentState(payload); + break; + case 'on_logged_out': + this.reset(); + break; + } + } + + _contentState(payload) { + const editorStateMap = this._state.editorStateMap; + editorStateMap[payload.room_id] = convertToRaw(payload.content_state); + localStorage.setItem('content_state', JSON.stringify(editorStateMap)); + this._setState({ + editorStateMap: editorStateMap, + }); + } + + getContentState(roomId) { + return this._state.editorStateMap[roomId] ? + convertFromRaw(this._state.editorStateMap[roomId]) : null; + } + + reset() { + this._state = Object.assign({}, INITIAL_STATE); + } +} + +let singletonMessageComposerStore = null; +if (!singletonMessageComposerStore) { + singletonMessageComposerStore = new MessageComposerStore(); +} +module.exports = singletonMessageComposerStore; diff --git a/src/stores/RoomViewStore.js b/src/stores/RoomViewStore.js index 2f7d55b71f..865caa8997 100644 --- a/src/stores/RoomViewStore.js +++ b/src/stores/RoomViewStore.js @@ -141,6 +141,10 @@ class RoomViewStore extends Store { shouldPeek: payload.should_peek === undefined ? true : payload.should_peek, }; + if (payload.joined) { + newState.joining = false; + } + // If an event ID wasn't specified, default to the one saved for this room // via update_scroll_state. Assume initialEventPixelOffset should be set. if (!newState.initialEventId) { diff --git a/src/stripped-emoji.json b/src/stripped-emoji.json new file mode 100644 index 0000000000..00928833c1 --- /dev/null +++ b/src/stripped-emoji.json @@ -0,0 +1 @@ +[{"name":"hundred points symbol","shortname":":100:","category":"symbols","emoji_order":"856"},{"name":"input symbol for numbers","shortname":":1234:","category":"symbols","emoji_order":"913"},{"name":"grinning face","shortname":":grinning:","category":"people","emoji_order":"1"},{"name":"grimacing face","shortname":":grimacing:","category":"people","emoji_order":"2"},{"name":"grinning face with smiling eyes","shortname":":grin:","category":"people","emoji_order":"3"},{"name":"face with tears of joy","shortname":":joy:","category":"people","emoji_order":"4","aliases_ascii":[":')",":'-)"]},{"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_ascii":[">:)",">;)",">:-)",">=)"]},{"name":"smiling face with halo","shortname":":innocent:","category":"people","emoji_order":"9","aliases_ascii":["O:-)","0:-3","0:3","0:-)","0:)","0;^)","O:)","O;-)","O=)","0;-)","O:-3","O:3"]},{"name":"winking face","shortname":":wink:","category":"people","emoji_order":"10","aliases_ascii":[";)",";-)","*-)","*)",";-]",";]",";D",";^)"]},{"name":"smiling face with smiling eyes","shortname":":blush:","category":"people","emoji_order":"11"},{"name":"slightly smiling face","shortname":":slight_smile:","category":"people","emoji_order":"12","aliases_ascii":[":)",":-)","=]","=)",":]"]},{"name":"upside-down face","shortname":":upside_down:","category":"people","emoji_order":"13"},{"name":"white smiling face","shortname":":relaxed:","category":"people","emoji_order":"14"},{"name":"face savouring delicious food","shortname":":yum:","category":"people","emoji_order":"15"},{"name":"relieved face","shortname":":relieved:","category":"people","emoji_order":"16"},{"name":"smiling face with heart-shaped eyes","shortname":":heart_eyes:","category":"people","emoji_order":"17"},{"name":"face throwing a kiss","shortname":":kissing_heart:","category":"people","emoji_order":"18","aliases_ascii":[":*",":-*","=*",":^*"]},{"name":"kissing face","shortname":":kissing:","category":"people","emoji_order":"19"},{"name":"kissing face with smiling eyes","shortname":":kissing_smiling_eyes:","category":"people","emoji_order":"20"},{"name":"kissing face with closed eyes","shortname":":kissing_closed_eyes:","category":"people","emoji_order":"21"},{"name":"face with stuck-out tongue and winking eye","shortname":":stuck_out_tongue_winking_eye:","category":"people","emoji_order":"22","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":"23"},{"name":"face with stuck-out tongue","shortname":":stuck_out_tongue:","category":"people","emoji_order":"24","aliases_ascii":[":P",":-P","=P",":-p",":p","=p",":-Þ",":Þ",":þ",":-þ",":-b",":b","d:"]},{"name":"money-mouth face","shortname":":money_mouth:","category":"people","emoji_order":"25"},{"name":"nerd face","shortname":":nerd:","category":"people","emoji_order":"26"},{"name":"smiling face with sunglasses","shortname":":sunglasses:","category":"people","emoji_order":"27","aliases_ascii":["B-)","B)","8)","8-)","B-D","8-D"]},{"name":"hugging face","shortname":":hugging:","category":"people","emoji_order":"28"},{"name":"smirking face","shortname":":smirk:","category":"people","emoji_order":"29"},{"name":"face without mouth","shortname":":no_mouth:","category":"people","emoji_order":"30","aliases_ascii":[":-X",":X",":-#",":#","=X","=x",":x",":-x","=#"]},{"name":"neutral face","shortname":":neutral_face:","category":"people","emoji_order":"31"},{"name":"expressionless face","shortname":":expressionless:","category":"people","emoji_order":"32","aliases_ascii":["-_-","-__-","-___-"]},{"name":"unamused face","shortname":":unamused:","category":"people","emoji_order":"33"},{"name":"face with rolling eyes","shortname":":rolling_eyes:","category":"people","emoji_order":"34"},{"name":"thinking face","shortname":":thinking:","category":"people","emoji_order":"35"},{"name":"flushed face","shortname":":flushed:","category":"people","emoji_order":"36","aliases_ascii":[":$","=$"]},{"name":"disappointed face","shortname":":disappointed:","category":"people","emoji_order":"37","aliases_ascii":[">:[",":-(",":(",":-[",":[","=("]},{"name":"worried face","shortname":":worried:","category":"people","emoji_order":"38"},{"name":"angry face","shortname":":angry:","category":"people","emoji_order":"39","aliases_ascii":[">:(",">:-(",":@"]},{"name":"pouting face","shortname":":rage:","category":"people","emoji_order":"40"},{"name":"pensive face","shortname":":pensive:","category":"people","emoji_order":"41"},{"name":"confused face","shortname":":confused:","category":"people","emoji_order":"42","aliases_ascii":[">:\\",">:/",":-/",":-.",":/",":\\","=/","=\\",":L","=L"]},{"name":"slightly frowning face","shortname":":slight_frown:","category":"people","emoji_order":"43"},{"name":"white frowning face","shortname":":frowning2:","category":"people","emoji_order":"44"},{"name":"persevering face","shortname":":persevere:","category":"people","emoji_order":"45","aliases_ascii":[">.<"]},{"name":"confounded face","shortname":":confounded:","category":"people","emoji_order":"46"},{"name":"tired face","shortname":":tired_face:","category":"people","emoji_order":"47"},{"name":"weary face","shortname":":weary:","category":"people","emoji_order":"48"},{"name":"face with look of triumph","shortname":":triumph:","category":"people","emoji_order":"49"},{"name":"face with open mouth","shortname":":open_mouth:","category":"people","emoji_order":"50","aliases_ascii":[":-O",":O",":-o",":o","O_O",">:O"]},{"name":"face screaming in fear","shortname":":scream:","category":"people","emoji_order":"51"},{"name":"fearful face","shortname":":fearful:","category":"people","emoji_order":"52","aliases_ascii":["D:"]},{"name":"face with open mouth and cold sweat","shortname":":cold_sweat:","category":"people","emoji_order":"53"},{"name":"hushed face","shortname":":hushed:","category":"people","emoji_order":"54"},{"name":"frowning face with open mouth","shortname":":frowning:","category":"people","emoji_order":"55"},{"name":"anguished face","shortname":":anguished:","category":"people","emoji_order":"56"},{"name":"crying face","shortname":":cry:","category":"people","emoji_order":"57","aliases_ascii":[":'(",":'-(",";(",";-("]},{"name":"disappointed but relieved face","shortname":":disappointed_relieved:","category":"people","emoji_order":"58"},{"name":"sleepy face","shortname":":sleepy:","category":"people","emoji_order":"59"},{"name":"face with cold sweat","shortname":":sweat:","category":"people","emoji_order":"60","aliases_ascii":["':(","':-(","'=("]},{"name":"loudly crying face","shortname":":sob:","category":"people","emoji_order":"61"},{"name":"dizzy face","shortname":":dizzy_face:","category":"people","emoji_order":"62","aliases_ascii":["#-)","#)","%-)","%)","X)","X-)"]},{"name":"astonished face","shortname":":astonished:","category":"people","emoji_order":"63"},{"name":"zipper-mouth face","shortname":":zipper_mouth:","category":"people","emoji_order":"64"},{"name":"face with medical mask","shortname":":mask:","category":"people","emoji_order":"65"},{"name":"face with thermometer","shortname":":thermometer_face:","category":"people","emoji_order":"66"},{"name":"face with head-bandage","shortname":":head_bandage:","category":"people","emoji_order":"67"},{"name":"sleeping face","shortname":":sleeping:","category":"people","emoji_order":"68"},{"name":"sleeping symbol","shortname":":zzz:","category":"people","emoji_order":"69"},{"name":"pile of poo","shortname":":poop:","category":"people","emoji_order":"70"},{"name":"smiling face with horns","shortname":":smiling_imp:","category":"people","emoji_order":"71"},{"name":"imp","shortname":":imp:","category":"people","emoji_order":"72"},{"name":"japanese ogre","shortname":":japanese_ogre:","category":"people","emoji_order":"73"},{"name":"japanese goblin","shortname":":japanese_goblin:","category":"people","emoji_order":"74"},{"name":"skull","shortname":":skull:","category":"people","emoji_order":"75"},{"name":"ghost","shortname":":ghost:","category":"people","emoji_order":"76"},{"name":"extraterrestrial alien","shortname":":alien:","category":"people","emoji_order":"77"},{"name":"robot face","shortname":":robot:","category":"people","emoji_order":"78"},{"name":"smiling cat face with open mouth","shortname":":smiley_cat:","category":"people","emoji_order":"79"},{"name":"grinning cat face with smiling eyes","shortname":":smile_cat:","category":"people","emoji_order":"80"},{"name":"cat face with tears of joy","shortname":":joy_cat:","category":"people","emoji_order":"81"},{"name":"smiling cat face with heart-shaped eyes","shortname":":heart_eyes_cat:","category":"people","emoji_order":"82"},{"name":"cat face with wry smile","shortname":":smirk_cat:","category":"people","emoji_order":"83"},{"name":"kissing cat face with closed eyes","shortname":":kissing_cat:","category":"people","emoji_order":"84"},{"name":"weary cat face","shortname":":scream_cat:","category":"people","emoji_order":"85"},{"name":"crying cat face","shortname":":crying_cat_face:","category":"people","emoji_order":"86"},{"name":"pouting cat face","shortname":":pouting_cat:","category":"people","emoji_order":"87"},{"name":"person raising both hands in celebration","shortname":":raised_hands:","category":"people","emoji_order":"88"},{"name":"clapping hands sign","shortname":":clap:","category":"people","emoji_order":"89"},{"name":"waving hand sign","shortname":":wave:","category":"people","emoji_order":"90"},{"name":"thumbs up sign","shortname":":thumbsup:","category":"people","emoji_order":"91"},{"name":"thumbs down sign","shortname":":thumbsdown:","category":"people","emoji_order":"92"},{"name":"fisted hand sign","shortname":":punch:","category":"people","emoji_order":"93"},{"name":"raised fist","shortname":":fist:","category":"people","emoji_order":"94"},{"name":"victory hand","shortname":":v:","category":"people","emoji_order":"95"},{"name":"ok hand sign","shortname":":ok_hand:","category":"people","emoji_order":"96"},{"name":"raised hand","shortname":":raised_hand:","category":"people","emoji_order":"97"},{"name":"open hands sign","shortname":":open_hands:","category":"people","emoji_order":"98"},{"name":"flexed biceps","shortname":":muscle:","category":"people","emoji_order":"99"},{"name":"person with folded hands","shortname":":pray:","category":"people","emoji_order":"100"},{"name":"white up pointing index","shortname":":point_up:","category":"people","emoji_order":"101"},{"name":"white up pointing backhand index","shortname":":point_up_2:","category":"people","emoji_order":"102"},{"name":"white down pointing backhand index","shortname":":point_down:","category":"people","emoji_order":"103"},{"name":"white left pointing backhand index","shortname":":point_left:","category":"people","emoji_order":"104"},{"name":"white right pointing backhand index","shortname":":point_right:","category":"people","emoji_order":"105"},{"name":"reversed hand with middle finger extended","shortname":":middle_finger:","category":"people","emoji_order":"106"},{"name":"raised hand with fingers splayed","shortname":":hand_splayed:","category":"people","emoji_order":"107"},{"name":"sign of the horns","shortname":":metal:","category":"people","emoji_order":"108"},{"name":"raised hand with part between middle and ring fingers","shortname":":vulcan:","category":"people","emoji_order":"109"},{"name":"writing hand","shortname":":writing_hand:","category":"people","emoji_order":"110"},{"name":"nail polish","shortname":":nail_care:","category":"people","emoji_order":"111"},{"name":"mouth","shortname":":lips:","category":"people","emoji_order":"112"},{"name":"tongue","shortname":":tongue:","category":"people","emoji_order":"113"},{"name":"ear","shortname":":ear:","category":"people","emoji_order":"114"},{"name":"nose","shortname":":nose:","category":"people","emoji_order":"115"},{"name":"eye","shortname":":eye:","category":"people","emoji_order":"116"},{"name":"eyes","shortname":":eyes:","category":"people","emoji_order":"117"},{"name":"bust in silhouette","shortname":":bust_in_silhouette:","category":"people","emoji_order":"118"},{"name":"busts in silhouette","shortname":":busts_in_silhouette:","category":"people","emoji_order":"119"},{"name":"speaking head in silhouette","shortname":":speaking_head:","category":"people","emoji_order":"120"},{"name":"baby","shortname":":baby:","category":"people","emoji_order":"121"},{"name":"boy","shortname":":boy:","category":"people","emoji_order":"122"},{"name":"girl","shortname":":girl:","category":"people","emoji_order":"123"},{"name":"man","shortname":":man:","category":"people","emoji_order":"124"},{"name":"woman","shortname":":woman:","category":"people","emoji_order":"125"},{"name":"person with blond hair","shortname":":person_with_blond_hair:","category":"people","emoji_order":"126"},{"name":"older man","shortname":":older_man:","category":"people","emoji_order":"127"},{"name":"older woman","shortname":":older_woman:","category":"people","emoji_order":"128"},{"name":"man with gua pi mao","shortname":":man_with_gua_pi_mao:","category":"people","emoji_order":"129"},{"name":"man with turban","shortname":":man_with_turban:","category":"people","emoji_order":"130"},{"name":"police officer","shortname":":cop:","category":"people","emoji_order":"131"},{"name":"construction worker","shortname":":construction_worker:","category":"people","emoji_order":"132"},{"name":"guardsman","shortname":":guardsman:","category":"people","emoji_order":"133"},{"name":"sleuth or spy","shortname":":spy:","category":"people","emoji_order":"134"},{"name":"father christmas","shortname":":santa:","category":"people","emoji_order":"135"},{"name":"baby angel","shortname":":angel:","category":"people","emoji_order":"136"},{"name":"princess","shortname":":princess:","category":"people","emoji_order":"137"},{"name":"bride with veil","shortname":":bride_with_veil:","category":"people","emoji_order":"138"},{"name":"pedestrian","shortname":":walking:","category":"people","emoji_order":"139"},{"name":"runner","shortname":":runner:","category":"people","emoji_order":"140"},{"name":"dancer","shortname":":dancer:","category":"people","emoji_order":"141"},{"name":"woman with bunny ears","shortname":":dancers:","category":"people","emoji_order":"142"},{"name":"man and woman holding hands","shortname":":couple:","category":"people","emoji_order":"143"},{"name":"two men holding hands","shortname":":two_men_holding_hands:","category":"people","emoji_order":"144"},{"name":"two women holding hands","shortname":":two_women_holding_hands:","category":"people","emoji_order":"145"},{"name":"person bowing deeply","shortname":":bow:","category":"people","emoji_order":"146"},{"name":"information desk person","shortname":":information_desk_person:","category":"people","emoji_order":"147"},{"name":"face with no good gesture","shortname":":no_good:","category":"people","emoji_order":"148"},{"name":"face with ok gesture","shortname":":ok_woman:","category":"people","emoji_order":"149","aliases_ascii":["*\\0/*","\\0/","*\\O/*","\\O/"]},{"name":"happy person raising one hand","shortname":":raising_hand:","category":"people","emoji_order":"150"},{"name":"person with pouting face","shortname":":person_with_pouting_face:","category":"people","emoji_order":"151"},{"name":"person frowning","shortname":":person_frowning:","category":"people","emoji_order":"152"},{"name":"haircut","shortname":":haircut:","category":"people","emoji_order":"153"},{"name":"face massage","shortname":":massage:","category":"people","emoji_order":"154"},{"name":"couple with heart","shortname":":couple_with_heart:","category":"people","emoji_order":"155"},{"name":"couple (woman,woman)","shortname":":couple_ww:","category":"people","emoji_order":"156"},{"name":"couple (man,man)","shortname":":couple_mm:","category":"people","emoji_order":"157"},{"name":"kiss","shortname":":couplekiss:","category":"people","emoji_order":"158"},{"name":"kiss (woman,woman)","shortname":":kiss_ww:","category":"people","emoji_order":"159"},{"name":"kiss (man,man)","shortname":":kiss_mm:","category":"people","emoji_order":"160"},{"name":"family","shortname":":family:","category":"people","emoji_order":"161"},{"name":"family (man,woman,girl)","shortname":":family_mwg:","category":"people","emoji_order":"162"},{"name":"family (man,woman,girl,boy)","shortname":":family_mwgb:","category":"people","emoji_order":"163"},{"name":"family (man,woman,boy,boy)","shortname":":family_mwbb:","category":"people","emoji_order":"164"},{"name":"family (man,woman,girl,girl)","shortname":":family_mwgg:","category":"people","emoji_order":"165"},{"name":"family (woman,woman,boy)","shortname":":family_wwb:","category":"people","emoji_order":"166"},{"name":"family (woman,woman,girl)","shortname":":family_wwg:","category":"people","emoji_order":"167"},{"name":"family (woman,woman,girl,boy)","shortname":":family_wwgb:","category":"people","emoji_order":"168"},{"name":"family (woman,woman,boy,boy)","shortname":":family_wwbb:","category":"people","emoji_order":"169"},{"name":"family (woman,woman,girl,girl)","shortname":":family_wwgg:","category":"people","emoji_order":"170"},{"name":"family (man,man,boy)","shortname":":family_mmb:","category":"people","emoji_order":"171"},{"name":"family (man,man,girl)","shortname":":family_mmg:","category":"people","emoji_order":"172"},{"name":"family (man,man,girl,boy)","shortname":":family_mmgb:","category":"people","emoji_order":"173"},{"name":"family (man,man,boy,boy)","shortname":":family_mmbb:","category":"people","emoji_order":"174"},{"name":"family (man,man,girl,girl)","shortname":":family_mmgg:","category":"people","emoji_order":"175"},{"name":"womans clothes","shortname":":womans_clothes:","category":"people","emoji_order":"176"},{"name":"t-shirt","shortname":":shirt:","category":"people","emoji_order":"177"},{"name":"jeans","shortname":":jeans:","category":"people","emoji_order":"178"},{"name":"necktie","shortname":":necktie:","category":"people","emoji_order":"179"},{"name":"dress","shortname":":dress:","category":"people","emoji_order":"180"},{"name":"bikini","shortname":":bikini:","category":"people","emoji_order":"181"},{"name":"kimono","shortname":":kimono:","category":"people","emoji_order":"182"},{"name":"lipstick","shortname":":lipstick:","category":"people","emoji_order":"183"},{"name":"kiss mark","shortname":":kiss:","category":"people","emoji_order":"184"},{"name":"footprints","shortname":":footprints:","category":"people","emoji_order":"185"},{"name":"high-heeled shoe","shortname":":high_heel:","category":"people","emoji_order":"186"},{"name":"womans sandal","shortname":":sandal:","category":"people","emoji_order":"187"},{"name":"womans boots","shortname":":boot:","category":"people","emoji_order":"188"},{"name":"mans shoe","shortname":":mans_shoe:","category":"people","emoji_order":"189"},{"name":"athletic shoe","shortname":":athletic_shoe:","category":"people","emoji_order":"190"},{"name":"womans hat","shortname":":womans_hat:","category":"people","emoji_order":"191"},{"name":"top hat","shortname":":tophat:","category":"people","emoji_order":"192"},{"name":"helmet with white cross","shortname":":helmet_with_cross:","category":"people","emoji_order":"193"},{"name":"graduation cap","shortname":":mortar_board:","category":"people","emoji_order":"194"},{"name":"crown","shortname":":crown:","category":"people","emoji_order":"195"},{"name":"school satchel","shortname":":school_satchel:","category":"people","emoji_order":"196"},{"name":"pouch","shortname":":pouch:","category":"people","emoji_order":"197"},{"name":"purse","shortname":":purse:","category":"people","emoji_order":"198"},{"name":"handbag","shortname":":handbag:","category":"people","emoji_order":"199"},{"name":"briefcase","shortname":":briefcase:","category":"people","emoji_order":"200"},{"name":"eyeglasses","shortname":":eyeglasses:","category":"people","emoji_order":"201"},{"name":"dark sunglasses","shortname":":dark_sunglasses:","category":"people","emoji_order":"202"},{"name":"ring","shortname":":ring:","category":"people","emoji_order":"203"},{"name":"closed umbrella","shortname":":closed_umbrella:","category":"people","emoji_order":"204"},{"name":"dog face","shortname":":dog:","category":"nature","emoji_order":"205"},{"name":"cat face","shortname":":cat:","category":"nature","emoji_order":"206"},{"name":"mouse face","shortname":":mouse:","category":"nature","emoji_order":"207"},{"name":"hamster face","shortname":":hamster:","category":"nature","emoji_order":"208"},{"name":"rabbit face","shortname":":rabbit:","category":"nature","emoji_order":"209"},{"name":"bear face","shortname":":bear:","category":"nature","emoji_order":"210"},{"name":"panda face","shortname":":panda_face:","category":"nature","emoji_order":"211"},{"name":"koala","shortname":":koala:","category":"nature","emoji_order":"212"},{"name":"tiger face","shortname":":tiger:","category":"nature","emoji_order":"213"},{"name":"lion face","shortname":":lion_face:","category":"nature","emoji_order":"214"},{"name":"cow face","shortname":":cow:","category":"nature","emoji_order":"215"},{"name":"pig face","shortname":":pig:","category":"nature","emoji_order":"216"},{"name":"pig nose","shortname":":pig_nose:","category":"nature","emoji_order":"217"},{"name":"frog face","shortname":":frog:","category":"nature","emoji_order":"218"},{"name":"octopus","shortname":":octopus:","category":"nature","emoji_order":"219"},{"name":"monkey face","shortname":":monkey_face:","category":"nature","emoji_order":"220"},{"name":"see-no-evil monkey","shortname":":see_no_evil:","category":"nature","emoji_order":"221"},{"name":"hear-no-evil monkey","shortname":":hear_no_evil:","category":"nature","emoji_order":"222"},{"name":"speak-no-evil monkey","shortname":":speak_no_evil:","category":"nature","emoji_order":"223"},{"name":"monkey","shortname":":monkey:","category":"nature","emoji_order":"224"},{"name":"chicken","shortname":":chicken:","category":"nature","emoji_order":"225"},{"name":"penguin","shortname":":penguin:","category":"nature","emoji_order":"226"},{"name":"bird","shortname":":bird:","category":"nature","emoji_order":"227"},{"name":"baby chick","shortname":":baby_chick:","category":"nature","emoji_order":"228"},{"name":"hatching chick","shortname":":hatching_chick:","category":"nature","emoji_order":"229"},{"name":"front-facing baby chick","shortname":":hatched_chick:","category":"nature","emoji_order":"230"},{"name":"wolf face","shortname":":wolf:","category":"nature","emoji_order":"231"},{"name":"boar","shortname":":boar:","category":"nature","emoji_order":"232"},{"name":"horse face","shortname":":horse:","category":"nature","emoji_order":"233"},{"name":"unicorn face","shortname":":unicorn:","category":"nature","emoji_order":"234"},{"name":"honeybee","shortname":":bee:","category":"nature","emoji_order":"235"},{"name":"bug","shortname":":bug:","category":"nature","emoji_order":"236"},{"name":"snail","shortname":":snail:","category":"nature","emoji_order":"237"},{"name":"lady beetle","shortname":":beetle:","category":"nature","emoji_order":"238"},{"name":"ant","shortname":":ant:","category":"nature","emoji_order":"239"},{"name":"spider","shortname":":spider:","category":"nature","emoji_order":"240"},{"name":"scorpion","shortname":":scorpion:","category":"nature","emoji_order":"241"},{"name":"crab","shortname":":crab:","category":"nature","emoji_order":"242"},{"name":"snake","shortname":":snake:","category":"nature","emoji_order":"243"},{"name":"turtle","shortname":":turtle:","category":"nature","emoji_order":"244"},{"name":"tropical fish","shortname":":tropical_fish:","category":"nature","emoji_order":"245"},{"name":"fish","shortname":":fish:","category":"nature","emoji_order":"246"},{"name":"blowfish","shortname":":blowfish:","category":"nature","emoji_order":"247"},{"name":"dolphin","shortname":":dolphin:","category":"nature","emoji_order":"248"},{"name":"spouting whale","shortname":":whale:","category":"nature","emoji_order":"249"},{"name":"whale","shortname":":whale2:","category":"nature","emoji_order":"250"},{"name":"crocodile","shortname":":crocodile:","category":"nature","emoji_order":"251"},{"name":"leopard","shortname":":leopard:","category":"nature","emoji_order":"252"},{"name":"tiger","shortname":":tiger2:","category":"nature","emoji_order":"253"},{"name":"water buffalo","shortname":":water_buffalo:","category":"nature","emoji_order":"254"},{"name":"ox","shortname":":ox:","category":"nature","emoji_order":"255"},{"name":"cow","shortname":":cow2:","category":"nature","emoji_order":"256"},{"name":"dromedary camel","shortname":":dromedary_camel:","category":"nature","emoji_order":"257"},{"name":"bactrian camel","shortname":":camel:","category":"nature","emoji_order":"258"},{"name":"elephant","shortname":":elephant:","category":"nature","emoji_order":"259"},{"name":"goat","shortname":":goat:","category":"nature","emoji_order":"260"},{"name":"ram","shortname":":ram:","category":"nature","emoji_order":"261"},{"name":"sheep","shortname":":sheep:","category":"nature","emoji_order":"262"},{"name":"horse","shortname":":racehorse:","category":"nature","emoji_order":"263"},{"name":"pig","shortname":":pig2:","category":"nature","emoji_order":"264"},{"name":"rat","shortname":":rat:","category":"nature","emoji_order":"265"},{"name":"mouse","shortname":":mouse2:","category":"nature","emoji_order":"266"},{"name":"rooster","shortname":":rooster:","category":"nature","emoji_order":"267"},{"name":"turkey","shortname":":turkey:","category":"nature","emoji_order":"268"},{"name":"dove of peace","shortname":":dove:","category":"nature","emoji_order":"269"},{"name":"dog","shortname":":dog2:","category":"nature","emoji_order":"270"},{"name":"poodle","shortname":":poodle:","category":"nature","emoji_order":"271"},{"name":"cat","shortname":":cat2:","category":"nature","emoji_order":"272"},{"name":"rabbit","shortname":":rabbit2:","category":"nature","emoji_order":"273"},{"name":"chipmunk","shortname":":chipmunk:","category":"nature","emoji_order":"274"},{"name":"paw prints","shortname":":feet:","category":"nature","emoji_order":"275"},{"name":"dragon","shortname":":dragon:","category":"nature","emoji_order":"276"},{"name":"dragon face","shortname":":dragon_face:","category":"nature","emoji_order":"277"},{"name":"cactus","shortname":":cactus:","category":"nature","emoji_order":"278"},{"name":"christmas tree","shortname":":christmas_tree:","category":"nature","emoji_order":"279"},{"name":"evergreen tree","shortname":":evergreen_tree:","category":"nature","emoji_order":"280"},{"name":"deciduous tree","shortname":":deciduous_tree:","category":"nature","emoji_order":"281"},{"name":"palm tree","shortname":":palm_tree:","category":"nature","emoji_order":"282"},{"name":"seedling","shortname":":seedling:","category":"nature","emoji_order":"283"},{"name":"herb","shortname":":herb:","category":"nature","emoji_order":"284"},{"name":"shamrock","shortname":":shamrock:","category":"nature","emoji_order":"285"},{"name":"four leaf clover","shortname":":four_leaf_clover:","category":"nature","emoji_order":"286"},{"name":"pine decoration","shortname":":bamboo:","category":"nature","emoji_order":"287"},{"name":"tanabata tree","shortname":":tanabata_tree:","category":"nature","emoji_order":"288"},{"name":"leaf fluttering in wind","shortname":":leaves:","category":"nature","emoji_order":"289"},{"name":"fallen leaf","shortname":":fallen_leaf:","category":"nature","emoji_order":"290"},{"name":"maple leaf","shortname":":maple_leaf:","category":"nature","emoji_order":"291"},{"name":"ear of rice","shortname":":ear_of_rice:","category":"nature","emoji_order":"292"},{"name":"hibiscus","shortname":":hibiscus:","category":"nature","emoji_order":"293"},{"name":"sunflower","shortname":":sunflower:","category":"nature","emoji_order":"294"},{"name":"rose","shortname":":rose:","category":"nature","emoji_order":"295"},{"name":"tulip","shortname":":tulip:","category":"nature","emoji_order":"296"},{"name":"blossom","shortname":":blossom:","category":"nature","emoji_order":"297"},{"name":"cherry blossom","shortname":":cherry_blossom:","category":"nature","emoji_order":"298"},{"name":"bouquet","shortname":":bouquet:","category":"nature","emoji_order":"299"},{"name":"mushroom","shortname":":mushroom:","category":"nature","emoji_order":"300"},{"name":"chestnut","shortname":":chestnut:","category":"nature","emoji_order":"301"},{"name":"jack-o-lantern","shortname":":jack_o_lantern:","category":"nature","emoji_order":"302"},{"name":"spiral shell","shortname":":shell:","category":"nature","emoji_order":"303"},{"name":"spider web","shortname":":spider_web:","category":"nature","emoji_order":"304"},{"name":"earth globe americas","shortname":":earth_americas:","category":"nature","emoji_order":"305"},{"name":"earth globe europe-africa","shortname":":earth_africa:","category":"nature","emoji_order":"306"},{"name":"earth globe asia-australia","shortname":":earth_asia:","category":"nature","emoji_order":"307"},{"name":"full moon symbol","shortname":":full_moon:","category":"nature","emoji_order":"308"},{"name":"waning gibbous moon symbol","shortname":":waning_gibbous_moon:","category":"nature","emoji_order":"309"},{"name":"last quarter moon symbol","shortname":":last_quarter_moon:","category":"nature","emoji_order":"310"},{"name":"waning crescent moon symbol","shortname":":waning_crescent_moon:","category":"nature","emoji_order":"311"},{"name":"new moon symbol","shortname":":new_moon:","category":"nature","emoji_order":"312"},{"name":"waxing crescent moon symbol","shortname":":waxing_crescent_moon:","category":"nature","emoji_order":"313"},{"name":"first quarter moon symbol","shortname":":first_quarter_moon:","category":"nature","emoji_order":"314"},{"name":"waxing gibbous moon symbol","shortname":":waxing_gibbous_moon:","category":"nature","emoji_order":"315"},{"name":"new moon with face","shortname":":new_moon_with_face:","category":"nature","emoji_order":"316"},{"name":"full moon with face","shortname":":full_moon_with_face:","category":"nature","emoji_order":"317"},{"name":"first quarter moon with face","shortname":":first_quarter_moon_with_face:","category":"nature","emoji_order":"318"},{"name":"last quarter moon with face","shortname":":last_quarter_moon_with_face:","category":"nature","emoji_order":"319"},{"name":"sun with face","shortname":":sun_with_face:","category":"nature","emoji_order":"320"},{"name":"crescent moon","shortname":":crescent_moon:","category":"nature","emoji_order":"321"},{"name":"white medium star","shortname":":star:","category":"nature","emoji_order":"322"},{"name":"glowing star","shortname":":star2:","category":"nature","emoji_order":"323"},{"name":"dizzy symbol","shortname":":dizzy:","category":"nature","emoji_order":"324"},{"name":"sparkles","shortname":":sparkles:","category":"nature","emoji_order":"325"},{"name":"comet","shortname":":comet:","category":"nature","emoji_order":"326"},{"name":"black sun with rays","shortname":":sunny:","category":"nature","emoji_order":"327"},{"name":"white sun with small cloud","shortname":":white_sun_small_cloud:","category":"nature","emoji_order":"328"},{"name":"sun behind cloud","shortname":":partly_sunny:","category":"nature","emoji_order":"329"},{"name":"white sun behind cloud","shortname":":white_sun_cloud:","category":"nature","emoji_order":"330"},{"name":"white sun behind cloud with rain","shortname":":white_sun_rain_cloud:","category":"nature","emoji_order":"331"},{"name":"cloud","shortname":":cloud:","category":"nature","emoji_order":"332"},{"name":"cloud with rain","shortname":":cloud_rain:","category":"nature","emoji_order":"333"},{"name":"thunder cloud and rain","shortname":":thunder_cloud_rain:","category":"nature","emoji_order":"334"},{"name":"cloud with lightning","shortname":":cloud_lightning:","category":"nature","emoji_order":"335"},{"name":"high voltage sign","shortname":":zap:","category":"nature","emoji_order":"336"},{"name":"fire","shortname":":fire:","category":"nature","emoji_order":"337"},{"name":"collision symbol","shortname":":boom:","category":"nature","emoji_order":"338"},{"name":"snowflake","shortname":":snowflake:","category":"nature","emoji_order":"339"},{"name":"cloud with snow","shortname":":cloud_snow:","category":"nature","emoji_order":"340"},{"name":"snowman","shortname":":snowman2:","category":"nature","emoji_order":"341"},{"name":"snowman without snow","shortname":":snowman:","category":"nature","emoji_order":"342"},{"name":"wind blowing face","shortname":":wind_blowing_face:","category":"nature","emoji_order":"343"},{"name":"dash symbol","shortname":":dash:","category":"nature","emoji_order":"344"},{"name":"cloud with tornado","shortname":":cloud_tornado:","category":"nature","emoji_order":"345"},{"name":"fog","shortname":":fog:","category":"nature","emoji_order":"346"},{"name":"umbrella","shortname":":umbrella2:","category":"nature","emoji_order":"347"},{"name":"umbrella with rain drops","shortname":":umbrella:","category":"nature","emoji_order":"348"},{"name":"droplet","shortname":":droplet:","category":"nature","emoji_order":"349"},{"name":"splashing sweat symbol","shortname":":sweat_drops:","category":"nature","emoji_order":"350"},{"name":"water wave","shortname":":ocean:","category":"nature","emoji_order":"351"},{"name":"green apple","shortname":":green_apple:","category":"food","emoji_order":"352"},{"name":"red apple","shortname":":apple:","category":"food","emoji_order":"353"},{"name":"pear","shortname":":pear:","category":"food","emoji_order":"354"},{"name":"tangerine","shortname":":tangerine:","category":"food","emoji_order":"355"},{"name":"lemon","shortname":":lemon:","category":"food","emoji_order":"356"},{"name":"banana","shortname":":banana:","category":"food","emoji_order":"357"},{"name":"watermelon","shortname":":watermelon:","category":"food","emoji_order":"358"},{"name":"grapes","shortname":":grapes:","category":"food","emoji_order":"359"},{"name":"strawberry","shortname":":strawberry:","category":"food","emoji_order":"360"},{"name":"melon","shortname":":melon:","category":"food","emoji_order":"361"},{"name":"cherries","shortname":":cherries:","category":"food","emoji_order":"362"},{"name":"peach","shortname":":peach:","category":"food","emoji_order":"363"},{"name":"pineapple","shortname":":pineapple:","category":"food","emoji_order":"364"},{"name":"tomato","shortname":":tomato:","category":"food","emoji_order":"365"},{"name":"aubergine","shortname":":eggplant:","category":"food","emoji_order":"366"},{"name":"hot pepper","shortname":":hot_pepper:","category":"food","emoji_order":"367"},{"name":"ear of maize","shortname":":corn:","category":"food","emoji_order":"368"},{"name":"roasted sweet potato","shortname":":sweet_potato:","category":"food","emoji_order":"369"},{"name":"honey pot","shortname":":honey_pot:","category":"food","emoji_order":"370"},{"name":"bread","shortname":":bread:","category":"food","emoji_order":"371"},{"name":"cheese wedge","shortname":":cheese:","category":"food","emoji_order":"372"},{"name":"poultry leg","shortname":":poultry_leg:","category":"food","emoji_order":"373"},{"name":"meat on bone","shortname":":meat_on_bone:","category":"food","emoji_order":"374"},{"name":"fried shrimp","shortname":":fried_shrimp:","category":"food","emoji_order":"375"},{"name":"egg","shortname":":egg:","category":"unicode9","emoji_order":"75"},{"name":"hamburger","shortname":":hamburger:","category":"food","emoji_order":"377"},{"name":"french fries","shortname":":fries:","category":"food","emoji_order":"378"},{"name":"hot dog","shortname":":hotdog:","category":"food","emoji_order":"379"},{"name":"slice of pizza","shortname":":pizza:","category":"food","emoji_order":"380"},{"name":"spaghetti","shortname":":spaghetti:","category":"food","emoji_order":"381"},{"name":"taco","shortname":":taco:","category":"food","emoji_order":"382"},{"name":"burrito","shortname":":burrito:","category":"food","emoji_order":"383"},{"name":"steaming bowl","shortname":":ramen:","category":"food","emoji_order":"384"},{"name":"pot of food","shortname":":stew:","category":"food","emoji_order":"385"},{"name":"fish cake with swirl design","shortname":":fish_cake:","category":"food","emoji_order":"386"},{"name":"sushi","shortname":":sushi:","category":"food","emoji_order":"387"},{"name":"bento box","shortname":":bento:","category":"food","emoji_order":"388"},{"name":"curry and rice","shortname":":curry:","category":"food","emoji_order":"389"},{"name":"rice ball","shortname":":rice_ball:","category":"food","emoji_order":"390"},{"name":"cooked rice","shortname":":rice:","category":"food","emoji_order":"391"},{"name":"rice cracker","shortname":":rice_cracker:","category":"food","emoji_order":"392"},{"name":"oden","shortname":":oden:","category":"food","emoji_order":"393"},{"name":"dango","shortname":":dango:","category":"food","emoji_order":"394"},{"name":"shaved ice","shortname":":shaved_ice:","category":"food","emoji_order":"395"},{"name":"ice cream","shortname":":ice_cream:","category":"food","emoji_order":"396"},{"name":"soft ice cream","shortname":":icecream:","category":"food","emoji_order":"397"},{"name":"shortcake","shortname":":cake:","category":"food","emoji_order":"398"},{"name":"birthday cake","shortname":":birthday:","category":"food","emoji_order":"399"},{"name":"custard","shortname":":custard:","category":"food","emoji_order":"400"},{"name":"candy","shortname":":candy:","category":"food","emoji_order":"401"},{"name":"lollipop","shortname":":lollipop:","category":"food","emoji_order":"402"},{"name":"chocolate bar","shortname":":chocolate_bar:","category":"food","emoji_order":"403"},{"name":"popcorn","shortname":":popcorn:","category":"food","emoji_order":"404"},{"name":"doughnut","shortname":":doughnut:","category":"food","emoji_order":"405"},{"name":"cookie","shortname":":cookie:","category":"food","emoji_order":"406"},{"name":"beer mug","shortname":":beer:","category":"food","emoji_order":"407"},{"name":"clinking beer mugs","shortname":":beers:","category":"food","emoji_order":"408"},{"name":"wine glass","shortname":":wine_glass:","category":"food","emoji_order":"409"},{"name":"cocktail glass","shortname":":cocktail:","category":"food","emoji_order":"410"},{"name":"tropical drink","shortname":":tropical_drink:","category":"food","emoji_order":"411"},{"name":"bottle with popping cork","shortname":":champagne:","category":"food","emoji_order":"412"},{"name":"sake bottle and cup","shortname":":sake:","category":"food","emoji_order":"413"},{"name":"teacup without handle","shortname":":tea:","category":"food","emoji_order":"414"},{"name":"hot beverage","shortname":":coffee:","category":"food","emoji_order":"415"},{"name":"baby bottle","shortname":":baby_bottle:","category":"food","emoji_order":"416"},{"name":"fork and knife","shortname":":fork_and_knife:","category":"food","emoji_order":"417"},{"name":"fork and knife with plate","shortname":":fork_knife_plate:","category":"food","emoji_order":"418"},{"name":"soccer ball","shortname":":soccer:","category":"activity","emoji_order":"419"},{"name":"basketball and hoop","shortname":":basketball:","category":"activity","emoji_order":"420"},{"name":"american football","shortname":":football:","category":"activity","emoji_order":"421"},{"name":"baseball","shortname":":baseball:","category":"activity","emoji_order":"422"},{"name":"tennis racquet and ball","shortname":":tennis:","category":"activity","emoji_order":"423"},{"name":"volleyball","shortname":":volleyball:","category":"activity","emoji_order":"424"},{"name":"rugby football","shortname":":rugby_football:","category":"activity","emoji_order":"425"},{"name":"billiards","shortname":":8ball:","category":"activity","emoji_order":"426"},{"name":"flag in hole","shortname":":golf:","category":"activity","emoji_order":"427"},{"name":"golfer","shortname":":golfer:","category":"activity","emoji_order":"428"},{"name":"table tennis paddle and ball","shortname":":ping_pong:","category":"activity","emoji_order":"429"},{"name":"badminton racquet","shortname":":badminton:","category":"activity","emoji_order":"430"},{"name":"ice hockey stick and puck","shortname":":hockey:","category":"activity","emoji_order":"431"},{"name":"field hockey stick and ball","shortname":":field_hockey:","category":"activity","emoji_order":"432"},{"name":"cricket bat and ball","shortname":":cricket:","category":"activity","emoji_order":"433"},{"name":"ski and ski boot","shortname":":ski:","category":"activity","emoji_order":"434"},{"name":"skier","shortname":":skier:","category":"activity","emoji_order":"435"},{"name":"snowboarder","shortname":":snowboarder:","category":"activity","emoji_order":"436"},{"name":"ice skate","shortname":":ice_skate:","category":"activity","emoji_order":"437"},{"name":"bow and arrow","shortname":":bow_and_arrow:","category":"activity","emoji_order":"438"},{"name":"fishing pole and fish","shortname":":fishing_pole_and_fish:","category":"activity","emoji_order":"439"},{"name":"rowboat","shortname":":rowboat:","category":"activity","emoji_order":"440"},{"name":"swimmer","shortname":":swimmer:","category":"activity","emoji_order":"441"},{"name":"surfer","shortname":":surfer:","category":"activity","emoji_order":"442"},{"name":"bath","shortname":":bath:","category":"activity","emoji_order":"443"},{"name":"person with ball","shortname":":basketball_player:","category":"activity","emoji_order":"444"},{"name":"weight lifter","shortname":":lifter:","category":"activity","emoji_order":"445"},{"name":"bicyclist","shortname":":bicyclist:","category":"activity","emoji_order":"446"},{"name":"mountain bicyclist","shortname":":mountain_bicyclist:","category":"activity","emoji_order":"447"},{"name":"horse racing","shortname":":horse_racing:","category":"activity","emoji_order":"448"},{"name":"man in business suit levitating","shortname":":levitate:","category":"activity","emoji_order":"449"},{"name":"trophy","shortname":":trophy:","category":"activity","emoji_order":"450"},{"name":"running shirt with sash","shortname":":running_shirt_with_sash:","category":"activity","emoji_order":"451"},{"name":"sports medal","shortname":":medal:","category":"activity","emoji_order":"452"},{"name":"military medal","shortname":":military_medal:","category":"activity","emoji_order":"453"},{"name":"reminder ribbon","shortname":":reminder_ribbon:","category":"activity","emoji_order":"454"},{"name":"rosette","shortname":":rosette:","category":"activity","emoji_order":"455"},{"name":"ticket","shortname":":ticket:","category":"activity","emoji_order":"456"},{"name":"admission tickets","shortname":":tickets:","category":"activity","emoji_order":"457"},{"name":"performing arts","shortname":":performing_arts:","category":"activity","emoji_order":"458"},{"name":"artist palette","shortname":":art:","category":"activity","emoji_order":"459"},{"name":"circus tent","shortname":":circus_tent:","category":"activity","emoji_order":"460"},{"name":"microphone","shortname":":microphone:","category":"activity","emoji_order":"461"},{"name":"headphone","shortname":":headphones:","category":"activity","emoji_order":"462"},{"name":"musical score","shortname":":musical_score:","category":"activity","emoji_order":"463"},{"name":"musical keyboard","shortname":":musical_keyboard:","category":"activity","emoji_order":"464"},{"name":"saxophone","shortname":":saxophone:","category":"activity","emoji_order":"465"},{"name":"trumpet","shortname":":trumpet:","category":"activity","emoji_order":"466"},{"name":"guitar","shortname":":guitar:","category":"activity","emoji_order":"467"},{"name":"violin","shortname":":violin:","category":"activity","emoji_order":"468"},{"name":"clapper board","shortname":":clapper:","category":"activity","emoji_order":"469"},{"name":"video game","shortname":":video_game:","category":"activity","emoji_order":"470"},{"name":"alien monster","shortname":":space_invader:","category":"activity","emoji_order":"471"},{"name":"direct hit","shortname":":dart:","category":"activity","emoji_order":"472"},{"name":"game die","shortname":":game_die:","category":"activity","emoji_order":"473"},{"name":"slot machine","shortname":":slot_machine:","category":"activity","emoji_order":"474"},{"name":"bowling","shortname":":bowling:","category":"activity","emoji_order":"475"},{"name":"automobile","shortname":":red_car:","category":"travel","emoji_order":"476"},{"name":"taxi","shortname":":taxi:","category":"travel","emoji_order":"477"},{"name":"recreational vehicle","shortname":":blue_car:","category":"travel","emoji_order":"478"},{"name":"bus","shortname":":bus:","category":"travel","emoji_order":"479"},{"name":"trolleybus","shortname":":trolleybus:","category":"travel","emoji_order":"480"},{"name":"racing car","shortname":":race_car:","category":"travel","emoji_order":"481"},{"name":"police car","shortname":":police_car:","category":"travel","emoji_order":"482"},{"name":"ambulance","shortname":":ambulance:","category":"travel","emoji_order":"483"},{"name":"fire engine","shortname":":fire_engine:","category":"travel","emoji_order":"484"},{"name":"minibus","shortname":":minibus:","category":"travel","emoji_order":"485"},{"name":"delivery truck","shortname":":truck:","category":"travel","emoji_order":"486"},{"name":"articulated lorry","shortname":":articulated_lorry:","category":"travel","emoji_order":"487"},{"name":"tractor","shortname":":tractor:","category":"travel","emoji_order":"488"},{"name":"racing motorcycle","shortname":":motorcycle:","category":"travel","emoji_order":"489"},{"name":"bicycle","shortname":":bike:","category":"travel","emoji_order":"490"},{"name":"police cars revolving light","shortname":":rotating_light:","category":"travel","emoji_order":"491"},{"name":"oncoming police car","shortname":":oncoming_police_car:","category":"travel","emoji_order":"492"},{"name":"oncoming bus","shortname":":oncoming_bus:","category":"travel","emoji_order":"493"},{"name":"oncoming automobile","shortname":":oncoming_automobile:","category":"travel","emoji_order":"494"},{"name":"oncoming taxi","shortname":":oncoming_taxi:","category":"travel","emoji_order":"495"},{"name":"aerial tramway","shortname":":aerial_tramway:","category":"travel","emoji_order":"496"},{"name":"mountain cableway","shortname":":mountain_cableway:","category":"travel","emoji_order":"497"},{"name":"suspension railway","shortname":":suspension_railway:","category":"travel","emoji_order":"498"},{"name":"railway car","shortname":":railway_car:","category":"travel","emoji_order":"499"},{"name":"tram car","shortname":":train:","category":"travel","emoji_order":"500"},{"name":"monorail","shortname":":monorail:","category":"travel","emoji_order":"501"},{"name":"high-speed train","shortname":":bullettrain_side:","category":"travel","emoji_order":"502"},{"name":"high-speed train with bullet nose","shortname":":bullettrain_front:","category":"travel","emoji_order":"503"},{"name":"light rail","shortname":":light_rail:","category":"travel","emoji_order":"504"},{"name":"mountain railway","shortname":":mountain_railway:","category":"travel","emoji_order":"505"},{"name":"steam locomotive","shortname":":steam_locomotive:","category":"travel","emoji_order":"506"},{"name":"train","shortname":":train2:","category":"travel","emoji_order":"507"},{"name":"metro","shortname":":metro:","category":"travel","emoji_order":"508"},{"name":"tram","shortname":":tram:","category":"travel","emoji_order":"509"},{"name":"station","shortname":":station:","category":"travel","emoji_order":"510"},{"name":"helicopter","shortname":":helicopter:","category":"travel","emoji_order":"511"},{"name":"small airplane","shortname":":airplane_small:","category":"travel","emoji_order":"512"},{"name":"airplane","shortname":":airplane:","category":"travel","emoji_order":"513"},{"name":"airplane departure","shortname":":airplane_departure:","category":"travel","emoji_order":"514"},{"name":"airplane arriving","shortname":":airplane_arriving:","category":"travel","emoji_order":"515"},{"name":"sailboat","shortname":":sailboat:","category":"travel","emoji_order":"516"},{"name":"motorboat","shortname":":motorboat:","category":"travel","emoji_order":"517"},{"name":"speedboat","shortname":":speedboat:","category":"travel","emoji_order":"518"},{"name":"ferry","shortname":":ferry:","category":"travel","emoji_order":"519"},{"name":"passenger ship","shortname":":cruise_ship:","category":"travel","emoji_order":"520"},{"name":"rocket","shortname":":rocket:","category":"travel","emoji_order":"521"},{"name":"satellite","shortname":":satellite_orbital:","category":"travel","emoji_order":"522"},{"name":"seat","shortname":":seat:","category":"travel","emoji_order":"523"},{"name":"anchor","shortname":":anchor:","category":"travel","emoji_order":"524"},{"name":"construction sign","shortname":":construction:","category":"travel","emoji_order":"525"},{"name":"fuel pump","shortname":":fuelpump:","category":"travel","emoji_order":"526"},{"name":"bus stop","shortname":":busstop:","category":"travel","emoji_order":"527"},{"name":"vertical traffic light","shortname":":vertical_traffic_light:","category":"travel","emoji_order":"528"},{"name":"horizontal traffic light","shortname":":traffic_light:","category":"travel","emoji_order":"529"},{"name":"chequered flag","shortname":":checkered_flag:","category":"travel","emoji_order":"530"},{"name":"ship","shortname":":ship:","category":"travel","emoji_order":"531"},{"name":"ferris wheel","shortname":":ferris_wheel:","category":"travel","emoji_order":"532"},{"name":"roller coaster","shortname":":roller_coaster:","category":"travel","emoji_order":"533"},{"name":"carousel horse","shortname":":carousel_horse:","category":"travel","emoji_order":"534"},{"name":"building construction","shortname":":construction_site:","category":"travel","emoji_order":"535"},{"name":"foggy","shortname":":foggy:","category":"travel","emoji_order":"536"},{"name":"tokyo tower","shortname":":tokyo_tower:","category":"travel","emoji_order":"537"},{"name":"factory","shortname":":factory:","category":"travel","emoji_order":"538"},{"name":"fountain","shortname":":fountain:","category":"travel","emoji_order":"539"},{"name":"moon viewing ceremony","shortname":":rice_scene:","category":"travel","emoji_order":"540"},{"name":"mountain","shortname":":mountain:","category":"travel","emoji_order":"541"},{"name":"snow capped mountain","shortname":":mountain_snow:","category":"travel","emoji_order":"542"},{"name":"mount fuji","shortname":":mount_fuji:","category":"travel","emoji_order":"543"},{"name":"volcano","shortname":":volcano:","category":"travel","emoji_order":"544"},{"name":"silhouette of japan","shortname":":japan:","category":"travel","emoji_order":"545"},{"name":"camping","shortname":":camping:","category":"travel","emoji_order":"546"},{"name":"tent","shortname":":tent:","category":"travel","emoji_order":"547"},{"name":"national park","shortname":":park:","category":"travel","emoji_order":"548"},{"name":"motorway","shortname":":motorway:","category":"travel","emoji_order":"549"},{"name":"railway track","shortname":":railway_track:","category":"travel","emoji_order":"550"},{"name":"sunrise","shortname":":sunrise:","category":"travel","emoji_order":"551"},{"name":"sunrise over mountains","shortname":":sunrise_over_mountains:","category":"travel","emoji_order":"552"},{"name":"desert","shortname":":desert:","category":"travel","emoji_order":"553"},{"name":"beach with umbrella","shortname":":beach:","category":"travel","emoji_order":"554"},{"name":"desert island","shortname":":island:","category":"travel","emoji_order":"555"},{"name":"sunset over buildings","shortname":":city_sunset:","category":"travel","emoji_order":"556"},{"name":"cityscape at dusk","shortname":":city_dusk:","category":"travel","emoji_order":"557"},{"name":"cityscape","shortname":":cityscape:","category":"travel","emoji_order":"558"},{"name":"night with stars","shortname":":night_with_stars:","category":"travel","emoji_order":"559"},{"name":"bridge at night","shortname":":bridge_at_night:","category":"travel","emoji_order":"560"},{"name":"milky way","shortname":":milky_way:","category":"travel","emoji_order":"561"},{"name":"shooting star","shortname":":stars:","category":"travel","emoji_order":"562"},{"name":"firework sparkler","shortname":":sparkler:","category":"travel","emoji_order":"563"},{"name":"fireworks","shortname":":fireworks:","category":"travel","emoji_order":"564"},{"name":"rainbow","shortname":":rainbow:","category":"travel","emoji_order":"565"},{"name":"house buildings","shortname":":homes:","category":"travel","emoji_order":"566"},{"name":"european castle","shortname":":european_castle:","category":"travel","emoji_order":"567"},{"name":"japanese castle","shortname":":japanese_castle:","category":"travel","emoji_order":"568"},{"name":"stadium","shortname":":stadium:","category":"travel","emoji_order":"569"},{"name":"statue of liberty","shortname":":statue_of_liberty:","category":"travel","emoji_order":"570"},{"name":"house building","shortname":":house:","category":"travel","emoji_order":"571"},{"name":"house with garden","shortname":":house_with_garden:","category":"travel","emoji_order":"572"},{"name":"derelict house building","shortname":":house_abandoned:","category":"travel","emoji_order":"573"},{"name":"office building","shortname":":office:","category":"travel","emoji_order":"574"},{"name":"department store","shortname":":department_store:","category":"travel","emoji_order":"575"},{"name":"japanese post office","shortname":":post_office:","category":"travel","emoji_order":"576"},{"name":"european post office","shortname":":european_post_office:","category":"travel","emoji_order":"577"},{"name":"hospital","shortname":":hospital:","category":"travel","emoji_order":"578"},{"name":"bank","shortname":":bank:","category":"travel","emoji_order":"579"},{"name":"hotel","shortname":":hotel:","category":"travel","emoji_order":"580"},{"name":"convenience store","shortname":":convenience_store:","category":"travel","emoji_order":"581"},{"name":"school","shortname":":school:","category":"travel","emoji_order":"582"},{"name":"love hotel","shortname":":love_hotel:","category":"travel","emoji_order":"583"},{"name":"wedding","shortname":":wedding:","category":"travel","emoji_order":"584"},{"name":"classical building","shortname":":classical_building:","category":"travel","emoji_order":"585"},{"name":"church","shortname":":church:","category":"travel","emoji_order":"586"},{"name":"mosque","shortname":":mosque:","category":"travel","emoji_order":"587"},{"name":"synagogue","shortname":":synagogue:","category":"travel","emoji_order":"588"},{"name":"kaaba","shortname":":kaaba:","category":"travel","emoji_order":"589"},{"name":"shinto shrine","shortname":":shinto_shrine:","category":"travel","emoji_order":"590"},{"name":"watch","shortname":":watch:","category":"objects","emoji_order":"591"},{"name":"mobile phone","shortname":":iphone:","category":"objects","emoji_order":"592"},{"name":"mobile phone with rightwards arrow at left","shortname":":calling:","category":"objects","emoji_order":"593"},{"name":"personal computer","shortname":":computer:","category":"objects","emoji_order":"594"},{"name":"keyboard","shortname":":keyboard:","category":"objects","emoji_order":"595"},{"name":"desktop computer","shortname":":desktop:","category":"objects","emoji_order":"596"},{"name":"printer","shortname":":printer:","category":"objects","emoji_order":"597"},{"name":"three button mouse","shortname":":mouse_three_button:","category":"objects","emoji_order":"598"},{"name":"trackball","shortname":":trackball:","category":"objects","emoji_order":"599"},{"name":"joystick","shortname":":joystick:","category":"objects","emoji_order":"600"},{"name":"compression","shortname":":compression:","category":"objects","emoji_order":"601"},{"name":"minidisc","shortname":":minidisc:","category":"objects","emoji_order":"602"},{"name":"floppy disk","shortname":":floppy_disk:","category":"objects","emoji_order":"603"},{"name":"optical disc","shortname":":cd:","category":"objects","emoji_order":"604"},{"name":"dvd","shortname":":dvd:","category":"objects","emoji_order":"605"},{"name":"videocassette","shortname":":vhs:","category":"objects","emoji_order":"606"},{"name":"camera","shortname":":camera:","category":"objects","emoji_order":"607"},{"name":"camera with flash","shortname":":camera_with_flash:","category":"objects","emoji_order":"608"},{"name":"video camera","shortname":":video_camera:","category":"objects","emoji_order":"609"},{"name":"movie camera","shortname":":movie_camera:","category":"objects","emoji_order":"610"},{"name":"film projector","shortname":":projector:","category":"objects","emoji_order":"611"},{"name":"film frames","shortname":":film_frames:","category":"objects","emoji_order":"612"},{"name":"telephone receiver","shortname":":telephone_receiver:","category":"objects","emoji_order":"613"},{"name":"black telephone","shortname":":telephone:","category":"objects","emoji_order":"614"},{"name":"pager","shortname":":pager:","category":"objects","emoji_order":"615"},{"name":"fax machine","shortname":":fax:","category":"objects","emoji_order":"616"},{"name":"television","shortname":":tv:","category":"objects","emoji_order":"617"},{"name":"radio","shortname":":radio:","category":"objects","emoji_order":"618"},{"name":"studio microphone","shortname":":microphone2:","category":"objects","emoji_order":"619"},{"name":"level slider","shortname":":level_slider:","category":"objects","emoji_order":"620"},{"name":"control knobs","shortname":":control_knobs:","category":"objects","emoji_order":"621"},{"name":"stopwatch","shortname":":stopwatch:","category":"objects","emoji_order":"622"},{"name":"timer clock","shortname":":timer:","category":"objects","emoji_order":"623"},{"name":"alarm clock","shortname":":alarm_clock:","category":"objects","emoji_order":"624"},{"name":"mantlepiece clock","shortname":":clock:","category":"objects","emoji_order":"625"},{"name":"hourglass with flowing sand","shortname":":hourglass_flowing_sand:","category":"objects","emoji_order":"626"},{"name":"hourglass","shortname":":hourglass:","category":"objects","emoji_order":"627"},{"name":"satellite antenna","shortname":":satellite:","category":"objects","emoji_order":"628"},{"name":"battery","shortname":":battery:","category":"objects","emoji_order":"629"},{"name":"electric plug","shortname":":electric_plug:","category":"objects","emoji_order":"630"},{"name":"electric light bulb","shortname":":bulb:","category":"objects","emoji_order":"631"},{"name":"electric torch","shortname":":flashlight:","category":"objects","emoji_order":"632"},{"name":"candle","shortname":":candle:","category":"objects","emoji_order":"633"},{"name":"wastebasket","shortname":":wastebasket:","category":"objects","emoji_order":"634"},{"name":"oil drum","shortname":":oil:","category":"objects","emoji_order":"635"},{"name":"money with wings","shortname":":money_with_wings:","category":"objects","emoji_order":"636"},{"name":"banknote with dollar sign","shortname":":dollar:","category":"objects","emoji_order":"637"},{"name":"banknote with yen sign","shortname":":yen:","category":"objects","emoji_order":"638"},{"name":"banknote with euro sign","shortname":":euro:","category":"objects","emoji_order":"639"},{"name":"banknote with pound sign","shortname":":pound:","category":"objects","emoji_order":"640"},{"name":"money bag","shortname":":moneybag:","category":"objects","emoji_order":"641"},{"name":"credit card","shortname":":credit_card:","category":"objects","emoji_order":"642"},{"name":"gem stone","shortname":":gem:","category":"objects","emoji_order":"643"},{"name":"scales","shortname":":scales:","category":"objects","emoji_order":"644"},{"name":"wrench","shortname":":wrench:","category":"objects","emoji_order":"645"},{"name":"hammer","shortname":":hammer:","category":"objects","emoji_order":"646"},{"name":"hammer and pick","shortname":":hammer_pick:","category":"objects","emoji_order":"647"},{"name":"hammer and wrench","shortname":":tools:","category":"objects","emoji_order":"648"},{"name":"pick","shortname":":pick:","category":"objects","emoji_order":"649"},{"name":"nut and bolt","shortname":":nut_and_bolt:","category":"objects","emoji_order":"650"},{"name":"gear","shortname":":gear:","category":"objects","emoji_order":"651"},{"name":"chains","shortname":":chains:","category":"objects","emoji_order":"652"},{"name":"pistol","shortname":":gun:","category":"objects","emoji_order":"653"},{"name":"bomb","shortname":":bomb:","category":"objects","emoji_order":"654"},{"name":"hocho","shortname":":knife:","category":"objects","emoji_order":"655"},{"name":"dagger knife","shortname":":dagger:","category":"objects","emoji_order":"656"},{"name":"crossed swords","shortname":":crossed_swords:","category":"objects","emoji_order":"657"},{"name":"shield","shortname":":shield:","category":"objects","emoji_order":"658"},{"name":"smoking symbol","shortname":":smoking:","category":"objects","emoji_order":"659"},{"name":"skull and crossbones","shortname":":skull_crossbones:","category":"objects","emoji_order":"660"},{"name":"coffin","shortname":":coffin:","category":"objects","emoji_order":"661"},{"name":"funeral urn","shortname":":urn:","category":"objects","emoji_order":"662"},{"name":"amphora","shortname":":amphora:","category":"objects","emoji_order":"663"},{"name":"crystal ball","shortname":":crystal_ball:","category":"objects","emoji_order":"664"},{"name":"prayer beads","shortname":":prayer_beads:","category":"objects","emoji_order":"665"},{"name":"barber pole","shortname":":barber:","category":"objects","emoji_order":"666"},{"name":"alembic","shortname":":alembic:","category":"objects","emoji_order":"667"},{"name":"telescope","shortname":":telescope:","category":"objects","emoji_order":"668"},{"name":"microscope","shortname":":microscope:","category":"objects","emoji_order":"669"},{"name":"hole","shortname":":hole:","category":"objects","emoji_order":"670"},{"name":"pill","shortname":":pill:","category":"objects","emoji_order":"671"},{"name":"syringe","shortname":":syringe:","category":"objects","emoji_order":"672"},{"name":"thermometer","shortname":":thermometer:","category":"objects","emoji_order":"673"},{"name":"label","shortname":":label:","category":"objects","emoji_order":"674"},{"name":"bookmark","shortname":":bookmark:","category":"objects","emoji_order":"675"},{"name":"toilet","shortname":":toilet:","category":"objects","emoji_order":"676"},{"name":"shower","shortname":":shower:","category":"objects","emoji_order":"677"},{"name":"bathtub","shortname":":bathtub:","category":"objects","emoji_order":"678"},{"name":"key","shortname":":key:","category":"objects","emoji_order":"679"},{"name":"old key","shortname":":key2:","category":"objects","emoji_order":"680"},{"name":"couch and lamp","shortname":":couch:","category":"objects","emoji_order":"681"},{"name":"sleeping accommodation","shortname":":sleeping_accommodation:","category":"objects","emoji_order":"682"},{"name":"bed","shortname":":bed:","category":"objects","emoji_order":"683"},{"name":"door","shortname":":door:","category":"objects","emoji_order":"684"},{"name":"bellhop bell","shortname":":bellhop:","category":"objects","emoji_order":"685"},{"name":"frame with picture","shortname":":frame_photo:","category":"objects","emoji_order":"686"},{"name":"world map","shortname":":map:","category":"objects","emoji_order":"687"},{"name":"umbrella on ground","shortname":":beach_umbrella:","category":"objects","emoji_order":"688"},{"name":"moyai","shortname":":moyai:","category":"objects","emoji_order":"689"},{"name":"shopping bags","shortname":":shopping_bags:","category":"objects","emoji_order":"690"},{"name":"balloon","shortname":":balloon:","category":"objects","emoji_order":"691"},{"name":"carp streamer","shortname":":flags:","category":"objects","emoji_order":"692"},{"name":"ribbon","shortname":":ribbon:","category":"objects","emoji_order":"693"},{"name":"wrapped present","shortname":":gift:","category":"objects","emoji_order":"694"},{"name":"confetti ball","shortname":":confetti_ball:","category":"objects","emoji_order":"695"},{"name":"party popper","shortname":":tada:","category":"objects","emoji_order":"696"},{"name":"japanese dolls","shortname":":dolls:","category":"objects","emoji_order":"697"},{"name":"wind chime","shortname":":wind_chime:","category":"objects","emoji_order":"698"},{"name":"crossed flags","shortname":":crossed_flags:","category":"objects","emoji_order":"699"},{"name":"izakaya lantern","shortname":":izakaya_lantern:","category":"objects","emoji_order":"700"},{"name":"envelope","shortname":":envelope:","category":"objects","emoji_order":"701"},{"name":"envelope with downwards arrow above","shortname":":envelope_with_arrow:","category":"objects","emoji_order":"702"},{"name":"incoming envelope","shortname":":incoming_envelope:","category":"objects","emoji_order":"703"},{"name":"e-mail symbol","shortname":":e-mail:","category":"objects","emoji_order":"704"},{"name":"love letter","shortname":":love_letter:","category":"objects","emoji_order":"705"},{"name":"postbox","shortname":":postbox:","category":"objects","emoji_order":"706"},{"name":"closed mailbox with lowered flag","shortname":":mailbox_closed:","category":"objects","emoji_order":"707"},{"name":"closed mailbox with raised flag","shortname":":mailbox:","category":"objects","emoji_order":"708"},{"name":"open mailbox with raised flag","shortname":":mailbox_with_mail:","category":"objects","emoji_order":"709"},{"name":"open mailbox with lowered flag","shortname":":mailbox_with_no_mail:","category":"objects","emoji_order":"710"},{"name":"package","shortname":":package:","category":"objects","emoji_order":"711"},{"name":"postal horn","shortname":":postal_horn:","category":"objects","emoji_order":"712"},{"name":"inbox tray","shortname":":inbox_tray:","category":"objects","emoji_order":"713"},{"name":"outbox tray","shortname":":outbox_tray:","category":"objects","emoji_order":"714"},{"name":"scroll","shortname":":scroll:","category":"objects","emoji_order":"715"},{"name":"page with curl","shortname":":page_with_curl:","category":"objects","emoji_order":"716"},{"name":"bookmark tabs","shortname":":bookmark_tabs:","category":"objects","emoji_order":"717"},{"name":"bar chart","shortname":":bar_chart:","category":"objects","emoji_order":"718"},{"name":"chart with upwards trend","shortname":":chart_with_upwards_trend:","category":"objects","emoji_order":"719"},{"name":"chart with downwards trend","shortname":":chart_with_downwards_trend:","category":"objects","emoji_order":"720"},{"name":"page facing up","shortname":":page_facing_up:","category":"objects","emoji_order":"721"},{"name":"calendar","shortname":":date:","category":"objects","emoji_order":"722"},{"name":"tear-off calendar","shortname":":calendar:","category":"objects","emoji_order":"723"},{"name":"spiral calendar pad","shortname":":calendar_spiral:","category":"objects","emoji_order":"724"},{"name":"card index","shortname":":card_index:","category":"objects","emoji_order":"725"},{"name":"card file box","shortname":":card_box:","category":"objects","emoji_order":"726"},{"name":"ballot box with ballot","shortname":":ballot_box:","category":"objects","emoji_order":"727"},{"name":"file cabinet","shortname":":file_cabinet:","category":"objects","emoji_order":"728"},{"name":"clipboard","shortname":":clipboard:","category":"objects","emoji_order":"729"},{"name":"spiral note pad","shortname":":notepad_spiral:","category":"objects","emoji_order":"730"},{"name":"file folder","shortname":":file_folder:","category":"objects","emoji_order":"731"},{"name":"open file folder","shortname":":open_file_folder:","category":"objects","emoji_order":"732"},{"name":"card index dividers","shortname":":dividers:","category":"objects","emoji_order":"733"},{"name":"rolled-up newspaper","shortname":":newspaper2:","category":"objects","emoji_order":"734"},{"name":"newspaper","shortname":":newspaper:","category":"objects","emoji_order":"735"},{"name":"notebook","shortname":":notebook:","category":"objects","emoji_order":"736"},{"name":"closed book","shortname":":closed_book:","category":"objects","emoji_order":"737"},{"name":"green book","shortname":":green_book:","category":"objects","emoji_order":"738"},{"name":"blue book","shortname":":blue_book:","category":"objects","emoji_order":"739"},{"name":"orange book","shortname":":orange_book:","category":"objects","emoji_order":"740"},{"name":"notebook with decorative cover","shortname":":notebook_with_decorative_cover:","category":"objects","emoji_order":"741"},{"name":"ledger","shortname":":ledger:","category":"objects","emoji_order":"742"},{"name":"books","shortname":":books:","category":"objects","emoji_order":"743"},{"name":"open book","shortname":":book:","category":"objects","emoji_order":"744"},{"name":"link symbol","shortname":":link:","category":"objects","emoji_order":"745"},{"name":"paperclip","shortname":":paperclip:","category":"objects","emoji_order":"746"},{"name":"linked paperclips","shortname":":paperclips:","category":"objects","emoji_order":"747"},{"name":"black scissors","shortname":":scissors:","category":"objects","emoji_order":"748"},{"name":"triangular ruler","shortname":":triangular_ruler:","category":"objects","emoji_order":"749"},{"name":"straight ruler","shortname":":straight_ruler:","category":"objects","emoji_order":"750"},{"name":"pushpin","shortname":":pushpin:","category":"objects","emoji_order":"751"},{"name":"round pushpin","shortname":":round_pushpin:","category":"objects","emoji_order":"752"},{"name":"triangular flag on post","shortname":":triangular_flag_on_post:","category":"objects","emoji_order":"753"},{"name":"waving white flag","shortname":":flag_white:","category":"objects","emoji_order":"754"},{"name":"waving black flag","shortname":":flag_black:","category":"objects","emoji_order":"755"},{"name":"closed lock with key","shortname":":closed_lock_with_key:","category":"objects","emoji_order":"756"},{"name":"lock","shortname":":lock:","category":"objects","emoji_order":"757"},{"name":"open lock","shortname":":unlock:","category":"objects","emoji_order":"758"},{"name":"lock with ink pen","shortname":":lock_with_ink_pen:","category":"objects","emoji_order":"759"},{"name":"lower left ballpoint pen","shortname":":pen_ballpoint:","category":"objects","emoji_order":"760"},{"name":"lower left fountain pen","shortname":":pen_fountain:","category":"objects","emoji_order":"761"},{"name":"black nib","shortname":":black_nib:","category":"objects","emoji_order":"762"},{"name":"memo","shortname":":pencil:","category":"objects","emoji_order":"763"},{"name":"pencil","shortname":":pencil2:","category":"objects","emoji_order":"764"},{"name":"lower left crayon","shortname":":crayon:","category":"objects","emoji_order":"765"},{"name":"lower left paintbrush","shortname":":paintbrush:","category":"objects","emoji_order":"766"},{"name":"left-pointing magnifying glass","shortname":":mag:","category":"objects","emoji_order":"767"},{"name":"right-pointing magnifying glass","shortname":":mag_right:","category":"objects","emoji_order":"768"},{"name":"heavy black heart","shortname":":heart:","category":"symbols","emoji_order":"769","aliases_ascii":["<3"]},{"name":"yellow heart","shortname":":yellow_heart:","category":"symbols","emoji_order":"770"},{"name":"green heart","shortname":":green_heart:","category":"symbols","emoji_order":"771"},{"name":"blue heart","shortname":":blue_heart:","category":"symbols","emoji_order":"772"},{"name":"purple heart","shortname":":purple_heart:","category":"symbols","emoji_order":"773"},{"name":"broken heart","shortname":":broken_heart:","category":"symbols","emoji_order":"774","aliases_ascii":[" { - // initialise with a few events - for (var i = 0; i < 10; i++) { - events.push(i+90); + return q().then(() => { + // initialise with a load of events + for (let i = 0; i < 20; i++) { + events.push(i+80); } tester.setTileKeys(events); - expect(tester.fillCounts.b).toEqual(1); - expect(tester.fillCounts.f).toEqual(2); - expect(scrollingDiv.scrollHeight).toEqual(1550) // 10*150 + 50 - expect(scrollingDiv.scrollTop).toEqual(1550 - 600); + expect(scrollingDiv.scrollHeight).toEqual(3050); // 20*150 + 50 + expect(scrollingDiv.scrollTop).toEqual(3050 - 600); return tester.awaitScroll(); }).then(() => { - expect(tester.lastScrollEvent).toBe(950); + expect(tester.lastScrollEvent).toBe(3050 - 600); - // we want to simulate back-filling as we scroll up - tester.addFillHandler('b', function() { - var newEvents = []; - for (var i = 0; i < 10; i++) { - newEvents.push(i+80); - } - events.unshift.apply(events, newEvents); - tester.setTileKeys(events); - return q(true); - }); - - // simulate scrolling up; this should trigger the backfill - scrollingDiv.scrollTop = 200; - - return tester.awaitFill('b'); - }).then(() => { - console.log('filled'); + tester.scrollPanel().scrollToToken("92", 0); // at this point, ScrollPanel will have updated scrollTop, but - // the event hasn't fired. Stamp over the scrollTop. - expect(tester.lastScrollEvent).toEqual(200); - expect(scrollingDiv.scrollTop).toEqual(10*150 + 200); + // the event hasn't fired. + expect(tester.lastScrollEvent).toEqual(3050 - 600); + expect(scrollingDiv.scrollTop).toEqual(1950); + + // now stamp over the scrollTop. + console.log('faking #528'); scrollingDiv.scrollTop = 500; return tester.awaitScroll(); }).then(() => { - expect(tester.lastScrollEvent).toBe(10*150 + 200); - expect(scrollingDiv.scrollTop).toEqual(10*150 + 200); - }).done(done); + expect(tester.lastScrollEvent).toBe(1950); + expect(scrollingDiv.scrollTop).toEqual(1950); + }); }); it('should not get stuck in #528 workaround', function(done) { @@ -250,7 +235,7 @@ describe('ScrollPanel', function() { tester.setTileKeys(events); expect(tester.fillCounts.b).toEqual(1); expect(tester.fillCounts.f).toEqual(2); - expect(scrollingDiv.scrollHeight).toEqual(6050) // 40*150 + 50 + expect(scrollingDiv.scrollHeight).toEqual(6050); // 40*150 + 50 expect(scrollingDiv.scrollTop).toEqual(6050 - 600); // try to scroll up, to a non-integer offset. diff --git a/test/components/views/rooms/MessageComposerInput-test.js b/test/components/views/rooms/MessageComposerInput-test.js index e2e2836a50..80fd158608 100644 --- a/test/components/views/rooms/MessageComposerInput-test.js +++ b/test/components/views/rooms/MessageComposerInput-test.js @@ -99,7 +99,7 @@ describe('MessageComposerInput', () => { }); it('should not change content unnecessarily on Markdown -> RTE conversion', () => { - const spy = sinon.spy(client, 'sendHtmlMessage'); + const spy = sinon.spy(client, 'sendTextMessage'); mci.enableRichtext(false); addTextToDraft('a'); mci.handleKeyCommand('toggle-mode'); @@ -109,8 +109,8 @@ describe('MessageComposerInput', () => { expect(spy.args[0][1]).toEqual('a'); }); - it('should send emoji messages in rich text', () => { - const spy = sinon.spy(client, 'sendHtmlMessage'); + it('should send emoji messages when rich text is enabled', () => { + const spy = sinon.spy(client, 'sendTextMessage'); mci.enableRichtext(true); addTextToDraft('☹'); mci.handleReturn(sinon.stub()); @@ -118,7 +118,7 @@ describe('MessageComposerInput', () => { expect(spy.calledOnce).toEqual(true, 'should send message'); }); - it('should send emoji messages in Markdown', () => { + it('should send emoji messages when Markdown is enabled', () => { const spy = sinon.spy(client, 'sendTextMessage'); mci.enableRichtext(false); addTextToDraft('☹');