Merge branches 'develop' and 't3chguy/restore_composer_history' of github.com:matrix-org/matrix-react-sdk into t3chguy/restore_composer_history

# Conflicts:
#	src/components/views/rooms/MessageComposerInput.js
This commit is contained in:
Michael Telatynski 2019-06-14 12:26:05 +01:00
commit 876acc0f76
35 changed files with 362 additions and 253 deletions

View file

@ -50,7 +50,6 @@
@import "./views/context_menus/_TopLeftMenu.scss"; @import "./views/context_menus/_TopLeftMenu.scss";
@import "./views/dialogs/_AddressPickerDialog.scss"; @import "./views/dialogs/_AddressPickerDialog.scss";
@import "./views/dialogs/_Analytics.scss"; @import "./views/dialogs/_Analytics.scss";
@import "./views/dialogs/_BugReportDialog.scss";
@import "./views/dialogs/_ChangelogDialog.scss"; @import "./views/dialogs/_ChangelogDialog.scss";
@import "./views/dialogs/_ChatCreateOrReuseChatDialog.scss"; @import "./views/dialogs/_ChatCreateOrReuseChatDialog.scss";
@import "./views/dialogs/_ConfirmUserActionDialog.scss"; @import "./views/dialogs/_ConfirmUserActionDialog.scss";

View file

@ -72,7 +72,6 @@ limitations under the License.
} }
.mx_Field input { .mx_Field input {
width: 100%;
box-sizing: border-box; box-sizing: border-box;
} }
@ -110,7 +109,6 @@ limitations under the License.
.mx_AuthBody_fieldRow > .mx_Field { .mx_AuthBody_fieldRow > .mx_Field {
margin: 0 5px; margin: 0 5px;
flex: 1;
} }
.mx_AuthBody_fieldRow > .mx_Field:first-child { .mx_AuthBody_fieldRow > .mx_Field:first-child {

View file

@ -20,7 +20,6 @@ limitations under the License.
} }
.mx_ServerConfig_fields .mx_Field { .mx_ServerConfig_fields .mx_Field {
flex: 1;
margin: 0 5px; margin: 0 5px;
} }

View file

@ -1,25 +0,0 @@
/*
Copyright 2017 OpenMarket 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.
*/
.mx_BugReportDialog .mx_Field {
flex: 1;
}
.mx_BugReportDialog_field_input {
// TODO: We should really apply this to all .mx_Field inputs.
// See https://github.com/vector-im/riot-web/issues/9344.
flex: 1;
}

View file

@ -23,7 +23,11 @@ limitations under the License.
cursor: default !important; cursor: default !important;
} }
.mx_DevTools_RoomStateExplorer_button, .mx_DevTools_ServersInRoomList_button, .mx_DevTools_RoomStateExplorer_query { .mx_DevTools_RoomStateExplorer_query {
margin-bottom: 10px;
}
.mx_DevTools_RoomStateExplorer_button, .mx_DevTools_ServersInRoomList_button {
margin-bottom: 10px; margin-bottom: 10px;
width: 100%; width: 100%;
} }
@ -75,7 +79,6 @@ limitations under the License.
max-width: 684px; max-width: 684px;
min-height: 250px; min-height: 250px;
padding: 10px; padding: 10px;
width: 100%;
} }
.mx_DevTools_content .mx_Field_input { .mx_DevTools_content .mx_Field_input {

View file

@ -21,7 +21,6 @@ limitations under the License.
color: $primary-fg-color; color: $primary-fg-color;
background-color: $primary-bg-color; background-color: $primary-bg-color;
font-size: 15px; font-size: 15px;
width: 100%;
max-width: 280px; max-width: 280px;
margin-bottom: 10px; margin-bottom: 10px;
} }

View file

@ -42,12 +42,6 @@ limitations under the License.
margin-right: 5px; margin-right: 5px;
} }
.mx_EditableItemList_newItem .mx_Field input {
// Use 100% of the space available for the input, but don't let the 10px
// padding on either side of the input to push it out of alignment.
width: calc(100% - 20px);
}
.mx_EditableItemList_label { .mx_EditableItemList_label {
margin-bottom: 5px; margin-bottom: 5px;
} }

View file

@ -42,6 +42,7 @@ limitations under the License.
padding: 8px 9px; padding: 8px 9px;
color: $primary-fg-color; color: $primary-fg-color;
background-color: $primary-bg-color; background-color: $primary-bg-color;
flex: 1;
} }
.mx_Field select { .mx_Field select {

View file

@ -20,6 +20,5 @@ limitations under the License.
.mx_PowerSelector .mx_Field select, .mx_PowerSelector .mx_Field select,
.mx_PowerSelector .mx_Field input { .mx_PowerSelector .mx_Field input {
width: 100%;
box-sizing: border-box; box-sizing: border-box;
} }

View file

@ -43,6 +43,8 @@ limitations under the License.
.mx_MemberInfo_name h2 { .mx_MemberInfo_name h2 {
flex: 1; flex: 1;
overflow-x: auto;
max-height: 50px;
} }
.mx_MemberInfo h2 { .mx_MemberInfo h2 {

View file

@ -35,9 +35,3 @@ limitations under the License.
.mx_ExistingEmailAddress_confirmBtn { .mx_ExistingEmailAddress_confirmBtn {
margin-right: 5px; margin-right: 5px;
} }
.mx_EmailAddresses_new .mx_Field input {
// Use 100% of the space available for the input, but don't let the 10px
// padding on either side of the input to push it out of alignment.
width: calc(100% - 20px);
}

View file

@ -36,12 +36,6 @@ limitations under the License.
margin-right: 5px; margin-right: 5px;
} }
.mx_PhoneNumbers_new .mx_Field input {
// Use 100% of the space available for the input, but don't let the 10px
// padding on either side of the input to push it out of alignment.
width: calc(100% - 20px);
}
.mx_PhoneNumbers_input { .mx_PhoneNumbers_input {
display: flex; display: flex;
align-items: center; align-items: center;

View file

@ -22,11 +22,6 @@ limitations under the License.
flex-grow: 1; flex-grow: 1;
} }
.mx_ProfileSettings_controls .mx_Field #profileDisplayName,
.mx_ProfileSettings_controls .mx_Field #profileTopic {
width: calc(100% - 20px); // subtract 10px padding on left and right
}
.mx_ProfileSettings_controls .mx_Field #profileTopic { .mx_ProfileSettings_controls .mx_Field #profileTopic {
height: 4em; height: 4em;
} }

