Merge branch 'develop' into hs/custom-notif-sounds
This commit is contained in:
commit
efc93abb50
48 changed files with 2034 additions and 1165 deletions
|
@ -50,7 +50,6 @@ import SettingsStore, {SettingLevel} from "../../settings/SettingsStore";
|
|||
import { startAnyRegistrationFlow } from "../../Registration.js";
|
||||
import { messageForSyncError } from '../../utils/ErrorUtils';
|
||||
import ResizeNotifier from "../../utils/ResizeNotifier";
|
||||
import TimelineExplosionDialog from "../views/dialogs/TimelineExplosionDialog";
|
||||
|
||||
const AutoDiscovery = Matrix.AutoDiscovery;
|
||||
|
||||
|
@ -250,17 +249,6 @@ export default React.createClass({
|
|||
return this.state.defaultIsUrl || "https://vector.im";
|
||||
},
|
||||
|
||||
/**
|
||||
* Whether to skip the server details phase of registration and start at the
|
||||
* actual form.
|
||||
* @return {boolean}
|
||||
* If there was a configured default HS or default server name, skip the
|
||||
* the server details.
|
||||
*/
|
||||
skipServerDetailsForRegistration() {
|
||||
return !!this.state.defaultHsUrl;
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
SdkConfig.put(this.props.config);
|
||||
|
||||
|
@ -1307,17 +1295,6 @@ export default React.createClass({
|
|||
return self._loggedInView.child.canResetTimelineInRoom(roomId);
|
||||
});
|
||||
|
||||
cli.on('sync.unexpectedError', function(err) {
|
||||
if (err.message && err.message.includes("live timeline ") && err.message.includes(" is no longer live ")) {
|
||||
console.error("Caught timeline explosion - trying to ask user for more information");
|
||||
if (Modal.hasDialogs()) {
|
||||
console.warn("User has another dialog open - skipping prompt");
|
||||
return;
|
||||
}
|
||||
Modal.createTrackedDialog('Timeline exploded', '', TimelineExplosionDialog, {});
|
||||
}
|
||||
});
|
||||
|
||||
cli.on('sync', function(state, prevState, data) {
|
||||
// LifecycleStore and others cannot directly subscribe to matrix client for
|
||||
// events because flux only allows store state changes during flux dispatches.
|
||||
|
@ -1985,7 +1962,6 @@ export default React.createClass({
|
|||
defaultServerDiscoveryError={this.state.defaultServerDiscoveryError}
|
||||
defaultHsUrl={this.getDefaultHsUrl()}
|
||||
defaultIsUrl={this.getDefaultIsUrl()}
|
||||
skipServerDetails={this.skipServerDetailsForRegistration()}
|
||||
brand={this.props.config.brand}
|
||||
customHsUrl={this.getCurrentHsUrl()}
|
||||
customIsUrl={this.getCurrentIsUrl()}
|
||||
|
|
|
@ -44,11 +44,10 @@ const READ_RECEIPT_INTERVAL_MS = 500;
|
|||
|
||||
const DEBUG = false;
|
||||
|
||||
let debuglog = function() {};
|
||||
if (DEBUG) {
|
||||
// using bind means that we get to keep useful line numbers in the console
|
||||
var debuglog = console.log.bind(console);
|
||||
} else {
|
||||
var debuglog = function() {};
|
||||
debuglog = console.log.bind(console);
|
||||
}
|
||||
|
||||
/*
|
||||
|
@ -56,7 +55,7 @@ if (DEBUG) {
|
|||
*
|
||||
* Also responsible for handling and sending read receipts.
|
||||
*/
|
||||
var TimelinePanel = React.createClass({
|
||||
const TimelinePanel = React.createClass({
|
||||
displayName: 'TimelinePanel',
|
||||
|
||||
propTypes: {
|
||||
|
@ -445,6 +444,7 @@ var TimelinePanel = React.createClass({
|
|||
|
||||
const updatedState = {events: events};
|
||||
|
||||
let callRMUpdated;
|
||||
if (this.props.manageReadMarkers) {
|
||||
// when a new event arrives when the user is not watching the
|
||||
// window, but the window is in its auto-scroll mode, make sure the
|
||||
|
@ -456,7 +456,7 @@ var TimelinePanel = React.createClass({
|
|||
//
|
||||
const myUserId = MatrixClientPeg.get().credentials.userId;
|
||||
const sender = ev.sender ? ev.sender.userId : null;
|
||||
var callRMUpdated = false;
|
||||
callRMUpdated = false;
|
||||
if (sender != myUserId && !UserActivity.sharedInstance().userActiveRecently()) {
|
||||
updatedState.readMarkerVisible = true;
|
||||
} else if (lastEv && this.getReadMarkerPosition() === 0) {
|
||||
|
@ -566,7 +566,7 @@ var TimelinePanel = React.createClass({
|
|||
UserActivity.sharedInstance().timeWhileActiveRecently(this._readMarkerActivityTimer);
|
||||
try {
|
||||
await this._readMarkerActivityTimer.finished();
|
||||
} catch(e) { continue; /* aborted */ }
|
||||
} catch (e) { continue; /* aborted */ }
|
||||
// outside of try/catch to not swallow errors
|
||||
this.updateReadMarker();
|
||||
}
|
||||
|
@ -578,7 +578,7 @@ var TimelinePanel = React.createClass({
|
|||
UserActivity.sharedInstance().timeWhileActiveNow(this._readReceiptActivityTimer);
|
||||
try {
|
||||
await this._readReceiptActivityTimer.finished();
|
||||
} catch(e) { continue; /* aborted */ }
|
||||
} catch (e) { continue; /* aborted */ }
|
||||
// outside of try/catch to not swallow errors
|
||||
this.sendReadReceipt();
|
||||
}
|
||||
|
@ -732,7 +732,8 @@ var TimelinePanel = React.createClass({
|
|||
const events = this._timelineWindow.getEvents();
|
||||
|
||||
// first find where the current RM is
|
||||
for (var i = 0; i < events.length; i++) {
|
||||
let i;
|
||||
for (i = 0; i < events.length; i++) {
|
||||
if (events[i].getId() == this.state.readMarkerEventId) {
|
||||
break;
|
||||
}
|
||||
|
@ -744,7 +745,7 @@ var TimelinePanel = React.createClass({
|
|||
// now think about advancing it
|
||||
const myUserId = MatrixClientPeg.get().credentials.userId;
|
||||
for (i++; i < events.length; i++) {
|
||||
var ev = events[i];
|
||||
const ev = events[i];
|
||||
if (!ev.sender || ev.sender.userId != myUserId) {
|
||||
break;
|
||||
}
|
||||
|
@ -752,7 +753,7 @@ var TimelinePanel = React.createClass({
|
|||
// i is now the first unread message which we didn't send ourselves.
|
||||
i--;
|
||||
|
||||
var ev = events[i];
|
||||
const ev = events[i];
|
||||
this._setReadMarker(ev.getId(), ev.getTs());
|
||||
},
|
||||
|
||||
|
@ -882,7 +883,7 @@ var TimelinePanel = React.createClass({
|
|||
return ret;
|
||||
},
|
||||
|
||||
/**
|
||||
/*
|
||||
* called by the parent component when PageUp/Down/etc is pressed.
|
||||
*
|
||||
* We pass it down to the scroll panel.
|
||||
|
@ -975,11 +976,10 @@ var TimelinePanel = React.createClass({
|
|||
};
|
||||
|
||||
const onError = (error) => {
|
||||
this.setState({timelineLoading: false});
|
||||
this.setState({ timelineLoading: false });
|
||||
console.error(
|
||||
`Error loading timeline panel at ${eventId}: ${error}`,
|
||||
);
|
||||
const msg = error.message ? error.message : JSON.stringify(error);
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
|
||||
let onFinished;
|
||||
|
@ -997,9 +997,18 @@ var TimelinePanel = React.createClass({
|
|||
});
|
||||
};
|
||||
}
|
||||
const 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.");
|
||||
let message;
|
||||
if (error.errcode == 'M_FORBIDDEN') {
|
||||
message = _t(
|
||||
"Tried to load a specific point in this room's timeline, but you " +
|
||||
"do not have permission to view the message in question.",
|
||||
);
|
||||
} else {
|
||||
message = _t(
|
||||
"Tried to load a specific point in this room's timeline, but was " +
|
||||
"unable to find it.",
|
||||
);
|
||||
}
|
||||
Modal.createTrackedDialog('Failed to load timeline position', '', ErrorDialog, {
|
||||
title: _t("Failed to load timeline position"),
|
||||
description: message,
|
||||
|
@ -1104,12 +1113,13 @@ var TimelinePanel = React.createClass({
|
|||
},
|
||||
|
||||
/**
|
||||
* get the id of the event corresponding to our user's latest read-receipt.
|
||||
* Get the id of the event corresponding to our user's latest read-receipt.
|
||||
*
|
||||
* @param {Boolean} ignoreSynthesized If true, return only receipts that
|
||||
* have been sent by the server, not
|
||||
* implicit ones generated by the JS
|
||||
* SDK.
|
||||
* @return {String} the event ID
|
||||
*/
|
||||
_getCurrentReadReceipt: function(ignoreSynthesized) {
|
||||
const client = MatrixClientPeg.get();
|
||||
|
|
|
@ -28,8 +28,6 @@ import SdkConfig from '../../../SdkConfig';
|
|||
import { messageForResourceLimitError } from '../../../utils/ErrorUtils';
|
||||
import * as ServerType from '../../views/auth/ServerTypeSelector';
|
||||
|
||||
const MIN_PASSWORD_LENGTH = 6;
|
||||
|
||||
// Phases
|
||||
// Show controls to configure server details
|
||||
const PHASE_SERVER_DETAILS = 0;
|
||||
|
@ -60,7 +58,6 @@ module.exports = React.createClass({
|
|||
customIsUrl: PropTypes.string,
|
||||
defaultHsUrl: PropTypes.string,
|
||||
defaultIsUrl: PropTypes.string,
|
||||
skipServerDetails: PropTypes.bool,
|
||||
brand: PropTypes.string,
|
||||
email: PropTypes.string,
|
||||
// registration shouldn't know or care how login is done.
|
||||
|
@ -71,26 +68,6 @@ module.exports = React.createClass({
|
|||
getInitialState: function() {
|
||||
const serverType = ServerType.getTypeFromHsUrl(this.props.customHsUrl);
|
||||
|
||||
const customURLsAllowed = !SdkConfig.get()['disable_custom_urls'];
|
||||
let initialPhase = this.getDefaultPhaseForServerType(serverType);
|
||||
if (
|
||||
// if we have these two, skip to the good bit
|
||||
// (they could come in from the URL params in a
|
||||
// registration email link)
|
||||
(this.props.clientSecret && this.props.sessionId) ||
|
||||
// if custom URLs aren't allowed, skip to form
|
||||
!customURLsAllowed ||
|
||||
// if other logic says to, skip to form
|
||||
this.props.skipServerDetails
|
||||
) {
|
||||
// TODO: It would seem we've now added enough conditions here that the initial
|
||||
// phase will _always_ be the form. It's tempting to remove the complexity and
|
||||
// just do that, but we keep tweaking and changing auth, so let's wait until
|
||||
// things settle a bit.
|
||||
// Filed https://github.com/vector-im/riot-web/issues/8886 to track this.
|
||||
initialPhase = PHASE_REGISTRATION;
|
||||
}
|
||||
|
||||
return {
|
||||
busy: false,
|
||||
errorText: null,
|
||||
|
@ -113,7 +90,7 @@ module.exports = React.createClass({
|
|||
hsUrl: this.props.customHsUrl,
|
||||
isUrl: this.props.customIsUrl,
|
||||
// Phase of the overall registration dialog.
|
||||
phase: initialPhase,
|
||||
phase: PHASE_REGISTRATION,
|
||||
flows: null,
|
||||
};
|
||||
},
|
||||
|
@ -308,58 +285,6 @@ module.exports = React.createClass({
|
|||
});
|
||||
},
|
||||
|
||||
onFormValidationChange: function(fieldErrors) {
|
||||
// `fieldErrors` is an object mapping field IDs to error codes when there is an
|
||||
// error or `null` for no error, so the values array will be something like:
|
||||
// `[ null, "RegistrationForm.ERR_PASSWORD_MISSING", null]`
|
||||
// Find the first non-null error code and show that.
|
||||
const errCode = Object.values(fieldErrors).find(value => !!value);
|
||||
if (!errCode) {
|
||||
this.setState({
|
||||
errorText: null,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
let errMsg;
|
||||
switch (errCode) {
|
||||
case "RegistrationForm.ERR_PASSWORD_MISSING":
|
||||
errMsg = _t('Missing password.');
|
||||
break;
|
||||
case "RegistrationForm.ERR_PASSWORD_MISMATCH":
|
||||
errMsg = _t('Passwords don\'t match.');
|
||||
break;
|
||||
case "RegistrationForm.ERR_PASSWORD_LENGTH":
|
||||
errMsg = _t('Password too short (min %(MIN_PASSWORD_LENGTH)s).', {MIN_PASSWORD_LENGTH});
|
||||
break;
|
||||
case "RegistrationForm.ERR_EMAIL_INVALID":
|
||||
errMsg = _t('This doesn\'t look like a valid email address.');
|
||||
break;
|
||||
case "RegistrationForm.ERR_PHONE_NUMBER_INVALID":
|
||||
errMsg = _t('This doesn\'t look like a valid phone number.');
|
||||
break;
|
||||
case "RegistrationForm.ERR_MISSING_EMAIL":
|
||||
errMsg = _t('An email address is required to register on this homeserver.');
|
||||
break;
|
||||
case "RegistrationForm.ERR_MISSING_PHONE_NUMBER":
|
||||
errMsg = _t('A phone number is required to register on this homeserver.');
|
||||
break;
|
||||
case "RegistrationForm.ERR_USERNAME_INVALID":
|
||||
errMsg = _t("A username can only contain lower case letters, numbers and '=_-./'");
|
||||
break;
|
||||
case "RegistrationForm.ERR_USERNAME_BLANK":
|
||||
errMsg = _t('You need to enter a username.');
|
||||
break;
|
||||
default:
|
||||
console.error("Unknown error code: %s", errCode);
|
||||
errMsg = _t('An unknown error occurred.');
|
||||
break;
|
||||
}
|
||||
this.setState({
|
||||
errorText: errMsg,
|
||||
});
|
||||
},
|
||||
|
||||
onLoginClick: function(ev) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
@ -534,8 +459,6 @@ module.exports = React.createClass({
|
|||
defaultPhoneCountry={this.state.formVals.phoneCountry}
|
||||
defaultPhoneNumber={this.state.formVals.phoneNumber}
|
||||
defaultPassword={this.state.formVals.password}
|
||||
minPasswordLength={MIN_PASSWORD_LENGTH}
|
||||
onValidationChange={this.onFormValidationChange}
|
||||
onRegisterClick={this.onFormSubmit}
|
||||
onEditServerDetailsClick={onEditServerDetailsClick}
|
||||
flows={this.state.flows}
|
||||
|
|
|
@ -25,6 +25,7 @@ import Modal from '../../../Modal';
|
|||
import { _t } from '../../../languageHandler';
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
import { SAFE_LOCALPART_REGEX } from '../../../Registration';
|
||||
import withValidation from '../elements/Validation';
|
||||
|
||||
const FIELD_EMAIL = 'field_email';
|
||||
const FIELD_PHONE_NUMBER = 'field_phone_number';
|
||||
|
@ -32,6 +33,8 @@ const FIELD_USERNAME = 'field_username';
|
|||
const FIELD_PASSWORD = 'field_password';
|
||||
const FIELD_PASSWORD_CONFIRM = 'field_password_confirm';
|
||||
|
||||
const PASSWORD_MIN_SCORE = 3; // safely unguessable: moderate protection from offline slow-hash scenario.
|
||||
|
||||
/**
|
||||
* A pure UI component which displays a registration form.
|
||||
*/
|
||||
|
@ -45,8 +48,6 @@ module.exports = React.createClass({
|
|||
defaultPhoneNumber: PropTypes.string,
|
||||
defaultUsername: PropTypes.string,
|
||||
defaultPassword: PropTypes.string,
|
||||
minPasswordLength: PropTypes.number,
|
||||
onValidationChange: PropTypes.func,
|
||||
onRegisterClick: PropTypes.func.isRequired, // onRegisterClick(Object) => ?Promise
|
||||
onEditServerDetailsClick: PropTypes.func,
|
||||
flows: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
|
@ -59,7 +60,6 @@ module.exports = React.createClass({
|
|||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
minPasswordLength: 6,
|
||||
onValidationChange: console.error,
|
||||
};
|
||||
},
|
||||
|
@ -67,7 +67,7 @@ module.exports = React.createClass({
|
|||
getInitialState: function() {
|
||||
return {
|
||||
// Field error codes by field ID
|
||||
fieldErrors: {},
|
||||
fieldValid: {},
|
||||
// The ISO2 country code selected in the phone number entry
|
||||
phoneCountry: this.props.defaultPhoneCountry,
|
||||
username: "",
|
||||
|
@ -75,44 +75,37 @@ module.exports = React.createClass({
|
|||
phoneNumber: "",
|
||||
password: "",
|
||||
passwordConfirm: "",
|
||||
passwordComplexity: null,
|
||||
};
|
||||
},
|
||||
|
||||
onSubmit: function(ev) {
|
||||
onSubmit: async function(ev) {
|
||||
ev.preventDefault();
|
||||
|
||||
// validate everything, in reverse order so
|
||||
// the error that ends up being displayed
|
||||
// is the one from the first invalid field.
|
||||
// It's not super ideal that this just calls
|
||||
// onValidationChange once for each invalid field.
|
||||
this.validateField(FIELD_PHONE_NUMBER, ev.type);
|
||||
this.validateField(FIELD_EMAIL, ev.type);
|
||||
this.validateField(FIELD_PASSWORD_CONFIRM, ev.type);
|
||||
this.validateField(FIELD_PASSWORD, ev.type);
|
||||
this.validateField(FIELD_USERNAME, ev.type);
|
||||
const allFieldsValid = await this.verifyFieldsBeforeSubmit();
|
||||
if (!allFieldsValid) {
|
||||
return;
|
||||
}
|
||||
|
||||
const self = this;
|
||||
if (this.allFieldsValid()) {
|
||||
if (this.state.email == '') {
|
||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
Modal.createTrackedDialog('If you don\'t specify an email address...', '', QuestionDialog, {
|
||||
title: _t("Warning!"),
|
||||
description:
|
||||
<div>
|
||||
{ _t("If you don't specify an email address, you won't be able to reset your password. " +
|
||||
"Are you sure?") }
|
||||
</div>,
|
||||
button: _t("Continue"),
|
||||
onFinished: function(confirmed) {
|
||||
if (confirmed) {
|
||||
self._doSubmit(ev);
|
||||
}
|
||||
},
|
||||
});
|
||||
} else {
|
||||
self._doSubmit(ev);
|
||||
}
|
||||
if (this.state.email == '') {
|
||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
Modal.createTrackedDialog('If you don\'t specify an email address...', '', QuestionDialog, {
|
||||
title: _t("Warning!"),
|
||||
description:
|
||||
<div>
|
||||
{ _t("If you don't specify an email address, you won't be able to reset your password. " +
|
||||
"Are you sure?") }
|
||||
</div>,
|
||||
button: _t("Continue"),
|
||||
onFinished: function(confirmed) {
|
||||
if (confirmed) {
|
||||
self._doSubmit(ev);
|
||||
}
|
||||
},
|
||||
});
|
||||
} else {
|
||||
self._doSubmit(ev);
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -134,118 +127,81 @@ module.exports = React.createClass({
|
|||
}
|
||||
},
|
||||
|
||||
async verifyFieldsBeforeSubmit() {
|
||||
// Blur the active element if any, so we first run its blur validation,
|
||||
// which is less strict than the pass we're about to do below for all fields.
|
||||
const activeElement = document.activeElement;
|
||||
if (activeElement) {
|
||||
activeElement.blur();
|
||||
}
|
||||
|
||||
const fieldIDsInDisplayOrder = [
|
||||
FIELD_USERNAME,
|
||||
FIELD_PASSWORD,
|
||||
FIELD_PASSWORD_CONFIRM,
|
||||
FIELD_EMAIL,
|
||||
FIELD_PHONE_NUMBER,
|
||||
];
|
||||
|
||||
// Run all fields with stricter validation that no longer allows empty
|
||||
// values for required fields.
|
||||
for (const fieldID of fieldIDsInDisplayOrder) {
|
||||
const field = this[fieldID];
|
||||
if (!field) {
|
||||
continue;
|
||||
}
|
||||
field.validate({ allowEmpty: false });
|
||||
}
|
||||
|
||||
// Validation and state updates are async, so we need to wait for them to complete
|
||||
// first. Queue a `setState` callback and wait for it to resolve.
|
||||
await new Promise(resolve => this.setState({}, resolve));
|
||||
|
||||
if (this.allFieldsValid()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const invalidField = this.findFirstInvalidField(fieldIDsInDisplayOrder);
|
||||
|
||||
if (!invalidField) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Focus the first invalid field and show feedback in the stricter mode
|
||||
// that no longer allows empty values for required fields.
|
||||
invalidField.focus();
|
||||
invalidField.validate({ allowEmpty: false, focused: true });
|
||||
return false;
|
||||
},
|
||||
|
||||
/**
|
||||
* @returns {boolean} true if all fields were valid last time they were validated.
|
||||
*/
|
||||
allFieldsValid: function() {
|
||||
const keys = Object.keys(this.state.fieldErrors);
|
||||
const keys = Object.keys(this.state.fieldValid);
|
||||
for (let i = 0; i < keys.length; ++i) {
|
||||
if (this.state.fieldErrors[keys[i]]) {
|
||||
if (!this.state.fieldValid[keys[i]]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
validateField: function(fieldID, eventType) {
|
||||
const pwd1 = this.state.password.trim();
|
||||
const pwd2 = this.state.passwordConfirm.trim();
|
||||
const allowEmpty = eventType === "blur";
|
||||
|
||||
switch (fieldID) {
|
||||
case FIELD_EMAIL: {
|
||||
const email = this.state.email;
|
||||
const emailValid = email === '' || Email.looksValid(email);
|
||||
if (this._authStepIsRequired('m.login.email.identity') && (!emailValid || email === '')) {
|
||||
this.markFieldValid(fieldID, false, "RegistrationForm.ERR_MISSING_EMAIL");
|
||||
} else this.markFieldValid(fieldID, emailValid, "RegistrationForm.ERR_EMAIL_INVALID");
|
||||
break;
|
||||
findFirstInvalidField(fieldIDs) {
|
||||
for (const fieldID of fieldIDs) {
|
||||
if (!this.state.fieldValid[fieldID] && this[fieldID]) {
|
||||
return this[fieldID];
|
||||
}
|
||||
case FIELD_PHONE_NUMBER: {
|
||||
const phoneNumber = this.state.phoneNumber;
|
||||
const phoneNumberValid = phoneNumber === '' || phoneNumberLooksValid(phoneNumber);
|
||||
if (this._authStepIsRequired('m.login.msisdn') && (!phoneNumberValid || phoneNumber === '')) {
|
||||
this.markFieldValid(fieldID, false, "RegistrationForm.ERR_MISSING_PHONE_NUMBER");
|
||||
} else this.markFieldValid(fieldID, phoneNumberValid, "RegistrationForm.ERR_PHONE_NUMBER_INVALID");
|
||||
break;
|
||||
}
|
||||
case FIELD_USERNAME: {
|
||||
const username = this.state.username;
|
||||
if (allowEmpty && username === '') {
|
||||
this.markFieldValid(fieldID, true);
|
||||
} else if (!SAFE_LOCALPART_REGEX.test(username)) {
|
||||
this.markFieldValid(
|
||||
fieldID,
|
||||
false,
|
||||
"RegistrationForm.ERR_USERNAME_INVALID",
|
||||
);
|
||||
} else if (username == '') {
|
||||
this.markFieldValid(
|
||||
fieldID,
|
||||
false,
|
||||
"RegistrationForm.ERR_USERNAME_BLANK",
|
||||
);
|
||||
} else {
|
||||
this.markFieldValid(fieldID, true);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case FIELD_PASSWORD:
|
||||
if (allowEmpty && pwd1 === "") {
|
||||
this.markFieldValid(fieldID, true);
|
||||
} else if (pwd1 == '') {
|
||||
this.markFieldValid(
|
||||
fieldID,
|
||||
false,
|
||||
"RegistrationForm.ERR_PASSWORD_MISSING",
|
||||
);
|
||||
} else if (pwd1.length < this.props.minPasswordLength) {
|
||||
this.markFieldValid(
|
||||
fieldID,
|
||||
false,
|
||||
"RegistrationForm.ERR_PASSWORD_LENGTH",
|
||||
);
|
||||
} else {
|
||||
this.markFieldValid(fieldID, true);
|
||||
}
|
||||
break;
|
||||
case FIELD_PASSWORD_CONFIRM:
|
||||
if (allowEmpty && pwd2 === "") {
|
||||
this.markFieldValid(fieldID, true);
|
||||
} else {
|
||||
this.markFieldValid(
|
||||
fieldID, pwd1 == pwd2,
|
||||
"RegistrationForm.ERR_PASSWORD_MISMATCH",
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
markFieldValid: function(fieldID, valid, errorCode) {
|
||||
const { fieldErrors } = this.state;
|
||||
if (valid) {
|
||||
fieldErrors[fieldID] = null;
|
||||
} else {
|
||||
fieldErrors[fieldID] = errorCode;
|
||||
}
|
||||
markFieldValid: function(fieldID, valid) {
|
||||
const { fieldValid } = this.state;
|
||||
fieldValid[fieldID] = valid;
|
||||
this.setState({
|
||||
fieldErrors,
|
||||
fieldValid,
|
||||
});
|
||||
this.props.onValidationChange(fieldErrors);
|
||||
},
|
||||
|
||||
_classForField: function(fieldID, ...baseClasses) {
|
||||
let cls = baseClasses.join(' ');
|
||||
if (this.state.fieldErrors[fieldID]) {
|
||||
if (cls) cls += ' ';
|
||||
cls += 'error';
|
||||
}
|
||||
return cls;
|
||||
},
|
||||
|
||||
onEmailBlur(ev) {
|
||||
this.validateField(FIELD_EMAIL, ev.type);
|
||||
},
|
||||
|
||||
onEmailChange(ev) {
|
||||
|
@ -254,26 +210,113 @@ module.exports = React.createClass({
|
|||
});
|
||||
},
|
||||
|
||||
onPasswordBlur(ev) {
|
||||
this.validateField(FIELD_PASSWORD, ev.type);
|
||||
async onEmailValidate(fieldState) {
|
||||
const result = await this.validateEmailRules(fieldState);
|
||||
this.markFieldValid(FIELD_EMAIL, result.valid);
|
||||
return result;
|
||||
},
|
||||
|
||||
validateEmailRules: withValidation({
|
||||
description: () => _t("Use an email address to recover your account"),
|
||||
rules: [
|
||||
{
|
||||
key: "required",
|
||||
test: function({ value, allowEmpty }) {
|
||||
return allowEmpty || !this._authStepIsRequired('m.login.email.identity') || !!value;
|
||||
},
|
||||
invalid: () => _t("Enter email address (required on this homeserver)"),
|
||||
},
|
||||
{
|
||||
key: "email",
|
||||
test: ({ value }) => !value || Email.looksValid(value),
|
||||
invalid: () => _t("Doesn't look like a valid email address"),
|
||||
},
|
||||
],
|
||||
}),
|
||||
|
||||
onPasswordChange(ev) {
|
||||
this.setState({
|
||||
password: ev.target.value,
|
||||
});
|
||||
},
|
||||
|
||||
onPasswordConfirmBlur(ev) {
|
||||
this.validateField(FIELD_PASSWORD_CONFIRM, ev.type);
|
||||
async onPasswordValidate(fieldState) {
|
||||
const result = await this.validatePasswordRules(fieldState);
|
||||
this.markFieldValid(FIELD_PASSWORD, result.valid);
|
||||
return result;
|
||||
},
|
||||
|
||||
validatePasswordRules: withValidation({
|
||||
description: function() {
|
||||
const complexity = this.state.passwordComplexity;
|
||||
const score = complexity ? complexity.score : 0;
|
||||
return <progress
|
||||
className="mx_AuthBody_passwordScore"
|
||||
max={PASSWORD_MIN_SCORE}
|
||||
value={score}
|
||||
/>;
|
||||
},
|
||||
rules: [
|
||||
{
|
||||
key: "required",
|
||||
test: ({ value, allowEmpty }) => allowEmpty || !!value,
|
||||
invalid: () => _t("Enter password"),
|
||||
},
|
||||
{
|
||||
key: "complexity",
|
||||
test: async function({ value }) {
|
||||
if (!value) {
|
||||
return false;
|
||||
}
|
||||
const { scorePassword } = await import('../../../utils/PasswordScorer');
|
||||
const complexity = scorePassword(value);
|
||||
this.setState({
|
||||
passwordComplexity: complexity,
|
||||
});
|
||||
return complexity.score >= PASSWORD_MIN_SCORE;
|
||||
},
|
||||
valid: () => _t("Nice, strong password!"),
|
||||
invalid: function() {
|
||||
const complexity = this.state.passwordComplexity;
|
||||
if (!complexity) {
|
||||
return null;
|
||||
}
|
||||
const { feedback } = complexity;
|
||||
return feedback.warning || feedback.suggestions[0] || _t("Keep going...");
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
|
||||
onPasswordConfirmChange(ev) {
|
||||
this.setState({
|
||||
passwordConfirm: ev.target.value,
|
||||
});
|
||||
},
|
||||
|
||||
async onPasswordConfirmValidate(fieldState) {
|
||||
const result = await this.validatePasswordConfirmRules(fieldState);
|
||||
this.markFieldValid(FIELD_PASSWORD_CONFIRM, result.valid);
|
||||
return result;
|
||||
},
|
||||
|
||||
validatePasswordConfirmRules: withValidation({
|
||||
rules: [
|
||||
{
|
||||
key: "required",
|
||||
test: ({ value, allowEmpty }) => allowEmpty || !!value,
|
||||
invalid: () => _t("Confirm password"),
|
||||
},
|
||||
{
|
||||
key: "match",
|
||||
test: function({ value }) {
|
||||
return !value || value === this.state.password;
|
||||
},
|
||||
invalid: () => _t("Passwords don't match"),
|
||||
},
|
||||
],
|
||||
}),
|
||||
|
||||
onPhoneCountryChange(newVal) {
|
||||
this.setState({
|
||||
phoneCountry: newVal.iso2,
|
||||
|
@ -281,26 +324,64 @@ module.exports = React.createClass({
|
|||
});
|
||||
},
|
||||
|
||||
onPhoneNumberBlur(ev) {
|
||||
this.validateField(FIELD_PHONE_NUMBER, ev.type);
|
||||
},
|
||||
|
||||
onPhoneNumberChange(ev) {
|
||||
this.setState({
|
||||
phoneNumber: ev.target.value,
|
||||
});
|
||||
},
|
||||
|
||||
onUsernameBlur(ev) {
|
||||
this.validateField(FIELD_USERNAME, ev.type);
|
||||
async onPhoneNumberValidate(fieldState) {
|
||||
const result = await this.validatePhoneNumberRules(fieldState);
|
||||
this.markFieldValid(FIELD_PHONE_NUMBER, result.valid);
|
||||
return result;
|
||||
},
|
||||
|
||||
validatePhoneNumberRules: withValidation({
|
||||
description: () => _t("Other users can invite you to rooms using your contact details"),
|
||||
rules: [
|
||||
{
|
||||
key: "required",
|
||||
test: function({ value, allowEmpty }) {
|
||||
return allowEmpty || !this._authStepIsRequired('m.login.msisdn') || !!value;
|
||||
},
|
||||
invalid: () => _t("Enter phone number (required on this homeserver)"),
|
||||
},
|
||||
{
|
||||
key: "email",
|
||||
test: ({ value }) => !value || phoneNumberLooksValid(value),
|
||||
invalid: () => _t("Doesn't look like a valid phone number"),
|
||||
},
|
||||
],
|
||||
}),
|
||||
|
||||
onUsernameChange(ev) {
|
||||
this.setState({
|
||||
username: ev.target.value,
|
||||
});
|
||||
},
|
||||
|
||||
async onUsernameValidate(fieldState) {
|
||||
const result = await this.validateUsernameRules(fieldState);
|
||||
this.markFieldValid(FIELD_USERNAME, result.valid);
|
||||
return result;
|
||||
},
|
||||
|
||||
validateUsernameRules: withValidation({
|
||||
description: () => _t("Use letters, numbers, dashes and underscores only"),
|
||||
rules: [
|
||||
{
|
||||
key: "required",
|
||||
test: ({ value, allowEmpty }) => allowEmpty || !!value,
|
||||
invalid: () => _t("Enter username"),
|
||||
},
|
||||
{
|
||||
key: "safeLocalpart",
|
||||
test: ({ value }) => !value || SAFE_LOCALPART_REGEX.test(value),
|
||||
invalid: () => _t("Some characters not allowed"),
|
||||
},
|
||||
],
|
||||
}),
|
||||
|
||||
/**
|
||||
* A step is required if all flows include that step.
|
||||
*
|
||||
|
@ -325,9 +406,99 @@ module.exports = React.createClass({
|
|||
});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
renderEmail() {
|
||||
if (!this._authStepIsUsed('m.login.email.identity')) {
|
||||
return null;
|
||||
}
|
||||
const Field = sdk.getComponent('elements.Field');
|
||||
const emailPlaceholder = this._authStepIsRequired('m.login.email.identity') ?
|
||||
_t("Email") :
|
||||
_t("Email (optional)");
|
||||
return <Field
|
||||
id="mx_RegistrationForm_email"
|
||||
ref={field => this[FIELD_EMAIL] = field}
|
||||
type="text"
|
||||
label={emailPlaceholder}
|
||||
defaultValue={this.props.defaultEmail}
|
||||
value={this.state.email}
|
||||
onChange={this.onEmailChange}
|
||||
onValidate={this.onEmailValidate}
|
||||
/>;
|
||||
},
|
||||
|
||||
renderPassword() {
|
||||
const Field = sdk.getComponent('elements.Field');
|
||||
return <Field
|
||||
id="mx_RegistrationForm_password"
|
||||
ref={field => this[FIELD_PASSWORD] = field}
|
||||
type="password"
|
||||
label={_t("Password")}
|
||||
defaultValue={this.props.defaultPassword}
|
||||
value={this.state.password}
|
||||
onChange={this.onPasswordChange}
|
||||
onValidate={this.onPasswordValidate}
|
||||
/>;
|
||||
},
|
||||
|
||||
renderPasswordConfirm() {
|
||||
const Field = sdk.getComponent('elements.Field');
|
||||
return <Field
|
||||
id="mx_RegistrationForm_passwordConfirm"
|
||||
ref={field => this[FIELD_PASSWORD_CONFIRM] = field}
|
||||
type="password"
|
||||
label={_t("Confirm")}
|
||||
defaultValue={this.props.defaultPassword}
|
||||
value={this.state.passwordConfirm}
|
||||
onChange={this.onPasswordConfirmChange}
|
||||
onValidate={this.onPasswordConfirmValidate}
|
||||
/>;
|
||||
},
|
||||
|
||||
renderPhoneNumber() {
|
||||
const threePidLogin = !SdkConfig.get().disable_3pid_login;
|
||||
if (!threePidLogin || !this._authStepIsUsed('m.login.msisdn')) {
|
||||
return null;
|
||||
}
|
||||
const CountryDropdown = sdk.getComponent('views.auth.CountryDropdown');
|
||||
const Field = sdk.getComponent('elements.Field');
|
||||
const phoneLabel = this._authStepIsRequired('m.login.msisdn') ?
|
||||
_t("Phone") :
|
||||
_t("Phone (optional)");
|
||||
const phoneCountry = <CountryDropdown
|
||||
value={this.state.phoneCountry}
|
||||
isSmall={true}
|
||||
showPrefix={true}
|
||||
onOptionChange={this.onPhoneCountryChange}
|
||||
/>;
|
||||
return <Field
|
||||
id="mx_RegistrationForm_phoneNumber"
|
||||
ref={field => this[FIELD_PHONE_NUMBER] = field}
|
||||
type="text"
|
||||
label={phoneLabel}
|
||||
defaultValue={this.props.defaultPhoneNumber}
|
||||
value={this.state.phoneNumber}
|
||||
prefix={phoneCountry}
|
||||
onChange={this.onPhoneNumberChange}
|
||||
onValidate={this.onPhoneNumberValidate}
|
||||
/>;
|
||||
},
|
||||
|
||||
renderUsername() {
|
||||
const Field = sdk.getComponent('elements.Field');
|
||||
return <Field
|
||||
id="mx_RegistrationForm_username"
|
||||
ref={field => this[FIELD_USERNAME] = field}
|
||||
type="text"
|
||||
autoFocus={true}
|
||||
label={_t("Username")}
|
||||
defaultValue={this.props.defaultUsername}
|
||||
value={this.state.username}
|
||||
onChange={this.onUsernameChange}
|
||||
onValidate={this.onUsernameValidate}
|
||||
/>;
|
||||
},
|
||||
|
||||
render: function() {
|
||||
let yourMatrixAccountText = _t('Create your Matrix account');
|
||||
if (this.props.hsName) {
|
||||
yourMatrixAccountText = _t('Create your Matrix account on %(serverName)s', {
|
||||
|
@ -353,53 +524,6 @@ module.exports = React.createClass({
|
|||
</a>;
|
||||
}
|
||||
|
||||
let emailSection;
|
||||
if (this._authStepIsUsed('m.login.email.identity')) {
|
||||
const emailPlaceholder = this._authStepIsRequired('m.login.email.identity') ?
|
||||
_t("Email") :
|
||||
_t("Email (optional)");
|
||||
|
||||
emailSection = (
|
||||
<Field
|
||||
className={this._classForField(FIELD_EMAIL)}
|
||||
id="mx_RegistrationForm_email"
|
||||
type="text"
|
||||
label={emailPlaceholder}
|
||||
defaultValue={this.props.defaultEmail}
|
||||
value={this.state.email}
|
||||
onBlur={this.onEmailBlur}
|
||||
onChange={this.onEmailChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const threePidLogin = !SdkConfig.get().disable_3pid_login;
|
||||
const CountryDropdown = sdk.getComponent('views.auth.CountryDropdown');
|
||||
let phoneSection;
|
||||
if (threePidLogin && this._authStepIsUsed('m.login.msisdn')) {
|
||||
const phoneLabel = this._authStepIsRequired('m.login.msisdn') ?
|
||||
_t("Phone") :
|
||||
_t("Phone (optional)");
|
||||
const phoneCountry = <CountryDropdown
|
||||
value={this.state.phoneCountry}
|
||||
isSmall={true}
|
||||
showPrefix={true}
|
||||
onOptionChange={this.onPhoneCountryChange}
|
||||
/>;
|
||||
|
||||
phoneSection = <Field
|
||||
className={this._classForField(FIELD_PHONE_NUMBER)}
|
||||
id="mx_RegistrationForm_phoneNumber"
|
||||
type="text"
|
||||
label={phoneLabel}
|
||||
defaultValue={this.props.defaultPhoneNumber}
|
||||
value={this.state.phoneNumber}
|
||||
prefix={phoneCountry}
|
||||
onBlur={this.onPhoneNumberBlur}
|
||||
onChange={this.onPhoneNumberChange}
|
||||
/>;
|
||||
}
|
||||
|
||||
const registerButton = (
|
||||
<input className="mx_Login_submit" type="submit" value={_t("Register")} />
|
||||
);
|
||||
|
@ -412,48 +536,18 @@ module.exports = React.createClass({
|
|||
</h3>
|
||||
<form onSubmit={this.onSubmit}>
|
||||
<div className="mx_AuthBody_fieldRow">
|
||||
<Field
|
||||
className={this._classForField(FIELD_USERNAME)}
|
||||
id="mx_RegistrationForm_username"
|
||||
type="text"
|
||||
autoFocus={true}
|
||||
label={_t("Username")}
|
||||
defaultValue={this.props.defaultUsername}
|
||||
value={this.state.username}
|
||||
onBlur={this.onUsernameBlur}
|
||||
onChange={this.onUsernameChange}
|
||||
/>
|
||||
{this.renderUsername()}
|
||||
</div>
|
||||
<div className="mx_AuthBody_fieldRow">
|
||||
<Field
|
||||
className={this._classForField(FIELD_PASSWORD)}
|
||||
id="mx_RegistrationForm_password"
|
||||
type="password"
|
||||
label={_t("Password")}
|
||||
defaultValue={this.props.defaultPassword}
|
||||
value={this.state.password}
|
||||
onBlur={this.onPasswordBlur}
|
||||
onChange={this.onPasswordChange}
|
||||
/>
|
||||
<Field
|
||||
className={this._classForField(FIELD_PASSWORD_CONFIRM)}
|
||||
id="mx_RegistrationForm_passwordConfirm"
|
||||
type="password"
|
||||
label={_t("Confirm")}
|
||||
defaultValue={this.props.defaultPassword}
|
||||
value={this.state.passwordConfirm}
|
||||
onBlur={this.onPasswordConfirmBlur}
|
||||
onChange={this.onPasswordConfirmChange}
|
||||
/>
|
||||
{this.renderPassword()}
|
||||
{this.renderPasswordConfirm()}
|
||||
</div>
|
||||
<div className="mx_AuthBody_fieldRow">
|
||||
{ emailSection }
|
||||
{ phoneSection }
|
||||
{this.renderEmail()}
|
||||
{this.renderPhoneNumber()}
|
||||
</div>
|
||||
{_t(
|
||||
"Use an email address to recover your account. Other users " +
|
||||
"can invite you to rooms using your contact details.",
|
||||
)}
|
||||
{_t("Use an email address to recover your account.") + " "}
|
||||
{_t("Other users can invite you to rooms using your contact details.")}
|
||||
{ registerButton }
|
||||
</form>
|
||||
</div>
|
||||
|
|
|
@ -27,6 +27,7 @@ import Modal from '../../../Modal';
|
|||
import Resend from '../../../Resend';
|
||||
import SettingsStore from '../../../settings/SettingsStore';
|
||||
import { isUrlPermitted } from '../../../HtmlUtils';
|
||||
import { isContentActionable } from '../../../utils/EventUtils';
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'MessageContextMenu',
|
||||
|
@ -201,14 +202,6 @@ module.exports = React.createClass({
|
|||
this.closeMenu();
|
||||
},
|
||||
|
||||
onReplyClick: function() {
|
||||
dis.dispatch({
|
||||
action: 'reply_to_event',
|
||||
event: this.props.mxEvent,
|
||||
});
|
||||
this.closeMenu();
|
||||
},
|
||||
|
||||
onCollapseReplyThreadClick: function() {
|
||||
this.props.collapseReplyThread();
|
||||
this.closeMenu();
|
||||
|
@ -226,7 +219,6 @@ module.exports = React.createClass({
|
|||
let unhidePreviewButton;
|
||||
let externalURLButton;
|
||||
let quoteButton;
|
||||
let replyButton;
|
||||
let collapseReplyThread;
|
||||
|
||||
// status is SENT before remote-echo, null after
|
||||
|
@ -256,28 +248,19 @@ module.exports = React.createClass({
|
|||
);
|
||||
}
|
||||
|
||||
if (isSent && mxEvent.getType() === 'm.room.message') {
|
||||
const content = mxEvent.getContent();
|
||||
if (content.msgtype && content.msgtype !== 'm.bad.encrypted' && content.hasOwnProperty('body')) {
|
||||
forwardButton = (
|
||||
<div className="mx_MessageContextMenu_field" onClick={this.onForwardClick}>
|
||||
{ _t('Forward Message') }
|
||||
if (isContentActionable(mxEvent)) {
|
||||
forwardButton = (
|
||||
<div className="mx_MessageContextMenu_field" onClick={this.onForwardClick}>
|
||||
{ _t('Forward Message') }
|
||||
</div>
|
||||
);
|
||||
|
||||
if (this.state.canPin) {
|
||||
pinButton = (
|
||||
<div className="mx_MessageContextMenu_field" onClick={this.onPinClick}>
|
||||
{ this._isPinned() ? _t('Unpin Message') : _t('Pin Message') }
|
||||
</div>
|
||||
);
|
||||
|
||||
replyButton = (
|
||||
<div className="mx_MessageContextMenu_field" onClick={this.onReplyClick}>
|
||||
{ _t('Reply') }
|
||||
</div>
|
||||
);
|
||||
|
||||
if (this.state.canPin) {
|
||||
pinButton = (
|
||||
<div className="mx_MessageContextMenu_field" onClick={this.onPinClick}>
|
||||
{ this._isPinned() ? _t('Unpin Message') : _t('Pin Message') }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -368,7 +351,6 @@ module.exports = React.createClass({
|
|||
{ unhidePreviewButton }
|
||||
{ permalinkButton }
|
||||
{ quoteButton }
|
||||
{ replyButton }
|
||||
{ externalURLButton }
|
||||
{ collapseReplyThread }
|
||||
{ e2eInfo }
|
||||
|
|
|
@ -130,7 +130,7 @@ export default class LogoutDialog extends React.Component {
|
|||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
let setupButtonCaption;
|
||||
if (this.state.backupInfo) {
|
||||
setupButtonCaption = _t("Use Key Backup");
|
||||
setupButtonCaption = _t("Connect this device to Key Backup");
|
||||
} else {
|
||||
// if there's an error fetching the backup info, we'll just assume there's
|
||||
// no backup for the purpose of the button caption
|
||||
|
|
|
@ -1,130 +0,0 @@
|
|||
/*
|
||||
Copyright 2019 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import sdk from '../../../index';
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
// Dev note: this should be a temporary dialog while we work out what is
|
||||
// actually going on. See https://github.com/vector-im/riot-web/issues/8593
|
||||
// for more details. This dialog is almost entirely a copy/paste job of
|
||||
// BugReportDialog.
|
||||
export default class TimelineExplosionDialog extends React.Component {
|
||||
static propTypes = {
|
||||
onFinished: React.PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
this.state = {
|
||||
busy: false,
|
||||
progress: null,
|
||||
};
|
||||
}
|
||||
|
||||
_onCancel() {
|
||||
console.log("Reloading without sending logs for timeline explosion");
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
_onSubmit = () => {
|
||||
const userText = "Caught timeline explosion\n\nhttps://github.com/vector-im/riot-web/issues/8593";
|
||||
|
||||
this.setState({busy: true, progress: null});
|
||||
this._sendProgressCallback(_t("Preparing to send logs"));
|
||||
|
||||
require(['../../../rageshake/submit-rageshake'], (s) => {
|
||||
s(SdkConfig.get().bug_report_endpoint_url, {
|
||||
userText,
|
||||
sendLogs: true,
|
||||
progressCallback: this._sendProgressCallback,
|
||||
}).then(() => {
|
||||
console.log("Logs sent for timeline explosion - reloading Riot");
|
||||
window.location.reload();
|
||||
}, (err) => {
|
||||
console.error("Error sending logs for timeline explosion - reloading anyways.", err);
|
||||
window.location.reload();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
_sendProgressCallback = (progress) => {
|
||||
this.setState({progress: progress});
|
||||
};
|
||||
|
||||
render() {
|
||||
const Loader = sdk.getComponent("elements.Spinner");
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||
|
||||
let progress = null;
|
||||
if (this.state.busy) {
|
||||
progress = (
|
||||
<div className="progress">
|
||||
{this.state.progress} ...
|
||||
<Loader />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<BaseDialog className="mx_TimelineExplosionDialog" onFinished={this._onCancel}
|
||||
title={_t('Error showing you your room')} contentId='mx_Dialog_content'
|
||||
>
|
||||
<div className="mx_Dialog_content" id='mx_Dialog_content'>
|
||||
<p>
|
||||
{_t(
|
||||
"Riot has run into a problem which makes it difficult to show you " +
|
||||
"your messages right now. Nothing has been lost and reloading the app " +
|
||||
"should fix this for you. In order to assist us in troubleshooting the " +
|
||||
"problem, we'd like to take a look at your debug logs. You do not need " +
|
||||
"to send your logs unless you want to, but we would really appreciate " +
|
||||
"it if you did. We'd also like to apologize for having to show this " +
|
||||
"message to you - we hope your debug logs are the key to solving the " +
|
||||
"issue once and for all. If you'd like more information on the bug you've " +
|
||||
"accidentally run into, please visit <a>the issue</a>.",
|
||||
{},
|
||||
{
|
||||
'a': (sub) => {
|
||||
return <a href="https://github.com/vector-im/riot-web/issues/8593"
|
||||
target="_blank" rel="noopener">{sub}</a>;
|
||||
},
|
||||
},
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
{_t(
|
||||
"Debug logs contain application usage data including your " +
|
||||
"username, the IDs or aliases of the rooms or groups you " +
|
||||
"have visited and the usernames of other users. They do " +
|
||||
"not contain messages.",
|
||||
)}
|
||||
</p>
|
||||
{progress}
|
||||
</div>
|
||||
<DialogButtons primaryButton={_t("Send debug logs and reload Riot")}
|
||||
onPrimaryButtonClick={this._onSubmit}
|
||||
cancelButton={_t("Reload Riot without sending logs")}
|
||||
focus={true}
|
||||
onCancel={this._onCancel}
|
||||
disabled={this.state.busy}
|
||||
/>
|
||||
</BaseDialog>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -18,6 +18,10 @@ import React from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import sdk from '../../../index';
|
||||
import { throttle } from 'lodash';
|
||||
|
||||
// Invoke validation from user input (when typing, etc.) at most once every N ms.
|
||||
const VALIDATION_THROTTLE_MS = 200;
|
||||
|
||||
export default class Field extends React.PureComponent {
|
||||
static propTypes = {
|
||||
|
@ -53,20 +57,73 @@ export default class Field extends React.PureComponent {
|
|||
};
|
||||
}
|
||||
|
||||
onChange = (ev) => {
|
||||
if (this.props.onValidate) {
|
||||
const result = this.props.onValidate(ev.target.value);
|
||||
this.setState({
|
||||
valid: result.valid,
|
||||
feedback: result.feedback,
|
||||
});
|
||||
onFocus = (ev) => {
|
||||
this.validate({
|
||||
focused: true,
|
||||
});
|
||||
// Parent component may have supplied its own `onFocus` as well
|
||||
if (this.props.onFocus) {
|
||||
this.props.onFocus(ev);
|
||||
}
|
||||
};
|
||||
|
||||
onChange = (ev) => {
|
||||
this.validateOnChange();
|
||||
// Parent component may have supplied its own `onChange` as well
|
||||
if (this.props.onChange) {
|
||||
this.props.onChange(ev);
|
||||
}
|
||||
};
|
||||
|
||||
onBlur = (ev) => {
|
||||
this.validate({
|
||||
focused: false,
|
||||
});
|
||||
// Parent component may have supplied its own `onBlur` as well
|
||||
if (this.props.onBlur) {
|
||||
this.props.onBlur(ev);
|
||||
}
|
||||
};
|
||||
|
||||
focus() {
|
||||
this.input.focus();
|
||||
}
|
||||
|
||||
async validate({ focused, allowEmpty = true }) {
|
||||
if (!this.props.onValidate) {
|
||||
return;
|
||||
}
|
||||
const value = this.input ? this.input.value : null;
|
||||
const { valid, feedback } = await this.props.onValidate({
|
||||
value,
|
||||
focused,
|
||||
allowEmpty,
|
||||
});
|
||||
|
||||
if (feedback) {
|
||||
this.setState({
|
||||
valid,
|
||||
feedback,
|
||||
feedbackVisible: true,
|
||||
});
|
||||
} else {
|
||||
// When we receive null `feedback`, we want to hide the tooltip.
|
||||
// We leave the previous `feedback` content in state without updating it,
|
||||
// so that we can hide the tooltip containing the most recent feedback
|
||||
// via CSS animation.
|
||||
this.setState({
|
||||
valid,
|
||||
feedbackVisible: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
validateOnChange = throttle(() => {
|
||||
this.validate({
|
||||
focused: true,
|
||||
});
|
||||
}, VALIDATION_THROTTLE_MS);
|
||||
|
||||
render() {
|
||||
const { element, prefix, onValidate, children, ...inputProps } = this.props;
|
||||
|
||||
|
@ -74,10 +131,12 @@ export default class Field extends React.PureComponent {
|
|||
|
||||
// Set some defaults for the <input> element
|
||||
inputProps.type = inputProps.type || "text";
|
||||
inputProps.ref = "fieldInput";
|
||||
inputProps.ref = input => this.input = input;
|
||||
inputProps.placeholder = inputProps.placeholder || inputProps.label;
|
||||
|
||||
inputProps.onFocus = this.onFocus;
|
||||
inputProps.onChange = this.onChange;
|
||||
inputProps.onBlur = this.onBlur;
|
||||
|
||||
const fieldInput = React.createElement(inputElement, inputProps, children);
|
||||
|
||||
|
@ -95,12 +154,13 @@ export default class Field extends React.PureComponent {
|
|||
mx_Field_invalid: onValidate && this.state.valid === false,
|
||||
});
|
||||
|
||||
// handle displaying feedback on validity
|
||||
// Handle displaying feedback on validity
|
||||
const Tooltip = sdk.getComponent("elements.Tooltip");
|
||||
let feedback;
|
||||
let tooltip;
|
||||
if (this.state.feedback) {
|
||||
feedback = <Tooltip
|
||||
tooltip = <Tooltip
|
||||
tooltipClassName="mx_Field_tooltip"
|
||||
visible={this.state.feedbackVisible}
|
||||
label={this.state.feedback}
|
||||
/>;
|
||||
}
|
||||
|
@ -109,7 +169,7 @@ export default class Field extends React.PureComponent {
|
|||
{prefixContainer}
|
||||
{fieldInput}
|
||||
<label htmlFor={this.props.id}>{this.props.label}</label>
|
||||
{feedback}
|
||||
{tooltip}
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,10 +31,20 @@ module.exports = React.createClass({
|
|||
className: React.PropTypes.string,
|
||||
// Class applied to the tooltip itself
|
||||
tooltipClassName: React.PropTypes.string,
|
||||
// Whether the tooltip is visible or hidden.
|
||||
// The hidden state allows animating the tooltip away via CSS.
|
||||
// Defaults to visible if unset.
|
||||
visible: React.PropTypes.bool,
|
||||
// the react element to put into the tooltip
|
||||
label: React.PropTypes.node,
|
||||
},
|
||||
|
||||
getDefaultProps() {
|
||||
return {
|
||||
visible: true,
|
||||
};
|
||||
},
|
||||
|
||||
// Create a wrapper for the tooltip outside the parent and attach it to the body element
|
||||
componentDidMount: function() {
|
||||
this.tooltipContainer = document.createElement("div");
|
||||
|
@ -85,7 +95,10 @@ module.exports = React.createClass({
|
|||
style = this._updatePosition(style);
|
||||
style.display = "block";
|
||||
|
||||
const tooltipClasses = classNames("mx_Tooltip", this.props.tooltipClassName);
|
||||
const tooltipClasses = classNames("mx_Tooltip", this.props.tooltipClassName, {
|
||||
"mx_Tooltip_visible": this.props.visible,
|
||||
"mx_Tooltip_invisible": !this.props.visible,
|
||||
});
|
||||
|
||||
const tooltip = (
|
||||
<div className={tooltipClasses} style={style}>
|
||||
|
|
131
src/components/views/elements/Validation.js
Normal file
131
src/components/views/elements/Validation.js
Normal file
|
@ -0,0 +1,131 @@
|
|||
/*
|
||||
Copyright 2019 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
/* eslint-disable babel/no-invalid-this */
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
/**
|
||||
* Creates a validation function from a set of rules describing what to validate.
|
||||
*
|
||||
* @param {Function} description
|
||||
* Function that returns a string summary of the kind of value that will
|
||||
* meet the validation rules. Shown at the top of the validation feedback.
|
||||
* @param {Object} rules
|
||||
* An array of rules describing how to check to input value. Each rule in an object
|
||||
* and may have the following properties:
|
||||
* - `key`: A unique ID for the rule. Required.
|
||||
* - `test`: A function used to determine the rule's current validity. Required.
|
||||
* - `valid`: Function returning text to show when the rule is valid. Only shown if set.
|
||||
* - `invalid`: Function returning text to show when the rule is invalid. Only shown if set.
|
||||
* @returns {Function}
|
||||
* A validation function that takes in the current input value and returns
|
||||
* the overall validity and a feedback UI that can be rendered for more detail.
|
||||
*/
|
||||
export default function withValidation({ description, rules }) {
|
||||
return async function onValidate({ value, focused, allowEmpty = true }) {
|
||||
if (!value && allowEmpty) {
|
||||
return {
|
||||
valid: null,
|
||||
feedback: null,
|
||||
};
|
||||
}
|
||||
|
||||
const results = [];
|
||||
let valid = true;
|
||||
if (rules && rules.length) {
|
||||
for (const rule of rules) {
|
||||
if (!rule.key || !rule.test) {
|
||||
continue;
|
||||
}
|
||||
// We're setting `this` to whichever component holds the validation
|
||||
// function. That allows rules to access the state of the component.
|
||||
const ruleValid = await rule.test.call(this, { value, allowEmpty });
|
||||
valid = valid && ruleValid;
|
||||
if (ruleValid && rule.valid) {
|
||||
// If the rule's result is valid and has text to show for
|
||||
// the valid state, show it.
|
||||
const text = rule.valid.call(this);
|
||||
if (!text) {
|
||||
continue;
|
||||
}
|
||||
results.push({
|
||||
key: rule.key,
|
||||
valid: true,
|
||||
text,
|
||||
});
|
||||
} else if (!ruleValid && rule.invalid) {
|
||||
// If the rule's result is invalid and has text to show for
|
||||
// the invalid state, show it.
|
||||
const text = rule.invalid.call(this);
|
||||
if (!text) {
|
||||
continue;
|
||||
}
|
||||
results.push({
|
||||
key: rule.key,
|
||||
valid: false,
|
||||
text,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Hide feedback when not focused
|
||||
if (!focused) {
|
||||
return {
|
||||
valid,
|
||||
feedback: null,
|
||||
};
|
||||
}
|
||||
|
||||
let details;
|
||||
if (results && results.length) {
|
||||
details = <ul className="mx_Validation_details">
|
||||
{results.map(result => {
|
||||
const classes = classNames({
|
||||
"mx_Validation_detail": true,
|
||||
"mx_Validation_valid": result.valid,
|
||||
"mx_Validation_invalid": !result.valid,
|
||||
});
|
||||
return <li key={result.key} className={classes}>
|
||||
{result.text}
|
||||
</li>;
|
||||
})}
|
||||
</ul>;
|
||||
}
|
||||
|
||||
let summary;
|
||||
if (description) {
|
||||
// We're setting `this` to whichever component holds the validation
|
||||
// function. That allows rules to access the state of the component.
|
||||
const content = description.call(this);
|
||||
summary = <div className="mx_Validation_description">{content}</div>;
|
||||
}
|
||||
|
||||
let feedback;
|
||||
if (summary || details) {
|
||||
feedback = <div className="mx_Validation">
|
||||
{summary}
|
||||
{details}
|
||||
</div>;
|
||||
}
|
||||
|
||||
return {
|
||||
valid,
|
||||
feedback,
|
||||
};
|
||||
};
|
||||
}
|
223
src/components/views/messages/MessageActionBar.js
Normal file
223
src/components/views/messages/MessageActionBar.js
Normal file
|
@ -0,0 +1,223 @@
|
|||
/*
|
||||
Copyright 2019 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { _t } from '../../../languageHandler';
|
||||
import sdk from '../../../index';
|
||||
import dis from '../../../dispatcher';
|
||||
import Modal from '../../../Modal';
|
||||
import { createMenu } from '../../structures/ContextualMenu';
|
||||
import SettingsStore from '../../../settings/SettingsStore';
|
||||
import { isContentActionable } from '../../../utils/EventUtils';
|
||||
|
||||
export default class MessageActionBar extends React.PureComponent {
|
||||
static propTypes = {
|
||||
mxEvent: PropTypes.object.isRequired,
|
||||
permalinkCreator: PropTypes.object,
|
||||
getTile: PropTypes.func,
|
||||
getReplyThread: PropTypes.func,
|
||||
onFocusChange: PropTypes.func,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
agreeDimension: null,
|
||||
likeDimension: null,
|
||||
};
|
||||
}
|
||||
|
||||
onFocusChange = (focused) => {
|
||||
if (!this.props.onFocusChange) {
|
||||
return;
|
||||
}
|
||||
this.props.onFocusChange(focused);
|
||||
}
|
||||
|
||||
onCryptoClicked = () => {
|
||||
const event = this.props.mxEvent;
|
||||
Modal.createTrackedDialogAsync('Encrypted Event Dialog', '',
|
||||
import('../../../async-components/views/dialogs/EncryptedEventDialog'),
|
||||
{event},
|
||||
);
|
||||
}
|
||||
|
||||
onAgreeClick = (ev) => {
|
||||
this.toggleDimensionValue("agreeDimension", "agree");
|
||||
}
|
||||
|
||||
onDisagreeClick = (ev) => {
|
||||
this.toggleDimensionValue("agreeDimension", "disagree");
|
||||
}
|
||||
|
||||
onLikeClick = (ev) => {
|
||||
this.toggleDimensionValue("likeDimension", "like");
|
||||
}
|
||||
|
||||
onDislikeClick = (ev) => {
|
||||
this.toggleDimensionValue("likeDimension", "dislike");
|
||||
}
|
||||
|
||||
toggleDimensionValue(dimension, value) {
|
||||
const state = this.state[dimension];
|
||||
const newState = state !== value ? value : null;
|
||||
this.setState({
|
||||
[dimension]: newState,
|
||||
});
|
||||
// TODO: Send the reaction event
|
||||
}
|
||||
|
||||
onReplyClick = (ev) => {
|
||||
dis.dispatch({
|
||||
action: 'reply_to_event',
|
||||
event: this.props.mxEvent,
|
||||
});
|
||||
}
|
||||
|
||||
onOptionsClick = (ev) => {
|
||||
const MessageContextMenu = sdk.getComponent('context_menus.MessageContextMenu');
|
||||
const buttonRect = ev.target.getBoundingClientRect();
|
||||
|
||||
// The window X and Y offsets are to adjust position when zoomed in to page
|
||||
const x = buttonRect.right + window.pageXOffset;
|
||||
const y = (buttonRect.top + (buttonRect.height / 2) + window.pageYOffset) - 19;
|
||||
|
||||
const { getTile, getReplyThread } = this.props;
|
||||
const tile = getTile && getTile();
|
||||
const replyThread = getReplyThread && getReplyThread();
|
||||
|
||||
let e2eInfoCallback = null;
|
||||
if (this.props.mxEvent.isEncrypted()) {
|
||||
e2eInfoCallback = () => this.onCryptoClicked();
|
||||
}
|
||||
|
||||
createMenu(MessageContextMenu, {
|
||||
chevronOffset: 10,
|
||||
mxEvent: this.props.mxEvent,
|
||||
left: x,
|
||||
top: y,
|
||||
permalinkCreator: this.props.permalinkCreator,
|
||||
eventTileOps: tile && tile.getEventTileOps ? tile.getEventTileOps() : undefined,
|
||||
collapseReplyThread: replyThread && replyThread.canCollapse() ? replyThread.collapse : undefined,
|
||||
e2eInfoCallback: e2eInfoCallback,
|
||||
onFinished: () => {
|
||||
this.onFocusChange(false);
|
||||
},
|
||||
});
|
||||
|
||||
this.onFocusChange(true);
|
||||
}
|
||||
|
||||
isReactionsEnabled() {
|
||||
return SettingsStore.isFeatureEnabled("feature_reactions");
|
||||
}
|
||||
|
||||
renderAgreeDimension() {
|
||||
if (!this.isReactionsEnabled()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const state = this.state.agreeDimension;
|
||||
const options = [
|
||||
{
|
||||
key: "agree",
|
||||
content: "👍",
|
||||
onClick: this.onAgreeClick,
|
||||
},
|
||||
{
|
||||
key: "disagree",
|
||||
content: "👎",
|
||||
onClick: this.onDisagreeClick,
|
||||
},
|
||||
];
|
||||
|
||||
return <span className="mx_MessageActionBar_reactionDimension"
|
||||
title={_t("Agree or Disagree")}
|
||||
>
|
||||
{this.renderReactionDimensionItems(state, options)}
|
||||
</span>;
|
||||
}
|
||||
|
||||
renderLikeDimension() {
|
||||
if (!this.isReactionsEnabled()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const state = this.state.likeDimension;
|
||||
const options = [
|
||||
{
|
||||
key: "like",
|
||||
content: "🙂",
|
||||
onClick: this.onLikeClick,
|
||||
},
|
||||
{
|
||||
key: "dislike",
|
||||
content: "😔",
|
||||
onClick: this.onDislikeClick,
|
||||
},
|
||||
];
|
||||
|
||||
return <span className="mx_MessageActionBar_reactionDimension"
|
||||
title={_t("Like or Dislike")}
|
||||
>
|
||||
{this.renderReactionDimensionItems(state, options)}
|
||||
</span>;
|
||||
}
|
||||
|
||||
renderReactionDimensionItems(state, options) {
|
||||
return options.map(option => {
|
||||
const disabled = state && state !== option.key;
|
||||
const classes = classNames({
|
||||
mx_MessageActionBar_reactionDisabled: disabled,
|
||||
});
|
||||
return <span key={option.key}
|
||||
className={classes}
|
||||
onClick={option.onClick}
|
||||
>
|
||||
{option.content}
|
||||
</span>;
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
let agreeDimensionReactionButtons;
|
||||
let likeDimensionReactionButtons;
|
||||
let replyButton;
|
||||
|
||||
if (isContentActionable(this.props.mxEvent)) {
|
||||
agreeDimensionReactionButtons = this.renderAgreeDimension();
|
||||
likeDimensionReactionButtons = this.renderLikeDimension();
|
||||
replyButton = <span className="mx_MessageActionBar_maskButton mx_MessageActionBar_replyButton"
|
||||
title={_t("Reply")}
|
||||
onClick={this.onReplyClick}
|
||||
/>;
|
||||
}
|
||||
|
||||
return <div className="mx_MessageActionBar">
|
||||
{agreeDimensionReactionButtons}
|
||||
{likeDimensionReactionButtons}
|
||||
{replyButton}
|
||||
<span className="mx_MessageActionBar_maskButton mx_MessageActionBar_optionsButton"
|
||||
title={_t("Options")}
|
||||
onClick={this.onOptionsClick}
|
||||
/>
|
||||
</div>;
|
||||
}
|
||||
}
|
65
src/components/views/messages/ReactionsRow.js
Normal file
65
src/components/views/messages/ReactionsRow.js
Normal file
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
Copyright 2019 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import sdk from '../../../index';
|
||||
import { isContentActionable } from '../../../utils/EventUtils';
|
||||
|
||||
// TODO: Actually load reactions from the timeline
|
||||
// Since we don't yet load reactions, let's inject some dummy data for testing the UI
|
||||
// only. The UI assumes these are already sorted into the order we want to present,
|
||||
// presumably highest vote first.
|
||||
const SAMPLE_REACTIONS = {
|
||||
"👍": 4,
|
||||
"👎": 2,
|
||||
"🙂": 1,
|
||||
};
|
||||
|
||||
export default class ReactionsRow extends React.PureComponent {
|
||||
static propTypes = {
|
||||
// The event we're displaying reactions for
|
||||
mxEvent: PropTypes.object.isRequired,
|
||||
}
|
||||
|
||||
render() {
|
||||
const { mxEvent } = this.props;
|
||||
|
||||
if (!isContentActionable(mxEvent)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const content = mxEvent.getContent();
|
||||
// TODO: Remove this once we load real reactions
|
||||
if (!content.body || content.body !== "reactions test") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const ReactionsRowButton = sdk.getComponent('messages.ReactionsRowButton');
|
||||
const items = Object.entries(SAMPLE_REACTIONS).map(([content, count]) => {
|
||||
return <ReactionsRowButton
|
||||
key={content}
|
||||
content={content}
|
||||
count={count}
|
||||
/>;
|
||||
});
|
||||
|
||||
return <div className="mx_ReactionsRow">
|
||||
{items}
|
||||
</div>;
|
||||
}
|
||||
}
|
65
src/components/views/messages/ReactionsRowButton.js
Normal file
65
src/components/views/messages/ReactionsRowButton.js
Normal file
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
Copyright 2019 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
|
||||
export default class ReactionsRowButton extends React.PureComponent {
|
||||
static propTypes = {
|
||||
content: PropTypes.string.isRequired,
|
||||
count: PropTypes.number.isRequired,
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
// TODO: This should be derived from actual reactions you may have sent
|
||||
// once we have some API to read them.
|
||||
this.state = {
|
||||
selected: false,
|
||||
};
|
||||
}
|
||||
|
||||
onClick = (ev) => {
|
||||
const state = this.state.selected;
|
||||
this.setState({
|
||||
selected: !state,
|
||||
});
|
||||
// TODO: Send the reaction event
|
||||
};
|
||||
|
||||
render() {
|
||||
const { content, count } = this.props;
|
||||
const { selected } = this.state;
|
||||
|
||||
const classes = classNames({
|
||||
mx_ReactionsRowButton: true,
|
||||
mx_ReactionsRowButton_selected: selected,
|
||||
});
|
||||
|
||||
let adjustedCount = count;
|
||||
if (selected) {
|
||||
adjustedCount++;
|
||||
}
|
||||
|
||||
return <span className={classes}
|
||||
onClick={this.onClick}
|
||||
>
|
||||
{content} {adjustedCount}
|
||||
</span>;
|
||||
}
|
||||
}
|
|
@ -17,7 +17,6 @@ limitations under the License.
|
|||
|
||||
'use strict';
|
||||
|
||||
|
||||
import ReplyThread from "../elements/ReplyThread";
|
||||
|
||||
const React = require('react');
|
||||
|
@ -30,7 +29,6 @@ const sdk = require('../../../index');
|
|||
const TextForEvent = require('../../../TextForEvent');
|
||||
import withMatrixClient from '../../../wrappers/withMatrixClient';
|
||||
|
||||
const ContextualMenu = require('../../structures/ContextualMenu');
|
||||
import dis from '../../../dispatcher';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import {EventStatus} from 'matrix-js-sdk';
|
||||
|
@ -172,8 +170,8 @@ module.exports = withMatrixClient(React.createClass({
|
|||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
// Whether the context menu is being displayed.
|
||||
menu: false,
|
||||
// Whether the action bar is focused.
|
||||
actionBarFocused: false,
|
||||
// Whether all read receipts are being displayed. If not, only display
|
||||
// a truncation of them.
|
||||
allReadAvatars: false,
|
||||
|
@ -309,36 +307,6 @@ module.exports = withMatrixClient(React.createClass({
|
|||
return actions.tweaks.highlight;
|
||||
},
|
||||
|
||||
onEditClicked: function(e) {
|
||||
const MessageContextMenu = sdk.getComponent('context_menus.MessageContextMenu');
|
||||
const buttonRect = e.target.getBoundingClientRect();
|
||||
|
||||
// The window X and Y offsets are to adjust position when zoomed in to page
|
||||
const x = buttonRect.right + window.pageXOffset;
|
||||
const y = (buttonRect.top + (buttonRect.height / 2) + window.pageYOffset) - 19;
|
||||
const self = this;
|
||||
|
||||
const {tile, replyThread} = this.refs;
|
||||
|
||||
let e2eInfoCallback = null;
|
||||
if (this.props.mxEvent.isEncrypted()) e2eInfoCallback = () => this.onCryptoClicked();
|
||||
|
||||
ContextualMenu.createMenu(MessageContextMenu, {
|
||||
chevronOffset: 10,
|
||||
mxEvent: this.props.mxEvent,
|
||||
left: x,
|
||||
top: y,
|
||||
permalinkCreator: this.props.permalinkCreator,
|
||||
eventTileOps: tile && tile.getEventTileOps ? tile.getEventTileOps() : undefined,
|
||||
collapseReplyThread: replyThread && replyThread.canCollapse() ? replyThread.collapse : undefined,
|
||||
e2eInfoCallback: e2eInfoCallback,
|
||||
onFinished: function() {
|
||||
self.setState({menu: false});
|
||||
},
|
||||
});
|
||||
this.setState({menu: true});
|
||||
},
|
||||
|
||||
toggleAllReadAvatars: function() {
|
||||
this.setState({
|
||||
allReadAvatars: !this.state.allReadAvatars,
|
||||
|
@ -490,6 +458,20 @@ module.exports = withMatrixClient(React.createClass({
|
|||
return null;
|
||||
},
|
||||
|
||||
onActionBarFocusChange(focused) {
|
||||
this.setState({
|
||||
actionBarFocused: focused,
|
||||
});
|
||||
},
|
||||
|
||||
getTile() {
|
||||
return this.refs.tile;
|
||||
},
|
||||
|
||||
getReplyThread() {
|
||||
return this.refs.replyThread;
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const MessageTimestamp = sdk.getComponent('messages.MessageTimestamp');
|
||||
const SenderProfile = sdk.getComponent('messages.SenderProfile');
|
||||
|
@ -536,7 +518,7 @@ module.exports = withMatrixClient(React.createClass({
|
|||
mx_EventTile_continuation: this.props.tileShape ? '' : this.props.continuation,
|
||||
mx_EventTile_last: this.props.last,
|
||||
mx_EventTile_contextual: this.props.contextual,
|
||||
menu: this.state.menu,
|
||||
mx_EventTile_actionBarFocused: this.state.actionBarFocused,
|
||||
mx_EventTile_verified: this.state.verified === true,
|
||||
mx_EventTile_unverified: this.state.verified === false,
|
||||
mx_EventTile_bad: isEncryptionFailure,
|
||||
|
@ -602,9 +584,14 @@ module.exports = withMatrixClient(React.createClass({
|
|||
}
|
||||
}
|
||||
|
||||
const editButton = (
|
||||
<span className="mx_EventTile_editButton" title={_t("Options")} onClick={this.onEditClicked} />
|
||||
);
|
||||
const MessageActionBar = sdk.getComponent('messages.MessageActionBar');
|
||||
const actionBar = <MessageActionBar
|
||||
mxEvent={this.props.mxEvent}
|
||||
permalinkCreator={this.props.permalinkCreator}
|
||||
getTile={this.getTile}
|
||||
getReplyThread={this.getReplyThread}
|
||||
onFocusChange={this.onActionBarFocusChange}
|
||||
/>;
|
||||
|
||||
const timestamp = this.props.mxEvent.getTs() ?
|
||||
<MessageTimestamp showTwelveHour={this.props.isTwelveHour} ts={this.props.mxEvent.getTs()} /> : null;
|
||||
|
@ -643,6 +630,14 @@ module.exports = withMatrixClient(React.createClass({
|
|||
<ToolTipButton helpText={keyRequestHelpText} />
|
||||
</div> : null;
|
||||
|
||||
let reactions;
|
||||
if (SettingsStore.isFeatureEnabled("feature_reactions")) {
|
||||
const ReactionsRow = sdk.getComponent('messages.ReactionsRow');
|
||||
reactions = <ReactionsRow
|
||||
mxEvent={this.props.mxEvent}
|
||||
/>;
|
||||
}
|
||||
|
||||
switch (this.props.tileShape) {
|
||||
case 'notif': {
|
||||
const EmojiText = sdk.getComponent('elements.EmojiText');
|
||||
|
@ -755,7 +750,8 @@ module.exports = withMatrixClient(React.createClass({
|
|||
showUrlPreview={this.props.showUrlPreview}
|
||||
onHeightChanged={this.props.onHeightChanged} />
|
||||
{ keyRequestInfo }
|
||||
{ editButton }
|
||||
{ reactions }
|
||||
{ actionBar }
|
||||
</div>
|
||||
{
|
||||
// The avatar goes after the event tile as it's absolutly positioned to be over the
|
||||
|
|
|
@ -117,6 +117,7 @@ export default class RoomBreadcrumbs extends React.Component {
|
|||
};
|
||||
|
||||
onRoomTimeline = (event, room) => {
|
||||
if (!room) return; // Can be null for the notification timeline, etc.
|
||||
if (this.state.rooms.map(r => r.room.roomId).includes(room.roomId)) {
|
||||
this._calculateRoomBadges(room);
|
||||
}
|
||||
|
|
|
@ -24,7 +24,6 @@ import MatrixClientPeg from '../../../MatrixClientPeg';
|
|||
import dis from '../../../dispatcher';
|
||||
import classNames from 'classnames';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import {getUserNameColorClass} from '../../../utils/FormattingUtils';
|
||||
|
||||
const MessageCase = Object.freeze({
|
||||
NotLoggedIn: "NotLoggedIn",
|
||||
|
@ -105,12 +104,6 @@ module.exports = React.createClass({
|
|||
}
|
||||
},
|
||||
|
||||
_onInviterClick(evt) {
|
||||
evt.preventDefault();
|
||||
const member = this._getInviteMember();
|
||||
dis.dispatch({action: 'view_user_info', userId: member.userId});
|
||||
},
|
||||
|
||||
_getMessageCase() {
|
||||
const isGuest = MatrixClientPeg.get().isGuest();
|
||||
|
||||
|
@ -118,8 +111,7 @@ module.exports = React.createClass({
|
|||
return MessageCase.NotLoggedIn;
|
||||
}
|
||||
|
||||
const myMember = this.props.room &&
|
||||
this.props.room.getMember(MatrixClientPeg.get().getUserId());
|
||||
const myMember = this._getMyMember();
|
||||
|
||||
if (myMember) {
|
||||
if (myMember.isKicked()) {
|
||||
|
@ -158,9 +150,7 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
_getKickOrBanInfo() {
|
||||
const myMember = this.props.room ?
|
||||
this.props.room.getMember(MatrixClientPeg.get().getUserId()) :
|
||||
null;
|
||||
const myMember = this._getMyMember();
|
||||
if (!myMember) {
|
||||
return {};
|
||||
}
|
||||
|
@ -194,6 +184,13 @@ module.exports = React.createClass({
|
|||
}
|
||||
},
|
||||
|
||||
_getMyMember() {
|
||||
return (
|
||||
this.props.room &&
|
||||
this.props.room.getMember(MatrixClientPeg.get().getUserId())
|
||||
);
|
||||
},
|
||||
|
||||
_getInviteMember: function() {
|
||||
const {room} = this.props;
|
||||
if (!room) {
|
||||
|
@ -208,6 +205,16 @@ module.exports = React.createClass({
|
|||
return room.currentState.getMember(inviterUserId);
|
||||
},
|
||||
|
||||
_isDMInvite() {
|
||||
const myMember = this._getMyMember();
|
||||
if (!myMember) {
|
||||
return false;
|
||||
}
|
||||
const memberEvent = myMember.events.member;
|
||||
const memberContent = memberEvent.getContent();
|
||||
return memberContent.membership === "invite" && memberContent.is_direct;
|
||||
},
|
||||
|
||||
onLoginClick: function() {
|
||||
dis.dispatch({ action: 'start_login' });
|
||||
},
|
||||
|
@ -279,7 +286,8 @@ module.exports = React.createClass({
|
|||
break;
|
||||
}
|
||||
case MessageCase.OtherThreePIDError: {
|
||||
title = _t("Something went wrong with your invite to this room");
|
||||
title = _t("Something went wrong with your invite to %(roomName)s",
|
||||
{roomName: this._roomName()});
|
||||
const joinRule = this._joinRule();
|
||||
const errCodeMessage = _t("%(errcode)s was returned while trying to valide your invite. You could try to pass this information on to a room admin.",
|
||||
{errcode: this.state.threePidFetchError.errcode},
|
||||
|
@ -305,14 +313,19 @@ module.exports = React.createClass({
|
|||
break;
|
||||
}
|
||||
case MessageCase.InvitedEmailMismatch: {
|
||||
title = _t("The room invite wasn't sent to your account");
|
||||
title = _t("This invite to %(roomName)s wasn't sent to your account",
|
||||
{roomName: this._roomName()});
|
||||
const joinRule = this._joinRule();
|
||||
if (joinRule === "public") {
|
||||
subTitle = _t("You can still join it because this is a public room.");
|
||||
primaryActionLabel = _t("Join the discussion");
|
||||
primaryActionHandler = this.props.onJoinClick;
|
||||
} else {
|
||||
subTitle = _t("Sign in with a different account, ask for another invite, or add the e-mail address %(email)s to this account.", {email: this.props.invitedEmail});
|
||||
subTitle = _t(
|
||||
"Sign in with a different account, ask for another invite, or " +
|
||||
"add the e-mail address %(email)s to this account.",
|
||||
{email: this.props.invitedEmail},
|
||||
);
|
||||
if (joinRule !== "invite") {
|
||||
primaryActionLabel = _t("Try to join anyway");
|
||||
primaryActionHandler = this.props.onJoinClick;
|
||||
|
@ -321,26 +334,29 @@ module.exports = React.createClass({
|
|||
break;
|
||||
}
|
||||
case MessageCase.Invite: {
|
||||
const RoomAvatar = sdk.getComponent("views.avatars.RoomAvatar");
|
||||
const avatar = <RoomAvatar room={this.props.room} />;
|
||||
|
||||
const inviteMember = this._getInviteMember();
|
||||
let avatar;
|
||||
let inviterElement;
|
||||
if (inviteMember) {
|
||||
const MemberAvatar = sdk.getComponent("views.avatars.MemberAvatar");
|
||||
avatar = (<MemberAvatar member={inviteMember} onClick={this._onInviterClick} />);
|
||||
const inviterClasses = [
|
||||
"mx_RoomPreviewBar_inviter",
|
||||
getUserNameColorClass(inviteMember.userId),
|
||||
].join(" ");
|
||||
inviterElement = (
|
||||
<a onClick={this._onInviterClick} className={inviterClasses}>
|
||||
{inviteMember.name}
|
||||
</a>
|
||||
);
|
||||
inviterElement = <span>
|
||||
<span className="mx_RoomPreviewBar_inviter">
|
||||
{inviteMember.rawDisplayName}
|
||||
</span> ({inviteMember.userId})
|
||||
</span>;
|
||||
} else {
|
||||
inviterElement = (<span className="mx_RoomPreviewBar_inviter">{this.props.inviterName}</span>);
|
||||
}
|
||||
|
||||
title = _t("Do you want to join this room?");
|
||||
const isDM = this._isDMInvite();
|
||||
if (isDM) {
|
||||
title = _t("Do you want to chat with %(user)s?",
|
||||
{ user: inviteMember.name });
|
||||
} else {
|
||||
title = _t("Do you want to join %(roomName)s?",
|
||||
{ roomName: this._roomName() });
|
||||
}
|
||||
subTitle = [
|
||||
avatar,
|
||||
_t("<userName/> invited you", {}, {userName: () => inviterElement}),
|
||||
|
@ -354,7 +370,8 @@ module.exports = React.createClass({
|
|||
}
|
||||
case MessageCase.ViewingRoom: {
|
||||
if (this.props.canPreview) {
|
||||
title = _t("You're previewing this room. Want to join it?");
|
||||
title = _t("You're previewing %(roomName)s. Want to join it?",
|
||||
{roomName: this._roomName()});
|
||||
} else {
|
||||
title = _t("%(roomName)s can't be previewed. Do you want to join it?",
|
||||
{roomName: this._roomName(true)});
|
||||
|
@ -372,7 +389,10 @@ module.exports = React.createClass({
|
|||
title = _t("%(roomName)s is not accessible at this time.", {roomName: this._roomName(true)});
|
||||
subTitle = [
|
||||
_t("Try again later, or ask a room admin to check if you have access."),
|
||||
_t("%(errcode)s was returned while trying to access the room. If you think you're seeing this message in error, please <issueLink>submit a bug report</issueLink>.",
|
||||
_t(
|
||||
"%(errcode)s was returned while trying to access the room. " +
|
||||
"If you think you're seeing this message in error, please " +
|
||||
"<issueLink>submit a bug report</issueLink>.",
|
||||
{ errcode: this.props.error.errcode },
|
||||
{ issueLink: label => <a href="https://github.com/vector-im/riot-web/issues/new/choose"
|
||||
target="_blank" rel="noopener">{ label }</a> },
|
||||
|
|
|
@ -119,7 +119,7 @@ export default class RoomRecoveryReminder extends React.PureComponent {
|
|||
|
||||
let setupCaption;
|
||||
if (this.state.backupInfo) {
|
||||
setupCaption = _t("Use Key Backup");
|
||||
setupCaption = _t("Connect this device to Key Backup");
|
||||
} else {
|
||||
setupCaption = _t("Start using Key Backup");
|
||||
}
|
||||
|
|
|
@ -507,6 +507,7 @@ module.exports = React.createClass({
|
|||
//'.m.rule.member_event': 'vector',
|
||||
'.m.rule.call': 'vector',
|
||||
'.m.rule.suppress_notices': 'vector',
|
||||
'.m.rule.tombstone': 'vector',
|
||||
|
||||
// Others go to others
|
||||
};
|
||||
|
@ -562,6 +563,7 @@ module.exports = React.createClass({
|
|||
//'im.vector.rule.member_event',
|
||||
'.m.rule.call',
|
||||
'.m.rule.suppress_notices',
|
||||
'.m.rule.tombstone',
|
||||
];
|
||||
for (const i in vectorRuleIds) {
|
||||
const vectorRuleId = vectorRuleIds[i];
|
||||
|
@ -702,6 +704,10 @@ module.exports = React.createClass({
|
|||
const rows = [];
|
||||
for (const i in this.state.vectorPushRules) {
|
||||
const rule = this.state.vectorPushRules[i];
|
||||
if (rule.rule === undefined && rule.vectorRuleId.startsWith(".m.")) {
|
||||
console.warn(`Skipping render of rule ${rule.vectorRuleId} due to no underlying rule`);
|
||||
continue;
|
||||
}
|
||||
//console.log("rendering: " + rule.description + ", " + rule.vectorRuleId + ", " + rule.vectorState);
|
||||
rows.push(this.renderNotifRulesTableRow(rule.description, rule.vectorRuleId, rule.vectorState));
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue