Remove backbone as a dependency
Co-authored-by: Yash <yash@signal.org> Co-authored-by: ayumi-signal <143036029+ayumi-signal@users.noreply.github.com> Co-authored-by: Jamie Kyle <113370520+jamiebuilds-signal@users.noreply.github.com> Co-authored-by: Fedor Indutny <79877362+indutny-signal@users.noreply.github.com> Co-authored-by: trevor-signal <131492920+trevor-signal@users.noreply.github.com>
This commit is contained in:
parent
4fc9793cae
commit
237e239e05
69 changed files with 963 additions and 2110 deletions
|
@ -104,7 +104,7 @@ const rules = {
|
||||||
// Prefer functional components with default params
|
// Prefer functional components with default params
|
||||||
'react/require-default-props': 'off',
|
'react/require-default-props': 'off',
|
||||||
|
|
||||||
// Empty fragments are used in adapters between backbone and react views.
|
// Empty fragments are used in adapters between models and react views.
|
||||||
'react/jsx-no-useless-fragment': [
|
'react/jsx-no-useless-fragment': [
|
||||||
'error',
|
'error',
|
||||||
{
|
{
|
||||||
|
|
|
@ -2959,31 +2959,6 @@ Signal Desktop makes use of the following open source projects.
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
SOFTWARE
|
SOFTWARE
|
||||||
|
|
||||||
## backbone
|
|
||||||
|
|
||||||
Copyright (c) 2010-2024 Jeremy Ashkenas, DocumentCloud
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person
|
|
||||||
obtaining a copy of this software and associated documentation
|
|
||||||
files (the "Software"), to deal in the Software without
|
|
||||||
restriction, including without limitation the rights to use,
|
|
||||||
copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the
|
|
||||||
Software is furnished to do so, subject to the following
|
|
||||||
conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be
|
|
||||||
included in all copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
||||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
|
||||||
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
||||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
|
||||||
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
||||||
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
||||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
|
||||||
OTHER DEALINGS IN THE SOFTWARE.
|
|
||||||
|
|
||||||
## blob-util
|
## blob-util
|
||||||
|
|
||||||
Apache License
|
Apache License
|
||||||
|
|
|
@ -3,7 +3,6 @@
|
||||||
|
|
||||||
import { run } from 'endanger';
|
import { run } from 'endanger';
|
||||||
|
|
||||||
import migrateBackboneToRedux from './rules/migrateBackboneToRedux';
|
|
||||||
import packageJsonVersionsShouldBePinned from './rules/packageJsonVersionsShouldBePinned';
|
import packageJsonVersionsShouldBePinned from './rules/packageJsonVersionsShouldBePinned';
|
||||||
import pnpmLockDepsShouldHaveIntegrity from './rules/pnpmLockDepsShouldHaveIntegrity';
|
import pnpmLockDepsShouldHaveIntegrity from './rules/pnpmLockDepsShouldHaveIntegrity';
|
||||||
|
|
||||||
|
@ -19,7 +18,6 @@ function isGitDeletedError(error: unknown) {
|
||||||
async function main() {
|
async function main() {
|
||||||
try {
|
try {
|
||||||
await run(
|
await run(
|
||||||
migrateBackboneToRedux(),
|
|
||||||
packageJsonVersionsShouldBePinned(),
|
packageJsonVersionsShouldBePinned(),
|
||||||
pnpmLockDepsShouldHaveIntegrity()
|
pnpmLockDepsShouldHaveIntegrity()
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,54 +0,0 @@
|
||||||
// Copyright 2022 Signal Messenger, LLC
|
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
|
|
||||||
import { Line, Rule } from 'endanger';
|
|
||||||
|
|
||||||
export default function migrateBackboneToRedux() {
|
|
||||||
return new Rule({
|
|
||||||
match: {
|
|
||||||
files: ['**/*.{js,jsx,ts,tsx}'],
|
|
||||||
},
|
|
||||||
messages: {
|
|
||||||
foundNewBackboneFile: `
|
|
||||||
**Prefer Redux**
|
|
||||||
Don't create new Backbone files, use Redux
|
|
||||||
`,
|
|
||||||
foundBackboneFileWithManyChanges: `
|
|
||||||
**Prefer Redux**
|
|
||||||
Migrate Backbone files to Redux when making major changes
|
|
||||||
`,
|
|
||||||
},
|
|
||||||
async run({ files, context }) {
|
|
||||||
for (let file of files.modifiedOrCreated) {
|
|
||||||
let lines = await file.lines();
|
|
||||||
let matchedLine: Line | null = null;
|
|
||||||
|
|
||||||
for (let line of lines) {
|
|
||||||
// Check for the most stable part of the backbone `import`
|
|
||||||
if (
|
|
||||||
(await line.contains("from 'backbone'")) ||
|
|
||||||
(await line.contains('window.Backbone'))
|
|
||||||
) {
|
|
||||||
matchedLine = line;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!matchedLine) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (file.created) {
|
|
||||||
context.warn('foundNewBackboneFile', { file, line: matchedLine });
|
|
||||||
} else if (file.modifiedOnly) {
|
|
||||||
if (await file.diff().changedBy({ added: 0.1 })) {
|
|
||||||
context.warn('foundBackboneFileWithManyChanges', {
|
|
||||||
file,
|
|
||||||
line: matchedLine,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -20,7 +20,7 @@ function has<T extends object, const K extends T[any]>(
|
||||||
return Object.hasOwn(value, key);
|
return Object.hasOwn(value, key);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function migrateBackboneToRedux() {
|
export default function pnpmLockDepsShouldHaveIntegrity() {
|
||||||
return new Rule({
|
return new Rule({
|
||||||
match: {
|
match: {
|
||||||
files: ['pnpm-lock.yaml'],
|
files: ['pnpm-lock.yaml'],
|
||||||
|
|
|
@ -139,7 +139,6 @@
|
||||||
"@tanstack/react-virtual": "3.11.2",
|
"@tanstack/react-virtual": "3.11.2",
|
||||||
"@types/dom-mediacapture-transform": "0.1.11",
|
"@types/dom-mediacapture-transform": "0.1.11",
|
||||||
"@types/fabric": "4.5.3",
|
"@types/fabric": "4.5.3",
|
||||||
"backbone": "1.6.0",
|
|
||||||
"blob-util": "2.0.2",
|
"blob-util": "2.0.2",
|
||||||
"blueimp-load-image": "5.16.0",
|
"blueimp-load-image": "5.16.0",
|
||||||
"blurhash": "2.0.5",
|
"blurhash": "2.0.5",
|
||||||
|
@ -256,7 +255,6 @@
|
||||||
"@storybook/types": "8.1.11",
|
"@storybook/types": "8.1.11",
|
||||||
"@tailwindcss/cli": "4.1.7",
|
"@tailwindcss/cli": "4.1.7",
|
||||||
"@tailwindcss/postcss": "4.1.7",
|
"@tailwindcss/postcss": "4.1.7",
|
||||||
"@types/backbone": "1.4.22",
|
|
||||||
"@types/blueimp-load-image": "5.16.6",
|
"@types/blueimp-load-image": "5.16.6",
|
||||||
"@types/chai": "4.3.16",
|
"@types/chai": "4.3.16",
|
||||||
"@types/chai-as-promised": "7.1.4",
|
"@types/chai-as-promised": "7.1.4",
|
||||||
|
@ -378,7 +376,6 @@
|
||||||
"react-contextmenu>react-dom": "18.3.1"
|
"react-contextmenu>react-dom": "18.3.1"
|
||||||
},
|
},
|
||||||
"patchedDependencies": {
|
"patchedDependencies": {
|
||||||
"@types/backbone@1.4.22": "patches/@types+backbone+1.4.22.patch",
|
|
||||||
"casual@1.6.2": "patches/casual+1.6.2.patch",
|
"casual@1.6.2": "patches/casual+1.6.2.patch",
|
||||||
"protobufjs@7.3.2": "patches/protobufjs+7.3.2.patch",
|
"protobufjs@7.3.2": "patches/protobufjs+7.3.2.patch",
|
||||||
"@types/express@4.17.21": "patches/@types+express+4.17.21.patch",
|
"@types/express@4.17.21": "patches/@types+express+4.17.21.patch",
|
||||||
|
@ -393,7 +390,6 @@
|
||||||
"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",
|
"@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",
|
"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",
|
||||||
"app-builder-lib": "patches/app-builder-lib.patch",
|
"app-builder-lib": "patches/app-builder-lib.patch",
|
||||||
|
|
|
@ -1,49 +0,0 @@
|
||||||
diff --git a/index.d.ts b/index.d.ts
|
|
||||||
index 15d9d4b..a431841 100644
|
|
||||||
--- a/index.d.ts
|
|
||||||
+++ b/index.d.ts
|
|
||||||
@@ -66,7 +66,7 @@ declare namespace Backbone {
|
|
||||||
collection?: Collection<TModel> | undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
- type CombinedModelConstructorOptions<E, M extends Model<any, any, E> = Model> = ModelConstructorOptions<M> & E;
|
|
||||||
+ type CombinedModelConstructorOptions<E, M extends Model<any, any, E> = Model<any, any, E>> = ModelConstructorOptions<M> & E;
|
|
||||||
|
|
||||||
interface ModelSetOptions extends Silenceable, Validable {}
|
|
||||||
|
|
||||||
@@ -204,7 +204,7 @@ declare namespace Backbone {
|
|
||||||
*/
|
|
||||||
static extend(properties: any, classProperties?: any): any;
|
|
||||||
|
|
||||||
- attributes: Partial<T>;
|
|
||||||
+ attributes: T;
|
|
||||||
changed: Partial<T>;
|
|
||||||
cidPrefix: string;
|
|
||||||
cid: string;
|
|
||||||
@@ -220,7 +220,7 @@ declare namespace Backbone {
|
|
||||||
* That works only if you set it in the constructor or the initialize method.
|
|
||||||
*/
|
|
||||||
defaults(): Partial<T>;
|
|
||||||
- id: string | number;
|
|
||||||
+ id: string;
|
|
||||||
idAttribute: string;
|
|
||||||
validationError: any;
|
|
||||||
|
|
||||||
@@ -251,7 +251,7 @@ declare namespace Backbone {
|
|
||||||
* return super.get("name");
|
|
||||||
* }
|
|
||||||
*/
|
|
||||||
- get<A extends _StringKey<T>>(attributeName: A): T[A] | undefined;
|
|
||||||
+ get<A extends _StringKey<T>>(attributeName: A): T[A];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* For strongly-typed assignment of attributes, use the `set` method only privately in public setter properties.
|
|
||||||
@@ -285,7 +285,7 @@ declare namespace Backbone {
|
|
||||||
previousAttributes(): Partial<T>;
|
|
||||||
save(attributes?: Partial<T> | null, options?: ModelSaveOptions): JQueryXHR;
|
|
||||||
unset(attribute: _StringKey<T>, options?: Silenceable): this;
|
|
||||||
- validate(attributes: Partial<T>, options?: any): any;
|
|
||||||
+ validate(attributes: T, options?: any): any;
|
|
||||||
private _validate(attributes: Partial<T>, options: any): boolean;
|
|
||||||
|
|
||||||
// mixins from underscore
|
|
File diff suppressed because one or more lines are too long
32
pnpm-lock.yaml
generated
32
pnpm-lock.yaml
generated
|
@ -15,9 +15,6 @@ overrides:
|
||||||
react-contextmenu>react-dom: 18.3.1
|
react-contextmenu>react-dom: 18.3.1
|
||||||
|
|
||||||
patchedDependencies:
|
patchedDependencies:
|
||||||
'@types/backbone@1.4.22':
|
|
||||||
hash: 9dace206a9f53e0e3b0203051b26aec1e92ad49744b156ad8076946356c6c8e7
|
|
||||||
path: patches/@types+backbone+1.4.22.patch
|
|
||||||
'@types/express@4.17.21':
|
'@types/express@4.17.21':
|
||||||
hash: 85d9b3f3cac67003e41b22245281f53b51d7d1badd0bcc222d547ab802599bae
|
hash: 85d9b3f3cac67003e41b22245281f53b51d7d1badd0bcc222d547ab802599bae
|
||||||
path: patches/@types+express+4.17.21.patch
|
path: patches/@types+express+4.17.21.patch
|
||||||
|
@ -36,9 +33,6 @@ patchedDependencies:
|
||||||
app-builder-lib:
|
app-builder-lib:
|
||||||
hash: b412b44a47bb3d2be98e6edffed5dc4286cc62ac3c02fef42d1557927baa2420
|
hash: b412b44a47bb3d2be98e6edffed5dc4286cc62ac3c02fef42d1557927baa2420
|
||||||
path: patches/app-builder-lib.patch
|
path: patches/app-builder-lib.patch
|
||||||
backbone@1.6.0:
|
|
||||||
hash: 342b4b6012f8aecfa041554256444cb25af75bc933cf2ab1e91c4f66a8e47a31
|
|
||||||
path: patches/backbone+1.6.0.patch
|
|
||||||
casual@1.6.2:
|
casual@1.6.2:
|
||||||
hash: b88b5052437cbdc1882137778b76ca5037f71b2a030ae9ef39dc97f51670d599
|
hash: b88b5052437cbdc1882137778b76ca5037f71b2a030ae9ef39dc97f51670d599
|
||||||
path: patches/casual+1.6.2.patch
|
path: patches/casual+1.6.2.patch
|
||||||
|
@ -158,9 +152,6 @@ importers:
|
||||||
'@types/fabric':
|
'@types/fabric':
|
||||||
specifier: 4.5.3
|
specifier: 4.5.3
|
||||||
version: 4.5.3(patch_hash=e5f339ecf72fbab1c91505e7713e127a7184bfe8164aa3a9afe9bf45a0ad6b89)
|
version: 4.5.3(patch_hash=e5f339ecf72fbab1c91505e7713e127a7184bfe8164aa3a9afe9bf45a0ad6b89)
|
||||||
backbone:
|
|
||||||
specifier: 1.6.0
|
|
||||||
version: 1.6.0(patch_hash=342b4b6012f8aecfa041554256444cb25af75bc933cf2ab1e91c4f66a8e47a31)
|
|
||||||
blob-util:
|
blob-util:
|
||||||
specifier: 2.0.2
|
specifier: 2.0.2
|
||||||
version: 2.0.2
|
version: 2.0.2
|
||||||
|
@ -504,9 +495,6 @@ importers:
|
||||||
'@tailwindcss/postcss':
|
'@tailwindcss/postcss':
|
||||||
specifier: 4.1.7
|
specifier: 4.1.7
|
||||||
version: 4.1.7
|
version: 4.1.7
|
||||||
'@types/backbone':
|
|
||||||
specifier: 1.4.22
|
|
||||||
version: 1.4.22(patch_hash=9dace206a9f53e0e3b0203051b26aec1e92ad49744b156ad8076946356c6c8e7)
|
|
||||||
'@types/blueimp-load-image':
|
'@types/blueimp-load-image':
|
||||||
specifier: 5.16.6
|
specifier: 5.16.6
|
||||||
version: 5.16.6
|
version: 5.16.6
|
||||||
|
@ -3780,9 +3768,6 @@ packages:
|
||||||
'@types/babel__traverse@7.20.6':
|
'@types/babel__traverse@7.20.6':
|
||||||
resolution: {integrity: sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==}
|
resolution: {integrity: sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==}
|
||||||
|
|
||||||
'@types/backbone@1.4.22':
|
|
||||||
resolution: {integrity: sha512-i79hj6XPfsJ37yBHUb9560luep8SPoAbGcpA9TeW1R6Jufk4hHZn5q0l2xuTVtugBcoLlxGQ5qOjaNLBPmqaAg==}
|
|
||||||
|
|
||||||
'@types/blueimp-load-image@5.16.6':
|
'@types/blueimp-load-image@5.16.6':
|
||||||
resolution: {integrity: sha512-e7s6CdDCUoBQdCe62Q6OS+DF68M8+ABxCEMh2Isjt4Fl3xuddljCHMN8mak48AMSVGGwUUtNRaZbkzgL5PEWew==}
|
resolution: {integrity: sha512-e7s6CdDCUoBQdCe62Q6OS+DF68M8+ABxCEMh2Isjt4Fl3xuddljCHMN8mak48AMSVGGwUUtNRaZbkzgL5PEWew==}
|
||||||
|
|
||||||
|
@ -4046,9 +4031,6 @@ packages:
|
||||||
'@types/stack-utils@2.0.3':
|
'@types/stack-utils@2.0.3':
|
||||||
resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==}
|
resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==}
|
||||||
|
|
||||||
'@types/underscore@1.13.0':
|
|
||||||
resolution: {integrity: sha512-L6LBgy1f0EFQZ+7uSA57+n2g/s4Qs5r06Vwrwn0/nuK1de+adz00NWaztRQ30aEqw5qOaWbPI8u2cGQ52lj6VA==}
|
|
||||||
|
|
||||||
'@types/unist@2.0.11':
|
'@types/unist@2.0.11':
|
||||||
resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==}
|
resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==}
|
||||||
|
|
||||||
|
@ -4625,9 +4607,6 @@ packages:
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@babel/core': ^7.0.0
|
'@babel/core': ^7.0.0
|
||||||
|
|
||||||
backbone@1.6.0:
|
|
||||||
resolution: {integrity: sha512-13PUjmsgw/49EowNcQvfG4gmczz1ximTMhUktj0Jfrjth0MVaTxehpU+qYYX4MxnuIuhmvBLC6/ayxuAGnOhbA==}
|
|
||||||
|
|
||||||
bail@1.0.5:
|
bail@1.0.5:
|
||||||
resolution: {integrity: sha512-xFbRxM1tahm08yHBP16MMjVUAvDaBMD38zsM9EMAUN61omwLmKlOpB/Zku5QkjZ8TZ4vn53pj+t518cH0S03RQ==}
|
resolution: {integrity: sha512-xFbRxM1tahm08yHBP16MMjVUAvDaBMD38zsM9EMAUN61omwLmKlOpB/Zku5QkjZ8TZ4vn53pj+t518cH0S03RQ==}
|
||||||
|
|
||||||
|
@ -14587,11 +14566,6 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/types': 7.26.8
|
'@babel/types': 7.26.8
|
||||||
|
|
||||||
'@types/backbone@1.4.22(patch_hash=9dace206a9f53e0e3b0203051b26aec1e92ad49744b156ad8076946356c6c8e7)':
|
|
||||||
dependencies:
|
|
||||||
'@types/jquery': 3.5.32
|
|
||||||
'@types/underscore': 1.13.0
|
|
||||||
|
|
||||||
'@types/blueimp-load-image@5.16.6': {}
|
'@types/blueimp-load-image@5.16.6': {}
|
||||||
|
|
||||||
'@types/body-parser@1.19.5':
|
'@types/body-parser@1.19.5':
|
||||||
|
@ -14890,8 +14864,6 @@ snapshots:
|
||||||
|
|
||||||
'@types/stack-utils@2.0.3': {}
|
'@types/stack-utils@2.0.3': {}
|
||||||
|
|
||||||
'@types/underscore@1.13.0': {}
|
|
||||||
|
|
||||||
'@types/unist@2.0.11': {}
|
'@types/unist@2.0.11': {}
|
||||||
|
|
||||||
'@types/use-sync-external-store@0.0.6': {}
|
'@types/use-sync-external-store@0.0.6': {}
|
||||||
|
@ -15611,10 +15583,6 @@ snapshots:
|
||||||
babel-plugin-jest-hoist: 29.6.3
|
babel-plugin-jest-hoist: 29.6.3
|
||||||
babel-preset-current-node-syntax: 1.1.0(@babel/core@7.26.0)
|
babel-preset-current-node-syntax: 1.1.0(@babel/core@7.26.0)
|
||||||
|
|
||||||
backbone@1.6.0(patch_hash=342b4b6012f8aecfa041554256444cb25af75bc933cf2ab1e91c4f66a8e47a31):
|
|
||||||
dependencies:
|
|
||||||
underscore: 1.13.7
|
|
||||||
|
|
||||||
bail@1.0.5: {}
|
bail@1.0.5: {}
|
||||||
|
|
||||||
balanced-match@1.0.2: {}
|
balanced-match@1.0.2: {}
|
||||||
|
|
|
@ -96,7 +96,7 @@ module.exports = {
|
||||||
// Prefer functional components with default params
|
// Prefer functional components with default params
|
||||||
'react/require-default-props': 'off',
|
'react/require-default-props': 'off',
|
||||||
|
|
||||||
// Empty fragments are used in adapters between backbone and react views.
|
// Empty fragments are used in adapters between models and react views.
|
||||||
'react/jsx-no-useless-fragment': [
|
'react/jsx-no-useless-fragment': [
|
||||||
'error',
|
'error',
|
||||||
{
|
{
|
||||||
|
|
2
ts/CI.ts
2
ts/CI.ts
|
@ -220,7 +220,7 @@ export function getCI({
|
||||||
}
|
}
|
||||||
|
|
||||||
function unlink() {
|
function unlink() {
|
||||||
window.Whisper.events.trigger('unlinkAndDisconnect');
|
window.Whisper.events.emit('unlinkAndDisconnect');
|
||||||
}
|
}
|
||||||
|
|
||||||
function print(...args: ReadonlyArray<unknown>) {
|
function print(...args: ReadonlyArray<unknown>) {
|
||||||
|
|
|
@ -96,7 +96,7 @@ export async function populateConversationWithMessages({
|
||||||
postSaveUpdates,
|
postSaveUpdates,
|
||||||
});
|
});
|
||||||
|
|
||||||
conversation.set('active_at', Date.now());
|
conversation.set({ active_at: Date.now() });
|
||||||
await DataWriter.updateConversation(conversation.attributes);
|
await DataWriter.updateConversation(conversation.attributes);
|
||||||
log.info(`${logId}: populating conversation complete`);
|
log.info(`${logId}: populating conversation complete`);
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,15 +4,6 @@
|
||||||
import { debounce, pick, uniq, without } from 'lodash';
|
import { debounce, pick, uniq, without } from 'lodash';
|
||||||
import PQueue from 'p-queue';
|
import PQueue from 'p-queue';
|
||||||
import { v4 as generateUuid } from 'uuid';
|
import { v4 as generateUuid } from 'uuid';
|
||||||
import { batch as batchDispatch } from 'react-redux';
|
|
||||||
|
|
||||||
import type {
|
|
||||||
ConversationModelCollectionType,
|
|
||||||
ConversationAttributesType,
|
|
||||||
ConversationAttributesTypeType,
|
|
||||||
ConversationRenderInfoType,
|
|
||||||
} from './model-types.d';
|
|
||||||
import type { ConversationModel } from './models/conversations';
|
|
||||||
|
|
||||||
import { DataReader, DataWriter } from './sql/Client';
|
import { DataReader, DataWriter } from './sql/Client';
|
||||||
import { createLogger } from './logging/log';
|
import { createLogger } from './logging/log';
|
||||||
|
@ -21,8 +12,12 @@ import { getAuthorId } from './messages/helpers';
|
||||||
import { maybeDeriveGroupV2Id } from './groups';
|
import { maybeDeriveGroupV2Id } from './groups';
|
||||||
import { assertDev, strictAssert } from './util/assert';
|
import { assertDev, strictAssert } from './util/assert';
|
||||||
import { drop } from './util/drop';
|
import { drop } from './util/drop';
|
||||||
import { isGroup, isGroupV1, isGroupV2 } from './util/whatTypeOfConversation';
|
import {
|
||||||
import type { ServiceIdString, AciString, PniString } from './types/ServiceId';
|
isDirectConversation,
|
||||||
|
isGroup,
|
||||||
|
isGroupV1,
|
||||||
|
isGroupV2,
|
||||||
|
} from './util/whatTypeOfConversation';
|
||||||
import {
|
import {
|
||||||
isServiceIdString,
|
isServiceIdString,
|
||||||
normalizePni,
|
normalizePni,
|
||||||
|
@ -42,6 +37,18 @@ import { isTestOrMockEnvironment } from './environment';
|
||||||
import { isConversationAccepted } from './util/isConversationAccepted';
|
import { isConversationAccepted } from './util/isConversationAccepted';
|
||||||
import { areWePending } from './util/groupMembershipUtils';
|
import { areWePending } from './util/groupMembershipUtils';
|
||||||
import { conversationJobQueue } from './jobs/conversationJobQueue';
|
import { conversationJobQueue } from './jobs/conversationJobQueue';
|
||||||
|
import { createBatcher } from './util/batcher';
|
||||||
|
import { validateConversation } from './util/validateConversation';
|
||||||
|
import { ConversationModel } from './models/conversations';
|
||||||
|
import { INITIAL_EXPIRE_TIMER_VERSION } from './util/expirationTimer';
|
||||||
|
import { missingCaseError } from './util/missingCaseError';
|
||||||
|
|
||||||
|
import type {
|
||||||
|
ConversationAttributesType,
|
||||||
|
ConversationAttributesTypeType,
|
||||||
|
ConversationRenderInfoType,
|
||||||
|
} from './model-types.d';
|
||||||
|
import type { ServiceIdString, AciString, PniString } from './types/ServiceId';
|
||||||
|
|
||||||
const log = createLogger('ConversationController');
|
const log = createLogger('ConversationController');
|
||||||
|
|
||||||
|
@ -129,11 +136,7 @@ async function safeCombineConversations(
|
||||||
|
|
||||||
const MAX_MESSAGE_BODY_LENGTH = 64 * 1024;
|
const MAX_MESSAGE_BODY_LENGTH = 64 * 1024;
|
||||||
|
|
||||||
const {
|
const { getAllConversations, getMessagesBySentAt } = DataReader;
|
||||||
getAllConversations,
|
|
||||||
getAllGroupsInvolvingServiceId,
|
|
||||||
getMessagesBySentAt,
|
|
||||||
} = DataReader;
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
migrateConversationMessages,
|
migrateConversationMessages,
|
||||||
|
@ -143,57 +146,197 @@ const {
|
||||||
updateConversations,
|
updateConversations,
|
||||||
} = DataWriter;
|
} = DataWriter;
|
||||||
|
|
||||||
// We have to run this in background.js, after all backbone models and collections on
|
|
||||||
// Whisper.* have been created. Once those are in typescript we can use more reasonable
|
|
||||||
// require statements for referencing these things, giving us more flexibility here.
|
|
||||||
export function start(): void {
|
|
||||||
const conversations = new window.Whisper.ConversationCollection();
|
|
||||||
|
|
||||||
window.ConversationController = new ConversationController(conversations);
|
|
||||||
window.getConversations = () => conversations;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ConversationController {
|
export class ConversationController {
|
||||||
#_initialFetchComplete = false;
|
#_initialFetchComplete = false;
|
||||||
#isReadOnly = false;
|
#isReadOnly = false;
|
||||||
|
|
||||||
private _initialPromise: undefined | Promise<void>;
|
#_initialPromise: undefined | Promise<void>;
|
||||||
|
|
||||||
|
#_conversations: Array<ConversationModel> = [];
|
||||||
#_conversationOpenStart = new Map<string, number>();
|
#_conversationOpenStart = new Map<string, number>();
|
||||||
#_hasQueueEmptied = false;
|
#_hasQueueEmptied = false;
|
||||||
#_combineConversationsQueue = new PQueue({ concurrency: 1 });
|
#_combineConversationsQueue = new PQueue({ concurrency: 1 });
|
||||||
#_signalConversationId: undefined | string;
|
#_signalConversationId: undefined | string;
|
||||||
|
|
||||||
constructor(private _conversations: ConversationModelCollectionType) {
|
#delayBeforeUpdatingRedux: (() => number) | undefined;
|
||||||
const debouncedUpdateUnreadCount = debounce(
|
#isAppStillLoading: (() => boolean) | undefined;
|
||||||
this.updateUnreadCount.bind(this),
|
|
||||||
SECOND,
|
|
||||||
{
|
|
||||||
leading: true,
|
|
||||||
maxWait: SECOND,
|
|
||||||
trailing: true,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
|
// lookups
|
||||||
|
#_byE164: Record<string, ConversationModel> = Object.create(null);
|
||||||
|
#_byServiceId: Record<string, ConversationModel> = Object.create(null);
|
||||||
|
#_byPni: Record<string, ConversationModel> = Object.create(null);
|
||||||
|
#_byGroupId: Record<string, ConversationModel> = Object.create(null);
|
||||||
|
#_byId: Record<string, ConversationModel> = Object.create(null);
|
||||||
|
|
||||||
|
#debouncedUpdateUnreadCount = debounce(
|
||||||
|
this.updateUnreadCount.bind(this),
|
||||||
|
SECOND,
|
||||||
|
{
|
||||||
|
leading: true,
|
||||||
|
maxWait: SECOND,
|
||||||
|
trailing: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
#convoUpdateBatcher = createBatcher<
|
||||||
|
| { type: 'change' | 'add'; conversation: ConversationModel }
|
||||||
|
| { type: 'remove'; id: string }
|
||||||
|
>({
|
||||||
|
name: 'changedConvoBatcher',
|
||||||
|
processBatch: batch => {
|
||||||
|
let changedOrAddedBatch = new Array<ConversationModel>();
|
||||||
|
const {
|
||||||
|
conversationsUpdated,
|
||||||
|
conversationRemoved,
|
||||||
|
onConversationClosed,
|
||||||
|
} = window.reduxActions.conversations;
|
||||||
|
|
||||||
|
function flushChangedOrAddedBatch() {
|
||||||
|
if (!changedOrAddedBatch.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
conversationsUpdated(
|
||||||
|
changedOrAddedBatch.map(conversation => conversation.format())
|
||||||
|
);
|
||||||
|
changedOrAddedBatch = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const item of batch) {
|
||||||
|
if (item.type === 'add' || item.type === 'change') {
|
||||||
|
changedOrAddedBatch.push(item.conversation);
|
||||||
|
} else {
|
||||||
|
strictAssert(item.type === 'remove', 'must be remove');
|
||||||
|
flushChangedOrAddedBatch();
|
||||||
|
|
||||||
|
onConversationClosed(item.id, 'removed');
|
||||||
|
conversationRemoved(item.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
flushChangedOrAddedBatch();
|
||||||
|
},
|
||||||
|
|
||||||
|
wait: () => {
|
||||||
|
return this.#delayBeforeUpdatingRedux?.() ?? 1;
|
||||||
|
},
|
||||||
|
maxSize: Infinity,
|
||||||
|
});
|
||||||
|
|
||||||
|
constructor() {
|
||||||
// A few things can cause us to update the app-level unread count
|
// A few things can cause us to update the app-level unread count
|
||||||
window.Whisper.events.on('updateUnreadCount', debouncedUpdateUnreadCount);
|
window.Whisper.events.on(
|
||||||
this._conversations.on(
|
'updateUnreadCount',
|
||||||
'add remove change:active_at change:unreadCount change:markedUnread change:isArchived change:muteExpiresAt',
|
this.#debouncedUpdateUnreadCount
|
||||||
debouncedUpdateUnreadCount
|
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// If the conversation is muted we set a timeout so when the mute expires
|
registerDelayBeforeUpdatingRedux(
|
||||||
// we can reset the mute state on the model. If the mute has already expired
|
delayBeforeUpdatingRedux: () => number
|
||||||
// then we reset the state right away.
|
): void {
|
||||||
this._conversations.on('add', (model: ConversationModel): void => {
|
this.#delayBeforeUpdatingRedux = delayBeforeUpdatingRedux;
|
||||||
// Don't modify conversations in backup integration testing
|
}
|
||||||
if (isTestOrMockEnvironment()) {
|
registerIsAppStillLoading(isAppStillLoading: () => boolean): void {
|
||||||
return;
|
this.#isAppStillLoading = isAppStillLoading;
|
||||||
|
}
|
||||||
|
|
||||||
|
conversationUpdated(
|
||||||
|
conversation: ConversationModel,
|
||||||
|
previousAttributes: ConversationAttributesType
|
||||||
|
): void {
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
conversation.cachedProps = undefined;
|
||||||
|
|
||||||
|
const hasAttributeChanged = (name: keyof ConversationAttributesType) => {
|
||||||
|
return (
|
||||||
|
name in conversation.attributes &&
|
||||||
|
conversation.attributes[name] !== previousAttributes[name]
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
this.#convoUpdateBatcher.add({ type: 'change', conversation });
|
||||||
|
|
||||||
|
if (isDirectConversation(conversation.attributes)) {
|
||||||
|
const updateLastMessage =
|
||||||
|
hasAttributeChanged('name') ||
|
||||||
|
hasAttributeChanged('profileName') ||
|
||||||
|
hasAttributeChanged('profileFamilyName') ||
|
||||||
|
hasAttributeChanged('e164');
|
||||||
|
|
||||||
|
const memberVerifiedChange = hasAttributeChanged('verified');
|
||||||
|
|
||||||
|
if (updateLastMessage || memberVerifiedChange) {
|
||||||
|
this.#updateAllGroupsWithMember(conversation, {
|
||||||
|
updateLastMessage,
|
||||||
|
memberVerifiedChange,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#updateAllGroupsWithMember(
|
||||||
|
member: ConversationModel,
|
||||||
|
{
|
||||||
|
updateLastMessage,
|
||||||
|
memberVerifiedChange,
|
||||||
|
}: { updateLastMessage: boolean; memberVerifiedChange: boolean }
|
||||||
|
): void {
|
||||||
|
const memberServiceId = member.getServiceId();
|
||||||
|
if (!memberServiceId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!updateLastMessage && !memberVerifiedChange) {
|
||||||
|
log.error(
|
||||||
|
`updateAllGroupsWithMember: Called for ${member.idForLogging()} but neither option set`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const groups = this.getAllGroupsInvolvingServiceId(memberServiceId);
|
||||||
|
|
||||||
|
groups.forEach(conversation => {
|
||||||
|
if (updateLastMessage) {
|
||||||
|
conversation.debouncedUpdateLastMessage();
|
||||||
|
}
|
||||||
|
if (memberVerifiedChange) {
|
||||||
|
conversation.onMemberVerifiedChange();
|
||||||
}
|
}
|
||||||
model.startMuteTimer();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#addConversation(conversation: ConversationModel): void {
|
||||||
|
this.#_conversations.push(conversation);
|
||||||
|
this.#addToLookup(conversation);
|
||||||
|
this.#debouncedUpdateUnreadCount();
|
||||||
|
|
||||||
|
// Don't modify conversations in backup integration testing
|
||||||
|
if (!isTestOrMockEnvironment()) {
|
||||||
|
// If the conversation is muted we set a timeout so when the mute expires
|
||||||
|
// we can reset the mute state on the model. If the mute has already expired
|
||||||
|
// then we reset the state right away.
|
||||||
|
conversation.startMuteTimer();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.#isAppStillLoading?.()) {
|
||||||
|
// The redux update will happen inside the batcher
|
||||||
|
this.#convoUpdateBatcher.add({ type: 'add', conversation });
|
||||||
|
} else {
|
||||||
|
const { conversationsUpdated } = window.reduxActions.conversations;
|
||||||
|
|
||||||
|
// During normal app usage, we require conversations to be added synchronously
|
||||||
|
conversationsUpdated([conversation.format()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#removeConversation(conversation: ConversationModel): void {
|
||||||
|
this.#_conversations = without(this.#_conversations, conversation);
|
||||||
|
this.#removeFromLookup(conversation);
|
||||||
|
this.#debouncedUpdateUnreadCount();
|
||||||
|
|
||||||
|
const { id } = conversation || {};
|
||||||
|
|
||||||
|
// The redux update call will happen inside the batcher
|
||||||
|
this.#convoUpdateBatcher.add({ type: 'remove', id });
|
||||||
|
}
|
||||||
|
|
||||||
updateUnreadCount(): void {
|
updateUnreadCount(): void {
|
||||||
if (!this.#_hasQueueEmptied) {
|
if (!this.#_hasQueueEmptied) {
|
||||||
return;
|
return;
|
||||||
|
@ -203,7 +346,7 @@ export class ConversationController {
|
||||||
window.storage.get('badge-count-muted-conversations') || false;
|
window.storage.get('badge-count-muted-conversations') || false;
|
||||||
|
|
||||||
const unreadStats = countAllConversationsUnreadStats(
|
const unreadStats = countAllConversationsUnreadStats(
|
||||||
this._conversations.map(
|
this.#_conversations.map(
|
||||||
(conversation): ConversationPropsForUnreadStats => {
|
(conversation): ConversationPropsForUnreadStats => {
|
||||||
// Need to pull this out manually into the Redux shape
|
// Need to pull this out manually into the Redux shape
|
||||||
// because `conversation.format()` can return cached props by the
|
// because `conversation.format()` can return cached props by the
|
||||||
|
@ -251,24 +394,39 @@ export class ConversationController {
|
||||||
'ConversationController.get() needs complete initial fetch'
|
'ConversationController.get() needs complete initial fetch'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (!id) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
// This function takes null just fine. Backbone typings are too restrictive.
|
return (
|
||||||
return this._conversations.get(id as string);
|
this.#_byE164[id] ||
|
||||||
|
this.#_byE164[`+${id}`] ||
|
||||||
|
this.#_byServiceId[id] ||
|
||||||
|
this.#_byPni[id] ||
|
||||||
|
this.#_byGroupId[id] ||
|
||||||
|
this.#_byId[id]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
getAll(): Array<ConversationModel> {
|
getAll(): Array<ConversationModel> {
|
||||||
return this._conversations.models;
|
return this.#_conversations;
|
||||||
}
|
}
|
||||||
|
|
||||||
dangerouslyCreateAndAdd(
|
dangerouslyCreateAndAdd(
|
||||||
attributes: Partial<ConversationAttributesType>
|
attributes: ConversationAttributesType
|
||||||
): ConversationModel {
|
): ConversationModel {
|
||||||
return this._conversations.add(attributes);
|
const model = new ConversationModel(attributes);
|
||||||
|
this.#addConversation(model);
|
||||||
|
return model;
|
||||||
}
|
}
|
||||||
|
|
||||||
dangerouslyRemoveById(id: string): void {
|
dangerouslyRemoveById(id: string): void {
|
||||||
this._conversations.remove(id);
|
const model = this.get(id);
|
||||||
this._conversations.resetLookups();
|
if (!model) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#removeConversation(model);
|
||||||
}
|
}
|
||||||
|
|
||||||
getOrCreate(
|
getOrCreate(
|
||||||
|
@ -292,7 +450,7 @@ export class ConversationController {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let conversation = this._conversations.get(identifier);
|
let conversation = this.get(identifier);
|
||||||
if (conversation) {
|
if (conversation) {
|
||||||
return conversation;
|
return conversation;
|
||||||
}
|
}
|
||||||
|
@ -304,44 +462,64 @@ export class ConversationController {
|
||||||
const id = generateUuid();
|
const id = generateUuid();
|
||||||
|
|
||||||
if (type === 'group') {
|
if (type === 'group') {
|
||||||
conversation = this._conversations.add({
|
conversation = new ConversationModel({
|
||||||
id,
|
id,
|
||||||
serviceId: undefined,
|
serviceId: undefined,
|
||||||
e164: undefined,
|
e164: undefined,
|
||||||
groupId: identifier,
|
groupId: identifier,
|
||||||
type,
|
type,
|
||||||
version: 2,
|
version: 2,
|
||||||
|
expireTimerVersion: INITIAL_EXPIRE_TIMER_VERSION,
|
||||||
|
unreadCount: 0,
|
||||||
|
verified: window.textsecure.storage.protocol.VerifiedStatus.DEFAULT,
|
||||||
|
messageCount: 0,
|
||||||
|
sentMessageCount: 0,
|
||||||
...additionalInitialProps,
|
...additionalInitialProps,
|
||||||
});
|
});
|
||||||
|
this.#addConversation(conversation);
|
||||||
} else if (isServiceIdString(identifier)) {
|
} else if (isServiceIdString(identifier)) {
|
||||||
conversation = this._conversations.add({
|
conversation = new ConversationModel({
|
||||||
id,
|
id,
|
||||||
serviceId: identifier,
|
serviceId: identifier,
|
||||||
e164: undefined,
|
e164: undefined,
|
||||||
groupId: undefined,
|
groupId: undefined,
|
||||||
type,
|
type,
|
||||||
version: 2,
|
version: 2,
|
||||||
|
expireTimerVersion: INITIAL_EXPIRE_TIMER_VERSION,
|
||||||
|
unreadCount: 0,
|
||||||
|
verified: window.textsecure.storage.protocol.VerifiedStatus.DEFAULT,
|
||||||
|
messageCount: 0,
|
||||||
|
sentMessageCount: 0,
|
||||||
...additionalInitialProps,
|
...additionalInitialProps,
|
||||||
});
|
});
|
||||||
|
this.#addConversation(conversation);
|
||||||
} else {
|
} else {
|
||||||
conversation = this._conversations.add({
|
conversation = new ConversationModel({
|
||||||
id,
|
id,
|
||||||
serviceId: undefined,
|
serviceId: undefined,
|
||||||
e164: identifier,
|
e164: identifier,
|
||||||
groupId: undefined,
|
groupId: undefined,
|
||||||
type,
|
type,
|
||||||
version: 2,
|
version: 2,
|
||||||
|
expireTimerVersion: INITIAL_EXPIRE_TIMER_VERSION,
|
||||||
|
unreadCount: 0,
|
||||||
|
verified: window.textsecure.storage.protocol.VerifiedStatus.DEFAULT,
|
||||||
|
messageCount: 0,
|
||||||
|
sentMessageCount: 0,
|
||||||
...additionalInitialProps,
|
...additionalInitialProps,
|
||||||
});
|
});
|
||||||
|
this.#addConversation(conversation);
|
||||||
}
|
}
|
||||||
|
|
||||||
const create = async () => {
|
const create = async () => {
|
||||||
if (!conversation.isValid()) {
|
const validationErrorString = validateConversation(
|
||||||
const validationError = conversation.validationError || {};
|
conversation.attributes
|
||||||
|
);
|
||||||
|
if (validationErrorString) {
|
||||||
log.error(
|
log.error(
|
||||||
'Contact is not valid. Not saving, but adding to collection:',
|
'Contact is not valid. Not saving, but adding to collection:',
|
||||||
conversation.idForLogging(),
|
conversation.idForLogging(),
|
||||||
Errors.toLogFormat(validationError)
|
validationErrorString
|
||||||
);
|
);
|
||||||
|
|
||||||
return conversation;
|
return conversation;
|
||||||
|
@ -755,7 +933,7 @@ export class ConversationController {
|
||||||
(targetOldServiceIds.pni !== pni ||
|
(targetOldServiceIds.pni !== pni ||
|
||||||
(aci && targetOldServiceIds.aci !== aci))
|
(aci && targetOldServiceIds.aci !== aci))
|
||||||
) {
|
) {
|
||||||
targetConversation.unset('needsTitleTransition');
|
targetConversation.set({ needsTitleTransition: undefined });
|
||||||
mergePromises.push(
|
mergePromises.push(
|
||||||
targetConversation.addPhoneNumberDiscoveryIfNeeded(
|
targetConversation.addPhoneNumberDiscoveryIfNeeded(
|
||||||
targetOldServiceIds.pni
|
targetOldServiceIds.pni
|
||||||
|
@ -873,12 +1051,10 @@ export class ConversationController {
|
||||||
// We also want to find duplicate GV1 IDs. You might expect to see a "byGroupV1Id" map
|
// We also want to find duplicate GV1 IDs. You might expect to see a "byGroupV1Id" map
|
||||||
// here. Instead, we check for duplicates on the derived GV2 ID.
|
// here. Instead, we check for duplicates on the derived GV2 ID.
|
||||||
|
|
||||||
const { models } = this._conversations;
|
|
||||||
|
|
||||||
// We iterate from the oldest conversations to the newest. This allows us, in a
|
// We iterate from the oldest conversations to the newest. This allows us, in a
|
||||||
// conflict case, to keep the one with activity the most recently.
|
// conflict case, to keep the one with activity the most recently.
|
||||||
for (let i = models.length - 1; i >= 0; i -= 1) {
|
for (let i = this.#_conversations.length - 1; i >= 0; i -= 1) {
|
||||||
const conversation = models[i];
|
const conversation = this.#_conversations[i];
|
||||||
assertDev(
|
assertDev(
|
||||||
conversation,
|
conversation,
|
||||||
'Expected conversation to be found in array during iteration'
|
'Expected conversation to be found in array during iteration'
|
||||||
|
@ -1090,15 +1266,14 @@ export class ConversationController {
|
||||||
} else {
|
} else {
|
||||||
activeAt = obsoleteActiveAt || currentActiveAt;
|
activeAt = obsoleteActiveAt || currentActiveAt;
|
||||||
}
|
}
|
||||||
current.set('active_at', activeAt);
|
current.set({ active_at: activeAt });
|
||||||
|
|
||||||
current.set(
|
current.set({
|
||||||
'expireTimerVersion',
|
expireTimerVersion: Math.max(
|
||||||
Math.max(
|
|
||||||
obsolete.get('expireTimerVersion') ?? 1,
|
obsolete.get('expireTimerVersion') ?? 1,
|
||||||
current.get('expireTimerVersion') ?? 1
|
current.get('expireTimerVersion') ?? 1
|
||||||
)
|
),
|
||||||
);
|
});
|
||||||
|
|
||||||
const obsoleteExpireTimer = obsolete.get('expireTimer');
|
const obsoleteExpireTimer = obsolete.get('expireTimer');
|
||||||
const currentExpireTimer = current.get('expireTimer');
|
const currentExpireTimer = current.get('expireTimer');
|
||||||
|
@ -1106,7 +1281,7 @@ export class ConversationController {
|
||||||
!currentExpireTimer ||
|
!currentExpireTimer ||
|
||||||
(obsoleteExpireTimer && obsoleteExpireTimer < currentExpireTimer)
|
(obsoleteExpireTimer && obsoleteExpireTimer < currentExpireTimer)
|
||||||
) {
|
) {
|
||||||
current.set('expireTimer', obsoleteExpireTimer);
|
current.set({ expireTimer: obsoleteExpireTimer });
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentHadMessages = (current.get('messageCount') ?? 0) > 0;
|
const currentHadMessages = (current.get('messageCount') ?? 0) > 0;
|
||||||
|
@ -1136,11 +1311,11 @@ export class ConversationController {
|
||||||
>;
|
>;
|
||||||
keys.forEach(key => {
|
keys.forEach(key => {
|
||||||
if (current.get(key) === undefined) {
|
if (current.get(key) === undefined) {
|
||||||
current.set(key, dataToCopy[key]);
|
current.set({ [key]: dataToCopy[key] });
|
||||||
|
|
||||||
// To ensure that any files on disk don't get deleted out from under us
|
// To ensure that any files on disk don't get deleted out from under us
|
||||||
if (key === 'draftAttachments') {
|
if (key === 'draftAttachments') {
|
||||||
obsolete.set(key, undefined);
|
obsolete.set({ [key]: undefined });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -1244,8 +1419,7 @@ export class ConversationController {
|
||||||
log.warn(
|
log.warn(
|
||||||
`${logId}: Eliminate old conversation from ConversationController lookups`
|
`${logId}: Eliminate old conversation from ConversationController lookups`
|
||||||
);
|
);
|
||||||
this._conversations.remove(obsolete);
|
this.#removeConversation(obsolete);
|
||||||
this._conversations.resetLookups();
|
|
||||||
|
|
||||||
current.captureChange('combineConversations');
|
current.captureChange('combineConversations');
|
||||||
drop(current.updateLastMessage());
|
drop(current.updateLastMessage());
|
||||||
|
@ -1305,22 +1479,25 @@ export class ConversationController {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAllGroupsInvolvingServiceId(
|
getAllGroupsInvolvingServiceId(
|
||||||
serviceId: ServiceIdString
|
serviceId: ServiceIdString
|
||||||
): Promise<Array<ConversationModel>> {
|
): Array<ConversationModel> {
|
||||||
const groups = await getAllGroupsInvolvingServiceId(serviceId);
|
return this.#_conversations
|
||||||
return groups.map(group => {
|
.map(conversation => {
|
||||||
const existing = this.get(group.id);
|
if (!isGroup(conversation.attributes)) {
|
||||||
if (existing) {
|
return;
|
||||||
return existing;
|
}
|
||||||
}
|
if (!conversation.hasMember(serviceId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
return this._conversations.add(group);
|
return conversation;
|
||||||
});
|
})
|
||||||
|
.filter(isNotNil);
|
||||||
}
|
}
|
||||||
|
|
||||||
getByDerivedGroupV2Id(groupId: string): ConversationModel | undefined {
|
getByDerivedGroupV2Id(groupId: string): ConversationModel | undefined {
|
||||||
return this._conversations.find(
|
return this.#_conversations.find(
|
||||||
item => item.get('derivedGroupV2Id') === groupId
|
item => item.get('derivedGroupV2Id') === groupId
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1336,14 +1513,18 @@ export class ConversationController {
|
||||||
}
|
}
|
||||||
|
|
||||||
reset(): void {
|
reset(): void {
|
||||||
delete this._initialPromise;
|
const { removeAllConversations } = window.reduxActions.conversations;
|
||||||
|
|
||||||
|
this.#_initialPromise = undefined;
|
||||||
this.#_initialFetchComplete = false;
|
this.#_initialFetchComplete = false;
|
||||||
this._conversations.reset([]);
|
this.#_conversations = [];
|
||||||
|
removeAllConversations();
|
||||||
|
this.#resetLookups();
|
||||||
}
|
}
|
||||||
|
|
||||||
load(): Promise<void> {
|
load(): Promise<void> {
|
||||||
this._initialPromise ||= this.#doLoad();
|
this.#_initialPromise ||= this.#doLoad();
|
||||||
return this._initialPromise;
|
return this.#_initialPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
// A number of things outside conversation.attributes affect conversation re-rendering.
|
// A number of things outside conversation.attributes affect conversation re-rendering.
|
||||||
|
@ -1354,7 +1535,7 @@ export class ConversationController {
|
||||||
let count = 0;
|
let count = 0;
|
||||||
const conversations = identifiers
|
const conversations = identifiers
|
||||||
? identifiers.map(identifier => this.get(identifier)).filter(isNotNil)
|
? identifiers.map(identifier => this.get(identifier)).filter(isNotNil)
|
||||||
: this._conversations.models.slice();
|
: this.#_conversations.slice();
|
||||||
log.info(
|
log.info(
|
||||||
`forceRerender: Starting to loop through ${conversations.length} conversations`
|
`forceRerender: Starting to loop through ${conversations.length} conversations`
|
||||||
);
|
);
|
||||||
|
@ -1366,7 +1547,7 @@ export class ConversationController {
|
||||||
conversation.oldCachedProps = conversation.cachedProps;
|
conversation.oldCachedProps = conversation.cachedProps;
|
||||||
conversation.cachedProps = null;
|
conversation.cachedProps = null;
|
||||||
|
|
||||||
conversation.trigger('props-change', conversation, false);
|
this.conversationUpdated(conversation, conversation.attributes);
|
||||||
count += 1;
|
count += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1426,8 +1607,10 @@ export class ConversationController {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
conversation.set('avatar', undefined);
|
conversation.set({
|
||||||
conversation.set('profileAvatar', undefined);
|
avatar: undefined,
|
||||||
|
profileAvatar: undefined,
|
||||||
|
});
|
||||||
drop(updateConversation(conversation.attributes));
|
drop(updateConversation(conversation.attributes));
|
||||||
numberOfConversationsMigrated += 1;
|
numberOfConversationsMigrated += 1;
|
||||||
}
|
}
|
||||||
|
@ -1449,7 +1632,7 @@ export class ConversationController {
|
||||||
}
|
}
|
||||||
|
|
||||||
log.warn(`Repairing ${convo.idForLogging()}'s isPinned`);
|
log.warn(`Repairing ${convo.idForLogging()}'s isPinned`);
|
||||||
convo.set('isPinned', true);
|
convo.set({ isPinned: true });
|
||||||
|
|
||||||
drop(updateConversation(convo.attributes));
|
drop(updateConversation(convo.attributes));
|
||||||
}
|
}
|
||||||
|
@ -1469,7 +1652,7 @@ export class ConversationController {
|
||||||
|
|
||||||
await updateConversations(
|
await updateConversations(
|
||||||
sharedWith.map(c => {
|
sharedWith.map(c => {
|
||||||
c.unset('shareMyPhoneNumber');
|
c.set({ shareMyPhoneNumber: undefined });
|
||||||
return c.attributes;
|
return c.attributes;
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
@ -1496,15 +1679,14 @@ export class ConversationController {
|
||||||
|
|
||||||
// eslint-disable-next-line no-await-in-loop
|
// eslint-disable-next-line no-await-in-loop
|
||||||
await removeConversation(convo.id);
|
await removeConversation(convo.id);
|
||||||
this._conversations.remove(convo);
|
this.#removeConversation(convo);
|
||||||
this._conversations.resetLookups();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async #doLoad(): Promise<void> {
|
async #doLoad(): Promise<void> {
|
||||||
log.info('starting initial fetch');
|
log.info('starting initial fetch');
|
||||||
|
|
||||||
if (this._conversations.length) {
|
if (this.#_conversations.length) {
|
||||||
throw new Error('ConversationController: Already loaded!');
|
throw new Error('ConversationController: Already loaded!');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1540,14 +1722,16 @@ export class ConversationController {
|
||||||
this.#_initialFetchComplete = true;
|
this.#_initialFetchComplete = true;
|
||||||
|
|
||||||
// Hydrate the final set of conversations
|
// Hydrate the final set of conversations
|
||||||
batchDispatch(() => {
|
|
||||||
this._conversations.add(
|
collection
|
||||||
collection.filter(conversation => !conversation.isTemporary)
|
.filter(conversation => !conversation.isTemporary)
|
||||||
|
.forEach(conversation =>
|
||||||
|
this.#_conversations.push(new ConversationModel(conversation))
|
||||||
);
|
);
|
||||||
});
|
this.#generateLookups();
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
this._conversations.map(async conversation => {
|
this.#_conversations.map(async conversation => {
|
||||||
try {
|
try {
|
||||||
// Hydrate contactCollection, now that initial fetch is complete
|
// Hydrate contactCollection, now that initial fetch is complete
|
||||||
conversation.fetchContacts();
|
conversation.fetchContacts();
|
||||||
|
@ -1587,13 +1771,14 @@ export class ConversationController {
|
||||||
);
|
);
|
||||||
log.info(
|
log.info(
|
||||||
'done with initial fetch, ' +
|
'done with initial fetch, ' +
|
||||||
`got ${this._conversations.length} conversations`
|
`got ${this.#_conversations.length} conversations`
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error('initial fetch failed', Errors.toLogFormat(error));
|
log.error('initial fetch failed', Errors.toLogFormat(error));
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async archiveSessionsForConversation(
|
async archiveSessionsForConversation(
|
||||||
conversationId: string | undefined
|
conversationId: string | undefined
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
@ -1635,4 +1820,203 @@ export class ConversationController {
|
||||||
|
|
||||||
log.info(`${logId}: Complete!`);
|
log.info(`${logId}: Complete!`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
idUpdated(
|
||||||
|
model: ConversationModel,
|
||||||
|
idProp: 'e164' | 'serviceId' | 'pni' | 'groupId',
|
||||||
|
oldValue: string | undefined
|
||||||
|
): void {
|
||||||
|
const logId = `idUpdated/${model.idForLogging()}/${idProp}`;
|
||||||
|
if (oldValue) {
|
||||||
|
if (idProp === 'e164') {
|
||||||
|
delete this.#_byE164[oldValue];
|
||||||
|
} else if (idProp === 'serviceId') {
|
||||||
|
delete this.#_byServiceId[oldValue];
|
||||||
|
} else if (idProp === 'pni') {
|
||||||
|
delete this.#_byPni[oldValue];
|
||||||
|
} else if (idProp === 'groupId') {
|
||||||
|
delete this.#_byGroupId[oldValue];
|
||||||
|
} else {
|
||||||
|
throw missingCaseError(idProp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (idProp === 'e164') {
|
||||||
|
const e164 = model.get('e164');
|
||||||
|
if (e164) {
|
||||||
|
const existing = this.#_byE164[e164];
|
||||||
|
if (existing) {
|
||||||
|
log.warn(`${logId}: Existing match found on lookup`);
|
||||||
|
}
|
||||||
|
this.#_byE164[e164] = model;
|
||||||
|
}
|
||||||
|
} else if (idProp === 'serviceId') {
|
||||||
|
const serviceId = model.getServiceId();
|
||||||
|
if (serviceId) {
|
||||||
|
const existing = this.#_byServiceId[serviceId];
|
||||||
|
if (existing) {
|
||||||
|
log.warn(`${logId}: Existing match found on lookup`);
|
||||||
|
}
|
||||||
|
this.#_byServiceId[serviceId] = model;
|
||||||
|
}
|
||||||
|
} else if (idProp === 'pni') {
|
||||||
|
const pni = model.get('pni');
|
||||||
|
if (pni) {
|
||||||
|
const existing = this.#_byPni[pni];
|
||||||
|
if (existing) {
|
||||||
|
log.warn(`${logId}: Existing match found on lookup`);
|
||||||
|
}
|
||||||
|
this.#_byPni[pni] = model;
|
||||||
|
}
|
||||||
|
} else if (idProp === 'groupId') {
|
||||||
|
const groupId = model.get('groupId');
|
||||||
|
if (groupId) {
|
||||||
|
const existing = this.#_byGroupId[groupId];
|
||||||
|
if (existing) {
|
||||||
|
log.warn(`${logId}: Existing match found on lookup`);
|
||||||
|
}
|
||||||
|
this.#_byGroupId[groupId] = model;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw missingCaseError(idProp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#resetLookups(): void {
|
||||||
|
this.#eraseLookups();
|
||||||
|
this.#generateLookups();
|
||||||
|
}
|
||||||
|
|
||||||
|
#addToLookup(conversation: ConversationModel): void {
|
||||||
|
const logId = `addToLookup/${conversation.idForLogging()}`;
|
||||||
|
const id = conversation.get('id');
|
||||||
|
if (id) {
|
||||||
|
const existing = this.#_byId[id];
|
||||||
|
if (existing) {
|
||||||
|
log.warn(`${logId}: Conflict found by id`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!existing || (existing && !existing.getServiceId())) {
|
||||||
|
this.#_byId[id] = conversation;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const e164 = conversation.get('e164');
|
||||||
|
if (e164) {
|
||||||
|
const existing = this.#_byE164[e164];
|
||||||
|
if (existing) {
|
||||||
|
log.warn(`${logId}: Conflict found by e164`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!existing || (existing && !existing.getServiceId())) {
|
||||||
|
this.#_byE164[e164] = conversation;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const serviceId = conversation.getServiceId();
|
||||||
|
if (serviceId) {
|
||||||
|
const existing = this.#_byServiceId[serviceId];
|
||||||
|
if (existing) {
|
||||||
|
log.warn(`${logId}: Conflict found by serviceId`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!existing || (existing && !existing.get('e164'))) {
|
||||||
|
this.#_byServiceId[serviceId] = conversation;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const pni = conversation.getPni();
|
||||||
|
if (pni) {
|
||||||
|
const existing = this.#_byPni[pni];
|
||||||
|
if (existing) {
|
||||||
|
log.warn(`${logId}: Conflict found by pni`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!existing || (existing && !existing.getServiceId())) {
|
||||||
|
this.#_byPni[pni] = conversation;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const groupId = conversation.get('groupId');
|
||||||
|
if (groupId) {
|
||||||
|
const existing = this.#_byGroupId[groupId];
|
||||||
|
if (existing) {
|
||||||
|
log.warn(`${logId}: Conflict found by groupId`);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#_byGroupId[groupId] = conversation;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#removeFromLookup(conversation: ConversationModel): void {
|
||||||
|
const logId = `removeFromLookup/${conversation.idForLogging()}`;
|
||||||
|
const id = conversation.get('id');
|
||||||
|
if (id) {
|
||||||
|
const existing = this.#_byId[id];
|
||||||
|
if (existing && existing !== conversation) {
|
||||||
|
log.warn(`${logId}: By id; model in lookup didn't match conversation`);
|
||||||
|
} else {
|
||||||
|
delete this.#_byId[id];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const e164 = conversation.get('e164');
|
||||||
|
if (e164) {
|
||||||
|
const existing = this.#_byE164[e164];
|
||||||
|
if (existing && existing !== conversation) {
|
||||||
|
log.warn(
|
||||||
|
`${logId}: By e164; model in lookup didn't match conversation`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
delete this.#_byE164[e164];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const serviceId = conversation.getServiceId();
|
||||||
|
if (serviceId) {
|
||||||
|
const existing = this.#_byServiceId[serviceId];
|
||||||
|
if (existing && existing !== conversation) {
|
||||||
|
log.warn(
|
||||||
|
`${logId}: By serviceId; model in lookup didn't match conversation`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
delete this.#_byServiceId[serviceId];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const pni = conversation.getPni();
|
||||||
|
if (pni) {
|
||||||
|
const existing = this.#_byPni[pni];
|
||||||
|
if (existing && existing !== conversation) {
|
||||||
|
log.warn(`${logId}: By pni; model in lookup didn't match conversation`);
|
||||||
|
} else {
|
||||||
|
delete this.#_byPni[pni];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const groupId = conversation.get('groupId');
|
||||||
|
if (groupId) {
|
||||||
|
const existing = this.#_byGroupId[groupId];
|
||||||
|
if (existing && existing !== conversation) {
|
||||||
|
log.warn(
|
||||||
|
`${logId}: By groupId; model in lookup didn't match conversation`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
delete this.#_byGroupId[groupId];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#generateLookups(): void {
|
||||||
|
this.#_conversations.forEach(conversation =>
|
||||||
|
this.#addToLookup(conversation)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#eraseLookups(): void {
|
||||||
|
this.#_byE164 = Object.create(null);
|
||||||
|
this.#_byServiceId = Object.create(null);
|
||||||
|
this.#_byPni = Object.create(null);
|
||||||
|
this.#_byGroupId = Object.create(null);
|
||||||
|
this.#_byId = Object.create(null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2621,11 +2621,13 @@ export class SignalProtocolStore extends EventEmitter {
|
||||||
|
|
||||||
async removeAllConfiguration(): Promise<void> {
|
async removeAllConfiguration(): Promise<void> {
|
||||||
// Conversations. These properties are not present in redux.
|
// Conversations. These properties are not present in redux.
|
||||||
window.getConversations().forEach(conversation => {
|
window.ConversationController.getAll().forEach(conversation => {
|
||||||
conversation.unset('storageID');
|
conversation.set({
|
||||||
conversation.unset('needsStorageServiceSync');
|
storageID: undefined,
|
||||||
conversation.unset('storageUnknownFields');
|
needsStorageServiceSync: undefined,
|
||||||
conversation.unset('senderKeyInfo');
|
storageUnknownFields: undefined,
|
||||||
|
senderKeyInfo: undefined,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
await DataWriter.removeAllConfiguration();
|
await DataWriter.removeAllConfiguration();
|
||||||
|
|
|
@ -1,159 +0,0 @@
|
||||||
// Copyright 2017 Signal Messenger, LLC
|
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
|
|
||||||
import type * as Backbone from 'backbone';
|
|
||||||
import { createLogger } from '../logging/log';
|
|
||||||
|
|
||||||
const log = createLogger('reliable_trigger');
|
|
||||||
|
|
||||||
type InternalBackboneEvent = {
|
|
||||||
callback: (...args: Array<unknown>) => unknown;
|
|
||||||
ctx: unknown;
|
|
||||||
};
|
|
||||||
|
|
||||||
/* eslint-disable */
|
|
||||||
|
|
||||||
// This file was taken from Backbone and then modified. It does not conform to this
|
|
||||||
// project's standards.
|
|
||||||
|
|
||||||
// Note: this is all the code required to customize Backbone's trigger() method to make
|
|
||||||
// it resilient to exceptions thrown by event handlers. Indentation and code styles
|
|
||||||
// were kept inline with the Backbone implementation for easier diffs.
|
|
||||||
|
|
||||||
// The changes are:
|
|
||||||
// 1. added 'name' parameter to triggerEvents to give it access to the
|
|
||||||
// current event name
|
|
||||||
// 2. added try/catch handlers to triggerEvents with error logging inside
|
|
||||||
// every while loop
|
|
||||||
|
|
||||||
// And of course, we update the prototypes of Backbone.Model/Backbone.View as well as
|
|
||||||
// Backbone.Events itself
|
|
||||||
|
|
||||||
// Regular expression used to split event strings.
|
|
||||||
const eventSplitter = /\s+/;
|
|
||||||
|
|
||||||
// Implement fancy features of the Events API such as multiple event
|
|
||||||
// names `"change blur"` and jQuery-style event maps `{change: action}`
|
|
||||||
// in terms of the existing API.
|
|
||||||
const eventsApi = function (
|
|
||||||
obj: Backbone.Events,
|
|
||||||
name: string | Record<string, unknown>,
|
|
||||||
rest: ReadonlyArray<unknown>
|
|
||||||
) {
|
|
||||||
if (!name) return true;
|
|
||||||
|
|
||||||
// Handle event maps.
|
|
||||||
if (typeof name === 'object') {
|
|
||||||
for (const key in name) {
|
|
||||||
obj.trigger(key, name[key], ...rest);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle space separated event names.
|
|
||||||
if (eventSplitter.test(name)) {
|
|
||||||
const names = name.split(eventSplitter);
|
|
||||||
for (let i = 0, l = names.length; i < l; i++) {
|
|
||||||
obj.trigger(names[i], ...rest);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
// A difficult-to-believe, but optimized internal dispatch function for
|
|
||||||
// triggering events. Tries to keep the usual cases speedy (most internal
|
|
||||||
// Backbone events have 3 arguments).
|
|
||||||
const triggerEvents = function (
|
|
||||||
events: ReadonlyArray<InternalBackboneEvent>,
|
|
||||||
name: string,
|
|
||||||
args: Array<unknown>
|
|
||||||
) {
|
|
||||||
let ev,
|
|
||||||
i = -1,
|
|
||||||
l = events.length,
|
|
||||||
a1 = args[0],
|
|
||||||
a2 = args[1],
|
|
||||||
a3 = args[2];
|
|
||||||
const logError = function (error: unknown) {
|
|
||||||
log.error(
|
|
||||||
'Model caught error triggering',
|
|
||||||
name,
|
|
||||||
'event:',
|
|
||||||
error && error instanceof Error && error.stack ? error.stack : error
|
|
||||||
);
|
|
||||||
};
|
|
||||||
switch (args.length) {
|
|
||||||
case 0:
|
|
||||||
while (++i < l) {
|
|
||||||
try {
|
|
||||||
(ev = events[i]).callback.call(ev.ctx);
|
|
||||||
} catch (error) {
|
|
||||||
logError(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
case 1:
|
|
||||||
while (++i < l) {
|
|
||||||
try {
|
|
||||||
(ev = events[i]).callback.call(ev.ctx, a1);
|
|
||||||
} catch (error) {
|
|
||||||
logError(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
case 2:
|
|
||||||
while (++i < l) {
|
|
||||||
try {
|
|
||||||
(ev = events[i]).callback.call(ev.ctx, a1, a2);
|
|
||||||
} catch (error) {
|
|
||||||
logError(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
case 3:
|
|
||||||
while (++i < l) {
|
|
||||||
try {
|
|
||||||
(ev = events[i]).callback.call(ev.ctx, a1, a2, a3);
|
|
||||||
} catch (error) {
|
|
||||||
logError(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
default:
|
|
||||||
while (++i < l) {
|
|
||||||
try {
|
|
||||||
(ev = events[i]).callback.apply(ev.ctx, args);
|
|
||||||
} catch (error) {
|
|
||||||
logError(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Trigger one or many events, firing all bound callbacks. Callbacks are
|
|
||||||
// passed the same arguments as `trigger` is, apart from the event name
|
|
||||||
// (unless you're listening on `"all"`, which will cause your callback to
|
|
||||||
// receive the true name of the event as the first argument).
|
|
||||||
function trigger<
|
|
||||||
T extends Backbone.Events & {
|
|
||||||
_events: undefined | Record<string, ReadonlyArray<InternalBackboneEvent>>;
|
|
||||||
},
|
|
||||||
>(this: T, name: string, ...args: Array<unknown>): T {
|
|
||||||
if (!this._events) return this;
|
|
||||||
if (!eventsApi(this, name, args)) return this;
|
|
||||||
const events = this._events[name];
|
|
||||||
const allEvents = this._events.all;
|
|
||||||
if (events) triggerEvents(events, name, args);
|
|
||||||
if (allEvents) triggerEvents(allEvents, name, [...arguments]);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
[
|
|
||||||
window.Backbone.Model.prototype,
|
|
||||||
window.Backbone.Collection.prototype,
|
|
||||||
window.Backbone.Events,
|
|
||||||
].forEach(proto => {
|
|
||||||
Object.assign(proto, { trigger });
|
|
||||||
});
|
|
167
ts/background.ts
167
ts/background.ts
|
@ -1,12 +1,11 @@
|
||||||
// Copyright 2020 Signal Messenger, LLC
|
// Copyright 2020 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import { isNumber, groupBy, throttle } from 'lodash';
|
import { isNumber, throttle } from 'lodash';
|
||||||
import { createRoot } from 'react-dom/client';
|
import { createRoot } from 'react-dom/client';
|
||||||
import PQueue from 'p-queue';
|
import PQueue from 'p-queue';
|
||||||
import pMap from 'p-map';
|
import pMap from 'p-map';
|
||||||
import { v7 as generateUuid } from 'uuid';
|
import { v7 as generateUuid } from 'uuid';
|
||||||
import { batch as batchDispatch } from 'react-redux';
|
|
||||||
|
|
||||||
import * as Registration from './util/registration';
|
import * as Registration from './util/registration';
|
||||||
import MessageReceiver from './textsecure/MessageReceiver';
|
import MessageReceiver from './textsecure/MessageReceiver';
|
||||||
|
@ -25,8 +24,6 @@ import * as Bytes from './Bytes';
|
||||||
import * as Timers from './Timers';
|
import * as Timers from './Timers';
|
||||||
import * as indexedDb from './indexeddb';
|
import * as indexedDb from './indexeddb';
|
||||||
import type { MenuOptionsType } from './types/menu';
|
import type { MenuOptionsType } from './types/menu';
|
||||||
import type { Receipt } from './types/Receipt';
|
|
||||||
import { ReceiptType } from './types/Receipt';
|
|
||||||
import { SocketStatus } from './types/SocketStatus';
|
import { SocketStatus } from './types/SocketStatus';
|
||||||
import { DEFAULT_CONVERSATION_COLOR } from './types/Colors';
|
import { DEFAULT_CONVERSATION_COLOR } from './types/Colors';
|
||||||
import { ThemeType } from './types/Util';
|
import { ThemeType } from './types/Util';
|
||||||
|
@ -153,10 +150,7 @@ import { deleteAllLogs } from './util/deleteAllLogs';
|
||||||
import { startInteractionMode } from './services/InteractionMode';
|
import { startInteractionMode } from './services/InteractionMode';
|
||||||
import { ReactionSource } from './reactions/ReactionSource';
|
import { ReactionSource } from './reactions/ReactionSource';
|
||||||
import { singleProtoJobQueue } from './jobs/singleProtoJobQueue';
|
import { singleProtoJobQueue } from './jobs/singleProtoJobQueue';
|
||||||
import {
|
import { conversationJobQueue } from './jobs/conversationJobQueue';
|
||||||
conversationJobQueue,
|
|
||||||
conversationQueueJobEnum,
|
|
||||||
} from './jobs/conversationJobQueue';
|
|
||||||
import { SeenStatus } from './MessageSeenStatus';
|
import { SeenStatus } from './MessageSeenStatus';
|
||||||
import MessageSender from './textsecure/SendMessage';
|
import MessageSender from './textsecure/SendMessage';
|
||||||
import type AccountManager from './textsecure/AccountManager';
|
import type AccountManager from './textsecure/AccountManager';
|
||||||
|
@ -304,29 +298,7 @@ export async function startApp(): Promise<void> {
|
||||||
const onRetryRequestQueue = new PQueue({ concurrency: 1 });
|
const onRetryRequestQueue = new PQueue({ concurrency: 1 });
|
||||||
onRetryRequestQueue.pause();
|
onRetryRequestQueue.pause();
|
||||||
|
|
||||||
window.Whisper.deliveryReceiptQueue = new PQueue({
|
|
||||||
concurrency: 1,
|
|
||||||
timeout: durations.MINUTE * 30,
|
|
||||||
});
|
|
||||||
window.Whisper.deliveryReceiptQueue.pause();
|
window.Whisper.deliveryReceiptQueue.pause();
|
||||||
window.Whisper.deliveryReceiptBatcher = createBatcher<Receipt>({
|
|
||||||
name: 'Whisper.deliveryReceiptBatcher',
|
|
||||||
wait: 500,
|
|
||||||
maxSize: 100,
|
|
||||||
processBatch: async deliveryReceipts => {
|
|
||||||
const groups = groupBy(deliveryReceipts, 'conversationId');
|
|
||||||
await Promise.all(
|
|
||||||
Object.keys(groups).map(async conversationId => {
|
|
||||||
await conversationJobQueue.add({
|
|
||||||
type: conversationQueueJobEnum.enum.Receipts,
|
|
||||||
conversationId,
|
|
||||||
receiptsType: ReceiptType.Delivery,
|
|
||||||
receipts: groups[conversationId],
|
|
||||||
});
|
|
||||||
})
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (window.platform === 'darwin') {
|
if (window.platform === 'darwin') {
|
||||||
window.addEventListener('dblclick', (event: Event) => {
|
window.addEventListener('dblclick', (event: Event) => {
|
||||||
|
@ -441,7 +413,7 @@ export async function startApp(): Promise<void> {
|
||||||
});
|
});
|
||||||
|
|
||||||
accountManager.addEventListener('endRegistration', () => {
|
accountManager.addEventListener('endRegistration', () => {
|
||||||
window.Whisper.events.trigger('userChanged', false);
|
window.Whisper.events.emit('userChanged', false);
|
||||||
|
|
||||||
drop(window.storage.put('postRegistrationSyncsStatus', 'incomplete'));
|
drop(window.storage.put('postRegistrationSyncsStatus', 'incomplete'));
|
||||||
registrationCompleted?.resolve();
|
registrationCompleted?.resolve();
|
||||||
|
@ -596,6 +568,23 @@ export async function startApp(): Promise<void> {
|
||||||
storage: window.storage,
|
storage: window.storage,
|
||||||
serverTrustRoot: window.getServerTrustRoot(),
|
serverTrustRoot: window.getServerTrustRoot(),
|
||||||
});
|
});
|
||||||
|
window.ConversationController.registerDelayBeforeUpdatingRedux(() => {
|
||||||
|
if (backupsService.isImportRunning()) {
|
||||||
|
return 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (messageReceiver && !messageReceiver.hasEmptied()) {
|
||||||
|
return 250;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
});
|
||||||
|
window.ConversationController.registerIsAppStillLoading(() => {
|
||||||
|
return (
|
||||||
|
backupsService.isImportRunning() ||
|
||||||
|
!window.reduxStore?.getState().app.hasInitialLoadCompleted
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
function queuedEventListener<E extends Event>(
|
function queuedEventListener<E extends Event>(
|
||||||
handler: (event: E) => Promise<void> | void
|
handler: (event: E) => Promise<void> | void
|
||||||
|
@ -1215,114 +1204,6 @@ export async function startApp(): Promise<void> {
|
||||||
function setupAppState() {
|
function setupAppState() {
|
||||||
initializeRedux(getParametersForRedux());
|
initializeRedux(getParametersForRedux());
|
||||||
|
|
||||||
// Here we set up a full redux store with initial state for our LeftPane Root
|
|
||||||
const convoCollection = window.getConversations();
|
|
||||||
|
|
||||||
const {
|
|
||||||
conversationsUpdated,
|
|
||||||
conversationRemoved,
|
|
||||||
removeAllConversations,
|
|
||||||
onConversationClosed,
|
|
||||||
} = window.reduxActions.conversations;
|
|
||||||
|
|
||||||
// Conversation add/update/remove actions are batched in this batcher to ensure
|
|
||||||
// that we retain correct orderings
|
|
||||||
const convoUpdateBatcher = createBatcher<
|
|
||||||
| { type: 'change' | 'add'; conversation: ConversationModel }
|
|
||||||
| { type: 'remove'; id: string }
|
|
||||||
>({
|
|
||||||
name: 'changedConvoBatcher',
|
|
||||||
processBatch(batch) {
|
|
||||||
let changedOrAddedBatch = new Array<ConversationModel>();
|
|
||||||
function flushChangedOrAddedBatch() {
|
|
||||||
if (!changedOrAddedBatch.length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
conversationsUpdated(
|
|
||||||
changedOrAddedBatch.map(conversation => conversation.format())
|
|
||||||
);
|
|
||||||
changedOrAddedBatch = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
batchDispatch(() => {
|
|
||||||
for (const item of batch) {
|
|
||||||
if (item.type === 'add' || item.type === 'change') {
|
|
||||||
changedOrAddedBatch.push(item.conversation);
|
|
||||||
} else {
|
|
||||||
strictAssert(item.type === 'remove', 'must be remove');
|
|
||||||
|
|
||||||
flushChangedOrAddedBatch();
|
|
||||||
|
|
||||||
onConversationClosed(item.id, 'removed');
|
|
||||||
conversationRemoved(item.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
flushChangedOrAddedBatch();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
wait: () => {
|
|
||||||
if (backupsService.isImportRunning()) {
|
|
||||||
return 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (messageReceiver && !messageReceiver.hasEmptied()) {
|
|
||||||
return 250;
|
|
||||||
}
|
|
||||||
|
|
||||||
// This delay ensures that the .format() call isn't synchronous as a
|
|
||||||
// Backbone property is changed. Important because our _byUuid/_byE164
|
|
||||||
// lookups aren't up-to-date as the change happens; just a little bit
|
|
||||||
// after.
|
|
||||||
return 1;
|
|
||||||
},
|
|
||||||
maxSize: Infinity,
|
|
||||||
});
|
|
||||||
|
|
||||||
convoCollection.on('add', (conversation: ConversationModel | undefined) => {
|
|
||||||
if (!conversation) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
backupsService.isImportRunning() ||
|
|
||||||
!window.reduxStore.getState().app.hasInitialLoadCompleted
|
|
||||||
) {
|
|
||||||
convoUpdateBatcher.add({ type: 'add', conversation });
|
|
||||||
} else {
|
|
||||||
// During normal app usage, we require conversations to be added synchronously
|
|
||||||
conversationsUpdated([conversation.format()]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
convoCollection.on('remove', conversation => {
|
|
||||||
const { id } = conversation || {};
|
|
||||||
|
|
||||||
convoUpdateBatcher.add({ type: 'remove', id });
|
|
||||||
});
|
|
||||||
|
|
||||||
convoCollection.on(
|
|
||||||
'props-change',
|
|
||||||
(conversation: ConversationModel | undefined, isBatched?: boolean) => {
|
|
||||||
if (!conversation) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// `isBatched` is true when the `.set()` call on the conversation model already
|
|
||||||
// runs from within `react-redux`'s batch. Instead of batching the redux update
|
|
||||||
// for later, update immediately. To ensure correct update ordering, only do this
|
|
||||||
// optimization if there are no other pending conversation updates
|
|
||||||
if (isBatched && !convoUpdateBatcher.anyPending()) {
|
|
||||||
conversationsUpdated([conversation.format()]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
convoUpdateBatcher.add({ type: 'change', conversation });
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// Called by SignalProtocolStore#removeAllData()
|
|
||||||
convoCollection.on('reset', removeAllConversations);
|
|
||||||
|
|
||||||
window.Whisper.events.on('userChanged', (reconnect = false) => {
|
window.Whisper.events.on('userChanged', (reconnect = false) => {
|
||||||
const newDeviceId = window.textsecure.storage.user.getDeviceId();
|
const newDeviceId = window.textsecure.storage.user.getDeviceId();
|
||||||
const newNumber = window.textsecure.storage.user.getNumber();
|
const newNumber = window.textsecure.storage.user.getNumber();
|
||||||
|
@ -1332,7 +1213,7 @@ export async function startApp(): Promise<void> {
|
||||||
window.ConversationController.getOurConversation();
|
window.ConversationController.getOurConversation();
|
||||||
|
|
||||||
if (ourConversation?.get('e164') !== newNumber) {
|
if (ourConversation?.get('e164') !== newNumber) {
|
||||||
ourConversation?.set('e164', newNumber);
|
ourConversation?.set({ e164: newNumber });
|
||||||
}
|
}
|
||||||
|
|
||||||
window.reduxActions.user.userChanged({
|
window.reduxActions.user.userChanged({
|
||||||
|
@ -1566,7 +1447,7 @@ export async function startApp(): Promise<void> {
|
||||||
window.IPC.setMenuBarVisibility(!hideMenuBar);
|
window.IPC.setMenuBarVisibility(!hideMenuBar);
|
||||||
|
|
||||||
startTimeTravelDetector(() => {
|
startTimeTravelDetector(() => {
|
||||||
window.Whisper.events.trigger('timetravel');
|
window.Whisper.events.emit('timetravel');
|
||||||
});
|
});
|
||||||
|
|
||||||
updateExpiringMessagesService();
|
updateExpiringMessagesService();
|
||||||
|
@ -3145,7 +3026,7 @@ export async function startApp(): Promise<void> {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function unlinkAndDisconnect(): Promise<void> {
|
async function unlinkAndDisconnect(): Promise<void> {
|
||||||
window.Whisper.events.trigger('unauthorized');
|
window.Whisper.events.emit('unauthorized');
|
||||||
|
|
||||||
log.warn(
|
log.warn(
|
||||||
'unlinkAndDisconnect: Client is no longer authorized; ' +
|
'unlinkAndDisconnect: Client is no longer authorized; ' +
|
||||||
|
@ -3192,7 +3073,7 @@ export async function startApp(): Promise<void> {
|
||||||
const ourConversation =
|
const ourConversation =
|
||||||
window.ConversationController.getOurConversation();
|
window.ConversationController.getOurConversation();
|
||||||
if (ourConversation) {
|
if (ourConversation) {
|
||||||
ourConversation.unset('username');
|
ourConversation.set({ username: undefined });
|
||||||
await DataWriter.updateConversation(ourConversation.attributes);
|
await DataWriter.updateConversation(ourConversation.attributes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -554,6 +554,9 @@ export function CallsList({
|
||||||
};
|
};
|
||||||
|
|
||||||
let timer = setTimeout(() => {
|
let timer = setTimeout(() => {
|
||||||
|
if (controller.signal.aborted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
setSearchState(prevSearchState => {
|
setSearchState(prevSearchState => {
|
||||||
if (prevSearchState.state === 'init') {
|
if (prevSearchState.state === 'init') {
|
||||||
return defaultPendingState;
|
return defaultPendingState;
|
||||||
|
@ -561,6 +564,10 @@ export function CallsList({
|
||||||
return prevSearchState;
|
return prevSearchState;
|
||||||
});
|
});
|
||||||
timer = setTimeout(() => {
|
timer = setTimeout(() => {
|
||||||
|
if (controller.signal.aborted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Show loading indicator after a delay
|
// Show loading indicator after a delay
|
||||||
setSearchState(defaultPendingState);
|
setSearchState(defaultPendingState);
|
||||||
}, 300);
|
}, 300);
|
||||||
|
|
|
@ -140,7 +140,7 @@ type PropsHousekeepingType = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PropsActionsType = {
|
export type PropsActionsType = {
|
||||||
// From Backbone
|
// From Model
|
||||||
acknowledgeGroupMemberNameCollisions: (
|
acknowledgeGroupMemberNameCollisions: (
|
||||||
conversationId: string,
|
conversationId: string,
|
||||||
groupNameCollisions: ReadonlyDeep<GroupNameCollisionsWithIdsByTitle>
|
groupNameCollisions: ReadonlyDeep<GroupNameCollisionsWithIdsByTitle>
|
||||||
|
|
|
@ -3304,7 +3304,11 @@ async function updateGroup(
|
||||||
});
|
});
|
||||||
|
|
||||||
if (idChanged) {
|
if (idChanged) {
|
||||||
conversation.trigger('idUpdated', conversation, 'groupId', previousId);
|
window.ConversationController.idUpdated(
|
||||||
|
conversation,
|
||||||
|
'groupId',
|
||||||
|
previousId
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save these most recent updates to conversation
|
// Save these most recent updates to conversation
|
||||||
|
|
|
@ -104,7 +104,7 @@ export async function onDelete(item: DeleteForMeAttributesType): Promise<void> {
|
||||||
|
|
||||||
let result: boolean;
|
let result: boolean;
|
||||||
if (item.deleteAttachmentData) {
|
if (item.deleteAttachmentData) {
|
||||||
// This will find the message, then work with a backbone model to mirror what
|
// This will find the message, then work with a model to mirror what
|
||||||
// modifyTargetMessage does.
|
// modifyTargetMessage does.
|
||||||
result = await deleteAttachmentFromMessage(
|
result = await deleteAttachmentFromMessage(
|
||||||
conversation.id,
|
conversation.id,
|
||||||
|
|
|
@ -297,7 +297,7 @@ const deleteSentProtoBatcher = createWaitBatcher({
|
||||||
|
|
||||||
// `deleteSentProtoRecipient` has already updated the database so there
|
// `deleteSentProtoRecipient` has already updated the database so there
|
||||||
// is no need in calling `updateConversation`
|
// is no need in calling `updateConversation`
|
||||||
convo.unset('shareMyPhoneNumber');
|
convo.set({ shareMyPhoneNumber: undefined });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -57,7 +57,7 @@ export async function saveAndNotify(
|
||||||
conversation.incrementSentMessageCount();
|
conversation.incrementSentMessageCount();
|
||||||
}
|
}
|
||||||
|
|
||||||
window.Whisper.events.trigger('incrementProgress');
|
window.Whisper.events.emit('incrementProgress');
|
||||||
confirm();
|
confirm();
|
||||||
|
|
||||||
if (!isStory(message.attributes)) {
|
if (!isStory(message.attributes)) {
|
||||||
|
|
8
ts/model-types.d.ts
vendored
8
ts/model-types.d.ts
vendored
|
@ -1,14 +1,12 @@
|
||||||
// Copyright 2020 Signal Messenger, LLC
|
// Copyright 2020 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import * as Backbone from 'backbone';
|
|
||||||
import type { ReadonlyDeep } from 'type-fest';
|
import type { ReadonlyDeep } from 'type-fest';
|
||||||
|
|
||||||
import type { GroupV2ChangeType } from './groups';
|
import type { GroupV2ChangeType } from './groups';
|
||||||
import type { DraftBodyRanges, RawBodyRange } from './types/BodyRange';
|
import type { DraftBodyRanges, RawBodyRange } from './types/BodyRange';
|
||||||
import type { CustomColorType, ConversationColorType } from './types/Colors';
|
import type { CustomColorType, ConversationColorType } from './types/Colors';
|
||||||
import type { SendMessageChallengeData } from './textsecure/Errors';
|
import type { SendMessageChallengeData } from './textsecure/Errors';
|
||||||
import type { ConversationModel } from './models/conversations';
|
|
||||||
import type { ProfileNameChangeType } from './util/getStringForProfileChange';
|
import type { ProfileNameChangeType } from './util/getStringForProfileChange';
|
||||||
import type { CapabilitiesType } from './textsecure/WebAPI';
|
import type { CapabilitiesType } from './textsecure/WebAPI';
|
||||||
import type { ReadStatus } from './messages/MessageReadStatus';
|
import type { ReadStatus } from './messages/MessageReadStatus';
|
||||||
|
@ -486,7 +484,7 @@ export type ConversationAttributesType = {
|
||||||
groupInviteLinkPassword?: string;
|
groupInviteLinkPassword?: string;
|
||||||
previousGroupV1Id?: string;
|
previousGroupV1Id?: string;
|
||||||
previousGroupV1Members?: Array<string>;
|
previousGroupV1Members?: Array<string>;
|
||||||
acknowledgedGroupNameCollisions?: GroupNameCollisionsWithIdsByTitle;
|
acknowledgedGroupNameCollisions?: ReadonlyDeep<GroupNameCollisionsWithIdsByTitle>;
|
||||||
|
|
||||||
// Used only when user is waiting for approval to join via link
|
// Used only when user is waiting for approval to join via link
|
||||||
isTemporary?: boolean;
|
isTemporary?: boolean;
|
||||||
|
@ -561,7 +559,3 @@ export type ShallowChallengeError = CustomError & {
|
||||||
readonly retryAfter: number;
|
readonly retryAfter: number;
|
||||||
readonly data: SendMessageChallengeData;
|
readonly data: SendMessageChallengeData;
|
||||||
};
|
};
|
||||||
|
|
||||||
export declare class ConversationModelCollectionType extends Backbone.Collection<ConversationModel> {
|
|
||||||
resetLookups(): void;
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
// Copyright 2020 Signal Messenger, LLC
|
// Copyright 2020 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import { compact, has, isNumber, throttle, debounce } from 'lodash';
|
import { compact, isNumber, throttle, debounce } from 'lodash';
|
||||||
import { batch as batchDispatch } from 'react-redux';
|
|
||||||
import { v4 as generateGuid } from 'uuid';
|
import { v4 as generateGuid } from 'uuid';
|
||||||
import PQueue from 'p-queue';
|
import PQueue from 'p-queue';
|
||||||
|
|
||||||
|
@ -193,22 +192,10 @@ import { getTypingIndicatorSetting } from '../types/Util';
|
||||||
import { INITIAL_EXPIRE_TIMER_VERSION } from '../util/expirationTimer';
|
import { INITIAL_EXPIRE_TIMER_VERSION } from '../util/expirationTimer';
|
||||||
import { maybeNotify } from '../messages/maybeNotify';
|
import { maybeNotify } from '../messages/maybeNotify';
|
||||||
import { missingCaseError } from '../util/missingCaseError';
|
import { missingCaseError } from '../util/missingCaseError';
|
||||||
|
import * as Message from '../types/Message2';
|
||||||
|
|
||||||
const log = createLogger('conversations');
|
const log = createLogger('conversations');
|
||||||
|
|
||||||
window.Whisper = window.Whisper || {};
|
|
||||||
|
|
||||||
const { Message } = window.Signal.Types;
|
|
||||||
const {
|
|
||||||
copyIntoTempDirectory,
|
|
||||||
deleteAttachmentData,
|
|
||||||
doesAttachmentExist,
|
|
||||||
getAbsoluteAttachmentPath,
|
|
||||||
getAbsoluteTempPath,
|
|
||||||
readStickerData,
|
|
||||||
upgradeMessageSchema,
|
|
||||||
writeNewAttachmentData,
|
|
||||||
} = window.Signal.Migrations;
|
|
||||||
const {
|
const {
|
||||||
getConversationRangeCenteredOnMessage,
|
getConversationRangeCenteredOnMessage,
|
||||||
getOlderMessagesByConversation,
|
getOlderMessagesByConversation,
|
||||||
|
@ -228,15 +215,6 @@ const SEND_REPORTING_THRESHOLD_MS = 25;
|
||||||
|
|
||||||
const MESSAGE_LOAD_CHUNK_SIZE = 30;
|
const MESSAGE_LOAD_CHUNK_SIZE = 30;
|
||||||
|
|
||||||
const ATTRIBUTES_THAT_DONT_INVALIDATE_PROPS_CACHE = new Set([
|
|
||||||
'lastProfile',
|
|
||||||
'profileLastFetchedAt',
|
|
||||||
'needsStorageServiceSync',
|
|
||||||
'storageID',
|
|
||||||
'storageVersion',
|
|
||||||
'storageUnknownFields',
|
|
||||||
]);
|
|
||||||
|
|
||||||
const MAX_EXPIRE_TIMER_VERSION = 0xffffffff;
|
const MAX_EXPIRE_TIMER_VERSION = 0xffffffff;
|
||||||
|
|
||||||
type CachedIdenticon = {
|
type CachedIdenticon = {
|
||||||
|
@ -245,11 +223,13 @@ type CachedIdenticon = {
|
||||||
readonly path?: string;
|
readonly path?: string;
|
||||||
readonly url: string;
|
readonly url: string;
|
||||||
};
|
};
|
||||||
|
type StringKey<T> = keyof T & string;
|
||||||
|
|
||||||
export class ConversationModel extends window.Backbone
|
export class ConversationModel {
|
||||||
.Model<ConversationAttributesType> {
|
|
||||||
static COLORS: string;
|
static COLORS: string;
|
||||||
|
|
||||||
|
#_attributes: ConversationAttributesType;
|
||||||
|
|
||||||
cachedProps?: ConversationType | null;
|
cachedProps?: ConversationType | null;
|
||||||
|
|
||||||
oldCachedProps?: ConversationType | null;
|
oldCachedProps?: ConversationType | null;
|
||||||
|
@ -263,7 +243,7 @@ export class ConversationModel extends window.Backbone
|
||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
|
|
||||||
contactCollection?: Backbone.Collection<ConversationModel>;
|
contactCollection?: Array<ConversationModel>;
|
||||||
|
|
||||||
debouncedUpdateLastMessage: (() => void) & { flush(): void };
|
debouncedUpdateLastMessage: (() => void) & { flush(): void };
|
||||||
|
|
||||||
|
@ -305,19 +285,70 @@ export class ConversationModel extends window.Backbone
|
||||||
|
|
||||||
#lastIsTyping?: boolean;
|
#lastIsTyping?: boolean;
|
||||||
#muteTimer?: NodeJS.Timeout;
|
#muteTimer?: NodeJS.Timeout;
|
||||||
#isInReduxBatch = false;
|
|
||||||
#privVerifiedEnum?: typeof window.textsecure.storage.protocol.VerifiedStatus;
|
#privVerifiedEnum?: typeof window.textsecure.storage.protocol.VerifiedStatus;
|
||||||
#isShuttingDown = false;
|
#isShuttingDown = false;
|
||||||
#savePromises = new Set<Promise<void>>();
|
#savePromises = new Set<Promise<void>>();
|
||||||
|
|
||||||
override defaults(): Partial<ConversationAttributesType> {
|
public get id(): string {
|
||||||
return {
|
return this.#_attributes.id;
|
||||||
unreadCount: 0,
|
}
|
||||||
verified: window.textsecure.storage.protocol.VerifiedStatus.DEFAULT,
|
|
||||||
messageCount: 0,
|
public get<keyName extends StringKey<ConversationAttributesType>>(
|
||||||
sentMessageCount: 0,
|
key: keyName
|
||||||
expireTimerVersion: INITIAL_EXPIRE_TIMER_VERSION,
|
): ConversationAttributesType[keyName] {
|
||||||
|
return this.attributes[key];
|
||||||
|
}
|
||||||
|
public set(
|
||||||
|
attributes: Partial<ConversationAttributesType>,
|
||||||
|
{ noTrigger }: { noTrigger?: boolean } = {}
|
||||||
|
): void {
|
||||||
|
const previousAttributes = this.#_attributes;
|
||||||
|
this.#_attributes = {
|
||||||
|
...previousAttributes,
|
||||||
|
...attributes,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (noTrigger) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasAttributeChanged = (name: keyof ConversationAttributesType) => {
|
||||||
|
return (
|
||||||
|
name in attributes && attributes[name] !== previousAttributes[name]
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (hasAttributeChanged('profileKey')) {
|
||||||
|
this.onChangeProfileKey();
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearUsernameTriggers: Array<keyof ConversationAttributesType> = [
|
||||||
|
'name',
|
||||||
|
'profileName',
|
||||||
|
'profileFamilyName',
|
||||||
|
'e164',
|
||||||
|
'systemGivenName',
|
||||||
|
'systemFamilyName',
|
||||||
|
'systemNickname',
|
||||||
|
];
|
||||||
|
|
||||||
|
if (clearUsernameTriggers.some(attrName => hasAttributeChanged(attrName))) {
|
||||||
|
drop(this.maybeClearUsername());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasAttributeChanged('members') || hasAttributeChanged('membersV2')) {
|
||||||
|
this.fetchContacts();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasAttributeChanged('active_at')) {
|
||||||
|
drop(this.#onActiveAtChange());
|
||||||
|
}
|
||||||
|
|
||||||
|
window.ConversationController.conversationUpdated(this, previousAttributes);
|
||||||
|
}
|
||||||
|
|
||||||
|
public get attributes(): Readonly<ConversationAttributesType> {
|
||||||
|
return this.#_attributes;
|
||||||
}
|
}
|
||||||
|
|
||||||
idForLogging(): string {
|
idForLogging(): string {
|
||||||
|
@ -328,20 +359,8 @@ export class ConversationModel extends window.Backbone
|
||||||
return getSendTarget(this.attributes);
|
return getSendTarget(this.attributes);
|
||||||
}
|
}
|
||||||
|
|
||||||
getContactCollection(): Backbone.Collection<ConversationModel> {
|
|
||||||
const collection = new window.Backbone.Collection<ConversationModel>();
|
|
||||||
const collator = new Intl.Collator(undefined, { sensitivity: 'base' });
|
|
||||||
collection.comparator = (
|
|
||||||
left: ConversationModel,
|
|
||||||
right: ConversationModel
|
|
||||||
) => {
|
|
||||||
return collator.compare(left.getTitle(), right.getTitle());
|
|
||||||
};
|
|
||||||
return collection;
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(attributes: ConversationAttributesType) {
|
constructor(attributes: ConversationAttributesType) {
|
||||||
super(attributes);
|
this.#_attributes = attributes;
|
||||||
|
|
||||||
// Note that we intentionally don't use `initialize()` method because it
|
// Note that we intentionally don't use `initialize()` method because it
|
||||||
// isn't compatible with esnext output of esbuild.
|
// isn't compatible with esnext output of esbuild.
|
||||||
|
@ -354,7 +373,7 @@ export class ConversationModel extends window.Backbone
|
||||||
'ConversationModel.initialize: normalizing serviceId from ' +
|
'ConversationModel.initialize: normalizing serviceId from ' +
|
||||||
`${serviceId} to ${normalizedServiceId}`
|
`${serviceId} to ${normalizedServiceId}`
|
||||||
);
|
);
|
||||||
this.set('serviceId', normalizedServiceId);
|
this.set({ serviceId: normalizedServiceId });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isValidE164(attributes.id, false)) {
|
if (isValidE164(attributes.id, false)) {
|
||||||
|
@ -374,71 +393,35 @@ export class ConversationModel extends window.Backbone
|
||||||
200
|
200
|
||||||
);
|
);
|
||||||
|
|
||||||
this.contactCollection = this.getContactCollection();
|
this.contactCollection = [];
|
||||||
this.contactCollection.on(
|
|
||||||
'change:name change:profileName change:profileFamilyName change:e164',
|
|
||||||
this.debouncedUpdateLastMessage,
|
|
||||||
this
|
|
||||||
);
|
|
||||||
if (!isDirectConversation(this.attributes)) {
|
|
||||||
this.contactCollection.on(
|
|
||||||
'change:verified',
|
|
||||||
this.onMemberVerifiedChange.bind(this)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.on('change:profileKey', this.onChangeProfileKey);
|
|
||||||
this.on(
|
|
||||||
'change:name change:profileName change:profileFamilyName change:e164 ' +
|
|
||||||
'change:systemGivenName change:systemFamilyName change:systemNickname',
|
|
||||||
() => this.maybeClearUsername()
|
|
||||||
);
|
|
||||||
|
|
||||||
const sealedSender = this.get('sealedSender');
|
const sealedSender = this.get('sealedSender');
|
||||||
if (sealedSender === undefined) {
|
if (sealedSender === undefined) {
|
||||||
this.set({ sealedSender: SEALED_SENDER.UNKNOWN });
|
this.set({ sealedSender: SEALED_SENDER.UNKNOWN });
|
||||||
}
|
}
|
||||||
// @ts-expect-error -- Removing legacy prop
|
|
||||||
this.unset('unidentifiedDelivery');
|
|
||||||
// @ts-expect-error -- Removing legacy prop
|
|
||||||
this.unset('unidentifiedDeliveryUnrestricted');
|
|
||||||
// @ts-expect-error -- Removing legacy prop
|
|
||||||
this.unset('hasFetchedProfile');
|
|
||||||
// @ts-expect-error -- Removing legacy prop
|
|
||||||
this.unset('tokens');
|
|
||||||
|
|
||||||
this.on('change:members change:membersV2', this.fetchContacts);
|
if (
|
||||||
this.on('change:active_at', this.#onActiveAtChange);
|
// @ts-expect-error -- Removing legacy prop
|
||||||
|
this.get('unidentifiedDelivery') ||
|
||||||
|
// @ts-expect-error -- Removing legacy prop
|
||||||
|
this.get('unidentifiedDeliveryUnrestricted') ||
|
||||||
|
// @ts-expect-error -- Removing legacy prop
|
||||||
|
this.get('hasFetchedProfile') ||
|
||||||
|
// @ts-expect-error -- Removing legacy prop
|
||||||
|
this.get('tokens')
|
||||||
|
) {
|
||||||
|
this.set({
|
||||||
|
// @ts-expect-error -- Removing legacy prop
|
||||||
|
unidentifiedDelivery: undefined,
|
||||||
|
unidentifiedDeliveryUnrestricted: undefined,
|
||||||
|
hasFetchedProfile: undefined,
|
||||||
|
tokens: undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
this.typingRefreshTimer = null;
|
this.typingRefreshTimer = null;
|
||||||
this.typingPauseTimer = null;
|
this.typingPauseTimer = null;
|
||||||
|
|
||||||
// We clear our cached props whenever we change so that the next call to format() will
|
|
||||||
// result in refresh via a getProps() call. See format() below.
|
|
||||||
this.on(
|
|
||||||
'change',
|
|
||||||
(_model: ConversationModel, options: { force?: boolean } = {}) => {
|
|
||||||
const changedKeys = Object.keys(this.changed || {});
|
|
||||||
const isPropsCacheStillValid =
|
|
||||||
!options.force &&
|
|
||||||
Boolean(
|
|
||||||
changedKeys.length &&
|
|
||||||
changedKeys.every(key =>
|
|
||||||
ATTRIBUTES_THAT_DONT_INVALIDATE_PROPS_CACHE.has(key)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
if (isPropsCacheStillValid) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.cachedProps) {
|
|
||||||
this.oldCachedProps = this.cachedProps;
|
|
||||||
}
|
|
||||||
this.cachedProps = null;
|
|
||||||
this.trigger('props-change', this, this.#isInReduxBatch);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// Set `isFetchingUUID` eagerly to avoid UI flicker when opening the
|
// Set `isFetchingUUID` eagerly to avoid UI flicker when opening the
|
||||||
// conversation for the first time.
|
// conversation for the first time.
|
||||||
this.isFetchingUUID = this.isSMSOnly();
|
this.isFetchingUUID = this.isSMSOnly();
|
||||||
|
@ -468,7 +451,7 @@ export class ConversationModel extends window.Backbone
|
||||||
|
|
||||||
const migratedColor = this.getColor();
|
const migratedColor = this.getColor();
|
||||||
if (this.get('color') !== migratedColor) {
|
if (this.get('color') !== migratedColor) {
|
||||||
this.set('color', migratedColor);
|
this.set({ color: migratedColor });
|
||||||
// Not saving the conversation here we're hoping it'll be saved elsewhere
|
// Not saving the conversation here we're hoping it'll be saved elsewhere
|
||||||
// this may cause some color thrashing if Signal is restarted without
|
// this may cause some color thrashing if Signal is restarted without
|
||||||
// the convo saving. If that is indeed the case and it's too disruptive
|
// the convo saving. If that is indeed the case and it's too disruptive
|
||||||
|
@ -942,8 +925,7 @@ export class ConversationModel extends window.Backbone
|
||||||
}
|
}
|
||||||
|
|
||||||
if (blocked && !wasBlocked) {
|
if (blocked && !wasBlocked) {
|
||||||
// We need to force a props refresh - blocked state is not in backbone attributes
|
window.ConversationController.conversationUpdated(this, this.attributes);
|
||||||
this.trigger('change', this, { force: true });
|
|
||||||
|
|
||||||
if (!viaStorageServiceSync) {
|
if (!viaStorageServiceSync) {
|
||||||
this.captureChange('block');
|
this.captureChange('block');
|
||||||
|
@ -975,7 +957,7 @@ export class ConversationModel extends window.Backbone
|
||||||
|
|
||||||
if (unblocked && wasBlocked) {
|
if (unblocked && wasBlocked) {
|
||||||
// We need to force a props refresh - blocked state is not in backbone attributes
|
// We need to force a props refresh - blocked state is not in backbone attributes
|
||||||
this.trigger('change', this, { force: true });
|
window.ConversationController.conversationUpdated(this, this.attributes);
|
||||||
|
|
||||||
if (!viaStorageServiceSync) {
|
if (!viaStorageServiceSync) {
|
||||||
this.captureChange('unblock');
|
this.captureChange('unblock');
|
||||||
|
@ -1213,7 +1195,7 @@ export class ConversationModel extends window.Backbone
|
||||||
);
|
);
|
||||||
|
|
||||||
this.isFetchingUUID = true;
|
this.isFetchingUUID = true;
|
||||||
this.trigger('change', this, { force: true });
|
window.ConversationController.conversationUpdated(this, this.attributes);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Attempt to fetch UUID
|
// Attempt to fetch UUID
|
||||||
|
@ -1225,7 +1207,7 @@ export class ConversationModel extends window.Backbone
|
||||||
} finally {
|
} finally {
|
||||||
// No redux update here
|
// No redux update here
|
||||||
this.isFetchingUUID = false;
|
this.isFetchingUUID = false;
|
||||||
this.trigger('change', this, { force: true });
|
window.ConversationController.conversationUpdated(this, this.attributes);
|
||||||
|
|
||||||
log.info(
|
log.info(
|
||||||
`Done fetching uuid for a sms-only conversation ${this.idForLogging()}`
|
`Done fetching uuid for a sms-only conversation ${this.idForLogging()}`
|
||||||
|
@ -1240,14 +1222,6 @@ export class ConversationModel extends window.Backbone
|
||||||
this.setRegistered();
|
this.setRegistered();
|
||||||
}
|
}
|
||||||
|
|
||||||
override isValid(): boolean {
|
|
||||||
return (
|
|
||||||
isDirectConversation(this.attributes) ||
|
|
||||||
isGroupV1(this.attributes) ||
|
|
||||||
isGroupV2(this.attributes)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async maybeMigrateV1Group(): Promise<void> {
|
async maybeMigrateV1Group(): Promise<void> {
|
||||||
if (!isGroupV1(this.attributes)) {
|
if (!isGroupV1(this.attributes)) {
|
||||||
return;
|
return;
|
||||||
|
@ -2078,7 +2052,7 @@ export class ConversationModel extends window.Backbone
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.set('e164', e164 || undefined);
|
this.set({ e164: e164 || undefined });
|
||||||
|
|
||||||
// This user changed their phone number
|
// This user changed their phone number
|
||||||
if (oldValue && e164 && this.get('sharingPhoneNumber')) {
|
if (oldValue && e164 && this.get('sharingPhoneNumber')) {
|
||||||
|
@ -2086,7 +2060,7 @@ export class ConversationModel extends window.Backbone
|
||||||
}
|
}
|
||||||
|
|
||||||
drop(DataWriter.updateConversation(this.attributes));
|
drop(DataWriter.updateConversation(this.attributes));
|
||||||
this.trigger('idUpdated', this, 'e164', oldValue);
|
window.ConversationController.idUpdated(this, 'e164', oldValue);
|
||||||
this.captureChange('updateE164');
|
this.captureChange('updateE164');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2096,14 +2070,13 @@ export class ConversationModel extends window.Backbone
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.set(
|
this.set({
|
||||||
'serviceId',
|
serviceId: serviceId
|
||||||
serviceId
|
|
||||||
? normalizeServiceId(serviceId, 'Conversation.updateServiceId')
|
? normalizeServiceId(serviceId, 'Conversation.updateServiceId')
|
||||||
: undefined
|
: undefined,
|
||||||
);
|
});
|
||||||
drop(DataWriter.updateConversation(this.attributes));
|
drop(DataWriter.updateConversation(this.attributes));
|
||||||
this.trigger('idUpdated', this, 'serviceId', oldValue);
|
window.ConversationController.idUpdated(this, 'serviceId', oldValue);
|
||||||
|
|
||||||
// We should delete the old sessions and identity information in all situations except
|
// We should delete the old sessions and identity information in all situations except
|
||||||
// for the case where we need to do old and new PNI comparisons. We'll wait
|
// for the case where we need to do old and new PNI comparisons. We'll wait
|
||||||
|
@ -2144,17 +2117,16 @@ export class ConversationModel extends window.Backbone
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.set(
|
this.set({
|
||||||
'pni',
|
pni: pni ? normalizePni(pni, 'Conversation.updatePni') : undefined,
|
||||||
pni ? normalizePni(pni, 'Conversation.updatePni') : undefined
|
});
|
||||||
);
|
|
||||||
const newPniSignatureVerified = pni ? pniSignatureVerified : false;
|
const newPniSignatureVerified = pni ? pniSignatureVerified : false;
|
||||||
if (this.get('pniSignatureVerified') !== newPniSignatureVerified) {
|
if (this.get('pniSignatureVerified') !== newPniSignatureVerified) {
|
||||||
log.warn(
|
log.warn(
|
||||||
`updatePni/${this.idForLogging()}: setting ` +
|
`updatePni/${this.idForLogging()}: setting ` +
|
||||||
`pniSignatureVerified to ${newPniSignatureVerified}`
|
`pniSignatureVerified to ${newPniSignatureVerified}`
|
||||||
);
|
);
|
||||||
this.set('pniSignatureVerified', newPniSignatureVerified);
|
this.set({ pniSignatureVerified: newPniSignatureVerified });
|
||||||
this.captureChange('pniSignatureVerified');
|
this.captureChange('pniSignatureVerified');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2211,16 +2183,16 @@ export class ConversationModel extends window.Backbone
|
||||||
}
|
}
|
||||||
|
|
||||||
drop(DataWriter.updateConversation(this.attributes));
|
drop(DataWriter.updateConversation(this.attributes));
|
||||||
this.trigger('idUpdated', this, 'pni', oldValue);
|
window.ConversationController.idUpdated(this, 'pni', oldValue);
|
||||||
this.captureChange('updatePni');
|
this.captureChange('updatePni');
|
||||||
}
|
}
|
||||||
|
|
||||||
updateGroupId(groupId?: string): void {
|
updateGroupId(groupId?: string): void {
|
||||||
const oldValue = this.get('groupId');
|
const oldValue = this.get('groupId');
|
||||||
if (groupId && groupId !== oldValue) {
|
if (groupId && groupId !== oldValue) {
|
||||||
this.set('groupId', groupId);
|
this.set({ groupId });
|
||||||
drop(DataWriter.updateConversation(this.attributes));
|
drop(DataWriter.updateConversation(this.attributes));
|
||||||
this.trigger('idUpdated', this, 'groupId', oldValue);
|
window.ConversationController.idUpdated(this, 'groupId', oldValue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2232,7 +2204,7 @@ export class ConversationModel extends window.Backbone
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.set('reportingToken', newValue);
|
this.set({ reportingToken: newValue });
|
||||||
await DataWriter.updateConversation(this.attributes);
|
await DataWriter.updateConversation(this.attributes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3021,7 +2993,7 @@ export class ConversationModel extends window.Backbone
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (contacts.length === 1 && isMe(contacts.first()?.attributes)) {
|
if (contacts.length === 1 && isMe(contacts[0]?.attributes)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3158,9 +3130,7 @@ export class ConversationModel extends window.Backbone
|
||||||
|
|
||||||
onMemberVerifiedChange(): void {
|
onMemberVerifiedChange(): void {
|
||||||
// If the verified state of a member changes, our aggregate state changes.
|
// If the verified state of a member changes, our aggregate state changes.
|
||||||
// We trigger both events to replicate the behavior of window.Backbone.Model.set()
|
window.ConversationController.conversationUpdated(this, this.attributes);
|
||||||
this.trigger('change:verified', this);
|
|
||||||
this.trigger('change', this, { force: true });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async toggleVerified(): Promise<unknown> {
|
async toggleVerified(): Promise<unknown> {
|
||||||
|
@ -3527,7 +3497,7 @@ export class ConversationModel extends window.Backbone
|
||||||
const notificationId = await this.addNotification(
|
const notificationId = await this.addNotification(
|
||||||
'universal-timer-notification'
|
'universal-timer-notification'
|
||||||
);
|
);
|
||||||
this.set('pendingUniversalTimer', notificationId);
|
this.set({ pendingUniversalTimer: notificationId });
|
||||||
}
|
}
|
||||||
|
|
||||||
async maybeApplyUniversalTimer(): Promise<void> {
|
async maybeApplyUniversalTimer(): Promise<void> {
|
||||||
|
@ -3560,7 +3530,7 @@ export class ConversationModel extends window.Backbone
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.set('pendingUniversalTimer', undefined);
|
this.set({ pendingUniversalTimer: undefined });
|
||||||
log.info(
|
log.info(
|
||||||
`maybeRemoveUniversalTimer(${this.idForLogging()}): removed notification`
|
`maybeRemoveUniversalTimer(${this.idForLogging()}): removed notification`
|
||||||
);
|
);
|
||||||
|
@ -3593,7 +3563,7 @@ export class ConversationModel extends window.Backbone
|
||||||
const notificationId = await this.addNotification(
|
const notificationId = await this.addNotification(
|
||||||
'contact-removed-notification'
|
'contact-removed-notification'
|
||||||
);
|
);
|
||||||
this.set('pendingRemovedContactNotification', notificationId);
|
this.set({ pendingRemovedContactNotification: notificationId });
|
||||||
await DataWriter.updateConversation(this.attributes);
|
await DataWriter.updateConversation(this.attributes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3603,7 +3573,7 @@ export class ConversationModel extends window.Backbone
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.set('pendingRemovedContactNotification', undefined);
|
this.set({ pendingRemovedContactNotification: undefined });
|
||||||
log.info(
|
log.info(
|
||||||
`maybeClearContactRemoved(${this.idForLogging()}): removed notification`
|
`maybeClearContactRemoved(${this.idForLogging()}): removed notification`
|
||||||
);
|
);
|
||||||
|
@ -3679,10 +3649,6 @@ export class ConversationModel extends window.Backbone
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
override validate(attributes = this.attributes): string | null {
|
|
||||||
return validateConversation(attributes);
|
|
||||||
}
|
|
||||||
|
|
||||||
async queueJob<T>(
|
async queueJob<T>(
|
||||||
name: string,
|
name: string,
|
||||||
callback: (abortSignal: AbortSignal) => Promise<T>
|
callback: (abortSignal: AbortSignal) => Promise<T>
|
||||||
|
@ -3838,6 +3804,8 @@ export class ConversationModel extends window.Backbone
|
||||||
}
|
}
|
||||||
|
|
||||||
async sendStickerMessage(packId: string, stickerId: number): Promise<void> {
|
async sendStickerMessage(packId: string, stickerId: number): Promise<void> {
|
||||||
|
const { readStickerData } = window.Signal.Migrations;
|
||||||
|
|
||||||
const packData = Stickers.getStickerPack(packId);
|
const packData = Stickers.getStickerPack(packId);
|
||||||
const stickerData = Stickers.getSticker(packId, stickerId);
|
const stickerData = Stickers.getSticker(packId, stickerId);
|
||||||
if (!stickerData || !packData) {
|
if (!stickerData || !packData) {
|
||||||
|
@ -3927,18 +3895,6 @@ export class ConversationModel extends window.Backbone
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
batchReduxChanges(callback: () => void): void {
|
|
||||||
strictAssert(!this.#isInReduxBatch, 'Nested redux batching is not allowed');
|
|
||||||
this.#isInReduxBatch = true;
|
|
||||||
batchDispatch(() => {
|
|
||||||
try {
|
|
||||||
callback();
|
|
||||||
} finally {
|
|
||||||
this.#isInReduxBatch = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
beforeMessageSend({
|
beforeMessageSend({
|
||||||
message,
|
message,
|
||||||
dontAddMessage,
|
dontAddMessage,
|
||||||
|
@ -3952,57 +3908,53 @@ export class ConversationModel extends window.Backbone
|
||||||
now: number;
|
now: number;
|
||||||
extraReduxActions?: () => void;
|
extraReduxActions?: () => void;
|
||||||
}): void {
|
}): void {
|
||||||
this.batchReduxChanges(() => {
|
const { clearUnreadMetrics } = window.reduxActions.conversations;
|
||||||
const { clearUnreadMetrics } = window.reduxActions.conversations;
|
clearUnreadMetrics(this.id);
|
||||||
clearUnreadMetrics(this.id);
|
|
||||||
|
|
||||||
const enabledProfileSharing = Boolean(!this.get('profileSharing'));
|
const enabledProfileSharing = Boolean(!this.get('profileSharing'));
|
||||||
const unarchivedConversation = Boolean(this.get('isArchived'));
|
const unarchivedConversation = Boolean(this.get('isArchived'));
|
||||||
|
|
||||||
log.info(
|
log.info(
|
||||||
`beforeMessageSend(${this.idForLogging()}): ` +
|
`beforeMessageSend(${this.idForLogging()}): ` +
|
||||||
`clearDraft(${!dontClearDraft}) addMessage(${!dontAddMessage})`
|
`clearDraft(${!dontClearDraft}) addMessage(${!dontAddMessage})`
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!dontAddMessage) {
|
if (!dontAddMessage) {
|
||||||
this.#doAddSingleMessage(message, { isJustSent: true });
|
this.#doAddSingleMessage(message, { isJustSent: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
const draftProperties = dontClearDraft
|
const draftProperties = dontClearDraft
|
||||||
? {}
|
? {}
|
||||||
: {
|
: {
|
||||||
draft: '',
|
draft: '',
|
||||||
draftEditMessage: undefined,
|
draftEditMessage: undefined,
|
||||||
draftBodyRanges: [],
|
draftBodyRanges: [],
|
||||||
draftTimestamp: null,
|
draftTimestamp: null,
|
||||||
quotedMessageId: undefined,
|
quotedMessageId: undefined,
|
||||||
};
|
};
|
||||||
const lastMessageProperties = this.getLastMessageData(message, message);
|
const lastMessageProperties = this.getLastMessageData(message, message);
|
||||||
const isEditMessage = Boolean(message.editHistory);
|
const isEditMessage = Boolean(message.editHistory);
|
||||||
|
|
||||||
this.set({
|
this.set({
|
||||||
...draftProperties,
|
...draftProperties,
|
||||||
...lastMessageProperties,
|
...lastMessageProperties,
|
||||||
...(enabledProfileSharing ? { profileSharing: true } : {}),
|
...(enabledProfileSharing ? { profileSharing: true } : {}),
|
||||||
...(dontAddMessage
|
...(dontAddMessage ? {} : this.incrementSentMessageCount({ dry: true })),
|
||||||
? {}
|
// If it's an edit message we don't want to optimistically set the
|
||||||
: this.incrementSentMessageCount({ dry: true })),
|
// active_at & timestamp to now. We want it to stay the same.
|
||||||
// If it's an edit message we don't want to optimistically set the
|
active_at: isEditMessage ? this.get('active_at') : now,
|
||||||
// active_at & timestamp to now. We want it to stay the same.
|
timestamp: isEditMessage ? this.get('timestamp') : now,
|
||||||
active_at: isEditMessage ? this.get('active_at') : now,
|
...(unarchivedConversation ? { isArchived: false } : {}),
|
||||||
timestamp: isEditMessage ? this.get('timestamp') : now,
|
|
||||||
...(unarchivedConversation ? { isArchived: false } : {}),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (enabledProfileSharing) {
|
|
||||||
this.captureChange('beforeMessageSend/mandatoryProfileSharing');
|
|
||||||
}
|
|
||||||
if (unarchivedConversation) {
|
|
||||||
this.captureChange('beforeMessageSend/unarchive');
|
|
||||||
}
|
|
||||||
|
|
||||||
extraReduxActions?.();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (enabledProfileSharing) {
|
||||||
|
this.captureChange('beforeMessageSend/mandatoryProfileSharing');
|
||||||
|
}
|
||||||
|
if (unarchivedConversation) {
|
||||||
|
this.captureChange('beforeMessageSend/unarchive');
|
||||||
|
}
|
||||||
|
|
||||||
|
extraReduxActions?.();
|
||||||
}
|
}
|
||||||
|
|
||||||
async enqueueMessageForSend(
|
async enqueueMessageForSend(
|
||||||
|
@ -4037,6 +3989,9 @@ export class ConversationModel extends window.Backbone
|
||||||
extraReduxActions?: () => void;
|
extraReduxActions?: () => void;
|
||||||
} = {}
|
} = {}
|
||||||
): Promise<MessageAttributesType | undefined> {
|
): Promise<MessageAttributesType | undefined> {
|
||||||
|
const { deleteAttachmentData, upgradeMessageSchema } =
|
||||||
|
window.Signal.Migrations;
|
||||||
|
|
||||||
if (this.isGroupV1AndDisabled()) {
|
if (this.isGroupV1AndDisabled()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -4266,7 +4221,7 @@ export class ConversationModel extends window.Backbone
|
||||||
|
|
||||||
log.info(`maybeClearUsername(${this.idForLogging()}): clearing username`);
|
log.info(`maybeClearUsername(${this.idForLogging()}): clearing username`);
|
||||||
|
|
||||||
this.unset('username');
|
this.set({ username: undefined });
|
||||||
|
|
||||||
if (this.get('needsTitleTransition') && getProfileName(this.attributes)) {
|
if (this.get('needsTitleTransition') && getProfileName(this.attributes)) {
|
||||||
log.info(
|
log.info(
|
||||||
|
@ -4274,7 +4229,7 @@ export class ConversationModel extends window.Backbone
|
||||||
);
|
);
|
||||||
const { type, e164, username } = this.attributes;
|
const { type, e164, username } = this.attributes;
|
||||||
|
|
||||||
this.unset('needsTitleTransition');
|
this.set({ needsTitleTransition: undefined });
|
||||||
|
|
||||||
await this.addNotification('title-transition-notification', {
|
await this.addNotification('title-transition-notification', {
|
||||||
readStatus: ReadStatus.Read,
|
readStatus: ReadStatus.Read,
|
||||||
|
@ -4310,7 +4265,7 @@ export class ConversationModel extends window.Backbone
|
||||||
|
|
||||||
log.info(`updateUsername(${this.idForLogging()}): updating username`);
|
log.info(`updateUsername(${this.idForLogging()}): updating username`);
|
||||||
|
|
||||||
this.set('username', username);
|
this.set({ username });
|
||||||
this.captureChange('updateUsername');
|
this.captureChange('updateUsername');
|
||||||
|
|
||||||
if (shouldSave) {
|
if (shouldSave) {
|
||||||
|
@ -4469,7 +4424,7 @@ export class ConversationModel extends window.Backbone
|
||||||
|
|
||||||
async #onActiveAtChange(): Promise<void> {
|
async #onActiveAtChange(): Promise<void> {
|
||||||
if (this.get('active_at') && this.get('messagesDeleted')) {
|
if (this.get('active_at') && this.get('messagesDeleted')) {
|
||||||
this.set('messagesDeleted', false);
|
this.set({ messagesDeleted: false });
|
||||||
await DataWriter.updateConversation(this.attributes);
|
await DataWriter.updateConversation(this.attributes);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4713,7 +4668,7 @@ export class ConversationModel extends window.Backbone
|
||||||
'updateExpirationTimer: Resetting expireTimerVersion since this is initialSync'
|
'updateExpirationTimer: Resetting expireTimerVersion since this is initialSync'
|
||||||
);
|
);
|
||||||
// This is reset after unlink, but we do it here as well to recover from errors
|
// This is reset after unlink, but we do it here as well to recover from errors
|
||||||
this.set('expireTimerVersion', INITIAL_EXPIRE_TIMER_VERSION);
|
this.set({ expireTimerVersion: INITIAL_EXPIRE_TIMER_VERSION });
|
||||||
}
|
}
|
||||||
|
|
||||||
let expireTimer: DurationInSeconds | undefined = providedExpireTimer;
|
let expireTimer: DurationInSeconds | undefined = providedExpireTimer;
|
||||||
|
@ -5006,6 +4961,12 @@ export class ConversationModel extends window.Backbone
|
||||||
decryptionKey?: Uint8Array | null | undefined;
|
decryptionKey?: Uint8Array | null | undefined;
|
||||||
forceFetch?: boolean;
|
forceFetch?: boolean;
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
|
const {
|
||||||
|
deleteAttachmentData,
|
||||||
|
doesAttachmentExist,
|
||||||
|
writeNewAttachmentData,
|
||||||
|
} = window.Signal.Migrations;
|
||||||
|
|
||||||
const { avatarUrl, decryptionKey, forceFetch } = options;
|
const { avatarUrl, decryptionKey, forceFetch } = options;
|
||||||
if (isMe(this.attributes)) {
|
if (isMe(this.attributes)) {
|
||||||
if (avatarUrl) {
|
if (avatarUrl) {
|
||||||
|
@ -5106,7 +5067,7 @@ export class ConversationModel extends window.Backbone
|
||||||
|
|
||||||
const { type, e164, username } = this.attributes;
|
const { type, e164, username } = this.attributes;
|
||||||
|
|
||||||
this.unset('needsTitleTransition');
|
this.set({ needsTitleTransition: undefined });
|
||||||
|
|
||||||
await this.addNotification('title-transition-notification', {
|
await this.addNotification('title-transition-notification', {
|
||||||
readStatus: ReadStatus.Read,
|
readStatus: ReadStatus.Read,
|
||||||
|
@ -5122,7 +5083,7 @@ export class ConversationModel extends window.Backbone
|
||||||
}
|
}
|
||||||
|
|
||||||
// Don't trigger immediate profile fetches when syncing to remote storage
|
// Don't trigger immediate profile fetches when syncing to remote storage
|
||||||
this.set({ profileKey }, { silent: viaStorageServiceSync });
|
this.set({ profileKey }, { noTrigger: viaStorageServiceSync });
|
||||||
|
|
||||||
// If our profile key was cleared above, we don't tell our linked devices about it.
|
// If our profile key was cleared above, we don't tell our linked devices about it.
|
||||||
// We want linked devices to tell us what it should be, instead of telling them to
|
// We want linked devices to tell us what it should be, instead of telling them to
|
||||||
|
@ -5244,10 +5205,7 @@ export class ConversationModel extends window.Backbone
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchContacts(): void {
|
fetchContacts(): void {
|
||||||
const members = this.getMembers();
|
this.contactCollection = this.getMembers();
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
||||||
this.contactCollection!.reset(members);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async destroyMessages({
|
async destroyMessages({
|
||||||
|
@ -5423,7 +5381,7 @@ export class ConversationModel extends window.Backbone
|
||||||
}
|
}
|
||||||
|
|
||||||
const newVersion = expireTimerVersion + 1;
|
const newVersion = expireTimerVersion + 1;
|
||||||
this.set('expireTimerVersion', newVersion);
|
this.set({ expireTimerVersion: newVersion });
|
||||||
await DataWriter.updateConversation(this.attributes);
|
await DataWriter.updateConversation(this.attributes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5511,6 +5469,8 @@ export class ConversationModel extends window.Backbone
|
||||||
url: string;
|
url: string;
|
||||||
absolutePath?: string;
|
absolutePath?: string;
|
||||||
}> {
|
}> {
|
||||||
|
const { getAbsoluteTempPath } = window.Signal.Migrations;
|
||||||
|
|
||||||
const saveToDisk = shouldSaveNotificationAvatarToDisk();
|
const saveToDisk = shouldSaveNotificationAvatarToDisk();
|
||||||
const avatarUrl = getLocalAvatarUrl(this.attributes);
|
const avatarUrl = getLocalAvatarUrl(this.attributes);
|
||||||
if (avatarUrl) {
|
if (avatarUrl) {
|
||||||
|
@ -5532,6 +5492,13 @@ export class ConversationModel extends window.Backbone
|
||||||
}
|
}
|
||||||
|
|
||||||
async #getTemporaryAvatarPath(): Promise<string | undefined> {
|
async #getTemporaryAvatarPath(): Promise<string | undefined> {
|
||||||
|
const {
|
||||||
|
copyIntoTempDirectory,
|
||||||
|
deleteAttachmentData,
|
||||||
|
getAbsoluteAttachmentPath,
|
||||||
|
getAbsoluteTempPath,
|
||||||
|
} = window.Signal.Migrations;
|
||||||
|
|
||||||
const avatar = getAvatar(this.attributes);
|
const avatar = getAvatar(this.attributes);
|
||||||
if (avatar?.path == null) {
|
if (avatar?.path == null) {
|
||||||
return undefined;
|
return undefined;
|
||||||
|
@ -5672,13 +5639,19 @@ export class ConversationModel extends window.Backbone
|
||||||
);
|
);
|
||||||
// User was not previously typing before. State change!
|
// User was not previously typing before. State change!
|
||||||
if (!record) {
|
if (!record) {
|
||||||
this.trigger('change', this, { force: true });
|
window.ConversationController.conversationUpdated(
|
||||||
|
this,
|
||||||
|
this.attributes
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
delete this.contactTypingTimers[typingToken];
|
delete this.contactTypingTimers[typingToken];
|
||||||
if (record) {
|
if (record) {
|
||||||
// User was previously typing, and is no longer. State change!
|
// User was previously typing, and is no longer. State change!
|
||||||
this.trigger('change', this, { force: true });
|
window.ConversationController.conversationUpdated(
|
||||||
|
this,
|
||||||
|
this.attributes
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5692,7 +5665,7 @@ export class ConversationModel extends window.Backbone
|
||||||
delete this.contactTypingTimers[typingToken];
|
delete this.contactTypingTimers[typingToken];
|
||||||
|
|
||||||
// User was previously typing, but timed out or we received message. State change!
|
// User was previously typing, but timed out or we received message. State change!
|
||||||
this.trigger('change', this, { force: true });
|
window.ConversationController.conversationUpdated(this, this.attributes);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5701,11 +5674,11 @@ export class ConversationModel extends window.Backbone
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const validationError = this.validate();
|
const validationErrorString = validateConversation(this.attributes);
|
||||||
if (validationError) {
|
if (validationErrorString) {
|
||||||
log.error(
|
log.error(
|
||||||
`not pinning ${this.idForLogging()} because of ` +
|
`not pinning ${this.idForLogging()} because of ` +
|
||||||
`validation error ${validationError}`
|
`validation error ${validationErrorString}`
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -5719,7 +5692,7 @@ export class ConversationModel extends window.Backbone
|
||||||
|
|
||||||
this.writePinnedConversations([...pinnedConversationIds]);
|
this.writePinnedConversations([...pinnedConversationIds]);
|
||||||
|
|
||||||
this.set('isPinned', true);
|
this.set({ isPinned: true });
|
||||||
|
|
||||||
if (this.get('isArchived')) {
|
if (this.get('isArchived')) {
|
||||||
this.set({ isArchived: false });
|
this.set({ isArchived: false });
|
||||||
|
@ -5742,7 +5715,7 @@ export class ConversationModel extends window.Backbone
|
||||||
|
|
||||||
this.writePinnedConversations([...pinnedConversationIds]);
|
this.writePinnedConversations([...pinnedConversationIds]);
|
||||||
|
|
||||||
this.set('isPinned', false);
|
this.set({ isPinned: false });
|
||||||
drop(DataWriter.updateConversation(this.attributes));
|
drop(DataWriter.updateConversation(this.attributes));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5771,7 +5744,7 @@ export class ConversationModel extends window.Backbone
|
||||||
acknowledgeGroupMemberNameCollisions(
|
acknowledgeGroupMemberNameCollisions(
|
||||||
groupNameCollisions: ReadonlyDeep<GroupNameCollisionsWithIdsByTitle>
|
groupNameCollisions: ReadonlyDeep<GroupNameCollisionsWithIdsByTitle>
|
||||||
): void {
|
): void {
|
||||||
this.set('acknowledgedGroupNameCollisions', groupNameCollisions);
|
this.set({ acknowledgedGroupNameCollisions: groupNameCollisions });
|
||||||
drop(DataWriter.updateConversation(this.attributes));
|
drop(DataWriter.updateConversation(this.attributes));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5851,176 +5824,3 @@ export class ConversationModel extends window.Backbone
|
||||||
log.info(`conversation ${this.idForLogging()} jobQueue shutdown complete`);
|
log.info(`conversation ${this.idForLogging()} jobQueue shutdown complete`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
window.Whisper.Conversation = ConversationModel;
|
|
||||||
|
|
||||||
window.Whisper.ConversationCollection = window.Backbone.Collection.extend({
|
|
||||||
model: window.Whisper.Conversation,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* window.Backbone defines a `_byId` field. Here we set up additional `_byE164`,
|
|
||||||
* `_byServiceId`, and `_byGroupId` fields so we can track conversations by more
|
|
||||||
* than just their id.
|
|
||||||
*/
|
|
||||||
initialize() {
|
|
||||||
this.eraseLookups();
|
|
||||||
this.on(
|
|
||||||
'idUpdated',
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
(model: ConversationModel, idProp: string, oldValue: any) => {
|
|
||||||
if (oldValue) {
|
|
||||||
if (idProp === 'e164') {
|
|
||||||
delete this._byE164[oldValue];
|
|
||||||
}
|
|
||||||
if (idProp === 'serviceId') {
|
|
||||||
delete this._byServiceId[oldValue];
|
|
||||||
}
|
|
||||||
if (idProp === 'pni') {
|
|
||||||
delete this._byPni[oldValue];
|
|
||||||
}
|
|
||||||
if (idProp === 'groupId') {
|
|
||||||
delete this._byGroupId[oldValue];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const e164 = model.get('e164');
|
|
||||||
if (e164) {
|
|
||||||
this._byE164[e164] = model;
|
|
||||||
}
|
|
||||||
const serviceId = model.getServiceId();
|
|
||||||
if (serviceId) {
|
|
||||||
this._byServiceId[serviceId] = model;
|
|
||||||
}
|
|
||||||
const pni = model.getPni();
|
|
||||||
if (pni) {
|
|
||||||
this._byPni[pni] = model;
|
|
||||||
}
|
|
||||||
const groupId = model.get('groupId');
|
|
||||||
if (groupId) {
|
|
||||||
this._byGroupId[groupId] = model;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
reset(models?: Array<ConversationModel>, options?: Backbone.Silenceable) {
|
|
||||||
window.Backbone.Collection.prototype.reset.call(this, models, options);
|
|
||||||
this.resetLookups();
|
|
||||||
},
|
|
||||||
|
|
||||||
resetLookups() {
|
|
||||||
this.eraseLookups();
|
|
||||||
this.generateLookups(this.models);
|
|
||||||
},
|
|
||||||
|
|
||||||
generateLookups(models: ReadonlyArray<ConversationModel>) {
|
|
||||||
models.forEach(model => {
|
|
||||||
const e164 = model.get('e164');
|
|
||||||
if (e164) {
|
|
||||||
const existing = this._byE164[e164];
|
|
||||||
|
|
||||||
// Prefer the contact with both e164 and serviceId
|
|
||||||
if (!existing || (existing && !existing.getServiceId())) {
|
|
||||||
this._byE164[e164] = model;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const serviceId = model.getServiceId();
|
|
||||||
if (serviceId) {
|
|
||||||
const existing = this._byServiceId[serviceId];
|
|
||||||
|
|
||||||
// Prefer the contact with both e164 and seviceId
|
|
||||||
if (!existing || (existing && !existing.get('e164'))) {
|
|
||||||
this._byServiceId[serviceId] = model;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const pni = model.getPni();
|
|
||||||
if (pni) {
|
|
||||||
const existing = this._byPni[pni];
|
|
||||||
|
|
||||||
// Prefer the contact with both serviceId and pni
|
|
||||||
if (!existing || (existing && !existing.getServiceId())) {
|
|
||||||
this._byPni[pni] = model;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const groupId = model.get('groupId');
|
|
||||||
if (groupId) {
|
|
||||||
this._byGroupId[groupId] = model;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
eraseLookups() {
|
|
||||||
this._byE164 = Object.create(null);
|
|
||||||
this._byServiceId = Object.create(null);
|
|
||||||
this._byPni = Object.create(null);
|
|
||||||
this._byGroupId = Object.create(null);
|
|
||||||
},
|
|
||||||
|
|
||||||
add(
|
|
||||||
data:
|
|
||||||
| ConversationModel
|
|
||||||
| ConversationAttributesType
|
|
||||||
| Array<ConversationModel>
|
|
||||||
| Array<ConversationAttributesType>
|
|
||||||
) {
|
|
||||||
let hydratedData: Array<ConversationModel> | ConversationModel;
|
|
||||||
|
|
||||||
// First, we need to ensure that the data we're working with is Conversation models
|
|
||||||
if (Array.isArray(data)) {
|
|
||||||
hydratedData = [];
|
|
||||||
for (let i = 0, max = data.length; i < max; i += 1) {
|
|
||||||
const item = data[i];
|
|
||||||
|
|
||||||
// We create a new model if it's not already a model
|
|
||||||
if (has(item, 'get')) {
|
|
||||||
hydratedData.push(item as ConversationModel);
|
|
||||||
} else {
|
|
||||||
hydratedData.push(
|
|
||||||
new window.Whisper.Conversation(item as ConversationAttributesType)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (has(data, 'get')) {
|
|
||||||
hydratedData = data as ConversationModel;
|
|
||||||
} else {
|
|
||||||
hydratedData = new window.Whisper.Conversation(
|
|
||||||
data as ConversationAttributesType
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Next, we update our lookups first to prevent infinite loops on the 'add' event
|
|
||||||
this.generateLookups(
|
|
||||||
Array.isArray(hydratedData) ? hydratedData : [hydratedData]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Lastly, we fire off the add events related to this change
|
|
||||||
// Go home Backbone, you're drunk.
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
window.Backbone.Collection.prototype.add.call(this, hydratedData as any);
|
|
||||||
|
|
||||||
return hydratedData;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* window.Backbone collections have a `_byId` field that `get` defers to. Here, we
|
|
||||||
* override `get` to first access our custom `_byE164`, `_byServiceId`, and
|
|
||||||
* `_byGroupId` functions, followed by falling back to the original
|
|
||||||
* window.Backbone implementation.
|
|
||||||
*/
|
|
||||||
get(id: string) {
|
|
||||||
return (
|
|
||||||
this._byE164[id] ||
|
|
||||||
this._byE164[`+${id}`] ||
|
|
||||||
this._byServiceId[id] ||
|
|
||||||
this._byPni[id] ||
|
|
||||||
this._byGroupId[id] ||
|
|
||||||
window.Backbone.Collection.prototype.get.call(this, id)
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
comparator(m: ConversationModel) {
|
|
||||||
return -(m.get('active_at') || 0);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
|
@ -73,7 +73,7 @@ export async function enqueueReactionForSend({
|
||||||
) {
|
) {
|
||||||
log.info('Enabling profile sharing for reaction send');
|
log.info('Enabling profile sharing for reaction send');
|
||||||
if (!messageConversation.get('profileSharing')) {
|
if (!messageConversation.get('profileSharing')) {
|
||||||
messageConversation.set('profileSharing', true);
|
messageConversation.set({ profileSharing: true });
|
||||||
await DataWriter.updateConversation(messageConversation.attributes);
|
await DataWriter.updateConversation(messageConversation.attributes);
|
||||||
}
|
}
|
||||||
await messageConversation.restoreContact();
|
await messageConversation.restoreContact();
|
||||||
|
|
|
@ -703,8 +703,9 @@ export class BackupImportStream extends Writable {
|
||||||
svrPin,
|
svrPin,
|
||||||
}: Backups.IAccountData): Promise<void> {
|
}: Backups.IAccountData): Promise<void> {
|
||||||
strictAssert(this.#ourConversation === undefined, 'Duplicate AccountData');
|
strictAssert(this.#ourConversation === undefined, 'Duplicate AccountData');
|
||||||
const me =
|
const me = {
|
||||||
window.ConversationController.getOurConversationOrThrow().attributes;
|
...window.ConversationController.getOurConversationOrThrow().attributes,
|
||||||
|
};
|
||||||
this.#ourConversation = me;
|
this.#ourConversation = me;
|
||||||
|
|
||||||
const { storage } = window;
|
const { storage } = window;
|
||||||
|
|
|
@ -97,7 +97,7 @@ async function updateConversationFromContactSync(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
window.Whisper.events.trigger('incrementProgress');
|
window.Whisper.events.emit('incrementProgress');
|
||||||
}
|
}
|
||||||
|
|
||||||
const queue = new PQueue({ concurrency: 1 });
|
const queue = new PQueue({ concurrency: 1 });
|
||||||
|
@ -182,11 +182,11 @@ async function doContactSync({
|
||||||
type: 'private',
|
type: 'private',
|
||||||
};
|
};
|
||||||
|
|
||||||
const validationError = validateConversation(partialConversation);
|
const validationErrorString = validateConversation(partialConversation);
|
||||||
if (validationError) {
|
if (validationErrorString) {
|
||||||
log.error(
|
log.error(
|
||||||
`${logId}: Invalid contact received`,
|
`${logId}: Invalid contact received`,
|
||||||
Errors.toLogFormat(validationError)
|
Errors.toLogFormat(validationErrorString)
|
||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
@ -261,7 +261,7 @@ async function doContactSync({
|
||||||
await Promise.all(promises);
|
await Promise.all(promises);
|
||||||
|
|
||||||
await window.storage.put('synced_at', Date.now());
|
await window.storage.put('synced_at', Date.now());
|
||||||
window.Whisper.events.trigger('contactSync:complete');
|
window.Whisper.events.emit('contactSync:complete');
|
||||||
if (isInitialSync) {
|
if (isInitialSync) {
|
||||||
isInitialSync = false;
|
isInitialSync = false;
|
||||||
}
|
}
|
||||||
|
|
|
@ -553,7 +553,7 @@ async function doGetProfile(
|
||||||
// Record that the accessKey we have in the conversation is invalid
|
// Record that the accessKey we have in the conversation is invalid
|
||||||
const sealedSender = c.get('sealedSender');
|
const sealedSender = c.get('sealedSender');
|
||||||
if (sealedSender !== SEALED_SENDER.DISABLED) {
|
if (sealedSender !== SEALED_SENDER.DISABLED) {
|
||||||
c.set('sealedSender', SEALED_SENDER.DISABLED);
|
c.set({ sealedSender: SEALED_SENDER.DISABLED });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Retry fetch using last known profileKey or fetch unversioned profile.
|
// Retry fetch using last known profileKey or fetch unversioned profile.
|
||||||
|
@ -580,7 +580,7 @@ async function doGetProfile(
|
||||||
if (error.code === 404) {
|
if (error.code === 404) {
|
||||||
log.info(`${logId}: Profile not found`);
|
log.info(`${logId}: Profile not found`);
|
||||||
|
|
||||||
c.set('profileLastFetchedAt', Date.now());
|
c.set({ profileLastFetchedAt: Date.now() });
|
||||||
|
|
||||||
if (!isVersioned || ignoreProfileKey) {
|
if (!isVersioned || ignoreProfileKey) {
|
||||||
log.info(`${logId}: Marking conversation unregistered`);
|
log.info(`${logId}: Marking conversation unregistered`);
|
||||||
|
@ -655,20 +655,20 @@ async function doGetProfile(
|
||||||
if (isFieldDefined(profile.about)) {
|
if (isFieldDefined(profile.about)) {
|
||||||
if (updatedDecryptionKey != null) {
|
if (updatedDecryptionKey != null) {
|
||||||
const decrypted = decryptField(profile.about, updatedDecryptionKey);
|
const decrypted = decryptField(profile.about, updatedDecryptionKey);
|
||||||
c.set('about', formatTextField(decrypted));
|
c.set({ about: formatTextField(decrypted) });
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
c.unset('about');
|
c.set({ about: undefined });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step #: Save profile `aboutEmoji` to conversation
|
// Step #: Save profile `aboutEmoji` to conversation
|
||||||
if (isFieldDefined(profile.aboutEmoji)) {
|
if (isFieldDefined(profile.aboutEmoji)) {
|
||||||
if (updatedDecryptionKey != null) {
|
if (updatedDecryptionKey != null) {
|
||||||
const decrypted = decryptField(profile.aboutEmoji, updatedDecryptionKey);
|
const decrypted = decryptField(profile.aboutEmoji, updatedDecryptionKey);
|
||||||
c.set('aboutEmoji', formatTextField(decrypted));
|
c.set({ aboutEmoji: formatTextField(decrypted) });
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
c.unset('aboutEmoji');
|
c.set({ aboutEmoji: undefined });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step #: Save profile `phoneNumberSharing` to conversation
|
// Step #: Save profile `phoneNumberSharing` to conversation
|
||||||
|
@ -681,10 +681,10 @@ async function doGetProfile(
|
||||||
// It should be one byte, but be conservative about it and
|
// It should be one byte, but be conservative about it and
|
||||||
// set `sharingPhoneNumber` to `false` in all cases except [0x01].
|
// set `sharingPhoneNumber` to `false` in all cases except [0x01].
|
||||||
const sharingPhoneNumber = decrypted.length === 1 && decrypted[0] === 1;
|
const sharingPhoneNumber = decrypted.length === 1 && decrypted[0] === 1;
|
||||||
c.set('sharingPhoneNumber', sharingPhoneNumber);
|
c.set({ sharingPhoneNumber });
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
c.unset('sharingPhoneNumber');
|
c.set({ sharingPhoneNumber: undefined });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step #: Save our own `paymentAddress` to Storage
|
// Step #: Save our own `paymentAddress` to Storage
|
||||||
|
@ -697,7 +697,7 @@ async function doGetProfile(
|
||||||
if (profile.capabilities != null) {
|
if (profile.capabilities != null) {
|
||||||
c.set({ capabilities: profile.capabilities });
|
c.set({ capabilities: profile.capabilities });
|
||||||
} else {
|
} else {
|
||||||
c.unset('capabilities');
|
c.set({ capabilities: undefined });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step #: Save our own `observedCapabilities` to Storage and trigger sync if changed
|
// Step #: Save our own `observedCapabilities` to Storage and trigger sync if changed
|
||||||
|
@ -752,7 +752,7 @@ async function doGetProfile(
|
||||||
})),
|
})),
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
c.unset('badges');
|
c.set({ badges: undefined });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step #: Save updated (or clear if missing) profile `credential` to conversation
|
// Step #: Save updated (or clear if missing) profile `credential` to conversation
|
||||||
|
@ -771,7 +771,7 @@ async function doGetProfile(
|
||||||
log.warn(
|
log.warn(
|
||||||
`${logId}: Included credential request, but got no credential. Clearing profileKeyCredential.`
|
`${logId}: Included credential request, but got no credential. Clearing profileKeyCredential.`
|
||||||
);
|
);
|
||||||
c.unset('profileKeyCredential');
|
c.set({ profileKeyCredential: undefined });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -822,7 +822,7 @@ async function doGetProfile(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
c.set('profileLastFetchedAt', Date.now());
|
c.set({ profileLastFetchedAt: Date.now() });
|
||||||
|
|
||||||
// After we successfully decrypted - update lastProfile property
|
// After we successfully decrypted - update lastProfile property
|
||||||
if (
|
if (
|
||||||
|
|
|
@ -85,6 +85,7 @@ import { fromPniUuidBytesOrUntaggedString } from '../util/ServiceId';
|
||||||
import { isDone as isRegistrationDone } from '../util/registration';
|
import { isDone as isRegistrationDone } from '../util/registration';
|
||||||
import { callLinkRefreshJobQueue } from '../jobs/callLinkRefreshJobQueue';
|
import { callLinkRefreshJobQueue } from '../jobs/callLinkRefreshJobQueue';
|
||||||
import { isMockEnvironment } from '../environment';
|
import { isMockEnvironment } from '../environment';
|
||||||
|
import { validateConversation } from '../util/validateConversation';
|
||||||
|
|
||||||
const log = createLogger('storage');
|
const log = createLogger('storage');
|
||||||
|
|
||||||
|
@ -241,9 +242,9 @@ async function generateManifest(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const conversations = window.getConversations();
|
const conversations = window.ConversationController.getAll();
|
||||||
for (let i = 0; i < conversations.length; i += 1) {
|
for (let i = 0; i < conversations.length; i += 1) {
|
||||||
const conversation = conversations.models[i];
|
const conversation = conversations[i];
|
||||||
|
|
||||||
let identifierType;
|
let identifierType;
|
||||||
let storageRecord;
|
let storageRecord;
|
||||||
|
@ -267,10 +268,12 @@ async function generateManifest(
|
||||||
let shouldDrop = false;
|
let shouldDrop = false;
|
||||||
let dropReason: string | undefined;
|
let dropReason: string | undefined;
|
||||||
|
|
||||||
const validationError = conversation.validate();
|
const validationErrorString = validateConversation(
|
||||||
if (validationError) {
|
conversation.attributes
|
||||||
|
);
|
||||||
|
if (validationErrorString) {
|
||||||
shouldDrop = true;
|
shouldDrop = true;
|
||||||
dropReason = `local validation error=${validationError}`;
|
dropReason = `local validation error=${validationErrorString}`;
|
||||||
} else if (conversation.isUnregisteredAndStale()) {
|
} else if (conversation.isUnregisteredAndStale()) {
|
||||||
shouldDrop = true;
|
shouldDrop = true;
|
||||||
dropReason = 'unregistered and stale';
|
dropReason = 'unregistered and stale';
|
||||||
|
@ -294,7 +297,7 @@ async function generateManifest(
|
||||||
`dropping contact=${recordID} ` +
|
`dropping contact=${recordID} ` +
|
||||||
`due to ${dropReason}`
|
`due to ${dropReason}`
|
||||||
);
|
);
|
||||||
conversation.unset('storageID');
|
conversation.set({ storageID: undefined });
|
||||||
deleteKeys.add(droppedID);
|
deleteKeys.add(droppedID);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
@ -1267,7 +1270,7 @@ async function processManifest(
|
||||||
const localVersions = new Map<string, number | undefined>();
|
const localVersions = new Map<string, number | undefined>();
|
||||||
let localRecordCount = 0;
|
let localRecordCount = 0;
|
||||||
|
|
||||||
const conversations = window.getConversations();
|
const conversations = window.ConversationController.getAll();
|
||||||
conversations.forEach((conversation: ConversationModel) => {
|
conversations.forEach((conversation: ConversationModel) => {
|
||||||
const storageID = conversation.get('storageID');
|
const storageID = conversation.get('storageID');
|
||||||
if (storageID) {
|
if (storageID) {
|
||||||
|
@ -1387,44 +1390,45 @@ async function processManifest(
|
||||||
// new storageID for that record, and upload.
|
// new storageID for that record, and upload.
|
||||||
// This might happen if a device pushes a manifest which doesn't contain
|
// This might happen if a device pushes a manifest which doesn't contain
|
||||||
// the keys that we have in our local database.
|
// the keys that we have in our local database.
|
||||||
window.getConversations().forEach((conversation: ConversationModel) => {
|
window.ConversationController.getAll().forEach(
|
||||||
const storageID = conversation.get('storageID');
|
(conversation: ConversationModel) => {
|
||||||
if (storageID && !remoteKeys.has(storageID)) {
|
const storageID = conversation.get('storageID');
|
||||||
const storageVersion = conversation.get('storageVersion');
|
if (storageID && !remoteKeys.has(storageID)) {
|
||||||
const missingKey = redactStorageID(
|
const storageVersion = conversation.get('storageVersion');
|
||||||
storageID,
|
const missingKey = redactStorageID(
|
||||||
storageVersion,
|
storageID,
|
||||||
conversation
|
storageVersion,
|
||||||
);
|
conversation
|
||||||
|
|
||||||
// Remote might have dropped this conversation already, but our value of
|
|
||||||
// `firstUnregisteredAt` is too high for us to drop it. Don't reupload it!
|
|
||||||
if (
|
|
||||||
isDirectConversation(conversation.attributes) &&
|
|
||||||
conversation.isUnregistered()
|
|
||||||
) {
|
|
||||||
log.info(
|
|
||||||
`process(${version}): localKey=${missingKey} is ` +
|
|
||||||
'unregistered and not in remote manifest'
|
|
||||||
);
|
);
|
||||||
conversation.setUnregistered({
|
|
||||||
timestamp: Date.now() - getMessageQueueTime(),
|
|
||||||
fromStorageService: true,
|
|
||||||
|
|
||||||
// Saving below
|
// Remote might have dropped this conversation already, but our value of
|
||||||
shouldSave: false,
|
// `firstUnregisteredAt` is too high for us to drop it. Don't reupload it!
|
||||||
});
|
if (
|
||||||
} else {
|
isDirectConversation(conversation.attributes) &&
|
||||||
log.info(
|
conversation.isUnregistered()
|
||||||
`process(${version}): localKey=${missingKey} ` +
|
) {
|
||||||
'was not in remote manifest'
|
log.info(
|
||||||
);
|
`process(${version}): localKey=${missingKey} is ` +
|
||||||
|
'unregistered and not in remote manifest'
|
||||||
|
);
|
||||||
|
conversation.setUnregistered({
|
||||||
|
timestamp: Date.now() - getMessageQueueTime(),
|
||||||
|
fromStorageService: true,
|
||||||
|
|
||||||
|
// Saving below
|
||||||
|
shouldSave: false,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
log.info(
|
||||||
|
`process(${version}): localKey=${missingKey} ` +
|
||||||
|
'was not in remote manifest'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
conversation.set({ storageID: undefined, storageVersion: undefined });
|
||||||
|
drop(updateConversation(conversation.attributes));
|
||||||
}
|
}
|
||||||
conversation.unset('storageID');
|
|
||||||
conversation.unset('storageVersion');
|
|
||||||
drop(updateConversation(conversation.attributes));
|
|
||||||
}
|
}
|
||||||
});
|
);
|
||||||
|
|
||||||
// Refetch various records post-merge
|
// Refetch various records post-merge
|
||||||
{
|
{
|
||||||
|
@ -2192,10 +2196,12 @@ export async function eraseAllStorageServiceState({
|
||||||
window.reduxActions.user.eraseStorageServiceState();
|
window.reduxActions.user.eraseStorageServiceState();
|
||||||
|
|
||||||
// Conversations. These properties are not present in redux.
|
// Conversations. These properties are not present in redux.
|
||||||
window.getConversations().forEach(conversation => {
|
window.ConversationController.getAll().forEach(conversation => {
|
||||||
conversation.unset('storageID');
|
conversation.set({
|
||||||
conversation.unset('needsStorageServiceSync');
|
storageID: undefined,
|
||||||
conversation.unset('storageUnknownFields');
|
needsStorageServiceSync: undefined,
|
||||||
|
storageUnknownFields: undefined,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Then make sure outstanding conversation saves are flushed
|
// Then make sure outstanding conversation saves are flushed
|
||||||
|
@ -2290,7 +2296,7 @@ export const runStorageServiceSyncJob = debounce(
|
||||||
await sync({ reason });
|
await sync({ reason });
|
||||||
|
|
||||||
// Notify listeners about sync completion
|
// Notify listeners about sync completion
|
||||||
window.Whisper.events.trigger('storageService:syncComplete');
|
window.Whisper.events.emit('storageService:syncComplete');
|
||||||
},
|
},
|
||||||
`sync v${window.storage.get('manifestVersion')}`
|
`sync v${window.storage.get('manifestVersion')}`
|
||||||
)
|
)
|
||||||
|
|
|
@ -212,7 +212,7 @@ function addUnknownFields(
|
||||||
// If the record doesn't have unknown fields attached but we have them
|
// If the record doesn't have unknown fields attached but we have them
|
||||||
// saved locally then we need to clear it out
|
// saved locally then we need to clear it out
|
||||||
details.push('clearing unknown fields');
|
details.push('clearing unknown fields');
|
||||||
conversation.unset('storageUnknownFields');
|
conversation.set({ storageUnknownFields: undefined });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1487,9 +1487,10 @@ export async function mergeAccountRecord(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pinnedConversations) {
|
if (pinnedConversations) {
|
||||||
const modelPinnedConversations = window
|
const modelPinnedConversations =
|
||||||
.getConversations()
|
window.ConversationController.getAll().filter(convo =>
|
||||||
.filter(convo => Boolean(convo.get('isPinned')));
|
Boolean(convo.get('isPinned'))
|
||||||
|
);
|
||||||
|
|
||||||
const modelPinnedConversationIds = modelPinnedConversations.map(convo =>
|
const modelPinnedConversationIds = modelPinnedConversations.map(convo =>
|
||||||
convo.get('id')
|
convo.get('id')
|
||||||
|
|
|
@ -210,7 +210,7 @@ async function updateUsernameAndSyncProfile(
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const me = window.ConversationController.getOurConversationOrThrow();
|
const me = window.ConversationController.getOurConversationOrThrow();
|
||||||
|
|
||||||
// Update backbone, update DB, then tell linked devices about profile update
|
// Update model, update DB, then tell linked devices about profile update
|
||||||
await me.updateUsername(username);
|
await me.updateUsername(username);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -135,7 +135,7 @@ export async function writeProfile(
|
||||||
maybeProfileAvatarUpdate = { profileAvatar: undefined };
|
maybeProfileAvatarUpdate = { profileAvatar: undefined };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update backbone, update DB, run storage service upload
|
// Update model, update DB, run storage service upload
|
||||||
model.set({
|
model.set({
|
||||||
about: aboutText,
|
about: aboutText,
|
||||||
aboutEmoji,
|
aboutEmoji,
|
||||||
|
|
|
@ -2,14 +2,14 @@
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
export async function toggleVerification(id: string): Promise<void> {
|
export async function toggleVerification(id: string): Promise<void> {
|
||||||
const contact = window.getConversations().get(id);
|
const contact = window.ConversationController.get(id);
|
||||||
if (contact) {
|
if (contact) {
|
||||||
await contact.toggleVerified();
|
await contact.toggleVerified();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function reloadProfiles(id: string): Promise<void> {
|
export async function reloadProfiles(id: string): Promise<void> {
|
||||||
const contact = window.getConversations().get(id);
|
const contact = window.ConversationController.get(id);
|
||||||
if (contact) {
|
if (contact) {
|
||||||
await contact.getProfiles();
|
await contact.getProfiles();
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@ import { explodePromise } from '../util/explodePromise';
|
||||||
// Matching Whisper.events.trigger API
|
// Matching Whisper.events.trigger API
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
export function trigger(name: string, ...rest: Array<any>): void {
|
export function trigger(name: string, ...rest: Array<any>): void {
|
||||||
window.Whisper.events.trigger(name, ...rest);
|
window.Whisper.events.emit(name, ...rest);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const waitForEvent = (
|
export const waitForEvent = (
|
||||||
|
|
|
@ -7,7 +7,6 @@ import type { ReadonlyDeep } from 'type-fest';
|
||||||
|
|
||||||
import * as Crypto from './Crypto';
|
import * as Crypto from './Crypto';
|
||||||
import * as Curve from './Curve';
|
import * as Curve from './Curve';
|
||||||
import { start as conversationControllerStart } from './ConversationController';
|
|
||||||
import * as Groups from './groups';
|
import * as Groups from './groups';
|
||||||
import OS from './util/os/osMain';
|
import OS from './util/os/osMain';
|
||||||
import { isProduction } from './util/version';
|
import { isProduction } from './util/version';
|
||||||
|
@ -486,8 +485,6 @@ export const setup = (options: {
|
||||||
Components,
|
Components,
|
||||||
Crypto,
|
Crypto,
|
||||||
Curve,
|
Curve,
|
||||||
// Note: used in test/index.html, and not type-checked!
|
|
||||||
conversationControllerStart,
|
|
||||||
Groups,
|
Groups,
|
||||||
Migrations,
|
Migrations,
|
||||||
OS,
|
OS,
|
||||||
|
|
|
@ -699,10 +699,6 @@ type ReadableInterface = {
|
||||||
|
|
||||||
getAllConversations: () => Array<ConversationType>;
|
getAllConversations: () => Array<ConversationType>;
|
||||||
getAllConversationIds: () => Array<string>;
|
getAllConversationIds: () => Array<string>;
|
||||||
getAllGroupsInvolvingServiceId: (
|
|
||||||
serviceId: ServiceIdString
|
|
||||||
) => Array<ConversationType>;
|
|
||||||
|
|
||||||
getGroupSendCombinedEndorsementExpiration: (groupId: string) => number | null;
|
getGroupSendCombinedEndorsementExpiration: (groupId: string) => number | null;
|
||||||
getGroupSendEndorsementsData: (
|
getGroupSendEndorsementsData: (
|
||||||
groupId: string
|
groupId: string
|
||||||
|
|
|
@ -372,7 +372,6 @@ export const DataReader: ServerReadableInterface = {
|
||||||
|
|
||||||
getAllConversations,
|
getAllConversations,
|
||||||
getAllConversationIds,
|
getAllConversationIds,
|
||||||
getAllGroupsInvolvingServiceId,
|
|
||||||
|
|
||||||
getGroupSendCombinedEndorsementExpiration,
|
getGroupSendCombinedEndorsementExpiration,
|
||||||
getGroupSendEndorsementsData,
|
getGroupSendEndorsementsData,
|
||||||
|
@ -1945,27 +1944,6 @@ function getAllConversationIds(db: ReadableDB): Array<string> {
|
||||||
return rows.map(row => row.id);
|
return rows.map(row => row.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getAllGroupsInvolvingServiceId(
|
|
||||||
db: ReadableDB,
|
|
||||||
serviceId: ServiceIdString
|
|
||||||
): Array<ConversationType> {
|
|
||||||
const rows: ConversationRows = db
|
|
||||||
.prepare(
|
|
||||||
`
|
|
||||||
SELECT json, profileLastFetchedAt, expireTimerVersion
|
|
||||||
FROM conversations WHERE
|
|
||||||
type = 'group' AND
|
|
||||||
members LIKE $serviceId
|
|
||||||
ORDER BY id ASC;
|
|
||||||
`
|
|
||||||
)
|
|
||||||
.all({
|
|
||||||
serviceId: `%${serviceId}%`,
|
|
||||||
});
|
|
||||||
|
|
||||||
return rows.map(row => rowToConversation(row));
|
|
||||||
}
|
|
||||||
|
|
||||||
function searchMessages(
|
function searchMessages(
|
||||||
db: ReadableDB,
|
db: ReadableDB,
|
||||||
{
|
{
|
||||||
|
|
|
@ -900,8 +900,10 @@ function addPendingAttachment(
|
||||||
|
|
||||||
const conversation = window.ConversationController.get(conversationId);
|
const conversation = window.ConversationController.get(conversationId);
|
||||||
if (conversation) {
|
if (conversation) {
|
||||||
conversation.attributes.draftAttachments = nextAttachments;
|
conversation.set({
|
||||||
conversation.attributes.draftChanged = true;
|
draftAttachments: nextAttachments,
|
||||||
|
draftChanged: true,
|
||||||
|
});
|
||||||
drop(DataWriter.updateConversation(conversation.attributes));
|
drop(DataWriter.updateConversation(conversation.attributes));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -1202,8 +1204,10 @@ function removeAttachment(
|
||||||
|
|
||||||
const conversation = window.ConversationController.get(conversationId);
|
const conversation = window.ConversationController.get(conversationId);
|
||||||
if (conversation) {
|
if (conversation) {
|
||||||
conversation.attributes.draftAttachments = nextAttachments;
|
conversation.set({
|
||||||
conversation.attributes.draftChanged = true;
|
draftAttachments: nextAttachments,
|
||||||
|
draftChanged: true,
|
||||||
|
});
|
||||||
await DataWriter.updateConversation(conversation.attributes);
|
await DataWriter.updateConversation(conversation.attributes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1541,12 +1541,10 @@ async function getAvatarsAndUpdateConversation(
|
||||||
const nextAvatars = getNextAvatarsData(avatars, nextAvatarId);
|
const nextAvatars = getNextAvatarsData(avatars, nextAvatarId);
|
||||||
// We don't save buffers to the db, but we definitely want it in-memory so
|
// We don't save buffers to the db, but we definitely want it in-memory so
|
||||||
// we don't have to re-generate them.
|
// we don't have to re-generate them.
|
||||||
//
|
|
||||||
// Mutating here because we don't want to trigger a model change
|
conversation.set({
|
||||||
// because we're updating redux here manually ourselves. Au revoir Backbone!
|
avatars: nextAvatars.map(avatarData => omit(avatarData, ['buffer'])),
|
||||||
conversation.attributes.avatars = nextAvatars.map(avatarData =>
|
});
|
||||||
omit(avatarData, ['buffer'])
|
|
||||||
);
|
|
||||||
await DataWriter.updateConversation(conversation.attributes);
|
await DataWriter.updateConversation(conversation.attributes);
|
||||||
|
|
||||||
return nextAvatars;
|
return nextAvatars;
|
||||||
|
@ -1922,15 +1920,12 @@ function discardEditMessage(
|
||||||
conversationId: string
|
conversationId: string
|
||||||
): ThunkAction<void, RootStateType, unknown, never> {
|
): ThunkAction<void, RootStateType, unknown, never> {
|
||||||
return () => {
|
return () => {
|
||||||
window.ConversationController.get(conversationId)?.set(
|
window.ConversationController.get(conversationId)?.set({
|
||||||
{
|
draftEditMessage: undefined,
|
||||||
draftEditMessage: undefined,
|
draftBodyRanges: undefined,
|
||||||
draftBodyRanges: undefined,
|
draft: undefined,
|
||||||
draft: undefined,
|
quotedMessageId: undefined,
|
||||||
quotedMessageId: undefined,
|
});
|
||||||
},
|
|
||||||
{ unset: true }
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2036,7 +2031,7 @@ function generateNewGroupLink(
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Not an actual redux action creator, so it doesn't produce an action (or dispatch
|
* Not an actual redux action creator, so it doesn't produce an action (or dispatch
|
||||||
* itself) because updates are managed through the backbone model, which will trigger
|
* itself) because updates are managed through the model, which will trigger
|
||||||
* necessary updates and refresh conversation_view.
|
* necessary updates and refresh conversation_view.
|
||||||
*
|
*
|
||||||
* In practice, it's similar to an already-connected thunk action. Later on we will
|
* In practice, it's similar to an already-connected thunk action. Later on we will
|
||||||
|
@ -2229,9 +2224,8 @@ function myProfileChanged(
|
||||||
avatarUpdateOptions
|
avatarUpdateOptions
|
||||||
);
|
);
|
||||||
|
|
||||||
// writeProfile above updates the backbone model which in turn updates
|
// writeProfile above updates the model which in turn updates
|
||||||
// redux through it's on:change event listener. Once we lose Backbone
|
// redux through it's on:change event listener.
|
||||||
// we'll need to manually sync these new changes.
|
|
||||||
|
|
||||||
// We just want to clear whatever error was there before:
|
// We just want to clear whatever error was there before:
|
||||||
dispatch({
|
dispatch({
|
||||||
|
@ -2267,7 +2261,7 @@ function removeCustomColorOnConversations(
|
||||||
): ThunkAction<void, RootStateType, unknown, CustomColorRemovedActionType> {
|
): ThunkAction<void, RootStateType, unknown, CustomColorRemovedActionType> {
|
||||||
return async dispatch => {
|
return async dispatch => {
|
||||||
const conversationsToUpdate: Array<ConversationAttributesType> = [];
|
const conversationsToUpdate: Array<ConversationAttributesType> = [];
|
||||||
window.getConversations().forEach(conversation => {
|
window.ConversationController.getAll().forEach(conversation => {
|
||||||
if (conversation.get('customColorId') === colorId) {
|
if (conversation.get('customColorId') === colorId) {
|
||||||
conversation.set({
|
conversation.set({
|
||||||
conversationColor: undefined,
|
conversationColor: undefined,
|
||||||
|
@ -2301,7 +2295,7 @@ function resetAllChatColors(): ThunkAction<
|
||||||
// Calling this with no args unsets all the colors in the db
|
// Calling this with no args unsets all the colors in the db
|
||||||
await DataWriter.updateAllConversationColors();
|
await DataWriter.updateAllConversationColors();
|
||||||
|
|
||||||
window.getConversations().forEach(conversation => {
|
window.ConversationController.getAll().forEach(conversation => {
|
||||||
conversation.set({
|
conversation.set({
|
||||||
conversationColor: undefined,
|
conversationColor: undefined,
|
||||||
customColor: undefined,
|
customColor: undefined,
|
||||||
|
|
|
@ -182,7 +182,7 @@ function stickerPackAdded(
|
||||||
): StickerPackAddedAction {
|
): StickerPackAddedAction {
|
||||||
const { status, attemptedStatus } = payload;
|
const { status, attemptedStatus } = payload;
|
||||||
|
|
||||||
// We do this to trigger a toast, which is still done via Backbone
|
// We do this to trigger a toast, which is still done via Whisper.events
|
||||||
if (
|
if (
|
||||||
status === 'error' &&
|
status === 'error' &&
|
||||||
attemptedStatus === 'installed' &&
|
attemptedStatus === 'installed' &&
|
||||||
|
@ -336,7 +336,7 @@ function stickerPackUpdated(
|
||||||
): StickerPackUpdatedAction {
|
): StickerPackUpdatedAction {
|
||||||
const { status, attemptedStatus } = patch;
|
const { status, attemptedStatus } = patch;
|
||||||
|
|
||||||
// We do this to trigger a toast, which is still done via Backbone
|
// We do this to trigger a toast, which is still done via Whisper.events
|
||||||
if (
|
if (
|
||||||
status === 'error' &&
|
status === 'error' &&
|
||||||
attemptedStatus === 'installed' &&
|
attemptedStatus === 'installed' &&
|
||||||
|
|
|
@ -113,7 +113,7 @@ export function getInitialState(
|
||||||
}
|
}
|
||||||
|
|
||||||
export function generateConversationsState(): ConversationsStateType {
|
export function generateConversationsState(): ConversationsStateType {
|
||||||
const convoCollection = window.getConversations();
|
const convoCollection = window.ConversationController.getAll();
|
||||||
const formattedConversations = convoCollection.map(conversation =>
|
const formattedConversations = convoCollection.map(conversation =>
|
||||||
conversation.format()
|
conversation.format()
|
||||||
);
|
);
|
||||||
|
|
|
@ -44,7 +44,7 @@ export function initializeRedux(data: ReduxInitData): void {
|
||||||
window.reduxStore = store;
|
window.reduxStore = store;
|
||||||
|
|
||||||
// Binding these actions to our redux store and exposing them allows us to update
|
// Binding these actions to our redux store and exposing them allows us to update
|
||||||
// redux when things change in the backbone world.
|
// redux when things change in the rest of the app.
|
||||||
window.reduxActions = {
|
window.reduxActions = {
|
||||||
accounts: bindActionCreators(actionCreators.accounts, store.dispatch),
|
accounts: bindActionCreators(actionCreators.accounts, store.dispatch),
|
||||||
app: bindActionCreators(actionCreators.app, store.dispatch),
|
app: bindActionCreators(actionCreators.app, store.dispatch),
|
||||||
|
|
|
@ -834,7 +834,7 @@ export const getComposeSelectedContacts = createSelector(
|
||||||
// What needs to happen to pull that selector logic here?
|
// What needs to happen to pull that selector logic here?
|
||||||
// 1) contactTypingTimers - that UI-only state needs to be moved to redux
|
// 1) contactTypingTimers - that UI-only state needs to be moved to redux
|
||||||
// 2) all of the message selectors need to be reselect-based; today those
|
// 2) all of the message selectors need to be reselect-based; today those
|
||||||
// Backbone-based prop-generation functions expect to get Conversation information
|
// model-based prop-generation functions expect to get Conversation information
|
||||||
// directly via ConversationController
|
// directly via ConversationController
|
||||||
export function _conversationSelector(
|
export function _conversationSelector(
|
||||||
conversation?: ConversationType
|
conversation?: ConversationType
|
||||||
|
|
|
@ -100,8 +100,7 @@ async function uploadProfile({
|
||||||
lastName: string;
|
lastName: string;
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
const us = window.ConversationController.getOurConversationOrThrow();
|
const us = window.ConversationController.getOurConversationOrThrow();
|
||||||
us.set('profileName', firstName);
|
us.set({ profileName: firstName, profileFamilyName: lastName });
|
||||||
us.set('profileFamilyName', lastName);
|
|
||||||
us.captureChange('standaloneProfile');
|
us.captureChange('standaloneProfile');
|
||||||
await DataWriter.updateConversation(us.attributes);
|
await DataWriter.updateConversation(us.attributes);
|
||||||
|
|
||||||
|
|
|
@ -577,7 +577,7 @@ export function SmartPreferences(): JSX.Element | null {
|
||||||
createItemsAccess('call-ringtone-notification', true);
|
createItemsAccess('call-ringtone-notification', true);
|
||||||
const [hasCountMutedConversations, onCountMutedConversationsChange] =
|
const [hasCountMutedConversations, onCountMutedConversationsChange] =
|
||||||
createItemsAccess('badge-count-muted-conversations', false, () => {
|
createItemsAccess('badge-count-muted-conversations', false, () => {
|
||||||
window.Whisper.events.trigger('updateUnreadCount');
|
window.Whisper.events.emit('updateUnreadCount');
|
||||||
});
|
});
|
||||||
const [hasHideMenuBar, onHideMenuBarChange] = createItemsAccess(
|
const [hasHideMenuBar, onHideMenuBarChange] = createItemsAccess(
|
||||||
'hide-menu-bar',
|
'hide-menu-bar',
|
||||||
|
|
|
@ -1,138 +0,0 @@
|
||||||
// Copyright 2017 Signal Messenger, LLC
|
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
|
|
||||||
import { assert } from 'chai';
|
|
||||||
import { Model } from 'backbone';
|
|
||||||
|
|
||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
||||||
|
|
||||||
describe('reliable trigger', () => {
|
|
||||||
describe('trigger', () => {
|
|
||||||
let model: Model;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
model = new Model();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns successfully if this._events is falsey', () => {
|
|
||||||
(model as any)._events = null;
|
|
||||||
model.trigger('click');
|
|
||||||
});
|
|
||||||
it('handles space-separated list of events to trigger', () => {
|
|
||||||
let a = false;
|
|
||||||
let b = false;
|
|
||||||
|
|
||||||
model.on('a', () => {
|
|
||||||
a = true;
|
|
||||||
});
|
|
||||||
model.on('b', () => {
|
|
||||||
b = true;
|
|
||||||
});
|
|
||||||
|
|
||||||
model.trigger('a b');
|
|
||||||
|
|
||||||
assert.strictEqual(a, true);
|
|
||||||
assert.strictEqual(b, true);
|
|
||||||
});
|
|
||||||
it('calls all clients registered for "all" event', () => {
|
|
||||||
let count = 0;
|
|
||||||
model.on('all', () => {
|
|
||||||
count += 1;
|
|
||||||
});
|
|
||||||
|
|
||||||
model.trigger('left');
|
|
||||||
model.trigger('right');
|
|
||||||
|
|
||||||
assert.strictEqual(count, 2);
|
|
||||||
});
|
|
||||||
it('calls all clients registered for target event', () => {
|
|
||||||
let a = false;
|
|
||||||
let b = false;
|
|
||||||
|
|
||||||
model.on('event', () => {
|
|
||||||
a = true;
|
|
||||||
});
|
|
||||||
model.on('event', () => {
|
|
||||||
b = true;
|
|
||||||
});
|
|
||||||
|
|
||||||
model.trigger('event');
|
|
||||||
|
|
||||||
assert.strictEqual(a, true);
|
|
||||||
assert.strictEqual(b, true);
|
|
||||||
});
|
|
||||||
it('successfully returns and calls all clients even if first failed', () => {
|
|
||||||
let a = false;
|
|
||||||
let b = false;
|
|
||||||
|
|
||||||
model.on('event', () => {
|
|
||||||
a = true;
|
|
||||||
throw new Error('a is set, but exception is thrown');
|
|
||||||
});
|
|
||||||
model.on('event', () => {
|
|
||||||
b = true;
|
|
||||||
});
|
|
||||||
|
|
||||||
model.trigger('event');
|
|
||||||
|
|
||||||
assert.strictEqual(a, true);
|
|
||||||
assert.strictEqual(b, true);
|
|
||||||
});
|
|
||||||
it('calls clients with no args', () => {
|
|
||||||
let called = false;
|
|
||||||
model.on('event', () => {
|
|
||||||
called = true;
|
|
||||||
});
|
|
||||||
|
|
||||||
model.trigger('event');
|
|
||||||
|
|
||||||
assert.strictEqual(called, true);
|
|
||||||
});
|
|
||||||
it('calls clients with 1 arg', () => {
|
|
||||||
let args: Array<unknown> = [];
|
|
||||||
model.on('event', (...eventArgs) => {
|
|
||||||
args = eventArgs;
|
|
||||||
});
|
|
||||||
|
|
||||||
model.trigger('event', 1);
|
|
||||||
|
|
||||||
assert.strictEqual(args[0], 1);
|
|
||||||
});
|
|
||||||
it('calls clients with 2 args', () => {
|
|
||||||
let args: Array<unknown> = [];
|
|
||||||
model.on('event', (...eventArgs) => {
|
|
||||||
args = eventArgs;
|
|
||||||
});
|
|
||||||
|
|
||||||
model.trigger('event', 1, 2);
|
|
||||||
|
|
||||||
assert.strictEqual(args[0], 1);
|
|
||||||
assert.strictEqual(args[1], 2);
|
|
||||||
});
|
|
||||||
it('calls clients with 3 args', () => {
|
|
||||||
let args: Array<unknown> = [];
|
|
||||||
model.on('event', (...eventArgs) => {
|
|
||||||
args = eventArgs;
|
|
||||||
});
|
|
||||||
|
|
||||||
model.trigger('event', 1, 2, 3);
|
|
||||||
|
|
||||||
assert.strictEqual(args[0], 1);
|
|
||||||
assert.strictEqual(args[1], 2);
|
|
||||||
assert.strictEqual(args[2], 3);
|
|
||||||
});
|
|
||||||
it('calls clients with 4+ args', () => {
|
|
||||||
let args: Array<unknown> = [];
|
|
||||||
model.on('event', (...eventArgs) => {
|
|
||||||
args = eventArgs;
|
|
||||||
});
|
|
||||||
|
|
||||||
model.trigger('event', 1, 2, 3, 4);
|
|
||||||
|
|
||||||
assert.strictEqual(args[0], 1);
|
|
||||||
assert.strictEqual(args[1], 2);
|
|
||||||
assert.strictEqual(args[2], 3);
|
|
||||||
assert.strictEqual(args[3], 4);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -10,6 +10,7 @@ import { IMAGE_PNG } from '../../types/MIME';
|
||||||
import { generateAci, generatePni } from '../../types/ServiceId';
|
import { generateAci, generatePni } from '../../types/ServiceId';
|
||||||
import { MessageModel } from '../../models/messages';
|
import { MessageModel } from '../../models/messages';
|
||||||
import { DurationInSeconds } from '../../util/durations';
|
import { DurationInSeconds } from '../../util/durations';
|
||||||
|
import { ConversationModel } from '../../models/conversations';
|
||||||
|
|
||||||
describe('Conversations', () => {
|
describe('Conversations', () => {
|
||||||
async function resetConversationController(): Promise<void> {
|
async function resetConversationController(): Promise<void> {
|
||||||
|
@ -32,7 +33,7 @@ describe('Conversations', () => {
|
||||||
|
|
||||||
it('updates lastMessage even in race conditions with db', async () => {
|
it('updates lastMessage even in race conditions with db', async () => {
|
||||||
// Creating a fake conversation
|
// Creating a fake conversation
|
||||||
const conversation = new window.Whisper.Conversation({
|
const conversation = new ConversationModel({
|
||||||
avatars: [],
|
avatars: [],
|
||||||
id: generateUuid(),
|
id: generateUuid(),
|
||||||
e164: '+15551234567',
|
e164: '+15551234567',
|
||||||
|
@ -111,7 +112,7 @@ describe('Conversations', () => {
|
||||||
|
|
||||||
it('only produces attachments on a quote with an image', async () => {
|
it('only produces attachments on a quote with an image', async () => {
|
||||||
// Creating a fake conversation
|
// Creating a fake conversation
|
||||||
const conversation = new window.Whisper.Conversation({
|
const conversation = new ConversationModel({
|
||||||
avatars: [],
|
avatars: [],
|
||||||
id: generateUuid(),
|
id: generateUuid(),
|
||||||
e164: '+15551234567',
|
e164: '+15551234567',
|
||||||
|
|
|
@ -86,8 +86,8 @@ describe('MessageCache', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('register: syncing with backbone', () => {
|
describe('register: syncing with models', () => {
|
||||||
it('backbone to redux', () => {
|
it('model to redux', () => {
|
||||||
const message1 = new MessageModel({
|
const message1 = new MessageModel({
|
||||||
conversationId: 'xyz',
|
conversationId: 'xyz',
|
||||||
id: uuid(),
|
id: uuid(),
|
||||||
|
@ -126,7 +126,7 @@ describe('MessageCache', () => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('redux to backbone (working with models)', () => {
|
it('redux to model (working with models)', () => {
|
||||||
const message = new MessageModel({
|
const message = new MessageModel({
|
||||||
conversationId: 'xyz',
|
conversationId: 'xyz',
|
||||||
id: uuid(),
|
id: uuid(),
|
||||||
|
|
|
@ -130,7 +130,7 @@ describe('both/state/ducks/conversations', () => {
|
||||||
|
|
||||||
sinonSandbox = sinon.createSandbox();
|
sinonSandbox = sinon.createSandbox();
|
||||||
|
|
||||||
sinonSandbox.stub(window.Whisper.events, 'trigger');
|
sinonSandbox.stub(window.Whisper.events, 'emit');
|
||||||
|
|
||||||
createGroupStub = sinon.stub();
|
createGroupStub = sinon.stub();
|
||||||
});
|
});
|
||||||
|
|
|
@ -68,7 +68,7 @@ describe('updateConversationsWithUuidLookup', () => {
|
||||||
return { conversation: convoUuid, mergePromises: [] };
|
return { conversation: convoUuid, mergePromises: [] };
|
||||||
}
|
}
|
||||||
|
|
||||||
convoE164.unset('e164');
|
convoE164.set({ e164: undefined });
|
||||||
convoUuid.updateE164(e164);
|
convoUuid.updateE164(e164);
|
||||||
return { conversation: convoUuid, mergePromises: [] };
|
return { conversation: convoUuid, mergePromises: [] };
|
||||||
}
|
}
|
||||||
|
|
|
@ -140,6 +140,9 @@ Bootstrap.benchmark(async (bootstrap: Bootstrap): Promise<void> => {
|
||||||
const CallsTabDetailsTitle = CallsTabDetails.locator(
|
const CallsTabDetailsTitle = CallsTabDetails.locator(
|
||||||
'.ConversationDetailsHeader__title'
|
'.ConversationDetailsHeader__title'
|
||||||
);
|
);
|
||||||
|
const AnyCallListAvatar = CallsTabSidebar.locator(
|
||||||
|
'.CallsList__ItemAvatar'
|
||||||
|
).first();
|
||||||
|
|
||||||
debug('waiting for unread badge to hit correct value', unreadCount);
|
debug('waiting for unread badge to hit correct value', unreadCount);
|
||||||
await CallsNavTabUnread.getByText(`${unreadCount} unread`).waitFor();
|
await CallsNavTabUnread.getByText(`${unreadCount} unread`).waitFor();
|
||||||
|
@ -147,6 +150,9 @@ Bootstrap.benchmark(async (bootstrap: Bootstrap): Promise<void> => {
|
||||||
debug('opening calls tab');
|
debug('opening calls tab');
|
||||||
await CallsNavTab.click();
|
await CallsNavTab.click();
|
||||||
|
|
||||||
|
await CreateCallLink.waitFor();
|
||||||
|
await AnyCallListAvatar.waitFor();
|
||||||
|
|
||||||
async function measure(runId: number): Promise<number> {
|
async function measure(runId: number): Promise<number> {
|
||||||
// setup
|
// setup
|
||||||
const searchContact = contacts[runId % contacts.length];
|
const searchContact = contacts[runId % contacts.length];
|
||||||
|
@ -182,6 +188,7 @@ Bootstrap.benchmark(async (bootstrap: Bootstrap): Promise<void> => {
|
||||||
await NewCallDetailsTitle.waitFor();
|
await NewCallDetailsTitle.waitFor();
|
||||||
await SearchBar.clear();
|
await SearchBar.clear();
|
||||||
await CreateCallLink.waitFor();
|
await CreateCallLink.waitFor();
|
||||||
|
await AnyCallListAvatar.waitFor();
|
||||||
|
|
||||||
// measure
|
// measure
|
||||||
const end = Date.now();
|
const end = Date.now();
|
||||||
|
|
|
@ -365,7 +365,7 @@ export class SocketManager extends EventListener {
|
||||||
error instanceof LibSignalErrorBase &&
|
error instanceof LibSignalErrorBase &&
|
||||||
error.code === ErrorCode.AppExpired
|
error.code === ErrorCode.AppExpired
|
||||||
) {
|
) {
|
||||||
window.Whisper.events.trigger('httpResponse499');
|
window.Whisper.events.emit('httpResponse499');
|
||||||
return;
|
return;
|
||||||
} else if (
|
} else if (
|
||||||
error instanceof LibSignalErrorBase &&
|
error instanceof LibSignalErrorBase &&
|
||||||
|
|
|
@ -64,7 +64,7 @@ export class UpdateKeysListener {
|
||||||
(error.code === 422 || error.code === 403)
|
(error.code === 422 || error.code === 403)
|
||||||
) {
|
) {
|
||||||
log.error(`run: Got a ${error.code} uploading PNI keys; unlinking`);
|
log.error(`run: Got a ${error.code} uploading PNI keys; unlinking`);
|
||||||
window.Whisper.events.trigger('unlinkAndDisconnect');
|
window.Whisper.events.emit('unlinkAndDisconnect');
|
||||||
} else {
|
} else {
|
||||||
const errorString =
|
const errorString =
|
||||||
error instanceof HTTPError
|
error instanceof HTTPError
|
||||||
|
|
|
@ -5,7 +5,7 @@ import type { HTTPError } from './Errors';
|
||||||
|
|
||||||
export async function handleStatusCode(status: number): Promise<void> {
|
export async function handleStatusCode(status: number): Promise<void> {
|
||||||
if (status === 499) {
|
if (status === 499) {
|
||||||
window.Whisper.events.trigger('httpResponse499');
|
window.Whisper.events.emit('httpResponse499');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -470,7 +470,7 @@ async function _promiseAjax<Type extends ResponseType, OutputShape>(
|
||||||
|
|
||||||
if (!unauthenticated && response.status === 401) {
|
if (!unauthenticated && response.status === 401) {
|
||||||
log.warn('Got 401 from Signal Server. We might be unlinked.');
|
log.warn('Got 401 from Signal Server. We might be unlinked.');
|
||||||
window.Whisper.events.trigger('mightBeUnlinked');
|
window.Whisper.events.emit('mightBeUnlinked');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2048,23 +2048,23 @@ export function initialize({
|
||||||
});
|
});
|
||||||
|
|
||||||
socketManager.on('statusChange', () => {
|
socketManager.on('statusChange', () => {
|
||||||
window.Whisper.events.trigger('socketStatusChange');
|
window.Whisper.events.emit('socketStatusChange');
|
||||||
});
|
});
|
||||||
|
|
||||||
socketManager.on('online', () => {
|
socketManager.on('online', () => {
|
||||||
window.Whisper.events.trigger('online');
|
window.Whisper.events.emit('online');
|
||||||
});
|
});
|
||||||
|
|
||||||
socketManager.on('offline', () => {
|
socketManager.on('offline', () => {
|
||||||
window.Whisper.events.trigger('offline');
|
window.Whisper.events.emit('offline');
|
||||||
});
|
});
|
||||||
|
|
||||||
socketManager.on('authError', () => {
|
socketManager.on('authError', () => {
|
||||||
window.Whisper.events.trigger('unlinkAndDisconnect');
|
window.Whisper.events.emit('unlinkAndDisconnect');
|
||||||
});
|
});
|
||||||
|
|
||||||
socketManager.on('firstEnvelope', incoming => {
|
socketManager.on('firstEnvelope', incoming => {
|
||||||
window.Whisper.events.trigger('firstEnvelope', incoming);
|
window.Whisper.events.emit('firstEnvelope', incoming);
|
||||||
});
|
});
|
||||||
|
|
||||||
socketManager.on('serverAlerts', alerts => {
|
socketManager.on('serverAlerts', alerts => {
|
||||||
|
|
|
@ -58,7 +58,7 @@ export class User {
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Notify redux about phone number change
|
// Notify redux about phone number change
|
||||||
window.Whisper.events.trigger('userChanged', true);
|
window.Whisper.events.emit('userChanged', true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public getNumber(): string | undefined {
|
public getNumber(): string | undefined {
|
||||||
|
|
|
@ -1262,10 +1262,12 @@ async function saveCallHistory({
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
conversation.set(
|
conversation.set({
|
||||||
'active_at',
|
active_at: Math.max(
|
||||||
Math.max(conversation.get('active_at') ?? 0, callHistory.timestamp)
|
conversation.get('active_at') ?? 0,
|
||||||
);
|
callHistory.timestamp
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
if (canConversationBeUnarchived(conversation.attributes)) {
|
if (canConversationBeUnarchived(conversation.attributes)) {
|
||||||
conversation.setArchived(false);
|
conversation.setArchived(false);
|
||||||
|
|
|
@ -15,20 +15,20 @@ export async function checkOurPniIdentityKey(): Promise<void> {
|
||||||
const { pni: remotePni } = await server.whoami();
|
const { pni: remotePni } = await server.whoami();
|
||||||
if (remotePni !== ourPni) {
|
if (remotePni !== ourPni) {
|
||||||
log.warn(`remote pni mismatch, ${remotePni} != ${ourPni}`);
|
log.warn(`remote pni mismatch, ${remotePni} != ${ourPni}`);
|
||||||
window.Whisper.events.trigger('unlinkAndDisconnect');
|
window.Whisper.events.emit('unlinkAndDisconnect');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const localKeyPair = await window.storage.protocol.getIdentityKeyPair(ourPni);
|
const localKeyPair = await window.storage.protocol.getIdentityKeyPair(ourPni);
|
||||||
if (!localKeyPair) {
|
if (!localKeyPair) {
|
||||||
log.warn(`no local key pair for ${ourPni}, unlinking`);
|
log.warn(`no local key pair for ${ourPni}, unlinking`);
|
||||||
window.Whisper.events.trigger('unlinkAndDisconnect');
|
window.Whisper.events.emit('unlinkAndDisconnect');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { identityKey: remoteKey } = await server.getKeysForServiceId(ourPni);
|
const { identityKey: remoteKey } = await server.getKeysForServiceId(ourPni);
|
||||||
if (!constantTimeEqual(localKeyPair.publicKey.serialize(), remoteKey)) {
|
if (!constantTimeEqual(localKeyPair.publicKey.serialize(), remoteKey)) {
|
||||||
log.warn(`local/remote key mismatch for ${ourPni}, unlinking`);
|
log.warn(`local/remote key mismatch for ${ourPni}, unlinking`);
|
||||||
window.Whisper.events.trigger('unlinkAndDisconnect');
|
window.Whisper.events.emit('unlinkAndDisconnect');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -124,7 +124,7 @@ export async function cleanupMessages(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Removes a message from redux caches & backbone, but does NOT delete files on disk,
|
/** Removes a message from redux caches & MessageCache, but does NOT delete files on disk,
|
||||||
* story replies, edit histories, attachments, etc. Should ONLY be called in conjunction
|
* story replies, edit histories, attachments, etc. Should ONLY be called in conjunction
|
||||||
* with deleteMessageData. */
|
* with deleteMessageData. */
|
||||||
export function cleanupMessageFromMemory(message: MessageAttributesType): void {
|
export function cleanupMessageFromMemory(message: MessageAttributesType): void {
|
||||||
|
|
|
@ -26,7 +26,7 @@ export function isSignalConnection(
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getSignalConnections(): Array<ConversationModel> {
|
export function getSignalConnections(): Array<ConversationModel> {
|
||||||
return window
|
return window.ConversationController.getAll().filter(conversation =>
|
||||||
.getConversations()
|
isSignalConnection(conversation.attributes)
|
||||||
.filter(conversation => isSignalConnection(conversation.attributes));
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -116,7 +116,7 @@ function processError(error: unknown): void {
|
||||||
log.warn(
|
log.warn(
|
||||||
`Got 401/403 for ${conversation.idForLogging()}, setting sealedSender = DISABLED`
|
`Got 401/403 for ${conversation.idForLogging()}, setting sealedSender = DISABLED`
|
||||||
);
|
);
|
||||||
conversation.set('sealedSender', SEALED_SENDER.DISABLED);
|
conversation.set({ sealedSender: SEALED_SENDER.DISABLED });
|
||||||
drop(updateConversation(conversation.attributes));
|
drop(updateConversation(conversation.attributes));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -84,7 +84,7 @@ async function fetchAndUpdateDeviceName() {
|
||||||
}
|
}
|
||||||
|
|
||||||
await window.storage.user.setDeviceName(newName);
|
await window.storage.user.setDeviceName(newName);
|
||||||
window.Whisper.events.trigger('deviceNameChanged');
|
window.Whisper.events.emit('deviceNameChanged');
|
||||||
log.info(
|
log.info(
|
||||||
'fetchAndUpdateDeviceName: successfully updated new device name locally'
|
'fetchAndUpdateDeviceName: successfully updated new device name locally'
|
||||||
);
|
);
|
||||||
|
|
|
@ -214,7 +214,7 @@ export async function onStoryRecipientUpdate(
|
||||||
});
|
});
|
||||||
|
|
||||||
if (handledMessages.length) {
|
if (handledMessages.length) {
|
||||||
window.Whisper.events.trigger('incrementProgress');
|
window.Whisper.events.emit('incrementProgress');
|
||||||
confirm();
|
confirm();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -241,7 +241,7 @@ export async function sendStoryMessage(
|
||||||
group => group.getStorySendMode() !== StorySendMode.Always
|
group => group.getStorySendMode() !== StorySendMode.Always
|
||||||
);
|
);
|
||||||
for (const group of groupsToUpdate) {
|
for (const group of groupsToUpdate) {
|
||||||
group.set('storySendMode', StorySendMode.Always);
|
group.set({ storySendMode: StorySendMode.Always });
|
||||||
}
|
}
|
||||||
void DataWriter.updateConversations(
|
void DataWriter.updateConversations(
|
||||||
groupsToUpdate.map(group => group.attributes)
|
groupsToUpdate.map(group => group.attributes)
|
||||||
|
|
|
@ -2,7 +2,11 @@
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import type { ValidateConversationType } from '../model-types.d';
|
import type { ValidateConversationType } from '../model-types.d';
|
||||||
import { isDirectConversation } from './whatTypeOfConversation';
|
import {
|
||||||
|
isDirectConversation,
|
||||||
|
isGroupV1,
|
||||||
|
isGroupV2,
|
||||||
|
} from './whatTypeOfConversation';
|
||||||
import { isServiceIdString } from '../types/ServiceId';
|
import { isServiceIdString } from '../types/ServiceId';
|
||||||
|
|
||||||
export function validateConversation(
|
export function validateConversation(
|
||||||
|
@ -22,6 +26,14 @@ export function validateConversation(
|
||||||
return error;
|
return error;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!isDirectConversation(attributes) &&
|
||||||
|
!isGroupV1(attributes) &&
|
||||||
|
!isGroupV2(attributes)
|
||||||
|
) {
|
||||||
|
return 'Conversation is not direct, groupv1 or groupv2';
|
||||||
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
14
ts/window.d.ts
vendored
14
ts/window.d.ts
vendored
|
@ -3,15 +3,14 @@
|
||||||
|
|
||||||
// Captures the globals put in place by preload.js, background.js and others
|
// Captures the globals put in place by preload.js, background.js and others
|
||||||
|
|
||||||
|
import type EventEmitter from 'node:events';
|
||||||
import type { Store } from 'redux';
|
import type { Store } from 'redux';
|
||||||
import type * as Backbone from 'backbone';
|
|
||||||
import type { SystemPreferences } from 'electron';
|
import type { SystemPreferences } from 'electron';
|
||||||
import type PQueue from 'p-queue/dist';
|
import type PQueue from 'p-queue/dist';
|
||||||
import type { assert } from 'chai';
|
import type { assert } from 'chai';
|
||||||
import type { PhoneNumber, PhoneNumberFormat } from 'google-libphonenumber';
|
import type { PhoneNumber, PhoneNumberFormat } from 'google-libphonenumber';
|
||||||
import type { MochaOptions } from 'mocha';
|
import type { MochaOptions } from 'mocha';
|
||||||
|
|
||||||
import type { ConversationModelCollectionType } from './model-types.d';
|
|
||||||
import type { textsecure } from './textsecure';
|
import type { textsecure } from './textsecure';
|
||||||
import type { Storage } from './textsecure/Storage';
|
import type { Storage } from './textsecure/Storage';
|
||||||
import type {
|
import type {
|
||||||
|
@ -34,7 +33,6 @@ import type { Receipt } from './types/Receipt';
|
||||||
import type { ConversationController } from './ConversationController';
|
import type { ConversationController } from './ConversationController';
|
||||||
import type { ReduxActions } from './state/types';
|
import type { ReduxActions } from './state/types';
|
||||||
import type { createApp } from './state/roots/createApp';
|
import type { createApp } from './state/roots/createApp';
|
||||||
import type { ConversationModel } from './models/conversations';
|
|
||||||
import type { BatcherType } from './util/batcher';
|
import type { BatcherType } from './util/batcher';
|
||||||
import type { ConfirmationDialog } from './components/ConfirmationDialog';
|
import type { ConfirmationDialog } from './components/ConfirmationDialog';
|
||||||
import type { SignalProtocolStore } from './SignalProtocolStore';
|
import type { SignalProtocolStore } from './SignalProtocolStore';
|
||||||
|
@ -183,7 +181,6 @@ export type SignalCoreType = {
|
||||||
createApp: typeof createApp;
|
createApp: typeof createApp;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
conversationControllerStart: () => void;
|
|
||||||
challengeHandler?: ChallengeHandler;
|
challengeHandler?: ChallengeHandler;
|
||||||
|
|
||||||
// Only for debugging in Dev Tools
|
// Only for debugging in Dev Tools
|
||||||
|
@ -206,7 +203,6 @@ declare global {
|
||||||
enterMouseMode: () => void;
|
enterMouseMode: () => void;
|
||||||
getAccountManager: () => AccountManager;
|
getAccountManager: () => AccountManager;
|
||||||
getAppInstance: () => string | undefined;
|
getAppInstance: () => string | undefined;
|
||||||
getConversations: () => ConversationModelCollectionType;
|
|
||||||
getBuildCreation: () => number;
|
getBuildCreation: () => number;
|
||||||
getBuildExpiration: () => number;
|
getBuildExpiration: () => number;
|
||||||
getHostName: () => string;
|
getHostName: () => string;
|
||||||
|
@ -247,9 +243,6 @@ declare global {
|
||||||
// The types below have been somewhat organized. See DESKTOP-4801
|
// The types below have been somewhat organized. See DESKTOP-4801
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
|
|
||||||
// Backbone
|
|
||||||
Backbone: typeof Backbone;
|
|
||||||
|
|
||||||
ConversationController: ConversationController;
|
ConversationController: ConversationController;
|
||||||
Events: IPCEventsType;
|
Events: IPCEventsType;
|
||||||
FontFace: typeof FontFace;
|
FontFace: typeof FontFace;
|
||||||
|
@ -331,10 +324,7 @@ declare global {
|
||||||
}
|
}
|
||||||
|
|
||||||
export type WhisperType = {
|
export type WhisperType = {
|
||||||
Conversation: typeof ConversationModel;
|
|
||||||
ConversationCollection: typeof ConversationModelCollectionType;
|
|
||||||
|
|
||||||
deliveryReceiptQueue: PQueue;
|
deliveryReceiptQueue: PQueue;
|
||||||
deliveryReceiptBatcher: BatcherType<Receipt>;
|
deliveryReceiptBatcher: BatcherType<Receipt>;
|
||||||
events: Backbone.Events;
|
events: EventEmitter;
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
// Copyright 2022 Signal Messenger, LLC
|
// Copyright 2022 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import EventEmitter from 'node:events';
|
||||||
import { ipcRenderer as ipc } from 'electron';
|
import { ipcRenderer as ipc } from 'electron';
|
||||||
import * as semver from 'semver';
|
import * as semver from 'semver';
|
||||||
import { mapValues } from 'lodash';
|
import { groupBy, mapValues } from 'lodash';
|
||||||
|
import PQueue from 'p-queue';
|
||||||
|
|
||||||
import type { IPCType } from '../../window.d';
|
import type { IPCType } from '../../window.d';
|
||||||
import { parseIntWithFallback } from '../../util/parseIntWithFallback';
|
import { parseIntWithFallback } from '../../util/parseIntWithFallback';
|
||||||
|
@ -24,6 +26,15 @@ import { AggregatedStats } from '../../textsecure/WebsocketResources';
|
||||||
import { UNAUTHENTICATED_CHANNEL_NAME } from '../../textsecure/SocketManager';
|
import { UNAUTHENTICATED_CHANNEL_NAME } from '../../textsecure/SocketManager';
|
||||||
import { isProduction } from '../../util/version';
|
import { isProduction } from '../../util/version';
|
||||||
import { ToastType } from '../../types/Toast';
|
import { ToastType } from '../../types/Toast';
|
||||||
|
import { ConversationController } from '../../ConversationController';
|
||||||
|
import { createBatcher } from '../../util/batcher';
|
||||||
|
import { ReceiptType } from '../../types/Receipt';
|
||||||
|
import type { Receipt } from '../../types/Receipt';
|
||||||
|
import { MINUTE } from '../../util/durations';
|
||||||
|
import {
|
||||||
|
conversationJobQueue,
|
||||||
|
conversationQueueJobEnum,
|
||||||
|
} from '../../jobs/conversationJobQueue';
|
||||||
|
|
||||||
const log = createLogger('phase1-ipc');
|
const log = createLogger('phase1-ipc');
|
||||||
|
|
||||||
|
@ -47,6 +58,32 @@ window.Flags = Flags;
|
||||||
|
|
||||||
window.RETRY_DELAY = false;
|
window.RETRY_DELAY = false;
|
||||||
|
|
||||||
|
window.Whisper = {
|
||||||
|
events: new EventEmitter(),
|
||||||
|
deliveryReceiptQueue: new PQueue({
|
||||||
|
concurrency: 1,
|
||||||
|
timeout: MINUTE * 30,
|
||||||
|
}),
|
||||||
|
deliveryReceiptBatcher: createBatcher<Receipt>({
|
||||||
|
name: 'Whisper.deliveryReceiptBatcher',
|
||||||
|
wait: 500,
|
||||||
|
maxSize: 100,
|
||||||
|
processBatch: async deliveryReceipts => {
|
||||||
|
const groups = groupBy(deliveryReceipts, 'conversationId');
|
||||||
|
await Promise.all(
|
||||||
|
Object.keys(groups).map(async conversationId => {
|
||||||
|
await conversationJobQueue.add({
|
||||||
|
type: conversationQueueJobEnum.enum.Receipts,
|
||||||
|
conversationId,
|
||||||
|
receiptsType: ReceiptType.Delivery,
|
||||||
|
receipts: groups[conversationId],
|
||||||
|
});
|
||||||
|
})
|
||||||
|
);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
window.ConversationController = new ConversationController();
|
||||||
window.platform = process.platform;
|
window.platform = process.platform;
|
||||||
window.getTitle = () => title;
|
window.getTitle = () => title;
|
||||||
window.getAppInstance = () => config.appInstance;
|
window.getAppInstance = () => config.appInstance;
|
||||||
|
@ -272,35 +309,35 @@ ipc.on('additional-log-data-request', async event => {
|
||||||
});
|
});
|
||||||
|
|
||||||
ipc.on('open-settings-tab', () => {
|
ipc.on('open-settings-tab', () => {
|
||||||
window.Whisper.events.trigger('openSettingsTab');
|
window.Whisper.events.emit('openSettingsTab');
|
||||||
});
|
});
|
||||||
|
|
||||||
ipc.on('set-up-as-new-device', () => {
|
ipc.on('set-up-as-new-device', () => {
|
||||||
window.Whisper.events.trigger('setupAsNewDevice');
|
window.Whisper.events.emit('setupAsNewDevice');
|
||||||
});
|
});
|
||||||
|
|
||||||
ipc.on('set-up-as-standalone', () => {
|
ipc.on('set-up-as-standalone', () => {
|
||||||
window.Whisper.events.trigger('setupAsStandalone');
|
window.Whisper.events.emit('setupAsStandalone');
|
||||||
});
|
});
|
||||||
|
|
||||||
ipc.on('stage-local-backup-for-import', () => {
|
ipc.on('stage-local-backup-for-import', () => {
|
||||||
window.Whisper.events.trigger('stageLocalBackupForImport');
|
window.Whisper.events.emit('stageLocalBackupForImport');
|
||||||
});
|
});
|
||||||
|
|
||||||
ipc.on('challenge:response', (_event, response) => {
|
ipc.on('challenge:response', (_event, response) => {
|
||||||
window.Whisper.events.trigger('challengeResponse', response);
|
window.Whisper.events.emit('challengeResponse', response);
|
||||||
});
|
});
|
||||||
|
|
||||||
ipc.on('power-channel:suspend', () => {
|
ipc.on('power-channel:suspend', () => {
|
||||||
window.Whisper.events.trigger('powerMonitorSuspend');
|
window.Whisper.events.emit('powerMonitorSuspend');
|
||||||
});
|
});
|
||||||
|
|
||||||
ipc.on('power-channel:resume', () => {
|
ipc.on('power-channel:resume', () => {
|
||||||
window.Whisper.events.trigger('powerMonitorResume');
|
window.Whisper.events.emit('powerMonitorResume');
|
||||||
});
|
});
|
||||||
|
|
||||||
ipc.on('power-channel:lock-screen', () => {
|
ipc.on('power-channel:lock-screen', () => {
|
||||||
window.Whisper.events.trigger('powerMonitorLockScreen');
|
window.Whisper.events.emit('powerMonitorLockScreen');
|
||||||
});
|
});
|
||||||
|
|
||||||
ipc.on(
|
ipc.on(
|
||||||
|
@ -328,7 +365,7 @@ ipc.on('window:set-menu-options', (_event, options) => {
|
||||||
if (!window.Whisper.events) {
|
if (!window.Whisper.events) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
window.Whisper.events.trigger('setMenuOptions', options);
|
window.Whisper.events.emit('setMenuOptions', options);
|
||||||
});
|
});
|
||||||
|
|
||||||
window.sendChallengeRequest = request => ipc.send('challenge:request', request);
|
window.sendChallengeRequest = request => ipc.send('challenge:request', request);
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
// Copyright 2022 Signal Messenger, LLC
|
// Copyright 2022 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import Backbone from 'backbone';
|
|
||||||
import { PhoneNumberUtil, PhoneNumberFormat } from 'google-libphonenumber';
|
import { PhoneNumberUtil, PhoneNumberFormat } from 'google-libphonenumber';
|
||||||
import * as moment from 'moment';
|
import * as moment from 'moment';
|
||||||
// @ts-expect-error -- no types
|
// @ts-expect-error -- no types
|
||||||
|
@ -21,7 +20,6 @@ const log = createLogger('phase2-dependencies');
|
||||||
initializeLogging();
|
initializeLogging();
|
||||||
|
|
||||||
window.nodeSetImmediate = setImmediate;
|
window.nodeSetImmediate = setImmediate;
|
||||||
window.Backbone = Backbone;
|
|
||||||
window.textsecure = textsecure;
|
window.textsecure = textsecure;
|
||||||
|
|
||||||
const { config } = window.SignalContext;
|
const { config } = window.SignalContext;
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
// Copyright 2017 Signal Messenger, LLC
|
// Copyright 2017 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import { clone, has } from 'lodash';
|
import { has } from 'lodash';
|
||||||
import { contextBridge } from 'electron';
|
import { contextBridge } from 'electron';
|
||||||
|
|
||||||
import { createLogger } from '../../logging/log';
|
import { createLogger } from '../../logging/log';
|
||||||
|
@ -17,7 +17,6 @@ import '../preload';
|
||||||
import './phase2-dependencies';
|
import './phase2-dependencies';
|
||||||
import './phase3-post-signal';
|
import './phase3-post-signal';
|
||||||
import './phase4-test';
|
import './phase4-test';
|
||||||
import '../../backbone/reliable_trigger';
|
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
CdsLookupOptionsType,
|
CdsLookupOptionsType,
|
||||||
|
@ -25,7 +24,6 @@ import type {
|
||||||
} from '../../textsecure/WebAPI';
|
} from '../../textsecure/WebAPI';
|
||||||
import type { FeatureFlagType } from '../../window.d';
|
import type { FeatureFlagType } from '../../window.d';
|
||||||
import type { StorageAccessType } from '../../types/Storage.d';
|
import type { StorageAccessType } from '../../types/Storage.d';
|
||||||
import { start as startConversationController } from '../../ConversationController';
|
|
||||||
import { initMessageCleanup } from '../../services/messageStateCleanup';
|
import { initMessageCleanup } from '../../services/messageStateCleanup';
|
||||||
import { Environment, getEnvironment } from '../../environment';
|
import { Environment, getEnvironment } from '../../environment';
|
||||||
import { isProduction } from '../../util/version';
|
import { isProduction } from '../../util/version';
|
||||||
|
@ -52,9 +50,7 @@ if (window.SignalContext.config.proxyUrl) {
|
||||||
log.info('Using provided proxy url');
|
log.info('Using provided proxy url');
|
||||||
}
|
}
|
||||||
|
|
||||||
window.Whisper.events = clone(window.Backbone.Events);
|
|
||||||
initMessageCleanup();
|
initMessageCleanup();
|
||||||
startConversationController();
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!isProduction(window.SignalContext.getVersion()) ||
|
!isProduction(window.SignalContext.getVersion()) ||
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue