diff --git a/.eslintignore.errorfiles b/.eslintignore.errorfiles index 55eaf75e4b..498dfb8818 100644 --- a/.eslintignore.errorfiles +++ b/.eslintignore.errorfiles @@ -33,7 +33,6 @@ src/components/views/create_room/CreateRoomButton.js src/components/views/create_room/Presets.js src/components/views/create_room/RoomAlias.js src/components/views/dialogs/ChatCreateOrReuseDialog.js -src/components/views/dialogs/ChatInviteDialog.js src/components/views/dialogs/DeactivateAccountDialog.js src/components/views/dialogs/InteractiveAuthDialog.js src/components/views/dialogs/SetMxIdDialog.js @@ -114,7 +113,6 @@ src/components/views/settings/EnableNotificationsButton.js src/ContentMessages.js src/HtmlUtils.js src/ImageUtils.js -src/Invite.js src/languageHandler.js src/linkify-matrix.js src/Login.js diff --git a/README.md b/README.md index 0f5ef73365..144e89c938 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ are currently filed against vector-im/riot-web rather than this project). Translation Status ================== -[![translationsstatus](https://translate.nordgedanken.de/widgets/riot-web/-/multi-auto.svg)](https://translate.nordgedanken.de/engage/riot-web/?utm_source=widget) +[![Translation status](https://translate.riot.im/widgets/riot-web/-/multi-auto.svg)](https://translate.riot.im/engage/riot-web/?utm_source=widget) Developer Guide =============== diff --git a/jenkins.sh b/jenkins.sh index a0e8d2e893..0979edfa13 100755 --- a/jenkins.sh +++ b/jenkins.sh @@ -4,7 +4,7 @@ set -e export NVM_DIR="$HOME/.nvm" [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" -nvm use 4 +nvm use 6 set -x diff --git a/package.json b/package.json index a3bab88d45..661db4b6bc 100644 --- a/package.json +++ b/package.json @@ -53,9 +53,9 @@ "classnames": "^2.1.2", "commonmark": "^0.27.0", "counterpart": "^0.18.0", - "draft-js": "^0.10.1", - "draft-js-export-html": "^0.5.0", - "draft-js-export-markdown": "^0.2.0", + "draft-js": "^0.11.0-alpha", + "draft-js-export-html": "^0.6.0", + "draft-js-export-markdown": "^0.3.0", "emojione": "2.2.7", "file-saver": "^1.3.3", "filesize": "3.5.6", diff --git a/src/Analytics.js b/src/Analytics.js index 92691da1ea..a82f57a144 100644 --- a/src/Analytics.js +++ b/src/Analytics.js @@ -15,7 +15,6 @@ */ import { getCurrentLanguage } from './languageHandler'; -import MatrixClientPeg from './MatrixClientPeg'; import PlatformPeg from './PlatformPeg'; import SdkConfig from './SdkConfig'; @@ -31,8 +30,18 @@ const customVariables = { 'User Type': 3, 'Chosen Language': 4, 'Instance': 5, + 'RTE: Uses Richtext Mode': 6, + 'Homeserver URL': 7, + 'Identity Server URL': 8, }; +function whitelistRedact(whitelist, str) { + if (whitelist.includes(str)) return str; + return ''; +} + +const whitelistedHSUrls = ["https://matrix.org"]; +const whitelistedISUrls = ["https://vector.im"]; class Analytics { constructor() { @@ -76,7 +85,7 @@ class Analytics { this._paq.push(['trackAllContentImpressions']); this._paq.push(['discardHashTag', false]); this._paq.push(['enableHeartBeatTimer']); - this._paq.push(['enableLinkTracking', true]); + // this._paq.push(['enableLinkTracking', true]); const platform = PlatformPeg.get(); this._setVisitVariable('App Platform', platform.getHumanReadableName()); @@ -130,20 +139,20 @@ class Analytics { this._paq.push(['deleteCookies']); } - login() { // not used currently - const cli = MatrixClientPeg.get(); - if (this.disabled || !cli) return; - - this._paq.push(['setUserId', `@${cli.getUserIdLocalpart()}:${cli.getDomain()}`]); - } - _setVisitVariable(key, value) { this._paq.push(['setCustomVariable', customVariables[key], key, value, 'visit']); } - setGuest(guest) { + setLoggedIn(isGuest, homeserverUrl, identityServerUrl) { if (this.disabled) return; - this._setVisitVariable('User Type', guest ? 'Guest' : 'Logged In'); + this._setVisitVariable('User Type', isGuest ? 'Guest' : 'Logged In'); + this._setVisitVariable('Homeserver URL', whitelistRedact(whitelistedHSUrls, homeserverUrl)); + this._setVisitVariable('Identity Server URL', whitelistRedact(whitelistedISUrls, identityServerUrl)); + } + + setRichtextMode(state) { + if (this.disabled) return; + this._setVisitVariable('RTE: Uses Richtext Mode', state ? 'on' : 'off'); } } diff --git a/src/CallHandler.js b/src/CallHandler.js index e3fbe9e5e3..8331d579df 100644 --- a/src/CallHandler.js +++ b/src/CallHandler.js @@ -143,7 +143,7 @@ function _setCallListeners(call) { pause("ringbackAudio"); play("busyAudio"); var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - Modal.createDialog(ErrorDialog, { + Modal.createTrackedDialog('Call Handler', 'Call Timeout', ErrorDialog, { title: _t('Call Timeout'), description: _t('The remote side failed to pick up') + '.', }); @@ -205,7 +205,7 @@ function _onAction(payload) { _setCallState(undefined, newCall.roomId, "ended"); console.log("Can't capture screen: " + screenCapErrorString); const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - Modal.createDialog(ErrorDialog, { + Modal.createTrackedDialog('Call Handler', 'Unable to capture screen', ErrorDialog, { title: _t('Unable to capture screen'), description: screenCapErrorString, }); @@ -225,7 +225,7 @@ function _onAction(payload) { case 'place_call': if (module.exports.getAnyActiveCall()) { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - Modal.createDialog(ErrorDialog, { + Modal.createTrackedDialog('Call Handler', 'Existing Call', ErrorDialog, { title: _t('Existing Call'), description: _t('You are already in a call.'), }); @@ -235,7 +235,7 @@ function _onAction(payload) { // if the runtime env doesn't do VoIP, whine. if (!MatrixClientPeg.get().supportsVoip()) { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - Modal.createDialog(ErrorDialog, { + Modal.createTrackedDialog('Call Handler', 'VoIP is unsupported', ErrorDialog, { title: _t('VoIP is unsupported'), description: _t('You cannot place VoIP calls in this browser.'), }); @@ -251,7 +251,7 @@ function _onAction(payload) { var members = room.getJoinedMembers(); if (members.length <= 1) { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - Modal.createDialog(ErrorDialog, { + Modal.createTrackedDialog('Call Handler', 'Cannot place call with self', ErrorDialog, { description: _t('You cannot place a call with yourself.'), }); return; @@ -277,13 +277,13 @@ function _onAction(payload) { console.log("Place conference call in %s", payload.room_id); if (!ConferenceHandler) { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - Modal.createDialog(ErrorDialog, { + Modal.createTrackedDialog('Call Handler', 'Conference call unsupported client', ErrorDialog, { description: _t('Conference calls are not supported in this client'), }); } else if (!MatrixClientPeg.get().supportsVoip()) { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - Modal.createDialog(ErrorDialog, { + Modal.createTrackedDialog('Call Handler', 'VoIP is unsupported', ErrorDialog, { title: _t('VoIP is unsupported'), description: _t('You cannot place VoIP calls in this browser.'), }); @@ -296,13 +296,13 @@ function _onAction(payload) { // participant. // Therefore we disable conference calling in E2E rooms. const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - Modal.createDialog(ErrorDialog, { + Modal.createTrackedDialog('Call Handler', 'Conference calls unsupported e2e', ErrorDialog, { description: _t('Conference calls are not supported in encrypted rooms'), }); } else { var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); - Modal.createDialog(QuestionDialog, { + Modal.createTrackedDialog('Call Handler', 'Conference calling in development', QuestionDialog, { title: _t('Warning!'), description: _t('Conference calling is in development and may not be reliable.'), onFinished: confirm=>{ @@ -314,7 +314,7 @@ function _onAction(payload) { }, function(err) { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); console.error("Conference call failed: " + err); - Modal.createDialog(ErrorDialog, { + Modal.createTrackedDialog('Call Handler', 'Failed to set up conference call', ErrorDialog, { title: _t('Failed to set up conference call'), description: _t('Conference call failed.') + ' ' + ((err && err.message) ? err.message : ''), }); diff --git a/src/ComposerHistoryManager.js b/src/ComposerHistoryManager.js index 1ae836574b..2fff3882b4 100644 --- a/src/ComposerHistoryManager.js +++ b/src/ComposerHistoryManager.js @@ -15,36 +15,38 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {ContentState} from 'draft-js'; +import {ContentState, convertToRaw, convertFromRaw} from 'draft-js'; import * as RichText from './RichText'; import Markdown from './Markdown'; -import _flow from 'lodash/flow'; import _clamp from 'lodash/clamp'; type MessageFormat = 'html' | 'markdown'; class HistoryItem { - message: string = ''; + + // Keeping message for backwards-compatibility + message: string; + rawContentState: RawDraftContentState; format: MessageFormat = 'html'; - constructor(message: string, format: MessageFormat) { - this.message = message; + constructor(contentState: ?ContentState, format: ?MessageFormat) { + this.rawContentState = contentState ? convertToRaw(contentState) : null; this.format = format; } - toContentState(format: MessageFormat): ContentState { - let {message} = this; - if (format === 'markdown') { + toContentState(outputFormat: MessageFormat): ContentState { + const contentState = convertFromRaw(this.rawContentState); + if (outputFormat === 'markdown') { if (this.format === 'html') { - message = _flow([RichText.htmlToContentState, RichText.stateToMarkdown])(message); + return ContentState.createFromText(RichText.stateToMarkdown(contentState)); } - return ContentState.createFromText(message); } else { if (this.format === 'markdown') { - message = new Markdown(message).toHTML(); + return RichText.htmlToContentState(new Markdown(contentState.getPlainText()).toHTML()); } - return RichText.htmlToContentState(message); } + // history item has format === outputFormat + return contentState; } } @@ -67,8 +69,8 @@ export default class ComposerHistoryManager { this.lastIndex = this.currentIndex; } - addItem(message: string, format: MessageFormat) { - const item = new HistoryItem(message, format); + save(contentState: ContentState, format: MessageFormat) { + const item = new HistoryItem(contentState, format); this.history.push(item); this.currentIndex = this.lastIndex + 1; sessionStorage.setItem(`${this.prefix}[${this.lastIndex++}]`, JSON.stringify(item)); diff --git a/src/ContentMessages.js b/src/ContentMessages.js index 9239de9d8f..93057fafed 100644 --- a/src/ContentMessages.js +++ b/src/ContentMessages.js @@ -360,7 +360,7 @@ class ContentMessages { desc = _t('The file \'%(fileName)s\' exceeds this home server\'s size limit for uploads', {fileName: upload.fileName}); } var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - Modal.createDialog(ErrorDialog, { + Modal.createTrackedDialog('Upload failed', '', ErrorDialog, { title: _t('Upload Failed'), description: desc, }); diff --git a/src/Invite.js b/src/Invite.js index 0e8aca2cb5..b8e33d318a 100644 --- a/src/Invite.js +++ b/src/Invite.js @@ -1,5 +1,6 @@ /* Copyright 2016 OpenMarket Ltd +Copyright 2017 New Vector Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -16,24 +17,11 @@ limitations under the License. import MatrixClientPeg from './MatrixClientPeg'; import MultiInviter from './utils/MultiInviter'; - -const emailRegex = /^\S+@\S+\.\S+$/; - -const mxidRegex = /^@\S+:\S+$/ - -export function getAddressType(inputText) { - const isEmailAddress = emailRegex.test(inputText); - const isMatrixId = mxidRegex.test(inputText); - - // sanity check the input for user IDs - if (isEmailAddress) { - return 'email'; - } else if (isMatrixId) { - return 'mx'; - } else { - return null; - } -} +import Modal from './Modal'; +import { getAddressType } from './UserAddress'; +import createRoom from './createRoom'; +import sdk from './'; +import { _t } from './languageHandler'; export function inviteToRoom(roomId, addr) { const addrType = getAddressType(addr); @@ -52,12 +40,116 @@ export function inviteToRoom(roomId, addr) { * Simpler interface to utils/MultiInviter but with * no option to cancel. * - * @param {roomId} The ID of the room to invite to - * @param {array} Array of strings of addresses to invite. May be matrix IDs or 3pids. - * @returns Promise + * @param {string} roomId The ID of the room to invite to + * @param {string[]} addrs Array of strings of addresses to invite. May be matrix IDs or 3pids. + * @returns {Promise} Promise */ export function inviteMultipleToRoom(roomId, addrs) { const inviter = new MultiInviter(roomId); return inviter.invite(addrs); } +export function showStartChatInviteDialog() { + const UserPickerDialog = sdk.getComponent("dialogs.UserPickerDialog"); + Modal.createTrackedDialog('Start a chat', '', UserPickerDialog, { + title: _t('Start a chat'), + description: _t("Who would you like to communicate with?"), + placeholder: _t("Email, name or matrix ID"), + button: _t("Start Chat"), + onFinished: _onStartChatFinished, + }); +} + +export function showRoomInviteDialog(roomId) { + const UserPickerDialog = sdk.getComponent("dialogs.UserPickerDialog"); + Modal.createTrackedDialog('Chat Invite', '', UserPickerDialog, { + title: _t('Invite new room members'), + description: _t('Who would you like to add to this room?'), + button: _t('Send Invites'), + placeholder: _t("Email, name or matrix ID"), + onFinished: (shouldInvite, addrs) => { + _onRoomInviteFinished(roomId, shouldInvite, addrs); + }, + }); +} + +function _onStartChatFinished(shouldInvite, addrs) { + if (!shouldInvite) return; + + const addrTexts = addrs.map((addr) => addr.address); + + if (_isDmChat(addrTexts)) { + // Start a new DM chat + createRoom({dmUserId: addrTexts[0]}).catch((err) => { + console.error(err.stack); + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + Modal.createTrackedDialog('Failed to invite user', '', ErrorDialog, { + title: _t("Failed to invite user"), + description: ((err && err.message) ? err.message : _t("Operation failed")), + }); + }); + } else { + // Start multi user chat + let room; + createRoom().then((roomId) => { + room = MatrixClientPeg.get().getRoom(roomId); + return inviteMultipleToRoom(roomId, addrTexts); + }).then((addrs) => { + return _showAnyInviteErrors(addrs, room); + }).catch((err) => { + console.error(err.stack); + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + Modal.createTrackedDialog('Failed to invite', '', ErrorDialog, { + title: _t("Failed to invite"), + description: ((err && err.message) ? err.message : _t("Operation failed")), + }); + }); + } +} + +function _onRoomInviteFinished(roomId, shouldInvite, addrs) { + if (!shouldInvite) return; + + const addrTexts = addrs.map((addr) => addr.address); + + // Invite new users to a room + inviteMultipleToRoom(roomId, addrTexts).then((addrs) => { + const room = MatrixClientPeg.get().getRoom(roomId); + return _showAnyInviteErrors(addrs, room); + }).catch((err) => { + console.error(err.stack); + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + Modal.createTrackedDialog('Failed to invite', '', ErrorDialog, { + title: _t("Failed to invite"), + description: ((err && err.message) ? err.message : _t("Operation failed")), + }); + }); +} + +function _isDmChat(addrTexts) { + if (addrTexts.length === 1 && getAddressType(addrTexts[0])) { + return true; + } else { + return false; + } +} + +function _showAnyInviteErrors(addrs, room) { + // Show user any errors + const errorList = []; + for (const addr of Object.keys(addrs)) { + if (addrs[addr] === "error") { + errorList.push(addr); + } + } + + if (errorList.length > 0) { + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + Modal.createTrackedDialog('Failed to invite the following users to the room', '', ErrorDialog, { + title: _t("Failed to invite the following users to the %(roomName)s room:", {roomName: room.name}), + description: errorList.join(", "), + }); + } + return addrs; +} + diff --git a/src/KeyRequestHandler.js b/src/KeyRequestHandler.js index 1da4922153..0b54d88e5f 100644 --- a/src/KeyRequestHandler.js +++ b/src/KeyRequestHandler.js @@ -125,7 +125,7 @@ export default class KeyRequestHandler { }; const KeyShareDialog = sdk.getComponent("dialogs.KeyShareDialog"); - Modal.createDialog(KeyShareDialog, { + Modal.createTrackedDialog('Key Share', 'Process Next Request', KeyShareDialog, { matrixClient: this._matrixClient, userId: userId, deviceId: deviceId, diff --git a/src/Lifecycle.js b/src/Lifecycle.js index eb2156e780..4d8911f7a6 100644 --- a/src/Lifecycle.js +++ b/src/Lifecycle.js @@ -240,7 +240,7 @@ function _handleRestoreFailure(e) { const SessionRestoreErrorDialog = sdk.getComponent('views.dialogs.SessionRestoreErrorDialog'); - Modal.createDialog(SessionRestoreErrorDialog, { + Modal.createTrackedDialog('Session Restore Error', '', SessionRestoreErrorDialog, { error: e.message, onFinished: (success) => { def.resolve(success); @@ -318,7 +318,7 @@ async function _doSetLoggedIn(credentials, clearStorage) { await _clearStorage(); } - Analytics.setGuest(credentials.guest); + Analytics.setLoggedIn(credentials.guest, credentials.homeserverUrl, credentials.identityServerUrl); // Resolves by default let teamPromise = Promise.resolve(null); diff --git a/src/Markdown.js b/src/Markdown.js index 5730e42a09..6e735c6f0e 100644 --- a/src/Markdown.js +++ b/src/Markdown.js @@ -55,6 +55,25 @@ function is_multi_line(node) { return par.firstChild != par.lastChild; } +import linkifyMatrix from './linkify-matrix'; +import * as linkify from 'linkifyjs'; +linkifyMatrix(linkify); + +// Thieved from draft-js-export-markdown +function escapeMarkdown(s) { + return s.replace(/[*_`]/g, '\\$&'); +} + +// Replace URLs, room aliases and user IDs with md-escaped URLs +function linkifyMarkdown(s) { + const links = linkify.find(s); + links.forEach((l) => { + // This may replace several instances of `l.value` at once, but that's OK + s = s.replace(l.value, escapeMarkdown(l.value)); + }); + return s; +} + /** * Class that wraps commonmark, adding the ability to see whether * a given message actually uses any markdown syntax or whether @@ -62,7 +81,7 @@ function is_multi_line(node) { */ export default class Markdown { constructor(input) { - this.input = input; + this.input = linkifyMarkdown(input); const parser = new commonmark.Parser(); this.parsed = parser.parse(this.input); diff --git a/src/Modal.js b/src/Modal.js index e100105a88..056b6d8bf2 100644 --- a/src/Modal.js +++ b/src/Modal.js @@ -103,13 +103,20 @@ class ModalManager { return container; } + createTrackedDialog(analyticsAction, analyticsInfo, Element, props, className) { + Analytics.trackEvent('Modal', analyticsAction, analyticsInfo); + return this.createDialog(Element, props, className); + } + createDialog(Element, props, className) { - if (props && props.title) { - Analytics.trackEvent('Modal', props.title, 'createDialog'); - } return this.createDialogAsync((cb) => {cb(Element);}, props, className); } + createTrackedDialogAsync(analyticsAction, analyticsInfo, loader, props, className) { + Analytics.trackEvent('Modal', analyticsAction, analyticsInfo); + return this.createDialogAsync(loader, props, className); + } + /** * Open a modal view. * diff --git a/src/Notifier.js b/src/Notifier.js index 40a65d4106..1bb435307d 100644 --- a/src/Notifier.js +++ b/src/Notifier.js @@ -142,7 +142,7 @@ const Notifier = { ? _t('Riot does not have permission to send you notifications - please check your browser settings') : _t('Riot was not given permission to send notifications - please try again'); const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog'); - Modal.createDialog(ErrorDialog, { + Modal.createTrackedDialog('Unable to enable Notifications', result, ErrorDialog, { title: _t('Unable to enable Notifications'), description, }); diff --git a/src/RichText.js b/src/RichText.js index c060565e2f..cbd3b9ae18 100644 --- a/src/RichText.js +++ b/src/RichText.js @@ -51,7 +51,8 @@ export const contentStateToHTML = (contentState: ContentState) => { }; export function htmlToContentState(html: string): ContentState { - return ContentState.createFromBlockArray(convertFromHTML(html)); + const blockArray = convertFromHTML(html).contentBlocks; + return ContentState.createFromBlockArray(blockArray); } function unicodeToEmojiUri(str) { @@ -90,7 +91,7 @@ function findWithRegex(regex, contentBlock: ContentBlock, callback: (start: numb // Workaround for https://github.com/facebook/draft-js/issues/414 let emojiDecorator = { - strategy: (contentBlock, callback) => { + strategy: (contentState, contentBlock, callback) => { findWithRegex(EMOJI_REGEX, contentBlock, callback); }, component: (props) => { @@ -119,7 +120,7 @@ export function getScopedRTDecorators(scope: any): CompositeDecorator { export function getScopedMDDecorators(scope: any): CompositeDecorator { let markdownDecorators = ['HR', 'BOLD', 'ITALIC', 'CODE', 'STRIKETHROUGH'].map( (style) => ({ - strategy: (contentBlock, callback) => { + strategy: (contentState, contentBlock, callback) => { return findWithRegex(MARKDOWN_REGEX[style], contentBlock, callback); }, component: (props) => ( @@ -130,7 +131,7 @@ export function getScopedMDDecorators(scope: any): CompositeDecorator { })); markdownDecorators.push({ - strategy: (contentBlock, callback) => { + strategy: (contentState, contentBlock, callback) => { return findWithRegex(MARKDOWN_REGEX.LINK, contentBlock, callback); }, component: (props) => ( @@ -201,31 +202,36 @@ export function selectionStateToTextOffsets(selectionState: SelectionState, export function textOffsetsToSelectionState({start, end}: SelectionRange, contentBlocks: Array): SelectionState { let selectionState = SelectionState.createEmpty(); - - for (let block of contentBlocks) { - let blockLength = block.getLength(); - - if (start !== -1 && start < blockLength) { - selectionState = selectionState.merge({ - anchorKey: block.getKey(), - anchorOffset: start, - }); - start = -1; - } else { - start -= blockLength; + // Subtract block lengths from `start` and `end` until they are less than the current + // block length (accounting for the NL at the end of each block). Set them to -1 to + // indicate that the corresponding selection state has been determined. + for (const block of contentBlocks) { + const blockLength = block.getLength(); + // -1 indicating that the position start position has been found + if (start !== -1) { + if (start < blockLength + 1) { + selectionState = selectionState.merge({ + anchorKey: block.getKey(), + anchorOffset: start, + }); + start = -1; // selection state for the start calculated + } else { + start -= blockLength + 1; // +1 to account for newline between blocks + } } - - if (end !== -1 && end <= blockLength) { - selectionState = selectionState.merge({ - focusKey: block.getKey(), - focusOffset: end, - }); - end = -1; - } else { - end -= blockLength; + // -1 indicating that the position end position has been found + if (end !== -1) { + if (end < blockLength + 1) { + selectionState = selectionState.merge({ + focusKey: block.getKey(), + focusOffset: end, + }); + end = -1; // selection state for the end calculated + } else { + end -= blockLength + 1; // +1 to account for newline between blocks + } } } - return selectionState; } @@ -242,7 +248,7 @@ export function attachImmutableEntitiesToEmoji(editorState: EditorState): Editor const existingEntityKey = block.getEntityAt(start); if (existingEntityKey) { // avoid manipulation in case the emoji already has an entity - const entity = Entity.get(existingEntityKey); + const entity = newContentState.getEntity(existingEntityKey); if (entity && entity.get('type') === 'emoji') { return; } @@ -252,7 +258,10 @@ export function attachImmutableEntitiesToEmoji(editorState: EditorState): Editor .set('anchorOffset', start) .set('focusOffset', end); const emojiText = plainText.substring(start, end); - const entityKey = Entity.create('emoji', 'IMMUTABLE', { emojiUnicode: emojiText }); + newContentState = newContentState.createEntity( + 'emoji', 'IMMUTABLE', { emojiUnicode: emojiText } + ); + const entityKey = newContentState.getLastCreatedEntityKey(); newContentState = Modifier.replaceText( newContentState, selection, diff --git a/src/SlashCommands.js b/src/SlashCommands.js index dea3d27751..e5378d4347 100644 --- a/src/SlashCommands.js +++ b/src/SlashCommands.js @@ -68,7 +68,7 @@ const commands = { ddg: new Command("ddg", "", function(roomId, args) { const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog'); // TODO Don't explain this away, actually show a search UI here. - Modal.createDialog(ErrorDialog, { + Modal.createTrackedDialog('Slash Commands', '/ddg is not a command', ErrorDialog, { title: _t('/ddg is not a command'), description: _t('To use it, just wait for autocomplete results to load and tab through them.'), }); @@ -326,13 +326,11 @@ const commands = { {deviceId: deviceId, fprint: fprint, userId: userId, fingerprint: fingerprint})); } - return MatrixClientPeg.get().setDeviceVerified( - userId, deviceId, true, - ); + return MatrixClientPeg.get().setDeviceVerified(userId, deviceId, true); }).then(() => { // Tell the user we verified everything const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); - Modal.createDialog(QuestionDialog, { + Modal.createTrackedDialog('Slash Commands', 'Verified key', QuestionDialog, { title: _t("Verified key"), description: (
diff --git a/src/UnknownDeviceErrorHandler.js b/src/UnknownDeviceErrorHandler.js index 2b1cf23380..e7d77b3b66 100644 --- a/src/UnknownDeviceErrorHandler.js +++ b/src/UnknownDeviceErrorHandler.js @@ -24,7 +24,7 @@ const onAction = function(payload) { if (payload.action === 'unknown_device_error' && !isDialogOpen) { const UnknownDeviceDialog = sdk.getComponent('dialogs.UnknownDeviceDialog'); isDialogOpen = true; - Modal.createDialog(UnknownDeviceDialog, { + Modal.createTrackedDialog('Unknown Device Error', '', UnknownDeviceDialog, { devices: payload.err.devices, room: payload.room, onFinished: (r) => { diff --git a/src/UserAddress.js b/src/UserAddress.js new file mode 100644 index 0000000000..9eee48629d --- /dev/null +++ b/src/UserAddress.js @@ -0,0 +1,54 @@ +/* +Copyright 2017 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +const emailRegex = /^\S+@\S+\.\S+$/; + +const mxidRegex = /^@\S+:\S+$/; + +import PropTypes from 'prop-types'; +export const addressTypes = [ + 'mx', 'email', +]; + +// PropType definition for an object describing +// an address that can be invited to a room (which +// could be a third party identifier or a matrix ID) +// along with some additional information about the +// address / target. +export const UserAddressType = PropTypes.shape({ + addressType: PropTypes.oneOf(addressTypes).isRequired, + address: PropTypes.string.isRequired, + displayName: PropTypes.string, + avatarMxc: PropTypes.string, + // true if the address is known to be a valid address (eg. is a real + // user we've seen) or false otherwise (eg. is just an address the + // user has entered) + isKnown: PropTypes.bool, +}); + +export function getAddressType(inputText) { + const isEmailAddress = emailRegex.test(inputText); + const isMatrixId = mxidRegex.test(inputText); + + // sanity check the input for user IDs + if (isEmailAddress) { + return 'email'; + } else if (isMatrixId) { + return 'mx'; + } else { + return null; + } +} diff --git a/src/UserSettingsStore.js b/src/UserSettingsStore.js index 4c66c90598..68a1ba229f 100644 --- a/src/UserSettingsStore.js +++ b/src/UserSettingsStore.js @@ -28,7 +28,10 @@ export default { { name: "-", id: 'matrix_apps', - default: false, + default: true, + + // XXX: Always use default, ignore localStorage and remove from labs + override: true, }, ], @@ -171,22 +174,36 @@ export default { localStorage.setItem('mx_local_settings', JSON.stringify(settings)); }, - isFeatureEnabled: function(feature: string): boolean { + getFeatureById(feature: string) { + for (let i = 0; i < this.LABS_FEATURES.length; i++) { + const f = this.LABS_FEATURES[i]; + if (f.id === feature) { + return f; + } + } + return null; + }, + + isFeatureEnabled: function(featureId: string): boolean { // Disable labs for guests. if (MatrixClientPeg.get().isGuest()) return false; - if (localStorage.getItem(`mx_labs_feature_${feature}`) === null) { - for (let i = 0; i < this.LABS_FEATURES.length; i++) { - const f = this.LABS_FEATURES[i]; - if (f.id === feature) { - return f.default; - } - } + const feature = this.getFeatureById(featureId); + if (!feature) { + console.warn(`Unknown feature "${featureId}"`); + return false; } - return localStorage.getItem(`mx_labs_feature_${feature}`) === 'true'; + // Return the default if this feature has an override to be the default value or + // if the feature has never been toggled and is therefore not in localStorage + if (Object.keys(feature).includes('override') || + localStorage.getItem(`mx_labs_feature_${featureId}`) === null + ) { + return feature.default; + } + return localStorage.getItem(`mx_labs_feature_${featureId}`) === 'true'; }, - setFeatureEnabled: function(feature: string, enabled: boolean) { - localStorage.setItem(`mx_labs_feature_${feature}`, enabled); + setFeatureEnabled: function(featureId: string, enabled: boolean) { + localStorage.setItem(`mx_labs_feature_${featureId}`, enabled); }, }; diff --git a/src/autocomplete/RoomProvider.js b/src/autocomplete/RoomProvider.js index 3749e7e693..1770089eb2 100644 --- a/src/autocomplete/RoomProvider.js +++ b/src/autocomplete/RoomProvider.js @@ -49,6 +49,12 @@ export default class RoomProvider extends AutocompleteProvider { async getCompletions(query: string, selection: {start: number, end: number}, force = false) { const RoomAvatar = sdk.getComponent('views.avatars.RoomAvatar'); + // Disable autocompletions when composing commands because of various issues + // (see https://github.com/vector-im/riot-web/issues/4762) + if (/^(\/join|\/leave)/.test(query)) { + return []; + } + const client = MatrixClientPeg.get(); let completions = []; const {command, range} = this.getCurrentCommand(query, selection, force); diff --git a/src/autocomplete/UserProvider.js b/src/autocomplete/UserProvider.js index 9c93cf537f..017491a07e 100644 --- a/src/autocomplete/UserProvider.js +++ b/src/autocomplete/UserProvider.js @@ -40,7 +40,7 @@ export default class UserProvider extends AutocompleteProvider { keys: ['name'], }); this.matcher = new FuzzyMatcher([], { - keys: ['name'], + keys: ['name', 'userId'], shouldMatchPrefix: true, }); } @@ -48,13 +48,21 @@ export default class UserProvider extends AutocompleteProvider { async getCompletions(query: string, selection: {start: number, end: number}, force = false) { const MemberAvatar = sdk.getComponent('views.avatars.MemberAvatar'); + // Disable autocompletions when composing commands because of various issues + // (see https://github.com/vector-im/riot-web/issues/4762) + if (/^(\/ban|\/unban|\/op|\/deop|\/invite|\/kick|\/verify)/.test(query)) { + return []; + } + let completions = []; let {command, range} = this.getCurrentCommand(query, selection, force); if (command) { completions = this.matcher.match(command[0]).map((user) => { const displayName = (user.name || user.userId || '').replace(' (IRC)', ''); // FIXME when groups are done return { - completion: displayName, + // Length of completion should equal length of text in decorator. draft-js + // relies on the length of the entity === length of the text in the decoration. + completion: user.rawDisplayName.replace(' (IRC)', ''), suffix: range.start === 0 ? ': ' : ' ', href: 'https://matrix.to/#/' + user.userId, component: ( diff --git a/src/components/structures/GroupView.js b/src/components/structures/GroupView.js index 5f7866773d..20fc4841ba 100644 --- a/src/components/structures/GroupView.js +++ b/src/components/structures/GroupView.js @@ -266,7 +266,7 @@ export default React.createClass({ this.setState({uploadingAvatar: false}); const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); console.error("Failed to upload avatar image", e); - Modal.createDialog(ErrorDialog, { + Modal.createTrackedDialog('Failed to upload image', '', ErrorDialog, { title: _t('Error'), description: _t('Failed to upload image'), }); @@ -288,7 +288,7 @@ export default React.createClass({ }); const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); console.error("Failed to save group profile", e); - Modal.createDialog(ErrorDialog, { + Modal.createTrackedDialog('Failed to update group', '', ErrorDialog, { title: _t('Error'), description: _t('Failed to update group'), }); diff --git a/src/components/structures/LoggedInView.js b/src/components/structures/LoggedInView.js index 093fae5d7b..0790a5766e 100644 --- a/src/components/structures/LoggedInView.js +++ b/src/components/structures/LoggedInView.js @@ -301,13 +301,13 @@ export default React.createClass({ case PageTypes.UserView: page_element = null; // deliberately null for now - right_panel = ; + right_panel = ; break; case PageTypes.GroupView: page_element = ; - //right_panel = ; + //right_panel = ; break; } diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 6d95c99948..c648b9db06 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -1,6 +1,7 @@ /* Copyright 2015, 2016 OpenMarket Ltd Copyright 2017 Vector Creations Ltd +Copyright 2017 New Vector Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -31,6 +32,7 @@ import dis from "../../dispatcher"; import Modal from "../../Modal"; import Tinter from "../../Tinter"; import sdk from '../../index'; +import { showStartChatInviteDialog, showRoomInviteDialog } from '../../Invite'; import * as Rooms from '../../Rooms'; import linkifyMatrix from "../../linkify-matrix"; import * as Lifecycle from '../../Lifecycle'; @@ -131,9 +133,6 @@ module.exports = React.createClass({ // the master view we are showing. view: VIEWS.LOADING, - // a thing to call showScreen with once login completes. - screenAfterLogin: this.props.initialScreenAfterLogin, - // What the LoggedInView would be showing if visible page_type: null, @@ -147,8 +146,6 @@ module.exports = React.createClass({ collapse_lhs: false, collapse_rhs: false, - ready: false, - width: 10000, leftOpacity: 1.0, middleOpacity: 1.0, rightOpacity: 1.0, @@ -274,6 +271,15 @@ module.exports = React.createClass({ register_hs_url: paramHs, }); } + + // a thing to call showScreen with once login completes. this is kept + // outside this.state because updating it should never trigger a + // rerender. + this._screenAfterLogin = this.props.initialScreenAfterLogin; + + this._windowWidth = 10000; + this.handleResize(); + window.addEventListener('resize', this.handleResize); }, componentDidMount: function() { @@ -294,9 +300,6 @@ module.exports = React.createClass({ linkifyMatrix.onGroupClick = this.onGroupClick; } - window.addEventListener('resize', this.handleResize); - this.handleResize(); - const teamServerConfig = this.props.config.teamServerConfig || {}; Lifecycle.initRtsClient(teamServerConfig.teamServerURL); @@ -312,13 +315,12 @@ module.exports = React.createClass({ // if the user has followed a login or register link, don't reanimate // the old creds, but rather go straight to the relevant page - const firstScreen = this.state.screenAfterLogin ? - this.state.screenAfterLogin.screen : null; + const firstScreen = this._screenAfterLogin ? + this._screenAfterLogin.screen : null; if (firstScreen === 'login' || firstScreen === 'register' || firstScreen === 'forgot_password') { - this.setState({loading: false}); this._showScreenAfterLogin(); return; } @@ -367,9 +369,9 @@ module.exports = React.createClass({ } const newState = { viewUserId: null, - }; - Object.assign(newState, state); - this.setState(newState); + }; + Object.assign(newState, state); + this.setState(newState); }, onAction: function(payload) { @@ -410,7 +412,7 @@ module.exports = React.createClass({ this._leaveRoom(payload.room_id); break; case 'reject_invite': - Modal.createDialog(QuestionDialog, { + Modal.createTrackedDialog('Reject invitation', '', QuestionDialog, { title: _t('Reject invitation'), description: _t('Are you sure you want to reject the invitation?'), onFinished: (confirm) => { @@ -426,7 +428,7 @@ module.exports = React.createClass({ } }, (err) => { modal.close(); - Modal.createDialog(ErrorDialog, { + Modal.createTrackedDialog('Failed to reject invitation', '', ErrorDialog, { title: _t('Failed to reject invitation'), description: err.toString(), }); @@ -512,7 +514,7 @@ module.exports = React.createClass({ this._createChat(); break; case 'view_invite': - this._invite(payload.roomId); + showRoomInviteDialog(payload.roomId); break; case 'notifier_enabled': this.forceUpdate(); @@ -728,7 +730,7 @@ module.exports = React.createClass({ _setMxId: function(payload) { const SetMxIdDialog = sdk.getComponent('views.dialogs.SetMxIdDialog'); - const close = Modal.createDialog(SetMxIdDialog, { + const close = Modal.createTrackedDialog('Set MXID', '', SetMxIdDialog, { homeserverUrl: MatrixClientPeg.get().getHomeserverUrl(), onFinished: (submitted, credentials) => { if (!submitted) { @@ -766,13 +768,7 @@ module.exports = React.createClass({ dis.dispatch({action: 'view_set_mxid'}); return; } - const ChatInviteDialog = sdk.getComponent("dialogs.ChatInviteDialog"); - Modal.createDialog(ChatInviteDialog, { - title: _t('Start a chat'), - description: _t("Who would you like to communicate with?"), - placeholder: _t("Email, name or matrix ID"), - button: _t("Start Chat"), - }); + showStartChatInviteDialog(); }, _createRoom: function() { @@ -787,7 +783,7 @@ module.exports = React.createClass({ return; } const CreateRoomDialog = sdk.getComponent('dialogs.CreateRoomDialog'); - Modal.createDialog(CreateRoomDialog, { + Modal.createTrackedDialog('Create Room', '', CreateRoomDialog, { onFinished: (shouldCreate, name, noFederate) => { if (shouldCreate) { const createOpts = {}; @@ -829,7 +825,7 @@ module.exports = React.createClass({ return; } - const close = Modal.createDialog(ChatCreateOrReuseDialog, { + const close = Modal.createTrackedDialog('Chat create or reuse', '', ChatCreateOrReuseDialog, { userId: userId, onFinished: (success) => { if (!success && goHomeOnCancel) { @@ -855,23 +851,12 @@ module.exports = React.createClass({ }).close; }, - _invite: function(roomId) { - const ChatInviteDialog = sdk.getComponent("dialogs.ChatInviteDialog"); - Modal.createDialog(ChatInviteDialog, { - title: _t('Invite new room members'), - description: _t('Who would you like to add to this room?'), - button: _t('Send Invites'), - placeholder: _t("Email, name or matrix ID"), - roomId: roomId, - }); - }, - _leaveRoom: function(roomId) { const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); const roomToLeave = MatrixClientPeg.get().getRoom(roomId); - Modal.createDialog(QuestionDialog, { + Modal.createTrackedDialog('Leave room', '', QuestionDialog, { title: _t("Leave room"), description: ( @@ -894,7 +879,7 @@ module.exports = React.createClass({ }, (err) => { modal.close(); console.error("Failed to leave room " + roomId + " " + err); - Modal.createDialog(ErrorDialog, { + Modal.createTrackedDialog('Failed to leave room', '', ErrorDialog, { title: _t("Failed to leave room"), description: (err && err.message ? err.message : _t("Server may be unavailable, overloaded, or you hit a bug.")), @@ -990,14 +975,12 @@ module.exports = React.createClass({ _showScreenAfterLogin: function() { // If screenAfterLogin is set, use that, then null it so that a second login will // result in view_home_page, _user_settings or _room_directory - if (this.state.screenAfterLogin && this.state.screenAfterLogin.screen) { + if (this._screenAfterLogin && this._screenAfterLogin.screen) { this.showScreen( - this.state.screenAfterLogin.screen, - this.state.screenAfterLogin.params, + this._screenAfterLogin.screen, + this._screenAfterLogin.params, ); - // XXX: is this necessary? `showScreen` should do it for us. - this.notifyNewScreen(this.state.screenAfterLogin.screen); - this.setState({screenAfterLogin: null}); + this._screenAfterLogin = null; } else if (localStorage && localStorage.getItem('mx_last_room_id')) { // Before defaulting to directory, show the last viewed room dis.dispatch({ @@ -1090,7 +1073,7 @@ module.exports = React.createClass({ }); cli.on('Session.logged_out', function(call) { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - Modal.createDialog(ErrorDialog, { + Modal.createTrackedDialog('Signed out', '', ErrorDialog, { title: _t('Signed Out'), description: _t('For security, this session has been signed out. Please sign in again.'), }); @@ -1203,21 +1186,24 @@ module.exports = React.createClass({ } else if (screen.indexOf('user/') == 0) { const userId = screen.substring(5); - if (params.action === 'chat') { - this._chatCreateOrReuse(userId); - return; - } + // Wait for the first sync so that `getRoom` gives us a room object if it's + // in the sync response + const waitFor = this.firstSyncPromise ? + this.firstSyncPromise.promise : Promise.resolve(); + waitFor.then(() => { + if (params.action === 'chat') { + this._chatCreateOrReuse(userId); + return; + } - this.setState({ viewUserId: userId }); - this._setPage(PageTypes.UserView); - this.notifyNewScreen('user/' + userId); - const member = new Matrix.RoomMember(null, userId); - if (member) { + this._setPage(PageTypes.UserView); + this.notifyNewScreen('user/' + userId); + const member = new Matrix.RoomMember(null, userId); dis.dispatch({ action: 'view_user', member: member, }); - } + }); } else if (screen.indexOf('group/') == 0) { const groupId = screen.substring(6); @@ -1274,20 +1260,20 @@ module.exports = React.createClass({ const hideRhsThreshold = 820; const showRhsThreshold = 820; - if (this.state.width > hideLhsThreshold && window.innerWidth <= hideLhsThreshold) { + if (this._windowWidth > hideLhsThreshold && window.innerWidth <= hideLhsThreshold) { dis.dispatch({ action: 'hide_left_panel' }); } - if (this.state.width <= showLhsThreshold && window.innerWidth > showLhsThreshold) { + if (this._windowWidth <= showLhsThreshold && window.innerWidth > showLhsThreshold) { dis.dispatch({ action: 'show_left_panel' }); } - if (this.state.width > hideRhsThreshold && window.innerWidth <= hideRhsThreshold) { + if (this._windowWidth > hideRhsThreshold && window.innerWidth <= hideRhsThreshold) { dis.dispatch({ action: 'hide_right_panel' }); } - if (this.state.width <= showRhsThreshold && window.innerWidth > showRhsThreshold) { + if (this._windowWidth <= showRhsThreshold && window.innerWidth > showRhsThreshold) { dis.dispatch({ action: 'show_right_panel' }); } - this.setState({width: window.innerWidth}); + this._windowWidth = window.innerWidth; }, onRoomCreated: function(roomId) { diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index 6f4a5460f6..02f224e942 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -307,13 +307,13 @@ module.exports = React.createClass({ for (i = 0; i < this.props.events.length; i++) { let mxEv = this.props.events[i]; let eventId = mxEv.getId(); - let readMarkerInMels = false; let last = (mxEv === lastShownEvent); const wantTile = this._shouldShowEvent(mxEv); // Wrap consecutive member events in a ListSummary, ignore if redacted if (isMembershipChange(mxEv) && wantTile) { + let readMarkerInMels = false; let ts1 = mxEv.getTs(); // Ensure that the key of the MemberEventListSummary does not change with new // member events. This will prevent it from being re-created unnecessarily, and @@ -330,6 +330,11 @@ module.exports = React.createClass({ ret.push(dateSeparator); } + // If RM event is the first in the MELS, append the RM after MELS + if (mxEv.getId() === this.props.readMarkerEventId) { + readMarkerInMels = true; + } + let summarisedEvents = [mxEv]; for (;i + 1 < this.props.events.length; i++) { const collapsedMxEv = this.props.events[i + 1]; diff --git a/src/components/structures/MyGroups.js b/src/components/structures/MyGroups.js index 3eb694acce..0b8055beda 100644 --- a/src/components/structures/MyGroups.js +++ b/src/components/structures/MyGroups.js @@ -63,7 +63,7 @@ export default withMatrixClient(React.createClass({ _onCreateGroupClick: function() { const CreateGroupDialog = sdk.getComponent("dialogs.CreateGroupDialog"); - Modal.createDialog(CreateGroupDialog); + Modal.createTrackedDialog('Create Group', '', CreateGroupDialog); }, _fetch: function() { diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 094251f4c1..f825d1efbb 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -544,7 +544,7 @@ module.exports = React.createClass({ } if (!userHasUsedEncryption) { const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); - Modal.createDialog(QuestionDialog, { + Modal.createTrackedDialog('E2E Warning', '', QuestionDialog, { title: _t("Warning!"), hasCancelButton: false, description: ( @@ -820,7 +820,7 @@ module.exports = React.createClass({ }); const SetMxIdDialog = sdk.getComponent('views.dialogs.SetMxIdDialog'); - const close = Modal.createDialog(SetMxIdDialog, { + const close = Modal.createTrackedDialog('Set MXID', '', SetMxIdDialog, { homeserverUrl: cli.getHomeserverUrl(), onFinished: (submitted, credentials) => { if (submitted) { @@ -934,7 +934,7 @@ module.exports = React.createClass({ } const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); console.error("Failed to upload file " + file + " " + error); - Modal.createDialog(ErrorDialog, { + Modal.createTrackedDialog('Failed to upload file', '', ErrorDialog, { title: _t('Failed to upload file'), description: ((error && error.message) ? error.message : _t("Server may be unavailable, overloaded, or the file too big")), }); @@ -1021,7 +1021,7 @@ module.exports = React.createClass({ }, function(error) { var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); console.error("Search failed: " + error); - Modal.createDialog(ErrorDialog, { + Modal.createTrackedDialog('Search failed', '', ErrorDialog, { title: _t("Search failed"), description: ((error && error.message) ? error.message : _t("Server may be unavailable, overloaded, or search timed out :(")), }); @@ -1148,7 +1148,7 @@ module.exports = React.createClass({ console.error(result.reason); }); var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - Modal.createDialog(ErrorDialog, { + Modal.createTrackedDialog('Failed to save room settings', '', ErrorDialog, { title: _t("Failed to save settings"), description: fails.map(function(result) { return result.reason; }).join("\n"), }); @@ -1195,7 +1195,7 @@ module.exports = React.createClass({ }, function(err) { var errCode = err.errcode || _t("unknown error code"); var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - Modal.createDialog(ErrorDialog, { + Modal.createTrackedDialog('Failed to forget room', '', ErrorDialog, { title: _t("Error"), description: _t("Failed to forget room %(errCode)s", { errCode: errCode }), }); @@ -1217,7 +1217,7 @@ module.exports = React.createClass({ var msg = error.message ? error.message : JSON.stringify(error); var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - Modal.createDialog(ErrorDialog, { + Modal.createTrackedDialog('Failed to reject invite', '', ErrorDialog, { title: _t("Failed to reject invite"), description: msg, }); diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index 0aee19545c..6be31361dd 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -923,7 +923,7 @@ var TimelinePanel = React.createClass({ var message = (error.errcode == 'M_FORBIDDEN') ? _t("Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.") : _t("Tried to load a specific point in this room's timeline, but was unable to find it."); - Modal.createDialog(ErrorDialog, { + Modal.createTrackedDialog('Failed to load timeline position', '', ErrorDialog, { title: _t("Failed to load timeline position"), description: message, onFinished: onFinished, diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index 1e0fcff445..3c139f77a6 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -85,6 +85,10 @@ const SETTINGS_LABELS = [ id: 'hideJoinLeaves', label: 'Hide join/leave messages (invites/kicks/bans unaffected)', }, + { + id: 'hideAvatarDisplaynameChanges', + label: 'Hide avatar and display name changes', + }, { id: 'useCompactLayout', label: 'Use compact timeline layout', @@ -101,6 +105,10 @@ const SETTINGS_LABELS = [ id: 'MessageComposerInput.autoReplaceEmoji', label: 'Automatically replace plain text Emoji', }, + { + id: 'Pill.shouldHidePillAvatar', + label: 'Hide avatars in user and room mentions', + }, /* { id: 'useFixedWidthFont', @@ -331,7 +339,7 @@ module.exports = React.createClass({ }, function(error) { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); console.error("Failed to load user settings: " + error); - Modal.createDialog(ErrorDialog, { + Modal.createTrackedDialog('Can\'t load user settings', '', ErrorDialog, { title: _t("Can't load user settings"), description: ((error && error.message) ? error.message : _t("Server may be unavailable or overloaded")), }); @@ -364,7 +372,7 @@ module.exports = React.createClass({ // const errMsg = (typeof err === "string") ? err : (err.error || ""); console.error("Failed to set avatar: " + err); const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - Modal.createDialog(ErrorDialog, { + Modal.createTrackedDialog('Failed to set avatar', '', ErrorDialog, { title: _t("Failed to set avatar."), description: ((err && err.message) ? err.message : _t("Operation failed")), }); @@ -373,7 +381,7 @@ module.exports = React.createClass({ onLogoutClicked: function(ev) { const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); - Modal.createDialog(QuestionDialog, { + Modal.createTrackedDialog('Logout E2E Export', '', QuestionDialog, { title: _t("Sign out"), description:
@@ -409,7 +417,7 @@ module.exports = React.createClass({ } const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); console.error("Failed to change password: " + errMsg); - Modal.createDialog(ErrorDialog, { + Modal.createTrackedDialog('Failed to change password', '', ErrorDialog, { title: _t("Error"), description: errMsg, }); @@ -417,7 +425,7 @@ module.exports = React.createClass({ onPasswordChanged: function() { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - Modal.createDialog(ErrorDialog, { + Modal.createTrackedDialog('Password changed', '', ErrorDialog, { title: _t("Success"), description: _t( "Your password was successfully changed. You will not receive " + @@ -442,7 +450,7 @@ module.exports = React.createClass({ const emailAddress = this.refs.add_email_input.value; if (!Email.looksValid(emailAddress)) { - Modal.createDialog(ErrorDialog, { + Modal.createTrackedDialog('Invalid email address', '', ErrorDialog, { title: _t("Invalid Email Address"), description: _t("This doesn't appear to be a valid email address"), }); @@ -452,7 +460,7 @@ module.exports = React.createClass({ // we always bind emails when registering, so let's do the // same here. this._addThreepid.addEmailAddress(emailAddress, true).done(() => { - Modal.createDialog(QuestionDialog, { + Modal.createTrackedDialog('Verification Pending', '', QuestionDialog, { title: _t("Verification Pending"), description: _t( "Please check your email and click on the link it contains. Once this " + @@ -464,7 +472,7 @@ module.exports = React.createClass({ }, (err) => { this.setState({email_add_pending: false}); console.error("Unable to add email address " + emailAddress + " " + err); - Modal.createDialog(ErrorDialog, { + Modal.createTrackedDialog('Unable to add email address', '', ErrorDialog, { title: _t("Unable to add email address"), description: ((err && err.message) ? err.message : _t("Operation failed")), }); @@ -475,7 +483,7 @@ module.exports = React.createClass({ onRemoveThreepidClicked: function(threepid) { const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); - Modal.createDialog(QuestionDialog, { + Modal.createTrackedDialog('Remove 3pid', '', QuestionDialog, { title: _t("Remove Contact Information?"), description: _t("Remove %(threePid)s?", { threePid: threepid.address }), button: _t('Remove'), @@ -489,7 +497,7 @@ module.exports = React.createClass({ }).catch((err) => { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); console.error("Unable to remove contact information: " + err); - Modal.createDialog(ErrorDialog, { + Modal.createTrackedDialog('Remove 3pid failed', '', ErrorDialog, { title: _t("Unable to remove contact information"), description: ((err && err.message) ? err.message : _t("Operation failed")), }); @@ -521,7 +529,7 @@ module.exports = React.createClass({ const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); const message = _t("Unable to verify email address.") + " " + _t("Please check your email and click on the link it contains. Once this is done, click continue."); - Modal.createDialog(QuestionDialog, { + Modal.createTrackedDialog('Verification Pending', '', QuestionDialog, { title: _t("Verification Pending"), description: message, button: _t('Continue'), @@ -530,7 +538,7 @@ module.exports = React.createClass({ } else { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); console.error("Unable to verify email address: " + err); - Modal.createDialog(ErrorDialog, { + Modal.createTrackedDialog('Unable to verify email address', '', ErrorDialog, { title: _t("Unable to verify email address."), description: ((err && err.message) ? err.message : _t("Operation failed")), }); @@ -540,7 +548,7 @@ module.exports = React.createClass({ _onDeactivateAccountClicked: function() { const DeactivateAccountDialog = sdk.getComponent("dialogs.DeactivateAccountDialog"); - Modal.createDialog(DeactivateAccountDialog, {}); + Modal.createTrackedDialog('Deactivate Account', '', DeactivateAccountDialog, {}); }, _onBugReportClicked: function() { @@ -548,7 +556,7 @@ module.exports = React.createClass({ if (!BugReportDialog) { return; } - Modal.createDialog(BugReportDialog, {}); + Modal.createTrackedDialog('Bug Report Dialog', '', BugReportDialog, {}); }, _onClearCacheClicked: function() { @@ -585,27 +593,23 @@ module.exports = React.createClass({ }, _onExportE2eKeysClicked: function() { - Modal.createDialogAsync( - (cb) => { - require.ensure(['../../async-components/views/dialogs/ExportE2eKeysDialog'], () => { - cb(require('../../async-components/views/dialogs/ExportE2eKeysDialog')); - }, "e2e-export"); - }, { - matrixClient: MatrixClientPeg.get(), - }, - ); + Modal.createTrackedDialogAsync('Export E2E Keys', '', (cb) => { + require.ensure(['../../async-components/views/dialogs/ExportE2eKeysDialog'], () => { + cb(require('../../async-components/views/dialogs/ExportE2eKeysDialog')); + }, "e2e-export"); + }, { + matrixClient: MatrixClientPeg.get(), + }); }, _onImportE2eKeysClicked: function() { - Modal.createDialogAsync( - (cb) => { - require.ensure(['../../async-components/views/dialogs/ImportE2eKeysDialog'], () => { - cb(require('../../async-components/views/dialogs/ImportE2eKeysDialog')); - }, "e2e-export"); - }, { - matrixClient: MatrixClientPeg.get(), - }, - ); + Modal.createTrackedDialogAsync('Import E2E Keys', '', (cb) => { + require.ensure(['../../async-components/views/dialogs/ImportE2eKeysDialog'], () => { + cb(require('../../async-components/views/dialogs/ImportE2eKeysDialog')); + }, "e2e-export"); + }, { + matrixClient: MatrixClientPeg.get(), + }); }, _renderReferral: function() { @@ -855,7 +859,13 @@ module.exports = React.createClass({ if (this.props.enableLabs === false) return null; UserSettingsStore.doTranslations(); - const features = UserSettingsStore.LABS_FEATURES.map((feature) => { + const features = []; + UserSettingsStore.LABS_FEATURES.forEach((feature) => { + // This feature has an override and will be set to the default, so do not + // show it here. + if (feature.override) { + return; + } // TODO: this ought to be a separate component so that we don't need // to rebind the onChange each time we render const onChange = (e) => { @@ -863,7 +873,7 @@ module.exports = React.createClass({ this.forceUpdate(); }; - return ( + features.push(
-
- ); +
); }); + + // No labs section when there are no features in labs + if (features.length === 0) { + return null; + } + return (

{ _t("Labs") }

@@ -1004,7 +1019,7 @@ module.exports = React.createClass({ this._refreshMediaDevices, function() { const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog'); - Modal.createDialog(ErrorDialog, { + Modal.createTrackedDialog('No media permissions', '', ErrorDialog, { title: _t('No media permissions'), description: _t('You may need to manually permit Riot to access your microphone/webcam'), }); diff --git a/src/components/structures/login/ForgotPassword.js b/src/components/structures/login/ForgotPassword.js index 18a9dca5dd..320d21f5b4 100644 --- a/src/components/structures/login/ForgotPassword.js +++ b/src/components/structures/login/ForgotPassword.js @@ -89,14 +89,14 @@ module.exports = React.createClass({ } else { var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); - Modal.createDialog(QuestionDialog, { + Modal.createTrackedDialog('Forgot Password Warning', '', QuestionDialog, { title: _t('Warning!'), description:
{ _t( 'Resetting password will currently reset any ' + 'end-to-end encryption keys on all devices, ' + - 'making encrypted chat history unreadable, ' + + 'making encrypted chat history unreadable, ' + 'unless you first export your room keys and re-import ' + 'them afterwards. In future this will be improved.' ) } @@ -121,15 +121,13 @@ module.exports = React.createClass({ }, _onExportE2eKeysClicked: function() { - Modal.createDialogAsync( - (cb) => { - require.ensure(['../../../async-components/views/dialogs/ExportE2eKeysDialog'], () => { - cb(require('../../../async-components/views/dialogs/ExportE2eKeysDialog')); - }, "e2e-export"); - }, { - matrixClient: MatrixClientPeg.get(), - } - ); + Modal.createTrackedDialogAsync('Export E2E Keys', 'Forgot Password', (cb) => { + require.ensure(['../../../async-components/views/dialogs/ExportE2eKeysDialog'], () => { + cb(require('../../../async-components/views/dialogs/ExportE2eKeysDialog')); + }, "e2e-export"); + }, { + matrixClient: MatrixClientPeg.get(), + }); }, onInputChanged: function(stateKey, ev) { @@ -152,7 +150,7 @@ module.exports = React.createClass({ showErrorDialog: function(body, title) { var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - Modal.createDialog(ErrorDialog, { + Modal.createTrackedDialog('Forgot Password Error', '', ErrorDialog, { title: title, description: body, }); diff --git a/src/components/structures/login/Login.js b/src/components/structures/login/Login.js index a081d2a205..a6c0a70c66 100644 --- a/src/components/structures/login/Login.js +++ b/src/components/structures/login/Login.js @@ -19,8 +19,11 @@ limitations under the License. import React from 'react'; import { _t, _tJsx } from '../../../languageHandler'; +import * as languageHandler from '../../../languageHandler'; import sdk from '../../../index'; import Login from '../../../Login'; +import UserSettingsStore from '../../../UserSettingsStore'; +import PlatformPeg from '../../../PlatformPeg'; // For validating phone numbers without country codes const PHONE_NUMBER_REGEX = /^[0-9\(\)\-\s]*$/; @@ -306,6 +309,23 @@ module.exports = React.createClass({ } }, + _onLanguageChange: function(newLang) { + if(languageHandler.getCurrentLanguage() !== newLang) { + UserSettingsStore.setLocalSetting('language', newLang); + PlatformPeg.get().reload(); + } + }, + + _renderLanguageSetting: function() { + const LanguageDropdown = sdk.getComponent('views.elements.LanguageDropdown'); + return
+ +
; + }, + render: function() { const Loader = sdk.getComponent("elements.Spinner"); const LoginHeader = sdk.getComponent("login.LoginHeader"); @@ -354,6 +374,7 @@ module.exports = React.createClass({ { loginAsGuestJsx } { returnToAppJsx } + { this._renderLanguageSetting() }
diff --git a/src/components/views/dialogs/DeactivateAccountDialog.js b/src/components/views/dialogs/DeactivateAccountDialog.js index e3b7cca078..0ee264b69b 100644 --- a/src/components/views/dialogs/DeactivateAccountDialog.js +++ b/src/components/views/dialogs/DeactivateAccountDialog.js @@ -17,6 +17,7 @@ limitations under the License. import React from 'react'; import sdk from '../../../index'; +import Analytics from '../../../Analytics'; import MatrixClientPeg from '../../../MatrixClientPeg'; import * as Lifecycle from '../../../Lifecycle'; import Velocity from 'velocity-vector'; @@ -54,6 +55,7 @@ export default class DeactivateAccountDialog extends React.Component { user: MatrixClientPeg.get().credentials.userId, password: this._passwordField.value, }).done(() => { + Analytics.trackEvent('Account', 'Deactivate Account'); Lifecycle.onLoggedOut(); this.props.onFinished(false); }, (err) => { diff --git a/src/components/views/dialogs/ErrorDialog.js b/src/components/views/dialogs/ErrorDialog.js index bf48d1757b..beca107252 100644 --- a/src/components/views/dialogs/ErrorDialog.js +++ b/src/components/views/dialogs/ErrorDialog.js @@ -16,7 +16,7 @@ limitations under the License. /* * Usage: - * Modal.createDialog(ErrorDialog, { + * Modal.createTrackedDialog('An Identifier', 'some detail', ErrorDialog, { * title: "some text", (default: "Error") * description: "some more text", * button: "Button Text", diff --git a/src/components/views/dialogs/KeyShareDialog.js b/src/components/views/dialogs/KeyShareDialog.js index 61391d281c..aed8e6a5af 100644 --- a/src/components/views/dialogs/KeyShareDialog.js +++ b/src/components/views/dialogs/KeyShareDialog.js @@ -88,7 +88,7 @@ export default React.createClass({ const DeviceVerifyDialog = sdk.getComponent('views.dialogs.DeviceVerifyDialog'); console.log("KeyShareDialog: Starting verify dialog"); - Modal.createDialog(DeviceVerifyDialog, { + Modal.createTrackedDialog('Key Share', 'Starting dialog', DeviceVerifyDialog, { userId: this.props.userId, device: this.state.deviceInfo, onFinished: (verified) => { diff --git a/src/components/views/dialogs/SessionRestoreErrorDialog.js b/src/components/views/dialogs/SessionRestoreErrorDialog.js index a3eb7c6962..010072e8c6 100644 --- a/src/components/views/dialogs/SessionRestoreErrorDialog.js +++ b/src/components/views/dialogs/SessionRestoreErrorDialog.js @@ -31,7 +31,7 @@ export default React.createClass({ _sendBugReport: function() { const BugReportDialog = sdk.getComponent("dialogs.BugReportDialog"); - Modal.createDialog(BugReportDialog, {}); + Modal.createTrackedDialog('Session Restore Error', 'Send Bug Report Dialog', BugReportDialog, {}); }, _continueClicked: function() { diff --git a/src/components/views/dialogs/SetEmailDialog.js b/src/components/views/dialogs/SetEmailDialog.js index 3c38064ee1..ed5cef2f67 100644 --- a/src/components/views/dialogs/SetEmailDialog.js +++ b/src/components/views/dialogs/SetEmailDialog.js @@ -55,7 +55,7 @@ export default React.createClass({ const emailAddress = this.state.emailAddress; if (!Email.looksValid(emailAddress)) { - Modal.createDialog(ErrorDialog, { + Modal.createTrackedDialog('Invalid Email Address', '', ErrorDialog, { title: _t("Invalid Email Address"), description: _t("This doesn't appear to be a valid email address"), }); @@ -65,7 +65,7 @@ export default React.createClass({ // we always bind emails when registering, so let's do the // same here. this._addThreepid.addEmailAddress(emailAddress, true).done(() => { - Modal.createDialog(QuestionDialog, { + Modal.createTrackedDialog('Verification Pending', '', QuestionDialog, { title: _t("Verification Pending"), description: _t( "Please check your email and click on the link it contains. Once this " + @@ -77,7 +77,7 @@ export default React.createClass({ }, (err) => { this.setState({emailBusy: false}); console.error("Unable to add email address " + emailAddress + " " + err); - Modal.createDialog(ErrorDialog, { + Modal.createTrackedDialog('Unable to add email address', '', ErrorDialog, { title: _t("Unable to add email address"), description: ((err && err.message) ? err.message : _t("Operation failed")), }); @@ -106,7 +106,7 @@ export default React.createClass({ const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); const message = _t("Unable to verify email address.") + " " + _t("Please check your email and click on the link it contains. Once this is done, click continue."); - Modal.createDialog(QuestionDialog, { + Modal.createTrackedDialog('Verification Pending', '3pid Auth Failed', QuestionDialog, { title: _t("Verification Pending"), description: message, button: _t('Continue'), @@ -115,7 +115,7 @@ export default React.createClass({ } else { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); console.error("Unable to verify email address: " + err); - Modal.createDialog(ErrorDialog, { + Modal.createTrackedDialog('Unable to verify email address', '', ErrorDialog, { title: _t("Unable to verify email address."), description: ((err && err.message) ? err.message : _t("Operation failed")), }); diff --git a/src/components/views/dialogs/SetMxIdDialog.js b/src/components/views/dialogs/SetMxIdDialog.js index 4d4f672f2b..554a244358 100644 --- a/src/components/views/dialogs/SetMxIdDialog.js +++ b/src/components/views/dialogs/SetMxIdDialog.js @@ -106,6 +106,16 @@ export default React.createClass({ }, _doUsernameCheck: function() { + // XXX: SPEC-1 + // Check if username is valid + // Naive impl copied from https://github.com/matrix-org/matrix-react-sdk/blob/66c3a6d9ca695780eb6b662e242e88323053ff33/src/components/views/login/RegistrationForm.js#L190 + if (encodeURIComponent(this.state.username) !== this.state.username) { + this.setState({ + usernameError: _t('User names may only contain letters, numbers, dots, hyphens and underscores.'), + }); + return Promise.reject(); + } + // Check if username is available return this._matrixClient.isUsernameAvailable(this.state.username).then( (isAvailable) => { @@ -242,7 +252,7 @@ export default React.createClass({ return (
diff --git a/src/components/views/dialogs/ChatInviteDialog.js b/src/components/views/dialogs/UserPickerDialog.js similarity index 59% rename from src/components/views/dialogs/ChatInviteDialog.js rename to src/components/views/dialogs/UserPickerDialog.js index d3a208a785..bde1ab0910 100644 --- a/src/components/views/dialogs/ChatInviteDialog.js +++ b/src/components/views/dialogs/UserPickerDialog.js @@ -1,5 +1,6 @@ /* Copyright 2015, 2016 OpenMarket Ltd +Copyright 2017 New Vector Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -15,40 +16,37 @@ limitations under the License. */ import React from 'react'; +import PropTypes from 'prop-types'; import { _t } from '../../../languageHandler'; import sdk from '../../../index'; -import { getAddressType, inviteMultipleToRoom } from '../../../Invite'; -import createRoom from '../../../createRoom'; import MatrixClientPeg from '../../../MatrixClientPeg'; -import DMRoomMap from '../../../utils/DMRoomMap'; -import Modal from '../../../Modal'; import AccessibleButton from '../elements/AccessibleButton'; import Promise from 'bluebird'; -import dis from '../../../dispatcher'; +import { addressTypes, getAddressType } from '../../../UserAddress.js'; const TRUNCATE_QUERY_LIST = 40; const QUERY_USER_DIRECTORY_DEBOUNCE_MS = 200; module.exports = React.createClass({ - displayName: "ChatInviteDialog", + displayName: "UserPickerDialog", + propTypes: { - title: React.PropTypes.string.isRequired, - description: React.PropTypes.oneOfType([ - React.PropTypes.element, - React.PropTypes.string, - ]), - value: React.PropTypes.string, - placeholder: React.PropTypes.string, - roomId: React.PropTypes.string, - button: React.PropTypes.string, - focus: React.PropTypes.bool, - onFinished: React.PropTypes.func.isRequired, + title: PropTypes.string.isRequired, + description: PropTypes.node, + value: PropTypes.string, + placeholder: PropTypes.string, + roomId: PropTypes.string, + button: PropTypes.string, + focus: PropTypes.bool, + validAddressTypes: PropTypes.arrayOf(PropTypes.oneOfType(addressTypes)), + onFinished: PropTypes.func.isRequired, }, getDefaultProps: function() { return { value: "", focus: true, + validAddressTypes: addressTypes, }; }, @@ -56,9 +54,9 @@ module.exports = React.createClass({ return { error: false, - // List of AddressTile.InviteAddressType objects representing + // List of UserAddressType objects representing // the list of addresses we're going to invite - inviteList: [], + userList: [], // Whether a search is ongoing busy: false, @@ -68,7 +66,7 @@ module.exports = React.createClass({ serverSupportsUserDirectory: true, // The query being searched for query: "", - // List of AddressTile.InviteAddressType objects representing + // List of UserAddressType objects representing // the set of auto-completion results for the current search // query. queryList: [], @@ -83,57 +81,14 @@ module.exports = React.createClass({ }, onButtonClick: function() { - let inviteList = this.state.inviteList.slice(); + let userList = this.state.userList.slice(); // Check the text input field to see if user has an unconverted address - // If there is and it's valid add it to the local inviteList + // If there is and it's valid add it to the local userList if (this.refs.textinput.value !== '') { - inviteList = this._addInputToList(); - if (inviteList === null) return; - } - - const addrTexts = inviteList.map(addr => addr.address); - if (inviteList.length > 0) { - if (this._isDmChat(addrTexts)) { - const userId = inviteList[0].address; - // Direct Message chat - const rooms = this._getDirectMessageRooms(userId); - if (rooms.length > 0) { - // A Direct Message room already exists for this user, so select a - // room from a list that is similar to the one in MemberInfo panel - const ChatCreateOrReuseDialog = sdk.getComponent( - "views.dialogs.ChatCreateOrReuseDialog", - ); - const close = Modal.createDialog(ChatCreateOrReuseDialog, { - userId: userId, - onFinished: (success) => { - this.props.onFinished(success); - }, - onNewDMClick: () => { - dis.dispatch({ - action: 'start_chat', - user_id: userId, - }); - close(true); - }, - onExistingRoomSelected: (roomId) => { - dis.dispatch({ - action: 'view_room', - room_id: roomId, - }); - close(true); - }, - }).close; - } else { - this._startChat(inviteList); - } - } else { - // Multi invite chat - this._startChat(inviteList); - } - } else { - // No addresses supplied - this.setState({ error: true }); + userList = this._addInputToList(); + if (userList === null) return; } + this.props.onFinished(true, userList); }, onCancel: function() { @@ -157,10 +112,10 @@ module.exports = React.createClass({ e.stopPropagation(); e.preventDefault(); if (this.addressSelector) this.addressSelector.chooseSelection(); - } else if (this.refs.textinput.value.length === 0 && this.state.inviteList.length && e.keyCode === 8) { // backspace + } else if (this.refs.textinput.value.length === 0 && this.state.userList.length && e.keyCode === 8) { // backspace e.stopPropagation(); e.preventDefault(); - this.onDismissed(this.state.inviteList.length - 1)(); + this.onDismissed(this.state.userList.length - 1)(); } else if (e.keyCode === 13) { // enter e.stopPropagation(); e.preventDefault(); @@ -201,12 +156,11 @@ module.exports = React.createClass({ }, onDismissed: function(index) { - var self = this; return () => { - var inviteList = self.state.inviteList.slice(); - inviteList.splice(index, 1); - self.setState({ - inviteList: inviteList, + const userList = this.state.userList.slice(); + userList.splice(index, 1); + this.setState({ + userList: userList, queryList: [], query: "", }); @@ -215,17 +169,16 @@ module.exports = React.createClass({ }, onClick: function(index) { - var self = this; - return function() { - self.onSelected(index); + return () => { + this.onSelected(index); }; }, onSelected: function(index) { - var inviteList = this.state.inviteList.slice(); - inviteList.push(this.state.queryList[index]); + const userList = this.state.userList.slice(); + userList.push(this.state.queryList[index]); this.setState({ - inviteList: inviteList, + userList: userList, queryList: [], query: "", }); @@ -297,7 +250,7 @@ module.exports = React.createClass({ return; } // Return objects, structure of which is defined - // by InviteAddressType + // by UserAddressType queryList.push({ addressType: 'mx', address: user.user_id, @@ -311,7 +264,7 @@ module.exports = React.createClass({ // This is important, otherwise there's no way to invite // a perfectly valid address if there are close matches. const addrType = getAddressType(query); - if (addrType !== null) { + if (this.props.validAddressTypes.includes(addrType)) { queryList.unshift({ addressType: addrType, address: query, @@ -330,132 +283,6 @@ module.exports = React.createClass({ }); }, - _getDirectMessageRooms: function(addr) { - const dmRoomMap = new DMRoomMap(MatrixClientPeg.get()); - const dmRooms = dmRoomMap.getDMRoomsForUserId(addr); - const rooms = []; - dmRooms.forEach(dmRoom => { - let room = MatrixClientPeg.get().getRoom(dmRoom); - if (room) { - const me = room.getMember(MatrixClientPeg.get().credentials.userId); - if (me.membership == 'join') { - rooms.push(room); - } - } - }); - return rooms; - }, - - _startChat: function(addrs) { - if (MatrixClientPeg.get().isGuest()) { - dis.dispatch({action: 'view_set_mxid'}); - return; - } - - const addrTexts = addrs.map((addr) => { - return addr.address; - }); - - if (this.props.roomId) { - // Invite new user to a room - var self = this; - inviteMultipleToRoom(this.props.roomId, addrTexts) - .then(function(addrs) { - var room = MatrixClientPeg.get().getRoom(self.props.roomId); - return self._showAnyInviteErrors(addrs, room); - }) - .catch(function(err) { - console.error(err.stack); - var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - Modal.createDialog(ErrorDialog, { - title: _t("Failed to invite"), - description: ((err && err.message) ? err.message : _t("Operation failed")), - }); - return null; - }) - .done(); - } else if (this._isDmChat(addrTexts)) { - // Start the DM chat - createRoom({dmUserId: addrTexts[0]}) - .catch(function(err) { - console.error(err.stack); - var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - Modal.createDialog(ErrorDialog, { - title: _t("Failed to invite user"), - description: ((err && err.message) ? err.message : _t("Operation failed")), - }); - return null; - }) - .done(); - } else { - // Start multi user chat - var self = this; - var room; - createRoom().then(function(roomId) { - room = MatrixClientPeg.get().getRoom(roomId); - return inviteMultipleToRoom(roomId, addrTexts); - }) - .then(function(addrs) { - return self._showAnyInviteErrors(addrs, room); - }) - .catch(function(err) { - console.error(err.stack); - var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - Modal.createDialog(ErrorDialog, { - title: _t("Failed to invite"), - description: ((err && err.message) ? err.message : _t("Operation failed")), - }); - return null; - }) - .done(); - } - - // Close - this will happen before the above, as that is async - this.props.onFinished(true, addrTexts); - }, - - _isOnInviteList: function(uid) { - for (let i = 0; i < this.state.inviteList.length; i++) { - if ( - this.state.inviteList[i].addressType == 'mx' && - this.state.inviteList[i].address.toLowerCase() === uid - ) { - return true; - } - } - return false; - }, - - _isDmChat: function(addrTexts) { - if (addrTexts.length === 1 && - getAddressType(addrTexts[0]) === "mx" && - !this.props.roomId - ) { - return true; - } else { - return false; - } - }, - - _showAnyInviteErrors: function(addrs, room) { - // Show user any errors - var errorList = []; - for (var addr in addrs) { - if (addrs.hasOwnProperty(addr) && addrs[addr] === "error") { - errorList.push(addr); - } - } - - if (errorList.length > 0) { - var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - Modal.createDialog(ErrorDialog, { - title: _t("Failed to invite the following users to the %(roomName)s room:", {roomName: room.name}), - description: errorList.join(", "), - }); - } - return addrs; - }, - _addInputToList: function() { const addressText = this.refs.textinput.value.trim(); const addrType = getAddressType(addressText); @@ -476,15 +303,15 @@ module.exports = React.createClass({ } } - const inviteList = this.state.inviteList.slice(); - inviteList.push(addrObj); + const userList = this.state.userList.slice(); + userList.push(addrObj); this.setState({ - inviteList: inviteList, + userList: userList, queryList: [], query: "", }); if (this._cancelThreepidLookup) this._cancelThreepidLookup(); - return inviteList; + return userList; }, _lookupThreepid: function(medium, address) { @@ -495,7 +322,7 @@ module.exports = React.createClass({ // not like they leak. this._cancelThreepidLookup = function() { cancelled = true; - } + }; // wait a bit to let the user finish typing return Promise.delay(500).then(() => { @@ -511,7 +338,7 @@ module.exports = React.createClass({ if (cancelled) return null; this.setState({ queryList: [{ - // an InviteAddressType + // a UserAddressType addressType: medium, address: address, displayName: res.displayname, @@ -527,20 +354,20 @@ module.exports = React.createClass({ const AddressSelector = sdk.getComponent("elements.AddressSelector"); this.scrollElement = null; - var query = []; + const query = []; // create the invite list - if (this.state.inviteList.length > 0) { - var AddressTile = sdk.getComponent("elements.AddressTile"); - for (let i = 0; i < this.state.inviteList.length; i++) { + if (this.state.userList.length > 0) { + const AddressTile = sdk.getComponent("elements.AddressTile"); + for (let i = 0; i < this.state.userList.length; i++) { query.push( - + , ); } } // Add the query at the end query.push( -