173 lines
4.6 KiB
TypeScript
173 lines
4.6 KiB
TypeScript
// Copyright 2025 Signal Messenger, LLC
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
import { createLogger } from '../logging/log';
|
|
import { isMoreRecentThan } from './timestamp';
|
|
import { DAY, WEEK } from './durations';
|
|
import { isNotNil } from './isNotNil';
|
|
import { clearTimeoutIfNecessary } from './clearTimeoutIfNecessary';
|
|
import { safeSetTimeout } from './timeout';
|
|
|
|
const log = createLogger('handleServerAlerts');
|
|
|
|
export enum ServerAlert {
|
|
CRITICAL_IDLE_PRIMARY_DEVICE = 'critical_idle_primary_device',
|
|
IDLE_PRIMARY_DEVICE = 'idle_primary_device',
|
|
}
|
|
|
|
export type ServerAlertsType = {
|
|
[ServerAlert.IDLE_PRIMARY_DEVICE]?: {
|
|
firstReceivedAt: number;
|
|
dismissedAt?: number;
|
|
};
|
|
[ServerAlert.CRITICAL_IDLE_PRIMARY_DEVICE]?: {
|
|
firstReceivedAt: number;
|
|
modalLastDismissedAt?: number;
|
|
};
|
|
};
|
|
|
|
export function parseServerAlertsFromHeader(
|
|
headerValue: string
|
|
): Array<ServerAlert> {
|
|
return headerValue
|
|
.split(',')
|
|
.map(value => value.toLowerCase().trim())
|
|
.map(header => {
|
|
if (header === 'critical-idle-primary-device') {
|
|
return ServerAlert.CRITICAL_IDLE_PRIMARY_DEVICE;
|
|
}
|
|
if (header === 'idle-primary-device') {
|
|
return ServerAlert.IDLE_PRIMARY_DEVICE;
|
|
}
|
|
log.warn(
|
|
'parseServerAlertFromHeader: unknown server alert received',
|
|
headerValue
|
|
);
|
|
return null;
|
|
})
|
|
.filter(isNotNil);
|
|
}
|
|
|
|
export async function handleServerAlerts(
|
|
receivedAlerts: Array<ServerAlert>
|
|
): Promise<void> {
|
|
const existingAlerts = window.storage.get('serverAlerts') ?? {};
|
|
const existingAlertNames = new Set(Object.keys(existingAlerts));
|
|
|
|
const now = Date.now();
|
|
const newAlerts: ServerAlertsType = {};
|
|
|
|
for (const alert of receivedAlerts) {
|
|
existingAlertNames.delete(alert);
|
|
|
|
const existingAlert = existingAlerts[alert];
|
|
if (existingAlert) {
|
|
newAlerts[alert] = existingAlert;
|
|
} else {
|
|
newAlerts[alert] = {
|
|
firstReceivedAt: now,
|
|
};
|
|
log.info(`got new alert: ${alert}`);
|
|
}
|
|
|
|
if (alert === ServerAlert.CRITICAL_IDLE_PRIMARY_DEVICE) {
|
|
maybeShowCriticalIdlePrimaryDeviceModal(newAlerts[alert]);
|
|
}
|
|
}
|
|
|
|
if (existingAlertNames.size > 0) {
|
|
log.info(`removed alerts: ${[...existingAlertNames].join(', ')}`);
|
|
}
|
|
|
|
await window.storage.put('serverAlerts', newAlerts);
|
|
}
|
|
|
|
export function getServerAlertToShow(
|
|
alerts: ServerAlertsType
|
|
): ServerAlert | null {
|
|
if (alerts[ServerAlert.CRITICAL_IDLE_PRIMARY_DEVICE]) {
|
|
return ServerAlert.CRITICAL_IDLE_PRIMARY_DEVICE;
|
|
}
|
|
|
|
if (
|
|
shouldShowIdlePrimaryDeviceAlert(alerts[ServerAlert.IDLE_PRIMARY_DEVICE])
|
|
) {
|
|
return ServerAlert.IDLE_PRIMARY_DEVICE;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function shouldShowIdlePrimaryDeviceAlert(
|
|
alertInfo: ServerAlertsType[ServerAlert.IDLE_PRIMARY_DEVICE]
|
|
): boolean {
|
|
if (!alertInfo) {
|
|
return false;
|
|
}
|
|
|
|
if (alertInfo.dismissedAt && isMoreRecentThan(alertInfo.dismissedAt, WEEK)) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
let criticalAlertModalTimeout: NodeJS.Timeout | null = null;
|
|
const DELAY_BEFORE_SHOWING_MODAL_FIRST_TIME = 3 * DAY;
|
|
const DELAY_BEFORE_SHOWING_MODAL_SUBSEQUENTLY = DAY;
|
|
function maybeShowCriticalIdlePrimaryDeviceModal(
|
|
alertInfo: ServerAlertsType[ServerAlert.CRITICAL_IDLE_PRIMARY_DEVICE]
|
|
) {
|
|
clearTimeoutIfNecessary(criticalAlertModalTimeout);
|
|
criticalAlertModalTimeout = null;
|
|
|
|
if (!alertInfo) {
|
|
return;
|
|
}
|
|
|
|
const { firstReceivedAt, modalLastDismissedAt } = alertInfo;
|
|
|
|
let nextModalShowsAt: number | undefined;
|
|
|
|
if (
|
|
isMoreRecentThan(firstReceivedAt, DELAY_BEFORE_SHOWING_MODAL_FIRST_TIME)
|
|
) {
|
|
nextModalShowsAt = firstReceivedAt + DELAY_BEFORE_SHOWING_MODAL_FIRST_TIME;
|
|
} else if (modalLastDismissedAt == null) {
|
|
nextModalShowsAt = Date.now();
|
|
} else {
|
|
nextModalShowsAt =
|
|
modalLastDismissedAt + DELAY_BEFORE_SHOWING_MODAL_SUBSEQUENTLY;
|
|
}
|
|
|
|
criticalAlertModalTimeout = safeSetTimeout(
|
|
() => window.reduxActions.globalModals.showCriticalIdlePrimaryDeviceModal(),
|
|
nextModalShowsAt - Date.now()
|
|
);
|
|
}
|
|
|
|
export async function onCriticalIdlePrimaryDeviceModalDismissed(): Promise<void> {
|
|
const existingAlerts = window.storage.get('serverAlerts') ?? {};
|
|
const existingAlert =
|
|
existingAlerts[ServerAlert.CRITICAL_IDLE_PRIMARY_DEVICE];
|
|
|
|
if (!existingAlert) {
|
|
log.warn(
|
|
'Critical idle primary device modal shown but the alert is not present'
|
|
);
|
|
return;
|
|
}
|
|
|
|
const newAlert = {
|
|
...existingAlert,
|
|
modalLastDismissedAt: Date.now(),
|
|
};
|
|
|
|
await window.storage.put('serverAlerts', {
|
|
...existingAlerts,
|
|
[ServerAlert.CRITICAL_IDLE_PRIMARY_DEVICE]: newAlert,
|
|
});
|
|
|
|
maybeShowCriticalIdlePrimaryDeviceModal(newAlert);
|
|
}
|