From 8926fcb3a62fc36914e89cf46ffeff27dc1eecda Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 24 May 2019 14:41:24 +0200 Subject: [PATCH 01/10] helper functions to find next & previous editable events in timeline --- src/utils/EventUtils.js | 35 ++++++++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/src/utils/EventUtils.js b/src/utils/EventUtils.js index 68f6d4b14e..d7c2285e7b 100644 --- a/src/utils/EventUtils.js +++ b/src/utils/EventUtils.js @@ -16,7 +16,8 @@ limitations under the License. import { EventStatus } from 'matrix-js-sdk'; import MatrixClientPeg from '../MatrixClientPeg'; - +import { findLastIndex, findIndex } from "lodash"; +import shouldHideEvent from "../shouldHideEvent"; /** * Returns whether an event should allow actions like reply, reactions, edit, etc. * which effectively checks whether it's a regular message that has been sent and that we @@ -50,3 +51,35 @@ export function canEditContent(mxEvent) { mxEvent.getOriginalContent().msgtype === "m.text" && mxEvent.getSender() === MatrixClientPeg.get().getUserId(); } + +export function findPreviousEditableEvent(room, fromEventId = undefined) { + const liveTimeline = room.getLiveTimeline(); + const events = liveTimeline.getEvents(); + let startFromIdx = events.length - 1; + if (fromEventId) { + const fromEventIdx = findLastIndex(events, e => e.getId() === fromEventId); + if (fromEventIdx !== -1) { + startFromIdx = fromEventIdx - 1; + } + } + const nextEventIdx = findLastIndex(events, e => !shouldHideEvent(e) && canEditContent(e), startFromIdx); + if (nextEventIdx !== -1) { + return events[nextEventIdx]; + } +} + +export function findNextEditableEvent(room, fromEventId = undefined) { + const liveTimeline = room.getLiveTimeline(); + const events = liveTimeline.getEvents(); + let startFromIdx = 0; + if (fromEventId) { + const fromEventIdx = findIndex(events, e => e.getId() === fromEventId); + if (fromEventIdx !== -1) { + startFromIdx = fromEventIdx + 1; + } + } + const nextEventIdx = findIndex(events, e => !shouldHideEvent(e) && canEditContent(e), startFromIdx); + if (nextEventIdx !== -1) { + return events[nextEventIdx]; + } +} From 3591eedcfa9f4701cf5e06ab2069ab861430fb3d Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 24 May 2019 14:42:33 +0200 Subject: [PATCH 02/10] move between editable events with arrow keys --- .../views/elements/MessageEditor.js | 38 ++++++++++++++++++- .../views/rooms/MessageComposerInput.js | 17 +++++---- 2 files changed, 47 insertions(+), 8 deletions(-) diff --git a/src/components/views/elements/MessageEditor.js b/src/components/views/elements/MessageEditor.js index 3478ce30a8..3662e34ba4 100644 --- a/src/components/views/elements/MessageEditor.js +++ b/src/components/views/elements/MessageEditor.js @@ -23,6 +23,7 @@ import EditorModel from '../../../editor/model'; import {setCaretPosition} from '../../../editor/caret'; import {getCaretOffsetAndText} from '../../../editor/dom'; import {htmlSerializeIfNeeded, textSerialize} from '../../../editor/serialize'; +import {findPreviousEditableEvent, findNextEditableEvent} from '../../../utils/EventUtils'; import {parseEvent} from '../../../editor/deserialize'; import Autocomplete from '../rooms/Autocomplete'; import {PartCreator} from '../../../editor/parts'; @@ -42,7 +43,7 @@ export default class MessageEditor extends React.Component { constructor(props, context) { super(props, context); - const room = this.context.matrixClient.getRoom(this.props.event.getRoomId()); + const room = this._getRoom(); const partCreator = new PartCreator( () => this._autocompleteRef, query => this.setState({query}), @@ -61,6 +62,10 @@ export default class MessageEditor extends React.Component { this._autocompleteRef = null; } + _getRoom() { + return this.context.matrixClient.getRoom(this.props.event.getRoomId()); + } + _updateEditorState = (caret) => { renderModel(this._editorRef, this.model); if (caret) { @@ -79,6 +84,16 @@ export default class MessageEditor extends React.Component { this.model.update(text, event.inputType, caret); } + _isCaretAtStart() { + const {caret} = getCaretOffsetAndText(this._editorRef, document.getSelection()); + return caret.offset === 0; + } + + _isCaretAtEnd() { + const {caret, text} = getCaretOffsetAndText(this._editorRef, document.getSelection()); + return caret.offset === text.length; + } + _onKeyDown = (event) => { // insert newline on Shift+Enter if (event.shiftKey && event.key === "Enter") { @@ -112,6 +127,27 @@ export default class MessageEditor extends React.Component { event.preventDefault(); } else if (event.key === "Escape") { this._cancelEdit(); + } else if (event.key === "ArrowUp") { + if (!this._isCaretAtStart()) { + return; + } + const previousEvent = findPreviousEditableEvent(this._getRoom(), this.props.event.getId()); + if (previousEvent) { + dis.dispatch({action: 'edit_event', event: previousEvent}); + event.preventDefault(); + } + } else if (event.key === "ArrowDown") { + if (!this._isCaretAtEnd()) { + return; + } + const nextEvent = findNextEditableEvent(this._getRoom(), this.props.event.getId()); + if (nextEvent) { + dis.dispatch({action: 'edit_event', event: nextEvent}); + } else { + dis.dispatch({action: 'edit_event', event: null}); + dis.dispatch({action: 'focus_composer'}); + } + event.preventDefault(); } } diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index df16310bda..729e13f13c 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -60,6 +60,7 @@ import RoomViewStore from '../../../stores/RoomViewStore'; import ReplyThread from "../elements/ReplyThread"; import {ContentHelpers} from 'matrix-js-sdk'; import AccessibleButton from '../elements/AccessibleButton'; +import { findPreviousEditableEvent } from '../../../utils/EventUtils'; const REGEX_EMOTICON_WHITESPACE = new RegExp('(?:^|\\s)(' + EMOTICON_REGEX.source + ')\\s$'); @@ -1188,14 +1189,16 @@ export default class MessageComposerInput extends React.Component { // and we must be at the edge of the document (up=start, down=end) if (up) { if (!selection.anchor.isAtStartOfNode(document)) 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(); + const editEvent = findPreviousEditableEvent(this.props.room); + 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); From bd3dbd1a89a6ef6a8af984ea6677cc375bb5d945 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 24 May 2019 14:42:57 +0200 Subject: [PATCH 03/10] remove edit history from main composer --- src/ComposerHistoryManager.js | 86 ------------------- .../views/rooms/MessageComposerInput.js | 57 ------------ 2 files changed, 143 deletions(-) delete mode 100644 src/ComposerHistoryManager.js diff --git a/src/ComposerHistoryManager.js b/src/ComposerHistoryManager.js deleted file mode 100644 index ecf773f2e7..0000000000 --- a/src/ComposerHistoryManager.js +++ /dev/null @@ -1,86 +0,0 @@ -//@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 729e13f13c..ba24df6d08 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -44,7 +44,6 @@ import * as HtmlUtils from '../../../HtmlUtils'; import Autocomplete from './Autocomplete'; import {Completion} from "../../../autocomplete/Autocompleter"; import Markdown from '../../../Markdown'; -import ComposerHistoryManager from '../../../ComposerHistoryManager'; import MessageComposerStore from '../../../stores/MessageComposerStore'; import ContentMessages from '../../../ContentMessages'; @@ -141,7 +140,6 @@ export default class MessageComposerInput extends React.Component { client: MatrixClient; autocomplete: Autocomplete; - historyManager: ComposerHistoryManager; constructor(props, context) { super(props, context); @@ -331,7 +329,6 @@ 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() { @@ -1032,7 +1029,6 @@ 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(), }, ()=>{ @@ -1110,11 +1106,6 @@ 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"); @@ -1206,54 +1197,6 @@ 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 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) { - 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 fbb79e4686c0a48a3cf8c13af3d982ba103f4b4b Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 24 May 2019 14:46:34 +0200 Subject: [PATCH 04/10] don't navigate to next/prev message when editor has modifications to prevent losing modifications by accident --- src/components/views/elements/MessageEditor.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/views/elements/MessageEditor.js b/src/components/views/elements/MessageEditor.js index 3662e34ba4..85e170373c 100644 --- a/src/components/views/elements/MessageEditor.js +++ b/src/components/views/elements/MessageEditor.js @@ -60,6 +60,7 @@ export default class MessageEditor extends React.Component { }; this._editorRef = null; this._autocompleteRef = null; + this._hasModifications = false; } _getRoom() { @@ -79,6 +80,7 @@ export default class MessageEditor extends React.Component { } _onInput = (event) => { + this._hasModifications = true; const sel = document.getSelection(); const {caret, text} = getCaretOffsetAndText(this._editorRef, sel); this.model.update(text, event.inputType, caret); @@ -128,7 +130,7 @@ export default class MessageEditor extends React.Component { } else if (event.key === "Escape") { this._cancelEdit(); } else if (event.key === "ArrowUp") { - if (!this._isCaretAtStart()) { + if (this._hasModifications || !this._isCaretAtStart()) { return; } const previousEvent = findPreviousEditableEvent(this._getRoom(), this.props.event.getId()); @@ -137,7 +139,7 @@ export default class MessageEditor extends React.Component { event.preventDefault(); } } else if (event.key === "ArrowDown") { - if (!this._isCaretAtEnd()) { + if (this._hasModifications || !this._isCaretAtEnd()) { return; } const nextEvent = findNextEditableEvent(this._getRoom(), this.props.event.getId()); From 4626581dbea1a3aa7b37ef0e01ffd6d221dd306f Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 24 May 2019 15:38:51 +0200 Subject: [PATCH 05/10] scroll event into view when starting to edit --- src/components/structures/MessagePanel.js | 7 +++++++ src/components/structures/TimelinePanel.js | 8 +++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index 17e44f2a0f..657b01c663 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -234,6 +234,13 @@ module.exports = React.createClass({ } }, + scrollToEventIfNeeded: function(eventId) { + const node = this.eventNodes[eventId]; + if (node) { + node.scrollIntoView({block: "nearest", behavior: "instant"}); + } + }, + /* check the scroll state and send out pagination requests if necessary. */ checkFillState: function() { diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index 0b7b315915..44741ad521 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -408,7 +408,13 @@ const TimelinePanel = React.createClass({ this.forceUpdate(); } if (payload.action === "edit_event") { - this.setState({editEvent: payload.event}); + this.setState({editEvent: payload.event}, () => { + if (payload.event && this.refs.messagePanel) { + this.refs.messagePanel.scrollToEventIfNeeded( + payload.event.getId(), + ); + } + }); } }, From e193522db1c7b2879a6ac25efa656c70ead79e6e Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 27 May 2019 16:22:55 +0200 Subject: [PATCH 06/10] PR feedback: put findPrev/NextEditableEvent in one function with flag --- .../views/elements/MessageEditor.js | 6 +-- .../views/rooms/MessageComposerInput.js | 4 +- src/utils/EventUtils.js | 45 +++++++++---------- 3 files changed, 25 insertions(+), 30 deletions(-) diff --git a/src/components/views/elements/MessageEditor.js b/src/components/views/elements/MessageEditor.js index 85e170373c..9ff0244564 100644 --- a/src/components/views/elements/MessageEditor.js +++ b/src/components/views/elements/MessageEditor.js @@ -23,7 +23,7 @@ import EditorModel from '../../../editor/model'; import {setCaretPosition} from '../../../editor/caret'; import {getCaretOffsetAndText} from '../../../editor/dom'; import {htmlSerializeIfNeeded, textSerialize} from '../../../editor/serialize'; -import {findPreviousEditableEvent, findNextEditableEvent} from '../../../utils/EventUtils'; +import {findEditableEvent} from '../../../utils/EventUtils'; import {parseEvent} from '../../../editor/deserialize'; import Autocomplete from '../rooms/Autocomplete'; import {PartCreator} from '../../../editor/parts'; @@ -133,7 +133,7 @@ export default class MessageEditor extends React.Component { if (this._hasModifications || !this._isCaretAtStart()) { return; } - const previousEvent = findPreviousEditableEvent(this._getRoom(), this.props.event.getId()); + const previousEvent = findEditableEvent(this._getRoom(), false, this.props.event.getId()); if (previousEvent) { dis.dispatch({action: 'edit_event', event: previousEvent}); event.preventDefault(); @@ -142,7 +142,7 @@ export default class MessageEditor extends React.Component { if (this._hasModifications || !this._isCaretAtEnd()) { return; } - const nextEvent = findNextEditableEvent(this._getRoom(), this.props.event.getId()); + const nextEvent = findEditableEvent(this._getRoom(), true, this.props.event.getId()); if (nextEvent) { dis.dispatch({action: 'edit_event', event: nextEvent}); } else { diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index ba24df6d08..30d7aa3237 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -59,7 +59,7 @@ import RoomViewStore from '../../../stores/RoomViewStore'; import ReplyThread from "../elements/ReplyThread"; import {ContentHelpers} from 'matrix-js-sdk'; import AccessibleButton from '../elements/AccessibleButton'; -import { findPreviousEditableEvent } from '../../../utils/EventUtils'; +import {findEditableEvent} from '../../../utils/EventUtils'; const REGEX_EMOTICON_WHITESPACE = new RegExp('(?:^|\\s)(' + EMOTICON_REGEX.source + ')\\s$'); @@ -1181,7 +1181,7 @@ export default class MessageComposerInput extends React.Component { if (up) { if (!selection.anchor.isAtStartOfNode(document)) return; - const editEvent = findPreviousEditableEvent(this.props.room); + const editEvent = findEditableEvent(this.props.room, false); if (editEvent) { // We're selecting history, so prevent the key event from doing anything else e.preventDefault(); diff --git a/src/utils/EventUtils.js b/src/utils/EventUtils.js index d7c2285e7b..f8a4dd708c 100644 --- a/src/utils/EventUtils.js +++ b/src/utils/EventUtils.js @@ -52,34 +52,29 @@ export function canEditContent(mxEvent) { mxEvent.getSender() === MatrixClientPeg.get().getUserId(); } -export function findPreviousEditableEvent(room, fromEventId = undefined) { +export function findEditableEvent(room, isForward, fromEventId = undefined) { const liveTimeline = room.getLiveTimeline(); const events = liveTimeline.getEvents(); - let startFromIdx = events.length - 1; - if (fromEventId) { - const fromEventIdx = findLastIndex(events, e => e.getId() === fromEventId); - if (fromEventIdx !== -1) { - startFromIdx = fromEventIdx - 1; - } + const maxIdx = events.length - 1; + const inc = isForward ? 1 : -1; + const beginIdx = isForward ? 0 : maxIdx; + let endIdx = isForward ? maxIdx : 0; + if (!fromEventId) { + endIdx = Math.min(Math.max(0, beginIdx + (inc * 100)), maxIdx); } - const nextEventIdx = findLastIndex(events, e => !shouldHideEvent(e) && canEditContent(e), startFromIdx); - if (nextEventIdx !== -1) { - return events[nextEventIdx]; + let foundFromEventId = !fromEventId; + for (let i = beginIdx; i !== (endIdx + inc); i += inc) { + const e = events[i]; + // find start event first + if (!foundFromEventId && e.getId() === fromEventId) { + foundFromEventId = true; + // don't look further than 100 events from `fromEventId` + // to not iterate potentially 1000nds of events on key up/down + endIdx = Math.min(Math.max(0, i + (inc * 100)), maxIdx); + } else if (foundFromEventId && !shouldHideEvent(e) && canEditContent(e)) { + // otherwise look for editable event + return e; + } } } -export function findNextEditableEvent(room, fromEventId = undefined) { - const liveTimeline = room.getLiveTimeline(); - const events = liveTimeline.getEvents(); - let startFromIdx = 0; - if (fromEventId) { - const fromEventIdx = findIndex(events, e => e.getId() === fromEventId); - if (fromEventIdx !== -1) { - startFromIdx = fromEventIdx + 1; - } - } - const nextEventIdx = findIndex(events, e => !shouldHideEvent(e) && canEditContent(e), startFromIdx); - if (nextEventIdx !== -1) { - return events[nextEventIdx]; - } -} From 25e2c747edd22ee070cf68aad991dee9fb67ba2a Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 27 May 2019 16:26:21 +0200 Subject: [PATCH 07/10] focus main composer after cancel or saving edit --- src/components/views/elements/MessageEditor.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/views/elements/MessageEditor.js b/src/components/views/elements/MessageEditor.js index 9ff0244564..ed86bcb0a3 100644 --- a/src/components/views/elements/MessageEditor.js +++ b/src/components/views/elements/MessageEditor.js @@ -155,6 +155,7 @@ export default class MessageEditor extends React.Component { _cancelEdit = () => { dis.dispatch({action: "edit_event", event: null}); + dis.dispatch({action: 'focus_composer'}); } _sendEdit = () => { @@ -185,6 +186,7 @@ export default class MessageEditor extends React.Component { this.context.matrixClient.sendMessage(roomId, content); dis.dispatch({action: "edit_event", event: null}); + dis.dispatch({action: 'focus_composer'}); } _onAutoCompleteConfirm = (completion) => { From 918de849c7aaeaaf7d92418e4e7aa999c69c1840 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 27 May 2019 16:41:03 +0200 Subject: [PATCH 08/10] make sure in the future arrow up/down only goes through own events --- src/utils/EventUtils.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/utils/EventUtils.js b/src/utils/EventUtils.js index f8a4dd708c..626bb206cf 100644 --- a/src/utils/EventUtils.js +++ b/src/utils/EventUtils.js @@ -52,6 +52,16 @@ export function canEditContent(mxEvent) { mxEvent.getSender() === MatrixClientPeg.get().getUserId(); } +export function canEditOwnEvent(mxEvent) { + // for now we only allow editing + // your own events. So this just call through + // In the future though, moderators will be able to + // edit other people's messages as well but we don't + // want findEditableEvent to return other people's events + // hence this method. + return canEditContent(mxEvent); +} + export function findEditableEvent(room, isForward, fromEventId = undefined) { const liveTimeline = room.getLiveTimeline(); const events = liveTimeline.getEvents(); @@ -71,7 +81,7 @@ export function findEditableEvent(room, isForward, fromEventId = undefined) { // don't look further than 100 events from `fromEventId` // to not iterate potentially 1000nds of events on key up/down endIdx = Math.min(Math.max(0, i + (inc * 100)), maxIdx); - } else if (foundFromEventId && !shouldHideEvent(e) && canEditContent(e)) { + } else if (foundFromEventId && !shouldHideEvent(e) && canEditOwnEvent(e)) { // otherwise look for editable event return e; } From 651cb8b9a7733b44cfce7a8a075da9d6c5c5a8fe Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 27 May 2019 16:43:12 +0200 Subject: [PATCH 09/10] lint --- src/utils/EventUtils.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/utils/EventUtils.js b/src/utils/EventUtils.js index 626bb206cf..76ef044b2a 100644 --- a/src/utils/EventUtils.js +++ b/src/utils/EventUtils.js @@ -16,7 +16,6 @@ limitations under the License. import { EventStatus } from 'matrix-js-sdk'; import MatrixClientPeg from '../MatrixClientPeg'; -import { findLastIndex, findIndex } from "lodash"; import shouldHideEvent from "../shouldHideEvent"; /** * Returns whether an event should allow actions like reply, reactions, edit, etc. From dee24ac27a06a3a108005beddb675965b9c4d752 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 27 May 2019 16:45:26 +0200 Subject: [PATCH 10/10] extract constant --- src/utils/EventUtils.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/utils/EventUtils.js b/src/utils/EventUtils.js index 76ef044b2a..ff20a68e3c 100644 --- a/src/utils/EventUtils.js +++ b/src/utils/EventUtils.js @@ -61,6 +61,7 @@ export function canEditOwnEvent(mxEvent) { return canEditContent(mxEvent); } +const MAX_JUMP_DISTANCE = 100; export function findEditableEvent(room, isForward, fromEventId = undefined) { const liveTimeline = room.getLiveTimeline(); const events = liveTimeline.getEvents(); @@ -69,7 +70,7 @@ export function findEditableEvent(room, isForward, fromEventId = undefined) { const beginIdx = isForward ? 0 : maxIdx; let endIdx = isForward ? maxIdx : 0; if (!fromEventId) { - endIdx = Math.min(Math.max(0, beginIdx + (inc * 100)), maxIdx); + endIdx = Math.min(Math.max(0, beginIdx + (inc * MAX_JUMP_DISTANCE)), maxIdx); } let foundFromEventId = !fromEventId; for (let i = beginIdx; i !== (endIdx + inc); i += inc) { @@ -77,9 +78,9 @@ export function findEditableEvent(room, isForward, fromEventId = undefined) { // find start event first if (!foundFromEventId && e.getId() === fromEventId) { foundFromEventId = true; - // don't look further than 100 events from `fromEventId` + // don't look further than MAX_JUMP_DISTANCE events from `fromEventId` // to not iterate potentially 1000nds of events on key up/down - endIdx = Math.min(Math.max(0, i + (inc * 100)), maxIdx); + endIdx = Math.min(Math.max(0, i + (inc * MAX_JUMP_DISTANCE)), maxIdx); } else if (foundFromEventId && !shouldHideEvent(e) && canEditOwnEvent(e)) { // otherwise look for editable event return e;