|  |  | @ -12,7 +12,7 @@ enum MessageMetadataViewMode: UInt { | 
			
		
	
		
		
			
				
					
					|  |  |  |     case focusOnMetadata |  |  |  |     case focusOnMetadata | 
			
		
	
		
		
			
				
					
					|  |  |  | } |  |  |  | } | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  | class MessageDetailViewController: OWSViewController, UIScrollViewDelegate, MediaDetailPresenter, MediaGalleryDataSourceDelegate { |  |  |  | class MessageDetailViewController: OWSViewController, MediaDetailPresenter, MediaGalleryDataSourceDelegate { | 
			
				
				
			
		
	
		
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |     // MARK: Properties |  |  |  |     // MARK: Properties | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
	
		
		
			
				
					|  |  | @ -28,15 +28,9 @@ class MessageDetailViewController: OWSViewController, UIScrollViewDelegate, Medi | 
			
		
	
		
		
			
				
					
					|  |  |  |     var message: TSMessage |  |  |  |     var message: TSMessage | 
			
		
	
		
		
			
				
					
					|  |  |  |     var wasDeleted: Bool = false |  |  |  |     var wasDeleted: Bool = false | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |     var mediaMessageView: MediaMessageView? |  |  |  |     var messageBubbleView: OWSMessageBubbleView? | 
			
				
				
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  |     var messageBubbleViewWidthLayoutConstraint: NSLayoutConstraint? | 
			
				
				
			
		
	
		
		
			
				
					
					|  |  |  |     // See comments on updateTextLayout. |  |  |  |     var messageBubbleViewHeightLayoutConstraint: NSLayoutConstraint? | 
			
				
				
			
		
	
		
		
			
				
					
					|  |  |  |     var messageTextView: UITextView? |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |     var messageTextProxyView: UIView? |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |     var messageTextTopConstraint: NSLayoutConstraint? |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |     var messageTextHeightLayoutConstraint: NSLayoutConstraint? |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |     var messageTextProxyViewHeightConstraint: NSLayoutConstraint? |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |     var bubbleViewWidthConstraint: NSLayoutConstraint? |  |  |  |  | 
			
		
	
		
		
	
		
		
	
		
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |     var scrollView: UIScrollView! |  |  |  |     var scrollView: UIScrollView! | 
			
		
	
		
		
			
				
					
					|  |  |  |     var contentView: UIView? |  |  |  |     var contentView: UIView? | 
			
		
	
	
		
		
			
				
					|  |  | @ -87,7 +81,7 @@ class MessageDetailViewController: OWSViewController, UIScrollViewDelegate, Medi | 
			
		
	
		
		
			
				
					
					|  |  |  |     override func viewWillAppear(_ animated: Bool) { |  |  |  |     override func viewWillAppear(_ animated: Bool) { | 
			
		
	
		
		
			
				
					
					|  |  |  |         super.viewWillAppear(animated) |  |  |  |         super.viewWillAppear(animated) | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |         updateTextLayout() |  |  |  |         updateMessageBubbleViewLayout() | 
			
				
				
			
		
	
		
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |         if mode == .focusOnMetadata { |  |  |  |         if mode == .focusOnMetadata { | 
			
		
	
		
		
			
				
					
					|  |  |  |             if let bubbleView = self.bubbleView { |  |  |  |             if let bubbleView = self.bubbleView { | 
			
		
	
	
		
		
			
				
					|  |  | @ -121,7 +115,6 @@ class MessageDetailViewController: OWSViewController, UIScrollViewDelegate, Medi | 
			
		
	
		
		
			
				
					
					|  |  |  |         view.backgroundColor = UIColor.white |  |  |  |         view.backgroundColor = UIColor.white | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |         let scrollView = UIScrollView() |  |  |  |         let scrollView = UIScrollView() | 
			
		
	
		
		
			
				
					
					|  |  |  |         scrollView.delegate = self |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         self.scrollView = scrollView |  |  |  |         self.scrollView = scrollView | 
			
		
	
		
		
			
				
					
					|  |  |  |         view.addSubview(scrollView) |  |  |  |         view.addSubview(scrollView) | 
			
		
	
		
		
			
				
					
					|  |  |  |         scrollView.autoPinWidthToSuperview(withMargin: 0) |  |  |  |         scrollView.autoPinWidthToSuperview(withMargin: 0) | 
			
		
	
	
		
		
			
				
					|  |  | @ -218,7 +211,7 @@ class MessageDetailViewController: OWSViewController, UIScrollViewDelegate, Medi | 
			
		
	
		
		
			
				
					
					|  |  |  |                 } |  |  |  |                 } | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |                 for recipientId in thread.recipientIdentifiers { |  |  |  |                 for recipientId in thread.recipientIdentifiers { | 
			
		
	
		
		
			
				
					
					|  |  |  |                     let (recipientStatus, shortStatusMessage, longStatusMessage) = MessageRecipientStatusUtils.recipientStatusAndStatusMessage(outgoingMessage: outgoingMessage, recipientId: recipientId, referenceView: self.view) |  |  |  |                     let (recipientStatus, shortStatusMessage, _) = MessageRecipientStatusUtils.recipientStatusAndStatusMessage(outgoingMessage: outgoingMessage, recipientId: recipientId, referenceView: self.view) | 
			
				
				
			
		
	
		
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |                     guard recipientStatus == recipientStatusGroup else { |  |  |  |                     guard recipientStatus == recipientStatusGroup else { | 
			
		
	
		
		
			
				
					
					|  |  |  |                         continue |  |  |  |                         continue | 
			
		
	
	
		
		
			
				
					|  |  | @ -299,11 +292,7 @@ class MessageDetailViewController: OWSViewController, UIScrollViewDelegate, Medi | 
			
		
	
		
		
			
				
					
					|  |  |  |             lastRow.autoPinEdge(toSuperviewEdge: .bottom, withInset: 20) |  |  |  |             lastRow.autoPinEdge(toSuperviewEdge: .bottom, withInset: 20) | 
			
		
	
		
		
			
				
					
					|  |  |  |         } |  |  |  |         } | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |         if let mediaMessageView = mediaMessageView { |  |  |  |         updateMessageBubbleViewLayout() | 
			
				
				
			
		
	
		
		
			
				
					
					|  |  |  |             mediaMessageView.autoMatch(.height, to: .width, of: mediaMessageView, withOffset: 0, relation: .lessThanOrEqual) |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         } |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         updateTextLayout() |  |  |  |  | 
			
		
	
		
		
	
		
		
			
				
					
					|  |  |  |     } |  |  |  |     } | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |     private func displayableTextIfText() -> String? { |  |  |  |     private func displayableTextIfText() -> String? { | 
			
		
	
	
		
		
			
				
					|  |  | @ -321,8 +310,6 @@ class MessageDetailViewController: OWSViewController, UIScrollViewDelegate, Medi | 
			
		
	
		
		
			
				
					
					|  |  |  |     } |  |  |  |     } | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |     let bubbleViewHMargin: CGFloat = 10 |  |  |  |     let bubbleViewHMargin: CGFloat = 10 | 
			
		
	
		
		
			
				
					
					|  |  |  |     let messageTailEdgeMargin: CGFloat = 15 |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |     let messageNoTailEdgeMargin: CGFloat = 10 |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |     private func contentRows() -> [UIView] { |  |  |  |     private func contentRows() -> [UIView] { | 
			
		
	
		
		
			
				
					
					|  |  |  |         var rows = [UIView]() |  |  |  |         var rows = [UIView]() | 
			
		
	
	
		
		
			
				
					|  |  | @ -331,60 +318,28 @@ class MessageDetailViewController: OWSViewController, UIScrollViewDelegate, Medi | 
			
		
	
		
		
			
				
					
					|  |  |  |             rows += addAttachmentRows() |  |  |  |             rows += addAttachmentRows() | 
			
		
	
		
		
			
				
					
					|  |  |  |         } |  |  |  |         } | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |         if let messageBody = displayableTextIfText() { |  |  |  |         if true { | 
			
				
				
			
		
	
		
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |             let messageBubbleView = OWSMessageBubbleView(frame: CGRect.zero) | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |             self.messageBubbleView = messageBubbleView | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |             messageBubbleView.viewItem = viewItem | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |             messageBubbleView.cellMediaCache = NSCache() | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |             messageBubbleView.contentWidth = contentWidth() | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |             messageBubbleView.alwaysShowBubbleTail = true | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |             messageBubbleView.configureViews() | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |             messageBubbleView.loadContent() | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |             self.messageBody = messageBody |  |  |  |             messageBubbleView.isUserInteractionEnabled = true | 
			
				
				
			
		
	
		
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |             messageBubbleView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(messageBubbleTapped))) | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |             let isIncoming = self.message as? TSIncomingMessage != nil |  |  |  |             let row = UIView() | 
			
				
				
			
		
	
		
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |             row.addSubview(messageBubbleView) | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |             messageBubbleView.autoPinHeightToSuperview() | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |             // UITextView can't render extremely long text due to constraints |  |  |  |             let isIncoming = self.message as? TSIncomingMessage != nil | 
			
				
				
			
		
	
		
		
			
				
					
					|  |  |  |             // on the size of its backing buffer, especially when we're |  |  |  |             messageBubbleView.autoPinEdge(toSuperviewEdge: isIncoming ? .leading : .trailing, withInset: bubbleViewHMargin) | 
			
				
				
			
		
	
		
		
			
				
					
					|  |  |  |             // embedding it "full-size' within a UIScrollView as we do in this view. |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |             // |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |             // Therefore we're doing something unusual here. |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |             // See comments on updateTextLayout. |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |             let messageTextView = UITextView() |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |             self.messageTextView = messageTextView |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |             messageTextView.font = UIFont.ows_dynamicTypeBody |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |             messageTextView.backgroundColor = UIColor.clear |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |             messageTextView.isOpaque = false |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |             messageTextView.isEditable = false |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |             messageTextView.isSelectable = true |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |             messageTextView.textContainerInset = UIEdgeInsets.zero |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |             messageTextView.contentInset = UIEdgeInsets.zero |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |             messageTextView.isScrollEnabled = true |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |             messageTextView.showsHorizontalScrollIndicator = false |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |             messageTextView.showsVerticalScrollIndicator = false |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |             messageTextView.isUserInteractionEnabled = false |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |             messageTextView.textColor = isIncoming ? UIColor.black : UIColor.white |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |             messageTextView.text = messageBody |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |             let bubbleImageData = bubbleFactory.bubble(message: message) |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |             let messageTextProxyView = UIView() |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |             messageTextProxyView.layoutMargins = UIEdgeInsets.zero |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |             self.messageTextProxyView = messageTextProxyView |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |             messageTextProxyView.addSubview(messageTextView) |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |             messageTextView.autoPinWidthToSuperview() |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |             self.messageTextTopConstraint = messageTextView.autoPinEdge(toSuperviewEdge: .top, withInset: 0) |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |             self.messageTextHeightLayoutConstraint = messageTextView.autoSetDimension(.height, toSize: 0) |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |             let bubbleView = UIImageView(image: bubbleImageData.messageBubbleImage) |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |             self.bubbleView = bubbleView |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |             bubbleView.layer.cornerRadius = 10 |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |             bubbleView.addSubview(messageTextProxyView) |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |             messageTextProxyView.autoPinEdge(toSuperviewEdge: isIncoming ? .leading : .trailing, withInset: messageTailEdgeMargin) |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |             messageTextProxyView.autoPinEdge(toSuperviewEdge: isIncoming ? .trailing : .leading, withInset: messageNoTailEdgeMargin) |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |             messageTextProxyView.autoPinHeightToSuperview(withMargin: 10) |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |             self.messageTextProxyViewHeightConstraint = messageTextProxyView.autoSetDimension(.height, toSize: 0) |  |  |  |  | 
			
		
	
		
		
	
		
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |             let row = UIView() |  |  |  |             self.messageBubbleViewWidthLayoutConstraint = messageBubbleView.autoSetDimension(.width, toSize: 0) | 
			
				
				
			
		
	
		
		
			
				
					
					|  |  |  |             row.addSubview(bubbleView) |  |  |  |             self.messageBubbleViewHeightLayoutConstraint = messageBubbleView.autoSetDimension(.height, toSize: 0) | 
			
				
				
			
		
	
		
		
			
				
					
					|  |  |  |             bubbleView.autoPinHeightToSuperview() |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |             bubbleView.autoPinEdge(toSuperviewEdge: isIncoming ? .leading : .trailing, withInset: bubbleViewHMargin) |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |             self.bubbleViewWidthConstraint = bubbleView.autoSetDimension(.width, toSize: 0) |  |  |  |  | 
			
		
	
		
		
	
		
		
	
		
		
			
				
					
					|  |  |  |             rows.append(row) |  |  |  |             rows.append(row) | 
			
		
	
		
		
			
				
					
					|  |  |  |         } |  |  |  |         } | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
	
		
		
			
				
					|  |  | @ -432,26 +387,6 @@ class MessageDetailViewController: OWSViewController, UIScrollViewDelegate, Medi | 
			
		
	
		
		
			
				
					
					|  |  |  |         } |  |  |  |         } | 
			
		
	
		
		
			
				
					
					|  |  |  |         self.attachmentStream = attachmentStream |  |  |  |         self.attachmentStream = attachmentStream | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |         if let filePath = attachmentStream.filePath() { |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |             dataSource = DataSourcePath.dataSource(withFilePath: filePath) |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         } |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         guard let dataSource = dataSource else { |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |             rows.append(valueRow(name: NSLocalizedString("MESSAGE_METADATA_VIEW_ATTACHMENT_MISSING_FILE", |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |                                                          comment: "Label for 'missing' attachments in the 'message metadata' view."), |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |                                  value: "")) |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |             return rows |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         } |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         let contentType = attachment.contentType |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         if let dataUTI = MIMETypeUtil.utiType(forMIMEType: contentType) { |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |             let attachment = SignalAttachment.attachment(dataSource: dataSource, dataUTI: dataUTI, imageQuality: .original) |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |             let mediaMessageView = MediaMessageView(attachment: attachment, mode: .small, mediaDetailPresenter: self) |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |             mediaMessageView.backgroundColor = UIColor.white |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |             self.mediaMessageView = mediaMessageView |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |             rows.append(mediaMessageView) |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         } |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         return rows |  |  |  |         return rows | 
			
		
	
		
		
			
				
					
					|  |  |  |     } |  |  |  |     } | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
	
		
		
			
				
					|  |  | @ -559,28 +494,6 @@ class MessageDetailViewController: OWSViewController, UIScrollViewDelegate, Medi | 
			
		
	
		
		
			
				
					
					|  |  |  |         AttachmentSharing.showShareUI(forAttachment: attachmentStream) |  |  |  |         AttachmentSharing.showShareUI(forAttachment: attachmentStream) | 
			
		
	
		
		
			
				
					
					|  |  |  |     } |  |  |  |     } | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |     func copyToPasteboard() { |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         if let messageBody = messageBody { |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |             UIPasteboard.general.string = messageBody |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |             return |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         } |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         guard let attachmentStream = attachmentStream else { |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |             Logger.error("\(logTag) Message has neither attachment nor message body.") |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |             return |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         } |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         guard let utiType = MIMETypeUtil.utiType(forMIMEType: attachmentStream.contentType) else { |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |             Logger.error("\(logTag) Attachment has invalid MIME type: \(attachmentStream.contentType).") |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |             return |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         } |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         guard let dataSource = dataSource else { |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |             Logger.error("\(logTag) Attachment missing data source.") |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |             return |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         } |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         let data = dataSource.data() |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         UIPasteboard.general.setData(data, forPasteboardType: utiType) |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |     } |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |     // MARK: - Actions |  |  |  |     // MARK: - Actions | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |     // This method should be called after self.databaseConnection.beginLongLivedReadTransaction(). |  |  |  |     // This method should be called after self.databaseConnection.beginLongLivedReadTransaction(). | 
			
		
	
	
		
		
			
				
					|  |  | @ -650,110 +563,64 @@ class MessageDetailViewController: OWSViewController, UIScrollViewDelegate, Medi | 
			
		
	
		
		
			
				
					
					|  |  |  |         } |  |  |  |         } | 
			
		
	
		
		
			
				
					
					|  |  |  |     } |  |  |  |     } | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |     // MARK: - Text Layout |  |  |  |     // MARK: - Message Bubble Layout | 
			
				
				
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
				
				
			
		
	
		
		
			
				
					
					|  |  |  |     // UITextView can't render extremely long text due to constraints on the size |  |  |  |     private func contentWidth() -> Int32 { | 
			
				
				
			
		
	
		
		
			
				
					
					|  |  |  |     // of its backing buffer, especially when we're embedding it "full-size'  |  |  |  |         return Int32(round(self.view.width() - (2 * bubbleViewHMargin))) | 
			
				
				
			
		
	
		
		
			
				
					
					|  |  |  |     // within a UIScrollView as we do in this view.  Therefore if we do the naive |  |  |  |     } | 
			
				
				
			
		
	
		
		
			
				
					
					|  |  |  |     // thing and embed a full-size UITextView inside our UIScrollView, it will  |  |  |  | 
 | 
			
				
				
			
		
	
		
		
			
				
					
					|  |  |  |     // fail to render any text if the text message is sufficiently long. |  |  |  |     private func updateMessageBubbleViewLayout() { | 
			
				
				
			
		
	
		
		
			
				
					
					|  |  |  |     // |  |  |  |         guard let messageBubbleView = messageBubbleView else { | 
			
				
				
			
		
	
		
		
			
				
					
					|  |  |  |     // Therefore we're doing something unusual.   |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |     // |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |     // * We use an empty UIView "messageTextProxyView" as a placeholder for the |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |     //   the UITextView.  It has the size and position of where the UITextView |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |     //   would be normally. |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |     // * We use a UITextView inside that proxy that is just large enough to |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |     //   render the content onscreen. We then move it around within the proxy |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |     //   bounds to render the parts of the proxy which are onscreen. |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |     private func updateTextLayout() { |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         guard let messageTextView = messageTextView else { |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |             return |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         } |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         guard let messageTextProxyView = messageTextProxyView else { |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |             owsFail("\(logTag) Missing messageTextProxyView") |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |             return |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         } |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         guard let scrollView = scrollView else { |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |             owsFail("\(logTag) Missing scrollView") |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |             return |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         } |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         guard let contentView = contentView else { |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |             owsFail("\(logTag) Missing contentView") |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |             return |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         } |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         guard let bubbleView = bubbleView else { |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |             owsFail("\(logTag) Missing bubbleView") |  |  |  |  | 
			
		
	
		
		
	
		
		
	
		
		
	
		
		
	
		
		
	
		
		
	
		
		
	
		
		
	
		
		
			
				
					
					|  |  |  |             return |  |  |  |             return | 
			
		
	
		
		
			
				
					
					|  |  |  |         } |  |  |  |         } | 
			
		
	
		
		
			
				
					
					|  |  |  |         guard let bubbleSuperview = bubbleView.superview else { |  |  |  |         guard let messageBubbleViewWidthLayoutConstraint = messageBubbleViewWidthLayoutConstraint else { | 
			
				
				
			
		
	
		
		
			
				
					
					|  |  |  |             owsFail("\(logTag) Missing bubbleSuperview") |  |  |  |  | 
			
		
	
		
		
	
		
		
			
				
					
					|  |  |  |             return |  |  |  |             return | 
			
		
	
		
		
			
				
					
					|  |  |  |         } |  |  |  |         } | 
			
		
	
		
		
			
				
					
					|  |  |  |         guard let messageTextTopConstraint = messageTextTopConstraint else { |  |  |  |         guard let messageBubbleViewHeightLayoutConstraint = messageBubbleViewHeightLayoutConstraint else { | 
			
				
				
			
		
	
		
		
			
				
					
					|  |  |  |             owsFail("\(logTag) Missing messageTextTopConstraint") |  |  |  |  | 
			
		
	
		
		
	
		
		
			
				
					
					|  |  |  |             return |  |  |  |             return | 
			
		
	
		
		
			
				
					
					|  |  |  |         } |  |  |  |         } | 
			
		
	
		
		
			
				
					
					|  |  |  |         guard let messageTextHeightLayoutConstraint = messageTextHeightLayoutConstraint else { |  |  |  | 
 | 
			
				
				
			
		
	
		
		
			
				
					
					|  |  |  |             owsFail("\(logTag) Missing messageTextHeightLayoutConstraint") |  |  |  |         messageBubbleView.contentWidth = contentWidth() | 
			
				
				
			
		
	
		
		
	
		
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |         let messageBubbleSize = messageBubbleView.size(forContentWidth: contentWidth()) | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |         messageBubbleViewWidthLayoutConstraint.constant = messageBubbleSize.width | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |         messageBubbleViewHeightLayoutConstraint.constant = messageBubbleSize.height | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |     } | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |     // MARK: - Event Handlers | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |     func messageBubbleTapped(sender: UIGestureRecognizer) { | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |         guard let messageBubbleView = messageBubbleView else { | 
			
		
	
		
		
			
				
					
					|  |  |  |             return |  |  |  |             return | 
			
		
	
		
		
			
				
					
					|  |  |  |         } |  |  |  |         } | 
			
		
	
		
		
			
				
					
					|  |  |  |         guard let messageTextProxyViewHeightConstraint = messageTextProxyViewHeightConstraint else { |  |  |  |         guard sender.state == .recognized else { | 
			
				
				
			
		
	
		
		
			
				
					
					|  |  |  |             owsFail("\(logTag) Missing messageTextProxyViewHeightConstraint") |  |  |  |  | 
			
		
	
		
		
	
		
		
			
				
					
					|  |  |  |             return |  |  |  |             return | 
			
		
	
		
		
			
				
					
					|  |  |  |         } |  |  |  |         } | 
			
		
	
		
		
			
				
					
					|  |  |  |         guard let bubbleViewWidthConstraint = bubbleViewWidthConstraint else { |  |  |  |         if let outgoingMessage = viewItem.interaction as? TSOutgoingMessage { | 
			
				
				
			
		
	
		
		
			
				
					
					|  |  |  |             owsFail("\(logTag) Missing bubbleViewWidthConstraint") |  |  |  |             switch outgoingMessage.messageState { | 
			
				
				
			
		
	
		
		
			
				
					
					|  |  |  |             return |  |  |  |             case .attemptingOut, | 
			
				
				
			
		
	
		
		
	
		
		
	
		
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |                  .unsent: | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |                 // Ignore taps on "unsent" and "sending" messages. | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |                 return | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |             default: | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |                 break | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |             } | 
			
		
	
		
		
			
				
					
					|  |  |  |         } |  |  |  |         } | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |         if messageTextView.width() != messageTextProxyView.width() { |  |  |  |     let locationInMessageBubble = sender.location(in: messageBubbleView) | 
			
				
				
			
		
	
		
		
			
				
					
					|  |  |  |             owsFail("\(logTag) messageTextView.width \(messageTextView.width) != messageTextProxyView.width \(messageTextProxyView.width)") |  |  |  |         switch messageBubbleView.gestureLocation(forLocation: locationInMessageBubble) { | 
			
				
				
			
		
	
		
		
			
				
					
					|  |  |  |         } |  |  |  |         case .default: | 
			
				
				
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  |             break | 
			
				
				
			
		
	
		
		
			
				
					
					|  |  |  |         let maxBubbleWidth = bubbleSuperview.width() - (bubbleViewHMargin * 2) |  |  |  |         case .oversizeText: | 
			
				
				
			
		
	
		
		
			
				
					
					|  |  |  |         let maxTextWidth = maxBubbleWidth - (messageTailEdgeMargin + messageNoTailEdgeMargin) |  |  |  |             let viewController = LongTextViewController(viewItem: viewItem) | 
			
				
				
			
		
	
		
		
			
				
					
					|  |  |  |         // Measure the total text size. |  |  |  |             self.navigationController?.pushViewController(viewController, animated: true) | 
			
				
				
			
		
	
		
		
			
				
					
					|  |  |  |         let textSize = messageTextView.sizeThatFits(CGSize(width: maxTextWidth, height: CGFloat.greatestFiniteMagnitude)) |  |  |  |             break | 
			
				
				
			
		
	
		
		
			
				
					
					|  |  |  |         // Measure the size of the scroll view viewport. |  |  |  |         case .media: | 
			
				
				
			
		
	
		
		
			
				
					
					|  |  |  |         let scrollViewSize = scrollView.frame.size |  |  |  |             // TODO: We could show MediaGalleryViewController? | 
			
				
				
			
		
	
		
		
			
				
					
					|  |  |  |         // Obtain the current scroll view content offset (scroll state). |  |  |  |             break | 
			
				
				
			
		
	
		
		
			
				
					
					|  |  |  |         let scrollViewContentOffset = scrollView.contentOffset |  |  |  |         case .quotedReply: | 
			
				
				
			
		
	
		
		
			
				
					
					|  |  |  |         // Obtain the location of the text view proxy relative to the content view. |  |  |  |             break | 
			
				
				
			
		
	
		
		
			
				
					
					|  |  |  |         let textProxyOffset = contentView.convert(CGPoint.zero, from: messageTextProxyView) |  |  |  |         } | 
			
				
				
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         // 1. The bubble view's width should fit the text content. |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         let bubbleViewWidth = ceil(textSize.width + messageTailEdgeMargin + messageNoTailEdgeMargin) |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         bubbleViewWidthConstraint.constant = bubbleViewWidth |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         // 2. The text proxy's height should reflect the entire text content. |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         let messageTextProxyViewHeight = ceil(textSize.height) |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         messageTextProxyViewHeightConstraint.constant = messageTextProxyViewHeight |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         // 3. We only want to render a single screenful of text content at a time. |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         //    The height of the text view should reflect the height of the scrollview's |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         //    viewport. |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         let messageTextViewHeight = ceil(min(textSize.height, scrollViewSize.height)) |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         messageTextHeightLayoutConstraint.constant = messageTextViewHeight |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         // 4. We want to move the text view around within the proxy in response to |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         //    scroll state changes so that it can render the part of the proxy which |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         //    is on screen. |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         let minMessageTextViewY = CGFloat(0) |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         let maxMessageTextViewY = messageTextProxyViewHeight - messageTextViewHeight |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         let rawMessageTextViewY = -textProxyOffset.y + scrollViewContentOffset.y |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         let messageTextViewY = max(minMessageTextViewY, min(maxMessageTextViewY, rawMessageTextViewY)) |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         messageTextTopConstraint.constant = messageTextViewY |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         // 5. We want to scroll the text view's content so that the text view |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         //    renders the appropriate content for the scrollview's scroll state. |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         messageTextView.contentOffset = CGPoint(x: 0, y: messageTextViewY) |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |     } |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |     public func scrollViewDidScroll(_ scrollView: UIScrollView) { |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         Logger.verbose("\(logTag) scrollViewDidScroll") |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         updateTextLayout() |  |  |  |  | 
			
		
	
		
		
	
		
		
	
		
		
	
		
		
	
		
		
	
		
		
	
		
		
	
		
		
	
		
		
	
		
		
	
		
		
	
		
		
	
		
		
	
		
		
	
		
		
			
				
					
					|  |  |  |     } |  |  |  |     } | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |     // MediaGalleryDataSourceDelegate |  |  |  |     // MediaGalleryDataSourceDelegate | 
			
		
	
	
		
		
			
				
					|  |  | 
 |