mirror of
https://github.com/zulip/zulip.git
synced 2025-11-02 13:03:29 +00:00
topic_list: Limit number of unread topics shown at once.
This avoids a stream having potentially near-infinite height when opened in a stream with a large number of unread topics; the benefit is that you can easily access the next stream. We show an unread count next to "more topics" to make it hard to miss that there might be more, older topics with unread messages. With CSS work by Anders Kaseorg. Fixes #13087. Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
This commit is contained in:
@@ -317,9 +317,22 @@ exports.update_dom_with_unread_counts = function (counts) {
|
||||
|
||||
// counts.topic_count maps streams to hashes of topics to counts
|
||||
counts.topic_count.each(function (topic_hash, stream_id) {
|
||||
// Because the topic_list data structure doesn't keep track of
|
||||
// which topics the "more topics" unread count came from, we
|
||||
// need to compute the correct value from scratch here.
|
||||
let more_topics_total = 0;
|
||||
topic_hash.each(function (count, topic) {
|
||||
topic_list.set_count(stream_id, topic, count);
|
||||
const in_more_topics = topic_list.set_count(stream_id, topic, count);
|
||||
if (in_more_topics === true) {
|
||||
more_topics_total += count;
|
||||
}
|
||||
});
|
||||
if (topic_list.active_stream_id() === stream_id) {
|
||||
// Update the "more topics" unread count; we communicate
|
||||
// this to the `topic_list` library by passing `null` as
|
||||
// the topic.
|
||||
topic_list.set_count(stream_id, null, more_topics_total);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -61,7 +61,7 @@ function update_unread_count(unread_count_elem, count) {
|
||||
}
|
||||
|
||||
if (count === 0) {
|
||||
unread_count_elem.hide();
|
||||
unread_count_elem.addClass("zero_count");
|
||||
value_span.text('');
|
||||
} else {
|
||||
unread_count_elem.removeClass("zero_count");
|
||||
@@ -74,10 +74,10 @@ exports.set_count = function (stream_id, topic, count) {
|
||||
const widget = active_widgets.get(stream_id);
|
||||
|
||||
if (widget === undefined) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
widget.set_count(topic, count);
|
||||
return widget.set_count(topic, count);
|
||||
};
|
||||
|
||||
exports.widget = function (parent_elem, my_stream_id) {
|
||||
@@ -85,19 +85,36 @@ exports.widget = function (parent_elem, my_stream_id) {
|
||||
|
||||
self.build_list = function () {
|
||||
self.topic_items = new Dict({fold_case: true});
|
||||
let topics_selected = 0;
|
||||
let more_topics_unreads = 0;
|
||||
|
||||
const max_topics = 5;
|
||||
const max_topics_with_unread = 8;
|
||||
const topic_names = topic_data.get_recent_names(my_stream_id);
|
||||
|
||||
const ul = $('<ul class="topic-list">');
|
||||
|
||||
_.each(topic_names, function (topic_name, idx) {
|
||||
const num_unread = unread.num_unread_for_topic(my_stream_id, topic_name);
|
||||
const is_active_topic = self.active_topic === topic_name.toLowerCase();
|
||||
|
||||
if (!zoomed) {
|
||||
// We limit the number of topics we show to at most
|
||||
// max_topics_with_unread when not zoomed.
|
||||
//
|
||||
// Ideally, this logic would first check whether the active topic
|
||||
// is in the set of those with unreads to avoid ending up with
|
||||
// max_topics_with_unread + 1 total topics if the active topic comes
|
||||
// after the first several topics with unread messages.
|
||||
if (topics_selected >= max_topics_with_unread && !is_active_topic) {
|
||||
if (num_unread > 0) {
|
||||
more_topics_unreads += num_unread;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Show the most recent topics, as well as any with unread messages
|
||||
const show_topic = idx < max_topics || num_unread > 0 ||
|
||||
self.active_topic === topic_name.toLowerCase();
|
||||
const show_topic = idx < max_topics || num_unread > 0 || is_active_topic;
|
||||
|
||||
if (!show_topic) {
|
||||
return;
|
||||
@@ -114,13 +131,14 @@ exports.widget = function (parent_elem, my_stream_id) {
|
||||
const li = $(render_topic_list_item(topic_info));
|
||||
self.topic_items.set(topic_name, li);
|
||||
ul.append(li);
|
||||
topics_selected += 1;
|
||||
});
|
||||
|
||||
// Now, we decide whether we need to show the "more topics"
|
||||
// widget. We need it if there are at least 5 topics in the
|
||||
// frontend's cache, or if we (possibly) don't have all
|
||||
// historical topics in the browser's cache.
|
||||
const show_more = self.build_more_topics_section();
|
||||
const show_more = self.build_more_topics_section(more_topics_unreads);
|
||||
const sub = stream_data.get_sub_by_id(my_stream_id);
|
||||
|
||||
if (topic_names.length > max_topics || !stream_data.all_topics_in_cache(sub)) {
|
||||
@@ -129,8 +147,10 @@ exports.widget = function (parent_elem, my_stream_id) {
|
||||
return ul;
|
||||
};
|
||||
|
||||
self.build_more_topics_section = function () {
|
||||
const show_more_html = render_more_topics();
|
||||
self.build_more_topics_section = function (more_topics_unreads) {
|
||||
const show_more_html = render_more_topics({
|
||||
more_topics_unreads: more_topics_unreads,
|
||||
});
|
||||
return $(show_more_html);
|
||||
};
|
||||
|
||||
@@ -155,15 +175,65 @@ exports.widget = function (parent_elem, my_stream_id) {
|
||||
};
|
||||
|
||||
self.set_count = function (topic, count) {
|
||||
if (!self.topic_items.has(topic)) {
|
||||
// This can happen for truncated topic lists. No need
|
||||
// to warn about it.
|
||||
return;
|
||||
}
|
||||
const topic_li = self.topic_items.get(topic);
|
||||
const unread_count_elem = topic_li.find('.topic-unread-count').expectOne();
|
||||
let unread_count_elem;
|
||||
if (topic === null) {
|
||||
// null is used for updating the "more topics" count.
|
||||
if (zoomed) {
|
||||
return false;
|
||||
}
|
||||
const unread_count_parent = $(".show-more-topics");
|
||||
if (unread_count_parent.length === 0) {
|
||||
// If no show-more-topics element is present in the
|
||||
// DOM, there are two possibilities. The most likely
|
||||
// is that there are simply no unreads on that topic
|
||||
// and there should continue to not be a "more topics"
|
||||
// button; we can check this by looking at count.
|
||||
if (count === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// The alternative is that there is these new messages
|
||||
// create the need for a "more topics" widget with a
|
||||
// nonzero unread count, and we need to create one and
|
||||
// add it to the DOM.
|
||||
//
|
||||
// With our current implementation, this code path
|
||||
// will always have its results overwritten shortly
|
||||
// after, because (1) the can only happen when we just
|
||||
// added unread counts, (not removing them), and (2)
|
||||
// when learning about new (unread) messages,
|
||||
// stream_list.update_dom_with_unread_count is always
|
||||
// immediately followed by
|
||||
// stream_list.update_streams_sidebar, which will
|
||||
// rebuilds the topic list from scratch anyway.
|
||||
//
|
||||
// So this code mostly exists to document this corner
|
||||
// case if in the future we adjust the model for
|
||||
// managing unread counts. The code for updating this
|
||||
// element would look something like the following:
|
||||
//
|
||||
// var show_more = self.build_more_topics_section(count);
|
||||
// var topic_list_ul = exports.get_stream_li().find(".topic-list").expectOne();
|
||||
// topic_list_ul.append(show_more);
|
||||
return false;
|
||||
}
|
||||
unread_count_elem = unread_count_parent.find(".topic-unread-count");
|
||||
update_unread_count(unread_count_elem, count);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!self.topic_items.has(topic)) {
|
||||
// `topic_li` may not exist if the topic is behind "more
|
||||
// topics"; We need to update the "more topics" count
|
||||
// instead in that case; we do this by returning true to
|
||||
// notify the caller to accumulate these.
|
||||
return true;
|
||||
}
|
||||
|
||||
const topic_li = self.topic_items.get(topic);
|
||||
unread_count_elem = topic_li.find('.topic-unread-count');
|
||||
update_unread_count(unread_count_elem, count);
|
||||
return false;
|
||||
};
|
||||
|
||||
self.activate_topic = function () {
|
||||
@@ -315,6 +385,9 @@ exports.initialize = function () {
|
||||
if (e.metaKey || e.ctrlKey) {
|
||||
return;
|
||||
}
|
||||
if ($(e.target).closest('.show-more-topics').length > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// In a more componentized world, we would delegate some
|
||||
// of this stuff back up to our parents.
|
||||
|
||||
@@ -39,8 +39,10 @@ $topic_indent: calc($far_left_gutter_size + $left_col_size + 4px);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
li.show-more-topics a {
|
||||
font-size: 12px;
|
||||
li.show-more-topics {
|
||||
a {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
#left-sidebar #user-list,
|
||||
@@ -425,7 +427,6 @@ ul.expanded_private_messages {
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
li.show-more-topics,
|
||||
li.topic-list-item {
|
||||
position: relative;
|
||||
padding-right: 5px;
|
||||
@@ -469,6 +470,7 @@ li.expanded_private_message a {
|
||||
}
|
||||
|
||||
.zero-pm-unreads .pm-box,
|
||||
.zero-topic-unreads .more-topics-box,
|
||||
.zero-topic-unreads .topic-box {
|
||||
margin-right: 15px;
|
||||
}
|
||||
@@ -516,10 +518,6 @@ li.expanded_private_message a {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
li.show-more-topics a {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.searching-for-more-topics {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
<li class="show-more-topics bottom_left_row">
|
||||
<a href="#">{{t "more topics" }}</a>
|
||||
<li class="topic-list-item show-more-topics bottom_left_row {{#unless more_topics_unreads}}zero-topic-unreads{{/unless}}">
|
||||
<span class='topic-box'>
|
||||
<a class="topic-name" href="#">{{t "more topics" }}</a>
|
||||
<div class="topic-unread-count {{#unless more_topics_unreads}}zero_count{{/unless}}">
|
||||
<div class="value">{{more_topics_unreads}}</div>
|
||||
</div>
|
||||
</span>
|
||||
</li>
|
||||
<li class="searching-for-more-topics">
|
||||
<img src="/static/images/loading-ellipsis.svg" alt="" />
|
||||
|
||||
Reference in New Issue
Block a user