| 
									
										
										
										
											2018-04-10 16:51:48 -07:00
										 |  |  | /* global _: false */ | 
					
						
							|  |  |  | /* global Backbone: false */ | 
					
						
							| 
									
										
										
										
											2018-07-09 14:29:13 -07:00
										 |  |  | /* global storage: false */ | 
					
						
							|  |  |  | /* global filesize: false */ | 
					
						
							| 
									
										
										
										
											2018-04-10 16:51:48 -07:00
										 |  |  | /* global ConversationController: false */ | 
					
						
							|  |  |  | /* global getAccountManager: false */ | 
					
						
							| 
									
										
										
										
											2018-05-07 12:50:11 -04:00
										 |  |  | /* global i18n: false */ | 
					
						
							|  |  |  | /* global Signal: false */ | 
					
						
							|  |  |  | /* global textsecure: false */ | 
					
						
							|  |  |  | /* global Whisper: false */ | 
					
						
							| 
									
										
										
										
											2018-05-24 18:54:06 -07:00
										 |  |  | /* global wrapDeferred: false */ | 
					
						
							| 
									
										
										
										
											2018-03-20 15:03:26 -04:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-04-10 16:51:48 -07:00
										 |  |  | /* eslint-disable more/no-then */ | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // eslint-disable-next-line func-names
 | 
					
						
							| 
									
										
										
										
											2018-04-27 17:25:04 -04:00
										 |  |  | (function() { | 
					
						
							| 
									
										
										
										
											2018-04-10 16:51:48 -07:00
										 |  |  |   'use strict'; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   window.Whisper = window.Whisper || {}; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-07-09 14:29:13 -07:00
										 |  |  |   const { Message: TypedMessage, Contact, PhoneNumber } = Signal.Types; | 
					
						
							|  |  |  |   const { | 
					
						
							| 
									
										
										
										
											2018-07-24 11:55:24 -07:00
										 |  |  |     deleteExternalMessageFiles, | 
					
						
							| 
									
										
										
										
											2018-07-09 14:29:13 -07:00
										 |  |  |     getAbsoluteAttachmentPath, | 
					
						
							| 
									
										
										
										
											2018-07-26 18:13:56 -07:00
										 |  |  |     loadAttachmentData, | 
					
						
							|  |  |  |     loadQuoteData, | 
					
						
							|  |  |  |   } = window.Signal.Migrations; | 
					
						
							| 
									
										
										
										
											2018-07-09 14:29:13 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  |   window.AccountCache = Object.create(null); | 
					
						
							|  |  |  |   window.AccountJobs = Object.create(null); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   window.doesAcountCheckJobExist = number => | 
					
						
							|  |  |  |     Boolean(window.AccountJobs[number]); | 
					
						
							|  |  |  |   window.checkForSignalAccount = number => { | 
					
						
							|  |  |  |     if (window.AccountJobs[number]) { | 
					
						
							|  |  |  |       return window.AccountJobs[number]; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-08-07 12:33:56 -07:00
										 |  |  |     let job; | 
					
						
							|  |  |  |     if (textsecure.messaging) { | 
					
						
							|  |  |  |       // eslint-disable-next-line more/no-then
 | 
					
						
							|  |  |  |       job = textsecure.messaging | 
					
						
							|  |  |  |         .getProfile(number) | 
					
						
							|  |  |  |         .then(() => { | 
					
						
							|  |  |  |           window.AccountCache[number] = true; | 
					
						
							|  |  |  |         }) | 
					
						
							|  |  |  |         .catch(() => { | 
					
						
							|  |  |  |           window.AccountCache[number] = false; | 
					
						
							|  |  |  |         }); | 
					
						
							|  |  |  |     } else { | 
					
						
							|  |  |  |       // We're offline!
 | 
					
						
							|  |  |  |       job = Promise.resolve().then(() => { | 
					
						
							| 
									
										
										
										
											2018-07-09 14:29:13 -07:00
										 |  |  |         window.AccountCache[number] = false; | 
					
						
							|  |  |  |       }); | 
					
						
							| 
									
										
										
										
											2018-08-07 12:33:56 -07:00
										 |  |  |     } | 
					
						
							| 
									
										
										
										
											2018-07-09 14:29:13 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  |     window.AccountJobs[number] = job; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     return job; | 
					
						
							|  |  |  |   }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   window.isSignalAccountCheckComplete = number => | 
					
						
							|  |  |  |     window.AccountCache[number] !== undefined; | 
					
						
							|  |  |  |   window.hasSignalAccount = number => window.AccountCache[number]; | 
					
						
							| 
									
										
										
										
											2018-04-10 16:51:48 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  |   window.Whisper.Message = Backbone.Model.extend({ | 
					
						
							| 
									
										
										
										
											2018-08-01 12:38:48 -07:00
										 |  |  |     // Keeping this for legacy upgrade pre-migrate to SQLCipher
 | 
					
						
							|  |  |  |     database: Whisper.Database, | 
					
						
							|  |  |  |     storeName: 'messages', | 
					
						
							| 
									
										
										
										
											2018-04-10 16:51:48 -07:00
										 |  |  |     initialize(attributes) { | 
					
						
							|  |  |  |       if (_.isObject(attributes)) { | 
					
						
							| 
									
										
										
										
											2018-07-21 12:00:08 -07:00
										 |  |  |         this.set( | 
					
						
							|  |  |  |           TypedMessage.initializeSchemaVersion({ | 
					
						
							|  |  |  |             message: attributes, | 
					
						
							|  |  |  |             logger: window.log, | 
					
						
							|  |  |  |           }) | 
					
						
							|  |  |  |         ); | 
					
						
							| 
									
										
										
										
											2018-04-10 16:51:48 -07:00
										 |  |  |       } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-07-09 14:29:13 -07:00
										 |  |  |       this.OUR_NUMBER = textsecure.storage.user.getNumber(); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-04-10 16:51:48 -07:00
										 |  |  |       this.on('destroy', this.onDestroy); | 
					
						
							|  |  |  |       this.on('change:expirationStartTimestamp', this.setToExpire); | 
					
						
							|  |  |  |       this.on('change:expireTimer', this.setToExpire); | 
					
						
							| 
									
										
										
										
											2018-04-11 23:55:32 -07:00
										 |  |  |       this.on('unload', this.unload); | 
					
						
							| 
									
										
										
										
											2018-08-09 16:18:10 -07:00
										 |  |  |       this.on('expired', this.onExpired); | 
					
						
							| 
									
										
										
										
											2018-04-10 16:51:48 -07:00
										 |  |  |       this.setToExpire(); | 
					
						
							|  |  |  |     }, | 
					
						
							|  |  |  |     idForLogging() { | 
					
						
							| 
									
										
										
										
											2018-04-27 17:25:04 -04:00
										 |  |  |       return `${this.get('source')}.${this.get('sourceDevice')} ${this.get( | 
					
						
							|  |  |  |         'sent_at' | 
					
						
							|  |  |  |       )}`;
 | 
					
						
							| 
									
										
										
										
											2018-04-10 16:51:48 -07:00
										 |  |  |     }, | 
					
						
							|  |  |  |     defaults() { | 
					
						
							|  |  |  |       return { | 
					
						
							|  |  |  |         timestamp: new Date().getTime(), | 
					
						
							|  |  |  |         attachments: [], | 
					
						
							|  |  |  |       }; | 
					
						
							|  |  |  |     }, | 
					
						
							|  |  |  |     validate(attributes) { | 
					
						
							|  |  |  |       const required = ['conversationId', 'received_at', 'sent_at']; | 
					
						
							|  |  |  |       const missing = _.filter(required, attr => !attributes[attr]); | 
					
						
							|  |  |  |       if (missing.length) { | 
					
						
							| 
									
										
										
										
											2018-07-21 12:00:08 -07:00
										 |  |  |         window.log.warn(`Message missing attributes: ${missing}`); | 
					
						
							| 
									
										
										
										
											2018-04-10 16:51:48 -07:00
										 |  |  |       } | 
					
						
							|  |  |  |     }, | 
					
						
							|  |  |  |     isEndSession() { | 
					
						
							|  |  |  |       const flag = textsecure.protobuf.DataMessage.Flags.END_SESSION; | 
					
						
							|  |  |  |       // eslint-disable-next-line no-bitwise
 | 
					
						
							|  |  |  |       return !!(this.get('flags') & flag); | 
					
						
							|  |  |  |     }, | 
					
						
							|  |  |  |     isExpirationTimerUpdate() { | 
					
						
							| 
									
										
										
										
											2018-04-27 17:25:04 -04:00
										 |  |  |       const flag = | 
					
						
							|  |  |  |         textsecure.protobuf.DataMessage.Flags.EXPIRATION_TIMER_UPDATE; | 
					
						
							| 
									
										
										
										
											2018-04-10 16:51:48 -07:00
										 |  |  |       // eslint-disable-next-line no-bitwise
 | 
					
						
							|  |  |  |       return !!(this.get('flags') & flag); | 
					
						
							|  |  |  |     }, | 
					
						
							|  |  |  |     isGroupUpdate() { | 
					
						
							| 
									
										
										
										
											2018-04-27 17:25:04 -04:00
										 |  |  |       return !!this.get('group_update'); | 
					
						
							| 
									
										
										
										
											2018-04-10 16:51:48 -07:00
										 |  |  |     }, | 
					
						
							|  |  |  |     isIncoming() { | 
					
						
							|  |  |  |       return this.get('type') === 'incoming'; | 
					
						
							|  |  |  |     }, | 
					
						
							|  |  |  |     isUnread() { | 
					
						
							|  |  |  |       return !!this.get('unread'); | 
					
						
							|  |  |  |     }, | 
					
						
							| 
									
										
										
										
											2018-07-25 15:02:27 -07:00
										 |  |  |     // Important to allow for this.unset('unread'), save to db, then fetch()
 | 
					
						
							|  |  |  |     // to propagate. We don't want the unset key in the db so our unread index
 | 
					
						
							|  |  |  |     // stays small.
 | 
					
						
							|  |  |  |     merge(model) { | 
					
						
							|  |  |  |       const attributes = model.attributes || model; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       const { unread } = attributes; | 
					
						
							|  |  |  |       if (typeof unread === 'undefined') { | 
					
						
							|  |  |  |         this.unset('unread'); | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       this.set(attributes); | 
					
						
							| 
									
										
										
										
											2018-04-10 16:51:48 -07:00
										 |  |  |     }, | 
					
						
							|  |  |  |     getNameForNumber(number) { | 
					
						
							|  |  |  |       const conversation = ConversationController.get(number); | 
					
						
							|  |  |  |       if (!conversation) { | 
					
						
							|  |  |  |         return number; | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |       return conversation.getDisplayName(); | 
					
						
							|  |  |  |     }, | 
					
						
							|  |  |  |     getDescription() { | 
					
						
							|  |  |  |       if (this.isGroupUpdate()) { | 
					
						
							|  |  |  |         const groupUpdate = this.get('group_update'); | 
					
						
							|  |  |  |         if (groupUpdate.left === 'You') { | 
					
						
							|  |  |  |           return i18n('youLeftTheGroup'); | 
					
						
							|  |  |  |         } else if (groupUpdate.left) { | 
					
						
							|  |  |  |           return i18n('leftTheGroup', this.getNameForNumber(groupUpdate.left)); | 
					
						
							|  |  |  |         } | 
					
						
							| 
									
										
										
										
											2018-03-16 18:35:27 -04:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-07-09 14:29:13 -07:00
										 |  |  |         const messages = []; | 
					
						
							| 
									
										
										
										
											2018-07-24 14:21:21 -07:00
										 |  |  |         if (!groupUpdate.name && !groupUpdate.joined) { | 
					
						
							|  |  |  |           messages.push(i18n('updatedTheGroup')); | 
					
						
							|  |  |  |         } | 
					
						
							| 
									
										
										
										
											2018-04-10 16:51:48 -07:00
										 |  |  |         if (groupUpdate.name) { | 
					
						
							|  |  |  |           messages.push(i18n('titleIsNow', groupUpdate.name)); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |         if (groupUpdate.joined && groupUpdate.joined.length) { | 
					
						
							| 
									
										
										
										
											2018-04-27 17:25:04 -04:00
										 |  |  |           const names = _.map( | 
					
						
							|  |  |  |             groupUpdate.joined, | 
					
						
							|  |  |  |             this.getNameForNumber.bind(this) | 
					
						
							|  |  |  |           ); | 
					
						
							| 
									
										
										
										
											2018-04-10 16:51:48 -07:00
										 |  |  |           if (names.length > 1) { | 
					
						
							|  |  |  |             messages.push(i18n('multipleJoinedTheGroup', names.join(', '))); | 
					
						
							|  |  |  |           } else { | 
					
						
							|  |  |  |             messages.push(i18n('joinedTheGroup', names[0])); | 
					
						
							|  |  |  |           } | 
					
						
							|  |  |  |         } | 
					
						
							| 
									
										
										
										
											2015-03-23 15:44:47 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-07-09 14:29:13 -07:00
										 |  |  |         return messages.join(', '); | 
					
						
							| 
									
										
										
										
											2018-04-10 16:51:48 -07:00
										 |  |  |       } | 
					
						
							|  |  |  |       if (this.isEndSession()) { | 
					
						
							|  |  |  |         return i18n('sessionEnded'); | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |       if (this.isIncoming() && this.hasErrors()) { | 
					
						
							|  |  |  |         return i18n('incomingError'); | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |       return this.get('body'); | 
					
						
							|  |  |  |     }, | 
					
						
							| 
									
										
										
										
											2018-07-09 14:29:13 -07:00
										 |  |  |     isVerifiedChange() { | 
					
						
							|  |  |  |       return this.get('type') === 'verified-change'; | 
					
						
							|  |  |  |     }, | 
					
						
							| 
									
										
										
										
											2018-04-10 16:51:48 -07:00
										 |  |  |     isKeyChange() { | 
					
						
							|  |  |  |       return this.get('type') === 'keychange'; | 
					
						
							|  |  |  |     }, | 
					
						
							|  |  |  |     getNotificationText() { | 
					
						
							|  |  |  |       const description = this.getDescription(); | 
					
						
							|  |  |  |       if (description) { | 
					
						
							|  |  |  |         return description; | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |       if (this.get('attachments').length > 0) { | 
					
						
							|  |  |  |         return i18n('mediaMessage'); | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |       if (this.isExpirationTimerUpdate()) { | 
					
						
							|  |  |  |         const { expireTimer } = this.get('expirationTimerUpdate'); | 
					
						
							| 
									
										
										
										
											2018-07-24 14:38:58 -07:00
										 |  |  |         if (!expireTimer) { | 
					
						
							|  |  |  |           return i18n('disappearingMessagesDisabled'); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-04-10 16:51:48 -07:00
										 |  |  |         return i18n( | 
					
						
							|  |  |  |           'timerSetTo', | 
					
						
							| 
									
										
										
										
											2018-07-20 16:37:57 -07:00
										 |  |  |           Whisper.ExpirationTimerOptions.getAbbreviated(expireTimer || 0) | 
					
						
							| 
									
										
										
										
											2018-04-10 16:51:48 -07:00
										 |  |  |         ); | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |       if (this.isKeyChange()) { | 
					
						
							| 
									
										
										
										
											2018-07-09 14:29:13 -07:00
										 |  |  |         const phoneNumber = this.get('key_changed'); | 
					
						
							|  |  |  |         const conversation = this.findContact(phoneNumber); | 
					
						
							|  |  |  |         return i18n( | 
					
						
							|  |  |  |           'safetyNumberChangedGroup', | 
					
						
							|  |  |  |           conversation ? conversation.getTitle() : null | 
					
						
							|  |  |  |         ); | 
					
						
							| 
									
										
										
										
											2018-04-10 16:51:48 -07:00
										 |  |  |       } | 
					
						
							| 
									
										
										
										
											2018-05-08 18:19:35 -07:00
										 |  |  |       const contacts = this.get('contact'); | 
					
						
							|  |  |  |       if (contacts && contacts.length) { | 
					
						
							|  |  |  |         return Contact.getName(contacts[0]); | 
					
						
							|  |  |  |       } | 
					
						
							| 
									
										
										
										
											2018-04-10 16:51:48 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  |       return ''; | 
					
						
							|  |  |  |     }, | 
					
						
							| 
									
										
										
										
											2018-07-24 11:55:24 -07:00
										 |  |  |     onDestroy() { | 
					
						
							| 
									
										
										
										
											2018-07-26 18:13:56 -07:00
										 |  |  |       this.cleanup(); | 
					
						
							|  |  |  |     }, | 
					
						
							|  |  |  |     async cleanup() { | 
					
						
							| 
									
										
										
										
											2018-07-24 11:55:24 -07:00
										 |  |  |       this.unload(); | 
					
						
							| 
									
										
										
										
											2018-07-26 18:13:56 -07:00
										 |  |  |       await deleteExternalMessageFiles(this.attributes); | 
					
						
							| 
									
										
										
										
											2018-04-10 16:51:48 -07:00
										 |  |  |     }, | 
					
						
							| 
									
										
										
										
											2018-04-11 23:55:32 -07:00
										 |  |  |     unload() { | 
					
						
							|  |  |  |       if (this.quotedMessage) { | 
					
						
							|  |  |  |         this.quotedMessage = null; | 
					
						
							|  |  |  |       } | 
					
						
							| 
									
										
										
										
											2018-04-10 16:51:48 -07:00
										 |  |  |     }, | 
					
						
							| 
									
										
										
										
											2018-08-09 16:18:10 -07:00
										 |  |  |     onExpired() { | 
					
						
							|  |  |  |       this.hasExpired = true; | 
					
						
							|  |  |  |     }, | 
					
						
							| 
									
										
										
										
											2018-07-09 14:29:13 -07:00
										 |  |  |     getPropsForTimerNotification() { | 
					
						
							|  |  |  |       const { expireTimer, fromSync, source } = this.get( | 
					
						
							|  |  |  |         'expirationTimerUpdate' | 
					
						
							|  |  |  |       ); | 
					
						
							|  |  |  |       const timespan = Whisper.ExpirationTimerOptions.getName(expireTimer || 0); | 
					
						
							| 
									
										
										
										
											2018-07-24 14:38:58 -07:00
										 |  |  |       const disabled = !expireTimer; | 
					
						
							| 
									
										
										
										
											2018-07-09 14:29:13 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  |       const basicProps = { | 
					
						
							|  |  |  |         type: 'fromOther', | 
					
						
							|  |  |  |         ...this.findAndFormatContact(source), | 
					
						
							|  |  |  |         timespan, | 
					
						
							| 
									
										
										
										
											2018-07-24 14:38:58 -07:00
										 |  |  |         disabled, | 
					
						
							| 
									
										
										
										
											2018-07-09 14:29:13 -07:00
										 |  |  |       }; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-07-26 18:13:56 -07:00
										 |  |  |       if (fromSync) { | 
					
						
							| 
									
										
										
										
											2018-07-09 14:29:13 -07:00
										 |  |  |         return { | 
					
						
							|  |  |  |           ...basicProps, | 
					
						
							| 
									
										
										
										
											2018-07-26 18:13:56 -07:00
										 |  |  |           type: 'fromSync', | 
					
						
							| 
									
										
										
										
											2018-07-09 14:29:13 -07:00
										 |  |  |         }; | 
					
						
							| 
									
										
										
										
											2018-07-26 18:13:56 -07:00
										 |  |  |       } else if (source === this.OUR_NUMBER) { | 
					
						
							| 
									
										
										
										
											2018-07-09 14:29:13 -07:00
										 |  |  |         return { | 
					
						
							|  |  |  |           ...basicProps, | 
					
						
							| 
									
										
										
										
											2018-07-26 18:13:56 -07:00
										 |  |  |           type: 'fromMe', | 
					
						
							| 
									
										
										
										
											2018-07-09 14:29:13 -07:00
										 |  |  |         }; | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       return basicProps; | 
					
						
							|  |  |  |     }, | 
					
						
							|  |  |  |     getPropsForSafetyNumberNotification() { | 
					
						
							|  |  |  |       const conversation = this.getConversation(); | 
					
						
							|  |  |  |       const isGroup = conversation && !conversation.isPrivate(); | 
					
						
							|  |  |  |       const phoneNumber = this.get('key_changed'); | 
					
						
							|  |  |  |       const onVerify = () => | 
					
						
							|  |  |  |         this.trigger('show-identity', this.findContact(phoneNumber)); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       return { | 
					
						
							|  |  |  |         isGroup, | 
					
						
							|  |  |  |         contact: this.findAndFormatContact(phoneNumber), | 
					
						
							|  |  |  |         onVerify, | 
					
						
							|  |  |  |       }; | 
					
						
							|  |  |  |     }, | 
					
						
							|  |  |  |     getPropsForVerificationNotification() { | 
					
						
							|  |  |  |       const type = this.get('verified') ? 'markVerified' : 'markNotVerified'; | 
					
						
							|  |  |  |       const isLocal = this.get('local'); | 
					
						
							|  |  |  |       const phoneNumber = this.get('verifiedChanged'); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       return { | 
					
						
							|  |  |  |         type, | 
					
						
							|  |  |  |         isLocal, | 
					
						
							|  |  |  |         contact: this.findAndFormatContact(phoneNumber), | 
					
						
							|  |  |  |       }; | 
					
						
							|  |  |  |     }, | 
					
						
							|  |  |  |     getPropsForResetSessionNotification() { | 
					
						
							|  |  |  |       // It doesn't need anything right now!
 | 
					
						
							|  |  |  |       return {}; | 
					
						
							|  |  |  |     }, | 
					
						
							|  |  |  |     findContact(phoneNumber) { | 
					
						
							|  |  |  |       return ConversationController.get(phoneNumber); | 
					
						
							|  |  |  |     }, | 
					
						
							|  |  |  |     findAndFormatContact(phoneNumber) { | 
					
						
							|  |  |  |       const { format } = PhoneNumber; | 
					
						
							|  |  |  |       const regionCode = storage.get('regionCode'); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       const contactModel = this.findContact(phoneNumber); | 
					
						
							|  |  |  |       const avatar = contactModel ? contactModel.getAvatar() : null; | 
					
						
							|  |  |  |       const color = contactModel ? contactModel.getColor() : null; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       return { | 
					
						
							|  |  |  |         phoneNumber: format(phoneNumber, { | 
					
						
							|  |  |  |           ourRegionCode: regionCode, | 
					
						
							|  |  |  |         }), | 
					
						
							|  |  |  |         color, | 
					
						
							|  |  |  |         avatarPath: avatar ? avatar.url : null, | 
					
						
							|  |  |  |         name: contactModel ? contactModel.getName() : null, | 
					
						
							|  |  |  |         profileName: contactModel ? contactModel.getProfileName() : null, | 
					
						
							|  |  |  |         title: contactModel ? contactModel.getTitle() : null, | 
					
						
							|  |  |  |       }; | 
					
						
							|  |  |  |     }, | 
					
						
							|  |  |  |     getPropsForGroupNotification() { | 
					
						
							|  |  |  |       const groupUpdate = this.get('group_update'); | 
					
						
							|  |  |  |       const changes = []; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       if (!groupUpdate.name && !groupUpdate.left && !groupUpdate.joined) { | 
					
						
							|  |  |  |         changes.push({ | 
					
						
							|  |  |  |           type: 'general', | 
					
						
							|  |  |  |         }); | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       if (groupUpdate.joined) { | 
					
						
							|  |  |  |         changes.push({ | 
					
						
							|  |  |  |           type: 'add', | 
					
						
							|  |  |  |           contacts: _.map( | 
					
						
							|  |  |  |             Array.isArray(groupUpdate.joined) | 
					
						
							|  |  |  |               ? groupUpdate.joined | 
					
						
							|  |  |  |               : [groupUpdate.joined], | 
					
						
							|  |  |  |             phoneNumber => this.findAndFormatContact(phoneNumber) | 
					
						
							|  |  |  |           ), | 
					
						
							|  |  |  |         }); | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       if (groupUpdate.left === 'You') { | 
					
						
							|  |  |  |         changes.push({ | 
					
						
							|  |  |  |           type: 'remove', | 
					
						
							|  |  |  |           isMe: true, | 
					
						
							|  |  |  |         }); | 
					
						
							|  |  |  |       } else if (groupUpdate.left) { | 
					
						
							|  |  |  |         changes.push({ | 
					
						
							|  |  |  |           type: 'remove', | 
					
						
							|  |  |  |           contacts: _.map( | 
					
						
							|  |  |  |             Array.isArray(groupUpdate.left) | 
					
						
							|  |  |  |               ? groupUpdate.left | 
					
						
							|  |  |  |               : [groupUpdate.left], | 
					
						
							|  |  |  |             phoneNumber => this.findAndFormatContact(phoneNumber) | 
					
						
							|  |  |  |           ), | 
					
						
							|  |  |  |         }); | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       if (groupUpdate.name) { | 
					
						
							|  |  |  |         changes.push({ | 
					
						
							|  |  |  |           type: 'name', | 
					
						
							|  |  |  |           newName: groupUpdate.name, | 
					
						
							|  |  |  |         }); | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       return { | 
					
						
							|  |  |  |         changes, | 
					
						
							|  |  |  |       }; | 
					
						
							|  |  |  |     }, | 
					
						
							|  |  |  |     getMessagePropStatus() { | 
					
						
							|  |  |  |       if (this.hasErrors()) { | 
					
						
							|  |  |  |         return 'error'; | 
					
						
							|  |  |  |       } | 
					
						
							| 
									
										
										
										
											2018-08-06 09:04:27 -07:00
										 |  |  |       if (!this.isOutgoing()) { | 
					
						
							|  |  |  |         return null; | 
					
						
							|  |  |  |       } | 
					
						
							| 
									
										
										
										
											2018-07-09 14:29:13 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  |       const readBy = this.get('read_by') || []; | 
					
						
							|  |  |  |       if (readBy.length > 0) { | 
					
						
							|  |  |  |         return 'read'; | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |       const delivered = this.get('delivered'); | 
					
						
							|  |  |  |       const deliveredTo = this.get('delivered_to') || []; | 
					
						
							|  |  |  |       if (delivered || deliveredTo.length > 0) { | 
					
						
							|  |  |  |         return 'delivered'; | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |       const sent = this.get('sent'); | 
					
						
							|  |  |  |       const sentTo = this.get('sent_to') || []; | 
					
						
							|  |  |  |       if (sent || sentTo.length > 0) { | 
					
						
							|  |  |  |         return 'sent'; | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       return 'sending'; | 
					
						
							|  |  |  |     }, | 
					
						
							|  |  |  |     getPropsForMessage() { | 
					
						
							|  |  |  |       const phoneNumber = this.getSource(); | 
					
						
							|  |  |  |       const contact = this.findAndFormatContact(phoneNumber); | 
					
						
							|  |  |  |       const contactModel = this.findContact(phoneNumber); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       const authorColor = contactModel ? contactModel.getColor() : null; | 
					
						
							|  |  |  |       const authorAvatar = contactModel ? contactModel.getAvatar() : null; | 
					
						
							| 
									
										
										
										
											2018-07-26 18:13:56 -07:00
										 |  |  |       const authorAvatarPath = authorAvatar ? authorAvatar.url : null; | 
					
						
							| 
									
										
										
										
											2018-07-09 14:29:13 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  |       const expirationLength = this.get('expireTimer') * 1000; | 
					
						
							|  |  |  |       const expireTimerStart = this.get('expirationStartTimestamp'); | 
					
						
							|  |  |  |       const expirationTimestamp = | 
					
						
							|  |  |  |         expirationLength && expireTimerStart | 
					
						
							|  |  |  |           ? expireTimerStart + expirationLength | 
					
						
							|  |  |  |           : null; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       const conversation = this.getConversation(); | 
					
						
							|  |  |  |       const isGroup = conversation && !conversation.isPrivate(); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       const attachments = this.get('attachments'); | 
					
						
							|  |  |  |       const firstAttachment = attachments && attachments[0]; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       return { | 
					
						
							|  |  |  |         text: this.createNonBreakingLastSeparator(this.get('body')), | 
					
						
							|  |  |  |         id: this.id, | 
					
						
							|  |  |  |         direction: this.isIncoming() ? 'incoming' : 'outgoing', | 
					
						
							|  |  |  |         timestamp: this.get('sent_at'), | 
					
						
							|  |  |  |         status: this.getMessagePropStatus(), | 
					
						
							|  |  |  |         contact: this.getPropsForEmbeddedContact(), | 
					
						
							|  |  |  |         authorName: contact.name, | 
					
						
							|  |  |  |         authorProfileName: contact.profileName, | 
					
						
							|  |  |  |         authorPhoneNumber: contact.phoneNumber, | 
					
						
							|  |  |  |         authorColor, | 
					
						
							|  |  |  |         conversationType: isGroup ? 'group' : 'direct', | 
					
						
							|  |  |  |         attachment: this.getPropsForAttachment(firstAttachment), | 
					
						
							|  |  |  |         quote: this.getPropsForQuote(), | 
					
						
							|  |  |  |         authorAvatarPath, | 
					
						
							| 
									
										
										
										
											2018-08-09 16:18:10 -07:00
										 |  |  |         isExpired: this.hasExpired, | 
					
						
							| 
									
										
										
										
											2018-07-09 14:29:13 -07:00
										 |  |  |         expirationLength, | 
					
						
							|  |  |  |         expirationTimestamp, | 
					
						
							|  |  |  |         onReply: () => this.trigger('reply', this), | 
					
						
							|  |  |  |         onRetrySend: () => this.retrySend(), | 
					
						
							|  |  |  |         onShowDetail: () => this.trigger('show-message-detail', this), | 
					
						
							|  |  |  |         onDelete: () => this.trigger('delete', this), | 
					
						
							|  |  |  |         onClickAttachment: () => | 
					
						
							|  |  |  |           this.trigger('show-lightbox', { | 
					
						
							|  |  |  |             attachment: firstAttachment, | 
					
						
							|  |  |  |             message: this, | 
					
						
							|  |  |  |           }), | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         onDownload: () => | 
					
						
							|  |  |  |           this.trigger('download', { | 
					
						
							|  |  |  |             attachment: firstAttachment, | 
					
						
							|  |  |  |             message: this, | 
					
						
							|  |  |  |           }), | 
					
						
							|  |  |  |       }; | 
					
						
							|  |  |  |     }, | 
					
						
							|  |  |  |     createNonBreakingLastSeparator(text) { | 
					
						
							|  |  |  |       if (!text) { | 
					
						
							|  |  |  |         return null; | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       const nbsp = '\xa0'; | 
					
						
							|  |  |  |       const regex = /(\S)( +)(\S+\s*)$/; | 
					
						
							|  |  |  |       return text.replace(regex, (match, start, spaces, end) => { | 
					
						
							|  |  |  |         const newSpaces = _.reduce( | 
					
						
							|  |  |  |           spaces, | 
					
						
							|  |  |  |           accumulator => accumulator + nbsp, | 
					
						
							|  |  |  |           '' | 
					
						
							|  |  |  |         ); | 
					
						
							|  |  |  |         return `${start}${newSpaces}${end}`; | 
					
						
							|  |  |  |       }); | 
					
						
							|  |  |  |     }, | 
					
						
							|  |  |  |     getPropsForEmbeddedContact() { | 
					
						
							|  |  |  |       const regionCode = storage.get('regionCode'); | 
					
						
							|  |  |  |       const { contactSelector } = Contact; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       const contacts = this.get('contact'); | 
					
						
							|  |  |  |       if (!contacts || !contacts.length) { | 
					
						
							|  |  |  |         return null; | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       const contact = contacts[0]; | 
					
						
							|  |  |  |       const firstNumber = | 
					
						
							|  |  |  |         contact.number && contact.number[0] && contact.number[0].value; | 
					
						
							|  |  |  |       const onSendMessage = firstNumber | 
					
						
							|  |  |  |         ? () => { | 
					
						
							|  |  |  |             this.trigger('open-conversation', firstNumber); | 
					
						
							|  |  |  |           } | 
					
						
							|  |  |  |         : null; | 
					
						
							|  |  |  |       const onClick = async () => { | 
					
						
							|  |  |  |         // First let's be sure that the signal account check is complete.
 | 
					
						
							|  |  |  |         await window.checkForSignalAccount(firstNumber); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         this.trigger('show-contact-detail', { | 
					
						
							|  |  |  |           contact, | 
					
						
							|  |  |  |           hasSignalAccount: window.hasSignalAccount(firstNumber), | 
					
						
							|  |  |  |         }); | 
					
						
							|  |  |  |       }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       // Would be nice to do this before render, on initial load of message
 | 
					
						
							|  |  |  |       if (!window.isSignalAccountCheckComplete(firstNumber)) { | 
					
						
							|  |  |  |         window.checkForSignalAccount(firstNumber).then(() => { | 
					
						
							|  |  |  |           this.trigger('change'); | 
					
						
							|  |  |  |         }); | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       return contactSelector(contact, { | 
					
						
							|  |  |  |         regionCode, | 
					
						
							|  |  |  |         getAbsoluteAttachmentPath, | 
					
						
							|  |  |  |         onSendMessage, | 
					
						
							|  |  |  |         onClick, | 
					
						
							|  |  |  |         hasSignalAccount: window.hasSignalAccount(firstNumber), | 
					
						
							|  |  |  |       }); | 
					
						
							|  |  |  |     }, | 
					
						
							| 
									
										
										
										
											2018-08-15 12:31:29 -07:00
										 |  |  |     processQuoteAttachment(attachment) { | 
					
						
							|  |  |  |       const { thumbnail } = attachment; | 
					
						
							|  |  |  |       const path = | 
					
						
							|  |  |  |         thumbnail && | 
					
						
							|  |  |  |         thumbnail.path && | 
					
						
							|  |  |  |         getAbsoluteAttachmentPath(thumbnail.path); | 
					
						
							|  |  |  |       const objectUrl = thumbnail && thumbnail.objectUrl; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       const thumbnailWithObjectUrl = | 
					
						
							|  |  |  |         !path && !objectUrl | 
					
						
							|  |  |  |           ? null | 
					
						
							|  |  |  |           : Object.assign({}, attachment.thumbnail || {}, { | 
					
						
							|  |  |  |               objectUrl: path || objectUrl, | 
					
						
							|  |  |  |             }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       return Object.assign({}, attachment, { | 
					
						
							|  |  |  |         isVoiceMessage: Signal.Types.Attachment.isVoiceMessage(attachment), | 
					
						
							|  |  |  |         thumbnail: thumbnailWithObjectUrl, | 
					
						
							|  |  |  |       }); | 
					
						
							|  |  |  |     }, | 
					
						
							| 
									
										
										
										
											2018-04-17 14:31:16 -07:00
										 |  |  |     getPropsForQuote() { | 
					
						
							|  |  |  |       const quote = this.get('quote'); | 
					
						
							|  |  |  |       if (!quote) { | 
					
						
							|  |  |  |         return null; | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-08-15 12:31:29 -07:00
										 |  |  |       const { author, id, referencedMessageNotFound } = quote; | 
					
						
							|  |  |  |       const contact = author && ConversationController.get(author); | 
					
						
							| 
									
										
										
										
											2018-04-17 14:31:16 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-07-09 14:29:13 -07:00
										 |  |  |       const authorPhoneNumber = author; | 
					
						
							| 
									
										
										
										
											2018-04-17 14:31:16 -07:00
										 |  |  |       const authorProfileName = contact ? contact.getProfileName() : null; | 
					
						
							| 
									
										
										
										
											2018-07-09 14:29:13 -07:00
										 |  |  |       const authorName = contact ? contact.getName() : null; | 
					
						
							| 
									
										
										
										
											2018-04-17 14:31:16 -07:00
										 |  |  |       const authorColor = contact ? contact.getColor() : 'grey'; | 
					
						
							| 
									
										
										
										
											2018-07-09 14:29:13 -07:00
										 |  |  |       const isFromMe = contact ? contact.id === this.OUR_NUMBER : false; | 
					
						
							| 
									
										
										
										
											2018-04-17 14:31:16 -07:00
										 |  |  |       const onClick = () => { | 
					
						
							| 
									
										
										
										
											2018-08-15 12:31:29 -07:00
										 |  |  |         this.trigger('scroll-to-message', { | 
					
						
							|  |  |  |           author, | 
					
						
							|  |  |  |           id, | 
					
						
							|  |  |  |           referencedMessageNotFound, | 
					
						
							|  |  |  |         }); | 
					
						
							| 
									
										
										
										
											2018-04-17 14:31:16 -07:00
										 |  |  |       }; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-07-17 20:40:19 -07:00
										 |  |  |       const firstAttachment = quote.attachments && quote.attachments[0]; | 
					
						
							| 
									
										
										
										
											2018-07-09 14:29:13 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-04-17 14:31:16 -07:00
										 |  |  |       return { | 
					
						
							| 
									
										
										
										
											2018-07-09 14:29:13 -07:00
										 |  |  |         text: this.createNonBreakingLastSeparator(quote.text), | 
					
						
							|  |  |  |         attachment: firstAttachment | 
					
						
							| 
									
										
										
										
											2018-08-15 12:31:29 -07:00
										 |  |  |           ? this.processQuoteAttachment(firstAttachment) | 
					
						
							| 
									
										
										
										
											2018-07-09 14:29:13 -07:00
										 |  |  |           : null, | 
					
						
							| 
									
										
										
										
											2018-04-17 14:31:16 -07:00
										 |  |  |         isFromMe, | 
					
						
							| 
									
										
										
										
											2018-07-09 14:29:13 -07:00
										 |  |  |         authorPhoneNumber, | 
					
						
							|  |  |  |         authorProfileName, | 
					
						
							|  |  |  |         authorName, | 
					
						
							|  |  |  |         authorColor, | 
					
						
							| 
									
										
										
										
											2018-08-15 12:31:29 -07:00
										 |  |  |         onClick, | 
					
						
							|  |  |  |         referencedMessageNotFound, | 
					
						
							| 
									
										
										
										
											2018-04-17 14:31:16 -07:00
										 |  |  |       }; | 
					
						
							|  |  |  |     }, | 
					
						
							| 
									
										
										
										
											2018-07-09 14:29:13 -07:00
										 |  |  |     getPropsForAttachment(attachment) { | 
					
						
							|  |  |  |       if (!attachment) { | 
					
						
							|  |  |  |         return null; | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       const { path, flags, size, screenshot, thumbnail } = attachment; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       return { | 
					
						
							|  |  |  |         ...attachment, | 
					
						
							|  |  |  |         fileSize: size ? filesize(size) : null, | 
					
						
							|  |  |  |         isVoiceMessage: | 
					
						
							|  |  |  |           flags && | 
					
						
							|  |  |  |           // eslint-disable-next-line no-bitwise
 | 
					
						
							|  |  |  |           flags & textsecure.protobuf.AttachmentPointer.Flags.VOICE_MESSAGE, | 
					
						
							|  |  |  |         url: getAbsoluteAttachmentPath(path), | 
					
						
							|  |  |  |         screenshot: screenshot | 
					
						
							|  |  |  |           ? { | 
					
						
							|  |  |  |               ...screenshot, | 
					
						
							|  |  |  |               url: getAbsoluteAttachmentPath(screenshot.path), | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |           : null, | 
					
						
							|  |  |  |         thumbnail: thumbnail | 
					
						
							|  |  |  |           ? { | 
					
						
							|  |  |  |               ...thumbnail, | 
					
						
							|  |  |  |               url: getAbsoluteAttachmentPath(thumbnail.path), | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |           : null, | 
					
						
							|  |  |  |       }; | 
					
						
							|  |  |  |     }, | 
					
						
							|  |  |  |     getPropsForMessageDetail() { | 
					
						
							|  |  |  |       const newIdentity = i18n('newIdentity'); | 
					
						
							|  |  |  |       const OUTGOING_KEY_ERROR = 'OutgoingIdentityKeyError'; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       // Older messages don't have the recipients included on the message, so we fall
 | 
					
						
							|  |  |  |       //   back to the conversation's current recipients
 | 
					
						
							|  |  |  |       const phoneNumbers = this.isIncoming() | 
					
						
							|  |  |  |         ? [this.get('source')] | 
					
						
							|  |  |  |         : this.get('recipients') || this.conversation.getRecipients(); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       // This will make the error message for outgoing key errors a bit nicer
 | 
					
						
							|  |  |  |       const allErrors = (this.get('errors') || []).map(error => { | 
					
						
							|  |  |  |         if (error.name === OUTGOING_KEY_ERROR) { | 
					
						
							|  |  |  |           // eslint-disable-next-line no-param-reassign
 | 
					
						
							|  |  |  |           error.message = newIdentity; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         return error; | 
					
						
							|  |  |  |       }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       // If an error has a specific number it's associated with, we'll show it next to
 | 
					
						
							|  |  |  |       //   that contact. Otherwise, it will be a standalone entry.
 | 
					
						
							|  |  |  |       const errors = _.reject(allErrors, error => Boolean(error.number)); | 
					
						
							|  |  |  |       const errorsGroupedById = _.groupBy(allErrors, 'number'); | 
					
						
							|  |  |  |       const finalContacts = (phoneNumbers || []).map(id => { | 
					
						
							|  |  |  |         const errorsForContact = errorsGroupedById[id]; | 
					
						
							|  |  |  |         const isOutgoingKeyError = Boolean( | 
					
						
							|  |  |  |           _.find(errorsForContact, error => error.name === OUTGOING_KEY_ERROR) | 
					
						
							|  |  |  |         ); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         return { | 
					
						
							|  |  |  |           ...this.findAndFormatContact(id), | 
					
						
							|  |  |  |           status: this.getStatus(id), | 
					
						
							|  |  |  |           errors: errorsForContact, | 
					
						
							|  |  |  |           isOutgoingKeyError, | 
					
						
							|  |  |  |           onSendAnyway: () => | 
					
						
							|  |  |  |             this.trigger('force-send', { | 
					
						
							|  |  |  |               contact: this.findContact(id), | 
					
						
							|  |  |  |               message: this, | 
					
						
							|  |  |  |             }), | 
					
						
							|  |  |  |           onShowSafetyNumber: () => | 
					
						
							|  |  |  |             this.trigger('show-identity', this.findContact(id)), | 
					
						
							|  |  |  |         }; | 
					
						
							|  |  |  |       }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       // The prefix created here ensures that contacts with errors are listed
 | 
					
						
							|  |  |  |       //   first; otherwise it's alphabetical
 | 
					
						
							|  |  |  |       const sortedContacts = _.sortBy( | 
					
						
							|  |  |  |         finalContacts, | 
					
						
							|  |  |  |         contact => `${contact.errors ? '0' : '1'}${contact.title}` | 
					
						
							|  |  |  |       ); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       return { | 
					
						
							|  |  |  |         sentAt: this.get('sent_at'), | 
					
						
							|  |  |  |         receivedAt: this.get('received_at'), | 
					
						
							|  |  |  |         message: { | 
					
						
							|  |  |  |           ...this.getPropsForMessage(), | 
					
						
							|  |  |  |           disableMenu: true, | 
					
						
							|  |  |  |           // To ensure that group avatar doesn't show up
 | 
					
						
							|  |  |  |           conversationType: 'direct', | 
					
						
							|  |  |  |         }, | 
					
						
							|  |  |  |         errors, | 
					
						
							|  |  |  |         contacts: sortedContacts, | 
					
						
							|  |  |  |       }; | 
					
						
							|  |  |  |     }, | 
					
						
							| 
									
										
										
										
											2018-07-26 18:13:56 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  |     // One caller today: event handler for the 'Retry Send' entry in triple-dot menu
 | 
					
						
							|  |  |  |     async retrySend() { | 
					
						
							| 
									
										
										
										
											2018-08-07 12:33:56 -07:00
										 |  |  |       if (!textsecure.messaging) { | 
					
						
							|  |  |  |         window.log.error('retrySend: Cannot retry since we are offline!'); | 
					
						
							|  |  |  |         return null; | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-07-26 18:13:56 -07:00
										 |  |  |       const [retries, errors] = _.partition( | 
					
						
							| 
									
										
										
										
											2018-07-09 14:29:13 -07:00
										 |  |  |         this.get('errors'), | 
					
						
							|  |  |  |         this.isReplayableError.bind(this) | 
					
						
							|  |  |  |       ); | 
					
						
							| 
									
										
										
										
											2018-07-26 18:13:56 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  |       // Remove the errors that aren't replayable
 | 
					
						
							|  |  |  |       this.set({ errors }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       const profileKey = null; | 
					
						
							|  |  |  |       const numbers = retries.map(retry => retry.number); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       if (!numbers.length) { | 
					
						
							|  |  |  |         window.log.error( | 
					
						
							|  |  |  |           'retrySend: Attempted to retry, but no numbers to send to!' | 
					
						
							|  |  |  |         ); | 
					
						
							|  |  |  |         return null; | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       const attachmentsWithData = await Promise.all( | 
					
						
							|  |  |  |         (this.get('attachments') || []).map(loadAttachmentData) | 
					
						
							|  |  |  |       ); | 
					
						
							|  |  |  |       const quoteWithData = await loadQuoteData(this.get('quote')); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       const conversation = this.getConversation(); | 
					
						
							|  |  |  |       let promise; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       if (conversation.isPrivate()) { | 
					
						
							|  |  |  |         const [number] = numbers; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         promise = textsecure.messaging.sendMessageToNumber( | 
					
						
							|  |  |  |           number, | 
					
						
							|  |  |  |           this.get('body'), | 
					
						
							|  |  |  |           attachmentsWithData, | 
					
						
							|  |  |  |           quoteWithData, | 
					
						
							|  |  |  |           this.get('sent_at'), | 
					
						
							|  |  |  |           this.get('expireTimer'), | 
					
						
							|  |  |  |           profileKey | 
					
						
							|  |  |  |         ); | 
					
						
							|  |  |  |       } else { | 
					
						
							|  |  |  |         // Because this is a partial group send, we manually construct the request like
 | 
					
						
							|  |  |  |         //   sendMessageToGroup does.
 | 
					
						
							|  |  |  |         promise = textsecure.messaging.sendMessage({ | 
					
						
							|  |  |  |           recipients: numbers, | 
					
						
							|  |  |  |           body: this.get('body'), | 
					
						
							|  |  |  |           timestamp: this.get('sent_at'), | 
					
						
							|  |  |  |           attachments: attachmentsWithData, | 
					
						
							|  |  |  |           quote: quoteWithData, | 
					
						
							|  |  |  |           needsSync: !this.get('synced'), | 
					
						
							|  |  |  |           expireTimer: this.get('expireTimer'), | 
					
						
							|  |  |  |           profileKey, | 
					
						
							|  |  |  |           group: { | 
					
						
							|  |  |  |             id: this.get('conversationId'), | 
					
						
							|  |  |  |             type: textsecure.protobuf.GroupContext.Type.DELIVER, | 
					
						
							|  |  |  |           }, | 
					
						
							|  |  |  |         }); | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       return this.send(promise); | 
					
						
							| 
									
										
										
										
											2018-07-09 14:29:13 -07:00
										 |  |  |     }, | 
					
						
							| 
									
										
										
										
											2018-07-26 18:13:56 -07:00
										 |  |  |     isReplayableError(e) { | 
					
						
							|  |  |  |       return ( | 
					
						
							|  |  |  |         e.name === 'MessageError' || | 
					
						
							|  |  |  |         e.name === 'OutgoingMessageError' || | 
					
						
							|  |  |  |         e.name === 'SendMessageNetworkError' || | 
					
						
							|  |  |  |         e.name === 'SignedPreKeyRotationError' || | 
					
						
							|  |  |  |         e.name === 'OutgoingIdentityKeyError' | 
					
						
							|  |  |  |       ); | 
					
						
							|  |  |  |     }, | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     // Called when the user ran into an error with a specific user, wants to send to them
 | 
					
						
							|  |  |  |     //   One caller today: ConversationView.forceSend()
 | 
					
						
							|  |  |  |     async resend(number) { | 
					
						
							|  |  |  |       const error = this.removeOutgoingErrors(number); | 
					
						
							|  |  |  |       if (error) { | 
					
						
							|  |  |  |         const profileKey = null; | 
					
						
							|  |  |  |         const attachmentsWithData = await Promise.all( | 
					
						
							|  |  |  |           (this.get('attachments') || []).map(loadAttachmentData) | 
					
						
							|  |  |  |         ); | 
					
						
							|  |  |  |         const quoteWithData = await loadQuoteData(this.get('quote')); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         const promise = textsecure.messaging.sendMessageToNumber( | 
					
						
							|  |  |  |           number, | 
					
						
							|  |  |  |           this.get('body'), | 
					
						
							|  |  |  |           attachmentsWithData, | 
					
						
							|  |  |  |           quoteWithData, | 
					
						
							|  |  |  |           this.get('sent_at'), | 
					
						
							|  |  |  |           this.get('expireTimer'), | 
					
						
							|  |  |  |           profileKey | 
					
						
							|  |  |  |         ); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         this.send(promise); | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |     }, | 
					
						
							|  |  |  |     removeOutgoingErrors(number) { | 
					
						
							|  |  |  |       const errors = _.partition( | 
					
						
							|  |  |  |         this.get('errors'), | 
					
						
							|  |  |  |         e => | 
					
						
							|  |  |  |           e.number === number && | 
					
						
							|  |  |  |           (e.name === 'MessageError' || | 
					
						
							|  |  |  |             e.name === 'OutgoingMessageError' || | 
					
						
							|  |  |  |             e.name === 'SendMessageNetworkError' || | 
					
						
							|  |  |  |             e.name === 'SignedPreKeyRotationError' || | 
					
						
							|  |  |  |             e.name === 'OutgoingIdentityKeyError') | 
					
						
							|  |  |  |       ); | 
					
						
							|  |  |  |       this.set({ errors: errors[1] }); | 
					
						
							|  |  |  |       return errors[0][0]; | 
					
						
							|  |  |  |     }, | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-04-10 16:51:48 -07:00
										 |  |  |     getConversation() { | 
					
						
							|  |  |  |       // This needs to be an unsafe call, because this method is called during
 | 
					
						
							|  |  |  |       //   initial module setup. We may be in the middle of the initial fetch to
 | 
					
						
							|  |  |  |       //   the database.
 | 
					
						
							|  |  |  |       return ConversationController.getUnsafe(this.get('conversationId')); | 
					
						
							|  |  |  |     }, | 
					
						
							| 
									
										
										
										
											2018-07-09 14:29:13 -07:00
										 |  |  |     getIncomingContact() { | 
					
						
							| 
									
										
										
										
											2018-04-10 16:51:48 -07:00
										 |  |  |       if (!this.isIncoming()) { | 
					
						
							| 
									
										
										
										
											2018-07-09 14:29:13 -07:00
										 |  |  |         return null; | 
					
						
							| 
									
										
										
										
											2018-04-10 16:51:48 -07:00
										 |  |  |       } | 
					
						
							| 
									
										
										
										
											2018-07-09 14:29:13 -07:00
										 |  |  |       const source = this.get('source'); | 
					
						
							|  |  |  |       if (!source) { | 
					
						
							|  |  |  |         return null; | 
					
						
							| 
									
										
										
										
											2018-04-10 16:51:48 -07:00
										 |  |  |       } | 
					
						
							| 
									
										
										
										
											2018-07-09 14:29:13 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  |       return ConversationController.getOrCreate(source, 'private'); | 
					
						
							| 
									
										
										
										
											2018-04-10 16:51:48 -07:00
										 |  |  |     }, | 
					
						
							| 
									
										
										
										
											2018-08-15 12:31:29 -07:00
										 |  |  |     getQuoteContact() { | 
					
						
							|  |  |  |       const quote = this.get('quote'); | 
					
						
							|  |  |  |       if (!quote) { | 
					
						
							|  |  |  |         return null; | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |       const { author } = quote; | 
					
						
							|  |  |  |       if (!author) { | 
					
						
							|  |  |  |         return null; | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       return ConversationController.get(author); | 
					
						
							|  |  |  |     }, | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-07-09 14:29:13 -07:00
										 |  |  |     getSource() { | 
					
						
							|  |  |  |       if (this.isIncoming()) { | 
					
						
							|  |  |  |         return this.get('source'); | 
					
						
							| 
									
										
										
										
											2018-04-10 16:51:48 -07:00
										 |  |  |       } | 
					
						
							| 
									
										
										
										
											2018-07-09 14:29:13 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  |       return this.OUR_NUMBER; | 
					
						
							|  |  |  |     }, | 
					
						
							|  |  |  |     getContact() { | 
					
						
							|  |  |  |       return ConversationController.getOrCreate(this.getSource(), 'private'); | 
					
						
							| 
									
										
										
										
											2018-04-10 16:51:48 -07:00
										 |  |  |     }, | 
					
						
							|  |  |  |     isOutgoing() { | 
					
						
							|  |  |  |       return this.get('type') === 'outgoing'; | 
					
						
							|  |  |  |     }, | 
					
						
							|  |  |  |     hasErrors() { | 
					
						
							|  |  |  |       return _.size(this.get('errors')) > 0; | 
					
						
							|  |  |  |     }, | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     getStatus(number) { | 
					
						
							|  |  |  |       const readBy = this.get('read_by') || []; | 
					
						
							|  |  |  |       if (readBy.indexOf(number) >= 0) { | 
					
						
							|  |  |  |         return 'read'; | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |       const deliveredTo = this.get('delivered_to') || []; | 
					
						
							|  |  |  |       if (deliveredTo.indexOf(number) >= 0) { | 
					
						
							|  |  |  |         return 'delivered'; | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |       const sentTo = this.get('sent_to') || []; | 
					
						
							|  |  |  |       if (sentTo.indexOf(number) >= 0) { | 
					
						
							|  |  |  |         return 'sent'; | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       return null; | 
					
						
							|  |  |  |     }, | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     send(promise) { | 
					
						
							|  |  |  |       this.trigger('pending'); | 
					
						
							| 
									
										
										
										
											2018-04-27 17:25:04 -04:00
										 |  |  |       return promise | 
					
						
							| 
									
										
										
										
											2018-07-25 15:02:27 -07:00
										 |  |  |         .then(async result => { | 
					
						
							| 
									
										
										
										
											2018-04-27 17:25:04 -04:00
										 |  |  |           this.trigger('done'); | 
					
						
							| 
									
										
										
										
											2018-07-26 18:13:56 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  |           // This is used by sendSyncMessage, then set to null
 | 
					
						
							| 
									
										
										
										
											2018-04-27 17:25:04 -04:00
										 |  |  |           if (result.dataMessage) { | 
					
						
							|  |  |  |             this.set({ dataMessage: result.dataMessage }); | 
					
						
							| 
									
										
										
										
											2018-04-10 16:51:48 -07:00
										 |  |  |           } | 
					
						
							| 
									
										
										
										
											2018-07-26 18:13:56 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-04-27 17:25:04 -04:00
										 |  |  |           const sentTo = this.get('sent_to') || []; | 
					
						
							| 
									
										
										
										
											2018-07-25 15:02:27 -07:00
										 |  |  |           this.set({ | 
					
						
							| 
									
										
										
										
											2018-04-27 17:25:04 -04:00
										 |  |  |             sent_to: _.union(sentTo, result.successfulNumbers), | 
					
						
							|  |  |  |             sent: true, | 
					
						
							| 
									
										
										
										
											2018-08-06 11:33:51 -07:00
										 |  |  |             expirationStartTimestamp: Date.now(), | 
					
						
							| 
									
										
										
										
											2018-04-27 17:25:04 -04:00
										 |  |  |           }); | 
					
						
							| 
									
										
										
										
											2018-07-25 15:02:27 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  |           await window.Signal.Data.saveMessage(this.attributes, { | 
					
						
							|  |  |  |             Message: Whisper.Message, | 
					
						
							|  |  |  |           }); | 
					
						
							| 
									
										
										
										
											2018-07-17 20:25:55 -07:00
										 |  |  |           this.trigger('sent', this); | 
					
						
							| 
									
										
										
										
											2018-04-27 17:25:04 -04:00
										 |  |  |           this.sendSyncMessage(); | 
					
						
							|  |  |  |         }) | 
					
						
							|  |  |  |         .catch(result => { | 
					
						
							|  |  |  |           this.trigger('done'); | 
					
						
							| 
									
										
										
										
											2018-07-26 18:13:56 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-04-27 17:25:04 -04:00
										 |  |  |           if (result.dataMessage) { | 
					
						
							|  |  |  |             this.set({ dataMessage: result.dataMessage }); | 
					
						
							| 
									
										
										
										
											2018-04-10 16:51:48 -07:00
										 |  |  |           } | 
					
						
							| 
									
										
										
										
											2018-04-27 17:25:04 -04:00
										 |  |  | 
 | 
					
						
							|  |  |  |           let promises = []; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |           if (result instanceof Error) { | 
					
						
							|  |  |  |             this.saveErrors(result); | 
					
						
							|  |  |  |             if (result.name === 'SignedPreKeyRotationError') { | 
					
						
							|  |  |  |               promises.push(getAccountManager().rotateSignedPreKey()); | 
					
						
							|  |  |  |             } else if (result.name === 'OutgoingIdentityKeyError') { | 
					
						
							|  |  |  |               const c = ConversationController.get(result.number); | 
					
						
							| 
									
										
										
										
											2018-04-10 16:51:48 -07:00
										 |  |  |               promises.push(c.getProfiles()); | 
					
						
							|  |  |  |             } | 
					
						
							| 
									
										
										
										
											2018-04-27 17:25:04 -04:00
										 |  |  |           } else { | 
					
						
							|  |  |  |             if (result.successfulNumbers.length > 0) { | 
					
						
							|  |  |  |               const sentTo = this.get('sent_to') || []; | 
					
						
							| 
									
										
										
										
											2018-08-06 11:33:51 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-09-04 16:19:51 -07:00
										 |  |  |               // In groups, we don't treat unregistered users as a user-visible
 | 
					
						
							|  |  |  |               //   error. The message will look successful, but the details
 | 
					
						
							|  |  |  |               //   screen will show that we didn't send to these unregistered users.
 | 
					
						
							|  |  |  |               const filteredErrors = _.reject( | 
					
						
							|  |  |  |                 result.errors, | 
					
						
							|  |  |  |                 error => error.name === 'UnregisteredUserError' | 
					
						
							|  |  |  |               ); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |               // We don't start the expiration timer if there are real errors
 | 
					
						
							|  |  |  |               //   left after filtering out all of the unregistered user errors.
 | 
					
						
							|  |  |  |               const expirationStartTimestamp = filteredErrors.length | 
					
						
							|  |  |  |                 ? null | 
					
						
							|  |  |  |                 : Date.now(); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |               this.saveErrors(filteredErrors); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-04-27 17:25:04 -04:00
										 |  |  |               this.set({ | 
					
						
							|  |  |  |                 sent_to: _.union(sentTo, result.successfulNumbers), | 
					
						
							|  |  |  |                 sent: true, | 
					
						
							| 
									
										
										
										
											2018-09-04 16:19:51 -07:00
										 |  |  |                 expirationStartTimestamp, | 
					
						
							| 
									
										
										
										
											2018-04-27 17:25:04 -04:00
										 |  |  |               }); | 
					
						
							|  |  |  |               promises.push(this.sendSyncMessage()); | 
					
						
							| 
									
										
										
										
											2018-09-04 16:19:51 -07:00
										 |  |  |             } else { | 
					
						
							|  |  |  |               this.saveErrors(result.errors); | 
					
						
							| 
									
										
										
										
											2018-04-27 17:25:04 -04:00
										 |  |  |             } | 
					
						
							|  |  |  |             promises = promises.concat( | 
					
						
							|  |  |  |               _.map(result.errors, error => { | 
					
						
							|  |  |  |                 if (error.name === 'OutgoingIdentityKeyError') { | 
					
						
							|  |  |  |                   const c = ConversationController.get(error.number); | 
					
						
							|  |  |  |                   promises.push(c.getProfiles()); | 
					
						
							|  |  |  |                 } | 
					
						
							|  |  |  |               }) | 
					
						
							|  |  |  |             ); | 
					
						
							|  |  |  |           } | 
					
						
							| 
									
										
										
										
											2015-09-13 20:25:04 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-07-26 18:13:56 -07:00
										 |  |  |           this.trigger('send-error', this.get('errors')); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |           return Promise.all(promises); | 
					
						
							| 
									
										
										
										
											2018-04-10 16:51:48 -07:00
										 |  |  |         }); | 
					
						
							|  |  |  |     }, | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     someRecipientsFailed() { | 
					
						
							|  |  |  |       const c = this.getConversation(); | 
					
						
							|  |  |  |       if (!c || c.isPrivate()) { | 
					
						
							|  |  |  |         return false; | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       const recipients = c.contactCollection.length - 1; | 
					
						
							|  |  |  |       const errors = this.get('errors'); | 
					
						
							|  |  |  |       if (!errors) { | 
					
						
							|  |  |  |         return false; | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       if (errors.length > 0 && recipients > 0 && errors.length < recipients) { | 
					
						
							|  |  |  |         return true; | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       return false; | 
					
						
							|  |  |  |     }, | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     sendSyncMessage() { | 
					
						
							|  |  |  |       this.syncPromise = this.syncPromise || Promise.resolve(); | 
					
						
							|  |  |  |       this.syncPromise = this.syncPromise.then(() => { | 
					
						
							|  |  |  |         const dataMessage = this.get('dataMessage'); | 
					
						
							|  |  |  |         if (this.get('synced') || !dataMessage) { | 
					
						
							|  |  |  |           return Promise.resolve(); | 
					
						
							|  |  |  |         } | 
					
						
							| 
									
										
										
										
											2018-04-27 17:25:04 -04:00
										 |  |  |         return textsecure.messaging | 
					
						
							|  |  |  |           .sendSyncMessage( | 
					
						
							|  |  |  |             dataMessage, | 
					
						
							|  |  |  |             this.get('sent_at'), | 
					
						
							|  |  |  |             this.get('destination'), | 
					
						
							|  |  |  |             this.get('expirationStartTimestamp') | 
					
						
							|  |  |  |           ) | 
					
						
							|  |  |  |           .then(() => { | 
					
						
							| 
									
										
										
										
											2018-07-25 15:02:27 -07:00
										 |  |  |             this.set({ | 
					
						
							|  |  |  |               synced: true, | 
					
						
							|  |  |  |               dataMessage: null, | 
					
						
							|  |  |  |             }); | 
					
						
							|  |  |  |             return window.Signal.Data.saveMessage(this.attributes, { | 
					
						
							|  |  |  |               Message: Whisper.Message, | 
					
						
							|  |  |  |             }); | 
					
						
							| 
									
										
										
										
											2018-04-27 17:25:04 -04:00
										 |  |  |           }); | 
					
						
							| 
									
										
										
										
											2018-04-10 16:51:48 -07:00
										 |  |  |       }); | 
					
						
							|  |  |  |     }, | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-07-25 15:02:27 -07:00
										 |  |  |     async saveErrors(providedErrors) { | 
					
						
							| 
									
										
										
										
											2018-04-10 16:51:48 -07:00
										 |  |  |       let errors = providedErrors; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       if (!(errors instanceof Array)) { | 
					
						
							|  |  |  |         errors = [errors]; | 
					
						
							|  |  |  |       } | 
					
						
							| 
									
										
										
										
											2018-04-27 17:25:04 -04:00
										 |  |  |       errors.forEach(e => { | 
					
						
							| 
									
										
										
										
											2018-07-21 12:00:08 -07:00
										 |  |  |         window.log.error( | 
					
						
							| 
									
										
										
										
											2018-04-10 16:51:48 -07:00
										 |  |  |           'Message.saveErrors:', | 
					
						
							|  |  |  |           e && e.reason ? e.reason : null, | 
					
						
							|  |  |  |           e && e.stack ? e.stack : e | 
					
						
							|  |  |  |         ); | 
					
						
							|  |  |  |       }); | 
					
						
							| 
									
										
										
										
											2018-04-27 17:25:04 -04:00
										 |  |  |       errors = errors.map(e => { | 
					
						
							|  |  |  |         if ( | 
					
						
							|  |  |  |           e.constructor === Error || | 
					
						
							|  |  |  |           e.constructor === TypeError || | 
					
						
							|  |  |  |           e.constructor === ReferenceError | 
					
						
							|  |  |  |         ) { | 
					
						
							| 
									
										
										
										
											2018-04-10 16:51:48 -07:00
										 |  |  |           return _.pick(e, 'name', 'message', 'code', 'number', 'reason'); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |         return e; | 
					
						
							|  |  |  |       }); | 
					
						
							|  |  |  |       errors = errors.concat(this.get('errors') || []); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-07-25 15:02:27 -07:00
										 |  |  |       this.set({ errors }); | 
					
						
							|  |  |  |       await window.Signal.Data.saveMessage(this.attributes, { | 
					
						
							|  |  |  |         Message: Whisper.Message, | 
					
						
							|  |  |  |       }); | 
					
						
							| 
									
										
										
										
											2018-04-10 16:51:48 -07:00
										 |  |  |     }, | 
					
						
							|  |  |  |     hasNetworkError() { | 
					
						
							|  |  |  |       const error = _.find( | 
					
						
							|  |  |  |         this.get('errors'), | 
					
						
							| 
									
										
										
										
											2018-04-27 17:25:04 -04:00
										 |  |  |         e => | 
					
						
							|  |  |  |           e.name === 'MessageError' || | 
					
						
							|  |  |  |           e.name === 'OutgoingMessageError' || | 
					
						
							|  |  |  |           e.name === 'SendMessageNetworkError' || | 
					
						
							|  |  |  |           e.name === 'SignedPreKeyRotationError' | 
					
						
							| 
									
										
										
										
											2018-04-10 16:51:48 -07:00
										 |  |  |       ); | 
					
						
							|  |  |  |       return !!error; | 
					
						
							|  |  |  |     }, | 
					
						
							|  |  |  |     handleDataMessage(dataMessage, confirm) { | 
					
						
							|  |  |  |       // This function is called from the background script in a few scenarios:
 | 
					
						
							|  |  |  |       //   1. on an incoming message
 | 
					
						
							|  |  |  |       //   2. on a sent message sync'd from another device
 | 
					
						
							|  |  |  |       //   3. in rare cases, an incoming message can be retried, though it will
 | 
					
						
							|  |  |  |       //      still go through one of the previous two codepaths
 | 
					
						
							|  |  |  |       const message = this; | 
					
						
							|  |  |  |       const source = message.get('source'); | 
					
						
							|  |  |  |       const type = message.get('type'); | 
					
						
							|  |  |  |       let conversationId = message.get('conversationId'); | 
					
						
							|  |  |  |       if (dataMessage.group) { | 
					
						
							|  |  |  |         conversationId = dataMessage.group.id; | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |       const GROUP_TYPES = textsecure.protobuf.GroupContext.Type; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       const conversation = ConversationController.get(conversationId); | 
					
						
							| 
									
										
										
										
											2018-07-25 15:02:27 -07:00
										 |  |  |       return conversation.queueJob(async () => { | 
					
						
							|  |  |  |         try { | 
					
						
							|  |  |  |           const now = new Date().getTime(); | 
					
						
							|  |  |  |           let attributes = { type: 'private' }; | 
					
						
							|  |  |  |           if (dataMessage.group) { | 
					
						
							|  |  |  |             let groupUpdate = null; | 
					
						
							|  |  |  |             attributes = { | 
					
						
							|  |  |  |               type: 'group', | 
					
						
							|  |  |  |               groupId: dataMessage.group.id, | 
					
						
							|  |  |  |             }; | 
					
						
							|  |  |  |             if (dataMessage.group.type === GROUP_TYPES.UPDATE) { | 
					
						
							| 
									
										
										
										
											2018-04-27 17:25:04 -04:00
										 |  |  |               attributes = { | 
					
						
							|  |  |  |                 type: 'group', | 
					
						
							|  |  |  |                 groupId: dataMessage.group.id, | 
					
						
							| 
									
										
										
										
											2018-07-25 15:02:27 -07:00
										 |  |  |                 name: dataMessage.group.name, | 
					
						
							|  |  |  |                 avatar: dataMessage.group.avatar, | 
					
						
							|  |  |  |                 members: _.union( | 
					
						
							|  |  |  |                   dataMessage.group.members, | 
					
						
							| 
									
										
										
										
											2018-04-27 17:25:04 -04:00
										 |  |  |                   conversation.get('members') | 
					
						
							| 
									
										
										
										
											2018-07-25 15:02:27 -07:00
										 |  |  |                 ), | 
					
						
							|  |  |  |               }; | 
					
						
							|  |  |  |               groupUpdate = | 
					
						
							|  |  |  |                 conversation.changedAttributes( | 
					
						
							|  |  |  |                   _.pick(dataMessage.group, 'name', 'avatar') | 
					
						
							|  |  |  |                 ) || {}; | 
					
						
							|  |  |  |               const difference = _.difference( | 
					
						
							|  |  |  |                 attributes.members, | 
					
						
							|  |  |  |                 conversation.get('members') | 
					
						
							|  |  |  |               ); | 
					
						
							|  |  |  |               if (difference.length > 0) { | 
					
						
							|  |  |  |                 groupUpdate.joined = difference; | 
					
						
							| 
									
										
										
										
											2018-04-27 17:25:04 -04:00
										 |  |  |               } | 
					
						
							| 
									
										
										
										
											2018-07-25 15:02:27 -07:00
										 |  |  |               if (conversation.get('left')) { | 
					
						
							|  |  |  |                 window.log.warn('re-added to a left group'); | 
					
						
							|  |  |  |                 attributes.left = false; | 
					
						
							| 
									
										
										
										
											2018-04-27 17:25:04 -04:00
										 |  |  |               } | 
					
						
							| 
									
										
										
										
											2018-07-25 15:02:27 -07:00
										 |  |  |             } else if (dataMessage.group.type === GROUP_TYPES.QUIT) { | 
					
						
							|  |  |  |               if (source === textsecure.storage.user.getNumber()) { | 
					
						
							|  |  |  |                 attributes.left = true; | 
					
						
							|  |  |  |                 groupUpdate = { left: 'You' }; | 
					
						
							|  |  |  |               } else { | 
					
						
							|  |  |  |                 groupUpdate = { left: source }; | 
					
						
							|  |  |  |               } | 
					
						
							|  |  |  |               attributes.members = _.without( | 
					
						
							|  |  |  |                 conversation.get('members'), | 
					
						
							|  |  |  |                 source | 
					
						
							| 
									
										
										
										
											2018-04-27 17:25:04 -04:00
										 |  |  |               ); | 
					
						
							| 
									
										
										
										
											2018-04-10 16:51:48 -07:00
										 |  |  |             } | 
					
						
							| 
									
										
										
										
											2018-04-27 17:25:04 -04:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-07-25 15:02:27 -07:00
										 |  |  |             if (groupUpdate !== null) { | 
					
						
							|  |  |  |               message.set({ group_update: groupUpdate }); | 
					
						
							| 
									
										
										
										
											2017-06-15 12:27:41 -07:00
										 |  |  |             } | 
					
						
							| 
									
										
										
										
											2018-07-25 15:02:27 -07:00
										 |  |  |           } | 
					
						
							|  |  |  |           message.set({ | 
					
						
							|  |  |  |             attachments: dataMessage.attachments, | 
					
						
							|  |  |  |             body: dataMessage.body, | 
					
						
							|  |  |  |             contact: dataMessage.contact, | 
					
						
							|  |  |  |             conversationId: conversation.id, | 
					
						
							|  |  |  |             decrypted_at: now, | 
					
						
							|  |  |  |             errors: [], | 
					
						
							|  |  |  |             flags: dataMessage.flags, | 
					
						
							|  |  |  |             hasAttachments: dataMessage.hasAttachments, | 
					
						
							|  |  |  |             hasFileAttachments: dataMessage.hasFileAttachments, | 
					
						
							|  |  |  |             hasVisualMediaAttachments: dataMessage.hasVisualMediaAttachments, | 
					
						
							|  |  |  |             quote: dataMessage.quote, | 
					
						
							|  |  |  |             schemaVersion: dataMessage.schemaVersion, | 
					
						
							|  |  |  |           }); | 
					
						
							|  |  |  |           if (type === 'outgoing') { | 
					
						
							|  |  |  |             const receipts = Whisper.DeliveryReceipts.forMessage( | 
					
						
							|  |  |  |               conversation, | 
					
						
							|  |  |  |               message | 
					
						
							|  |  |  |             ); | 
					
						
							|  |  |  |             receipts.forEach(() => | 
					
						
							|  |  |  |               message.set({ | 
					
						
							|  |  |  |                 delivered: (message.get('delivered') || 0) + 1, | 
					
						
							|  |  |  |               }) | 
					
						
							|  |  |  |             ); | 
					
						
							|  |  |  |           } | 
					
						
							|  |  |  |           attributes.active_at = now; | 
					
						
							|  |  |  |           conversation.set(attributes); | 
					
						
							| 
									
										
										
										
											2015-12-04 12:02:19 -08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-07-25 15:02:27 -07:00
										 |  |  |           if (message.isExpirationTimerUpdate()) { | 
					
						
							|  |  |  |             message.set({ | 
					
						
							|  |  |  |               expirationTimerUpdate: { | 
					
						
							|  |  |  |                 source, | 
					
						
							|  |  |  |                 expireTimer: dataMessage.expireTimer, | 
					
						
							|  |  |  |               }, | 
					
						
							|  |  |  |             }); | 
					
						
							|  |  |  |             conversation.set({ expireTimer: dataMessage.expireTimer }); | 
					
						
							|  |  |  |           } else if (dataMessage.expireTimer) { | 
					
						
							|  |  |  |             message.set({ expireTimer: dataMessage.expireTimer }); | 
					
						
							|  |  |  |           } | 
					
						
							| 
									
										
										
										
											2015-11-19 16:57:48 -08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-07-25 15:02:27 -07:00
										 |  |  |           // NOTE: Remove once the above uses
 | 
					
						
							|  |  |  |           // `Conversation::updateExpirationTimer`:
 | 
					
						
							|  |  |  |           const { expireTimer } = dataMessage; | 
					
						
							|  |  |  |           const shouldLogExpireTimerChange = | 
					
						
							|  |  |  |             message.isExpirationTimerUpdate() || expireTimer; | 
					
						
							|  |  |  |           if (shouldLogExpireTimerChange) { | 
					
						
							|  |  |  |             window.log.info("Update conversation 'expireTimer'", { | 
					
						
							|  |  |  |               id: conversation.idForLogging(), | 
					
						
							|  |  |  |               expireTimer, | 
					
						
							|  |  |  |               source: 'handleDataMessage', | 
					
						
							|  |  |  |             }); | 
					
						
							|  |  |  |           } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |           if (!message.isEndSession()) { | 
					
						
							|  |  |  |             if (dataMessage.expireTimer) { | 
					
						
							|  |  |  |               if (dataMessage.expireTimer !== conversation.get('expireTimer')) { | 
					
						
							| 
									
										
										
										
											2018-04-27 17:25:04 -04:00
										 |  |  |                 conversation.updateExpirationTimer( | 
					
						
							| 
									
										
										
										
											2018-07-25 15:02:27 -07:00
										 |  |  |                   dataMessage.expireTimer, | 
					
						
							| 
									
										
										
										
											2018-04-27 17:25:04 -04:00
										 |  |  |                   source, | 
					
						
							| 
									
										
										
										
											2018-07-25 15:02:27 -07:00
										 |  |  |                   message.get('received_at'), | 
					
						
							|  |  |  |                   { | 
					
						
							|  |  |  |                     fromGroupUpdate: message.isGroupUpdate(), | 
					
						
							|  |  |  |                   } | 
					
						
							| 
									
										
										
										
											2018-04-27 17:25:04 -04:00
										 |  |  |                 ); | 
					
						
							|  |  |  |               } | 
					
						
							| 
									
										
										
										
											2018-07-25 15:02:27 -07:00
										 |  |  |             } else if ( | 
					
						
							|  |  |  |               conversation.get('expireTimer') && | 
					
						
							|  |  |  |               // We only turn off timers if it's not a group update
 | 
					
						
							|  |  |  |               !message.isGroupUpdate() | 
					
						
							|  |  |  |             ) { | 
					
						
							|  |  |  |               conversation.updateExpirationTimer( | 
					
						
							|  |  |  |                 null, | 
					
						
							|  |  |  |                 source, | 
					
						
							|  |  |  |                 message.get('received_at') | 
					
						
							|  |  |  |               ); | 
					
						
							| 
									
										
										
										
											2018-04-27 17:25:04 -04:00
										 |  |  |             } | 
					
						
							| 
									
										
										
										
											2018-07-25 15:02:27 -07:00
										 |  |  |           } | 
					
						
							|  |  |  |           if (type === 'incoming') { | 
					
						
							|  |  |  |             const readSync = Whisper.ReadSyncs.forMessage(message); | 
					
						
							|  |  |  |             if (readSync) { | 
					
						
							|  |  |  |               if ( | 
					
						
							|  |  |  |                 message.get('expireTimer') && | 
					
						
							|  |  |  |                 !message.get('expirationStartTimestamp') | 
					
						
							|  |  |  |               ) { | 
					
						
							|  |  |  |                 message.set( | 
					
						
							|  |  |  |                   'expirationStartTimestamp', | 
					
						
							|  |  |  |                   Math.min(readSync.get('read_at'), Date.now()) | 
					
						
							| 
									
										
										
										
											2018-04-27 17:25:04 -04:00
										 |  |  |                 ); | 
					
						
							|  |  |  |               } | 
					
						
							|  |  |  |             } | 
					
						
							| 
									
										
										
										
											2018-07-25 15:02:27 -07:00
										 |  |  |             if (readSync || message.isExpirationTimerUpdate()) { | 
					
						
							|  |  |  |               message.unset('unread'); | 
					
						
							|  |  |  |               // This is primarily to allow the conversation to mark all older
 | 
					
						
							|  |  |  |               // messages as read, as is done when we receive a read sync for
 | 
					
						
							|  |  |  |               // a message we already know about.
 | 
					
						
							| 
									
										
										
										
											2018-08-07 12:33:56 -07:00
										 |  |  |               const c = message.getConversation(); | 
					
						
							|  |  |  |               if (c) { | 
					
						
							|  |  |  |                 c.onReadMessage(message); | 
					
						
							|  |  |  |               } | 
					
						
							| 
									
										
										
										
											2018-07-25 15:02:27 -07:00
										 |  |  |             } else { | 
					
						
							|  |  |  |               conversation.set( | 
					
						
							|  |  |  |                 'unreadCount', | 
					
						
							|  |  |  |                 conversation.get('unreadCount') + 1 | 
					
						
							| 
									
										
										
										
											2018-04-27 17:25:04 -04:00
										 |  |  |               ); | 
					
						
							|  |  |  |             } | 
					
						
							| 
									
										
										
										
											2018-07-25 15:02:27 -07:00
										 |  |  |           } | 
					
						
							| 
									
										
										
										
											2017-07-03 16:46:39 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-07-25 15:02:27 -07:00
										 |  |  |           if (type === 'outgoing') { | 
					
						
							|  |  |  |             const reads = Whisper.ReadReceipts.forMessage( | 
					
						
							|  |  |  |               conversation, | 
					
						
							|  |  |  |               message | 
					
						
							|  |  |  |             ); | 
					
						
							|  |  |  |             if (reads.length) { | 
					
						
							|  |  |  |               const readBy = reads.map(receipt => receipt.get('reader')); | 
					
						
							|  |  |  |               message.set({ | 
					
						
							|  |  |  |                 read_by: _.union(message.get('read_by'), readBy), | 
					
						
							| 
									
										
										
										
											2018-04-27 17:25:04 -04:00
										 |  |  |               }); | 
					
						
							|  |  |  |             } | 
					
						
							| 
									
										
										
										
											2018-04-10 16:51:48 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-07-25 15:02:27 -07:00
										 |  |  |             message.set({ recipients: conversation.getRecipients() }); | 
					
						
							|  |  |  |           } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |           const conversationTimestamp = conversation.get('timestamp'); | 
					
						
							|  |  |  |           if ( | 
					
						
							|  |  |  |             !conversationTimestamp || | 
					
						
							|  |  |  |             message.get('sent_at') > conversationTimestamp | 
					
						
							|  |  |  |           ) { | 
					
						
							|  |  |  |             conversation.set({ | 
					
						
							|  |  |  |               lastMessage: message.getNotificationText(), | 
					
						
							|  |  |  |               timestamp: message.get('sent_at'), | 
					
						
							|  |  |  |             }); | 
					
						
							|  |  |  |           } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |           if (dataMessage.profileKey) { | 
					
						
							|  |  |  |             const profileKey = dataMessage.profileKey.toArrayBuffer(); | 
					
						
							|  |  |  |             if (source === textsecure.storage.user.getNumber()) { | 
					
						
							|  |  |  |               conversation.set({ profileSharing: true }); | 
					
						
							|  |  |  |             } else if (conversation.isPrivate()) { | 
					
						
							|  |  |  |               conversation.set({ profileKey }); | 
					
						
							|  |  |  |             } else { | 
					
						
							|  |  |  |               ConversationController.getOrCreateAndWait(source, 'private').then( | 
					
						
							|  |  |  |                 sender => { | 
					
						
							| 
									
										
										
										
											2018-04-27 17:25:04 -04:00
										 |  |  |                   sender.setProfileKey(profileKey); | 
					
						
							| 
									
										
										
										
											2018-07-25 15:02:27 -07:00
										 |  |  |                 } | 
					
						
							|  |  |  |               ); | 
					
						
							| 
									
										
										
										
											2018-04-10 16:51:48 -07:00
										 |  |  |             } | 
					
						
							| 
									
										
										
										
											2018-07-25 15:02:27 -07:00
										 |  |  |           } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |           const id = await window.Signal.Data.saveMessage(message.attributes, { | 
					
						
							|  |  |  |             Message: Whisper.Message, | 
					
						
							|  |  |  |           }); | 
					
						
							|  |  |  |           message.set({ id }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |           await wrapDeferred(conversation.save()); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |           conversation.trigger('newmessage', message); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |           try { | 
					
						
							|  |  |  |             // We fetch() here because, between the message.save() above and
 | 
					
						
							|  |  |  |             // the previous line's trigger() call, we might have marked all
 | 
					
						
							|  |  |  |             // messages unread in the database. This message might already
 | 
					
						
							|  |  |  |             // be read!
 | 
					
						
							|  |  |  |             const fetched = await window.Signal.Data.getMessageById( | 
					
						
							|  |  |  |               message.get('id'), | 
					
						
							|  |  |  |               { | 
					
						
							|  |  |  |                 Message: Whisper.Message, | 
					
						
							|  |  |  |               } | 
					
						
							|  |  |  |             ); | 
					
						
							|  |  |  |             const previousUnread = message.get('unread'); | 
					
						
							| 
									
										
										
										
											2017-07-03 16:46:39 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-07-25 15:02:27 -07:00
										 |  |  |             // Important to update message with latest read state from database
 | 
					
						
							|  |  |  |             message.merge(fetched); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             if (previousUnread !== message.get('unread')) { | 
					
						
							|  |  |  |               window.log.warn( | 
					
						
							|  |  |  |                 'Caught race condition on new message read state! ' + | 
					
						
							|  |  |  |                   'Manually starting timers.' | 
					
						
							| 
									
										
										
										
											2018-04-27 17:25:04 -04:00
										 |  |  |               ); | 
					
						
							| 
									
										
										
										
											2018-07-25 15:02:27 -07:00
										 |  |  |               // We call markRead() even though the message is already
 | 
					
						
							|  |  |  |               // marked read because we need to start expiration
 | 
					
						
							|  |  |  |               // timers, etc.
 | 
					
						
							|  |  |  |               message.markRead(); | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |           } catch (error) { | 
					
						
							|  |  |  |             window.log.warn( | 
					
						
							|  |  |  |               'handleDataMessage: Message', | 
					
						
							|  |  |  |               message.idForLogging(), | 
					
						
							|  |  |  |               'was deleted' | 
					
						
							|  |  |  |             ); | 
					
						
							|  |  |  |           } | 
					
						
							| 
									
										
										
										
											2015-09-28 13:33:26 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-07-25 15:02:27 -07:00
										 |  |  |           if (message.get('unread')) { | 
					
						
							|  |  |  |             await conversation.notify(message); | 
					
						
							|  |  |  |           } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |           confirm(); | 
					
						
							|  |  |  |         } catch (error) { | 
					
						
							|  |  |  |           const errorForLog = error && error.stack ? error.stack : error; | 
					
						
							|  |  |  |           window.log.error( | 
					
						
							|  |  |  |             'handleDataMessage', | 
					
						
							|  |  |  |             message.idForLogging(), | 
					
						
							|  |  |  |             'error:', | 
					
						
							|  |  |  |             errorForLog | 
					
						
							|  |  |  |           ); | 
					
						
							| 
									
										
										
										
											2018-08-08 09:40:12 -07:00
										 |  |  |           throw error; | 
					
						
							| 
									
										
										
										
											2018-07-25 15:02:27 -07:00
										 |  |  |         } | 
					
						
							|  |  |  |       }); | 
					
						
							| 
									
										
										
										
											2018-04-10 16:51:48 -07:00
										 |  |  |     }, | 
					
						
							| 
									
										
										
										
											2018-05-24 18:54:06 -07:00
										 |  |  |     async markRead(readAt) { | 
					
						
							| 
									
										
										
										
											2018-04-10 16:51:48 -07:00
										 |  |  |       this.unset('unread'); | 
					
						
							| 
									
										
										
										
											2018-07-25 15:02:27 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-04-10 16:51:48 -07:00
										 |  |  |       if (this.get('expireTimer') && !this.get('expirationStartTimestamp')) { | 
					
						
							| 
									
										
										
										
											2018-07-25 15:02:27 -07:00
										 |  |  |         const expirationStartTimestamp = Math.min( | 
					
						
							|  |  |  |           Date.now(), | 
					
						
							|  |  |  |           readAt || Date.now() | 
					
						
							|  |  |  |         ); | 
					
						
							|  |  |  |         this.set({ expirationStartTimestamp }); | 
					
						
							| 
									
										
										
										
											2018-04-10 16:51:48 -07:00
										 |  |  |       } | 
					
						
							| 
									
										
										
										
											2018-07-25 15:02:27 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-04-27 17:25:04 -04:00
										 |  |  |       Whisper.Notifications.remove( | 
					
						
							|  |  |  |         Whisper.Notifications.where({ | 
					
						
							|  |  |  |           messageId: this.id, | 
					
						
							|  |  |  |         }) | 
					
						
							|  |  |  |       ); | 
					
						
							| 
									
										
										
										
											2018-07-25 15:02:27 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  |       await window.Signal.Data.saveMessage(this.attributes, { | 
					
						
							|  |  |  |         Message: Whisper.Message, | 
					
						
							|  |  |  |       }); | 
					
						
							| 
									
										
										
										
											2018-04-10 16:51:48 -07:00
										 |  |  |     }, | 
					
						
							|  |  |  |     isExpiring() { | 
					
						
							|  |  |  |       return this.get('expireTimer') && this.get('expirationStartTimestamp'); | 
					
						
							|  |  |  |     }, | 
					
						
							|  |  |  |     isExpired() { | 
					
						
							|  |  |  |       return this.msTilExpire() <= 0; | 
					
						
							|  |  |  |     }, | 
					
						
							|  |  |  |     msTilExpire() { | 
					
						
							|  |  |  |       if (!this.isExpiring()) { | 
					
						
							|  |  |  |         return Infinity; | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |       const now = Date.now(); | 
					
						
							|  |  |  |       const start = this.get('expirationStartTimestamp'); | 
					
						
							|  |  |  |       const delta = this.get('expireTimer') * 1000; | 
					
						
							| 
									
										
										
										
											2018-04-27 17:25:04 -04:00
										 |  |  |       let msFromNow = start + delta - now; | 
					
						
							| 
									
										
										
										
											2018-04-10 16:51:48 -07:00
										 |  |  |       if (msFromNow < 0) { | 
					
						
							|  |  |  |         msFromNow = 0; | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |       return msFromNow; | 
					
						
							|  |  |  |     }, | 
					
						
							| 
									
										
										
										
											2018-08-06 11:31:41 -07:00
										 |  |  |     async setToExpire(force = false) { | 
					
						
							|  |  |  |       if (this.isExpiring() && (force || !this.get('expires_at'))) { | 
					
						
							| 
									
										
										
										
											2018-04-10 16:51:48 -07:00
										 |  |  |         const start = this.get('expirationStartTimestamp'); | 
					
						
							|  |  |  |         const delta = this.get('expireTimer') * 1000; | 
					
						
							|  |  |  |         const expiresAt = start + delta; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-07-25 15:02:27 -07:00
										 |  |  |         this.set({ expires_at: expiresAt }); | 
					
						
							| 
									
										
										
										
											2018-07-26 18:13:56 -07:00
										 |  |  |         const id = this.get('id'); | 
					
						
							|  |  |  |         if (id) { | 
					
						
							|  |  |  |           await window.Signal.Data.saveMessage(this.attributes, { | 
					
						
							|  |  |  |             Message: Whisper.Message, | 
					
						
							|  |  |  |           }); | 
					
						
							|  |  |  |         } | 
					
						
							| 
									
										
										
										
											2015-03-18 16:26:55 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-07-21 12:00:08 -07:00
										 |  |  |         window.log.info('Set message expiration', { | 
					
						
							| 
									
										
										
										
											2018-05-03 13:10:44 -04:00
										 |  |  |           expiresAt, | 
					
						
							|  |  |  |           sentAt: this.get('sent_at'), | 
					
						
							|  |  |  |         }); | 
					
						
							| 
									
										
										
										
											2018-04-10 16:51:48 -07:00
										 |  |  |       } | 
					
						
							|  |  |  |     }, | 
					
						
							|  |  |  |   }); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-08-02 21:12:27 -07:00
										 |  |  |   Whisper.Message.refreshExpirationTimer = () => | 
					
						
							|  |  |  |     Whisper.ExpiringMessagesListener.update(); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-04-10 16:51:48 -07:00
										 |  |  |   Whisper.MessageCollection = Backbone.Collection.extend({ | 
					
						
							|  |  |  |     model: Whisper.Message, | 
					
						
							| 
									
										
										
										
											2018-07-26 18:13:56 -07:00
										 |  |  |     // Keeping this for legacy upgrade pre-migrate to SQLCipher
 | 
					
						
							| 
									
										
										
										
											2018-04-10 16:51:48 -07:00
										 |  |  |     database: Whisper.Database, | 
					
						
							|  |  |  |     storeName: 'messages', | 
					
						
							|  |  |  |     comparator(left, right) { | 
					
						
							|  |  |  |       if (left.get('received_at') === right.get('received_at')) { | 
					
						
							|  |  |  |         return (left.get('sent_at') || 0) - (right.get('sent_at') || 0); | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       return (left.get('received_at') || 0) - (right.get('received_at') || 0); | 
					
						
							|  |  |  |     }, | 
					
						
							|  |  |  |     initialize(models, options) { | 
					
						
							|  |  |  |       if (options) { | 
					
						
							|  |  |  |         this.conversation = options.conversation; | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |     }, | 
					
						
							| 
									
										
										
										
											2018-07-25 15:02:27 -07:00
										 |  |  |     async destroyAll() { | 
					
						
							|  |  |  |       await Promise.all( | 
					
						
							|  |  |  |         this.models.map(message => | 
					
						
							|  |  |  |           window.Signal.Data.removeMessage(message.id, { | 
					
						
							|  |  |  |             Message: Whisper.Message, | 
					
						
							|  |  |  |           }) | 
					
						
							| 
									
										
										
										
											2018-04-27 17:25:04 -04:00
										 |  |  |         ) | 
					
						
							|  |  |  |       ); | 
					
						
							| 
									
										
										
										
											2018-07-25 15:02:27 -07:00
										 |  |  |       this.reset([]); | 
					
						
							| 
									
										
										
										
											2018-04-10 16:51:48 -07:00
										 |  |  |     }, | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     getLoadedUnreadCount() { | 
					
						
							|  |  |  |       return this.reduce((total, model) => { | 
					
						
							|  |  |  |         const unread = model.get('unread') && model.isIncoming(); | 
					
						
							|  |  |  |         return total + (unread ? 1 : 0); | 
					
						
							|  |  |  |       }, 0); | 
					
						
							|  |  |  |     }, | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-07-25 15:02:27 -07:00
										 |  |  |     async fetchConversation(conversationId, limit = 100, unreadCount = 0) { | 
					
						
							|  |  |  |       const startingLoadedUnread = | 
					
						
							|  |  |  |         unreadCount > 0 ? this.getLoadedUnreadCount() : 0; | 
					
						
							| 
									
										
										
										
											2015-02-17 18:03:05 -08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-07-25 15:02:27 -07:00
										 |  |  |       // We look for older messages if we've fetched once already
 | 
					
						
							|  |  |  |       const receivedAt = | 
					
						
							|  |  |  |         this.length === 0 ? Number.MAX_VALUE : this.at(0).get('received_at'); | 
					
						
							| 
									
										
										
										
											2017-02-21 15:32:40 -08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-07-25 15:02:27 -07:00
										 |  |  |       const messages = await window.Signal.Data.getMessagesByConversation( | 
					
						
							|  |  |  |         conversationId, | 
					
						
							|  |  |  |         { | 
					
						
							|  |  |  |           limit, | 
					
						
							|  |  |  |           receivedAt, | 
					
						
							|  |  |  |           MessageCollection: Whisper.MessageCollection, | 
					
						
							| 
									
										
										
										
											2014-11-13 14:35:37 -08:00
										 |  |  |         } | 
					
						
							| 
									
										
										
										
											2018-07-25 15:02:27 -07:00
										 |  |  |       ); | 
					
						
							| 
									
										
										
										
											2018-04-10 16:51:48 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-07-26 18:13:56 -07:00
										 |  |  |       const models = messages.filter(message => Boolean(message.id)); | 
					
						
							|  |  |  |       const eliminated = messages.length - models.length; | 
					
						
							|  |  |  |       if (eliminated > 0) { | 
					
						
							|  |  |  |         window.log.warn( | 
					
						
							|  |  |  |           `fetchConversation: Eliminated ${eliminated} messages without an id` | 
					
						
							|  |  |  |         ); | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       this.add(models); | 
					
						
							| 
									
										
										
										
											2018-04-10 16:51:48 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-07-25 15:02:27 -07:00
										 |  |  |       if (unreadCount <= 0) { | 
					
						
							|  |  |  |         return; | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |       const loadedUnread = this.getLoadedUnreadCount(); | 
					
						
							|  |  |  |       if (loadedUnread >= unreadCount) { | 
					
						
							|  |  |  |         return; | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |       if (startingLoadedUnread === loadedUnread) { | 
					
						
							|  |  |  |         // that fetch didn't get us any more unread. stop fetching more.
 | 
					
						
							|  |  |  |         return; | 
					
						
							|  |  |  |       } | 
					
						
							| 
									
										
										
										
											2018-04-10 16:51:48 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-07-25 15:02:27 -07:00
										 |  |  |       window.log.info( | 
					
						
							|  |  |  |         'fetchConversation: doing another fetch to get all unread' | 
					
						
							|  |  |  |       ); | 
					
						
							|  |  |  |       await this.fetchConversation(conversationId, limit, unreadCount); | 
					
						
							| 
									
										
										
										
											2018-04-10 16:51:48 -07:00
										 |  |  |     }, | 
					
						
							|  |  |  |   }); | 
					
						
							| 
									
										
										
										
											2018-04-27 17:25:04 -04:00
										 |  |  | })(); |