recent view: Add button to fetch more conversations.

We add a new timerender format for this context, where there's plenty
of space.

Fixes: #18461
This commit is contained in:
evykassirer
2023-10-04 10:07:01 -07:00
committed by Tim Abbott
parent 4b4279b450
commit 34ceafadd5
9 changed files with 182 additions and 22 deletions

View File

@@ -29,6 +29,7 @@ const consts = {
recent_view_initial_fetch_size: 400,
narrowed_view_backward_batch_size: 100,
narrowed_view_forward_batch_size: 100,
recent_view_fetch_more_batch_size: 1000,
catch_up_batch_size: 1000,
};
@@ -36,6 +37,8 @@ function process_result(data, opts) {
let messages = data.messages;
messages = messages.map((message) => message_helper.process_new_message(message));
const has_found_oldest = opts.msg_list?.data.fetch_status.has_found_oldest() ?? false;
const has_found_newest = opts.msg_list?.data.fetch_status.has_found_newest() ?? false;
// In some rare situations, we expect to discover new unread
// messages not tracked in unread.js during this fetching process.
@@ -58,6 +61,17 @@ function process_result(data, opts) {
}
}
if (messages.length > 0 && opts.msg_list === message_lists.home) {
// We keep track of how far back we've fetched messages for, for messaging in
// the recent view. This assumes `data.messages` is already sorted.
const oldest_timestamp = all_messages_data.first().timestamp;
recent_view_ui.set_oldest_message_date(
oldest_timestamp,
has_found_oldest,
has_found_newest,
);
}
huddle_data.process_loaded_messages(messages);
recent_view_ui.process_messages(messages);
stream_list.update_streams_sidebar();
@@ -74,8 +88,6 @@ function process_result(data, opts) {
// the messages we requested, and all of them are in muted
// topics, but there are older messages for this stream that
// we need to ask the server for.
const has_found_oldest = opts.msg_list.data.fetch_status.has_found_oldest();
const has_found_newest = opts.msg_list.data.fetch_status.has_found_newest();
if (has_found_oldest && has_found_newest) {
// Even after loading more messages, we have
// no messages to display in this narrow.
@@ -407,7 +419,9 @@ export function maybe_load_older_messages(opts) {
do_backfill({
msg_list,
num_before: consts.narrowed_view_backward_batch_size,
num_before: opts.recent_view
? consts.recent_view_fetch_more_batch_size
: consts.narrowed_view_backward_batch_size,
});
}

View File

@@ -118,6 +118,80 @@ export function set_default_focus() {
compose_closed_ui.set_standard_text_for_reply_button();
}
// When there are no messages loaded, we don't show a banner yet.
const NO_MESSAGES_LOADED = 0;
// When some messages are loaded, but we're still loading newer messages,
// we show a simple loading banner.
const SOME_MESSAGES_LOADED = 1;
// Once we've found the newest message, we allow the user to load
// more messages further back in time.
const SOME_MESSAGES_LOADED_INCLUDING_NEWEST = 2;
// Once all messages are loaded, we hide the banner.
const ALL_MESSAGES_LOADED = 3;
let loading_state = NO_MESSAGES_LOADED;
let oldest_message_timestamp;
export function set_oldest_message_date(timestamp, has_found_oldest, has_found_newest) {
if (has_found_oldest) {
loading_state = ALL_MESSAGES_LOADED;
} else if (has_found_newest) {
loading_state = SOME_MESSAGES_LOADED_INCLUDING_NEWEST;
} else {
loading_state = SOME_MESSAGES_LOADED;
}
oldest_message_timestamp = timestamp;
// We might be loading messages in another narrow before the recent view
// is shown, so we keep the state updated and update the banner only
// once it's actually rendered.
if ($("#recent_view_table table tbody").length) {
update_load_more_banner();
}
}
function update_load_more_banner() {
if (loading_state === NO_MESSAGES_LOADED) {
return;
}
if (loading_state === ALL_MESSAGES_LOADED) {
$(".recent-view-load-more-container").toggleClass("notvisible", true);
return;
}
// There are some messages loaded, but not all messages yet. The banner was
// hidden on page load, and we make sure to show it now that there are messages
// we can display.
$(".recent-view-load-more-container").toggleClass("notvisible", false);
// Until we've found the newest message, we only show the banner with a messages
// explaining we're still fetching messages. We don't allow the user to fetch
// more messages.
if (loading_state === SOME_MESSAGES_LOADED) {
return;
}
const $button = $(".recent-view-load-more-container .fetch-messages-button");
const $button_label = $(".recent-view-load-more-container .button-label");
const $banner_text = $(".recent-view-load-more-container .last-fetched-message");
$button.toggleClass("notvisible", false);
const time_obj = new Date(oldest_message_timestamp * 1000);
const time_string = timerender.get_localized_date_or_time_for_format(
time_obj,
"full_weekday_dayofyear_year_time",
);
$banner_text.text($t({defaultMessage: "Showing messages since {time_string}."}, {time_string}));
$button_label.toggleClass("invisible", false);
$button.prop("disabled", false);
loading.destroy_indicator(
$(".recent-view-load-more-container .fetch-messages-button .loading-indicator"),
);
}
function get_min_load_count(already_rendered_count, load_count) {
const extra_rows_for_viewing_pleasure = 15;
if (row_focus > already_rendered_count + load_count) {
@@ -874,6 +948,9 @@ export function complete_rerender() {
// was not the first view loaded in the app.
show_selected_filters();
// Update the banner now that it's rendered.
update_load_more_banner();
const $container = $("#recent_view_table table tbody");
$container.empty();
topics_widget = ListWidget.create($container, mapped_topic_values, {
@@ -1244,7 +1321,12 @@ export function change_focused_element($elt, input_key) {
return false;
}
export function initialize({on_click_participant, on_mark_pm_as_read, on_mark_topic_as_read}) {
export function initialize({
on_click_participant,
on_mark_pm_as_read,
on_mark_topic_as_read,
maybe_load_older_messages,
}) {
// load filters from local storage.
if (!page_params.is_spectator) {
// A user may have a stored filter and can log out
@@ -1388,4 +1470,14 @@ export function initialize({on_click_participant, on_mark_pm_as_read, on_mark_to
$("#recent_view_search").val("");
update_filters_view();
});
$("body").on("click", ".recent-view-load-more-container .fetch-messages-button", () => {
maybe_load_older_messages();
$(".recent-view-load-more-container .button-label").toggleClass("invisible", true);
$(".recent-view-load-more-container .fetch-messages-button").prop("disabled", true);
loading.make_indicator(
$(".recent-view-load-more-container .fetch-messages-button .loading-indicator"),
{width: 20},
);
});
}

View File

@@ -25,7 +25,11 @@ export function clear_for_testing(): void {
}
type DateFormat = "weekday" | "dayofyear" | "weekday_dayofyear_year" | "dayofyear_year";
type DateWithTimeFormat = "dayofyear_time" | "dayofyear_year_time" | "weekday_dayofyear_year_time";
type DateWithTimeFormat =
| "dayofyear_time"
| "dayofyear_year_time"
| "weekday_dayofyear_year_time"
| "full_weekday_dayofyear_year_time";
type TimeFormat = "time" | "time_sec";
type DateOrTimeFormat = DateFormat | TimeFormat | DateWithTimeFormat;
@@ -83,6 +87,8 @@ function get_format_options_for_type(type: DateOrTimeFormat): Intl.DateTimeForma
return full_format_options;
case "weekday_dayofyear_year_time": // Wed, Jul 27, 2016, 13:30
return {...long_format_options, ...time_format_options};
case "full_weekday_dayofyear_year_time": // Wednesday, July 27, 2016, 13:30
return {...long_format_options, ...time_format_options, weekday: "long", month: "long"};
default:
throw new Error("Wrong format provided.");
}

View File

@@ -604,6 +604,12 @@ export function initialize_everything() {
},
on_mark_pm_as_read: unread_ops.mark_pm_as_read,
on_mark_topic_as_read: unread_ops.mark_topic_as_read,
maybe_load_older_messages() {
message_fetch.maybe_load_older_messages({
msg_list: message_lists.home,
recent_view: true,
});
},
});
inbox_ui.initialize();
alert_words.initialize(alert_words_params);

View File

@@ -80,9 +80,7 @@
);
}
.table_fix_head table {
/* To keep border properties to the thead th. */
border-collapse: separate;
.recent-view-container {
/*
Add margin bottom equal to `#bottom-whitespace`. This helps us keep
#compose visible at its max-height without overlapping with any visible
@@ -93,6 +91,31 @@
user is at the bottom of scroll container when the compose box is open.
*/
margin-bottom: var(--max-unexpanded-compose-height);
}
.recent-view-load-more-container {
margin: 20px 10px;
align-items: center;
}
.fetch-messages-button {
display: grid;
justify-items: center;
.loading_indicator_spinner {
height: 20px;
width: 20px;
}
path {
fill: var(--color-recent-view-loading-spinner);
}
}
.table_fix_head table {
/* To keep border properties to the thead th. */
border-collapse: separate;
border-spacing: 0;
width: 100%;

View File

@@ -255,6 +255,7 @@ body {
/* Message feed loading indicator colors */
--color-zulip-logo: hsl(0deg 0% 0% / 34%);
--color-zulip-logo-loading: hsl(0deg 0% 27%);
--color-recent-view-loading-spinner: hsl(0deg 0% 27%);
--color-zulip-logo-z: hsl(0deg 0% 100%);
/* Message collapsing/condensing button colors */
@@ -383,6 +384,7 @@ body {
/* Message feed loading indicator colors */
--color-zulip-logo: hsl(0deg 0% 100% / 50%);
--color-zulip-logo-loading: hsl(0deg 0% 100%);
--color-recent-view-loading-spinner: hsl(0deg 0% 100% / 60%);
--color-zulip-logo-z: hsl(214deg 27% 18%);
/* Message collapsing/condensing button colors */

View File

@@ -10,18 +10,28 @@
</div>
</div>
<div class="table_fix_head" data-simplebar>
<table class="table table-responsive">
<tbody data-empty="{{t 'No conversations match your filters.' }}" class="required-text"></tbody>
<thead>
<tr>
<th data-sort="stream_sort">{{t 'Stream' }}</th>
<th data-sort="topic_sort">{{t 'Topic' }}</th>
<th data-sort="unread_sort" data-tippy-content="{{t 'Sort by unread message count' }}" class="unread_sort tippy-zulip-delayed-tooltip">
<i class="zulip-icon zulip-icon-unread"></i>
</th>
<th class='participants_header'>{{t 'Participants' }}</th>
<th data-sort="numeric" data-sort-prop="last_msg_id" class="last_msg_time_header active descend">{{t 'Time' }}</th>
</tr>
</thead>
</table>
<div class="recent-view-container">
<table class="table table-responsive">
<tbody data-empty="{{t 'No conversations match your filters.' }}" class="required-text"></tbody>
<thead>
<tr>
<th data-sort="stream_sort">{{t 'Stream' }}</th>
<th data-sort="topic_sort">{{t 'Topic' }}</th>
<th data-sort="unread_sort" data-tippy-content="{{t 'Sort by unread message count' }}" class="unread_sort tippy-zulip-delayed-tooltip">
<i class="zulip-icon zulip-icon-unread"></i>
</th>
<th class='participants_header'>{{t 'Participants' }}</th>
<th data-sort="numeric" data-sort-prop="last_msg_id" class="last_msg_time_header active descend">{{t 'Time' }}</th>
</tr>
</thead>
</table>
{{!-- Don't show the banner until we have some messages loaded. --}}
<div class="recent-view-load-more-container main-view-banner info notvisible">
<div class="last-fetched-message banner_content">{{t "This view is still loading messages."}}</div>
<button class="fetch-messages-button main-view-banner-action-button right_edge notvisible">
<div class="loading-indicator"></div>
<span class="button-label">{{t "Load more"}}</span>
</button>
</div>
</div>
</div>

View File

@@ -28,6 +28,7 @@ mock_esm("../src/recent_view_ui", {
process_messages: noop,
show_loading_indicator: noop,
hide_loading_indicator: noop,
set_oldest_message_date: noop,
});
mock_esm("../src/ui_report", {
hide_error: noop,

View File

@@ -105,6 +105,12 @@ run_test("get_localized_date_or_time_for_format returns correct format", () => {
date: "Wed, Jan 27, 2021, 1:53 AM",
},
},
{
format: "full_weekday_dayofyear_year_time",
expected: {
date: "Wednesday, January 27, 2021 at 1:53 AM",
},
},
];
for (const format of formats) {