mirror of
https://github.com/zulip/zulip.git
synced 2025-11-05 22:43:42 +00:00
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:
@@ -180,14 +180,7 @@
|
|||||||
<source class="notification-sound-source-mp3" type="audio/mpeg" />
|
<source class="notification-sound-source-mp3" type="audio/mpeg" />
|
||||||
</audio>
|
</audio>
|
||||||
|
|
||||||
<div class="alert-box">
|
<div id="popup_banners_wrapper" class="banner-wrapper 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 class="alert alert_sidebar alert-error home-error-bar" id="zephyr-mirror-error">
|
<div class="alert alert_sidebar alert-error home-error-bar" id="zephyr-mirror-error">
|
||||||
<div class="exit"></div>
|
<div class="exit"></div>
|
||||||
{# The below isn't tagged for translation
|
{# The below isn't tagged for translation
|
||||||
|
|||||||
@@ -182,6 +182,7 @@ EXEMPT_FILES = make_set(
|
|||||||
"web/src/popover_menus.ts",
|
"web/src/popover_menus.ts",
|
||||||
"web/src/popover_menus_data.ts",
|
"web/src/popover_menus_data.ts",
|
||||||
"web/src/popovers.ts",
|
"web/src/popovers.ts",
|
||||||
|
"web/src/popup_banners.ts",
|
||||||
"web/src/read_receipts.ts",
|
"web/src/read_receipts.ts",
|
||||||
"web/src/realm_icon.ts",
|
"web/src/realm_icon.ts",
|
||||||
"web/src/realm_logo.ts",
|
"web/src/realm_logo.ts",
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import $ from "jquery";
|
|
||||||
import assert from "minimalistic-assert";
|
import assert from "minimalistic-assert";
|
||||||
import {z} from "zod";
|
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 * as narrow_banner from "./narrow_banner.ts";
|
||||||
import {page_params} from "./page_params.ts";
|
import {page_params} from "./page_params.ts";
|
||||||
import * as people from "./people.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 * as recent_view_ui from "./recent_view_ui.ts";
|
||||||
import type {NarrowTerm} from "./state_data.ts";
|
import type {NarrowTerm} from "./state_data.ts";
|
||||||
import {narrow_term_schema} from "./state_data.ts";
|
import {narrow_term_schema} from "./state_data.ts";
|
||||||
import * as stream_data from "./stream_data.ts";
|
import * as stream_data from "./stream_data.ts";
|
||||||
import * as stream_list from "./stream_list.ts";
|
import * as stream_list from "./stream_list.ts";
|
||||||
import * as ui_report from "./ui_report.ts";
|
|
||||||
import * as util from "./util.ts";
|
import * as util from "./util.ts";
|
||||||
|
|
||||||
const response_schema = z.object({
|
const response_schema = z.object({
|
||||||
@@ -392,18 +391,16 @@ export function load_messages(opts: MessageFetchOptions, attempt = 1): void {
|
|||||||
url: "/json/messages",
|
url: "/json/messages",
|
||||||
data,
|
data,
|
||||||
success(raw_data) {
|
success(raw_data) {
|
||||||
if (!$("#connection-error").hasClass("get-events-error")) {
|
popup_banners.close_connection_error_popup_banner(true);
|
||||||
ui_report.hide_error($("#connection-error"));
|
|
||||||
}
|
|
||||||
const data = response_schema.parse(raw_data);
|
const data = response_schema.parse(raw_data);
|
||||||
get_messages_success(data, opts);
|
get_messages_success(data, opts);
|
||||||
},
|
},
|
||||||
error(xhr) {
|
error(xhr) {
|
||||||
if (xhr.status === 400 && !$("#connection-error").hasClass("get-events-error")) {
|
if (xhr.status === 400) {
|
||||||
// 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.
|
||||||
ui_report.hide_error($("#connection-error"));
|
popup_banners.close_connection_error_popup_banner(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -443,7 +440,11 @@ export function load_messages(opts: MessageFetchOptions, attempt = 1): void {
|
|||||||
return;
|
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);
|
const delay_secs = util.get_retry_backoff_seconds(xhr, attempt, true);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
|||||||
79
web/src/popup_banners.ts
Normal file
79
web/src/popup_banners.ts
Normal 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);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -7,12 +7,12 @@ import * as echo from "./echo.ts";
|
|||||||
import * as loading from "./loading.ts";
|
import * as loading from "./loading.ts";
|
||||||
import * as message_events from "./message_events.ts";
|
import * as message_events from "./message_events.ts";
|
||||||
import {page_params} from "./page_params.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 from "./reload.ts";
|
||||||
import * as reload_state from "./reload_state.ts";
|
import * as reload_state from "./reload_state.ts";
|
||||||
import * as sent_messages from "./sent_messages.ts";
|
import * as sent_messages from "./sent_messages.ts";
|
||||||
import * as server_events_dispatch from "./server_events_dispatch.js";
|
import * as server_events_dispatch from "./server_events_dispatch.js";
|
||||||
import {server_message_schema} from "./server_message.ts";
|
import {server_message_schema} from "./server_message.ts";
|
||||||
import * as ui_report from "./ui_report.ts";
|
|
||||||
import * as util from "./util.ts";
|
import * as util from "./util.ts";
|
||||||
import * as watchdog from "./watchdog.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} = {}) {
|
function get_events({dont_block = false} = {}) {
|
||||||
if (reload_state.is_in_progress()) {
|
if (reload_state.is_in_progress()) {
|
||||||
return;
|
return;
|
||||||
@@ -202,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;
|
||||||
hide_ui_connection_error();
|
popup_banners.close_connection_error_popup_banner();
|
||||||
|
|
||||||
get_events_success(data.events);
|
get_events_success(data.events);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -230,15 +220,20 @@ 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;
|
||||||
hide_ui_connection_error();
|
popup_banners.close_connection_error_popup_banner();
|
||||||
} else {
|
} else {
|
||||||
get_events_failures += 1;
|
get_events_failures += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (get_events_failures >= 8) {
|
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 {
|
} else {
|
||||||
hide_ui_connection_error();
|
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);
|
||||||
@@ -287,9 +282,6 @@ export function initialize(params) {
|
|||||||
get_events_failures = 0;
|
get_events_failures = 0;
|
||||||
restart_get_events({dont_block: true});
|
restart_get_events({dont_block: true});
|
||||||
});
|
});
|
||||||
$(".restart_get_events_button").on("click", () => {
|
|
||||||
restart_get_events({dont_block: true});
|
|
||||||
});
|
|
||||||
|
|
||||||
get_events();
|
get_events();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ import * as pm_conversations from "./pm_conversations.ts";
|
|||||||
import * as pm_list from "./pm_list.ts";
|
import * as pm_list from "./pm_list.ts";
|
||||||
import * as popover_menus from "./popover_menus.ts";
|
import * as popover_menus from "./popover_menus.ts";
|
||||||
import * as popovers from "./popovers.ts";
|
import * as popovers from "./popovers.ts";
|
||||||
|
import * as popup_banners from "./popup_banners.ts";
|
||||||
import * as presence from "./presence.ts";
|
import * as presence from "./presence.ts";
|
||||||
import * as pygments_data from "./pygments_data.ts";
|
import * as pygments_data from "./pygments_data.ts";
|
||||||
import * as realm_logo from "./realm_logo.ts";
|
import * as realm_logo from "./realm_logo.ts";
|
||||||
@@ -525,6 +526,7 @@ export function initialize_everything(state_data) {
|
|||||||
message_viewport.initialize();
|
message_viewport.initialize();
|
||||||
banners.initialize();
|
banners.initialize();
|
||||||
navbar_alerts.initialize();
|
navbar_alerts.initialize();
|
||||||
|
popup_banners.initialize();
|
||||||
message_list_hover.initialize();
|
message_list_hover.initialize();
|
||||||
initialize_kitchen_sink_stuff();
|
initialize_kitchen_sink_stuff();
|
||||||
local_message.initialize(state_data.local_message);
|
local_message.initialize(state_data.local_message);
|
||||||
|
|||||||
@@ -12,6 +12,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.alert-animations {
|
.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 {
|
&.show {
|
||||||
animation-name: fadeIn;
|
animation-name: fadeIn;
|
||||||
animation-duration: 0.3s;
|
animation-duration: 0.3s;
|
||||||
|
|||||||
@@ -145,3 +145,55 @@
|
|||||||
color: var(--color-text-danger-banner);
|
color: var(--color-text-danger-banner);
|
||||||
border-color: var(--color-border-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%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -23,10 +23,8 @@ page_params.test_suite = false;
|
|||||||
// we also directly write to pointer
|
// we also directly write to pointer
|
||||||
set_global("pointer", {});
|
set_global("pointer", {});
|
||||||
|
|
||||||
mock_esm("../src/ui_report", {
|
mock_esm("../src/popup_banners", {
|
||||||
hide_error() {
|
close_connection_error_popup_banner() {},
|
||||||
return false;
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
mock_esm("../src/stream_events", {
|
mock_esm("../src/stream_events", {
|
||||||
|
|||||||
Reference in New Issue
Block a user