View file

@ -17,7 +17,3 @@ limitations under the License.
.mx_GeneralRoomSettingsTab_profileSection { .mx_GeneralRoomSettingsTab_profileSection {
margin-top: 10px; margin-top: 10px;
} }
.mx_GeneralRoomSettingsTab .mx_AliasSettings .mx_Field select {
width: 100%;
}

View file

@ -14,31 +14,15 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
.mx_GeneralUserSettingsTab_changePassword,
.mx_GeneralUserSettingsTab_themeSection {
display: block;
}
.mx_GeneralUserSettingsTab_changePassword .mx_Field, .mx_GeneralUserSettingsTab_changePassword .mx_Field,
.mx_GeneralUserSettingsTab_themeSection .mx_Field { .mx_GeneralUserSettingsTab_themeSection .mx_Field {
display: block;
margin-right: 100px; // Align with the other fields on the page margin-right: 100px; // Align with the other fields on the page
} }
.mx_GeneralUserSettingsTab_changePassword .mx_Field input {
display: block;
width: calc(100% - 20px); // subtract 10px padding on left and right
}
.mx_GeneralUserSettingsTab_changePassword .mx_Field:first-child { .mx_GeneralUserSettingsTab_changePassword .mx_Field:first-child {
margin-top: 0; margin-top: 0;
} }
.mx_GeneralUserSettingsTab_themeSection .mx_Field select {
display: block;
width: 100%;
}
.mx_GeneralUserSettingsTab_accountSection > .mx_EmailAddresses, .mx_GeneralUserSettingsTab_accountSection > .mx_EmailAddresses,
.mx_GeneralUserSettingsTab_accountSection > .mx_PhoneNumbers, .mx_GeneralUserSettingsTab_accountSection > .mx_PhoneNumbers,
.mx_GeneralUserSettingsTab_languageInput { .mx_GeneralUserSettingsTab_languageInput {

View file

@ -17,11 +17,3 @@ limitations under the License.
.mx_PreferencesUserSettingsTab .mx_Field { .mx_PreferencesUserSettingsTab .mx_Field {
margin-right: 100px; // Align with the rest of the controls margin-right: 100px; // Align with the rest of the controls
} }
.mx_PreferencesUserSettingsTab .mx_Field input {
display: block;
// Subtract 10px padding on left and right
// This is to keep the input aligned with the rest of the tab's controls.
width: calc(100% - 20px);
}

View file

@ -14,11 +14,6 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
.mx_VoiceUserSettingsTab .mx_Field select {
width: 100%;
max-width: 100%;
}
.mx_VoiceUserSettingsTab .mx_Field { .mx_VoiceUserSettingsTab .mx_Field {
margin-right: 100px; // align with the rest of the fields margin-right: 100px; // align with the rest of the fields
} }

View file

@ -517,7 +517,8 @@ module.exports = React.createClass({
const DateSeparator = sdk.getComponent('messages.DateSeparator'); const DateSeparator = sdk.getComponent('messages.DateSeparator');
const ret = []; const ret = [];
const isEditing = this.props.editEvent && this.props.editEvent.getId() === mxEv.getId(); const isEditing = this.props.editState &&
this.props.editState.getEvent().getId() === mxEv.getId();
// is this a continuation of the previous message? // is this a continuation of the previous message?
let continuation = false; let continuation = false;
@ -585,7 +586,7 @@ module.exports = React.createClass({
continuation={continuation} continuation={continuation}
isRedacted={mxEv.isRedacted()} isRedacted={mxEv.isRedacted()}
replacingEventId={mxEv.replacingEventId()} replacingEventId={mxEv.replacingEventId()}
isEditing={isEditing} editState={isEditing && this.props.editState}
onHeightChanged={this._onHeightChanged} onHeightChanged={this._onHeightChanged}
readReceipts={readReceipts} readReceipts={readReceipts}
readReceiptMap={this._readReceiptMap} readReceiptMap={this._readReceiptMap}

View file

@ -35,6 +35,7 @@ const Modal = require("../../Modal");
const UserActivity = require("../../UserActivity"); const UserActivity = require("../../UserActivity");
import { KeyCode } from '../../Keyboard'; import { KeyCode } from '../../Keyboard';
import Timer from '../../utils/Timer'; import Timer from '../../utils/Timer';
import EditorStateTransfer from '../../utils/EditorStateTransfer';
const PAGINATE_SIZE = 20; const PAGINATE_SIZE = 20;
const INITIAL_SIZE = 20; const INITIAL_SIZE = 20;
@ -411,7 +412,8 @@ const TimelinePanel = React.createClass({
this.forceUpdate(); this.forceUpdate();
} }
if (payload.action === "edit_event") { if (payload.action === "edit_event") {
this.setState({editEvent: payload.event}, () => { const editState = payload.event ? new EditorStateTransfer(payload.event) : null;
this.setState({editState}, () => {
if (payload.event && this.refs.messagePanel) { if (payload.event && this.refs.messagePanel) {
this.refs.messagePanel.scrollToEventIfNeeded( this.refs.messagePanel.scrollToEventIfNeeded(
payload.event.getId(), payload.event.getId(),
@ -1306,7 +1308,7 @@ const TimelinePanel = React.createClass({
tileShape={this.props.tileShape} tileShape={this.props.tileShape}
resizeNotifier={this.props.resizeNotifier} resizeNotifier={this.props.resizeNotifier}
getRelationsForEvent={this.getRelationsForEvent} getRelationsForEvent={this.getRelationsForEvent}
editEvent={this.state.editEvent} editState={this.state.editState}
showReactions={this.props.showReactions} showReactions={this.props.showReactions}
/> />
); );

View file

@ -28,6 +28,7 @@ import { messageForResourceLimitError } from '../../../utils/ErrorUtils';
import * as ServerType from '../../views/auth/ServerTypeSelector'; import * as ServerType from '../../views/auth/ServerTypeSelector';
import AutoDiscoveryUtils, {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils"; import AutoDiscoveryUtils, {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
import classNames from "classnames"; import classNames from "classnames";
import * as Lifecycle from '../../../Lifecycle';
// Phases // Phases
// Show controls to configure server details // Show controls to configure server details
@ -80,6 +81,9 @@ module.exports = React.createClass({
// Phase of the overall registration dialog. // Phase of the overall registration dialog.
phase: PHASE_REGISTRATION, phase: PHASE_REGISTRATION,
flows: null, flows: null,
// If set, we've registered but are not going to log
// the user in to their new account automatically.
completedNoSignin: false,
// We perform liveliness checks later, but for now suppress the errors. // We perform liveliness checks later, but for now suppress the errors.
// We also track the server dead errors independently of the regular errors so // We also track the server dead errors independently of the regular errors so
@ -209,6 +213,7 @@ module.exports = React.createClass({
errorText: _t("Registration has been disabled on this homeserver."), errorText: _t("Registration has been disabled on this homeserver."),
}); });
} else { } else {
console.log("Unable to query for supported registration methods.", e);
this.setState({ this.setState({
errorText: _t("Unable to query for supported registration methods."), errorText: _t("Unable to query for supported registration methods."),
}); });
@ -282,12 +287,10 @@ module.exports = React.createClass({
return; return;
} }
this.setState({ const newState = {
// we're still busy until we get unmounted: don't show the registration form again
busy: true,
doingUIAuth: false, doingUIAuth: false,
}); };
if (response.access_token) {
const cli = await this.props.onLoggedIn({ const cli = await this.props.onLoggedIn({
userId: response.user_id, userId: response.user_id,
deviceId: response.device_id, deviceId: response.device_id,
@ -297,6 +300,14 @@ module.exports = React.createClass({
}); });
this._setupPushers(cli); this._setupPushers(cli);
// we're still busy until we get unmounted: don't show the registration form again
newState.busy = true;
} else {
newState.busy = false;
newState.completedNoSignin = true;
}
this.setState(newState);
}, },
_setupPushers: function(matrixClient) { _setupPushers: function(matrixClient) {
@ -353,6 +364,12 @@ module.exports = React.createClass({
}, },
_makeRegisterRequest: function(auth) { _makeRegisterRequest: function(auth) {
// We inhibit login if we're trying to register with an email address: this
// avoids a lot of complex race conditions that can occur if we try to log
// the user in one one or both of the tabs they might end up with after
// clicking the email link.
let inhibitLogin = Boolean(this.state.formVals.email);
// Only send the bind params if we're sending username / pw params // Only send the bind params if we're sending username / pw params
// (Since we need to send no params at all to use the ones saved in the // (Since we need to send no params at all to use the ones saved in the
// session). // session).
@ -360,6 +377,8 @@ module.exports = React.createClass({
email: true, email: true,
msisdn: true, msisdn: true,
} : {}; } : {};
// Likewise inhibitLogin
if (!this.state.formVals.password) inhibitLogin = null;
return this.state.matrixClient.register( return this.state.matrixClient.register(
this.state.formVals.username, this.state.formVals.username,
@ -368,6 +387,7 @@ module.exports = React.createClass({
auth, auth,
bindThreepids, bindThreepids,
null, null,
inhibitLogin,
); );
}, },
@ -379,6 +399,19 @@ module.exports = React.createClass({
}; };
}, },
// Links to the login page shown after registration is completed are routed through this
// which checks the user hasn't already logged in somewhere else (perhaps we should do
// this more generally?)
_onLoginClickWithCheck: async function(ev) {
ev.preventDefault();
const sessionLoaded = await Lifecycle.loadSession({});
if (!sessionLoaded) {
// ok fine, there's still no session: really go to the login page
this.props.onLoginClick();
}
},
renderServerComponent() { renderServerComponent() {
const ServerTypeSelector = sdk.getComponent("auth.ServerTypeSelector"); const ServerTypeSelector = sdk.getComponent("auth.ServerTypeSelector");
const ServerConfig = sdk.getComponent("auth.ServerConfig"); const ServerConfig = sdk.getComponent("auth.ServerConfig");
@ -528,10 +561,34 @@ module.exports = React.createClass({
</a>; </a>;
} }
return ( let body;
<AuthPage> if (this.state.completedNoSignin) {
<AuthHeader /> let regDoneText;
<AuthBody> if (this.state.formVals.password) {
// We're the client that started the registration
regDoneText = _t(
"<a>Log in</a> to your new account.", {},
{
a: (sub) => <a href="#/login" onClick={this._onLoginClickWithCheck}>{sub}</a>,
},
);
} else {
// We're not the original client: the user probably got to us by clicking the
// email validation link. We can't offer a 'go straight to your account' link
// as we don't have the original creds.
regDoneText = _t(
"You can now close this window or <a>log in</a> to your new account.", {},
{
a: (sub) => <a href="#/login" onClick={this._onLoginClickWithCheck}>{sub}</a>,
},
);
}
body = <div>
<h2>{_t("Registration Successful")}</h2>
<h3>{ regDoneText }</h3>
</div>;
} else {
body = <div>
<h2>{ _t('Create your account') }</h2> <h2>{ _t('Create your account') }</h2>
{ errorText } { errorText }
{ serverDeadSection } { serverDeadSection }
@ -539,6 +596,14 @@ module.exports = React.createClass({
{ this.renderRegisterComponent() } { this.renderRegisterComponent() }
{ goBack } { goBack }
{ signIn } { signIn }
</div>;
}
return (
<AuthPage>
<AuthHeader />
<AuthBody>
{ body }
</AuthBody> </AuthBody>
</AuthPage> </AuthPage>
); );

View file

@ -101,6 +101,14 @@ export default class ServerConfig extends React.PureComponent {
return result; return result;
} catch (e) { } catch (e) {
console.error(e); console.error(e);
const stateForError = AutoDiscoveryUtils.authComponentStateForError(e);
if (!stateForError.isFatalError) {
// carry on anyway
const result = await AutoDiscoveryUtils.validateServerConfigWithStaticUrls(hsUrl, isUrl, true);
this.props.onServerConfigChange(result);
return result;
} else {
let message = _t("Unable to validate homeserver/identity server"); let message = _t("Unable to validate homeserver/identity server");
if (e.translatedMessage) { if (e.translatedMessage) {
message = e.translatedMessage; message = e.translatedMessage;
@ -113,6 +121,7 @@ export default class ServerConfig extends React.PureComponent {
return null; return null;
} }
} }
}
onHomeserverBlur = (ev) => { onHomeserverBlur = (ev) => {
this._hsTimeoutId = this._waitThenInvoke(this._hsTimeoutId, () => { this._hsTimeoutId = this._waitThenInvoke(this._hsTimeoutId, () => {

View file

@ -28,13 +28,14 @@ import {parseEvent} from '../../../editor/deserialize';
import Autocomplete from '../rooms/Autocomplete'; import Autocomplete from '../rooms/Autocomplete';
import {PartCreator} from '../../../editor/parts'; import {PartCreator} from '../../../editor/parts';
import {renderModel} from '../../../editor/render'; import {renderModel} from '../../../editor/render';
import {MatrixEvent, MatrixClient} from 'matrix-js-sdk'; import EditorStateTransfer from '../../../utils/EditorStateTransfer';
import {MatrixClient} from 'matrix-js-sdk';
import classNames from 'classnames'; import classNames from 'classnames';
export default class MessageEditor extends React.Component { export default class MessageEditor extends React.Component {
static propTypes = { static propTypes = {
// the message event being edited // the message event being edited
event: PropTypes.instanceOf(MatrixEvent).isRequired, editState: PropTypes.instanceOf(EditorStateTransfer).isRequired,
}; };
static contextTypes = { static contextTypes = {
@ -44,16 +45,7 @@ export default class MessageEditor extends React.Component {
constructor(props, context) { constructor(props, context) {
super(props, context); super(props, context);
const room = this._getRoom(); const room = this._getRoom();
const partCreator = new PartCreator( this.model = null;
() => this._autocompleteRef,
query => this.setState({query}),
room,
);
this.model = new EditorModel(
parseEvent(this.props.event, room),
partCreator,
this._updateEditorState,
);
this.state = { this.state = {
autoComplete: null, autoComplete: null,
room, room,
@ -64,7 +56,7 @@ export default class MessageEditor extends React.Component {
} }
_getRoom() { _getRoom() {
return this.context.matrixClient.getRoom(this.props.event.getRoomId()); return this.context.matrixClient.getRoom(this.props.editState.getEvent().getRoomId());
} }
_updateEditorState = (caret) => { _updateEditorState = (caret) => {
@ -133,7 +125,7 @@ export default class MessageEditor extends React.Component {
if (this._hasModifications || !this._isCaretAtStart()) { if (this._hasModifications || !this._isCaretAtStart()) {
return; return;
} }
const previousEvent = findEditableEvent(this._getRoom(), false, this.props.event.getId()); const previousEvent = findEditableEvent(this._getRoom(), false, this.props.editState.getEvent().getId());
if (previousEvent) { if (previousEvent) {
dis.dispatch({action: 'edit_event', event: previousEvent}); dis.dispatch({action: 'edit_event', event: previousEvent});
event.preventDefault(); event.preventDefault();
@ -142,7 +134,7 @@ export default class MessageEditor extends React.Component {
if (this._hasModifications || !this._isCaretAtEnd()) { if (this._hasModifications || !this._isCaretAtEnd()) {
return; return;
} }
const nextEvent = findEditableEvent(this._getRoom(), true, this.props.event.getId()); const nextEvent = findEditableEvent(this._getRoom(), true, this.props.editState.getEvent().getId());
if (nextEvent) { if (nextEvent) {
dis.dispatch({action: 'edit_event', event: nextEvent}); dis.dispatch({action: 'edit_event', event: nextEvent});
} else { } else {
@ -178,11 +170,11 @@ export default class MessageEditor extends React.Component {
"m.new_content": newContent, "m.new_content": newContent,
"m.relates_to": { "m.relates_to": {
"rel_type": "m.replace", "rel_type": "m.replace",
"event_id": this.props.event.getId(), "event_id": this.props.editState.getEvent().getId(),
}, },
}, contentBody); }, contentBody);
const roomId = this.props.event.getRoomId(); const roomId = this.props.editState.getEvent().getRoomId();
this.context.matrixClient.sendMessage(roomId, content); this.context.matrixClient.sendMessage(roomId, content);
dis.dispatch({action: "edit_event", event: null}); dis.dispatch({action: "edit_event", event: null});
@ -197,12 +189,63 @@ export default class MessageEditor extends React.Component {
this.model.autoComplete.onComponentSelectionChange(completion); this.model.autoComplete.onComponentSelectionChange(completion);
} }
componentWillUnmount() {
const sel = document.getSelection();
const {caret} = getCaretOffsetAndText(this._editorRef, sel);
const parts = this.model.serializeParts();
this.props.editState.setEditorState(caret, parts);
}
componentDidMount() { componentDidMount() {
this.model = this._createEditorModel();
// initial render of model
this._updateEditorState(); this._updateEditorState();
setCaretPosition(this._editorRef, this.model, this.model.getPositionAtEnd()); // initial caret position
this._initializeCaret();
this._editorRef.focus(); this._editorRef.focus();
} }
_createEditorModel() {
const {editState} = this.props;
const room = this._getRoom();
const partCreator = new PartCreator(
() => this._autocompleteRef,
query => this.setState({query}),
room,
this.context.matrixClient,
);
let parts;
if (editState.hasEditorState()) {
// if restoring state from a previous editor,
// restore serialized parts from the state
parts = editState.getSerializedParts().map(p => partCreator.deserializePart(p));
} else {
// otherwise, parse the body of the event
parts = parseEvent(editState.getEvent(), room, this.context.matrixClient);
}
return new EditorModel(
parts,
partCreator,
this._updateEditorState,
);
}
_initializeCaret() {
const {editState} = this.props;
let caretPosition;
if (editState.hasEditorState()) {
// if restoring state from a previous editor,
// restore caret position from the state
const caret = editState.getCaret();
caretPosition = this.model.positionForOffset(caret.offset, caret.atNodeEnd);
} else {
// otherwise, set it at the end
caretPosition = this.model.getPositionAtEnd();
}
setCaretPosition(this._editorRef, this.model, caretPosition);
}
render() { render() {
let autoComplete; let autoComplete;
if (this.state.autoComplete) { if (this.state.autoComplete) {

View file

@ -90,7 +90,7 @@ module.exports = React.createClass({
tileShape={this.props.tileShape} tileShape={this.props.tileShape}
maxImageHeight={this.props.maxImageHeight} maxImageHeight={this.props.maxImageHeight}
replacingEventId={this.props.replacingEventId} replacingEventId={this.props.replacingEventId}
isEditing={this.props.isEditing} editState={this.props.editState}
onHeightChanged={this.props.onHeightChanged} />; onHeightChanged={this.props.onHeightChanged} />;
}, },
}); });

View file

@ -90,7 +90,7 @@ module.exports = React.createClass({
componentDidMount: function() { componentDidMount: function() {
this._unmounted = false; this._unmounted = false;
if (!this.props.isEditing) { if (!this.props.editState) {
this._applyFormatting(); this._applyFormatting();
} }
}, },
@ -131,8 +131,8 @@ module.exports = React.createClass({
}, },
componentDidUpdate: function(prevProps) { componentDidUpdate: function(prevProps) {
if (!this.props.isEditing) { if (!this.props.editState) {
const stoppedEditing = prevProps.isEditing && !this.props.isEditing; const stoppedEditing = prevProps.editState && !this.props.editState;
const messageWasEdited = prevProps.replacingEventId !== this.props.replacingEventId; const messageWasEdited = prevProps.replacingEventId !== this.props.replacingEventId;
if (messageWasEdited || stoppedEditing) { if (messageWasEdited || stoppedEditing) {
this._applyFormatting(); this._applyFormatting();
@ -153,7 +153,7 @@ module.exports = React.createClass({
nextProps.replacingEventId !== this.props.replacingEventId || nextProps.replacingEventId !== this.props.replacingEventId ||
nextProps.highlightLink !== this.props.highlightLink || nextProps.highlightLink !== this.props.highlightLink ||
nextProps.showUrlPreview !== this.props.showUrlPreview || nextProps.showUrlPreview !== this.props.showUrlPreview ||
nextProps.isEditing !== this.props.isEditing || nextProps.editState !== this.props.editState ||
nextState.links !== this.state.links || nextState.links !== this.state.links ||
nextState.editedMarkerHovered !== this.state.editedMarkerHovered || nextState.editedMarkerHovered !== this.state.editedMarkerHovered ||
nextState.widgetHidden !== this.state.widgetHidden); nextState.widgetHidden !== this.state.widgetHidden);
@ -469,9 +469,9 @@ module.exports = React.createClass({
}, },
render: function() { render: function() {
if (this.props.isEditing) { if (this.props.editState) {
const MessageEditor = sdk.getComponent('elements.MessageEditor'); const MessageEditor = sdk.getComponent('elements.MessageEditor');
return <MessageEditor event={this.props.mxEvent} className="mx_EventTile_content" />; return <MessageEditor editState={this.props.editState} className="mx_EventTile_content" />;
} }
const mxEvent = this.props.mxEvent; const mxEvent = this.props.mxEvent;
const content = mxEvent.getContent(); const content = mxEvent.getContent();

View file

@ -171,26 +171,13 @@ export default class Autocomplete extends React.Component {
} }
// called from MessageComposerInput // called from MessageComposerInput
onUpArrow(): ?Completion { moveSelection(delta): ?Completion {
const completionCount = this.countCompletions(); const completionCount = this.countCompletions();
// completionCount + 1, since 0 means composer is selected if (completionCount === 0) return; // there are no items to move the selection through
const selectionOffset = (completionCount + 1 + this.state.selectionOffset - 1)
% (completionCount + 1);
if (!completionCount) {
return null;
}
this.setSelection(selectionOffset);
}
// called from MessageComposerInput // Note: selectionOffset 0 represents the unsubstituted text, while 1 means first pill selected
onDownArrow(): ?Completion { const index = (this.state.selectionOffset + delta + completionCount + 1) % (completionCount + 1);
const completionCount = this.countCompletions(); this.setSelection(index);
// completionCount + 1, since 0 means composer is selected
const selectionOffset = (this.state.selectionOffset + 1) % (completionCount + 1);
if (!completionCount) {
return null;
}
this.setSelection(selectionOffset);
} }
onEscape(e): boolean { onEscape(e): boolean {

View file

@ -552,13 +552,14 @@ module.exports = withMatrixClient(React.createClass({
const isRedacted = isMessageEvent(this.props.mxEvent) && this.props.isRedacted; const isRedacted = isMessageEvent(this.props.mxEvent) && this.props.isRedacted;
const isEncryptionFailure = this.props.mxEvent.isDecryptionFailure(); const isEncryptionFailure = this.props.mxEvent.isDecryptionFailure();
const isEditing = !!this.props.editState;
const classes = classNames({ const classes = classNames({
mx_EventTile: true, mx_EventTile: true,
mx_EventTile_isEditing: this.props.isEditing, mx_EventTile_isEditing: isEditing,
mx_EventTile_info: isInfoMessage, mx_EventTile_info: isInfoMessage,
mx_EventTile_12hr: this.props.isTwelveHour, mx_EventTile_12hr: this.props.isTwelveHour,
mx_EventTile_encrypting: this.props.eventSendStatus === 'encrypting', mx_EventTile_encrypting: this.props.eventSendStatus === 'encrypting',
mx_EventTile_sending: isSending, mx_EventTile_sending: !isEditing && isSending,
mx_EventTile_notSent: this.props.eventSendStatus === 'not_sent', mx_EventTile_notSent: this.props.eventSendStatus === 'not_sent',
mx_EventTile_highlight: this.props.tileShape === 'notif' ? false : this.shouldHighlight(), mx_EventTile_highlight: this.props.tileShape === 'notif' ? false : this.shouldHighlight(),
mx_EventTile_selected: this.props.isSelectedEvent, mx_EventTile_selected: this.props.isSelectedEvent,
@ -632,7 +633,7 @@ module.exports = withMatrixClient(React.createClass({
} }
const MessageActionBar = sdk.getComponent('messages.MessageActionBar'); const MessageActionBar = sdk.getComponent('messages.MessageActionBar');
const actionBar = !this.props.isEditing ? <MessageActionBar const actionBar = !isEditing ? <MessageActionBar
mxEvent={this.props.mxEvent} mxEvent={this.props.mxEvent}
reactions={this.state.reactions} reactions={this.state.reactions}
permalinkCreator={this.props.permalinkCreator} permalinkCreator={this.props.permalinkCreator}
@ -794,7 +795,7 @@ module.exports = withMatrixClient(React.createClass({
<EventTileType ref="tile" <EventTileType ref="tile"
mxEvent={this.props.mxEvent} mxEvent={this.props.mxEvent}
replacingEventId={this.props.replacingEventId} replacingEventId={this.props.replacingEventId}
isEditing={this.props.isEditing} editState={this.props.editState}
highlights={this.props.highlights} highlights={this.props.highlights}
highlightLink={this.props.highlightLink} highlightLink={this.props.highlightLink}
showUrlPreview={this.props.showUrlPreview} showUrlPreview={this.props.showUrlPreview}

View file

@ -676,6 +676,31 @@ export default class MessageComposerInput extends React.Component {
onKeyDown = (ev: KeyboardEvent, change: Change, editor: Editor) => { onKeyDown = (ev: KeyboardEvent, change: Change, editor: Editor) => {
this.suppressAutoComplete = false; this.suppressAutoComplete = false;
this.direction = '';
// Navigate autocomplete list with arrow keys
if (this.autocomplete.countCompletions() > 0) {
if (!(ev.ctrlKey || ev.shiftKey || ev.altKey || ev.metaKey)) {
switch (ev.keyCode) {
case KeyCode.LEFT:
this.autocomplete.moveSelection(-1);
ev.preventDefault();
return true;
case KeyCode.RIGHT:
this.autocomplete.moveSelection(+1);
ev.preventDefault();
return true;
case KeyCode.UP:
this.autocomplete.moveSelection(-1);
ev.preventDefault();
return true;
case KeyCode.DOWN:
this.autocomplete.moveSelection(+1);
ev.preventDefault();
return true;
}
}
}
// skip void nodes - see // skip void nodes - see
// https://github.com/ianstormtaylor/slate/issues/762#issuecomment-304855095 // https://github.com/ianstormtaylor/slate/issues/762#issuecomment-304855095
@ -683,8 +708,6 @@ export default class MessageComposerInput extends React.Component {
this.direction = 'Previous'; this.direction = 'Previous';
} else if (ev.keyCode === KeyCode.RIGHT) { } else if (ev.keyCode === KeyCode.RIGHT) {
this.direction = 'Next'; this.direction = 'Next';
} else {
this.direction = '';
} }
switch (ev.keyCode) { switch (ev.keyCode) {
@ -1181,12 +1204,9 @@ export default class MessageComposerInput extends React.Component {
}; };
onVerticalArrow = (e, up) => { onVerticalArrow = (e, up) => {
if (e.ctrlKey || e.altKey || e.metaKey) { if (e.ctrlKey || e.shiftKey || e.metaKey) return;
return;
}
// Select history only if we are not currently auto-completing // Select history
if (this.autocomplete.state.completionList.length === 0) {
const selection = this.state.editorState.selection; const selection = this.state.editorState.selection;
// selection must be collapsed // selection must be collapsed
@ -1197,7 +1217,7 @@ export default class MessageComposerInput extends React.Component {
if (up) { if (up) {
if (!selection.anchor.isAtStartOfNode(document)) return; if (!selection.anchor.isAtStartOfNode(document)) return;
if (!e.shiftKey) { if (!e.altKey) {
const editEvent = findEditableEvent(this.props.room, false); const editEvent = findEditableEvent(this.props.room, false);
if (editEvent) { if (editEvent) {
// We're selecting history, so prevent the key event from doing anything else // We're selecting history, so prevent the key event from doing anything else
@ -1209,8 +1229,6 @@ export default class MessageComposerInput extends React.Component {
} }
return; return;
} }
} else {
if (!selection.anchor.isAtEndOfNode(document)) return;
} }
const selected = this.selectHistory(up); const selected = this.selectHistory(up);
@ -1218,10 +1236,6 @@ export default class MessageComposerInput extends React.Component {
// We're selecting history, so prevent the key event from doing anything else // We're selecting history, so prevent the key event from doing anything else
e.preventDefault(); e.preventDefault();
} }
} else {
this.moveAutocompleteSelection(up);
e.preventDefault();
}
}; };
selectHistory = async (up) => { selectHistory = async (up) => {
@ -1277,23 +1291,19 @@ export default class MessageComposerInput extends React.Component {
someCompletions: null, someCompletions: null,
}); });
e.preventDefault(); e.preventDefault();
if (this.autocomplete.state.completionList.length === 0) { if (this.autocomplete.countCompletions() === 0) {
// Force completions to show for the text currently entered // Force completions to show for the text currently entered
const completionCount = await this.autocomplete.forceComplete(); const completionCount = await this.autocomplete.forceComplete();
this.setState({ this.setState({
someCompletions: completionCount > 0, someCompletions: completionCount > 0,
}); });
// Select the first item by moving "down" // Select the first item by moving "down"
await this.moveAutocompleteSelection(false); await this.autocomplete.moveSelection(+1);
} else { } else {
await this.moveAutocompleteSelection(e.shiftKey); await this.autocomplete.moveSelection(e.shiftKey ? -1 : +1);
} }
}; };
moveAutocompleteSelection = (up) => {
up ? this.autocomplete.onUpArrow() : this.autocomplete.onDownArrow();
};
onEscape = async (e) => { onEscape = async (e) => {
e.preventDefault(); e.preventDefault();
if (this.autocomplete) { if (this.autocomplete) {

View file

@ -18,12 +18,13 @@ limitations under the License.
import {UserPillPart, RoomPillPart, PlainPart} from "./parts"; import {UserPillPart, RoomPillPart, PlainPart} from "./parts";
export default class AutocompleteWrapperModel { export default class AutocompleteWrapperModel {
constructor(updateCallback, getAutocompleterComponent, updateQuery, room) { constructor(updateCallback, getAutocompleterComponent, updateQuery, room, client) {
this._updateCallback = updateCallback; this._updateCallback = updateCallback;
this._getAutocompleterComponent = getAutocompleterComponent; this._getAutocompleterComponent = getAutocompleterComponent;
this._updateQuery = updateQuery; this._updateQuery = updateQuery;
this._query = null; this._query = null;
this._room = room; this._room = room;
this._client = client;
} }
onEscape(e) { onEscape(e) {
@ -42,17 +43,13 @@ export default class AutocompleteWrapperModel {
async onTab(e) { async onTab(e) {
const acComponent = this._getAutocompleterComponent(); const acComponent = this._getAutocompleterComponent();
if (acComponent.state.completionList.length === 0) { if (acComponent.countCompletions() === 0) {
// Force completions to show for the text currently entered // Force completions to show for the text currently entered
await acComponent.forceComplete(); await acComponent.forceComplete();
// Select the first item by moving "down" // Select the first item by moving "down"
await acComponent.onDownArrow(); await acComponent.moveSelection(+1);
} else { } else {
if (e.shiftKey) { await acComponent.moveSelection(e.shiftKey ? -1 : +1);
await acComponent.onUpArrow();
} else {
await acComponent.onDownArrow();
}
} }
this._updateCallback({ this._updateCallback({
close: true, close: true,
@ -60,11 +57,11 @@ export default class AutocompleteWrapperModel {
} }
onUpArrow() { onUpArrow() {
this._getAutocompleterComponent().onUpArrow(); this._getAutocompleterComponent().moveSelection(-1);
} }
onDownArrow() { onDownArrow() {
this._getAutocompleterComponent().onDownArrow(); this._getAutocompleterComponent().moveSelection(+1);
} }
onPartUpdate(part, offset) { onPartUpdate(part, offset) {
@ -106,7 +103,7 @@ export default class AutocompleteWrapperModel {
} }
case "#": { case "#": {
const displayAlias = completion.completionId; const displayAlias = completion.completionId;
return new RoomPillPart(displayAlias); return new RoomPillPart(displayAlias, this._client);
} }
// also used for emoji completion // also used for emoji completion
default: default:

View file

@ -21,7 +21,7 @@ import { walkDOMDepthFirst } from "./dom";
const REGEX_MATRIXTO = new RegExp(MATRIXTO_URL_PATTERN); const REGEX_MATRIXTO = new RegExp(MATRIXTO_URL_PATTERN);
function parseLink(a, room) { function parseLink(a, room, client) {
const {href} = a; const {href} = a;
const pillMatch = REGEX_MATRIXTO.exec(href) || []; const pillMatch = REGEX_MATRIXTO.exec(href) || [];
const resourceId = pillMatch[1]; // The room/user ID const resourceId = pillMatch[1]; // The room/user ID
@ -34,7 +34,7 @@ function parseLink(a, room) {
room.getMember(resourceId), room.getMember(resourceId),
); );
case "#": case "#":
return new RoomPillPart(resourceId); return new RoomPillPart(resourceId, client);
default: { default: {
if (href === a.textContent) { if (href === a.textContent) {
return new PlainPart(a.textContent); return new PlainPart(a.textContent);
@ -57,10 +57,10 @@ function parseCodeBlock(n) {
return parts; return parts;
} }
function parseElement(n, room) { function parseElement(n, room, client) {
switch (n.nodeName) { switch (n.nodeName) {
case "A": case "A":
return parseLink(n, room); return parseLink(n, room, client);
case "BR": case "BR":
return new NewlinePart("\n"); return new NewlinePart("\n");
case "EM": case "EM":
@ -140,7 +140,7 @@ function prefixQuoteLines(isFirstNode, parts) {
} }
} }
function parseHtmlMessage(html, room) { function parseHtmlMessage(html, room, client) {
// no nodes from parsing here should be inserted in the document, // no nodes from parsing here should be inserted in the document,
// as scripts in event handlers, etc would be executed then. // as scripts in event handlers, etc would be executed then.
// we're only taking text, so that is fine // we're only taking text, so that is fine
@ -165,7 +165,7 @@ function parseHtmlMessage(html, room) {
if (n.nodeType === Node.TEXT_NODE) { if (n.nodeType === Node.TEXT_NODE) {
newParts.push(new PlainPart(n.nodeValue)); newParts.push(new PlainPart(n.nodeValue));
} else if (n.nodeType === Node.ELEMENT_NODE) { } else if (n.nodeType === Node.ELEMENT_NODE) {
const parseResult = parseElement(n, room); const parseResult = parseElement(n, room, client);
if (parseResult) { if (parseResult) {
if (Array.isArray(parseResult)) { if (Array.isArray(parseResult)) {
newParts.push(...parseResult); newParts.push(...parseResult);
@ -205,10 +205,10 @@ function parseHtmlMessage(html, room) {
return parts; return parts;
} }
export function parseEvent(event, room) { export function parseEvent(event, room, client) {
const content = event.getContent(); const content = event.getContent();
if (content.format === "org.matrix.custom.html") { if (content.format === "org.matrix.custom.html") {
return parseHtmlMessage(content.formatted_body || "", room); return parseHtmlMessage(content.formatted_body || "", room, client);
} else { } else {
const body = content.body || ""; const body = content.body || "";
const lines = body.split("\n"); const lines = body.split("\n");

View file

@ -73,7 +73,7 @@ export default class EditorModel {
} }
serializeParts() { serializeParts() {
return this._parts.map(({type, text}) => {return {type, text};}); return this._parts.map(p => p.serialize());
} }
_diff(newValue, inputType, caret) { _diff(newValue, inputType, caret) {
@ -88,7 +88,7 @@ export default class EditorModel {
update(newValue, inputType, caret) { update(newValue, inputType, caret) {
const diff = this._diff(newValue, inputType, caret); const diff = this._diff(newValue, inputType, caret);
const position = this._positionForOffset(diff.at, caret.atNodeEnd); const position = this.positionForOffset(diff.at, caret.atNodeEnd);
let removedOffsetDecrease = 0; let removedOffsetDecrease = 0;
if (diff.removed) { if (diff.removed) {
removedOffsetDecrease = this._removeText(position, diff.removed.length); removedOffsetDecrease = this._removeText(position, diff.removed.length);
@ -99,7 +99,7 @@ export default class EditorModel {
} }
this._mergeAdjacentParts(); this._mergeAdjacentParts();
const caretOffset = diff.at - removedOffsetDecrease + addedLen; const caretOffset = diff.at - removedOffsetDecrease + addedLen;
let newPosition = this._positionForOffset(caretOffset, true); let newPosition = this.positionForOffset(caretOffset, true);
newPosition = newPosition.skipUneditableParts(this._parts); newPosition = newPosition.skipUneditableParts(this._parts);
this._setActivePart(newPosition); this._setActivePart(newPosition);
this._updateCallback(newPosition); this._updateCallback(newPosition);
@ -248,7 +248,7 @@ export default class EditorModel {
return addLen; return addLen;
} }
_positionForOffset(totalOffset, atPartEnd) { positionForOffset(totalOffset, atPartEnd) {
let currentOffset = 0; let currentOffset = 0;
const index = this._parts.findIndex(part => { const index = this._parts.findIndex(part => {
const partLen = part.text.length; const partLen = part.text.length;

View file

@ -17,7 +17,6 @@ limitations under the License.
import AutocompleteWrapperModel from "./autocomplete"; import AutocompleteWrapperModel from "./autocomplete";
import Avatar from "../Avatar"; import Avatar from "../Avatar";
import MatrixClientPeg from "../MatrixClientPeg";
class BasePart { class BasePart {
constructor(text = "") { constructor(text = "") {
@ -102,6 +101,10 @@ class BasePart {
toString() { toString() {
return `${this.type}(${this.text})`; return `${this.type}(${this.text})`;
} }
serialize() {
return {type: this.type, text: this.text};
}
} }
export class PlainPart extends BasePart { export class PlainPart extends BasePart {
@ -233,13 +236,12 @@ export class NewlinePart extends BasePart {
} }
export class RoomPillPart extends PillPart { export class RoomPillPart extends PillPart {
constructor(displayAlias) { constructor(displayAlias, client) {
super(displayAlias, displayAlias); super(displayAlias, displayAlias);
this._room = this._findRoomByAlias(displayAlias); this._room = this._findRoomByAlias(displayAlias, client);
} }
_findRoomByAlias(alias) { _findRoomByAlias(alias, client) {
const client = MatrixClientPeg.get();
if (alias[0] === '#') { if (alias[0] === '#') {
return client.getRooms().find((r) => { return client.getRooms().find((r) => {
return r.getAliases().includes(alias); return r.getAliases().includes(alias);
@ -300,6 +302,12 @@ export class UserPillPart extends PillPart {
get className() { get className() {
return "mx_UserPill mx_Pill"; return "mx_UserPill mx_Pill";
} }
serialize() {
const obj = super.serialize();
obj.userId = this.resourceId;
return obj;
}
} }
@ -335,13 +343,16 @@ export class PillCandidatePart extends PlainPart {
} }
export class PartCreator { export class PartCreator {
constructor(getAutocompleterComponent, updateQuery, room) { constructor(getAutocompleterComponent, updateQuery, room, client) {
this._room = room;
this._client = client;
this._autoCompleteCreator = (updateCallback) => { this._autoCompleteCreator = (updateCallback) => {
return new AutocompleteWrapperModel( return new AutocompleteWrapperModel(
updateCallback, updateCallback,
getAutocompleterComponent, getAutocompleterComponent,
updateQuery, updateQuery,
room, room,
client,
); );
}; };
} }
@ -362,5 +373,22 @@ export class PartCreator {
createDefaultPart(text) { createDefaultPart(text) {
return new PlainPart(text); return new PlainPart(text);
} }
deserializePart(part) {
switch (part.type) {
case "plain":
return new PlainPart(part.text);
case "newline":
return new NewlinePart(part.text);
case "pill-candidate":
return new PillCandidatePart(part.text, this._autoCompleteCreator);
case "room-pill":
return new RoomPillPart(part.text, this._client);
case "user-pill": {
const member = this._room.getMember(part.userId);
return new UserPillPart(part.userId, part.text, member);
}
}
}
} }

View file

@ -1557,6 +1557,9 @@
"Registration has been disabled on this homeserver.": "Registration has been disabled on this homeserver.", "Registration has been disabled on this homeserver.": "Registration has been disabled on this homeserver.",
"Unable to query for supported registration methods.": "Unable to query for supported registration methods.", "Unable to query for supported registration methods.": "Unable to query for supported registration methods.",
"This server does not support authentication with a phone number.": "This server does not support authentication with a phone number.", "This server does not support authentication with a phone number.": "This server does not support authentication with a phone number.",
"<a>Log in</a> to your new account.": "<a>Log in</a> to your new account.",
"You can now close this window or <a>log in</a> to your new account.": "You can now close this window or <a>log in</a> to your new account.",
"Registration Successful": "Registration Successful",
"Create your account": "Create your account", "Create your account": "Create your account",
"Commands": "Commands", "Commands": "Commands",
"Results from DuckDuckGo": "Results from DuckDuckGo", "Results from DuckDuckGo": "Results from DuckDuckGo",

View file

@ -0,0 +1,49 @@
/*
Copyright 2019 The Matrix.org Foundation C.I.C.
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.
*/
/**
* Used while editing, to pass the event, and to preserve editor state
* from one editor instance to another when remounting the editor
* upon receiving the remote echo for an unsent event.
*/
export default class EditorStateTransfer {
constructor(event) {
this._event = event;
this._serializedParts = null;
this.caret = null;
}
setEditorState(caret, serializedParts) {
this._caret = caret;
this._serializedParts = serializedParts;
}
hasEditorState() {
return !!this._serializedParts;
}
getSerializedParts() {
return this._serializedParts;
}
getCaret() {
return this._caret;
}
getEvent() {
return this._event;
}
}

View file

@ -46,7 +46,8 @@ export function isContentActionable(mxEvent) {
} }
export function canEditContent(mxEvent) { export function canEditContent(mxEvent) {
return isContentActionable(mxEvent) && return mxEvent.status !== EventStatus.CANCELLED &&
mxEvent.getType() === 'm.room.message' &&
mxEvent.getOriginalContent().msgtype === "m.text" && mxEvent.getOriginalContent().msgtype === "m.text" &&
mxEvent.getSender() === MatrixClientPeg.get().getUserId(); mxEvent.getSender() === MatrixClientPeg.get().getUserId();
} }
@ -64,7 +65,7 @@ export function canEditOwnEvent(mxEvent) {
const MAX_JUMP_DISTANCE = 100; const MAX_JUMP_DISTANCE = 100;
export function findEditableEvent(room, isForward, fromEventId = undefined) { export function findEditableEvent(room, isForward, fromEventId = undefined) {
const liveTimeline = room.getLiveTimeline(); const liveTimeline = room.getLiveTimeline();
const events = liveTimeline.getEvents(); const events = liveTimeline.getEvents().concat(room.getPendingEvents());
const maxIdx = events.length - 1; const maxIdx = events.length - 1;
const inc = isForward ? 1 : -1; const inc = isForward ? 1 : -1;
const beginIdx = isForward ? 0 : maxIdx; const beginIdx = isForward ? 0 : maxIdx;

View file

@ -103,12 +103,6 @@ describe('InteractiveAuthDialog', function() {
password: "s3kr3t", password: "s3kr3t",
user: "@user:id", user: "@user:id",
})).toBe(true); })).toBe(true);
// there should now be a spinner
ReactTestUtils.findRenderedComponentWithType(
dlg, sdk.getComponent('elements.Spinner'),
);
// let the request complete // let the request complete
return Promise.delay(1); return Promise.delay(1);
}).then(() => { }).then(() => {