Skip to content

Commit 99c0f13

Browse files
committed
Fixes #3340 Save unsent messages when switching chats
1 parent 3d5b8b0 commit 99c0f13

File tree

7 files changed

+100
-17
lines changed

7 files changed

+100
-17
lines changed

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' },

src/plugins/chatview/message-form.js

+7
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,13 @@ export default class MessageForm extends CustomElement {
2525
await this.model.initialized;
2626
this.listenTo(this.model.messages, "change:correcting", this.onMessageCorrecting);
2727
this.listenTo(this.model, "change:composing_spoiler", () => 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) {

src/plugins/chatview/templates/message-form.js

+12-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,22 +39,26 @@ 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
5964
${show_send_button ? "chat-textarea-send-button" : ""}

src/plugins/chatview/tests/spoilers.js

+41
Original file line numberDiff line numberDiff line change
@@ -201,4 +201,45 @@ describe("A spoiler message", function () {
201201
spoiler_toggle.click();
202202
await u.waitUntil(() => Array.from(spoiler_msg_el.classList).includes('hidden'));
203203
}));
204+
205+
it("can be saved as an unsent draft",
206+
mock.initConverse(['chatBoxesFetched'], { ...settings, view_mode: 'fullscreen' }, async (_converse) => {
207+
208+
const { api } = _converse;
209+
await mock.waitForRoster(_converse, 'current', 2);
210+
mock.openControlBox(_converse);
211+
const contact1_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
212+
const contact2_jid = mock.cur_names[1].replace(/ /g,'.').toLowerCase() + '@montague.lit';
213+
214+
// XXX: We need to send a presence from the contact, so that we
215+
// have a resource, that resource is then queried to see
216+
// whether Strophe.NS.SPOILER is supported, in which case
217+
// the spoiler button will appear.
218+
const presence = stx`<presence xmlns="jabber:client" from="${contact1_jid}/phone" to="[email protected]"/>`;
219+
api.connection.get()._dataRecv(mock.createRequest(presence));
220+
221+
await mock.openChatBoxFor(_converse, contact1_jid);
222+
await mock.waitUntilDiscoConfirmed(_converse, contact1_jid+'/phone', [], [Strophe.NS.SPOILER]);
223+
const view = _converse.chatboxviews.get(contact1_jid);
224+
spyOn(api.connection.get(), 'send');
225+
226+
await u.waitUntil(() => view.querySelector('.toggle-compose-spoiler'));
227+
let spoiler_toggle = view.querySelector('.toggle-compose-spoiler');
228+
spoiler_toggle.click();
229+
230+
let hint_input = view.querySelector('.spoiler-hint');
231+
hint_input.value = 'This is the hint';
232+
233+
let textarea = view.querySelector('.chat-textarea');
234+
textarea.value = 'This is the spoiler';
235+
236+
await mock.openChatBoxFor(_converse, contact2_jid);
237+
await mock.openChatBoxFor(_converse, contact1_jid);
238+
239+
hint_input = view.querySelector('.spoiler-hint');
240+
expect(hint_input.value).toBe('This is the hint');
241+
242+
textarea = view.querySelector('.chat-textarea');
243+
expect(textarea.value).toBe('This is the spoiler');
244+
}));
204245
});

src/plugins/muc-views/styles/muc.scss

+1
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@
9595
.chat-info {
9696
color: var(--info-color);
9797
line-height: normal;
98+
margin-bottom: 0.3em;
9899
&.badge {
99100
color: var(--muc-color);
100101
}

src/plugins/muc-views/templates/message-form.js

+14-10
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 "plugins/chatview/utils.js";
55

@@ -16,12 +16,10 @@ 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 {HTMLInputElement} */ (el.querySelector(".chat-textarea"))?.value;
2119
return html` <form class="setNicknameButtonForm hidden">
2220
<input type="submit" class="btn btn-primary" name="join" value="Join" />
2321
</form>
24-
<form class="chat-message-form" @submit=${(ev) => el.onFormSubmitted(ev)}>
22+
<form class="chat-message-form" @submit="${/** @param {SubmitEvent} ev */ (ev) => el.onFormSubmitted(ev)}">
2523
${show_toolbar
2624
? html` <converse-chat-toolbar
2725
class="btn-toolbar chat-toolbar no-text-select"
@@ -40,27 +38,33 @@ export default (el) => {
4038
<input
4139
type="text"
4240
placeholder="${label_spoiler_hint || ""}"
43-
value="${hint_value || ""}"
41+
.value="${el.model.get("draft_hint") ?? ""}"
42+
@change="${
43+
/** @param {Event} ev */ (ev) =>
44+
u.safeSave(el.model, { draft_hint: /** @type {HTMLInputElement} */ (ev.target).value })
45+
}"
4446
class="${composing_spoiler ? "" : "hidden"} spoiler-hint"
4547
/>
4648
<div class="suggestion-box">
4749
<ul class="suggestion-box__results suggestion-box__results--above" hidden=""></ul>
4850
<textarea
4951
autofocus
5052
type="text"
51-
@drop=${(ev) => el.onDrop(ev)}
53+
.value="${el.model.get("draft") ?? ""}"
54+
@drop="${/** @param {DragEvent} ev */ (ev) => el.onDrop(ev)}"
5255
@input=${resetElementHeight}
5356
@keydown="${/** @param {KeyboardEvent} ev */ (ev) => el.onKeyDown(ev)}"
5457
@keyup="${/** @param {KeyboardEvent} ev */ (ev) => el.onKeyUp(ev)}"
5558
@paste="${/** @param {ClipboardEvent} ev */ (ev) => el.onPaste(ev)}"
56-
@change=${(ev) => el.model.set({ "draft": ev.target.value })}
59+
@change="${
60+
/** @param {Event} ev */ (ev) =>
61+
u.safeSave(el.model, { draft: /** @type {HTMLTextAreaElement} */ (ev.target).value })
62+
}"
5763
class="chat-textarea suggestion-box__input
5864
${show_send_button ? "chat-textarea-send-button" : ""}
5965
${composing_spoiler ? "spoiler" : ""}"
6066
placeholder="${label_message}"
61-
>
62-
${message_value || ""}</textarea
63-
>
67+
></textarea>
6468
<span
6569
class="suggestion-box__additions visually-hidden"
6670
role="status"

src/plugins/muc-views/tests/drafts.js

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/*global mock, converse */
2+
const { u } = converse.env;
3+
4+
describe("An unsent groupchat message", function () {
5+
it(
6+
"will be saved as a draft when switching chats",
7+
mock.initConverse([], { view_mode: "fullscreen" }, async function (_converse) {
8+
const muc1_jid = "[email protected]";
9+
const muc2_jid = "[email protected]";
10+
11+
await mock.openAndEnterMUC(_converse, muc1_jid, "romeo");
12+
const view = _converse.chatboxviews.get(muc1_jid);
13+
14+
const textarea = await u.waitUntil(() => view.querySelector(".chat-textarea"));
15+
textarea.value = "This is an unsaved message";
16+
17+
await mock.openAndEnterMUC(_converse, muc2_jid, "romeo");
18+
19+
// Switch back to the room with the draft
20+
document.querySelector(`converse-rooms-list li[data-room-jid="${muc1_jid}"] a`).click();
21+
expect(view.querySelector(".chat-textarea").value).toBe("This is an unsaved message");
22+
})
23+
);
24+
});

0 commit comments

Comments
 (0)