mirror of
https://github.com/oxen-io/session-ios.git
synced 2023-12-13 21:30:14 +01:00
411de65b40
TODO: - [ ] layout is shite // FREEBIE
730 lines
30 KiB
Swift
730 lines
30 KiB
Swift
//
|
|
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
|
|
//
|
|
|
|
import Foundation
|
|
import SignalServiceKit
|
|
import SignalMessaging
|
|
|
|
@objc
|
|
enum MessageMetadataViewMode: UInt {
|
|
case focusOnMessage
|
|
case focusOnMetadata
|
|
}
|
|
|
|
class MessageDetailViewController: OWSViewController, UIScrollViewDelegate {
|
|
|
|
static let TAG = "[MessageDetailViewController]"
|
|
let TAG = "[MessageDetailViewController]"
|
|
|
|
// MARK: Properties
|
|
|
|
let contactsManager: OWSContactsManager
|
|
|
|
let databaseConnection: YapDatabaseConnection
|
|
|
|
let bubbleFactory = OWSMessagesBubbleImageFactory()
|
|
var bubbleView: UIView?
|
|
|
|
let mode: MessageMetadataViewMode
|
|
let viewItem: ConversationViewItem
|
|
var message: TSMessage
|
|
|
|
var mediaMessageView: MediaMessageView?
|
|
|
|
// See comments on updateTextLayout.
|
|
var messageTextView: UITextView?
|
|
var messageTextProxyView: UIView?
|
|
var messageTextTopConstraint: NSLayoutConstraint?
|
|
var messageTextHeightLayoutConstraint: NSLayoutConstraint?
|
|
var messageTextProxyViewHeightConstraint: NSLayoutConstraint?
|
|
var bubbleViewWidthConstraint: NSLayoutConstraint?
|
|
|
|
var scrollView: UIScrollView!
|
|
var contentView: UIView?
|
|
|
|
var attachment: TSAttachment?
|
|
var dataSource: DataSource?
|
|
var attachmentStream: TSAttachmentStream?
|
|
var messageBody: String?
|
|
|
|
// MARK: Initializers
|
|
|
|
@available(*, unavailable, message:"use other constructor instead.")
|
|
required init?(coder aDecoder: NSCoder) {
|
|
fatalError("\(#function) is unimplemented.")
|
|
}
|
|
|
|
required init(viewItem: ConversationViewItem, message: TSMessage, mode: MessageMetadataViewMode) {
|
|
self.contactsManager = Environment.current().contactsManager
|
|
self.viewItem = viewItem
|
|
self.message = message
|
|
self.mode = mode
|
|
self.databaseConnection = TSStorageManager.shared().newDatabaseConnection()!
|
|
super.init(nibName: nil, bundle: nil)
|
|
}
|
|
|
|
// MARK: View Lifecycle
|
|
|
|
override func viewDidLoad() {
|
|
super.viewDidLoad()
|
|
|
|
self.databaseConnection.beginLongLivedReadTransaction()
|
|
updateDBConnectionAndMessageToLatest()
|
|
|
|
self.navigationItem.title = NSLocalizedString("MESSAGE_METADATA_VIEW_TITLE",
|
|
comment: "Title for the 'message metadata' view.")
|
|
|
|
createViews()
|
|
|
|
self.view.layoutIfNeeded()
|
|
|
|
NotificationCenter.default.addObserver(self,
|
|
selector: #selector(yapDatabaseModified),
|
|
name: NSNotification.Name.YapDatabaseModified,
|
|
object: nil)
|
|
}
|
|
|
|
override func viewWillAppear(_ animated: Bool) {
|
|
super.viewWillAppear(animated)
|
|
|
|
mediaMessageView?.viewWillAppear(animated)
|
|
|
|
updateTextLayout()
|
|
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
|
|
override func viewWillDisappear(_ animated: Bool) {
|
|
super.viewWillDisappear(animated)
|
|
|
|
mediaMessageView?.viewWillDisappear(animated)
|
|
}
|
|
|
|
// MARK: - Create Views
|
|
|
|
private func createViews() {
|
|
view.backgroundColor = UIColor.white
|
|
|
|
let scrollView = UIScrollView()
|
|
scrollView.delegate = self
|
|
self.scrollView = scrollView
|
|
view.addSubview(scrollView)
|
|
scrollView.autoPinWidthToSuperview(withMargin: 0)
|
|
scrollView.autoPin(toTopLayoutGuideOf: self, withInset: 0)
|
|
|
|
// See notes on how to use UIScrollView with iOS Auto Layout:
|
|
//
|
|
// https://developer.apple.com/library/content/releasenotes/General/RN-iOSSDK-6_0/
|
|
let contentView = UIView.container()
|
|
self.contentView = contentView
|
|
scrollView.addSubview(contentView)
|
|
contentView.autoPinLeadingToSuperview()
|
|
contentView.autoPinTrailingToSuperview()
|
|
contentView.autoPinEdge(toSuperviewEdge: .top)
|
|
contentView.autoPinEdge(toSuperviewEdge: .bottom)
|
|
scrollView.layoutMargins = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
|
|
|
|
let hasAttachment = message.attachmentIds.count > 0
|
|
|
|
if hasAttachment {
|
|
let footer = UIToolbar()
|
|
footer.barTintColor = UIColor.ows_materialBlue()
|
|
view.addSubview(footer)
|
|
footer.autoPinWidthToSuperview(withMargin: 0)
|
|
footer.autoPinEdge(.top, to: .bottom, of: scrollView)
|
|
footer.autoPin(toBottomLayoutGuideOf: self, withInset: 0)
|
|
|
|
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.autoPin(toBottomLayoutGuideOf: self, withInset: 0)
|
|
}
|
|
|
|
updateContent()
|
|
}
|
|
|
|
private func updateContent() {
|
|
guard let contentView = contentView else {
|
|
owsFail("\(TAG) Missing contentView")
|
|
return
|
|
}
|
|
|
|
// Remove any existing content views.
|
|
for subview in contentView.subviews {
|
|
subview.removeFromSuperview()
|
|
}
|
|
|
|
var rows = [UIView]()
|
|
let contactsManager = Environment.current().contactsManager!
|
|
let thread = message.thread
|
|
|
|
// Content
|
|
rows += contentRows()
|
|
|
|
// Sender?
|
|
if let incomingMessage = message as? TSIncomingMessage {
|
|
let senderId = incomingMessage.authorId
|
|
let senderName = contactsManager.contactOrProfileName(forPhoneIdentifier: senderId)
|
|
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 {
|
|
|
|
let isGroupThread = message.thread.isGroupThread()
|
|
|
|
let recipientStatusGroups: [MessageRecipientStatus] = [
|
|
.read,
|
|
.uploading,
|
|
.delivered,
|
|
.sent,
|
|
.sending,
|
|
.failed
|
|
]
|
|
for recipientStatusGroup in recipientStatusGroups {
|
|
var groupRows = [UIView]()
|
|
|
|
// TODO: It'd be nice to inset these dividers from the edge of the screen.
|
|
let addDivider = {
|
|
let divider = UIView()
|
|
divider.backgroundColor = UIColor(white: 0.9, alpha: 1.0)
|
|
divider.autoSetDimension(.height, toSize: 0.5)
|
|
groupRows.append(divider)
|
|
}
|
|
|
|
for recipientId in thread.recipientIdentifiers {
|
|
let (recipientStatus, statusMessage) = MessageRecipientStatusUtils.recipientStatusAndStatusMessage(outgoingMessage: outgoingMessage, recipientId: recipientId, referenceView: self.view)
|
|
|
|
guard recipientStatus == recipientStatusGroup else {
|
|
continue
|
|
}
|
|
|
|
if groupRows.count < 1 {
|
|
if isGroupThread {
|
|
groupRows.append(valueRow(name: string(for: recipientStatusGroup),
|
|
value: ""))
|
|
}
|
|
|
|
addDivider()
|
|
}
|
|
|
|
let cell = ContactTableViewCell()
|
|
cell.configure(withRecipientId: recipientId, contactsManager: self.contactsManager)
|
|
let statusLabel = UILabel()
|
|
statusLabel.text = statusMessage
|
|
statusLabel.textColor = UIColor.ows_darkGray()
|
|
statusLabel.font = UIFont.ows_footnote()
|
|
statusLabel.sizeToFit()
|
|
cell.accessoryView = statusLabel
|
|
cell.autoSetDimension(.height, toSize: ContactTableViewCell.rowHeight())
|
|
cell.setContentHuggingLow()
|
|
cell.isUserInteractionEnabled = false
|
|
groupRows.append(cell)
|
|
}
|
|
|
|
if groupRows.count > 0 {
|
|
addDivider()
|
|
|
|
let spacer = UIView()
|
|
spacer.autoSetDimension(.height, toSize: 10)
|
|
groupRows.append(spacer)
|
|
}
|
|
|
|
Logger.verbose("\(groupRows.count) rows for \(recipientStatusGroup)")
|
|
guard groupRows.count > 0 else {
|
|
continue
|
|
}
|
|
rows += groupRows
|
|
}
|
|
}
|
|
|
|
rows.append(valueRow(name: NSLocalizedString("MESSAGE_METADATA_VIEW_SENT_DATE_TIME",
|
|
comment: "Label for the 'sent date & time' field of the 'message metadata' view."),
|
|
value: DateUtil.formatPastTimestampRelativeToNow(message.timestamp)))
|
|
|
|
if message as? TSIncomingMessage != nil {
|
|
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.timestampForSorting())))
|
|
}
|
|
|
|
rows += addAttachmentMetadataRows()
|
|
|
|
// TODO: We could include the "disappearing messages" state here.
|
|
|
|
var lastRow: UIView?
|
|
for row in rows {
|
|
contentView.addSubview(row)
|
|
row.autoPinLeadingToSuperview()
|
|
row.autoPinTrailingToSuperview()
|
|
|
|
if let lastRow = lastRow {
|
|
row.autoPinEdge(.top, to: .bottom, of: lastRow, withOffset: 5)
|
|
} else {
|
|
row.autoPinEdge(toSuperviewEdge: .top, withInset: 20)
|
|
}
|
|
|
|
lastRow = row
|
|
}
|
|
if let lastRow = lastRow {
|
|
lastRow.autoPinEdge(toSuperviewEdge: .bottom, withInset: 20)
|
|
}
|
|
|
|
if let mediaMessageView = mediaMessageView {
|
|
mediaMessageView.autoPinToSquareAspectRatio()
|
|
}
|
|
}
|
|
|
|
private func displayableTextIfText() -> String? {
|
|
guard viewItem.hasText else {
|
|
return nil
|
|
}
|
|
guard let displayableText = viewItem.displayableText() else {
|
|
return nil
|
|
}
|
|
let messageBody = displayableText.fullText
|
|
guard messageBody.count > 0 else {
|
|
return nil
|
|
}
|
|
return messageBody
|
|
}
|
|
|
|
let bubbleViewHMargin: CGFloat = 10
|
|
let messageTailEdgeMargin: CGFloat = 15
|
|
let messageNoTailEdgeMargin: CGFloat = 10
|
|
|
|
private func contentRows() -> [UIView] {
|
|
var rows = [UIView]()
|
|
|
|
if message.attachmentIds.count > 0 {
|
|
rows += addAttachmentRows()
|
|
}
|
|
|
|
if let messageBody = displayableTextIfText() {
|
|
|
|
self.messageBody = messageBody
|
|
|
|
let isIncoming = self.message as? TSIncomingMessage != nil
|
|
|
|
// UITextView can't render extremely long text due to constraints
|
|
// on the size of its backing buffer, especially when we're
|
|
// 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()
|
|
row.addSubview(bubbleView)
|
|
bubbleView.autoPinHeightToSuperview()
|
|
bubbleView.autoPinEdge(toSuperviewEdge: isIncoming ? .leading : .trailing, withInset: bubbleViewHMargin)
|
|
self.bubbleViewWidthConstraint = bubbleView.autoSetDimension(.width, toSize:0)
|
|
rows.append(row)
|
|
}
|
|
|
|
if rows.count == 0 {
|
|
// Neither attachment nor body.
|
|
owsFail("\(self.TAG) 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 addAttachmentRows() -> [UIView] {
|
|
var rows = [UIView]()
|
|
|
|
guard let attachmentId = message.attachmentIds[0] as? String else {
|
|
owsFail("Invalid attachment")
|
|
return rows
|
|
}
|
|
|
|
guard let attachment = TSAttachment.fetch(uniqueId: attachmentId) else {
|
|
owsFail("Missing attachment")
|
|
return rows
|
|
}
|
|
self.attachment = attachment
|
|
|
|
guard let attachmentStream = attachment as? TSAttachmentStream else {
|
|
rows.append(valueRow(name: NSLocalizedString("MESSAGE_METADATA_VIEW_ATTACHMENT_NOT_YET_DOWNLOADED",
|
|
comment: "Label for 'not yet downloaded' attachments in the 'message metadata' view."),
|
|
value: ""))
|
|
return rows
|
|
}
|
|
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)
|
|
mediaMessageView.backgroundColor = UIColor.white
|
|
self.mediaMessageView = mediaMessageView
|
|
rows.append(mediaMessageView)
|
|
}
|
|
return rows
|
|
}
|
|
|
|
private func addAttachmentMetadataRows() -> [UIView] {
|
|
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 nameLabel(text: String) -> UILabel {
|
|
let label = UILabel()
|
|
label.textColor = UIColor.black
|
|
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 = UIColor.black
|
|
label.font = UIFont.ows_regularFont(withSize: 14)
|
|
label.text = text
|
|
label.setContentHuggingHorizontalLow()
|
|
return label
|
|
}
|
|
|
|
private func valueRow(name: String, value: String, subtitle: String = "") -> UIView {
|
|
let row = UIView.container()
|
|
let nameLabel = self.nameLabel(text: name)
|
|
let valueLabel = self.valueLabel(text: value)
|
|
row.addSubview(nameLabel)
|
|
row.addSubview(valueLabel)
|
|
nameLabel.autoPinLeadingToSuperview(withMargin: 20)
|
|
valueLabel.autoPinTrailingToSuperview(withMargin: 20)
|
|
valueLabel.autoPinLeading(toTrailingOf: nameLabel, margin: 10)
|
|
nameLabel.autoPinEdge(toSuperviewEdge: .top)
|
|
valueLabel.autoPinEdge(toSuperviewEdge: .top)
|
|
|
|
if subtitle.count > 0 {
|
|
let subtitleLabel = self.valueLabel(text: subtitle)
|
|
subtitleLabel.textColor = UIColor.ows_darkGray()
|
|
row.addSubview(subtitleLabel)
|
|
subtitleLabel.autoPinTrailingToSuperview()
|
|
subtitleLabel.autoPinLeading(toTrailingOf: nameLabel, margin: 10)
|
|
subtitleLabel.autoPinEdge(.top, to: .bottom, of: valueLabel, withOffset: 1)
|
|
subtitleLabel.autoPinEdge(toSuperviewEdge: .bottom)
|
|
} else if value.count > 0 {
|
|
valueLabel.autoPinEdge(toSuperviewEdge: .bottom)
|
|
} else {
|
|
nameLabel.autoPinEdge(toSuperviewEdge: .bottom)
|
|
}
|
|
|
|
return row
|
|
}
|
|
|
|
// MARK: - Actions
|
|
|
|
func shareButtonPressed() {
|
|
if let messageBody = messageBody {
|
|
UIPasteboard.general.string = messageBody
|
|
return
|
|
}
|
|
|
|
guard let attachmentStream = attachmentStream else {
|
|
Logger.error("\(TAG) Message has neither attachment nor message body.")
|
|
return
|
|
}
|
|
AttachmentSharing.showShareUI(forAttachment: attachmentStream)
|
|
}
|
|
|
|
func copyToPasteboard() {
|
|
if let messageBody = messageBody {
|
|
UIPasteboard.general.string = messageBody
|
|
return
|
|
}
|
|
|
|
guard let attachmentStream = attachmentStream else {
|
|
Logger.error("\(TAG) Message has neither attachment nor message body.")
|
|
return
|
|
}
|
|
guard let utiType = MIMETypeUtil.utiType(forMIMEType: attachmentStream.contentType) else {
|
|
Logger.error("\(TAG) Attachment has invalid MIME type: \(attachmentStream.contentType).")
|
|
return
|
|
}
|
|
guard let dataSource = dataSource else {
|
|
Logger.error("\(TAG) Attachment missing data source.")
|
|
return
|
|
}
|
|
let data = dataSource.data()
|
|
UIPasteboard.general.setData(data, forPasteboardType: utiType)
|
|
}
|
|
|
|
// MARK: - Actions
|
|
|
|
// This method should be called after self.databaseConnection.beginLongLivedReadTransaction().
|
|
private func updateDBConnectionAndMessageToLatest() {
|
|
|
|
AssertIsOnMainThread()
|
|
|
|
self.databaseConnection.read { transaction in
|
|
guard let uniqueId = self.message.uniqueId else {
|
|
Logger.error("\(self.TAG) Message is missing uniqueId.")
|
|
return
|
|
}
|
|
guard let newMessage = TSInteraction.fetch(uniqueId: uniqueId, transaction: transaction) as? TSMessage else {
|
|
Logger.error("\(self.TAG) Couldn't reload message.")
|
|
return
|
|
}
|
|
self.message = newMessage
|
|
}
|
|
}
|
|
|
|
internal func yapDatabaseModified(notification: NSNotification) {
|
|
AssertIsOnMainThread()
|
|
|
|
let notifications = self.databaseConnection.beginLongLivedReadTransaction()
|
|
|
|
guard let uniqueId = self.message.uniqueId else {
|
|
Logger.error("\(self.TAG) Message is missing uniqueId.")
|
|
return
|
|
}
|
|
guard self.databaseConnection.hasChange(forKey: uniqueId,
|
|
inCollection: TSInteraction.collection(),
|
|
in: notifications) else {
|
|
Logger.debug("\(TAG) No relevant changes.")
|
|
return
|
|
}
|
|
|
|
updateDBConnectionAndMessageToLatest()
|
|
|
|
updateContent()
|
|
}
|
|
|
|
private func string(for messageRecipientStatus: MessageRecipientStatus) -> String {
|
|
switch messageRecipientStatus {
|
|
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.")
|
|
}
|
|
}
|
|
|
|
// MARK: - Text Layout
|
|
|
|
// UITextView can't render extremely long text due to constraints on the size
|
|
// of its backing buffer, especially when we're embedding it "full-size'
|
|
// 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.
|
|
//
|
|
// 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("\(TAG) Missing messageTextProxyView")
|
|
return
|
|
}
|
|
guard let scrollView = scrollView else {
|
|
owsFail("\(TAG) Missing scrollView")
|
|
return
|
|
}
|
|
guard let contentView = contentView else {
|
|
owsFail("\(TAG) Missing contentView")
|
|
return
|
|
}
|
|
guard let bubbleView = bubbleView else {
|
|
owsFail("\(TAG) Missing bubbleView")
|
|
return
|
|
}
|
|
guard let bubbleSuperview = bubbleView.superview else {
|
|
owsFail("\(TAG) Missing bubbleSuperview")
|
|
return
|
|
}
|
|
guard let messageTextTopConstraint = messageTextTopConstraint else {
|
|
owsFail("\(TAG) Missing messageTextTopConstraint")
|
|
return
|
|
}
|
|
guard let messageTextHeightLayoutConstraint = messageTextHeightLayoutConstraint else {
|
|
owsFail("\(TAG) Missing messageTextHeightLayoutConstraint")
|
|
return
|
|
}
|
|
guard let messageTextProxyViewHeightConstraint = messageTextProxyViewHeightConstraint else {
|
|
owsFail("\(TAG) Missing messageTextProxyViewHeightConstraint")
|
|
return
|
|
}
|
|
guard let bubbleViewWidthConstraint = bubbleViewWidthConstraint else {
|
|
owsFail("\(TAG) Missing bubbleViewWidthConstraint")
|
|
return
|
|
}
|
|
|
|
if messageTextView.width() != messageTextProxyView.width() {
|
|
owsFail("\(TAG) messageTextView.width \(messageTextView.width) != messageTextProxyView.width \(messageTextProxyView.width)")
|
|
}
|
|
|
|
let maxBubbleWidth = bubbleSuperview.width() - (bubbleViewHMargin * 2)
|
|
let maxTextWidth = maxBubbleWidth - (messageTailEdgeMargin + messageNoTailEdgeMargin)
|
|
// Measure the total text size.
|
|
let textSize = messageTextView.sizeThatFits(CGSize(width:maxTextWidth, height:CGFloat.greatestFiniteMagnitude))
|
|
// Measure the size of the scroll view viewport.
|
|
let scrollViewSize = scrollView.frame.size
|
|
// Obtain the current scroll view content offset (scroll state).
|
|
let scrollViewContentOffset = scrollView.contentOffset
|
|
// Obtain the location of the text view proxy relative to the content view.
|
|
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("\(TAG) scrollViewDidScroll")
|
|
|
|
updateTextLayout()
|
|
}
|
|
}
|