From 34ceafadd574ef22c11cf6ebffdd262f285c107c Mon Sep 17 00:00:00 2001 From: evykassirer Date: Wed, 4 Oct 2023 10:07:01 -0700 Subject: [PATCH] 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 --- web/src/message_fetch.js | 20 +++++- web/src/recent_view_ui.js | 94 ++++++++++++++++++++++++++++- web/src/timerender.ts | 8 ++- web/src/ui_init.js | 6 ++ web/styles/recent_view.css | 29 ++++++++- web/styles/zulip.css | 2 + web/templates/recent_view_table.hbs | 38 +++++++----- web/tests/message_fetch.test.js | 1 + web/tests/timerender.test.js | 6 ++ 9 files changed, 182 insertions(+), 22 deletions(-) diff --git a/web/src/message_fetch.js b/web/src/message_fetch.js index 8fe2f9bab2..d8e0a513fb 100644 --- a/web/src/message_fetch.js +++ b/web/src/message_fetch.js @@ -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, }); } diff --git a/web/src/recent_view_ui.js b/web/src/recent_view_ui.js index ab3945d85f..fb0d5e2ddc 100644 --- a/web/src/recent_view_ui.js +++ b/web/src/recent_view_ui.js @@ -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}, + ); + }); } diff --git a/web/src/timerender.ts b/web/src/timerender.ts index c39e0a5528..acbce6937b 100644 --- a/web/src/timerender.ts +++ b/web/src/timerender.ts @@ -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."); } diff --git a/web/src/ui_init.js b/web/src/ui_init.js index 27429c9f17..ae01031429 100644 --- a/web/src/ui_init.js +++ b/web/src/ui_init.js @@ -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); diff --git a/web/styles/recent_view.css b/web/styles/recent_view.css index 139803e344..e6c8629c5f 100644 --- a/web/styles/recent_view.css +++ b/web/styles/recent_view.css @@ -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%; diff --git a/web/styles/zulip.css b/web/styles/zulip.css index 6c2cd7eff3..dfa00eff84 100644 --- a/web/styles/zulip.css +++ b/web/styles/zulip.css @@ -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 */ diff --git a/web/templates/recent_view_table.hbs b/web/templates/recent_view_table.hbs index de6202ba4f..3489e0aa4b 100644 --- a/web/templates/recent_view_table.hbs +++ b/web/templates/recent_view_table.hbs @@ -10,18 +10,28 @@
- - - - - - - - - - - -
{{t 'Stream' }}{{t 'Topic' }} - - {{t 'Participants' }}{{t 'Time' }}
+
+ + + + + + + + + + + +
{{t 'Stream' }}{{t 'Topic' }} + + {{t 'Participants' }}{{t 'Time' }}
+ {{!-- Don't show the banner until we have some messages loaded. --}} +
+ + +
+
diff --git a/web/tests/message_fetch.test.js b/web/tests/message_fetch.test.js index e92d840a9c..9d1736c745 100644 --- a/web/tests/message_fetch.test.js +++ b/web/tests/message_fetch.test.js @@ -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, diff --git a/web/tests/timerender.test.js b/web/tests/timerender.test.js index 52b722b40f..93edd2117c 100644 --- a/web/tests/timerender.test.js +++ b/web/tests/timerender.test.js @@ -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) {