mirror of https://github.com/oxen-io/session-ios
				
				
				
			
			You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			729 lines
		
	
	
		
			27 KiB
		
	
	
	
		
			Swift
		
	
			
		
		
	
	
			729 lines
		
	
	
		
			27 KiB
		
	
	
	
		
			Swift
		
	
| //
 | |
| //  Copyright (c) 2019 Open Whisper Systems. All rights reserved.
 | |
| //
 | |
| 
 | |
| import Foundation
 | |
| import SignalUtilitiesKit
 | |
| import SignalUtilitiesKit
 | |
| 
 | |
| @objc
 | |
| enum MessageMetadataViewMode: UInt {
 | |
|     case focusOnMessage
 | |
|     case focusOnMetadata
 | |
| }
 | |
| 
 | |
| @objc
 | |
| protocol MessageDetailViewDelegate: AnyObject {
 | |
|     func detailViewMessageWasDeleted(_ messageDetailViewController: MessageDetailViewController)
 | |
| }
 | |
| 
 | |
| @objc
 | |
| class MessageDetailViewController: OWSViewController, MediaGalleryDataSourceDelegate, OWSMessageBubbleViewDelegate {
 | |
| 
 | |
|     @objc
 | |
|     weak var delegate: MessageDetailViewDelegate?
 | |
| 
 | |
|     // MARK: Properties
 | |
| 
 | |
|     let uiDatabaseConnection: YapDatabaseConnection
 | |
| 
 | |
|     var bubbleView: UIView?
 | |
| 
 | |
|     let mode: MessageMetadataViewMode
 | |
|     let viewItem: ConversationViewItem
 | |
|     var message: TSMessage
 | |
|     var wasDeleted: Bool = false
 | |
| 
 | |
|     var messageBubbleView: OWSMessageBubbleView?
 | |
|     var messageBubbleViewWidthLayoutConstraint: NSLayoutConstraint?
 | |
|     var messageBubbleViewHeightLayoutConstraint: NSLayoutConstraint?
 | |
| 
 | |
|     var scrollView: UIScrollView!
 | |
|     var contentView: UIView?
 | |
| 
 | |
|     var attachment: TSAttachment?
 | |
|     var dataSource: DataSource?
 | |
|     var attachmentStream: TSAttachmentStream?
 | |
|     var messageBody: String?
 | |
| 
 | |
|     lazy var shouldShowUD: Bool = {
 | |
|         return self.preferences.shouldShowUnidentifiedDeliveryIndicators()
 | |
|     }()
 | |
| 
 | |
|     var conversationStyle: ConversationStyle
 | |
| 
 | |
|     // MARK: Dependencies
 | |
| 
 | |
|     var preferences: OWSPreferences {
 | |
|         return Environment.shared.preferences
 | |
|     }
 | |
| 
 | |
|     // MARK: Initializers
 | |
| 
 | |
|     @available(*, unavailable, message:"use other constructor instead.")
 | |
|     required init?(coder aDecoder: NSCoder) {
 | |
|         notImplemented()
 | |
|     }
 | |
| 
 | |
|     @objc
 | |
|     required init(viewItem: ConversationViewItem, message: TSMessage, thread: TSThread, mode: MessageMetadataViewMode) {
 | |
|         self.viewItem = viewItem
 | |
|         self.message = message
 | |
|         self.mode = mode
 | |
|         self.uiDatabaseConnection = OWSPrimaryStorage.shared().uiDatabaseConnection
 | |
|         self.conversationStyle = ConversationStyle(thread: thread)
 | |
| 
 | |
|         super.init(nibName: nil, bundle: nil)
 | |
|     }
 | |
| 
 | |
|     // MARK: View Lifecycle
 | |
| 
 | |
|     override func viewDidLoad() {
 | |
|         super.viewDidLoad()
 | |
| 
 | |
|         do {
 | |
|             try updateMessageToLatest()
 | |
|         } catch DetailViewError.messageWasDeleted {
 | |
|             self.delegate?.detailViewMessageWasDeleted(self)
 | |
|         } catch {
 | |
|             owsFailDebug("unexpected error")
 | |
|         }
 | |
| 
 | |
|         self.conversationStyle.viewWidth = view.width()
 | |
| 
 | |
|         ViewControllerUtilities.setUpDefaultSessionStyle(for: self, title: NSLocalizedString("MESSAGE_METADATA_VIEW_TITLE", comment: "Title for the 'message metadata' view."), hasCustomBackButton: false)
 | |
| 
 | |
|         createViews()
 | |
| 
 | |
|         self.view.layoutIfNeeded()
 | |
| 
 | |
|         NotificationCenter.default.addObserver(self,
 | |
|             selector: #selector(uiDatabaseDidUpdate),
 | |
|             name: .OWSUIDatabaseConnectionDidUpdate,
 | |
|             object: OWSPrimaryStorage.shared().dbNotificationObject)
 | |
|     }
 | |
| 
 | |
|     override public func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
 | |
|         Logger.debug("")
 | |
| 
 | |
|         super.viewWillTransition(to: size, with: coordinator)
 | |
| 
 | |
|         self.conversationStyle.viewWidth = size.width
 | |
|     }
 | |
| 
 | |
|     override func viewWillAppear(_ animated: Bool) {
 | |
|         super.viewWillAppear(animated)
 | |
| 
 | |
|         updateMessageBubbleViewLayout()
 | |
| 
 | |
|         if mode == .focusOnMetadata {
 | |
|             if let bubbleView = self.bubbleView {
 | |
|                 // Force layout.
 | |
|                 view.setNeedsLayout()
 | |
|                 view.layoutIfNeeded()
 | |
| 
 | |
|                 let contentHeight = scrollView.contentSize.height
 | |
|                 let scrollViewHeight = scrollView.frame.size.height
 | |
|                 guard contentHeight >=  scrollViewHeight else {
 | |
|                     // All content is visible within the scroll view. No need to offset.
 | |
|                     return
 | |
|                 }
 | |
| 
 | |
|                 // We want to include at least a little portion of the message, but scroll no farther than necessary.
 | |
|                 let showAtLeast: CGFloat = 50
 | |
|                 let bubbleViewBottom = bubbleView.superview!.convert(bubbleView.frame, to: scrollView).maxY
 | |
|                 let maxOffset =  bubbleViewBottom - showAtLeast
 | |
|                 let lastPage = contentHeight - scrollViewHeight
 | |
| 
 | |
|                 let offset = CGPoint(x: 0, y: min(maxOffset, lastPage))
 | |
| 
 | |
|                 scrollView.setContentOffset(offset, animated: false)
 | |
|             }
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     // MARK: - Create Views
 | |
| 
 | |
|     private func createViews() {
 | |
|         view.backgroundColor = .clear
 | |
| 
 | |
|         let scrollView = UIScrollView()
 | |
|         self.scrollView = scrollView
 | |
|         view.addSubview(scrollView)
 | |
|         scrollView.autoPinWidthToSuperview(withMargin: 0)
 | |
| 
 | |
|         if scrollView.applyInsetsFix() {
 | |
|             scrollView.autoPinEdge(.top, to: .top, of: view)
 | |
|         } else {
 | |
|             scrollView.autoPinEdge(toSuperviewEdge: .top)
 | |
|         }
 | |
| 
 | |
|         let contentView = UIView.container()
 | |
|         self.contentView = contentView
 | |
|         scrollView.addSubview(contentView)
 | |
|         contentView.autoPinLeadingToSuperviewMargin()
 | |
|         contentView.autoPinTrailingToSuperviewMargin()
 | |
|         contentView.autoPinEdge(toSuperviewEdge: .top)
 | |
|         contentView.autoPinEdge(toSuperviewEdge: .bottom)
 | |
|         scrollView.layoutMargins = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
 | |
|         scrollView.contentInset = UIEdgeInsets(top: 20, left: 0, bottom: 20, right: 0)
 | |
| 
 | |
|         if hasMediaAttachment {
 | |
|             let footer = UIToolbar()
 | |
|             view.addSubview(footer)
 | |
|             footer.autoPinWidthToSuperview(withMargin: 0)
 | |
|             footer.autoPinEdge(.top, to: .bottom, of: scrollView)
 | |
|             footer.autoPinEdge(.bottom, to: .bottom, of: view)
 | |
| 
 | |
|             footer.items = [
 | |
|                 UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil),
 | |
|                 UIBarButtonItem(barButtonSystemItem: .action, target: self, action: #selector(shareButtonPressed)),
 | |
|                 UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
 | |
|             ]
 | |
|         } else {
 | |
|             scrollView.autoPinEdge(toSuperviewEdge: .bottom)
 | |
|         }
 | |
| 
 | |
|         updateContent()
 | |
|     }
 | |
| 
 | |
|     lazy var thread: TSThread = {
 | |
|         var thread: TSThread?
 | |
|         self.uiDatabaseConnection.read { transaction in
 | |
|             thread = self.message.thread(with: transaction)
 | |
|         }
 | |
|         return thread!
 | |
|     }()
 | |
| 
 | |
|     private func updateContent() {
 | |
|         guard let contentView = contentView else {
 | |
|             owsFailDebug("Missing contentView")
 | |
|             return
 | |
|         }
 | |
| 
 | |
|         // Remove any existing content views.
 | |
|         for subview in contentView.subviews {
 | |
|             subview.removeFromSuperview()
 | |
|         }
 | |
| 
 | |
|         var rows = [UIView]()
 | |
| 
 | |
|         // Content
 | |
|         rows += contentRows()
 | |
| 
 | |
|         // Sender?
 | |
|         if let incomingMessage = message as? TSIncomingMessage {
 | |
|             let senderId = incomingMessage.authorId
 | |
|             let threadID = thread.uniqueId!
 | |
|             var senderName: String!
 | |
|             Storage.writeSync { transaction in
 | |
|                 senderName = DisplayNameUtilities2.getDisplayName(for: senderId, inThreadWithID: threadID, using: transaction)
 | |
|             }
 | |
|             rows.append(valueRow(name: NSLocalizedString("MESSAGE_METADATA_VIEW_SENDER",
 | |
|                                                          comment: "Label for the 'sender' field of the 'message metadata' view."),
 | |
|                                  value: senderName))
 | |
|         }
 | |
| 
 | |
|         // Recipient(s)
 | |
|         if let outgoingMessage = message as? TSOutgoingMessage {
 | |
| 
 | |
|             func getSeparator() -> UIView {
 | |
|                 let result = UIView()
 | |
|                 result.set(.height, to: Values.separatorThickness)
 | |
|                 result.backgroundColor = Colors.separator
 | |
|                 return result
 | |
|             }
 | |
| 
 | |
|             if !outgoingMessage.recipientIds().isEmpty {
 | |
|                 rows += [ getSeparator() ]
 | |
|             }
 | |
| 
 | |
|             rows += outgoingMessage.recipientIds().flatMap { publicKey -> [UIView] in
 | |
|                 // We use ContactCellView, not ContactTableViewCell.
 | |
|                 // Table view cells don't layout properly outside the
 | |
|                 // context of a table view.
 | |
|                 let cellView = ContactCellView()
 | |
|                 cellView.configure(withRecipientId: publicKey)
 | |
|                 let wrapper = UIView()
 | |
|                 wrapper.layoutMargins = UIEdgeInsets(top: 8, left: 20, bottom: 8, right: 20)
 | |
|                 wrapper.addSubview(cellView)
 | |
|                 cellView.autoPinEdgesToSuperviewMargins()
 | |
|                 return [ wrapper, getSeparator() ]
 | |
|             }
 | |
| 
 | |
|             if !outgoingMessage.recipientIds().isEmpty {
 | |
|                 rows += [ UIView.vSpacer(10) ]
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         let sentText = DateUtil.formatPastTimestampRelativeToNow(message.timestamp)
 | |
|         let sentRow: UIStackView = valueRow(name: NSLocalizedString("MESSAGE_METADATA_VIEW_SENT_DATE_TIME",
 | |
|                                                                     comment: "Label for the 'sent date & time' field of the 'message metadata' view."),
 | |
|                                             value: sentText)
 | |
|         if let incomingMessage = message as? TSIncomingMessage {
 | |
|             if self.shouldShowUD, incomingMessage.wasReceivedByUD {
 | |
|                 let icon = #imageLiteral(resourceName: "ic_secret_sender_indicator").withRenderingMode(.alwaysTemplate)
 | |
|                 let iconView = UIImageView(image: icon)
 | |
|                 iconView.tintColor = Theme.secondaryColor
 | |
|                 iconView.setContentHuggingHigh()
 | |
|                 sentRow.addArrangedSubview(iconView)
 | |
|                 // keep the icon close to the label.
 | |
|                 let spacerView = UIView()
 | |
|                 spacerView.setContentHuggingLow()
 | |
|                 sentRow.addArrangedSubview(spacerView)
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         sentRow.isUserInteractionEnabled = true
 | |
|         sentRow.addGestureRecognizer(UILongPressGestureRecognizer(target: self, action: #selector(didLongPressSent)))
 | |
|         rows.append(sentRow)
 | |
| 
 | |
|         if message is TSIncomingMessage {
 | |
|             rows.append(valueRow(name: NSLocalizedString("MESSAGE_METADATA_VIEW_RECEIVED_DATE_TIME",
 | |
|                                                          comment: "Label for the 'received date & time' field of the 'message metadata' view."),
 | |
|                                  value: DateUtil.formatPastTimestampRelativeToNow(message.receivedAtTimestamp)))
 | |
|         }
 | |
| 
 | |
|         rows += addAttachmentMetadataRows()
 | |
| 
 | |
|         // TODO: We could include the "disappearing messages" state here.
 | |
| 
 | |
|         let rowStack = UIStackView(arrangedSubviews: rows)
 | |
|         rowStack.axis = .vertical
 | |
|         rowStack.spacing = 5
 | |
|         contentView.addSubview(rowStack)
 | |
|         rowStack.autoPinEdgesToSuperviewMargins()
 | |
|         contentView.layoutIfNeeded()
 | |
|         updateMessageBubbleViewLayout()
 | |
|     }
 | |
| 
 | |
|     private func displayableTextIfText() -> String? {
 | |
|         guard viewItem.hasBodyText else {
 | |
|                 return nil
 | |
|         }
 | |
|         guard let displayableText = viewItem.displayableBodyText else {
 | |
|                 return nil
 | |
|         }
 | |
|         let messageBody = displayableText.fullText
 | |
|         guard messageBody.count > 0  else {
 | |
|             return nil
 | |
|         }
 | |
|         return messageBody
 | |
|     }
 | |
| 
 | |
|     let bubbleViewHMargin: CGFloat = 10
 | |
| 
 | |
|     private func contentRows() -> [UIView] {
 | |
|         var rows = [UIView]()
 | |
| 
 | |
|         let messageBubbleView = OWSMessageBubbleView(frame: CGRect.zero)
 | |
|         messageBubbleView.delegate = self
 | |
|         messageBubbleView.addTapGestureHandler()
 | |
|         self.messageBubbleView = messageBubbleView
 | |
|         messageBubbleView.viewItem = viewItem
 | |
|         messageBubbleView.cellMediaCache = NSCache()
 | |
|         messageBubbleView.conversationStyle = conversationStyle
 | |
|         messageBubbleView.configureViews()
 | |
|         messageBubbleView.loadContent()
 | |
| 
 | |
|         assert(messageBubbleView.isUserInteractionEnabled)
 | |
| 
 | |
|         let row = UIView()
 | |
|         row.addSubview(messageBubbleView)
 | |
|         messageBubbleView.autoPinHeightToSuperview()
 | |
| 
 | |
|         let isIncoming = self.message as? TSIncomingMessage != nil
 | |
|         messageBubbleView.autoPinEdge(toSuperviewEdge: isIncoming ? .leading : .trailing, withInset: bubbleViewHMargin)
 | |
| 
 | |
|         self.messageBubbleViewWidthLayoutConstraint = messageBubbleView.autoSetDimension(.width, toSize: 0)
 | |
|         self.messageBubbleViewHeightLayoutConstraint = messageBubbleView.autoSetDimension(.height, toSize: 0)
 | |
|         rows.append(row)
 | |
| 
 | |
|         if rows.isEmpty {
 | |
|             // Neither attachment nor body.
 | |
|             owsFailDebug("Message has neither attachment nor body.")
 | |
|             rows.append(valueRow(name: NSLocalizedString("MESSAGE_METADATA_VIEW_NO_ATTACHMENT_OR_BODY",
 | |
|                                                          comment: "Label for messages without a body or attachment in the 'message metadata' view."),
 | |
|                                  value: ""))
 | |
|         }
 | |
| 
 | |
|         let spacer = UIView()
 | |
|         spacer.autoSetDimension(.height, toSize: 15)
 | |
|         rows.append(spacer)
 | |
| 
 | |
|         return rows
 | |
|     }
 | |
| 
 | |
|     private func fetchAttachment(transaction: YapDatabaseReadTransaction) -> TSAttachment? {
 | |
|         // TODO: Support multi-image messages.
 | |
|         guard let attachmentId = message.attachmentIds.firstObject as? String else {
 | |
|             return nil
 | |
|         }
 | |
| 
 | |
|         guard let attachment = TSAttachment.fetch(uniqueId: attachmentId, transaction: transaction) else {
 | |
|             Logger.warn("Missing attachment. Was it deleted?")
 | |
|             return nil
 | |
|         }
 | |
| 
 | |
|         return attachment
 | |
|     }
 | |
| 
 | |
|     var hasMediaAttachment: Bool {
 | |
|         guard let attachment = self.attachment else {
 | |
|             return false
 | |
|         }
 | |
| 
 | |
|         guard attachment.contentType != OWSMimeTypeOversizeTextMessage else {
 | |
|             // to the user, oversized text attachments should behave
 | |
|             // just like regular text messages.
 | |
|             return false
 | |
|         }
 | |
| 
 | |
|         return true
 | |
|     }
 | |
| 
 | |
|     private func addAttachmentMetadataRows() -> [UIView] {
 | |
|         guard hasMediaAttachment else {
 | |
|             return []
 | |
|         }
 | |
| 
 | |
|         var rows = [UIView]()
 | |
| 
 | |
|         if let attachment = self.attachment {
 | |
|             // Only show MIME types in DEBUG builds.
 | |
|             if _isDebugAssertConfiguration() {
 | |
|                 let contentType = attachment.contentType
 | |
|                 rows.append(valueRow(name: NSLocalizedString("MESSAGE_METADATA_VIEW_ATTACHMENT_MIME_TYPE",
 | |
|                                                              comment: "Label for the MIME type of attachments in the 'message metadata' view."),
 | |
|                                      value: contentType))
 | |
|             }
 | |
| 
 | |
|             if let sourceFilename = attachment.sourceFilename {
 | |
|                 rows.append(valueRow(name: NSLocalizedString("MESSAGE_METADATA_VIEW_SOURCE_FILENAME",
 | |
|                                                              comment: "Label for the original filename of any attachment in the 'message metadata' view."),
 | |
|                                      value: sourceFilename))
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         if let dataSource = self.dataSource {
 | |
|             let fileSize = dataSource.dataLength()
 | |
|             rows.append(valueRow(name: NSLocalizedString("MESSAGE_METADATA_VIEW_ATTACHMENT_FILE_SIZE",
 | |
|                                                          comment: "Label for file size of attachments in the 'message metadata' view."),
 | |
|                                  value: OWSFormat.formatFileSize(UInt(fileSize))))
 | |
|         }
 | |
| 
 | |
|         return rows
 | |
|     }
 | |
| 
 | |
|     private func buildUDAccessoryView(text: String) -> UIView {
 | |
|         let label = UILabel()
 | |
|         label.textColor = Theme.secondaryColor
 | |
|         label.text = text
 | |
|         label.textAlignment = .right
 | |
|         label.font = UIFont.ows_mediumFont(withSize: 13)
 | |
| 
 | |
|         let image = #imageLiteral(resourceName: "ic_secret_sender_indicator").withRenderingMode(.alwaysTemplate)
 | |
|         let imageView = UIImageView(image: image)
 | |
|         imageView.tintColor = Theme.middleGrayColor
 | |
| 
 | |
|         let hStack = UIStackView(arrangedSubviews: [imageView, label])
 | |
|         hStack.axis = .horizontal
 | |
|         hStack.spacing = 8
 | |
| 
 | |
|         return hStack
 | |
|     }
 | |
| 
 | |
|     private func nameLabel(text: String) -> UILabel {
 | |
|         let label = UILabel()
 | |
|         label.textColor = Theme.primaryColor
 | |
|         label.font = UIFont.ows_mediumFont(withSize: 14)
 | |
|         label.text = text
 | |
|         label.setContentHuggingHorizontalHigh()
 | |
|         return label
 | |
|     }
 | |
| 
 | |
|     private func valueLabel(text: String) -> UILabel {
 | |
|         let label = UILabel()
 | |
|         label.textColor = Theme.primaryColor
 | |
|         label.font = UIFont.ows_regularFont(withSize: 14)
 | |
|         label.text = text
 | |
|         label.setContentHuggingHorizontalLow()
 | |
|         return label
 | |
|     }
 | |
| 
 | |
|     private func valueRow(name: String, value: String, subtitle: String = "") -> UIStackView {
 | |
|         let nameLabel = self.nameLabel(text: name)
 | |
|         let valueLabel = self.valueLabel(text: value)
 | |
|         let hStackView = UIStackView(arrangedSubviews: [nameLabel, valueLabel])
 | |
|         hStackView.axis = .horizontal
 | |
|         hStackView.spacing = 10
 | |
|         hStackView.layoutMargins = UIEdgeInsets(top: 0, left: 20, bottom: 0, right: 20)
 | |
|         hStackView.isLayoutMarginsRelativeArrangement = true
 | |
| 
 | |
|         if subtitle.count > 0 {
 | |
|             let subtitleLabel = self.valueLabel(text: subtitle)
 | |
|             subtitleLabel.textColor = Theme.secondaryColor
 | |
|             hStackView.addArrangedSubview(subtitleLabel)
 | |
|         }
 | |
| 
 | |
|         return hStackView
 | |
|     }
 | |
| 
 | |
|     // MARK: - Actions
 | |
| 
 | |
|     @objc func shareButtonPressed() {
 | |
|         guard let attachmentStream = attachmentStream else {
 | |
|             Logger.error("Share button should only be shown with attachment, but no attachment found.")
 | |
|             return
 | |
|         }
 | |
|         AttachmentSharing.showShareUI(forAttachment: attachmentStream)
 | |
|     }
 | |
| 
 | |
|     // MARK: - Actions
 | |
| 
 | |
|     enum DetailViewError: Error {
 | |
|         case messageWasDeleted
 | |
|     }
 | |
| 
 | |
|     // This method should be called after self.databaseConnection.beginLongLivedReadTransaction().
 | |
|     private func updateMessageToLatest() throws {
 | |
| 
 | |
|         AssertIsOnMainThread()
 | |
| 
 | |
|         try self.uiDatabaseConnection.read { transaction in
 | |
|             guard let uniqueId = self.message.uniqueId else {
 | |
|                 Logger.error("Message is missing uniqueId.")
 | |
|                 return
 | |
|             }
 | |
|             guard let newMessage = TSInteraction.fetch(uniqueId: uniqueId, transaction: transaction) as? TSMessage else {
 | |
|                 Logger.error("Message was deleted")
 | |
|                 throw DetailViewError.messageWasDeleted
 | |
|             }
 | |
|             self.message = newMessage
 | |
|             self.attachment = self.fetchAttachment(transaction: transaction)
 | |
|             self.attachmentStream = self.attachment as? TSAttachmentStream
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     @objc internal func uiDatabaseDidUpdate(notification: NSNotification) {
 | |
|         AssertIsOnMainThread()
 | |
| 
 | |
|         guard !wasDeleted else {
 | |
|             // Item was deleted in the tile view gallery.
 | |
|             // Don't bother re-rendering, it will fail and we'll soon be dismissed.
 | |
|             return
 | |
|         }
 | |
| 
 | |
|         guard let notifications = notification.userInfo?[OWSUIDatabaseConnectionNotificationsKey] as? [Notification] else {
 | |
|             owsFailDebug("notifications was unexpectedly nil")
 | |
|             return
 | |
|         }
 | |
| 
 | |
|         guard let uniqueId = self.message.uniqueId else {
 | |
|             Logger.error("Message is missing uniqueId.")
 | |
|             return
 | |
|         }
 | |
| 
 | |
|         guard self.uiDatabaseConnection.hasChange(forKey: uniqueId,
 | |
|                                                  inCollection: TSInteraction.collection(),
 | |
|                                                  in: notifications) else {
 | |
|                                                     Logger.debug("No relevant changes.")
 | |
|                                                     return
 | |
|         }
 | |
| 
 | |
|         do {
 | |
|             try updateMessageToLatest()
 | |
|         } catch DetailViewError.messageWasDeleted {
 | |
|             DispatchQueue.main.async {
 | |
|                 self.delegate?.detailViewMessageWasDeleted(self)
 | |
|             }
 | |
|             return
 | |
|         } catch {
 | |
|             owsFailDebug("unexpected error: \(error)")
 | |
|         }
 | |
|         updateContent()
 | |
|     }
 | |
| 
 | |
|     private func string(for messageReceiptStatus: MessageReceiptStatus) -> String {
 | |
|         switch messageReceiptStatus {
 | |
|         case .uploading:
 | |
|             return NSLocalizedString("MESSAGE_METADATA_VIEW_MESSAGE_STATUS_UPLOADING",
 | |
|                               comment: "Status label for messages which are uploading.")
 | |
|         case .sending:
 | |
|             return NSLocalizedString("MESSAGE_METADATA_VIEW_MESSAGE_STATUS_SENDING",
 | |
|                               comment: "Status label for messages which are sending.")
 | |
|         case .sent:
 | |
|             return NSLocalizedString("MESSAGE_METADATA_VIEW_MESSAGE_STATUS_SENT",
 | |
|                               comment: "Status label for messages which are sent.")
 | |
|         case .delivered:
 | |
|             return NSLocalizedString("MESSAGE_METADATA_VIEW_MESSAGE_STATUS_DELIVERED",
 | |
|                               comment: "Status label for messages which are delivered.")
 | |
|         case .read:
 | |
|             return NSLocalizedString("MESSAGE_METADATA_VIEW_MESSAGE_STATUS_READ",
 | |
|                               comment: "Status label for messages which are read.")
 | |
|         case .failed:
 | |
|             return NSLocalizedString("MESSAGE_METADATA_VIEW_MESSAGE_STATUS_FAILED",
 | |
|                                      comment: "Status label for messages which are failed.")
 | |
|         case .skipped:
 | |
|             return NSLocalizedString("MESSAGE_METADATA_VIEW_MESSAGE_STATUS_SKIPPED",
 | |
|                                      comment: "Status label for messages which were skipped.")
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     // MARK: - Message Bubble Layout
 | |
| 
 | |
|     private func updateMessageBubbleViewLayout() {
 | |
|         guard let messageBubbleView = messageBubbleView else {
 | |
|             return
 | |
|         }
 | |
|         guard let messageBubbleViewWidthLayoutConstraint = messageBubbleViewWidthLayoutConstraint else {
 | |
|             return
 | |
|         }
 | |
|         guard let messageBubbleViewHeightLayoutConstraint = messageBubbleViewHeightLayoutConstraint else {
 | |
|             return
 | |
|         }
 | |
| 
 | |
|         let messageBubbleSize = messageBubbleView.measureSize()
 | |
|         messageBubbleViewWidthLayoutConstraint.constant = messageBubbleSize.width
 | |
|         messageBubbleViewHeightLayoutConstraint.constant = messageBubbleSize.height
 | |
|     }
 | |
| 
 | |
|     // MARK: OWSMessageBubbleViewDelegate
 | |
| 
 | |
|     func didTapImageViewItem(_ viewItem: ConversationViewItem, attachmentStream: TSAttachmentStream, imageView: UIView) {
 | |
|         let mediaGallery = MediaGallery(thread: self.thread)
 | |
| 
 | |
|         mediaGallery.addDataSourceDelegate(self)
 | |
|         mediaGallery.presentDetailView(fromViewController: self, mediaAttachment: attachmentStream, replacingView: imageView)
 | |
|     }
 | |
| 
 | |
|     func didTapVideoViewItem(_ viewItem: ConversationViewItem, attachmentStream: TSAttachmentStream, imageView: UIView) {
 | |
|         let mediaGallery = MediaGallery(thread: self.thread)
 | |
| 
 | |
|         mediaGallery.addDataSourceDelegate(self)
 | |
|         mediaGallery.presentDetailView(fromViewController: self, mediaAttachment: attachmentStream, replacingView: imageView)
 | |
|     }
 | |
| 
 | |
|     var audioAttachmentPlayer: OWSAudioPlayer?
 | |
| 
 | |
|     func didTapAudioViewItem(_ viewItem: ConversationViewItem, attachmentStream: TSAttachmentStream) {
 | |
|         AssertIsOnMainThread()
 | |
| 
 | |
|         guard let mediaURL = attachmentStream.originalMediaURL else {
 | |
|             owsFailDebug("mediaURL was unexpectedly nil for attachment: \(attachmentStream)")
 | |
|             return
 | |
|         }
 | |
| 
 | |
|         guard FileManager.default.fileExists(atPath: mediaURL.path) else {
 | |
|             owsFailDebug("audio file missing at path: \(mediaURL)")
 | |
|             return
 | |
|         }
 | |
| 
 | |
|         if let audioAttachmentPlayer = self.audioAttachmentPlayer {
 | |
|             // Is this player associated with this media adapter?
 | |
|             if audioAttachmentPlayer.owner === viewItem {
 | |
|                 // Tap to pause & unpause.
 | |
|                 audioAttachmentPlayer.togglePlayState()
 | |
|                 return
 | |
|             }
 | |
|             audioAttachmentPlayer.stop()
 | |
|             self.audioAttachmentPlayer = nil
 | |
|         }
 | |
| 
 | |
|         let audioAttachmentPlayer = OWSAudioPlayer(mediaUrl: mediaURL, audioBehavior: .audioMessagePlayback, delegate: viewItem)
 | |
|         self.audioAttachmentPlayer = audioAttachmentPlayer
 | |
| 
 | |
|         // Associate the player with this media adapter.
 | |
|         audioAttachmentPlayer.owner = viewItem
 | |
|         audioAttachmentPlayer.play()
 | |
|     }
 | |
| 
 | |
|     func didPanAudioViewItem(toCurrentTime currentTime: TimeInterval) {
 | |
|         // TODO: Implement
 | |
|     }
 | |
| 
 | |
|     func didTapTruncatedTextMessage(_ conversationItem: ConversationViewItem) {
 | |
|         guard let navigationController = self.navigationController else {
 | |
|             owsFailDebug("navigationController was unexpectedly nil")
 | |
|             return
 | |
|         }
 | |
| 
 | |
|         let viewController = LongTextViewController(viewItem: viewItem)
 | |
|         viewController.delegate = self
 | |
|         navigationController.pushViewController(viewController, animated: true)
 | |
|     }
 | |
| 
 | |
|     func didTapFailedIncomingAttachment(_ viewItem: ConversationViewItem) {
 | |
|         // no - op
 | |
|     }
 | |
| 
 | |
|     func didTapFailedOutgoingMessage(_ message: TSOutgoingMessage) {
 | |
|         // no - op
 | |
|     }
 | |
| 
 | |
|     func didTapConversationItem(_ viewItem: ConversationViewItem, quotedReply: OWSQuotedReplyModel) {
 | |
|         // no - op
 | |
|     }
 | |
| 
 | |
|     func didTapConversationItem(_ viewItem: ConversationViewItem, quotedReply: OWSQuotedReplyModel, failedThumbnailDownloadAttachmentPointer attachmentPointer: TSAttachmentPointer) {
 | |
|         // no - op
 | |
|     }
 | |
| 
 | |
|     func didTapConversationItem(_ viewItem: ConversationViewItem, linkPreview: OWSLinkPreview) {
 | |
|         guard let urlString = linkPreview.urlString else {
 | |
|             owsFailDebug("Missing url.")
 | |
|             return
 | |
|         }
 | |
|         guard let url = URL(string: urlString) else {
 | |
|             owsFailDebug("Invalid url: \(urlString).")
 | |
|             return
 | |
|         }
 | |
|         UIApplication.shared.openURL(url)
 | |
|     }
 | |
| 
 | |
|     @objc func didLongPressSent(sender: UIGestureRecognizer) {
 | |
|         guard sender.state == .began else {
 | |
|             return
 | |
|         }
 | |
|         let messageTimestamp = "\(message.timestamp)"
 | |
|         UIPasteboard.general.string = messageTimestamp
 | |
|     }
 | |
| 
 | |
|     var lastSearchedText: String? {
 | |
|         return nil
 | |
|     }
 | |
| 
 | |
|     // MediaGalleryDataSourceDelegate
 | |
| 
 | |
|     func mediaGalleryDataSource(_ mediaGalleryDataSource: MediaGalleryDataSource, willDelete items: [MediaGalleryItem], initiatedBy: AnyObject) {
 | |
|         Logger.info("")
 | |
| 
 | |
|         guard (items.map({ $0.message }) == [self.message]) else {
 | |
|             // Should only be one message we can delete when viewing message details
 | |
|             owsFailDebug("Unexpectedly informed of irrelevant message deletion")
 | |
|             return
 | |
|         }
 | |
| 
 | |
|         self.wasDeleted = true
 | |
|     }
 | |
| 
 | |
|     func mediaGalleryDataSource(_ mediaGalleryDataSource: MediaGalleryDataSource, deletedSections: IndexSet, deletedItems: [IndexPath]) {
 | |
|         self.dismiss(animated: true) {
 | |
|             self.navigationController?.popViewController(animated: true)
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     // MARK: - ContactShareViewHelperDelegate
 | |
| 
 | |
|     public func didCreateOrEditContact() {
 | |
|         updateContent()
 | |
|         self.dismiss(animated: true)
 | |
|     }
 | |
| }
 | |
| 
 | |
| extension MessageDetailViewController: LongTextViewDelegate {
 | |
|     func longTextViewMessageWasDeleted(_ longTextViewController: LongTextViewController) {
 | |
|         self.delegate?.detailViewMessageWasDeleted(self)
 | |
|     }
 | |
| }
 |