Merge remote-tracking branch 'remotes/origin/develop' into set_default_federate_by_settings

# Conflicts:
#	src/components/structures/MatrixChat.js
This commit is contained in:
Michael Telatynski 2017-08-17 13:39:19 +01:00
commit bdb2d6b475
No known key found for this signature in database
GPG key ID: 3F879DA5AD802A5E
93 changed files with 5888 additions and 1285 deletions

View file

@ -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'),
});

View file

@ -301,13 +301,13 @@ export default React.createClass({
case PageTypes.UserView:
page_element = null; // deliberately null for now
right_panel = <RightPanel userId={this.props.viewUserId} opacity={this.props.rightOpacity} />;
right_panel = <RightPanel opacity={this.props.rightOpacity} />;
break;
case PageTypes.GroupView:
page_element = <GroupView
groupId={this.props.currentGroupId}
/>;
//right_panel = <RightPanel userId={this.props.viewUserId} opacity={this.props.rightOpacity} />;
//right_panel = <RightPanel opacity={this.props.rightOpacity} />;
break;
}

View file

@ -1,6 +1,7 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2017 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -31,6 +32,7 @@ import dis from "../../dispatcher";
import Modal from "../../Modal";
import Tinter from "../../Tinter";
import sdk from '../../index';
import { showStartChatInviteDialog, showRoomInviteDialog } from '../../Invite';
import * as Rooms from '../../Rooms';
import linkifyMatrix from "../../linkify-matrix";
import * as Lifecycle from '../../Lifecycle';
@ -131,9 +133,6 @@ module.exports = React.createClass({
// the master view we are showing.
view: VIEWS.LOADING,
// a thing to call showScreen with once login completes.
screenAfterLogin: this.props.initialScreenAfterLogin,
// What the LoggedInView would be showing if visible
page_type: null,
@ -147,8 +146,6 @@ module.exports = React.createClass({
collapse_lhs: false,
collapse_rhs: false,
ready: false,
width: 10000,
leftOpacity: 1.0,
middleOpacity: 1.0,
rightOpacity: 1.0,
@ -274,6 +271,15 @@ module.exports = React.createClass({
register_hs_url: paramHs,
});
}
// a thing to call showScreen with once login completes. this is kept
// outside this.state because updating it should never trigger a
// rerender.
this._screenAfterLogin = this.props.initialScreenAfterLogin;
this._windowWidth = 10000;
this.handleResize();
window.addEventListener('resize', this.handleResize);
},
componentDidMount: function() {
@ -294,9 +300,6 @@ module.exports = React.createClass({
linkifyMatrix.onGroupClick = this.onGroupClick;
}
window.addEventListener('resize', this.handleResize);
this.handleResize();
const teamServerConfig = this.props.config.teamServerConfig || {};
Lifecycle.initRtsClient(teamServerConfig.teamServerURL);
@ -312,13 +315,12 @@ module.exports = React.createClass({
// if the user has followed a login or register link, don't reanimate
// the old creds, but rather go straight to the relevant page
const firstScreen = this.state.screenAfterLogin ?
this.state.screenAfterLogin.screen : null;
const firstScreen = this._screenAfterLogin ?
this._screenAfterLogin.screen : null;
if (firstScreen === 'login' ||
firstScreen === 'register' ||
firstScreen === 'forgot_password') {
this.setState({loading: false});
this._showScreenAfterLogin();
return;
}
@ -367,9 +369,9 @@ module.exports = React.createClass({
}
const newState = {
viewUserId: null,
};
Object.assign(newState, state);
this.setState(newState);
};
Object.assign(newState, state);
this.setState(newState);
},
onAction: function(payload) {
@ -410,7 +412,7 @@ module.exports = React.createClass({
this._leaveRoom(payload.room_id);
break;
case 'reject_invite':
Modal.createDialog(QuestionDialog, {
Modal.createTrackedDialog('Reject invitation', '', QuestionDialog, {
title: _t('Reject invitation'),
description: _t('Are you sure you want to reject the invitation?'),
onFinished: (confirm) => {
@ -426,7 +428,7 @@ module.exports = React.createClass({
}
}, (err) => {
modal.close();
Modal.createDialog(ErrorDialog, {
Modal.createTrackedDialog('Failed to reject invitation', '', ErrorDialog, {
title: _t('Failed to reject invitation'),
description: err.toString(),
});
@ -512,7 +514,7 @@ module.exports = React.createClass({
this._createChat();
break;
case 'view_invite':
this._invite(payload.roomId);
showRoomInviteDialog(payload.roomId);
break;
case 'notifier_enabled':
this.forceUpdate();
@ -728,7 +730,7 @@ module.exports = React.createClass({
_setMxId: function(payload) {
const SetMxIdDialog = sdk.getComponent('views.dialogs.SetMxIdDialog');
const close = Modal.createDialog(SetMxIdDialog, {
const close = Modal.createTrackedDialog('Set MXID', '', SetMxIdDialog, {
homeserverUrl: MatrixClientPeg.get().getHomeserverUrl(),
onFinished: (submitted, credentials) => {
if (!submitted) {
@ -766,13 +768,7 @@ module.exports = React.createClass({
dis.dispatch({action: 'view_set_mxid'});
return;
}
const ChatInviteDialog = sdk.getComponent("dialogs.ChatInviteDialog");
Modal.createDialog(ChatInviteDialog, {
title: _t('Start a chat'),
description: _t("Who would you like to communicate with?"),
placeholder: _t("Email, name or matrix ID"),
button: _t("Start Chat"),
});
showStartChatInviteDialog();
},
_createRoom: function() {
@ -787,7 +783,7 @@ module.exports = React.createClass({
return;
}
const CreateRoomDialog = sdk.getComponent('dialogs.CreateRoomDialog');
Modal.createDialog(CreateRoomDialog, {
Modal.createTrackedDialog('Create Room', '', CreateRoomDialog, {
onFinished: (shouldCreate, name, noFederate) => {
if (shouldCreate) {
const createOpts = {};
@ -829,7 +825,7 @@ module.exports = React.createClass({
return;
}
const close = Modal.createDialog(ChatCreateOrReuseDialog, {
const close = Modal.createTrackedDialog('Chat create or reuse', '', ChatCreateOrReuseDialog, {
userId: userId,
onFinished: (success) => {
if (!success && goHomeOnCancel) {
@ -855,23 +851,12 @@ module.exports = React.createClass({
}).close;
},
_invite: function(roomId) {
const ChatInviteDialog = sdk.getComponent("dialogs.ChatInviteDialog");
Modal.createDialog(ChatInviteDialog, {
title: _t('Invite new room members'),
description: _t('Who would you like to add to this room?'),
button: _t('Send Invites'),
placeholder: _t("Email, name or matrix ID"),
roomId: roomId,
});
},
_leaveRoom: function(roomId) {
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
const roomToLeave = MatrixClientPeg.get().getRoom(roomId);
Modal.createDialog(QuestionDialog, {
Modal.createTrackedDialog('Leave room', '', QuestionDialog, {
title: _t("Leave room"),
description: (
<span>
@ -894,7 +879,7 @@ module.exports = React.createClass({
}, (err) => {
modal.close();
console.error("Failed to leave room " + roomId + " " + err);
Modal.createDialog(ErrorDialog, {
Modal.createTrackedDialog('Failed to leave room', '', ErrorDialog, {
title: _t("Failed to leave room"),
description: (err && err.message ? err.message :
_t("Server may be unavailable, overloaded, or you hit a bug.")),
@ -990,14 +975,12 @@ module.exports = React.createClass({
_showScreenAfterLogin: function() {
// If screenAfterLogin is set, use that, then null it so that a second login will
// result in view_home_page, _user_settings or _room_directory
if (this.state.screenAfterLogin && this.state.screenAfterLogin.screen) {
if (this._screenAfterLogin && this._screenAfterLogin.screen) {
this.showScreen(
this.state.screenAfterLogin.screen,
this.state.screenAfterLogin.params,
this._screenAfterLogin.screen,
this._screenAfterLogin.params,
);
// XXX: is this necessary? `showScreen` should do it for us.
this.notifyNewScreen(this.state.screenAfterLogin.screen);
this.setState({screenAfterLogin: null});
this._screenAfterLogin = null;
} else if (localStorage && localStorage.getItem('mx_last_room_id')) {
// Before defaulting to directory, show the last viewed room
dis.dispatch({
@ -1090,7 +1073,7 @@ module.exports = React.createClass({
});
cli.on('Session.logged_out', function(call) {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, {
Modal.createTrackedDialog('Signed out', '', ErrorDialog, {
title: _t('Signed Out'),
description: _t('For security, this session has been signed out. Please sign in again.'),
});
@ -1203,21 +1186,24 @@ module.exports = React.createClass({
} else if (screen.indexOf('user/') == 0) {
const userId = screen.substring(5);
if (params.action === 'chat') {
this._chatCreateOrReuse(userId);
return;
}
// Wait for the first sync so that `getRoom` gives us a room object if it's
// in the sync response
const waitFor = this.firstSyncPromise ?
this.firstSyncPromise.promise : Promise.resolve();
waitFor.then(() => {
if (params.action === 'chat') {
this._chatCreateOrReuse(userId);
return;
}
this.setState({ viewUserId: userId });
this._setPage(PageTypes.UserView);
this.notifyNewScreen('user/' + userId);
const member = new Matrix.RoomMember(null, userId);
if (member) {
this._setPage(PageTypes.UserView);
this.notifyNewScreen('user/' + userId);
const member = new Matrix.RoomMember(null, userId);
dis.dispatch({
action: 'view_user',
member: member,
});
}
});
} else if (screen.indexOf('group/') == 0) {
const groupId = screen.substring(6);
@ -1274,20 +1260,20 @@ module.exports = React.createClass({
const hideRhsThreshold = 820;
const showRhsThreshold = 820;
if (this.state.width > hideLhsThreshold && window.innerWidth <= hideLhsThreshold) {
if (this._windowWidth > hideLhsThreshold && window.innerWidth <= hideLhsThreshold) {
dis.dispatch({ action: 'hide_left_panel' });
}
if (this.state.width <= showLhsThreshold && window.innerWidth > showLhsThreshold) {
if (this._windowWidth <= showLhsThreshold && window.innerWidth > showLhsThreshold) {
dis.dispatch({ action: 'show_left_panel' });
}
if (this.state.width > hideRhsThreshold && window.innerWidth <= hideRhsThreshold) {
if (this._windowWidth > hideRhsThreshold && window.innerWidth <= hideRhsThreshold) {
dis.dispatch({ action: 'hide_right_panel' });
}
if (this.state.width <= showRhsThreshold && window.innerWidth > showRhsThreshold) {
if (this._windowWidth <= showRhsThreshold && window.innerWidth > showRhsThreshold) {
dis.dispatch({ action: 'show_right_panel' });
}
this.setState({width: window.innerWidth});
this._windowWidth = window.innerWidth;
},
onRoomCreated: function(roomId) {

View file

@ -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];

View file

@ -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() {

View file

@ -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,
});

View file

@ -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,

View file

@ -85,6 +85,10 @@ const SETTINGS_LABELS = [
id: 'hideJoinLeaves',
label: 'Hide join/leave messages (invites/kicks/bans unaffected)',
},
{
id: 'hideAvatarDisplaynameChanges',
label: 'Hide avatar and display name changes',
},
{
id: 'useCompactLayout',
label: 'Use compact timeline layout',
@ -101,6 +105,10 @@ const SETTINGS_LABELS = [
id: 'MessageComposerInput.autoReplaceEmoji',
label: 'Automatically replace plain text Emoji',
},
{
id: 'Pill.shouldHidePillAvatar',
label: 'Hide avatars in user and room mentions',
},
/*
{
id: 'useFixedWidthFont',
@ -331,7 +339,7 @@ module.exports = React.createClass({
}, function(error) {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
console.error("Failed to load user settings: " + error);
Modal.createDialog(ErrorDialog, {
Modal.createTrackedDialog('Can\'t load user settings', '', ErrorDialog, {
title: _t("Can't load user settings"),
description: ((error && error.message) ? error.message : _t("Server may be unavailable or overloaded")),
});
@ -364,7 +372,7 @@ module.exports = React.createClass({
// const errMsg = (typeof err === "string") ? err : (err.error || "");
console.error("Failed to set avatar: " + err);
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, {
Modal.createTrackedDialog('Failed to set avatar', '', ErrorDialog, {
title: _t("Failed to set avatar."),
description: ((err && err.message) ? err.message : _t("Operation failed")),
});
@ -373,7 +381,7 @@ module.exports = React.createClass({
onLogoutClicked: function(ev) {
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
Modal.createDialog(QuestionDialog, {
Modal.createTrackedDialog('Logout E2E Export', '', QuestionDialog, {
title: _t("Sign out"),
description:
<div>
@ -409,7 +417,7 @@ module.exports = React.createClass({
}
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
console.error("Failed to change password: " + errMsg);
Modal.createDialog(ErrorDialog, {
Modal.createTrackedDialog('Failed to change password', '', ErrorDialog, {
title: _t("Error"),
description: errMsg,
});
@ -417,7 +425,7 @@ module.exports = React.createClass({
onPasswordChanged: function() {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, {
Modal.createTrackedDialog('Password changed', '', ErrorDialog, {
title: _t("Success"),
description: _t(
"Your password was successfully changed. You will not receive " +
@ -442,7 +450,7 @@ module.exports = React.createClass({
const emailAddress = this.refs.add_email_input.value;
if (!Email.looksValid(emailAddress)) {
Modal.createDialog(ErrorDialog, {
Modal.createTrackedDialog('Invalid email address', '', ErrorDialog, {
title: _t("Invalid Email Address"),
description: _t("This doesn't appear to be a valid email address"),
});
@ -452,7 +460,7 @@ module.exports = React.createClass({
// we always bind emails when registering, so let's do the
// same here.
this._addThreepid.addEmailAddress(emailAddress, true).done(() => {
Modal.createDialog(QuestionDialog, {
Modal.createTrackedDialog('Verification Pending', '', QuestionDialog, {
title: _t("Verification Pending"),
description: _t(
"Please check your email and click on the link it contains. Once this " +
@ -464,7 +472,7 @@ module.exports = React.createClass({
}, (err) => {
this.setState({email_add_pending: false});
console.error("Unable to add email address " + emailAddress + " " + err);
Modal.createDialog(ErrorDialog, {
Modal.createTrackedDialog('Unable to add email address', '', ErrorDialog, {
title: _t("Unable to add email address"),
description: ((err && err.message) ? err.message : _t("Operation failed")),
});
@ -475,7 +483,7 @@ module.exports = React.createClass({
onRemoveThreepidClicked: function(threepid) {
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
Modal.createDialog(QuestionDialog, {
Modal.createTrackedDialog('Remove 3pid', '', QuestionDialog, {
title: _t("Remove Contact Information?"),
description: _t("Remove %(threePid)s?", { threePid: threepid.address }),
button: _t('Remove'),
@ -489,7 +497,7 @@ module.exports = React.createClass({
}).catch((err) => {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
console.error("Unable to remove contact information: " + err);
Modal.createDialog(ErrorDialog, {
Modal.createTrackedDialog('Remove 3pid failed', '', ErrorDialog, {
title: _t("Unable to remove contact information"),
description: ((err && err.message) ? err.message : _t("Operation failed")),
});
@ -521,7 +529,7 @@ module.exports = React.createClass({
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
const message = _t("Unable to verify email address.") + " " +
_t("Please check your email and click on the link it contains. Once this is done, click continue.");
Modal.createDialog(QuestionDialog, {
Modal.createTrackedDialog('Verification Pending', '', QuestionDialog, {
title: _t("Verification Pending"),
description: message,
button: _t('Continue'),
@ -530,7 +538,7 @@ module.exports = React.createClass({
} else {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
console.error("Unable to verify email address: " + err);
Modal.createDialog(ErrorDialog, {
Modal.createTrackedDialog('Unable to verify email address', '', ErrorDialog, {
title: _t("Unable to verify email address."),
description: ((err && err.message) ? err.message : _t("Operation failed")),
});
@ -540,7 +548,7 @@ module.exports = React.createClass({
_onDeactivateAccountClicked: function() {
const DeactivateAccountDialog = sdk.getComponent("dialogs.DeactivateAccountDialog");
Modal.createDialog(DeactivateAccountDialog, {});
Modal.createTrackedDialog('Deactivate Account', '', DeactivateAccountDialog, {});
},
_onBugReportClicked: function() {
@ -548,7 +556,7 @@ module.exports = React.createClass({
if (!BugReportDialog) {
return;
}
Modal.createDialog(BugReportDialog, {});
Modal.createTrackedDialog('Bug Report Dialog', '', BugReportDialog, {});
},
_onClearCacheClicked: function() {
@ -585,27 +593,23 @@ module.exports = React.createClass({
},
_onExportE2eKeysClicked: function() {
Modal.createDialogAsync(
(cb) => {
require.ensure(['../../async-components/views/dialogs/ExportE2eKeysDialog'], () => {
cb(require('../../async-components/views/dialogs/ExportE2eKeysDialog'));
}, "e2e-export");
}, {
matrixClient: MatrixClientPeg.get(),
},
);
Modal.createTrackedDialogAsync('Export E2E Keys', '', (cb) => {
require.ensure(['../../async-components/views/dialogs/ExportE2eKeysDialog'], () => {
cb(require('../../async-components/views/dialogs/ExportE2eKeysDialog'));
}, "e2e-export");
}, {
matrixClient: MatrixClientPeg.get(),
});
},
_onImportE2eKeysClicked: function() {
Modal.createDialogAsync(
(cb) => {
require.ensure(['../../async-components/views/dialogs/ImportE2eKeysDialog'], () => {
cb(require('../../async-components/views/dialogs/ImportE2eKeysDialog'));
}, "e2e-export");
}, {
matrixClient: MatrixClientPeg.get(),
},
);
Modal.createTrackedDialogAsync('Import E2E Keys', '', (cb) => {
require.ensure(['../../async-components/views/dialogs/ImportE2eKeysDialog'], () => {
cb(require('../../async-components/views/dialogs/ImportE2eKeysDialog'));
}, "e2e-export");
}, {
matrixClient: MatrixClientPeg.get(),
});
},
_renderReferral: function() {
@ -855,7 +859,13 @@ module.exports = React.createClass({
if (this.props.enableLabs === false) return null;
UserSettingsStore.doTranslations();
const features = UserSettingsStore.LABS_FEATURES.map((feature) => {
const features = [];
UserSettingsStore.LABS_FEATURES.forEach((feature) => {
// This feature has an override and will be set to the default, so do not
// show it here.
if (feature.override) {
return;
}
// TODO: this ought to be a separate component so that we don't need
// to rebind the onChange each time we render
const onChange = (e) => {
@ -863,7 +873,7 @@ module.exports = React.createClass({
this.forceUpdate();
};
return (
features.push(
<div key={feature.id} className="mx_UserSettings_toggle">
<input
type="checkbox"
@ -873,9 +883,14 @@ module.exports = React.createClass({
onChange={ onChange }
/>
<label htmlFor={feature.id}>{feature.name}</label>
</div>
);
</div>);
});
// No labs section when there are no features in labs
if (features.length === 0) {
return null;
}
return (
<div>
<h3>{ _t("Labs") }</h3>
@ -1004,7 +1019,7 @@ module.exports = React.createClass({
this._refreshMediaDevices,
function() {
const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog');
Modal.createDialog(ErrorDialog, {
Modal.createTrackedDialog('No media permissions', '', ErrorDialog, {
title: _t('No media permissions'),
description: _t('You may need to manually permit Riot to access your microphone/webcam'),
});

View file

@ -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:
<div>
{ _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,
});

View file

@ -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 <div className="mx_Login_language_div">
<LanguageDropdown onOptionChange={this._onLanguageChange}
className="mx_Login_language"
value={languageHandler.getCurrentLanguage()}
/>
</div>;
},
render: function() {
const Loader = sdk.getComponent("elements.Spinner");
const LoginHeader = sdk.getComponent("login.LoginHeader");
@ -354,6 +374,7 @@ module.exports = React.createClass({
</a>
{ loginAsGuestJsx }
{ returnToAppJsx }
{ this._renderLanguageSetting() }
<LoginFooter />
</div>
</div>

View file

@ -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) => {

View file

@ -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",

View file

@ -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) => {

View file

@ -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() {

View file

@ -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")),
});

View file

@ -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 (
<BaseDialog className="mx_SetMxIdDialog"
onFinished={this.props.onFinished}
title="To get started, please pick a username!"
title={_t('To get started, please pick a username!')}
>
<div className="mx_Dialog_content">
<div className="mx_SetMxIdDialog_input_group">

View file

@ -1,5 +1,6 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -15,40 +16,37 @@ limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler';
import sdk from '../../../index';
import { getAddressType, inviteMultipleToRoom } from '../../../Invite';
import createRoom from '../../../createRoom';
import MatrixClientPeg from '../../../MatrixClientPeg';
import DMRoomMap from '../../../utils/DMRoomMap';
import Modal from '../../../Modal';
import AccessibleButton from '../elements/AccessibleButton';
import Promise from 'bluebird';
import dis from '../../../dispatcher';
import { addressTypes, getAddressType } from '../../../UserAddress.js';
const TRUNCATE_QUERY_LIST = 40;
const QUERY_USER_DIRECTORY_DEBOUNCE_MS = 200;
module.exports = React.createClass({
displayName: "ChatInviteDialog",
displayName: "UserPickerDialog",
propTypes: {
title: React.PropTypes.string.isRequired,
description: React.PropTypes.oneOfType([
React.PropTypes.element,
React.PropTypes.string,
]),
value: React.PropTypes.string,
placeholder: React.PropTypes.string,
roomId: React.PropTypes.string,
button: React.PropTypes.string,
focus: React.PropTypes.bool,
onFinished: React.PropTypes.func.isRequired,
title: PropTypes.string.isRequired,
description: PropTypes.node,
value: PropTypes.string,
placeholder: PropTypes.string,
roomId: PropTypes.string,
button: PropTypes.string,
focus: PropTypes.bool,
validAddressTypes: PropTypes.arrayOf(PropTypes.oneOfType(addressTypes)),
onFinished: PropTypes.func.isRequired,
},
getDefaultProps: function() {
return {
value: "",
focus: true,
validAddressTypes: addressTypes,
};
},
@ -56,9 +54,9 @@ module.exports = React.createClass({
return {
error: false,
// List of AddressTile.InviteAddressType objects representing
// List of UserAddressType objects representing
// the list of addresses we're going to invite
inviteList: [],
userList: [],
// Whether a search is ongoing
busy: false,
@ -68,7 +66,7 @@ module.exports = React.createClass({
serverSupportsUserDirectory: true,
// The query being searched for
query: "",
// List of AddressTile.InviteAddressType objects representing
// List of UserAddressType objects representing
// the set of auto-completion results for the current search
// query.
queryList: [],
@ -83,57 +81,14 @@ module.exports = React.createClass({
},
onButtonClick: function() {
let inviteList = this.state.inviteList.slice();
let userList = this.state.userList.slice();
// Check the text input field to see if user has an unconverted address
// If there is and it's valid add it to the local inviteList
// If there is and it's valid add it to the local userList
if (this.refs.textinput.value !== '') {
inviteList = this._addInputToList();
if (inviteList === null) return;
}
const addrTexts = inviteList.map(addr => addr.address);
if (inviteList.length > 0) {
if (this._isDmChat(addrTexts)) {
const userId = inviteList[0].address;
// Direct Message chat
const rooms = this._getDirectMessageRooms(userId);
if (rooms.length > 0) {
// A Direct Message room already exists for this user, so select a
// room from a list that is similar to the one in MemberInfo panel
const ChatCreateOrReuseDialog = sdk.getComponent(
"views.dialogs.ChatCreateOrReuseDialog",
);
const close = Modal.createDialog(ChatCreateOrReuseDialog, {
userId: userId,
onFinished: (success) => {
this.props.onFinished(success);
},
onNewDMClick: () => {
dis.dispatch({
action: 'start_chat',
user_id: userId,
});
close(true);
},
onExistingRoomSelected: (roomId) => {
dis.dispatch({
action: 'view_room',
room_id: roomId,
});
close(true);
},
}).close;
} else {
this._startChat(inviteList);
}
} else {
// Multi invite chat
this._startChat(inviteList);
}
} else {
// No addresses supplied
this.setState({ error: true });
userList = this._addInputToList();
if (userList === null) return;
}
this.props.onFinished(true, userList);
},
onCancel: function() {
@ -157,10 +112,10 @@ module.exports = React.createClass({
e.stopPropagation();
e.preventDefault();
if (this.addressSelector) this.addressSelector.chooseSelection();
} else if (this.refs.textinput.value.length === 0 && this.state.inviteList.length && e.keyCode === 8) { // backspace
} else if (this.refs.textinput.value.length === 0 && this.state.userList.length && e.keyCode === 8) { // backspace
e.stopPropagation();
e.preventDefault();
this.onDismissed(this.state.inviteList.length - 1)();
this.onDismissed(this.state.userList.length - 1)();
} else if (e.keyCode === 13) { // enter
e.stopPropagation();
e.preventDefault();
@ -201,12 +156,11 @@ module.exports = React.createClass({
},
onDismissed: function(index) {
var self = this;
return () => {
var inviteList = self.state.inviteList.slice();
inviteList.splice(index, 1);
self.setState({
inviteList: inviteList,
const userList = this.state.userList.slice();
userList.splice(index, 1);
this.setState({
userList: userList,
queryList: [],
query: "",
});
@ -215,17 +169,16 @@ module.exports = React.createClass({
},
onClick: function(index) {
var self = this;
return function() {
self.onSelected(index);
return () => {
this.onSelected(index);
};
},
onSelected: function(index) {
var inviteList = this.state.inviteList.slice();
inviteList.push(this.state.queryList[index]);
const userList = this.state.userList.slice();
userList.push(this.state.queryList[index]);
this.setState({
inviteList: inviteList,
userList: userList,
queryList: [],
query: "",
});
@ -297,7 +250,7 @@ module.exports = React.createClass({
return;
}
// Return objects, structure of which is defined
// by InviteAddressType
// by UserAddressType
queryList.push({
addressType: 'mx',
address: user.user_id,
@ -311,7 +264,7 @@ module.exports = React.createClass({
// This is important, otherwise there's no way to invite
// a perfectly valid address if there are close matches.
const addrType = getAddressType(query);
if (addrType !== null) {
if (this.props.validAddressTypes.includes(addrType)) {
queryList.unshift({
addressType: addrType,
address: query,
@ -330,132 +283,6 @@ module.exports = React.createClass({
});
},
_getDirectMessageRooms: function(addr) {
const dmRoomMap = new DMRoomMap(MatrixClientPeg.get());
const dmRooms = dmRoomMap.getDMRoomsForUserId(addr);
const rooms = [];
dmRooms.forEach(dmRoom => {
let room = MatrixClientPeg.get().getRoom(dmRoom);
if (room) {
const me = room.getMember(MatrixClientPeg.get().credentials.userId);
if (me.membership == 'join') {
rooms.push(room);
}
}
});
return rooms;
},
_startChat: function(addrs) {
if (MatrixClientPeg.get().isGuest()) {
dis.dispatch({action: 'view_set_mxid'});
return;
}
const addrTexts = addrs.map((addr) => {
return addr.address;
});
if (this.props.roomId) {
// Invite new user to a room
var self = this;
inviteMultipleToRoom(this.props.roomId, addrTexts)
.then(function(addrs) {
var room = MatrixClientPeg.get().getRoom(self.props.roomId);
return self._showAnyInviteErrors(addrs, room);
})
.catch(function(err) {
console.error(err.stack);
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, {
title: _t("Failed to invite"),
description: ((err && err.message) ? err.message : _t("Operation failed")),
});
return null;
})
.done();
} else if (this._isDmChat(addrTexts)) {
// Start the DM chat
createRoom({dmUserId: addrTexts[0]})
.catch(function(err) {
console.error(err.stack);
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, {
title: _t("Failed to invite user"),
description: ((err && err.message) ? err.message : _t("Operation failed")),
});
return null;
})
.done();
} else {
// Start multi user chat
var self = this;
var room;
createRoom().then(function(roomId) {
room = MatrixClientPeg.get().getRoom(roomId);
return inviteMultipleToRoom(roomId, addrTexts);
})
.then(function(addrs) {
return self._showAnyInviteErrors(addrs, room);
})
.catch(function(err) {
console.error(err.stack);
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, {
title: _t("Failed to invite"),
description: ((err && err.message) ? err.message : _t("Operation failed")),
});
return null;
})
.done();
}
// Close - this will happen before the above, as that is async
this.props.onFinished(true, addrTexts);
},
_isOnInviteList: function(uid) {
for (let i = 0; i < this.state.inviteList.length; i++) {
if (
this.state.inviteList[i].addressType == 'mx' &&
this.state.inviteList[i].address.toLowerCase() === uid
) {
return true;
}
}
return false;
},
_isDmChat: function(addrTexts) {
if (addrTexts.length === 1 &&
getAddressType(addrTexts[0]) === "mx" &&
!this.props.roomId
) {
return true;
} else {
return false;
}
},
_showAnyInviteErrors: function(addrs, room) {
// Show user any errors
var errorList = [];
for (var addr in addrs) {
if (addrs.hasOwnProperty(addr) && addrs[addr] === "error") {
errorList.push(addr);
}
}
if (errorList.length > 0) {
var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, {
title: _t("Failed to invite the following users to the %(roomName)s room:", {roomName: room.name}),
description: errorList.join(", "),
});
}
return addrs;
},
_addInputToList: function() {
const addressText = this.refs.textinput.value.trim();
const addrType = getAddressType(addressText);
@ -476,15 +303,15 @@ module.exports = React.createClass({
}
}
const inviteList = this.state.inviteList.slice();
inviteList.push(addrObj);
const userList = this.state.userList.slice();
userList.push(addrObj);
this.setState({
inviteList: inviteList,
userList: userList,
queryList: [],
query: "",
});
if (this._cancelThreepidLookup) this._cancelThreepidLookup();
return inviteList;
return userList;
},
_lookupThreepid: function(medium, address) {
@ -495,7 +322,7 @@ module.exports = React.createClass({
// not like they leak.
this._cancelThreepidLookup = function() {
cancelled = true;
}
};
// wait a bit to let the user finish typing
return Promise.delay(500).then(() => {
@ -511,7 +338,7 @@ module.exports = React.createClass({
if (cancelled) return null;
this.setState({
queryList: [{
// an InviteAddressType
// a UserAddressType
addressType: medium,
address: address,
displayName: res.displayname,
@ -527,20 +354,20 @@ module.exports = React.createClass({
const AddressSelector = sdk.getComponent("elements.AddressSelector");
this.scrollElement = null;
var query = [];
const query = [];
// create the invite list
if (this.state.inviteList.length > 0) {
var AddressTile = sdk.getComponent("elements.AddressTile");
for (let i = 0; i < this.state.inviteList.length; i++) {
if (this.state.userList.length > 0) {
const AddressTile = sdk.getComponent("elements.AddressTile");
for (let i = 0; i < this.state.userList.length; i++) {
query.push(
<AddressTile key={i} address={this.state.inviteList[i]} canDismiss={true} onDismissed={ this.onDismissed(i) } />
<AddressTile key={i} address={this.state.userList[i]} canDismiss={true} onDismissed={ this.onDismissed(i) } />,
);
}
}
// Add the query at the end
query.push(
<textarea key={this.state.inviteList.length}
<textarea key={this.state.userList.length}
rows="1"
id="textinput"
ref="textinput"
@ -555,7 +382,9 @@ module.exports = React.createClass({
let error;
let addressSelector;
if (this.state.error) {
error = <div className="mx_ChatInviteDialog_error">{_t("You have entered an invalid contact. Try using their Matrix ID or email address.")}</div>;
error = <div className="mx_ChatInviteDialog_error">
{_t("You have entered an invalid contact. Try using their Matrix ID or email address.")}
</div>;
} else if (this.state.searchError) {
error = <div className="mx_ChatInviteDialog_error">{this.state.searchError}</div>;
} else if (
@ -598,5 +427,5 @@ module.exports = React.createClass({
</div>
</div>
);
}
},
});

View file

@ -20,7 +20,7 @@ limitations under the License.
import React from 'react';
import sdk from '../../../index';
import classNames from 'classnames';
import { InviteAddressType } from './AddressTile';
import { UserAddressType } from '../../../UserAddress';
export default React.createClass({
displayName: 'AddressSelector',
@ -29,7 +29,7 @@ export default React.createClass({
onSelected: React.PropTypes.func.isRequired,
// List of the addresses to display
addressList: React.PropTypes.arrayOf(InviteAddressType).isRequired,
addressList: React.PropTypes.arrayOf(UserAddressType).isRequired,
truncateAt: React.PropTypes.number.isRequired,
selected: React.PropTypes.number,

View file

@ -1,5 +1,6 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -14,38 +15,19 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
'use strict';
import React from 'react';
import classNames from 'classnames';
import sdk from "../../../index";
import MatrixClientPeg from "../../../MatrixClientPeg";
import { _t } from '../../../languageHandler';
// React PropType definition for an object describing
// an address that can be invited to a room (which
// could be a third party identifier or a matrix ID)
// along with some additional information about the
// address / target.
export const InviteAddressType = React.PropTypes.shape({
addressType: React.PropTypes.oneOf([
'mx', 'email'
]).isRequired,
address: React.PropTypes.string.isRequired,
displayName: React.PropTypes.string,
avatarMxc: React.PropTypes.string,
// true if the address is known to be a valid address (eg. is a real
// user we've seen) or false otherwise (eg. is just an address the
// user has entered)
isKnown: React.PropTypes.bool,
});
import { UserAddressType } from '../../../UserAddress.js';
export default React.createClass({
displayName: 'AddressTile',
propTypes: {
address: InviteAddressType.isRequired,
address: UserAddressType.isRequired,
canDismiss: React.PropTypes.bool,
onDismissed: React.PropTypes.func,
justified: React.PropTypes.bool,

View file

@ -37,6 +37,7 @@ export default class AppPermission extends React.Component {
if(wurl && wurl.hostname && (
wurl.hostname === 'scalar.vector.im' ||
wurl.hostname === 'scalar-staging.riot.im' ||
wurl.hostname === 'scalar-develop.riot.im' ||
wurl.hostname === 'demo.riot.im' ||
wurl.hostname === 'localhost'
)) {

View file

@ -25,6 +25,7 @@ import Modal from '../../../Modal';
import { _t } from '../../../languageHandler';
import sdk from '../../../index';
import AppPermission from './AppPermission';
import AppWarning from './AppWarning';
import MessageSpinner from './MessageSpinner';
import WidgetUtils from '../../../WidgetUtils';
@ -70,6 +71,18 @@ export default React.createClass({
return scalarUrl && this.props.url.startsWith(scalarUrl);
},
isMixedContent: function() {
const parentContentProtocol = window.location.protocol;
const u = url.parse(this.props.url);
const childContentProtocol = u.protocol;
if (parentContentProtocol === 'https:' && childContentProtocol !== 'https:') {
console.warn("Refusing to load mixed-content app:",
parentContentProtocol, childContentProtocol, window.location, this.props.url);
return true;
}
return false;
},
componentWillMount: function() {
if (!this.isScalarUrl()) {
return;
@ -110,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");
},
@ -197,16 +210,26 @@ export default React.createClass({
</div>
);
} else if (this.state.hasPermissionToLoad == true) {
appTileBody = (
<div className="mx_AppTileBody">
<iframe
ref="appFrame"
src={safeWidgetUrl}
allowFullScreen="true"
sandbox={sandboxFlags}
></iframe>
</div>
);
if (this.isMixedContent()) {
appTileBody = (
<div className="mx_AppTileBody">
<AppWarning
errorMsg="Error - Mixed content"
/>
</div>
);
} else {
appTileBody = (
<div className="mx_AppTileBody">
<iframe
ref="appFrame"
src={safeWidgetUrl}
allowFullScreen="true"
sandbox={sandboxFlags}
></iframe>
</div>
);
}
} else {
appTileBody = (
<div className="mx_AppTileBody">

View file

@ -0,0 +1,25 @@
import React from 'react'; // eslint-disable-line no-unused-vars
import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler';
const AppWarning = (props) => {
return (
<div className='mx_AppPermissionWarning'>
<div className='mx_AppPermissionWarningImage'>
<img src='img/warning.svg' alt={_t('Warning!')}/>
</div>
<div className='mx_AppPermissionWarningText'>
<span className='mx_AppPermissionWarningTextLabel'>{props.errorMsg}</span>
</div>
</div>
);
};
AppWarning.propTypes = {
errorMsg: PropTypes.string,
};
AppWarning.defaultProps = {
errorMsg: 'Error',
};
export default AppWarning;

View file

@ -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,
});

View file

@ -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';
@ -47,6 +48,8 @@ const Pill = React.createClass({
inMessage: PropTypes.bool,
// The room in which this pill is being rendered
room: PropTypes.instanceOf(Room),
// Whether to include an avatar in the pill
shouldShowPillAvatar: PropTypes.bool,
},
getInitialState() {
@ -138,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');
@ -148,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
@ -155,8 +166,12 @@ const Pill = React.createClass({
if (member) {
userId = member.userId;
linkText = member.rawDisplayName.replace(' (IRC)', ''); // FIXME when groups are done
avatar = <MemberAvatar member={member} width={16} height={16}/>;
if (this.props.shouldShowPillAvatar) {
avatar = <MemberAvatar member={member} width={16} height={16}/>;
}
pillClass = 'mx_UserPill';
href = null;
onClick = this.onUserPillClicked.bind(this);
}
}
break;
@ -164,7 +179,9 @@ const Pill = React.createClass({
const room = this.state.room;
if (room) {
linkText = (room ? getDisplayAliasForRoom(room) : null) || resource;
avatar = <RoomAvatar room={room} width={16} height={16}/>;
if (this.props.shouldShowPillAvatar) {
avatar = <RoomAvatar room={room} width={16} height={16}/>;
}
pillClass = 'mx_RoomPill';
}
}
@ -177,7 +194,7 @@ const Pill = React.createClass({
if (this.state.pillType) {
return this.props.inMessage ?
<a className={classes} href={this.props.url} title={resource} data-offset-key={this.props.offsetKey}>
<a className={classes} href={href} onClick={onClick} title={resource} data-offset-key={this.props.offsetKey}>
{avatar}
{linkText}
</a> :

View file

@ -17,7 +17,7 @@ limitations under the License.
'use strict';
import React from 'react';
import { _t } from '../../../languageHandler';
import { _t, _tJsx } from '../../../languageHandler';
var DIV_ID = 'mx_recaptcha';
@ -66,7 +66,11 @@ module.exports = React.createClass({
// * jumping straight to a hosted captcha page (but we don't support that yet)
// * embedding the captcha in an iframe (if that works)
// * using a better captcha lib
warning.innerHTML = "Robot check is currently unavailable on desktop - please use a <a href='https://riot.im/app'>web browser</a>.";
warning.innerHTML = _tJsx(
"Robot check is currently unavailable on desktop - please use a <a>web browser</a>",
/<a>(.*?)<\/a>/,
(sub) => { return "<a href='https://riot.im/app'>{ sub }</a>"; }
);
this.refs.recaptchaContainer.appendChild(warning);
}
else {

View file

@ -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:
<div>

View file

@ -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() {

View file

@ -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(() => {

View file

@ -170,6 +170,7 @@ module.exports = React.createClass({
},
pillifyLinks: function(nodes) {
const shouldShowPillAvatar = !UserSettingsStore.getSyncedSetting("Pill.shouldHidePillAvatar", false);
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i];
if (node.tagName === "A" && node.getAttribute("href")) {
@ -181,7 +182,12 @@ module.exports = React.createClass({
const pillContainer = document.createElement('span');
const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId());
const pill = <Pill url={href} inMessage={true} room={room}/>;
const pill = <Pill
url={href}
inMessage={true}
room={room}
shouldShowPillAvatar={shouldShowPillAvatar}
/>;
ReactDOM.render(pill, pillContainer);
node.parentNode.replaceChild(pillContainer, node);
@ -269,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;
}
};
},
@ -299,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:
<div>

View file

@ -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 }),
});

View file

@ -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");
},

View file

@ -172,7 +172,7 @@ export default class Autocomplete extends React.Component {
}
hide() {
this.setState({hide: true, selectionOffset: 0});
this.setState({hide: true, selectionOffset: 0, completions: [], completionList: []});
}
forceComplete() {

View file

@ -155,7 +155,9 @@ module.exports = withMatrixClient(React.createClass({
},
componentWillReceiveProps: function(nextProps) {
if (nextProps.mxEvent !== this.props.mxEvent) {
// re-check the sender verification as outgoing events progress through
// the send process.
if (nextProps.eventSendStatus !== this.props.eventSendStatus) {
this._verifyEvent(nextProps.mxEvent);
}
},
@ -367,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,
@ -386,6 +388,36 @@ module.exports = withMatrixClient(React.createClass({
});
},
_renderE2EPadlock: function() {
const ev = this.props.mxEvent;
const props = {onClick: this.onCryptoClicked};
if (ev.getContent().msgtype === 'm.bad.encrypted') {
return <E2ePadlockUndecryptable {...props}/>;
} else if (ev.isEncrypted()) {
if (this.state.verified) {
return <E2ePadlockVerified {...props}/>;
} else {
return <E2ePadlockUnverified {...props}/>;
}
} else {
// XXX: if the event is being encrypted (ie eventSendStatus ===
// encrypting), it might be nice to show something other than the
// open padlock?
// if the event is not encrypted, but it's an e2e room, show the
// open padlock
const e2eEnabled = this.props.matrixClient.isRoomEncrypted(ev.getRoomId());
if (e2eEnabled) {
return <E2ePadlockUnencrypted {...props}/>;
}
}
// no padlock needed
return null;
},
render: function() {
var MessageTimestamp = sdk.getComponent('messages.MessageTimestamp');
var SenderProfile = sdk.getComponent('messages.SenderProfile');
@ -407,7 +439,6 @@ module.exports = withMatrixClient(React.createClass({
throw new Error("Event type not supported");
}
var e2eEnabled = this.props.matrixClient.isRoomEncrypted(this.props.mxEvent.getRoomId());
var isSending = (['sending', 'queued', 'encrypting'].indexOf(this.props.eventSendStatus) !== -1);
const isRedacted = (eventType === 'm.room.message') && this.props.isRedacted;
@ -485,26 +516,7 @@ module.exports = withMatrixClient(React.createClass({
const editButton = (
<span className="mx_EventTile_editButton" title={ _t("Options") } onClick={this.onEditClicked} />
);
let e2e;
// cosmetic padlocks:
if ((e2eEnabled && this.props.eventSendStatus) || this.props.mxEvent.getType() === 'm.room.encryption') {
e2e = <img style={{ cursor: 'initial', marginLeft: '-1px' }} className="mx_EventTile_e2eIcon" alt={_t("Encrypted by a verified device")} src="img/e2e-verified.svg" width="10" height="12" />;
}
// real padlocks
else if (this.props.mxEvent.isEncrypted() || (e2eEnabled && this.props.eventSendStatus)) {
if (this.props.mxEvent.getContent().msgtype === 'm.bad.encrypted') {
e2e = <img onClick={ this.onCryptoClicked } className="mx_EventTile_e2eIcon" alt={_t("Undecryptable")} src="img/e2e-blocked.svg" width="12" height="12" style={{ marginLeft: "-1px" }} />;
}
else if (this.state.verified == true || (e2eEnabled && this.props.eventSendStatus)) {
e2e = <img onClick={ this.onCryptoClicked } className="mx_EventTile_e2eIcon" alt={_t("Encrypted by a verified device")} src="img/e2e-verified.svg" width="10" height="12"/>;
}
else {
e2e = <img onClick={ this.onCryptoClicked } className="mx_EventTile_e2eIcon" alt={_t("Encrypted by an unverified device")} src="img/e2e-warning.svg" width="15" height="12" style={{ marginLeft: "-2px" }}/>;
}
}
else if (e2eEnabled) {
e2e = <img onClick={ this.onCryptoClicked } className="mx_EventTile_e2eIcon" alt={_t("Unencrypted message")} src="img/e2e-unencrypted.svg" width="12" height="12"/>;
}
const timestamp = this.props.mxEvent.getTs() ?
<MessageTimestamp showTwelveHour={this.props.isTwelveHour} ts={this.props.mxEvent.getTs()} /> : null;
@ -572,7 +584,7 @@ module.exports = withMatrixClient(React.createClass({
<a href={ permalink } onClick={this.onPermalinkClicked}>
{ timestamp }
</a>
{ e2e }
{ this._renderE2EPadlock() }
<EventTileType ref="tile"
mxEvent={this.props.mxEvent}
highlights={this.props.highlights}
@ -597,3 +609,39 @@ module.exports.haveTileForEvent = function(e) {
return true;
}
};
function E2ePadlockUndecryptable(props) {
return (
<E2ePadlock alt={_t("Undecryptable")}
src="img/e2e-blocked.svg" width="12" height="12"
style={{ marginLeft: "-1px" }} {...props} />
);
}
function E2ePadlockVerified(props) {
return (
<E2ePadlock alt={_t("Encrypted by a verified device")}
src="img/e2e-verified.svg" width="10" height="12"
{...props} />
);
}
function E2ePadlockUnverified(props) {
return (
<E2ePadlock alt={_t("Encrypted by an unverified device")}
src="img/e2e-warning.svg" width="15" height="12"
style={{ marginLeft: "-2px" }} {...props} />
);
}
function E2ePadlockUnencrypted(props) {
return (
<E2ePadlock alt={_t("Unencrypted message")}
src="img/e2e-unencrypted.svg" width="12" height="12"
{...props} />
);
}
function E2ePadlock(props) {
return <img className="mx_EventTile_e2eIcon" {...props} />;
}

View file

@ -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:
<div>

View file

@ -99,7 +99,7 @@ export default class MessageComposer extends React.Component {
</li>);
}
Modal.createDialog(QuestionDialog, {
Modal.createTrackedDialog('Upload Files confirmation', '', QuestionDialog, {
title: _t('Upload Files'),
description: (
<div>

View file

@ -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';
@ -97,20 +98,39 @@ export default class MessageComposerInput extends React.Component {
onInputStateChanged: React.PropTypes.func,
};
static getKeyBinding(e: SyntheticKeyboardEvent): string {
// C-m => Toggles between rich text and markdown modes
if (e.keyCode === KeyCode.KEY_M && KeyBindingUtil.isCtrlKeyCommand(e)) {
return 'toggle-mode';
static getKeyBinding(ev: SyntheticKeyboardEvent): string {
const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0;
let ctrlCmdOnly;
if (isMac) {
ctrlCmdOnly = ev.metaKey && !ev.altKey && !ev.ctrlKey && !ev.shiftKey;
} else {
ctrlCmdOnly = ev.ctrlKey && !ev.altKey && !ev.metaKey && !ev.shiftKey;
}
// Allow opening of dev tools. getDefaultKeyBinding would be 'italic' for KEY_I
if (e.keyCode === KeyCode.KEY_I && e.shiftKey && e.ctrlKey) {
// When null is returned, draft-js will NOT preventDefault, allowing dev tools
// to be toggled when the editor is focussed
return null;
// Restrict a subset of key bindings to ONLY having ctrl/meta* pressed and
// importantly NOT having alt, shift, meta/ctrl* pressed. draft-js does not
// handle this in `getDefaultKeyBinding` so we do it ourselves here.
//
// * if macOS, read second option
const ctrlCmdCommand = {
// C-m => Toggles between rich text and markdown modes
[KeyCode.KEY_M]: 'toggle-mode',
[KeyCode.KEY_B]: 'bold',
[KeyCode.KEY_I]: 'italic',
[KeyCode.KEY_U]: 'underline',
[KeyCode.KEY_J]: 'code',
[KeyCode.KEY_O]: 'split-block',
}[ev.keyCode];
if (ctrlCmdCommand) {
if (!ctrlCmdOnly) {
return null;
}
return ctrlCmdCommand;
}
return getDefaultKeyBinding(e);
// Handle keys such as return, left and right arrows etc.
return getDefaultKeyBinding(ev);
}
static getBlockStyle(block: ContentBlock): ?string {
@ -141,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,
@ -165,17 +187,18 @@ export default class MessageComposerInput extends React.Component {
this.client = MatrixClientPeg.get();
}
findLinkEntities(contentBlock, callback) {
findLinkEntities(contentState: ContentState, contentBlock: ContentBlock, callback) {
contentBlock.findEntityRanges(
(character) => {
const entityKey = character.getEntity();
return (
entityKey !== null &&
Entity.get(entityKey).getType() === 'LINK'
contentState.getEntity(entityKey).getType() === 'LINK'
);
}, callback,
);
}
/*
* "Does the right thing" to create an EditorState, based on:
* - whether we've got rich text mode enabled
@ -184,13 +207,19 @@ export default class MessageComposerInput extends React.Component {
createEditorState(richText: boolean, contentState: ?ContentState): EditorState {
const decorators = richText ? RichText.getScopedRTDecorators(this.props) :
RichText.getScopedMDDecorators(this.props);
const shouldShowPillAvatar = !UserSettingsStore.getSyncedSetting("Pill.shouldHidePillAvatar", false);
decorators.push({
strategy: this.findLinkEntities.bind(this),
component: (entityProps) => {
const Pill = sdk.getComponent('elements.Pill');
const {url} = Entity.get(entityProps.entityKey).getData();
const {url} = entityProps.contentState.getEntity(entityProps.entityKey).getData();
if (Pill.isPillUrl(url)) {
return <Pill url={url} room={this.props.room} offsetKey={entityProps.offsetKey}/>;
return <Pill
url={url}
room={this.props.room}
offsetKey={entityProps.offsetKey}
shouldShowPillAvatar={shouldShowPillAvatar}
/>;
}
return (
@ -243,7 +272,8 @@ export default class MessageComposerInput extends React.Component {
// paths for inserting a user pill is not fun
const selection = this.state.editorState.getSelection();
const member = this.props.room.getMember(payload.user_id);
const completion = member ? member.name.replace(' (IRC)', '') : payload.user_id;
const completion = member ?
member.rawDisplayName.replace(' (IRC)', '') : payload.user_id;
this.setDisplayedCompletion({
completion,
selection,
@ -253,10 +283,12 @@ export default class MessageComposerInput extends React.Component {
}
break;
case 'quote': {
let {body, formatted_body} = payload.event.getContent();
formatted_body = formatted_body || escape(body);
if (formatted_body) {
let content = RichText.htmlToContentState(`<blockquote>${formatted_body}</blockquote>`);
/// 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.
let body = escape(payload.text);
if (body) {
let content = RichText.htmlToContentState(`<blockquote>${body}</blockquote>`);
if (!this.state.isRichtextEnabled) {
content = ContentState.createFromText(RichText.stateToMarkdown(content));
}
@ -427,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 {
@ -483,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,
@ -515,7 +562,8 @@ export default class MessageComposerInput extends React.Component {
newState = RichUtils.toggleInlineStyle(this.state.editorState, 'STRIKETHROUGH');
} else if (shouldToggleBlockFormat) {
const currentStartOffset = this.state.editorState.getSelection().getStartOffset();
if (currentStartOffset === 0) {
const currentEndOffset = this.state.editorState.getSelection().getEndOffset();
if (currentStartOffset === 0 && currentEndOffset === 0) {
// Toggle current block type (setting it to 'unstyled')
newState = RichUtils.toggleBlockType(this.state.editorState, currentBlockType);
}
@ -673,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.")),
});
@ -681,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,
});
@ -713,7 +762,7 @@ export default class MessageComposerInput extends React.Component {
const hasLink = blocks.some((block) => {
return block.getCharacterList().filter((c) => {
const entityKey = c.getEntity();
return entityKey && Entity.get(entityKey).getType() === 'LINK';
return entityKey && contentState.getEntity(entityKey).getType() === 'LINK';
}).size > 0;
});
shouldSendHTML = hasLink;
@ -734,8 +783,8 @@ export default class MessageComposerInput extends React.Component {
const pt = contentState.getBlocksAsArray().map((block) => {
let blockText = block.getText();
let offset = 0;
this.findLinkEntities(block, (start, end) => {
const entity = Entity.get(block.getEntityAt(start));
this.findLinkEntities(contentState, block, (start, end) => {
const entity = contentState.getEntity(block.getEntityAt(start));
if (entity.getType() !== 'LINK') {
return;
}
@ -759,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);
@ -841,6 +885,7 @@ export default class MessageComposerInput extends React.Component {
}
} else {
this.moveAutocompleteSelection(up);
e.preventDefault();
}
};
@ -936,32 +981,27 @@ export default class MessageComposerInput extends React.Component {
}
const {range = null, completion = '', href = null, suffix = ''} = displayedCompletion;
let contentState = activeEditorState.getCurrentContent();
let entityKey;
let mdCompletion;
if (href) {
entityKey = Entity.create('LINK', 'IMMUTABLE', {
contentState = contentState.createEntity('LINK', 'IMMUTABLE', {
url: href,
isCompletion: true,
});
entityKey = contentState.getLastCreatedEntityKey();
}
let selection;
if (range) {
selection = RichText.textOffsetsToSelectionState(
range, activeEditorState.getCurrentContent().getBlocksAsArray(),
range, contentState.getBlocksAsArray(),
);
} else {
selection = activeEditorState.getSelection();
}
let contentState = Modifier.replaceText(
activeEditorState.getCurrentContent(),
selection,
mdCompletion || completion,
null,
entityKey,
);
contentState = Modifier.replaceText(contentState, selection, completion, null, entityKey);
// Move the selection to the end of the block
const afterSelection = contentState.getSelectionAfter();
@ -1047,7 +1087,7 @@ export default class MessageComposerInput extends React.Component {
offset -= sum;
const entityKey = block.getEntityAt(offset);
const entity = entityKey ? Entity.get(entityKey) : null;
const entity = entityKey ? contentState.getEntity(entityKey) : null;
if (entity && entity.getData().isCompletion) {
// This is a completed mention, so do not insert MD link, just text
return text;

View file

@ -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."),
});

View file

@ -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'),
});
@ -424,7 +424,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:
<div>
@ -517,7 +517,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,
@ -550,7 +550,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 }),
});
@ -561,7 +561,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: (
<div>

View file

@ -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(<div key="_error" className="error">{msg}</div>);
}
Modal.createDialog(TextInputDialog, {
Modal.createTrackedDialog('Prompt for MSISDN Verification Code', '', TextInputDialog, {
title: _t("Enter Code"),
description: <div>{msgElements}</div>,
button: _t("Submit"),

View file

@ -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:
<div>
@ -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() {

View file

@ -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,