mirror of
https://github.com/zulip/zulip.git
synced 2025-11-13 18:36:36 +00:00
popup_banners: Improve connection error banner.
This commit makes the following changes: - Prevents the codepath in load_messages in message_fetch and get_events in server_events from interfering with each other. - The banner label now displays the time interval after which the connection will be retried. - The loading indicator now shows on the banner when the retry is in progress via the JS logic. Co-authored-by: Sayam Samal <sayam@zulip.com> Fixes #33924.
This commit is contained in:
@@ -371,6 +371,11 @@ export function get_parameters_for_message_fetch_api(
|
|||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// We keep track of the load messages timeout at a module level
|
||||||
|
// to prevent multiple load messages requests from the error codepath
|
||||||
|
// from stacking up by cancelling the previous timeout.
|
||||||
|
let load_messages_timeout: ReturnType<typeof setTimeout> | undefined;
|
||||||
|
|
||||||
export function load_messages(opts: MessageFetchOptions, attempt = 1): void {
|
export function load_messages(opts: MessageFetchOptions, attempt = 1): void {
|
||||||
const data = get_parameters_for_message_fetch_api(opts);
|
const data = get_parameters_for_message_fetch_api(opts);
|
||||||
let update_loading_indicator =
|
let update_loading_indicator =
|
||||||
@@ -393,7 +398,10 @@ export function load_messages(opts: MessageFetchOptions, attempt = 1): void {
|
|||||||
url: "/json/messages",
|
url: "/json/messages",
|
||||||
data,
|
data,
|
||||||
success(raw_data) {
|
success(raw_data) {
|
||||||
popup_banners.close_connection_error_popup_banner(true);
|
if (load_messages_timeout !== undefined) {
|
||||||
|
clearTimeout(load_messages_timeout);
|
||||||
|
}
|
||||||
|
popup_banners.close_connection_error_popup_banner("message_fetch");
|
||||||
const data = response_schema.parse(raw_data);
|
const data = response_schema.parse(raw_data);
|
||||||
get_messages_success(data, opts);
|
get_messages_success(data, opts);
|
||||||
},
|
},
|
||||||
@@ -402,7 +410,10 @@ export function load_messages(opts: MessageFetchOptions, attempt = 1): void {
|
|||||||
// We successfully reached the server, so hide the
|
// We successfully reached the server, so hide the
|
||||||
// connection error notice, even if the request failed
|
// connection error notice, even if the request failed
|
||||||
// for other reasons.
|
// for other reasons.
|
||||||
popup_banners.close_connection_error_popup_banner(true);
|
if (load_messages_timeout !== undefined) {
|
||||||
|
clearTimeout(load_messages_timeout);
|
||||||
|
}
|
||||||
|
popup_banners.close_connection_error_popup_banner("message_fetch");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -442,14 +453,16 @@ export function load_messages(opts: MessageFetchOptions, attempt = 1): void {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const delay_secs = util.get_retry_backoff_seconds(xhr, attempt, true);
|
||||||
popup_banners.open_connection_error_popup_banner({
|
popup_banners.open_connection_error_popup_banner({
|
||||||
|
caller: "message_fetch",
|
||||||
|
retry_delay_secs: delay_secs,
|
||||||
on_retry_callback() {
|
on_retry_callback() {
|
||||||
load_messages(opts, attempt + 1);
|
load_messages(opts, attempt + 1);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const delay_secs = util.get_retry_backoff_seconds(xhr, attempt, true);
|
load_messages_timeout = setTimeout(() => {
|
||||||
setTimeout(() => {
|
|
||||||
load_messages(opts, attempt + 1);
|
load_messages(opts, attempt + 1);
|
||||||
}, delay_secs * 1000);
|
}, delay_secs * 1000);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ import type {Banner} from "./banners.ts";
|
|||||||
import * as buttons from "./buttons.ts";
|
import * as buttons from "./buttons.ts";
|
||||||
import {$t} from "./i18n.ts";
|
import {$t} from "./i18n.ts";
|
||||||
|
|
||||||
|
let retry_connection_interval: ReturnType<typeof setInterval> | undefined;
|
||||||
|
let original_retry_delay_secs = 0;
|
||||||
|
|
||||||
function fade_out_popup_banner($banner: JQuery): void {
|
function fade_out_popup_banner($banner: JQuery): void {
|
||||||
$banner.addClass("fade-out");
|
$banner.addClass("fade-out");
|
||||||
// The delay is the same as the animation duration for fade-out.
|
// The delay is the same as the animation duration for fade-out.
|
||||||
@@ -13,11 +16,25 @@ function fade_out_popup_banner($banner: JQuery): void {
|
|||||||
}, 300);
|
}, 300);
|
||||||
}
|
}
|
||||||
|
|
||||||
const CONNECTION_ERROR_POPUP_BANNER: Banner = {
|
const get_connection_error_label = (retry_delay_secs: number): string => {
|
||||||
|
if (original_retry_delay_secs < 5) {
|
||||||
|
// When the retry delay is less than 5 seconds, we don't show the retry
|
||||||
|
// delay time in the banner, and instead just show "Retrying soon…" to
|
||||||
|
// constant flickering of the banner label for very short times.
|
||||||
|
return $t({defaultMessage: "Unable to connect to Zulip. Retrying soon…"});
|
||||||
|
}
|
||||||
|
return $t(
|
||||||
|
{
|
||||||
|
defaultMessage:
|
||||||
|
"Unable to connect to Zulip. {retry_delay_secs, plural, one {Trying again in {retry_delay_secs} second…} other {Trying again in {retry_delay_secs} seconds…}}",
|
||||||
|
},
|
||||||
|
{retry_delay_secs},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const connection_error_popup_banner = (retry_seconds: number): Banner => ({
|
||||||
intent: "danger",
|
intent: "danger",
|
||||||
label: $t({
|
label: get_connection_error_label(retry_seconds),
|
||||||
defaultMessage: "Unable to connect to Zulip. Retrying soon…",
|
|
||||||
}),
|
|
||||||
buttons: [
|
buttons: [
|
||||||
{
|
{
|
||||||
attention: "quiet",
|
attention: "quiet",
|
||||||
@@ -27,6 +44,29 @@ const CONNECTION_ERROR_POPUP_BANNER: Banner = {
|
|||||||
],
|
],
|
||||||
close_button: true,
|
close_button: true,
|
||||||
custom_classes: "connection-error-banner popup-banner",
|
custom_classes: "connection-error-banner popup-banner",
|
||||||
|
});
|
||||||
|
|
||||||
|
const update_connection_error_banner = ($banner: JQuery, retry_delay_secs: number): void => {
|
||||||
|
original_retry_delay_secs = retry_delay_secs;
|
||||||
|
if (retry_connection_interval !== undefined) {
|
||||||
|
clearInterval(retry_connection_interval);
|
||||||
|
}
|
||||||
|
const $banner_label = $banner.find(".banner-label");
|
||||||
|
retry_connection_interval = setInterval(() => {
|
||||||
|
retry_delay_secs -= 1;
|
||||||
|
if (retry_delay_secs <= 0) {
|
||||||
|
// When the retry delay is over, stop the retry interval.
|
||||||
|
clearInterval(retry_connection_interval);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (retry_delay_secs <= 1) {
|
||||||
|
// One second before the retry, show the loading indicator to
|
||||||
|
// visually indicate that the retry sequence is being executed.
|
||||||
|
const $retry_connection_button = $banner.find(".retry-connection");
|
||||||
|
buttons.show_button_loading_indicator($retry_connection_button);
|
||||||
|
}
|
||||||
|
$banner_label.text(get_connection_error_label(retry_delay_secs));
|
||||||
|
}, 1000);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Show user a banner with a button to allow user to navigate
|
// Show user a banner with a button to allow user to navigate
|
||||||
@@ -74,13 +114,23 @@ export function close_found_missing_unreads_banner(): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function open_connection_error_popup_banner(opts: {
|
export function open_connection_error_popup_banner(opts: {
|
||||||
|
caller: "server_events" | "message_fetch";
|
||||||
|
retry_delay_secs: number;
|
||||||
on_retry_callback: () => void;
|
on_retry_callback: () => void;
|
||||||
is_get_events_error?: boolean;
|
|
||||||
}): void {
|
}): void {
|
||||||
|
opts.retry_delay_secs = Math.round(opts.retry_delay_secs);
|
||||||
// If the banner is already open, don't open it again, and instead remove
|
// If the banner is already open, don't open it again, and instead remove
|
||||||
// the loading indicator on the retry button, if it was being shown.
|
// the loading indicator on the retry button, if it was being shown.
|
||||||
const $banner = $("#popup_banners_wrapper").find(".connection-error-banner");
|
let $banner = $("#popup_banners_wrapper").find(".connection-error-banner");
|
||||||
if ($banner.length > 0) {
|
if ($banner.length > 0) {
|
||||||
|
if ($banner.attr("data-caller") !== opts.caller) {
|
||||||
|
// Only the original caller should be able to modify the banner.
|
||||||
|
// This prevents the interference between the server errors from
|
||||||
|
// get_events in web/src/server_events.js and the one from
|
||||||
|
// load_messages in web/src/message_fetch.ts.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
update_connection_error_banner($banner, opts.retry_delay_secs);
|
||||||
const $retry_connection_button = $banner.find(".retry-connection");
|
const $retry_connection_button = $banner.find(".retry-connection");
|
||||||
if ($retry_connection_button.find(".button-loading-indicator").length > 0) {
|
if ($retry_connection_button.find(".button-loading-indicator").length > 0) {
|
||||||
// Add some delay before hiding the loading indicator, to visually
|
// Add some delay before hiding the loading indicator, to visually
|
||||||
@@ -91,18 +141,30 @@ export function open_connection_error_popup_banner(opts: {
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Prevent the interference between the server errors from
|
|
||||||
// get_events in web/src/server_events.js and the one from
|
banners.append(
|
||||||
// load_messages in web/src/message_fetch.ts.
|
connection_error_popup_banner(opts.retry_delay_secs),
|
||||||
if (opts.is_get_events_error) {
|
$("#popup_banners_wrapper"),
|
||||||
CONNECTION_ERROR_POPUP_BANNER.custom_classes += " get-events-error";
|
);
|
||||||
|
|
||||||
|
$banner = $("#popup_banners_wrapper").find(".connection-error-banner");
|
||||||
|
if (opts.caller === "server_events") {
|
||||||
|
$banner.attr("data-caller", "server_events");
|
||||||
|
} else if (opts.caller === "message_fetch") {
|
||||||
|
$banner.attr("data-caller", "message_fetch");
|
||||||
}
|
}
|
||||||
banners.append(CONNECTION_ERROR_POPUP_BANNER, $("#popup_banners_wrapper"));
|
|
||||||
|
update_connection_error_banner($banner, opts.retry_delay_secs);
|
||||||
|
|
||||||
$("#popup_banners_wrapper").on("click", ".retry-connection", function (this: HTMLElement, e) {
|
$("#popup_banners_wrapper").on("click", ".retry-connection", function (this: HTMLElement, e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
|
const $banner = $(this).closest(".banner");
|
||||||
|
$banner
|
||||||
|
.find(".banner-label")
|
||||||
|
.text($t({defaultMessage: "Unable to connect to Zulip. Retrying now…"}));
|
||||||
|
|
||||||
const $button = $(this);
|
const $button = $(this);
|
||||||
|
|
||||||
// If the loading indicator is already being shown, this logic
|
// If the loading indicator is already being shown, this logic
|
||||||
@@ -118,14 +180,20 @@ export function open_connection_error_popup_banner(opts: {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function close_connection_error_popup_banner(check_if_get_events_error = false): void {
|
export function close_connection_error_popup_banner(
|
||||||
|
caller: "server_events" | "message_fetch",
|
||||||
|
): void {
|
||||||
const $banner = $("#popup_banners_wrapper").find(".connection-error-banner");
|
const $banner = $("#popup_banners_wrapper").find(".connection-error-banner");
|
||||||
if ($banner.length === 0) {
|
if ($banner.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (check_if_get_events_error && $banner.hasClass("get-events-error")) {
|
if ($banner.attr("data-caller") !== caller) {
|
||||||
|
// Only the original caller should be able to modify the banner.
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (retry_connection_interval !== undefined) {
|
||||||
|
clearInterval(retry_connection_interval);
|
||||||
|
}
|
||||||
fade_out_popup_banner($banner);
|
fade_out_popup_banner($banner);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -192,7 +192,7 @@ function get_events({dont_block = false} = {}) {
|
|||||||
try {
|
try {
|
||||||
get_events_xhr = undefined;
|
get_events_xhr = undefined;
|
||||||
get_events_failures = 0;
|
get_events_failures = 0;
|
||||||
popup_banners.close_connection_error_popup_banner();
|
popup_banners.close_connection_error_popup_banner("server_events");
|
||||||
|
|
||||||
get_events_success(data.events);
|
get_events_success(data.events);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -201,6 +201,7 @@ function get_events({dont_block = false} = {}) {
|
|||||||
get_events_timeout = setTimeout(get_events, 0);
|
get_events_timeout = setTimeout(get_events, 0);
|
||||||
},
|
},
|
||||||
error(xhr, error_type) {
|
error(xhr, error_type) {
|
||||||
|
const retry_delay_secs = util.get_retry_backoff_seconds(xhr, get_events_failures);
|
||||||
try {
|
try {
|
||||||
get_events_xhr = undefined;
|
get_events_xhr = undefined;
|
||||||
// If we're old enough that our message queue has been
|
// If we're old enough that our message queue has been
|
||||||
@@ -220,26 +221,24 @@ function get_events({dont_block = false} = {}) {
|
|||||||
} else if (error_type === "timeout") {
|
} else if (error_type === "timeout") {
|
||||||
// Retry indefinitely on timeout.
|
// Retry indefinitely on timeout.
|
||||||
get_events_failures = 0;
|
get_events_failures = 0;
|
||||||
popup_banners.close_connection_error_popup_banner();
|
popup_banners.close_connection_error_popup_banner("server_events");
|
||||||
} else {
|
} else {
|
||||||
get_events_failures += 1;
|
get_events_failures += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (get_events_failures >= 8) {
|
if (get_events_failures >= 8) {
|
||||||
popup_banners.open_connection_error_popup_banner({
|
popup_banners.open_connection_error_popup_banner({
|
||||||
|
caller: "server_events",
|
||||||
|
retry_delay_secs,
|
||||||
on_retry_callback() {
|
on_retry_callback() {
|
||||||
restart_get_events({dont_block: true});
|
restart_get_events({dont_block: true});
|
||||||
},
|
},
|
||||||
is_get_events_error: true,
|
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
popup_banners.close_connection_error_popup_banner();
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
blueslip.error("Failed to handle get_events error", undefined, error);
|
blueslip.error("Failed to handle get_events error", undefined, error);
|
||||||
}
|
}
|
||||||
|
|
||||||
const retry_delay_secs = util.get_retry_backoff_seconds(xhr, get_events_failures);
|
|
||||||
get_events_timeout = setTimeout(get_events, retry_delay_secs * 1000);
|
get_events_timeout = setTimeout(get_events, retry_delay_secs * 1000);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user