Support server-determined build expiration
This commit is contained in:
parent
a04f9a0e51
commit
d87335f5a6
14 changed files with 147 additions and 28 deletions
|
@ -1543,6 +1543,25 @@
|
||||||
await window.Signal.RemoteConfig.maybeRefreshRemoteConfig();
|
await window.Signal.RemoteConfig.maybeRefreshRemoteConfig();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Listen for changes to the `desktop.clientExpiration` remote flag
|
||||||
|
window.Signal.RemoteConfig.onChange(
|
||||||
|
'desktop.clientExpiration',
|
||||||
|
({ value }) => {
|
||||||
|
const remoteBuildExpirationTimestamp = window.Signal.Util.parseRemoteClientExpiration(
|
||||||
|
value
|
||||||
|
);
|
||||||
|
if (remoteBuildExpirationTimestamp) {
|
||||||
|
window.storage.put(
|
||||||
|
'remoteBuildExpiration',
|
||||||
|
remoteBuildExpirationTimestamp
|
||||||
|
);
|
||||||
|
window.reduxActions.expiration.hydrateExpirationStatus(
|
||||||
|
window.Signal.Util.hasExpired()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
// Listen for changes to the `desktop.messageRequests` remote configuration flag
|
// Listen for changes to the `desktop.messageRequests` remote configuration flag
|
||||||
const removeMessageRequestListener = window.Signal.RemoteConfig.onChange(
|
const removeMessageRequestListener = window.Signal.RemoteConfig.onChange(
|
||||||
'desktop.messageRequests',
|
'desktop.messageRequests',
|
||||||
|
|
10
preload.js
10
preload.js
|
@ -34,7 +34,15 @@ try {
|
||||||
window.getEnvironment = () => config.environment;
|
window.getEnvironment = () => config.environment;
|
||||||
window.getAppInstance = () => config.appInstance;
|
window.getAppInstance = () => config.appInstance;
|
||||||
window.getVersion = () => config.version;
|
window.getVersion = () => config.version;
|
||||||
window.getExpiration = () => config.buildExpiration;
|
window.getExpiration = () => {
|
||||||
|
const remoteBuildExpiration = window.storage.get('remoteBuildExpiration');
|
||||||
|
if (remoteBuildExpiration) {
|
||||||
|
return remoteBuildExpiration < config.buildExpiration
|
||||||
|
? remoteBuildExpiration
|
||||||
|
: config.buildExpiration;
|
||||||
|
}
|
||||||
|
return config.buildExpiration;
|
||||||
|
};
|
||||||
window.getNodeVersion = () => config.node_version;
|
window.getNodeVersion = () => config.node_version;
|
||||||
window.getHostName = () => config.hostname;
|
window.getHostName = () => config.hostname;
|
||||||
window.getServerTrustRoot = () => config.serverTrustRoot;
|
window.getServerTrustRoot = () => config.serverTrustRoot;
|
||||||
|
|
|
@ -1,11 +1,16 @@
|
||||||
import { get, throttle } from 'lodash';
|
import { get, throttle } from 'lodash';
|
||||||
import { WebAPIType } from './textsecure/WebAPI';
|
import { WebAPIType } from './textsecure/WebAPI';
|
||||||
|
|
||||||
type ConfigKeyType = 'desktop.messageRequests' | 'desktop.gv2' | 'desktop.cds';
|
type ConfigKeyType =
|
||||||
|
| 'desktop.messageRequests'
|
||||||
|
| 'desktop.gv2'
|
||||||
|
| 'desktop.cds'
|
||||||
|
| 'desktop.clientExpiration';
|
||||||
type ConfigValueType = {
|
type ConfigValueType = {
|
||||||
name: ConfigKeyType;
|
name: ConfigKeyType;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
enabledAt?: number;
|
enabledAt?: number;
|
||||||
|
value?: unknown;
|
||||||
};
|
};
|
||||||
type ConfigMapType = { [key: string]: ConfigValueType };
|
type ConfigMapType = { [key: string]: ConfigValueType };
|
||||||
type ConfigListenerType = (value: ConfigValueType) => unknown;
|
type ConfigListenerType = (value: ConfigValueType) => unknown;
|
||||||
|
@ -51,31 +56,36 @@ export const refreshRemoteConfig = async () => {
|
||||||
// The old configuration is not set as the initial value in reduce because
|
// The old configuration is not set as the initial value in reduce because
|
||||||
// flags may have been deleted
|
// flags may have been deleted
|
||||||
const oldConfig = config;
|
const oldConfig = config;
|
||||||
config = newConfig.reduce((previous, { name, enabled }) => {
|
config = newConfig.reduce((acc, { name, enabled, value }) => {
|
||||||
const previouslyEnabled: boolean = get(oldConfig, [name, 'enabled'], false);
|
const previouslyEnabled: boolean = get(oldConfig, [name, 'enabled'], false);
|
||||||
|
const previousValue: unknown = get(oldConfig, [name, 'value'], undefined);
|
||||||
// If a flag was previously not enabled and is now enabled, record the time it was enabled
|
// If a flag was previously not enabled and is now enabled, record the time it was enabled
|
||||||
const enabledAt: number | undefined =
|
const enabledAt: number | undefined =
|
||||||
previouslyEnabled && enabled ? now : get(oldConfig, [name, 'enabledAt']);
|
previouslyEnabled && enabled ? now : get(oldConfig, [name, 'enabledAt']);
|
||||||
|
|
||||||
const value = {
|
const configValue = {
|
||||||
name: name as ConfigKeyType,
|
name: name as ConfigKeyType,
|
||||||
enabled,
|
enabled,
|
||||||
enabledAt,
|
enabledAt,
|
||||||
|
value,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const hasChanged =
|
||||||
|
previouslyEnabled !== enabled || previousValue !== configValue.value;
|
||||||
|
|
||||||
// If enablement changes at all, notify listeners
|
// If enablement changes at all, notify listeners
|
||||||
const currentListeners = listeners[name] || [];
|
const currentListeners = listeners[name] || [];
|
||||||
if (previouslyEnabled !== enabled) {
|
if (hasChanged) {
|
||||||
window.log.info(`Remote Config: Flag ${name} has been enabled`);
|
window.log.info(`Remote Config: Flag ${name} has changed`);
|
||||||
currentListeners.forEach(listener => {
|
currentListeners.forEach(listener => {
|
||||||
listener(value);
|
listener(configValue);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return new configuration object
|
// Return new configuration object
|
||||||
return {
|
return {
|
||||||
...previous,
|
...acc,
|
||||||
[name]: value,
|
[name]: configValue,
|
||||||
};
|
};
|
||||||
}, {});
|
}, {});
|
||||||
|
|
||||||
|
|
|
@ -62,7 +62,7 @@ type ToggleVerifiedFulfilledActionType = {
|
||||||
payload: ToggleVerifiedAsyncActionType;
|
payload: ToggleVerifiedAsyncActionType;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SafetyNumberActionTypes =
|
export type SafetyNumberActionType =
|
||||||
| GenerateActionType
|
| GenerateActionType
|
||||||
| GenerateFulfilledActionType
|
| GenerateFulfilledActionType
|
||||||
| ToggleVerifiedActionType
|
| ToggleVerifiedActionType
|
||||||
|
@ -161,7 +161,7 @@ function getEmptyState(): SafetyNumberStateType {
|
||||||
|
|
||||||
export function reducer(
|
export function reducer(
|
||||||
state: SafetyNumberStateType = getEmptyState(),
|
state: SafetyNumberStateType = getEmptyState(),
|
||||||
action: SafetyNumberActionTypes
|
action: SafetyNumberActionType
|
||||||
): SafetyNumberStateType {
|
): SafetyNumberStateType {
|
||||||
if (action.type === TOGGLE_VERIFIED_PENDING) {
|
if (action.type === TOGGLE_VERIFIED_PENDING) {
|
||||||
const { contact } = action.payload;
|
const { contact } = action.payload;
|
||||||
|
|
|
@ -106,7 +106,7 @@ type SearchInConversationActionType = {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SEARCH_TYPES =
|
export type SearchActionType =
|
||||||
| SearchMessagesResultsKickoffActionType
|
| SearchMessagesResultsKickoffActionType
|
||||||
| SearchDiscussionsResultsKickoffActionType
|
| SearchDiscussionsResultsKickoffActionType
|
||||||
| SearchMessagesResultsFulfilledActionType
|
| SearchMessagesResultsFulfilledActionType
|
||||||
|
@ -336,7 +336,7 @@ function getEmptyState(): SearchStateType {
|
||||||
// tslint:disable-next-line cyclomatic-complexity max-func-body-length
|
// tslint:disable-next-line cyclomatic-complexity max-func-body-length
|
||||||
export function reducer(
|
export function reducer(
|
||||||
state: SearchStateType = getEmptyState(),
|
state: SearchStateType = getEmptyState(),
|
||||||
action: SEARCH_TYPES
|
action: SearchActionType
|
||||||
): SearchStateType {
|
): SearchStateType {
|
||||||
if (action.type === 'SHOW_ARCHIVED_CONVERSATIONS') {
|
if (action.type === 'SHOW_ARCHIVED_CONVERSATIONS') {
|
||||||
return getEmptyState();
|
return getEmptyState();
|
||||||
|
|
|
@ -32,12 +32,12 @@ import {
|
||||||
} from './ducks/network';
|
} from './ducks/network';
|
||||||
import {
|
import {
|
||||||
reducer as safetyNumber,
|
reducer as safetyNumber,
|
||||||
SafetyNumberActionTypes,
|
SafetyNumberActionType,
|
||||||
SafetyNumberStateType,
|
SafetyNumberStateType,
|
||||||
} from './ducks/safetyNumber';
|
} from './ducks/safetyNumber';
|
||||||
import {
|
import {
|
||||||
reducer as search,
|
reducer as search,
|
||||||
SEARCH_TYPES as SearchActionType,
|
SearchActionType,
|
||||||
SearchStateType,
|
SearchStateType,
|
||||||
} from './ducks/search';
|
} from './ducks/search';
|
||||||
import {
|
import {
|
||||||
|
@ -73,7 +73,7 @@ export type ActionsType =
|
||||||
| ConversationActionType
|
| ConversationActionType
|
||||||
| ItemsActionType
|
| ItemsActionType
|
||||||
| NetworkActionType
|
| NetworkActionType
|
||||||
| SafetyNumberActionTypes
|
| SafetyNumberActionType
|
||||||
| StickersActionType
|
| StickersActionType
|
||||||
| SearchActionType
|
| SearchActionType
|
||||||
| UpdatesActionType;
|
| UpdatesActionType;
|
||||||
|
|
25
ts/state/types.ts
Normal file
25
ts/state/types.ts
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
import { actions as calling } from './ducks/calling';
|
||||||
|
import { actions as conversations } from './ducks/conversations';
|
||||||
|
import { actions as emojis } from './ducks/emojis';
|
||||||
|
import { actions as expiration } from './ducks/expiration';
|
||||||
|
import { actions as items } from './ducks/items';
|
||||||
|
import { actions as network } from './ducks/network';
|
||||||
|
import { actions as safetyNumber } from './ducks/safetyNumber';
|
||||||
|
import { actions as search } from './ducks/search';
|
||||||
|
import { actions as stickers } from './ducks/stickers';
|
||||||
|
import { actions as updates } from './ducks/updates';
|
||||||
|
import { actions as user } from './ducks/user';
|
||||||
|
|
||||||
|
export type ReduxActions = {
|
||||||
|
calling: typeof calling;
|
||||||
|
conversations: typeof conversations;
|
||||||
|
emojis: typeof emojis;
|
||||||
|
expiration: typeof expiration;
|
||||||
|
items: typeof items;
|
||||||
|
network: typeof network;
|
||||||
|
safetyNumber: typeof safetyNumber;
|
||||||
|
search: typeof search;
|
||||||
|
stickers: typeof stickers;
|
||||||
|
updates: typeof updates;
|
||||||
|
user: typeof user;
|
||||||
|
};
|
|
@ -30,6 +30,7 @@ import {
|
||||||
getRandomValue,
|
getRandomValue,
|
||||||
splitUuids,
|
splitUuids,
|
||||||
} from '../Crypto';
|
} from '../Crypto';
|
||||||
|
import { getUserAgent } from '../util/getUserAgent';
|
||||||
|
|
||||||
import PQueue from 'p-queue';
|
import PQueue from 'p-queue';
|
||||||
import { v4 as getGuid } from 'uuid';
|
import { v4 as getGuid } from 'uuid';
|
||||||
|
@ -243,7 +244,8 @@ function _createSocket(
|
||||||
{
|
{
|
||||||
certificateAuthority,
|
certificateAuthority,
|
||||||
proxyUrl,
|
proxyUrl,
|
||||||
}: { certificateAuthority: string; proxyUrl?: string }
|
version,
|
||||||
|
}: { certificateAuthority: string; proxyUrl?: string; version: string }
|
||||||
) {
|
) {
|
||||||
let requestOptions;
|
let requestOptions;
|
||||||
if (proxyUrl) {
|
if (proxyUrl) {
|
||||||
|
@ -256,8 +258,10 @@ function _createSocket(
|
||||||
ca: certificateAuthority,
|
ca: certificateAuthority,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
const headers = {
|
||||||
return new WebSocket(url, undefined, undefined, undefined, requestOptions, {
|
'User-Agent': getUserAgent(version),
|
||||||
|
};
|
||||||
|
return new WebSocket(url, undefined, undefined, headers, requestOptions, {
|
||||||
maxReceivedFrameSize: 0x210000,
|
maxReceivedFrameSize: 0x210000,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -366,7 +370,7 @@ async function _promiseAjax(
|
||||||
method: options.type,
|
method: options.type,
|
||||||
body: options.data,
|
body: options.data,
|
||||||
headers: {
|
headers: {
|
||||||
'User-Agent': `Signal Desktop ${options.version}`,
|
'User-Agent': getUserAgent(options.version),
|
||||||
'X-Signal-Agent': 'OWD',
|
'X-Signal-Agent': 'OWD',
|
||||||
...options.headers,
|
...options.headers,
|
||||||
} as HeaderListType,
|
} as HeaderListType,
|
||||||
|
@ -412,6 +416,13 @@ async function _promiseAjax(
|
||||||
fetch(url, fetchOptions)
|
fetch(url, fetchOptions)
|
||||||
// tslint:disable-next-line max-func-body-length
|
// tslint:disable-next-line max-func-body-length
|
||||||
.then(async response => {
|
.then(async response => {
|
||||||
|
// Build expired!
|
||||||
|
if (response.status === 499) {
|
||||||
|
window.log.error('Error: build expired');
|
||||||
|
window.storage.put('remoteBuildExpiration', Date.now());
|
||||||
|
window.reduxActions.expiration.hydrateExpirationStatus(true);
|
||||||
|
}
|
||||||
|
|
||||||
let resultPromise;
|
let resultPromise;
|
||||||
if (
|
if (
|
||||||
(options.responseType === 'json' ||
|
(options.responseType === 'json' ||
|
||||||
|
@ -797,7 +808,9 @@ export type WebAPIType = {
|
||||||
options: GroupCredentialsType
|
options: GroupCredentialsType
|
||||||
) => Promise<string>;
|
) => Promise<string>;
|
||||||
whoami: () => Promise<any>;
|
whoami: () => Promise<any>;
|
||||||
getConfig: () => Promise<Array<{ name: string; enabled: boolean }>>;
|
getConfig: () => Promise<
|
||||||
|
Array<{ name: string; enabled: boolean; value: string | null }>
|
||||||
|
>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SignedPreKeyType = {
|
export type SignedPreKeyType = {
|
||||||
|
@ -1021,7 +1034,7 @@ export function initialize({
|
||||||
|
|
||||||
async function getConfig() {
|
async function getConfig() {
|
||||||
type ResType = {
|
type ResType = {
|
||||||
config: Array<{ name: string; enabled: boolean }>;
|
config: Array<{ name: string; enabled: boolean; value: string | null }>;
|
||||||
};
|
};
|
||||||
const res: ResType = await _ajax({
|
const res: ResType = await _ajax({
|
||||||
call: 'config',
|
call: 'config',
|
||||||
|
@ -2023,7 +2036,7 @@ export function initialize({
|
||||||
|
|
||||||
return _createSocket(
|
return _createSocket(
|
||||||
`${fixedScheme}/v1/websocket/?login=${login}&password=${pass}&agent=OWD&version=${clientVersion}`,
|
`${fixedScheme}/v1/websocket/?login=${login}&password=${pass}&agent=OWD&version=${clientVersion}`,
|
||||||
{ certificateAuthority, proxyUrl }
|
{ certificateAuthority, proxyUrl, version }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2037,7 +2050,7 @@ export function initialize({
|
||||||
|
|
||||||
return _createSocket(
|
return _createSocket(
|
||||||
`${fixedScheme}/v1/websocket/provisioning/?agent=OWD&version=${clientVersion}`,
|
`${fixedScheme}/v1/websocket/provisioning/?agent=OWD&version=${clientVersion}`,
|
||||||
{ certificateAuthority, proxyUrl }
|
{ certificateAuthority, proxyUrl, version }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -22,6 +22,7 @@ import { app, BrowserWindow, dialog, ipcMain } from 'electron';
|
||||||
|
|
||||||
import { getTempPath } from '../../app/attachments';
|
import { getTempPath } from '../../app/attachments';
|
||||||
import { Dialogs } from '../types/Dialogs';
|
import { Dialogs } from '../types/Dialogs';
|
||||||
|
import { getUserAgent } from '../util/getUserAgent';
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import * as packageJson from '../../package.json';
|
import * as packageJson from '../../package.json';
|
||||||
|
@ -324,7 +325,7 @@ function getGotOptions(): GotOptions<null> {
|
||||||
ca,
|
ca,
|
||||||
headers: {
|
headers: {
|
||||||
'Cache-Control': 'no-cache',
|
'Cache-Control': 'no-cache',
|
||||||
'User-Agent': `Signal Desktop ${packageJson.version}`,
|
'User-Agent': getUserAgent(packageJson.version),
|
||||||
},
|
},
|
||||||
useElectronNet: false,
|
useElectronNet: false,
|
||||||
};
|
};
|
||||||
|
|
3
ts/util/getUserAgent.ts
Normal file
3
ts/util/getUserAgent.ts
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
export function getUserAgent(appVersion: string): string {
|
||||||
|
return `Signal-Desktop/${appVersion}`;
|
||||||
|
}
|
|
@ -11,11 +11,13 @@ import {
|
||||||
getPlaceholder as getSafetyNumberPlaceholder,
|
getPlaceholder as getSafetyNumberPlaceholder,
|
||||||
} from './safetyNumber';
|
} from './safetyNumber';
|
||||||
import { getStringForProfileChange } from './getStringForProfileChange';
|
import { getStringForProfileChange } from './getStringForProfileChange';
|
||||||
|
import { getUserAgent } from './getUserAgent';
|
||||||
import { hasExpired } from './hasExpired';
|
import { hasExpired } from './hasExpired';
|
||||||
import { isFileDangerous } from './isFileDangerous';
|
import { isFileDangerous } from './isFileDangerous';
|
||||||
import { makeLookup } from './makeLookup';
|
import { makeLookup } from './makeLookup';
|
||||||
import { migrateColor } from './migrateColor';
|
import { migrateColor } from './migrateColor';
|
||||||
import { missingCaseError } from './missingCaseError';
|
import { missingCaseError } from './missingCaseError';
|
||||||
|
import { parseRemoteClientExpiration } from './parseRemoteClientExpiration';
|
||||||
import * as zkgroup from './zkgroup';
|
import * as zkgroup from './zkgroup';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
@ -28,12 +30,14 @@ export {
|
||||||
generateSecurityNumber,
|
generateSecurityNumber,
|
||||||
getSafetyNumberPlaceholder,
|
getSafetyNumberPlaceholder,
|
||||||
getStringForProfileChange,
|
getStringForProfileChange,
|
||||||
|
getUserAgent,
|
||||||
GoogleChrome,
|
GoogleChrome,
|
||||||
hasExpired,
|
hasExpired,
|
||||||
isFileDangerous,
|
isFileDangerous,
|
||||||
makeLookup,
|
makeLookup,
|
||||||
migrateColor,
|
migrateColor,
|
||||||
missingCaseError,
|
missingCaseError,
|
||||||
|
parseRemoteClientExpiration,
|
||||||
Registration,
|
Registration,
|
||||||
zkgroup,
|
zkgroup,
|
||||||
};
|
};
|
||||||
|
|
|
@ -12920,7 +12920,7 @@
|
||||||
"rule": "jQuery-wrap(",
|
"rule": "jQuery-wrap(",
|
||||||
"path": "ts/textsecure/WebAPI.js",
|
"path": "ts/textsecure/WebAPI.js",
|
||||||
"line": " const byteBuffer = window.dcodeIO.ByteBuffer.wrap(quote, 'binary', window.dcodeIO.ByteBuffer.LITTLE_ENDIAN);",
|
"line": " const byteBuffer = window.dcodeIO.ByteBuffer.wrap(quote, 'binary', window.dcodeIO.ByteBuffer.LITTLE_ENDIAN);",
|
||||||
"lineNumber": 1213,
|
"lineNumber": 1223,
|
||||||
"reasonCategory": "falseMatch",
|
"reasonCategory": "falseMatch",
|
||||||
"updated": "2020-09-08T23:07:22.682Z"
|
"updated": "2020-09-08T23:07:22.682Z"
|
||||||
},
|
},
|
||||||
|
@ -12928,8 +12928,8 @@
|
||||||
"rule": "jQuery-wrap(",
|
"rule": "jQuery-wrap(",
|
||||||
"path": "ts/textsecure/WebAPI.ts",
|
"path": "ts/textsecure/WebAPI.ts",
|
||||||
"line": " const byteBuffer = window.dcodeIO.ByteBuffer.wrap(",
|
"line": " const byteBuffer = window.dcodeIO.ByteBuffer.wrap(",
|
||||||
"lineNumber": 2063,
|
"lineNumber": 2076,
|
||||||
"reasonCategory": "falseMatch",
|
"reasonCategory": "falseMatch",
|
||||||
"updated": "2020-09-08T23:07:22.682Z"
|
"updated": "2020-09-08T23:07:22.682Z"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
33
ts/util/parseRemoteClientExpiration.ts
Normal file
33
ts/util/parseRemoteClientExpiration.ts
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
import semver from 'semver';
|
||||||
|
|
||||||
|
type RemoteVersion = {
|
||||||
|
'min-version': string;
|
||||||
|
iso8601: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function parseRemoteClientExpiration(
|
||||||
|
remoteExpirationValue: string
|
||||||
|
): number | null {
|
||||||
|
const remoteVersions = JSON.parse(remoteExpirationValue) || [];
|
||||||
|
const ourVersion = window.getVersion();
|
||||||
|
|
||||||
|
return remoteVersions.reduce(
|
||||||
|
(acc: number | null, remoteVersion: RemoteVersion) => {
|
||||||
|
const minVersion = remoteVersion['min-version'];
|
||||||
|
const { iso8601 } = remoteVersion;
|
||||||
|
|
||||||
|
if (semver.gt(minVersion, ourVersion)) {
|
||||||
|
const timestamp = new Date(iso8601).getTime();
|
||||||
|
|
||||||
|
if (!acc) {
|
||||||
|
return timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
return timestamp < acc ? timestamp : acc;
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
null
|
||||||
|
);
|
||||||
|
}
|
3
ts/window.d.ts
vendored
3
ts/window.d.ts
vendored
|
@ -22,6 +22,7 @@ import { LocalizerType } from './types/Util';
|
||||||
import { CallHistoryDetailsType } from './types/Calling';
|
import { CallHistoryDetailsType } from './types/Calling';
|
||||||
import { ColorType } from './types/Colors';
|
import { ColorType } from './types/Colors';
|
||||||
import { ConversationController } from './ConversationController';
|
import { ConversationController } from './ConversationController';
|
||||||
|
import { ReduxActions } from './state/types';
|
||||||
import { SendOptionsType } from './textsecure/SendMessage';
|
import { SendOptionsType } from './textsecure/SendMessage';
|
||||||
import AccountManager from './textsecure/AccountManager';
|
import AccountManager from './textsecure/AccountManager';
|
||||||
import Data from './sql/Client';
|
import Data from './sql/Client';
|
||||||
|
@ -49,6 +50,7 @@ declare global {
|
||||||
getSocketStatus: () => number;
|
getSocketStatus: () => number;
|
||||||
getTitle: () => string;
|
getTitle: () => string;
|
||||||
waitForEmptyEventQueue: () => Promise<void>;
|
waitForEmptyEventQueue: () => Promise<void>;
|
||||||
|
getVersion: () => string;
|
||||||
showCallingPermissionsPopup: (forCamera: boolean) => Promise<void>;
|
showCallingPermissionsPopup: (forCamera: boolean) => Promise<void>;
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
isValidGuid: (maybeGuid: string) => boolean;
|
isValidGuid: (maybeGuid: string) => boolean;
|
||||||
|
@ -65,6 +67,7 @@ declare global {
|
||||||
};
|
};
|
||||||
normalizeUuids: (obj: any, paths: Array<string>, context: string) => any;
|
normalizeUuids: (obj: any, paths: Array<string>, context: string) => any;
|
||||||
platform: string;
|
platform: string;
|
||||||
|
reduxActions: ReduxActions;
|
||||||
restart: () => void;
|
restart: () => void;
|
||||||
showWindow: () => void;
|
showWindow: () => void;
|
||||||
setBadgeCount: (count: number) => void;
|
setBadgeCount: (count: number) => void;
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue