import {addDays} from "date-fns"; import $ from "jquery"; import assert from "minimalistic-assert"; import render_bankruptcy_alert_content from "../templates/navbar_alerts/bankruptcy.hbs"; import render_configure_email_alert_content from "../templates/navbar_alerts/configure_outgoing_email.hbs"; import render_demo_organization_deadline_content from "../templates/navbar_alerts/demo_organization_deadline.hbs"; import render_desktop_notifications_alert_content from "../templates/navbar_alerts/desktop_notifications.hbs"; import render_empty_required_profile_fields from "../templates/navbar_alerts/empty_required_profile_fields.hbs"; import render_insecure_desktop_app_alert_content from "../templates/navbar_alerts/insecure_desktop_app.hbs"; import render_navbar_alert_wrapper from "../templates/navbar_alerts/navbar_alert_wrapper.hbs"; import render_profile_incomplete_alert_content from "../templates/navbar_alerts/profile_incomplete.hbs"; import render_server_needs_upgrade_alert_content from "../templates/navbar_alerts/server_needs_upgrade.hbs"; import * as desktop_notifications from "./desktop_notifications.ts"; import * as keydown_util from "./keydown_util.ts"; import type {LocalStorage} from "./localstorage.ts"; import {localstorage} from "./localstorage.ts"; import {page_params} from "./page_params.ts"; import * as people from "./people.ts"; import {current_user, realm} from "./state_data.ts"; import {should_display_profile_incomplete_alert} from "./timerender.ts"; import * as unread from "./unread.ts"; import * as unread_ops from "./unread_ops.ts"; import * as unread_ui from "./unread_ui.ts"; import * as util from "./util.ts"; const show_step = function ($process: JQuery, step: number): void { $process .find("[data-step]") .hide() .filter("[data-step=" + step + "]") .show(); }; const get_step = function ($process: JQuery): number { return Number($process.find("[data-step]:visible").attr("data-step")); }; export function should_show_notifications(ls: LocalStorage): boolean { // if the user said to never show banner on this computer again, it will // be stored as `true` so we want to negate that. if (localstorage.supported() && ls.get("dontAskForNotifications") === true) { return false; } return ( // Spectators cannot receive desktop notifications, so never // request permissions to send them. !page_params.is_spectator && // notifications *basically* don't work on any mobile platforms, so don't // event show the banners. This prevents trying to access things that // don't exist like `Notification.permission`. !util.is_mobile() && // if permission has not been granted yet. !desktop_notifications.granted_desktop_notifications_permission() && // if permission is allowed to be requested (e.g. not in "denied" state). desktop_notifications.permission_state() !== "denied" ); } export function should_show_server_upgrade_notification(ls: LocalStorage): boolean { // We do not show the server upgrade nag for a week after the user // clicked "dismiss". if (!localstorage.supported() || ls.get("lastUpgradeNagDismissalTime") === undefined) { return true; } const last_notification_dismissal_time = ls.get("lastUpgradeNagDismissalTime"); assert(typeof last_notification_dismissal_time === "number"); const upgrade_nag_dismissal_duration = addDays( new Date(last_notification_dismissal_time), 7, ).getTime(); // show the notification only if the time duration is completed. return Date.now() > upgrade_nag_dismissal_duration; } export function maybe_show_empty_required_profile_fields_alert(): void { const $navbar_alert = $("#navbar_alerts_wrapper").children(".alert").first(); const empty_required_profile_fields_exist = realm.custom_profile_fields .map((f) => ({ ...f, value: people.my_custom_profile_data(f.id)?.value, })) .find((f) => f.required && !f.value); if (!empty_required_profile_fields_exist) { if ($navbar_alert.attr("data-process") === "profile-missing-required") { $navbar_alert.hide(); } return; } if (!$navbar_alert?.length || $navbar_alert.is(":hidden")) { open({ data_process: "profile-missing-required", rendered_alert_content_html: render_empty_required_profile_fields(), }); } } export function dismiss_upgrade_nag(ls: LocalStorage): void { $(".alert[data-process='server-needs-upgrade'").hide(); if (localstorage.supported()) { ls.set("lastUpgradeNagDismissalTime", Date.now()); } } export function check_profile_incomplete(): boolean { if (!current_user.is_admin) { return false; } if (!should_display_profile_incomplete_alert(realm.realm_date_created)) { return false; } // Eventually, we might also check realm.realm_icon_source, // but it feels too aggressive to ask users to do change that // since their organization might not have a logo yet. if ( realm.realm_description === "" || /^Organization imported from [A-Za-z]+[!.]$/.test(realm.realm_description) ) { return true; } return false; } export function show_profile_incomplete(is_profile_incomplete: boolean): void { if (is_profile_incomplete) { // Note that this will be a noop unless we'd already displayed // the notice in this session. This seems OK, given that // this is meant to be a one-time task for administrators. $("[data-process='profile-incomplete']").show(); } else { $("[data-process='profile-incomplete']").hide(); } } export function get_demo_organization_deadline_days_remaining(): number { const now = Date.now(); assert(realm.demo_organization_scheduled_deletion_date !== undefined); const deadline = realm.demo_organization_scheduled_deletion_date * 1000; const day = 24 * 60 * 60 * 1000; // hours * minutes * seconds * milliseconds const days_remaining = Math.round(Math.abs(deadline - now) / day); return days_remaining; } export function initialize(): void { const ls = localstorage(); if (realm.demo_organization_scheduled_deletion_date) { const days_remaining = get_demo_organization_deadline_days_remaining(); open({ data_process: "demo-organization-deadline", custom_class: days_remaining <= 7 ? "red" : "", rendered_alert_content_html: render_demo_organization_deadline_content({ days_remaining, }), }); } else if (page_params.insecure_desktop_app) { open({ data_process: "insecure-desktop-app", custom_class: "red", rendered_alert_content_html: render_insecure_desktop_app_alert_content(), }); } else if (realm.server_needs_upgrade) { if (should_show_server_upgrade_notification(ls)) { open({ data_process: "server-needs-upgrade", custom_class: "red", rendered_alert_content_html: render_server_needs_upgrade_alert_content(), }); } } else if (page_params.warn_no_email === true && current_user.is_admin) { // if email has not been set up and the user is the admin, // display a warning to tell them to set up an email server. open({ data_process: "email-server", custom_class: "red", rendered_alert_content_html: render_configure_email_alert_content(), }); } else if (should_show_notifications(ls)) { open({ data_process: "notifications", rendered_alert_content_html: render_desktop_notifications_alert_content(), }); } else if (unread_ui.should_display_bankruptcy_banner()) { const old_unreads_missing = unread.old_unreads_missing; const unread_msgs_count = unread.get_unread_message_count(); open({ data_process: "bankruptcy", custom_class: "bankruptcy", rendered_alert_content_html: render_bankruptcy_alert_content({ old_unreads_missing, unread_msgs_count, }), }); } else if (check_profile_incomplete()) { open({ data_process: "profile-incomplete", rendered_alert_content_html: render_profile_incomplete_alert_content(), }); } else { maybe_show_empty_required_profile_fields_alert(); } // Configure click handlers. $(".request-desktop-notifications").on("click", function (e) { e.preventDefault(); $(this).closest(".alert").hide(); desktop_notifications.request_desktop_notifications_permission(); $(window).trigger("resize"); }); $(".reject-notifications").on("click", function () { $(this).closest(".alert").hide(); ls.set("dontAskForNotifications", true); $(window).trigger("resize"); }); $(".accept-bankruptcy").on("click", function (e) { e.preventDefault(); const $process = $(this).closest("[data-process]"); show_step($process, 2); setTimeout(unread_ops.mark_all_as_read, 1000); $(window).trigger("resize"); }); $(".dismiss-upgrade-nag").on("click", (e: JQuery.ClickEvent) => { e.preventDefault(); e.stopPropagation(); dismiss_upgrade_nag(ls); }); $("#navbar_alerts_wrapper").on( "click", ".alert .close, .alert .exit", function (this: HTMLElement, e) { e.stopPropagation(); const $process = $(this).closest("[data-process]"); if (get_step($process) === 1 && $process.attr("data-process") === "notifications") { show_step($process, 2); } else { $(this).closest(".alert").hide(); if ($process.attr("data-process") !== "profile-missing-required") { maybe_show_empty_required_profile_fields_alert(); } } $(window).trigger("resize"); }, ); // Treat Enter with links in the navbar alerts UI focused like a click., $("#navbar_alerts_wrapper").on("keyup", ".alert-link[role=button]", function (e) { e.stopPropagation(); if (keydown_util.is_enter_event(e)) { $(this).trigger("click"); } }); } export function open(args: { data_process: string; rendered_alert_content_html: string; custom_class?: string | undefined; }): void { const rendered_alert_wrapper_html = render_navbar_alert_wrapper(args); // Note: We only support one alert being rendered at a time; as a // result, we just replace the alert area in the DOM with the // indicated alert. We do this to avoid bad UX, as it'd look weird // to have more than one alert visible at a time. $("#navbar_alerts_wrapper").html(rendered_alert_wrapper_html); $(window).trigger("resize"); }