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",
|
"messageformat": "Update Downloaded",
|
||||||
"description": "The title of update dialog when update download is completed."
|
"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": {
|
"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."
|
||||||
|
@@ -221,7 +221,7 @@
|
|||||||
"@indutny/parallel-prettier": "3.0.0",
|
"@indutny/parallel-prettier": "3.0.0",
|
||||||
"@indutny/rezip-electron": "2.0.1",
|
"@indutny/rezip-electron": "2.0.1",
|
||||||
"@napi-rs/canvas": "0.1.61",
|
"@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-a11y": "8.4.4",
|
||||||
"@storybook/addon-actions": "8.4.4",
|
"@storybook/addon-actions": "8.4.4",
|
||||||
"@storybook/addon-controls": "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",
|
"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",
|
"growing-file@0.1.3": "patches/growing-file+0.1.3.patch",
|
||||||
"websocket@1.0.34": "patches/websocket+1.0.34.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",
|
"backbone@1.6.0": "patches/backbone+1.6.0.patch",
|
||||||
"node-fetch@2.6.7": "patches/node-fetch+2.6.7.patch",
|
"node-fetch@2.6.7": "patches/node-fetch+2.6.7.patch",
|
||||||
"zod@3.23.8": "patches/zod+3.23.8.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
|
diff --git a/lib/WebSocketConnection.js b/lib/WebSocketConnection.js
|
||||||
index 219de63..93d3800 100644
|
index 219de631dabe578e64574732fc95353a1c1047fa..93d380004b5bc58ba9895fb5a760709389641071 100644
|
||||||
--- a/lib/WebSocketConnection.js
|
--- a/lib/WebSocketConnection.js
|
||||||
+++ b/lib/WebSocketConnection.js
|
+++ b/lib/WebSocketConnection.js
|
||||||
@@ -271,7 +271,7 @@ WebSocketConnection.prototype.handleSocketData = function(data) {
|
@@ -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':
|
'@types/node-fetch@2.6.12':
|
||||||
hash: bc43fb8cfed85fb4f7917b5bd3b47644ae15a000c26a6fe23078b2f5217efaf0
|
hash: bc43fb8cfed85fb4f7917b5bd3b47644ae15a000c26a6fe23078b2f5217efaf0
|
||||||
path: patches/@types+node-fetch+2.6.12.patch
|
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':
|
'@vitest/expect@2.0.5':
|
||||||
hash: e8a96f71e52bf903c9f1eadba4740489a0beb48da33db52354adca484fe1f495
|
hash: e8a96f71e52bf903c9f1eadba4740489a0beb48da33db52354adca484fe1f495
|
||||||
path: patches/@vitest+expect+2.0.5.patch
|
path: patches/@vitest+expect+2.0.5.patch
|
||||||
@@ -72,7 +75,7 @@ patchedDependencies:
|
|||||||
hash: 94614db18e4db7ff5eee4c2acf88e3ef245bff2f9637416e99e401bec3531286
|
hash: 94614db18e4db7ff5eee4c2acf88e3ef245bff2f9637416e99e401bec3531286
|
||||||
path: patches/react-textarea-autosize+8.5.5.patch
|
path: patches/react-textarea-autosize+8.5.5.patch
|
||||||
websocket@1.0.34:
|
websocket@1.0.34:
|
||||||
hash: 4ce5bb237501a5576bd9e09fbff46551dd3e57f7702f6033bcffa49dde7f71bb
|
hash: b8d361a6a73e44000bb51102dea0d841c22d2bb455dd6c54de566d0e0bd86355
|
||||||
path: patches/websocket+1.0.34.patch
|
path: patches/websocket+1.0.34.patch
|
||||||
zod@3.23.8:
|
zod@3.23.8:
|
||||||
hash: 239818e5d88990616205c8cdc1de1660bf5e18b157d00c4a5f726dde6094af4d
|
hash: 239818e5d88990616205c8cdc1de1660bf5e18b157d00c4a5f726dde6094af4d
|
||||||
@@ -375,7 +378,7 @@ importers:
|
|||||||
version: 11.0.2
|
version: 11.0.2
|
||||||
websocket:
|
websocket:
|
||||||
specifier: 1.0.34
|
specifier: 1.0.34
|
||||||
version: 1.0.34(patch_hash=4ce5bb237501a5576bd9e09fbff46551dd3e57f7702f6033bcffa49dde7f71bb)
|
version: 1.0.34(patch_hash=b8d361a6a73e44000bb51102dea0d841c22d2bb455dd6c54de566d0e0bd86355)
|
||||||
write-file-atomic:
|
write-file-atomic:
|
||||||
specifier: 6.0.0
|
specifier: 6.0.0
|
||||||
version: 6.0.0
|
version: 6.0.0
|
||||||
@@ -429,8 +432,8 @@ importers:
|
|||||||
specifier: 0.1.61
|
specifier: 0.1.61
|
||||||
version: 0.1.61
|
version: 0.1.61
|
||||||
'@signalapp/mock-server':
|
'@signalapp/mock-server':
|
||||||
specifier: 11.0.0
|
specifier: 11.1.0
|
||||||
version: 11.0.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)
|
version: 11.1.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)
|
||||||
'@storybook/addon-a11y':
|
'@storybook/addon-a11y':
|
||||||
specifier: 8.4.4
|
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))
|
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
|
version: 10.0.0
|
||||||
'@types/websocket':
|
'@types/websocket':
|
||||||
specifier: 1.0.0
|
specifier: 1.0.0
|
||||||
version: 1.0.0
|
version: 1.0.0(patch_hash=53fdd65a6b35f379eb8f5807d315f00c66e9bfe9741f4859a0fa21026bdb30fa)
|
||||||
'@types/write-file-atomic':
|
'@types/write-file-atomic':
|
||||||
specifier: 4.0.3
|
specifier: 4.0.3
|
||||||
version: 4.0.3
|
version: 4.0.3
|
||||||
@@ -2535,8 +2538,8 @@ packages:
|
|||||||
'@signalapp/libsignal-client@0.67.3':
|
'@signalapp/libsignal-client@0.67.3':
|
||||||
resolution: {integrity: sha512-GIiXJMqiIByPZbomytoYQcQLJ3pNgHBCjt5BvlE/3rkrmNwyXW1UVqszX6/WfZi91aqUapO4+7Op+8JCbDGRWA==}
|
resolution: {integrity: sha512-GIiXJMqiIByPZbomytoYQcQLJ3pNgHBCjt5BvlE/3rkrmNwyXW1UVqszX6/WfZi91aqUapO4+7Op+8JCbDGRWA==}
|
||||||
|
|
||||||
'@signalapp/mock-server@11.0.0':
|
'@signalapp/mock-server@11.1.0':
|
||||||
resolution: {integrity: sha512-JHEqdjXvWcXyLJ90OtaTIQVtizH/w+rmnPplNmHuUiDZIQIEvE3lRyZFyIvgVTNg29lQWs/Gw/o+hICerdOChQ==}
|
resolution: {integrity: sha512-SYYek3QCh57vZZGNVQW+fSXMr+xHjnO8sMAFfj8DovjqW4HIBWbt3itGyyjxSoIXnaCMK346O6I/R79w8xY/aw==}
|
||||||
|
|
||||||
'@signalapp/parchment-cjs@3.0.1':
|
'@signalapp/parchment-cjs@3.0.1':
|
||||||
resolution: {integrity: sha512-hSBMQ1M7wE4GcC8ZeNtvpJF+DAJg3eIRRf1SiHS3I3Algav/sgJJNm6HIYm6muHuK7IJmuEjkL3ILSXgmu0RfQ==}
|
resolution: {integrity: sha512-hSBMQ1M7wE4GcC8ZeNtvpJF+DAJg3eIRRf1SiHS3I3Algav/sgJJNm6HIYm6muHuK7IJmuEjkL3ILSXgmu0RfQ==}
|
||||||
@@ -12266,7 +12269,7 @@ snapshots:
|
|||||||
type-fest: 4.26.1
|
type-fest: 4.26.1
|
||||||
uuid: 8.3.2
|
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:
|
dependencies:
|
||||||
'@indutny/parallel-prettier': 3.0.0(prettier@3.3.3)
|
'@indutny/parallel-prettier': 3.0.0(prettier@3.3.3)
|
||||||
'@signalapp/libsignal-client': 0.60.2
|
'@signalapp/libsignal-client': 0.60.2
|
||||||
@@ -13134,7 +13137,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 20.17.6
|
'@types/node': 20.17.6
|
||||||
|
|
||||||
'@types/websocket@1.0.0':
|
'@types/websocket@1.0.0(patch_hash=53fdd65a6b35f379eb8f5807d315f00c66e9bfe9741f4859a0fa21026bdb30fa)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 20.17.6
|
'@types/node': 20.17.6
|
||||||
|
|
||||||
@@ -20849,7 +20852,7 @@ snapshots:
|
|||||||
|
|
||||||
websocket-extensions@0.1.4: {}
|
websocket-extensions@0.1.4: {}
|
||||||
|
|
||||||
websocket@1.0.34(patch_hash=4ce5bb237501a5576bd9e09fbff46551dd3e57f7702f6033bcffa49dde7f71bb):
|
websocket@1.0.34(patch_hash=b8d361a6a73e44000bb51102dea0d841c22d2bb455dd6c54de566d0e0bd86355):
|
||||||
dependencies:
|
dependencies:
|
||||||
bufferutil: 4.0.9
|
bufferutil: 4.0.9
|
||||||
debug: 2.6.9
|
debug: 2.6.9
|
||||||
|
@@ -208,6 +208,7 @@ import { handleDataMessage } from './messages/handleDataMessage';
|
|||||||
import { MessageModel } from './models/messages';
|
import { MessageModel } from './models/messages';
|
||||||
import { waitForEvent } from './shims/events';
|
import { waitForEvent } from './shims/events';
|
||||||
import { sendSyncRequests } from './textsecure/syncRequests';
|
import { sendSyncRequests } from './textsecure/syncRequests';
|
||||||
|
import { handleServerAlerts } from './util/handleServerAlerts';
|
||||||
|
|
||||||
export function isOverHourIntoPast(timestamp: number): boolean {
|
export function isOverHourIntoPast(timestamp: number): boolean {
|
||||||
return isNumber(timestamp) && isOlderThan(timestamp, HOUR);
|
return isNumber(timestamp) && isOlderThan(timestamp, HOUR);
|
||||||
@@ -511,6 +512,8 @@ export async function startApp(): Promise<void> {
|
|||||||
restoreRemoteConfigFromStorage();
|
restoreRemoteConfigFromStorage();
|
||||||
|
|
||||||
window.Whisper.events.on('firstEnvelope', checkFirstEnvelope);
|
window.Whisper.events.on('firstEnvelope', checkFirstEnvelope);
|
||||||
|
window.Whisper.events.on('serverAlerts', handleServerAlerts);
|
||||||
|
|
||||||
server = window.WebAPI.connect({
|
server = window.WebAPI.connect({
|
||||||
...window.textsecure.storage.user.getWebAPICredentials(),
|
...window.textsecure.storage.user.getWebAPICredentials(),
|
||||||
hasStoriesDisabled: window.storage.get('hasStoriesDisabled', false),
|
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,
|
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';
|
||||||
|
|
||||||
const i18n = setupI18n('en', enMessages);
|
const i18n = setupI18n('en', enMessages);
|
||||||
|
|
||||||
@@ -173,6 +174,7 @@ 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,6 +273,9 @@ 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}
|
||||||
@@ -393,6 +398,15 @@ export function InboxBackupMediaDownloadWithDialogsAndUnpinnedConversations(): J
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
export function InboxCriticalIdlePrimaryDeviceAlert(): JSX.Element {
|
||||||
|
return (
|
||||||
|
<LeftPaneInContainer
|
||||||
|
{...useProps({
|
||||||
|
hasCriticalIdlePrimaryDeviceAlert: true,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function InboxUsernameCorrupted(): JSX.Element {
|
export function InboxUsernameCorrupted(): JSX.Element {
|
||||||
return (
|
return (
|
||||||
|
@@ -69,6 +69,7 @@ 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;
|
||||||
@@ -180,6 +181,12 @@ 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;
|
||||||
@@ -206,6 +213,7 @@ export function LeftPane({
|
|||||||
getPreferredBadge,
|
getPreferredBadge,
|
||||||
hasExpiredDialog,
|
hasExpiredDialog,
|
||||||
hasFailedStorySends,
|
hasFailedStorySends,
|
||||||
|
hasCriticalIdlePrimaryDeviceAlert,
|
||||||
hasNetworkDialog,
|
hasNetworkDialog,
|
||||||
hasPendingUpdate,
|
hasPendingUpdate,
|
||||||
hasRelinkDialog,
|
hasRelinkDialog,
|
||||||
@@ -227,6 +235,7 @@ export function LeftPane({
|
|||||||
renderCaptchaDialog,
|
renderCaptchaDialog,
|
||||||
renderCrashReportDialog,
|
renderCrashReportDialog,
|
||||||
renderExpiredBuildDialog,
|
renderExpiredBuildDialog,
|
||||||
|
renderCriticalIdlePrimaryDeviceDialog,
|
||||||
renderMessageSearchResult,
|
renderMessageSearchResult,
|
||||||
renderNetworkStatus,
|
renderNetworkStatus,
|
||||||
renderUnsupportedOSDialog,
|
renderUnsupportedOSDialog,
|
||||||
@@ -611,6 +620,9 @@ export function LeftPane({
|
|||||||
maybeYellowDialog = renderNetworkStatus(commonDialogProps);
|
maybeYellowDialog = renderNetworkStatus(commonDialogProps);
|
||||||
} else if (hasRelinkDialog) {
|
} else if (hasRelinkDialog) {
|
||||||
maybeYellowDialog = renderRelinkDialog(commonDialogProps);
|
maybeYellowDialog = renderRelinkDialog(commonDialogProps);
|
||||||
|
} else if (hasCriticalIdlePrimaryDeviceAlert) {
|
||||||
|
maybeYellowDialog =
|
||||||
|
renderCriticalIdlePrimaryDeviceDialog(commonDialogProps);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update dialog
|
// Update dialog
|
||||||
|
@@ -23,6 +23,7 @@ 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';
|
||||||
@@ -55,6 +56,7 @@ export const actionCreators: ReduxActions = {
|
|||||||
network,
|
network,
|
||||||
safetyNumber,
|
safetyNumber,
|
||||||
search,
|
search,
|
||||||
|
server,
|
||||||
stickers,
|
stickers,
|
||||||
stories,
|
stories,
|
||||||
storyDistributionLists,
|
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 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';
|
||||||
@@ -144,6 +145,7 @@ 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,6 +83,7 @@ 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,6 +27,7 @@ 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';
|
||||||
@@ -60,6 +61,7 @@ export const reducer = combineReducers({
|
|||||||
preferredReactions,
|
preferredReactions,
|
||||||
safetyNumber,
|
safetyNumber,
|
||||||
search,
|
search,
|
||||||
|
server,
|
||||||
stickers,
|
stickers,
|
||||||
stories,
|
stories,
|
||||||
storyDistributionLists,
|
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,
|
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} />;
|
||||||
@@ -123,7 +126,14 @@ 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} />;
|
||||||
}
|
}
|
||||||
@@ -297,6 +307,10 @@ export const SmartLeftPane = memo(function SmartLeftPane({
|
|||||||
const backupMediaDownloadProgress = useSelector(
|
const backupMediaDownloadProgress = useSelector(
|
||||||
getBackupMediaDownloadProgress
|
getBackupMediaDownloadProgress
|
||||||
);
|
);
|
||||||
|
const hasCriticalIdlePrimaryDeviceAlert = useSelector(
|
||||||
|
getHasCriticalIdlePrimaryDeviceAlert
|
||||||
|
);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
blockConversation,
|
blockConversation,
|
||||||
clearGroupCreationError,
|
clearGroupCreationError,
|
||||||
@@ -388,6 +402,7 @@ 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}
|
||||||
@@ -409,6 +424,9 @@ 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}
|
||||||
|
@@ -23,6 +23,7 @@ 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';
|
||||||
@@ -54,6 +55,7 @@ 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;
|
||||||
|
@@ -252,12 +252,15 @@ export async function createGroup(
|
|||||||
await phone.setStorageState(state);
|
await phone.setStorageState(state);
|
||||||
return group;
|
return group;
|
||||||
}
|
}
|
||||||
|
export function getLeftPane(page: Page): Locator {
|
||||||
|
return page.locator('#LeftPane');
|
||||||
|
}
|
||||||
|
|
||||||
export async function clickOnConversationWithAci(
|
export async function clickOnConversationWithAci(
|
||||||
page: Page,
|
page: Page,
|
||||||
aci: string
|
aci: string
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const leftPane = page.locator('#LeftPane');
|
const leftPane = getLeftPane(page);
|
||||||
await leftPane.getByTestId(aci).click();
|
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 type { connection as WebSocket } from 'websocket';
|
||||||
import qs from 'querystring';
|
import qs from 'querystring';
|
||||||
import EventListener from 'events';
|
import EventListener from 'events';
|
||||||
|
import type { IncomingMessage } from 'http';
|
||||||
|
|
||||||
import type { AbortableProcess } from '../util/AbortableProcess';
|
import type { AbortableProcess } from '../util/AbortableProcess';
|
||||||
import { strictAssert } from '../util/assert';
|
import { strictAssert } from '../util/assert';
|
||||||
@@ -50,6 +51,7 @@ 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 { ServerAlert } from '../state/ducks/server';
|
||||||
|
|
||||||
const FIVE_MINUTES = 5 * durations.MINUTE;
|
const FIVE_MINUTES = 5 * durations.MINUTE;
|
||||||
|
|
||||||
@@ -220,6 +222,9 @@ export class SocketManager extends EventListener {
|
|||||||
Authorization: getBasicAuth({ username, password }),
|
Authorization: getBasicAuth({ username, password }),
|
||||||
'X-Signal-Receive-Stories': String(!this.#hasStoriesDisabled),
|
'X-Signal-Receive-Stories': String(!this.#hasStoriesDisabled),
|
||||||
},
|
},
|
||||||
|
onUpgradeResponse: (response: IncomingMessage) => {
|
||||||
|
this.#handleAuthenticatedUpgradeResponseHeaders(response.headers);
|
||||||
|
},
|
||||||
proxyAgent,
|
proxyAgent,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -758,6 +763,7 @@ export class SocketManager extends EventListener {
|
|||||||
resourceOptions,
|
resourceOptions,
|
||||||
query = {},
|
query = {},
|
||||||
extraHeaders = {},
|
extraHeaders = {},
|
||||||
|
onUpgradeResponse,
|
||||||
timeout,
|
timeout,
|
||||||
}: {
|
}: {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -766,6 +772,7 @@ export class SocketManager extends EventListener {
|
|||||||
resourceOptions: WebSocketResourceOptions;
|
resourceOptions: WebSocketResourceOptions;
|
||||||
query?: Record<string, string>;
|
query?: Record<string, string>;
|
||||||
extraHeaders?: Record<string, string>;
|
extraHeaders?: Record<string, string>;
|
||||||
|
onUpgradeResponse?: (response: IncomingMessage) => void;
|
||||||
timeout?: number;
|
timeout?: number;
|
||||||
}): AbortableProcess<IWebSocketResource> {
|
}): AbortableProcess<IWebSocketResource> {
|
||||||
const queryWithDefaults = {
|
const queryWithDefaults = {
|
||||||
@@ -787,6 +794,7 @@ export class SocketManager extends EventListener {
|
|||||||
timeout,
|
timeout,
|
||||||
|
|
||||||
extraHeaders,
|
extraHeaders,
|
||||||
|
onUpgradeResponse,
|
||||||
|
|
||||||
createResource(socket: WebSocket): WebSocketResource {
|
createResource(socket: WebSocket): WebSocketResource {
|
||||||
const duration = (performance.now() - start).toFixed(1);
|
const duration = (performance.now() - start).toFixed(1);
|
||||||
@@ -947,6 +955,33 @@ export class SocketManager extends EventListener {
|
|||||||
return this.#lazyProxyAgent;
|
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
|
// EventEmitter types
|
||||||
|
|
||||||
public override on(type: 'authError', callback: () => void): this;
|
public override on(type: 'authError', callback: () => void): this;
|
||||||
@@ -957,6 +992,10 @@ export class SocketManager extends EventListener {
|
|||||||
type: 'firstEnvelope',
|
type: 'firstEnvelope',
|
||||||
callback: (incoming: IncomingWebSocketRequest) => void
|
callback: (incoming: IncomingWebSocketRequest) => void
|
||||||
): this;
|
): this;
|
||||||
|
public override on(
|
||||||
|
type: 'serverAlerts',
|
||||||
|
callback: (alerts: Array<ServerAlert>) => void
|
||||||
|
): this;
|
||||||
|
|
||||||
public override on(
|
public override on(
|
||||||
type: string | symbol,
|
type: string | symbol,
|
||||||
@@ -974,6 +1013,10 @@ export class SocketManager extends EventListener {
|
|||||||
type: 'firstEnvelope',
|
type: 'firstEnvelope',
|
||||||
incoming: IncomingWebSocketRequest
|
incoming: IncomingWebSocketRequest
|
||||||
): boolean;
|
): boolean;
|
||||||
|
public override emit(
|
||||||
|
type: 'serverAlerts',
|
||||||
|
alerts: Array<ServerAlert>
|
||||||
|
): boolean;
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
public override emit(type: string | symbol, ...args: Array<any>): boolean {
|
public override emit(type: string | symbol, ...args: Array<any>): boolean {
|
||||||
|
@@ -1812,6 +1812,10 @@ export function initialize({
|
|||||||
window.Whisper.events.trigger('firstEnvelope', incoming);
|
window.Whisper.events.trigger('firstEnvelope', incoming);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
socketManager.on('serverAlerts', alerts => {
|
||||||
|
window.Whisper.events.trigger('serverAlerts', alerts);
|
||||||
|
});
|
||||||
|
|
||||||
if (useWebSocket) {
|
if (useWebSocket) {
|
||||||
void socketManager.authenticate({ username, password });
|
void socketManager.authenticate({ username, password });
|
||||||
}
|
}
|
||||||
|
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
import { client as WebSocketClient } from 'websocket';
|
import { client as WebSocketClient } from 'websocket';
|
||||||
import type { connection as WebSocket } from 'websocket';
|
import type { connection as WebSocket } from 'websocket';
|
||||||
|
import type { IncomingMessage } from 'http';
|
||||||
|
|
||||||
import { AbortableProcess } from '../util/AbortableProcess';
|
import { AbortableProcess } from '../util/AbortableProcess';
|
||||||
import { strictAssert } from '../util/assert';
|
import { strictAssert } from '../util/assert';
|
||||||
@@ -32,6 +33,7 @@ export type ConnectOptionsType<Resource extends IResource> = Readonly<{
|
|||||||
proxyAgent?: ProxyAgent;
|
proxyAgent?: ProxyAgent;
|
||||||
timeout?: number;
|
timeout?: number;
|
||||||
extraHeaders?: Record<string, string>;
|
extraHeaders?: Record<string, string>;
|
||||||
|
onUpgradeResponse?: (response: IncomingMessage) => void;
|
||||||
|
|
||||||
createResource(socket: WebSocket): Resource;
|
createResource(socket: WebSocket): Resource;
|
||||||
}>;
|
}>;
|
||||||
@@ -44,6 +46,7 @@ export function connect<Resource extends IResource>({
|
|||||||
proxyAgent,
|
proxyAgent,
|
||||||
extraHeaders = {},
|
extraHeaders = {},
|
||||||
timeout = WEBSOCKET_CONNECT_TIMEOUT,
|
timeout = WEBSOCKET_CONNECT_TIMEOUT,
|
||||||
|
onUpgradeResponse,
|
||||||
createResource,
|
createResource,
|
||||||
}: ConnectOptionsType<Resource>): AbortableProcess<Resource> {
|
}: ConnectOptionsType<Resource>): AbortableProcess<Resource> {
|
||||||
const fixedScheme = url
|
const fixedScheme = url
|
||||||
@@ -84,6 +87,10 @@ export function connect<Resource extends IResource>({
|
|||||||
resolve(resource);
|
resolve(resource);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
client.on('upgradeResponse', response => {
|
||||||
|
onUpgradeResponse?.(response);
|
||||||
|
});
|
||||||
|
|
||||||
client.on('httpResponse', async response => {
|
client.on('httpResponse', async response => {
|
||||||
Timers.clearTimeout(timer);
|
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