RTE drafts (#12674)

* Add drafts to the RTE and tests

* test drafts in threads

* lint

* Add unit test.

* Fix test failure

* Remove unused import

* Clean up wysiwyg drafts and add test.

* Fix typo

* Add timeout to allow for wasm loading.

---------

Co-authored-by: Florian Duros <florian.duros@ormaz.fr>
This commit is contained in:
David Langley 2024-08-22 13:54:01 +01:00 committed by GitHub
parent fdc5acd5a4
commit 70665d3ce3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 266 additions and 38 deletions

View file

@ -18,6 +18,7 @@ import { logger } from "matrix-js-sdk/src/logger";
import { MatrixClientPeg } from "./MatrixClientPeg";
import { EDITOR_STATE_STORAGE_PREFIX } from "./components/views/rooms/SendMessageComposer";
import { WYSIWYG_EDITOR_STATE_STORAGE_PREFIX } from "./components/views/rooms/MessageComposer";
// The key used to persist the the timestamp we last cleaned up drafts
export const DRAFT_LAST_CLEANUP_KEY = "mx_draft_cleanup";
@ -61,14 +62,21 @@ function shouldCleanupDrafts(): boolean {
}
/**
* Clear all drafts for the CIDER editor if the room does not exist in the known rooms.
* Clear all drafts for the CIDER and WYSIWYG editors if the room does not exist in the known rooms.
*/
function cleaupDrafts(): void {
for (let i = 0; i < localStorage.length; i++) {
const keyName = localStorage.key(i);
if (!keyName?.startsWith(EDITOR_STATE_STORAGE_PREFIX)) continue;
if (!keyName) continue;
let roomId: string | undefined = undefined;
if (keyName.startsWith(EDITOR_STATE_STORAGE_PREFIX)) {
roomId = keyName.slice(EDITOR_STATE_STORAGE_PREFIX.length).split("_$")[0];
}
if (keyName.startsWith(WYSIWYG_EDITOR_STATE_STORAGE_PREFIX)) {
roomId = keyName.slice(WYSIWYG_EDITOR_STATE_STORAGE_PREFIX.length).split("_$")[0];
}
if (!roomId) continue;
// Remove the prefix and the optional event id suffix to leave the room id
const roomId = keyName.slice(EDITOR_STATE_STORAGE_PREFIX.length).split("_$")[0];
const room = MatrixClientPeg.safeGet().getRoom(roomId);
if (!room) {
logger.debug(`Removing draft for unknown room with key ${keyName}`);

View file

@ -26,6 +26,7 @@ import {
} from "matrix-js-sdk/src/matrix";
import { Optional } from "matrix-events-sdk";
import { Tooltip } from "@vector-im/compound-web";
import { logger } from "matrix-js-sdk/src/logger";
import { _t } from "../../../languageHandler";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
@ -65,6 +66,9 @@ import { createCantStartVoiceMessageBroadcastDialog } from "../dialogs/CantStart
import { UIFeature } from "../../../settings/UIFeature";
import { formatTimeLeft } from "../../../DateUtils";
// The prefix used when persisting editor drafts to localstorage.
export const WYSIWYG_EDITOR_STATE_STORAGE_PREFIX = "mx_wysiwyg_state_";
let instanceCount = 0;
interface ISendButtonProps {
@ -109,6 +113,12 @@ interface IState {
initialComposerContent: string;
}
type WysiwygComposerState = {
content: string;
isRichText: boolean;
replyEventId?: string;
};
export class MessageComposer extends React.Component<IProps, IState> {
private dispatcherRef?: string;
private messageComposerInput = createRef<SendMessageComposerClass>();
@ -129,11 +139,32 @@ export class MessageComposer extends React.Component<IProps, IState> {
public constructor(props: IProps, context: React.ContextType<typeof RoomContext>) {
super(props, context);
this.context = context; // otherwise React will only set it prior to render due to type def above
VoiceRecordingStore.instance.on(UPDATE_EVENT, this.onVoiceStoreUpdate);
window.addEventListener("beforeunload", this.saveWysiwygEditorState);
const isWysiwygLabEnabled = SettingsStore.getValue<boolean>("feature_wysiwyg_composer");
let isRichTextEnabled = true;
let initialComposerContent = "";
if (isWysiwygLabEnabled) {
const wysiwygState = this.restoreWysiwygEditorState();
if (wysiwygState) {
isRichTextEnabled = wysiwygState.isRichText;
initialComposerContent = wysiwygState.content;
if (wysiwygState.replyEventId) {
dis.dispatch({
action: "reply_to_event",
event: this.props.room.findEventById(wysiwygState.replyEventId),
context: this.context.timelineRenderingType,
});
}
}
}
this.state = {
isComposerEmpty: true,
composerContent: "",
isComposerEmpty: initialComposerContent?.length === 0,
composerContent: initialComposerContent,
haveRecording: false,
recordingTimeLeftSeconds: undefined, // when set to a number, shows a toast
isMenuOpen: false,
@ -141,9 +172,9 @@ export class MessageComposer extends React.Component<IProps, IState> {
showStickersButton: SettingsStore.getValue("MessageComposerInput.showStickersButton"),
showPollsButton: SettingsStore.getValue("MessageComposerInput.showPollsButton"),
showVoiceBroadcastButton: SettingsStore.getValue(Features.VoiceBroadcast),
isWysiwygLabEnabled: SettingsStore.getValue<boolean>("feature_wysiwyg_composer"),
isRichTextEnabled: true,
initialComposerContent: "",
isWysiwygLabEnabled: isWysiwygLabEnabled,
isRichTextEnabled: isRichTextEnabled,
initialComposerContent: initialComposerContent,
};
this.instanceId = instanceCount++;
@ -154,6 +185,52 @@ export class MessageComposer extends React.Component<IProps, IState> {
SettingsStore.monitorSetting("feature_wysiwyg_composer", null);
}
private get editorStateKey(): string {
let key = WYSIWYG_EDITOR_STATE_STORAGE_PREFIX + this.props.room.roomId;
if (this.props.relation?.rel_type === THREAD_RELATION_TYPE.name) {
key += `_${this.props.relation.event_id}`;
}
return key;
}
private restoreWysiwygEditorState(): WysiwygComposerState | undefined {
const json = localStorage.getItem(this.editorStateKey);
if (json) {
try {
const state: WysiwygComposerState = JSON.parse(json);
return state;
} catch (e) {
logger.error(e);
}
}
return undefined;
}
private saveWysiwygEditorState = (): void => {
if (this.shouldSaveWysiwygEditorState()) {
const { isRichTextEnabled, composerContent } = this.state;
const replyEventId = this.props.replyToEvent ? this.props.replyToEvent.getId() : undefined;
const item: WysiwygComposerState = {
content: composerContent,
isRichText: isRichTextEnabled,
replyEventId: replyEventId,
};
localStorage.setItem(this.editorStateKey, JSON.stringify(item));
} else {
this.clearStoredEditorState();
}
};
// should save state when wysiwyg is enabled and has contents or reply is open
private shouldSaveWysiwygEditorState = (): boolean => {
const { isWysiwygLabEnabled, isComposerEmpty } = this.state;
return isWysiwygLabEnabled && (!isComposerEmpty || !!this.props.replyToEvent);
};
private clearStoredEditorState(): void {
localStorage.removeItem(this.editorStateKey);
}
private get voiceRecording(): Optional<VoiceMessageRecording> {
return this._voiceRecording;
}
@ -265,6 +342,8 @@ export class MessageComposer extends React.Component<IProps, IState> {
UIStore.instance.stopTrackingElementDimensions(`MessageComposer${this.instanceId}`);
UIStore.instance.removeListener(`MessageComposer${this.instanceId}`, this.onResize);
window.removeEventListener("beforeunload", this.saveWysiwygEditorState);
this.saveWysiwygEditorState();
// clean up our listeners by setting our cached recording to falsy (see internal setter)
this.voiceRecording = null;
}