narrow: Use message list id to track message lists in DOM.

This removes use of zfilt and zhome from codebase.
This commit is contained in:
Aman Agrawal 2024-01-17 06:53:40 +00:00 committed by Tim Abbott
parent 91ee0bf676
commit 633f64a79e
31 changed files with 297 additions and 179 deletions

View File

@ -255,12 +255,7 @@
<div id="loading_older_messages_indicator"></div>
<div id="page_loading_indicator"></div>
<div id="message_feed_errors_container"></div>
<div id="message-lists-container">
<div class="message-list focused-message-list" id="zhome" role="list" aria-live="polite" aria-label="{{ _('Messages') }}">
</div>
<div class="message-list" id="zfilt" role="list" aria-live="polite" aria-label="{{ _('Messages') }}">
</div>
</div>
<div id="message-lists-container"></div>
<div id="scheduled_message_indicator">
</div>
<div id="typing_notifications">

View File

@ -57,10 +57,14 @@ async function run() {
await page.goto(`${options.realmUri}/#narrow/id/${options.messageId}`, {
waitUntil: "networkidle2",
});
const messageSelector = `#zfilt${CSS.escape(options.messageId)}`;
// eslint-disable-next-line no-undef
const message_list_id = await page.evaluate(() => zulip_test.current_msg_list.id);
const messageSelector = `#message-row-${message_list_id}-${CSS.escape(options.messageId)}`;
await page.waitForSelector(messageSelector);
// remove unread marker and don't select message
const marker = `#zfilt${CSS.escape(options.messageId)} .unread_marker`;
const marker = `#message-row-${message_list_id}-${CSS.escape(
options.messageId,
)} .unread_marker`;
await page.evaluate((sel) => $(sel).remove(), marker);
const messageBox = await page.$(messageSelector);
await page.evaluate((msg) => $(msg).removeClass("selected_message"), messageSelector);

View File

@ -28,14 +28,14 @@ function get_message_selector(text: string): string {
}
async function test_send_messages(page: Page): Promise<void> {
const initial_msgs_count = (await page.$$("#zhome .message_row")).length;
const initial_msgs_count = (await page.$$(".message-list .message_row")).length;
await common.send_multiple_messages(page, [
{stream_name: "Verona", topic: "Reply test", content: "Compose stream reply test"},
{recipient: "cordelia@zulip.com", content: "Compose direct message reply test"},
]);
assert.equal((await page.$$("#zhome .message_row")).length, initial_msgs_count + 2);
assert.equal((await page.$$(".message-list .message_row")).length, initial_msgs_count + 2);
}
async function test_stream_compose_keyboard_shortcut(page: Page): Promise<void> {
@ -138,7 +138,9 @@ async function test_send_multirecipient_pm_from_cordelia_pm_narrow(page: Page):
// Go back to all messages view and make sure all messages are loaded.
await page.click("#left-sidebar-navigation-list .top_left_all_messages");
await page.waitForSelector("#zhome .message_row", {visible: true});
await page.waitForSelector(".message-list .message_row", {visible: true});
// Assert that there is only one message list.
assert.equal((await page.$$(".message-list")).length, 1);
const pm = await page.waitForSelector(
`xpath/(//*[${common.has_class_x(
"messagebox",
@ -217,7 +219,7 @@ async function test_markdown_preview(page: Page): Promise<void> {
async function compose_tests(page: Page): Promise<void> {
await common.log_in(page);
await page.click("#left-sidebar-navigation-list .top_left_all_messages");
await page.waitForSelector("#zhome .message_row", {visible: true});
await page.waitForSelector(".message-list .message_row", {visible: true});
await test_send_messages(page);
await test_keyboard_shortcuts(page);
await test_reply_by_click_prepopulates_stream_topic_names(page);

View File

@ -12,7 +12,7 @@ async function copy_messages(
return await page.evaluate(
(start_message: string, end_message: string) => {
function get_message_node(message: string): Element {
return [...document.querySelectorAll("#zhome .message_content")].find(
return [...document.querySelectorAll(".message-list .message_content")].find(
(node) => node.textContent?.trim() === message,
)!;
}
@ -130,7 +130,9 @@ async function test_copying_messages_from_several_topics(page: Page): Promise<vo
async function copy_paste_test(page: Page): Promise<void> {
await common.log_in(page);
await page.click("#left-sidebar-navigation-list .top_left_all_messages");
await page.waitForSelector("#zhome .message_row", {visible: true});
await page.waitForSelector(".message-list .message_row", {visible: true});
// Assert that there is only one message list.
assert.equal((await page.$$(".message-list")).length, 1);
await common.send_multiple_messages(page, [
{stream_name: "Verona", topic: "copy-paste-topic #1", content: "copy paste test A"},
@ -148,7 +150,8 @@ async function copy_paste_test(page: Page): Promise<void> {
{stream_name: "Verona", topic: "copy-paste-topic #3", content: "copy paste test G"},
]);
await common.check_messages_sent(page, "zhome", [
const message_list_id = await common.get_current_msg_list_id(page, true);
await common.check_messages_sent(page, message_list_id, [
["Verona > copy-paste-topic #1", ["copy paste test A", "copy paste test B"]],
[
"Verona > copy-paste-topic #2",

View File

@ -5,7 +5,7 @@ import type {Page} from "puppeteer";
import * as common from "./lib/common";
async function click_delete_and_return_last_msg_id(page: Page): Promise<string> {
const msg = (await page.$$("#zhome .message_row")).at(-1);
const msg = (await page.$$(".message-list .message_row")).at(-1);
assert.ok(msg !== undefined);
const id = await (await msg.getProperty("id")).jsonValue();
await msg.hover();
@ -23,8 +23,14 @@ async function click_delete_and_return_last_msg_id(page: Page): Promise<string>
async function delete_message_test(page: Page): Promise<void> {
await common.log_in(page);
await page.click("#left-sidebar-navigation-list .top_left_all_messages");
await page.waitForSelector("#zhome .message_row", {visible: true});
const messages_quantity = (await page.$$("#zhome .message_row")).length;
const message_list_id = await common.get_current_msg_list_id(page, true);
await page.waitForSelector(
`.message-list[data-message-list-id='${message_list_id}'] .message_row`,
{visible: true},
);
// Assert that there is only one message list.
assert.equal((await page.$$(".message-list")).length, 1);
const messages_quantity = (await page.$$(".message-list .message_row")).length;
const last_message_id = await click_delete_and_return_last_msg_id(page);
await common.wait_for_micromodal_to_open(page);
@ -34,7 +40,7 @@ async function delete_message_test(page: Page): Promise<void> {
await common.wait_for_micromodal_to_close(page);
await page.waitForSelector(`#${CSS.escape(last_message_id)}`, {hidden: true});
assert.equal((await page.$$("#zhome .message_row")).length, messages_quantity - 1);
assert.equal((await page.$$(".message-list .message_row")).length, messages_quantity - 1);
}
common.run_test(delete_message_test);

View File

@ -246,7 +246,9 @@ async function test_save_draft_by_reloading(page: Page): Promise<void> {
async function drafts_test(page: Page): Promise<void> {
await common.log_in(page);
await page.click("#left-sidebar-navigation-list .top_left_all_messages");
await page.waitForSelector("#zhome .message_row", {visible: true});
await page.waitForSelector(".message-list .message_row", {visible: true});
// Assert that there is only one message list.
assert.equal((await page.$$(".message-list")).length, 1);
await test_empty_drafts(page);

View File

@ -5,7 +5,7 @@ import type {Page} from "puppeteer";
import * as common from "./lib/common";
async function trigger_edit_last_message(page: Page): Promise<void> {
const msg = (await page.$$("#zhome .message_row")).at(-1);
const msg = (await page.$$(".message-list .message_row")).at(-1);
assert.ok(msg !== undefined);
const id = await (await msg.getProperty("id")).jsonValue();
await msg.hover();
@ -29,7 +29,7 @@ async function edit_stream_message(page: Page, content: string): Promise<void> {
await common.wait_for_fully_processed_message(page, content);
}
async function test_stream_message_edit(page: Page): Promise<void> {
async function test_stream_message_edit(page: Page, message_list_id: number): Promise<void> {
await common.send_message(page, "stream", {
stream_name: "Verona",
topic: "edits",
@ -38,11 +38,13 @@ async function test_stream_message_edit(page: Page): Promise<void> {
await edit_stream_message(page, "test edited");
await common.check_messages_sent(page, "zhome", [["Verona > edits", ["test edited"]]]);
await common.check_messages_sent(page, message_list_id, [["Verona > edits", ["test edited"]]]);
}
async function test_edit_message_with_slash_me(page: Page): Promise<void> {
const last_message_xpath = `(//*[@id="zhome"]//*[${common.has_class_x("messagebox")}])[last()]`;
const last_message_xpath = `(//*[${common.has_class_x("message-list")}]//*[${common.has_class_x(
"messagebox",
)}])[last()]`;
await common.send_message(page, "stream", {
stream_name: "Verona",
@ -74,7 +76,7 @@ async function test_edit_message_with_slash_me(page: Page): Promise<void> {
);
}
async function test_edit_private_message(page: Page): Promise<void> {
async function test_edit_private_message(page: Page, message_list_id: number): Promise<void> {
await common.send_message(page, "private", {
recipient: "cordelia@zulip.com",
content: "test editing pm",
@ -85,7 +87,7 @@ async function test_edit_private_message(page: Page): Promise<void> {
await page.click(".message_edit_save");
await common.wait_for_fully_processed_message(page, "test edited pm");
await common.check_messages_sent(page, "zhome", [
await common.check_messages_sent(page, message_list_id, [
["You and Cordelia, Lear's daughter", ["test edited pm"]],
]);
}
@ -93,11 +95,12 @@ async function test_edit_private_message(page: Page): Promise<void> {
async function edit_tests(page: Page): Promise<void> {
await common.log_in(page);
await page.click("#left-sidebar-navigation-list .top_left_all_messages");
await page.waitForSelector("#zhome .message_row", {visible: true});
await page.waitForSelector(".message-list .message_row", {visible: true});
const message_list_id = await common.get_current_msg_list_id(page, true);
await test_stream_message_edit(page);
await test_stream_message_edit(page, message_list_id);
await test_edit_message_with_slash_me(page);
await test_edit_private_message(page);
await test_edit_private_message(page, message_list_id);
}
common.run_test(edit_tests);

View File

@ -26,6 +26,8 @@ export const is_firefox = process.env.PUPPETEER_PRODUCT === "firefox";
let realm_url = "http://zulip.zulipdev.com:9981/";
const gps = new StackTraceGPS({ajax: async (url) => (await fetch(url)).text()});
let last_current_msg_list_id: number | null = null;
export const pm_recipient = {
async set(page: Page, recipient: string): Promise<void> {
// Without using the delay option here there seems to be
@ -483,9 +485,11 @@ export async function send_multiple_messages(page: Page, msgs: Message[]): Promi
*/
export async function get_rendered_messages(
page: Page,
table = "zhome",
message_list_id: number,
): Promise<[string, string[]][]> {
const recipient_rows = await page.$$(`#${CSS.escape(table)} .recipient_row`);
const recipient_rows = await page.$$(
`.message-list[data-message-list-id='${message_list_id}'] .recipient_row`,
);
return Promise.all(
recipient_rows.map(async (element): Promise<[string, string[]]> => {
const stream_label = await element.$(".stream_label");
@ -519,11 +523,13 @@ export async function get_rendered_messages(
// messages array passed exist in the order they are passed.
export async function check_messages_sent(
page: Page,
table: string,
message_list_id: number,
messages: [string, string[]][],
): Promise<void> {
await page.waitForSelector(`#${CSS.escape(table)}`, {visible: true});
const rendered_messages = await get_rendered_messages(page, table);
await page.waitForSelector(`.message-list[data-message-list-id='${message_list_id}']`, {
visible: true,
});
const rendered_messages = await get_rendered_messages(page, message_list_id);
// We only check the last n messages because if we run
// the test with --interactive there will be duplicates.
@ -714,3 +720,23 @@ export function run_test(test_function: (page: Page) => Promise<void>): void {
process.exit(1);
});
}
export async function get_current_msg_list_id(
page: Page,
wait_for_change = false,
): Promise<number> {
if (wait_for_change) {
// Wait for the current_msg_list to change if the in the middle of switching narrows.
// Also works as a way to verify that the current message list did change.
// NOTE: This only checks if the current message list id changed from the last call to this function,
// so, make sure to have a call to this function before changing to the narrow that you want to check.
await page.waitForFunction(
(last_current_msg_list_id) =>
zulip_test.current_msg_list.id !== last_current_msg_list_id,
{},
last_current_msg_list_id,
);
}
last_current_msg_list_id = await page.evaluate(() => zulip_test.current_msg_list.id);
return last_current_msg_list_id!;
}

View File

@ -7,7 +7,7 @@ import * as common from "./lib/common";
async function test_mention(page: Page): Promise<void> {
await common.log_in(page);
await page.click("#left-sidebar-navigation-list .top_left_all_messages");
await page.waitForSelector("#zhome .message_row", {visible: true});
await page.waitForSelector(".message-list .message_row", {visible: true});
await page.keyboard.press("KeyC");
await page.waitForSelector("#compose", {visible: true});
@ -36,7 +36,10 @@ async function test_mention(page: Page): Promise<void> {
await page.click("#compose_banners .wildcard_warning .main-view-banner-action-button");
await page.waitForSelector(".wildcard_warning", {hidden: true});
await common.check_messages_sent(page, "zhome", [["Verona > Test mention all", ["@all"]]]);
const message_list_id = await common.get_current_msg_list_id(page, true);
await common.check_messages_sent(page, message_list_id, [
["Verona > Test mention all", ["@all"]],
]);
}
common.run_test(test_mention);

View File

@ -10,7 +10,8 @@ async function get_stream_li(page: Page, stream_name: string): Promise<string> {
}
async function expect_home(page: Page): Promise<void> {
await common.check_messages_sent(page, "zhome", [
const message_list_id = await common.get_current_msg_list_id(page, true);
await common.check_messages_sent(page, message_list_id, [
["Verona > test", ["verona test a", "verona test b"]],
["Verona > other topic", ["verona other topic c"]],
["Denmark > test", ["denmark message"]],
@ -26,8 +27,11 @@ async function expect_home(page: Page): Promise<void> {
}
async function expect_verona_stream(page: Page): Promise<void> {
await page.waitForSelector("#zfilt", {visible: true});
await common.check_messages_sent(page, "zfilt", [
const message_list_id = await common.get_current_msg_list_id(page, true);
await page.waitForSelector(`.message-list[data-message-list-id='${message_list_id}']`, {
visible: true,
});
await common.check_messages_sent(page, message_list_id, [
["Verona > test", ["verona test a", "verona test b"]],
["Verona > other topic", ["verona other topic c"]],
["Verona > test", ["verona test d"]],
@ -36,8 +40,11 @@ async function expect_verona_stream(page: Page): Promise<void> {
}
async function expect_verona_stream_test_topic(page: Page): Promise<void> {
await page.waitForSelector("#zfilt", {visible: true});
await common.check_messages_sent(page, "zfilt", [
const message_list_id = await common.get_current_msg_list_id(page, true);
await page.waitForSelector(`.message-list[data-message-list-id='${message_list_id}']`, {
visible: true,
});
await common.check_messages_sent(page, message_list_id, [
["Verona > test", ["verona test a", "verona test b", "verona test d"]],
]);
assert.strictEqual(
@ -47,15 +54,21 @@ async function expect_verona_stream_test_topic(page: Page): Promise<void> {
}
async function expect_verona_other_topic(page: Page): Promise<void> {
await page.waitForSelector("#zfilt", {visible: true});
await common.check_messages_sent(page, "zfilt", [
const message_list_id = await common.get_current_msg_list_id(page, true);
await page.waitForSelector(`.message-list[data-message-list-id='${message_list_id}']`, {
visible: true,
});
await common.check_messages_sent(page, message_list_id, [
["Verona > other topic", ["verona other topic c"]],
]);
}
async function expect_test_topic(page: Page): Promise<void> {
await page.waitForSelector("#zfilt", {visible: true});
await common.check_messages_sent(page, "zfilt", [
const message_list_id = await common.get_current_msg_list_id(page, true);
await page.waitForSelector(`.message-list[data-message-list-id='${message_list_id}']`, {
visible: true,
});
await common.check_messages_sent(page, message_list_id, [
["Verona > test", ["verona test a", "verona test b"]],
["Denmark > test", ["denmark message"]],
["Verona > test", ["verona test d"]],
@ -63,8 +76,11 @@ async function expect_test_topic(page: Page): Promise<void> {
}
async function expect_group_direct_messages(page: Page): Promise<void> {
await page.waitForSelector("#zfilt", {visible: true});
await common.check_messages_sent(page, "zfilt", [
const message_list_id = await common.get_current_msg_list_id(page, true);
await page.waitForSelector(`.message-list[data-message-list-id='${message_list_id}']`, {
visible: true,
});
await common.check_messages_sent(page, message_list_id, [
[
"You and Cordelia, Lear's daughter, King Hamlet",
["group direct message a", "group direct message b", "group direct message d"],
@ -77,8 +93,11 @@ async function expect_group_direct_messages(page: Page): Promise<void> {
}
async function expect_cordelia_direct_messages(page: Page): Promise<void> {
await page.waitForSelector("#zfilt", {visible: true});
await common.check_messages_sent(page, "zfilt", [
const message_list_id = await common.get_current_msg_list_id(page, true);
await page.waitForSelector(`.message-list[data-message-list-id='${message_list_id}']`, {
visible: true,
});
await common.check_messages_sent(page, message_list_id, [
["You and Cordelia, Lear's daughter", ["direct message c", "direct message e"]],
]);
}
@ -88,7 +107,9 @@ async function un_narrow(page: Page): Promise<void> {
await page.keyboard.press("Escape");
}
await page.click("#left-sidebar-navigation-list .top_left_all_messages");
await page.waitForSelector("#zhome .message_row", {visible: true});
await page.waitForSelector(".message-list .message_row", {visible: true});
// Assert that there is only one message list.
assert.equal((await page.$$(".message-list")).length, 1);
assert.strictEqual(await page.title(), "All messages - Zulip Dev - Zulip");
}
@ -104,7 +125,7 @@ async function expect_recent_view(page: Page): Promise<void> {
async function test_navigations_from_home(page: Page): Promise<void> {
return; // No idea why this is broken.
console.log("Narrowing by clicking stream");
await page.click(`#zhome [title='Narrow to stream "Verona"']`);
await page.click(`.focused-message-list [title='Narrow to stream "Verona"']`);
await expect_verona_stream(page);
assert.strictEqual(await page.title(), "#Verona - Zulip Dev - Zulip");
@ -112,7 +133,7 @@ async function test_navigations_from_home(page: Page): Promise<void> {
await expect_home(page);
console.log("Narrowing by clicking topic");
await page.click(`#zhome [title='Narrow to stream "Verona", topic "test"']`);
await page.click(`.focused-message-list [title='Narrow to stream "Verona", topic "test"']`);
await expect_verona_stream_test_topic(page);
await un_narrow(page);
@ -121,7 +142,7 @@ async function test_navigations_from_home(page: Page): Promise<void> {
return; // TODO: rest of this test seems nondeterministically broken
console.log("Narrowing by clicking group personal header");
await page.click(
`#zhome [title="Narrow to your direct messages with Cordelia, Lear's daughter, King Hamlet"]`,
`.focused-message-list [title="Narrow to your direct messages with Cordelia, Lear's daughter, King Hamlet"]`,
);
await expect_group_direct_messages(page);
@ -129,7 +150,7 @@ async function test_navigations_from_home(page: Page): Promise<void> {
await expect_home(page);
await page.click(
`#zhome [title="Narrow to your direct messages with Cordelia, Lear's daughter, King Hamlet"]`,
`.focused-message-list [title="Narrow to your direct messages with Cordelia, Lear's daughter, King Hamlet"]`,
);
await un_narrow_by_clicking_org_icon(page);
await expect_recent_view(page);
@ -161,11 +182,13 @@ async function search_silent_user(page: Page, str: string, item: string): Promis
await common.get_text_from_selector(page, ".empty_feed_notice"),
expect_message,
);
await common.get_current_msg_list_id(page, true);
await un_narrow(page);
await expect_home(page);
}
async function expect_non_existing_user(page: Page): Promise<void> {
await common.get_current_msg_list_id(page, true);
await page.waitForSelector(".empty_feed_notice", {visible: true});
const expected_message = "This user does not exist!";
assert.strictEqual(
@ -175,6 +198,7 @@ async function expect_non_existing_user(page: Page): Promise<void> {
}
async function expect_non_existing_users(page: Page): Promise<void> {
await common.get_current_msg_list_id(page, true);
await page.waitForSelector(".empty_feed_notice", {visible: true});
const expected_message = "One or more of these users do not exist!";
assert.strictEqual(
@ -263,8 +287,11 @@ async function search_tests(page: Page): Promise<void> {
}
async function expect_all_direct_messages(page: Page): Promise<void> {
await page.waitForSelector("#zfilt", {visible: true});
await common.check_messages_sent(page, "zfilt", [
const message_list_id = await common.get_current_msg_list_id(page, true);
await page.waitForSelector(`.message-list[data-message-list-id='${message_list_id}']`, {
visible: true,
});
await common.check_messages_sent(page, message_list_id, [
[
"You and Cordelia, Lear's daughter, King Hamlet",
["group direct message a", "group direct message b"],
@ -477,18 +504,34 @@ async function test_narrow_public_streams(page: Page): Promise<void> {
await page.click(".subscriptions-header .exit-sign");
await page.waitForSelector("#subscription_overlay", {hidden: true});
await page.goto(`http://zulip.zulipdev.com:9981/#narrow/stream/${stream_id}-Denmark`);
await page.waitForSelector("#zfilt .recipient_row ~ .recipient_row ~ .recipient_row");
assert.ok((await page.$("#zfilt .stream-status")) !== null);
let message_list_id = await common.get_current_msg_list_id(page, true);
await page.waitForSelector(
`.message-list[data-message-list-id='${message_list_id}'] .recipient_row ~ .recipient_row ~ .recipient_row`,
);
assert.ok(
(await page.$(
`.message-list[data-message-list-id='${message_list_id}'] .stream-status`,
)) !== null,
);
await page.goto("http://zulip.zulipdev.com:9981/#narrow/streams/public");
await page.waitForSelector("#zfilt .recipient_row ~ .recipient_row ~ .recipient_row");
assert.ok((await page.$("#zfilt .stream-status")) === null);
message_list_id = await common.get_current_msg_list_id(page, true);
await page.waitForSelector(
`.message-list[data-message-list-id='${message_list_id}'] .recipient_row ~ .recipient_row ~ .recipient_row`,
);
assert.ok(
(await page.$(
`.message-list[data-message-list-id='${message_list_id}'] .stream-status`,
)) === null,
);
}
async function message_basic_tests(page: Page): Promise<void> {
await common.log_in(page);
await page.click("#left-sidebar-navigation-list .top_left_all_messages");
await page.waitForSelector("#zhome .message_row", {visible: true});
await page.waitForSelector(".message-list .message_row", {visible: true});
// Assert that there is only one message list.
assert.equal((await page.$$(".message-list")).length, 1);
console.log("Sending messages");
await common.send_multiple_messages(page, [

View File

@ -71,7 +71,10 @@ async function test_reload_hash(page: Page): Promise<void> {
await page.evaluate(() => zulip_test.initiate_reload({immediate: true}));
await page.waitForNavigation();
await page.waitForSelector("#zfilt", {visible: true});
const message_list_id = await common.get_current_msg_list_id(page, true);
await page.waitForSelector(`.message-list[data-message-list-id='${message_list_id}']`, {
visible: true,
});
const page_load_time = await page.evaluate(() => zulip_test.page_load_time);
assert.ok(page_load_time > initial_page_load_time, "Page not reloaded.");

View File

@ -7,12 +7,12 @@ import * as common from "./lib/common";
const message = "test star";
async function stars_count(page: Page): Promise<number> {
return (await page.$$("#zhome .zulip-icon-star-filled:not(.empty-star)")).length;
return (await page.$$(".message-list .zulip-icon-star-filled:not(.empty-star)")).length;
}
async function toggle_test_star_message(page: Page): Promise<void> {
const messagebox = await page.waitForSelector(
`xpath/(//*[@id="zhome"]//*[${common.has_class_x(
`xpath/(//*[${common.has_class_x("message-list")}]//*[${common.has_class_x(
"message_content",
)} and normalize-space()="${message}"])[last()]/ancestor::*[${common.has_class_x(
"messagebox",
@ -29,17 +29,24 @@ async function toggle_test_star_message(page: Page): Promise<void> {
async function test_narrow_to_starred_messages(page: Page): Promise<void> {
await page.click('#left-sidebar-navigation-list a[href^="#narrow/is/starred"]');
await common.check_messages_sent(page, "zfilt", [["Verona > stars", [message]]]);
const message_list_id = await common.get_current_msg_list_id(page, true);
await common.check_messages_sent(page, message_list_id, [["Verona > stars", [message]]]);
// Go back to all messages narrow.
await page.click("#left-sidebar-navigation-list .top_left_all_messages");
await page.waitForSelector("#zhome .message_row", {visible: true});
await page.waitForSelector(".message-list .message_row", {visible: true});
}
async function stars_test(page: Page): Promise<void> {
await common.log_in(page);
await page.click("#left-sidebar-navigation-list .top_left_all_messages");
await page.waitForSelector("#zhome .message_row", {visible: true});
const message_list_id = await common.get_current_msg_list_id(page, true);
await page.waitForSelector(
`.message-list[data-message-list-id='${message_list_id}'] .message_row`,
{visible: true},
);
// Assert that there is only one message list.
assert.equal((await page.$$(".message-list")).length, 1);
await common.send_message(page, "stream", {
stream_name: "Verona",
topic: "stars",
@ -49,7 +56,10 @@ async function stars_test(page: Page): Promise<void> {
assert.strictEqual(await stars_count(page), 0, "Unexpected already starred message(s).");
await toggle_test_star_message(page);
await page.waitForSelector("#zhome .zulip-icon-star-filled", {visible: true});
await page.waitForSelector(
`.message-list[data-message-list-id='${message_list_id}'] .zulip-icon-star-filled`,
{visible: true},
);
assert.strictEqual(
await stars_count(page),
1,

View File

@ -42,8 +42,7 @@ export function set_focused_recipient(msg_type) {
}
function display_messages_normally() {
const $table = rows.get_table(message_lists.current.table_name);
$table.find(".recipient_row").removeClass("message-fade");
message_lists.current.view.$list.find(".recipient_row").removeClass("message-fade");
normal_display = true;
}
@ -77,9 +76,7 @@ function fade_messages() {
// Defer updating all message groups so that the compose box can open sooner
setTimeout(
(expected_msg_list, expected_recipient) => {
const all_groups = rows
.get_table(message_lists.current.table_name)
.find(".recipient_row");
const $all_groups = message_lists.current.view.$list.find(".recipient_row");
if (
message_lists.current !== expected_msg_list ||
@ -93,8 +90,8 @@ function fade_messages() {
// Note: The below algorithm relies on the fact that all_elts is
// sorted as it would be displayed in the message view
for (i = 0; i < all_groups.length; i += 1) {
const $group_elt = $(all_groups[i]);
for (i = 0; i < $all_groups.length; i += 1) {
const $group_elt = $($all_groups[i]);
should_fade_group = compose_fade_helper.should_fade_message(
rows.recipient_from_group($group_elt),
);

View File

@ -17,6 +17,7 @@ import * as stream_data from "./stream_data";
import {user_settings} from "./user_settings";
export class MessageList {
static id_counter = 0;
// A MessageList is the main interface for a message feed that is
// rendered in the DOM. Code outside the message feed rendering
// internals will directly call this module in order to manipulate
@ -30,6 +31,8 @@ export class MessageList {
// is not particularly well-defined; it could be nice to figure
// out a good rule.
constructor(opts) {
MessageList.id_counter += 1;
this.id = MessageList.id_counter;
// The MessageListData keeps track of the actual sequence of
// messages displayed by this MessageList. Most
// configuration/logic questions in this module will be
@ -46,11 +49,6 @@ export class MessageList {
});
}
// The table_name is the outer HTML element for this message
// list in the DOM.
const table_name = opts.table_name;
this.table_name = table_name;
// TODO: This property should likely just be inlined into
// having the MessageListView code that needs to access it
// query .data.filter directly.
@ -59,7 +57,7 @@ export class MessageList {
// The MessageListView object that is responsible for
// maintaining this message feed's HTML representation in the
// DOM.
this.view = new MessageListView(this, table_name, collapse_messages);
this.view = new MessageListView(this, collapse_messages, opts.is_node_test);
// Whether this is a narrowed message list. The only message
// list that is not is the home_msg_list global.
@ -266,7 +264,7 @@ export class MessageList {
// false.
if (!opts.use_closest && closest_id !== id) {
error_data = {
table_name: this.table_name,
filter_terms: this.filter.terms(),
id,
closest_id,
};
@ -275,7 +273,7 @@ export class MessageList {
if (closest_id === -1 && !opts.empty_ok) {
error_data = {
table_name: this.table_name,
filter_terms: this.filter.terms(),
id,
items_length: this.data.num_items(),
};
@ -526,7 +524,6 @@ export class MessageList {
export function initialize() {
/* Create home_msg_list and register it. */
const home_msg_list = new MessageList({
table_name: "zhome",
filter: new Filter([{operator: "in", operand: "home"}]),
excludes_muted_topics: true,
});

View File

@ -5,6 +5,7 @@ import * as resolved_topic from "../shared/src/resolved_topic";
import render_bookend from "../templates/bookend.hbs";
import render_login_to_view_image_button from "../templates/login_to_view_image_button.hbs";
import render_message_group from "../templates/message_group.hbs";
import render_message_list from "../templates/message_list.hbs";
import render_recipient_row from "../templates/recipient_row.hbs";
import render_single_message from "../templates/single_message.hbs";
@ -260,9 +261,12 @@ export class MessageListView {
// Logic to compute context, render templates, insert them into
// the DOM, and generally
constructor(list, table_name, collapse_messages) {
constructor(list, collapse_messages, is_node_test = false) {
// The MessageList that this MessageListView is responsible for rendering.
this.list = list;
this._add_message_list_to_DOM();
// The jQuery element for the rendered list element.
this.$list = $(`.message-list[data-message-list-id="${this.list.id}"]`);
// TODO: Access this via .list.data.
this.collapse_messages = collapse_messages;
@ -285,12 +289,10 @@ export class MessageListView {
this.message_containers = new Map();
this._message_groups = [];
// TODO: Should this be just accessing .list.table_name?
this.table_name = table_name;
if (this.table_name) {
if (!is_node_test) {
// Skip running this in node tests.
this.clear_table();
}
// For performance reasons, this module renders at most
// _RENDER_WINDOW_SIZE messages into the DOM at a time, and
// will transparently adjust which messages are rendered
@ -312,6 +314,10 @@ export class MessageListView {
// trigger a re-render
_RENDER_THRESHOLD = 50;
_add_message_list_to_DOM() {
$("#message-lists-container").append(render_message_list({message_list_id: this.list.id}));
}
_get_msg_timestring(message_container) {
let last_edit_timestamp;
if (message_container.msg.local_edit_timestamp !== undefined) {
@ -782,7 +788,7 @@ export class MessageListView {
message_container.msg.message_reactions = msg_reactions;
const msg_to_render = {
...message_container,
table_name: this.table_name,
message_list_id: this.list.id,
};
return render_single_message(msg_to_render);
}
@ -790,13 +796,12 @@ export class MessageListView {
_render_group(opts) {
const message_groups = opts.message_groups;
const use_match_properties = opts.use_match_properties;
const table_name = opts.table_name;
return $(
render_message_group({
message_groups,
use_match_properties,
table_name,
message_list_id: this.list.id,
}),
);
}
@ -810,8 +815,6 @@ export class MessageListView {
}
const list = this.list; // for convenience
const table_name = this.table_name;
const $table = rows.get_table(table_name);
let orig_scrolltop_offset;
// If we start with the message feed scrolled up (i.e.
@ -857,7 +860,7 @@ export class MessageListView {
return undefined;
}
const new_message_groups = this.build_message_groups(message_containers, this.table_name);
const new_message_groups = this.build_message_groups(message_containers);
const message_actions = this.merge_message_groups(new_message_groups, where);
const new_dom_elements = [];
let $rendered_groups;
@ -876,7 +879,6 @@ export class MessageListView {
$rendered_groups = this._render_group({
message_groups: message_actions.prepend_groups,
use_match_properties: this.list.is_keyword_search(),
table_name: this.table_name,
});
$dom_messages = $rendered_groups.find(".message_row");
@ -886,8 +888,8 @@ export class MessageListView {
// The date row will be included in the message groups or will be
// added in a rerendered in the group below
$table.find(".recipient_row").first().prev(".date_row").remove();
$table.prepend($rendered_groups);
this.$list.find(".recipient_row").first().prev(".date_row").remove();
this.$list.prepend($rendered_groups);
condense.condense_and_collapse($dom_messages);
}
@ -903,7 +905,6 @@ export class MessageListView {
$rendered_groups = this._render_group({
message_groups: [message_group],
use_match_properties: this.list.is_keyword_search(),
table_name: this.table_name,
});
$dom_messages = $rendered_groups.find(".message_row");
@ -917,7 +918,7 @@ export class MessageListView {
// Insert new messages in to the last message group
if (message_actions.append_messages.length > 0) {
$last_message_row = $table.find(".message_row").last().expectOne();
$last_message_row = this.$list.find(".message_row").last().expectOne();
$last_group_row = rows.get_message_recipient_row($last_message_row);
$dom_messages = $(
message_actions.append_messages
@ -940,7 +941,6 @@ export class MessageListView {
$rendered_groups = this._render_group({
message_groups: message_actions.append_groups,
use_match_properties: this.list.is_keyword_search(),
table_name: this.table_name,
});
$dom_messages = $rendered_groups.find(".message_row");
@ -962,7 +962,7 @@ export class MessageListView {
// this next line seems to prevent the Chrome bug from firing.
message_viewport.scrollTop();
$table.append($rendered_groups);
this.$list.append($rendered_groups);
condense.condense_and_collapse($dom_messages);
}
@ -1436,7 +1436,7 @@ export class MessageListView {
// We do not want to call .empty() because that also clears
// jQuery data. This does mean, however, that we need to be
// mindful of memory leaks.
rows.get_table(this.table_name).children().detach();
this.$list.children().detach();
this._rows.clear();
this._message_groups = [];
this.message_containers.clear();
@ -1455,7 +1455,7 @@ export class MessageListView {
}
clear_trailing_bookend() {
const $trailing_bookend = rows.get_table(this.table_name).find(".trailing_bookend");
const $trailing_bookend = this.$list.find(".trailing_bookend");
$trailing_bookend.remove();
}
@ -1484,7 +1484,7 @@ export class MessageListView {
is_web_public,
}),
);
rows.get_table(this.table_name).append($rendered_trailing_bookend);
this.$list.append($rendered_trailing_bookend);
}
selected_row() {
@ -1501,7 +1501,7 @@ export class MessageListView {
this._rows.delete(old_id);
$row.attr("zid", new_id);
$row.attr("id", this.table_name + new_id);
$row.attr("id", `message-row-${this.list.id}-` + new_id);
$row.removeClass("local");
this._rows.set(new_id, $row);
}
@ -1570,8 +1570,7 @@ export class MessageListView {
return 0;
}
const $table = rows.get_table(this.table_name);
const $headers = $table.find(".message_header");
const $headers = this.$list.find(".message_header");
const iterable_headers = $headers.toArray();
let start = 0;
let end = iterable_headers.length - 1;
@ -1670,8 +1669,7 @@ export class MessageListView {
}
update_recipient_bar_background_color() {
const $table = rows.get_table(this.table_name);
const $stream_headers = $table.find(".message_header_stream");
const $stream_headers = this.$list.find(".message_header_stream");
for (const stream_header of $stream_headers) {
const $stream_header = $(stream_header);
stream_color.update_stream_recipient_color($stream_header);
@ -1689,8 +1687,7 @@ export class MessageListView {
}
show_messages_as_unread(message_ids) {
const $table = rows.get_table(this.table_name);
const $rows_to_show_as_unread = $table.find(".message_row").filter((_index, $row) => {
const $rows_to_show_as_unread = this.$list.find(".message_row").filter((_index, $row) => {
const message_id = Number.parseFloat($row.getAttribute("zid"));
return message_ids.includes(message_id);
});

View File

@ -1,4 +1,3 @@
import $ from "jquery";
import assert from "minimalistic-assert";
import * as blueslip from "./blueslip";
@ -15,11 +14,12 @@ type MessageListView = {
_render_win_start: number;
_render_win_end: number;
sticky_recipient_message_id: number | undefined;
$list: JQuery;
};
export type RenderInfo = {need_user_to_scroll: boolean};
export type MessageList = {
table_name: string;
id: number;
view: MessageListView;
selected_id: () => number;
selected_row: () => JQuery;
@ -61,7 +61,7 @@ export function all_rendered_message_lists(): MessageList[] {
export function all_current_message_rows(): JQuery {
assert(current !== undefined);
return $(`#${CSS.escape(current.table_name)}.message-list .message_row`);
return current.view.$list.find(".message_row");
}
export function update_recipient_bar_background_color(): void {

View File

@ -420,16 +420,19 @@ export function activate(raw_terms, opts) {
const msg_list = new message_list.MessageList({
data: msg_data,
table_name: "zfilt",
});
// Show the new set of messages. It is important to set message_lists.current to
// the view right as it's being shown, because we rely on message_lists.current
// being shown for deciding when to condense messages.
$("body").addClass("narrowed_view");
$("#zfilt").addClass("focused-message-list");
$("#zhome").removeClass("focused-message-list");
msg_list.view.$list.addClass("focused-message-list");
message_lists.home.view.$list.removeClass("focused-message-list");
// Remove old message list from DOM.
if (message_lists.current !== message_lists.home) {
message_lists.current?.view.$list.remove();
}
message_lists.set_current(msg_list);
let then_select_offset;
@ -1003,7 +1006,7 @@ function handle_post_narrow_deactivate_processes(msg_list) {
message_feed_top_notices.update_top_of_narrow_notices(msg_list);
// We may need to scroll to the selected message after swapping
// the currently displayed center panel to zhome.
// the currently displayed center panel to All messages.
message_viewport.maybe_scroll_to_selected();
}
@ -1076,11 +1079,14 @@ export function deactivate() {
narrow_state.set_has_shown_message_list_view();
$("body").removeClass("narrowed_view");
$("#zfilt").removeClass("focused-message-list");
$("#zhome").addClass("focused-message-list");
message_lists.home.view.$list.addClass("focused-message-list");
// Remove old message list from DOM.
if (message_lists.current !== message_lists.home) {
message_lists.current?.view.$list.remove();
}
message_lists.set_current(message_lists.home);
message_lists.current.resume_reading();
condense.condense_and_collapse($("#zhome div.message_row"));
condense.condense_and_collapse(message_lists.home.view.$list.find(".message_row"));
reset_ui_state();
compose_recipient.handle_middle_pane_transition();

View File

@ -1,6 +1,5 @@
import * as message_lists from "./message_lists";
import * as message_viewport from "./message_viewport";
import * as rows from "./rows";
import * as unread_ops from "./unread_ops";
function go_to_row(msg_id) {
@ -23,9 +22,9 @@ export function down(with_centering) {
if (with_centering) {
// At the last message, scroll to the bottom so we have
// lots of nice whitespace for new messages coming in.
const $current_msg_table = rows.get_table(message_lists.current.table_name);
const $current_msg_list = message_lists.current.view.$list;
message_viewport.scrollTop(
($current_msg_table.outerHeight(true) ?? 0) - message_viewport.height() * 0.1,
($current_msg_list.outerHeight(true) ?? 0) - message_viewport.height() * 0.1,
);
unread_ops.process_scrolled_to_bottom();
}

View File

@ -112,16 +112,6 @@ export function local_echo_id($message_row: JQuery): string | undefined {
return zid;
}
const valid_table_names = new Set(["zhome", "zfilt"]);
export function get_table(table_name: string): JQuery {
if (!valid_table_names.has(table_name)) {
return $();
}
return $(`#${CSS.escape(table_name)}`);
}
export function get_message_id(elem: string): number | undefined {
// Gets the message_id for elem, where elem is a DOM
// element inside a message. This is typically used

View File

@ -4,7 +4,6 @@ import * as inbox_util from "./inbox_util";
import * as message_lists from "./message_lists";
import * as message_view_header from "./message_view_header";
import * as overlays from "./overlays";
import * as rows from "./rows";
import * as stream_color from "./stream_color";
import * as stream_data from "./stream_data";
@ -30,8 +29,11 @@ function update_stream_privacy_color(id, color) {
function update_message_recipient_color(stream_name, color) {
const recipient_color = stream_color.get_recipient_bar_color(color);
for (const msg_list of message_lists.all_rendered_message_lists()) {
const $table = rows.get_table(msg_list.table_name);
update_table_message_recipient_stream_color($table, stream_name, recipient_color);
update_table_message_recipient_stream_color(
msg_list.view.$list,
stream_name,
recipient_color,
);
}
// Update color for drafts if open.

View File

@ -404,8 +404,8 @@ export function update_timestamps(): void {
const className = entry.className;
const $elements = $(`.${CSS.escape(className)}`);
// The element might not exist any more (because it
// was in the zfilt table, or because we added
// messages above it and re-collapsed).
// was in the narrowed message list which was removed,
// or because we added messages above it and re-collapsed).
if ($elements.length > 0) {
const time = entry.time;
const rendered_time = render_now(time, today);

View File

@ -118,7 +118,7 @@ export function get_item(key, config, file_id) {
return "message-edit-file-input";
case "drag_drop_container":
return $(
`#${message_lists.current.table_name}${CSS.escape(
`#message-row-${message_lists.current.id}-${CSS.escape(
config.row,
)} .message_edit_form`,
);

View File

@ -49,7 +49,10 @@ export function activate(in_opts) {
});
};
if ($row.attr("id").startsWith("zhome") && narrow_state.active()) {
if (
$row.attr("id").startsWith(`message-row-${message_lists.home.id}-`) &&
narrow_state.active()
) {
// Don't place widget in a home message row if we are narrowed
// to active state
return;

View File

@ -10,7 +10,7 @@
{{> recipient_row use_match_properties=../use_match_properties}}
{{#each message_containers}}
{{#with this}}
{{> single_message use_match_properties=../../use_match_properties table_name=../../table_name}}
{{> single_message use_match_properties=../../use_match_properties message_list_id=../../message_list_id}}
{{/with}}
{{/each}}
</div>

View File

@ -0,0 +1 @@
<div class="message-list" data-message-list-id="{{ message_list_id }}" role="list" aria-live="polite" aria-label="{{t 'Messages' }}"></div>

View File

@ -1,4 +1,4 @@
<div zid="{{msg/id}}" id="{{table_name}}{{msg/id}}"
<div zid="{{msg/id}}" id="message-row-{{message_list_id}}-{{msg/id}}"
class="message_row{{#unless msg/is_stream}} private-message{{/unless}}{{#include_sender}} include-sender{{/include_sender}}{{#if mention_classname}} {{mention_classname}}{{/if}}{{#msg.unread}} unread{{/msg.unread}} {{#if msg.locally_echoed}}locally-echoed{{/if}} selectable_row"
role="listitem">
{{#if want_date_divider}}

View File

@ -65,11 +65,9 @@ const alice = {
people.add_active_user(alice);
function make_home_msg_list() {
const table_name = "whatever";
const filter = new Filter([]);
const list = new message_list.MessageList({
table_name,
filter,
});
return list;
@ -297,7 +295,6 @@ function simulate_narrow() {
const filter = new Filter([{operator: "dm", operand: alice.email}]);
const msg_list = new message_list.MessageList({
table_name: "zfilt",
filter,
});
message_lists.current = msg_list;

View File

@ -20,18 +20,6 @@ mock_esm("../src/timerender", {
},
});
mock_esm("../src/rows", {
get_table() {
return {
children() {
return {
detach: noop,
};
},
};
},
});
mock_esm("../src/people", {
sender_is_bot: () => false,
sender_is_guest: () => false,
@ -47,9 +35,10 @@ const muted_users = zrequire("muted_users");
let next_timestamp = 1500000000;
function test(label, f) {
run_test(label, ({override}) => {
run_test(label, ({override, mock_template}) => {
muted_users.set_muted_users([]);
f({override});
mock_template("message_list.hbs", false, noop);
f({override, mock_template});
});
}
@ -80,7 +69,13 @@ test("msg_moved_var", () => {
}
function build_list(message_groups) {
const list = new MessageListView(undefined, undefined, true);
const list = new MessageListView(
{
id: 1,
},
true,
true,
);
list._message_groups = message_groups;
return list;
}
@ -229,7 +224,13 @@ test("msg_edited_vars", () => {
}
function build_list(message_groups) {
const list = new MessageListView(undefined, undefined, true);
const list = new MessageListView(
{
id: 1,
},
true,
true,
);
list._message_groups = message_groups;
return list;
}
@ -293,7 +294,13 @@ test("muted_message_vars", () => {
}
function build_list(message_groups) {
const list = new MessageListView(undefined, undefined, true);
const list = new MessageListView(
{
id: 1,
},
true,
true,
);
list._message_groups = message_groups;
return list;
}
@ -403,7 +410,8 @@ test("muted_message_vars", () => {
})();
});
test("merge_message_groups", () => {
test("merge_message_groups", ({mock_template}) => {
mock_template("message_list.hbs", false, noop);
// MessageListView has lots of DOM code, so we are going to test the message
// group merging logic on its own.
@ -433,15 +441,14 @@ test("merge_message_groups", () => {
}
function build_list(message_groups) {
const table_name = "zfilt";
const filter = new Filter([{operator: "stream", operand: "foo"}]);
const list = new message_list.MessageList({
table_name,
filter,
is_node_test: true,
});
const view = new MessageListView(list, table_name, true);
const view = new MessageListView(list, true, true);
view._message_groups = message_groups;
view.list.unsubscribed_bookend_content = noop;
view.list.subscribed_bookend_content = noop;
@ -687,19 +694,19 @@ test("merge_message_groups", () => {
})();
});
test("render_windows", () => {
test("render_windows", ({mock_template}) => {
mock_template("message_list.hbs", false, noop);
// We only render up to 400 messages at a time in our message list,
// and we only change the window (which is a range, really, with
// start/end) when the pointer moves outside of the window or close
// to the edges.
const view = (function make_view() {
const table_name = "zfilt";
const filter = new Filter([]);
const list = new message_list.MessageList({
table_name,
filter,
is_node_test: true,
});
const view = list.view;
@ -731,6 +738,7 @@ test("render_windows", () => {
id: i,
}));
list.selected_idx = () => 0;
list.view.clear_table = noop;
list.clear();
list.add_messages(messages, {});

View File

@ -21,8 +21,23 @@ const compose_recipient = mock_esm("../src/compose_recipient");
const message_fetch = mock_esm("../src/message_fetch");
const message_list = mock_esm("../src/message_list");
const message_lists = mock_esm("../src/message_lists", {
home: {},
current: {},
home: {
view: {
$list: {
removeClass: noop,
addClass: noop,
},
},
},
current: {
view: {
$list: {
remove: noop,
removeClass: noop,
addClass: noop,
},
},
},
set_current(msg_list) {
message_lists.current = msg_list;
},
@ -117,6 +132,12 @@ function stub_message_list() {
set_message_offset(offset) {
this.offset = offset;
},
$list: {
remove: noop,
removeClass: noop,
addClass: noop,
},
};
get(msg_id) {

View File

@ -30,7 +30,7 @@ const compose_ui = zrequire("compose_ui");
const upload = zrequire("upload");
const message_lists = zrequire("message_lists");
message_lists.current = {
table_name: "zfilt",
id: "1",
};
function test(label, f) {
run_test(label, (helpers) => {
@ -129,7 +129,7 @@ test("get_item", () => {
assert.equal(upload.get_item("source", {mode: "edit", row: 123}), "message-edit-file-input");
assert.equal(
upload.get_item("drag_drop_container", {mode: "edit", row: 1}),
$(`#zfilt${CSS.escape(1)} .message_edit_form`),
$(`#message-row-1-${CSS.escape(1)} .message_edit_form`),
);
assert.equal(
upload.get_item("markdown_preview_hide_button", {mode: "edit", row: 65}),
@ -769,7 +769,7 @@ test("main_file_drop_edit_mode", ({override, override_rewire}) => {
prevent_default_counter += 1;
},
};
const $drag_drop_container = $(`#zfilt${CSS.escape(40)} .message_edit_form`);
const $drag_drop_container = $(`#message-row-1-${CSS.escape(40)} .message_edit_form`);
// Dragover event test
const dragover_handler = $(".app, #navbar-fixed-container").get_on_handler("dragover");

View File

@ -54,7 +54,7 @@ const fake_poll_widget = {
},
};
const message_lists = mock_esm("../src/message_lists", {current: {}});
const message_lists = mock_esm("../src/message_lists", {current: {}, home: {id: 1}});
const narrow_state = mock_esm("../src/narrow_state");
mock_esm("../src/poll_widget", fake_poll_widget);
@ -80,8 +80,8 @@ test("activate", ({override}) => {
// Both widgetize.activate and widgetize.handle_event are tested
// here to use the "caching" of widgets
const $row = $.create("<stub message row>");
$row.attr("id", "zhome2909");
const $message_content = $.create("#zhome2909");
$row.attr("id", "message-row-1-2909");
const $message_content = $.create("#message-row-1-2909");
$row.set_find_results(".message_content", $message_content);
let narrow_active;