From 6cb59f7071dc39de76fd0fb232937ccf3acd27f2 Mon Sep 17 00:00:00 2001 From: Pierre Boyer Date: Tue, 14 May 2019 08:54:00 +0200 Subject: [PATCH 01/21] Allow left/right arrow keys to navigate through the autocompletion list --- .../views/rooms/MessageComposerInput.js | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index a525fcb874..e569c5bb3b 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -670,6 +670,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.state.completionList.length > 0) { + if (!(ev.ctrlKey || ev.shiftKey || ev.altKey || ev.metaKey)) { + switch (ev.keyCode) { + case KeyCode.LEFT: + this.moveAutocompleteSelection(true); + ev.preventDefault(); + return true; + case KeyCode.RIGHT: + this.moveAutocompleteSelection(false); + ev.preventDefault(); + return true; + case KeyCode.UP: + this.moveAutocompleteSelection(true); + ev.preventDefault(); + return true; + case KeyCode.DOWN: + this.moveAutocompleteSelection(false); + ev.preventDefault(); + return true; + } + } + } // skip void nodes - see // https://github.com/ianstormtaylor/slate/issues/762#issuecomment-304855095 From 6e4c3bfe5680bb6685d828913dd9f706e205e874 Mon Sep 17 00:00:00 2001 From: Pierre Boyer Date: Tue, 14 May 2019 09:27:20 +0200 Subject: [PATCH 02/21] Remove now unused code --- .../views/rooms/MessageComposerInput.js | 43 ++++++++----------- 1 file changed, 17 insertions(+), 26 deletions(-) diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index e569c5bb3b..8b54a2d8bb 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -702,8 +702,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) { @@ -1197,35 +1195,28 @@ export default class MessageComposerInput extends React.Component { }; onVerticalArrow = (e, up) => { - if (e.ctrlKey || e.shiftKey || e.altKey || e.metaKey) { - return; - } + if (e.ctrlKey || e.shiftKey || e.altKey || e.metaKey) return; - // Select history only if we are not currently auto-completing - if (this.autocomplete.state.completionList.length === 0) { - const selection = this.state.editorState.selection; + // Select history + const selection = this.state.editorState.selection; - // selection must be collapsed - if (!selection.isCollapsed) return; - const document = this.state.editorState.document; + // 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; + // and we must be at the edge of the document (up=start, down=end) + if (up) { + if (!selection.anchor.isAtStartOfNode(document)) return; - 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, - }); - } + 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, + }); } - } else { - this.moveAutocompleteSelection(up); - e.preventDefault(); } }; From bb133c1ebcb291c62c3f6f02d202f180f3d2ced4 Mon Sep 17 00:00:00 2001 From: Pierre Boyer Date: Tue, 14 May 2019 10:13:04 +0200 Subject: [PATCH 03/21] Merge onUpArrow and onDownArrow into more general moveSelection --- src/components/views/rooms/Autocomplete.js | 23 +++++----------------- 1 file changed, 5 insertions(+), 18 deletions(-) diff --git a/src/components/views/rooms/Autocomplete.js b/src/components/views/rooms/Autocomplete.js index 9aef5433c3..243cfe2f75 100644 --- a/src/components/views/rooms/Autocomplete.js +++ b/src/components/views/rooms/Autocomplete.js @@ -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 { From 97d4d1b73a3d21289c882ec37043aae8b5c28f8c Mon Sep 17 00:00:00 2001 From: Pierre Boyer Date: Tue, 14 May 2019 10:16:10 +0200 Subject: [PATCH 04/21] Update composer to correctly call countCompletions and moveSelection --- .../views/rooms/MessageComposerInput.js | 20 ++++++++----------- 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index 8b54a2d8bb..74f358c161 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -673,23 +673,23 @@ export default class MessageComposerInput extends React.Component { this.direction = ''; // Navigate autocomplete list with arrow keys - if (this.autocomplete.state.completionList.length > 0) { + if (this.autocomplete.countCompletions() > 0) { if (!(ev.ctrlKey || ev.shiftKey || ev.altKey || ev.metaKey)) { switch (ev.keyCode) { case KeyCode.LEFT: - this.moveAutocompleteSelection(true); + this.autocomplete.moveSelection(-1); ev.preventDefault(); return true; case KeyCode.RIGHT: - this.moveAutocompleteSelection(false); + this.autocomplete.moveSelection(+1); ev.preventDefault(); return true; case KeyCode.UP: - this.moveAutocompleteSelection(true); + this.autocomplete.moveSelection(-1); ev.preventDefault(); return true; case KeyCode.DOWN: - this.moveAutocompleteSelection(false); + this.autocomplete.moveSelection(+1); ev.preventDefault(); return true; } @@ -1225,23 +1225,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) { From ed6427571e724c53f75d8fd41807df5c7b4950eb Mon Sep 17 00:00:00 2001 From: Pierre Boyer Date: Tue, 4 Jun 2019 13:21:39 +0200 Subject: [PATCH 05/21] Update src/editor/autocomplete to correctly call countCompletions and moveSelection --- src/editor/autocomplete.js | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/src/editor/autocomplete.js b/src/editor/autocomplete.js index ceaf18c444..ba18207de1 100644 --- a/src/editor/autocomplete.js +++ b/src/editor/autocomplete.js @@ -42,31 +42,19 @@ export default class AutocompleteWrapperModel { async onTab(e) { const acComponent = this._getAutocompleterComponent(); - if (acComponent.state.completionList.length === 0) { + if (acComponent.countCompletions() === 0) { // Force completions to show for the text currently entered await acComponent.forceComplete(); // Select the first item by moving "down" - await acComponent.onDownArrow(); + await acComponent.moveSelection(+1); } else { - if (e.shiftKey) { - await acComponent.onUpArrow(); - } else { - await acComponent.onDownArrow(); - } + await acComponent.moveSelection(e.shiftKey ? -1 : +1); } this._updateCallback({ close: true, }); } - onUpArrow() { - this._getAutocompleterComponent().onUpArrow(); - } - - onDownArrow() { - this._getAutocompleterComponent().onDownArrow(); - } - onPartUpdate(part, offset) { // cache the typed value and caret here // so we can restore it in onComponentSelectionChange when the value is undefined (meaning it should be the typed text) From a4dec88c651a731fd40175b0c3260dc85169176b Mon Sep 17 00:00:00 2001 From: Pierre Boyer Date: Tue, 4 Jun 2019 13:57:15 +0200 Subject: [PATCH 06/21] Add back on..Arrow functions. Add left/right key navigation in MessageEditor --- src/components/views/elements/MessageEditor.js | 4 ++++ src/editor/autocomplete.js | 16 ++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/src/components/views/elements/MessageEditor.js b/src/components/views/elements/MessageEditor.js index ed86bcb0a3..98569023cb 100644 --- a/src/components/views/elements/MessageEditor.js +++ b/src/components/views/elements/MessageEditor.js @@ -116,6 +116,10 @@ export default class MessageEditor extends React.Component { autoComplete.onUpArrow(event); break; case "ArrowDown": autoComplete.onDownArrow(event); break; + case "ArrowLeft": + autoComplete.onLeftArrow(event); break; + case "ArrowRight": + autoComplete.onRightArrow(event); break; case "Tab": autoComplete.onTab(event); break; case "Escape": diff --git a/src/editor/autocomplete.js b/src/editor/autocomplete.js index ba18207de1..c0dc020897 100644 --- a/src/editor/autocomplete.js +++ b/src/editor/autocomplete.js @@ -55,6 +55,22 @@ export default class AutocompleteWrapperModel { }); } + onUpArrow() { + this._getAutocompleterComponent().moveSelection(-1); + } + + onDownArrow() { + this._getAutocompleterComponent().moveSelection(+1); + } + + onLeftArrow() { + this._getAutocompleterComponent().moveSelection(-1); + } + + onRightArrow() { + this._getAutocompleterComponent().moveSelection(+1); + } + onPartUpdate(part, offset) { // cache the typed value and caret here // so we can restore it in onComponentSelectionChange when the value is undefined (meaning it should be the typed text) From 81585676407bfe603691b7ec49431e0ba9591ed2 Mon Sep 17 00:00:00 2001 From: Pierre Boyer Date: Wed, 5 Jun 2019 10:49:49 +0200 Subject: [PATCH 07/21] Remove left/right autocomplete navigation for MessageEditor --- src/components/views/elements/MessageEditor.js | 4 ---- src/editor/autocomplete.js | 8 -------- 2 files changed, 12 deletions(-) diff --git a/src/components/views/elements/MessageEditor.js b/src/components/views/elements/MessageEditor.js index 98569023cb..ed86bcb0a3 100644 --- a/src/components/views/elements/MessageEditor.js +++ b/src/components/views/elements/MessageEditor.js @@ -116,10 +116,6 @@ export default class MessageEditor extends React.Component { autoComplete.onUpArrow(event); break; case "ArrowDown": autoComplete.onDownArrow(event); break; - case "ArrowLeft": - autoComplete.onLeftArrow(event); break; - case "ArrowRight": - autoComplete.onRightArrow(event); break; case "Tab": autoComplete.onTab(event); break; case "Escape": diff --git a/src/editor/autocomplete.js b/src/editor/autocomplete.js index c0dc020897..ce0550d88e 100644 --- a/src/editor/autocomplete.js +++ b/src/editor/autocomplete.js @@ -63,14 +63,6 @@ export default class AutocompleteWrapperModel { this._getAutocompleterComponent().moveSelection(+1); } - onLeftArrow() { - this._getAutocompleterComponent().moveSelection(-1); - } - - onRightArrow() { - this._getAutocompleterComponent().moveSelection(+1); - } - onPartUpdate(part, offset) { // cache the typed value and caret here // so we can restore it in onComponentSelectionChange when the value is undefined (meaning it should be the typed text) From f78aeae83aafc75fe235e816e8b067eac32761a7 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 12 Jun 2019 11:19:17 +0200 Subject: [PATCH 08/21] also consider pending events when looking for next/prev event to edit --- src/utils/EventUtils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/EventUtils.js b/src/utils/EventUtils.js index ff20a68e3c..8b219d2a03 100644 --- a/src/utils/EventUtils.js +++ b/src/utils/EventUtils.js @@ -64,7 +64,7 @@ export function canEditOwnEvent(mxEvent) { const MAX_JUMP_DISTANCE = 100; export function findEditableEvent(room, isForward, fromEventId = undefined) { const liveTimeline = room.getLiveTimeline(); - const events = liveTimeline.getEvents(); + const events = liveTimeline.getEvents().concat(room.getPendingEvents()); const maxIdx = events.length - 1; const inc = isForward ? 1 : -1; const beginIdx = isForward ? 0 : maxIdx; From d13b3aa16c34db05811edb6591cd2e5d877eaf40 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 12 Jun 2019 11:20:21 +0200 Subject: [PATCH 09/21] don't block unsent events from being edited --- src/utils/EventUtils.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/utils/EventUtils.js b/src/utils/EventUtils.js index 8b219d2a03..219b53bc5e 100644 --- a/src/utils/EventUtils.js +++ b/src/utils/EventUtils.js @@ -46,7 +46,8 @@ export function isContentActionable(mxEvent) { } export function canEditContent(mxEvent) { - return isContentActionable(mxEvent) && + return mxEvent.status !== EventStatus.CANCELLED && + mxEvent.getType() === 'm.room.message' && mxEvent.getOriginalContent().msgtype === "m.text" && mxEvent.getSender() === MatrixClientPeg.get().getUserId(); } From 678fd37549c81f17d09669171a17b37cf4d34ae9 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 12 Jun 2019 18:29:21 +0200 Subject: [PATCH 10/21] helper class to preserve editor state between remounting the editor --- src/utils/EditorStateTransfer.js | 49 ++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 src/utils/EditorStateTransfer.js diff --git a/src/utils/EditorStateTransfer.js b/src/utils/EditorStateTransfer.js new file mode 100644 index 0000000000..23c68a4ce5 --- /dev/null +++ b/src/utils/EditorStateTransfer.js @@ -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 the next 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; + } +} From e674f39e3b3bd8b33321151f46ab4eb879da56c3 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 12 Jun 2019 18:32:32 +0200 Subject: [PATCH 11/21] support (de)serializing parts with other dependencies than text --- src/editor/autocomplete.js | 5 +++-- src/editor/deserialize.js | 16 ++++++++-------- src/editor/model.js | 2 +- src/editor/parts.js | 39 +++++++++++++++++++++++++++++++++----- 4 files changed, 46 insertions(+), 16 deletions(-) diff --git a/src/editor/autocomplete.js b/src/editor/autocomplete.js index ceaf18c444..92c4db415e 100644 --- a/src/editor/autocomplete.js +++ b/src/editor/autocomplete.js @@ -18,12 +18,13 @@ limitations under the License. import {UserPillPart, RoomPillPart, PlainPart} from "./parts"; export default class AutocompleteWrapperModel { - constructor(updateCallback, getAutocompleterComponent, updateQuery, room) { + constructor(updateCallback, getAutocompleterComponent, updateQuery, room, client) { this._updateCallback = updateCallback; this._getAutocompleterComponent = getAutocompleterComponent; this._updateQuery = updateQuery; this._query = null; this._room = room; + this._client = client; } onEscape(e) { @@ -106,7 +107,7 @@ export default class AutocompleteWrapperModel { } case "#": { const displayAlias = completion.completionId; - return new RoomPillPart(displayAlias); + return new RoomPillPart(displayAlias, this._client); } // also used for emoji completion default: diff --git a/src/editor/deserialize.js b/src/editor/deserialize.js index 64b219c2a9..48625cba5f 100644 --- a/src/editor/deserialize.js +++ b/src/editor/deserialize.js @@ -21,7 +21,7 @@ import { walkDOMDepthFirst } from "./dom"; const REGEX_MATRIXTO = new RegExp(MATRIXTO_URL_PATTERN); -function parseLink(a, room) { +function parseLink(a, room, client) { const {href} = a; const pillMatch = REGEX_MATRIXTO.exec(href) || []; const resourceId = pillMatch[1]; // The room/user ID @@ -34,7 +34,7 @@ function parseLink(a, room) { room.getMember(resourceId), ); case "#": - return new RoomPillPart(resourceId); + return new RoomPillPart(resourceId, client); default: { if (href === a.textContent) { return new PlainPart(a.textContent); @@ -57,10 +57,10 @@ function parseCodeBlock(n) { return parts; } -function parseElement(n, room) { +function parseElement(n, room, client) { switch (n.nodeName) { case "A": - return parseLink(n, room); + return parseLink(n, room, client); case "BR": return new NewlinePart("\n"); 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, // as scripts in event handlers, etc would be executed then. // we're only taking text, so that is fine @@ -165,7 +165,7 @@ function parseHtmlMessage(html, room) { if (n.nodeType === Node.TEXT_NODE) { newParts.push(new PlainPart(n.nodeValue)); } else if (n.nodeType === Node.ELEMENT_NODE) { - const parseResult = parseElement(n, room); + const parseResult = parseElement(n, room, client); if (parseResult) { if (Array.isArray(parseResult)) { newParts.push(...parseResult); @@ -205,10 +205,10 @@ function parseHtmlMessage(html, room) { return parts; } -export function parseEvent(event, room) { +export function parseEvent(event, room, client) { const content = event.getContent(); if (content.format === "org.matrix.custom.html") { - return parseHtmlMessage(content.formatted_body || "", room); + return parseHtmlMessage(content.formatted_body || "", room, client); } else { const body = content.body || ""; const lines = body.split("\n"); diff --git a/src/editor/model.js b/src/editor/model.js index fb6b417530..a5d2f25f95 100644 --- a/src/editor/model.js +++ b/src/editor/model.js @@ -73,7 +73,7 @@ export default class EditorModel { } serializeParts() { - return this._parts.map(({type, text}) => {return {type, text};}); + return this._parts.map(p => p.serialize()); } _diff(newValue, inputType, caret) { diff --git a/src/editor/parts.js b/src/editor/parts.js index be3080db12..193b35a5ea 100644 --- a/src/editor/parts.js +++ b/src/editor/parts.js @@ -102,6 +102,10 @@ class BasePart { toString() { return `${this.type}(${this.text})`; } + + serialize() { + return {type: this.type, text: this.text}; + } } export class PlainPart extends BasePart { @@ -233,13 +237,12 @@ export class NewlinePart extends BasePart { } export class RoomPillPart extends PillPart { - constructor(displayAlias) { + constructor(displayAlias, client) { super(displayAlias, displayAlias); - this._room = this._findRoomByAlias(displayAlias); + this._room = this._findRoomByAlias(displayAlias, client); } - _findRoomByAlias(alias) { - const client = MatrixClientPeg.get(); + _findRoomByAlias(alias, client) { if (alias[0] === '#') { return client.getRooms().find((r) => { return r.getAliases().includes(alias); @@ -300,6 +303,12 @@ export class UserPillPart extends PillPart { get className() { return "mx_UserPill mx_Pill"; } + + serialize() { + const obj = super.serialize(); + obj.userId = this.resourceId; + return obj; + } } @@ -335,13 +344,16 @@ export class PillCandidatePart extends PlainPart { } export class PartCreator { - constructor(getAutocompleterComponent, updateQuery, room) { + constructor(getAutocompleterComponent, updateQuery, room, client) { + this._room = room; + this._client = client; this._autoCompleteCreator = (updateCallback) => { return new AutocompleteWrapperModel( updateCallback, getAutocompleterComponent, updateQuery, room, + client, ); }; } @@ -362,5 +374,22 @@ export class PartCreator { createDefaultPart(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); + } + } + } } From 41e41269dc0bc42432d4aa6a5a58e7518c6abacf Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 12 Jun 2019 18:52:34 +0200 Subject: [PATCH 12/21] use EditorStateTransfer to pass on state to newly mounted editor --- src/components/structures/MessagePanel.js | 5 +- src/components/structures/TimelinePanel.js | 6 +- .../views/elements/MessageEditor.js | 79 ++++++++++++++----- src/components/views/messages/MessageEvent.js | 2 +- src/components/views/messages/TextualBody.js | 12 +-- src/components/views/rooms/EventTile.js | 9 ++- src/editor/model.js | 6 +- 7 files changed, 83 insertions(+), 36 deletions(-) diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index 698768067a..52fd6d9be4 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -517,7 +517,8 @@ module.exports = React.createClass({ const DateSeparator = sdk.getComponent('messages.DateSeparator'); 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? let continuation = false; @@ -585,7 +586,7 @@ module.exports = React.createClass({ continuation={continuation} isRedacted={mxEv.isRedacted()} replacingEventId={mxEv.replacingEventId()} - isEditing={isEditing} + editState={isEditing && this.props.editState} onHeightChanged={this._onHeightChanged} readReceipts={readReceipts} readReceiptMap={this._readReceiptMap} diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index 220c56d754..9c48b8ede1 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -35,6 +35,7 @@ const Modal = require("../../Modal"); const UserActivity = require("../../UserActivity"); import { KeyCode } from '../../Keyboard'; import Timer from '../../utils/Timer'; +import EditorStateTransfer from '../../utils/EditorStateTransfer'; const PAGINATE_SIZE = 20; const INITIAL_SIZE = 20; @@ -411,7 +412,8 @@ const TimelinePanel = React.createClass({ this.forceUpdate(); } 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) { this.refs.messagePanel.scrollToEventIfNeeded( payload.event.getId(), @@ -1306,7 +1308,7 @@ const TimelinePanel = React.createClass({ tileShape={this.props.tileShape} resizeNotifier={this.props.resizeNotifier} getRelationsForEvent={this.getRelationsForEvent} - editEvent={this.state.editEvent} + editState={this.state.editState} showReactions={this.props.showReactions} /> ); diff --git a/src/components/views/elements/MessageEditor.js b/src/components/views/elements/MessageEditor.js index ed86bcb0a3..0aff6781ee 100644 --- a/src/components/views/elements/MessageEditor.js +++ b/src/components/views/elements/MessageEditor.js @@ -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) { diff --git a/src/components/views/messages/MessageEvent.js b/src/components/views/messages/MessageEvent.js index 8c90ec5a46..6d7aada542 100644 --- a/src/components/views/messages/MessageEvent.js +++ b/src/components/views/messages/MessageEvent.js @@ -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} />; }, }); diff --git a/src/components/views/messages/TextualBody.js b/src/components/views/messages/TextualBody.js index 1fc16d6a53..6f480b8d3c 100644 --- a/src/components/views/messages/TextualBody.js +++ b/src/components/views/messages/TextualBody.js @@ -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 ; + return ; } const mxEvent = this.props.mxEvent; const content = mxEvent.getContent(); diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index 850f496e24..9837b4a029 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -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 ? { const partLen = part.text.length; From d40f49e2c2233e6df80d16988405772c1d356028 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 12 Jun 2019 19:09:27 +0200 Subject: [PATCH 13/21] fix lint --- src/editor/parts.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/editor/parts.js b/src/editor/parts.js index 193b35a5ea..a122c7ab7a 100644 --- a/src/editor/parts.js +++ b/src/editor/parts.js @@ -17,7 +17,6 @@ limitations under the License. import AutocompleteWrapperModel from "./autocomplete"; import Avatar from "../Avatar"; -import MatrixClientPeg from "../MatrixClientPeg"; class BasePart { constructor(text = "") { From 4fda6c21de90d8afa5fa8aa9262093a342ecdc9a Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 12 Jun 2019 21:58:10 +0100 Subject: [PATCH 14/21] Use overflow on MemberInfo name/mxid so that the back button stays Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- res/css/views/rooms/_MemberInfo.scss | 2 ++ 1 file changed, 2 insertions(+) diff --git a/res/css/views/rooms/_MemberInfo.scss b/res/css/views/rooms/_MemberInfo.scss index c3b3ca2f7d..bb38c41581 100644 --- a/res/css/views/rooms/_MemberInfo.scss +++ b/res/css/views/rooms/_MemberInfo.scss @@ -43,6 +43,8 @@ limitations under the License. .mx_MemberInfo_name h2 { flex: 1; + overflow-x: auto; + max-height: 50px; } .mx_MemberInfo h2 { From 89cc45892c7795c2fa8d5cd8c619199c36c0612e Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 13 Jun 2019 13:28:21 +0200 Subject: [PATCH 15/21] fix grammar fail --- src/utils/EditorStateTransfer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/EditorStateTransfer.js b/src/utils/EditorStateTransfer.js index 23c68a4ce5..c7782a9ea8 100644 --- a/src/utils/EditorStateTransfer.js +++ b/src/utils/EditorStateTransfer.js @@ -16,7 +16,7 @@ limitations under the License. /** * Used while editing, to pass the event, and to preserve editor state - * from one editor instance to another the next when remounting the editor + * from one editor instance to another when remounting the editor * upon receiving the remote echo for an unsent event. */ export default class EditorStateTransfer { From 8b16f91b3df14ae91ab08aa5a38c618d20bf55df Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 13 Jun 2019 14:22:50 +0200 Subject: [PATCH 16/21] fix karma tests? --- test/components/views/dialogs/InteractiveAuthDialog-test.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/test/components/views/dialogs/InteractiveAuthDialog-test.js b/test/components/views/dialogs/InteractiveAuthDialog-test.js index 88d1c804ca..2d1fb29bd9 100644 --- a/test/components/views/dialogs/InteractiveAuthDialog-test.js +++ b/test/components/views/dialogs/InteractiveAuthDialog-test.js @@ -103,12 +103,6 @@ describe('InteractiveAuthDialog', function() { password: "s3kr3t", user: "@user:id", })).toBe(true); - - // there should now be a spinner - ReactTestUtils.findRenderedComponentWithType( - dlg, sdk.getComponent('elements.Spinner'), - ); - // let the request complete return Promise.delay(1); }).then(() => { From 048d8d2ec706c6fcb3f8e0891842b85ee0968032 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 13 Jun 2019 16:24:09 +0100 Subject: [PATCH 17/21] Simplify email registration You now don't get automatically logged in after finishing registration. This makes a whole class of failures involving race conditions and multiple devices impossible. https://github.com/vector-im/riot-web/issues/9586 --- .../structures/auth/Registration.js | 108 ++++++++++++++---- src/i18n/strings/en_EN.json | 3 + 2 files changed, 91 insertions(+), 20 deletions(-) diff --git a/src/components/structures/auth/Registration.js b/src/components/structures/auth/Registration.js index bf4a86e410..9a7c927603 100644 --- a/src/components/structures/auth/Registration.js +++ b/src/components/structures/auth/Registration.js @@ -28,6 +28,7 @@ import { messageForResourceLimitError } from '../../../utils/ErrorUtils'; import * as ServerType from '../../views/auth/ServerTypeSelector'; import AutoDiscoveryUtils, {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils"; import classNames from "classnames"; +import * as Lifecycle from '../../../Lifecycle'; // Phases // Show controls to configure server details @@ -80,6 +81,9 @@ module.exports = React.createClass({ // Phase of the overall registration dialog. phase: PHASE_REGISTRATION, 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 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."), }); } else { + console.log("Unable to query for supported registration methods.", e); this.setState({ errorText: _t("Unable to query for supported registration methods."), }); @@ -282,21 +287,27 @@ module.exports = React.createClass({ return; } - this.setState({ - // we're still busy until we get unmounted: don't show the registration form again - busy: true, + const newState = { doingUIAuth: false, - }); + }; + if (response.access_token) { + const cli = await this.props.onLoggedIn({ + userId: response.user_id, + deviceId: response.device_id, + homeserverUrl: this.state.matrixClient.getHomeserverUrl(), + identityServerUrl: this.state.matrixClient.getIdentityServerUrl(), + accessToken: response.access_token, + }); - const cli = await this.props.onLoggedIn({ - userId: response.user_id, - deviceId: response.device_id, - homeserverUrl: this.state.matrixClient.getHomeserverUrl(), - identityServerUrl: this.state.matrixClient.getIdentityServerUrl(), - accessToken: response.access_token, - }); + 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._setupPushers(cli); + this.setState(newState); }, _setupPushers: function(matrixClient) { @@ -352,7 +363,16 @@ module.exports = React.createClass({ }); }, - _makeRegisterRequest: function(auth) { + _makeRegisterRequest: function(auth, inhibitLogin) { + // default is to inhibit login if we're trying to register with an email address + // We do this so that the client that gets spawned when clicking on the email + // verification link doesn't get logged in (it can't choose different params + // because it doesn't have the password and it can only supply a complete + // set of parameters). If the original client is still around when the + // registration completes, it can resubmit with inhibitLogin=false to + // log itself in! + if (inhibitLogin === undefined) inhibitLogin = Boolean(this.state.formVals.email); + // 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 // session). @@ -360,6 +380,8 @@ module.exports = React.createClass({ email: true, msisdn: true, } : {}; + // Likewise inhibitLogin + if (!this.state.formVals.password) inhibitLogin = null; return this.state.matrixClient.register( this.state.formVals.username, @@ -368,6 +390,7 @@ module.exports = React.createClass({ auth, bindThreepids, null, + inhibitLogin, ); }, @@ -379,6 +402,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() { const ServerTypeSelector = sdk.getComponent("auth.ServerTypeSelector"); const ServerConfig = sdk.getComponent("auth.ServerConfig"); @@ -528,17 +564,49 @@ module.exports = React.createClass({ ; } + let body; + if (this.state.completedNoSignin) { + let regDoneText; + if (this.state.formVals.password) { + // We're the client that started the registration + regDoneText = _t( + "Log in to your new account.", {}, + { + a: (sub) => {sub}, + }, + ); + } 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 log in to your new account.", {}, + { + a: (sub) => {sub}, + }, + ); + } + body =
+

