Support idle primary device warning alert from server
This commit is contained in:
@@ -6683,6 +6683,14 @@
|
|||||||
"messageformat": "Your account will be deleted soon unless you open Signal on your phone. This message will go away if you've done it successfully. <learnMoreLink>Learn more</learnMoreLink>",
|
"messageformat": "Your account will be deleted soon unless you open Signal on your phone. This message will go away if you've done it successfully. <learnMoreLink>Learn more</learnMoreLink>",
|
||||||
"description": "The text in a banner alerting users if their primary device (e.g. phone) has not been logged into recently. "
|
"description": "The text in a banner alerting users if their primary device (e.g. phone) has not been logged into recently. "
|
||||||
},
|
},
|
||||||
|
"icu:IdlePrimaryDevice__body": {
|
||||||
|
"messageformat": "Open Signal on your phone to keep your account active",
|
||||||
|
"description": "The text in a banner alerting users if their primary device (e.g. phone) has not been logged into recently. "
|
||||||
|
},
|
||||||
|
"icu:IdlePrimaryDevice__learnMore": {
|
||||||
|
"messageformat": "Learn more",
|
||||||
|
"description": "The text in a link that will open a support page URL with more information"
|
||||||
|
},
|
||||||
"icu:DialogNetworkStatus__outage": {
|
"icu:DialogNetworkStatus__outage": {
|
||||||
"messageformat": "Signal is experiencing technical difficulties. We are working hard to restore service as quickly as possible.",
|
"messageformat": "Signal is experiencing technical difficulties. We are working hard to restore service as quickly as possible.",
|
||||||
"description": "The title of outage dialog during service outage."
|
"description": "The title of outage dialog during service outage."
|
||||||
|
@@ -21,7 +21,6 @@
|
|||||||
$warning-background-color: variables.$color-accent-yellow;
|
$warning-background-color: variables.$color-accent-yellow;
|
||||||
$warning-text-color: variables.$color-black;
|
$warning-text-color: variables.$color-black;
|
||||||
|
|
||||||
align-items: center;
|
|
||||||
background: $default-background-color;
|
background: $default-background-color;
|
||||||
color: $default-text-color;
|
color: $default-text-color;
|
||||||
cursor: inherit;
|
cursor: inherit;
|
||||||
@@ -37,10 +36,6 @@
|
|||||||
letter-spacing: -0.0025em;
|
letter-spacing: -0.0025em;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
|
|
||||||
&--width-narrow {
|
|
||||||
padding-inline-start: 36px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__retry {
|
&__retry {
|
||||||
@include mixins.button-reset;
|
@include mixins.button-reset;
|
||||||
& {
|
& {
|
||||||
@@ -58,6 +53,10 @@
|
|||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&--width-narrow &__container {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
&__container-close {
|
&__container-close {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
@@ -82,10 +81,21 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__icon {
|
&__icon-container {
|
||||||
width: 24px;
|
width: 24px;
|
||||||
height: 24px;
|
height: 24px;
|
||||||
|
flex-shrink: 0;
|
||||||
margin-inline-end: 18px;
|
margin-inline-end: 18px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--width-narrow &__icon-container {
|
||||||
|
margin-inline-end: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__icon {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
background-color: variables.$color-white;
|
background-color: variables.$color-white;
|
||||||
-webkit-mask-size: contain;
|
-webkit-mask-size: contain;
|
||||||
|
|
||||||
@@ -119,6 +129,25 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__icon-background {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 50%;
|
||||||
|
outline-width: 5px;
|
||||||
|
outline-offset: -1px; // avoids a gap between background-color and outline
|
||||||
|
outline-style: solid;
|
||||||
|
&--warning {
|
||||||
|
outline-color: $warning-background-color;
|
||||||
|
background-color: $warning-background-color;
|
||||||
|
.LeftPaneDialog__icon {
|
||||||
|
background-color: $warning-text-color;
|
||||||
|
@media (forced-colors: active) {
|
||||||
|
background-color: WindowText;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&__action-text {
|
&__action-text {
|
||||||
@include mixins.button-reset;
|
@include mixins.button-reset;
|
||||||
& {
|
& {
|
||||||
@@ -227,6 +256,46 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&--info {
|
||||||
|
width: unset;
|
||||||
|
margin-inline: 10px;
|
||||||
|
margin-block-end: 6px;
|
||||||
|
margin-block-start: 2px;
|
||||||
|
border-radius: 10px;
|
||||||
|
color: inherit;
|
||||||
|
|
||||||
|
@include mixins.light-theme {
|
||||||
|
background-color: variables.$color-white;
|
||||||
|
--tooltip-background-color: variables.$color-white;
|
||||||
|
border: 1px solid variables.$color-gray-20;
|
||||||
|
}
|
||||||
|
@include mixins.dark-theme {
|
||||||
|
background: variables.$color-gray-75;
|
||||||
|
border: 1px solid variables.$color-gray-60;
|
||||||
|
}
|
||||||
|
.LeftPaneDialog__close-button::before {
|
||||||
|
@include mixins.light-theme {
|
||||||
|
background-color: variables.$color-gray-45;
|
||||||
|
}
|
||||||
|
@include mixins.dark-theme {
|
||||||
|
background-color: variables.$color-gray-20;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&--info &__action-text {
|
||||||
|
@include mixins.button-reset;
|
||||||
|
& {
|
||||||
|
@include mixins.font-subtitle-bold;
|
||||||
|
@include mixins.light-theme {
|
||||||
|
color: variables.$color-ultramarine;
|
||||||
|
}
|
||||||
|
@include mixins.dark-theme {
|
||||||
|
color: variables.$color-ultramarine-light;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&--warning {
|
&--warning {
|
||||||
background-color: $warning-background-color;
|
background-color: $warning-background-color;
|
||||||
color: $warning-text-color;
|
color: $warning-text-color;
|
||||||
|
@@ -1930,7 +1930,7 @@ export async function startApp(): Promise<void> {
|
|||||||
log.info('afterAuthSocketConnect/afterEveryAuthConnect');
|
log.info('afterAuthSocketConnect/afterEveryAuthConnect');
|
||||||
|
|
||||||
strictAssert(server, 'afterEveryAuthConnect: server');
|
strictAssert(server, 'afterEveryAuthConnect: server');
|
||||||
handleServerAlerts(server.getServerAlerts());
|
drop(handleServerAlerts(server.getServerAlerts()));
|
||||||
|
|
||||||
strictAssert(challengeHandler, 'afterEveryAuthConnect: challengeHandler');
|
strictAssert(challengeHandler, 'afterEveryAuthConnect: challengeHandler');
|
||||||
drop(challengeHandler.onOnline());
|
drop(challengeHandler.onOnline());
|
||||||
|
@@ -33,7 +33,7 @@ import {
|
|||||||
useUuidFetchState,
|
useUuidFetchState,
|
||||||
} from '../test-both/helpers/fakeLookupConversationWithoutServiceId';
|
} from '../test-both/helpers/fakeLookupConversationWithoutServiceId';
|
||||||
import type { GroupListItemConversationType } from './conversationList/GroupListItem';
|
import type { GroupListItemConversationType } from './conversationList/GroupListItem';
|
||||||
import { CriticalIdlePrimaryDeviceDialog } from './CriticalIdlePrimaryDeviceDialog';
|
import { ServerAlert } from '../util/handleServerAlerts';
|
||||||
|
|
||||||
const { i18n } = window.SignalContext;
|
const { i18n } = window.SignalContext;
|
||||||
|
|
||||||
@@ -172,7 +172,6 @@ const useProps = (overrideProps: OverridePropsType = {}): PropsType => {
|
|||||||
getPreferredBadge: () => undefined,
|
getPreferredBadge: () => undefined,
|
||||||
hasFailedStorySends: false,
|
hasFailedStorySends: false,
|
||||||
hasPendingUpdate: false,
|
hasPendingUpdate: false,
|
||||||
hasCriticalIdlePrimaryDeviceAlert: false,
|
|
||||||
i18n,
|
i18n,
|
||||||
isMacOS: false,
|
isMacOS: false,
|
||||||
preferredWidthFromStorage: 320,
|
preferredWidthFromStorage: 320,
|
||||||
@@ -271,9 +270,6 @@ const useProps = (overrideProps: OverridePropsType = {}): PropsType => {
|
|||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
renderExpiredBuildDialog: props => <DialogExpiredBuild {...props} />,
|
renderExpiredBuildDialog: props => <DialogExpiredBuild {...props} />,
|
||||||
renderCriticalIdlePrimaryDeviceDialog: props => (
|
|
||||||
<CriticalIdlePrimaryDeviceDialog {...props} />
|
|
||||||
),
|
|
||||||
renderUnsupportedOSDialog: props => (
|
renderUnsupportedOSDialog: props => (
|
||||||
<UnsupportedOSDialog
|
<UnsupportedOSDialog
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
@@ -400,7 +396,38 @@ export function InboxCriticalIdlePrimaryDeviceAlert(): JSX.Element {
|
|||||||
return (
|
return (
|
||||||
<LeftPaneInContainer
|
<LeftPaneInContainer
|
||||||
{...useProps({
|
{...useProps({
|
||||||
hasCriticalIdlePrimaryDeviceAlert: true,
|
serverAlerts: {
|
||||||
|
[ServerAlert.CRITICAL_IDLE_PRIMARY_DEVICE]: {
|
||||||
|
firstReceivedAt: Date.now(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
export function InboxIdlePrimaryDeviceAlert(): JSX.Element {
|
||||||
|
return (
|
||||||
|
<LeftPaneInContainer
|
||||||
|
{...useProps({
|
||||||
|
serverAlerts: {
|
||||||
|
[ServerAlert.IDLE_PRIMARY_DEVICE]: {
|
||||||
|
firstReceivedAt: Date.now(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
export function InboxIdlePrimaryDeviceAlertNonDismissable(): JSX.Element {
|
||||||
|
return (
|
||||||
|
<LeftPaneInContainer
|
||||||
|
{...useProps({
|
||||||
|
serverAlerts: {
|
||||||
|
[ServerAlert.IDLE_PRIMARY_DEVICE]: {
|
||||||
|
firstReceivedAt: Date.now() - 10 * DAY,
|
||||||
|
dismissedAt: Date.now() - 8 * DAY,
|
||||||
|
},
|
||||||
|
},
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@@ -57,6 +57,8 @@ import { ContextMenu } from './ContextMenu';
|
|||||||
import { EditState as ProfileEditorEditState } from './ProfileEditor';
|
import { EditState as ProfileEditorEditState } from './ProfileEditor';
|
||||||
import type { UnreadStats } from '../util/countUnreadStats';
|
import type { UnreadStats } from '../util/countUnreadStats';
|
||||||
import { BackupMediaDownloadProgress } from './BackupMediaDownloadProgress';
|
import { BackupMediaDownloadProgress } from './BackupMediaDownloadProgress';
|
||||||
|
import type { ServerAlertsType } from '../util/handleServerAlerts';
|
||||||
|
import { getServerAlertDialog } from './ServerAlerts';
|
||||||
|
|
||||||
export type PropsType = {
|
export type PropsType = {
|
||||||
backupMediaDownloadProgress: {
|
backupMediaDownloadProgress: {
|
||||||
@@ -69,7 +71,6 @@ export type PropsType = {
|
|||||||
otherTabsUnreadStats: UnreadStats;
|
otherTabsUnreadStats: UnreadStats;
|
||||||
hasExpiredDialog: boolean;
|
hasExpiredDialog: boolean;
|
||||||
hasFailedStorySends: boolean;
|
hasFailedStorySends: boolean;
|
||||||
hasCriticalIdlePrimaryDeviceAlert: boolean;
|
|
||||||
hasNetworkDialog: boolean;
|
hasNetworkDialog: boolean;
|
||||||
hasPendingUpdate: boolean;
|
hasPendingUpdate: boolean;
|
||||||
hasRelinkDialog: boolean;
|
hasRelinkDialog: boolean;
|
||||||
@@ -147,6 +148,7 @@ export type PropsType = {
|
|||||||
setComposeGroupName: (_: string) => void;
|
setComposeGroupName: (_: string) => void;
|
||||||
setComposeSearchTerm: (composeSearchTerm: string) => void;
|
setComposeSearchTerm: (composeSearchTerm: string) => void;
|
||||||
setComposeSelectedRegion: (newRegion: string) => void;
|
setComposeSelectedRegion: (newRegion: string) => void;
|
||||||
|
serverAlerts?: ServerAlertsType;
|
||||||
showArchivedConversations: () => void;
|
showArchivedConversations: () => void;
|
||||||
showChooseGroupMembers: () => void;
|
showChooseGroupMembers: () => void;
|
||||||
showFindByUsername: () => void;
|
showFindByUsername: () => void;
|
||||||
@@ -181,12 +183,6 @@ export type PropsType = {
|
|||||||
renderCaptchaDialog: (props: { onSkip(): void }) => JSX.Element;
|
renderCaptchaDialog: (props: { onSkip(): void }) => JSX.Element;
|
||||||
renderCrashReportDialog: () => JSX.Element;
|
renderCrashReportDialog: () => JSX.Element;
|
||||||
renderExpiredBuildDialog: (_: DialogExpiredBuildPropsType) => JSX.Element;
|
renderExpiredBuildDialog: (_: DialogExpiredBuildPropsType) => JSX.Element;
|
||||||
renderCriticalIdlePrimaryDeviceDialog: (
|
|
||||||
_: Readonly<{
|
|
||||||
containerWidthBreakpoint: WidthBreakpoint;
|
|
||||||
i18n: LocalizerType;
|
|
||||||
}>
|
|
||||||
) => JSX.Element;
|
|
||||||
renderToastManager: (_: {
|
renderToastManager: (_: {
|
||||||
containerWidthBreakpoint: WidthBreakpoint;
|
containerWidthBreakpoint: WidthBreakpoint;
|
||||||
}) => JSX.Element;
|
}) => JSX.Element;
|
||||||
@@ -213,7 +209,6 @@ export function LeftPane({
|
|||||||
getPreferredBadge,
|
getPreferredBadge,
|
||||||
hasExpiredDialog,
|
hasExpiredDialog,
|
||||||
hasFailedStorySends,
|
hasFailedStorySends,
|
||||||
hasCriticalIdlePrimaryDeviceAlert,
|
|
||||||
hasNetworkDialog,
|
hasNetworkDialog,
|
||||||
hasPendingUpdate,
|
hasPendingUpdate,
|
||||||
hasRelinkDialog,
|
hasRelinkDialog,
|
||||||
@@ -235,7 +230,6 @@ export function LeftPane({
|
|||||||
renderCaptchaDialog,
|
renderCaptchaDialog,
|
||||||
renderCrashReportDialog,
|
renderCrashReportDialog,
|
||||||
renderExpiredBuildDialog,
|
renderExpiredBuildDialog,
|
||||||
renderCriticalIdlePrimaryDeviceDialog,
|
|
||||||
renderMessageSearchResult,
|
renderMessageSearchResult,
|
||||||
renderNetworkStatus,
|
renderNetworkStatus,
|
||||||
renderUnsupportedOSDialog,
|
renderUnsupportedOSDialog,
|
||||||
@@ -263,6 +257,7 @@ export function LeftPane({
|
|||||||
showConversation,
|
showConversation,
|
||||||
showInbox,
|
showInbox,
|
||||||
showUserNotFoundModal,
|
showUserNotFoundModal,
|
||||||
|
serverAlerts,
|
||||||
startComposing,
|
startComposing,
|
||||||
startSearch,
|
startSearch,
|
||||||
startSettingGroupMetadata,
|
startSettingGroupMetadata,
|
||||||
@@ -608,6 +603,10 @@ export function LeftPane({
|
|||||||
scrollBehavior = ScrollBehavior.Hard;
|
scrollBehavior = ScrollBehavior.Hard;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const maybeServerAlert = getServerAlertDialog(
|
||||||
|
serverAlerts,
|
||||||
|
commonDialogProps
|
||||||
|
);
|
||||||
// Yellow dialogs
|
// Yellow dialogs
|
||||||
let maybeYellowDialog: JSX.Element | undefined;
|
let maybeYellowDialog: JSX.Element | undefined;
|
||||||
|
|
||||||
@@ -620,9 +619,8 @@ export function LeftPane({
|
|||||||
maybeYellowDialog = renderNetworkStatus(commonDialogProps);
|
maybeYellowDialog = renderNetworkStatus(commonDialogProps);
|
||||||
} else if (hasRelinkDialog) {
|
} else if (hasRelinkDialog) {
|
||||||
maybeYellowDialog = renderRelinkDialog(commonDialogProps);
|
maybeYellowDialog = renderRelinkDialog(commonDialogProps);
|
||||||
} else if (hasCriticalIdlePrimaryDeviceAlert) {
|
} else if (maybeServerAlert) {
|
||||||
maybeYellowDialog =
|
maybeYellowDialog = maybeServerAlert;
|
||||||
renderCriticalIdlePrimaryDeviceDialog(commonDialogProps);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update dialog
|
// Update dialog
|
||||||
|
@@ -88,6 +88,13 @@ export const Warning = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const Info = {
|
||||||
|
args: {
|
||||||
|
type: 'info',
|
||||||
|
icon: 'error',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export const Error = {
|
export const Error = {
|
||||||
args: {
|
args: {
|
||||||
type: 'error',
|
type: 'error',
|
||||||
@@ -125,6 +132,14 @@ export const NarrowWarning = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const NarrowInfo = {
|
||||||
|
args: {
|
||||||
|
type: 'info',
|
||||||
|
icon: 'warning',
|
||||||
|
containerWidthBreakpoint: WidthBreakpoint.Narrow,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export const NarrowError = {
|
export const NarrowError = {
|
||||||
args: {
|
args: {
|
||||||
type: 'error',
|
type: 'error',
|
||||||
|
@@ -11,8 +11,8 @@ const BASE_CLASS_NAME = 'LeftPaneDialog';
|
|||||||
const TOOLTIP_CLASS_NAME = `${BASE_CLASS_NAME}__tooltip`;
|
const TOOLTIP_CLASS_NAME = `${BASE_CLASS_NAME}__tooltip`;
|
||||||
|
|
||||||
export type PropsType = {
|
export type PropsType = {
|
||||||
type?: 'warning' | 'error';
|
type?: 'warning' | 'error' | 'info';
|
||||||
icon?: 'update' | 'relink' | 'network' | 'warning' | 'error' | ReactChild;
|
icon?: 'update' | 'relink' | 'network' | 'warning' | 'error' | JSX.Element;
|
||||||
title?: string;
|
title?: string;
|
||||||
subtitle?: string;
|
subtitle?: string;
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
@@ -84,14 +84,6 @@ export function LeftPaneDialog({
|
|||||||
onClose?.();
|
onClose?.();
|
||||||
};
|
};
|
||||||
|
|
||||||
const iconClassName =
|
|
||||||
typeof icon === 'string'
|
|
||||||
? classNames([
|
|
||||||
`${BASE_CLASS_NAME}__icon`,
|
|
||||||
`${BASE_CLASS_NAME}__icon--${icon}`,
|
|
||||||
])
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
let action: ReactNode;
|
let action: ReactNode;
|
||||||
if (hasAction) {
|
if (hasAction) {
|
||||||
action = (
|
action = (
|
||||||
@@ -139,11 +131,18 @@ export function LeftPaneDialog({
|
|||||||
{action}
|
{action}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
const content = (
|
const content = (
|
||||||
<>
|
<>
|
||||||
<div className={`${BASE_CLASS_NAME}__container`}>
|
<div className={`${BASE_CLASS_NAME}__container`}>
|
||||||
{typeof icon === 'string' ? <div className={iconClassName} /> : icon}
|
{icon ? (
|
||||||
|
<div className={`${BASE_CLASS_NAME}__icon-container`}>
|
||||||
|
{typeof icon === 'string' ? (
|
||||||
|
<LeftPaneDialogIcon type={icon} />
|
||||||
|
) : (
|
||||||
|
icon
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
{containerWidthBreakpoint !== WidthBreakpoint.Narrow && (
|
{containerWidthBreakpoint !== WidthBreakpoint.Narrow && (
|
||||||
<div className={`${BASE_CLASS_NAME}__message`}>{message}</div>
|
<div className={`${BASE_CLASS_NAME}__message`}>{message}</div>
|
||||||
)}
|
)}
|
||||||
@@ -192,3 +191,31 @@ export function LeftPaneDialog({
|
|||||||
|
|
||||||
return dialogNode;
|
return dialogNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function LeftPaneDialogIcon({
|
||||||
|
type,
|
||||||
|
}: {
|
||||||
|
type?: 'update' | 'relink' | 'network' | 'warning' | 'error';
|
||||||
|
}): JSX.Element {
|
||||||
|
const iconClassName = classNames([
|
||||||
|
`${BASE_CLASS_NAME}__icon`,
|
||||||
|
`${BASE_CLASS_NAME}__icon--${type}`,
|
||||||
|
]);
|
||||||
|
return <div className={iconClassName} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LeftPaneDialogIconBackground({
|
||||||
|
type,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
type?: 'warning';
|
||||||
|
children: React.ReactNode;
|
||||||
|
}): JSX.Element {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`${BASE_CLASS_NAME}__icon-background ${BASE_CLASS_NAME}__icon-background--${type}`}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
63
ts/components/ServerAlerts.tsx
Normal file
63
ts/components/ServerAlerts.tsx
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
// Copyright 2025 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
getServerAlertToShow,
|
||||||
|
ServerAlert,
|
||||||
|
type ServerAlertsType,
|
||||||
|
} from '../util/handleServerAlerts';
|
||||||
|
import type { WidthBreakpoint } from './_util';
|
||||||
|
import type { LocalizerType } from '../types/I18N';
|
||||||
|
import { CriticalIdlePrimaryDeviceDialog } from './CriticalIdlePrimaryDeviceDialog';
|
||||||
|
import { strictAssert } from '../util/assert';
|
||||||
|
import { WarningIdlePrimaryDeviceDialog } from './WarningIdlePrimaryDeviceDialog';
|
||||||
|
|
||||||
|
export function getServerAlertDialog(
|
||||||
|
alerts: ServerAlertsType | undefined,
|
||||||
|
dialogProps: {
|
||||||
|
containerWidthBreakpoint: WidthBreakpoint;
|
||||||
|
i18n: LocalizerType;
|
||||||
|
}
|
||||||
|
): JSX.Element | null {
|
||||||
|
if (!alerts) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const alertToShow = getServerAlertToShow(alerts);
|
||||||
|
if (!alertToShow) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (alertToShow === ServerAlert.CRITICAL_IDLE_PRIMARY_DEVICE) {
|
||||||
|
return <CriticalIdlePrimaryDeviceDialog {...dialogProps} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (alertToShow === ServerAlert.IDLE_PRIMARY_DEVICE) {
|
||||||
|
const alert = alerts[ServerAlert.IDLE_PRIMARY_DEVICE];
|
||||||
|
strictAssert(alert, 'alert must exist');
|
||||||
|
|
||||||
|
// Only allow dismissing it once
|
||||||
|
const isDismissable = alert.dismissedAt == null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<WarningIdlePrimaryDeviceDialog
|
||||||
|
{...dialogProps}
|
||||||
|
handleClose={
|
||||||
|
isDismissable
|
||||||
|
? async () => {
|
||||||
|
await window.storage.put('serverAlerts', {
|
||||||
|
...alerts,
|
||||||
|
[ServerAlert.IDLE_PRIMARY_DEVICE]: {
|
||||||
|
...alert,
|
||||||
|
dismissedAt: Date.now(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
52
ts/components/WarningIdlePrimaryDeviceDialog.tsx
Normal file
52
ts/components/WarningIdlePrimaryDeviceDialog.tsx
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
// Copyright 2025 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
LeftPaneDialog,
|
||||||
|
LeftPaneDialogIcon,
|
||||||
|
LeftPaneDialogIconBackground,
|
||||||
|
} from './LeftPaneDialog';
|
||||||
|
import type { WidthBreakpoint } from './_util';
|
||||||
|
import type { LocalizerType } from '../types/I18N';
|
||||||
|
|
||||||
|
export type Props = {
|
||||||
|
containerWidthBreakpoint: WidthBreakpoint;
|
||||||
|
i18n: LocalizerType;
|
||||||
|
};
|
||||||
|
|
||||||
|
const SUPPORT_PAGE =
|
||||||
|
'https://support.signal.org/hc/articles/9021007554074-Open-Signal-on-your-phone-to-keep-your-account-active';
|
||||||
|
|
||||||
|
export function WarningIdlePrimaryDeviceDialog({
|
||||||
|
containerWidthBreakpoint,
|
||||||
|
i18n,
|
||||||
|
handleClose,
|
||||||
|
}: Props & { handleClose?: VoidFunction }): JSX.Element {
|
||||||
|
return (
|
||||||
|
<LeftPaneDialog
|
||||||
|
containerWidthBreakpoint={containerWidthBreakpoint}
|
||||||
|
type="info"
|
||||||
|
icon={
|
||||||
|
<LeftPaneDialogIconBackground type="warning">
|
||||||
|
<LeftPaneDialogIcon type="error" />
|
||||||
|
</LeftPaneDialogIconBackground>
|
||||||
|
}
|
||||||
|
{...(handleClose == null
|
||||||
|
? { hasXButton: false }
|
||||||
|
: {
|
||||||
|
hasXButton: true,
|
||||||
|
onClose: handleClose,
|
||||||
|
closeLabel: i18n('icu:close'),
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{i18n('icu:IdlePrimaryDevice__body')}
|
||||||
|
<div>
|
||||||
|
<a href={SUPPORT_PAGE} rel="noreferrer" target="_blank">
|
||||||
|
{i18n('icu:IdlePrimaryDevice__learnMore')}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</LeftPaneDialog>
|
||||||
|
);
|
||||||
|
}
|
@@ -23,7 +23,6 @@ import { actions as mediaGallery } from './ducks/mediaGallery';
|
|||||||
import { actions as network } from './ducks/network';
|
import { actions as network } from './ducks/network';
|
||||||
import { actions as safetyNumber } from './ducks/safetyNumber';
|
import { actions as safetyNumber } from './ducks/safetyNumber';
|
||||||
import { actions as search } from './ducks/search';
|
import { actions as search } from './ducks/search';
|
||||||
import { actions as server } from './ducks/server';
|
|
||||||
import { actions as stickers } from './ducks/stickers';
|
import { actions as stickers } from './ducks/stickers';
|
||||||
import { actions as stories } from './ducks/stories';
|
import { actions as stories } from './ducks/stories';
|
||||||
import { actions as storyDistributionLists } from './ducks/storyDistributionLists';
|
import { actions as storyDistributionLists } from './ducks/storyDistributionLists';
|
||||||
@@ -56,7 +55,6 @@ export const actionCreators: ReduxActions = {
|
|||||||
network,
|
network,
|
||||||
safetyNumber,
|
safetyNumber,
|
||||||
search,
|
search,
|
||||||
server,
|
|
||||||
stickers,
|
stickers,
|
||||||
stories,
|
stories,
|
||||||
storyDistributionLists,
|
storyDistributionLists,
|
||||||
|
@@ -1,69 +0,0 @@
|
|||||||
// Copyright 2020 Signal Messenger, LLC
|
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
|
|
||||||
import type { ReadonlyDeep } from 'type-fest';
|
|
||||||
|
|
||||||
// State
|
|
||||||
|
|
||||||
export enum ServerAlert {
|
|
||||||
CRITICAL_IDLE_PRIMARY_DEVICE = 'critical_idle_primary_device',
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ServerStateType = ReadonlyDeep<{
|
|
||||||
alerts: Array<ServerAlert>;
|
|
||||||
}>;
|
|
||||||
|
|
||||||
export function parseServerAlertFromHeader(
|
|
||||||
headerValue: string
|
|
||||||
): ServerAlert | undefined {
|
|
||||||
if (headerValue.toLowerCase() === 'critical-idle-primary-device') {
|
|
||||||
return ServerAlert.CRITICAL_IDLE_PRIMARY_DEVICE;
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Actions
|
|
||||||
|
|
||||||
const UPDATE_SERVER_ALERTS = 'server/UPDATE_SERVER_ALERTS';
|
|
||||||
|
|
||||||
type UpdateServerAlertsType = ReadonlyDeep<{
|
|
||||||
type: 'server/UPDATE_SERVER_ALERTS';
|
|
||||||
payload: { alerts: Array<ServerAlert> };
|
|
||||||
}>;
|
|
||||||
|
|
||||||
export type ServerActionType = ReadonlyDeep<UpdateServerAlertsType>;
|
|
||||||
|
|
||||||
// Action Creators
|
|
||||||
|
|
||||||
function updateServerAlerts(alerts: Array<ServerAlert>): ServerActionType {
|
|
||||||
return {
|
|
||||||
type: UPDATE_SERVER_ALERTS,
|
|
||||||
payload: { alerts },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export const actions = {
|
|
||||||
updateServerAlerts,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Reducer
|
|
||||||
|
|
||||||
export function getEmptyState(): ServerStateType {
|
|
||||||
return {
|
|
||||||
alerts: [],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function reducer(
|
|
||||||
state: Readonly<ServerStateType> = getEmptyState(),
|
|
||||||
action: Readonly<ServerActionType>
|
|
||||||
): ServerStateType {
|
|
||||||
if (action.type === UPDATE_SERVER_ALERTS) {
|
|
||||||
return {
|
|
||||||
alerts: action.payload.alerts,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return state;
|
|
||||||
}
|
|
@@ -25,7 +25,6 @@ import { getEmptyState as networkEmptyState } from './ducks/network';
|
|||||||
import { getEmptyState as preferredReactionsEmptyState } from './ducks/preferredReactions';
|
import { getEmptyState as preferredReactionsEmptyState } from './ducks/preferredReactions';
|
||||||
import { getEmptyState as safetyNumberEmptyState } from './ducks/safetyNumber';
|
import { getEmptyState as safetyNumberEmptyState } from './ducks/safetyNumber';
|
||||||
import { getEmptyState as searchEmptyState } from './ducks/search';
|
import { getEmptyState as searchEmptyState } from './ducks/search';
|
||||||
import { getEmptyState as serverEmptyState } from './ducks/server';
|
|
||||||
import { getEmptyState as stickersEmptyState } from './ducks/stickers';
|
import { getEmptyState as stickersEmptyState } from './ducks/stickers';
|
||||||
import { getEmptyState as storiesEmptyState } from './ducks/stories';
|
import { getEmptyState as storiesEmptyState } from './ducks/stories';
|
||||||
import { getEmptyState as storyDistributionListsEmptyState } from './ducks/storyDistributionLists';
|
import { getEmptyState as storyDistributionListsEmptyState } from './ducks/storyDistributionLists';
|
||||||
@@ -145,7 +144,6 @@ function getEmptyState(): StateType {
|
|||||||
preferredReactions: preferredReactionsEmptyState(),
|
preferredReactions: preferredReactionsEmptyState(),
|
||||||
safetyNumber: safetyNumberEmptyState(),
|
safetyNumber: safetyNumberEmptyState(),
|
||||||
search: searchEmptyState(),
|
search: searchEmptyState(),
|
||||||
server: serverEmptyState(),
|
|
||||||
stickers: stickersEmptyState(),
|
stickers: stickersEmptyState(),
|
||||||
stories: storiesEmptyState(),
|
stories: storiesEmptyState(),
|
||||||
storyDistributionLists: storyDistributionListsEmptyState(),
|
storyDistributionLists: storyDistributionListsEmptyState(),
|
||||||
|
@@ -83,7 +83,6 @@ export function initializeRedux(data: ReduxInitData): void {
|
|||||||
store.dispatch
|
store.dispatch
|
||||||
),
|
),
|
||||||
search: bindActionCreators(actionCreators.search, store.dispatch),
|
search: bindActionCreators(actionCreators.search, store.dispatch),
|
||||||
server: bindActionCreators(actionCreators.server, store.dispatch),
|
|
||||||
stickers: bindActionCreators(actionCreators.stickers, store.dispatch),
|
stickers: bindActionCreators(actionCreators.stickers, store.dispatch),
|
||||||
stories: bindActionCreators(actionCreators.stories, store.dispatch),
|
stories: bindActionCreators(actionCreators.stories, store.dispatch),
|
||||||
storyDistributionLists: bindActionCreators(
|
storyDistributionLists: bindActionCreators(
|
||||||
|
@@ -27,7 +27,6 @@ import { reducer as network } from './ducks/network';
|
|||||||
import { reducer as preferredReactions } from './ducks/preferredReactions';
|
import { reducer as preferredReactions } from './ducks/preferredReactions';
|
||||||
import { reducer as safetyNumber } from './ducks/safetyNumber';
|
import { reducer as safetyNumber } from './ducks/safetyNumber';
|
||||||
import { reducer as search } from './ducks/search';
|
import { reducer as search } from './ducks/search';
|
||||||
import { reducer as server } from './ducks/server';
|
|
||||||
import { reducer as stickers } from './ducks/stickers';
|
import { reducer as stickers } from './ducks/stickers';
|
||||||
import { reducer as stories } from './ducks/stories';
|
import { reducer as stories } from './ducks/stories';
|
||||||
import { reducer as storyDistributionLists } from './ducks/storyDistributionLists';
|
import { reducer as storyDistributionLists } from './ducks/storyDistributionLists';
|
||||||
@@ -61,7 +60,6 @@ export const reducer = combineReducers({
|
|||||||
preferredReactions,
|
preferredReactions,
|
||||||
safetyNumber,
|
safetyNumber,
|
||||||
search,
|
search,
|
||||||
server,
|
|
||||||
stickers,
|
stickers,
|
||||||
stories,
|
stories,
|
||||||
storyDistributionLists,
|
storyDistributionLists,
|
||||||
|
@@ -273,3 +273,8 @@ export const getBackupMediaDownloadProgress = createSelector(
|
|||||||
downloadBannerDismissed: state.backupMediaDownloadBannerDismissed ?? false,
|
downloadBannerDismissed: state.backupMediaDownloadBannerDismissed ?? false,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const getServerAlerts = createSelector(
|
||||||
|
getItems,
|
||||||
|
(state: ItemsStateType) => state.serverAlerts ?? {}
|
||||||
|
);
|
||||||
|
@@ -1,17 +0,0 @@
|
|||||||
// Copyright 2025 Signal Messenger, LLC
|
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
|
|
||||||
import type { StateType } from '../reducer';
|
|
||||||
import { ServerAlert } from '../ducks/server';
|
|
||||||
|
|
||||||
export const getServerAlerts = (state: StateType): ReadonlyArray<ServerAlert> =>
|
|
||||||
state.server.alerts;
|
|
||||||
|
|
||||||
export const getHasCriticalIdlePrimaryDeviceAlert = createSelector(
|
|
||||||
getServerAlerts,
|
|
||||||
(alerts): boolean => {
|
|
||||||
return alerts.includes(ServerAlert.CRITICAL_IDLE_PRIMARY_DEVICE);
|
|
||||||
}
|
|
||||||
);
|
|
@@ -60,6 +60,7 @@ import {
|
|||||||
getBackupMediaDownloadProgress,
|
getBackupMediaDownloadProgress,
|
||||||
getNavTabsCollapsed,
|
getNavTabsCollapsed,
|
||||||
getPreferredLeftPaneWidth,
|
getPreferredLeftPaneWidth,
|
||||||
|
getServerAlerts,
|
||||||
getUsernameCorrupted,
|
getUsernameCorrupted,
|
||||||
getUsernameLinkCorrupted,
|
getUsernameLinkCorrupted,
|
||||||
} from '../selectors/items';
|
} from '../selectors/items';
|
||||||
@@ -104,9 +105,6 @@ import {
|
|||||||
pauseBackupMediaDownload,
|
pauseBackupMediaDownload,
|
||||||
resumeBackupMediaDownload,
|
resumeBackupMediaDownload,
|
||||||
} from '../../util/backupMediaDownload';
|
} from '../../util/backupMediaDownload';
|
||||||
import { getHasCriticalIdlePrimaryDeviceAlert } from '../selectors/server';
|
|
||||||
import { CriticalIdlePrimaryDeviceDialog } from '../../components/CriticalIdlePrimaryDeviceDialog';
|
|
||||||
import type { LocalizerType } from '../../types/I18N';
|
|
||||||
|
|
||||||
function renderMessageSearchResult(id: string): JSX.Element {
|
function renderMessageSearchResult(id: string): JSX.Element {
|
||||||
return <SmartMessageSearchResult id={id} />;
|
return <SmartMessageSearchResult id={id} />;
|
||||||
@@ -126,14 +124,6 @@ function renderUpdateDialog(
|
|||||||
): JSX.Element {
|
): JSX.Element {
|
||||||
return <SmartUpdateDialog {...props} />;
|
return <SmartUpdateDialog {...props} />;
|
||||||
}
|
}
|
||||||
function renderCriticalIdlePrimaryDeviceDialog(
|
|
||||||
props: Readonly<{
|
|
||||||
containerWidthBreakpoint: WidthBreakpoint;
|
|
||||||
i18n: LocalizerType;
|
|
||||||
}>
|
|
||||||
): JSX.Element {
|
|
||||||
return <CriticalIdlePrimaryDeviceDialog {...props} />;
|
|
||||||
}
|
|
||||||
function renderCaptchaDialog({ onSkip }: { onSkip(): void }): JSX.Element {
|
function renderCaptchaDialog({ onSkip }: { onSkip(): void }): JSX.Element {
|
||||||
return <SmartCaptchaDialog onSkip={onSkip} />;
|
return <SmartCaptchaDialog onSkip={onSkip} />;
|
||||||
}
|
}
|
||||||
@@ -307,9 +297,8 @@ export const SmartLeftPane = memo(function SmartLeftPane({
|
|||||||
const backupMediaDownloadProgress = useSelector(
|
const backupMediaDownloadProgress = useSelector(
|
||||||
getBackupMediaDownloadProgress
|
getBackupMediaDownloadProgress
|
||||||
);
|
);
|
||||||
const hasCriticalIdlePrimaryDeviceAlert = useSelector(
|
|
||||||
getHasCriticalIdlePrimaryDeviceAlert
|
const serverAlerts = useSelector(getServerAlerts);
|
||||||
);
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
blockConversation,
|
blockConversation,
|
||||||
@@ -402,7 +391,6 @@ export const SmartLeftPane = memo(function SmartLeftPane({
|
|||||||
getPreferredBadge={getPreferredBadge}
|
getPreferredBadge={getPreferredBadge}
|
||||||
hasExpiredDialog={hasExpiredDialog}
|
hasExpiredDialog={hasExpiredDialog}
|
||||||
hasFailedStorySends={hasFailedStorySends}
|
hasFailedStorySends={hasFailedStorySends}
|
||||||
hasCriticalIdlePrimaryDeviceAlert={hasCriticalIdlePrimaryDeviceAlert}
|
|
||||||
hasNetworkDialog={hasNetworkDialog}
|
hasNetworkDialog={hasNetworkDialog}
|
||||||
hasPendingUpdate={hasPendingUpdate}
|
hasPendingUpdate={hasPendingUpdate}
|
||||||
hasRelinkDialog={hasRelinkDialog}
|
hasRelinkDialog={hasRelinkDialog}
|
||||||
@@ -424,9 +412,6 @@ export const SmartLeftPane = memo(function SmartLeftPane({
|
|||||||
renderCaptchaDialog={renderCaptchaDialog}
|
renderCaptchaDialog={renderCaptchaDialog}
|
||||||
renderCrashReportDialog={renderCrashReportDialog}
|
renderCrashReportDialog={renderCrashReportDialog}
|
||||||
renderExpiredBuildDialog={renderExpiredBuildDialog}
|
renderExpiredBuildDialog={renderExpiredBuildDialog}
|
||||||
renderCriticalIdlePrimaryDeviceDialog={
|
|
||||||
renderCriticalIdlePrimaryDeviceDialog
|
|
||||||
}
|
|
||||||
renderMessageSearchResult={renderMessageSearchResult}
|
renderMessageSearchResult={renderMessageSearchResult}
|
||||||
renderNetworkStatus={renderNetworkStatus}
|
renderNetworkStatus={renderNetworkStatus}
|
||||||
renderRelinkDialog={renderRelinkDialog}
|
renderRelinkDialog={renderRelinkDialog}
|
||||||
@@ -437,6 +422,7 @@ export const SmartLeftPane = memo(function SmartLeftPane({
|
|||||||
savePreferredLeftPaneWidth={savePreferredLeftPaneWidth}
|
savePreferredLeftPaneWidth={savePreferredLeftPaneWidth}
|
||||||
searchInConversation={searchInConversation}
|
searchInConversation={searchInConversation}
|
||||||
selectedConversationId={selectedConversationId}
|
selectedConversationId={selectedConversationId}
|
||||||
|
serverAlerts={serverAlerts}
|
||||||
setChallengeStatus={setChallengeStatus}
|
setChallengeStatus={setChallengeStatus}
|
||||||
setComposeGroupAvatar={setComposeGroupAvatar}
|
setComposeGroupAvatar={setComposeGroupAvatar}
|
||||||
setComposeGroupExpireTimer={setComposeGroupExpireTimer}
|
setComposeGroupExpireTimer={setComposeGroupExpireTimer}
|
||||||
|
@@ -23,7 +23,6 @@ import type { actions as mediaGallery } from './ducks/mediaGallery';
|
|||||||
import type { actions as network } from './ducks/network';
|
import type { actions as network } from './ducks/network';
|
||||||
import type { actions as safetyNumber } from './ducks/safetyNumber';
|
import type { actions as safetyNumber } from './ducks/safetyNumber';
|
||||||
import type { actions as search } from './ducks/search';
|
import type { actions as search } from './ducks/search';
|
||||||
import type { actions as server } from './ducks/server';
|
|
||||||
import type { actions as stickers } from './ducks/stickers';
|
import type { actions as stickers } from './ducks/stickers';
|
||||||
import type { actions as stories } from './ducks/stories';
|
import type { actions as stories } from './ducks/stories';
|
||||||
import type { actions as storyDistributionLists } from './ducks/storyDistributionLists';
|
import type { actions as storyDistributionLists } from './ducks/storyDistributionLists';
|
||||||
@@ -55,7 +54,6 @@ export type ReduxActions = {
|
|||||||
network: typeof network;
|
network: typeof network;
|
||||||
safetyNumber: typeof safetyNumber;
|
safetyNumber: typeof safetyNumber;
|
||||||
search: typeof search;
|
search: typeof search;
|
||||||
server: typeof server;
|
|
||||||
stickers: typeof stickers;
|
stickers: typeof stickers;
|
||||||
stories: typeof stories;
|
stories: typeof stories;
|
||||||
storyDistributionLists: typeof storyDistributionLists;
|
storyDistributionLists: typeof storyDistributionLists;
|
||||||
|
45
ts/test-both/util/serverAlerts_test.ts
Normal file
45
ts/test-both/util/serverAlerts_test.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
// Copyright 2025 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import { assert } from 'chai';
|
||||||
|
import {
|
||||||
|
getServerAlertToShow,
|
||||||
|
ServerAlert,
|
||||||
|
} from '../../util/handleServerAlerts';
|
||||||
|
import { DAY, MONTH, WEEK } from '../../util/durations';
|
||||||
|
|
||||||
|
describe('serverAlerts', () => {
|
||||||
|
it('should prefer critical alerts', () => {
|
||||||
|
assert.strictEqual(
|
||||||
|
getServerAlertToShow({
|
||||||
|
[ServerAlert.CRITICAL_IDLE_PRIMARY_DEVICE]: {
|
||||||
|
firstReceivedAt: Date.now(),
|
||||||
|
},
|
||||||
|
[ServerAlert.IDLE_PRIMARY_DEVICE]: { firstReceivedAt: Date.now() },
|
||||||
|
}),
|
||||||
|
ServerAlert.CRITICAL_IDLE_PRIMARY_DEVICE
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it('should not show idle device warning if dismissed < 1 week', () => {
|
||||||
|
assert.strictEqual(
|
||||||
|
getServerAlertToShow({
|
||||||
|
[ServerAlert.IDLE_PRIMARY_DEVICE]: {
|
||||||
|
firstReceivedAt: Date.now() - MONTH,
|
||||||
|
dismissedAt: Date.now() - DAY,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
null
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it('should show idle device warning if dismissed > 1 week', () => {
|
||||||
|
assert.strictEqual(
|
||||||
|
getServerAlertToShow({
|
||||||
|
[ServerAlert.IDLE_PRIMARY_DEVICE]: {
|
||||||
|
firstReceivedAt: Date.now() - MONTH,
|
||||||
|
dismissedAt: Date.now() - WEEK - 1,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
ServerAlert.IDLE_PRIMARY_DEVICE
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
@@ -1,6 +1,7 @@
|
|||||||
// Copyright 2025 Signal Messenger, LLC
|
// Copyright 2025 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
import createDebug from 'debug';
|
import createDebug from 'debug';
|
||||||
|
import type { Page } from 'playwright';
|
||||||
|
|
||||||
import type { App } from '../playwright';
|
import type { App } from '../playwright';
|
||||||
import { Bootstrap } from '../bootstrap';
|
import { Bootstrap } from '../bootstrap';
|
||||||
@@ -33,26 +34,60 @@ describe('serverAlerts', function (this: Mocha.Suite) {
|
|||||||
await bootstrap.teardown();
|
await bootstrap.teardown();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows critical idle primary device alert using classic desktop socket', async () => {
|
const TEST_CASES = [
|
||||||
bootstrap.server.setWebsocketUpgradeResponseHeaders({
|
{
|
||||||
'X-Signal-Alert': 'critical-idle-primary-device',
|
name: 'shows critical idle primary device alert',
|
||||||
});
|
headers: {
|
||||||
app = await bootstrap.link();
|
'X-Signal-Alert': 'critical-idle-primary-device',
|
||||||
const window = await app.getWindow();
|
},
|
||||||
await getLeftPane(window).getByText('Open Signal on your phone').waitFor();
|
test: async (window: Page) => {
|
||||||
});
|
await getLeftPane(window)
|
||||||
|
.getByText('Your account will be deleted soon')
|
||||||
|
.waitFor();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'handles different ordering of response values',
|
||||||
|
headers: {
|
||||||
|
'X-Signal-Alert':
|
||||||
|
'idle-primary-device, unknown-alert, critical-idle-primary-device',
|
||||||
|
},
|
||||||
|
test: async (window: Page) => {
|
||||||
|
await getLeftPane(window)
|
||||||
|
.getByText('Your account will be deleted soon')
|
||||||
|
.waitFor();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'shows idle primary device warning',
|
||||||
|
headers: {
|
||||||
|
'X-Signal-Alert': 'idle-primary-device',
|
||||||
|
},
|
||||||
|
test: async (window: Page) => {
|
||||||
|
await getLeftPane(window)
|
||||||
|
.getByText('Open signal on your phone to keep your account active')
|
||||||
|
.waitFor();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
] as const;
|
||||||
|
|
||||||
it('shows critical idle primary device alert using libsignal socket', async () => {
|
for (const testCase of TEST_CASES) {
|
||||||
bootstrap.server.setWebsocketUpgradeResponseHeaders({
|
for (const transport of ['classic', 'libsignal']) {
|
||||||
'X-Signal-Alert': 'critical-idle-primary-device',
|
// eslint-disable-next-line no-loop-func
|
||||||
});
|
it(`${testCase.name}: ${transport} socket`, async () => {
|
||||||
|
bootstrap.server.setWebsocketUpgradeResponseHeaders(testCase.headers);
|
||||||
|
app =
|
||||||
|
transport === 'classic'
|
||||||
|
? await bootstrap.link()
|
||||||
|
: await setupAppToUseLibsignalWebsockets(bootstrap);
|
||||||
|
const window = await app.getWindow();
|
||||||
|
await testCase.test(window);
|
||||||
|
|
||||||
app = await setupAppToUseLibsignalWebsockets(bootstrap);
|
if (transport === 'libsignal') {
|
||||||
|
debug('confirming that app was actually using libsignal');
|
||||||
const window = await app.getWindow();
|
await assertAppWasUsingLibsignalWebsockets(app);
|
||||||
await getLeftPane(window).getByText('Open Signal on your phone').waitFor();
|
}
|
||||||
|
});
|
||||||
debug('confirming that app was actually using libsignal');
|
}
|
||||||
await assertAppWasUsingLibsignalWebsockets(app);
|
}
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
@@ -51,8 +51,10 @@ import { isNightly, isBeta, isStaging } from '../util/version';
|
|||||||
import { getBasicAuth } from '../util/getBasicAuth';
|
import { getBasicAuth } from '../util/getBasicAuth';
|
||||||
import { isTestOrMockEnvironment } from '../environment';
|
import { isTestOrMockEnvironment } from '../environment';
|
||||||
import type { ConfigKeyType } from '../RemoteConfig';
|
import type { ConfigKeyType } from '../RemoteConfig';
|
||||||
import type { ServerAlert } from '../state/ducks/server';
|
import {
|
||||||
import { parseServerAlertFromHeader } from '../state/ducks/server';
|
parseServerAlertsFromHeader,
|
||||||
|
type ServerAlert,
|
||||||
|
} from '../util/handleServerAlerts';
|
||||||
|
|
||||||
const FIVE_MINUTES = 5 * durations.MINUTE;
|
const FIVE_MINUTES = 5 * durations.MINUTE;
|
||||||
|
|
||||||
@@ -977,8 +979,8 @@ export class SocketManager extends EventListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const serverAlerts: Array<ServerAlert> = alerts
|
const serverAlerts: Array<ServerAlert> = alerts
|
||||||
.map(parseServerAlertFromHeader)
|
.map(parseServerAlertsFromHeader)
|
||||||
.filter(v => v !== undefined);
|
.flat();
|
||||||
|
|
||||||
this.emit('serverAlerts', serverAlerts);
|
this.emit('serverAlerts', serverAlerts);
|
||||||
}
|
}
|
||||||
|
@@ -82,7 +82,7 @@ import { getMockServerPort } from '../util/getMockServerPort';
|
|||||||
import { pemToDer } from '../util/pemToDer';
|
import { pemToDer } from '../util/pemToDer';
|
||||||
import { ToastType } from '../types/Toast';
|
import { ToastType } from '../types/Toast';
|
||||||
import { isProduction } from '../util/version';
|
import { isProduction } from '../util/version';
|
||||||
import type { ServerAlert } from '../state/ducks/server';
|
import type { ServerAlert } from '../util/handleServerAlerts';
|
||||||
|
|
||||||
// Note: this will break some code that expects to be able to use err.response when a
|
// Note: this will break some code that expects to be able to use err.response when a
|
||||||
// web request fails, because it will force it to text. But it is very useful for
|
// web request fails, because it will force it to text. But it is very useful for
|
||||||
|
@@ -58,8 +58,10 @@ import { AbortableProcess } from '../util/AbortableProcess';
|
|||||||
import type { WebAPICredentials } from './Types';
|
import type { WebAPICredentials } from './Types';
|
||||||
import { NORMAL_DISCONNECT_CODE } from './SocketManager';
|
import { NORMAL_DISCONNECT_CODE } from './SocketManager';
|
||||||
import { parseUnknown } from '../util/schemas';
|
import { parseUnknown } from '../util/schemas';
|
||||||
import type { ServerAlert } from '../state/ducks/server';
|
import {
|
||||||
import { parseServerAlertFromHeader } from '../state/ducks/server';
|
parseServerAlertsFromHeader,
|
||||||
|
type ServerAlert,
|
||||||
|
} from '../util/handleServerAlerts';
|
||||||
|
|
||||||
const THIRTY_SECONDS = 30 * durations.SECOND;
|
const THIRTY_SECONDS = 30 * durations.SECOND;
|
||||||
|
|
||||||
@@ -396,9 +398,7 @@ export function connectAuthenticatedLibsignal({
|
|||||||
this.resource = undefined;
|
this.resource = undefined;
|
||||||
},
|
},
|
||||||
onReceivedAlerts(alerts: Array<string>): void {
|
onReceivedAlerts(alerts: Array<string>): void {
|
||||||
onReceivedAlerts(
|
onReceivedAlerts(alerts.map(parseServerAlertsFromHeader).flat());
|
||||||
alerts.map(parseServerAlertFromHeader).filter(v => v !== undefined)
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
return connectLibsignal(
|
return connectLibsignal(
|
||||||
|
2
ts/types/Storage.d.ts
vendored
2
ts/types/Storage.d.ts
vendored
@@ -21,6 +21,7 @@ import type { BackupCredentialWrapperType } from './backups';
|
|||||||
import type { ServiceIdString } from './ServiceId';
|
import type { ServiceIdString } from './ServiceId';
|
||||||
|
|
||||||
import type { RegisteredChallengeType } from '../challenge';
|
import type { RegisteredChallengeType } from '../challenge';
|
||||||
|
import type { ServerAlertsType } from '../util/handleServerAlerts';
|
||||||
|
|
||||||
export type AutoDownloadAttachmentType = {
|
export type AutoDownloadAttachmentType = {
|
||||||
photos: boolean;
|
photos: boolean;
|
||||||
@@ -194,6 +195,7 @@ export type StorageAccessType = {
|
|||||||
entropy: Uint8Array;
|
entropy: Uint8Array;
|
||||||
serverId: Uint8Array;
|
serverId: Uint8Array;
|
||||||
};
|
};
|
||||||
|
serverAlerts: ServerAlertsType;
|
||||||
needOrphanedAttachmentCheck: boolean;
|
needOrphanedAttachmentCheck: boolean;
|
||||||
observedCapabilities: {
|
observedCapabilities: {
|
||||||
deleteSync?: true;
|
deleteSync?: true;
|
||||||
|
@@ -1,8 +1,106 @@
|
|||||||
// Copyright 2025 Signal Messenger, LLC
|
// Copyright 2025 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import type { ServerAlert } from '../state/ducks/server';
|
import * as log from '../logging/log';
|
||||||
|
import { isMoreRecentThan } from './timestamp';
|
||||||
|
import { WEEK } from './durations';
|
||||||
|
import { isNotNil } from './isNotNil';
|
||||||
|
|
||||||
export function handleServerAlerts(alerts: Array<ServerAlert>): void {
|
export enum ServerAlert {
|
||||||
window.reduxActions.server.updateServerAlerts(alerts);
|
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;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
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(`handleServerAlerts: got new alert: ${alert}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existingAlertNames.size > 0) {
|
||||||
|
log.info(
|
||||||
|
`handleServerAlerts: 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;
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user