diff --git a/.eslintrc.js b/.eslintrc.js index c52d76b4be0..3d4f0f5e1c2 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -104,7 +104,7 @@ const rules = { // Prefer functional components with default params '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': [ 'error', { diff --git a/ACKNOWLEDGMENTS.md b/ACKNOWLEDGMENTS.md index c1c70fbf34d..9276fccf101 100644 --- a/ACKNOWLEDGMENTS.md +++ b/ACKNOWLEDGMENTS.md @@ -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 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 Apache License diff --git a/danger/rules.ts b/danger/rules.ts index 134bc7c9a99..58b4a2a0620 100644 --- a/danger/rules.ts +++ b/danger/rules.ts @@ -3,7 +3,6 @@ import { run } from 'endanger'; -import migrateBackboneToRedux from './rules/migrateBackboneToRedux'; import packageJsonVersionsShouldBePinned from './rules/packageJsonVersionsShouldBePinned'; import pnpmLockDepsShouldHaveIntegrity from './rules/pnpmLockDepsShouldHaveIntegrity'; @@ -19,7 +18,6 @@ function isGitDeletedError(error: unknown) { async function main() { try { await run( - migrateBackboneToRedux(), packageJsonVersionsShouldBePinned(), pnpmLockDepsShouldHaveIntegrity() ); diff --git a/danger/rules/migrateBackboneToRedux.ts b/danger/rules/migrateBackboneToRedux.ts deleted file mode 100644 index 00e4b0bf274..00000000000 --- a/danger/rules/migrateBackboneToRedux.ts +++ /dev/null @@ -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, - }); - } - } - } - }, - }); -} diff --git a/danger/rules/pnpmLockDepsShouldHaveIntegrity.ts b/danger/rules/pnpmLockDepsShouldHaveIntegrity.ts index 93a3ada09ac..704b1c7ea9c 100644 --- a/danger/rules/pnpmLockDepsShouldHaveIntegrity.ts +++ b/danger/rules/pnpmLockDepsShouldHaveIntegrity.ts @@ -20,7 +20,7 @@ function has( return Object.hasOwn(value, key); } -export default function migrateBackboneToRedux() { +export default function pnpmLockDepsShouldHaveIntegrity() { return new Rule({ match: { files: ['pnpm-lock.yaml'], diff --git a/package.json b/package.json index 70f965a462c..00f05cff5bd 100644 --- a/package.json +++ b/package.json @@ -139,7 +139,6 @@ "@tanstack/react-virtual": "3.11.2", "@types/dom-mediacapture-transform": "0.1.11", "@types/fabric": "4.5.3", - "backbone": "1.6.0", "blob-util": "2.0.2", "blueimp-load-image": "5.16.0", "blurhash": "2.0.5", @@ -256,7 +255,6 @@ "@storybook/types": "8.1.11", "@tailwindcss/cli": "4.1.7", "@tailwindcss/postcss": "4.1.7", - "@types/backbone": "1.4.22", "@types/blueimp-load-image": "5.16.6", "@types/chai": "4.3.16", "@types/chai-as-promised": "7.1.4", @@ -378,7 +376,6 @@ "react-contextmenu>react-dom": "18.3.1" }, "patchedDependencies": { - "@types/backbone@1.4.22": "patches/@types+backbone+1.4.22.patch", "casual@1.6.2": "patches/casual+1.6.2.patch", "protobufjs@7.3.2": "patches/protobufjs+7.3.2.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", "websocket@1.0.34": "patches/websocket+1.0.34.patch", "@types/websocket@1.0.0": "patches/@types+websocket+1.0.0.patch", - "backbone@1.6.0": "patches/backbone+1.6.0.patch", "node-fetch@2.6.7": "patches/node-fetch+2.6.7.patch", "zod@3.23.8": "patches/zod+3.23.8.patch", "app-builder-lib": "patches/app-builder-lib.patch", diff --git a/patches/@types+backbone+1.4.22.patch b/patches/@types+backbone+1.4.22.patch deleted file mode 100644 index 04ebf95e844..00000000000 --- a/patches/@types+backbone+1.4.22.patch +++ /dev/null @@ -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 | undefined; - } - -- type CombinedModelConstructorOptions = Model> = ModelConstructorOptions & E; -+ type CombinedModelConstructorOptions = Model> = ModelConstructorOptions & E; - - interface ModelSetOptions extends Silenceable, Validable {} - -@@ -204,7 +204,7 @@ declare namespace Backbone { - */ - static extend(properties: any, classProperties?: any): any; - -- attributes: Partial; -+ attributes: T; - changed: Partial; - 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; -- id: string | number; -+ id: string; - idAttribute: string; - validationError: any; - -@@ -251,7 +251,7 @@ declare namespace Backbone { - * return super.get("name"); - * } - */ -- get>(attributeName: A): T[A] | undefined; -+ get>(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; - save(attributes?: Partial | null, options?: ModelSaveOptions): JQueryXHR; - unset(attribute: _StringKey, options?: Silenceable): this; -- validate(attributes: Partial, options?: any): any; -+ validate(attributes: T, options?: any): any; - private _validate(attributes: Partial, options: any): boolean; - - // mixins from underscore diff --git a/patches/backbone+1.6.0.patch b/patches/backbone+1.6.0.patch deleted file mode 100644 index 66490892131..00000000000 --- a/patches/backbone+1.6.0.patch +++ /dev/null @@ -1,775 +0,0 @@ -diff --git a/backbone-min.js b/backbone-min.js -deleted file mode 100644 -index 9bcce86..0000000 ---- a/backbone-min.js -+++ /dev/null -@@ -1,2 +0,0 @@ --(function(r){var n=typeof self=="object"&&self.self===self&&self||typeof global=="object"&&global.global===global&&global;if(typeof define==="function"&&define.amd){define(["underscore","jquery","exports"],function(t,e,i){n.Backbone=r(n,i,t,e)})}else if(typeof exports!=="undefined"){var t=require("underscore"),e;try{e=require("jquery")}catch(t){}r(n,exports,t,e)}else{n.Backbone=r(n,{},n._,n.jQuery||n.Zepto||n.ender||n.$)}})(function(t,h,x,e){var i=t.Backbone;var a=Array.prototype.slice;h.VERSION="1.6.0";h.$=e;h.noConflict=function(){t.Backbone=i;return this};h.emulateHTTP=false;h.emulateJSON=false;var r=h.Events={};var o=/\s+/;var l;var u=function(t,e,i,r,n){var s=0,a;if(i&&typeof i==="object"){if(r!==void 0&&"context"in n&&n.context===void 0)n.context=r;for(a=x.keys(i);sthis.length)r=this.length;if(r<0)r+=this.length+1;var n=[];var s=[];var a=[];var o=[];var h={};var l=e.add;var u=e.merge;var c=e.remove;var f=false;var d=this.comparator&&r==null&&e.sort!==false;var v=x.isString(this.comparator)?this.comparator:null;var p,g;for(g=0;g0&&!e.silent)delete e.index;return i},_isModel:function(t){return t instanceof g},_addReference:function(t,e){this._byId[t.cid]=t;var i=this.modelId(t.attributes,t.idAttribute);if(i!=null)this._byId[i]=t;t.on("all",this._onModelEvent,this)},_removeReference:function(t,e){delete this._byId[t.cid];var i=this.modelId(t.attributes,t.idAttribute);if(i!=null)delete this._byId[i];if(this===t.collection)delete t.collection;t.off("all",this._onModelEvent,this)},_onModelEvent:function(t,e,i,r){if(e){if((t==="add"||t==="remove")&&i!==this)return;if(t==="destroy")this.remove(e,r);if(t==="changeId"){var n=this.modelId(e.previousAttributes(),e.idAttribute);var s=this.modelId(e.attributes,e.idAttribute);if(n!=null)delete this._byId[n];if(s!=null)this._byId[s]=e}}this.trigger.apply(this,arguments)},_forwardPristineError:function(t,e,i){if(this.has(t))return;this._onModelEvent("error",t,e,i)}});var y=typeof Symbol==="function"&&Symbol.iterator;if(y){m.prototype[y]=m.prototype.values}var b=function(t,e){this._collection=t;this._kind=e;this._index=0};var S=1;var I=2;var k=3;if(y){b.prototype[y]=function(){return this}}b.prototype.next=function(){if(this._collection){if(this._index7);this._useHashChange=this._wantsHashChange&&this._hasHashChange;this._wantsPushState=!!this.options.pushState;this._hasPushState=!!(this.history&&this.history.pushState);this._usePushState=this._wantsPushState&&this._hasPushState;this.fragment=this.getFragment();this.root=("/"+this.root+"/").replace(L,"/");if(this._wantsHashChange&&this._wantsPushState){if(!this._hasPushState&&!this.atRoot()){var e=this.root.slice(0,-1)||"/";this.location.replace(e+"#"+this.getPath());return true}else if(this._hasPushState&&this.atRoot()){this.navigate(this.getHash(),{replace:true})}}if(!this._hasHashChange&&this._wantsHashChange&&!this._usePushState){this.iframe=document.createElement("iframe");this.iframe.src="javascript:0";this.iframe.style.display="none";this.iframe.tabIndex=-1;var i=document.body;var r=i.insertBefore(this.iframe,i.firstChild).contentWindow;r.document.open();r.document.close();r.location.hash="#"+this.fragment}var n=window.addEventListener||function(t,e){return attachEvent("on"+t,e)};if(this._usePushState){n("popstate",this.checkUrl,false)}else if(this._useHashChange&&!this.iframe){n("hashchange",this.checkUrl,false)}else if(this._wantsHashChange){this._checkUrlInterval=setInterval(this.checkUrl,this.interval)}if(!this.options.silent)return this.loadUrl()},stop:function(){var t=window.removeEventListener||function(t,e){return detachEvent("on"+t,e)};if(this._usePushState){t("popstate",this.checkUrl,false)}else if(this._useHashChange&&!this.iframe){t("hashchange",this.checkUrl,false)}if(this.iframe){document.body.removeChild(this.iframe);this.iframe=null}if(this._checkUrlInterval)clearInterval(this._checkUrlInterval);B.started=false},route:function(t,e){this.handlers.unshift({route:t,callback:e})},checkUrl:function(t){var e=this.getFragment();if(e===this.fragment&&this.iframe){e=this.getHash(this.iframe.contentWindow)}if(e===this.fragment){if(!this.matchRoot())return this.notfound();return false}if(this.iframe)this.navigate(e);this.loadUrl()},loadUrl:function(e){if(!this.matchRoot())return this.notfound();e=this.fragment=this.getFragment(e);return x.some(this.handlers,function(t){if(t.route.test(e)){t.callback(e);return true}})||this.notfound()},notfound:function(){this.trigger("notfound");return false},navigate:function(t,e){if(!B.started)return false;if(!e||e===true)e={trigger:!!e};t=this.getFragment(t||"");var i=this.root;if(!this._trailingSlash&&(t===""||t.charAt(0)==="?")){i=i.slice(0,-1)||"/"}var r=i+t;t=t.replace(W,"");var n=this.decodeFragment(t);if(this.fragment===n)return;this.fragment=n;if(this._usePushState){this.history[e.replace?"replaceState":"pushState"]({},document.title,r)}else if(this._wantsHashChange){this._updateHash(this.location,t,e.replace);if(this.iframe&&t!==this.getHash(this.iframe.contentWindow)){var s=this.iframe.contentWindow;if(!e.replace){s.document.open();s.document.close()}this._updateHash(s.location,t,e.replace)}}else{return this.location.assign(r)}if(e.trigger)return this.loadUrl(t)},_updateHash:function(t,e,i){if(i){var r=t.href.replace(/(javascript:|#).*$/,"");t.replace(r+"#"+e)}else{t.hash="#"+e}}});h.history=new B;var D=function(t,e){var i=this;var r;if(t&&x.has(t,"constructor")){r=t.constructor}else{r=function(){return i.apply(this,arguments)}}x.extend(r,i,e);r.prototype=x.create(i.prototype,t);r.prototype.constructor=r;r.__super__=i.prototype;return r};g.extend=m.extend=O.extend=A.extend=B.extend=D;var V=function(){throw new Error('A "url" property or function must be specified')};var G=function(e,i){var r=i.error;i.error=function(t){if(r)r.call(i.context,e,t,i);e.trigger("error",e,t,i)}};h._debug=function(){return{root:t,_:x}};return h}); --//# sourceMappingURL=backbone-min.js.map -\ No newline at end of file -diff --git a/backbone-min.js.map b/backbone-min.js.map -deleted file mode 100644 -index 2ba6c35..0000000 ---- a/backbone-min.js.map -+++ /dev/null -@@ -1 +0,0 @@ --{"version":3,"sources":["backbone.js"],"names":["factory","root","self","global","define","amd","_","$","exports","Backbone","require","e","jQuery","Zepto","ender","previousBackbone","slice","Array","prototype","VERSION","noConflict","this","emulateHTTP","emulateJSON","Events","eventSplitter","_listening","eventsApi","iteratee","events","name","callback","opts","i","names","context","keys","length","test","split","on","_events","onApi","ctx","listening","listeners","_listeners","id","interop","listenTo","obj","_listenId","uniqueId","listeningTo","_listeningTo","Listening","error","tryCatchOn","options","handlers","count","push","off","offApi","stopListening","ids","isEmpty","cleanup","remaining","j","handler","_callback","once","onceMap","bind","listenToOnce","map","offer","apply","arguments","trigger","Math","max","args","triggerApi","objEvents","allEvents","all","triggerEvents","concat","ev","l","a1","a2","a3","call","listener","unbind","extend","Model","attributes","attrs","preinitialize","cid","cidPrefix","collection","parse","defaults","result","set","changed","initialize","validationError","idAttribute","toJSON","clone","sync","get","attr","escape","has","matches","key","val","_validate","unset","silent","changes","changing","_changing","_previousAttributes","current","prev","isEqual","prevId","_pending","clear","hasChanged","changedAttributes","diff","old","previous","previousAttributes","fetch","model","success","resp","serverAttrs","wrapError","save","validate","wait","method","isNew","patch","xhr","destroy","defer","url","base","urlError","replace","encodeURIComponent","constructor","isValid","Collection","models","comparator","_reset","reset","setOptions","add","remove","merge","addOptions","splice","array","insert","at","min","tail","singular","isArray","removed","_removeModels","added","merged","_isModel","toAdd","toMerge","toRemove","modelMap","sort","sortable","sortAttr","isString","existing","_prepareModel","_addReference","orderChanged","some","m","index","_removeReference","previousModels","pop","unshift","shift","_byId","modelId","where","first","findWhere","Error","isFunction","sortBy","pluck","create","callbackOpts","_forwardPristineError","values","CollectionIterator","ITERATOR_VALUES","ITERATOR_KEYS","entries","ITERATOR_KEYSVALUES","indexOf","_onModelEvent","event","$$iterator","Symbol","iterator","kind","_collection","_kind","_index","next","value","done","View","pick","viewOptions","_ensureElement","delegateEventSplitter","tagName","selector","$el","find","render","_removeElement","setElement","element","undelegateEvents","_setElement","delegateEvents","el","match","delegate","eventName","undelegate","_createElement","document","createElement","className","_setAttributes","addMethod","attribute","cb","defaultVal","addUnderscoreMethods","Class","methods","each","instance","isObject","modelMatcher","matcher","collectionMethods","forEach","collect","reduce","foldl","inject","reduceRight","foldr","detect","filter","select","reject","every","any","include","includes","contains","invoke","toArray","size","head","take","initial","rest","drop","last","without","difference","shuffle","lastIndexOf","chain","sample","partition","groupBy","countBy","indexBy","findIndex","findLastIndex","modelMethods","pairs","invert","omit","config","Base","mixin","mappings","functions","memo","type","methodMap","params","dataType","data","contentType","JSON","stringify","_method","beforeSend","setRequestHeader","processData","textStatus","errorThrown","ajax","update","delete","read","Router","routes","_bindRoutes","optionalParam","namedParam","splatParam","escapeRegExp","route","isRegExp","_routeToRegExp","router","history","fragment","_extractParameters","execute","navigate","optional","RegExp","exec","param","decodeURIComponent","History","checkUrl","window","location","routeStripper","rootStripper","pathStripper","started","interval","atRoot","path","pathname","getSearch","matchRoot","decodeFragment","rootPath","decodeURI","href","getHash","getPath","charAt","getFragment","_usePushState","_wantsHashChange","start","_trailingSlash","trailingSlash","hashChange","_hasHashChange","documentMode","_useHashChange","_wantsPushState","pushState","_hasPushState","iframe","src","style","display","tabIndex","body","iWindow","insertBefore","firstChild","contentWindow","open","close","hash","addEventListener","attachEvent","_checkUrlInterval","setInterval","loadUrl","stop","removeEventListener","detachEvent","removeChild","clearInterval","notfound","decodedFragment","title","_updateHash","assign","protoProps","staticProps","parent","child","__super__","_debug"],"mappings":"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"} -\ No newline at end of file -diff --git a/backbone-min.map b/backbone-min.map -deleted file mode 100644 -index 2ba6c35..0000000 ---- a/backbone-min.map -+++ /dev/null -@@ -1 +0,0 @@ --{"version":3,"sources":["backbone.js"],"names":["factory","root","self","global","define","amd","_","$","exports","Backbone","require","e","jQuery","Zepto","ender","previousBackbone","slice","Array","prototype","VERSION","noConflict","this","emulateHTTP","emulateJSON","Events","eventSplitter","_listening","eventsApi","iteratee","events","name","callback","opts","i","names","context","keys","length","test","split","on","_events","onApi","ctx","listening","listeners","_listeners","id","interop","listenTo","obj","_listenId","uniqueId","listeningTo","_listeningTo","Listening","error","tryCatchOn","options","handlers","count","push","off","offApi","stopListening","ids","isEmpty","cleanup","remaining","j","handler","_callback","once","onceMap","bind","listenToOnce","map","offer","apply","arguments","trigger","Math","max","args","triggerApi","objEvents","allEvents","all","triggerEvents","concat","ev","l","a1","a2","a3","call","listener","unbind","extend","Model","attributes","attrs","preinitialize","cid","cidPrefix","collection","parse","defaults","result","set","changed","initialize","validationError","idAttribute","toJSON","clone","sync","get","attr","escape","has","matches","key","val","_validate","unset","silent","changes","changing","_changing","_previousAttributes","current","prev","isEqual","prevId","_pending","clear","hasChanged","changedAttributes","diff","old","previous","previousAttributes","fetch","model","success","resp","serverAttrs","wrapError","save","validate","wait","method","isNew","patch","xhr","destroy","defer","url","base","urlError","replace","encodeURIComponent","constructor","isValid","Collection","models","comparator","_reset","reset","setOptions","add","remove","merge","addOptions","splice","array","insert","at","min","tail","singular","isArray","removed","_removeModels","added","merged","_isModel","toAdd","toMerge","toRemove","modelMap","sort","sortable","sortAttr","isString","existing","_prepareModel","_addReference","orderChanged","some","m","index","_removeReference","previousModels","pop","unshift","shift","_byId","modelId","where","first","findWhere","Error","isFunction","sortBy","pluck","create","callbackOpts","_forwardPristineError","values","CollectionIterator","ITERATOR_VALUES","ITERATOR_KEYS","entries","ITERATOR_KEYSVALUES","indexOf","_onModelEvent","event","$$iterator","Symbol","iterator","kind","_collection","_kind","_index","next","value","done","View","pick","viewOptions","_ensureElement","delegateEventSplitter","tagName","selector","$el","find","render","_removeElement","setElement","element","undelegateEvents","_setElement","delegateEvents","el","match","delegate","eventName","undelegate","_createElement","document","createElement","className","_setAttributes","addMethod","attribute","cb","defaultVal","addUnderscoreMethods","Class","methods","each","instance","isObject","modelMatcher","matcher","collectionMethods","forEach","collect","reduce","foldl","inject","reduceRight","foldr","detect","filter","select","reject","every","any","include","includes","contains","invoke","toArray","size","head","take","initial","rest","drop","last","without","difference","shuffle","lastIndexOf","chain","sample","partition","groupBy","countBy","indexBy","findIndex","findLastIndex","modelMethods","pairs","invert","omit","config","Base","mixin","mappings","functions","memo","type","methodMap","params","dataType","data","contentType","JSON","stringify","_method","beforeSend","setRequestHeader","processData","textStatus","errorThrown","ajax","update","delete","read","Router","routes","_bindRoutes","optionalParam","namedParam","splatParam","escapeRegExp","route","isRegExp","_routeToRegExp","router","history","fragment","_extractParameters","execute","navigate","optional","RegExp","exec","param","decodeURIComponent","History","checkUrl","window","location","routeStripper","rootStripper","pathStripper","started","interval","atRoot","path","pathname","getSearch","matchRoot","decodeFragment","rootPath","decodeURI","href","getHash","getPath","charAt","getFragment","_usePushState","_wantsHashChange","start","_trailingSlash","trailingSlash","hashChange","_hasHashChange","documentMode","_useHashChange","_wantsPushState","pushState","_hasPushState","iframe","src","style","display","tabIndex","body","iWindow","insertBefore","firstChild","contentWindow","open","close","hash","addEventListener","attachEvent","_checkUrlInterval","setInterval","loadUrl","stop","removeEventListener","detachEvent","removeChild","clearInterval","notfound","decodedFragment","title","_updateHash","assign","protoProps","staticProps","parent","child","__super__","_debug"],"mappings":"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"} -\ No newline at end of file -diff --git a/backbone.js b/backbone.js -index aa36f1e..9dfedbb 100644 ---- a/backbone.js -+++ b/backbone.js -@@ -12,24 +12,8 @@ - var root = typeof self == 'object' && self.self === self && self || - typeof global == 'object' && global.global === global && global; - -- // Set up Backbone appropriately for the environment. Start with AMD. -- if (typeof define === 'function' && define.amd) { -- define(['underscore', 'jquery', 'exports'], function(_, $, exports) { -- // Export global even in AMD case in case this script is loaded with -- // others that may still expect a global Backbone. -- root.Backbone = factory(root, exports, _, $); -- }); -- -- // Next for Node.js or CommonJS. jQuery may not be needed as a module. -- } else if (typeof exports !== 'undefined') { -- var _ = require('underscore'), $; -- try { $ = require('jquery'); } catch (e) {} -- factory(root, exports, _, $); -- -- // Finally, as a browser global. -- } else { -- root.Backbone = factory(root, {}, root._, root.jQuery || root.Zepto || root.ender || root.$); -- } -+ var _ = require('lodash'), $; -+ factory(root, exports, _, $); - - })(function(root, Backbone, _, $) { - -@@ -46,10 +30,6 @@ - // Current version of the library. Keep in sync with `package.json`. - Backbone.VERSION = '1.6.0'; - -- // For Backbone's purposes, jQuery, Zepto, Ender, or My Library (kidding) owns -- // the `$` variable. -- Backbone.$ = $; -- - // Runs Backbone.js in *noConflict* mode, returning the `Backbone` variable - // to its previous owner. Returns a reference to this Backbone object. - Backbone.noConflict = function() { -@@ -1319,174 +1299,6 @@ - return {value: void 0, done: true}; - }; - -- // Backbone.View -- // ------------- -- -- // Backbone Views are almost more convention than they are actual code. A View -- // is simply a JavaScript object that represents a logical chunk of UI in the -- // DOM. This might be a single item, an entire list, a sidebar or panel, or -- // even the surrounding frame which wraps your whole app. Defining a chunk of -- // UI as a **View** allows you to define your DOM events declaratively, without -- // having to worry about render order ... and makes it easy for the view to -- // react to specific changes in the state of your models. -- -- // Creating a Backbone.View creates its initial element outside of the DOM, -- // if an existing element is not provided... -- var View = Backbone.View = function(options) { -- this.cid = _.uniqueId('view'); -- this.preinitialize.apply(this, arguments); -- _.extend(this, _.pick(options, viewOptions)); -- this._ensureElement(); -- this.initialize.apply(this, arguments); -- }; -- -- // Cached regex to split keys for `delegate`. -- var delegateEventSplitter = /^(\S+)\s*(.*)$/; -- -- // List of view options to be set as properties. -- var viewOptions = ['model', 'collection', 'el', 'id', 'attributes', 'className', 'tagName', 'events']; -- -- // Set up all inheritable **Backbone.View** properties and methods. -- _.extend(View.prototype, Events, { -- -- // The default `tagName` of a View's element is `"div"`. -- tagName: 'div', -- -- // jQuery delegate for element lookup, scoped to DOM elements within the -- // current view. This should be preferred to global lookups where possible. -- $: function(selector) { -- return this.$el.find(selector); -- }, -- -- // preinitialize is an empty function by default. You can override it with a function -- // or object. preinitialize will run before any instantiation logic is run in the View -- preinitialize: function(){}, -- -- // Initialize is an empty function by default. Override it with your own -- // initialization logic. -- initialize: function(){}, -- -- // **render** is the core function that your view should override, in order -- // to populate its element (`this.el`), with the appropriate HTML. The -- // convention is for **render** to always return `this`. -- render: function() { -- return this; -- }, -- -- // Remove this view by taking the element out of the DOM, and removing any -- // applicable Backbone.Events listeners. -- remove: function() { -- this._removeElement(); -- this.stopListening(); -- return this; -- }, -- -- // Remove this view's element from the document and all event listeners -- // attached to it. Exposed for subclasses using an alternative DOM -- // manipulation API. -- _removeElement: function() { -- this.$el.remove(); -- }, -- -- // Change the view's element (`this.el` property) and re-delegate the -- // view's events on the new element. -- setElement: function(element) { -- this.undelegateEvents(); -- this._setElement(element); -- this.delegateEvents(); -- return this; -- }, -- -- // Creates the `this.el` and `this.$el` references for this view using the -- // given `el`. `el` can be a CSS selector or an HTML string, a jQuery -- // context or an element. Subclasses can override this to utilize an -- // alternative DOM manipulation API and are only required to set the -- // `this.el` property. -- _setElement: function(el) { -- this.$el = el instanceof Backbone.$ ? el : Backbone.$(el); -- this.el = this.$el[0]; -- }, -- -- // Set callbacks, where `this.events` is a hash of -- // -- // *{"event selector": "callback"}* -- // -- // { -- // 'mousedown .title': 'edit', -- // 'click .button': 'save', -- // 'click .open': function(e) { ... } -- // } -- // -- // pairs. Callbacks will be bound to the view, with `this` set properly. -- // Uses event delegation for efficiency. -- // Omitting the selector binds the event to `this.el`. -- delegateEvents: function(events) { -- events || (events = _.result(this, 'events')); -- if (!events) return this; -- this.undelegateEvents(); -- for (var key in events) { -- var method = events[key]; -- if (!_.isFunction(method)) method = this[method]; -- if (!method) continue; -- var match = key.match(delegateEventSplitter); -- this.delegate(match[1], match[2], method.bind(this)); -- } -- return this; -- }, -- -- // Add a single event listener to the view's element (or a child element -- // using `selector`). This only works for delegate-able events: not `focus`, -- // `blur`, and not `change`, `submit`, and `reset` in Internet Explorer. -- delegate: function(eventName, selector, listener) { -- this.$el.on(eventName + '.delegateEvents' + this.cid, selector, listener); -- return this; -- }, -- -- // Clears all callbacks previously bound to the view by `delegateEvents`. -- // You usually don't need to use this, but may wish to if you have multiple -- // Backbone views attached to the same DOM element. -- undelegateEvents: function() { -- if (this.$el) this.$el.off('.delegateEvents' + this.cid); -- return this; -- }, -- -- // A finer-grained `undelegateEvents` for removing a single delegated event. -- // `selector` and `listener` are both optional. -- undelegate: function(eventName, selector, listener) { -- this.$el.off(eventName + '.delegateEvents' + this.cid, selector, listener); -- return this; -- }, -- -- // Produces a DOM element to be assigned to your view. Exposed for -- // subclasses using an alternative DOM manipulation API. -- _createElement: function(tagName) { -- return document.createElement(tagName); -- }, -- -- // Ensure that the View has a DOM element to render into. -- // If `this.el` is a string, pass it through `$()`, take the first -- // matching element, and re-assign it to `el`. Otherwise, create -- // an element from the `id`, `className` and `tagName` properties. -- _ensureElement: function() { -- if (!this.el) { -- var attrs = _.extend({}, _.result(this, 'attributes')); -- if (this.id) attrs.id = _.result(this, 'id'); -- if (this.className) attrs['class'] = _.result(this, 'className'); -- this.setElement(this._createElement(_.result(this, 'tagName'))); -- this._setAttributes(attrs); -- } else { -- this.setElement(_.result(this, 'el')); -- } -- }, -- -- // Set attributes from a hash on this view's element. Exposed for -- // subclasses using an alternative DOM manipulation API. -- _setAttributes: function(attributes) { -- this.$el.attr(attributes); -- } -- -- }); -- - // Proxy Backbone class methods to Underscore functions, wrapping the model's - // `attributes` object or collection's `models` array behind the scenes. - // -@@ -1575,523 +1387,6 @@ - addUnderscoreMethods(Base, _, methods, attribute); - }); - -- // Backbone.sync -- // ------------- -- -- // Override this function to change the manner in which Backbone persists -- // models to the server. You will be passed the type of request, and the -- // model in question. By default, makes a RESTful Ajax request -- // to the model's `url()`. Some possible customizations could be: -- // -- // * Use `setTimeout` to batch rapid-fire updates into a single request. -- // * Send up the models as XML instead of JSON. -- // * Persist models via WebSockets instead of Ajax. -- // -- // Turn on `Backbone.emulateHTTP` in order to send `PUT` and `DELETE` requests -- // as `POST`, with a `_method` parameter containing the true HTTP method, -- // as well as all requests with the body as `application/x-www-form-urlencoded` -- // instead of `application/json` with the model in a param named `model`. -- // Useful when interfacing with server-side languages like **PHP** that make -- // it difficult to read the body of `PUT` requests. -- Backbone.sync = function(method, model, options) { -- var type = methodMap[method]; -- -- // Default options, unless specified. -- _.defaults(options || (options = {}), { -- emulateHTTP: Backbone.emulateHTTP, -- emulateJSON: Backbone.emulateJSON -- }); -- -- // Default JSON-request options. -- var params = {type: type, dataType: 'json'}; -- -- // Ensure that we have a URL. -- if (!options.url) { -- params.url = _.result(model, 'url') || urlError(); -- } -- -- // Ensure that we have the appropriate request data. -- if (options.data == null && model && (method === 'create' || method === 'update' || method === 'patch')) { -- params.contentType = 'application/json'; -- params.data = JSON.stringify(options.attrs || model.toJSON(options)); -- } -- -- // For older servers, emulate JSON by encoding the request into an HTML-form. -- if (options.emulateJSON) { -- params.contentType = 'application/x-www-form-urlencoded'; -- params.data = params.data ? {model: params.data} : {}; -- } -- -- // For older servers, emulate HTTP by mimicking the HTTP method with `_method` -- // And an `X-HTTP-Method-Override` header. -- if (options.emulateHTTP && (type === 'PUT' || type === 'DELETE' || type === 'PATCH')) { -- params.type = 'POST'; -- if (options.emulateJSON) params.data._method = type; -- var beforeSend = options.beforeSend; -- options.beforeSend = function(xhr) { -- xhr.setRequestHeader('X-HTTP-Method-Override', type); -- if (beforeSend) return beforeSend.apply(this, arguments); -- }; -- } -- -- // Don't process data on a non-GET request. -- if (params.type !== 'GET' && !options.emulateJSON) { -- params.processData = false; -- } -- -- // Pass along `textStatus` and `errorThrown` from jQuery. -- var error = options.error; -- options.error = function(xhr, textStatus, errorThrown) { -- options.textStatus = textStatus; -- options.errorThrown = errorThrown; -- if (error) error.call(options.context, xhr, textStatus, errorThrown); -- }; -- -- // Make the request, allowing the user to override any Ajax options. -- var xhr = options.xhr = Backbone.ajax(_.extend(params, options)); -- model.trigger('request', model, xhr, options); -- return xhr; -- }; -- -- // Map from CRUD to HTTP for our default `Backbone.sync` implementation. -- var methodMap = { -- 'create': 'POST', -- 'update': 'PUT', -- 'patch': 'PATCH', -- 'delete': 'DELETE', -- 'read': 'GET' -- }; -- -- // Set the default implementation of `Backbone.ajax` to proxy through to `$`. -- // Override this if you'd like to use a different library. -- Backbone.ajax = function() { -- return Backbone.$.ajax.apply(Backbone.$, arguments); -- }; -- -- // Backbone.Router -- // --------------- -- -- // Routers map faux-URLs to actions, and fire events when routes are -- // matched. Creating a new one sets its `routes` hash, if not set statically. -- var Router = Backbone.Router = function(options) { -- options || (options = {}); -- this.preinitialize.apply(this, arguments); -- if (options.routes) this.routes = options.routes; -- this._bindRoutes(); -- this.initialize.apply(this, arguments); -- }; -- -- // Cached regular expressions for matching named param parts and splatted -- // parts of route strings. -- var optionalParam = /\((.*?)\)/g; -- var namedParam = /(\(\?)?:\w+/g; -- var splatParam = /\*\w+/g; -- var escapeRegExp = /[\-{}\[\]+?.,\\\^$|#\s]/g; -- -- // Set up all inheritable **Backbone.Router** properties and methods. -- _.extend(Router.prototype, Events, { -- -- // preinitialize is an empty function by default. You can override it with a function -- // or object. preinitialize will run before any instantiation logic is run in the Router. -- preinitialize: function(){}, -- -- // Initialize is an empty function by default. Override it with your own -- // initialization logic. -- initialize: function(){}, -- -- // Manually bind a single named route to a callback. For example: -- // -- // this.route('search/:query/p:num', 'search', function(query, num) { -- // ... -- // }); -- // -- route: function(route, name, callback) { -- if (!_.isRegExp(route)) route = this._routeToRegExp(route); -- if (_.isFunction(name)) { -- callback = name; -- name = ''; -- } -- if (!callback) callback = this[name]; -- var router = this; -- Backbone.history.route(route, function(fragment) { -- var args = router._extractParameters(route, fragment); -- if (router.execute(callback, args, name) !== false) { -- router.trigger.apply(router, ['route:' + name].concat(args)); -- router.trigger('route', name, args); -- Backbone.history.trigger('route', router, name, args); -- } -- }); -- return this; -- }, -- -- // Execute a route handler with the provided parameters. This is an -- // excellent place to do pre-route setup or post-route cleanup. -- execute: function(callback, args, name) { -- if (callback) callback.apply(this, args); -- }, -- -- // Simple proxy to `Backbone.history` to save a fragment into the history. -- navigate: function(fragment, options) { -- Backbone.history.navigate(fragment, options); -- return this; -- }, -- -- // Bind all defined routes to `Backbone.history`. We have to reverse the -- // order of the routes here to support behavior where the most general -- // routes can be defined at the bottom of the route map. -- _bindRoutes: function() { -- if (!this.routes) return; -- this.routes = _.result(this, 'routes'); -- var route, routes = _.keys(this.routes); -- while ((route = routes.pop()) != null) { -- this.route(route, this.routes[route]); -- } -- }, -- -- // Convert a route string into a regular expression, suitable for matching -- // against the current location hash. -- _routeToRegExp: function(route) { -- route = route.replace(escapeRegExp, '\\$&') -- .replace(optionalParam, '(?:$1)?') -- .replace(namedParam, function(match, optional) { -- return optional ? match : '([^/?]+)'; -- }) -- .replace(splatParam, '([^?]*?)'); -- return new RegExp('^' + route + '(?:\\?([\\s\\S]*))?$'); -- }, -- -- // Given a route, and a URL fragment that it matches, return the array of -- // extracted decoded parameters. Empty or unmatched parameters will be -- // treated as `null` to normalize cross-browser behavior. -- _extractParameters: function(route, fragment) { -- var params = route.exec(fragment).slice(1); -- return _.map(params, function(param, i) { -- // Don't decode the search params. -- if (i === params.length - 1) return param || null; -- return param ? decodeURIComponent(param) : null; -- }); -- } -- -- }); -- -- // Backbone.History -- // ---------------- -- -- // Handles cross-browser history management, based on either -- // [pushState](http://diveintohtml5.info/history.html) and real URLs, or -- // [onhashchange](https://developer.mozilla.org/en-US/docs/DOM/window.onhashchange) -- // and URL fragments. If the browser supports neither (old IE, natch), -- // falls back to polling. -- var History = Backbone.History = function() { -- this.handlers = []; -- this.checkUrl = this.checkUrl.bind(this); -- -- // Ensure that `History` can be used outside of the browser. -- if (typeof window !== 'undefined') { -- this.location = window.location; -- this.history = window.history; -- } -- }; -- -- // Cached regex for stripping a leading hash/slash and trailing space. -- var routeStripper = /^[#\/]|\s+$/g; -- -- // Cached regex for stripping leading and trailing slashes. -- var rootStripper = /^\/+|\/+$/g; -- -- // Cached regex for stripping urls of hash. -- var pathStripper = /#.*$/; -- -- // Has the history handling already been started? -- History.started = false; -- -- // Set up all inheritable **Backbone.History** properties and methods. -- _.extend(History.prototype, Events, { -- -- // The default interval to poll for hash changes, if necessary, is -- // twenty times a second. -- interval: 50, -- -- // Are we at the app root? -- atRoot: function() { -- var path = this.location.pathname.replace(/[^\/]$/, '$&/'); -- return path === this.root && !this.getSearch(); -- }, -- -- // Does the pathname match the root? -- matchRoot: function() { -- var path = this.decodeFragment(this.location.pathname); -- var rootPath = path.slice(0, this.root.length - 1) + '/'; -- return rootPath === this.root; -- }, -- -- // Unicode characters in `location.pathname` are percent encoded so they're -- // decoded for comparison. `%25` should not be decoded since it may be part -- // of an encoded parameter. -- decodeFragment: function(fragment) { -- return decodeURI(fragment.replace(/%25/g, '%2525')); -- }, -- -- // In IE6, the hash fragment and search params are incorrect if the -- // fragment contains `?`. -- getSearch: function() { -- var match = this.location.href.replace(/#.*/, '').match(/\?.+/); -- return match ? match[0] : ''; -- }, -- -- // Gets the true hash value. Cannot use location.hash directly due to bug -- // in Firefox where location.hash will always be decoded. -- getHash: function(window) { -- var match = (window || this).location.href.match(/#(.*)$/); -- return match ? match[1] : ''; -- }, -- -- // Get the pathname and search params, without the root. -- getPath: function() { -- var path = this.decodeFragment( -- this.location.pathname + this.getSearch() -- ).slice(this.root.length - 1); -- return path.charAt(0) === '/' ? path.slice(1) : path; -- }, -- -- // Get the cross-browser normalized URL fragment from the path or hash. -- getFragment: function(fragment) { -- if (fragment == null) { -- if (this._usePushState || !this._wantsHashChange) { -- fragment = this.getPath(); -- } else { -- fragment = this.getHash(); -- } -- } -- return fragment.replace(routeStripper, ''); -- }, -- -- // Start the hash change handling, returning `true` if the current URL matches -- // an existing route, and `false` otherwise. -- start: function(options) { -- if (History.started) throw new Error('Backbone.history has already been started'); -- History.started = true; -- -- // Figure out the initial configuration. Do we need an iframe? -- // Is pushState desired ... is it available? -- this.options = _.extend({root: '/'}, this.options, options); -- this.root = this.options.root; -- this._trailingSlash = this.options.trailingSlash; -- this._wantsHashChange = this.options.hashChange !== false; -- this._hasHashChange = 'onhashchange' in window && (document.documentMode === void 0 || document.documentMode > 7); -- this._useHashChange = this._wantsHashChange && this._hasHashChange; -- this._wantsPushState = !!this.options.pushState; -- this._hasPushState = !!(this.history && this.history.pushState); -- this._usePushState = this._wantsPushState && this._hasPushState; -- this.fragment = this.getFragment(); -- -- // Normalize root to always include a leading and trailing slash. -- this.root = ('/' + this.root + '/').replace(rootStripper, '/'); -- -- // Transition from hashChange to pushState or vice versa if both are -- // requested. -- if (this._wantsHashChange && this._wantsPushState) { -- -- // If we've started off with a route from a `pushState`-enabled -- // browser, but we're currently in a browser that doesn't support it... -- if (!this._hasPushState && !this.atRoot()) { -- var rootPath = this.root.slice(0, -1) || '/'; -- this.location.replace(rootPath + '#' + this.getPath()); -- // Return immediately as browser will do redirect to new url -- return true; -- -- // Or if we've started out with a hash-based route, but we're currently -- // in a browser where it could be `pushState`-based instead... -- } else if (this._hasPushState && this.atRoot()) { -- this.navigate(this.getHash(), {replace: true}); -- } -- -- } -- -- // Proxy an iframe to handle location events if the browser doesn't -- // support the `hashchange` event, HTML5 history, or the user wants -- // `hashChange` but not `pushState`. -- if (!this._hasHashChange && this._wantsHashChange && !this._usePushState) { -- this.iframe = document.createElement('iframe'); -- this.iframe.src = 'javascript:0'; -- this.iframe.style.display = 'none'; -- this.iframe.tabIndex = -1; -- var body = document.body; -- // Using `appendChild` will throw on IE < 9 if the document is not ready. -- var iWindow = body.insertBefore(this.iframe, body.firstChild).contentWindow; -- iWindow.document.open(); -- iWindow.document.close(); -- iWindow.location.hash = '#' + this.fragment; -- } -- -- // Add a cross-platform `addEventListener` shim for older browsers. -- var addEventListener = window.addEventListener || function(eventName, listener) { -- return attachEvent('on' + eventName, listener); -- }; -- -- // Depending on whether we're using pushState or hashes, and whether -- // 'onhashchange' is supported, determine how we check the URL state. -- if (this._usePushState) { -- addEventListener('popstate', this.checkUrl, false); -- } else if (this._useHashChange && !this.iframe) { -- addEventListener('hashchange', this.checkUrl, false); -- } else if (this._wantsHashChange) { -- this._checkUrlInterval = setInterval(this.checkUrl, this.interval); -- } -- -- if (!this.options.silent) return this.loadUrl(); -- }, -- -- // Disable Backbone.history, perhaps temporarily. Not useful in a real app, -- // but possibly useful for unit testing Routers. -- stop: function() { -- // Add a cross-platform `removeEventListener` shim for older browsers. -- var removeEventListener = window.removeEventListener || function(eventName, listener) { -- return detachEvent('on' + eventName, listener); -- }; -- -- // Remove window listeners. -- if (this._usePushState) { -- removeEventListener('popstate', this.checkUrl, false); -- } else if (this._useHashChange && !this.iframe) { -- removeEventListener('hashchange', this.checkUrl, false); -- } -- -- // Clean up the iframe if necessary. -- if (this.iframe) { -- document.body.removeChild(this.iframe); -- this.iframe = null; -- } -- -- // Some environments will throw when clearing an undefined interval. -- if (this._checkUrlInterval) clearInterval(this._checkUrlInterval); -- History.started = false; -- }, -- -- // Add a route to be tested when the fragment changes. Routes added later -- // may override previous routes. -- route: function(route, callback) { -- this.handlers.unshift({route: route, callback: callback}); -- }, -- -- // Checks the current URL to see if it has changed, and if it has, -- // calls `loadUrl`, normalizing across the hidden iframe. -- checkUrl: function(e) { -- var current = this.getFragment(); -- -- // If the user pressed the back button, the iframe's hash will have -- // changed and we should use that for comparison. -- if (current === this.fragment && this.iframe) { -- current = this.getHash(this.iframe.contentWindow); -- } -- -- if (current === this.fragment) { -- if (!this.matchRoot()) return this.notfound(); -- return false; -- } -- if (this.iframe) this.navigate(current); -- this.loadUrl(); -- }, -- -- // Attempt to load the current URL fragment. If a route succeeds with a -- // match, returns `true`. If no defined routes matches the fragment, -- // returns `false`. -- loadUrl: function(fragment) { -- // If the root doesn't match, no routes can match either. -- if (!this.matchRoot()) return this.notfound(); -- fragment = this.fragment = this.getFragment(fragment); -- return _.some(this.handlers, function(handler) { -- if (handler.route.test(fragment)) { -- handler.callback(fragment); -- return true; -- } -- }) || this.notfound(); -- }, -- -- // When no route could be matched, this method is called internally to -- // trigger the `'notfound'` event. It returns `false` so that it can be used -- // in tail position. -- notfound: function() { -- this.trigger('notfound'); -- return false; -- }, -- -- // Save a fragment into the hash history, or replace the URL state if the -- // 'replace' option is passed. You are responsible for properly URL-encoding -- // the fragment in advance. -- // -- // The options object can contain `trigger: true` if you wish to have the -- // route callback be fired (not usually desirable), or `replace: true`, if -- // you wish to modify the current URL without adding an entry to the history. -- navigate: function(fragment, options) { -- if (!History.started) return false; -- if (!options || options === true) options = {trigger: !!options}; -- -- // Normalize the fragment. -- fragment = this.getFragment(fragment || ''); -- -- // Strip trailing slash on the root unless _trailingSlash is true -- var rootPath = this.root; -- if (!this._trailingSlash && (fragment === '' || fragment.charAt(0) === '?')) { -- rootPath = rootPath.slice(0, -1) || '/'; -- } -- var url = rootPath + fragment; -- -- // Strip the fragment of the query and hash for matching. -- fragment = fragment.replace(pathStripper, ''); -- -- // Decode for matching. -- var decodedFragment = this.decodeFragment(fragment); -- -- if (this.fragment === decodedFragment) return; -- this.fragment = decodedFragment; -- -- // If pushState is available, we use it to set the fragment as a real URL. -- if (this._usePushState) { -- this.history[options.replace ? 'replaceState' : 'pushState']({}, document.title, url); -- -- // If hash changes haven't been explicitly disabled, update the hash -- // fragment to store history. -- } else if (this._wantsHashChange) { -- this._updateHash(this.location, fragment, options.replace); -- if (this.iframe && fragment !== this.getHash(this.iframe.contentWindow)) { -- var iWindow = this.iframe.contentWindow; -- -- // Opening and closing the iframe tricks IE7 and earlier to push a -- // history entry on hash-tag change. When replace is true, we don't -- // want this. -- if (!options.replace) { -- iWindow.document.open(); -- iWindow.document.close(); -- } -- -- this._updateHash(iWindow.location, fragment, options.replace); -- } -- -- // If you've told us that you explicitly don't want fallback hashchange- -- // based history, then `navigate` becomes a page refresh. -- } else { -- return this.location.assign(url); -- } -- if (options.trigger) return this.loadUrl(fragment); -- }, -- -- // Update the hash location, either replacing the current entry, or adding -- // a new one to the browser history. -- _updateHash: function(location, fragment, replace) { -- if (replace) { -- var href = location.href.replace(/(javascript:|#).*$/, ''); -- location.replace(href + '#' + fragment); -- } else { -- // Some browsers require that `hash` contains a leading #. -- location.hash = '#' + fragment; -- } -- } -- -- }); -- -- // Create the default Backbone.history. -- Backbone.history = new History; - - // Helpers - // ------- -@@ -2128,7 +1423,7 @@ - }; - - // Set up inheritance for the model, collection, router, view and history. -- Model.extend = Collection.extend = Router.extend = View.extend = History.extend = extend; -+ Model.extend = Collection.extend = extend; - - // Throw an error when a URL is needed, and none is supplied. - var urlError = function() { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0289851047b..b1d6d57e749 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -15,9 +15,6 @@ overrides: react-contextmenu>react-dom: 18.3.1 patchedDependencies: - '@types/backbone@1.4.22': - hash: 9dace206a9f53e0e3b0203051b26aec1e92ad49744b156ad8076946356c6c8e7 - path: patches/@types+backbone+1.4.22.patch '@types/express@4.17.21': hash: 85d9b3f3cac67003e41b22245281f53b51d7d1badd0bcc222d547ab802599bae path: patches/@types+express+4.17.21.patch @@ -36,9 +33,6 @@ patchedDependencies: app-builder-lib: hash: b412b44a47bb3d2be98e6edffed5dc4286cc62ac3c02fef42d1557927baa2420 path: patches/app-builder-lib.patch - backbone@1.6.0: - hash: 342b4b6012f8aecfa041554256444cb25af75bc933cf2ab1e91c4f66a8e47a31 - path: patches/backbone+1.6.0.patch casual@1.6.2: hash: b88b5052437cbdc1882137778b76ca5037f71b2a030ae9ef39dc97f51670d599 path: patches/casual+1.6.2.patch @@ -158,9 +152,6 @@ importers: '@types/fabric': specifier: 4.5.3 version: 4.5.3(patch_hash=e5f339ecf72fbab1c91505e7713e127a7184bfe8164aa3a9afe9bf45a0ad6b89) - backbone: - specifier: 1.6.0 - version: 1.6.0(patch_hash=342b4b6012f8aecfa041554256444cb25af75bc933cf2ab1e91c4f66a8e47a31) blob-util: specifier: 2.0.2 version: 2.0.2 @@ -504,9 +495,6 @@ importers: '@tailwindcss/postcss': specifier: 4.1.7 version: 4.1.7 - '@types/backbone': - specifier: 1.4.22 - version: 1.4.22(patch_hash=9dace206a9f53e0e3b0203051b26aec1e92ad49744b156ad8076946356c6c8e7) '@types/blueimp-load-image': specifier: 5.16.6 version: 5.16.6 @@ -3780,9 +3768,6 @@ packages: '@types/babel__traverse@7.20.6': resolution: {integrity: sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==} - '@types/backbone@1.4.22': - resolution: {integrity: sha512-i79hj6XPfsJ37yBHUb9560luep8SPoAbGcpA9TeW1R6Jufk4hHZn5q0l2xuTVtugBcoLlxGQ5qOjaNLBPmqaAg==} - '@types/blueimp-load-image@5.16.6': resolution: {integrity: sha512-e7s6CdDCUoBQdCe62Q6OS+DF68M8+ABxCEMh2Isjt4Fl3xuddljCHMN8mak48AMSVGGwUUtNRaZbkzgL5PEWew==} @@ -4046,9 +4031,6 @@ packages: '@types/stack-utils@2.0.3': resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} - '@types/underscore@1.13.0': - resolution: {integrity: sha512-L6LBgy1f0EFQZ+7uSA57+n2g/s4Qs5r06Vwrwn0/nuK1de+adz00NWaztRQ30aEqw5qOaWbPI8u2cGQ52lj6VA==} - '@types/unist@2.0.11': resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} @@ -4625,9 +4607,6 @@ packages: peerDependencies: '@babel/core': ^7.0.0 - backbone@1.6.0: - resolution: {integrity: sha512-13PUjmsgw/49EowNcQvfG4gmczz1ximTMhUktj0Jfrjth0MVaTxehpU+qYYX4MxnuIuhmvBLC6/ayxuAGnOhbA==} - bail@1.0.5: resolution: {integrity: sha512-xFbRxM1tahm08yHBP16MMjVUAvDaBMD38zsM9EMAUN61omwLmKlOpB/Zku5QkjZ8TZ4vn53pj+t518cH0S03RQ==} @@ -14587,11 +14566,6 @@ snapshots: dependencies: '@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/body-parser@1.19.5': @@ -14890,8 +14864,6 @@ snapshots: '@types/stack-utils@2.0.3': {} - '@types/underscore@1.13.0': {} - '@types/unist@2.0.11': {} '@types/use-sync-external-store@0.0.6': {} @@ -15611,10 +15583,6 @@ snapshots: babel-plugin-jest-hoist: 29.6.3 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: {} balanced-match@1.0.2: {} diff --git a/sticker-creator/.eslintrc.cjs b/sticker-creator/.eslintrc.cjs index d7d9c5029e5..08ad8ff7e0a 100644 --- a/sticker-creator/.eslintrc.cjs +++ b/sticker-creator/.eslintrc.cjs @@ -96,7 +96,7 @@ module.exports = { // Prefer functional components with default params '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': [ 'error', { diff --git a/ts/CI.ts b/ts/CI.ts index ba068dd3f36..3e8006cd6b5 100644 --- a/ts/CI.ts +++ b/ts/CI.ts @@ -220,7 +220,7 @@ export function getCI({ } function unlink() { - window.Whisper.events.trigger('unlinkAndDisconnect'); + window.Whisper.events.emit('unlinkAndDisconnect'); } function print(...args: ReadonlyArray) { diff --git a/ts/CI/benchmarkConversationOpen.ts b/ts/CI/benchmarkConversationOpen.ts index fac9f1f9a98..26d7d466381 100644 --- a/ts/CI/benchmarkConversationOpen.ts +++ b/ts/CI/benchmarkConversationOpen.ts @@ -96,7 +96,7 @@ export async function populateConversationWithMessages({ postSaveUpdates, }); - conversation.set('active_at', Date.now()); + conversation.set({ active_at: Date.now() }); await DataWriter.updateConversation(conversation.attributes); log.info(`${logId}: populating conversation complete`); } diff --git a/ts/ConversationController.ts b/ts/ConversationController.ts index 3df134a8796..eaca25b2b01 100644 --- a/ts/ConversationController.ts +++ b/ts/ConversationController.ts @@ -4,15 +4,6 @@ import { debounce, pick, uniq, without } from 'lodash'; import PQueue from 'p-queue'; 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 { createLogger } from './logging/log'; @@ -21,8 +12,12 @@ import { getAuthorId } from './messages/helpers'; import { maybeDeriveGroupV2Id } from './groups'; import { assertDev, strictAssert } from './util/assert'; import { drop } from './util/drop'; -import { isGroup, isGroupV1, isGroupV2 } from './util/whatTypeOfConversation'; -import type { ServiceIdString, AciString, PniString } from './types/ServiceId'; +import { + isDirectConversation, + isGroup, + isGroupV1, + isGroupV2, +} from './util/whatTypeOfConversation'; import { isServiceIdString, normalizePni, @@ -42,6 +37,18 @@ import { isTestOrMockEnvironment } from './environment'; import { isConversationAccepted } from './util/isConversationAccepted'; import { areWePending } from './util/groupMembershipUtils'; 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'); @@ -129,11 +136,7 @@ async function safeCombineConversations( const MAX_MESSAGE_BODY_LENGTH = 64 * 1024; -const { - getAllConversations, - getAllGroupsInvolvingServiceId, - getMessagesBySentAt, -} = DataReader; +const { getAllConversations, getMessagesBySentAt } = DataReader; const { migrateConversationMessages, @@ -143,57 +146,197 @@ const { updateConversations, } = 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 { #_initialFetchComplete = false; #isReadOnly = false; - private _initialPromise: undefined | Promise; + #_initialPromise: undefined | Promise; + #_conversations: Array = []; #_conversationOpenStart = new Map(); #_hasQueueEmptied = false; #_combineConversationsQueue = new PQueue({ concurrency: 1 }); #_signalConversationId: undefined | string; - constructor(private _conversations: ConversationModelCollectionType) { - const debouncedUpdateUnreadCount = debounce( - this.updateUnreadCount.bind(this), - SECOND, - { - leading: true, - maxWait: SECOND, - trailing: true, - } - ); + #delayBeforeUpdatingRedux: (() => number) | undefined; + #isAppStillLoading: (() => boolean) | undefined; + // lookups + #_byE164: Record = Object.create(null); + #_byServiceId: Record = Object.create(null); + #_byPni: Record = Object.create(null); + #_byGroupId: Record = Object.create(null); + #_byId: Record = 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(); + 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 - window.Whisper.events.on('updateUnreadCount', debouncedUpdateUnreadCount); - this._conversations.on( - 'add remove change:active_at change:unreadCount change:markedUnread change:isArchived change:muteExpiresAt', - debouncedUpdateUnreadCount + window.Whisper.events.on( + 'updateUnreadCount', + this.#debouncedUpdateUnreadCount ); + } - // 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. - this._conversations.on('add', (model: ConversationModel): void => { - // Don't modify conversations in backup integration testing - if (isTestOrMockEnvironment()) { - return; + registerDelayBeforeUpdatingRedux( + delayBeforeUpdatingRedux: () => number + ): void { + this.#delayBeforeUpdatingRedux = delayBeforeUpdatingRedux; + } + registerIsAppStillLoading(isAppStillLoading: () => boolean): void { + 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 { if (!this.#_hasQueueEmptied) { return; @@ -203,7 +346,7 @@ export class ConversationController { window.storage.get('badge-count-muted-conversations') || false; const unreadStats = countAllConversationsUnreadStats( - this._conversations.map( + this.#_conversations.map( (conversation): ConversationPropsForUnreadStats => { // Need to pull this out manually into the Redux shape // because `conversation.format()` can return cached props by the @@ -251,24 +394,39 @@ export class ConversationController { 'ConversationController.get() needs complete initial fetch' ); } + if (!id) { + return undefined; + } - // This function takes null just fine. Backbone typings are too restrictive. - return this._conversations.get(id as string); + return ( + this.#_byE164[id] || + this.#_byE164[`+${id}`] || + this.#_byServiceId[id] || + this.#_byPni[id] || + this.#_byGroupId[id] || + this.#_byId[id] + ); } getAll(): Array { - return this._conversations.models; + return this.#_conversations; } dangerouslyCreateAndAdd( - attributes: Partial + attributes: ConversationAttributesType ): ConversationModel { - return this._conversations.add(attributes); + const model = new ConversationModel(attributes); + this.#addConversation(model); + return model; } dangerouslyRemoveById(id: string): void { - this._conversations.remove(id); - this._conversations.resetLookups(); + const model = this.get(id); + if (!model) { + return; + } + + this.#removeConversation(model); } getOrCreate( @@ -292,7 +450,7 @@ export class ConversationController { ); } - let conversation = this._conversations.get(identifier); + let conversation = this.get(identifier); if (conversation) { return conversation; } @@ -304,44 +462,64 @@ export class ConversationController { const id = generateUuid(); if (type === 'group') { - conversation = this._conversations.add({ + conversation = new ConversationModel({ id, serviceId: undefined, e164: undefined, groupId: identifier, type, version: 2, + expireTimerVersion: INITIAL_EXPIRE_TIMER_VERSION, + unreadCount: 0, + verified: window.textsecure.storage.protocol.VerifiedStatus.DEFAULT, + messageCount: 0, + sentMessageCount: 0, ...additionalInitialProps, }); + this.#addConversation(conversation); } else if (isServiceIdString(identifier)) { - conversation = this._conversations.add({ + conversation = new ConversationModel({ id, serviceId: identifier, e164: undefined, groupId: undefined, type, version: 2, + expireTimerVersion: INITIAL_EXPIRE_TIMER_VERSION, + unreadCount: 0, + verified: window.textsecure.storage.protocol.VerifiedStatus.DEFAULT, + messageCount: 0, + sentMessageCount: 0, ...additionalInitialProps, }); + this.#addConversation(conversation); } else { - conversation = this._conversations.add({ + conversation = new ConversationModel({ id, serviceId: undefined, e164: identifier, groupId: undefined, type, version: 2, + expireTimerVersion: INITIAL_EXPIRE_TIMER_VERSION, + unreadCount: 0, + verified: window.textsecure.storage.protocol.VerifiedStatus.DEFAULT, + messageCount: 0, + sentMessageCount: 0, ...additionalInitialProps, }); + this.#addConversation(conversation); } const create = async () => { - if (!conversation.isValid()) { - const validationError = conversation.validationError || {}; + const validationErrorString = validateConversation( + conversation.attributes + ); + if (validationErrorString) { log.error( 'Contact is not valid. Not saving, but adding to collection:', conversation.idForLogging(), - Errors.toLogFormat(validationError) + validationErrorString ); return conversation; @@ -755,7 +933,7 @@ export class ConversationController { (targetOldServiceIds.pni !== pni || (aci && targetOldServiceIds.aci !== aci)) ) { - targetConversation.unset('needsTitleTransition'); + targetConversation.set({ needsTitleTransition: undefined }); mergePromises.push( targetConversation.addPhoneNumberDiscoveryIfNeeded( 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 // 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 // conflict case, to keep the one with activity the most recently. - for (let i = models.length - 1; i >= 0; i -= 1) { - const conversation = models[i]; + for (let i = this.#_conversations.length - 1; i >= 0; i -= 1) { + const conversation = this.#_conversations[i]; assertDev( conversation, 'Expected conversation to be found in array during iteration' @@ -1090,15 +1266,14 @@ export class ConversationController { } else { activeAt = obsoleteActiveAt || currentActiveAt; } - current.set('active_at', activeAt); + current.set({ active_at: activeAt }); - current.set( - 'expireTimerVersion', - Math.max( + current.set({ + expireTimerVersion: Math.max( obsolete.get('expireTimerVersion') ?? 1, current.get('expireTimerVersion') ?? 1 - ) - ); + ), + }); const obsoleteExpireTimer = obsolete.get('expireTimer'); const currentExpireTimer = current.get('expireTimer'); @@ -1106,7 +1281,7 @@ export class ConversationController { !currentExpireTimer || (obsoleteExpireTimer && obsoleteExpireTimer < currentExpireTimer) ) { - current.set('expireTimer', obsoleteExpireTimer); + current.set({ expireTimer: obsoleteExpireTimer }); } const currentHadMessages = (current.get('messageCount') ?? 0) > 0; @@ -1136,11 +1311,11 @@ export class ConversationController { >; keys.forEach(key => { 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 if (key === 'draftAttachments') { - obsolete.set(key, undefined); + obsolete.set({ [key]: undefined }); } } }); @@ -1244,8 +1419,7 @@ export class ConversationController { log.warn( `${logId}: Eliminate old conversation from ConversationController lookups` ); - this._conversations.remove(obsolete); - this._conversations.resetLookups(); + this.#removeConversation(obsolete); current.captureChange('combineConversations'); drop(current.updateLastMessage()); @@ -1305,22 +1479,25 @@ export class ConversationController { return null; } - async getAllGroupsInvolvingServiceId( + getAllGroupsInvolvingServiceId( serviceId: ServiceIdString - ): Promise> { - const groups = await getAllGroupsInvolvingServiceId(serviceId); - return groups.map(group => { - const existing = this.get(group.id); - if (existing) { - return existing; - } + ): Array { + return this.#_conversations + .map(conversation => { + if (!isGroup(conversation.attributes)) { + return; + } + if (!conversation.hasMember(serviceId)) { + return; + } - return this._conversations.add(group); - }); + return conversation; + }) + .filter(isNotNil); } getByDerivedGroupV2Id(groupId: string): ConversationModel | undefined { - return this._conversations.find( + return this.#_conversations.find( item => item.get('derivedGroupV2Id') === groupId ); } @@ -1336,14 +1513,18 @@ export class ConversationController { } reset(): void { - delete this._initialPromise; + const { removeAllConversations } = window.reduxActions.conversations; + + this.#_initialPromise = undefined; this.#_initialFetchComplete = false; - this._conversations.reset([]); + this.#_conversations = []; + removeAllConversations(); + this.#resetLookups(); } load(): Promise { - this._initialPromise ||= this.#doLoad(); - return this._initialPromise; + this.#_initialPromise ||= this.#doLoad(); + return this.#_initialPromise; } // A number of things outside conversation.attributes affect conversation re-rendering. @@ -1354,7 +1535,7 @@ export class ConversationController { let count = 0; const conversations = identifiers ? identifiers.map(identifier => this.get(identifier)).filter(isNotNil) - : this._conversations.models.slice(); + : this.#_conversations.slice(); log.info( `forceRerender: Starting to loop through ${conversations.length} conversations` ); @@ -1366,7 +1547,7 @@ export class ConversationController { conversation.oldCachedProps = conversation.cachedProps; conversation.cachedProps = null; - conversation.trigger('props-change', conversation, false); + this.conversationUpdated(conversation, conversation.attributes); count += 1; } @@ -1426,8 +1607,10 @@ export class ConversationController { ); } - conversation.set('avatar', undefined); - conversation.set('profileAvatar', undefined); + conversation.set({ + avatar: undefined, + profileAvatar: undefined, + }); drop(updateConversation(conversation.attributes)); numberOfConversationsMigrated += 1; } @@ -1449,7 +1632,7 @@ export class ConversationController { } log.warn(`Repairing ${convo.idForLogging()}'s isPinned`); - convo.set('isPinned', true); + convo.set({ isPinned: true }); drop(updateConversation(convo.attributes)); } @@ -1469,7 +1652,7 @@ export class ConversationController { await updateConversations( sharedWith.map(c => { - c.unset('shareMyPhoneNumber'); + c.set({ shareMyPhoneNumber: undefined }); return c.attributes; }) ); @@ -1496,15 +1679,14 @@ export class ConversationController { // eslint-disable-next-line no-await-in-loop await removeConversation(convo.id); - this._conversations.remove(convo); - this._conversations.resetLookups(); + this.#removeConversation(convo); } } async #doLoad(): Promise { log.info('starting initial fetch'); - if (this._conversations.length) { + if (this.#_conversations.length) { throw new Error('ConversationController: Already loaded!'); } @@ -1540,14 +1722,16 @@ export class ConversationController { this.#_initialFetchComplete = true; // Hydrate the final set of conversations - batchDispatch(() => { - this._conversations.add( - collection.filter(conversation => !conversation.isTemporary) + + collection + .filter(conversation => !conversation.isTemporary) + .forEach(conversation => + this.#_conversations.push(new ConversationModel(conversation)) ); - }); + this.#generateLookups(); await Promise.all( - this._conversations.map(async conversation => { + this.#_conversations.map(async conversation => { try { // Hydrate contactCollection, now that initial fetch is complete conversation.fetchContacts(); @@ -1587,13 +1771,14 @@ export class ConversationController { ); log.info( 'done with initial fetch, ' + - `got ${this._conversations.length} conversations` + `got ${this.#_conversations.length} conversations` ); } catch (error) { log.error('initial fetch failed', Errors.toLogFormat(error)); throw error; } } + async archiveSessionsForConversation( conversationId: string | undefined ): Promise { @@ -1635,4 +1820,203 @@ export class ConversationController { 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); + } } diff --git a/ts/SignalProtocolStore.ts b/ts/SignalProtocolStore.ts index 11048b43784..7436955009f 100644 --- a/ts/SignalProtocolStore.ts +++ b/ts/SignalProtocolStore.ts @@ -2621,11 +2621,13 @@ export class SignalProtocolStore extends EventEmitter { async removeAllConfiguration(): Promise { // Conversations. These properties are not present in redux. - window.getConversations().forEach(conversation => { - conversation.unset('storageID'); - conversation.unset('needsStorageServiceSync'); - conversation.unset('storageUnknownFields'); - conversation.unset('senderKeyInfo'); + window.ConversationController.getAll().forEach(conversation => { + conversation.set({ + storageID: undefined, + needsStorageServiceSync: undefined, + storageUnknownFields: undefined, + senderKeyInfo: undefined, + }); }); await DataWriter.removeAllConfiguration(); diff --git a/ts/backbone/reliable_trigger.ts b/ts/backbone/reliable_trigger.ts deleted file mode 100644 index 2ddc1008100..00000000000 --- a/ts/backbone/reliable_trigger.ts +++ /dev/null @@ -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; - 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, - rest: ReadonlyArray -) { - 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, - name: string, - args: Array -) { - 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>; - }, ->(this: T, name: string, ...args: Array): 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 }); -}); diff --git a/ts/background.ts b/ts/background.ts index 71113a8aedc..d00496f9e70 100644 --- a/ts/background.ts +++ b/ts/background.ts @@ -1,12 +1,11 @@ // Copyright 2020 Signal Messenger, LLC // 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 PQueue from 'p-queue'; import pMap from 'p-map'; import { v7 as generateUuid } from 'uuid'; -import { batch as batchDispatch } from 'react-redux'; import * as Registration from './util/registration'; import MessageReceiver from './textsecure/MessageReceiver'; @@ -25,8 +24,6 @@ import * as Bytes from './Bytes'; import * as Timers from './Timers'; import * as indexedDb from './indexeddb'; import type { MenuOptionsType } from './types/menu'; -import type { Receipt } from './types/Receipt'; -import { ReceiptType } from './types/Receipt'; import { SocketStatus } from './types/SocketStatus'; import { DEFAULT_CONVERSATION_COLOR } from './types/Colors'; import { ThemeType } from './types/Util'; @@ -153,10 +150,7 @@ import { deleteAllLogs } from './util/deleteAllLogs'; import { startInteractionMode } from './services/InteractionMode'; import { ReactionSource } from './reactions/ReactionSource'; import { singleProtoJobQueue } from './jobs/singleProtoJobQueue'; -import { - conversationJobQueue, - conversationQueueJobEnum, -} from './jobs/conversationJobQueue'; +import { conversationJobQueue } from './jobs/conversationJobQueue'; import { SeenStatus } from './MessageSeenStatus'; import MessageSender from './textsecure/SendMessage'; import type AccountManager from './textsecure/AccountManager'; @@ -304,29 +298,7 @@ export async function startApp(): Promise { const onRetryRequestQueue = new PQueue({ concurrency: 1 }); onRetryRequestQueue.pause(); - window.Whisper.deliveryReceiptQueue = new PQueue({ - concurrency: 1, - timeout: durations.MINUTE * 30, - }); window.Whisper.deliveryReceiptQueue.pause(); - window.Whisper.deliveryReceiptBatcher = createBatcher({ - 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') { window.addEventListener('dblclick', (event: Event) => { @@ -441,7 +413,7 @@ export async function startApp(): Promise { }); accountManager.addEventListener('endRegistration', () => { - window.Whisper.events.trigger('userChanged', false); + window.Whisper.events.emit('userChanged', false); drop(window.storage.put('postRegistrationSyncsStatus', 'incomplete')); registrationCompleted?.resolve(); @@ -596,6 +568,23 @@ export async function startApp(): Promise { storage: window.storage, 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( handler: (event: E) => Promise | void @@ -1215,114 +1204,6 @@ export async function startApp(): Promise { function setupAppState() { 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(); - 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) => { const newDeviceId = window.textsecure.storage.user.getDeviceId(); const newNumber = window.textsecure.storage.user.getNumber(); @@ -1332,7 +1213,7 @@ export async function startApp(): Promise { window.ConversationController.getOurConversation(); if (ourConversation?.get('e164') !== newNumber) { - ourConversation?.set('e164', newNumber); + ourConversation?.set({ e164: newNumber }); } window.reduxActions.user.userChanged({ @@ -1566,7 +1447,7 @@ export async function startApp(): Promise { window.IPC.setMenuBarVisibility(!hideMenuBar); startTimeTravelDetector(() => { - window.Whisper.events.trigger('timetravel'); + window.Whisper.events.emit('timetravel'); }); updateExpiringMessagesService(); @@ -3145,7 +3026,7 @@ export async function startApp(): Promise { } async function unlinkAndDisconnect(): Promise { - window.Whisper.events.trigger('unauthorized'); + window.Whisper.events.emit('unauthorized'); log.warn( 'unlinkAndDisconnect: Client is no longer authorized; ' + @@ -3192,7 +3073,7 @@ export async function startApp(): Promise { const ourConversation = window.ConversationController.getOurConversation(); if (ourConversation) { - ourConversation.unset('username'); + ourConversation.set({ username: undefined }); await DataWriter.updateConversation(ourConversation.attributes); } diff --git a/ts/components/CallsList.tsx b/ts/components/CallsList.tsx index 5f7976dafe2..f277bae69be 100644 --- a/ts/components/CallsList.tsx +++ b/ts/components/CallsList.tsx @@ -554,6 +554,9 @@ export function CallsList({ }; let timer = setTimeout(() => { + if (controller.signal.aborted) { + return; + } setSearchState(prevSearchState => { if (prevSearchState.state === 'init') { return defaultPendingState; @@ -561,6 +564,10 @@ export function CallsList({ return prevSearchState; }); timer = setTimeout(() => { + if (controller.signal.aborted) { + return; + } + // Show loading indicator after a delay setSearchState(defaultPendingState); }, 300); diff --git a/ts/components/conversation/Timeline.tsx b/ts/components/conversation/Timeline.tsx index 0d92fa1e669..db2b99fba91 100644 --- a/ts/components/conversation/Timeline.tsx +++ b/ts/components/conversation/Timeline.tsx @@ -140,7 +140,7 @@ type PropsHousekeepingType = { }; export type PropsActionsType = { - // From Backbone + // From Model acknowledgeGroupMemberNameCollisions: ( conversationId: string, groupNameCollisions: ReadonlyDeep diff --git a/ts/groups.ts b/ts/groups.ts index c6ea6e9a99b..3cfaf320828 100644 --- a/ts/groups.ts +++ b/ts/groups.ts @@ -3304,7 +3304,11 @@ async function updateGroup( }); if (idChanged) { - conversation.trigger('idUpdated', conversation, 'groupId', previousId); + window.ConversationController.idUpdated( + conversation, + 'groupId', + previousId + ); } // Save these most recent updates to conversation diff --git a/ts/messageModifiers/DeletesForMe.ts b/ts/messageModifiers/DeletesForMe.ts index 087f041151b..3e0d9a388ef 100644 --- a/ts/messageModifiers/DeletesForMe.ts +++ b/ts/messageModifiers/DeletesForMe.ts @@ -104,7 +104,7 @@ export async function onDelete(item: DeleteForMeAttributesType): Promise { let result: boolean; 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. result = await deleteAttachmentFromMessage( conversation.id, diff --git a/ts/messageModifiers/MessageReceipts.ts b/ts/messageModifiers/MessageReceipts.ts index 49dcb138dae..faeee892b60 100644 --- a/ts/messageModifiers/MessageReceipts.ts +++ b/ts/messageModifiers/MessageReceipts.ts @@ -297,7 +297,7 @@ const deleteSentProtoBatcher = createWaitBatcher({ // `deleteSentProtoRecipient` has already updated the database so there // is no need in calling `updateConversation` - convo.unset('shareMyPhoneNumber'); + convo.set({ shareMyPhoneNumber: undefined }); } }, }); diff --git a/ts/messages/saveAndNotify.ts b/ts/messages/saveAndNotify.ts index c1701feff69..cecee2576f2 100644 --- a/ts/messages/saveAndNotify.ts +++ b/ts/messages/saveAndNotify.ts @@ -57,7 +57,7 @@ export async function saveAndNotify( conversation.incrementSentMessageCount(); } - window.Whisper.events.trigger('incrementProgress'); + window.Whisper.events.emit('incrementProgress'); confirm(); if (!isStory(message.attributes)) { diff --git a/ts/model-types.d.ts b/ts/model-types.d.ts index 3c4f20ba7d6..9adac64d9bc 100644 --- a/ts/model-types.d.ts +++ b/ts/model-types.d.ts @@ -1,14 +1,12 @@ // Copyright 2020 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -import * as Backbone from 'backbone'; import type { ReadonlyDeep } from 'type-fest'; import type { GroupV2ChangeType } from './groups'; import type { DraftBodyRanges, RawBodyRange } from './types/BodyRange'; import type { CustomColorType, ConversationColorType } from './types/Colors'; import type { SendMessageChallengeData } from './textsecure/Errors'; -import type { ConversationModel } from './models/conversations'; import type { ProfileNameChangeType } from './util/getStringForProfileChange'; import type { CapabilitiesType } from './textsecure/WebAPI'; import type { ReadStatus } from './messages/MessageReadStatus'; @@ -486,7 +484,7 @@ export type ConversationAttributesType = { groupInviteLinkPassword?: string; previousGroupV1Id?: string; previousGroupV1Members?: Array; - acknowledgedGroupNameCollisions?: GroupNameCollisionsWithIdsByTitle; + acknowledgedGroupNameCollisions?: ReadonlyDeep; // Used only when user is waiting for approval to join via link isTemporary?: boolean; @@ -561,7 +559,3 @@ export type ShallowChallengeError = CustomError & { readonly retryAfter: number; readonly data: SendMessageChallengeData; }; - -export declare class ConversationModelCollectionType extends Backbone.Collection { - resetLookups(): void; -} diff --git a/ts/models/conversations.ts b/ts/models/conversations.ts index cc14618adfb..f93ccd5238c 100644 --- a/ts/models/conversations.ts +++ b/ts/models/conversations.ts @@ -1,8 +1,7 @@ // Copyright 2020 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -import { compact, has, isNumber, throttle, debounce } from 'lodash'; -import { batch as batchDispatch } from 'react-redux'; +import { compact, isNumber, throttle, debounce } from 'lodash'; import { v4 as generateGuid } from 'uuid'; import PQueue from 'p-queue'; @@ -193,22 +192,10 @@ import { getTypingIndicatorSetting } from '../types/Util'; import { INITIAL_EXPIRE_TIMER_VERSION } from '../util/expirationTimer'; import { maybeNotify } from '../messages/maybeNotify'; import { missingCaseError } from '../util/missingCaseError'; +import * as Message from '../types/Message2'; 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 { getConversationRangeCenteredOnMessage, getOlderMessagesByConversation, @@ -228,15 +215,6 @@ const SEND_REPORTING_THRESHOLD_MS = 25; 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; type CachedIdenticon = { @@ -245,11 +223,13 @@ type CachedIdenticon = { readonly path?: string; readonly url: string; }; +type StringKey = keyof T & string; -export class ConversationModel extends window.Backbone - .Model { +export class ConversationModel { static COLORS: string; + #_attributes: ConversationAttributesType; + cachedProps?: ConversationType | null; oldCachedProps?: ConversationType | null; @@ -263,7 +243,7 @@ export class ConversationModel extends window.Backbone } >; - contactCollection?: Backbone.Collection; + contactCollection?: Array; debouncedUpdateLastMessage: (() => void) & { flush(): void }; @@ -305,19 +285,70 @@ export class ConversationModel extends window.Backbone #lastIsTyping?: boolean; #muteTimer?: NodeJS.Timeout; - #isInReduxBatch = false; #privVerifiedEnum?: typeof window.textsecure.storage.protocol.VerifiedStatus; #isShuttingDown = false; #savePromises = new Set>(); - override defaults(): Partial { - return { - unreadCount: 0, - verified: window.textsecure.storage.protocol.VerifiedStatus.DEFAULT, - messageCount: 0, - sentMessageCount: 0, - expireTimerVersion: INITIAL_EXPIRE_TIMER_VERSION, + public get id(): string { + return this.#_attributes.id; + } + + public get>( + key: keyName + ): ConversationAttributesType[keyName] { + return this.attributes[key]; + } + public set( + attributes: Partial, + { 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 = [ + '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 { + return this.#_attributes; } idForLogging(): string { @@ -328,20 +359,8 @@ export class ConversationModel extends window.Backbone return getSendTarget(this.attributes); } - getContactCollection(): Backbone.Collection { - const collection = new window.Backbone.Collection(); - 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) { - super(attributes); + this.#_attributes = attributes; // Note that we intentionally don't use `initialize()` method because it // isn't compatible with esnext output of esbuild. @@ -354,7 +373,7 @@ export class ConversationModel extends window.Backbone 'ConversationModel.initialize: normalizing serviceId from ' + `${serviceId} to ${normalizedServiceId}` ); - this.set('serviceId', normalizedServiceId); + this.set({ serviceId: normalizedServiceId }); } if (isValidE164(attributes.id, false)) { @@ -374,71 +393,35 @@ export class ConversationModel extends window.Backbone 200 ); - this.contactCollection = this.getContactCollection(); - 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() - ); + this.contactCollection = []; const sealedSender = this.get('sealedSender'); if (sealedSender === undefined) { 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); - this.on('change:active_at', this.#onActiveAtChange); + if ( + // @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.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 // conversation for the first time. this.isFetchingUUID = this.isSMSOnly(); @@ -468,7 +451,7 @@ export class ConversationModel extends window.Backbone const migratedColor = this.getColor(); 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 // 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 @@ -942,8 +925,7 @@ export class ConversationModel extends window.Backbone } if (blocked && !wasBlocked) { - // 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) { this.captureChange('block'); @@ -975,7 +957,7 @@ export class ConversationModel extends window.Backbone if (unblocked && wasBlocked) { // 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) { this.captureChange('unblock'); @@ -1213,7 +1195,7 @@ export class ConversationModel extends window.Backbone ); this.isFetchingUUID = true; - this.trigger('change', this, { force: true }); + window.ConversationController.conversationUpdated(this, this.attributes); try { // Attempt to fetch UUID @@ -1225,7 +1207,7 @@ export class ConversationModel extends window.Backbone } finally { // No redux update here this.isFetchingUUID = false; - this.trigger('change', this, { force: true }); + window.ConversationController.conversationUpdated(this, this.attributes); log.info( `Done fetching uuid for a sms-only conversation ${this.idForLogging()}` @@ -1240,14 +1222,6 @@ export class ConversationModel extends window.Backbone this.setRegistered(); } - override isValid(): boolean { - return ( - isDirectConversation(this.attributes) || - isGroupV1(this.attributes) || - isGroupV2(this.attributes) - ); - } - async maybeMigrateV1Group(): Promise { if (!isGroupV1(this.attributes)) { return; @@ -2078,7 +2052,7 @@ export class ConversationModel extends window.Backbone return; } - this.set('e164', e164 || undefined); + this.set({ e164: e164 || undefined }); // This user changed their phone number if (oldValue && e164 && this.get('sharingPhoneNumber')) { @@ -2086,7 +2060,7 @@ export class ConversationModel extends window.Backbone } drop(DataWriter.updateConversation(this.attributes)); - this.trigger('idUpdated', this, 'e164', oldValue); + window.ConversationController.idUpdated(this, 'e164', oldValue); this.captureChange('updateE164'); } @@ -2096,14 +2070,13 @@ export class ConversationModel extends window.Backbone return; } - this.set( - 'serviceId', - serviceId + this.set({ + serviceId: serviceId ? normalizeServiceId(serviceId, 'Conversation.updateServiceId') - : undefined - ); + : undefined, + }); 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 // 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; } - this.set( - 'pni', - pni ? normalizePni(pni, 'Conversation.updatePni') : undefined - ); + this.set({ + pni: pni ? normalizePni(pni, 'Conversation.updatePni') : undefined, + }); const newPniSignatureVerified = pni ? pniSignatureVerified : false; if (this.get('pniSignatureVerified') !== newPniSignatureVerified) { log.warn( `updatePni/${this.idForLogging()}: setting ` + `pniSignatureVerified to ${newPniSignatureVerified}` ); - this.set('pniSignatureVerified', newPniSignatureVerified); + this.set({ pniSignatureVerified: newPniSignatureVerified }); this.captureChange('pniSignatureVerified'); } @@ -2211,16 +2183,16 @@ export class ConversationModel extends window.Backbone } drop(DataWriter.updateConversation(this.attributes)); - this.trigger('idUpdated', this, 'pni', oldValue); + window.ConversationController.idUpdated(this, 'pni', oldValue); this.captureChange('updatePni'); } updateGroupId(groupId?: string): void { const oldValue = this.get('groupId'); if (groupId && groupId !== oldValue) { - this.set('groupId', groupId); + this.set({ groupId }); 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; } - this.set('reportingToken', newValue); + this.set({ reportingToken: newValue }); await DataWriter.updateConversation(this.attributes); } @@ -3021,7 +2993,7 @@ export class ConversationModel extends window.Backbone return false; } - if (contacts.length === 1 && isMe(contacts.first()?.attributes)) { + if (contacts.length === 1 && isMe(contacts[0]?.attributes)) { return false; } @@ -3158,9 +3130,7 @@ export class ConversationModel extends window.Backbone onMemberVerifiedChange(): void { // 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() - this.trigger('change:verified', this); - this.trigger('change', this, { force: true }); + window.ConversationController.conversationUpdated(this, this.attributes); } async toggleVerified(): Promise { @@ -3527,7 +3497,7 @@ export class ConversationModel extends window.Backbone const notificationId = await this.addNotification( 'universal-timer-notification' ); - this.set('pendingUniversalTimer', notificationId); + this.set({ pendingUniversalTimer: notificationId }); } async maybeApplyUniversalTimer(): Promise { @@ -3560,7 +3530,7 @@ export class ConversationModel extends window.Backbone return false; } - this.set('pendingUniversalTimer', undefined); + this.set({ pendingUniversalTimer: undefined }); log.info( `maybeRemoveUniversalTimer(${this.idForLogging()}): removed notification` ); @@ -3593,7 +3563,7 @@ export class ConversationModel extends window.Backbone const notificationId = await this.addNotification( 'contact-removed-notification' ); - this.set('pendingRemovedContactNotification', notificationId); + this.set({ pendingRemovedContactNotification: notificationId }); await DataWriter.updateConversation(this.attributes); } @@ -3603,7 +3573,7 @@ export class ConversationModel extends window.Backbone return false; } - this.set('pendingRemovedContactNotification', undefined); + this.set({ pendingRemovedContactNotification: undefined }); log.info( `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( name: string, callback: (abortSignal: AbortSignal) => Promise @@ -3838,6 +3804,8 @@ export class ConversationModel extends window.Backbone } async sendStickerMessage(packId: string, stickerId: number): Promise { + const { readStickerData } = window.Signal.Migrations; + const packData = Stickers.getStickerPack(packId); const stickerData = Stickers.getSticker(packId, stickerId); 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({ message, dontAddMessage, @@ -3952,57 +3908,53 @@ export class ConversationModel extends window.Backbone now: number; extraReduxActions?: () => void; }): void { - this.batchReduxChanges(() => { - const { clearUnreadMetrics } = window.reduxActions.conversations; - clearUnreadMetrics(this.id); + const { clearUnreadMetrics } = window.reduxActions.conversations; + clearUnreadMetrics(this.id); - const enabledProfileSharing = Boolean(!this.get('profileSharing')); - const unarchivedConversation = Boolean(this.get('isArchived')); + const enabledProfileSharing = Boolean(!this.get('profileSharing')); + const unarchivedConversation = Boolean(this.get('isArchived')); - log.info( - `beforeMessageSend(${this.idForLogging()}): ` + - `clearDraft(${!dontClearDraft}) addMessage(${!dontAddMessage})` - ); + log.info( + `beforeMessageSend(${this.idForLogging()}): ` + + `clearDraft(${!dontClearDraft}) addMessage(${!dontAddMessage})` + ); - if (!dontAddMessage) { - this.#doAddSingleMessage(message, { isJustSent: true }); - } + if (!dontAddMessage) { + this.#doAddSingleMessage(message, { isJustSent: true }); + } - const draftProperties = dontClearDraft - ? {} - : { - draft: '', - draftEditMessage: undefined, - draftBodyRanges: [], - draftTimestamp: null, - quotedMessageId: undefined, - }; - const lastMessageProperties = this.getLastMessageData(message, message); - const isEditMessage = Boolean(message.editHistory); + const draftProperties = dontClearDraft + ? {} + : { + draft: '', + draftEditMessage: undefined, + draftBodyRanges: [], + draftTimestamp: null, + quotedMessageId: undefined, + }; + const lastMessageProperties = this.getLastMessageData(message, message); + const isEditMessage = Boolean(message.editHistory); - this.set({ - ...draftProperties, - ...lastMessageProperties, - ...(enabledProfileSharing ? { profileSharing: true } : {}), - ...(dontAddMessage - ? {} - : this.incrementSentMessageCount({ dry: true })), - // If it's an edit message we don't want to optimistically set the - // active_at & timestamp to now. We want it to stay the same. - active_at: isEditMessage ? this.get('active_at') : now, - timestamp: isEditMessage ? this.get('timestamp') : now, - ...(unarchivedConversation ? { isArchived: false } : {}), - }); - - if (enabledProfileSharing) { - this.captureChange('beforeMessageSend/mandatoryProfileSharing'); - } - if (unarchivedConversation) { - this.captureChange('beforeMessageSend/unarchive'); - } - - extraReduxActions?.(); + this.set({ + ...draftProperties, + ...lastMessageProperties, + ...(enabledProfileSharing ? { profileSharing: true } : {}), + ...(dontAddMessage ? {} : this.incrementSentMessageCount({ dry: true })), + // If it's an edit message we don't want to optimistically set the + // active_at & timestamp to now. We want it to stay the same. + active_at: isEditMessage ? this.get('active_at') : now, + timestamp: isEditMessage ? this.get('timestamp') : now, + ...(unarchivedConversation ? { isArchived: false } : {}), }); + + if (enabledProfileSharing) { + this.captureChange('beforeMessageSend/mandatoryProfileSharing'); + } + if (unarchivedConversation) { + this.captureChange('beforeMessageSend/unarchive'); + } + + extraReduxActions?.(); } async enqueueMessageForSend( @@ -4037,6 +3989,9 @@ export class ConversationModel extends window.Backbone extraReduxActions?: () => void; } = {} ): Promise { + const { deleteAttachmentData, upgradeMessageSchema } = + window.Signal.Migrations; + if (this.isGroupV1AndDisabled()) { return; } @@ -4266,7 +4221,7 @@ export class ConversationModel extends window.Backbone log.info(`maybeClearUsername(${this.idForLogging()}): clearing username`); - this.unset('username'); + this.set({ username: undefined }); if (this.get('needsTitleTransition') && getProfileName(this.attributes)) { log.info( @@ -4274,7 +4229,7 @@ export class ConversationModel extends window.Backbone ); const { type, e164, username } = this.attributes; - this.unset('needsTitleTransition'); + this.set({ needsTitleTransition: undefined }); await this.addNotification('title-transition-notification', { readStatus: ReadStatus.Read, @@ -4310,7 +4265,7 @@ export class ConversationModel extends window.Backbone log.info(`updateUsername(${this.idForLogging()}): updating username`); - this.set('username', username); + this.set({ username }); this.captureChange('updateUsername'); if (shouldSave) { @@ -4469,7 +4424,7 @@ export class ConversationModel extends window.Backbone async #onActiveAtChange(): Promise { if (this.get('active_at') && this.get('messagesDeleted')) { - this.set('messagesDeleted', false); + this.set({ messagesDeleted: false }); await DataWriter.updateConversation(this.attributes); } } @@ -4713,7 +4668,7 @@ export class ConversationModel extends window.Backbone 'updateExpirationTimer: Resetting expireTimerVersion since this is initialSync' ); // 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; @@ -5006,6 +4961,12 @@ export class ConversationModel extends window.Backbone decryptionKey?: Uint8Array | null | undefined; forceFetch?: boolean; }): Promise { + const { + deleteAttachmentData, + doesAttachmentExist, + writeNewAttachmentData, + } = window.Signal.Migrations; + const { avatarUrl, decryptionKey, forceFetch } = options; if (isMe(this.attributes)) { if (avatarUrl) { @@ -5106,7 +5067,7 @@ export class ConversationModel extends window.Backbone const { type, e164, username } = this.attributes; - this.unset('needsTitleTransition'); + this.set({ needsTitleTransition: undefined }); await this.addNotification('title-transition-notification', { readStatus: ReadStatus.Read, @@ -5122,7 +5083,7 @@ export class ConversationModel extends window.Backbone } // 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. // 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 { - const members = this.getMembers(); - - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - this.contactCollection!.reset(members); + this.contactCollection = this.getMembers(); } async destroyMessages({ @@ -5423,7 +5381,7 @@ export class ConversationModel extends window.Backbone } const newVersion = expireTimerVersion + 1; - this.set('expireTimerVersion', newVersion); + this.set({ expireTimerVersion: newVersion }); await DataWriter.updateConversation(this.attributes); } @@ -5511,6 +5469,8 @@ export class ConversationModel extends window.Backbone url: string; absolutePath?: string; }> { + const { getAbsoluteTempPath } = window.Signal.Migrations; + const saveToDisk = shouldSaveNotificationAvatarToDisk(); const avatarUrl = getLocalAvatarUrl(this.attributes); if (avatarUrl) { @@ -5532,6 +5492,13 @@ export class ConversationModel extends window.Backbone } async #getTemporaryAvatarPath(): Promise { + const { + copyIntoTempDirectory, + deleteAttachmentData, + getAbsoluteAttachmentPath, + getAbsoluteTempPath, + } = window.Signal.Migrations; + const avatar = getAvatar(this.attributes); if (avatar?.path == null) { return undefined; @@ -5672,13 +5639,19 @@ export class ConversationModel extends window.Backbone ); // User was not previously typing before. State change! if (!record) { - this.trigger('change', this, { force: true }); + window.ConversationController.conversationUpdated( + this, + this.attributes + ); } } else { delete this.contactTypingTimers[typingToken]; if (record) { // 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]; // 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; } - const validationError = this.validate(); - if (validationError) { + const validationErrorString = validateConversation(this.attributes); + if (validationErrorString) { log.error( `not pinning ${this.idForLogging()} because of ` + - `validation error ${validationError}` + `validation error ${validationErrorString}` ); return; } @@ -5719,7 +5692,7 @@ export class ConversationModel extends window.Backbone this.writePinnedConversations([...pinnedConversationIds]); - this.set('isPinned', true); + this.set({ isPinned: true }); if (this.get('isArchived')) { this.set({ isArchived: false }); @@ -5742,7 +5715,7 @@ export class ConversationModel extends window.Backbone this.writePinnedConversations([...pinnedConversationIds]); - this.set('isPinned', false); + this.set({ isPinned: false }); drop(DataWriter.updateConversation(this.attributes)); } @@ -5771,7 +5744,7 @@ export class ConversationModel extends window.Backbone acknowledgeGroupMemberNameCollisions( groupNameCollisions: ReadonlyDeep ): void { - this.set('acknowledgedGroupNameCollisions', groupNameCollisions); + this.set({ acknowledgedGroupNameCollisions: groupNameCollisions }); drop(DataWriter.updateConversation(this.attributes)); } @@ -5851,176 +5824,3 @@ export class ConversationModel extends window.Backbone 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, options?: Backbone.Silenceable) { - window.Backbone.Collection.prototype.reset.call(this, models, options); - this.resetLookups(); - }, - - resetLookups() { - this.eraseLookups(); - this.generateLookups(this.models); - }, - - generateLookups(models: ReadonlyArray) { - 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 - | Array - ) { - let hydratedData: Array | 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); - }, -}); diff --git a/ts/reactions/enqueueReactionForSend.ts b/ts/reactions/enqueueReactionForSend.ts index ebff115e3e8..cc7f42382f5 100644 --- a/ts/reactions/enqueueReactionForSend.ts +++ b/ts/reactions/enqueueReactionForSend.ts @@ -73,7 +73,7 @@ export async function enqueueReactionForSend({ ) { log.info('Enabling profile sharing for reaction send'); if (!messageConversation.get('profileSharing')) { - messageConversation.set('profileSharing', true); + messageConversation.set({ profileSharing: true }); await DataWriter.updateConversation(messageConversation.attributes); } await messageConversation.restoreContact(); diff --git a/ts/services/backups/import.ts b/ts/services/backups/import.ts index e3c00406fd4..84ee30b4075 100644 --- a/ts/services/backups/import.ts +++ b/ts/services/backups/import.ts @@ -703,8 +703,9 @@ export class BackupImportStream extends Writable { svrPin, }: Backups.IAccountData): Promise { strictAssert(this.#ourConversation === undefined, 'Duplicate AccountData'); - const me = - window.ConversationController.getOurConversationOrThrow().attributes; + const me = { + ...window.ConversationController.getOurConversationOrThrow().attributes, + }; this.#ourConversation = me; const { storage } = window; diff --git a/ts/services/contactSync.ts b/ts/services/contactSync.ts index 6e876b702b2..ca3f13f2174 100644 --- a/ts/services/contactSync.ts +++ b/ts/services/contactSync.ts @@ -97,7 +97,7 @@ async function updateConversationFromContactSync( ); } - window.Whisper.events.trigger('incrementProgress'); + window.Whisper.events.emit('incrementProgress'); } const queue = new PQueue({ concurrency: 1 }); @@ -182,11 +182,11 @@ async function doContactSync({ type: 'private', }; - const validationError = validateConversation(partialConversation); - if (validationError) { + const validationErrorString = validateConversation(partialConversation); + if (validationErrorString) { log.error( `${logId}: Invalid contact received`, - Errors.toLogFormat(validationError) + Errors.toLogFormat(validationErrorString) ); continue; } @@ -261,7 +261,7 @@ async function doContactSync({ await Promise.all(promises); await window.storage.put('synced_at', Date.now()); - window.Whisper.events.trigger('contactSync:complete'); + window.Whisper.events.emit('contactSync:complete'); if (isInitialSync) { isInitialSync = false; } diff --git a/ts/services/profiles.ts b/ts/services/profiles.ts index 36d119a461f..11c2de204d5 100644 --- a/ts/services/profiles.ts +++ b/ts/services/profiles.ts @@ -553,7 +553,7 @@ async function doGetProfile( // Record that the accessKey we have in the conversation is invalid const sealedSender = c.get('sealedSender'); 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. @@ -580,7 +580,7 @@ async function doGetProfile( if (error.code === 404) { log.info(`${logId}: Profile not found`); - c.set('profileLastFetchedAt', Date.now()); + c.set({ profileLastFetchedAt: Date.now() }); if (!isVersioned || ignoreProfileKey) { log.info(`${logId}: Marking conversation unregistered`); @@ -655,20 +655,20 @@ async function doGetProfile( if (isFieldDefined(profile.about)) { if (updatedDecryptionKey != null) { const decrypted = decryptField(profile.about, updatedDecryptionKey); - c.set('about', formatTextField(decrypted)); + c.set({ about: formatTextField(decrypted) }); } } else { - c.unset('about'); + c.set({ about: undefined }); } // Step #: Save profile `aboutEmoji` to conversation if (isFieldDefined(profile.aboutEmoji)) { if (updatedDecryptionKey != null) { const decrypted = decryptField(profile.aboutEmoji, updatedDecryptionKey); - c.set('aboutEmoji', formatTextField(decrypted)); + c.set({ aboutEmoji: formatTextField(decrypted) }); } } else { - c.unset('aboutEmoji'); + c.set({ aboutEmoji: undefined }); } // Step #: Save profile `phoneNumberSharing` to conversation @@ -681,10 +681,10 @@ async function doGetProfile( // It should be one byte, but be conservative about it and // set `sharingPhoneNumber` to `false` in all cases except [0x01]. const sharingPhoneNumber = decrypted.length === 1 && decrypted[0] === 1; - c.set('sharingPhoneNumber', sharingPhoneNumber); + c.set({ sharingPhoneNumber }); } } else { - c.unset('sharingPhoneNumber'); + c.set({ sharingPhoneNumber: undefined }); } // Step #: Save our own `paymentAddress` to Storage @@ -697,7 +697,7 @@ async function doGetProfile( if (profile.capabilities != null) { c.set({ capabilities: profile.capabilities }); } else { - c.unset('capabilities'); + c.set({ capabilities: undefined }); } // Step #: Save our own `observedCapabilities` to Storage and trigger sync if changed @@ -752,7 +752,7 @@ async function doGetProfile( })), }); } else { - c.unset('badges'); + c.set({ badges: undefined }); } // Step #: Save updated (or clear if missing) profile `credential` to conversation @@ -771,7 +771,7 @@ async function doGetProfile( log.warn( `${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 if ( diff --git a/ts/services/storage.ts b/ts/services/storage.ts index f0db9865496..b6b2bb0c404 100644 --- a/ts/services/storage.ts +++ b/ts/services/storage.ts @@ -85,6 +85,7 @@ import { fromPniUuidBytesOrUntaggedString } from '../util/ServiceId'; import { isDone as isRegistrationDone } from '../util/registration'; import { callLinkRefreshJobQueue } from '../jobs/callLinkRefreshJobQueue'; import { isMockEnvironment } from '../environment'; +import { validateConversation } from '../util/validateConversation'; 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) { - const conversation = conversations.models[i]; + const conversation = conversations[i]; let identifierType; let storageRecord; @@ -267,10 +268,12 @@ async function generateManifest( let shouldDrop = false; let dropReason: string | undefined; - const validationError = conversation.validate(); - if (validationError) { + const validationErrorString = validateConversation( + conversation.attributes + ); + if (validationErrorString) { shouldDrop = true; - dropReason = `local validation error=${validationError}`; + dropReason = `local validation error=${validationErrorString}`; } else if (conversation.isUnregisteredAndStale()) { shouldDrop = true; dropReason = 'unregistered and stale'; @@ -294,7 +297,7 @@ async function generateManifest( `dropping contact=${recordID} ` + `due to ${dropReason}` ); - conversation.unset('storageID'); + conversation.set({ storageID: undefined }); deleteKeys.add(droppedID); continue; } @@ -1267,7 +1270,7 @@ async function processManifest( const localVersions = new Map(); let localRecordCount = 0; - const conversations = window.getConversations(); + const conversations = window.ConversationController.getAll(); conversations.forEach((conversation: ConversationModel) => { const storageID = conversation.get('storageID'); if (storageID) { @@ -1387,44 +1390,45 @@ async function processManifest( // new storageID for that record, and upload. // This might happen if a device pushes a manifest which doesn't contain // the keys that we have in our local database. - window.getConversations().forEach((conversation: ConversationModel) => { - const storageID = conversation.get('storageID'); - if (storageID && !remoteKeys.has(storageID)) { - const storageVersion = conversation.get('storageVersion'); - const missingKey = redactStorageID( - storageID, - 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' + window.ConversationController.getAll().forEach( + (conversation: ConversationModel) => { + const storageID = conversation.get('storageID'); + if (storageID && !remoteKeys.has(storageID)) { + const storageVersion = conversation.get('storageVersion'); + const missingKey = redactStorageID( + storageID, + storageVersion, + conversation ); - conversation.setUnregistered({ - timestamp: Date.now() - getMessageQueueTime(), - fromStorageService: true, - // Saving below - shouldSave: false, - }); - } else { - log.info( - `process(${version}): localKey=${missingKey} ` + - 'was not in remote manifest' - ); + // 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 + 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 { @@ -2192,10 +2196,12 @@ export async function eraseAllStorageServiceState({ window.reduxActions.user.eraseStorageServiceState(); // Conversations. These properties are not present in redux. - window.getConversations().forEach(conversation => { - conversation.unset('storageID'); - conversation.unset('needsStorageServiceSync'); - conversation.unset('storageUnknownFields'); + window.ConversationController.getAll().forEach(conversation => { + conversation.set({ + storageID: undefined, + needsStorageServiceSync: undefined, + storageUnknownFields: undefined, + }); }); // Then make sure outstanding conversation saves are flushed @@ -2290,7 +2296,7 @@ export const runStorageServiceSyncJob = debounce( await sync({ reason }); // Notify listeners about sync completion - window.Whisper.events.trigger('storageService:syncComplete'); + window.Whisper.events.emit('storageService:syncComplete'); }, `sync v${window.storage.get('manifestVersion')}` ) diff --git a/ts/services/storageRecordOps.ts b/ts/services/storageRecordOps.ts index 69c2438b3af..df3023b31df 100644 --- a/ts/services/storageRecordOps.ts +++ b/ts/services/storageRecordOps.ts @@ -212,7 +212,7 @@ function addUnknownFields( // If the record doesn't have unknown fields attached but we have them // saved locally then we need to clear it out details.push('clearing unknown fields'); - conversation.unset('storageUnknownFields'); + conversation.set({ storageUnknownFields: undefined }); } } @@ -1487,9 +1487,10 @@ export async function mergeAccountRecord( } if (pinnedConversations) { - const modelPinnedConversations = window - .getConversations() - .filter(convo => Boolean(convo.get('isPinned'))); + const modelPinnedConversations = + window.ConversationController.getAll().filter(convo => + Boolean(convo.get('isPinned')) + ); const modelPinnedConversationIds = modelPinnedConversations.map(convo => convo.get('id') diff --git a/ts/services/username.ts b/ts/services/username.ts index 511fdc78aa3..e83f238bff2 100644 --- a/ts/services/username.ts +++ b/ts/services/username.ts @@ -210,7 +210,7 @@ async function updateUsernameAndSyncProfile( ): Promise { 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); try { diff --git a/ts/services/writeProfile.ts b/ts/services/writeProfile.ts index 020f10dc214..4cae4668061 100644 --- a/ts/services/writeProfile.ts +++ b/ts/services/writeProfile.ts @@ -135,7 +135,7 @@ export async function writeProfile( maybeProfileAvatarUpdate = { profileAvatar: undefined }; } - // Update backbone, update DB, run storage service upload + // Update model, update DB, run storage service upload model.set({ about: aboutText, aboutEmoji, diff --git a/ts/shims/contactVerification.ts b/ts/shims/contactVerification.ts index eb7857006a9..0de5b868e85 100644 --- a/ts/shims/contactVerification.ts +++ b/ts/shims/contactVerification.ts @@ -2,14 +2,14 @@ // SPDX-License-Identifier: AGPL-3.0-only export async function toggleVerification(id: string): Promise { - const contact = window.getConversations().get(id); + const contact = window.ConversationController.get(id); if (contact) { await contact.toggleVerified(); } } export async function reloadProfiles(id: string): Promise { - const contact = window.getConversations().get(id); + const contact = window.ConversationController.get(id); if (contact) { await contact.getProfiles(); } diff --git a/ts/shims/events.ts b/ts/shims/events.ts index d69262d4758..9f1436e1e01 100644 --- a/ts/shims/events.ts +++ b/ts/shims/events.ts @@ -8,7 +8,7 @@ import { explodePromise } from '../util/explodePromise'; // Matching Whisper.events.trigger API // eslint-disable-next-line @typescript-eslint/no-explicit-any export function trigger(name: string, ...rest: Array): void { - window.Whisper.events.trigger(name, ...rest); + window.Whisper.events.emit(name, ...rest); } export const waitForEvent = ( diff --git a/ts/signal.ts b/ts/signal.ts index 4865d527ea3..dda19da8bd6 100644 --- a/ts/signal.ts +++ b/ts/signal.ts @@ -7,7 +7,6 @@ import type { ReadonlyDeep } from 'type-fest'; import * as Crypto from './Crypto'; import * as Curve from './Curve'; -import { start as conversationControllerStart } from './ConversationController'; import * as Groups from './groups'; import OS from './util/os/osMain'; import { isProduction } from './util/version'; @@ -486,8 +485,6 @@ export const setup = (options: { Components, Crypto, Curve, - // Note: used in test/index.html, and not type-checked! - conversationControllerStart, Groups, Migrations, OS, diff --git a/ts/sql/Interface.ts b/ts/sql/Interface.ts index 9ddf3082598..7e0e154ab20 100644 --- a/ts/sql/Interface.ts +++ b/ts/sql/Interface.ts @@ -699,10 +699,6 @@ type ReadableInterface = { getAllConversations: () => Array; getAllConversationIds: () => Array; - getAllGroupsInvolvingServiceId: ( - serviceId: ServiceIdString - ) => Array; - getGroupSendCombinedEndorsementExpiration: (groupId: string) => number | null; getGroupSendEndorsementsData: ( groupId: string diff --git a/ts/sql/Server.ts b/ts/sql/Server.ts index 6533b23dc38..869103ebcad 100644 --- a/ts/sql/Server.ts +++ b/ts/sql/Server.ts @@ -372,7 +372,6 @@ export const DataReader: ServerReadableInterface = { getAllConversations, getAllConversationIds, - getAllGroupsInvolvingServiceId, getGroupSendCombinedEndorsementExpiration, getGroupSendEndorsementsData, @@ -1945,27 +1944,6 @@ function getAllConversationIds(db: ReadableDB): Array { return rows.map(row => row.id); } -function getAllGroupsInvolvingServiceId( - db: ReadableDB, - serviceId: ServiceIdString -): Array { - 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( db: ReadableDB, { diff --git a/ts/state/ducks/composer.ts b/ts/state/ducks/composer.ts index fc135bbf91b..1fb89bb3f94 100644 --- a/ts/state/ducks/composer.ts +++ b/ts/state/ducks/composer.ts @@ -900,8 +900,10 @@ function addPendingAttachment( const conversation = window.ConversationController.get(conversationId); if (conversation) { - conversation.attributes.draftAttachments = nextAttachments; - conversation.attributes.draftChanged = true; + conversation.set({ + draftAttachments: nextAttachments, + draftChanged: true, + }); drop(DataWriter.updateConversation(conversation.attributes)); } }; @@ -1202,8 +1204,10 @@ function removeAttachment( const conversation = window.ConversationController.get(conversationId); if (conversation) { - conversation.attributes.draftAttachments = nextAttachments; - conversation.attributes.draftChanged = true; + conversation.set({ + draftAttachments: nextAttachments, + draftChanged: true, + }); await DataWriter.updateConversation(conversation.attributes); } diff --git a/ts/state/ducks/conversations.ts b/ts/state/ducks/conversations.ts index d8eb2701f80..884029d310c 100644 --- a/ts/state/ducks/conversations.ts +++ b/ts/state/ducks/conversations.ts @@ -1541,12 +1541,10 @@ async function getAvatarsAndUpdateConversation( const nextAvatars = getNextAvatarsData(avatars, nextAvatarId); // We don't save buffers to the db, but we definitely want it in-memory so // we don't have to re-generate them. - // - // Mutating here because we don't want to trigger a model change - // because we're updating redux here manually ourselves. Au revoir Backbone! - conversation.attributes.avatars = nextAvatars.map(avatarData => - omit(avatarData, ['buffer']) - ); + + conversation.set({ + avatars: nextAvatars.map(avatarData => omit(avatarData, ['buffer'])), + }); await DataWriter.updateConversation(conversation.attributes); return nextAvatars; @@ -1922,15 +1920,12 @@ function discardEditMessage( conversationId: string ): ThunkAction { return () => { - window.ConversationController.get(conversationId)?.set( - { - draftEditMessage: undefined, - draftBodyRanges: undefined, - draft: undefined, - quotedMessageId: undefined, - }, - { unset: true } - ); + window.ConversationController.get(conversationId)?.set({ + draftEditMessage: undefined, + draftBodyRanges: undefined, + draft: undefined, + quotedMessageId: undefined, + }); }; } @@ -2036,7 +2031,7 @@ function generateNewGroupLink( /** * 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. * * In practice, it's similar to an already-connected thunk action. Later on we will @@ -2229,9 +2224,8 @@ function myProfileChanged( avatarUpdateOptions ); - // writeProfile above updates the backbone model which in turn updates - // redux through it's on:change event listener. Once we lose Backbone - // we'll need to manually sync these new changes. + // writeProfile above updates the model which in turn updates + // redux through it's on:change event listener. // We just want to clear whatever error was there before: dispatch({ @@ -2267,7 +2261,7 @@ function removeCustomColorOnConversations( ): ThunkAction { return async dispatch => { const conversationsToUpdate: Array = []; - window.getConversations().forEach(conversation => { + window.ConversationController.getAll().forEach(conversation => { if (conversation.get('customColorId') === colorId) { conversation.set({ conversationColor: undefined, @@ -2301,7 +2295,7 @@ function resetAllChatColors(): ThunkAction< // Calling this with no args unsets all the colors in the db await DataWriter.updateAllConversationColors(); - window.getConversations().forEach(conversation => { + window.ConversationController.getAll().forEach(conversation => { conversation.set({ conversationColor: undefined, customColor: undefined, diff --git a/ts/state/ducks/stickers.ts b/ts/state/ducks/stickers.ts index 7a0cddbc02b..e81ffb66e5e 100644 --- a/ts/state/ducks/stickers.ts +++ b/ts/state/ducks/stickers.ts @@ -182,7 +182,7 @@ function stickerPackAdded( ): StickerPackAddedAction { 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 ( status === 'error' && attemptedStatus === 'installed' && @@ -336,7 +336,7 @@ function stickerPackUpdated( ): StickerPackUpdatedAction { 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 ( status === 'error' && attemptedStatus === 'installed' && diff --git a/ts/state/getInitialState.ts b/ts/state/getInitialState.ts index 93d9962f59e..5c49e511b7b 100644 --- a/ts/state/getInitialState.ts +++ b/ts/state/getInitialState.ts @@ -113,7 +113,7 @@ export function getInitialState( } export function generateConversationsState(): ConversationsStateType { - const convoCollection = window.getConversations(); + const convoCollection = window.ConversationController.getAll(); const formattedConversations = convoCollection.map(conversation => conversation.format() ); diff --git a/ts/state/initializeRedux.ts b/ts/state/initializeRedux.ts index 2a11ace4845..90bd7130273 100644 --- a/ts/state/initializeRedux.ts +++ b/ts/state/initializeRedux.ts @@ -44,7 +44,7 @@ export function initializeRedux(data: ReduxInitData): void { window.reduxStore = store; // 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 = { accounts: bindActionCreators(actionCreators.accounts, store.dispatch), app: bindActionCreators(actionCreators.app, store.dispatch), diff --git a/ts/state/selectors/conversations.ts b/ts/state/selectors/conversations.ts index 64b7543a9c3..201da258097 100644 --- a/ts/state/selectors/conversations.ts +++ b/ts/state/selectors/conversations.ts @@ -834,7 +834,7 @@ export const getComposeSelectedContacts = createSelector( // What needs to happen to pull that selector logic here? // 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 -// Backbone-based prop-generation functions expect to get Conversation information +// model-based prop-generation functions expect to get Conversation information // directly via ConversationController export function _conversationSelector( conversation?: ConversationType diff --git a/ts/state/smart/App.tsx b/ts/state/smart/App.tsx index 2093a32e4be..90d1a71ea79 100644 --- a/ts/state/smart/App.tsx +++ b/ts/state/smart/App.tsx @@ -100,8 +100,7 @@ async function uploadProfile({ lastName: string; }): Promise { const us = window.ConversationController.getOurConversationOrThrow(); - us.set('profileName', firstName); - us.set('profileFamilyName', lastName); + us.set({ profileName: firstName, profileFamilyName: lastName }); us.captureChange('standaloneProfile'); await DataWriter.updateConversation(us.attributes); diff --git a/ts/state/smart/Preferences.tsx b/ts/state/smart/Preferences.tsx index 00b2e06fd89..4de0d52ccc1 100644 --- a/ts/state/smart/Preferences.tsx +++ b/ts/state/smart/Preferences.tsx @@ -577,7 +577,7 @@ export function SmartPreferences(): JSX.Element | null { createItemsAccess('call-ringtone-notification', true); const [hasCountMutedConversations, onCountMutedConversationsChange] = createItemsAccess('badge-count-muted-conversations', false, () => { - window.Whisper.events.trigger('updateUnreadCount'); + window.Whisper.events.emit('updateUnreadCount'); }); const [hasHideMenuBar, onHideMenuBarChange] = createItemsAccess( 'hide-menu-bar', diff --git a/ts/test-electron/backbone/reliable_trigger_test.ts b/ts/test-electron/backbone/reliable_trigger_test.ts deleted file mode 100644 index f21b349f55b..00000000000 --- a/ts/test-electron/backbone/reliable_trigger_test.ts +++ /dev/null @@ -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 = []; - model.on('event', (...eventArgs) => { - args = eventArgs; - }); - - model.trigger('event', 1); - - assert.strictEqual(args[0], 1); - }); - it('calls clients with 2 args', () => { - let args: Array = []; - 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 = []; - 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 = []; - 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); - }); - }); -}); diff --git a/ts/test-electron/models/conversations_test.ts b/ts/test-electron/models/conversations_test.ts index 80b6850c26a..7736b8428ea 100644 --- a/ts/test-electron/models/conversations_test.ts +++ b/ts/test-electron/models/conversations_test.ts @@ -10,6 +10,7 @@ import { IMAGE_PNG } from '../../types/MIME'; import { generateAci, generatePni } from '../../types/ServiceId'; import { MessageModel } from '../../models/messages'; import { DurationInSeconds } from '../../util/durations'; +import { ConversationModel } from '../../models/conversations'; describe('Conversations', () => { async function resetConversationController(): Promise { @@ -32,7 +33,7 @@ describe('Conversations', () => { it('updates lastMessage even in race conditions with db', async () => { // Creating a fake conversation - const conversation = new window.Whisper.Conversation({ + const conversation = new ConversationModel({ avatars: [], id: generateUuid(), e164: '+15551234567', @@ -111,7 +112,7 @@ describe('Conversations', () => { it('only produces attachments on a quote with an image', async () => { // Creating a fake conversation - const conversation = new window.Whisper.Conversation({ + const conversation = new ConversationModel({ avatars: [], id: generateUuid(), e164: '+15551234567', diff --git a/ts/test-electron/services/MessageCache_test.ts b/ts/test-electron/services/MessageCache_test.ts index 2f09138fcda..960ab6385ee 100644 --- a/ts/test-electron/services/MessageCache_test.ts +++ b/ts/test-electron/services/MessageCache_test.ts @@ -86,8 +86,8 @@ describe('MessageCache', () => { }); }); - describe('register: syncing with backbone', () => { - it('backbone to redux', () => { + describe('register: syncing with models', () => { + it('model to redux', () => { const message1 = new MessageModel({ conversationId: 'xyz', 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({ conversationId: 'xyz', id: uuid(), diff --git a/ts/test-electron/state/ducks/conversations_test.ts b/ts/test-electron/state/ducks/conversations_test.ts index bc4161809dc..57ca34d9e14 100644 --- a/ts/test-electron/state/ducks/conversations_test.ts +++ b/ts/test-electron/state/ducks/conversations_test.ts @@ -130,7 +130,7 @@ describe('both/state/ducks/conversations', () => { sinonSandbox = sinon.createSandbox(); - sinonSandbox.stub(window.Whisper.events, 'trigger'); + sinonSandbox.stub(window.Whisper.events, 'emit'); createGroupStub = sinon.stub(); }); diff --git a/ts/test-electron/updateConversationsWithUuidLookup_test.ts b/ts/test-electron/updateConversationsWithUuidLookup_test.ts index a6991a2ba89..88179db6d11 100644 --- a/ts/test-electron/updateConversationsWithUuidLookup_test.ts +++ b/ts/test-electron/updateConversationsWithUuidLookup_test.ts @@ -68,7 +68,7 @@ describe('updateConversationsWithUuidLookup', () => { return { conversation: convoUuid, mergePromises: [] }; } - convoE164.unset('e164'); + convoE164.set({ e164: undefined }); convoUuid.updateE164(e164); return { conversation: convoUuid, mergePromises: [] }; } diff --git a/ts/test-mock/benchmarks/call_history_search_bench.ts b/ts/test-mock/benchmarks/call_history_search_bench.ts index b9c44ef6c0c..25737fdffc0 100644 --- a/ts/test-mock/benchmarks/call_history_search_bench.ts +++ b/ts/test-mock/benchmarks/call_history_search_bench.ts @@ -140,6 +140,9 @@ Bootstrap.benchmark(async (bootstrap: Bootstrap): Promise => { const CallsTabDetailsTitle = CallsTabDetails.locator( '.ConversationDetailsHeader__title' ); + const AnyCallListAvatar = CallsTabSidebar.locator( + '.CallsList__ItemAvatar' + ).first(); debug('waiting for unread badge to hit correct value', unreadCount); await CallsNavTabUnread.getByText(`${unreadCount} unread`).waitFor(); @@ -147,6 +150,9 @@ Bootstrap.benchmark(async (bootstrap: Bootstrap): Promise => { debug('opening calls tab'); await CallsNavTab.click(); + await CreateCallLink.waitFor(); + await AnyCallListAvatar.waitFor(); + async function measure(runId: number): Promise { // setup const searchContact = contacts[runId % contacts.length]; @@ -182,6 +188,7 @@ Bootstrap.benchmark(async (bootstrap: Bootstrap): Promise => { await NewCallDetailsTitle.waitFor(); await SearchBar.clear(); await CreateCallLink.waitFor(); + await AnyCallListAvatar.waitFor(); // measure const end = Date.now(); diff --git a/ts/textsecure/SocketManager.ts b/ts/textsecure/SocketManager.ts index fb2fac0a34a..942b9e018ec 100644 --- a/ts/textsecure/SocketManager.ts +++ b/ts/textsecure/SocketManager.ts @@ -365,7 +365,7 @@ export class SocketManager extends EventListener { error instanceof LibSignalErrorBase && error.code === ErrorCode.AppExpired ) { - window.Whisper.events.trigger('httpResponse499'); + window.Whisper.events.emit('httpResponse499'); return; } else if ( error instanceof LibSignalErrorBase && diff --git a/ts/textsecure/UpdateKeysListener.ts b/ts/textsecure/UpdateKeysListener.ts index c10963a7665..e811a442405 100644 --- a/ts/textsecure/UpdateKeysListener.ts +++ b/ts/textsecure/UpdateKeysListener.ts @@ -64,7 +64,7 @@ export class UpdateKeysListener { (error.code === 422 || error.code === 403) ) { log.error(`run: Got a ${error.code} uploading PNI keys; unlinking`); - window.Whisper.events.trigger('unlinkAndDisconnect'); + window.Whisper.events.emit('unlinkAndDisconnect'); } else { const errorString = error instanceof HTTPError diff --git a/ts/textsecure/Utils.ts b/ts/textsecure/Utils.ts index 9dd44d65c1f..99bbd5b698f 100644 --- a/ts/textsecure/Utils.ts +++ b/ts/textsecure/Utils.ts @@ -5,7 +5,7 @@ import type { HTTPError } from './Errors'; export async function handleStatusCode(status: number): Promise { if (status === 499) { - window.Whisper.events.trigger('httpResponse499'); + window.Whisper.events.emit('httpResponse499'); } } diff --git a/ts/textsecure/WebAPI.ts b/ts/textsecure/WebAPI.ts index 14d34c54047..54e69c67862 100644 --- a/ts/textsecure/WebAPI.ts +++ b/ts/textsecure/WebAPI.ts @@ -470,7 +470,7 @@ async function _promiseAjax( if (!unauthenticated && response.status === 401) { 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', () => { - window.Whisper.events.trigger('socketStatusChange'); + window.Whisper.events.emit('socketStatusChange'); }); socketManager.on('online', () => { - window.Whisper.events.trigger('online'); + window.Whisper.events.emit('online'); }); socketManager.on('offline', () => { - window.Whisper.events.trigger('offline'); + window.Whisper.events.emit('offline'); }); socketManager.on('authError', () => { - window.Whisper.events.trigger('unlinkAndDisconnect'); + window.Whisper.events.emit('unlinkAndDisconnect'); }); socketManager.on('firstEnvelope', incoming => { - window.Whisper.events.trigger('firstEnvelope', incoming); + window.Whisper.events.emit('firstEnvelope', incoming); }); socketManager.on('serverAlerts', alerts => { diff --git a/ts/textsecure/storage/User.ts b/ts/textsecure/storage/User.ts index 8854f7a82b5..93beba6ffbe 100644 --- a/ts/textsecure/storage/User.ts +++ b/ts/textsecure/storage/User.ts @@ -58,7 +58,7 @@ export class User { ]); // Notify redux about phone number change - window.Whisper.events.trigger('userChanged', true); + window.Whisper.events.emit('userChanged', true); } public getNumber(): string | undefined { diff --git a/ts/util/callDisposition.ts b/ts/util/callDisposition.ts index d846190fef1..f5dcf5963e3 100644 --- a/ts/util/callDisposition.ts +++ b/ts/util/callDisposition.ts @@ -1262,10 +1262,12 @@ async function saveCallHistory({ ); }); - conversation.set( - 'active_at', - Math.max(conversation.get('active_at') ?? 0, callHistory.timestamp) - ); + conversation.set({ + active_at: Math.max( + conversation.get('active_at') ?? 0, + callHistory.timestamp + ), + }); if (canConversationBeUnarchived(conversation.attributes)) { conversation.setArchived(false); diff --git a/ts/util/checkOurPniIdentityKey.ts b/ts/util/checkOurPniIdentityKey.ts index 879e0eb306f..5af465e4556 100644 --- a/ts/util/checkOurPniIdentityKey.ts +++ b/ts/util/checkOurPniIdentityKey.ts @@ -15,20 +15,20 @@ export async function checkOurPniIdentityKey(): Promise { const { pni: remotePni } = await server.whoami(); if (remotePni !== ourPni) { log.warn(`remote pni mismatch, ${remotePni} != ${ourPni}`); - window.Whisper.events.trigger('unlinkAndDisconnect'); + window.Whisper.events.emit('unlinkAndDisconnect'); return; } const localKeyPair = await window.storage.protocol.getIdentityKeyPair(ourPni); if (!localKeyPair) { log.warn(`no local key pair for ${ourPni}, unlinking`); - window.Whisper.events.trigger('unlinkAndDisconnect'); + window.Whisper.events.emit('unlinkAndDisconnect'); return; } const { identityKey: remoteKey } = await server.getKeysForServiceId(ourPni); if (!constantTimeEqual(localKeyPair.publicKey.serialize(), remoteKey)) { log.warn(`local/remote key mismatch for ${ourPni}, unlinking`); - window.Whisper.events.trigger('unlinkAndDisconnect'); + window.Whisper.events.emit('unlinkAndDisconnect'); } } diff --git a/ts/util/cleanup.ts b/ts/util/cleanup.ts index eee32546db0..c5e513d78b9 100644 --- a/ts/util/cleanup.ts +++ b/ts/util/cleanup.ts @@ -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 * with deleteMessageData. */ export function cleanupMessageFromMemory(message: MessageAttributesType): void { diff --git a/ts/util/getSignalConnections.ts b/ts/util/getSignalConnections.ts index 8f25be696a3..42991770329 100644 --- a/ts/util/getSignalConnections.ts +++ b/ts/util/getSignalConnections.ts @@ -26,7 +26,7 @@ export function isSignalConnection( } export function getSignalConnections(): Array { - return window - .getConversations() - .filter(conversation => isSignalConnection(conversation.attributes)); + return window.ConversationController.getAll().filter(conversation => + isSignalConnection(conversation.attributes) + ); } diff --git a/ts/util/handleMessageSend.ts b/ts/util/handleMessageSend.ts index 57022fb5b90..4e8d9629758 100644 --- a/ts/util/handleMessageSend.ts +++ b/ts/util/handleMessageSend.ts @@ -116,7 +116,7 @@ function processError(error: unknown): void { log.warn( `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)); } } diff --git a/ts/util/onDeviceNameChangeSync.ts b/ts/util/onDeviceNameChangeSync.ts index f2811d620b1..60e3098fff9 100644 --- a/ts/util/onDeviceNameChangeSync.ts +++ b/ts/util/onDeviceNameChangeSync.ts @@ -84,7 +84,7 @@ async function fetchAndUpdateDeviceName() { } await window.storage.user.setDeviceName(newName); - window.Whisper.events.trigger('deviceNameChanged'); + window.Whisper.events.emit('deviceNameChanged'); log.info( 'fetchAndUpdateDeviceName: successfully updated new device name locally' ); diff --git a/ts/util/onStoryRecipientUpdate.ts b/ts/util/onStoryRecipientUpdate.ts index 36a2c418af6..65681292245 100644 --- a/ts/util/onStoryRecipientUpdate.ts +++ b/ts/util/onStoryRecipientUpdate.ts @@ -214,7 +214,7 @@ export async function onStoryRecipientUpdate( }); if (handledMessages.length) { - window.Whisper.events.trigger('incrementProgress'); + window.Whisper.events.emit('incrementProgress'); confirm(); } }) diff --git a/ts/util/sendStoryMessage.ts b/ts/util/sendStoryMessage.ts index a84e0f6a2f4..df93910b895 100644 --- a/ts/util/sendStoryMessage.ts +++ b/ts/util/sendStoryMessage.ts @@ -241,7 +241,7 @@ export async function sendStoryMessage( group => group.getStorySendMode() !== StorySendMode.Always ); for (const group of groupsToUpdate) { - group.set('storySendMode', StorySendMode.Always); + group.set({ storySendMode: StorySendMode.Always }); } void DataWriter.updateConversations( groupsToUpdate.map(group => group.attributes) diff --git a/ts/util/validateConversation.ts b/ts/util/validateConversation.ts index 041f9ec69b4..99339bb4b89 100644 --- a/ts/util/validateConversation.ts +++ b/ts/util/validateConversation.ts @@ -2,7 +2,11 @@ // SPDX-License-Identifier: AGPL-3.0-only import type { ValidateConversationType } from '../model-types.d'; -import { isDirectConversation } from './whatTypeOfConversation'; +import { + isDirectConversation, + isGroupV1, + isGroupV2, +} from './whatTypeOfConversation'; import { isServiceIdString } from '../types/ServiceId'; export function validateConversation( @@ -22,6 +26,14 @@ export function validateConversation( return error; } + if ( + !isDirectConversation(attributes) && + !isGroupV1(attributes) && + !isGroupV2(attributes) + ) { + return 'Conversation is not direct, groupv1 or groupv2'; + } + return null; } diff --git a/ts/window.d.ts b/ts/window.d.ts index b1398cf572d..aa485ca043d 100644 --- a/ts/window.d.ts +++ b/ts/window.d.ts @@ -3,15 +3,14 @@ // 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 * as Backbone from 'backbone'; import type { SystemPreferences } from 'electron'; import type PQueue from 'p-queue/dist'; import type { assert } from 'chai'; import type { PhoneNumber, PhoneNumberFormat } from 'google-libphonenumber'; import type { MochaOptions } from 'mocha'; -import type { ConversationModelCollectionType } from './model-types.d'; import type { textsecure } from './textsecure'; import type { Storage } from './textsecure/Storage'; import type { @@ -34,7 +33,6 @@ import type { Receipt } from './types/Receipt'; import type { ConversationController } from './ConversationController'; import type { ReduxActions } from './state/types'; import type { createApp } from './state/roots/createApp'; -import type { ConversationModel } from './models/conversations'; import type { BatcherType } from './util/batcher'; import type { ConfirmationDialog } from './components/ConfirmationDialog'; import type { SignalProtocolStore } from './SignalProtocolStore'; @@ -183,7 +181,6 @@ export type SignalCoreType = { createApp: typeof createApp; }; }; - conversationControllerStart: () => void; challengeHandler?: ChallengeHandler; // Only for debugging in Dev Tools @@ -206,7 +203,6 @@ declare global { enterMouseMode: () => void; getAccountManager: () => AccountManager; getAppInstance: () => string | undefined; - getConversations: () => ConversationModelCollectionType; getBuildCreation: () => number; getBuildExpiration: () => number; getHostName: () => string; @@ -247,9 +243,6 @@ declare global { // The types below have been somewhat organized. See DESKTOP-4801 // ======================================================================== - // Backbone - Backbone: typeof Backbone; - ConversationController: ConversationController; Events: IPCEventsType; FontFace: typeof FontFace; @@ -331,10 +324,7 @@ declare global { } export type WhisperType = { - Conversation: typeof ConversationModel; - ConversationCollection: typeof ConversationModelCollectionType; - deliveryReceiptQueue: PQueue; deliveryReceiptBatcher: BatcherType; - events: Backbone.Events; + events: EventEmitter; }; diff --git a/ts/windows/main/phase1-ipc.ts b/ts/windows/main/phase1-ipc.ts index 49cb2b4a3c3..b3342af4141 100644 --- a/ts/windows/main/phase1-ipc.ts +++ b/ts/windows/main/phase1-ipc.ts @@ -1,9 +1,11 @@ // Copyright 2022 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only +import EventEmitter from 'node:events'; import { ipcRenderer as ipc } from 'electron'; 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 { parseIntWithFallback } from '../../util/parseIntWithFallback'; @@ -24,6 +26,15 @@ import { AggregatedStats } from '../../textsecure/WebsocketResources'; import { UNAUTHENTICATED_CHANNEL_NAME } from '../../textsecure/SocketManager'; import { isProduction } from '../../util/version'; 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'); @@ -47,6 +58,32 @@ window.Flags = Flags; window.RETRY_DELAY = false; +window.Whisper = { + events: new EventEmitter(), + deliveryReceiptQueue: new PQueue({ + concurrency: 1, + timeout: MINUTE * 30, + }), + deliveryReceiptBatcher: createBatcher({ + 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.getTitle = () => title; window.getAppInstance = () => config.appInstance; @@ -272,35 +309,35 @@ ipc.on('additional-log-data-request', async event => { }); ipc.on('open-settings-tab', () => { - window.Whisper.events.trigger('openSettingsTab'); + window.Whisper.events.emit('openSettingsTab'); }); ipc.on('set-up-as-new-device', () => { - window.Whisper.events.trigger('setupAsNewDevice'); + window.Whisper.events.emit('setupAsNewDevice'); }); ipc.on('set-up-as-standalone', () => { - window.Whisper.events.trigger('setupAsStandalone'); + window.Whisper.events.emit('setupAsStandalone'); }); ipc.on('stage-local-backup-for-import', () => { - window.Whisper.events.trigger('stageLocalBackupForImport'); + window.Whisper.events.emit('stageLocalBackupForImport'); }); ipc.on('challenge:response', (_event, response) => { - window.Whisper.events.trigger('challengeResponse', response); + window.Whisper.events.emit('challengeResponse', response); }); ipc.on('power-channel:suspend', () => { - window.Whisper.events.trigger('powerMonitorSuspend'); + window.Whisper.events.emit('powerMonitorSuspend'); }); ipc.on('power-channel:resume', () => { - window.Whisper.events.trigger('powerMonitorResume'); + window.Whisper.events.emit('powerMonitorResume'); }); ipc.on('power-channel:lock-screen', () => { - window.Whisper.events.trigger('powerMonitorLockScreen'); + window.Whisper.events.emit('powerMonitorLockScreen'); }); ipc.on( @@ -328,7 +365,7 @@ ipc.on('window:set-menu-options', (_event, options) => { if (!window.Whisper.events) { return; } - window.Whisper.events.trigger('setMenuOptions', options); + window.Whisper.events.emit('setMenuOptions', options); }); window.sendChallengeRequest = request => ipc.send('challenge:request', request); diff --git a/ts/windows/main/phase2-dependencies.ts b/ts/windows/main/phase2-dependencies.ts index e6af85308c9..77ed1f9631d 100644 --- a/ts/windows/main/phase2-dependencies.ts +++ b/ts/windows/main/phase2-dependencies.ts @@ -1,7 +1,6 @@ // Copyright 2022 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -import Backbone from 'backbone'; import { PhoneNumberUtil, PhoneNumberFormat } from 'google-libphonenumber'; import * as moment from 'moment'; // @ts-expect-error -- no types @@ -21,7 +20,6 @@ const log = createLogger('phase2-dependencies'); initializeLogging(); window.nodeSetImmediate = setImmediate; -window.Backbone = Backbone; window.textsecure = textsecure; const { config } = window.SignalContext; diff --git a/ts/windows/main/start.ts b/ts/windows/main/start.ts index 9df71aaa264..34d6ab4d22b 100644 --- a/ts/windows/main/start.ts +++ b/ts/windows/main/start.ts @@ -1,7 +1,7 @@ // Copyright 2017 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -import { clone, has } from 'lodash'; +import { has } from 'lodash'; import { contextBridge } from 'electron'; import { createLogger } from '../../logging/log'; @@ -17,7 +17,6 @@ import '../preload'; import './phase2-dependencies'; import './phase3-post-signal'; import './phase4-test'; -import '../../backbone/reliable_trigger'; import type { CdsLookupOptionsType, @@ -25,7 +24,6 @@ import type { } from '../../textsecure/WebAPI'; import type { FeatureFlagType } from '../../window.d'; import type { StorageAccessType } from '../../types/Storage.d'; -import { start as startConversationController } from '../../ConversationController'; import { initMessageCleanup } from '../../services/messageStateCleanup'; import { Environment, getEnvironment } from '../../environment'; import { isProduction } from '../../util/version'; @@ -52,9 +50,7 @@ if (window.SignalContext.config.proxyUrl) { log.info('Using provided proxy url'); } -window.Whisper.events = clone(window.Backbone.Events); initMessageCleanup(); -startConversationController(); if ( !isProduction(window.SignalContext.getVersion()) ||