diff --git a/tools/test-js-with-node b/tools/test-js-with-node
index 81062a2b5f..e1d2e93401 100755
--- a/tools/test-js-with-node
+++ b/tools/test-js-with-node
@@ -98,6 +98,8 @@ EXEMPT_FILES = make_set(
"web/src/hbs.d.ts",
"web/src/hotkey.js",
"web/src/hotspots.js",
+ "web/src/inbox_ui.js",
+ "web/src/inbox_util.js",
"web/src/info_overlay.js",
"web/src/invite.js",
"web/src/lightbox.js",
diff --git a/web/shared/icons/arrow-down.svg b/web/shared/icons/arrow-down.svg
new file mode 100644
index 0000000000..1c8a90359d
Binary files /dev/null and b/web/shared/icons/arrow-down.svg differ
diff --git a/web/shared/icons/inbox.svg b/web/shared/icons/inbox.svg
new file mode 100644
index 0000000000..e82fb0e203
Binary files /dev/null and b/web/shared/icons/inbox.svg differ
diff --git a/web/shared/icons/search-inbox.svg b/web/shared/icons/search-inbox.svg
new file mode 100644
index 0000000000..27f62a33ad
Binary files /dev/null and b/web/shared/icons/search-inbox.svg differ
diff --git a/web/src/bundles/app.ts b/web/src/bundles/app.ts
index 7808bba9ff..d3cf6d5a09 100644
--- a/web/src/bundles/app.ts
+++ b/web/src/bundles/app.ts
@@ -58,6 +58,7 @@ import "../../styles/dark_theme.css";
import "../../styles/user_status.css";
import "../../styles/widgets.css";
import "../../styles/print.css";
+import "../../styles/inbox.css";
// This should be last.
import "../ui_init";
diff --git a/web/src/hashchange.js b/web/src/hashchange.js
index e3238ca21a..6a84f59c4d 100644
--- a/web/src/hashchange.js
+++ b/web/src/hashchange.js
@@ -7,6 +7,8 @@ import * as browser_history from "./browser_history";
import * as drafts from "./drafts";
import * as hash_util from "./hash_util";
import {$t_html} from "./i18n";
+import * as inbox_ui from "./inbox_ui";
+import * as inbox_util from "./inbox_util";
import * as info_overlay from "./info_overlay";
import * as left_sidebar_navigation_area from "./left_sidebar_navigation_area";
import * as message_lists from "./message_lists";
@@ -91,6 +93,14 @@ function maybe_hide_recent_view() {
return false;
}
+function maybe_hide_inbox() {
+ if (inbox_util.is_visible()) {
+ inbox_ui.hide();
+ return true;
+ }
+ return false;
+}
+
export function changehash(newhash) {
if (browser_history.state.changing_hash) {
return;
@@ -109,8 +119,9 @@ export function save_narrow(operators) {
function show_all_message_view() {
const coming_from_recent_view = maybe_hide_recent_view();
+ const coming_from_inbox = maybe_hide_inbox();
const is_actively_scrolling = message_scroll.is_actively_scrolling();
- narrow.deactivate(!coming_from_recent_view, is_actively_scrolling);
+ narrow.deactivate(!(coming_from_recent_view || coming_from_inbox), is_actively_scrolling);
left_sidebar_navigation_area.handle_narrow_deactivated();
// We need to maybe scroll to the selected message
// once we have the proper viewport set up
@@ -167,6 +178,7 @@ function do_hashchange_normal(from_reload) {
switch (hash[0]) {
case "#narrow": {
maybe_hide_recent_view();
+ maybe_hide_inbox();
let operators;
try {
// TODO: Show possible valid URLs to the user.
@@ -225,8 +237,13 @@ function do_hashchange_normal(from_reload) {
window.location.replace("#recent");
break;
case "#recent":
+ maybe_hide_inbox();
recent_view_ui.show();
break;
+ case "#inbox":
+ maybe_hide_recent_view();
+ inbox_ui.show();
+ break;
case "#all_messages":
show_all_message_view();
break;
diff --git a/web/src/hotkey.js b/web/src/hotkey.js
index 462c542816..75a325a109 100644
--- a/web/src/hotkey.js
+++ b/web/src/hotkey.js
@@ -21,6 +21,8 @@ import * as giphy from "./giphy";
import * as hash_util from "./hash_util";
import * as hashchange from "./hashchange";
import * as hotspots from "./hotspots";
+import * as inbox_ui from "./inbox_ui";
+import * as inbox_util from "./inbox_util";
import * as lightbox from "./lightbox";
import * as list_util from "./list_util";
import * as message_edit from "./message_edit";
@@ -147,6 +149,7 @@ const keypress_mappings = {
82: {name: "respond_to_author", message_view_only: true}, // 'R'
83: {name: "toggle_stream_subscription", message_view_only: true}, // 'S'
85: {name: "mark_unread", message_view_only: true}, // 'U'
+ 84: {name: "open_inbox", message_view_only: true}, // 'T'
86: {name: "view_selected_stream", message_view_only: false}, // 'V'
97: {name: "all_messages", message_view_only: true}, // 'a'
99: {name: "compose", message_view_only: true}, // 'c'
@@ -251,6 +254,10 @@ export function process_escape_key(e) {
return true;
}
+ if (inbox_util.is_in_focus() && inbox_ui.change_focused_element($(e.target), "escape")) {
+ return true;
+ }
+
if (feedback_widget.is_open()) {
feedback_widget.dismiss();
return true;
@@ -637,6 +644,19 @@ export function process_hotkey(e, hotkey) {
}
}
+ switch (event_name) {
+ case "up_arrow":
+ case "down_arrow":
+ case "left_arrow":
+ case "right_arrow":
+ case "tab":
+ case "shift_tab":
+ case "escape":
+ if (inbox_util.is_in_focus()) {
+ return inbox_ui.change_focused_element(event_name);
+ }
+ }
+
// We handle the most complex keys in their own functions.
switch (event_name) {
case "escape":
@@ -881,6 +901,9 @@ export function process_hotkey(e, hotkey) {
case "open_recent_view":
browser_history.go_to_location("#recent");
return true;
+ case "open_inbox":
+ browser_history.go_to_location("#inbox");
+ return true;
case "all_messages":
browser_history.go_to_location("#all_messages");
return true;
diff --git a/web/src/inbox_ui.js b/web/src/inbox_ui.js
new file mode 100644
index 0000000000..73eb763666
--- /dev/null
+++ b/web/src/inbox_ui.js
@@ -0,0 +1,878 @@
+import $ from "jquery";
+import _ from "lodash";
+
+import render_inbox_row from "../templates/inbox_view/inbox_row.hbs";
+import render_inbox_stream_container from "../templates/inbox_view/inbox_stream_container.hbs";
+import render_inbox_view from "../templates/inbox_view/inbox_view.hbs";
+
+import * as buddy_data from "./buddy_data";
+import * as compose_closed_ui from "./compose_closed_ui";
+import * as hash_util from "./hash_util";
+import * as hashchange from "./hashchange";
+import {is_visible, set_visible} from "./inbox_util";
+import * as keydown_util from "./keydown_util";
+import * as left_sidebar_navigation_area from "./left_sidebar_navigation_area";
+import {localstorage} from "./localstorage";
+import * as message_store from "./message_store";
+import * as message_view_header from "./message_view_header";
+import * as narrow from "./narrow";
+import * as narrow_state from "./narrow_state";
+import * as navigate from "./navigate";
+import * as people from "./people";
+import * as pm_list from "./pm_list";
+import * as search from "./search";
+import * as stream_color from "./stream_color";
+import * as stream_data from "./stream_data";
+import * as stream_list from "./stream_list";
+import * as sub_store from "./sub_store";
+import * as unread from "./unread";
+import * as unread_ops from "./unread_ops";
+import * as unread_ui from "./unread_ui";
+import * as user_topics from "./user_topics";
+import * as util from "./util";
+
+let dms_dict = {};
+let topics_dict = {};
+let streams_dict = {};
+let update_triggered_by_user = false;
+
+const COLUMNS = {
+ COLLAPSE_BUTTON: 0,
+ RECIPIENT: 1,
+ UNREAD_COUNT: 2,
+};
+let col_focus = COLUMNS.RECIPIENT;
+let row_focus = 0;
+
+const ls_key = "inbox_filters";
+const ls = localstorage();
+let filters = new Set();
+
+let search_keyword = "";
+const INBOX_SEARCH_ID = "inbox-search";
+const MUTED_FILTER_ID = "include_muted";
+export let current_focus_id = INBOX_SEARCH_ID;
+
+const STREAM_HEADER_PREFIX = "inbox-stream-header-";
+const CONVERSATION_ID_PREFIX = "inbox-row-conversation-";
+
+function get_row_from_conversation_key(key) {
+ return $(`#${CONVERSATION_ID_PREFIX}` + CSS.escape(`${key}`));
+}
+
+function save_filter() {
+ ls.set(ls_key, [...filters]);
+}
+
+function should_include_muted() {
+ return filters.has(MUTED_FILTER_ID);
+}
+
+export function show() {
+ // hashchange library is expected to hide other views.
+ if (narrow.has_shown_message_list_view) {
+ narrow.save_pre_narrow_offset_for_reload();
+ }
+
+ if (is_visible()) {
+ return;
+ }
+
+ left_sidebar_navigation_area.highlight_inbox_view();
+ stream_list.handle_narrow_deactivated();
+
+ $("#message_feed_container").hide();
+ $("#inbox-view").show();
+ set_visible(true);
+
+ unread_ui.hide_unread_banner();
+ narrow_state.reset_current_filter();
+ message_view_header.render_title_area();
+ narrow.handle_middle_pane_transition();
+
+ narrow_state.reset_current_filter();
+ narrow.update_narrow_title(narrow_state.filter());
+ message_view_header.render_title_area();
+ pm_list.handle_narrow_deactivated();
+ search.clear_search_form();
+ complete_rerender();
+ compose_closed_ui.set_standard_text_for_reply_button();
+}
+
+export function hide() {
+ const $focused_element = $(document.activeElement);
+
+ if ($("#inbox-view").has($focused_element)) {
+ $focused_element.trigger("blur");
+ }
+
+ $("#message_feed_container").show();
+ $("#inbox-view").hide();
+ set_visible(false);
+
+ // This solves a bug with message_view_header
+ // being broken sometimes when we narrow
+ // to a filter and back to recent topics
+ // before it completely re-rerenders.
+ message_view_header.render_title_area();
+
+ // Fire our custom event
+ $("#message_feed_container").trigger("message_feed_shown");
+
+ // This makes sure user lands on the selected message
+ // and not always at the top of the narrow.
+ navigate.plan_scroll_to_selected();
+}
+
+function get_topic_key(stream_id, topic) {
+ return stream_id + ":" + topic;
+}
+
+function get_stream_key(stream_id) {
+ return "stream_" + stream_id;
+}
+
+function get_stream_container(stream_key) {
+ return $(`#${CSS.escape(stream_key)}`);
+}
+
+function get_topics_container(stream_id) {
+ const $topics_container = get_stream_header_row(stream_id)
+ .next(".inbox-topic-container")
+ .expectOne();
+ return $topics_container;
+}
+
+function get_stream_header_row(stream_id) {
+ const $stream_header_row = $(`#${CSS.escape(STREAM_HEADER_PREFIX + stream_id)}`);
+ return $stream_header_row;
+}
+
+function load_filter() {
+ filters = new Set(ls.get(ls_key));
+ update_filters();
+}
+
+function update_filters() {
+ const $mute_checkbox = $("#inbox-filters #inbox_filter_mute_toggle");
+ const $mute_filter = $("#inbox-filters .btn-inbox-filter");
+ if (should_include_muted()) {
+ $mute_checkbox.removeClass("fa-square-o");
+ $mute_checkbox.addClass("fa-check-square-o");
+ $mute_filter.addClass("btn-inbox-selected");
+ } else {
+ $mute_checkbox.removeClass("fa-check-square-o");
+ $mute_checkbox.addClass("fa-square-o");
+ $mute_filter.removeClass("btn-inbox-selected");
+ }
+}
+
+export function toggle_muted_filter() {
+ const $mute_filter = $("#inbox-filters .btn-inbox-filter");
+ if ($mute_filter.hasClass("btn-inbox-selected")) {
+ filters.delete(MUTED_FILTER_ID);
+ } else {
+ filters.add(MUTED_FILTER_ID);
+ }
+
+ update_filters();
+ save_filter();
+ update();
+}
+
+function format_dm(user_ids_string, unread_count) {
+ const recipient_ids = people.user_ids_string_to_ids_array(user_ids_string);
+ if (!recipient_ids.length) {
+ // Self DM
+ recipient_ids.push(people.my_current_user_id());
+ }
+
+ const reply_to = people.user_ids_string_to_emails_string(user_ids_string);
+ const recipients_info = [];
+
+ for (const user_id of recipient_ids) {
+ const recipient_user_obj = people.get_by_user_id(user_id);
+ if (!recipient_user_obj.is_bot) {
+ const user_circle_class = buddy_data.get_user_circle_class(user_id);
+ recipient_user_obj.user_circle_class = user_circle_class;
+ }
+ recipients_info.push(recipient_user_obj);
+ }
+ const context = {
+ conversation_key: user_ids_string,
+ is_direct: true,
+ recipients_info,
+ dm_url: hash_util.pm_with_url(reply_to),
+ user_ids_string,
+ unread_count,
+ is_hidden: filter_should_hide_row({dm_key: user_ids_string}),
+ };
+
+ return context;
+}
+
+function rerender_dm_inbox_row_if_needed(new_dm_data, old_dm_data) {
+ if (old_dm_data === undefined) {
+ // This row is not rendered yet.
+ $("#inbox-direct-messages-container").append(render_inbox_row(new_dm_data));
+ return;
+ }
+
+ for (const property in new_dm_data) {
+ if (new_dm_data[property] !== old_dm_data[property]) {
+ const $rendered_row = get_row_from_conversation_key(new_dm_data.conversation_key);
+ $rendered_row.replaceWith(render_inbox_row(new_dm_data));
+ return;
+ }
+ }
+}
+
+function format_stream(stream_id, unread_count_info) {
+ const stream_info = sub_store.get(stream_id);
+ let unread_count = unread_count_info.unmuted_count;
+ if (should_include_muted()) {
+ unread_count += unread_count_info.muted_count;
+ }
+
+ return {
+ is_stream: true,
+ invite_only: stream_info.invite_only,
+ is_web_public: stream_info.is_web_public,
+ stream_name: stream_info.name,
+ stream_color: stream_color.get_stream_privacy_icon_color(stream_info.color),
+ stream_header_color: stream_color.get_recipient_bar_color(stream_info.color),
+ unread_count,
+ stream_url: hash_util.by_stream_url(stream_id),
+ stream_id,
+ // Will be displayed if any topic is visible.
+ is_hidden: true,
+ };
+}
+
+function rerender_stream_inbox_header_if_needed(new_stream_data, old_stream_data) {
+ for (const property in new_stream_data) {
+ if (new_stream_data[property] !== old_stream_data[property]) {
+ const $rendered_row = get_stream_header_row(new_stream_data.stream_id);
+ $rendered_row.replaceWith(render_inbox_row(new_stream_data));
+ return;
+ }
+ }
+}
+
+function format_topic(stream_id, topic, topic_unread_count) {
+ const context = {
+ is_topic: true,
+ stream_id,
+ topic_name: topic,
+ unread_count: topic_unread_count,
+ conversation_key: get_topic_key(stream_id, topic),
+ topic_url: hash_util.by_stream_topic_url(stream_id, topic),
+ is_hidden: filter_should_hide_row({stream_id, topic}),
+ };
+
+ return context;
+}
+
+function insert_stream(stream_id, topic_dict, stream_unread) {
+ const stream_data = format_stream(stream_id, stream_unread);
+ const stream_key = get_stream_key(stream_id);
+ topics_dict[stream_key] = {};
+ for (const [topic, topic_unread_count] of topic_dict) {
+ const topic_key = get_topic_key(stream_id, topic);
+ if (topic_unread_count) {
+ const topic_data = format_topic(stream_id, topic, topic_unread_count);
+ topics_dict[stream_key][topic_key] = topic_data;
+ if (!topic_data.is_hidden) {
+ stream_data.is_hidden = false;
+ }
+ }
+ }
+
+ streams_dict[stream_key] = stream_data;
+
+ const sorted_stream_keys = get_sorted_stream_keys();
+ const stream_index = sorted_stream_keys.indexOf(stream_key);
+ const rendered_stream = render_inbox_stream_container({
+ topics_dict: {
+ [stream_key]: topics_dict[stream_key],
+ },
+ streams_dict,
+ });
+
+ if (stream_index === 0) {
+ $("#inbox-streams-container").prepend(rendered_stream);
+ } else {
+ const previous_stream_key = sorted_stream_keys[stream_index - 1];
+ $(rendered_stream).insertAfter(get_stream_container(previous_stream_key));
+ }
+ return get_stream_container(stream_key);
+}
+
+function rerender_topic_inbox_row_if_needed(new_topic_data, old_topic_data) {
+ // This row is not rendered yet.
+ if (old_topic_data === undefined) {
+ const stream_key = get_stream_key(new_topic_data.stream_id);
+ const $topic_container = get_stream_container(stream_key).find(".inbox-topic-container");
+ $topic_container.prepend(render_inbox_row(new_topic_data));
+ return;
+ }
+
+ for (const property in new_topic_data) {
+ if (new_topic_data[property] !== old_topic_data[property]) {
+ const $rendered_row = get_row_from_conversation_key(new_topic_data.conversation_key);
+ $rendered_row.replaceWith(render_inbox_row(new_topic_data));
+ return;
+ }
+ }
+}
+
+function get_sorted_stream_keys() {
+ function compare_function(a, b) {
+ const stream_a = streams_dict[a];
+ const stream_b = streams_dict[b];
+ const stream_name_a = stream_a ? stream_a.stream_name : "";
+ const stream_name_b = stream_b ? stream_b.stream_name : "";
+ return util.strcmp(stream_name_a, stream_name_b);
+ }
+
+ return Object.keys(topics_dict).sort(compare_function);
+}
+
+function get_sorted_stream_topic_dict() {
+ const sorted_stream_keys = get_sorted_stream_keys();
+ const sorted_topic_dict = {};
+ for (const sorted_stream_key of sorted_stream_keys) {
+ sorted_topic_dict[sorted_stream_key] = topics_dict[sorted_stream_key];
+ }
+
+ return sorted_topic_dict;
+}
+
+function reset_data() {
+ dms_dict = {};
+ topics_dict = {};
+ streams_dict = {};
+
+ const unread_dms = unread.get_unread_pm();
+ const unread_dms_count = unread_dms.total_count;
+ const unread_dms_dict = unread_dms.pm_dict;
+
+ const unread_stream_message = unread.get_unread_topics();
+ const unread_stream_msg_count = unread_stream_message.stream_unread_messages;
+ const unread_streams_dict = unread_stream_message.stream_count;
+
+ let has_dms_post_filter = false;
+ if (unread_dms_count) {
+ for (const [key, value] of unread_dms_dict) {
+ if (value) {
+ const dm_data = format_dm(key, value);
+ dms_dict[key] = dm_data;
+ if (!dm_data.is_hidden) {
+ has_dms_post_filter = true;
+ }
+ }
+ }
+ }
+
+ const has_unread = unread_dms_count + unread_stream_msg_count > 0;
+
+ if (unread_stream_msg_count) {
+ for (const [stream_id, topic_dict] of unread_streams_dict) {
+ const stream_unread = unread.num_unread_for_stream(stream_id);
+ const stream_unread_count = stream_unread.unmuted_count + stream_unread.muted_count;
+ const stream_key = get_stream_key(stream_id);
+ if (stream_unread_count > 0) {
+ topics_dict[stream_key] = {};
+ const stream_data = format_stream(stream_id, stream_unread);
+ for (const [topic, topic_unread_count] of topic_dict) {
+ if (topic_unread_count) {
+ const topic_key = get_topic_key(stream_id, topic);
+ const topic_data = format_topic(stream_id, topic, topic_unread_count);
+ topics_dict[stream_key][topic_key] = topic_data;
+ if (!topic_data.is_hidden) {
+ stream_data.is_hidden = false;
+ }
+ }
+ }
+ streams_dict[stream_key] = stream_data;
+ } else {
+ delete topics_dict[stream_key];
+ }
+ }
+ }
+
+ topics_dict = get_sorted_stream_topic_dict();
+
+ return {
+ unread_dms_count,
+ has_dms_post_filter,
+ has_unread,
+ };
+}
+
+export function complete_rerender() {
+ if (!is_visible()) {
+ return;
+ }
+ load_filter();
+ const additional_context = reset_data();
+ $("#inbox-pane").html(
+ render_inbox_view({
+ search_val: $("#inbox_search").val() || "",
+ include_muted: should_include_muted(),
+ INBOX_SEARCH_ID,
+ MUTED_FILTER_ID,
+ dms_dict,
+ topics_dict,
+ streams_dict,
+ ...additional_context,
+ }),
+ );
+ update_filters();
+
+ setTimeout(() => {
+ // We don't want to focus on simplebar ever.
+ $("#inbox-list .simplebar-content-wrapper").attr("tabindex", "-1");
+ revive_current_focus();
+ }, 0);
+}
+
+export function search_and_update() {
+ search_keyword = $("#inbox-search").val() || "";
+ current_focus_id = INBOX_SEARCH_ID;
+ update_triggered_by_user = true;
+ update();
+}
+
+function row_in_search_results(keyword, text) {
+ if (keyword === "") {
+ return true;
+ }
+ const search_words = keyword.toLowerCase().split(/\s+/);
+ return search_words.every((word) => text.includes(word));
+}
+
+function filter_should_hide_row({stream_id, topic, dm_key}) {
+ let text;
+ if (dm_key !== undefined) {
+ const recipients_string = people.get_recipients(dm_key);
+ text = recipients_string.toLowerCase();
+ } else {
+ const sub = sub_store.get(stream_id);
+ if (sub === undefined || !sub.subscribed) {
+ return true;
+ }
+ if (
+ !should_include_muted() &&
+ (stream_data.is_muted(stream_id) || user_topics.is_topic_muted(stream_id, topic))
+ ) {
+ return true;
+ }
+ text = (sub.name + " " + topic).toLowerCase();
+ }
+
+ if (!row_in_search_results(search_keyword, text)) {
+ return true;
+ }
+
+ return false;
+}
+
+export function collapse_or_expand(container_id) {
+ let $toggle_icon;
+ let $container;
+ if (container_id === "inbox-dm-header") {
+ $container = $(`#inbox-direct-messages-container`);
+ $container.children().toggleClass("collapsed_container");
+ $toggle_icon = $("#inbox-dm-header .toggle-inbox-header-icon");
+ } else {
+ const stream_id = container_id.slice(STREAM_HEADER_PREFIX.length);
+ $container = get_topics_container(stream_id);
+ $container.children().toggleClass("collapsed_container");
+ $toggle_icon = $(
+ `#${CSS.escape(STREAM_HEADER_PREFIX + stream_id)} .toggle-inbox-header-icon`,
+ );
+ }
+ $toggle_icon.toggleClass("icon_collapsed_state");
+}
+
+function focus_current_id() {
+ $(`#${current_focus_id}`).trigger("focus");
+}
+
+function set_default_focus() {
+ current_focus_id = INBOX_SEARCH_ID;
+ focus_current_id();
+}
+
+function is_list_focused() {
+ return ![INBOX_SEARCH_ID, MUTED_FILTER_ID].includes(current_focus_id);
+}
+
+function get_all_rows() {
+ return $(".inbox-header, .inbox-row").not(".hidden_by_filters, .collapsed_container");
+}
+
+function get_row_index($elt) {
+ const $all_rows = get_all_rows();
+ const $row = $elt.closest(".inbox-row, .inbox-header");
+ return $all_rows.index($row);
+}
+
+function focus_clicked_element($elt) {
+ row_focus = get_row_index($elt);
+ update_triggered_by_user = true;
+}
+
+function revive_current_focus() {
+ if (is_list_focused()) {
+ set_list_focus();
+ } else {
+ focus_current_id();
+ }
+}
+
+function is_row_a_header($row) {
+ return $row.hasClass("inbox-header");
+}
+
+function set_list_focus(input_key) {
+ const $all_rows = get_all_rows();
+ const max_row_focus = $all_rows.length - 1;
+ if (max_row_focus < 0) {
+ set_default_focus();
+ return;
+ }
+
+ if (row_focus > max_row_focus) {
+ row_focus = max_row_focus;
+ } else if (row_focus < 0) {
+ row_focus = 0;
+ }
+
+ const $row_to_focus = $($all_rows.get(row_focus));
+ current_focus_id = $row_to_focus.attr("id");
+ const not_a_header_row = !is_row_a_header($row_to_focus);
+
+ if (col_focus > COLUMNS.UNREAD_COUNT) {
+ col_focus = COLUMNS.COLLAPSE_BUTTON;
+ } else if (col_focus < COLUMNS.COLLAPSE_BUTTON) {
+ col_focus = COLUMNS.UNREAD_COUNT;
+ }
+
+ // Since header rows always have a collapse button, other rows have one less element to focus.
+ if (col_focus === COLUMNS.COLLAPSE_BUTTON) {
+ if (not_a_header_row && ["left_arrow", "shift_tab"].includes(input_key)) {
+ // Focus on unread count.
+ col_focus = COLUMNS.UNREAD_COUNT;
+ } else {
+ $row_to_focus.trigger("focus");
+ return;
+ }
+ } else if (not_a_header_row && col_focus === COLUMNS.RECIPIENT) {
+ if (["right_arrow", "tab"].includes(input_key)) {
+ // Focus on unread count.
+ col_focus = COLUMNS.UNREAD_COUNT;
+ } else if (["left_arrow", "shift_tab"].includes(input_key)) {
+ col_focus = COLUMNS.COLLAPSE_BUTTON;
+ $row_to_focus.trigger("focus");
+ return;
+ } else {
+ $row_to_focus.trigger("focus");
+ return;
+ }
+ }
+
+ const $cols_to_focus = $row_to_focus.find("[tabindex=0]");
+ $($cols_to_focus.get(col_focus - 1)).trigger("focus");
+}
+
+function focus_muted_filter() {
+ current_focus_id = MUTED_FILTER_ID;
+ focus_current_id();
+}
+
+function is_search_focused() {
+ return current_focus_id === INBOX_SEARCH_ID;
+}
+
+function is_muted_filter_focused() {
+ return current_focus_id === MUTED_FILTER_ID;
+}
+
+export function change_focused_element(input_key) {
+ if (is_search_focused()) {
+ const textInput = $(`#${INBOX_SEARCH_ID}`).get(0);
+ const start = textInput.selectionStart;
+ const end = textInput.selectionEnd;
+ const text_length = textInput.value.length;
+ let is_selected = false;
+ if (end - start > 0) {
+ is_selected = true;
+ }
+
+ switch (input_key) {
+ case "down_arrow":
+ set_list_focus();
+ return true;
+ case "right_arrow":
+ case "tab":
+ if (end !== text_length || is_selected) {
+ return false;
+ }
+ focus_muted_filter();
+ return true;
+ case "escape":
+ set_list_focus();
+ return true;
+ case "shift_tab":
+ // Let user focus outside inbox view.
+ current_focus_id = "";
+ return false;
+ }
+ } else if (is_muted_filter_focused()) {
+ switch (input_key) {
+ case "down_arrow":
+ case "tab":
+ set_list_focus();
+ return true;
+ case "left_arrow":
+ case "shift_tab":
+ set_default_focus();
+ return true;
+ }
+ } else {
+ switch (input_key) {
+ case "down_arrow":
+ row_focus += 1;
+ set_list_focus();
+ return true;
+ case "up_arrow":
+ if (row_focus === 0) {
+ set_default_focus();
+ return true;
+ }
+ row_focus -= 1;
+ set_list_focus();
+ return true;
+ case "right_arrow":
+ case "tab":
+ col_focus += 1;
+ set_list_focus(input_key);
+ return true;
+ case "left_arrow":
+ case "shift_tab":
+ col_focus -= 1;
+ set_list_focus(input_key);
+ return true;
+ case "escape":
+ hashchange.set_hash_to_default_view();
+ return false;
+ }
+ }
+
+ return false;
+}
+
+export function update() {
+ if (!is_visible()) {
+ return;
+ }
+
+ const unread_dms = unread.get_unread_pm();
+ const unread_dms_count = unread_dms.total_count;
+ const unread_dms_dict = unread_dms.pm_dict;
+
+ const unread_stream_message = unread.get_unread_topics();
+ const unread_streams_dict = unread_stream_message.stream_count;
+
+ let has_dms_post_filter = false;
+
+ for (const [key, value] of unread_dms_dict) {
+ if (value !== 0) {
+ const old_dm_data = dms_dict[key];
+ const new_dm_data = format_dm(key, value);
+ rerender_dm_inbox_row_if_needed(new_dm_data, old_dm_data);
+ dms_dict[key] = new_dm_data;
+ if (!new_dm_data.is_hidden) {
+ has_dms_post_filter = true;
+ }
+ } else {
+ // If it is rendered.
+ if (dms_dict[key] !== undefined) {
+ delete dms_dict[key];
+ get_row_from_conversation_key(key).remove();
+ }
+ }
+ }
+
+ const $inbox_dm_header = $("#inbox-dm-header");
+ if (!has_dms_post_filter) {
+ $inbox_dm_header.addClass("hidden_by_filters");
+ } else {
+ $inbox_dm_header.removeClass("hidden_by_filters");
+ $inbox_dm_header.find(".unread_count").text(unread_dms_count);
+ }
+
+ for (const [stream_id, topic_dict] of unread_streams_dict) {
+ const stream_unread = unread.num_unread_for_stream(stream_id);
+ const stream_unread_count = stream_unread.unmuted_count + stream_unread.muted_count;
+ const stream_key = get_stream_key(stream_id);
+ if (stream_unread_count > 0) {
+ // Stream isn't rendered.
+ if (topics_dict[stream_key] === undefined) {
+ insert_stream(stream_id, topic_dict, stream_unread);
+ continue;
+ }
+
+ const new_stream_data = format_stream(stream_id, stream_unread);
+ for (const [topic, topic_unread_count] of topic_dict) {
+ const topic_key = get_topic_key(stream_id, topic);
+ if (topic_unread_count) {
+ const old_topic_data = topics_dict[stream_key][topic_key];
+ const new_topic_data = format_topic(stream_id, topic, topic_unread_count);
+ topics_dict[stream_key][topic_key] = new_topic_data;
+ rerender_topic_inbox_row_if_needed(new_topic_data, old_topic_data);
+ if (!new_topic_data.is_hidden) {
+ new_stream_data.is_hidden = false;
+ }
+ } else {
+ get_row_from_conversation_key(topic_key).remove();
+ }
+ }
+ const old_stream_data = streams_dict[stream_key];
+ streams_dict[stream_key] = new_stream_data;
+ rerender_stream_inbox_header_if_needed(new_stream_data, old_stream_data);
+ } else {
+ delete topics_dict[stream_key];
+ delete streams_dict[stream_key];
+ get_stream_container(stream_key).remove();
+ }
+ }
+
+ if (update_triggered_by_user) {
+ setTimeout(revive_current_focus, 0);
+ update_triggered_by_user = false;
+ }
+}
+
+function get_focus_class_for_header() {
+ let focus_class = ".collapsible-button";
+
+ switch (col_focus) {
+ case COLUMNS.RECIPIENT: {
+ focus_class = ".inbox-header-name a";
+ break;
+ }
+ case COLUMNS.UNREAD_COUNT: {
+ focus_class = ".unread_count";
+ break;
+ }
+ }
+
+ return focus_class;
+}
+
+function get_focus_class_for_row() {
+ let focus_class = ".inbox-left-part";
+ if (col_focus === COLUMNS.UNREAD_COUNT) {
+ focus_class = ".unread_count";
+ }
+ return focus_class;
+}
+
+export function initialize() {
+ $("body").on(
+ "keyup",
+ "#inbox-search",
+ _.debounce(() => {
+ search_and_update();
+ }, 300),
+ );
+
+ $("body").on("keydown", ".inbox-header", (e) => {
+ if (keydown_util.is_enter_event(e)) {
+ e.preventDefault();
+ e.stopPropagation();
+
+ const $elt = $(e.currentTarget);
+ $elt.find(get_focus_class_for_header()).trigger("click");
+ }
+ });
+
+ $("body").on("click", "#inbox-list .inbox-header .collapsible-button", (e) => {
+ const $elt = $(e.currentTarget);
+ const container_id = $elt.parents(".inbox-header").attr("id");
+ col_focus = COLUMNS.COLLAPSE_BUTTON;
+ focus_clicked_element($elt);
+ collapse_or_expand(container_id);
+ e.stopPropagation();
+ });
+
+ $("body").on("keydown", ".inbox-row", (e) => {
+ if (keydown_util.is_enter_event(e)) {
+ e.preventDefault();
+ e.stopPropagation();
+
+ const $elt = $(e.currentTarget);
+ $elt.find(get_focus_class_for_row()).trigger("click");
+ }
+ });
+
+ $("body").on("click", "#inbox-list .inbox-row, #inbox-list .inbox-header", (e) => {
+ const $elt = $(e.currentTarget);
+ col_focus = COLUMNS.RECIPIENT;
+ focus_clicked_element($elt);
+ window.location.href = $elt.find("a").attr("href");
+ });
+
+ $("body").on("click", "#include_muted", () => {
+ current_focus_id = MUTED_FILTER_ID;
+ update_triggered_by_user = true;
+ toggle_muted_filter();
+ });
+
+ $("body").on("click", "#inbox-list .on_hover_dm_read", (e) => {
+ e.stopPropagation();
+ e.preventDefault();
+ const $elt = $(e.currentTarget);
+ col_focus = COLUMNS.UNREAD_COUNT;
+ focus_clicked_element($elt);
+ const user_ids_string = $elt.attr("data-user-ids-string");
+ if (user_ids_string) {
+ // direct message row
+ unread_ops.mark_pm_as_read(user_ids_string);
+ }
+ });
+
+ $("body").on("click", "#inbox-list .on_hover_all_dms_read", (e) => {
+ e.stopPropagation();
+ e.preventDefault();
+ const unread_dms_msg_ids = unread.get_msg_ids_for_private();
+ const unread_dms_messages = unread_dms_msg_ids.map((msg_id) => message_store.get(msg_id));
+ unread_ops.notify_server_messages_read(unread_dms_messages);
+ set_default_focus();
+ update_triggered_by_user = true;
+ });
+
+ $("body").on("click", "#inbox-list .on_hover_topic_read", (e) => {
+ e.stopPropagation();
+ e.preventDefault();
+ const $elt = $(e.currentTarget);
+ col_focus = COLUMNS.UNREAD_COUNT;
+ focus_clicked_element($elt);
+ const user_ids_string = $elt.attr("data-user-ids-string");
+ if (user_ids_string) {
+ // direct message row
+ unread_ops.mark_pm_as_read(user_ids_string);
+ return;
+ }
+ const stream_id = Number.parseInt($elt.attr("data-stream-id"), 10);
+ const topic = $elt.attr("data-topic-name");
+ if (topic) {
+ unread_ops.mark_topic_as_read(stream_id, topic);
+ } else {
+ unread_ops.mark_stream_as_read(stream_id);
+ }
+ });
+}
diff --git a/web/src/inbox_util.js b/web/src/inbox_util.js
new file mode 100644
index 0000000000..db75df5167
--- /dev/null
+++ b/web/src/inbox_util.js
@@ -0,0 +1,51 @@
+import $ from "jquery";
+
+import * as compose_state from "./compose_state";
+import * as overlays from "./overlays";
+import * as popovers from "./popovers";
+import * as stream_color from "./stream_color";
+import * as stream_data from "./stream_data";
+
+let is_inbox_visible = false;
+
+export function set_visible(value) {
+ is_inbox_visible = value;
+}
+
+export function is_visible() {
+ return is_inbox_visible;
+}
+
+export function get_dm_key(msg) {
+ return "dm:" + msg.other_user_id;
+}
+
+export function is_in_focus() {
+ // Check if user is focused on
+ // inbox
+ return (
+ is_visible() &&
+ !compose_state.composing() &&
+ !popovers.any_active() &&
+ !overlays.is_overlay_or_modal_open() &&
+ !$(".home-page-input").is(":focus")
+ );
+}
+
+export function update_stream_colors() {
+ if (!is_visible()) {
+ return;
+ }
+
+ const $stream_headers = $("#inbox-streams-container .inbox-header");
+ $stream_headers.each((_index, stream_header) => {
+ const $stream_header = $(stream_header);
+ const stream_id = Number.parseInt($stream_header.attr("data-stream-id"), 10);
+ if (!stream_id) {
+ return;
+ }
+ const color = stream_data.get_color(stream_id);
+ const background_color = stream_color.get_recipient_bar_color(color);
+ $stream_header.css("background", background_color);
+ });
+}
diff --git a/web/src/left_sidebar_navigation_area.js b/web/src/left_sidebar_navigation_area.js
index 3c8fad2a54..c39352d952 100644
--- a/web/src/left_sidebar_navigation_area.js
+++ b/web/src/left_sidebar_navigation_area.js
@@ -49,6 +49,7 @@ export function deselect_top_left_corner_items() {
remove($(".top_left_starred_messages"));
remove($(".top_left_mentions"));
remove($(".top_left_recent_view"));
+ remove($(".top_left_inbox"));
}
export function handle_narrow_activated(filter) {
@@ -91,6 +92,7 @@ export function highlight_recent_view() {
remove($(".top_left_all_messages"));
remove($(".top_left_starred_messages"));
remove($(".top_left_mentions"));
+ remove($(".top_left_inbox"));
$(".top_left_recent_view").addClass("active-filter");
setTimeout(() => {
resize.resize_stream_filters_container();
@@ -120,3 +122,14 @@ function do_new_messages_animation($li) {
export function initialize() {
update_scheduled_messages_row();
}
+
+export function highlight_inbox_view() {
+ remove($(".top_left_all_messages"));
+ remove($(".top_left_starred_messages"));
+ remove($(".top_left_recent_view"));
+ remove($(".top_left_mentions"));
+ $(".top_left_inbox").addClass("active-filter");
+ setTimeout(() => {
+ resize.resize_stream_filters_container();
+ }, 0);
+}
diff --git a/web/src/message_events.js b/web/src/message_events.js
index 0a0077d519..5ab3de77d5 100644
--- a/web/src/message_events.js
+++ b/web/src/message_events.js
@@ -9,6 +9,7 @@ import * as compose_state from "./compose_state";
import * as compose_validate from "./compose_validate";
import * as drafts from "./drafts";
import * as huddle_data from "./huddle_data";
+import * as inbox_ui from "./inbox_ui";
import * as message_edit from "./message_edit";
import * as message_edit_history from "./message_edit_history";
import * as message_helper from "./message_helper";
@@ -165,6 +166,7 @@ export function insert_new_messages(messages, sent_by_this_client) {
stream_list.update_streams_sidebar();
pm_list.update_private_messages();
recent_view_ui.process_messages(messages);
+ inbox_ui.update();
}
export function update_messages(events) {
@@ -237,6 +239,7 @@ export function update_messages(events) {
anchor_message.topic,
);
recent_view_ui.inplace_rerender(topic_key);
+ inbox_ui.update();
}
}
@@ -511,6 +514,7 @@ export function update_messages(events) {
});
unread.clear_and_populate_unread_mention_topics();
recent_view_ui.process_topic_edit(...args);
+ inbox_ui.update();
}
// Rerender "Message edit history" if it was open to the edited message.
@@ -571,6 +575,7 @@ export function remove_messages(message_ids) {
}
recent_senders.update_topics_of_deleted_message_ids(message_ids);
recent_view_ui.update_topics_of_deleted_message_ids(message_ids);
+ inbox_ui.update();
starred_messages.remove(message_ids);
starred_messages_ui.rerender_ui();
}
diff --git a/web/src/message_fetch.js b/web/src/message_fetch.js
index 92c4e0d73a..1b1eed1027 100644
--- a/web/src/message_fetch.js
+++ b/web/src/message_fetch.js
@@ -4,6 +4,7 @@ import {all_messages_data} from "./all_messages_data";
import * as channel from "./channel";
import {Filter} from "./filter";
import * as huddle_data from "./huddle_data";
+import * as inbox_ui from "./inbox_ui";
import * as message_feed_loading from "./message_feed_loading";
import * as message_feed_top_notices from "./message_feed_top_notices";
import * as message_helper from "./message_helper";
@@ -53,6 +54,7 @@ function process_result(data, opts) {
huddle_data.process_loaded_messages(messages);
recent_view_ui.process_messages(messages);
+ inbox_ui.update();
stream_list.update_streams_sidebar();
stream_list.maybe_scroll_narrow_into_view();
diff --git a/web/src/message_lists.js b/web/src/message_lists.js
index 605e297151..21be92e7e1 100644
--- a/web/src/message_lists.js
+++ b/web/src/message_lists.js
@@ -1,6 +1,7 @@
import $ from "jquery";
import {Filter} from "./filter";
+import * as inbox_util from "./inbox_util";
import * as message_list from "./message_list";
import * as recent_view_util from "./recent_view_util";
import * as ui_util from "./ui_util";
@@ -28,6 +29,7 @@ export function update_recipient_bar_background_color() {
for (const msg_list of all_rendered_message_lists()) {
msg_list.view.update_recipient_bar_background_color();
}
+ inbox_util.update_stream_colors();
}
export function initialize() {
diff --git a/web/src/message_view_header.js b/web/src/message_view_header.js
index 711854b0ad..5d6c18e288 100644
--- a/web/src/message_view_header.js
+++ b/web/src/message_view_header.js
@@ -3,6 +3,7 @@ import $ from "jquery";
import render_message_view_header from "../templates/message_view_header.hbs";
import {$t} from "./i18n";
+import * as inbox_util from "./inbox_util";
import * as narrow_state from "./narrow_state";
import * as peer_data from "./peer_data";
import * as popovers from "./popovers";
@@ -26,6 +27,12 @@ function make_message_view_header(filter) {
icon: "clock-o",
};
}
+ if (inbox_util.is_visible()) {
+ return {
+ title: $t({defaultMessage: "Inbox"}),
+ zulip_icon: "inbox",
+ };
+ }
if (filter === undefined) {
return {
title: $t({defaultMessage: "All messages"}),
diff --git a/web/src/muted_users_ui.js b/web/src/muted_users_ui.js
index 6251ae49ef..7a537936f3 100644
--- a/web/src/muted_users_ui.js
+++ b/web/src/muted_users_ui.js
@@ -4,6 +4,7 @@ import * as activity from "./activity";
import * as channel from "./channel";
import * as confirm_dialog from "./confirm_dialog";
import {$t_html} from "./i18n";
+import * as inbox_ui from "./inbox_ui";
import * as message_lists from "./message_lists";
import * as muted_users from "./muted_users";
import * as overlays from "./overlays";
@@ -58,6 +59,7 @@ export function rerender_for_muted_user() {
// If a user is (un)muted, we want to update their avatars on the Recent Conversations
// participants column.
recent_view_ui.complete_rerender();
+ inbox_ui.update();
}
export function handle_user_updates(muted_user_ids) {
diff --git a/web/src/narrow.js b/web/src/narrow.js
index b98e93c9b2..ee50213338 100644
--- a/web/src/narrow.js
+++ b/web/src/narrow.js
@@ -16,6 +16,8 @@ import {Filter} from "./filter";
import * as hash_util from "./hash_util";
import * as hashchange from "./hashchange";
import {$t} from "./i18n";
+import * as inbox_ui from "./inbox_ui";
+import * as inbox_util from "./inbox_util";
import * as left_sidebar_navigation_area from "./left_sidebar_navigation_area";
import * as message_edit from "./message_edit";
import * as message_feed_loading from "./message_feed_loading";
@@ -195,6 +197,7 @@ export function activate(raw_operators, opts) {
}
const coming_from_recent_view = recent_view_util.is_visible();
+ const coming_from_inbox = inbox_util.is_visible();
// The empty narrow is the home view; so deactivate any narrow if
// no operators were specified. Take us to all messages when this
@@ -382,11 +385,12 @@ export function activate(raw_operators, opts) {
if (coming_from_recent_view) {
recent_view_ui.hide();
+ } else if (coming_from_inbox) {
+ inbox_ui.hide();
} else {
- // If Recent Conversations was not visible, then we are switching
- // from another message list view. Save the scroll position in
- // that message list, so that we can restore it if/when we
- // later navigate back to that view.
+ // We must instead be switching from another message view.
+ // Save the scroll position in that message list, so that
+ // we can restore it if/when we later navigate back to that view.
save_pre_narrow_offset_for_reload();
}
diff --git a/web/src/narrow_state.js b/web/src/narrow_state.js
index 619262ba58..53e8af4d90 100644
--- a/web/src/narrow_state.js
+++ b/web/src/narrow_state.js
@@ -1,5 +1,6 @@
import * as blueslip from "./blueslip";
import {Filter} from "./filter";
+import * as inbox_util from "./inbox_util";
import {page_params} from "./page_params";
import * as people from "./people";
import * as recent_view_util from "./recent_view_util";
@@ -34,7 +35,7 @@ export function operators() {
}
export function is_message_feed_visible() {
- return !recent_view_util.is_visible();
+ return !recent_view_util.is_visible() && !inbox_util.is_visible();
}
export function update_email(user_id, new_email) {
diff --git a/web/src/stream_color.js b/web/src/stream_color.js
index 1376348064..5668bbea4a 100644
--- a/web/src/stream_color.js
+++ b/web/src/stream_color.js
@@ -4,6 +4,7 @@ import mixPlugin from "colord/plugins/mix";
import $ from "jquery";
import {$t} from "./i18n";
+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";
@@ -85,6 +86,11 @@ function update_message_recipient_color(stream_name, color) {
recipient_color,
);
}
+
+ if (inbox_util.is_visible()) {
+ const stream_id = stream_data.get_stream_id(stream_name);
+ $(`#inbox-stream-header-${stream_id}`).css("background", recipient_color);
+ }
}
const stream_color_palette = [
diff --git a/web/src/stream_events.js b/web/src/stream_events.js
index eb290a043a..2b7fb2bfb3 100644
--- a/web/src/stream_events.js
+++ b/web/src/stream_events.js
@@ -4,6 +4,7 @@ import * as blueslip from "./blueslip";
import * as color_data from "./color_data";
import * as compose_fade from "./compose_fade";
import * as compose_recipient from "./compose_recipient";
+import * as inbox_ui from "./inbox_ui";
import * as message_lists from "./message_lists";
import * as message_view_header from "./message_view_header";
import * as narrow_state from "./narrow_state";
@@ -55,6 +56,7 @@ export function update_property(stream_id, property, value, other_values) {
stream_muting.update_is_muted(sub, value);
stream_list.refresh_muted_or_unmuted_stream(sub);
recent_view_ui.complete_rerender();
+ inbox_ui.update();
break;
case "desktop_notifications":
case "audible_notifications":
diff --git a/web/src/ui_init.js b/web/src/ui_init.js
index 5130e5c663..1f277eb32f 100644
--- a/web/src/ui_init.js
+++ b/web/src/ui_init.js
@@ -41,6 +41,7 @@ import * as hashchange from "./hashchange";
import * as hotkey from "./hotkey";
import * as hotspots from "./hotspots";
import * as i18n from "./i18n";
+import * as inbox_ui from "./inbox_ui";
import * as invite from "./invite";
import * as left_sidebar_navigation_area from "./left_sidebar_navigation_area";
import * as lightbox from "./lightbox";
@@ -185,6 +186,7 @@ function initialize_bottom_whitespace() {
function initialize_left_sidebar() {
const rendered_sidebar = render_left_sidebar({
is_guest: page_params.is_guest,
+ development_environment: page_params.development_environment,
});
$("#left-sidebar-container").html(rendered_sidebar);
@@ -682,6 +684,7 @@ export function initialize_everything() {
realm_logo.initialize();
message_lists.initialize();
recent_view_ui.initialize();
+ inbox_ui.initialize();
alert_words.initialize(alert_words_params);
emojisets.initialize();
scroll_bar.initialize();
diff --git a/web/src/unread.js b/web/src/unread.js
index a0122b0b14..b53af0b87a 100644
--- a/web/src/unread.js
+++ b/web/src/unread.js
@@ -260,11 +260,11 @@ class UnreadTopicCounter {
this.bucketer.delete(msg_id);
}
- get_counts() {
+ get_counts(include_per_topic_count = false) {
const res = {};
res.stream_unread_messages = 0;
res.stream_count = new Map(); // hash by stream_id -> count
- for (const [stream_id] of this.bucketer) {
+ for (const [stream_id, per_stream_bucketer] of this.bucketer) {
// We track unread counts for streams that may be currently
// unsubscribed. Since users may re-subscribe, we don't
// completely throw away the data. But we do ignore it here,
@@ -274,8 +274,24 @@ class UnreadTopicCounter {
continue;
}
- res.stream_count.set(stream_id, this.get_stream_count(stream_id));
- res.stream_unread_messages += res.stream_count.get(stream_id).unmuted_count;
+ if (include_per_topic_count) {
+ const topic_unread = new Map();
+ let stream_count = 0;
+ for (const [topic, msgs] of per_stream_bucketer) {
+ const topic_count = msgs.size;
+ topic_unread.set(topic, topic_count);
+ stream_count += topic_count;
+ }
+
+ // TODO: These don't agree with the else clause in how
+ // they handle muted streams/topics, and that's
+ // probably a bug.
+ res.stream_count.set(stream_id, topic_unread);
+ res.stream_unread_messages += stream_count;
+ } else {
+ res.stream_count.set(stream_id, this.get_stream_count(stream_id));
+ res.stream_unread_messages += res.stream_count.get(stream_id).unmuted_count;
+ }
}
return res;
@@ -731,6 +747,17 @@ export function declare_bankruptcy() {
unread_mention_topics.clear();
}
+export function get_unread_pm() {
+ const pm_res = unread_direct_message_counter.get_counts();
+ return pm_res;
+}
+
+export function get_unread_topics() {
+ const include_per_topic_count = true;
+ const topics_res = unread_topic_counter.get_counts(include_per_topic_count);
+ return topics_res;
+}
+
export function get_counts() {
const res = {};
diff --git a/web/src/unread_ops.js b/web/src/unread_ops.js
index 9ea902e50c..2871566da5 100644
--- a/web/src/unread_ops.js
+++ b/web/src/unread_ops.js
@@ -7,6 +7,7 @@ import * as channel from "./channel";
import * as confirm_dialog from "./confirm_dialog";
import * as dialog_widget from "./dialog_widget";
import {$t_html} from "./i18n";
+import * as inbox_ui from "./inbox_ui";
import * as loading from "./loading";
import * as message_flags from "./message_flags";
import * as message_lists from "./message_lists";
@@ -167,6 +168,7 @@ function process_newly_read_message(message, options) {
}
notifications.close_notification(message);
recent_view_ui.update_topic_unread_count(message);
+ inbox_ui.update();
}
export function mark_as_unread_from_here(
@@ -310,6 +312,7 @@ export function process_read_messages_event(message_ids) {
}
unread_ui.update_unread_counts();
+ inbox_ui.update();
}
export function process_unread_messages_event({message_ids, message_details}) {
@@ -386,6 +389,7 @@ export function process_unread_messages_event({message_ids, message_details}) {
}
unread_ui.update_unread_counts();
+ inbox_ui.update();
}
// Takes a list of messages and marks them as read.
diff --git a/web/src/user_topics_ui.js b/web/src/user_topics_ui.js
index e115542a55..73dd2ada21 100644
--- a/web/src/user_topics_ui.js
+++ b/web/src/user_topics_ui.js
@@ -1,5 +1,6 @@
import $ from "jquery";
+import * as inbox_ui from "./inbox_ui";
import * as message_lists from "./message_lists";
import * as overlays from "./overlays";
import * as popover_menus from "./popover_menus";
@@ -25,6 +26,7 @@ export function handle_topic_updates(user_topic_event) {
user_topic_event.stream_id,
user_topic_event.topic_name,
);
+ inbox_ui.update();
if (overlays.settings_open() && settings_user_topics.loaded) {
const stream_id = user_topic_event.stream_id;
diff --git a/web/styles/inbox.css b/web/styles/inbox.css
new file mode 100644
index 0000000000..8ba95b31d4
--- /dev/null
+++ b/web/styles/inbox.css
@@ -0,0 +1,318 @@
+.inbox-container {
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+ background: var(--color-background-inbox);
+ font-size: 15px;
+ padding: 0;
+ max-height: 100vh;
+ border-radius: 4px;
+ border-right: 1px solid var(--color-border-inbox);
+ border-left: 1px solid var(--color-border-inbox);
+
+ #inbox-pane {
+ max-width: 100%;
+ overflow: hidden;
+ display: flex;
+ flex-direction: column;
+ margin: var(--navbar-fixed-height) 25px 0;
+
+ a {
+ color: var(--color-text-message-header);
+ text-decoration: none;
+ }
+
+ .unread_count {
+ opacity: 0.7;
+
+ &:hover {
+ opacity: 1;
+ }
+ }
+
+ .search_group {
+ display: flex;
+ margin: 15px 0 10px;
+ }
+
+ .btn-inbox-filter {
+ border: none;
+ height: 30px;
+ border-radius: 5px;
+ background: transparent;
+ color: var(--color-text-default);
+ padding: 5px 10px;
+ margin-left: 10px;
+
+ &:focus {
+ background-color: var(--color-background-btn-inbox-focus);
+ outline: 0;
+ }
+ }
+
+ .btn-inbox-selected {
+ background-color: var(--color-background-btn-inbox-selected);
+ }
+
+ #inbox-filters {
+ .zulip-icon-search-inbox {
+ position: absolute;
+ top: calc(var(--navbar-fixed-height) + 23px);
+ left: 32px;
+ color: var(--color-icon-search-inbox);
+ }
+ }
+
+ #inbox-search {
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ width: var(--width-inbox-search);
+ height: var(--height-inbox-search);
+ background-color: var(--color-background-inbox-search);
+ border: 1px solid transparent;
+ padding-right: 20px;
+ padding-left: 30px;
+ font-size: 14px;
+ font-style: normal;
+ font-weight: 400;
+ line-height: 17px;
+ border-radius: 4px;
+
+ &:focus {
+ outline: none;
+ background: var(--color-background-inbox-search-focus);
+ border: 1px solid var(--color-border-inbox-search-focus);
+ }
+ }
+
+ #inbox-list {
+ /* 55px = height of filters
+ 42px = height of closed compose box */
+ height: calc(100vh - 55px - 42px - var(--navbar-fixed-height));
+
+ .simplebar-content {
+ overflow-x: hidden;
+ border-radius: 5px;
+ border: 1px solid hsl(0deg 0% 0% / 20%);
+ margin-bottom: var(--max-unexpanded-compose-height);
+ }
+
+ .inbox-header {
+ display: flex;
+ height: 30px;
+
+ .inbox-left-part {
+ grid-template: auto / auto min-content;
+ grid-template-areas: "header_name unread_count";
+ }
+
+ .inbox-header-name {
+ grid-area: header_name;
+ display: flex;
+ align-items: center;
+ overflow: hidden;
+ padding: 1px 6px;
+ outline: 0;
+
+ & a {
+ padding: 0 4px;
+ border-radius: 5px;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ }
+
+ &:focus a {
+ color: hsl(0deg 0% 100%);
+ background-color: hsl(0deg 0% 11%);
+ }
+ }
+
+ &:focus {
+ outline: 0;
+ box-shadow: inset 2px 0 0 0 var(--color-unread-marker);
+
+ .toggle-inbox-header-icon {
+ opacity: 1;
+ color: hsl(0deg 0% 11%);
+ }
+ }
+ }
+
+ .fa-group {
+ margin-right: 7px;
+ }
+
+ .fa-lock {
+ margin-right: 3px;
+ }
+
+ .fa-envelope,
+ .stream-privacy.filter-icon {
+ font-size: 16px;
+ margin: 0;
+ margin-right: 1px;
+ }
+
+ .fa-envelope {
+ position: relative;
+ top: -1px;
+ margin-right: 4px;
+ }
+
+ .collapsible-button {
+ &:hover {
+ cursor: pointer;
+ }
+
+ .zulip-icon-arrow-down {
+ font-size: 16px;
+ padding: 7px 4px;
+ margin-right: 9px;
+ opacity: 0.5;
+ }
+
+ .icon_collapsed_state {
+ transform: rotate(270deg);
+ }
+ }
+
+ .user_circle {
+ /* size of the user activity circle */
+ min-width: 6px;
+ height: 6px;
+ margin-right: 5px;
+ top: 0;
+ }
+
+ .zulip-icon-bot {
+ font-size: 11px;
+ margin-left: -2px;
+ margin-right: 5px;
+ }
+
+ .inbox-row {
+ display: flex;
+ min-height: 30px;
+ background-color: var(--color-background-inbox-row);
+
+ &:hover {
+ background: var(--color-background-inbox-row-hover);
+ }
+
+ &:focus {
+ outline: 0;
+ padding: 0;
+ box-shadow: inset 2px 0 0 0 var(--color-unread-marker);
+ }
+
+ .inbox-left-part {
+ grid-template: auto / min-content auto min-content;
+ grid-template-areas: "match_topic_and_dm_start recipient_info unread_count";
+ }
+
+ .fake-collapse-button,
+ .inbox-topic-container .user_circle {
+ grid-area: match_topic_and_dm_start;
+ }
+
+ .recipient_info,
+ .inbox-topic-name {
+ grid-area: recipient_info;
+ }
+ }
+
+ .unread_count {
+ grid-area: unread_count;
+ margin-right: 5px;
+ margin-left: 10px;
+ align-self: center;
+ }
+
+ .stream-privacy {
+ display: flex;
+ align-items: center;
+ margin-right: 4px;
+ margin-left: 17px;
+
+ .zulip-icon {
+ line-height: 14px;
+ font-size: 16px;
+ height: 16px;
+ width: 16px;
+ }
+ }
+
+ .inbox-topic-name {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ width: 100%;
+ }
+
+ .inbox-left-part-wrapper {
+ display: flex;
+ width: 50%;
+ }
+
+ #inbox-direct-messages-container .inbox-left-part {
+ padding: 3px 0;
+ }
+
+ #inbox-direct-messages-container .inbox-left-part,
+ .inbox-topic-container .inbox-left-part {
+ /* 50px - space occupied by user circle icon */
+ padding-left: 37px;
+ }
+
+ .inbox-left-part {
+ width: 100%;
+ display: grid;
+ align-items: center;
+
+ &:hover {
+ cursor: pointer;
+ }
+
+ .recipients_info {
+ display: flex;
+ flex-wrap: wrap;
+ column-gap: 10px;
+ grid-area: recipient_info;
+
+ .user_block {
+ display: flex;
+ align-items: center;
+ white-space: nowrap;
+ }
+
+ .recipients_name {
+ white-space: nowrap;
+ }
+ }
+ }
+ }
+
+ #inbox_filter_mute_toggle {
+ font-size: 16px;
+ width: 16px;
+ height: 16px;
+ position: relative;
+ top: 1px;
+ }
+ }
+}
+
+#inbox-view {
+ display: none;
+ position: relative;
+
+ #inbox-dm-header {
+ background-color: var(--color-background-private-message-header);
+ }
+
+ .hidden_by_filters,
+ .collapsed_container {
+ display: none !important;
+ }
+}
diff --git a/web/styles/left_sidebar.css b/web/styles/left_sidebar.css
index 9b03ebb23e..eec750e705 100644
--- a/web/styles/left_sidebar.css
+++ b/web/styles/left_sidebar.css
@@ -490,12 +490,19 @@ li.active-sub-filter {
& i {
opacity: 0.7;
}
+
+ .zulip-icon-inbox {
+ font-size: 14px;
+ top: 2px;
+ position: relative;
+ }
}
li.top_left_all_messages,
li.top_left_mentions,
li.top_left_starred_messages,
li.top_left_drafts,
+li.top_left_inbox,
li.top_left_recent_view,
li.top_left_scheduled_messages {
position: relative;
diff --git a/web/styles/zulip.css b/web/styles/zulip.css
index 827cae8b35..9a6eb43e45 100644
--- a/web/styles/zulip.css
+++ b/web/styles/zulip.css
@@ -198,6 +198,25 @@ body {
--color-background-text-hover-direct-mention: hsl(240deg 70% 70% / 30%);
--color-background-text-group-mention: hsl(183deg 60% 45% / 18%);
--color-background-text-hover-group-mention: hsl(183deg 60% 45% / 30%);
+
+ /* Inbox view constants - Values from Figma design */
+ --height-inbox-search: 26px;
+ --width-inbox-search: 346px;
+ --color-background-inbox: hsl(0deg 0% 100% / 50%);
+ --color-background-inbox-search: hsl(0deg 0% 0% / 5%);
+ --color-icon-search-inbox: hsl(0deg 0% 0% / 50%);
+ --color-background-inbox-search-focus: hsl(0deg 0% 100%);
+ --color-border-inbox-search-focus: hsl(0deg 0% 100% / 20%);
+ --color-background-inbox-row: hsl(0deg 0% 100%);
+ --color-background-inbox-row-hover: linear-gradient(
+ 0deg,
+ hsl(0deg 0% 0% / 5%) 0%,
+ hsl(0deg 0% 0% / 5%) 100%
+ ),
+ hsl(0deg 0% 100%);
+ --color-background-btn-inbox-selected: hsl(0deg 0% 0% / 5%);
+ --color-background-btn-inbox-focus: hsl(0deg 0% 80%);
+ --color-border-inbox: hsl(0deg 0% 84.31%);
}
%dark-theme {
@@ -258,6 +277,23 @@ body {
--color-background-text-hover-direct-mention: hsl(240deg 52% 60% / 45%);
--color-background-text-group-mention: hsl(183deg 52% 40% / 20%);
--color-background-text-hover-group-mention: hsl(183deg 52% 40% / 30%);
+
+ /* Inbox view */
+ --color-background-inbox: var(--color-background);
+ --color-background-inbox-search: hsl(0deg 0% 20%);
+ --color-icon-search-inbox: hsl(0deg 0% 100% / 50%);
+ --color-background-inbox-search-focus: hsl(0deg 0% 0%);
+ --color-border-inbox-search-focus: hsl(0deg 0% 0% / 20%);
+ --color-background-inbox-row: hsl(0deg 0% 14%);
+ --color-background-inbox-row-hover: linear-gradient(
+ 0deg,
+ hsl(0deg 0% 100% / 5%) 0%,
+ hsl(0deg 0% 100% / 5%) 100%
+ ),
+ hsl(0deg 0% 14.12%);
+ --color-background-btn-inbox-selected: hsl(0deg 0% 100% / 5%);
+ --color-background-btn-inbox-focus: hsl(0deg 0% 20%);
+ --color-border-inbox: hsl(0deg 0% 0% / 60%);
}
@media screen {
@@ -2024,6 +2060,11 @@ div.focused-message-list {
margin: 0 2px 0 5px;
}
}
+
+ .zulip-icon-inbox {
+ position: relative;
+ top: 2px;
+ }
}
.message-header-stream-settings-button {
diff --git a/web/templates/inbox_view/inbox_list.hbs b/web/templates/inbox_view/inbox_list.hbs
new file mode 100644
index 0000000000..c89741056c
--- /dev/null
+++ b/web/templates/inbox_view/inbox_list.hbs
@@ -0,0 +1,21 @@
+
+
+
+
+
+ {{~!-- squash whitespace --~}}
+ {{t 'Inbox' }}
+
+
+ {{/if}}
diff --git a/web/tests/hotkey.test.js b/web/tests/hotkey.test.js
index 4c72fad54a..f0c33f4a5d 100644
--- a/web/tests/hotkey.test.js
+++ b/web/tests/hotkey.test.js
@@ -273,7 +273,7 @@ run_test("allow normal typing when processing text", ({override, override_rewire
// Unmapped keys should immediately return false, without
// calling any functions outside of hotkey.js.
assert_unmapped("bfoyz");
- assert_unmapped("BEFHILNOQTWXYZ");
+ assert_unmapped("BEFHILNOQWXYZ");
// All letters should return false if we are composing text.
override_rewire(hotkey, "processing_text", () => true);
diff --git a/web/tests/left_sidebar_navigation_area.test.js b/web/tests/left_sidebar_navigation_area.test.js
index d479e8990d..175f4c39e1 100644
--- a/web/tests/left_sidebar_navigation_area.test.js
+++ b/web/tests/left_sidebar_navigation_area.test.js
@@ -41,6 +41,7 @@ run_test("narrowing", () => {
assert.ok(!$(".top_left_mentions").hasClass("active-filter"));
assert.ok(!$(".top_left_starred_messages").hasClass("active-filter"));
assert.ok(!$(".top_left_recent_view").hasClass("active-filter"));
+ assert.ok(!$(".top_left_inbox").hasClass("active-filter"));
set_global("setTimeout", (f) => {
f();
@@ -49,7 +50,16 @@ run_test("narrowing", () => {
assert.ok(!$(".top_left_all_messages").hasClass("active-filter"));
assert.ok(!$(".top_left_mentions").hasClass("active-filter"));
assert.ok(!$(".top_left_starred_messages").hasClass("active-filter"));
+ assert.ok(!$(".top_left_inbox").hasClass("active-filter"));
assert.ok($(".top_left_recent_view").hasClass("active-filter"));
+
+ left_sidebar_navigation_area.handle_narrow_deactivated();
+ left_sidebar_navigation_area.highlight_inbox_view();
+ assert.ok(!$(".top_left_all_messages").hasClass("active-filter"));
+ assert.ok(!$(".top_left_mentions").hasClass("active-filter"));
+ assert.ok(!$(".top_left_starred_messages").hasClass("active-filter"));
+ assert.ok(!$(".top_left_recent_view").hasClass("active-filter"));
+ assert.ok($(".top_left_inbox").hasClass("active-filter"));
});
run_test("update_count_in_dom", () => {
+
+
+
+
+
+
+
+
+ {{t 'Direct message'}}
+
+ {{unread_dms_count}}
+
+ {{#each dms_dict}}
+ {{> inbox_row }}
+ {{/each}}
+
+
+
+ {{> inbox_stream_container }}
+
diff --git a/web/templates/inbox_view/inbox_row.hbs b/web/templates/inbox_view/inbox_row.hbs
new file mode 100644
index 0000000000..ea6b0f1036
--- /dev/null
+++ b/web/templates/inbox_view/inbox_row.hbs
@@ -0,0 +1,37 @@
+{{#if is_stream}}
+ {{> inbox_stream_header_row}}
+{{else}}
+
+
+{{/if}}
diff --git a/web/templates/inbox_view/inbox_stream_container.hbs b/web/templates/inbox_view/inbox_stream_container.hbs
new file mode 100644
index 0000000000..156fe16003
--- /dev/null
+++ b/web/templates/inbox_view/inbox_stream_container.hbs
@@ -0,0 +1,10 @@
+{{#each topics_dict }}
+
+
+
+
+ {{#if is_direct}}
+
+ {{#each recipients_info}}
+
+ {{#if is_bot}}
+
+ {{else}}
+
+ {{/if}}
+ {{full_name}}
+
+ {{/each}}
+
+ {{unread_count}}
+ {{else if is_topic}}
+ {{!-- Invisible user circle element for alignment of topic text with DM user name --}}
+
+
+
+ {{topic_name}}
+
+
+ {{unread_count}}
+
+ {{/if}}
+
+ {{> inbox_row (lookup ../streams_dict @key)}}
+
+{{/each}}
diff --git a/web/templates/inbox_view/inbox_stream_header_row.hbs b/web/templates/inbox_view/inbox_stream_header_row.hbs
new file mode 100644
index 0000000000..e6c4d8ff20
--- /dev/null
+++ b/web/templates/inbox_view/inbox_stream_header_row.hbs
@@ -0,0 +1,14 @@
+
+ {{#each this}}
+ {{>inbox_row this}}
+ {{/each}}
+
+
+
diff --git a/web/templates/inbox_view/inbox_view.hbs b/web/templates/inbox_view/inbox_view.hbs
new file mode 100644
index 0000000000..2f26332a39
--- /dev/null
+++ b/web/templates/inbox_view/inbox_view.hbs
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+ {{> ../stream_privacy }}
+
+ {{stream_name}}
+
+ {{unread_count}}
+
+
diff --git a/web/templates/left_sidebar.hbs b/web/templates/left_sidebar.hbs
index cba38dab67..4f94533a24 100644
--- a/web/templates/left_sidebar.hbs
+++ b/web/templates/left_sidebar.hbs
@@ -13,6 +13,17 @@
+ {{#if development_environment}}
+
+
+
+
+
+
+ {{> inbox_list}}
+
+