Show critical-idle-primary-device banner in response to WS upgrade response headers
This commit is contained in:
@@ -6675,6 +6675,14 @@
|
||||
"messageformat": "Update Downloaded",
|
||||
"description": "The title of update dialog when update download is completed."
|
||||
},
|
||||
"icu:CriticalIdlePrimaryDevice__title": {
|
||||
"messageformat": "Open Signal on your phone",
|
||||
"description": "Title of a banner alerting users if their primary device (e.g. phone) has not been logged into recently."
|
||||
},
|
||||
"icu:CriticalIdlePrimaryDevice__body": {
|
||||
"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. "
|
||||
},
|
||||
"icu:DialogNetworkStatus__outage": {
|
||||
"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."
|
||||
|
@@ -221,7 +221,7 @@
|
||||
"@indutny/parallel-prettier": "3.0.0",
|
||||
"@indutny/rezip-electron": "2.0.1",
|
||||
"@napi-rs/canvas": "0.1.61",
|
||||
"@signalapp/mock-server": "11.0.0",
|
||||
"@signalapp/mock-server": "11.1.0",
|
||||
"@storybook/addon-a11y": "8.4.4",
|
||||
"@storybook/addon-actions": "8.4.4",
|
||||
"@storybook/addon-controls": "8.4.4",
|
||||
@@ -374,6 +374,7 @@
|
||||
"app-builder-lib@26.0.0-alpha.8": "patches/app-builder-lib+26.0.0-alpha.8.patch",
|
||||
"growing-file@0.1.3": "patches/growing-file+0.1.3.patch",
|
||||
"websocket@1.0.34": "patches/websocket+1.0.34.patch",
|
||||
"@types/websocket@1.0.0": "patches/@types+websocket+1.0.0.patch",
|
||||
"backbone@1.6.0": "patches/backbone+1.6.0.patch",
|
||||
"node-fetch@2.6.7": "patches/node-fetch+2.6.7.patch",
|
||||
"zod@3.23.8": "patches/zod+3.23.8.patch"
|
||||
|
16
patches/@types+websocket+1.0.0.patch
Normal file
16
patches/@types+websocket+1.0.0.patch
Normal file
@@ -0,0 +1,16 @@
|
||||
diff --git a/index.d.ts b/index.d.ts
|
||||
index eb23f3c479e6d368e1563de7531a8459566d5c56..146769d33ce61f036371c05743cd31fb44d25d49 100644
|
||||
--- a/index.d.ts
|
||||
+++ b/index.d.ts
|
||||
@@ -650,9 +650,11 @@ export class client extends events.EventEmitter {
|
||||
on(event: 'connect', cb: (connection: connection) => void): this;
|
||||
on(event: 'connectFailed', cb: (err: Error) => void): this;
|
||||
on(event: 'httpResponse', cb: (response: http.IncomingMessage, client: client) => void): this;
|
||||
+ on(event: 'upgradeResponse', cb: (response: http.IncomingMessage) => void): this;
|
||||
addListener(event: 'connect', cb: (connection: connection) => void): this;
|
||||
addListener(event: 'connectFailed', cb: (err: Error) => void): this;
|
||||
addListener(event: 'httpResponse', cb: (response: http.IncomingMessage, client: client) => void): this;
|
||||
+ addListener(event: 'upgradeResponse', cb: (response: http.IncomingMessage) => void): this;
|
||||
}
|
||||
|
||||
export interface IRouterRequest extends events.EventEmitter {
|
@@ -1,5 +1,17 @@
|
||||
diff --git a/lib/WebSocketClient.js b/lib/WebSocketClient.js
|
||||
index fb7572b382de289f7fa3ff35a9647f118f3bd4eb..0000a94607520b4b7ac71f5eee4ed297df8d1bf7 100644
|
||||
--- a/lib/WebSocketClient.js
|
||||
+++ b/lib/WebSocketClient.js
|
||||
@@ -258,6 +258,7 @@ WebSocketClient.prototype.connect = function(requestUrl, protocols, origin, head
|
||||
self.socket = socket;
|
||||
self.response = response;
|
||||
self.firstDataChunk = head;
|
||||
+ self.emit('upgradeResponse', response);
|
||||
self.validateHandshake();
|
||||
});
|
||||
req.on('error', handleRequestError);
|
||||
diff --git a/lib/WebSocketConnection.js b/lib/WebSocketConnection.js
|
||||
index 219de63..93d3800 100644
|
||||
index 219de631dabe578e64574732fc95353a1c1047fa..93d380004b5bc58ba9895fb5a760709389641071 100644
|
||||
--- a/lib/WebSocketConnection.js
|
||||
+++ b/lib/WebSocketConnection.js
|
||||
@@ -271,7 +271,7 @@ WebSocketConnection.prototype.handleSocketData = function(data) {
|
||||
|
23
pnpm-lock.yaml
generated
23
pnpm-lock.yaml
generated
@@ -26,6 +26,9 @@ patchedDependencies:
|
||||
'@types/node-fetch@2.6.12':
|
||||
hash: bc43fb8cfed85fb4f7917b5bd3b47644ae15a000c26a6fe23078b2f5217efaf0
|
||||
path: patches/@types+node-fetch+2.6.12.patch
|
||||
'@types/websocket@1.0.0':
|
||||
hash: 53fdd65a6b35f379eb8f5807d315f00c66e9bfe9741f4859a0fa21026bdb30fa
|
||||
path: patches/@types+websocket+1.0.0.patch
|
||||
'@vitest/expect@2.0.5':
|
||||
hash: e8a96f71e52bf903c9f1eadba4740489a0beb48da33db52354adca484fe1f495
|
||||
path: patches/@vitest+expect+2.0.5.patch
|
||||
@@ -72,7 +75,7 @@ patchedDependencies:
|
||||
hash: 94614db18e4db7ff5eee4c2acf88e3ef245bff2f9637416e99e401bec3531286
|
||||
path: patches/react-textarea-autosize+8.5.5.patch
|
||||
websocket@1.0.34:
|
||||
hash: 4ce5bb237501a5576bd9e09fbff46551dd3e57f7702f6033bcffa49dde7f71bb
|
||||
hash: b8d361a6a73e44000bb51102dea0d841c22d2bb455dd6c54de566d0e0bd86355
|
||||
path: patches/websocket+1.0.34.patch
|
||||
zod@3.23.8:
|
||||
hash: 239818e5d88990616205c8cdc1de1660bf5e18b157d00c4a5f726dde6094af4d
|
||||
@@ -375,7 +378,7 @@ importers:
|
||||
version: 11.0.2
|
||||
websocket:
|
||||
specifier: 1.0.34
|
||||
version: 1.0.34(patch_hash=4ce5bb237501a5576bd9e09fbff46551dd3e57f7702f6033bcffa49dde7f71bb)
|
||||
version: 1.0.34(patch_hash=b8d361a6a73e44000bb51102dea0d841c22d2bb455dd6c54de566d0e0bd86355)
|
||||
write-file-atomic:
|
||||
specifier: 6.0.0
|
||||
version: 6.0.0
|
||||
@@ -429,8 +432,8 @@ importers:
|
||||
specifier: 0.1.61
|
||||
version: 0.1.61
|
||||
'@signalapp/mock-server':
|
||||
specifier: 11.0.0
|
||||
version: 11.0.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)
|
||||
specifier: 11.1.0
|
||||
version: 11.1.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)
|
||||
'@storybook/addon-a11y':
|
||||
specifier: 8.4.4
|
||||
version: 8.4.4(storybook@8.4.4(bufferutil@4.0.9)(prettier@3.3.3)(utf-8-validate@5.0.10))
|
||||
@@ -586,7 +589,7 @@ importers:
|
||||
version: 10.0.0
|
||||
'@types/websocket':
|
||||
specifier: 1.0.0
|
||||
version: 1.0.0
|
||||
version: 1.0.0(patch_hash=53fdd65a6b35f379eb8f5807d315f00c66e9bfe9741f4859a0fa21026bdb30fa)
|
||||
'@types/write-file-atomic':
|
||||
specifier: 4.0.3
|
||||
version: 4.0.3
|
||||
@@ -2535,8 +2538,8 @@ packages:
|
||||
'@signalapp/libsignal-client@0.67.3':
|
||||
resolution: {integrity: sha512-GIiXJMqiIByPZbomytoYQcQLJ3pNgHBCjt5BvlE/3rkrmNwyXW1UVqszX6/WfZi91aqUapO4+7Op+8JCbDGRWA==}
|
||||
|
||||
'@signalapp/mock-server@11.0.0':
|
||||
resolution: {integrity: sha512-JHEqdjXvWcXyLJ90OtaTIQVtizH/w+rmnPplNmHuUiDZIQIEvE3lRyZFyIvgVTNg29lQWs/Gw/o+hICerdOChQ==}
|
||||
'@signalapp/mock-server@11.1.0':
|
||||
resolution: {integrity: sha512-SYYek3QCh57vZZGNVQW+fSXMr+xHjnO8sMAFfj8DovjqW4HIBWbt3itGyyjxSoIXnaCMK346O6I/R79w8xY/aw==}
|
||||
|
||||
'@signalapp/parchment-cjs@3.0.1':
|
||||
resolution: {integrity: sha512-hSBMQ1M7wE4GcC8ZeNtvpJF+DAJg3eIRRf1SiHS3I3Algav/sgJJNm6HIYm6muHuK7IJmuEjkL3ILSXgmu0RfQ==}
|
||||
@@ -12266,7 +12269,7 @@ snapshots:
|
||||
type-fest: 4.26.1
|
||||
uuid: 8.3.2
|
||||
|
||||
'@signalapp/mock-server@11.0.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)':
|
||||
'@signalapp/mock-server@11.1.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)':
|
||||
dependencies:
|
||||
'@indutny/parallel-prettier': 3.0.0(prettier@3.3.3)
|
||||
'@signalapp/libsignal-client': 0.60.2
|
||||
@@ -13134,7 +13137,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@types/node': 20.17.6
|
||||
|
||||
'@types/websocket@1.0.0':
|
||||
'@types/websocket@1.0.0(patch_hash=53fdd65a6b35f379eb8f5807d315f00c66e9bfe9741f4859a0fa21026bdb30fa)':
|
||||
dependencies:
|
||||
'@types/node': 20.17.6
|
||||
|
||||
@@ -20849,7 +20852,7 @@ snapshots:
|
||||
|
||||
websocket-extensions@0.1.4: {}
|
||||
|
||||
websocket@1.0.34(patch_hash=4ce5bb237501a5576bd9e09fbff46551dd3e57f7702f6033bcffa49dde7f71bb):
|
||||
websocket@1.0.34(patch_hash=b8d361a6a73e44000bb51102dea0d841c22d2bb455dd6c54de566d0e0bd86355):
|
||||
dependencies:
|
||||
bufferutil: 4.0.9
|
||||
debug: 2.6.9
|
||||
|
@@ -208,6 +208,7 @@ import { handleDataMessage } from './messages/handleDataMessage';
|
||||
import { MessageModel } from './models/messages';
|
||||
import { waitForEvent } from './shims/events';
|
||||
import { sendSyncRequests } from './textsecure/syncRequests';
|
||||
import { handleServerAlerts } from './util/handleServerAlerts';
|
||||
|
||||
export function isOverHourIntoPast(timestamp: number): boolean {
|
||||
return isNumber(timestamp) && isOlderThan(timestamp, HOUR);
|
||||
@@ -511,6 +512,8 @@ export async function startApp(): Promise<void> {
|
||||
restoreRemoteConfigFromStorage();
|
||||
|
||||
window.Whisper.events.on('firstEnvelope', checkFirstEnvelope);
|
||||
window.Whisper.events.on('serverAlerts', handleServerAlerts);
|
||||
|
||||
server = window.WebAPI.connect({
|
||||
...window.textsecure.storage.user.getWebAPICredentials(),
|
||||
hasStoriesDisabled: window.storage.get('hasStoriesDisabled', false),
|
||||
|
46
ts/components/CriticalIdlePrimaryDeviceDialog.tsx
Normal file
46
ts/components/CriticalIdlePrimaryDeviceDialog.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { LeftPaneDialog } from './LeftPaneDialog';
|
||||
import type { WidthBreakpoint } from './_util';
|
||||
import type { LocalizerType } from '../types/I18N';
|
||||
import { I18n } from './I18n';
|
||||
|
||||
export type PropsType = {
|
||||
containerWidthBreakpoint: WidthBreakpoint;
|
||||
i18n: LocalizerType;
|
||||
};
|
||||
|
||||
export function CriticalIdlePrimaryDeviceDialog({
|
||||
containerWidthBreakpoint,
|
||||
i18n,
|
||||
}: PropsType): JSX.Element {
|
||||
const learnMoreLink = (parts: Array<string | JSX.Element>) => (
|
||||
<a
|
||||
key="signal-support"
|
||||
// TODO: DESKTOP-8377
|
||||
href="https://support.signal.org/"
|
||||
rel="noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
{parts}
|
||||
</a>
|
||||
);
|
||||
return (
|
||||
<LeftPaneDialog
|
||||
containerWidthBreakpoint={containerWidthBreakpoint}
|
||||
type="warning"
|
||||
title={i18n('icu:CriticalIdlePrimaryDevice__title')}
|
||||
>
|
||||
<I18n
|
||||
id="icu:CriticalIdlePrimaryDevice__body"
|
||||
i18n={i18n}
|
||||
components={{
|
||||
learnMoreLink,
|
||||
}}
|
||||
/>
|
||||
</LeftPaneDialog>
|
||||
);
|
||||
}
|
@@ -35,6 +35,7 @@ import {
|
||||
useUuidFetchState,
|
||||
} from '../test-both/helpers/fakeLookupConversationWithoutServiceId';
|
||||
import type { GroupListItemConversationType } from './conversationList/GroupListItem';
|
||||
import { CriticalIdlePrimaryDeviceDialog } from './CriticalIdlePrimaryDeviceDialog';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
@@ -173,6 +174,7 @@ const useProps = (overrideProps: OverridePropsType = {}): PropsType => {
|
||||
getPreferredBadge: () => undefined,
|
||||
hasFailedStorySends: false,
|
||||
hasPendingUpdate: false,
|
||||
hasCriticalIdlePrimaryDeviceAlert: false,
|
||||
i18n,
|
||||
isMacOS: false,
|
||||
preferredWidthFromStorage: 320,
|
||||
@@ -271,6 +273,9 @@ const useProps = (overrideProps: OverridePropsType = {}): PropsType => {
|
||||
/>
|
||||
),
|
||||
renderExpiredBuildDialog: props => <DialogExpiredBuild {...props} />,
|
||||
renderCriticalIdlePrimaryDeviceDialog: props => (
|
||||
<CriticalIdlePrimaryDeviceDialog {...props} />
|
||||
),
|
||||
renderUnsupportedOSDialog: props => (
|
||||
<UnsupportedOSDialog
|
||||
i18n={i18n}
|
||||
@@ -393,6 +398,15 @@ export function InboxBackupMediaDownloadWithDialogsAndUnpinnedConversations(): J
|
||||
/>
|
||||
);
|
||||
}
|
||||
export function InboxCriticalIdlePrimaryDeviceAlert(): JSX.Element {
|
||||
return (
|
||||
<LeftPaneInContainer
|
||||
{...useProps({
|
||||
hasCriticalIdlePrimaryDeviceAlert: true,
|
||||
})}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function InboxUsernameCorrupted(): JSX.Element {
|
||||
return (
|
||||
|
@@ -69,6 +69,7 @@ export type PropsType = {
|
||||
otherTabsUnreadStats: UnreadStats;
|
||||
hasExpiredDialog: boolean;
|
||||
hasFailedStorySends: boolean;
|
||||
hasCriticalIdlePrimaryDeviceAlert: boolean;
|
||||
hasNetworkDialog: boolean;
|
||||
hasPendingUpdate: boolean;
|
||||
hasRelinkDialog: boolean;
|
||||
@@ -180,6 +181,12 @@ export type PropsType = {
|
||||
renderCaptchaDialog: (props: { onSkip(): void }) => JSX.Element;
|
||||
renderCrashReportDialog: () => JSX.Element;
|
||||
renderExpiredBuildDialog: (_: DialogExpiredBuildPropsType) => JSX.Element;
|
||||
renderCriticalIdlePrimaryDeviceDialog: (
|
||||
_: Readonly<{
|
||||
containerWidthBreakpoint: WidthBreakpoint;
|
||||
i18n: LocalizerType;
|
||||
}>
|
||||
) => JSX.Element;
|
||||
renderToastManager: (_: {
|
||||
containerWidthBreakpoint: WidthBreakpoint;
|
||||
}) => JSX.Element;
|
||||
@@ -206,6 +213,7 @@ export function LeftPane({
|
||||
getPreferredBadge,
|
||||
hasExpiredDialog,
|
||||
hasFailedStorySends,
|
||||
hasCriticalIdlePrimaryDeviceAlert,
|
||||
hasNetworkDialog,
|
||||
hasPendingUpdate,
|
||||
hasRelinkDialog,
|
||||
@@ -227,6 +235,7 @@ export function LeftPane({
|
||||
renderCaptchaDialog,
|
||||
renderCrashReportDialog,
|
||||
renderExpiredBuildDialog,
|
||||
renderCriticalIdlePrimaryDeviceDialog,
|
||||
renderMessageSearchResult,
|
||||
renderNetworkStatus,
|
||||
renderUnsupportedOSDialog,
|
||||
@@ -611,6 +620,9 @@ export function LeftPane({
|
||||
maybeYellowDialog = renderNetworkStatus(commonDialogProps);
|
||||
} else if (hasRelinkDialog) {
|
||||
maybeYellowDialog = renderRelinkDialog(commonDialogProps);
|
||||
} else if (hasCriticalIdlePrimaryDeviceAlert) {
|
||||
maybeYellowDialog =
|
||||
renderCriticalIdlePrimaryDeviceDialog(commonDialogProps);
|
||||
}
|
||||
|
||||
// Update dialog
|
||||
|
@@ -23,6 +23,7 @@ import { actions as mediaGallery } from './ducks/mediaGallery';
|
||||
import { actions as network } from './ducks/network';
|
||||
import { actions as safetyNumber } from './ducks/safetyNumber';
|
||||
import { actions as search } from './ducks/search';
|
||||
import { actions as server } from './ducks/server';
|
||||
import { actions as stickers } from './ducks/stickers';
|
||||
import { actions as stories } from './ducks/stories';
|
||||
import { actions as storyDistributionLists } from './ducks/storyDistributionLists';
|
||||
@@ -55,6 +56,7 @@ export const actionCreators: ReduxActions = {
|
||||
network,
|
||||
safetyNumber,
|
||||
search,
|
||||
server,
|
||||
stickers,
|
||||
stories,
|
||||
storyDistributionLists,
|
||||
|
59
ts/state/ducks/server.ts
Normal file
59
ts/state/ducks/server.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
// 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>;
|
||||
}>;
|
||||
|
||||
// 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,6 +25,7 @@ import { getEmptyState as networkEmptyState } from './ducks/network';
|
||||
import { getEmptyState as preferredReactionsEmptyState } from './ducks/preferredReactions';
|
||||
import { getEmptyState as safetyNumberEmptyState } from './ducks/safetyNumber';
|
||||
import { getEmptyState as searchEmptyState } from './ducks/search';
|
||||
import { getEmptyState as serverEmptyState } from './ducks/server';
|
||||
import { getEmptyState as stickersEmptyState } from './ducks/stickers';
|
||||
import { getEmptyState as storiesEmptyState } from './ducks/stories';
|
||||
import { getEmptyState as storyDistributionListsEmptyState } from './ducks/storyDistributionLists';
|
||||
@@ -144,6 +145,7 @@ function getEmptyState(): StateType {
|
||||
preferredReactions: preferredReactionsEmptyState(),
|
||||
safetyNumber: safetyNumberEmptyState(),
|
||||
search: searchEmptyState(),
|
||||
server: serverEmptyState(),
|
||||
stickers: stickersEmptyState(),
|
||||
stories: storiesEmptyState(),
|
||||
storyDistributionLists: storyDistributionListsEmptyState(),
|
||||
|
@@ -83,6 +83,7 @@ export function initializeRedux(data: ReduxInitData): void {
|
||||
store.dispatch
|
||||
),
|
||||
search: bindActionCreators(actionCreators.search, store.dispatch),
|
||||
server: bindActionCreators(actionCreators.server, store.dispatch),
|
||||
stickers: bindActionCreators(actionCreators.stickers, store.dispatch),
|
||||
stories: bindActionCreators(actionCreators.stories, store.dispatch),
|
||||
storyDistributionLists: bindActionCreators(
|
||||
|
@@ -27,6 +27,7 @@ import { reducer as network } from './ducks/network';
|
||||
import { reducer as preferredReactions } from './ducks/preferredReactions';
|
||||
import { reducer as safetyNumber } from './ducks/safetyNumber';
|
||||
import { reducer as search } from './ducks/search';
|
||||
import { reducer as server } from './ducks/server';
|
||||
import { reducer as stickers } from './ducks/stickers';
|
||||
import { reducer as stories } from './ducks/stories';
|
||||
import { reducer as storyDistributionLists } from './ducks/storyDistributionLists';
|
||||
@@ -60,6 +61,7 @@ export const reducer = combineReducers({
|
||||
preferredReactions,
|
||||
safetyNumber,
|
||||
search,
|
||||
server,
|
||||
stickers,
|
||||
stories,
|
||||
storyDistributionLists,
|
||||
|
17
ts/state/selectors/server.ts
Normal file
17
ts/state/selectors/server.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
// 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);
|
||||
}
|
||||
);
|
@@ -104,6 +104,9 @@ import {
|
||||
pauseBackupMediaDownload,
|
||||
resumeBackupMediaDownload,
|
||||
} 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 {
|
||||
return <SmartMessageSearchResult id={id} />;
|
||||
@@ -123,7 +126,14 @@ function renderUpdateDialog(
|
||||
): JSX.Element {
|
||||
return <SmartUpdateDialog {...props} />;
|
||||
}
|
||||
|
||||
function renderCriticalIdlePrimaryDeviceDialog(
|
||||
props: Readonly<{
|
||||
containerWidthBreakpoint: WidthBreakpoint;
|
||||
i18n: LocalizerType;
|
||||
}>
|
||||
): JSX.Element {
|
||||
return <CriticalIdlePrimaryDeviceDialog {...props} />;
|
||||
}
|
||||
function renderCaptchaDialog({ onSkip }: { onSkip(): void }): JSX.Element {
|
||||
return <SmartCaptchaDialog onSkip={onSkip} />;
|
||||
}
|
||||
@@ -297,6 +307,10 @@ export const SmartLeftPane = memo(function SmartLeftPane({
|
||||
const backupMediaDownloadProgress = useSelector(
|
||||
getBackupMediaDownloadProgress
|
||||
);
|
||||
const hasCriticalIdlePrimaryDeviceAlert = useSelector(
|
||||
getHasCriticalIdlePrimaryDeviceAlert
|
||||
);
|
||||
|
||||
const {
|
||||
blockConversation,
|
||||
clearGroupCreationError,
|
||||
@@ -388,6 +402,7 @@ export const SmartLeftPane = memo(function SmartLeftPane({
|
||||
getPreferredBadge={getPreferredBadge}
|
||||
hasExpiredDialog={hasExpiredDialog}
|
||||
hasFailedStorySends={hasFailedStorySends}
|
||||
hasCriticalIdlePrimaryDeviceAlert={hasCriticalIdlePrimaryDeviceAlert}
|
||||
hasNetworkDialog={hasNetworkDialog}
|
||||
hasPendingUpdate={hasPendingUpdate}
|
||||
hasRelinkDialog={hasRelinkDialog}
|
||||
@@ -409,6 +424,9 @@ export const SmartLeftPane = memo(function SmartLeftPane({
|
||||
renderCaptchaDialog={renderCaptchaDialog}
|
||||
renderCrashReportDialog={renderCrashReportDialog}
|
||||
renderExpiredBuildDialog={renderExpiredBuildDialog}
|
||||
renderCriticalIdlePrimaryDeviceDialog={
|
||||
renderCriticalIdlePrimaryDeviceDialog
|
||||
}
|
||||
renderMessageSearchResult={renderMessageSearchResult}
|
||||
renderNetworkStatus={renderNetworkStatus}
|
||||
renderRelinkDialog={renderRelinkDialog}
|
||||
|
@@ -23,6 +23,7 @@ import type { actions as mediaGallery } from './ducks/mediaGallery';
|
||||
import type { actions as network } from './ducks/network';
|
||||
import type { actions as safetyNumber } from './ducks/safetyNumber';
|
||||
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 stories } from './ducks/stories';
|
||||
import type { actions as storyDistributionLists } from './ducks/storyDistributionLists';
|
||||
@@ -54,6 +55,7 @@ export type ReduxActions = {
|
||||
network: typeof network;
|
||||
safetyNumber: typeof safetyNumber;
|
||||
search: typeof search;
|
||||
server: typeof server;
|
||||
stickers: typeof stickers;
|
||||
stories: typeof stories;
|
||||
storyDistributionLists: typeof storyDistributionLists;
|
||||
|
@@ -252,12 +252,15 @@ export async function createGroup(
|
||||
await phone.setStorageState(state);
|
||||
return group;
|
||||
}
|
||||
export function getLeftPane(page: Page): Locator {
|
||||
return page.locator('#LeftPane');
|
||||
}
|
||||
|
||||
export async function clickOnConversationWithAci(
|
||||
page: Page,
|
||||
aci: string
|
||||
): Promise<void> {
|
||||
const leftPane = page.locator('#LeftPane');
|
||||
const leftPane = getLeftPane(page);
|
||||
await leftPane.getByTestId(aci).click();
|
||||
}
|
||||
|
||||
|
40
ts/test-mock/network/serverAlerts_test.ts
Normal file
40
ts/test-mock/network/serverAlerts_test.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
import createDebug from 'debug';
|
||||
|
||||
import type { App } from '../playwright';
|
||||
import { Bootstrap } from '../bootstrap';
|
||||
import { getLeftPane } from '../helpers';
|
||||
import { MINUTE } from '../../util/durations';
|
||||
|
||||
export const debug = createDebug('mock:test:serverAlerts');
|
||||
|
||||
describe('serverAlerts', function (this: Mocha.Suite) {
|
||||
this.timeout(MINUTE);
|
||||
let bootstrap: Bootstrap;
|
||||
let app: App;
|
||||
|
||||
beforeEach(async () => {
|
||||
bootstrap = new Bootstrap();
|
||||
await bootstrap.init();
|
||||
});
|
||||
|
||||
afterEach(async function (this: Mocha.Context) {
|
||||
if (!bootstrap) {
|
||||
return;
|
||||
}
|
||||
|
||||
await bootstrap.maybeSaveLogs(this.currentTest, app);
|
||||
await app.close();
|
||||
await bootstrap.teardown();
|
||||
});
|
||||
|
||||
it('shows idle primary device alert', async () => {
|
||||
bootstrap.server.setWebsocketUpgradeResponseHeaders({
|
||||
'X-Signal-Alert': 'critical-idle-primary-device',
|
||||
});
|
||||
app = await bootstrap.link();
|
||||
const window = await app.getWindow();
|
||||
await getLeftPane(window).getByText('Open Signal on your phone').waitFor();
|
||||
});
|
||||
});
|
@@ -12,6 +12,7 @@ import { Headers } from 'node-fetch';
|
||||
import type { connection as WebSocket } from 'websocket';
|
||||
import qs from 'querystring';
|
||||
import EventListener from 'events';
|
||||
import type { IncomingMessage } from 'http';
|
||||
|
||||
import type { AbortableProcess } from '../util/AbortableProcess';
|
||||
import { strictAssert } from '../util/assert';
|
||||
@@ -50,6 +51,7 @@ import { isNightly, isBeta, isStaging } from '../util/version';
|
||||
import { getBasicAuth } from '../util/getBasicAuth';
|
||||
import { isTestOrMockEnvironment } from '../environment';
|
||||
import type { ConfigKeyType } from '../RemoteConfig';
|
||||
import { ServerAlert } from '../state/ducks/server';
|
||||
|
||||
const FIVE_MINUTES = 5 * durations.MINUTE;
|
||||
|
||||
@@ -220,6 +222,9 @@ export class SocketManager extends EventListener {
|
||||
Authorization: getBasicAuth({ username, password }),
|
||||
'X-Signal-Receive-Stories': String(!this.#hasStoriesDisabled),
|
||||
},
|
||||
onUpgradeResponse: (response: IncomingMessage) => {
|
||||
this.#handleAuthenticatedUpgradeResponseHeaders(response.headers);
|
||||
},
|
||||
proxyAgent,
|
||||
});
|
||||
|
||||
@@ -758,6 +763,7 @@ export class SocketManager extends EventListener {
|
||||
resourceOptions,
|
||||
query = {},
|
||||
extraHeaders = {},
|
||||
onUpgradeResponse,
|
||||
timeout,
|
||||
}: {
|
||||
name: string;
|
||||
@@ -766,6 +772,7 @@ export class SocketManager extends EventListener {
|
||||
resourceOptions: WebSocketResourceOptions;
|
||||
query?: Record<string, string>;
|
||||
extraHeaders?: Record<string, string>;
|
||||
onUpgradeResponse?: (response: IncomingMessage) => void;
|
||||
timeout?: number;
|
||||
}): AbortableProcess<IWebSocketResource> {
|
||||
const queryWithDefaults = {
|
||||
@@ -787,6 +794,7 @@ export class SocketManager extends EventListener {
|
||||
timeout,
|
||||
|
||||
extraHeaders,
|
||||
onUpgradeResponse,
|
||||
|
||||
createResource(socket: WebSocket): WebSocketResource {
|
||||
const duration = (performance.now() - start).toFixed(1);
|
||||
@@ -947,6 +955,33 @@ export class SocketManager extends EventListener {
|
||||
return this.#lazyProxyAgent;
|
||||
}
|
||||
|
||||
#handleAuthenticatedUpgradeResponseHeaders(
|
||||
headers: Record<string, string | Array<string> | undefined>
|
||||
) {
|
||||
let alerts: Array<string> = [];
|
||||
for (const [key, value] of Object.entries(headers)) {
|
||||
if (key.toLowerCase() === 'x-signal-alert') {
|
||||
if (value == null) {
|
||||
alerts = [];
|
||||
} else if (Array.isArray(value)) {
|
||||
alerts = value;
|
||||
} else {
|
||||
alerts = [value];
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const serverAlerts: Array<ServerAlert> = [];
|
||||
alerts.forEach(alert => {
|
||||
if (alert.toLowerCase() === 'critical-idle-primary-device') {
|
||||
serverAlerts.push(ServerAlert.CRITICAL_IDLE_PRIMARY_DEVICE);
|
||||
}
|
||||
});
|
||||
|
||||
this.emit('serverAlerts', serverAlerts);
|
||||
}
|
||||
|
||||
// EventEmitter types
|
||||
|
||||
public override on(type: 'authError', callback: () => void): this;
|
||||
@@ -957,6 +992,10 @@ export class SocketManager extends EventListener {
|
||||
type: 'firstEnvelope',
|
||||
callback: (incoming: IncomingWebSocketRequest) => void
|
||||
): this;
|
||||
public override on(
|
||||
type: 'serverAlerts',
|
||||
callback: (alerts: Array<ServerAlert>) => void
|
||||
): this;
|
||||
|
||||
public override on(
|
||||
type: string | symbol,
|
||||
@@ -974,6 +1013,10 @@ export class SocketManager extends EventListener {
|
||||
type: 'firstEnvelope',
|
||||
incoming: IncomingWebSocketRequest
|
||||
): boolean;
|
||||
public override emit(
|
||||
type: 'serverAlerts',
|
||||
alerts: Array<ServerAlert>
|
||||
): boolean;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
public override emit(type: string | symbol, ...args: Array<any>): boolean {
|
||||
|
@@ -1812,6 +1812,10 @@ export function initialize({
|
||||
window.Whisper.events.trigger('firstEnvelope', incoming);
|
||||
});
|
||||
|
||||
socketManager.on('serverAlerts', alerts => {
|
||||
window.Whisper.events.trigger('serverAlerts', alerts);
|
||||
});
|
||||
|
||||
if (useWebSocket) {
|
||||
void socketManager.authenticate({ username, password });
|
||||
}
|
||||
|
@@ -3,6 +3,7 @@
|
||||
|
||||
import { client as WebSocketClient } from 'websocket';
|
||||
import type { connection as WebSocket } from 'websocket';
|
||||
import type { IncomingMessage } from 'http';
|
||||
|
||||
import { AbortableProcess } from '../util/AbortableProcess';
|
||||
import { strictAssert } from '../util/assert';
|
||||
@@ -32,6 +33,7 @@ export type ConnectOptionsType<Resource extends IResource> = Readonly<{
|
||||
proxyAgent?: ProxyAgent;
|
||||
timeout?: number;
|
||||
extraHeaders?: Record<string, string>;
|
||||
onUpgradeResponse?: (response: IncomingMessage) => void;
|
||||
|
||||
createResource(socket: WebSocket): Resource;
|
||||
}>;
|
||||
@@ -44,6 +46,7 @@ export function connect<Resource extends IResource>({
|
||||
proxyAgent,
|
||||
extraHeaders = {},
|
||||
timeout = WEBSOCKET_CONNECT_TIMEOUT,
|
||||
onUpgradeResponse,
|
||||
createResource,
|
||||
}: ConnectOptionsType<Resource>): AbortableProcess<Resource> {
|
||||
const fixedScheme = url
|
||||
@@ -84,6 +87,10 @@ export function connect<Resource extends IResource>({
|
||||
resolve(resource);
|
||||
});
|
||||
|
||||
client.on('upgradeResponse', response => {
|
||||
onUpgradeResponse?.(response);
|
||||
});
|
||||
|
||||
client.on('httpResponse', async response => {
|
||||
Timers.clearTimeout(timer);
|
||||
|
||||
|
8
ts/util/handleServerAlerts.ts
Normal file
8
ts/util/handleServerAlerts.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import type { ServerAlert } from '../state/ducks/server';
|
||||
|
||||
export function handleServerAlerts(alerts: Array<ServerAlert>): void {
|
||||
window.reduxActions.server.updateServerAlerts(alerts);
|
||||
}
|
Reference in New Issue
Block a user