From 6cb59f7071dc39de76fd0fb232937ccf3acd27f2 Mon Sep 17 00:00:00 2001 From: Pierre Boyer Date: Tue, 14 May 2019 08:54:00 +0200 Subject: [PATCH 01/59] 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/59] 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/59] 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/59] 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/59] 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/59] 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/59] 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/59] 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/59] 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 5e775e24fb18855d9a2a144071e66d3c16cab47c Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 12 Jun 2019 14:04:08 +0100 Subject: [PATCH 10/59] Console log more helpfully Appending objects to strings isn't really useful --- src/Lifecycle.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Lifecycle.js b/src/Lifecycle.js index 7c78b71f2b..195377fbe9 100644 --- a/src/Lifecycle.js +++ b/src/Lifecycle.js @@ -233,7 +233,7 @@ function _registerAsGuest(hsUrl, isUrl, defaultDeviceDisplayName) { guest: true, }, true).then(() => true); }, (err) => { - console.error("Failed to register as guest: " + err + " " + err.data); + console.error("Failed to register as guest", err); return false; }); } From 4d8809882f41577ed81a02d6eeabbab0790bb5e3 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Wed, 12 Jun 2019 14:41:45 +0100 Subject: [PATCH 11/59] Raise action bar above read marker Use `z-index` to ensure the action bar appears above over things, like the read marker. Fixes https://github.com/vector-im/riot-web/issues/9619 --- res/css/views/messages/_MessageActionBar.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/res/css/views/messages/_MessageActionBar.scss b/res/css/views/messages/_MessageActionBar.scss index 749cfeebe6..05887eea3e 100644 --- a/res/css/views/messages/_MessageActionBar.scss +++ b/res/css/views/messages/_MessageActionBar.scss @@ -26,6 +26,7 @@ limitations under the License. top: -18px; right: 8px; user-select: none; + z-index: 1; > * { display: inline-block; From 5280d8469be0d94ff92a14558356a8aa712a3cc0 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Wed, 12 Jun 2019 17:23:11 +0100 Subject: [PATCH 12/59] Add comment --- res/css/views/messages/_MessageActionBar.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/res/css/views/messages/_MessageActionBar.scss b/res/css/views/messages/_MessageActionBar.scss index 05887eea3e..685c2bb018 100644 --- a/res/css/views/messages/_MessageActionBar.scss +++ b/res/css/views/messages/_MessageActionBar.scss @@ -26,6 +26,7 @@ limitations under the License. top: -18px; right: 8px; user-select: none; + // Ensure the action bar appears above over things, like the read marker. z-index: 1; > * { From 678fd37549c81f17d09669171a17b37cf4d34ae9 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 12 Jun 2019 18:29:21 +0200 Subject: [PATCH 13/59] 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 14/59] 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 10377b42e9177a3a66475d9fb469237fce609cb5 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 12 Jun 2019 17:42:09 +0100 Subject: [PATCH 15/59] Fix registration with email + non-default HS We were ignoring the hs/is from the query parameters so after clicking the link, the new client tried to use the wrong server. Broken by https://github.com/matrix-org/matrix-react-sdk/pull/2941 Fixes https://github.com/vector-im/riot-web/issues/9659 --- src/components/structures/MatrixChat.js | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 56cf81b985..2af2c6dccb 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -219,6 +219,18 @@ export default React.createClass({ return {serverConfig: props}; }, + getRegisterServerProperties() { + const props = this.getServerProperties(); + if (this.state.register_hs_url) { + props.hsUrl = this.state.register_hs_url; + } + if (this.state.register_is_url) { + props.isUrl = this.state.register_is_url; + } + + return {serverConfig: props}; + }, + componentWillMount: function() { SdkConfig.put(this.props.config); @@ -1871,7 +1883,7 @@ export default React.createClass({ onLoggedIn={this.onRegistered} onLoginClick={this.onLoginClick} onServerConfigChange={this.onServerConfigChange} - {...this.getServerProperties()} + {...this.getRegisterServerProperties()} /> ); } From 41e41269dc0bc42432d4aa6a5a58e7518c6abacf Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 12 Jun 2019 18:52:34 +0200 Subject: [PATCH 16/59] 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 17/59] 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 1139f68de800620d70848aeb4dfbe7f165d16431 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 12 Jun 2019 18:11:58 +0100 Subject: [PATCH 18/59] Pass in object of the right type --- src/components/structures/MatrixChat.js | 25 ++++++++----------------- 1 file changed, 8 insertions(+), 17 deletions(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 2af2c6dccb..15a244b50e 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -51,7 +51,8 @@ import SettingsStore, {SettingLevel} from "../../settings/SettingsStore"; import { startAnyRegistrationFlow } from "../../Registration.js"; import { messageForSyncError } from '../../utils/ErrorUtils'; import ResizeNotifier from "../../utils/ResizeNotifier"; -import {ValidatedServerConfig} from "../../utils/AutoDiscoveryUtils"; +import { ValidatedServerConfig } from "../../utils/AutoDiscoveryUtils"; +import AutoDiscoveryUtils from "../../utils/AutoDiscoveryUtils"; // Disable warnings for now: we use deprecated bluebird functions // and need to migrate, but they spam the console with warnings. @@ -219,18 +220,6 @@ export default React.createClass({ return {serverConfig: props}; }, - getRegisterServerProperties() { - const props = this.getServerProperties(); - if (this.state.register_hs_url) { - props.hsUrl = this.state.register_hs_url; - } - if (this.state.register_is_url) { - props.isUrl = this.state.register_is_url; - } - - return {serverConfig: props}; - }, - componentWillMount: function() { SdkConfig.put(this.props.config); @@ -688,7 +677,7 @@ export default React.createClass({ }); }, - _startRegistration: function(params) { + _startRegistration: async function(params) { const newState = { view: VIEWS.REGISTER, }; @@ -701,10 +690,12 @@ export default React.createClass({ params.is_url && params.sid ) { + newState.serverConfig = await AutoDiscoveryUtils.validateServerConfigWithStaticUrls( + params.hs_url, params.is_url, + ); + newState.register_client_secret = params.client_secret; newState.register_session_id = params.session_id; - newState.register_hs_url = params.hs_url; - newState.register_is_url = params.is_url; newState.register_id_sid = params.sid; } @@ -1883,7 +1874,7 @@ export default React.createClass({ onLoggedIn={this.onRegistered} onLoginClick={this.onLoginClick} onServerConfigChange={this.onServerConfigChange} - {...this.getRegisterServerProperties()} + {...this.getServerProperties()} /> ); } From acb813372cea786d90c295485c5a3ef7c86d7e2c Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 12 Jun 2019 21:32:47 +0100 Subject: [PATCH 19/59] Restore Composer History under shift-up & down Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/ComposerHistoryManager.js | 86 +++++++++++++++++++ .../views/rooms/MessageComposerInput.js | 83 ++++++++++++++++-- 2 files changed, 160 insertions(+), 9 deletions(-) create mode 100644 src/ComposerHistoryManager.js diff --git a/src/ComposerHistoryManager.js b/src/ComposerHistoryManager.js new file mode 100644 index 0000000000..1b3fb588eb --- /dev/null +++ b/src/ComposerHistoryManager.js @@ -0,0 +1,86 @@ +//@flow +/* +Copyright 2017 Aviral Dasgupta + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import {Value} from 'slate'; + +import _clamp from 'lodash/clamp'; + +type MessageFormat = 'rich' | 'markdown'; + +class HistoryItem { + // We store history items in their native format to ensure history is accurate + // and then convert them if our RTE has subsequently changed format. + value: Value; + format: MessageFormat = 'rich'; + + constructor(value: ?Value, format: ?MessageFormat) { + this.value = value; + this.format = format; + } + + static fromJSON(obj: Object): HistoryItem { + return new HistoryItem( + Value.fromJSON(obj.value), + obj.format, + ); + } + + toJSON(): Object { + return { + value: this.value.toJSON(), + format: this.format, + }; + } +} + +export default class ComposerHistoryManager { + history: Array = []; + prefix: string; + lastIndex: number = 0; // used for indexing the storage + currentIndex: number = 0; // used for indexing the loaded validated history Array + + constructor(roomId: string, prefix: string = 'mx_composer_history_') { + this.prefix = prefix + roomId; + + // TODO: Performance issues? + let item; + for (; item = sessionStorage.getItem(`${this.prefix}[${this.currentIndex}]`); this.currentIndex++) { + try { + this.history.push( + HistoryItem.fromJSON(JSON.parse(item)), + ); + } catch (e) { + console.warn("Throwing away unserialisable history", e); + } + } + this.lastIndex = this.currentIndex; + // reset currentIndex to account for any unserialisable history + this.currentIndex = this.history.length; + } + + save(value: Value, format: MessageFormat) { + const item = new HistoryItem(value, format); + this.history.push(item); + this.currentIndex = this.history.length; + sessionStorage.setItem(`${this.prefix}[${this.lastIndex++}]`, JSON.stringify(item.toJSON())); + } + + getItem(offset: number): ?HistoryItem { + this.currentIndex = _clamp(this.currentIndex + offset, 0, this.history.length - 1); + return this.history[this.currentIndex]; + } +} diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index 939ec4d9f5..6efea9a67d 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -60,6 +60,7 @@ import ReplyThread from "../elements/ReplyThread"; import {ContentHelpers} from 'matrix-js-sdk'; import AccessibleButton from '../elements/AccessibleButton'; import {findEditableEvent} from '../../../utils/EventUtils'; +import ComposerHistoryManager from "../../../ComposerHistoryManager"; const REGEX_EMOTICON_WHITESPACE = new RegExp('(?:^|\\s)(' + EMOTICON_REGEX.source + ')\\s$'); @@ -140,6 +141,7 @@ export default class MessageComposerInput extends React.Component { client: MatrixClient; autocomplete: Autocomplete; + historyManager: ComposerHistoryManager; constructor(props, context) { super(props, context); @@ -329,6 +331,7 @@ export default class MessageComposerInput extends React.Component { componentWillMount() { this.dispatcherRef = dis.register(this.onAction); + this.historyManager = new ComposerHistoryManager(this.props.room.roomId, 'mx_slate_composer_history_'); } componentWillUnmount() { @@ -1039,6 +1042,7 @@ export default class MessageComposerInput extends React.Component { if (cmd) { if (!cmd.error) { + this.historyManager.save(editorState, this.state.isRichTextEnabled ? 'rich' : 'markdown'); this.setState({ editorState: this.createEditorState(), }, ()=>{ @@ -1116,6 +1120,8 @@ export default class MessageComposerInput extends React.Component { let sendHtmlFn = ContentHelpers.makeHtmlMessage; let sendTextFn = ContentHelpers.makeTextMessage; + this.historyManager.save(editorState, this.state.isRichTextEnabled ? 'rich' : 'markdown'); + if (commandText && commandText.startsWith('/me')) { if (replyingToEv) { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); @@ -1175,7 +1181,7 @@ export default class MessageComposerInput extends React.Component { }; onVerticalArrow = (e, up) => { - if (e.ctrlKey || e.shiftKey || e.altKey || e.metaKey) { + if (e.ctrlKey || e.altKey || e.metaKey) { return; } @@ -1191,15 +1197,26 @@ export default class MessageComposerInput extends React.Component { 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, - }); + 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); @@ -1207,6 +1224,54 @@ export default class MessageComposerInput extends React.Component { } }; + selectHistory = async (up) => { + const delta = up ? -1 : 1; + + // True if we are not currently selecting history, but composing a messag + if (this.historyManager.currentIndex === this.historyManager.history.length) { + // We can't go any further - there isn't any more history, so nop. + if (!up) { + return; + } + this.setState({ + currentlyComposedEditorState: this.state.editorState, + }); + } else if (this.historyManager.currentIndex + delta === this.historyManager.history.length) { + // True when we return to the message being composed currently + this.setState({ + editorState: this.state.currentlyComposedEditorState, + }); + this.historyManager.currentIndex = this.historyManager.history.length; + return; + } + + let editorState; + const historyItem = this.historyManager.getItem(delta); + if (!historyItem) return; + + if (historyItem.format === 'rich' && !this.state.isRichTextEnabled) { + editorState = this.richToMdEditorState(historyItem.value); + } else if (historyItem.format === 'markdown' && this.state.isRichTextEnabled) { + editorState = this.mdToRichEditorState(historyItem.value); + } else { + editorState = historyItem.value; + } + + // Move selection to the end of the selected history + const change = editorState.change().moveToEndOfNode(editorState.document); + + // We don't call this.onChange(change) now, as fixups on stuff like pills + // should already have been done and persisted in the history. + editorState = change.value; + + this.suppressAutoComplete = true; + + this.setState({ editorState }, ()=>{ + this._editor.focus(); + }); + return true; + }; + onTab = async (e) => { this.setState({ someCompletions: null, 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 20/59] 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 21/59] 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 22/59] 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 23/59] 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 24/59] 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 25/59] 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 26/59] 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 27/59] 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 } From 0b17812b9cfc412a7a2cde11bcffab6722d1c414 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 14 Jun 2019 11:01:34 +0200 Subject: [PATCH 28/59] allow editing emotes --- src/utils/EventUtils.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/utils/EventUtils.js b/src/utils/EventUtils.js index 219b53bc5e..ffc47e2277 100644 --- a/src/utils/EventUtils.js +++ b/src/utils/EventUtils.js @@ -46,9 +46,12 @@ export function isContentActionable(mxEvent) { } export function canEditContent(mxEvent) { - return mxEvent.status !== EventStatus.CANCELLED && - mxEvent.getType() === 'm.room.message' && - mxEvent.getOriginalContent().msgtype === "m.text" && + if (mxEvent.status === EventStatus.CANCELLED || mxEvent.getType() !== "m.room.message") { + return false; + } + const content = mxEvent.getOriginalContent(); + const {msgtype} = content; + return (msgtype === "m.text" || msgtype === "m.emote") && mxEvent.getSender() === MatrixClientPeg.get().getUserId(); } From aecfbce55cb70fd6acad41847c95e75b97c04afb Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 14 Jun 2019 11:01:52 +0200 Subject: [PATCH 29/59] prepend "/me " to emotes when parsing them to edit --- src/editor/deserialize.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/editor/deserialize.js b/src/editor/deserialize.js index 48625cba5f..2d98bbc41a 100644 --- a/src/editor/deserialize.js +++ b/src/editor/deserialize.js @@ -207,12 +207,13 @@ function parseHtmlMessage(html, room, client) { export function parseEvent(event, room, client) { const content = event.getContent(); + let parts; if (content.format === "org.matrix.custom.html") { - return parseHtmlMessage(content.formatted_body || "", room, client); + parts = parseHtmlMessage(content.formatted_body || "", room, client); } else { const body = content.body || ""; const lines = body.split("\n"); - const parts = lines.reduce((parts, line, i) => { + parts = lines.reduce((parts, line, i) => { const isLast = i === lines.length - 1; const text = new PlainPart(line); const newLine = !isLast && new NewlinePart("\n"); @@ -222,6 +223,9 @@ export function parseEvent(event, room, client) { return parts.concat(text); } }, []); - return parts; } + if (content.msgtype === "m.emote") { + parts.unshift(new PlainPart("/me ")); + } + return parts; } From 3cfdd518ee51598d6fdbf3a359d86c08cfb88cd1 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 14 Jun 2019 11:02:20 +0200 Subject: [PATCH 30/59] detect emote when sending (and trim "/me " for content) --- src/components/views/elements/MessageEditor.js | 18 +++++++++++++++--- src/editor/model.js | 8 ++++++-- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/src/components/views/elements/MessageEditor.js b/src/components/views/elements/MessageEditor.js index 0aff6781ee..9faae4588b 100644 --- a/src/components/views/elements/MessageEditor.js +++ b/src/components/views/elements/MessageEditor.js @@ -150,16 +150,28 @@ export default class MessageEditor extends React.Component { dis.dispatch({action: 'focus_composer'}); } + _isEmote() { + const firstPart = this.model.parts[0]; + return firstPart && firstPart.type === "plain" && firstPart.text.startsWith("/me "); + } + _sendEdit = () => { + const isEmote = this._isEmote(); + let model = this.model; + if (isEmote) { + // trim "/me " + model = model.clone(); + model.removeText({index: 0, offset: 0}, 4); + } const newContent = { - "msgtype": "m.text", - "body": textSerialize(this.model), + "msgtype": isEmote ? "m.emote" : "m.text", + "body": textSerialize(model), }; const contentBody = { msgtype: newContent.msgtype, body: ` * ${newContent.body}`, }; - const formattedBody = htmlSerializeIfNeeded(this.model); + const formattedBody = htmlSerializeIfNeeded(model); if (formattedBody) { newContent.format = "org.matrix.custom.html"; newContent.formatted_body = formattedBody; diff --git a/src/editor/model.js b/src/editor/model.js index 04a56ab65b..7cc6041044 100644 --- a/src/editor/model.js +++ b/src/editor/model.js @@ -27,6 +27,10 @@ export default class EditorModel { this._updateCallback = updateCallback; } + clone() { + return new EditorModel(this._parts, this._partCreator, this._updateCallback); + } + _insertPart(index, part) { this._parts.splice(index, 0, part); if (this._activePartIdx >= index) { @@ -91,7 +95,7 @@ export default class EditorModel { const position = this.positionForOffset(diff.at, caret.atNodeEnd); let removedOffsetDecrease = 0; if (diff.removed) { - removedOffsetDecrease = this._removeText(position, diff.removed.length); + removedOffsetDecrease = this.removeText(position, diff.removed.length); } let addedLen = 0; if (diff.added) { @@ -177,7 +181,7 @@ export default class EditorModel { * @return {Number} how many characters before pos were also removed, * usually because of non-editable parts that can only be removed in their entirety. */ - _removeText(pos, len) { + removeText(pos, len) { let {index, offset} = pos; let removedOffsetDecrease = 0; while (len > 0) { From 6a10f0068dcc071cc200164448c711385f516a74 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 14 Jun 2019 12:26:52 +0100 Subject: [PATCH 31/59] Use Alt-UP/DOWN for Composer History instead of random room change Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/structures/LoggedInView.js | 10 ---------- src/components/views/rooms/MessageComposerInput.js | 2 +- 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/src/components/structures/LoggedInView.js b/src/components/structures/LoggedInView.js index 6c05b45111..cd752fc2ce 100644 --- a/src/components/structures/LoggedInView.js +++ b/src/components/structures/LoggedInView.js @@ -292,16 +292,6 @@ const LoggedInView = React.createClass({ const ctrlCmdOnly = isOnlyCtrlOrCmdKeyEvent(ev); switch (ev.keyCode) { - case KeyCode.UP: - case KeyCode.DOWN: - if (ev.altKey && !ev.shiftKey && !ev.ctrlKey && !ev.metaKey) { - const action = ev.keyCode == KeyCode.UP ? - 'view_prev_room' : 'view_next_room'; - dis.dispatch({action: action}); - handled = true; - } - break; - case KeyCode.PAGE_UP: case KeyCode.PAGE_DOWN: if (!ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) { diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index 95b59e048d..09dd12e494 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -1241,7 +1241,7 @@ export default class MessageComposerInput extends React.Component { selectHistory = async (up) => { const delta = up ? -1 : 1; - // True if we are not currently selecting history, but composing a messag + // True if we are not currently selecting history, but composing a message if (this.historyManager.currentIndex === this.historyManager.history.length) { // We can't go any further - there isn't any more history, so nop. if (!up) { From 8fa50b26a614ba4bc548aea95592edf0325a1ee3 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 14 Jun 2019 15:31:19 +0100 Subject: [PATCH 32/59] Fix welcome user https://github.com/matrix-org/matrix-react-sdk/pull/3101 meant we don't get logged straight in after registering if using an email address, but this was the point at which we made a chat with the welcome user. Instead, set a flag in memory that we should try & make a chat with the welcome user for that user ID if we get a session for them. Of course, if the user logs in on both tabs, this would mean each would make a chat with the welcome user (although actually this was a problem with the old code too). Check our m.direct to see if we've started a chat with the welcome user before making one (which also means we have to make sure the cached sync is up to date... see comments). --- src/MatrixClientPeg.js | 26 +++++++ src/components/structures/MatrixChat.js | 74 ++++++++++++++----- .../structures/auth/Registration.js | 3 + 3 files changed, 85 insertions(+), 18 deletions(-) diff --git a/src/MatrixClientPeg.js b/src/MatrixClientPeg.js index 574f05bf85..07499a3a87 100644 --- a/src/MatrixClientPeg.js +++ b/src/MatrixClientPeg.js @@ -51,6 +51,7 @@ interface MatrixClientCreds { class MatrixClientPeg { constructor() { this.matrixClient = null; + this._justRegisteredUserId = null; // These are the default options used when when the // client is started in 'start'. These can be altered @@ -85,6 +86,31 @@ class MatrixClientPeg { MatrixActionCreators.stop(); } + /* + * If we've registered a user ID we set this to the ID of the + * user we've just registered. If they then go & log in, we + * can send them to the welcome user (obviously this doesn't + * guarentee they'll get a chat with the welcome user). + * + * @param {string} uid The user ID of the user we've just registered + */ + setJustRegisteredUserId(uid) { + this._justRegisteredUserId = uid; + } + + /* + * Returns true if the current user has just been registered by this + * client as determined by setJustRegisteredUserId() + * + * @returns {bool} True if user has just been registered + */ + currentUserIsJustRegistered() { + return ( + this.matrixClient && + this.matrixClient.credentials.userId === this._justRegisteredUserId + ); + } + /** * Replace this MatrixClientPeg's client with a client instance that has * homeserver / identity server URLs and active credentials diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 15a244b50e..aa579ea8f9 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -53,6 +53,7 @@ import { messageForSyncError } from '../../utils/ErrorUtils'; import ResizeNotifier from "../../utils/ResizeNotifier"; import { ValidatedServerConfig } from "../../utils/AutoDiscoveryUtils"; import AutoDiscoveryUtils from "../../utils/AutoDiscoveryUtils"; +import DMRoomMap from '../../utils/DMRoomMap'; // Disable warnings for now: we use deprecated bluebird functions // and need to migrate, but they spam the console with warnings. @@ -887,6 +888,7 @@ export default React.createClass({ } return; } + MatrixClientPeg.setJustRegisteredUserId(credentials.user_id); this.onRegistered(credentials); }, onDifferentServerClicked: (ev) => { @@ -1134,26 +1136,65 @@ export default React.createClass({ /** * Called when a new logged in session has started */ - _onLoggedIn: async function() { + _onLoggedIn: function() { this.setStateForNewView({ view: VIEWS.LOGGED_IN }); - if (this._is_registered) { - this._is_registered = false; + if (MatrixClientPeg.currentUserIsJustRegistered()) { + MatrixClientPeg.setJustRegisteredUserId(null); if (this.props.config.welcomeUserId && getCurrentLanguage().startsWith("en")) { - const roomId = await createRoom({ - dmUserId: this.props.config.welcomeUserId, - // Only view the welcome user if we're NOT looking at a room - andView: !this.state.currentRoomId, - }); - // if successful, return because we're already - // viewing the welcomeUserId room - // else, if failed, fall through to view_home_page - if (roomId) { - return; + // We can end up with multiple tabs post-registration where the user + // might then end up with a session and we don't want them all making + // a chat with the welcome user: try to de-dupe. + // We need to wait for the first sync to complete for this to + // work though. + let waitFor; + if (!this.firstSyncComplete) { + waitFor = this.firstSyncPromise.promise; + } else { + waitFor = Promise.resolve(); } + waitFor.then(async () => { + const welcomeUserRooms = DMRoomMap.shared().getDMRoomsForUserId( + this.props.config.welcomeUserId, + ); + if (welcomeUserRooms.length === 0) { + const roomId = await createRoom({ + dmUserId: this.props.config.welcomeUserId, + // Only view the welcome user if we're NOT looking at a room + andView: !this.state.currentRoomId, + }); + // This is a bit of a hack, but since the deduplication relies + // on m.direct being up to date, we need to force a sync + // of the database, otherwise if the user goes to the other + // tab before the next save happens (a few minutes), the + // saved sync will be restored from the db and this code will + // run without the update to m.direct, making another welcome + // user room. + const saveWelcomeUser = (ev) => { + if ( + ev.getType() == 'm.direct' && + ev.getContent() && + ev.getContent()[this.props.config.welcomeUserId] + ) { + MatrixClientPeg.get().store.save(true); + MatrixClientPeg.get().removeListener( + "accountData", saveWelcomeUser, + ); + } + }; + MatrixClientPeg.get().on("accountData", saveWelcomeUser); + + // if successful, return because we're already + // viewing the welcomeUserId room + // else, if failed, fall through to view_home_page + if (roomId) { + return; + } + } + // The user has just logged in after registering + dis.dispatch({action: 'view_home_page'}); + }); } - // The user has just logged in after registering - dis.dispatch({action: 'view_home_page'}); } else { this._showScreenAfterLogin(); } @@ -1694,9 +1735,6 @@ export default React.createClass({ return MatrixClientPeg.get(); } } - // XXX: This should be in state or ideally store(s) because we risk not - // rendering the most up-to-date view of state otherwise. - this._is_registered = true; return Lifecycle.setLoggedIn(credentials); }, diff --git a/src/components/structures/auth/Registration.js b/src/components/structures/auth/Registration.js index 3103ee41df..1957275505 100644 --- a/src/components/structures/auth/Registration.js +++ b/src/components/structures/auth/Registration.js @@ -29,6 +29,7 @@ import * as ServerType from '../../views/auth/ServerTypeSelector'; import AutoDiscoveryUtils, {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils"; import classNames from "classnames"; import * as Lifecycle from '../../../Lifecycle'; +import MatrixClientPeg from "../../../MatrixClientPeg"; // Phases // Show controls to configure server details @@ -287,6 +288,8 @@ module.exports = React.createClass({ return; } + MatrixClientPeg.setJustRegisteredUserId(response.user_id); + const newState = { doingUIAuth: false, }; From 929e9dc6533a8513f218060fdd4440a4c4d205a6 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 14 Jun 2019 16:04:09 +0100 Subject: [PATCH 33/59] Don't forget to show the homepage if no welcome user --- src/components/structures/MatrixChat.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index aa579ea8f9..0b15b727ec 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -1191,9 +1191,14 @@ export default React.createClass({ return; } } - // The user has just logged in after registering + // We didn't rediret to the welcome user room, so show + // the homepage. dis.dispatch({action: 'view_home_page'}); }); + } else { + // The user has just logged in after registering, + // so show the homepage. + dis.dispatch({action: 'view_home_page'}); } } else { this._showScreenAfterLogin(); From 30726d6cf95be8a1b7e913102665a5fdc3bb6fd6 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 14 Jun 2019 16:29:26 +0100 Subject: [PATCH 34/59] Pull out welcome user chat code to a separate function also expand on comment --- src/components/structures/MatrixChat.js | 111 +++++++++++++----------- 1 file changed, 59 insertions(+), 52 deletions(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 0b15b727ec..4fc64b91bb 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -1133,68 +1133,75 @@ export default React.createClass({ } }, + /** + * Starts a chat with the welcome user, if the user doesn't already have one + * @returns {string} The room ID of the new room, or null if no room was created + */ + async _startWelcomeUserChat() { + // We can end up with multiple tabs post-registration where the user + // might then end up with a session and we don't want them all making + // a chat with the welcome user: try to de-dupe. + // We need to wait for the first sync to complete for this to + // work though. + let waitFor; + if (!this.firstSyncComplete) { + waitFor = this.firstSyncPromise.promise; + } else { + waitFor = Promise.resolve(); + } + await waitFor; + + const welcomeUserRooms = DMRoomMap.shared().getDMRoomsForUserId( + this.props.config.welcomeUserId, + ); + if (welcomeUserRooms.length === 0) { + const roomId = await createRoom({ + dmUserId: this.props.config.welcomeUserId, + // Only view the welcome user if we're NOT looking at a room + andView: !this.state.currentRoomId, + }); + // This is a bit of a hack, but since the deduplication relies + // on m.direct being up to date, we need to force a sync + // of the database, otherwise if the user goes to the other + // tab before the next save happens (a few minutes), the + // saved sync will be restored from the db and this code will + // run without the update to m.direct, making another welcome + // user room (it doesn't wait for new data from the server, just + // the saved sync to be loaded). + const saveWelcomeUser = (ev) => { + if ( + ev.getType() == 'm.direct' && + ev.getContent() && + ev.getContent()[this.props.config.welcomeUserId] + ) { + MatrixClientPeg.get().store.save(true); + MatrixClientPeg.get().removeListener( + "accountData", saveWelcomeUser, + ); + } + }; + MatrixClientPeg.get().on("accountData", saveWelcomeUser); + + return roomId; + } + return null; + }, + /** * Called when a new logged in session has started */ - _onLoggedIn: function() { + _onLoggedIn: async function() { this.setStateForNewView({ view: VIEWS.LOGGED_IN }); - if (MatrixClientPeg.currentUserIsJustRegistered()) { + if (true || MatrixClientPeg.currentUserIsJustRegistered()) { MatrixClientPeg.setJustRegisteredUserId(null); if (this.props.config.welcomeUserId && getCurrentLanguage().startsWith("en")) { - // We can end up with multiple tabs post-registration where the user - // might then end up with a session and we don't want them all making - // a chat with the welcome user: try to de-dupe. - // We need to wait for the first sync to complete for this to - // work though. - let waitFor; - if (!this.firstSyncComplete) { - waitFor = this.firstSyncPromise.promise; - } else { - waitFor = Promise.resolve(); - } - waitFor.then(async () => { - const welcomeUserRooms = DMRoomMap.shared().getDMRoomsForUserId( - this.props.config.welcomeUserId, - ); - if (welcomeUserRooms.length === 0) { - const roomId = await createRoom({ - dmUserId: this.props.config.welcomeUserId, - // Only view the welcome user if we're NOT looking at a room - andView: !this.state.currentRoomId, - }); - // This is a bit of a hack, but since the deduplication relies - // on m.direct being up to date, we need to force a sync - // of the database, otherwise if the user goes to the other - // tab before the next save happens (a few minutes), the - // saved sync will be restored from the db and this code will - // run without the update to m.direct, making another welcome - // user room. - const saveWelcomeUser = (ev) => { - if ( - ev.getType() == 'm.direct' && - ev.getContent() && - ev.getContent()[this.props.config.welcomeUserId] - ) { - MatrixClientPeg.get().store.save(true); - MatrixClientPeg.get().removeListener( - "accountData", saveWelcomeUser, - ); - } - }; - MatrixClientPeg.get().on("accountData", saveWelcomeUser); - - // if successful, return because we're already - // viewing the welcomeUserId room - // else, if failed, fall through to view_home_page - if (roomId) { - return; - } - } + const welcomeUserRoom = await this._startWelcomeUserChat(); + if (welcomeUserRoom === null) { // We didn't rediret to the welcome user room, so show // the homepage. dis.dispatch({action: 'view_home_page'}); - }); + } } else { // The user has just logged in after registering, // so show the homepage. From 794b04b89a0eb79c3f61626506374935729cd21d Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 14 Jun 2019 16:41:30 +0100 Subject: [PATCH 35/59] take the debugging out --- src/components/structures/MatrixChat.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 4fc64b91bb..789649c220 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -1192,7 +1192,7 @@ export default React.createClass({ */ _onLoggedIn: async function() { this.setStateForNewView({ view: VIEWS.LOGGED_IN }); - if (true || MatrixClientPeg.currentUserIsJustRegistered()) { + if (MatrixClientPeg.currentUserIsJustRegistered()) { MatrixClientPeg.setJustRegisteredUserId(null); if (this.props.config.welcomeUserId && getCurrentLanguage().startsWith("en")) { From 4c036c98ee9bdff4f4f8a75aa1cd614aefb8ca2e Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 14 Jun 2019 17:21:07 +0100 Subject: [PATCH 36/59] Fix double-spinner On registering, we showed a spinner, and then another spinner on top of the spinner, which led to an interesting spinner-in-box effect. Suppress the second type of spinner when we know we already have one. --- src/components/structures/MatrixChat.js | 1 + src/createRoom.js | 8 ++++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 789649c220..fb35ab548b 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -1159,6 +1159,7 @@ export default React.createClass({ dmUserId: this.props.config.welcomeUserId, // Only view the welcome user if we're NOT looking at a room andView: !this.state.currentRoomId, + spinner: false, // we're already showing one: we don't need another one }); // This is a bit of a hack, but since the deduplication relies // on m.direct being up to date, we need to force a sync diff --git a/src/createRoom.js b/src/createRoom.js index 39b634a0ef..120043247d 100644 --- a/src/createRoom.js +++ b/src/createRoom.js @@ -30,12 +30,15 @@ import {getAddressType} from "./UserAddress"; * @param {object=} opts parameters for creating the room * @param {string=} opts.dmUserId If specified, make this a DM room for this user and invite them * @param {object=} opts.createOpts set of options to pass to createRoom call. + * @param {bool=} opts.spinner True to show a modal spinner while the room is created. + * Default: True * * @returns {Promise} which resolves to the room id, or null if the * action was aborted or failed. */ function createRoom(opts) { opts = opts || {}; + if (opts.spinner === undefined) opts.spinner = true; const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); const Loader = sdk.getComponent("elements.Spinner"); @@ -87,11 +90,12 @@ function createRoom(opts) { }, ]; - const modal = Modal.createDialog(Loader, null, 'mx_Dialog_spinner'); + let modal; + if (opts.spinner) modal = Modal.createDialog(Loader, null, 'mx_Dialog_spinner'); let roomId; return client.createRoom(createOpts).finally(function() { - modal.close(); + if (modal) modal.close(); }).then(function(res) { roomId = res.room_id; if (opts.dmUserId) { From 03c37821f6784b350a1160db2d15bb2c192b3d81 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Sat, 15 Jun 2019 15:10:34 +0100 Subject: [PATCH 37/59] clean up onVerticalArrow Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .../views/rooms/MessageComposerInput.js | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index 09dd12e494..7684e1dced 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -1206,15 +1206,22 @@ export default class MessageComposerInput extends React.Component { onVerticalArrow = (e, up) => { if (e.ctrlKey || e.shiftKey || e.metaKey) return; - // Select history - const selection = this.state.editorState.selection; + if (e.altKey) { + // Try select composer history + const selected = this.selectHistory(up); + if (selected) { + // We're selecting history, so prevent the key event from doing anything else + e.preventDefault(); + } + } else if (!e.altKey && up) { + // Try edit the latest message + 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) { + // and we must be at the edge of the document (up=start, down=end) if (!selection.anchor.isAtStartOfNode(document)) return; if (!e.altKey) { @@ -1227,15 +1234,8 @@ export default class MessageComposerInput extends React.Component { event: editEvent, }); } - return; } } - - const selected = this.selectHistory(up); - if (selected) { - // We're selecting history, so prevent the key event from doing anything else - e.preventDefault(); - } }; selectHistory = async (up) => { From 2fc2e32e60160ee170c663ba1f763bf45966b0ec Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Sun, 16 Jun 2019 11:43:13 +0100 Subject: [PATCH 38/59] Add Upload All button to UploadConfirmDialog Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/ContentMessages.js | 26 ++++++++++++------- .../views/dialogs/UploadConfirmDialog.js | 15 ++++++++++- src/i18n/strings/en_EN.json | 1 + 3 files changed, 31 insertions(+), 11 deletions(-) diff --git a/src/ContentMessages.js b/src/ContentMessages.js index ee3e8f1390..2d58622db8 100644 --- a/src/ContentMessages.js +++ b/src/ContentMessages.js @@ -425,19 +425,25 @@ export default class ContentMessages { } const UploadConfirmDialog = sdk.getComponent("dialogs.UploadConfirmDialog"); + let uploadAll = false; for (let i = 0; i < okFiles.length; ++i) { const file = okFiles[i]; - const shouldContinue = await new Promise((resolve) => { - Modal.createTrackedDialog('Upload Files confirmation', '', UploadConfirmDialog, { - file, - currentIndex: i, - totalFiles: okFiles.length, - onFinished: (shouldContinue) => { - resolve(shouldContinue); - }, + if (!uploadAll) { + const shouldContinue = await new Promise((resolve) => { + Modal.createTrackedDialog('Upload Files confirmation', '', UploadConfirmDialog, { + file, + currentIndex: i, + totalFiles: okFiles.length, + onFinished: (shouldContinue, shouldUploadAll) => { + if (shouldUploadAll) { + uploadAll = true; + } + resolve(shouldContinue); + }, + }); }); - }); - if (!shouldContinue) break; + if (!shouldContinue) break; + } this._sendContentToRoom(file, roomId, matrixClient); } } diff --git a/src/components/views/dialogs/UploadConfirmDialog.js b/src/components/views/dialogs/UploadConfirmDialog.js index e7b22950d6..7e682a8301 100644 --- a/src/components/views/dialogs/UploadConfirmDialog.js +++ b/src/components/views/dialogs/UploadConfirmDialog.js @@ -49,6 +49,10 @@ export default class UploadConfirmDialog extends React.Component { this.props.onFinished(true); } + _onUploadAllClick = () => { + this.props.onFinished(true, true); + } + render() { const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); @@ -85,6 +89,13 @@ export default class UploadConfirmDialog extends React.Component { ; } + let uploadAllButton; + if (this.props.currentIndex + 1 < this.props.totalFiles) { + uploadAllButton = ; + } + return ( + > + {uploadAllButton} + ); } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 53fd82f6f2..e167659621 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1249,6 +1249,7 @@ "Upload files (%(current)s of %(total)s)": "Upload files (%(current)s of %(total)s)", "Upload files": "Upload files", "Upload": "Upload", + "Upload all": "Upload all", "This file is too large to upload. The file size limit is %(limit)s but this file is %(sizeOfThisFile)s.": "This file is too large to upload. The file size limit is %(limit)s but this file is %(sizeOfThisFile)s.", "These files are too large to upload. The file size limit is %(limit)s.": "These files are too large to upload. The file size limit is %(limit)s.", "Some files are too large to be uploaded. The file size limit is %(limit)s.": "Some files are too large to be uploaded. The file size limit is %(limit)s.", From c68074f53293d1b6b2f7391ccd0df7be61dd139c Mon Sep 17 00:00:00 2001 From: Luca Weiss Date: Sun, 16 Jun 2019 21:52:25 +0200 Subject: [PATCH 39/59] Fix display of canonicalAlias in group room info Signed-off-by: Luca Weiss --- src/components/views/groups/GroupRoomInfo.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/groups/GroupRoomInfo.js b/src/components/views/groups/GroupRoomInfo.js index db060218d4..7296b25344 100644 --- a/src/components/views/groups/GroupRoomInfo.js +++ b/src/components/views/groups/GroupRoomInfo.js @@ -224,7 +224,7 @@ module.exports = React.createClass({
- { this.state.groupRoom.canonical_alias } + { this.state.groupRoom.canonicalAlias }
From e3bf4a0b8e9208725502f179f79f1badd5678acf Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 17 Jun 2019 16:27:35 +0100 Subject: [PATCH 40/59] Re-enable register button on change to working HS Register button disabling is done via serverErrorIsFatal so we need to reset this on a successful validation. https://github.com/vector-im/riot-web/issues/10029 --- src/components/structures/auth/Registration.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/structures/auth/Registration.js b/src/components/structures/auth/Registration.js index 1957275505..20b13e4da9 100644 --- a/src/components/structures/auth/Registration.js +++ b/src/components/structures/auth/Registration.js @@ -180,7 +180,10 @@ module.exports = React.createClass({ serverConfig.hsUrl, serverConfig.isUrl, ); - this.setState({serverIsAlive: true}); + this.setState({ + serverIsAlive: true, + serverErrorIsFatal: false, + }); } catch (e) { this.setState({ busy: false, From 3f6bb0c7cad6fa788ae5019f8993e1db981f8cd8 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 17 Jun 2019 16:57:13 +0100 Subject: [PATCH 41/59] Use default cursor for disabled submit button --- res/css/structures/auth/_Login.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/res/css/structures/auth/_Login.scss b/res/css/structures/auth/_Login.scss index 9bcd79a357..68e5f5c19f 100644 --- a/res/css/structures/auth/_Login.scss +++ b/res/css/structures/auth/_Login.scss @@ -30,6 +30,7 @@ limitations under the License. .mx_Login_submit:disabled { opacity: 0.3; + cursor: default; } .mx_AuthBody a.mx_Login_sso_link:link, From 872c1acdea0126243866f815d0374d58caa145cb Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 17 Jun 2019 18:29:03 +0200 Subject: [PATCH 42/59] keep mx_Field stretching --- res/css/views/elements/_Field.scss | 2 ++ 1 file changed, 2 insertions(+) diff --git a/res/css/views/elements/_Field.scss b/res/css/views/elements/_Field.scss index f9cbf8c541..a6ac680116 100644 --- a/res/css/views/elements/_Field.scss +++ b/res/css/views/elements/_Field.scss @@ -18,6 +18,8 @@ limitations under the License. .mx_Field { display: flex; + flex: 1; + min-width: 0; position: relative; margin: 1em 0; border-radius: 4px; From 10f6abfe1795e292d1742c52abdaad2d470d8d48 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 17 Jun 2019 18:47:20 +0100 Subject: [PATCH 43/59] Allow changing server if validation has failed Show the server config section if there's an error and fix an if case where we forgot to un-set the busy flag --- src/components/structures/auth/Registration.js | 6 +++++- src/components/views/auth/ServerConfig.js | 3 +++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/components/structures/auth/Registration.js b/src/components/structures/auth/Registration.js index 20b13e4da9..662444379f 100644 --- a/src/components/structures/auth/Registration.js +++ b/src/components/structures/auth/Registration.js @@ -168,6 +168,8 @@ module.exports = React.createClass({ _replaceClient: async function(serverConfig) { this.setState({ errorText: null, + serverDeadError: null, + serverErrorIsFatal: false, // busy while we do liveness check (we need to avoid trying to render // the UI auth component while we don't have a matrix client) busy: true, @@ -429,7 +431,9 @@ module.exports = React.createClass({ // If we're on a different phase, we only show the server type selector, // which is always shown if we allow custom URLs at all. - if (PHASES_ENABLED && this.state.phase !== PHASE_SERVER_DETAILS) { + // (if there's a fatal server error, we need to show the full server + // config as the user may need to change servers to resolve the error). + if (PHASES_ENABLED && this.state.phase !== PHASE_SERVER_DETAILS && !this.state.serverErrorIsFatal) { return
Date: Mon, 17 Jun 2019 13:38:23 -0600 Subject: [PATCH 44/59] Convert IntegrationsManager to a class --- .eslintignore.errorfiles | 1 - .../views/settings/IntegrationsManager.js | 57 +++++++++---------- 2 files changed, 28 insertions(+), 30 deletions(-) diff --git a/.eslintignore.errorfiles b/.eslintignore.errorfiles index a89c083518..9ecd39ffc2 100644 --- a/.eslintignore.errorfiles +++ b/.eslintignore.errorfiles @@ -47,7 +47,6 @@ src/components/views/rooms/UserTile.js src/components/views/settings/ChangeAvatar.js src/components/views/settings/ChangePassword.js src/components/views/settings/DevicesPanel.js -src/components/views/settings/IntegrationsManager.js src/components/views/settings/Notifications.js src/GroupAddressPicker.js src/HtmlUtils.js diff --git a/src/components/views/settings/IntegrationsManager.js b/src/components/views/settings/IntegrationsManager.js index a517771f1d..2e292b63fc 100644 --- a/src/components/views/settings/IntegrationsManager.js +++ b/src/components/views/settings/IntegrationsManager.js @@ -1,5 +1,6 @@ /* Copyright 2015, 2016 OpenMarket Ltd +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. @@ -14,50 +15,48 @@ See the License for the specific language governing permissions and limitations under the License. */ -'use strict'; +import React from 'react'; +import PropTypes from 'prop-types'; +import sdk from '../../../index'; +import MatrixClientPeg from '../../../MatrixClientPeg'; +import { _t } from '../../../languageHandler'; +import Modal from '../../../Modal'; +import dis from '../../../dispatcher'; -const React = require('react'); -const sdk = require('../../../index'); -const MatrixClientPeg = require('../../../MatrixClientPeg'); -const dis = require('../../../dispatcher'); +export default class IntegrationsManager extends React.Component { + static propTypes = { + // the source of the integration manager being embedded + src: PropTypes.string.isRequired, -module.exports = React.createClass({ - displayName: 'IntegrationsManager', + // callback when the manager is dismissed + onFinished: PropTypes.func.isRequired, + }; - propTypes: { - src: React.PropTypes.string.isRequired, // the source of the integration manager being embedded - onFinished: React.PropTypes.func.isRequired, // callback when the lightbox is dismissed - }, - - // XXX: keyboard shortcuts for managing dialogs should be done by the modal - // dialog base class somehow, surely... - componentDidMount: function() { + componentDidMount() { this.dispatcherRef = dis.register(this.onAction); document.addEventListener("keydown", this.onKeyDown); - }, + } - componentWillUnmount: function() { + componentWillUnmount() { dis.unregister(this.dispatcherRef); document.removeEventListener("keydown", this.onKeyDown); - }, + } - onKeyDown: function(ev) { - if (ev.keyCode == 27) { // escape + onKeyDown = (ev) => { + if (ev.keyCode === 27) { // escape ev.stopPropagation(); ev.preventDefault(); this.props.onFinished(); } - }, + }; - onAction: function(payload) { + onAction = (payload) => { if (payload.action === 'close_scalar') { this.props.onFinished(); } - }, + }; - render: function() { - return ( - - ); - }, -}); + render() { + return ; + } +} \ No newline at end of file From 6cc443cd018af9b416e5470b60cddf80d2267334 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 17 Jun 2019 15:27:35 -0600 Subject: [PATCH 45/59] spelling --- src/CallHandler.js | 2 +- src/IntegrationManager.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/CallHandler.js b/src/CallHandler.js index e47209eebe..5b58400ae6 100644 --- a/src/CallHandler.js +++ b/src/CallHandler.js @@ -344,7 +344,7 @@ function _onAction(payload) { } async function _startCallApp(roomId, type) { - // check for a working intgrations manager. Technically we could put + // check for a working integrations manager. Technically we could put // the state event in anyway, but the resulting widget would then not // work for us. Better that the user knows before everyone else in the // room sees it. diff --git a/src/IntegrationManager.js b/src/IntegrationManager.js index 165ee6390d..4ca7fc57dc 100644 --- a/src/IntegrationManager.js +++ b/src/IntegrationManager.js @@ -44,7 +44,7 @@ export default class IntegrationManager { } /** - * Launch the integrations manager on the stickers integration page + * Launch the integrations manager on the specified integration page * @param {string} integName integration / widget type * @param {string} integId integration / widget ID * @param {function} onFinished Callback to invoke on integration manager close From a5f296457f53bd264c8e392c0d54b31c465eb508 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 17 Jun 2019 15:29:19 -0600 Subject: [PATCH 46/59] Make the Manage Integrations Button defer scalar auth to the manager This moves the responsibility of creating a URL to open from the button (and other components) to the integrations manager dialog itself. By doing this, we also cut down on scalar API calls because we don't pick up on account information until the user opens the dialog. --- .../views/settings/_IntegrationsManager.scss | 13 +++ src/ScalarAuthClient.js | 11 ++- .../views/elements/ManageIntegsButton.js | 87 +++---------------- .../views/settings/IntegrationsManager.js | 86 ++++++++++++++++-- src/i18n/strings/en_EN.json | 8 +- 5 files changed, 125 insertions(+), 80 deletions(-) diff --git a/res/css/views/settings/_IntegrationsManager.scss b/res/css/views/settings/_IntegrationsManager.scss index 93ee0e20fe..c5769d3645 100644 --- a/res/css/views/settings/_IntegrationsManager.scss +++ b/res/css/views/settings/_IntegrationsManager.scss @@ -29,3 +29,16 @@ limitations under the License. width: 100%; height: 100%; } + +.mx_IntegrationsManager_loading h3 { + text-align: center; +} + +.mx_IntegrationsManager_error { + text-align: center; + padding-top: 20px; +} + +.mx_IntegrationsManager_error h3 { + color: $warning-color; +} \ No newline at end of file diff --git a/src/ScalarAuthClient.js b/src/ScalarAuthClient.js index 24979aff65..27d8f0d0da 100644 --- a/src/ScalarAuthClient.js +++ b/src/ScalarAuthClient.js @@ -29,6 +29,14 @@ class ScalarAuthClient { this.scalarToken = null; } + /** + * Determines if setting up a ScalarAuthClient is even possible + * @returns {boolean} true if possible, false otherwise. + */ + static isPossible() { + return SdkConfig.get()['integrations_rest_url'] && SdkConfig.get()['integrations_ui_url']; + } + connect() { return this.getScalarToken().then((tok) => { this.scalarToken = tok; @@ -41,7 +49,8 @@ class ScalarAuthClient { // Returns a scalar_token string getScalarToken() { - const token = window.localStorage.getItem("mx_scalar_token"); + let token = this.scalarToken; + if (!token) token = window.localStorage.getItem("mx_scalar_token"); if (!token) { return this.registerForToken(); diff --git a/src/components/views/elements/ManageIntegsButton.js b/src/components/views/elements/ManageIntegsButton.js index 165cd20eb5..f5d087e452 100644 --- a/src/components/views/elements/ManageIntegsButton.js +++ b/src/components/views/elements/ManageIntegsButton.js @@ -1,5 +1,6 @@ /* Copyright 2017 New Vector Ltd +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. @@ -17,10 +18,7 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; import sdk from '../../../index'; -import classNames from 'classnames'; -import SdkConfig from '../../../SdkConfig'; import ScalarAuthClient from '../../../ScalarAuthClient'; -import ScalarMessaging from '../../../ScalarMessaging'; import Modal from "../../../Modal"; import { _t } from '../../../languageHandler'; import AccessibleButton from './AccessibleButton'; @@ -28,85 +26,28 @@ import AccessibleButton from './AccessibleButton'; export default class ManageIntegsButton extends React.Component { constructor(props) { super(props); - - this.state = { - scalarError: null, - }; - - this.onManageIntegrations = this.onManageIntegrations.bind(this); } - componentWillMount() { - ScalarMessaging.startListening(); - this.scalarClient = null; - - if (SdkConfig.get().integrations_ui_url && SdkConfig.get().integrations_rest_url) { - this.scalarClient = new ScalarAuthClient(); - this.scalarClient.connect().done(() => { - this.forceUpdate(); - }, (err) => { - this.setState({scalarError: err}); - console.error('Error whilst initialising scalarClient for ManageIntegsButton', err); - }); - } - } - - componentWillUnmount() { - ScalarMessaging.stopListening(); - } - - onManageIntegrations(ev) { + onManageIntegrations = (ev) => { ev.preventDefault(); - if (this.state.scalarError && !this.scalarClient.hasCredentials()) { - return; - } + const IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager"); - this.scalarClient.connect().done(() => { - Modal.createDialog(IntegrationsManager, { - src: (this.scalarClient !== null && this.scalarClient.hasCredentials()) ? - this.scalarClient.getScalarInterfaceUrlForRoom(this.props.room) : - null, - }, "mx_IntegrationsManager"); - }, (err) => { - this.setState({scalarError: err}); - console.error('Error ensuring a valid scalar_token exists', err); - }); - } + Modal.createDialog(IntegrationsManager, { + room: this.props.room, + }, "mx_IntegrationsManager"); + }; render() { let integrationsButton =
; - let integrationsWarningTriangle =
; - let integrationsErrorPopup =
; - if (this.scalarClient !== null) { - const integrationsButtonClasses = classNames({ - mx_RoomHeader_button: true, - mx_RoomHeader_manageIntegsButton: true, - mx_ManageIntegsButton_error: !!this.state.scalarError, - }); - - if (this.state.scalarError && !this.scalarClient.hasCredentials()) { - integrationsWarningTriangle = ; - // Popup shown when hovering over integrationsButton_error (via CSS) - integrationsErrorPopup = ( - - { _t('Could not connect to the integration server') } - - ); - } - + if (ScalarAuthClient.isPossible()) { + const AccessibleButton = sdk.getComponent("elements.AccessibleButton"); integrationsButton = ( - - { integrationsWarningTriangle } - { integrationsErrorPopup } - - ); + /> + ) } return integrationsButton; diff --git a/src/components/views/settings/IntegrationsManager.js b/src/components/views/settings/IntegrationsManager.js index 2e292b63fc..d9bf6351e9 100644 --- a/src/components/views/settings/IntegrationsManager.js +++ b/src/components/views/settings/IntegrationsManager.js @@ -18,20 +18,68 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; import sdk from '../../../index'; -import MatrixClientPeg from '../../../MatrixClientPeg'; import { _t } from '../../../languageHandler'; -import Modal from '../../../Modal'; import dis from '../../../dispatcher'; +import ScalarAuthClient from '../../../ScalarAuthClient'; export default class IntegrationsManager extends React.Component { static propTypes = { - // the source of the integration manager being embedded - src: PropTypes.string.isRequired, + // the room object where the integrations manager should be opened in + room: PropTypes.object.isRequired, + + // the screen name to open + screen: PropTypes.string, + + // the integration ID to open + integrationId: PropTypes.string, // callback when the manager is dismissed onFinished: PropTypes.func.isRequired, }; + constructor(props) { + super(props); + + this.state = { + loading: true, + configured: ScalarAuthClient.isPossible(), + connected: false, // true if a `src` is set and able to be connected to + src: null, // string for where to connect to + }; + } + + componentWillMount() { + if (!this.state.configured) return; + + const scalarClient = new ScalarAuthClient(); + scalarClient.connect().then(() => { + const hasCredentials = scalarClient.hasCredentials(); + if (!hasCredentials) { + this.setState({ + connected: false, + loading: false, + }); + } else { + const src = scalarClient.getScalarInterfaceUrlForRoom( + this.props.room, + this.props.screen, + this.props.integrationId, + ); + this.setState({ + loading: false, + connected: true, + src: src, + }); + } + }).catch(err => { + console.error(err); + this.setState({ + loading: false, + connected: false, + }); + }) + } + componentDidMount() { this.dispatcherRef = dis.register(this.onAction); document.addEventListener("keydown", this.onKeyDown); @@ -57,6 +105,34 @@ export default class IntegrationsManager extends React.Component { }; render() { - return ; + if (!this.state.configured) { + return ( +
+

{_t("No integrations server configured")}

+

{_t("This Riot instance does not have an integrations server configured.")}

+
+ ); + } + + if (this.state.loading) { + const Spinner = sdk.getComponent("elements.Spinner"); + return ( +
+

{_t("Connecting to integrations server...")}

+ +
+ ); + } + + if (!this.state.connected) { + return ( +
+

{_t("Cannot connect to integrations server")}

+

{_t("The integrations server is offline or it cannot reach your homeserver.")}

+
+ ); + } + + return ; } } \ No newline at end of file diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 53fd82f6f2..c1fd3662ee 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -483,6 +483,11 @@ "Email Address": "Email Address", "Disable Notifications": "Disable Notifications", "Enable Notifications": "Enable Notifications", + "No integrations server configured": "No integrations server configured", + "This Riot instance does not have an integrations server configured.": "This Riot instance does not have an integrations server configured.", + "Connecting to integrations server...": "Connecting to integrations server...", + "Cannot connect to integrations server": "Cannot connect to integrations server", + "The integrations server is offline or it cannot reach your homeserver.": "The integrations server is offline or it cannot reach your homeserver.", "Delete Backup": "Delete Backup", "Are you sure? You will lose your encrypted messages if your keys are not backed up properly.": "Are you sure? You will lose your encrypted messages if your keys are not backed up properly.", "Encrypted messages are secured with end-to-end encryption. Only you and the recipient(s) have the keys to read these messages.": "Encrypted messages are secured with end-to-end encryption. Only you and the recipient(s) have the keys to read these messages.", @@ -864,6 +869,8 @@ "This Room": "This Room", "All Rooms": "All Rooms", "Search…": "Search…", + "Failed to connect to integrations server": "Failed to connect to integrations server", + "No integrations server is configured to manage stickers with": "No integrations server is configured to manage stickers with", "You don't currently have any stickerpacks enabled": "You don't currently have any stickerpacks enabled", "Add some now": "Add some now", "Stickerpack": "Stickerpack", @@ -1017,7 +1024,6 @@ "Rotate Right": "Rotate Right", "Rotate clockwise": "Rotate clockwise", "Download this file": "Download this file", - "Integrations Error": "Integrations Error", "Manage Integrations": "Manage Integrations", "%(nameList)s %(transitionList)s": "%(nameList)s %(transitionList)s", "%(severalUsers)sjoined %(count)s times|other": "%(severalUsers)sjoined %(count)s times", From ebabc5238d068509e3c8dd9d40c6d21ccd8c1218 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 17 Jun 2019 15:30:01 -0600 Subject: [PATCH 47/59] Port integration manager class to new dialog props --- src/IntegrationManager.js | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/IntegrationManager.js b/src/IntegrationManager.js index 4ca7fc57dc..9cd984f12f 100644 --- a/src/IntegrationManager.js +++ b/src/IntegrationManager.js @@ -1,5 +1,6 @@ /* Copyright 2017 New Vector Ltd +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. @@ -19,11 +20,14 @@ import SdkConfig from './SdkConfig'; import ScalarMessaging from './ScalarMessaging'; import ScalarAuthClient from './ScalarAuthClient'; import RoomViewStore from './stores/RoomViewStore'; +import MatrixClientPeg from "./MatrixClientPeg"; if (!global.mxIntegrationManager) { global.mxIntegrationManager = {}; } +// TODO: TravisR - What even is this? + export default class IntegrationManager { static _init() { if (!global.mxIntegrationManager.client || !global.mxIntegrationManager.connected) { @@ -62,16 +66,10 @@ export default class IntegrationManager { console.error("Scalar error", global.mxIntegrationManager); return; } - const integType = 'type_' + integName; - const src = (global.mxIntegrationManager.client && global.mxIntegrationManager.client.hasCredentials()) ? - global.mxIntegrationManager.client.getScalarInterfaceUrlForRoom( - {roomId: RoomViewStore.getRoomId()}, - integType, - integId, - ) : - null; Modal.createTrackedDialog('Integrations Manager', '', IntegrationsManager, { - src: src, + room: MatrixClientPeg.get().getRoom(RoomViewStore.getRoomId()), + screen: 'type_' + integName, + integrationId: integId, onFinished: onFinished, }, "mx_IntegrationsManager"); } From f699fed720a2689b48810685c3df2f034841defa Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 17 Jun 2019 15:30:24 -0600 Subject: [PATCH 48/59] Defer sticker picker scalar auth to integration manager dialog or when needed, instead of up front. --- src/components/views/rooms/Stickerpicker.js | 78 +++++++++++---------- 1 file changed, 40 insertions(+), 38 deletions(-) diff --git a/src/components/views/rooms/Stickerpicker.js b/src/components/views/rooms/Stickerpicker.js index a0e3f1b7a9..809d948840 100644 --- a/src/components/views/rooms/Stickerpicker.js +++ b/src/components/views/rooms/Stickerpicker.js @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ import React from 'react'; -import { _t } from '../../../languageHandler'; +import {_t, _td} from '../../../languageHandler'; import AppTile from '../elements/AppTile'; import MatrixClientPeg from '../../../MatrixClientPeg'; import Modal from '../../../Modal'; @@ -53,6 +53,9 @@ export default class Stickerpicker extends React.Component { this.popoverWidth = 300; this.popoverHeight = 300; + // This is loaded by _acquireScalarClient on an as-needed basis. + this.scalarClient = null; + this.state = { showStickers: false, imError: null, @@ -63,14 +66,34 @@ export default class Stickerpicker extends React.Component { }; } - _removeStickerpickerWidgets() { + _acquireScalarClient() { + if (this.scalarClient) return Promise.resolve(this.scalarClient); + if (SdkConfig.get().integrations_ui_url && SdkConfig.get().integrations_rest_url) { + this.scalarClient = new ScalarAuthClient(); + return this.scalarClient.connect().then(() => { + this.forceUpdate(); + return this.scalarClient; + }).catch((e) => { + this._imError(_td("Failed to connect to integrations server"), e); + }); + } else { + this._imError(_td("No integrations server is configured to manage stickers with")); + } + } + + async _removeStickerpickerWidgets() { + const scalarClient = await this._acquireScalarClient(); console.warn('Removing Stickerpicker widgets'); if (this.state.widgetId) { - this.scalarClient.disableWidgetAssets(widgetType, this.state.widgetId).then(() => { - console.warn('Assets disabled'); - }).catch((err) => { - console.error('Failed to disable assets'); - }); + if (scalarClient) { + scalarClient.disableWidgetAssets(widgetType, this.state.widgetId).then(() => { + console.warn('Assets disabled'); + }).catch((err) => { + console.error('Failed to disable assets'); + }); + } else { + console.error("Cannot disable assets: no scalar client"); + } } else { console.warn('No widget ID specified, not disabling assets'); } @@ -87,19 +110,7 @@ export default class Stickerpicker extends React.Component { // Close the sticker picker when the window resizes window.addEventListener('resize', this._onResize); - this.scalarClient = null; - if (SdkConfig.get().integrations_ui_url && SdkConfig.get().integrations_rest_url) { - this.scalarClient = new ScalarAuthClient(); - this.scalarClient.connect().then(() => { - this.forceUpdate(); - }).catch((e) => { - this._imError("Failed to connect to integrations server", e); - }); - } - - if (!this.state.imError) { - this.dispatcherRef = dis.register(this._onWidgetAction); - } + this.dispatcherRef = dis.register(this._onWidgetAction); // Track updates to widget state in account data MatrixClientPeg.get().on('accountData', this._updateWidget); @@ -126,7 +137,7 @@ export default class Stickerpicker extends React.Component { console.error(errorMsg, e); this.setState({ showStickers: false, - imError: errorMsg, + imError: _t(errorMsg), }); } @@ -337,24 +348,15 @@ export default class Stickerpicker extends React.Component { /** * Launch the integrations manager on the stickers integration page */ - _launchManageIntegrations() { + async _launchManageIntegrations() { const IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager"); - this.scalarClient.connect().done(() => { - const src = (this.scalarClient !== null && this.scalarClient.hasCredentials()) ? - this.scalarClient.getScalarInterfaceUrlForRoom( - this.props.room, - 'type_' + widgetType, - this.state.widgetId, - ) : - null; - Modal.createTrackedDialog('Integrations Manager', '', IntegrationsManager, { - src: src, - }, "mx_IntegrationsManager"); - this.setState({showStickers: false}); - }, (err) => { - this.setState({imError: err}); - console.error('Error ensuring a valid scalar_token exists', err); - }); + + // The integrations manager will handle scalar auth for us. + Modal.createTrackedDialog('Integrations Manager', '', IntegrationsManager, { + room: this.props.room, + screen: `type_${widgetType}`, + integrationId: this.state.widgetId, + }, "mx_IntegrationsManager"); } render() { From d2d0cb2e9e6f671c380eb68228f024dcb5bab009 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 17 Jun 2019 15:34:30 -0600 Subject: [PATCH 49/59] Port AppTile (widgets) over to new integration manager dialog props --- src/components/views/elements/AppTile.js | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js index 959cee7ace..60bc8a337e 100644 --- a/src/components/views/elements/AppTile.js +++ b/src/components/views/elements/AppTile.js @@ -240,19 +240,13 @@ export default class AppTile extends React.Component { if (this.props.onEditClick) { this.props.onEditClick(); } else { + // The dialog handles scalar auth for us const IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager"); - this._scalarClient.connect().done(() => { - const src = this._scalarClient.getScalarInterfaceUrlForRoom( - this.props.room, 'type_' + this.props.type, this.props.id); - Modal.createTrackedDialog('Integrations Manager', '', IntegrationsManager, { - src: src, - }, "mx_IntegrationsManager"); - }, (err) => { - this.setState({ - error: err.message, - }); - console.error('Error ensuring a valid scalar_token exists', err); - }); + Modal.createTrackedDialog('Integrations Manager', '', IntegrationsManager, { + room: this.props.room, + screen: 'type_' + this.props.type, + integrationId: this.props.id + }, "mx_IntegrationsManager"); } } From 974a11ed201d11a08bfa165d61af6401cf34d957 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 17 Jun 2019 15:39:32 -0600 Subject: [PATCH 50/59] Defer scalar auth in AppsDrawer to widgets/manager dialog --- src/components/views/rooms/AppsDrawer.js | 30 ++++-------------------- 1 file changed, 4 insertions(+), 26 deletions(-) diff --git a/src/components/views/rooms/AppsDrawer.js b/src/components/views/rooms/AppsDrawer.js index e0e7a48b8c..3e5528996f 100644 --- a/src/components/views/rooms/AppsDrawer.js +++ b/src/components/views/rooms/AppsDrawer.js @@ -24,8 +24,6 @@ import AppTile from '../elements/AppTile'; import Modal from '../../../Modal'; import dis from '../../../dispatcher'; import sdk from '../../../index'; -import SdkConfig from '../../../SdkConfig'; -import ScalarAuthClient from '../../../ScalarAuthClient'; import ScalarMessaging from '../../../ScalarMessaging'; import { _t } from '../../../languageHandler'; import WidgetUtils from '../../../utils/WidgetUtils'; @@ -63,20 +61,6 @@ module.exports = React.createClass({ }, componentDidMount: function() { - this.scalarClient = null; - if (SdkConfig.get().integrations_ui_url && SdkConfig.get().integrations_rest_url) { - this.scalarClient = new ScalarAuthClient(); - this.scalarClient.connect().then(() => { - this.forceUpdate(); - }).catch((e) => { - console.log('Failed to connect to integrations server'); - // TODO -- Handle Scalar errors - // this.setState({ - // scalar_error: err, - // }); - }); - } - this.dispatcherRef = dis.register(this.onAction); }, @@ -144,16 +128,10 @@ module.exports = React.createClass({ _launchManageIntegrations: function() { const IntegrationsManager = sdk.getComponent('views.settings.IntegrationsManager'); - this.scalarClient.connect().done(() => { - const src = (this.scalarClient !== null && this.scalarClient.hasCredentials()) ? - this.scalarClient.getScalarInterfaceUrlForRoom(this.props.room, 'add_integ') : - null; - Modal.createTrackedDialog('Integrations Manager', '', IntegrationsManager, { - src: src, - }, 'mx_IntegrationsManager'); - }, (err) => { - console.error('Error ensuring a valid scalar_token exists', err); - }); + Modal.createTrackedDialog('Integrations Manager', '', IntegrationsManager, { + room: this.props.room, + screen: 'add_integ', + }, 'mx_IntegrationsManager'); }, onClickAddWidget: function(e) { From d58ab8e6d079b2a2f29c78b9a386cfdb56453491 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 17 Jun 2019 15:50:09 -0600 Subject: [PATCH 51/59] Remove excessive scalar auth checks in manager util class --- src/IntegrationManager.js | 65 ++++++++++----------------------------- 1 file changed, 16 insertions(+), 49 deletions(-) diff --git a/src/IntegrationManager.js b/src/IntegrationManager.js index 9cd984f12f..7a9d336e97 100644 --- a/src/IntegrationManager.js +++ b/src/IntegrationManager.js @@ -22,55 +22,22 @@ import ScalarAuthClient from './ScalarAuthClient'; import RoomViewStore from './stores/RoomViewStore'; import MatrixClientPeg from "./MatrixClientPeg"; -if (!global.mxIntegrationManager) { - global.mxIntegrationManager = {}; -} - -// TODO: TravisR - What even is this? - +// TODO: We should use this everywhere. export default class IntegrationManager { - static _init() { - if (!global.mxIntegrationManager.client || !global.mxIntegrationManager.connected) { - if (SdkConfig.get().integrations_ui_url && SdkConfig.get().integrations_rest_url) { - ScalarMessaging.startListening(); - global.mxIntegrationManager.client = new ScalarAuthClient(); - - return global.mxIntegrationManager.client.connect().then(() => { - global.mxIntegrationManager.connected = true; - }).catch((e) => { - console.error("Failed to connect to integrations server", e); - global.mxIntegrationManager.error = e; - }); - } else { - console.error('Invalid integration manager config', SdkConfig.get()); - } + /** + * Launch the integrations manager on the specified integration page + * @param {string} integName integration / widget type + * @param {string} integId integration / widget ID + * @param {function} onFinished Callback to invoke on integration manager close + */ + static async open(integName, integId, onFinished) { + // The dialog will take care of scalar auth for us + const IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager"); + Modal.createTrackedDialog('Integrations Manager', '', IntegrationsManager, { + room: MatrixClientPeg.get().getRoom(RoomViewStore.getRoomId()), + screen: 'type_' + integName, + integrationId: integId, + onFinished: onFinished, + }, "mx_IntegrationsManager"); } - } - - /** - * Launch the integrations manager on the specified integration page - * @param {string} integName integration / widget type - * @param {string} integId integration / widget ID - * @param {function} onFinished Callback to invoke on integration manager close - */ - static async open(integName, integId, onFinished) { - await IntegrationManager._init(); - if (global.mxIntegrationManager.client) { - await global.mxIntegrationManager.client.connect(); - } else { - return; - } - const IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager"); - if (global.mxIntegrationManager.error || - !(global.mxIntegrationManager.client && global.mxIntegrationManager.client.hasCredentials())) { - console.error("Scalar error", global.mxIntegrationManager); - return; - } - Modal.createTrackedDialog('Integrations Manager', '', IntegrationsManager, { - room: MatrixClientPeg.get().getRoom(RoomViewStore.getRoomId()), - screen: 'type_' + integName, - integrationId: integId, - onFinished: onFinished, - }, "mx_IntegrationsManager"); - } } From 8f6e8c1ec7dfe1ab5937ac5496a6506587e86c15 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 17 Jun 2019 15:51:14 -0600 Subject: [PATCH 52/59] Appease the linter --- src/components/views/elements/AppTile.js | 2 +- src/components/views/elements/ManageIntegsButton.js | 3 +-- src/components/views/settings/IntegrationsManager.js | 4 ++-- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js index 60bc8a337e..034a3318a5 100644 --- a/src/components/views/elements/AppTile.js +++ b/src/components/views/elements/AppTile.js @@ -245,7 +245,7 @@ export default class AppTile extends React.Component { Modal.createTrackedDialog('Integrations Manager', '', IntegrationsManager, { room: this.props.room, screen: 'type_' + this.props.type, - integrationId: this.props.id + integrationId: this.props.id, }, "mx_IntegrationsManager"); } } diff --git a/src/components/views/elements/ManageIntegsButton.js b/src/components/views/elements/ManageIntegsButton.js index f5d087e452..ef5604dba6 100644 --- a/src/components/views/elements/ManageIntegsButton.js +++ b/src/components/views/elements/ManageIntegsButton.js @@ -21,7 +21,6 @@ import sdk from '../../../index'; import ScalarAuthClient from '../../../ScalarAuthClient'; import Modal from "../../../Modal"; import { _t } from '../../../languageHandler'; -import AccessibleButton from './AccessibleButton'; export default class ManageIntegsButton extends React.Component { constructor(props) { @@ -47,7 +46,7 @@ export default class ManageIntegsButton extends React.Component { title={_t("Manage Integrations")} onClick={this.onManageIntegrations} /> - ) + ); } return integrationsButton; diff --git a/src/components/views/settings/IntegrationsManager.js b/src/components/views/settings/IntegrationsManager.js index d9bf6351e9..754693b73e 100644 --- a/src/components/views/settings/IntegrationsManager.js +++ b/src/components/views/settings/IntegrationsManager.js @@ -77,7 +77,7 @@ export default class IntegrationsManager extends React.Component { loading: false, connected: false, }); - }) + }); } componentDidMount() { @@ -135,4 +135,4 @@ export default class IntegrationsManager extends React.Component { return ; } -} \ No newline at end of file +} From d5db0077edf820717a6d9c74d55cd83130cc3218 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 17 Jun 2019 15:53:11 -0600 Subject: [PATCH 53/59] Remove unused imports --- src/IntegrationManager.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/IntegrationManager.js b/src/IntegrationManager.js index 7a9d336e97..7ff9aff86e 100644 --- a/src/IntegrationManager.js +++ b/src/IntegrationManager.js @@ -16,9 +16,6 @@ limitations under the License. */ import Modal from './Modal'; import sdk from './index'; -import SdkConfig from './SdkConfig'; -import ScalarMessaging from './ScalarMessaging'; -import ScalarAuthClient from './ScalarAuthClient'; import RoomViewStore from './stores/RoomViewStore'; import MatrixClientPeg from "./MatrixClientPeg"; From f95f194b6a21d65d10e2ed02b0c3bfdef0481f22 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 18 Jun 2019 11:49:36 +0200 Subject: [PATCH 54/59] keep old arrow-up behaviour when editing is not enabled also, move caret at end/start checks before choosing what to do also, selectHistory shouldn't return a promise --- .../views/rooms/MessageComposerInput.js | 49 ++++++++++--------- 1 file changed, 26 insertions(+), 23 deletions(-) diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index 7684e1dced..bf0287d376 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -1206,39 +1206,42 @@ export default class MessageComposerInput extends React.Component { onVerticalArrow = (e, up) => { if (e.ctrlKey || e.shiftKey || e.metaKey) return; - if (e.altKey) { + // selection must be collapsed + const selection = this.state.editorState.selection; + if (!selection.isCollapsed) return; + // and we must be at the edge of the document (up=start, down=end) + const document = this.state.editorState.document; + if (up) { + if (!selection.anchor.isAtStartOfNode(document)) return; + } else { + if (!selection.anchor.isAtEndOfNode(document)) return; + } + + const editingEnabled = SettingsStore.isFeatureEnabled("feature_message_editing"); + const shouldSelectHistory = (editingEnabled && e.altKey) || !editingEnabled; + const shouldEditLastMessage = editingEnabled && !e.altKey && up; + + if (shouldSelectHistory) { // Try select composer history const selected = this.selectHistory(up); if (selected) { // We're selecting history, so prevent the key event from doing anything else e.preventDefault(); } - } else if (!e.altKey && up) { - // Try edit the latest message - 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 (!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, - }); - } + } else if (shouldEditLastMessage) { + 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, + }); } } }; - selectHistory = async (up) => { + selectHistory = (up) => { const delta = up ? -1 : 1; // True if we are not currently selecting history, but composing a message From 02c9e29937ee82c00945c015d5cb91973210c0c1 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 18 Jun 2019 13:47:33 +0200 Subject: [PATCH 55/59] use renamed method that also takes local redactions into account now --- src/components/structures/MessagePanel.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index 52fd6d9be4..1644d87a7e 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -592,7 +592,7 @@ module.exports = React.createClass({ readReceiptMap={this._readReceiptMap} showUrlPreview={this.props.showUrlPreview} checkUnmounting={this._isUnmounting} - eventSendStatus={mxEv.replacementOrOwnStatus()} + eventSendStatus={mxEv.getAssociatedLocalEchoStatus()} tileShape={this.props.tileShape} isTwelveHour={this.props.isTwelveHour} permalinkCreator={this.props.permalinkCreator} From f9188bca92a06aa67377fdcb1f9950346622f9fe Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 18 Jun 2019 13:48:51 +0200 Subject: [PATCH 56/59] reduced opacity for pending redactions --- res/css/views/rooms/_EventTile.scss | 3 +++ 1 file changed, 3 insertions(+) diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index a6194832a3..62632eab27 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -169,6 +169,9 @@ limitations under the License. .mx_EventTile_sending .mx_RoomPill { opacity: 0.5; } +.mx_EventTile_sending.mx_EventTile_redacted .mx_UnknownBody { + opacity: 0.4; +} .mx_EventTile_notSent { color: $event-notsent-color; From c4fc2a80894557aff41e8429ee4f9ee9b927070b Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 18 Jun 2019 14:57:58 +0200 Subject: [PATCH 57/59] remove redundant localecho part from method name --- src/components/structures/MessagePanel.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index 1644d87a7e..8352872a2d 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -592,7 +592,7 @@ module.exports = React.createClass({ readReceiptMap={this._readReceiptMap} showUrlPreview={this.props.showUrlPreview} checkUnmounting={this._isUnmounting} - eventSendStatus={mxEv.getAssociatedLocalEchoStatus()} + eventSendStatus={mxEv.getAssociatedStatus()} tileShape={this.props.tileShape} isTwelveHour={this.props.isTwelveHour} permalinkCreator={this.props.permalinkCreator} From be37332bb07ef59822ba0c3fbe7444172db830c9 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 18 Jun 2019 07:55:43 -0600 Subject: [PATCH 58/59] Further simplify usage of integrations --- src/FromWidgetPostMessageApi.js | 14 ++++++-- src/IntegrationManager.js | 40 --------------------- src/components/views/rooms/Stickerpicker.js | 4 +-- 3 files changed, 14 insertions(+), 44 deletions(-) delete mode 100644 src/IntegrationManager.js diff --git a/src/FromWidgetPostMessageApi.js b/src/FromWidgetPostMessageApi.js index 61c51d4a20..79e5206f50 100644 --- a/src/FromWidgetPostMessageApi.js +++ b/src/FromWidgetPostMessageApi.js @@ -17,9 +17,12 @@ limitations under the License. import URL from 'url'; import dis from './dispatcher'; -import IntegrationManager from './IntegrationManager'; import WidgetMessagingEndpoint from './WidgetMessagingEndpoint'; import ActiveWidgetStore from './stores/ActiveWidgetStore'; +import sdk from "./index"; +import Modal from "./Modal"; +import MatrixClientPeg from "./MatrixClientPeg"; +import RoomViewStore from "./stores/RoomViewStore"; const WIDGET_API_VERSION = '0.0.2'; // Current API version const SUPPORTED_WIDGET_API_VERSIONS = [ @@ -189,7 +192,14 @@ export default class FromWidgetPostMessageApi { const data = event.data.data || event.data.widgetData; const integType = (data && data.integType) ? data.integType : null; const integId = (data && data.integId) ? data.integId : null; - IntegrationManager.open(integType, integId); + + // The dialog will take care of scalar auth for us + const IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager"); + Modal.createTrackedDialog('Integrations Manager', '', IntegrationsManager, { + room: MatrixClientPeg.get().getRoom(RoomViewStore.getRoomId()), + screen: 'type_' + integType, + integrationId: integId, + }, "mx_IntegrationsManager"); } else if (action === 'set_always_on_screen') { // This is a new message: there is no reason to support the deprecated widgetData here const data = event.data.data; diff --git a/src/IntegrationManager.js b/src/IntegrationManager.js deleted file mode 100644 index 7ff9aff86e..0000000000 --- a/src/IntegrationManager.js +++ /dev/null @@ -1,40 +0,0 @@ -/* -Copyright 2017 New Vector Ltd -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. -*/ -import Modal from './Modal'; -import sdk from './index'; -import RoomViewStore from './stores/RoomViewStore'; -import MatrixClientPeg from "./MatrixClientPeg"; - -// TODO: We should use this everywhere. -export default class IntegrationManager { - /** - * Launch the integrations manager on the specified integration page - * @param {string} integName integration / widget type - * @param {string} integId integration / widget ID - * @param {function} onFinished Callback to invoke on integration manager close - */ - static async open(integName, integId, onFinished) { - // The dialog will take care of scalar auth for us - const IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager"); - Modal.createTrackedDialog('Integrations Manager', '', IntegrationsManager, { - room: MatrixClientPeg.get().getRoom(RoomViewStore.getRoomId()), - screen: 'type_' + integName, - integrationId: integId, - onFinished: onFinished, - }, "mx_IntegrationsManager"); - } -} diff --git a/src/components/views/rooms/Stickerpicker.js b/src/components/views/rooms/Stickerpicker.js index 809d948840..2dc174ceac 100644 --- a/src/components/views/rooms/Stickerpicker.js +++ b/src/components/views/rooms/Stickerpicker.js @@ -68,7 +68,7 @@ export default class Stickerpicker extends React.Component { _acquireScalarClient() { if (this.scalarClient) return Promise.resolve(this.scalarClient); - if (SdkConfig.get().integrations_ui_url && SdkConfig.get().integrations_rest_url) { + if (ScalarAuthClient.isPossible()) { this.scalarClient = new ScalarAuthClient(); return this.scalarClient.connect().then(() => { this.forceUpdate(); @@ -348,7 +348,7 @@ export default class Stickerpicker extends React.Component { /** * Launch the integrations manager on the stickers integration page */ - async _launchManageIntegrations() { + _launchManageIntegrations() { const IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager"); // The integrations manager will handle scalar auth for us. From b6ca0ea6bf31f74a30ef031be9e1ac484854a7e3 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 18 Jun 2019 08:01:38 -0600 Subject: [PATCH 59/59] Appease the linter --- src/components/views/rooms/Stickerpicker.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/views/rooms/Stickerpicker.js b/src/components/views/rooms/Stickerpicker.js index 2dc174ceac..6918810842 100644 --- a/src/components/views/rooms/Stickerpicker.js +++ b/src/components/views/rooms/Stickerpicker.js @@ -19,7 +19,6 @@ import AppTile from '../elements/AppTile'; import MatrixClientPeg from '../../../MatrixClientPeg'; import Modal from '../../../Modal'; import sdk from '../../../index'; -import SdkConfig from '../../../SdkConfig'; import ScalarAuthClient from '../../../ScalarAuthClient'; import dis from '../../../dispatcher'; import AccessibleButton from '../elements/AccessibleButton';