unread_ops: Let server do the mark as unread for us.

Previous algorithm was not correct if we didn't have full data for
the current narrow loaded from the server. #23512 adds the support
to mark messages in a narrow unread after a give message_id.

Added a new alert banner to indicate loading and success states of
an ongoing request. This is useful when requests can take a long
time to complete.
This commit is contained in:
Aman Agrawal 2022-11-16 09:50:35 +00:00 committed by Tim Abbott
parent 0ba63826dd
commit c876e12b86
7 changed files with 228 additions and 11 deletions

View File

@ -4,7 +4,7 @@ import tippy from "tippy.js";
import {$t} from "./i18n"; import {$t} from "./i18n";
export const status_classes = "alert-error alert-success alert-info alert-warning"; export const status_classes = "alert-error alert-success alert-info alert-warning alert-loading";
// TODO: Move this to the portico codebase. // TODO: Move this to the portico codebase.
export function autofocus(selector: string): void { export function autofocus(selector: string): void {

View File

@ -90,3 +90,21 @@ export function hide_error($target: JQuery): void {
export function show_error($target: JQuery): void { export function show_error($target: JQuery): void {
$target.addClass("show"); $target.addClass("show");
} }
export function loading(
response_html: string,
$status_box: JQuery,
successfully_loaded: boolean = false,
): void {
$status_box.find(".alert-content").html(response_html);
if (!successfully_loaded) {
$status_box.removeClass(common.status_classes).addClass("alert-loading").stop(true);
} else {
$status_box.removeClass(common.status_classes).addClass("alert-success").stop(true);
setTimeout(() => {
$status_box.removeClass("show");
}, 2500);
}
$status_box.addClass("show");
}

View File

@ -1,4 +1,9 @@
import $ from "jquery";
import * as blueslip from "./blueslip";
import * as channel from "./channel"; import * as channel from "./channel";
import {$t_html} from "./i18n";
import * as loading from "./loading";
import * as message_flags from "./message_flags"; import * as message_flags from "./message_flags";
import * as message_list from "./message_list"; import * as message_list from "./message_list";
import * as message_lists from "./message_lists"; import * as message_lists from "./message_lists";
@ -10,9 +15,13 @@ import * as people from "./people";
import * as recent_topics_ui from "./recent_topics_ui"; import * as recent_topics_ui from "./recent_topics_ui";
import * as recent_topics_util from "./recent_topics_util"; import * as recent_topics_util from "./recent_topics_util";
import * as reload from "./reload"; import * as reload from "./reload";
import * as ui_report from "./ui_report";
import * as unread from "./unread"; import * as unread from "./unread";
import * as unread_ui from "./unread_ui"; import * as unread_ui from "./unread_ui";
const NUM_OF_MESSAGES_UPDATED_PER_BATCH = 5000;
let loading_indicator_displayed = false;
export function mark_all_as_read() { export function mark_all_as_read() {
unread.declare_bankruptcy(); unread.declare_bankruptcy();
unread_ui.update_unread_counts(); unread_ui.update_unread_counts();
@ -47,17 +56,98 @@ function process_newly_read_message(message, options) {
recent_topics_ui.update_topic_unread_count(message); recent_topics_ui.update_topic_unread_count(message);
} }
export function mark_as_unread_from_here(message_id) { export function mark_as_unread_from_here(
/* TODO: This algorithm is not correct if we don't have full data for message_id,
the current narrow loaded from the server. include_message = true,
messages_marked_unread_till_now = 0,
Currently, we turn off the feature when fetch_status suggests narrow,
we are in that that situation, but we plan to replace this ) {
logic with asking the server to do the marking as unread. if (narrow === undefined) {
*/ narrow = JSON.stringify(message_lists.current.data.filter.operators());
const message_ids = message_lists.current.ids_greater_or_equal_than(message_id); }
message_lists.current.prevent_reading(); message_lists.current.prevent_reading();
message_flags.mark_as_unread(message_ids); const opts = {
anchor: message_id,
include_anchor: include_message,
num_before: 0,
num_after: NUM_OF_MESSAGES_UPDATED_PER_BATCH,
narrow,
op: "remove",
flag: "read",
};
channel.post({
url: "/json/messages/flags/narrow",
data: opts,
success(data) {
messages_marked_unread_till_now += data.updated_count;
if (!data.found_newest) {
// If we weren't able to complete the request fully in
// the current batch, show a progress indicator.
ui_report.loading(
$t_html(
{
defaultMessage: "Working… {N} messages marked as unread so far.",
},
{N: messages_marked_unread_till_now},
),
$("#request-progress-status-banner"),
);
if (!loading_indicator_displayed) {
loading.make_indicator(
$("#request-progress-status-banner .loading-indicator"),
{abs_positioned: true},
);
loading_indicator_displayed = true;
}
mark_as_unread_from_here(
data.last_processed_id,
false,
messages_marked_unread_till_now,
narrow,
);
} else if (loading_indicator_displayed) {
// If we were showing a loading indicator, then
// display that we finished. For the common case where
// the operation succeeds in a single batch, we don't
// bother distracting the user with the indication;
// the success will be obvious from the UI updating.
loading_indicator_displayed = false;
ui_report.loading(
$t_html(
{
defaultMessage:
"Done! {messages_marked_unread_till_now} messages marked as unread.",
},
{messages_marked_unread_till_now},
),
$("#request-progress-status-banner"),
true,
);
}
},
error(xhr) {
// If we hit the rate limit, just continue without showing any error.
if (xhr.responseJSON.code === "RATE_LIMIT_HIT") {
const milliseconds_to_wait = 1000 * xhr.responseJSON["retry-after"];
setTimeout(
() =>
mark_as_unread_from_here(
message_id,
false,
messages_marked_unread_till_now,
narrow,
),
milliseconds_to_wait,
);
} else {
// TODO: Ideally, this case would communicate the
// failure to the user, with some manual retry
// offered, since the most likely cause is a 502.
blueslip.error("Unexpected error marking messages as unread: " + xhr.responseText);
}
},
});
} }
export function resume_reading() { export function resume_reading() {

View File

@ -230,3 +230,87 @@ $alert-error-red: hsl(0, 80%, 40%);
margin-left: 0; margin-left: 0;
} }
} }
#request-progress-status-banner {
display: none;
align-items: center;
padding: 5px 10px;
margin-top: 10px;
grid-template-columns: 80px auto 50px;
&.show {
display: grid !important;
}
&.alert-loading {
.alert-zulip-logo,
.loading-indicator {
display: block;
}
/* Hide exit button since clicking it will be useless in
this scenario. */
.exit {
display: none;
}
}
&.alert-success .success-indicator {
display: block;
}
&.alert-loading,
&.alert-success {
border-color: hsl(156, 28%, 70%);
box-shadow: 0 0 2px hsl(156, 28%, 70%);
.exit {
color: hsl(156, 30%, 50%);
}
}
&::before {
content: "";
margin-left: 0;
}
.alert-zulip-logo {
display: none;
margin: auto;
grid-column: 1 / 2;
grid-row-start: 1;
}
.loading-indicator {
display: none;
margin: auto;
grid-column: 1 / 2;
grid-row-start: 1;
}
.success-indicator {
display: none;
margin: auto;
grid-column: 1 / 2;
grid-row-start: 1;
padding: 7px;
font-size: 1.5em;
line-height: 0;
color: hsl(156, 30%, 50%);
}
.alert-content {
grid-column: 2 / 3;
grid-row-start: 1;
color: hsl(0, 0%, 20%);
}
.exit {
float: unset;
margin: 0;
margin-left: auto;
grid-column: 3 / 4;
grid-row-start: 1;
line-height: 0;
}
}

View File

@ -1013,6 +1013,14 @@ body.dark-theme {
border: 1px solid hsl(49, 38%, 46%); border: 1px solid hsl(49, 38%, 46%);
} }
#request-progress-status-banner {
background-color: hsl(212, 32%, 14%);
.alert-content {
color: hsl(236, 33%, 90%);
}
}
.alert.home-error-bar { .alert.home-error-bar {
color: hsl(236, 33%, 90%); color: hsl(236, 33%, 90%);
background-color: hsla(35, 84%, 62%, 0.25); background-color: hsla(35, 84%, 62%, 0.25);
@ -1160,6 +1168,7 @@ body.dark-theme {
background-color: hsl(212, 28%, 18%); background-color: hsl(212, 28%, 18%);
} }
.alert-zulip-logo,
.top-messages-logo, .top-messages-logo,
.bottom-messages-logo { .bottom-messages-logo {
svg path { svg path {
@ -1229,6 +1238,7 @@ body.dark-theme {
color: hsl(200, 79%, 66%); color: hsl(200, 79%, 66%);
} }
#request-progress-status-banner .loading-indicator,
#loading_older_messages_indicator, #loading_older_messages_indicator,
#recent_topics_loading_messages_indicator { #recent_topics_loading_messages_indicator {
path { path {

View File

@ -216,6 +216,7 @@ p.n-margin {
display: none; display: none;
} }
.alert-zulip-logo,
.top-messages-logo, .top-messages-logo,
.bottom-messages-logo { .bottom-messages-logo {
width: 24px; width: 24px;

View File

@ -132,6 +132,20 @@
</div> </div>
<div class="alert alert_sidebar alert-error home-error-bar" id="home-error"></div> <div class="alert alert_sidebar alert-error home-error-bar" id="home-error"></div>
<div class="alert alert_sidebar alert-error home-error-bar" id="reloading-application"></div> <div class="alert alert_sidebar alert-error home-error-bar" id="reloading-application"></div>
<div class="alert alert_sidebar" id="request-progress-status-banner">
<div class="alert-zulip-logo">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 773.12 773.12">
<circle cx="386.56" cy="386.56" r="386.56"></circle>
<path d="M566.66 527.25c0 33.03-24.23 60.05-53.84 60.05H260.29c-29.61 0-53.84-27.02-53.84-60.05 0-20.22 9.09-38.2 22.93-49.09l134.37-120c2.5-2.14 5.74 1.31 3.94 4.19l-49.29 98.69c-1.38 2.76.41 6.16 3.25 6.16h191.18c29.61 0 53.83 27.03 53.83 60.05zm0-281.39c0 20.22-9.09 38.2-22.93 49.09l-134.37 120c-2.5 2.14-5.74-1.31-3.94-4.19l49.29-98.69c1.38-2.76-.41-6.16-3.25-6.16H260.29c-29.61 0-53.84-27.02-53.84-60.05s24.23-60.05 53.84-60.05h252.54c29.61 0 53.83 27.02 53.83 60.05z"></path>
</svg>
</div>
<div class="loading-indicator"></div>
<div class="success-indicator">
<i class="fa fa-check"></i>
</div>
<div class="alert-content"></div>
<div class="exit"></div>
</div>
</div> </div>
<div class="app-main"> <div class="app-main">
<div class="column-left" id="left-sidebar-container"> <div class="column-left" id="left-sidebar-container">