Repository: zulip/zulip

Issue:
Create collapsible "Private messages" section in left sidebar
At present, private messages are collapsed in the left sidebar, unless the user is in a private message narrow. This has a few down sides:

1. Getting to a PM conversation generally requires multiple clicks.
2. It's not immediately clear who send you a new private message, which is important for determining whether one needs to read it right away.
3. It can be hard for new users to figure out how to view and send private messages.

In order to address this, we should try making a private messages section in the left sidebar that is open by default. Specifically:

1. Make a Private messages section just above STREAMS in the left sidebar that is open by default.
2. In the new PMs section, use the same algorithm we use for stream topics to decide how many conversations to show.
3. Make the PMs section collapsible, similar to the collapsible sections in #20072. The open/collapsed state should be sticky as the user navigates around Zulip, closes and reopens the window, logs out and in, etc.

Note that this will likely require experimentation for us to get it right. To avoid misdirected effort, please post screenshots in the #design stream on chat.zulip.org for feedback. Also, if (3) can't be implemented quickly, we can test the experience in chat.zulip.org without waiting for it to be completed.

[Prior discussion on CZO](https://chat.zulip.org/#narrow/stream/101-design/topic/private.20messages.20UI/near/1159032).

See also #11108.


Discussion:
Hello @zulip/server-sidebars members, this issue was labeled with the "area: left-sidebar" label, so you may want to check it out!

<!-- areaLabelAddition -->

@zulipbot claim
Hello @jai2201, it looks like you've currently claimed 1 issue in this repository. We encourage new contributors to focus their efforts on at most 1 issue at a time, so please complete your work on your other claimed issues before trying to claim this issue again.

We look forward to your valuable contributions!

@zulipbot  claim

Hello @jai2201, you have been unassigned from this issue because you have not updated this issue or any referenced pull requests for over 14 days.

You can reclaim this issue or claim any other issue by commenting `@zulipbot claim` on that issue.

Thanks for your contributions, and hope to see you again soon!

@zulipbot I'm still working on this issue.
@zulipbot  claim

Hello @jai2201, you have been unassigned from this issue because you have not updated this issue or any referenced pull requests for over 14 days.

You can reclaim this issue or claim any other issue by commenting `@zulipbot claim` on that issue.

Thanks for your contributions, and hope to see you again soon!

@zulipbot  claim
Hello @jai2201, you have been unassigned from this issue because you have not updated this issue or any referenced pull requests for over 14 days.

You can reclaim this issue or claim any other issue by commenting `@zulipbot claim` on that issue.

Thanks for your contributions, and hope to see you again soon!

@zulipbot  claim
@jai2201 You have been unassigned from this issue because you have not made any updates for over 14 days. Please feel free to reclaim the issue if you decide to pick up again. Thanks!


Resolving patch:
diff --git a/tools/lib/capitalization.py b/tools/lib/capitalization.py
--- a/tools/lib/capitalization.py
+++ b/tools/lib/capitalization.py
@@ -78,6 +78,9 @@
     r"more topics",
     # Used alone in a parenthetical where capitalized looks worse.
     r"^deprecated$",
+    # We want the similar text in the Private Messages section to have the same capitalization.
+    r"more conversations",
+    r"back to streams",
     # Capital 'i' looks weird in reminders popover
     r"in 1 hour",
     r"in 20 minutes",


Test patch:
diff --git a/frontend_tests/node_tests/dispatch.js b/frontend_tests/node_tests/dispatch.js
--- a/frontend_tests/node_tests/dispatch.js
+++ b/frontend_tests/node_tests/dispatch.js
@@ -40,6 +40,7 @@ const message_lists = mock_esm("../../static/js/message_lists");
 const muted_topics_ui = mock_esm("../../static/js/muted_topics_ui");
 const muted_users_ui = mock_esm("../../static/js/muted_users_ui");
 const notifications = mock_esm("../../static/js/notifications");
+const pm_list = mock_esm("../../static/js/pm_list");
 const reactions = mock_esm("../../static/js/reactions");
 const realm_icon = mock_esm("../../static/js/realm_icon");
 const realm_logo = mock_esm("../../static/js/realm_logo");
@@ -1020,6 +1021,7 @@ run_test("user_status", ({override}) => {
     {
         const stub = make_stub();
         override(activity, "redraw_user", stub.f);
+        override(pm_list, "update_private_messages", noop);
         dispatch(event);
         assert.equal(stub.num_calls, 1);
         const args = stub.get_args("user_id");
diff --git a/frontend_tests/node_tests/pm_list.js b/frontend_tests/node_tests/pm_list.js
--- a/frontend_tests/node_tests/pm_list.js
+++ b/frontend_tests/node_tests/pm_list.js
@@ -17,14 +17,16 @@ run_test("update_dom_with_unread_counts", () => {
     assert.equal(narrow_state.active(), true);
 
     const $total_count = $.create("total-count-stub");
-    const $private_li = $(".top_left_private_messages .private_messages_header");
+    const $private_li = $(
+        ".private_messages_container #private_messages_section #private_messages_section_header",
+    );
     $private_li.set_find_results(".unread_count", $total_count);
 
     counts = {
         private_message_count: 10,
     };
 
-    pm_list.update_dom_with_unread_counts(counts);
+    pm_list.set_count(counts.private_message_count);
     assert.equal($total_count.text(), "10");
     assert.ok($total_count.visible());
 
@@ -32,7 +34,7 @@ run_test("update_dom_with_unread_counts", () => {
         private_message_count: 0,
     };
 
-    pm_list.update_dom_with_unread_counts(counts);
+    pm_list.set_count(counts.private_message_count);
     assert.equal($total_count.text(), "");
     assert.ok(!$total_count.visible());
 });
diff --git a/frontend_tests/node_tests/recent_topics.js b/frontend_tests/node_tests/recent_topics.js
--- a/frontend_tests/node_tests/recent_topics.js
+++ b/frontend_tests/node_tests/recent_topics.js
@@ -107,6 +107,10 @@ const narrow = mock_esm("../../static/js/narrow", {
     handle_middle_pane_transition: noop,
     has_shown_message_list_view: true,
 });
+mock_esm("../../static/js/pm_list", {
+    update_private_messages: noop,
+    handle_narrow_deactivated: noop,
+});
 mock_esm("../../static/js/popovers", {
     any_active: () => false,
 });
diff --git a/frontend_tests/node_tests/stream_list.js b/frontend_tests/node_tests/stream_list.js
--- a/frontend_tests/node_tests/stream_list.js
+++ b/frontend_tests/node_tests/stream_list.js
@@ -409,6 +409,7 @@ test_ui("narrowing", ({mock_template}) => {
     topic_list.rebuild = noop;
     topic_list.active_stream_id = noop;
     topic_list.get_stream_li = noop;
+    $("#streams_header").outerHeight = () => 0;
 
     assert.ok(!$("<devel-sidebar-row-stub>").hasClass("active-filter"));
 
@@ -700,6 +701,7 @@ test_ui("refresh_pin", ({override, override_rewire, mock_template}) => {
 
     override_rewire(stream_list, "update_count_in_dom", noop);
     $("#stream_filters").append = noop;
+    $("#streams_header").outerHeight = () => 0;
 
     let scrolled;
     override(scroll_util, "scroll_element_into_container", ($li) => {
diff --git a/frontend_tests/node_tests/vdom.js b/frontend_tests/node_tests/vdom.js
--- a/frontend_tests/node_tests/vdom.js
+++ b/frontend_tests/node_tests/vdom.js
@@ -26,7 +26,7 @@ run_test("basics", () => {
 
 run_test("attribute escaping", () => {
     // So far most of the time our attributes are
-    // hard-coded classes like "expanded_private_messages",
+    // hard-coded classes like "pm-list",
     // but we need to be defensive about future code
     // that might use data from possibly malicious users.
     const opts = {
diff --git a/frontend_tests/puppeteer_tests/compose.ts b/frontend_tests/puppeteer_tests/compose.ts
--- a/frontend_tests/puppeteer_tests/compose.ts
+++ b/frontend_tests/puppeteer_tests/compose.ts
@@ -113,7 +113,7 @@ async function test_narrow_to_private_messages_with_cordelia(page: Page): Promis
         you_and_cordelia_selector,
     );
     const cordelia_user_id = await common.get_user_id_from_name(page, "Cordelia, Lear's daughter");
-    const pm_list_selector = `li[data-user-ids-string="${cordelia_user_id}"].expanded_private_message.active-sub-filter`;
+    const pm_list_selector = `li[data-user-ids-string="${cordelia_user_id}"].pm-list-item.active-sub-filter`;
     await page.waitForSelector(pm_list_selector, {visible: true});
     await close_compose_box(page);
 
diff --git a/frontend_tests/puppeteer_tests/message-basics.ts b/frontend_tests/puppeteer_tests/message-basics.ts
--- a/frontend_tests/puppeteer_tests/message-basics.ts
+++ b/frontend_tests/puppeteer_tests/message-basics.ts
@@ -280,7 +280,9 @@ async function test_narrow_by_clicking_the_left_sidebar(page: Page): Promise<voi
     await page.click(".top_left_all_messages a");
     await expect_home(page);
 
-    await page.click(".top_left_private_messages a");
+    const all_private_messages_icon = "#show_all_private_messages";
+    await page.waitForSelector(all_private_messages_icon, {visible: true});
+    await page.click(all_private_messages_icon);
     await expect_all_pm(page);
 
     await un_narrow(page);
diff --git a/frontend_tests/puppeteer_tests/navigation.ts b/frontend_tests/puppeteer_tests/navigation.ts
--- a/frontend_tests/puppeteer_tests/navigation.ts
+++ b/frontend_tests/puppeteer_tests/navigation.ts
@@ -64,6 +64,16 @@ async function navigate_to_subscriptions(page: Page): Promise<void> {
     await page.waitForSelector("#subscription_overlay", {hidden: true});
 }
 
+async function navigate_to_private_messages(page: Page): Promise<void> {
+    console.log("Navigate to private messages");
+
+    const all_private_messages_icon = "#show_all_private_messages";
+    await page.waitForSelector(all_private_messages_icon, {visible: true});
+    await page.click(all_private_messages_icon);
+
+    await page.waitForSelector("#message_view_header .fa-envelope", {visible: true});
+}
+
 async function test_reload_hash(page: Page): Promise<void> {
     const initial_page_load_time = await page.evaluate(
         (): number => zulip_test.page_params.page_load_time,
@@ -99,7 +109,7 @@ async function navigation_tests(page: Page): Promise<void> {
     await navigate_to_subscriptions(page);
     await navigate_using_left_sidebar(page, "all_messages", "message_feed_container");
     await navigate_to_settings(page);
-    await navigate_using_left_sidebar(page, "narrow/is/private", "message_feed_container");
+    await navigate_to_private_messages(page);
     await navigate_to_subscriptions(page);
     await navigate_using_left_sidebar(page, verona_narrow, "message_feed_container");