|  |  | @ -1231,210 +1231,6 @@ | 
			
		
	
		
		
			
				
					
					|  |  |  |       }); |  |  |  |       }); | 
			
		
	
		
		
			
				
					
					|  |  |  |     }, |  |  |  |     }, | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |     deleteSelectedMessages() { |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |       const ourPubkey = textsecure.storage.user.getNumber(); |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |       const selected = Array.from(this.model.selectedMessages); |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |       const isModerator = this.model.isModerator(ourPubkey); |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |       const isAllOurs = selected.every( |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         message => |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |           message.propsForMessage.authorPhoneNumber === message.OUR_NUMBER |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |       ); |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |       if (!isAllOurs && !isModerator) { |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         window.pushToast({ |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |           title: i18n('messageDeletionForbidden'), |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |           type: 'error', |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |           id: 'messageDeletionForbidden', |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         }); |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         return; |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |       } |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |       this.deleteMessages(selected, () => { |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         this.resetMessageSelection(); |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |       }); |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |     }, |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |     deleteMessages(messages, onSuccess) { |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |       const multiple = messages.length > 1; |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |       const isPublic = this.model.isPublic(); |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |       // In future, we may be able to unsend private messages also
 |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |       // isServerDeletable also defined in ConversationHeader.tsx for
 |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |       // future reference
 |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |       const isServerDeletable = isPublic; |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |       const warningMessage = (() => { |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         if (isPublic) { |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |           return multiple |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |             ? i18n('deleteMultiplePublicWarning') |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |             : i18n('deletePublicWarning'); |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         } |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         return multiple ? i18n('deleteMultipleWarning') : i18n('deleteWarning'); |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |       })(); |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |       const doDelete = async () => { |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         let toDeleteLocally; |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         if (isPublic) { |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |           toDeleteLocally = await this.model.deletePublicMessages(messages); |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |           if (toDeleteLocally.length === 0) { |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |             // Message failed to delete from server, show error?
 |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |             return; |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |           } |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         } else { |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |           messages.forEach(m => this.model.messageCollection.remove(m.id)); |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |           toDeleteLocally = messages; |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         } |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         await Promise.all( |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |           toDeleteLocally.map(async m => { |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |             await window.Signal.Data.removeMessage(m.id, { |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |               Message: Whisper.Message, |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |             }); |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |             m.trigger('unload'); |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |           }) |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         ); |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         this.resetPanel(); |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         this.updateHeader(); |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         if (onSuccess) { |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |           onSuccess(); |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         } |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |       }; |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |       // Only show a warning when at least one messages was successfully
 |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |       // saved in on the server
 |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |       if (!messages.some(m => !m.hasErrors())) { |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         doDelete(); |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         return; |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |       } |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |       // If removable from server, we "Unsend" - otherwise "Delete"
 |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |       let title; |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |       if (isPublic) { |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         title = multiple |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |           ? i18n('deleteMessagesForEveryone') |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |           : i18n('deleteMessageForEveryone'); |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |       } else { |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         title = multiple ? i18n('deleteMessages') : i18n('deleteMessage'); |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |       } |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |       const okText = isServerDeletable |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         ? i18n('deleteForEveryone') |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         : i18n('delete'); |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |       window.confirmationDialog({ |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         title, |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         message: warningMessage, |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         okText, |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         okTheme: 'danger', |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         resolve: doDelete, |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |       }); |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |     }, |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |     deleteMessage(message) { |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |       this.deleteMessages([message]); |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |     }, |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |     showChannelLightbox({ media, attachment, message }) { |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |       const selectedIndex = media.findIndex( |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         mediaMessage => mediaMessage.attachment.path === attachment.path |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |       ); |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |       this.lightboxGalleryView = new Whisper.ReactWrapperView({ |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         className: 'lightbox-wrapper', |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         Component: Signal.Components.LightboxGallery, |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         props: { |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |           media, |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |           onSave: () => this.downloadAttachment({ attachment, message }), |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |           selectedIndex, |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         }, |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         onClose: () => Signal.Backbone.Views.Lightbox.hide(), |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |       }); |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |       Signal.Backbone.Views.Lightbox.show(this.lightboxGalleryView.el); |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |     }, |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |     showLightbox({ attachment, message }) { |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |       const { contentType, path } = attachment; |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |       if ( |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         !Signal.Util.GoogleChrome.isImageTypeSupported(contentType) && |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         !Signal.Util.GoogleChrome.isVideoTypeSupported(contentType) |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |       ) { |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         this.downloadAttachment({ attachment, message }); |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         return; |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |       } |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |       const attachments = message.get('attachments') || []; |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |       const media = attachments |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         .filter(item => item.thumbnail && !item.pending && !item.error) |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         .map((item, index) => ({ |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |           objectURL: getAbsoluteAttachmentPath(item.path), |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |           path: item.path, |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |           contentType: item.contentType, |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |           index, |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |           message, |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |           attachment: item, |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         })); |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |       if (media.length === 1) { |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         const props = { |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |           objectURL: getAbsoluteAttachmentPath(path), |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |           contentType, |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |           caption: attachment.caption, |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |           onSave: () => this.downloadAttachment({ attachment, message }), |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         }; |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         this.lightboxView = new Whisper.ReactWrapperView({ |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |           className: 'lightbox-wrapper', |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |           Component: Signal.Components.Lightbox, |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |           props, |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |           onClose: () => { |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |             Signal.Backbone.Views.Lightbox.hide(); |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |             this.stopListening(message); |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |           }, |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         }); |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         this.listenTo(message, 'expired', () => this.lightboxView.remove()); |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         Signal.Backbone.Views.Lightbox.show(this.lightboxView.el); |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         return; |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |       } |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |       const selectedIndex = _.findIndex( |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         media, |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         item => attachment.path === item.path |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |       ); |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |       const onSave = async (options = {}) => { |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         Signal.Types.Attachment.save({ |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |           attachment: options.attachment, |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |           document, |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |           index: options.index + 1, |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |           getAbsolutePath: getAbsoluteAttachmentPath, |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |           timestamp: options.message.get('sent_at'), |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         }); |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |       }; |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |       const props = { |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         media, |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         selectedIndex: selectedIndex >= 0 ? selectedIndex : 0, |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         onSave, |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |       }; |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |       this.lightboxGalleryView = new Whisper.ReactWrapperView({ |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         className: 'lightbox-wrapper', |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         Component: Signal.Components.LightboxGallery, |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         props, |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         onClose: () => { |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |           Signal.Backbone.Views.Lightbox.hide(); |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |           this.stopListening(message); |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         }, |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |       }); |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |       this.listenTo(message, 'expired', () => |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         this.lightboxGalleryView.remove() |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |       ); |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |       Signal.Backbone.Views.Lightbox.show(this.lightboxGalleryView.el); |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |     }, |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |     async showMessageDetail(message) { |  |  |  |     async showMessageDetail(message) { | 
			
		
	
		
		
			
				
					
					|  |  |  |       const onClose = () => { |  |  |  |       const onClose = () => { | 
			
		
	
		
		
			
				
					
					|  |  |  |         this.stopListening(message, 'change', update); |  |  |  |         this.stopListening(message, 'change', update); | 
			
		
	
	
		
		
			
				
					|  |  | 
 |