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 'Stream' }} |
+ {{t 'Topic' }} |
+
+
+ |
+
+
+
+
+
+ {{!-- Don't show the banner until we have some messages loaded. --}}
+
+
{{t "This view is still loading messages."}}
+
+
+
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) {