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 1564 additions and 11 deletions

View File

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

View File

@@ -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",

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none">
<g opacity="0.5">
<path d="M12 5L8 11.9282L4 5L12 5Z" fill="#1D2E48"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 178 B

View File

@@ -0,0 +1,6 @@
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512">
<title></title>
<g id="icomoon-ignore">
</g>
<path fill="#000" d="M357.546 64.055c11.897 0.007 23.568 3.331 33.68 9.601 10.099 6.264 18.256 15.22 23.552 25.862l73.385 146.562c1.565 2.963 2.451 6.339 2.451 9.923v127.998c0 16.96-6.739 33.226-18.73 45.219-11.993 11.99-28.259 18.729-45.219 18.729h-341.332c-16.959 0-33.225-6.739-45.218-18.73-11.992-11.993-18.73-28.259-18.73-45.219v-128c0-3.583 0.886-6.959 2.45-9.922l73.389-146.566c5.294-10.641 13.45-19.594 23.55-25.858 10.111-6.27 21.781-9.594 33.678-9.601h203.092zM434.88 234.72l-58.205-116.237c-1.769-3.563-4.499-6.561-7.882-8.659-3.379-2.096-7.277-3.207-11.251-3.211h-203.083c-3.976 0.003-7.874 1.115-11.254 3.211-3.381 2.097-6.111 5.096-7.881 8.659v0.058l-58.202 116.178h93.546c7.115 0 13.76 3.556 17.706 9.476l36.349 54.524h62.556l36.351-54.524c3.945-5.921 10.589-9.476 17.706-9.476h93.546zM63.947 277.281v106.719c0 5.673 2.253 11.113 6.264 15.123 4.010 4.013 9.451 6.265 15.123 6.265h341.332c5.673 0 11.113-2.253 15.123-6.265 4.013-4.010 6.265-9.45 6.265-15.123v-106.719h-95.331l-36.351 54.524c-3.947 5.92-10.591 9.475-17.706 9.475h-85.333c-7.115 0-13.76-3.555-17.706-9.475l-36.349-54.524h-95.331z"></path>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none">
<path d="M14.3333 14.3333C14.5936 14.073 14.5936 13.6509 14.3333 13.3906L10.8761 9.93326C11.7103 8.8592 12.1038 7.5076 11.9765 6.15358C11.8491 4.79958 11.2105 3.54501 10.1907 2.64532C9.1708 1.74563 7.8464 1.26846 6.48708 1.31096C5.12776 1.35346 3.83575 1.91245 2.8741 2.8741C1.91245 3.83575 1.35346 5.12776 1.31096 6.48708C1.26846 7.8464 1.74563 9.1708 2.64532 10.1907C3.54501 11.2105 4.79958 11.8491 6.15358 11.9765C7.5076 12.1038 8.8592 11.7103 9.93326 10.8761L13.3906 14.3333C13.6509 14.5936 14.073 14.5936 14.3333 14.3333ZM6.66662 10.6666C5.8755 10.6666 5.10214 10.432 4.44434 9.99253C3.78655 9.553 3.27386 8.92826 2.97111 8.19733C2.66836 7.46646 2.58914 6.66219 2.74348 5.88626C2.89782 5.11034 3.27879 4.39761 3.8382 3.8382C4.39761 3.27879 5.11034 2.89782 5.88626 2.74348C6.66219 2.58914 7.46646 2.66836 8.19733 2.97111C8.92826 3.27386 9.553 3.78655 9.99253 4.44434C10.432 5.10214 10.6666 5.8755 10.6666 6.66662C10.6654 7.72713 10.2436 8.74386 9.49373 9.49373C8.74386 10.2436 7.72713 10.6654 6.66662 10.6666Z" fill="black" fill-opacity="0.5"/>
</svg>

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/widgets.css";
import "../../styles/print.css";
import "../../styles/inbox.css";
// This should be last.
import "../ui_init";

View File

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

View File

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

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

View File

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

View File

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

View File

@@ -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() {

View File

@@ -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"}),

View File

@@ -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) {

View File

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

View File

@@ -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) {

View File

@@ -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 = [

View File

@@ -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":

View File

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

View File

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

View File

@@ -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.

View File

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

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 {
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;

View File

@@ -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 {

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>
<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>
{{#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">
<a href="#recent" class="tippy-left-sidebar-tooltip global-filter-container" data-tooltip-template-id="recent-conversations-tooltip-template">
<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
// 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);

View File

@@ -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", () => {