diff --git a/package.json b/package.json index bb8db64d28..b32e5f0501 100644 --- a/package.json +++ b/package.json @@ -65,6 +65,7 @@ "file-saver": "^1.3.3", "filesize": "3.5.6", "flux": "2.1.1", + "focus-trap-react": "^3.0.5", "fuse.js": "^2.2.0", "glob": "^5.0.14", "highlight.js": "^8.9.1", diff --git a/src/Analytics.js b/src/Analytics.js index 5c39b48a35..5f4a0d0c77 100644 --- a/src/Analytics.js +++ b/src/Analytics.js @@ -92,6 +92,10 @@ class Analytics { */ disable() { this.trackEvent('Analytics', 'opt-out'); + // disableHeartBeatTimer is undocumented but exists in the piwik code + // the _paq.push method will result in an error being printed in the console + // if an unknown method signature is passed + this._paq.push(['disableHeartBeatTimer']); this.disabled = true; } diff --git a/src/DateUtils.js b/src/DateUtils.js index 986525eec8..108697238c 100644 --- a/src/DateUtils.js +++ b/src/DateUtils.js @@ -50,11 +50,15 @@ function pad(n) { return (n < 10 ? '0' : '') + n; } -function twelveHourTime(date) { +function twelveHourTime(date, showSeconds=false) { let hours = date.getHours() % 12; const minutes = pad(date.getMinutes()); const ampm = date.getHours() >= 12 ? _t('PM') : _t('AM'); hours = hours ? hours : 12; // convert 0 -> 12 + if (showSeconds) { + const seconds = pad(date.getSeconds()); + return `${hours}:${minutes}:${seconds}${ampm}`; + } return `${hours}:${minutes}${ampm}`; } @@ -101,10 +105,17 @@ export function formatFullDate(date, showTwelveHour=false) { monthName: months[date.getMonth()], day: date.getDate(), fullYear: date.getFullYear(), - time: formatTime(date, showTwelveHour), + time: formatFullTime(date, showTwelveHour), }); } +export function formatFullTime(date, showTwelveHour=false) { + if (showTwelveHour) { + return twelveHourTime(date, true); + } + return pad(date.getHours()) + ':' + pad(date.getMinutes()) + ':' + pad(date.getSeconds()); +} + export function formatTime(date, showTwelveHour=false) { if (showTwelveHour) { return twelveHourTime(date); diff --git a/src/GroupAddressPicker.js b/src/GroupAddressPicker.js index ef9010cbf2..c45a335ab6 100644 --- a/src/GroupAddressPicker.js +++ b/src/GroupAddressPicker.js @@ -22,28 +22,30 @@ import MatrixClientPeg from './MatrixClientPeg'; import GroupStoreCache from './stores/GroupStoreCache'; export function showGroupInviteDialog(groupId) { - const description =
-
{ _t("Who would you like to add to this community?") }
-
- { _t( - "Warning: any person you add to a community will be publicly "+ - "visible to anyone who knows the community ID", - ) } -
-
; + return new Promise((resolve, reject) => { + const description =
+
{ _t("Who would you like to add to this community?") }
+
+ { _t( + "Warning: any person you add to a community will be publicly "+ + "visible to anyone who knows the community ID", + ) } +
+
; - const AddressPickerDialog = sdk.getComponent("dialogs.AddressPickerDialog"); - Modal.createTrackedDialog('Group Invite', '', AddressPickerDialog, { - title: _t("Invite new community members"), - description: description, - placeholder: _t("Name or matrix ID"), - button: _t("Invite to Community"), - validAddressTypes: ['mx-user-id'], - onFinished: (success, addrs) => { - if (!success) return; + const AddressPickerDialog = sdk.getComponent("dialogs.AddressPickerDialog"); + Modal.createTrackedDialog('Group Invite', '', AddressPickerDialog, { + title: _t("Invite new community members"), + description: description, + placeholder: _t("Name or matrix ID"), + button: _t("Invite to Community"), + validAddressTypes: ['mx-user-id'], + onFinished: (success, addrs) => { + if (!success) return; - _onGroupInviteFinished(groupId, addrs); - }, + _onGroupInviteFinished(groupId, addrs).then(resolve, reject); + }, + }); }); } @@ -87,7 +89,7 @@ function _onGroupInviteFinished(groupId, addrs) { const addrTexts = addrs.map((addr) => addr.address); - multiInviter.invite(addrTexts).then((completionStates) => { + return multiInviter.invite(addrTexts).then((completionStates) => { // Show user any errors const errorList = []; for (const addr of Object.keys(completionStates)) { diff --git a/src/HtmlUtils.js b/src/HtmlUtils.js index 5c6cbd6c1b..e3b7ba47f5 100644 --- a/src/HtmlUtils.js +++ b/src/HtmlUtils.js @@ -410,8 +410,7 @@ class TextHighlighter extends BaseHighlighter { * opts.disableBigEmoji: optional argument to disable the big emoji class. */ export function bodyToHtml(content, highlights, opts={}) { - const isHtml = (content.format === "org.matrix.custom.html"); - const body = isHtml ? content.formatted_body : escape(content.body); + let isHtml = (content.format === "org.matrix.custom.html"); let bodyHasEmoji = false; @@ -431,9 +430,27 @@ export function bodyToHtml(content, highlights, opts={}) { return highlighter.applyHighlights(safeText, safeHighlights).join(''); }; } - safeBody = sanitizeHtml(body, sanitizeHtmlParams); - bodyHasEmoji = containsEmoji(body); - if (bodyHasEmoji) safeBody = unicodeToImage(safeBody); + + bodyHasEmoji = containsEmoji(isHtml ? content.formatted_body : content.body); + + // Only generate safeBody if the message was sent as org.matrix.custom.html + if (isHtml) { + safeBody = sanitizeHtml(content.formatted_body, sanitizeHtmlParams); + } else { + // ... or if there are emoji, which we insert as HTML alongside the + // escaped plaintext body. + if (bodyHasEmoji) { + isHtml = true; + safeBody = sanitizeHtml(escape(content.body), sanitizeHtmlParams); + } + } + + // An HTML message with emoji + // or a plaintext message with emoji that was escaped and sanitized into + // HTML. + if (bodyHasEmoji) { + safeBody = unicodeToImage(safeBody); + } } finally { delete sanitizeHtmlParams.textFilter; } @@ -451,7 +468,10 @@ export function bodyToHtml(content, highlights, opts={}) { 'mx_EventTile_bigEmoji': emojiBody, 'markdown-body': isHtml, }); - return ; + + return isHtml ? + : + { content.body }; } export function emojifyText(text) { diff --git a/src/Lifecycle.js b/src/Lifecycle.js index efd5c20d5c..ec1fca2bc6 100644 --- a/src/Lifecycle.js +++ b/src/Lifecycle.js @@ -362,7 +362,7 @@ async function _doSetLoggedIn(credentials, clearStorage) { dis.dispatch({action: 'on_logged_in', teamToken: teamToken}); }); - startMatrixClient(); + await startMatrixClient(); return MatrixClientPeg.get(); } @@ -423,7 +423,7 @@ export function logout() { * Starts the matrix client and all other react-sdk services that * listen for events while a session is logged in. */ -function startMatrixClient() { +async function startMatrixClient() { console.log(`Lifecycle: Starting MatrixClient`); // dispatch this before starting the matrix client: it's used @@ -437,7 +437,7 @@ function startMatrixClient() { Presence.start(); DMRoomMap.makeShared().start(); - MatrixClientPeg.start(); + await MatrixClientPeg.start(); // dispatch that we finished starting up to wire up any other bits // of the matrix client that cannot be set prior to starting up. diff --git a/src/MatrixClientPeg.js b/src/MatrixClientPeg.js index 14dfa91fa4..99841c986e 100644 --- a/src/MatrixClientPeg.js +++ b/src/MatrixClientPeg.js @@ -175,4 +175,4 @@ class MatrixClientPeg { if (!global.mxMatrixClientPeg) { global.mxMatrixClientPeg = new MatrixClientPeg(); } -module.exports = global.mxMatrixClientPeg; +export default global.mxMatrixClientPeg; diff --git a/src/Modal.js b/src/Modal.js index c9f08772e7..2565d5c73b 100644 --- a/src/Modal.js +++ b/src/Modal.js @@ -22,6 +22,7 @@ const ReactDOM = require('react-dom'); import PropTypes from 'prop-types'; import Analytics from './Analytics'; import sdk from './index'; +import dis from './dispatcher'; const DIALOG_CONTAINER_ID = "mx_Dialog_Container"; @@ -188,10 +189,22 @@ class ModalManager { _reRender() { if (this._modals.length == 0) { + // If there is no modal to render, make all of Riot available + // to screen reader users again + dis.dispatch({ + action: 'aria_unhide_main_app', + }); ReactDOM.unmountComponentAtNode(this.getOrCreateContainer()); return; } + // Hide the content outside the modal to screen reader users + // so they won't be able to navigate into it and act on it using + // screen reader specific features + dis.dispatch({ + action: 'aria_hide_main_app', + }); + const modal = this._modals[0]; const dialog = (
diff --git a/src/Presence.js b/src/Presence.js index 2652c64c96..fd9bcf516d 100644 --- a/src/Presence.js +++ b/src/Presence.js @@ -1,5 +1,6 @@ /* Copyright 2015, 2016 OpenMarket Ltd +Copyright 2018 New Vector Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -31,7 +32,7 @@ class Presence { this.running = true; if (undefined === this.state) { this._resetTimer(); - this.dispatcherRef = dis.register(this._onUserActivity.bind(this)); + this.dispatcherRef = dis.register(this._onAction.bind(this)); } } @@ -125,9 +126,10 @@ class Presence { this.setState("unavailable"); } - _onUserActivity(payload) { - if (payload.action === "sync_state" || payload.action === "self_presence_updated") return; - this._resetTimer(); + _onAction(payload) { + if (payload.action === "user_activity") { + this._resetTimer(); + } } /** diff --git a/src/ScalarMessaging.js b/src/ScalarMessaging.js index 3c164c6551..fc8ee9edf6 100644 --- a/src/ScalarMessaging.js +++ b/src/ScalarMessaging.js @@ -563,7 +563,7 @@ const onMessage = function(event) { const url = SdkConfig.get().integrations_ui_url; if ( event.origin.length === 0 || - !url.startsWith(event.origin) || + !url.startsWith(event.origin + '/') || !event.data.action || event.data.api // Ignore messages with specific API set ) { diff --git a/src/Tinter.js b/src/Tinter.js index c7402c15be..7667e6d912 100644 --- a/src/Tinter.js +++ b/src/Tinter.js @@ -252,7 +252,6 @@ class Tinter { setTheme(theme) { - console.trace("setTheme " + theme); this.theme = theme; // update keyRgb from the current theme CSS itself, if it defines it diff --git a/src/Unread.js b/src/Unread.js index 383b5c2e5a..55e60f2a9a 100644 --- a/src/Unread.js +++ b/src/Unread.js @@ -28,6 +28,8 @@ module.exports = { return false; } else if (ev.getType() == 'm.room.member') { return false; + } else if (ev.getType() == 'm.room.third_party_invite') { + return false; } else if (ev.getType() == 'm.call.answer' || ev.getType() == 'm.call.hangup') { return false; } else if (ev.getType == 'm.room.message' && ev.getContent().msgtype == 'm.notify') { diff --git a/src/actions/MatrixActionCreators.js b/src/actions/MatrixActionCreators.js index 33bdb53799..6e1d52a88f 100644 --- a/src/actions/MatrixActionCreators.js +++ b/src/actions/MatrixActionCreators.js @@ -62,6 +62,127 @@ function createAccountDataAction(matrixClient, accountDataEvent) { }; } +/** + * @typedef RoomAction + * @type {Object} + * @property {string} action 'MatrixActions.Room'. + * @property {Room} room the Room that was stored. + */ + +/** + * Create a MatrixActions.Room action that represents a MatrixClient `Room` + * matrix event, emitted when a Room is stored in the client. + * + * @param {MatrixClient} matrixClient the matrix client. + * @param {Room} room the Room that was stored. + * @returns {RoomAction} an action of type `MatrixActions.Room`. + */ +function createRoomAction(matrixClient, room) { + return { action: 'MatrixActions.Room', room }; +} + +/** + * @typedef RoomTagsAction + * @type {Object} + * @property {string} action 'MatrixActions.Room.tags'. + * @property {Room} room the Room whose tags changed. + */ + +/** + * Create a MatrixActions.Room.tags action that represents a MatrixClient + * `Room.tags` matrix event, emitted when the m.tag room account data + * event is updated. + * + * @param {MatrixClient} matrixClient the matrix client. + * @param {MatrixEvent} roomTagsEvent the m.tag event. + * @param {Room} room the Room whose tags were changed. + * @returns {RoomTagsAction} an action of type `MatrixActions.Room.tags`. + */ +function createRoomTagsAction(matrixClient, roomTagsEvent, room) { + return { action: 'MatrixActions.Room.tags', room }; +} + +/** + * @typedef RoomTimelineAction + * @type {Object} + * @property {string} action 'MatrixActions.Room.timeline'. + * @property {boolean} isLiveEvent whether the event was attached to a + * live timeline. + * @property {boolean} isLiveUnfilteredRoomTimelineEvent whether the + * event was attached to a timeline in the set of unfiltered timelines. + * @property {Room} room the Room whose tags changed. + */ + +/** + * Create a MatrixActions.Room.timeline action that represents a + * MatrixClient `Room.timeline` matrix event, emitted when an event + * is added to or removed from a timeline of a room. + * + * @param {MatrixClient} matrixClient the matrix client. + * @param {MatrixEvent} timelineEvent the event that was added/removed. + * @param {Room} room the Room that was stored. + * @param {boolean} toStartOfTimeline whether the event is being added + * to the start (and not the end) of the timeline. + * @param {boolean} removed whether the event was removed from the + * timeline. + * @param {Object} data + * @param {boolean} data.liveEvent whether the event is a live event, + * belonging to a live timeline. + * @param {EventTimeline} data.timeline the timeline being altered. + * @returns {RoomTimelineAction} an action of type `MatrixActions.Room.timeline`. + */ +function createRoomTimelineAction(matrixClient, timelineEvent, room, toStartOfTimeline, removed, data) { + return { + action: 'MatrixActions.Room.timeline', + event: timelineEvent, + isLiveEvent: data.liveEvent, + isLiveUnfilteredRoomTimelineEvent: + room && data.timeline.getTimelineSet() === room.getUnfilteredTimelineSet(), + }; +} + +/** + * @typedef RoomMembershipAction + * @type {Object} + * @property {string} action 'MatrixActions.RoomMember.membership'. + * @property {RoomMember} member the member whose membership was updated. + */ + +/** + * Create a MatrixActions.RoomMember.membership action that represents + * a MatrixClient `RoomMember.membership` matrix event, emitted when a + * member's membership is updated. + * + * @param {MatrixClient} matrixClient the matrix client. + * @param {MatrixEvent} membershipEvent the m.room.member event. + * @param {RoomMember} member the member whose membership was updated. + * @param {string} oldMembership the member's previous membership. + * @returns {RoomMembershipAction} an action of type `MatrixActions.RoomMember.membership`. + */ +function createRoomMembershipAction(matrixClient, membershipEvent, member, oldMembership) { + return { action: 'MatrixActions.RoomMember.membership', member }; +} + +/** + * @typedef EventDecryptedAction + * @type {Object} + * @property {string} action 'MatrixActions.Event.decrypted'. + * @property {MatrixEvent} event the matrix event that was decrypted. + */ + +/** + * Create a MatrixActions.Event.decrypted action that represents + * a MatrixClient `Event.decrypted` matrix event, emitted when a + * matrix event is decrypted. + * + * @param {MatrixClient} matrixClient the matrix client. + * @param {MatrixEvent} event the matrix event that was decrypted. + * @returns {EventDecryptedAction} an action of type `MatrixActions.Event.decrypted`. + */ +function createEventDecryptedAction(matrixClient, event) { + return { action: 'MatrixActions.Event.decrypted', event }; +} + /** * This object is responsible for dispatching actions when certain events are emitted by * the given MatrixClient. @@ -78,6 +199,11 @@ export default { start(matrixClient) { this._addMatrixClientListener(matrixClient, 'sync', createSyncAction); this._addMatrixClientListener(matrixClient, 'accountData', createAccountDataAction); + this._addMatrixClientListener(matrixClient, 'Room', createRoomAction); + this._addMatrixClientListener(matrixClient, 'Room.tags', createRoomTagsAction); + this._addMatrixClientListener(matrixClient, 'Room.timeline', createRoomTimelineAction); + this._addMatrixClientListener(matrixClient, 'RoomMember.membership', createRoomMembershipAction); + this._addMatrixClientListener(matrixClient, 'Event.decrypted', createEventDecryptedAction); }, /** @@ -91,7 +217,7 @@ export default { */ _addMatrixClientListener(matrixClient, eventName, actionCreator) { const listener = (...args) => { - dis.dispatch(actionCreator(matrixClient, ...args)); + dis.dispatch(actionCreator(matrixClient, ...args), true); }; matrixClient.on(eventName, listener); this._matrixClientListenersStop.push(() => { diff --git a/src/actions/RoomListActions.js b/src/actions/RoomListActions.js new file mode 100644 index 0000000000..e5911c4e32 --- /dev/null +++ b/src/actions/RoomListActions.js @@ -0,0 +1,146 @@ +/* +Copyright 2018 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { asyncAction } from './actionCreators'; +import RoomListStore from '../stores/RoomListStore'; + +import Modal from '../Modal'; +import * as Rooms from '../Rooms'; +import { _t } from '../languageHandler'; +import sdk from '../index'; + +const RoomListActions = {}; + +/** + * Creates an action thunk that will do an asynchronous request to + * tag room. + * + * @param {MatrixClient} matrixClient the matrix client to set the + * account data on. + * @param {Room} room the room to tag. + * @param {string} oldTag the tag to remove (unless oldTag ==== newTag) + * @param {string} newTag the tag with which to tag the room. + * @param {?number} oldIndex the previous position of the room in the + * list of rooms. + * @param {?number} newIndex the new position of the room in the list + * of rooms. + * @returns {function} an action thunk. + * @see asyncAction + */ +RoomListActions.tagRoom = function(matrixClient, room, oldTag, newTag, oldIndex, newIndex) { + let metaData = null; + + // Is the tag ordered manually? + if (newTag && !newTag.match(/^(m\.lowpriority|im\.vector\.fake\.(invite|recent|direct|archived))$/)) { + const lists = RoomListStore.getRoomLists(); + const newList = [...lists[newTag]]; + + newList.sort((a, b) => a.tags[newTag].order - b.tags[newTag].order); + + // If the room was moved "down" (increasing index) in the same list we + // need to use the orders of the tiles with indices shifted by +1 + const offset = ( + newTag === oldTag && oldIndex < newIndex + ) ? 1 : 0; + + const indexBefore = offset + newIndex - 1; + const indexAfter = offset + newIndex; + + const prevOrder = indexBefore <= 0 ? + 0 : newList[indexBefore].tags[newTag].order; + const nextOrder = indexAfter >= newList.length ? + 1 : newList[indexAfter].tags[newTag].order; + + metaData = { + order: (prevOrder + nextOrder) / 2.0, + }; + } + + return asyncAction('RoomListActions.tagRoom', () => { + const promises = []; + const roomId = room.roomId; + + // Evil hack to get DMs behaving + if ((oldTag === undefined && newTag === 'im.vector.fake.direct') || + (oldTag === 'im.vector.fake.direct' && newTag === undefined) + ) { + return Rooms.guessAndSetDMRoom( + room, newTag === 'im.vector.fake.direct', + ).catch((err) => { + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + console.error("Failed to set direct chat tag " + err); + Modal.createTrackedDialog('Failed to set direct chat tag', '', ErrorDialog, { + title: _t('Failed to set direct chat tag'), + description: ((err && err.message) ? err.message : _t('Operation failed')), + }); + }); + } + + const hasChangedSubLists = oldTag !== newTag; + + // More evilness: We will still be dealing with moving to favourites/low prio, + // but we avoid ever doing a request with 'im.vector.fake.direct`. + // + // if we moved lists, remove the old tag + if (oldTag && oldTag !== 'im.vector.fake.direct' && + hasChangedSubLists + ) { + const promiseToDelete = matrixClient.deleteRoomTag( + roomId, oldTag, + ).catch(function(err) { + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + console.error("Failed to remove tag " + oldTag + " from room: " + err); + Modal.createTrackedDialog('Failed to remove tag from room', '', ErrorDialog, { + title: _t('Failed to remove tag %(tagName)s from room', {tagName: oldTag}), + description: ((err && err.message) ? err.message : _t('Operation failed')), + }); + }); + + promises.push(promiseToDelete); + } + + // if we moved lists or the ordering changed, add the new tag + if (newTag && newTag !== 'im.vector.fake.direct' && + (hasChangedSubLists || metaData) + ) { + // metaData is the body of the PUT to set the tag, so it must + // at least be an empty object. + metaData = metaData || {}; + + const promiseToAdd = matrixClient.setRoomTag(roomId, newTag, metaData).catch(function(err) { + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + console.error("Failed to add tag " + newTag + " to room: " + err); + Modal.createTrackedDialog('Failed to add tag to room', '', ErrorDialog, { + title: _t('Failed to add tag %(tagName)s to room', {tagName: newTag}), + description: ((err && err.message) ? err.message : _t('Operation failed')), + }); + + throw err; + }); + + promises.push(promiseToAdd); + } + + return Promise.all(promises); + }, () => { + // For an optimistic update + return { + room, oldTag, newTag, metaData, + }; + }); +}; + +export default RoomListActions; diff --git a/src/actions/TagOrderActions.js b/src/actions/TagOrderActions.js index dd4df6a9d4..a257ff16d8 100644 --- a/src/actions/TagOrderActions.js +++ b/src/actions/TagOrderActions.js @@ -35,6 +35,7 @@ const TagOrderActions = {}; TagOrderActions.moveTag = function(matrixClient, tag, destinationIx) { // Only commit tags if the state is ready, i.e. not null let tags = TagOrderStore.getOrderedTags(); + let removedTags = TagOrderStore.getRemovedTagsAccountData() || []; if (!tags) { return; } @@ -42,17 +43,66 @@ TagOrderActions.moveTag = function(matrixClient, tag, destinationIx) { tags = tags.filter((t) => t !== tag); tags = [...tags.slice(0, destinationIx), tag, ...tags.slice(destinationIx)]; + removedTags = removedTags.filter((t) => t !== tag); + const storeId = TagOrderStore.getStoreId(); return asyncAction('TagOrderActions.moveTag', () => { Analytics.trackEvent('TagOrderActions', 'commitTagOrdering'); return matrixClient.setAccountData( 'im.vector.web.tag_ordering', - {tags, _storeId: storeId}, + {tags, removedTags, _storeId: storeId}, ); }, () => { // For an optimistic update - return {tags}; + return {tags, removedTags}; + }); +}; + +/** + * Creates an action thunk that will do an asynchronous request to + * label a tag as removed in im.vector.web.tag_ordering account data. + * + * The reason this is implemented with new state `removedTags` is that + * we incrementally and initially populate `tags` with groups that + * have been joined. If we remove a group from `tags`, it will just + * get added (as it looks like a group we've recently joined). + * + * NB: If we ever support adding of tags (which is planned), we should + * take special care to remove the tag from `removedTags` when we add + * it. + * + * @param {MatrixClient} matrixClient the matrix client to set the + * account data on. + * @param {string} tag the tag to remove. + * @returns {function} an action thunk that will dispatch actions + * indicating the status of the request. + * @see asyncAction + */ +TagOrderActions.removeTag = function(matrixClient, tag) { + // Don't change tags, just removedTags + const tags = TagOrderStore.getOrderedTags(); + const removedTags = TagOrderStore.getRemovedTagsAccountData() || []; + + if (removedTags.includes(tag)) { + // Return a thunk that doesn't do anything, we don't even need + // an asynchronous action here, the tag is already removed. + return () => {}; + } + + removedTags.push(tag); + + const storeId = TagOrderStore.getStoreId(); + + return asyncAction('TagOrderActions.removeTag', () => { + Analytics.trackEvent('TagOrderActions', 'removeTag'); + return matrixClient.setAccountData( + 'im.vector.web.tag_ordering', + {tags, removedTags, _storeId: storeId}, + ); + }, () => { + // For an optimistic update + return {removedTags}; }); }; diff --git a/src/actions/actionCreators.js b/src/actions/actionCreators.js index 0238eee8c0..967ce609e7 100644 --- a/src/actions/actionCreators.js +++ b/src/actions/actionCreators.js @@ -31,6 +31,15 @@ limitations under the License. * `${id}.pending` and either * `${id}.success` or * `${id}.failure`. + * + * The shape of each are: + * { action: '${id}.pending', request } + * { action: '${id}.success', result } + * { action: '${id}.failure', err } + * + * where `request` is returned by `pendingFn` and + * result is the result of the promise returned by + * `fn`. */ export function asyncAction(id, fn, pendingFn) { return (dispatch) => { diff --git a/src/autocomplete/CommandProvider.js b/src/autocomplete/CommandProvider.js index d47f1a161a..e33fa7861f 100644 --- a/src/autocomplete/CommandProvider.js +++ b/src/autocomplete/CommandProvider.js @@ -105,6 +105,11 @@ const COMMANDS = [ args: '', description: _td('Stops ignoring a user, showing their messages going forward'), }, + { + command: '/devtools', + args: '', + description: _td('Opens the Developer Tools dialog'), + }, // Omitting `/markdown` as it only seems to apply to OldComposer ]; diff --git a/src/autocomplete/UserProvider.js b/src/autocomplete/UserProvider.js index bceec3f144..e636f95751 100644 --- a/src/autocomplete/UserProvider.js +++ b/src/autocomplete/UserProvider.js @@ -44,6 +44,7 @@ export default class UserProvider extends AutocompleteProvider { this.matcher = new FuzzyMatcher([], { keys: ['name', 'userId'], shouldMatchPrefix: true, + shouldMatchWordsOnly: false }); this._onRoomTimelineBound = this._onRoomTimeline.bind(this); @@ -72,6 +73,7 @@ export default class UserProvider extends AutocompleteProvider { // updates from pagination will happen when the paginate completes. if (toStartOfTimeline || !data || !data.liveEvent) return; + // TODO: lazyload if we have no ev.sender room member? this.onUserSpoke(ev.sender); } @@ -147,6 +149,7 @@ export default class UserProvider extends AutocompleteProvider { onUserSpoke(user: RoomMember) { if (this.users === null) return; + if (!user) return; if (user.userId === MatrixClientPeg.get().credentials.userId) return; // Move the user that spoke to the front of the array diff --git a/src/components/structures/GroupView.js b/src/components/structures/GroupView.js index de96935838..30e897e5dc 100644 --- a/src/components/structures/GroupView.js +++ b/src/components/structures/GroupView.js @@ -29,6 +29,7 @@ import classnames from 'classnames'; import GroupStoreCache from '../../stores/GroupStoreCache'; import GroupStore from '../../stores/GroupStore'; +import FlairStore from '../../stores/FlairStore'; import { showGroupAddRoomDialog } from '../../GroupAddressPicker'; import GeminiScrollbar from 'react-gemini-scrollbar'; import {makeGroupPermalink, makeUserPermalink} from "../../matrix-to"; @@ -429,6 +430,7 @@ export default React.createClass({ editing: false, saving: false, uploadingAvatar: false, + avatarChanged: false, membershipBusy: false, publicityBusy: false, inviterProfile: null, @@ -590,6 +592,10 @@ export default React.createClass({ this.setState({ uploadingAvatar: false, profileForm: newProfileForm, + + // Indicate that FlairStore needs to be poked to show this change + // in TagTile (TagPanel), Flair and GroupTile (MyGroups). + avatarChanged: true, }); }).catch((e) => { this.setState({uploadingAvatar: false}); @@ -615,6 +621,11 @@ export default React.createClass({ }); dis.dispatch({action: 'panel_disable'}); this._initGroupStore(this.props.groupId); + + if (this.state.avatarChanged) { + // XXX: Evil - poking a store should be done from an async action + FlairStore.refreshGroupProfile(this._matrixClient, this.props.groupId); + } }).catch((e) => { this.setState({ saving: false, @@ -625,6 +636,10 @@ export default React.createClass({ title: _t('Error'), description: _t('Failed to update community'), }); + }).finally(() => { + this.setState({ + avatarChanged: false, + }); }).done(); }, @@ -692,8 +707,15 @@ export default React.createClass({ }); const header = this.state.editing ?

{ _t('Community Settings') }

:
; + const changeDelayWarning = this.state.editing && this.state.isUserPrivileged ? +
+ { _t( 'Changes made to your community might not be seen by other users ' + + 'for up to 30 minutes.', + ) } +
:
; return
{ header } + { changeDelayWarning } { this._getLongDescriptionNode() } { this._getRoomsNode() }
; diff --git a/src/components/structures/LoggedInView.js b/src/components/structures/LoggedInView.js index e97d9dd0a1..d9ac9de693 100644 --- a/src/components/structures/LoggedInView.js +++ b/src/components/structures/LoggedInView.js @@ -19,6 +19,7 @@ limitations under the License. import * as Matrix from 'matrix-js-sdk'; import React from 'react'; import PropTypes from 'prop-types'; +import { DragDropContext } from 'react-beautiful-dnd'; import { KeyCode, isOnlyCtrlOrCmdKeyEvent } from '../../Keyboard'; import Notifier from '../../Notifier'; @@ -30,6 +31,9 @@ import sessionStore from '../../stores/SessionStore'; import MatrixClientPeg from '../../MatrixClientPeg'; import SettingsStore from "../../settings/SettingsStore"; +import TagOrderActions from '../../actions/TagOrderActions'; +import RoomListActions from '../../actions/RoomListActions'; + /** * This is what our MatrixChat shows when we are logged in. The precise view is * determined by the page_type property. @@ -207,8 +211,51 @@ const LoggedInView = React.createClass({ } }, + _onDragEnd: function(result) { + // Dragged to an invalid destination, not onto a droppable + if (!result.destination) { + return; + } + + const dest = result.destination.droppableId; + + if (dest === 'tag-panel-droppable') { + // Could be "GroupTile +groupId:domain" + const draggableId = result.draggableId.split(' ').pop(); + + // Dispatch synchronously so that the TagPanel receives an + // optimistic update from TagOrderStore before the previous + // state is shown. + dis.dispatch(TagOrderActions.moveTag( + this._matrixClient, + draggableId, + result.destination.index, + ), true); + } else if (dest.startsWith('room-sub-list-droppable_')) { + this._onRoomTileEndDrag(result); + } + }, + + _onRoomTileEndDrag: function(result) { + let newTag = result.destination.droppableId.split('_')[1]; + let prevTag = result.source.droppableId.split('_')[1]; + if (newTag === 'undefined') newTag = undefined; + if (prevTag === 'undefined') prevTag = undefined; + + const roomId = result.draggableId.split('_')[1]; + + const oldIndex = result.source.index; + const newIndex = result.destination.index; + + dis.dispatch(RoomListActions.tagRoom( + this._matrixClient, + this._matrixClient.getRoom(roomId), + prevTag, newTag, + oldIndex, newIndex, + ), true); + }, + render: function() { - const TagPanel = sdk.getComponent('structures.TagPanel'); const LeftPanel = sdk.getComponent('structures.LeftPanel'); const RightPanel = sdk.getComponent('structures.RightPanel'); const RoomView = sdk.getComponent('structures.RoomView'); @@ -327,19 +374,20 @@ const LoggedInView = React.createClass({ } return ( -
+
{ topBar } -
- { SettingsStore.isFeatureEnabled("feature_tag_panel") ? :
} - -
- { page_element } -
- { right_panel } -
+ +
+ +
+ { page_element } +
+ { right_panel } +
+
); }, diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index b37da0144f..1312abda09 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -171,6 +171,10 @@ export default React.createClass({ register_hs_url: null, register_is_url: null, register_id_sid: null, + + // When showing Modal dialogs we need to set aria-hidden on the root app element + // and disable it when there are no dialogs + hideToSRUsers: false, }; return s; }, @@ -608,6 +612,16 @@ export default React.createClass({ case 'send_event': this.onSendEvent(payload.room_id, payload.event); break; + case 'aria_hide_main_app': + this.setState({ + hideToSRUsers: true, + }); + break; + case 'aria_unhide_main_app': + this.setState({ + hideToSRUsers: false, + }); + break; } }, @@ -1171,18 +1185,6 @@ export default React.createClass({ cli.on("crypto.warning", (type) => { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); switch (type) { - case 'CRYPTO_WARNING_ACCOUNT_MIGRATED': - Modal.createTrackedDialog('Crypto migrated', '', ErrorDialog, { - title: _t('Cryptography data migrated'), - description: _t( - "A one-off migration of cryptography data has been performed. "+ - "End-to-end encryption will not work if you go back to an older "+ - "version of Riot. If you need to use end-to-end cryptography on "+ - "an older version, log out of Riot first. To retain message history, "+ - "export and re-import your keys.", - ), - }); - break; case 'CRYPTO_WARNING_OLD_VERSION_DETECTED': Modal.createTrackedDialog('Crypto migrated', '', ErrorDialog, { title: _t('Old cryptography data detected'), diff --git a/src/components/structures/MyGroups.js b/src/components/structures/MyGroups.js index 22157beaca..da7bebd16a 100644 --- a/src/components/structures/MyGroups.js +++ b/src/components/structures/MyGroups.js @@ -73,8 +73,25 @@ export default withMatrixClient(React.createClass({ }); contentHeader = groupNodes.length > 0 ?

{ _t('Your Communities') }

:
; content = groupNodes.length > 0 ? - - { groupNodes } + +
+

+ { _t( + "Did you know: you can use communities to filter your Riot.im experience!", + ) } +

+

+ { _t( + "To set up a filter, drag a community avatar over to the filter panel on " + + "the far left hand side of the screen. You can click on an avatar in the " + + "filter panel at any time to see only the rooms and people associated " + + "with that community.", + ) } +

+
+
+ { groupNodes } +
:
{ _t( diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 5304f38901..b71978647f 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -627,8 +627,8 @@ module.exports = React.createClass({ const room = this.state.room; if (!room) return; - const color_scheme = SettingsStore.getValue("roomColor", room.room_id); console.log("Tinter.tint from updateTint"); + const color_scheme = SettingsStore.getValue("roomColor", room.roomId); Tinter.tint(color_scheme.primary_color, color_scheme.secondary_color); }, @@ -677,23 +677,7 @@ module.exports = React.createClass({ // a member state changed in this room // refresh the conf call notification state this._updateConfCallNotification(); - - // if we are now a member of the room, where we were not before, that - // means we have finished joining a room we were previously peeking - // into. - const me = MatrixClientPeg.get().credentials.userId; - if (this.state.joining && this.state.room.hasMembershipState(me, "join")) { - // Having just joined a room, check to see if it looks like a DM room, and if so, - // mark it as one. This is to work around the fact that some clients don't support - // is_direct. We should remove this once they do. - const me = this.state.room.getMember(MatrixClientPeg.get().credentials.userId); - if (Rooms.looksLikeDirectMessageRoom(this.state.room, me)) { - // XXX: There's not a whole lot we can really do if this fails: at best - // perhaps we could try a couple more times, but since it's a temporary - // compatability workaround, let's not bother. - Rooms.setDMRoom(this.state.room.roomId, me.events.member.getSender()).done(); - } - } + this._updateDMState(); }, 500), _checkIfAlone: function(room) { @@ -734,6 +718,44 @@ module.exports = React.createClass({ }); }, + _updateDMState() { + const me = this.state.room.getMember(MatrixClientPeg.get().credentials.userId); + if (!me || me.membership !== "join") { + return; + } + + // The user may have accepted an invite with is_direct set + if (me.events.member.getPrevContent().membership === "invite" && + me.events.member.getPrevContent().is_direct + ) { + // This is a DM with the sender of the invite event (which we assume + // preceded the join event) + Rooms.setDMRoom( + this.state.room.roomId, + me.events.member.getUnsigned().prev_sender, + ); + return; + } + + const invitedMembers = this.state.room.getMembersWithMembership("invite"); + const joinedMembers = this.state.room.getMembersWithMembership("join"); + + // There must be one invited member and one joined member + if (invitedMembers.length !== 1 || joinedMembers.length !== 1) { + return; + } + + // The user may have sent an invite with is_direct sent + const other = invitedMembers[0]; + if (other && + other.membership === "invite" && + other.events.member.getContent().is_direct + ) { + Rooms.setDMRoom(this.state.room.roomId, other.userId); + return; + } + }, + onSearchResultsResize: function() { dis.dispatch({ action: 'timeline_resize' }, true); }, @@ -826,18 +848,6 @@ module.exports = React.createClass({ action: 'join_room', opts: { inviteSignUrl: signUrl }, }); - - // if this is an invite and has the 'direct' hint set, mark it as a DM room now. - if (this.state.room) { - const me = this.state.room.getMember(MatrixClientPeg.get().credentials.userId); - if (me && me.membership == 'invite') { - if (me.events.member.getContent().is_direct) { - // The 'direct' hint is there, so declare that this is a DM room for - // whoever invited us. - return Rooms.setDMRoom(this.state.room.roomId, me.events.member.getSender()); - } - } - } return Promise.resolve(); }); }, diff --git a/src/components/structures/TagPanel.js b/src/components/structures/TagPanel.js index 49a7a4020a..7a187e4298 100644 --- a/src/components/structures/TagPanel.js +++ b/src/components/structures/TagPanel.js @@ -17,15 +17,16 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; import { MatrixClient } from 'matrix-js-sdk'; +import GeminiScrollbar from 'react-gemini-scrollbar'; import TagOrderStore from '../../stores/TagOrderStore'; import GroupActions from '../../actions/GroupActions'; -import TagOrderActions from '../../actions/TagOrderActions'; import sdk from '../../index'; import dis from '../../dispatcher'; +import { _t } from '../../languageHandler'; -import { DragDropContext, Droppable } from 'react-beautiful-dnd'; +import { Droppable } from 'react-beautiful-dnd'; const TagPanel = React.createClass({ displayName: 'TagPanel', @@ -44,7 +45,7 @@ const TagPanel = React.createClass({ componentWillMount: function() { this.unmounted = false; this.context.matrixClient.on("Group.myMembership", this._onGroupMyMembership); - this.context.matrixClient.on("sync", this.onClientSync); + this.context.matrixClient.on("sync", this._onClientSync); this._tagOrderStoreToken = TagOrderStore.addListener(() => { if (this.unmounted) { @@ -62,7 +63,7 @@ const TagPanel = React.createClass({ componentWillUnmount() { this.unmounted = true; this.context.matrixClient.removeListener("Group.myMembership", this._onGroupMyMembership); - this.context.matrixClient.removeListener("sync", this.onClientSync); + this.context.matrixClient.removeListener("sync", this._onClientSync); if (this._filterStoreToken) { this._filterStoreToken.remove(); } @@ -73,7 +74,7 @@ const TagPanel = React.createClass({ dis.dispatch(GroupActions.fetchJoinedGroups(this.context.matrixClient)); }, - onClientSync(syncState, prevState) { + _onClientSync(syncState, prevState) { // Consider the client reconnected if there is no error with syncing. // This means the state could be RECONNECTING, SYNCING or PREPARED. const reconnected = syncState !== "ERROR" && prevState !== syncState; @@ -83,9 +84,7 @@ const TagPanel = React.createClass({ } }, - onClick(e) { - // Ignore clicks on children - if (e.target !== e.currentTarget) return; + onMouseDown(e) { dis.dispatch({action: 'deselect_tags'}); }, @@ -94,26 +93,15 @@ const TagPanel = React.createClass({ dis.dispatch({action: 'view_create_group'}); }, - onTagTileEndDrag(result) { - // Dragged to an invalid destination, not onto a droppable - if (!result.destination) { - return; - } - - // Dispatch synchronously so that the TagPanel receives an - // optimistic update from TagOrderStore before the previous - // state is shown. - dis.dispatch(TagOrderActions.moveTag( - this.context.matrixClient, - result.draggableId, - result.destination.index, - ), true); + onClearFilterClick(ev) { + dis.dispatch({action: 'deselect_tags'}); }, render() { + const GroupsButton = sdk.getComponent('elements.GroupsButton'); + const DNDTagTile = sdk.getComponent('elements.DNDTagTile'); const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); const TintableSvg = sdk.getComponent('elements.TintableSvg'); - const DNDTagTile = sdk.getComponent('elements.DNDTagTile'); const tags = this.state.orderedTags.map((tag, index) => { return ; }); + + const clearButton = this.state.selectedTags.length > 0 ? + : +
; + return
- - + + { clearButton } + +
+ + { (provided, snapshot) => ( -
- { tags } - { provided.placeholder } -
+
+ { tags } + { provided.placeholder } +
) }
- - - - +
+
+
+ +
; }, }); diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index 4ade78af85..1a03b5d994 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -624,6 +624,7 @@ var TimelinePanel = React.createClass({ this.props.timelineSet.room.setUnreadNotificationCount('highlight', 0); dis.dispatch({ action: 'on_room_read', + roomId: this.props.timelineSet.room.roomId, }); } } @@ -1121,9 +1122,9 @@ var TimelinePanel = React.createClass({ // exist. if (this.state.timelineLoading) { return ( -
- -
+
+ +
); } diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index b1eedd1a90..f6629b4b09 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -79,6 +79,7 @@ const SIMPLE_SETTINGS = [ { id: "Pill.shouldHidePillAvatar" }, { id: "TextualBody.disableBigEmoji" }, { id: "VideoView.flipVideoHorizontally" }, + { id: "TagPanel.disableTagPanel" }, ]; // These settings must be defined in SettingsStore @@ -794,11 +795,18 @@ module.exports = React.createClass({ } return (
-

{ _t("Bug Report") }

+

{ _t("Debug Logs Submission") }

-

{ _t("Found a bug?") }

+

{ + _t( "If you've submitted a bug via GitHub, debug logs can help " + + "us track down the problem. Debug logs contain application " + + "usage data including your username, the IDs or aliases of " + + "the rooms or groups you have visited and the usernames of " + + "other users. They do not contian messages.", + ) + }

diff --git a/src/components/structures/login/Login.js b/src/components/structures/login/Login.js index 5042ca1fd0..7f4aa0325a 100644 --- a/src/components/structures/login/Login.js +++ b/src/components/structures/login/Login.js @@ -431,10 +431,10 @@ module.exports = React.createClass({ // FIXME: remove status.im theme tweaks const theme = SettingsStore.getValue("theme"); if (theme !== "status") { - header =

{ _t('Sign in') }

; + header =

{ _t('Sign in') } { loader }

; } else { if (!this.state.errorText) { - header =

{ _t('Sign in to get started') }

; + header =

{ _t('Sign in to get started') } { loader }

; } } diff --git a/src/components/views/avatars/BaseAvatar.js b/src/components/views/avatars/BaseAvatar.js index 5735a99125..6fb86c9cd8 100644 --- a/src/components/views/avatars/BaseAvatar.js +++ b/src/components/views/avatars/BaseAvatar.js @@ -1,5 +1,6 @@ /* Copyright 2015, 2016 OpenMarket Ltd +Copyright 2018 New Vector Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -82,7 +83,7 @@ module.exports = React.createClass({ } }, - onClientSync(syncState, prevState) { + onClientSync: function(syncState, prevState) { if (this.unmounted) return; // Consider the client reconnected if there is no error with syncing. diff --git a/src/components/views/avatars/RoomAvatar.js b/src/components/views/avatars/RoomAvatar.js index cae02ac408..e547cf0fa7 100644 --- a/src/components/views/avatars/RoomAvatar.js +++ b/src/components/views/avatars/RoomAvatar.js @@ -48,12 +48,33 @@ module.exports = React.createClass({ }; }, + componentWillMount: function() { + MatrixClientPeg.get().on("RoomState.events", this.onRoomStateEvents); + }, + + componentWillUnmount: function() { + const cli = MatrixClientPeg.get(); + if (cli) { + cli.removeListener("RoomState.events", this.onRoomStateEvents); + } + }, + componentWillReceiveProps: function(newProps) { this.setState({ urls: this.getImageUrls(newProps), }); }, + onRoomStateEvents: function(ev) { + if (ev.getRoomId() !== this.props.room.roomId || + ev.getType() !== 'm.room.avatar' + ) return; + + this.setState({ + urls: this.getImageUrls(this.props), + }); + }, + getImageUrls: function(props) { return [ ContentRepo.getHttpUriForMxc( diff --git a/src/components/views/dialogs/BaseDialog.js b/src/components/views/dialogs/BaseDialog.js index 66e5fcb0c0..21a2477c37 100644 --- a/src/components/views/dialogs/BaseDialog.js +++ b/src/components/views/dialogs/BaseDialog.js @@ -15,6 +15,7 @@ limitations under the License. */ import React from 'react'; +import FocusTrap from 'focus-trap-react'; import PropTypes from 'prop-types'; import { MatrixClient } from 'matrix-js-sdk'; @@ -37,9 +38,6 @@ export default React.createClass({ // onFinished callback to call when Escape is pressed onFinished: PropTypes.func.isRequired, - // callback to call when Enter is pressed - onEnterPressed: PropTypes.func, - // called when a key is pressed onKeyDown: PropTypes.func, @@ -52,6 +50,10 @@ export default React.createClass({ // children should be the content of the dialog children: PropTypes.node, + + // Id of content element + // If provided, this is used to add a aria-describedby attribute + contentId: React.PropTypes.string, }, childContextTypes: { @@ -76,12 +78,6 @@ export default React.createClass({ e.stopPropagation(); e.preventDefault(); this.props.onFinished(); - } else if (e.keyCode === KeyCode.ENTER) { - if (this.props.onEnterPressed) { - e.stopPropagation(); - e.preventDefault(); - this.props.onEnterPressed(e); - } } }, @@ -93,17 +89,28 @@ export default React.createClass({ const TintableSvg = sdk.getComponent("elements.TintableSvg"); return ( -
+ -
+
{ this.props.title }
{ this.props.children } -
+
); }, }); diff --git a/src/components/views/dialogs/ChatCreateOrReuseDialog.js b/src/components/views/dialogs/ChatCreateOrReuseDialog.js index dc4f3f77db..e2387064cf 100644 --- a/src/components/views/dialogs/ChatCreateOrReuseDialog.js +++ b/src/components/views/dialogs/ChatCreateOrReuseDialog.js @@ -59,6 +59,7 @@ export default class ChatCreateOrReuseDialog extends React.Component { ); tiles.push(
{ _t("Start new chat") }
; - content =
+ content =
{ _t('You already have existing direct chats with this user:') }
{ this.state.tiles } @@ -146,7 +147,7 @@ export default class ChatCreateOrReuseDialog extends React.Component { if (this.state.busyProfile) { profile = ; } else if (this.state.profileError) { - profile =
+ profile =
Unable to load profile information for { this.props.userId }
; } else { @@ -162,14 +163,14 @@ export default class ChatCreateOrReuseDialog extends React.Component {
; } content =
-
+

{ _t('Click on the button below to start chatting!') }

{ profile }
+ onPrimaryButtonClick={this.props.onNewDMClick} focus="true" />
; } @@ -178,6 +179,7 @@ export default class ChatCreateOrReuseDialog extends React.Component { { content } diff --git a/src/components/views/dialogs/ConfirmUserActionDialog.js b/src/components/views/dialogs/ConfirmUserActionDialog.js index f347261470..b65d98d78d 100644 --- a/src/components/views/dialogs/ConfirmUserActionDialog.js +++ b/src/components/views/dialogs/ConfirmUserActionDialog.js @@ -114,10 +114,10 @@ export default React.createClass({ return ( -
+
{ avatar }
diff --git a/src/components/views/dialogs/CreateGroupDialog.js b/src/components/views/dialogs/CreateGroupDialog.js index 86a2b2498c..04f99a0e15 100644 --- a/src/components/views/dialogs/CreateGroupDialog.js +++ b/src/components/views/dialogs/CreateGroupDialog.js @@ -112,7 +112,7 @@ export default React.createClass({ // XXX: We should catch errcodes and give sensible i18ned messages for them, // rather than displaying what the server gives us, but synapse doesn't give // any yet. - createErrorNode =
+ createErrorNode =
{ _t('Something went wrong whilst creating your community') }
{ this.state.createError.message }
; @@ -120,7 +120,6 @@ export default React.createClass({ return (
diff --git a/src/components/views/dialogs/CreateRoomDialog.js b/src/components/views/dialogs/CreateRoomDialog.js index d9287d23da..51693a19c9 100644 --- a/src/components/views/dialogs/CreateRoomDialog.js +++ b/src/components/views/dialogs/CreateRoomDialog.js @@ -45,30 +45,31 @@ export default React.createClass({ const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); return ( -
-
- -
-
- -
-
- -
- { _t('Advanced options') } -
- - + +
+
+
-
-
+
+ +
+
+ +
+ { _t('Advanced options') } +
+ + +
+
+
+ diff --git a/src/components/views/dialogs/ErrorDialog.js b/src/components/views/dialogs/ErrorDialog.js index 2af2d6214f..a055f07629 100644 --- a/src/components/views/dialogs/ErrorDialog.js +++ b/src/components/views/dialogs/ErrorDialog.js @@ -52,22 +52,18 @@ export default React.createClass({ }; }, - componentDidMount: function() { - if (this.props.focus) { - this.refs.button.focus(); - } - }, - render: function() { const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); return ( -
+ title={this.props.title || _t('Error')} + contentId='mx_Dialog_content' + > +
{ this.props.description || _t('An error has occurred.') }
-
diff --git a/src/components/views/dialogs/InteractiveAuthDialog.js b/src/components/views/dialogs/InteractiveAuthDialog.js index a47702305c..b682156072 100644 --- a/src/components/views/dialogs/InteractiveAuthDialog.js +++ b/src/components/views/dialogs/InteractiveAuthDialog.js @@ -73,11 +73,12 @@ export default React.createClass({ let content; if (this.state.authError) { content = ( -
-
{ this.state.authError.message || this.state.authError.toString() }
+
+
{ this.state.authError.message || this.state.authError.toString() }

{ _t("Dismiss") } @@ -85,7 +86,7 @@ export default React.createClass({ ); } else { content = ( -
+
{ content } diff --git a/src/components/views/dialogs/KeyShareDialog.js b/src/components/views/dialogs/KeyShareDialog.js index 00bcc942a1..b9b64a69d2 100644 --- a/src/components/views/dialogs/KeyShareDialog.js +++ b/src/components/views/dialogs/KeyShareDialog.js @@ -126,11 +126,11 @@ export default React.createClass({ text = _t(text, {displayName: displayName}); return ( -
+

{ text }

-