diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index 8d10968c9..f443f7ec0 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -154,6 +154,7 @@ 347850711FDAEB17007B8332 /* OWSUserProfile.m in Sources */ = {isa = PBXBuildFile; fileRef = 3478506F1FDAEB16007B8332 /* OWSUserProfile.m */; }; 347850721FDAEB17007B8332 /* OWSUserProfile.h in Headers */ = {isa = PBXBuildFile; fileRef = 347850701FDAEB16007B8332 /* OWSUserProfile.h */; settings = {ATTRIBUTES = (Public, ); }; }; 3496744D2076768700080B5F /* OWSMessageBubbleView.m in Sources */ = {isa = PBXBuildFile; fileRef = 3496744C2076768700080B5F /* OWSMessageBubbleView.m */; }; + 3496744F2076ACD000080B5F /* LongTextViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3496744E2076ACCE00080B5F /* LongTextViewController.swift */; }; 34A55F3720485465002CC6DE /* OWS2FARegistrationViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 34A55F3520485464002CC6DE /* OWS2FARegistrationViewController.m */; }; 34A910601FFEB114000C4745 /* OWSBackup.m in Sources */ = {isa = PBXBuildFile; fileRef = 34A9105F1FFEB114000C4745 /* OWSBackup.m */; }; 34B0796D1FCF46B100E248C2 /* MainAppContext.m in Sources */ = {isa = PBXBuildFile; fileRef = 34B0796B1FCF46B000E248C2 /* MainAppContext.m */; }; @@ -729,6 +730,7 @@ 3495BC911F1426B800B478F5 /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = translations/ar.lproj/Localizable.strings; sourceTree = ""; }; 3496744B2076768600080B5F /* OWSMessageBubbleView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSMessageBubbleView.h; sourceTree = ""; }; 3496744C2076768700080B5F /* OWSMessageBubbleView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSMessageBubbleView.m; sourceTree = ""; }; + 3496744E2076ACCE00080B5F /* LongTextViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LongTextViewController.swift; sourceTree = ""; }; 34A55F3520485464002CC6DE /* OWS2FARegistrationViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWS2FARegistrationViewController.m; sourceTree = ""; }; 34A55F3620485464002CC6DE /* OWS2FARegistrationViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWS2FARegistrationViewController.h; sourceTree = ""; }; 34A9105E1FFEB113000C4745 /* OWSBackup.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSBackup.h; sourceTree = ""; }; @@ -1590,8 +1592,10 @@ 34B3F8501E8DF1700035BE1A /* NewContactThreadViewController.m */, 34B3F8541E8DF1700035BE1A /* NewGroupViewController.h */, 34B3F8551E8DF1700035BE1A /* NewGroupViewController.m */, + 3496744E2076ACCE00080B5F /* LongTextViewController.swift */, 34A55F3620485464002CC6DE /* OWS2FARegistrationViewController.h */, 34A55F3520485464002CC6DE /* OWS2FARegistrationViewController.m */, + 45D2AC01204885170033C692 /* OWS2FAReminderViewController.swift */, 345BC30A2047030600257B7C /* OWS2FASettingsViewController.h */, 345BC30B2047030600257B7C /* OWS2FASettingsViewController.m */, 34C42D591F45F7A80072EC04 /* OWSNavigationController.h */, @@ -1604,7 +1608,6 @@ 34B3F86E1E8DF1700035BE1A /* SignalsNavigationController.m */, 340FC897204DAC8D007AEB0F /* ThreadSettings */, 34D1F0BE1F8EC1760066283D /* Utils */, - 45D2AC01204885170033C692 /* OWS2FAReminderViewController.swift */, ); path = ViewControllers; sourceTree = ""; @@ -3183,6 +3186,7 @@ 34A55F3720485465002CC6DE /* OWS2FARegistrationViewController.m in Sources */, 340FC8AD204DAC8D007AEB0F /* OWSLinkedDevicesTableViewController.m in Sources */, 340FC8AA204DAC8D007AEB0F /* NotificationSettingsViewController.m in Sources */, + 3496744F2076ACD000080B5F /* LongTextViewController.swift in Sources */, 34FD93701E3BD43A00109093 /* OWSAnyTouchGestureRecognizer.m in Sources */, 34B3F8931E8DF1710035BE1A /* SignalsNavigationController.m in Sources */, 34D1F0AC1F867BFC0066283D /* OWSExpirationTimerView.m in Sources */, diff --git a/Signal/src/Signal-Bridging-Header.h b/Signal/src/Signal-Bridging-Header.h index 8cd537cdd..ea9447f21 100644 --- a/Signal/src/Signal-Bridging-Header.h +++ b/Signal/src/Signal-Bridging-Header.h @@ -25,6 +25,7 @@ #import "OWSBubbleView.h" #import "OWSCallNotificationsAdaptee.h" #import "OWSDatabaseMigration.h" +#import "OWSMessageBubbleView.h" #import "OWSMessageCell.h" #import "OWSNavigationController.h" #import "OWSProgressView.h" diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageBubbleView.h b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageBubbleView.h index 3814281ef..d3fb6a1d7 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageBubbleView.h +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageBubbleView.h @@ -24,6 +24,8 @@ typedef NS_ENUM(NSUInteger, OWSMessageGestureLocation) { @property (nonatomic, nullable, readonly) UIView *bodyMediaView; +@property (nonatomic) BOOL alwaysShowBubbleTail; + - (instancetype)init NS_UNAVAILABLE; - (instancetype)initWithFrame:(CGRect)frame NS_DESIGNATED_INITIALIZER; @@ -35,7 +37,7 @@ typedef NS_ENUM(NSUInteger, OWSMessageGestureLocation) { - (void)loadContent; - (void)unloadContent; -- (CGSize)sizeForViewWidth:(int)viewWidth contentWidth:(int)contentWidth; +- (CGSize)sizeForContentWidth:(int)contentWidth; - (void)prepareForReuse; diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageBubbleView.m b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageBubbleView.m index 5dd78efb3..5212420e9 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageBubbleView.m +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageBubbleView.m @@ -234,7 +234,7 @@ NS_ASSUME_NONNULL_BEGIN CGSize bodyTextContentSize = [self bodyTextSizeForContentWidth:self.contentWidth includeMargins:NO]; self.bubbleView.isOutgoing = self.isOutgoing; - self.bubbleView.hideTail = self.viewItem.shouldHideBubbleTail; + self.bubbleView.hideTail = self.viewItem.shouldHideBubbleTail && !self.alwaysShowBubbleTail; if ([self.viewItem.interaction isKindOfClass:[TSMessage class]] && self.hasNonImageBodyContent) { TSMessage *message = (TSMessage *)self.viewItem.interaction; @@ -315,7 +315,6 @@ NS_ASSUME_NONNULL_BEGIN if (bodyMediaView) { OWSAssert(self.loadCellContentBlock); OWSAssert(self.unloadCellContentBlock); - OWSAssert(!lastSubview); bodyMediaView.clipsToBounds = YES; @@ -898,9 +897,7 @@ NS_ASSUME_NONNULL_BEGIN return (int)floor(contentWidth * 0.8f); } -- (CGSize)quotedMessageSizeForViewWidth:(int)viewWidth - contentWidth:(int)contentWidth - includeMargins:(BOOL)includeMargins +- (CGSize)quotedMessageSizeForContentWidth:(int)contentWidth includeMargins:(BOOL)includeMargins { OWSAssert(self.viewItem); OWSAssert([self.viewItem.interaction isKindOfClass:[TSMessage class]]); @@ -920,15 +917,14 @@ NS_ASSUME_NONNULL_BEGIN return result; } -- (CGSize)sizeForViewWidth:(int)viewWidth contentWidth:(int)contentWidth +- (CGSize)sizeForContentWidth:(int)contentWidth { OWSAssert(self.viewItem); OWSAssert([self.viewItem.interaction isKindOfClass:[TSMessage class]]); CGSize cellSize = CGSizeZero; - CGSize quotedMessageSize = - [self quotedMessageSizeForViewWidth:viewWidth contentWidth:contentWidth includeMargins:YES]; + CGSize quotedMessageSize = [self quotedMessageSizeForContentWidth:contentWidth includeMargins:YES]; cellSize.width = MAX(cellSize.width, quotedMessageSize.width); cellSize.height += quotedMessageSize.height; diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m index dee4d0962..9cf6cae7a 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m @@ -381,7 +381,7 @@ NS_ASSUME_NONNULL_BEGIN self.messageBubbleView.viewItem = self.viewItem; self.messageBubbleView.contentWidth = self.contentWidth; self.messageBubbleView.cellMediaCache = self.delegate.cellMediaCache; - CGSize messageBubbleSize = [self.messageBubbleView sizeForViewWidth:viewWidth contentWidth:contentWidth]; + CGSize messageBubbleSize = [self.messageBubbleView sizeForContentWidth:contentWidth]; CGSize cellSize = messageBubbleSize; diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m index 8cad36aaa..c72f83c47 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m @@ -2101,11 +2101,7 @@ typedef enum : NSUInteger { OWSAssert(conversationItem); OWSAssert([conversationItem.interaction isKindOfClass:[TSMessage class]]); - TSMessage *message = (TSMessage *)conversationItem.interaction; - MessageDetailViewController *view = - [[MessageDetailViewController alloc] initWithViewItem:conversationItem - message:message - mode:MessageMetadataViewModeFocusOnMessage]; + LongTextViewController *view = [[LongTextViewController alloc] initWithViewItem:conversationItem]; [self.navigationController pushViewController:view animated:YES]; } diff --git a/Signal/src/ViewControllers/LongTextViewController.swift b/Signal/src/ViewControllers/LongTextViewController.swift new file mode 100644 index 000000000..8dd23d156 --- /dev/null +++ b/Signal/src/ViewControllers/LongTextViewController.swift @@ -0,0 +1,100 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +import Foundation +import SignalServiceKit +import SignalMessaging + +@objc +public class LongTextViewController: OWSViewController { + + // MARK: Properties + + let viewItem: ConversationViewItem + + let messageBody: String + + var messageTextView: UITextView? + + // MARK: Initializers + + @available(*, unavailable, message:"use other constructor instead.") + public required init?(coder aDecoder: NSCoder) { + fatalError("\(#function) is unimplemented.") + } + + public required init(viewItem: ConversationViewItem) { + self.viewItem = viewItem + + self.messageBody = LongTextViewController.displayableText(viewItem: viewItem) + + super.init(nibName: nil, bundle: nil) + } + + private class func displayableText(viewItem: ConversationViewItem) -> String { + guard viewItem.hasBodyText else { + return "" + } + guard let displayableText = viewItem.displayableBodyText() else { + return "" + } + let messageBody = displayableText.fullText + return messageBody + } + + // MARK: View Lifecycle + + public override func viewDidLoad() { + super.viewDidLoad() + + self.navigationItem.title = NSLocalizedString("LONG_TEXT_VIEW_TITLE", + comment: "Title for the 'long text message' view.") + + createViews() + } + + // MARK: - Create Views + + private func createViews() { + view.backgroundColor = UIColor.white + + let messageTextView = UITextView() + self.messageTextView = messageTextView + messageTextView.font = UIFont.ows_dynamicTypeBody + messageTextView.backgroundColor = UIColor.white + messageTextView.isOpaque = true + messageTextView.isEditable = false + messageTextView.isSelectable = true + messageTextView.isScrollEnabled = true + messageTextView.showsHorizontalScrollIndicator = false + messageTextView.showsVerticalScrollIndicator = true + messageTextView.isUserInteractionEnabled = true + messageTextView.textColor = UIColor.black + messageTextView.text = messageBody + + view.addSubview(messageTextView) + messageTextView.autoPinLeadingToSuperviewMargin() + messageTextView.autoPinTrailingToSuperviewMargin() + messageTextView.autoPin(toTopLayoutGuideOf: self, withInset: 0) + + let footer = UIToolbar() + footer.barTintColor = UIColor.ows_materialBlue + view.addSubview(footer) + footer.autoPinWidthToSuperview(withMargin: 0) + footer.autoPinEdge(.top, to: .bottom, of: messageTextView) + 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) + ] + } + + // MARK: - Actions + + func shareButtonPressed() { + AttachmentSharing.showShareUI(forText: messageBody) + } +} diff --git a/Signal/src/ViewControllers/MessageDetailViewController.swift b/Signal/src/ViewControllers/MessageDetailViewController.swift index 2b7611f52..eaac9077c 100644 --- a/Signal/src/ViewControllers/MessageDetailViewController.swift +++ b/Signal/src/ViewControllers/MessageDetailViewController.swift @@ -12,7 +12,7 @@ enum MessageMetadataViewMode: UInt { case focusOnMetadata } -class MessageDetailViewController: OWSViewController, UIScrollViewDelegate, MediaDetailPresenter, MediaGalleryDataSourceDelegate { +class MessageDetailViewController: OWSViewController, MediaDetailPresenter, MediaGalleryDataSourceDelegate { // MARK: Properties @@ -28,15 +28,9 @@ class MessageDetailViewController: OWSViewController, UIScrollViewDelegate, Medi var message: TSMessage var wasDeleted: Bool = false - 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 messageBubbleView: OWSMessageBubbleView? + var messageBubbleViewWidthLayoutConstraint: NSLayoutConstraint? + var messageBubbleViewHeightLayoutConstraint: NSLayoutConstraint? var scrollView: UIScrollView! var contentView: UIView? @@ -87,7 +81,7 @@ class MessageDetailViewController: OWSViewController, UIScrollViewDelegate, Medi override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) - updateTextLayout() + updateMessageBubbleViewLayout() if mode == .focusOnMetadata { if let bubbleView = self.bubbleView { @@ -121,7 +115,6 @@ class MessageDetailViewController: OWSViewController, UIScrollViewDelegate, Medi view.backgroundColor = UIColor.white let scrollView = UIScrollView() - scrollView.delegate = self self.scrollView = scrollView view.addSubview(scrollView) scrollView.autoPinWidthToSuperview(withMargin: 0) @@ -218,7 +211,7 @@ class MessageDetailViewController: OWSViewController, UIScrollViewDelegate, Medi } 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 { continue @@ -299,11 +292,7 @@ class MessageDetailViewController: OWSViewController, UIScrollViewDelegate, Medi lastRow.autoPinEdge(toSuperviewEdge: .bottom, withInset: 20) } - if let mediaMessageView = mediaMessageView { - mediaMessageView.autoMatch(.height, to: .width, of: mediaMessageView, withOffset: 0, relation: .lessThanOrEqual) - } - - updateTextLayout() + updateMessageBubbleViewLayout() } private func displayableTextIfText() -> String? { @@ -321,8 +310,6 @@ class MessageDetailViewController: OWSViewController, UIScrollViewDelegate, Medi } let bubbleViewHMargin: CGFloat = 10 - let messageTailEdgeMargin: CGFloat = 15 - let messageNoTailEdgeMargin: CGFloat = 10 private func contentRows() -> [UIView] { var rows = [UIView]() @@ -331,60 +318,28 @@ class MessageDetailViewController: OWSViewController, UIScrollViewDelegate, Medi 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 - // 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 isIncoming = self.message as? TSIncomingMessage != nil + messageBubbleView.autoPinEdge(toSuperviewEdge: isIncoming ? .leading : .trailing, withInset: bubbleViewHMargin) - let row = UIView() - row.addSubview(bubbleView) - bubbleView.autoPinHeightToSuperview() - bubbleView.autoPinEdge(toSuperviewEdge: isIncoming ? .leading : .trailing, withInset: bubbleViewHMargin) - self.bubbleViewWidthConstraint = bubbleView.autoSetDimension(.width, toSize: 0) + self.messageBubbleViewWidthLayoutConstraint = messageBubbleView.autoSetDimension(.width, toSize: 0) + self.messageBubbleViewHeightLayoutConstraint = messageBubbleView.autoSetDimension(.height, toSize: 0) rows.append(row) } @@ -432,26 +387,6 @@ class MessageDetailViewController: OWSViewController, UIScrollViewDelegate, Medi } 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 } @@ -559,28 +494,6 @@ class MessageDetailViewController: OWSViewController, UIScrollViewDelegate, Medi 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 // This method should be called after self.databaseConnection.beginLongLivedReadTransaction(). @@ -650,110 +563,64 @@ class MessageDetailViewController: OWSViewController, UIScrollViewDelegate, Medi } } - // 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("\(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") + // MARK: - Message Bubble Layout + + private func contentWidth() -> Int32 { + return Int32(round(self.view.width() - (2 * bubbleViewHMargin))) + } + + private func updateMessageBubbleViewLayout() { + guard let messageBubbleView = messageBubbleView else { return } - guard let bubbleSuperview = bubbleView.superview else { - owsFail("\(logTag) Missing bubbleSuperview") + guard let messageBubbleViewWidthLayoutConstraint = messageBubbleViewWidthLayoutConstraint else { return } - guard let messageTextTopConstraint = messageTextTopConstraint else { - owsFail("\(logTag) Missing messageTextTopConstraint") + guard let messageBubbleViewHeightLayoutConstraint = messageBubbleViewHeightLayoutConstraint else { 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 } - guard let messageTextProxyViewHeightConstraint = messageTextProxyViewHeightConstraint else { - owsFail("\(logTag) Missing messageTextProxyViewHeightConstraint") + guard sender.state == .recognized else { return } - guard let bubbleViewWidthConstraint = bubbleViewWidthConstraint else { - owsFail("\(logTag) Missing bubbleViewWidthConstraint") - return + if let outgoingMessage = viewItem.interaction as? TSOutgoingMessage { + switch outgoingMessage.messageState { + case .attemptingOut, + .unsent: + // Ignore taps on "unsent" and "sending" messages. + return + default: + break + } } - if messageTextView.width() != messageTextProxyView.width() { - owsFail("\(logTag) 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("\(logTag) scrollViewDidScroll") - - updateTextLayout() + let locationInMessageBubble = sender.location(in: messageBubbleView) + switch messageBubbleView.gestureLocation(forLocation: locationInMessageBubble) { + case .default: + break + case .oversizeText: + let viewController = LongTextViewController(viewItem: viewItem) + self.navigationController?.pushViewController(viewController, animated: true) + break + case .media: + // TODO: We could show MediaGalleryViewController? + break + case .quotedReply: + break + } } // MediaGalleryDataSourceDelegate diff --git a/Signal/translations/en.lproj/Localizable.strings b/Signal/translations/en.lproj/Localizable.strings index 54c943161..5ce351ba6 100644 --- a/Signal/translations/en.lproj/Localizable.strings +++ b/Signal/translations/en.lproj/Localizable.strings @@ -971,6 +971,9 @@ /* No comment provided by engineer. */ "LOGGING_SECTION" = "Logging"; +/* Title for the 'long text message' view. */ +"LONG_TEXT_VIEW_TITLE" = "Message"; + /* nav bar button item */ "MEDIA_DETAIL_VIEW_ALL_MEDIA_BUTTON" = "All Media"; @@ -1010,9 +1013,6 @@ /* Label for the MIME type of attachments in the 'message metadata' view. */ "MESSAGE_METADATA_VIEW_ATTACHMENT_MIME_TYPE" = "MIME type"; -/* Label for 'missing' attachments in the 'message metadata' view. */ -"MESSAGE_METADATA_VIEW_ATTACHMENT_MISSING_FILE" = "Missing Attachment"; - /* Label for 'not yet downloaded' attachments in the 'message metadata' view. */ "MESSAGE_METADATA_VIEW_ATTACHMENT_NOT_YET_DOWNLOADED" = "Not yet downloaded"; diff --git a/SignalMessaging/utils/DisplayableText.swift b/SignalMessaging/utils/DisplayableText.swift index dc7e96b41..c5bbb26eb 100644 --- a/SignalMessaging/utils/DisplayableText.swift +++ b/SignalMessaging/utils/DisplayableText.swift @@ -217,20 +217,20 @@ extension String { @objc public class func displayableText(_ rawText: String) -> DisplayableText { // Only show up to N characters of text. - let kMaxTextDisplayLength = 1024 + let kMaxTextDisplayLength = 512 let fullText = rawText.filterStringForDisplay() var isTextTruncated = false var displayText = fullText if displayText.count > kMaxTextDisplayLength { // Trim whitespace before _AND_ after slicing the snipper from the string. let snippet = String(displayText.prefix(kMaxTextDisplayLength)).ows_stripped() - displayText = String(format:NSLocalizedString("OVERSIZE_TEXT_DISPLAY_FORMAT", comment: + displayText = String(format: NSLocalizedString("OVERSIZE_TEXT_DISPLAY_FORMAT", comment: "A display format for oversize text messages."), snippet) isTextTruncated = true } - let displayableText = DisplayableText(fullText: fullText, displayText: displayText, isTextTruncated:isTextTruncated) + let displayableText = DisplayableText(fullText: fullText, displayText: displayText, isTextTruncated: isTextTruncated) return displayableText } }