inbox: Add new narrow.

This commit is contained in:
Aman Agrawal 2023-08-09 05:23:30 +00:00 committed by Tim Abbott
parent 3d7b9e622d
commit 6ef0753a51
35 changed files with 1550 additions and 11 deletions

View File

@ -237,6 +237,11 @@
</div> </div>
</div> </div>
</div> </div>
<div id="inbox-view">
<div class="inbox-container">
<div id="inbox-pane"></div>
</div>
</div>
<div id="message_feed_container"> <div id="message_feed_container">
<div class="message-feed" id="main_div"> <div class="message-feed" id="main_div">
<div class="top-messages-logo"> <div class="top-messages-logo">

View File

@ -98,6 +98,8 @@ EXEMPT_FILES = make_set(
"web/src/hbs.d.ts", "web/src/hbs.d.ts",
"web/src/hotkey.js", "web/src/hotkey.js",
"web/src/hotspots.js", "web/src/hotspots.js",
"web/src/inbox_ui.js",
"web/src/inbox_util.js",
"web/src/info_overlay.js", "web/src/info_overlay.js",
"web/src/invite.js", "web/src/invite.js",
"web/src/lightbox.js", "web/src/lightbox.js",

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 B

BIN
web/shared/icons/inbox.svg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -58,6 +58,7 @@ import "../../styles/dark_theme.css";
import "../../styles/user_status.css"; import "../../styles/user_status.css";
import "../../styles/widgets.css"; import "../../styles/widgets.css";
import "../../styles/print.css"; import "../../styles/print.css";
import "../../styles/inbox.css";
// This should be last. // This should be last.
import "../ui_init"; import "../ui_init";

View File

@ -7,6 +7,8 @@ import * as browser_history from "./browser_history";
import * as drafts from "./drafts"; import * as drafts from "./drafts";
import * as hash_util from "./hash_util"; import * as hash_util from "./hash_util";
import {$t_html} from "./i18n"; 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 info_overlay from "./info_overlay";
import * as left_sidebar_navigation_area from "./left_sidebar_navigation_area"; import * as left_sidebar_navigation_area from "./left_sidebar_navigation_area";
import * as message_lists from "./message_lists"; import * as message_lists from "./message_lists";
@ -91,6 +93,14 @@ function maybe_hide_recent_view() {
return false; return false;
} }
function maybe_hide_inbox() {
if (inbox_util.is_visible()) {
inbox_ui.hide();
return true;
}
return false;
}
export function changehash(newhash) { export function changehash(newhash) {
if (browser_history.state.changing_hash) { if (browser_history.state.changing_hash) {
return; return;
@ -109,8 +119,9 @@ export function save_narrow(operators) {
function show_all_message_view() { function show_all_message_view() {
const coming_from_recent_view = maybe_hide_recent_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(); 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(); left_sidebar_navigation_area.handle_narrow_deactivated();
// We need to maybe scroll to the selected message // We need to maybe scroll to the selected message
// once we have the proper viewport set up // once we have the proper viewport set up
@ -167,6 +178,7 @@ function do_hashchange_normal(from_reload) {
switch (hash[0]) { switch (hash[0]) {
case "#narrow": { case "#narrow": {
maybe_hide_recent_view(); maybe_hide_recent_view();
maybe_hide_inbox();
let operators; let operators;
try { try {
// TODO: Show possible valid URLs to the user. // TODO: Show possible valid URLs to the user.
@ -225,8 +237,13 @@ function do_hashchange_normal(from_reload) {
window.location.replace("#recent"); window.location.replace("#recent");
break; break;
case "#recent": case "#recent":
maybe_hide_inbox();
recent_view_ui.show(); recent_view_ui.show();
break; break;
case "#inbox":
maybe_hide_recent_view();
inbox_ui.show();
break;
case "#all_messages": case "#all_messages":
show_all_message_view(); show_all_message_view();
break; break;

View File

@ -21,6 +21,8 @@ import * as giphy from "./giphy";
import * as hash_util from "./hash_util"; import * as hash_util from "./hash_util";
import * as hashchange from "./hashchange"; import * as hashchange from "./hashchange";
import * as hotspots from "./hotspots"; 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 lightbox from "./lightbox";
import * as list_util from "./list_util"; import * as list_util from "./list_util";
import * as message_edit from "./message_edit"; import * as message_edit from "./message_edit";
@ -147,6 +149,7 @@ const keypress_mappings = {
82: {name: "respond_to_author", message_view_only: true}, // 'R' 82: {name: "respond_to_author", message_view_only: true}, // 'R'
83: {name: "toggle_stream_subscription", message_view_only: true}, // 'S' 83: {name: "toggle_stream_subscription", message_view_only: true}, // 'S'
85: {name: "mark_unread", message_view_only: true}, // 'U' 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' 86: {name: "view_selected_stream", message_view_only: false}, // 'V'
97: {name: "all_messages", message_view_only: true}, // 'a' 97: {name: "all_messages", message_view_only: true}, // 'a'
99: {name: "compose", message_view_only: true}, // 'c' 99: {name: "compose", message_view_only: true}, // 'c'
@ -251,6 +254,10 @@ export function process_escape_key(e) {
return true; return true;
} }
if (inbox_util.is_in_focus() && inbox_ui.change_focused_element($(e.target), "escape")) {
return true;
}
if (feedback_widget.is_open()) { if (feedback_widget.is_open()) {
feedback_widget.dismiss(); feedback_widget.dismiss();
return true; 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. // We handle the most complex keys in their own functions.
switch (event_name) { switch (event_name) {
case "escape": case "escape":
@ -881,6 +901,9 @@ export function process_hotkey(e, hotkey) {
case "open_recent_view": case "open_recent_view":
browser_history.go_to_location("#recent"); browser_history.go_to_location("#recent");
return true; return true;
case "open_inbox":
browser_history.go_to_location("#inbox");
return true;
case "all_messages": case "all_messages":
browser_history.go_to_location("#all_messages"); browser_history.go_to_location("#all_messages");
return true; return true;

878
web/src/inbox_ui.js Normal file
View File

@ -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);
}
});
}

51
web/src/inbox_util.js Normal file
View File

@ -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);
});
}

View File

@ -49,6 +49,7 @@ export function deselect_top_left_corner_items() {
remove($(".top_left_starred_messages")); remove($(".top_left_starred_messages"));
remove($(".top_left_mentions")); remove($(".top_left_mentions"));
remove($(".top_left_recent_view")); remove($(".top_left_recent_view"));
remove($(".top_left_inbox"));
} }
export function handle_narrow_activated(filter) { export function handle_narrow_activated(filter) {
@ -91,6 +92,7 @@ export function highlight_recent_view() {
remove($(".top_left_all_messages")); remove($(".top_left_all_messages"));
remove($(".top_left_starred_messages")); remove($(".top_left_starred_messages"));
remove($(".top_left_mentions")); remove($(".top_left_mentions"));
remove($(".top_left_inbox"));
$(".top_left_recent_view").addClass("active-filter"); $(".top_left_recent_view").addClass("active-filter");
setTimeout(() => { setTimeout(() => {
resize.resize_stream_filters_container(); resize.resize_stream_filters_container();
@ -120,3 +122,14 @@ function do_new_messages_animation($li) {
export function initialize() { export function initialize() {
update_scheduled_messages_row(); 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);
}

View File

@ -9,6 +9,7 @@ import * as compose_state from "./compose_state";
import * as compose_validate from "./compose_validate"; import * as compose_validate from "./compose_validate";
import * as drafts from "./drafts"; import * as drafts from "./drafts";
import * as huddle_data from "./huddle_data"; 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 from "./message_edit";
import * as message_edit_history from "./message_edit_history"; import * as message_edit_history from "./message_edit_history";
import * as message_helper from "./message_helper"; 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(); stream_list.update_streams_sidebar();
pm_list.update_private_messages(); pm_list.update_private_messages();
recent_view_ui.process_messages(messages); recent_view_ui.process_messages(messages);
inbox_ui.update();
} }
export function update_messages(events) { export function update_messages(events) {
@ -237,6 +239,7 @@ export function update_messages(events) {
anchor_message.topic, anchor_message.topic,
); );
recent_view_ui.inplace_rerender(topic_key); 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(); unread.clear_and_populate_unread_mention_topics();
recent_view_ui.process_topic_edit(...args); recent_view_ui.process_topic_edit(...args);
inbox_ui.update();
} }
// Rerender "Message edit history" if it was open to the edited message. // 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_senders.update_topics_of_deleted_message_ids(message_ids);
recent_view_ui.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.remove(message_ids);
starred_messages_ui.rerender_ui(); starred_messages_ui.rerender_ui();
} }

View File

@ -4,6 +4,7 @@ import {all_messages_data} from "./all_messages_data";
import * as channel from "./channel"; import * as channel from "./channel";
import {Filter} from "./filter"; import {Filter} from "./filter";
import * as huddle_data from "./huddle_data"; 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_loading from "./message_feed_loading";
import * as message_feed_top_notices from "./message_feed_top_notices"; import * as message_feed_top_notices from "./message_feed_top_notices";
import * as message_helper from "./message_helper"; import * as message_helper from "./message_helper";
@ -53,6 +54,7 @@ function process_result(data, opts) {
huddle_data.process_loaded_messages(messages); huddle_data.process_loaded_messages(messages);
recent_view_ui.process_messages(messages); recent_view_ui.process_messages(messages);
inbox_ui.update();
stream_list.update_streams_sidebar(); stream_list.update_streams_sidebar();
stream_list.maybe_scroll_narrow_into_view(); stream_list.maybe_scroll_narrow_into_view();

View File

@ -1,6 +1,7 @@
import $ from "jquery"; import $ from "jquery";
import {Filter} from "./filter"; import {Filter} from "./filter";
import * as inbox_util from "./inbox_util";
import * as message_list from "./message_list"; import * as message_list from "./message_list";
import * as recent_view_util from "./recent_view_util"; import * as recent_view_util from "./recent_view_util";
import * as ui_util from "./ui_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()) { for (const msg_list of all_rendered_message_lists()) {
msg_list.view.update_recipient_bar_background_color(); msg_list.view.update_recipient_bar_background_color();
} }
inbox_util.update_stream_colors();
} }
export function initialize() { export function initialize() {

View File

@ -3,6 +3,7 @@ import $ from "jquery";
import render_message_view_header from "../templates/message_view_header.hbs"; import render_message_view_header from "../templates/message_view_header.hbs";
import {$t} from "./i18n"; import {$t} from "./i18n";
import * as inbox_util from "./inbox_util";
import * as narrow_state from "./narrow_state"; import * as narrow_state from "./narrow_state";
import * as peer_data from "./peer_data"; import * as peer_data from "./peer_data";
import * as popovers from "./popovers"; import * as popovers from "./popovers";
@ -26,6 +27,12 @@ function make_message_view_header(filter) {
icon: "clock-o", icon: "clock-o",
}; };
} }
if (inbox_util.is_visible()) {
return {
title: $t({defaultMessage: "Inbox"}),
zulip_icon: "inbox",
};
}
if (filter === undefined) { if (filter === undefined) {
return { return {
title: $t({defaultMessage: "All messages"}), title: $t({defaultMessage: "All messages"}),

View File

@ -4,6 +4,7 @@ import * as activity from "./activity";
import * as channel from "./channel"; import * as channel from "./channel";
import * as confirm_dialog from "./confirm_dialog"; import * as confirm_dialog from "./confirm_dialog";
import {$t_html} from "./i18n"; import {$t_html} from "./i18n";
import * as inbox_ui from "./inbox_ui";
import * as message_lists from "./message_lists"; import * as message_lists from "./message_lists";
import * as muted_users from "./muted_users"; import * as muted_users from "./muted_users";
import * as overlays from "./overlays"; 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 // If a user is (un)muted, we want to update their avatars on the Recent Conversations
// participants column. // participants column.
recent_view_ui.complete_rerender(); recent_view_ui.complete_rerender();
inbox_ui.update();
} }
export function handle_user_updates(muted_user_ids) { export function handle_user_updates(muted_user_ids) {

View File

@ -16,6 +16,8 @@ import {Filter} from "./filter";
import * as hash_util from "./hash_util"; import * as hash_util from "./hash_util";
import * as hashchange from "./hashchange"; import * as hashchange from "./hashchange";
import {$t} from "./i18n"; 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 left_sidebar_navigation_area from "./left_sidebar_navigation_area";
import * as message_edit from "./message_edit"; import * as message_edit from "./message_edit";
import * as message_feed_loading from "./message_feed_loading"; 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_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 // The empty narrow is the home view; so deactivate any narrow if
// no operators were specified. Take us to all messages when this // 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) { if (coming_from_recent_view) {
recent_view_ui.hide(); recent_view_ui.hide();
} else if (coming_from_inbox) {
inbox_ui.hide();
} else { } else {
// If Recent Conversations was not visible, then we are switching // We must instead be switching from another message view.
// from another message list view. Save the scroll position in // Save the scroll position in that message list, so that
// that message list, so that we can restore it if/when we // we can restore it if/when we later navigate back to that view.
// later navigate back to that view.
save_pre_narrow_offset_for_reload(); save_pre_narrow_offset_for_reload();
} }

View File

@ -1,5 +1,6 @@
import * as blueslip from "./blueslip"; import * as blueslip from "./blueslip";
import {Filter} from "./filter"; import {Filter} from "./filter";
import * as inbox_util from "./inbox_util";
import {page_params} from "./page_params"; import {page_params} from "./page_params";
import * as people from "./people"; import * as people from "./people";
import * as recent_view_util from "./recent_view_util"; import * as recent_view_util from "./recent_view_util";
@ -34,7 +35,7 @@ export function operators() {
} }
export function is_message_feed_visible() { 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) { export function update_email(user_id, new_email) {

View File

@ -4,6 +4,7 @@ import mixPlugin from "colord/plugins/mix";
import $ from "jquery"; import $ from "jquery";
import {$t} from "./i18n"; import {$t} from "./i18n";
import * as inbox_util from "./inbox_util";
import * as message_lists from "./message_lists"; import * as message_lists from "./message_lists";
import * as message_view_header from "./message_view_header"; import * as message_view_header from "./message_view_header";
import * as overlays from "./overlays"; import * as overlays from "./overlays";
@ -85,6 +86,11 @@ function update_message_recipient_color(stream_name, color) {
recipient_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 = [ const stream_color_palette = [

View File

@ -4,6 +4,7 @@ import * as blueslip from "./blueslip";
import * as color_data from "./color_data"; import * as color_data from "./color_data";
import * as compose_fade from "./compose_fade"; import * as compose_fade from "./compose_fade";
import * as compose_recipient from "./compose_recipient"; import * as compose_recipient from "./compose_recipient";
import * as inbox_ui from "./inbox_ui";
import * as message_lists from "./message_lists"; import * as message_lists from "./message_lists";
import * as message_view_header from "./message_view_header"; import * as message_view_header from "./message_view_header";
import * as narrow_state from "./narrow_state"; 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_muting.update_is_muted(sub, value);
stream_list.refresh_muted_or_unmuted_stream(sub); stream_list.refresh_muted_or_unmuted_stream(sub);
recent_view_ui.complete_rerender(); recent_view_ui.complete_rerender();
inbox_ui.update();
break; break;
case "desktop_notifications": case "desktop_notifications":
case "audible_notifications": case "audible_notifications":

View File

@ -41,6 +41,7 @@ import * as hashchange from "./hashchange";
import * as hotkey from "./hotkey"; import * as hotkey from "./hotkey";
import * as hotspots from "./hotspots"; import * as hotspots from "./hotspots";
import * as i18n from "./i18n"; import * as i18n from "./i18n";
import * as inbox_ui from "./inbox_ui";
import * as invite from "./invite"; import * as invite from "./invite";
import * as left_sidebar_navigation_area from "./left_sidebar_navigation_area"; import * as left_sidebar_navigation_area from "./left_sidebar_navigation_area";
import * as lightbox from "./lightbox"; import * as lightbox from "./lightbox";
@ -185,6 +186,7 @@ function initialize_bottom_whitespace() {
function initialize_left_sidebar() { function initialize_left_sidebar() {
const rendered_sidebar = render_left_sidebar({ const rendered_sidebar = render_left_sidebar({
is_guest: page_params.is_guest, is_guest: page_params.is_guest,
development_environment: page_params.development_environment,
}); });
$("#left-sidebar-container").html(rendered_sidebar); $("#left-sidebar-container").html(rendered_sidebar);
@ -682,6 +684,7 @@ export function initialize_everything() {
realm_logo.initialize(); realm_logo.initialize();
message_lists.initialize(); message_lists.initialize();
recent_view_ui.initialize(); recent_view_ui.initialize();
inbox_ui.initialize();
alert_words.initialize(alert_words_params); alert_words.initialize(alert_words_params);
emojisets.initialize(); emojisets.initialize();
scroll_bar.initialize(); scroll_bar.initialize();

View File

@ -260,11 +260,11 @@ class UnreadTopicCounter {
this.bucketer.delete(msg_id); this.bucketer.delete(msg_id);
} }
get_counts() { get_counts(include_per_topic_count = false) {
const res = {}; const res = {};
res.stream_unread_messages = 0; res.stream_unread_messages = 0;
res.stream_count = new Map(); // hash by stream_id -> count 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 // We track unread counts for streams that may be currently
// unsubscribed. Since users may re-subscribe, we don't // unsubscribed. Since users may re-subscribe, we don't
// completely throw away the data. But we do ignore it here, // completely throw away the data. But we do ignore it here,
@ -274,8 +274,24 @@ class UnreadTopicCounter {
continue; continue;
} }
res.stream_count.set(stream_id, this.get_stream_count(stream_id)); if (include_per_topic_count) {
res.stream_unread_messages += res.stream_count.get(stream_id).unmuted_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; return res;
@ -731,6 +747,17 @@ export function declare_bankruptcy() {
unread_mention_topics.clear(); 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() { export function get_counts() {
const res = {}; const res = {};

View File

@ -7,6 +7,7 @@ import * as channel from "./channel";
import * as confirm_dialog from "./confirm_dialog"; import * as confirm_dialog from "./confirm_dialog";
import * as dialog_widget from "./dialog_widget"; import * as dialog_widget from "./dialog_widget";
import {$t_html} from "./i18n"; import {$t_html} from "./i18n";
import * as inbox_ui from "./inbox_ui";
import * as loading from "./loading"; import * as loading from "./loading";
import * as message_flags from "./message_flags"; import * as message_flags from "./message_flags";
import * as message_lists from "./message_lists"; import * as message_lists from "./message_lists";
@ -167,6 +168,7 @@ function process_newly_read_message(message, options) {
} }
notifications.close_notification(message); notifications.close_notification(message);
recent_view_ui.update_topic_unread_count(message); recent_view_ui.update_topic_unread_count(message);
inbox_ui.update();
} }
export function mark_as_unread_from_here( export function mark_as_unread_from_here(
@ -310,6 +312,7 @@ export function process_read_messages_event(message_ids) {
} }
unread_ui.update_unread_counts(); unread_ui.update_unread_counts();
inbox_ui.update();
} }
export function process_unread_messages_event({message_ids, message_details}) { 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(); unread_ui.update_unread_counts();
inbox_ui.update();
} }
// Takes a list of messages and marks them as read. // Takes a list of messages and marks them as read.

View File

@ -1,5 +1,6 @@
import $ from "jquery"; import $ from "jquery";
import * as inbox_ui from "./inbox_ui";
import * as message_lists from "./message_lists"; import * as message_lists from "./message_lists";
import * as overlays from "./overlays"; import * as overlays from "./overlays";
import * as popover_menus from "./popover_menus"; 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.stream_id,
user_topic_event.topic_name, user_topic_event.topic_name,
); );
inbox_ui.update();
if (overlays.settings_open() && settings_user_topics.loaded) { if (overlays.settings_open() && settings_user_topics.loaded) {
const stream_id = user_topic_event.stream_id; const stream_id = user_topic_event.stream_id;

318
web/styles/inbox.css Normal file
View File

@ -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;
}
}

View File

@ -490,12 +490,19 @@ li.active-sub-filter {
& i { & i {
opacity: 0.7; opacity: 0.7;
} }
.zulip-icon-inbox {
font-size: 14px;
top: 2px;
position: relative;
}
} }
li.top_left_all_messages, li.top_left_all_messages,
li.top_left_mentions, li.top_left_mentions,
li.top_left_starred_messages, li.top_left_starred_messages,
li.top_left_drafts, li.top_left_drafts,
li.top_left_inbox,
li.top_left_recent_view, li.top_left_recent_view,
li.top_left_scheduled_messages { li.top_left_scheduled_messages {
position: relative; position: relative;

View File

@ -198,6 +198,25 @@ body {
--color-background-text-hover-direct-mention: hsl(240deg 70% 70% / 30%); --color-background-text-hover-direct-mention: hsl(240deg 70% 70% / 30%);
--color-background-text-group-mention: hsl(183deg 60% 45% / 18%); --color-background-text-group-mention: hsl(183deg 60% 45% / 18%);
--color-background-text-hover-group-mention: hsl(183deg 60% 45% / 30%); --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 { %dark-theme {
@ -258,6 +277,23 @@ body {
--color-background-text-hover-direct-mention: hsl(240deg 52% 60% / 45%); --color-background-text-hover-direct-mention: hsl(240deg 52% 60% / 45%);
--color-background-text-group-mention: hsl(183deg 52% 40% / 20%); --color-background-text-group-mention: hsl(183deg 52% 40% / 20%);
--color-background-text-hover-group-mention: hsl(183deg 52% 40% / 30%); --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 { @media screen {
@ -2024,6 +2060,11 @@ div.focused-message-list {
margin: 0 2px 0 5px; margin: 0 2px 0 5px;
} }
} }
.zulip-icon-inbox {
position: relative;
top: 2px;
}
} }
.message-header-stream-settings-button { .message-header-stream-settings-button {

View File

@ -0,0 +1,21 @@
<div id="inbox-dm-header" tabindex="0" class="inbox-header {{#unless has_dms_post_filter}}hidden_by_filters{{/unless}}">
<div class="inbox-left-part-wrapper">
<div class="collapsible-button"><i class="zulip-icon zulip-icon-arrow-down toggle-inbox-header-icon"></i></div>
<div class="inbox-left-part">
<div tabindex="0" class="inbox-header-name">
<i class="fa fa-envelope"></i>
<a tabindex="-1" role="button" href="/#narrow/is/private">{{t 'Direct message'}}</a>
</div>
<span class="unread_count tippy-zulip-tooltip on_hover_all_dms_read" data-tippy-content="{{t 'Mark as read' }}" role="button" tabindex="0" aria-label="{{t 'Mark as read' }}">{{unread_dms_count}}</span>
</div>
</div>
</div>
<div id="inbox-direct-messages-container">
{{#each dms_dict}}
{{> inbox_row }}
{{/each}}
</div>
<div id="inbox-streams-container">
{{> inbox_stream_container }}
</div>

View File

@ -0,0 +1,37 @@
{{#if is_stream}}
{{> inbox_stream_header_row}}
{{else}}
<div id="inbox-row-conversation-{{conversation_key}}" class="inbox-row {{#if is_hidden}}hidden_by_filters{{/if}}" tabindex="0">
<div class="inbox-left-part-wrapper">
<div class="inbox-left-part">
<div class="hide fake-collapse-button" tabindex="0"></div>
{{#if is_direct}}
<a class="recipients_info" href="{{dm_url}}">
{{#each recipients_info}}
<span class="user_block">
{{#if is_bot}}
<span class="zulip-icon zulip-icon-bot" aria-hidden="true"></span>
{{else}}
<span class="{{user_circle_class}} user_circle"></span>
{{/if}}
<span class="recipients_name">{{full_name}}</span>
</span>
{{/each}}
</a>
<span class="unread_count tippy-zulip-tooltip on_hover_dm_read" data-user-ids-string="{{user_ids_string}}" data-tippy-content="{{t 'Mark as read' }}" role="button" tabindex="0" aria-label="{{t 'Mark as read' }}">{{unread_count}}</span>
{{else if is_topic}}
{{!-- Invisible user circle element for alignment of topic text with DM user name --}}
<span class="user_circle_green user_circle invisible"></span>
<div class="inbox-topic-name">
<a tabindex="-1" href="{{topic_url}}">{{topic_name}}</a>
</div>
<span class="unread_count tippy-zulip-tooltip on_hover_topic_read"
data-stream-id="{{stream_id}}" data-topic-name="{{topic_name}}"
data-tippy-content="{{t 'Mark as read' }}" role="button" tabindex="0" aria-label="{{t 'Mark as read' }}">
{{unread_count}}
</span>
{{/if}}
</div>
</div>
</div>
{{/if}}

View File

@ -0,0 +1,10 @@
{{#each topics_dict }}
<div id="{{@key}}">
{{> inbox_row (lookup ../streams_dict @key)}}
<div class="inbox-topic-container">
{{#each this}}
{{>inbox_row this}}
{{/each}}
</div>
</div>
{{/each}}

View File

@ -0,0 +1,14 @@
<div id="inbox-stream-header-{{stream_id}}" class="inbox-header {{#if is_hidden}}hidden_by_filters{{/if}}" tabindex="0" data-stream-id="{{stream_id}}" style="background: {{stream_header_color}};">
<div class="inbox-left-part-wrapper">
<div class="collapsible-button"><i class="zulip-icon zulip-icon-arrow-down toggle-inbox-header-icon"></i></div>
<div class="inbox-left-part">
<div tabindex="0" class="inbox-header-name">
<span class="stream-privacy-original-color-{{stream_id}} stream-privacy filter-icon" style="color: {{stream_color}}">
{{> ../stream_privacy }}
</span>
<a tabindex="-1" href="{{stream_url}}">{{stream_name}}</a>
</div>
<span class="unread_count tippy-zulip-tooltip on_hover_topic_read" data-stream-id="{{stream_id}}" data-tippy-content="{{t 'Mark as read' }}" role="button" tabindex="0" aria-label="{{t 'Mark as read' }}">{{unread_count}}</span>
</div>
</div>
</div>

View File

@ -0,0 +1,13 @@
<div id="inbox-main" class="no-select">
<div class="search_group" id="inbox-filters" role="group">
<i class="zulip-icon zulip-icon-search-inbox"></i>
<input type="text" id="{{INBOX_SEARCH_ID}}" value="{{search_val}}" autocomplete="off" placeholder="{{t 'Filter' }}" />
<button data-filter="include_muted" id="{{MUTED_FILTER_ID}}" type="button" class="btn btn-default btn-inbox-filter {{#if is_spectator}}fake_disabled_button{{/if}}" role="checkbox" aria-checked="false" tabindex="0">
<i id="inbox_filter_mute_toggle" class="fa fa-square-o"></i>
{{t 'Include muted' }}
</button>
</div>
<div id="inbox-list" data-simplebar>
{{> inbox_list}}
</div>
</div>

View File

@ -13,6 +13,17 @@
</a> </a>
<span class="arrow all-messages-sidebar-menu-icon hidden-for-spectators"><i class="zulip-icon zulip-icon-ellipsis-v-solid" aria-hidden="true"></i></span> <span class="arrow all-messages-sidebar-menu-icon hidden-for-spectators"><i class="zulip-icon zulip-icon-ellipsis-v-solid" aria-hidden="true"></i></span>
</li> </li>
{{#if development_environment}}
<li class="top_left_inbox top_left_row hidden-for-spectators" title="{{t 'Inbox' }} (t)">
<a href="#inbox">
<span class="filter-icon">
<i class="zulip-icon zulip-icon-inbox" aria-hidden="true"></i>
</span>
{{~!-- squash whitespace --~}}
<span>{{t 'Inbox' }}</span>
</a>
</li>
{{/if}}
<li class="top_left_recent_view top_left_row"> <li class="top_left_recent_view top_left_row">
<a href="#recent" class="tippy-left-sidebar-tooltip global-filter-container" data-tooltip-template-id="recent-conversations-tooltip-template"> <a href="#recent" class="tippy-left-sidebar-tooltip global-filter-container" data-tooltip-template-id="recent-conversations-tooltip-template">
<span class="filter-icon"> <span class="filter-icon">

View File

@ -273,7 +273,7 @@ run_test("allow normal typing when processing text", ({override, override_rewire
// Unmapped keys should immediately return false, without // Unmapped keys should immediately return false, without
// calling any functions outside of hotkey.js. // calling any functions outside of hotkey.js.
assert_unmapped("bfoyz"); assert_unmapped("bfoyz");
assert_unmapped("BEFHILNOQTWXYZ"); assert_unmapped("BEFHILNOQWXYZ");
// All letters should return false if we are composing text. // All letters should return false if we are composing text.
override_rewire(hotkey, "processing_text", () => true); override_rewire(hotkey, "processing_text", () => true);

View File

@ -41,6 +41,7 @@ run_test("narrowing", () => {
assert.ok(!$(".top_left_mentions").hasClass("active-filter")); assert.ok(!$(".top_left_mentions").hasClass("active-filter"));
assert.ok(!$(".top_left_starred_messages").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_recent_view").hasClass("active-filter"));
assert.ok(!$(".top_left_inbox").hasClass("active-filter"));
set_global("setTimeout", (f) => { set_global("setTimeout", (f) => {
f(); f();
@ -49,7 +50,16 @@ run_test("narrowing", () => {
assert.ok(!$(".top_left_all_messages").hasClass("active-filter")); assert.ok(!$(".top_left_all_messages").hasClass("active-filter"));
assert.ok(!$(".top_left_mentions").hasClass("active-filter")); assert.ok(!$(".top_left_mentions").hasClass("active-filter"));
assert.ok(!$(".top_left_starred_messages").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")); 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", () => { run_test("update_count_in_dom", () => {