compose: Restore the last draft when compose box is opened.

Now when the user opens a narrow that has a draft saved for
that particular conversation, the draft will automatically
be restored in the compose box. This will make it easy to
return to a draft after clicking away, and also will make
it less confusing when people close the compose box by
accident. Note that this only restores the draft when
there is a full recipient specified (stream and topic,
or at least one PM recipient).

Fixes part of #18555.
Fixes #11218 and #17396.
This commit is contained in:
Riken Shah
2021-05-26 18:51:28 +00:00
committed by Tim Abbott
parent cb3f22c30e
commit d48de6da08
5 changed files with 76 additions and 4 deletions

View File

@@ -43,6 +43,23 @@ async function create_stream_message_draft(page: Page): Promise<void> {
await page.click("#compose_close"); await page.click("#compose_close");
} }
async function test_restore_stream_message_draft_by_opening_compose_box(page: Page): Promise<void> {
await page.click(".search_icon");
await page.waitForSelector("#search_query", {visible: true});
await common.select_item_via_typeahead(page, "#search_query", "stream:Denmark topic:tests", "");
await page.click("#left_bar_compose_reply_button_big");
await page.waitForSelector("#send_message_form", {visible: true});
await common.check_compose_state(page, {
stream: "Denmark",
topic: "tests",
content: "Test stream message.",
});
await page.click("#compose_close");
await page.waitForSelector("#send_message_form", {visible: false});
}
async function create_private_message_draft(page: Page): Promise<void> { async function create_private_message_draft(page: Page): Promise<void> {
console.log("Creating direct message draft"); console.log("Creating direct message draft");
await page.keyboard.press("KeyX"); await page.keyboard.press("KeyX");
@@ -50,7 +67,17 @@ async function create_private_message_draft(page: Page): Promise<void> {
await common.fill_form(page, "form#send_message_form", {content: "Test direct message."}); await common.fill_form(page, "form#send_message_form", {content: "Test direct message."});
await common.pm_recipient.set(page, "cordelia@zulip.com"); await common.pm_recipient.set(page, "cordelia@zulip.com");
await common.pm_recipient.set(page, "hamlet@zulip.com"); await common.pm_recipient.set(page, "hamlet@zulip.com");
}
async function test_restore_private_message_draft_by_opening_composebox(page: Page): Promise<void> {
await page.click("#left_bar_compose_reply_button_big");
await page.waitForSelector("#private_message_recipient", {visible: true});
await common.check_form_contents(page, "form#send_message_form", {
content: "Test direct message.",
});
await page.click("#compose_close"); await page.click("#compose_close");
await page.waitForSelector("#private_message_recipient", {visible: false});
} }
async function open_compose_markdown_preview(page: Page): Promise<void> { async function open_compose_markdown_preview(page: Page): Promise<void> {
@@ -99,7 +126,7 @@ async function test_previously_created_drafts_rendered(page: Page): Promise<void
page, page,
"#drafts_table .overlay-message-row .message_header_private_message .stream_label", "#drafts_table .overlay-message-row .message_header_private_message .stream_label",
), ),
"You and Cordelia, Lear's daughter, King Hamlet", "You and King Hamlet, Cordelia, Lear's daughter",
); );
assert.strictEqual( assert.strictEqual(
await common.get_text_from_selector( await common.get_text_from_selector(
@@ -174,7 +201,7 @@ async function test_restore_private_message_draft_via_draft_overlay(page: Page):
}); });
const cordelia_internal_email = await common.get_internal_email_from_name(page, "cordelia"); const cordelia_internal_email = await common.get_internal_email_from_name(page, "cordelia");
const hamlet_internal_email = await common.get_internal_email_from_name(page, "hamlet"); const hamlet_internal_email = await common.get_internal_email_from_name(page, "hamlet");
await common.pm_recipient.expect(page, `${cordelia_internal_email},${hamlet_internal_email}`); await common.pm_recipient.expect(page, `${hamlet_internal_email},${cordelia_internal_email}`);
assert.strictEqual( assert.strictEqual(
await common.get_text_from_selector(page, "title"), await common.get_text_from_selector(page, "title"),
"Cordelia, Lear's daughter, King Hamlet - Zulip Dev - Zulip", "Cordelia, Lear's daughter, King Hamlet - Zulip Dev - Zulip",
@@ -259,7 +286,22 @@ async function drafts_test(page: Page): Promise<void> {
await test_empty_drafts(page); await test_empty_drafts(page);
await create_stream_message_draft(page); await create_stream_message_draft(page);
await test_restore_stream_message_draft_by_opening_compose_box(page);
// Send a private message so that the draft we create is
// for an existing conversation.
await common.send_message(page, "private", {
recipient: "cordelia@zulip.com, hamlet@zulip.com",
content: "howdy doo",
outside_view: true,
});
await create_private_message_draft(page); await create_private_message_draft(page);
// Narrow to the conversation so that the compose box will restore it,
// then close and try restoring it by opening the composebox again.
await page.click("#compose .narrow_to_compose_recipients");
await page.click("#compose_close");
await test_restore_private_message_draft_by_opening_composebox(page);
await open_drafts_after_markdown_preview(page); await open_drafts_after_markdown_preview(page);
await test_previously_created_drafts_rendered(page); await test_previously_created_drafts_rendered(page);

View File

@@ -302,6 +302,20 @@ export function start(raw_opts: ComposeActionsStartOpts): void {
opts.private_message_recipient.replaceAll(/,\s*/g, ", "), opts.private_message_recipient.replaceAll(/,\s*/g, ", "),
); );
// If we're not explicitly opening a different draft, restore the last
// saved draft (if it exists).
if (
!opts.content &&
opts.draft_id === undefined &&
compose_state.message_content().length === 0
) {
const possible_last_draft = drafts.get_last_draft_based_on_compose_state();
if (possible_last_draft !== undefined) {
opts.draft_id = possible_last_draft.id;
opts.content = possible_last_draft.content;
}
}
if (opts.content !== undefined) { if (opts.content !== undefined) {
compose_ui.insert_and_scroll_into_view(opts.content, $("textarea#compose-textarea"), true); compose_ui.insert_and_scroll_into_view(opts.content, $("textarea#compose-textarea"), true);
$(".compose_control_button_container:has(.add-poll)").addClass("disabled-on-hover"); $(".compose_control_button_container:has(.add-poll)").addClass("disabled-on-hover");

View File

@@ -493,6 +493,20 @@ export function filter_drafts_by_compose_box_and_recipient(
return _.pick(drafts, narrow_drafts_ids); return _.pick(drafts, narrow_drafts_ids);
} }
export function get_last_draft_based_on_compose_state(): LocalStorageDraftWithId | undefined {
const current_drafts = draft_model.get();
const drafts_map_for_compose_state = filter_drafts_by_compose_box_and_recipient(current_drafts);
const drafts_for_compose_state = Object.entries(drafts_map_for_compose_state).map(
([draft_id, draft]) => ({
...draft,
id: draft_id,
}),
);
return drafts_for_compose_state
.sort((draft_a, draft_b) => draft_a.updatedAt - draft_b.updatedAt)
.pop();
}
export function remove_old_drafts(): void { export function remove_old_drafts(): void {
const old_date = subDays(new Date(), DRAFT_LIFETIME).getTime(); const old_date = subDays(new Date(), DRAFT_LIFETIME).getTime();
const drafts = draft_model.get(); const drafts = draft_model.get();

View File

@@ -45,6 +45,7 @@ mock_esm("../src/reload_state", {
mock_esm("../src/drafts", { mock_esm("../src/drafts", {
update_draft: noop, update_draft: noop,
update_compose_draft_count: noop, update_compose_draft_count: noop,
get_last_draft_based_on_compose_state: noop,
}); });
mock_esm("../src/unread_ops", { mock_esm("../src/unread_ops", {
notify_server_message_read: noop, notify_server_message_read: noop,

View File

@@ -65,9 +65,10 @@ const drafts_overlay_ui = zrequire("drafts_overlay_ui");
const timerender = zrequire("timerender"); const timerender = zrequire("timerender");
const mock_current_timestamp = 1234; const mock_current_timestamp = 1234;
const stream_id = 30;
const draft_1 = { const draft_1 = {
stream_id: 30, stream_id,
topic: "topic", topic: "topic",
type: "stream", type: "stream",
content: "Test stream message", content: "Test stream message",
@@ -81,7 +82,7 @@ const draft_2 = {
updatedAt: mock_current_timestamp, updatedAt: mock_current_timestamp,
}; };
const short_msg = { const short_msg = {
stream_id: 30, stream_id,
topic: "topic", topic: "topic",
type: "stream", type: "stream",
content: "a", content: "a",