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/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/Modal.js b/src/Modal.js index e100105a88..79fcaaefd1 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(analyticsId, loader, props, className) { + Analytics.trackEvent('Modal', analyticsId); + 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/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/UserSettingsStore.js b/src/UserSettingsStore.js index 4c66c90598..796dab4d60 100644 --- a/src/UserSettingsStore.js +++ b/src/UserSettingsStore.js @@ -29,6 +29,9 @@ export default { name: "-", id: 'matrix_apps', default: false, + + // 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/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 2dcb72deb9..775e491e31 100644 --- a/src/components/structures/LoggedInView.js +++ b/src/components/structures/LoggedInView.js @@ -301,7 +301,7 @@ export default React.createClass({ case PageTypes.UserView: page_element = null; // deliberately null for now - right_panel = ; + right_panel = ; break; case PageTypes.GroupView: page_element = { @@ -426,7 +426,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(), }); @@ -728,7 +728,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) { @@ -767,7 +767,7 @@ module.exports = React.createClass({ return; } const ChatInviteDialog = sdk.getComponent("dialogs.ChatInviteDialog"); - Modal.createDialog(ChatInviteDialog, { + Modal.createTrackedDialog('Start a chat', '', ChatInviteDialog, { title: _t('Start a chat'), description: _t("Who would you like to communicate with?"), placeholder: _t("Email, name or matrix ID"), @@ -787,7 +787,7 @@ module.exports = React.createClass({ return; } const TextInputDialog = sdk.getComponent("dialogs.TextInputDialog"); - Modal.createDialog(TextInputDialog, { + Modal.createTrackedDialog('Create Room', '', TextInputDialog, { title: _t('Create Room'), description: _t('Room name (optional)'), button: _t('Create Room'), @@ -831,7 +831,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) { @@ -859,7 +859,7 @@ module.exports = React.createClass({ _invite: function(roomId) { const ChatInviteDialog = sdk.getComponent("dialogs.ChatInviteDialog"); - Modal.createDialog(ChatInviteDialog, { + Modal.createTrackedDialog('Chat Invite', '', ChatInviteDialog, { title: _t('Invite new room members'), description: _t('Who would you like to add to this room?'), button: _t('Send Invites'), @@ -873,7 +873,7 @@ module.exports = React.createClass({ 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: ( @@ -896,7 +896,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.")), @@ -1090,7 +1090,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 +1203,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); 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 916e50d86b..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', @@ -335,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")), }); @@ -368,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")), }); @@ -377,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:
@@ -413,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, }); @@ -421,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 " + @@ -446,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"), }); @@ -456,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 " + @@ -468,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")), }); @@ -479,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'), @@ -493,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")), }); @@ -525,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'), @@ -534,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")), }); @@ -544,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() { @@ -552,7 +556,7 @@ module.exports = React.createClass({ if (!BugReportDialog) { return; } - Modal.createDialog(BugReportDialog, {}); + Modal.createTrackedDialog('Bug Report Dialog', '', BugReportDialog, {}); }, _onClearCacheClicked: function() { @@ -589,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() { @@ -859,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) => { @@ -867,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") }

