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

@ -101,16 +101,25 @@ export default class ServerConfig extends React.PureComponent {
return result;
} catch (e) {
console.error(e);
let message = _t("Unable to validate homeserver/identity server");
if (e.translatedMessage) {
message = e.translatedMessage;
}
this.setState({
busy: false,
errorText: message,
});
return null;
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");
if (e.translatedMessage) {
message = e.translatedMessage;
}
this.setState({
busy: false,
errorText: message,
});
return null;
}
}
}

View file

@ -28,13 +28,14 @@ import {parseEvent} from '../../../editor/deserialize';
import Autocomplete from '../rooms/Autocomplete';
import {PartCreator} from '../../../editor/parts';
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';
export default class MessageEditor extends React.Component {
static propTypes = {
// the message event being edited
event: PropTypes.instanceOf(MatrixEvent).isRequired,
editState: PropTypes.instanceOf(EditorStateTransfer).isRequired,
};
static contextTypes = {
@ -44,16 +45,7 @@ export default class MessageEditor extends React.Component {
constructor(props, context) {
super(props, context);
const room = this._getRoom();
const partCreator = new PartCreator(
() => this._autocompleteRef,
query => this.setState({query}),
room,
);
this.model = new EditorModel(
parseEvent(this.props.event, room),
partCreator,
this._updateEditorState,
);
this.model = null;
this.state = {
autoComplete: null,
room,
@ -64,7 +56,7 @@ export default class MessageEditor extends React.Component {
}
_getRoom() {
return this.context.matrixClient.getRoom(this.props.event.getRoomId());
return this.context.matrixClient.getRoom(this.props.editState.getEvent().getRoomId());
}
_updateEditorState = (caret) => {
@ -133,7 +125,7 @@ export default class MessageEditor extends React.Component {
if (this._hasModifications || !this._isCaretAtStart()) {
return;
}
const previousEvent = findEditableEvent(this._getRoom(), false, this.props.event.getId());
const previousEvent = findEditableEvent(this._getRoom(), false, this.props.editState.getEvent().getId());
if (previousEvent) {
dis.dispatch({action: 'edit_event', event: previousEvent});
event.preventDefault();
@ -142,7 +134,7 @@ export default class MessageEditor extends React.Component {
if (this._hasModifications || !this._isCaretAtEnd()) {
return;
}
const nextEvent = findEditableEvent(this._getRoom(), true, this.props.event.getId());
const nextEvent = findEditableEvent(this._getRoom(), true, this.props.editState.getEvent().getId());
if (nextEvent) {
dis.dispatch({action: 'edit_event', event: nextEvent});
} else {
@ -178,11 +170,11 @@ export default class MessageEditor extends React.Component {
"m.new_content": newContent,
"m.relates_to": {
"rel_type": "m.replace",
"event_id": this.props.event.getId(),
"event_id": this.props.editState.getEvent().getId(),
},
}, contentBody);
const roomId = this.props.event.getRoomId();
const roomId = this.props.editState.getEvent().getRoomId();
this.context.matrixClient.sendMessage(roomId, content);
dis.dispatch({action: "edit_event", event: null});
@ -197,12 +189,63 @@ export default class MessageEditor extends React.Component {
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() {
this.model = this._createEditorModel();
// initial render of model
this._updateEditorState();
setCaretPosition(this._editorRef, this.model, this.model.getPositionAtEnd());
// initial caret position
this._initializeCaret();
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() {
let autoComplete;
if (this.state.autoComplete) {

View file

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

View file

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

View file

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

View file

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

View file

@ -676,6 +676,31 @@ export default class MessageComposerInput extends React.Component {
onKeyDown = (ev: KeyboardEvent, change: Change, editor: Editor) => {
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
// https://github.com/ianstormtaylor/slate/issues/762#issuecomment-304855095
@ -683,8 +708,6 @@ export default class MessageComposerInput extends React.Component {
this.direction = 'Previous';
} else if (ev.keyCode === KeyCode.RIGHT) {
this.direction = 'Next';
} else {
this.direction = '';
}
switch (ev.keyCode) {
@ -1181,45 +1204,36 @@ export default class MessageComposerInput extends React.Component {
};
onVerticalArrow = (e, up) => {
if (e.ctrlKey || e.altKey || e.metaKey) {
return;
if (e.ctrlKey || e.shiftKey || e.metaKey) return;
// Select history
const selection = this.state.editorState.selection;
// selection must be collapsed
if (!selection.isCollapsed) return;
const document = this.state.editorState.document;
// and we must be at the edge of the document (up=start, down=end)
if (up) {
if (!selection.anchor.isAtStartOfNode(document)) return;
if (!e.altKey) {
const editEvent = findEditableEvent(this.props.room, false);
if (editEvent) {
// We're selecting history, so prevent the key event from doing anything else
e.preventDefault();
dis.dispatch({
action: 'edit_event',
event: editEvent,
});
}
return;
}
}
// Select history only if we are not currently auto-completing
if (this.autocomplete.state.completionList.length === 0) {
const selection = this.state.editorState.selection;
// selection must be collapsed
if (!selection.isCollapsed) return;
const document = this.state.editorState.document;
// and we must be at the edge of the document (up=start, down=end)
if (up) {
if (!selection.anchor.isAtStartOfNode(document)) return;
if (!e.shiftKey) {
const editEvent = findEditableEvent(this.props.room, false);
if (editEvent) {
// We're selecting history, so prevent the key event from doing anything else
e.preventDefault();
dis.dispatch({
action: 'edit_event',
event: editEvent,
});
}
return;
}
} else {
if (!selection.anchor.isAtEndOfNode(document)) return;
}
const selected = this.selectHistory(up);
if (selected) {
// We're selecting history, so prevent the key event from doing anything else
e.preventDefault();
}
} else {
this.moveAutocompleteSelection(up);
const selected = this.selectHistory(up);
if (selected) {
// We're selecting history, so prevent the key event from doing anything else
e.preventDefault();
}
};
@ -1277,23 +1291,19 @@ export default class MessageComposerInput extends React.Component {
someCompletions: null,
});
e.preventDefault();
if (this.autocomplete.state.completionList.length === 0) {
if (this.autocomplete.countCompletions() === 0) {
// Force completions to show for the text currently entered
const completionCount = await this.autocomplete.forceComplete();
this.setState({
someCompletions: completionCount > 0,
});
// Select the first item by moving "down"
await this.moveAutocompleteSelection(false);
await this.autocomplete.moveSelection(+1);
} 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) => {
e.preventDefault();
if (this.autocomplete) {