Skip to content

Commit 7af880e

Browse files
committed
Fixes #3340 Save unsent messages when switching chats
- Refactor message correction - Play well with message quoting
1 parent dde316b commit 7af880e

29 files changed

+258
-149
lines changed

Diff for: karma.conf.js

+1
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ module.exports = function(config) {
8585
{ pattern: "src/plugins/muc-views/tests/csn.js", type: 'module' },
8686
{ pattern: "src/plugins/muc-views/tests/deprecated-retractions.js", type: 'module' },
8787
{ pattern: "src/plugins/muc-views/tests/disco.js", type: 'module' },
88+
{ pattern: "src/plugins/muc-views/tests/drafts.js", type: 'module' },
8889
{ pattern: "src/plugins/muc-views/tests/emojis.js", type: 'module' },
8990
{ pattern: "src/plugins/muc-views/tests/hats.js", type: 'module' },
9091
{ pattern: "src/plugins/muc-views/tests/http-file-upload.js", type: 'module' },

Diff for: src/headless/shared/model-with-messages.js

+15-4
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ export default function ModelWithMessages(BaseModel) {
117117

118118
this.listenTo(this.messages, "add", (m) => this.onMessageAdded(m));
119119
this.listenTo(this.messages, "change:upload", (m) => this.onMessageUploadChanged(m));
120+
this.listenTo(this.messages, "change:correcting", (m) => this.onMessageCorrecting(m));
120121
}
121122

122123
fetchMessages() {
@@ -497,6 +498,17 @@ export default function ModelWithMessages(BaseModel) {
497498
}
498499
}
499500

501+
/**
502+
* @param {BaseMessage} message
503+
*/
504+
onMessageCorrecting(message) {
505+
if (message.get('correcting')) {
506+
this.save({ correcting: message.get('id'), draft: u.prefixMentions(message) });
507+
} else {
508+
this.save({ correcting: undefined, draft: undefined });
509+
}
510+
}
511+
500512
onScrolledChanged() {
501513
if (!this.ui.get("scrolled")) {
502514
this.clearUnreadMsgCounter();
@@ -562,12 +574,11 @@ export default function ModelWithMessages(BaseModel) {
562574
message =
563575
message ||
564576
this.messages
565-
.filter({ "sender": "me" })
577+
.filter({ sender: "me" })
566578
.reverse()
567579
.find((m) => m.get("editable"));
568-
if (message) {
569-
message.save("correcting", true);
570-
}
580+
581+
message?.save("correcting", true);
571582
}
572583

573584
editLaterMessage() {

Diff for: src/headless/types/plugins/chat/model.d.ts

+1
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ declare const ChatBox_base: {
101101
chat_state_timeout: NodeJS.Timeout;
102102
onMessageAdded(message: import("../../index.js").BaseMessage<any>): void;
103103
onMessageUploadChanged(message: import("../../index.js").BaseMessage<any>): Promise<void>;
104+
onMessageCorrecting(message: import("../../index.js").BaseMessage<any>): void;
104105
onScrolledChanged(): void;
105106
pruneHistoryWhenScrolledDown(): void;
106107
shouldShowErrorMessage(attrs: import("../../shared/types").MessageAttributes): Promise<boolean>;

Diff for: src/headless/types/plugins/muc/muc.d.ts

+1
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ declare const MUC_base: {
101101
chat_state_timeout: NodeJS.Timeout;
102102
onMessageAdded(message: import("../../shared/message.js").default<any>): void;
103103
onMessageUploadChanged(message: import("../../shared/message.js").default<any>): Promise<void>;
104+
onMessageCorrecting(message: import("../../shared/message.js").default<any>): void;
104105
onScrolledChanged(): void;
105106
pruneHistoryWhenScrolledDown(): void;
106107
shouldShowErrorMessage(attrs: import("../../shared/types").MessageAttributes): Promise<boolean>;

Diff for: src/headless/types/plugins/muc/occupant.d.ts

+1
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ declare const MUCOccupant_base: {
101101
chat_state_timeout: NodeJS.Timeout;
102102
onMessageAdded(message: import("../../index.js").BaseMessage<any>): void;
103103
onMessageUploadChanged(message: import("../../index.js").BaseMessage<any>): Promise<void>;
104+
onMessageCorrecting(message: import("../../index.js").BaseMessage<any>): void;
104105
onScrolledChanged(): void;
105106
pruneHistoryWhenScrolledDown(): void;
106107
shouldShowErrorMessage(attrs: import("../../shared/types").MessageAttributes): Promise<boolean>;

Diff for: src/headless/types/shared/chatbox.d.ts

+1
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ declare const ChatBoxBase_base: {
3131
chat_state_timeout: NodeJS.Timeout;
3232
onMessageAdded(message: import("./message.js").default<any>): void;
3333
onMessageUploadChanged(message: import("./message.js").default<any>): Promise<void>;
34+
onMessageCorrecting(message: import("./message.js").default<any>): void;
3435
onScrolledChanged(): void;
3536
pruneHistoryWhenScrolledDown(): void;
3637
shouldShowErrorMessage(attrs: import("./types.js").MessageAttributes): Promise<boolean>;

Diff for: src/headless/types/shared/model-with-messages.d.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,10 @@ export default function ModelWithMessages<T extends import("./types").ModelExten
119119
* @param {BaseMessage} message
120120
*/
121121
onMessageUploadChanged(message: import("./message").default<any>): Promise<void>;
122+
/**
123+
* @param {BaseMessage} message
124+
*/
125+
onMessageCorrecting(message: import("./message").default<any>): void;
122126
onScrolledChanged(): void;
123127
pruneHistoryWhenScrolledDown(): void;
124128
/**
@@ -295,5 +299,5 @@ export default function ModelWithMessages<T extends import("./types").ModelExten
295299
propertyIsEnumerable(v: PropertyKey): boolean;
296300
};
297301
} & T;
298-
import { Model } from '@converse/skeletor';
302+
import { Model } from "@converse/skeletor";
299303
//# sourceMappingURL=model-with-messages.d.ts.map

Diff for: src/headless/types/utils/index.d.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@ export function isEmptyMessage(attrs: any): boolean;
66
/**
77
* Given a message object, return its text with @ chars
88
* inserted before the mentioned nicknames.
9+
* @param {import('../shared/message').default} message
910
*/
10-
export function prefixMentions(message: any): any;
11+
export function prefixMentions(message: import("../shared/message").default<any>): any;
1112
export function getRandomInt(max: any): number;
1213
/**
1314
* @param {string} [suffix]

Diff for: src/headless/utils/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ export function isEmptyMessage (attrs) {
6363
/**
6464
* Given a message object, return its text with @ chars
6565
* inserted before the mentioned nicknames.
66+
* @param {import('../shared/message').default} message
6667
*/
6768
export function prefixMentions (message) {
6869
let text = message.getMessageText();

Diff for: src/plugins/chatview/message-form.js

+15-30
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,19 @@ export default class MessageForm extends CustomElement {
2323

2424
async initialize() {
2525
await this.model.initialized;
26-
this.listenTo(this.model.messages, "change:correcting", this.onMessageCorrecting);
2726
this.listenTo(this.model, "change:composing_spoiler", () => this.requestUpdate());
27+
this.listenTo(this.model, "change:draft", () => this.requestUpdate());
28+
this.listenTo(this.model, "change:hidden", () => {
29+
if (this.model.get("hidden")) {
30+
const draft_hint = /** @type {HTMLInputElement} */ (this.querySelector(".spoiler-hint"))?.value;
31+
const draft_message = /** @type {HTMLTextAreaElement} */ (this.querySelector(".chat-textarea"))?.value;
32+
u.safeSave(this.model, { draft: draft_message, draft_hint });
33+
}
34+
});
2835

2936
this.handleEmojiSelection = (/** @type { CustomEvent } */ { detail }) => {
3037
if (this.model.get("jid") === detail.jid) {
31-
this.insertIntoTextArea(detail.value, detail.autocompleting, false, detail.ac_position);
38+
this.insertIntoTextArea(detail.value, detail.autocompleting, detail.ac_position);
3239
}
3340
};
3441
document.addEventListener("emojiSelected", this.handleEmojiSelection);
@@ -54,13 +61,8 @@ export default class MessageForm extends CustomElement {
5461
* @param {number} [position] - The end index of the string to be
5562
* replaced with the new value.
5663
*/
57-
insertIntoTextArea(value, replace = false, correcting = false, position, separator = " ") {
64+
insertIntoTextArea(value, replace = false, position, separator = " ") {
5865
const textarea = /** @type {HTMLTextAreaElement} */ (this.querySelector(".chat-textarea"));
59-
if (correcting) {
60-
u.addClass("correcting", textarea);
61-
} else {
62-
u.removeClass("correcting", textarea);
63-
}
6466
if (replace) {
6567
if (position && typeof replace == "string") {
6668
textarea.value = textarea.value.replace(new RegExp(replace, "g"), (match, offset) =>
@@ -81,22 +83,6 @@ export default class MessageForm extends CustomElement {
8183
u.placeCaretAtEnd(textarea);
8284
}
8385

84-
/**
85-
* @param {import('@converse/headless').BaseMessage} message
86-
*/
87-
onMessageCorrecting(message) {
88-
if (message.get("correcting")) {
89-
this.insertIntoTextArea(u.prefixMentions(message), true, true);
90-
} else {
91-
const currently_correcting = this.model.messages.findWhere("correcting");
92-
if (currently_correcting && currently_correcting !== message) {
93-
this.insertIntoTextArea(u.prefixMentions(message), true, true);
94-
} else {
95-
this.insertIntoTextArea("", true, false);
96-
}
97-
}
98-
}
99-
10086
/**
10187
* Handles the escape key press event to stop correcting a message.
10288
* @param {KeyboardEvent} ev
@@ -107,7 +93,6 @@ export default class MessageForm extends CustomElement {
10793
if (message) {
10894
ev.preventDefault();
10995
message.save("correcting", false);
110-
this.insertIntoTextArea("", true, false);
11196
}
11297
}
11398

@@ -122,7 +107,7 @@ export default class MessageForm extends CustomElement {
122107
this.model.sendFiles(Array.from(ev.clipboardData.files));
123108
return;
124109
}
125-
this.model.set({ draft: ev.clipboardData.getData("text/plain") });
110+
this.model.save({ draft: ev.clipboardData.getData("text/plain") });
126111
}
127112

128113
/**
@@ -141,7 +126,8 @@ export default class MessageForm extends CustomElement {
141126
* @param {KeyboardEvent} ev
142127
*/
143128
onKeyUp(ev) {
144-
this.model.set({ draft: /** @type {HTMLTextAreaElement} */ (ev.target).value });
129+
// Trigger an event, for `<converse-message-limit-indicator/>`
130+
this.model.trigger('event:keyup', { ev });
145131
}
146132

147133
/**
@@ -171,13 +157,13 @@ export default class MessageForm extends CustomElement {
171157
return this.onFormSubmitted(ev);
172158
} else if (ev.key === converse.keycodes.UP_ARROW && !target.selectionEnd) {
173159
const textarea = /** @type {HTMLTextAreaElement} */ (this.querySelector(".chat-textarea"));
174-
if (!textarea.value || u.hasClass("correcting", textarea)) {
160+
if (!textarea.value || this.model.get('correcting')) {
175161
return this.model.editEarlierMessage();
176162
}
177163
} else if (
178164
ev.key === converse.keycodes.DOWN_ARROW &&
179165
target.selectionEnd === target.value.length &&
180-
u.hasClass("correcting", this.querySelector(".chat-textarea"))
166+
this.model.get('correcting')
181167
) {
182168
return this.model.editLaterMessage();
183169
}
@@ -231,7 +217,6 @@ export default class MessageForm extends CustomElement {
231217
if (is_command || message) {
232218
hint_el.value = "";
233219
textarea.value = "";
234-
u.removeClass("correcting", textarea);
235220
textarea.style.height = "auto";
236221
this.model.set({ "draft": "" });
237222
}

Diff for: src/plugins/chatview/templates/message-form.js

+13-7
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { __ } from "i18n";
2-
import { api } from "@converse/headless";
2+
import { api, u } from "@converse/headless";
33
import { html } from "lit";
44
import { resetElementHeight } from "../utils.js";
55

@@ -16,10 +16,11 @@ export default (el) => {
1616
const show_send_button = api.settings.get("show_send_button");
1717
const show_spoiler_button = api.settings.get("visible_toolbar_buttons").spoiler;
1818
const show_toolbar = api.settings.get("show_toolbar");
19-
const hint_value = /** @type {HTMLInputElement} */ (el.querySelector(".spoiler-hint"))?.value;
20-
const message_value = /** @type {HTMLTextAreaElement} */ (el.querySelector(".chat-textarea"))?.value;
2119

22-
return html` <form class="chat-message-form" @submit="${/** @param {SubmitEvent} ev */ (ev) => el.onFormSubmitted(ev)}">
20+
return html` <form
21+
class="chat-message-form"
22+
@submit="${/** @param {SubmitEvent} ev */ (ev) => el.onFormSubmitted(ev)}"
23+
>
2324
${show_toolbar
2425
? html` <converse-chat-toolbar
2526
class="btn-toolbar chat-toolbar no-text-select"
@@ -38,24 +39,29 @@ export default (el) => {
3839
type="text"
3940
enterkeyhint="send"
4041
placeholder="${label_spoiler_hint || ""}"
41-
value="${hint_value || ""}"
42+
.value="${el.model.get("draft_hint") ?? ""}"
43+
@change="${
44+
/** @param {Event} ev */ (ev) =>
45+
u.safeSave(el.model, { draft_hint: /** @type {HTMLInputElement} */ (ev.target).value })
46+
}"
4247
class="${composing_spoiler ? "" : "hidden"} spoiler-hint"
4348
/>
4449
<textarea
4550
autofocus
4651
type="text"
4752
enterkeyhint="send"
48-
.value="${message_value || ""}"
53+
.value="${el.model.get("draft") ?? ""}"
4954
@drop="${/** @param {DragEvent} ev */ (ev) => el.onDrop(ev)}"
5055
@input="${resetElementHeight}"
5156
@keydown="${/** @param {KeyboardEvent} ev */ (ev) => el.onKeyDown(ev)}"
5257
@keyup="${/** @param {KeyboardEvent} ev */ (ev) => el.onKeyUp(ev)}"
5358
@paste="${/** @param {ClipboardEvent} ev */ (ev) => el.onPaste(ev)}"
5459
@change="${
5560
/** @param {Event} ev */ (ev) =>
56-
el.model.set({ draft: /** @type {HTMLTextAreaElement} */ (ev.target).value })
61+
u.safeSave(el.model, { draft: /** @type {HTMLTextAreaElement} */ (ev.target).value })
5762
}"
5863
class="chat-textarea
64+
${el.model.get("correcting") ? "correcting" : ""}
5965
${show_send_button ? "chat-textarea-send-button" : ""}
6066
${composing_spoiler ? "spoiler" : ""}"
6167
placeholder="${label_message}"

Diff for: src/plugins/chatview/tests/actions.js

+10-3
Original file line numberDiff line numberDiff line change
@@ -79,14 +79,20 @@ describe("A Chat Message", function () {
7979
let firstAction = view.querySelector('.chat-msg__action-quote');
8080
expect(firstAction).not.toBeNull();
8181
firstAction.click();
82-
expect(textarea.value).toBe('> ' + firstMessageText + '\n');
82+
await u.waitUntil(() => textarea.value === `> ${firstMessageText}`);
83+
84+
8385
// Quote with already-present text
8486
textarea.value = 'Hi!';
87+
textarea.dispatchEvent(new Event('change'));
88+
8589
firstAction.click();
86-
expect(textarea.value).toBe('Hi!\n> ' + firstMessageText + '\n');
90+
await u.waitUntil(() => textarea.value === `Hi!\n> ${firstMessageText}\n`);
8791

8892
// Test messages from other users
8993
textarea.value = '';
94+
textarea.dispatchEvent(new Event('change'));
95+
9096
const secondMessageText = 'Hello';
9197
_converse.handleMessageStanza(
9298
$msg({
@@ -98,12 +104,13 @@ describe("A Chat Message", function () {
98104
.c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree()
99105
);
100106
await u.waitUntil(() => view.querySelectorAll('.chat-msg__text').length === 2);
107+
101108
const quoteActions = view.querySelectorAll('.chat-msg__action-quote');
102109
expect(quoteActions.length).toBe(2);
103110
let secondAction = quoteActions[quoteActions.length - 1];
104111
expect(secondAction).not.toBeNull();
105112
secondAction.click();
106-
expect(textarea.value).toBe('> ' + secondMessageText + '\n');
113+
await u.waitUntil(() => textarea.value === `> ${secondMessageText}`);
107114
}));
108115

109116
});

Diff for: src/plugins/chatview/tests/chatbox.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -255,8 +255,8 @@ describe("Chatboxes", function () {
255255
const toolbar = view.querySelector('.chat-toolbar');
256256
const counter = toolbar.querySelector('.message-limit');
257257
expect(counter.textContent).toBe('200');
258-
view.getMessageForm().insertIntoTextArea('hello world');
259-
await u.waitUntil(() => counter.textContent === '188');
258+
view.model.set({ draft: 'hello world' });
259+
await u.waitUntil(() => counter.textContent === '189');
260260

261261
toolbar.querySelector('.toggle-emojis').click();
262262
const picker = await u.waitUntil(() => view.querySelector('.emoji-picker__lists'));

0 commit comments

Comments
 (0)