popup_banners: Redesign connection banner to use new banner component.

This commit serves as the base commit for redesigning the alert banners
by migrating them to use the new banner component. We use a new name
to refer to these banners — "Popup banners", which is more descriptive
about their behavior.

The Popup banners are appended to the container in a stacking order,
i.e., the most recent popup banner appears on the top and the oldest one
is sent to the bottom of the stack. These banners also inherit the
animations from the alert banners for visual appeal.

This commit also fixes the bug where clicking on the "Try now" button
in the popup banner resulting from an error in the `/json/messages`
endpoint resulted in call to restart_get_events in server_events.js
instead of load_messages in message_fetch.ts.

Fixes #31282.
This commit is contained in:
Sayam Samal
2025-03-05 11:33:36 +05:30
committed by Tim Abbott
parent cd08c628ba
commit 3275fcc96e
9 changed files with 159 additions and 38 deletions

View File

@@ -180,14 +180,7 @@
<source class="notification-sound-source-mp3" type="audio/mpeg" />
</audio>
<div class="alert-box">
<div class="alert alert_sidebar alert-error home-error-bar" id="connection-error">
<div class="exit"></div>
<strong class="message">{{ _('Unable to connect to Zulip.') }}</strong>
{{ _('Updates may be delayed.') }}
{{ _('Retrying soon…') }}
<a class="restart_get_events_button">{{ _('Try now.') }}</a>
</div>
<div id="popup_banners_wrapper" class="banner-wrapper alert-box">
<div class="alert alert_sidebar alert-error home-error-bar" id="zephyr-mirror-error">
<div class="exit"></div>
{# The below isn't tagged for translation

View File

@@ -182,6 +182,7 @@ EXEMPT_FILES = make_set(
"web/src/popover_menus.ts",
"web/src/popover_menus_data.ts",
"web/src/popovers.ts",
"web/src/popup_banners.ts",
"web/src/read_receipts.ts",
"web/src/realm_icon.ts",
"web/src/realm_logo.ts",

View File

@@ -1,4 +1,3 @@
import $ from "jquery";
import assert from "minimalistic-assert";
import {z} from "zod";
@@ -22,12 +21,12 @@ import * as message_viewport from "./message_viewport.ts";
import * as narrow_banner from "./narrow_banner.ts";
import {page_params} from "./page_params.ts";
import * as people from "./people.ts";
import * as popup_banners from "./popup_banners.ts";
import * as recent_view_ui from "./recent_view_ui.ts";
import type {NarrowTerm} from "./state_data.ts";
import {narrow_term_schema} from "./state_data.ts";
import * as stream_data from "./stream_data.ts";
import * as stream_list from "./stream_list.ts";
import * as ui_report from "./ui_report.ts";
import * as util from "./util.ts";
const response_schema = z.object({
@@ -392,18 +391,16 @@ export function load_messages(opts: MessageFetchOptions, attempt = 1): void {
url: "/json/messages",
data,
success(raw_data) {
if (!$("#connection-error").hasClass("get-events-error")) {
ui_report.hide_error($("#connection-error"));
}
popup_banners.close_connection_error_popup_banner(true);
const data = response_schema.parse(raw_data);
get_messages_success(data, opts);
},
error(xhr) {
if (xhr.status === 400 && !$("#connection-error").hasClass("get-events-error")) {
if (xhr.status === 400) {
// We successfully reached the server, so hide the
// connection error notice, even if the request failed
// for other reasons.
ui_report.hide_error($("#connection-error"));
popup_banners.close_connection_error_popup_banner(true);
}
if (
@@ -443,7 +440,11 @@ export function load_messages(opts: MessageFetchOptions, attempt = 1): void {
return;
}
ui_report.show_error($("#connection-error"));
popup_banners.open_connection_error_popup_banner({
on_retry_callback() {
load_messages(opts, attempt + 1);
},
});
const delay_secs = util.get_retry_backoff_seconds(xhr, attempt, true);
setTimeout(() => {

79
web/src/popup_banners.ts Normal file
View File

@@ -0,0 +1,79 @@
import $ from "jquery";
import * as banners from "./banners.ts";
import type {Banner} from "./banners.ts";
import {$t} from "./i18n.ts";
const CONNECTION_ERROR_POPUP_BANNER: Banner = {
intent: "danger",
label: $t({
defaultMessage: "Unable to connect to Zulip. Retrying soon…",
}),
buttons: [
{
type: "quiet",
label: $t({defaultMessage: "Try now"}),
custom_classes: "retry-connection",
},
],
close_button: true,
custom_classes: "connection-error-banner popup-banner",
};
export function open_connection_error_popup_banner(opts: {
on_retry_callback: () => void;
is_get_events_error?: boolean;
}): void {
// If the banner is already open, don't open it again.
if ($("#popup_banners_wrapper").find(".connection-error-banner").length > 0) {
return;
}
// Prevent 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.
if (opts.is_get_events_error) {
CONNECTION_ERROR_POPUP_BANNER.custom_classes += " get-events-error";
}
banners.append(CONNECTION_ERROR_POPUP_BANNER, $("#popup_banners_wrapper"));
$("#popup_banners_wrapper").on("click", ".retry-connection", (e) => {
e.preventDefault();
e.stopPropagation();
opts.on_retry_callback();
});
}
export function close_connection_error_popup_banner(check_if_get_events_error = false): void {
const $banner = $("#popup_banners_wrapper").find(".connection-error-banner");
if ($banner.length === 0) {
return;
}
if (check_if_get_events_error && $banner.hasClass("get-events-error")) {
return;
}
$banner.addClass("fade-out");
// The delay is the same as the animation duration for fade-out.
setTimeout(() => {
banners.close($banner);
}, 300);
}
export function initialize(): void {
$("#popup_banners_wrapper").on(
"click",
".banner-close-action",
function (this: HTMLElement, e) {
// Override the banner close event listener in web/src/banners.ts,
// to add a fade-out animation when the banner is closed.
e.preventDefault();
e.stopPropagation();
const $banner = $(this).closest(".banner");
$banner.addClass("fade-out");
// The delay is the same as the animation duration for fade-out.
setTimeout(() => {
banners.close($banner);
}, 300);
},
);
}

View File

@@ -7,12 +7,12 @@ import * as echo from "./echo.ts";
import * as loading from "./loading.ts";
import * as message_events from "./message_events.ts";
import {page_params} from "./page_params.ts";
import * as popup_banners from "./popup_banners.ts";
import * as reload from "./reload.ts";
import * as reload_state from "./reload_state.ts";
import * as sent_messages from "./sent_messages.ts";
import * as server_events_dispatch from "./server_events_dispatch.js";
import {server_message_schema} from "./server_message.ts";
import * as ui_report from "./ui_report.ts";
import * as util from "./util.ts";
import * as watchdog from "./watchdog.ts";
@@ -146,16 +146,6 @@ function get_events_success(events) {
}
}
function show_ui_connection_error() {
ui_report.show_error($("#connection-error"));
$("#connection-error").addClass("get-events-error");
}
function hide_ui_connection_error() {
ui_report.hide_error($("#connection-error"));
$("#connection-error").removeClass("get-events-error");
}
function get_events({dont_block = false} = {}) {
if (reload_state.is_in_progress()) {
return;
@@ -202,7 +192,7 @@ function get_events({dont_block = false} = {}) {
try {
get_events_xhr = undefined;
get_events_failures = 0;
hide_ui_connection_error();
popup_banners.close_connection_error_popup_banner();
get_events_success(data.events);
} catch (error) {
@@ -230,15 +220,20 @@ function get_events({dont_block = false} = {}) {
} else if (error_type === "timeout") {
// Retry indefinitely on timeout.
get_events_failures = 0;
hide_ui_connection_error();
popup_banners.close_connection_error_popup_banner();
} else {
get_events_failures += 1;
}
if (get_events_failures >= 8) {
show_ui_connection_error();
popup_banners.open_connection_error_popup_banner({
on_retry_callback() {
restart_get_events({dont_block: true});
},
is_get_events_error: true,
});
} else {
hide_ui_connection_error();
popup_banners.close_connection_error_popup_banner();
}
} catch (error) {
blueslip.error("Failed to handle get_events error", undefined, error);
@@ -287,9 +282,6 @@ export function initialize(params) {
get_events_failures = 0;
restart_get_events({dont_block: true});
});
$(".restart_get_events_button").on("click", () => {
restart_get_events({dont_block: true});
});
get_events();
}

View File

@@ -90,6 +90,7 @@ import * as pm_conversations from "./pm_conversations.ts";
import * as pm_list from "./pm_list.ts";
import * as popover_menus from "./popover_menus.ts";
import * as popovers from "./popovers.ts";
import * as popup_banners from "./popup_banners.ts";
import * as presence from "./presence.ts";
import * as pygments_data from "./pygments_data.ts";
import * as realm_logo from "./realm_logo.ts";
@@ -525,6 +526,7 @@ export function initialize_everything(state_data) {
message_viewport.initialize();
banners.initialize();
navbar_alerts.initialize();
popup_banners.initialize();
message_list_hover.initialize();
initialize_kitchen_sink_stuff();
local_message.initialize(state_data.local_message);

View File

@@ -12,6 +12,9 @@
}
.alert-animations {
/* TODO: Remove these animations in favour of those
in web/styles/banners.css, once all the alert popups
have been converted to use the new banner component. */
&.show {
animation-name: fadeIn;
animation-duration: 0.3s;

View File

@@ -145,3 +145,55 @@
color: var(--color-text-danger-banner);
border-color: var(--color-border-danger-banner);
}
@keyframes popup-banner-fadeIn {
0% {
opacity: 0;
transform: translateY(
calc(-1 * var(--popup-banner-translate-y-distance))
);
}
100% {
opacity: 1;
transform: translateY(0);
}
}
@keyframes popup-banner-fadeOut {
0% {
opacity: 1;
transform: translateY(0);
}
100% {
opacity: 0;
transform: translateY(
calc(-1 * var(--popup-banner-translate-y-distance))
);
}
}
.popup-banner-animations {
animation-name: popup-banner-fadeIn;
animation-duration: 0.3s;
animation-fill-mode: forwards;
&.fade-out {
animation-name: popup-banner-fadeOut;
}
}
.popup-banner {
@extend .popup-banner-animations;
pointer-events: auto;
max-width: 900px;
width: 100%;
/* Here the container width == viewport width,
so we can work with the media breakpoints. */
@container banner (width < $lg_min) {
max-width: 90%;
}
}

View File

@@ -23,10 +23,8 @@ page_params.test_suite = false;
// we also directly write to pointer
set_global("pointer", {});
mock_esm("../src/ui_report", {
hide_error() {
return false;
},
mock_esm("../src/popup_banners", {
close_connection_error_popup_banner() {},
});
mock_esm("../src/stream_events", {