@@ -1008,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/ChatInviteDialog.js b/src/components/views/dialogs/ChatInviteDialog.js index d3a208a785..728860edec 100644 --- a/src/components/views/dialogs/ChatInviteDialog.js +++ b/src/components/views/dialogs/ChatInviteDialog.js @@ -103,7 +103,7 @@ module.exports = React.createClass({ const ChatCreateOrReuseDialog = sdk.getComponent( "views.dialogs.ChatCreateOrReuseDialog", ); - const close = Modal.createDialog(ChatCreateOrReuseDialog, { + const close = Modal.createTrackedDialog('Create or Reuse', '', ChatCreateOrReuseDialog, { userId: userId, onFinished: (success) => { this.props.onFinished(success); @@ -367,7 +367,7 @@ module.exports = React.createClass({ .catch(function(err) { console.error(err.stack); var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - Modal.createDialog(ErrorDialog, { + Modal.createTrackedDialog('Failed to invite', '', ErrorDialog, { title: _t("Failed to invite"), description: ((err && err.message) ? err.message : _t("Operation failed")), }); @@ -380,7 +380,7 @@ module.exports = React.createClass({ .catch(function(err) { console.error(err.stack); var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - Modal.createDialog(ErrorDialog, { + Modal.createTrackedDialog('Failed to invite user', '', ErrorDialog, { title: _t("Failed to invite user"), description: ((err && err.message) ? err.message : _t("Operation failed")), }); @@ -401,7 +401,7 @@ module.exports = React.createClass({ .catch(function(err) { console.error(err.stack); var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - Modal.createDialog(ErrorDialog, { + Modal.createTrackedDialog('Failed to invite', '', ErrorDialog, { title: _t("Failed to invite"), description: ((err && err.message) ? err.message : _t("Operation failed")), }); @@ -448,7 +448,7 @@ module.exports = React.createClass({ if (errorList.length > 0) { var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - Modal.createDialog(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(", "), }); 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/elements/AppTile.js b/src/components/views/elements/AppTile.js index 2fcacaaee2..a78b802ad7 100644 --- a/src/components/views/elements/AppTile.js +++ b/src/components/views/elements/AppTile.js @@ -123,7 +123,7 @@ export default React.createClass({ console.log("Edit widget ID ", this.props.id); const IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager"); const src = this._scalarClient.getScalarInterfaceUrlForRoom(this.props.room.roomId, 'type_' + this.props.type); - Modal.createDialog(IntegrationsManager, { + Modal.createTrackedDialog('Integrations Manager', '', IntegrationsManager, { src: src, }, "mx_IntegrationsManager"); }, diff --git a/src/components/views/elements/DeviceVerifyButtons.js b/src/components/views/elements/DeviceVerifyButtons.js index dfca7e2600..bfe45905a1 100644 --- a/src/components/views/elements/DeviceVerifyButtons.js +++ b/src/components/views/elements/DeviceVerifyButtons.js @@ -52,7 +52,7 @@ export default React.createClass({ onVerifyClick: function() { const DeviceVerifyDialog = sdk.getComponent('views.dialogs.DeviceVerifyDialog'); - Modal.createDialog(DeviceVerifyDialog, { + Modal.createTrackedDialog('Device Verify Dialog', '', DeviceVerifyDialog, { userId: this.props.userId, device: this.state.device, }); diff --git a/src/components/views/elements/Pill.js b/src/components/views/elements/Pill.js index b5fa163608..7dee5f6a40 100644 --- a/src/components/views/elements/Pill.js +++ b/src/components/views/elements/Pill.js @@ -15,6 +15,7 @@ limitations under the License. */ import React from 'react'; import sdk from '../../../index'; +import dis from '../../../dispatcher'; import classNames from 'classnames'; import { Room, RoomMember } from 'matrix-js-sdk'; import PropTypes from 'prop-types'; @@ -140,6 +141,12 @@ const Pill = React.createClass({ }); }, + onUserPillClicked: function() { + dis.dispatch({ + action: 'view_user', + member: this.state.member, + }); + }, render: function() { const MemberAvatar = sdk.getComponent('avatars.MemberAvatar'); const RoomAvatar = sdk.getComponent('avatars.RoomAvatar'); @@ -150,6 +157,8 @@ const Pill = React.createClass({ let linkText = resource; let pillClass; let userId; + let href = this.props.url; + let onClick; switch (this.state.pillType) { case Pill.TYPE_USER_MENTION: { // If this user is not a member of this room, default to the empty member @@ -161,6 +170,8 @@ const Pill = React.createClass({ avatar = ; } pillClass = 'mx_UserPill'; + href = null; + onClick = this.onUserPillClicked.bind(this); } } break; @@ -183,7 +194,7 @@ const Pill = React.createClass({ if (this.state.pillType) { return this.props.inMessage ? - + {avatar} {linkText} : diff --git a/src/components/views/login/RegistrationForm.js b/src/components/views/login/RegistrationForm.js index ff07cd36e5..d5b7bcf46a 100644 --- a/src/components/views/login/RegistrationForm.js +++ b/src/components/views/login/RegistrationForm.js @@ -95,7 +95,7 @@ module.exports = React.createClass({ if (this.allFieldsValid()) { if (this.refs.email.value == '') { var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); - Modal.createDialog(QuestionDialog, { + Modal.createTrackedDialog('If you don\'t specify an email address...', '', QuestionDialog, { title: _t("Warning!"), description:
diff --git a/src/components/views/login/ServerConfig.js b/src/components/views/login/ServerConfig.js index a63d02416c..0042ab5e9f 100644 --- a/src/components/views/login/ServerConfig.js +++ b/src/components/views/login/ServerConfig.js @@ -122,7 +122,7 @@ module.exports = React.createClass({ showHelpPopup: function() { var CustomServerDialog = sdk.getComponent('login.CustomServerDialog'); - Modal.createDialog(CustomServerDialog); + Modal.createTrackedDialog('Custom Server Dialog', '', CustomServerDialog); }, render: function() { diff --git a/src/components/views/messages/MFileBody.js b/src/components/views/messages/MFileBody.js index bccae923eb..53c36f234c 100644 --- a/src/components/views/messages/MFileBody.js +++ b/src/components/views/messages/MFileBody.js @@ -282,8 +282,8 @@ module.exports = React.createClass({ }); }).catch((err) => { console.warn("Unable to decrypt attachment: ", err); - Modal.createDialog(ErrorDialog, { - title: _t("Error"), + Modal.createTrackedDialog('Error decrypting attachment', '', ErrorDialog, { + title: _t("Error"), description: _t("Error decrypting attachment"), }); }).finally(() => { diff --git a/src/components/views/messages/TextualBody.js b/src/components/views/messages/TextualBody.js index 27dba76146..58273bee67 100644 --- a/src/components/views/messages/TextualBody.js +++ b/src/components/views/messages/TextualBody.js @@ -275,18 +275,21 @@ module.exports = React.createClass({ }, getEventTileOps: function() { - var self = this; return { - isWidgetHidden: function() { - return self.state.widgetHidden; + isWidgetHidden: () => { + return this.state.widgetHidden; }, - unhideWidget: function() { - self.setState({ widgetHidden: false }); + unhideWidget: () => { + this.setState({ widgetHidden: false }); if (global.localStorage) { - global.localStorage.removeItem("hide_preview_" + self.props.mxEvent.getId()); + global.localStorage.removeItem("hide_preview_" + this.props.mxEvent.getId()); } }, + + getInnerText: () => { + return this.refs.content.innerText; + } }; }, @@ -305,7 +308,7 @@ module.exports = React.createClass({ let completeUrl = scalarClient.getStarterLink(starterLink); let QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); let integrationsUrl = SdkConfig.get().integrations_ui_url; - Modal.createDialog(QuestionDialog, { + Modal.createTrackedDialog('Add an integration', '', QuestionDialog, { title: _t("Add an Integration"), description:
diff --git a/src/components/views/room_settings/AliasSettings.js b/src/components/views/room_settings/AliasSettings.js index ba0663153e..f37bd4271a 100644 --- a/src/components/views/room_settings/AliasSettings.js +++ b/src/components/views/room_settings/AliasSettings.js @@ -154,7 +154,7 @@ module.exports = React.createClass({ } else { var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - Modal.createDialog(ErrorDialog, { + Modal.createTrackedDialog('Invalid alias format', '', ErrorDialog, { title: _t('Invalid alias format'), description: _t('\'%(alias)s\' is not a valid format for an alias', { alias: alias }), }); @@ -170,7 +170,7 @@ module.exports = React.createClass({ } else { var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - Modal.createDialog(ErrorDialog, { + Modal.createTrackedDialog('Invalid address format', '', ErrorDialog, { title: _t('Invalid address format'), description: _t('\'%(alias)s\' is not a valid format for an address', { alias: alias }), }); diff --git a/src/components/views/rooms/AppsDrawer.js b/src/components/views/rooms/AppsDrawer.js index 5427d4ec6d..4bc98abb6f 100644 --- a/src/components/views/rooms/AppsDrawer.js +++ b/src/components/views/rooms/AppsDrawer.js @@ -166,7 +166,7 @@ module.exports = React.createClass({ const src = (this.scalarClient !== null && this.scalarClient.hasCredentials()) ? this.scalarClient.getScalarInterfaceUrlForRoom(this.props.room.roomId, 'add_integ') : null; - Modal.createDialog(IntegrationsManager, { + Modal.createTrackedDialog('Integrations Manager', '', IntegrationsManager, { src: src, }, "mx_IntegrationsManager"); }, diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index 815f0a3c6a..776d8a264b 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -369,7 +369,7 @@ module.exports = withMatrixClient(React.createClass({ onCryptoClicked: function(e) { var event = this.props.mxEvent; - Modal.createDialogAsync((cb) => { + Modal.createTrackedDialogAsync('Encrypted Event Dialog', '', (cb) => { require(['../../../async-components/views/dialogs/EncryptedEventDialog'], cb); }, { event: event, diff --git a/src/components/views/rooms/MemberInfo.js b/src/components/views/rooms/MemberInfo.js index 290bd35483..64eeddb406 100644 --- a/src/components/views/rooms/MemberInfo.js +++ b/src/components/views/rooms/MemberInfo.js @@ -229,7 +229,7 @@ module.exports = withMatrixClient(React.createClass({ const membership = this.props.member.membership; const kickLabel = membership === "invite" ? _t("Disinvite") : _t("Kick"); const ConfirmUserActionDialog = sdk.getComponent("dialogs.ConfirmUserActionDialog"); - Modal.createDialog(ConfirmUserActionDialog, { + Modal.createTrackedDialog('Confirm User Action Dialog', 'onKick', ConfirmUserActionDialog, { member: this.props.member, action: kickLabel, askReason: membership == "join", @@ -248,7 +248,7 @@ module.exports = withMatrixClient(React.createClass({ }, function(err) { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); console.error("Kick error: " + err); - Modal.createDialog(ErrorDialog, { + Modal.createTrackedDialog('Failed to kick', '', ErrorDialog, { title: _t("Failed to kick"), description: ((err && err.message) ? err.message : "Operation failed"), }); @@ -262,7 +262,7 @@ module.exports = withMatrixClient(React.createClass({ onBanOrUnban: function() { const ConfirmUserActionDialog = sdk.getComponent("dialogs.ConfirmUserActionDialog"); - Modal.createDialog(ConfirmUserActionDialog, { + Modal.createTrackedDialog('Confirm User Action Dialog', 'onBanOrUnban', ConfirmUserActionDialog, { member: this.props.member, action: this.props.member.membership == 'ban' ? _t("Unban") : _t("Ban"), askReason: this.props.member.membership != 'ban', @@ -290,7 +290,7 @@ module.exports = withMatrixClient(React.createClass({ }, function(err) { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); console.error("Ban error: " + err); - Modal.createDialog(ErrorDialog, { + Modal.createTrackedDialog('Failed to ban user', '', ErrorDialog, { title: _t("Error"), description: _t("Failed to ban user"), }); @@ -340,7 +340,7 @@ module.exports = withMatrixClient(React.createClass({ console.log("Mute toggle success"); }, function(err) { console.error("Mute error: " + err); - Modal.createDialog(ErrorDialog, { + Modal.createTrackedDialog('Failed to mute user', '', ErrorDialog, { title: _t("Error"), description: _t("Failed to mute user"), }); @@ -385,7 +385,7 @@ module.exports = withMatrixClient(React.createClass({ dis.dispatch({action: 'view_set_mxid'}); } else { console.error("Toggle moderator error:" + err); - Modal.createDialog(ErrorDialog, { + Modal.createTrackedDialog('Failed to toggle moderator status', '', ErrorDialog, { title: _t("Error"), description: _t("Failed to toggle moderator status"), }); @@ -406,7 +406,7 @@ module.exports = withMatrixClient(React.createClass({ }, function(err) { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); console.error("Failed to change power level " + err); - Modal.createDialog(ErrorDialog, { + Modal.createTrackedDialog('Failed to change power level', '', ErrorDialog, { title: _t("Error"), description: _t("Failed to change power level"), }); @@ -435,7 +435,7 @@ module.exports = withMatrixClient(React.createClass({ var myPower = powerLevelEvent.getContent().users[this.props.matrixClient.credentials.userId]; if (parseInt(myPower) === parseInt(powerLevel)) { var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); - Modal.createDialog(QuestionDialog, { + Modal.createTrackedDialog('Promote to PL100 Warning', '', QuestionDialog, { title: _t("Warning!"), description:
diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js index 14f52706ec..51b595eab0 100644 --- a/src/components/views/rooms/MessageComposer.js +++ b/src/components/views/rooms/MessageComposer.js @@ -99,7 +99,7 @@ export default class MessageComposer extends React.Component { ); } - Modal.createDialog(QuestionDialog, { + Modal.createTrackedDialog('Upload Files confirmation', '', QuestionDialog, { title: _t('Upload Files'), description: (
diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index b2c1436365..6d6cc0f3f4 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -31,6 +31,7 @@ import KeyCode from '../../../KeyCode'; import Modal from '../../../Modal'; import sdk from '../../../index'; import { _t } from '../../../languageHandler'; +import Analytics from '../../../Analytics'; import dis from '../../../dispatcher'; import UserSettingsStore from '../../../UserSettingsStore'; @@ -160,6 +161,8 @@ export default class MessageComposerInput extends React.Component { const isRichtextEnabled = UserSettingsStore.getSyncedSetting('MessageComposerInput.isRichTextEnabled', false); + Analytics.setRichtextMode(isRichtextEnabled); + this.state = { // whether we're in rich text or markdown mode isRichtextEnabled, @@ -280,11 +283,10 @@ export default class MessageComposerInput extends React.Component { } break; case 'quote': { - let {body} = payload.event.getContent(); /// XXX: Not doing rich-text quoting from formatted-body because draft-js /// has regressed such that when links are quoted, errors are thrown. See /// https://github.com/vector-im/riot-web/issues/4756. - body = escape(body); + let body = escape(payload.text); if (body) { let content = RichText.htmlToContentState(`
${body}
`); if (!this.state.isRichtextEnabled) { @@ -457,6 +459,19 @@ export default class MessageComposerInput extends React.Component { state.editorState = RichText.attachImmutableEntitiesToEmoji( state.editorState); + // Hide the autocomplete if the cursor location changes but the plaintext + // content stays the same. We don't hide if the pt has changed because the + // autocomplete will probably have different completions to show. + if ( + !state.editorState.getSelection().equals( + this.state.editorState.getSelection() + ) + && state.editorState.getCurrentContent().getPlainText() === + this.state.editorState.getCurrentContent().getPlainText() + ) { + this.autocomplete.hide(); + } + if (state.editorState.getCurrentContent().hasText()) { this.onTypingActivity(); } else { @@ -513,6 +528,8 @@ export default class MessageComposerInput extends React.Component { contentState = ContentState.createFromText(markdown); } + Analytics.setRichtextMode(enabled); + this.setState({ editorState: this.createEditorState(enabled, contentState), isRichtextEnabled: enabled, @@ -704,7 +721,7 @@ export default class MessageComposerInput extends React.Component { }, function(err) { console.error("Command failure: %s", err); const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - Modal.createDialog(ErrorDialog, { + Modal.createTrackedDialog('Server error', '', ErrorDialog, { title: _t("Server error"), description: ((err && err.message) ? err.message : _t("Server unavailable, overloaded, or something else went wrong.")), }); @@ -712,7 +729,8 @@ export default class MessageComposerInput extends React.Component { } else if (cmd.error) { console.error(cmd.error); const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - Modal.createDialog(ErrorDialog, { + // TODO possibly track which command they ran (not its Arguments) here + Modal.createTrackedDialog('Command error', '', ErrorDialog, { title: _t("Command error"), description: cmd.error, }); @@ -790,15 +808,10 @@ export default class MessageComposerInput extends React.Component { let sendHtmlFn = this.client.sendHtmlMessage; let sendTextFn = this.client.sendTextMessage; - if (this.state.isRichtextEnabled) { - this.historyManager.addItem( - contentHTML ? contentHTML : contentText, - contentHTML ? 'html' : 'markdown', - ); - } else { - // Always store MD input as input history - this.historyManager.addItem(contentText, 'markdown'); - } + this.historyManager.save( + contentState, + this.state.isRichtextEnabled ? 'html' : 'markdown', + ); if (contentText.startsWith('/me')) { contentText = contentText.substring(4); @@ -872,6 +885,7 @@ export default class MessageComposerInput extends React.Component { } } else { this.moveAutocompleteSelection(up); + e.preventDefault(); } }; diff --git a/src/components/views/rooms/RoomHeader.js b/src/components/views/rooms/RoomHeader.js index 85aedadf64..edd89e4a35 100644 --- a/src/components/views/rooms/RoomHeader.js +++ b/src/components/views/rooms/RoomHeader.js @@ -119,7 +119,7 @@ module.exports = React.createClass({ const errMsg = (typeof err === "string") ? err : (err.error || ""); const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); console.error("Failed to set avatar: " + errMsg); - Modal.createDialog(ErrorDialog, { + Modal.createTrackedDialog('Failed to set avatar', '', ErrorDialog, { title: _t("Error"), description: _t("Failed to set avatar."), }); diff --git a/src/components/views/rooms/RoomSettings.js b/src/components/views/rooms/RoomSettings.js index d6a973f648..58473f1fb3 100644 --- a/src/components/views/rooms/RoomSettings.js +++ b/src/components/views/rooms/RoomSettings.js @@ -46,7 +46,7 @@ const BannedUser = React.createClass({ _onUnbanClick: function() { const ConfirmUserActionDialog = sdk.getComponent("dialogs.ConfirmUserActionDialog"); - Modal.createDialog(ConfirmUserActionDialog, { + Modal.createTrackedDialog('Confirm User Action Dialog', 'onUnbanClick', ConfirmUserActionDialog, { member: this.props.member, action: _t('Unban'), danger: false, @@ -58,7 +58,7 @@ const BannedUser = React.createClass({ ).catch((err) => { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); console.error("Failed to unban: " + err); - Modal.createDialog(ErrorDialog, { + Modal.createTrackedDialog('Failed to unban', '', ErrorDialog, { title: _t('Error'), description: _t('Failed to unban'), }); @@ -423,7 +423,7 @@ module.exports = React.createClass({ ev.preventDefault(); var value = ev.target.value; - Modal.createDialog(QuestionDialog, { + Modal.createTrackedDialog('Privacy warning', '', QuestionDialog, { title: _t('Privacy warning'), description:
@@ -516,7 +516,7 @@ module.exports = React.createClass({ onManageIntegrations(ev) { ev.preventDefault(); var IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager"); - Modal.createDialog(IntegrationsManager, { + Modal.createTrackedDialog('Integrations Manager', 'onManageIntegrations', IntegrationsManager, { src: (this.scalarClient !== null && this.scalarClient.hasCredentials()) ? this.scalarClient.getScalarInterfaceUrlForRoom(this.props.room.roomId) : null, @@ -549,7 +549,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 }), }); @@ -560,7 +560,7 @@ module.exports = React.createClass({ if (!this.refs.encrypt.checked) return; var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); - Modal.createDialog(QuestionDialog, { + Modal.createTrackedDialog('E2E Enable Warning', '', QuestionDialog, { title: _t('Warning!'), description: (
diff --git a/src/components/views/settings/AddPhoneNumber.js b/src/components/views/settings/AddPhoneNumber.js index 7bc551477e..16e768a23f 100644 --- a/src/components/views/settings/AddPhoneNumber.js +++ b/src/components/views/settings/AddPhoneNumber.js @@ -82,7 +82,7 @@ export default withMatrixClient(React.createClass({ }).catch((err) => { console.error("Unable to add phone number: " + err); let msg = err.message; - Modal.createDialog(ErrorDialog, { + Modal.createTrackedDialog('Add Phone Number Error', '', ErrorDialog, { title: _t("Error"), description: msg, }); @@ -107,7 +107,7 @@ export default withMatrixClient(React.createClass({ } msgElements.push(
{msg}
); } - Modal.createDialog(TextInputDialog, { + Modal.createTrackedDialog('Prompt for MSISDN Verification Code', '', TextInputDialog, { title: _t("Enter Code"), description:
{msgElements}
, button: _t("Submit"), diff --git a/src/components/views/settings/ChangePassword.js b/src/components/views/settings/ChangePassword.js index 14ec9806b4..f3c0d9033c 100644 --- a/src/components/views/settings/ChangePassword.js +++ b/src/components/views/settings/ChangePassword.js @@ -104,7 +104,7 @@ module.exports = React.createClass({ } const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); - Modal.createDialog(QuestionDialog, { + Modal.createTrackedDialog('Change Password', '', QuestionDialog, { title: _t("Warning!"), description:
@@ -164,7 +164,7 @@ module.exports = React.createClass({ const deferred = Promise.defer(); // Ask for an email otherwise the user has no way to reset their password const SetEmailDialog = sdk.getComponent("dialogs.SetEmailDialog"); - Modal.createDialog(SetEmailDialog, { + Modal.createTrackedDialog('Do you want to set an email address?', '', SetEmailDialog, { title: _t('Do you want to set an email address?'), onFinished: (confirmed) => { // ignore confirmed, setting an email is optional @@ -175,15 +175,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', 'Change Password', (cb) => { + require.ensure(['../../../async-components/views/dialogs/ExportE2eKeysDialog'], () => { + cb(require('../../../async-components/views/dialogs/ExportE2eKeysDialog')); + }, "e2e-export"); + }, { + matrixClient: MatrixClientPeg.get(), + }); }, onClickChange: function() { diff --git a/src/components/views/settings/DevicesPanelEntry.js b/src/components/views/settings/DevicesPanelEntry.js index f295a7c2d5..69534f09b1 100644 --- a/src/components/views/settings/DevicesPanelEntry.js +++ b/src/components/views/settings/DevicesPanelEntry.js @@ -71,8 +71,8 @@ export default class DevicesPanelEntry extends React.Component { // pop up an interactive auth dialog var InteractiveAuthDialog = sdk.getComponent("dialogs.InteractiveAuthDialog"); - Modal.createDialog(InteractiveAuthDialog, { - title: _t("Authentication"), + Modal.createTrackedDialog('Delete Device Dialog', InteractiveAuthDialog, { + title: _t("Authentication"), matrixClient: MatrixClientPeg.get(), authData: error.data, makeRequest: this._makeDeleteRequest, diff --git a/src/createRoom.js b/src/createRoom.js index 74e4b3c2fc..944c6a70a1 100644 --- a/src/createRoom.js +++ b/src/createRoom.js @@ -115,7 +115,7 @@ function createRoom(opts) { action: 'join_room_error', }); console.error("Failed to create room " + roomId + " " + err); - Modal.createDialog(ErrorDialog, { + Modal.createTrackedDialog('Failure to create room', '', ErrorDialog, { title: _t("Failure to create room"), description: _t("Server may be unavailable, overloaded, or you hit a bug."), }); diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index a505570e04..3baee40a84 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -346,6 +346,7 @@ "Hangup": "Hangup", "Hide Apps": "Hide Apps", "Hide join/leave messages (invites/kicks/bans unaffected)": "Hide join/leave messages (invites/kicks/bans unaffected)", + "Hide avatar and display name changes": "Hide avatar and display name changes", "Hide read receipts": "Hide read receipts", "Hide Text Formatting Toolbar": "Hide Text Formatting Toolbar", "Historical": "Historical", @@ -575,6 +576,7 @@ "To configure the room": "To configure the room", "to demote": "to demote", "to favourite": "to favourite", + "To get started, please pick a username!": "To get started, please pick a username!", "To invite users into the room": "To invite users into the room", "To kick users": "To kick users", "To link to a room it must have an address.": "To link to a room it must have an address.", diff --git a/src/shouldHideEvent.js b/src/shouldHideEvent.js index afc8fdc596..1501e28875 100644 --- a/src/shouldHideEvent.js +++ b/src/shouldHideEvent.js @@ -14,38 +14,37 @@ limitations under the License. */ -function _isLeaveOrJoin(ev) { - const isMemberEvent = ev.getType() === 'm.room.member' && ev.getStateKey() !== undefined; - if (!isMemberEvent) { - return false; // bail early: all the checks below concern member events only - } +function memberEventDiff(ev) { + const diff = { + isMemberEvent: ev.getType() === 'm.room.member', + }; - // TODO: These checks are done to make sure we're dealing with membership transitions not avatar changes / dupe joins - // These checks are also being done in TextForEvent and should really reside in the JS SDK as a helper function - const membership = ev.getContent().membership; - const prevMembership = ev.getPrevContent().membership; - if (membership === prevMembership && membership === 'join') { - // join -> join : This happens when display names change / avatars are set / genuine dupe joins with no changes. - // Find out which we're dealing with. - if (ev.getPrevContent().displayname !== ev.getContent().displayname) { - return false; // display name changed - } - if (ev.getPrevContent().avatar_url !== ev.getContent().avatar_url) { - return false; // avatar url changed - } - // dupe join event, fall through to hide rules - } + // If is not a Member Event then the other checks do not apply, so bail early. + if (!diff.isMemberEvent) return diff; + const content = ev.getContent(); + const prevContent = ev.getPrevContent(); - // this only applies to joins/invited joins/leaves not invites/kicks/bans - const isJoin = membership === 'join' && prevMembership !== 'ban'; - const isLeave = membership === 'leave' && ev.getStateKey() === ev.getSender(); - return isJoin || isLeave; + diff.isJoin = content.membership === 'join' && prevContent.membership !== 'ban'; + diff.isPart = content.membership === 'leave' && ev.getStateKey() === ev.getSender(); + + const isJoinToJoin = content.membership === prevContent.membership && content.membership === 'join'; + diff.isDisplaynameChange = isJoinToJoin && content.displayname !== prevContent.displayname; + diff.isAvatarChange = isJoinToJoin && content.avatar_url !== prevContent.avatar_url; + return diff; } -export default function(ev, syncedSettings) { +export default function shouldHideEvent(ev, syncedSettings) { // Hide redacted events if (syncedSettings['hideRedactions'] && ev.isRedacted()) return true; - if (syncedSettings['hideJoinLeaves'] && _isLeaveOrJoin(ev)) return true; + + const eventDiff = memberEventDiff(ev); + + if (eventDiff.isMemberEvent) { + if (syncedSettings['hideJoinLeaves'] && (eventDiff.isJoin || eventDiff.isPart)) return true; + const isMemberAvatarDisplaynameChange = eventDiff.isAvatarChange || eventDiff.isDisplaynameChange; + if (syncedSettings['hideAvatarDisplaynameChanges'] && isMemberAvatarDisplaynameChange) return true; + } + return false; } diff --git a/src/stores/RoomViewStore.js b/src/stores/RoomViewStore.js index 865caa8997..bd9d3ea0fa 100644 --- a/src/stores/RoomViewStore.js +++ b/src/stores/RoomViewStore.js @@ -221,7 +221,7 @@ class RoomViewStore extends Store { }); const msg = err.message ? err.message : JSON.stringify(err); const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - Modal.createDialog(ErrorDialog, { + Modal.createTrackedDialog('Failed to join room', '', ErrorDialog, { title: _t("Failed to join room"), description: msg, });