mirror of https://github.com/vector-im/riot-web
bring back composer send history and arrow up to edit previous message
parent
ca3539d53e
commit
cc82353d8f
|
@ -15,38 +15,8 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {Value} from 'slate';
|
|
||||||
|
|
||||||
import _clamp from 'lodash/clamp';
|
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 {
|
export default class ComposerHistoryManager {
|
||||||
history: Array<HistoryItem> = [];
|
history: Array<HistoryItem> = [];
|
||||||
prefix: string;
|
prefix: string;
|
||||||
|
@ -57,26 +27,30 @@ export default class ComposerHistoryManager {
|
||||||
this.prefix = prefix + roomId;
|
this.prefix = prefix + roomId;
|
||||||
|
|
||||||
// TODO: Performance issues?
|
// TODO: Performance issues?
|
||||||
let item;
|
let index = 0;
|
||||||
for (; item = sessionStorage.getItem(`${this.prefix}[${this.currentIndex}]`); this.currentIndex++) {
|
let itemJSON;
|
||||||
|
|
||||||
|
while (itemJSON = sessionStorage.getItem(`${this.prefix}[${index}]`)) {
|
||||||
try {
|
try {
|
||||||
this.history.push(
|
const serializedParts = JSON.parse(itemJSON);
|
||||||
HistoryItem.fromJSON(JSON.parse(item)),
|
this.history.push(serializedParts);
|
||||||
);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn("Throwing away unserialisable history", e);
|
console.warn("Throwing away unserialisable history", e);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
++index;
|
||||||
}
|
}
|
||||||
this.lastIndex = this.currentIndex;
|
this.lastIndex = this.history.length - 1;
|
||||||
// reset currentIndex to account for any unserialisable history
|
// reset currentIndex to account for any unserialisable history
|
||||||
this.currentIndex = this.history.length;
|
this.currentIndex = this.lastIndex + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
save(value: Value, format: MessageFormat) {
|
save(editorModel: Object) {
|
||||||
const item = new HistoryItem(value, format);
|
const serializedParts = editorModel.serializeParts();
|
||||||
this.history.push(item);
|
this.history.push(serializedParts);
|
||||||
this.currentIndex = this.history.length;
|
this.currentIndex = this.history.length;
|
||||||
sessionStorage.setItem(`${this.prefix}[${this.lastIndex++}]`, JSON.stringify(item.toJSON()));
|
this.lastIndex += 1;
|
||||||
|
sessionStorage.setItem(`${this.prefix}[${this.lastIndex}]`, JSON.stringify(serializedParts));
|
||||||
}
|
}
|
||||||
|
|
||||||
getItem(offset: number): ?HistoryItem {
|
getItem(offset: number): ?HistoryItem {
|
||||||
|
|
|
@ -144,6 +144,10 @@ export default class BasicMessageEditor extends React.Component {
|
||||||
return this._lastCaret;
|
return this._lastCaret;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isSelectionCollapsed() {
|
||||||
|
return !this._lastSelection || this._lastSelection.isCollapsed;
|
||||||
|
}
|
||||||
|
|
||||||
isCaretAtStart() {
|
isCaretAtStart() {
|
||||||
return this.getCaret().offset === 0;
|
return this.getCaret().offset === 0;
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,6 +27,8 @@ import ReplyPreview from "./ReplyPreview";
|
||||||
import RoomViewStore from '../../../stores/RoomViewStore';
|
import RoomViewStore from '../../../stores/RoomViewStore';
|
||||||
import ReplyThread from "../elements/ReplyThread";
|
import ReplyThread from "../elements/ReplyThread";
|
||||||
import {parseEvent} from '../../../editor/deserialize';
|
import {parseEvent} from '../../../editor/deserialize';
|
||||||
|
import {findEditableEvent} from '../../../utils/EventUtils';
|
||||||
|
import ComposerHistoryManager from "../../../ComposerHistoryManager";
|
||||||
|
|
||||||
function addReplyToMessageContent(content, repliedToEvent, permalinkCreator) {
|
function addReplyToMessageContent(content, repliedToEvent, permalinkCreator) {
|
||||||
const replyContent = ReplyThread.makeReplyMixIn(repliedToEvent);
|
const replyContent = ReplyThread.makeReplyMixIn(repliedToEvent);
|
||||||
|
@ -79,6 +81,7 @@ export default class SendMessageComposer extends React.Component {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
this.model = null;
|
this.model = null;
|
||||||
this._editorRef = null;
|
this._editorRef = null;
|
||||||
|
this.currentlyComposedEditorState = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
_setEditorRef = ref => {
|
_setEditorRef = ref => {
|
||||||
|
@ -86,19 +89,75 @@ export default class SendMessageComposer extends React.Component {
|
||||||
};
|
};
|
||||||
|
|
||||||
_onKeyDown = (event) => {
|
_onKeyDown = (event) => {
|
||||||
if (event.metaKey || event.altKey || event.shiftKey) {
|
const hasModifier = event.altKey || event.ctrlKey || event.metaKey || event.shiftKey;
|
||||||
return;
|
if (event.key === "Enter" && !hasModifier) {
|
||||||
}
|
|
||||||
if (event.key === "Enter") {
|
|
||||||
this._sendMessage();
|
this._sendMessage();
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
} else if (event.key === "ArrowUp") {
|
||||||
|
this.onVerticalArrow(event, true);
|
||||||
|
} else if (event.key === "ArrowDown") {
|
||||||
|
this.onVerticalArrow(event, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onVerticalArrow(e, up) {
|
||||||
|
if (e.ctrlKey || e.shiftKey || e.metaKey) return;
|
||||||
|
|
||||||
|
const shouldSelectHistory = e.altKey;
|
||||||
|
const shouldEditLastMessage = !e.altKey && up && !RoomViewStore.getQuotingEvent();
|
||||||
|
|
||||||
|
if (shouldSelectHistory) {
|
||||||
|
// Try select composer history
|
||||||
|
const selected = this.selectSendHistory(up);
|
||||||
|
if (selected) {
|
||||||
|
// We're selecting history, so prevent the key event from doing anything else
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
} else if (shouldEditLastMessage) {
|
||||||
|
// selection must be collapsed and caret at start
|
||||||
|
if (this._editorRef.isSelectionCollapsed() && this._editorRef.isCaretAtStart()) {
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
selectSendHistory(up) {
|
||||||
|
const delta = up ? -1 : 1;
|
||||||
|
|
||||||
|
// True if we are not currently selecting history, but composing a message
|
||||||
|
if (this.sendHistoryManager.currentIndex === this.sendHistoryManager.history.length) {
|
||||||
|
// We can't go any further - there isn't any more history, so nop.
|
||||||
|
if (!up) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.currentlyComposedEditorState = this.model.serializeParts();
|
||||||
|
} else if (this.sendHistoryManager.currentIndex + delta === this.sendHistoryManager.history.length) {
|
||||||
|
// True when we return to the message being composed currently
|
||||||
|
this.model.reset(this.currentlyComposedEditorState);
|
||||||
|
this.sendHistoryManager.currentIndex = this.sendHistoryManager.history.length;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const serializedParts = this.sendHistoryManager.getItem(delta);
|
||||||
|
if (serializedParts) {
|
||||||
|
this.model.reset(serializedParts);
|
||||||
|
this._editorRef.focus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_sendMessage() {
|
_sendMessage() {
|
||||||
const isReply = !!RoomViewStore.getQuotingEvent();
|
const isReply = !!RoomViewStore.getQuotingEvent();
|
||||||
const {roomId} = this.props.room;
|
const {roomId} = this.props.room;
|
||||||
this.context.matrixClient.sendMessage(roomId, createMessageContent(this.model, this.props.permalinkCreator));
|
const content = createMessageContent(this.model, this.props.permalinkCreator);
|
||||||
|
this.context.matrixClient.sendMessage(roomId, content);
|
||||||
|
this.sendHistoryManager.save(this.model);
|
||||||
this.model.reset([]);
|
this.model.reset([]);
|
||||||
this._editorRef.clearUndoHistory();
|
this._editorRef.clearUndoHistory();
|
||||||
|
|
||||||
|
@ -125,6 +184,7 @@ export default class SendMessageComposer extends React.Component {
|
||||||
const partCreator = new PartCreator(this.props.room, this.context.matrixClient);
|
const partCreator = new PartCreator(this.props.room, this.context.matrixClient);
|
||||||
this.model = new EditorModel([], partCreator);
|
this.model = new EditorModel([], partCreator);
|
||||||
this.dispatcherRef = dis.register(this.onAction);
|
this.dispatcherRef = dis.register(this.onAction);
|
||||||
|
this.sendHistoryManager = new ComposerHistoryManager(this.props.room.roomId, 'mx_slate_composer_history_');
|
||||||
}
|
}
|
||||||
|
|
||||||
onAction = (payload) => {
|
onAction = (payload) => {
|
||||||
|
|
Loading…
Reference in New Issue