{_t("Registration Successful")}

+

{ regDoneText }

+
; + } else { + body =
+

{ _t('Create your account') }

+ { errorText } + { serverDeadSection } + { this.renderServerComponent() } + { this.renderRegisterComponent() } + { goBack } + { signIn } +
; + } + return ( -

{ _t('Create your account') }

- { errorText } - { serverDeadSection } - { this.renderServerComponent() } - { this.renderRegisterComponent() } - { goBack } - { signIn } + { body }
); diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 69ac59f984..53fd82f6f2 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1557,6 +1557,9 @@ "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.", "This server does not support authentication with a phone number.": "This server does not support authentication with a phone number.", + "Log in to your new account.": "Log in to your new account.", + "You can now close this window or log in to your new account.": "You can now close this window or log in to your new account.", + "Registration Successful": "Registration Successful", "Create your account": "Create your account", "Commands": "Commands", "Results from DuckDuckGo": "Results from DuckDuckGo", From 81327264f7dcbb6b5ba05ab20b355a0a803c6a08 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 13 Jun 2019 17:44:00 +0100 Subject: [PATCH 18/21] Remove unused inhibitlogin param and fix docs. --- src/components/structures/auth/Registration.js | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src/components/structures/auth/Registration.js b/src/components/structures/auth/Registration.js index 9a7c927603..3103ee41df 100644 --- a/src/components/structures/auth/Registration.js +++ b/src/components/structures/auth/Registration.js @@ -363,15 +363,12 @@ module.exports = React.createClass({ }); }, - _makeRegisterRequest: function(auth, inhibitLogin) { - // default is to inhibit login if we're trying to register with an email address - // We do this so that the client that gets spawned when clicking on the email - // verification link doesn't get logged in (it can't choose different params - // because it doesn't have the password and it can only supply a complete - // set of parameters). If the original client is still around when the - // registration completes, it can resubmit with inhibitLogin=false to - // log itself in! - if (inhibitLogin === undefined) inhibitLogin = Boolean(this.state.formVals.email); + _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 // (Since we need to send no params at all to use the ones saved in the From e884cccabe3c31acfdf15161b6d481abd62a1dc0 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 13 Jun 2019 18:23:33 +0100 Subject: [PATCH 19/21] Allow changing servers on nonfatal errors Fixes https://github.com/vector-im/riot-web/issues/10016 --- src/components/views/auth/ServerConfig.js | 28 +++++++++++++++-------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/src/components/views/auth/ServerConfig.js b/src/components/views/auth/ServerConfig.js index 8d2e2e7bba..5b76e3f0d8 100644 --- a/src/components/views/auth/ServerConfig.js +++ b/src/components/views/auth/ServerConfig.js @@ -101,16 +101,26 @@ 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; + } } } From 06a11f4d45cf8244c52e7ea0acbe2b868663bdfe Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 13 Jun 2019 18:31:04 +0100 Subject: [PATCH 20/21] Random blank lines --- src/components/views/auth/ServerConfig.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/views/auth/ServerConfig.js b/src/components/views/auth/ServerConfig.js index 5b76e3f0d8..de4f16b684 100644 --- a/src/components/views/auth/ServerConfig.js +++ b/src/components/views/auth/ServerConfig.js @@ -118,7 +118,6 @@ export default class ServerConfig extends React.PureComponent { errorText: message, }); - return null; } } From 1090b7d9124b3e0d51dc14d07325443a144bb48d Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 13 Jun 2019 23:48:47 +0100 Subject: [PATCH 21/21] Use flex: 1 for mx_Field to replace all the calc(100% - 20px) and more Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- res/css/_components.scss | 1 - res/css/views/auth/_AuthBody.scss | 2 -- res/css/views/auth/_ServerConfig.scss | 1 - res/css/views/dialogs/_BugReportDialog.scss | 25 ------------------- res/css/views/dialogs/_DevtoolsDialog.scss | 7 ++++-- res/css/views/dialogs/_SetPasswordDialog.scss | 1 - res/css/views/elements/_EditableItemList.scss | 8 +----- res/css/views/elements/_Field.scss | 1 + res/css/views/elements/_PowerSelector.scss | 1 - res/css/views/settings/_EmailAddresses.scss | 6 ----- res/css/views/settings/_PhoneNumbers.scss | 6 ----- res/css/views/settings/_ProfileSettings.scss | 5 ---- .../tabs/room/_GeneralRoomSettingsTab.scss | 4 --- .../tabs/user/_GeneralUserSettingsTab.scss | 18 +------------ .../user/_PreferencesUserSettingsTab.scss | 8 ------ .../tabs/user/_VoiceUserSettingsTab.scss | 5 ---- 16 files changed, 8 insertions(+), 91 deletions(-) delete mode 100644 res/css/views/dialogs/_BugReportDialog.scss diff --git a/res/css/_components.scss b/res/css/_components.scss index 2a91f08ee4..843f314bd1 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -50,7 +50,6 @@ @import "./views/context_menus/_TopLeftMenu.scss"; @import "./views/dialogs/_AddressPickerDialog.scss"; @import "./views/dialogs/_Analytics.scss"; -@import "./views/dialogs/_BugReportDialog.scss"; @import "./views/dialogs/_ChangelogDialog.scss"; @import "./views/dialogs/_ChatCreateOrReuseChatDialog.scss"; @import "./views/dialogs/_ConfirmUserActionDialog.scss"; diff --git a/res/css/views/auth/_AuthBody.scss b/res/css/views/auth/_AuthBody.scss index 16ac876869..cce3b5dbf5 100644 --- a/res/css/views/auth/_AuthBody.scss +++ b/res/css/views/auth/_AuthBody.scss @@ -72,7 +72,6 @@ limitations under the License. } .mx_Field input { - width: 100%; box-sizing: border-box; } @@ -110,7 +109,6 @@ limitations under the License. .mx_AuthBody_fieldRow > .mx_Field { margin: 0 5px; - flex: 1; } .mx_AuthBody_fieldRow > .mx_Field:first-child { diff --git a/res/css/views/auth/_ServerConfig.scss b/res/css/views/auth/_ServerConfig.scss index fe96da2019..a31feb75d7 100644 --- a/res/css/views/auth/_ServerConfig.scss +++ b/res/css/views/auth/_ServerConfig.scss @@ -20,7 +20,6 @@ limitations under the License. } .mx_ServerConfig_fields .mx_Field { - flex: 1; margin: 0 5px; } diff --git a/res/css/views/dialogs/_BugReportDialog.scss b/res/css/views/dialogs/_BugReportDialog.scss deleted file mode 100644 index 90ef55b945..0000000000 --- a/res/css/views/dialogs/_BugReportDialog.scss +++ /dev/null @@ -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; -} diff --git a/res/css/views/dialogs/_DevtoolsDialog.scss b/res/css/views/dialogs/_DevtoolsDialog.scss index 1f5d36b57a..8e669acd10 100644 --- a/res/css/views/dialogs/_DevtoolsDialog.scss +++ b/res/css/views/dialogs/_DevtoolsDialog.scss @@ -23,7 +23,11 @@ limitations under the License. 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; width: 100%; } @@ -75,7 +79,6 @@ limitations under the License. max-width: 684px; min-height: 250px; padding: 10px; - width: 100%; } .mx_DevTools_content .mx_Field_input { diff --git a/res/css/views/dialogs/_SetPasswordDialog.scss b/res/css/views/dialogs/_SetPasswordDialog.scss index 28a8b7c9d7..325ff6c6ed 100644 --- a/res/css/views/dialogs/_SetPasswordDialog.scss +++ b/res/css/views/dialogs/_SetPasswordDialog.scss @@ -21,7 +21,6 @@ limitations under the License. color: $primary-fg-color; background-color: $primary-bg-color; font-size: 15px; - width: 100%; max-width: 280px; margin-bottom: 10px; } diff --git a/res/css/views/elements/_EditableItemList.scss b/res/css/views/elements/_EditableItemList.scss index be96d811d3..51fa4c4423 100644 --- a/res/css/views/elements/_EditableItemList.scss +++ b/res/css/views/elements/_EditableItemList.scss @@ -42,12 +42,6 @@ limitations under the License. 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 { margin-bottom: 5px; -} \ No newline at end of file +} diff --git a/res/css/views/elements/_Field.scss b/res/css/views/elements/_Field.scss index 147bb3b471..f9cbf8c541 100644 --- a/res/css/views/elements/_Field.scss +++ b/res/css/views/elements/_Field.scss @@ -42,6 +42,7 @@ limitations under the License. padding: 8px 9px; color: $primary-fg-color; background-color: $primary-bg-color; + flex: 1; } .mx_Field select { diff --git a/res/css/views/elements/_PowerSelector.scss b/res/css/views/elements/_PowerSelector.scss index 69f3a8eebb..799f6f246e 100644 --- a/res/css/views/elements/_PowerSelector.scss +++ b/res/css/views/elements/_PowerSelector.scss @@ -20,6 +20,5 @@ limitations under the License. .mx_PowerSelector .mx_Field select, .mx_PowerSelector .mx_Field input { - width: 100%; box-sizing: border-box; } diff --git a/res/css/views/settings/_EmailAddresses.scss b/res/css/views/settings/_EmailAddresses.scss index eef804a33b..4f9541af2c 100644 --- a/res/css/views/settings/_EmailAddresses.scss +++ b/res/css/views/settings/_EmailAddresses.scss @@ -35,9 +35,3 @@ limitations under the License. .mx_ExistingEmailAddress_confirmBtn { 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); -} diff --git a/res/css/views/settings/_PhoneNumbers.scss b/res/css/views/settings/_PhoneNumbers.scss index 2f54babd6f..a3891882c2 100644 --- a/res/css/views/settings/_PhoneNumbers.scss +++ b/res/css/views/settings/_PhoneNumbers.scss @@ -36,12 +36,6 @@ limitations under the License. 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 { display: flex; align-items: center; diff --git a/res/css/views/settings/_ProfileSettings.scss b/res/css/views/settings/_ProfileSettings.scss index b2e449ac34..a972162618 100644 --- a/res/css/views/settings/_ProfileSettings.scss +++ b/res/css/views/settings/_ProfileSettings.scss @@ -22,11 +22,6 @@ limitations under the License. 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 { height: 4em; } diff --git a/res/css/views/settings/tabs/room/_GeneralRoomSettingsTab.scss b/res/css/views/settings/tabs/room/_GeneralRoomSettingsTab.scss index 91d7ed2c7d..af55820d66 100644 --- a/res/css/views/settings/tabs/room/_GeneralRoomSettingsTab.scss +++ b/res/css/views/settings/tabs/room/_GeneralRoomSettingsTab.scss @@ -17,7 +17,3 @@ limitations under the License. .mx_GeneralRoomSettingsTab_profileSection { margin-top: 10px; } - -.mx_GeneralRoomSettingsTab .mx_AliasSettings .mx_Field select { - width: 100%; -} diff --git a/res/css/views/settings/tabs/user/_GeneralUserSettingsTab.scss b/res/css/views/settings/tabs/user/_GeneralUserSettingsTab.scss index bec013674a..091c98ffb8 100644 --- a/res/css/views/settings/tabs/user/_GeneralUserSettingsTab.scss +++ b/res/css/views/settings/tabs/user/_GeneralUserSettingsTab.scss @@ -14,33 +14,17 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_GeneralUserSettingsTab_changePassword, -.mx_GeneralUserSettingsTab_themeSection { - display: block; -} - .mx_GeneralUserSettingsTab_changePassword .mx_Field, .mx_GeneralUserSettingsTab_themeSection .mx_Field { - display: block; 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 { margin-top: 0; } -.mx_GeneralUserSettingsTab_themeSection .mx_Field select { - display: block; - width: 100%; -} - .mx_GeneralUserSettingsTab_accountSection > .mx_EmailAddresses, .mx_GeneralUserSettingsTab_accountSection > .mx_PhoneNumbers, .mx_GeneralUserSettingsTab_languageInput { margin-right: 100px; // Align with the other fields on the page -} \ No newline at end of file +} diff --git a/res/css/views/settings/tabs/user/_PreferencesUserSettingsTab.scss b/res/css/views/settings/tabs/user/_PreferencesUserSettingsTab.scss index f447221b7a..b3430f47af 100644 --- a/res/css/views/settings/tabs/user/_PreferencesUserSettingsTab.scss +++ b/res/css/views/settings/tabs/user/_PreferencesUserSettingsTab.scss @@ -17,11 +17,3 @@ limitations under the License. .mx_PreferencesUserSettingsTab .mx_Field { 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); -} diff --git a/res/css/views/settings/tabs/user/_VoiceUserSettingsTab.scss b/res/css/views/settings/tabs/user/_VoiceUserSettingsTab.scss index f5dba9831e..36c8cfd896 100644 --- a/res/css/views/settings/tabs/user/_VoiceUserSettingsTab.scss +++ b/res/css/views/settings/tabs/user/_VoiceUserSettingsTab.scss @@ -14,11 +14,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_VoiceUserSettingsTab .mx_Field select { - width: 100%; - max-width: 100%; -} - .mx_VoiceUserSettingsTab .mx_Field { margin-right: 100px; // align with the rest of the fields }