Fix edge cases around oversize test messages.

// FREEBIE
This commit is contained in:
Matthew Chen 2017-10-26 14:34:30 -04:00
parent e1526876a6
commit 8cb3e5d35d
3 changed files with 188 additions and 71 deletions

View File

@ -303,15 +303,12 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
if (!displayableText) {
NSString *text = textBlock();
// Only show up to 2kb of text.
const NSUInteger kMaxTextDisplayLength = 2 * 1024;
text = [text ows_stripped];
// Only show up to N characters of text.
const NSUInteger kMaxTextDisplayLength = 1024;
NSString *_Nullable fullText = [DisplayableText displayableText:text];
BOOL isTextTruncated = NO;
if (!fullText) {
fullText = @"";
} else {
fullText = fullText;
}
NSString *_Nullable displayText = fullText;
if (displayText.length > kMaxTextDisplayLength) {
@ -324,8 +321,6 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
}
if (!displayText) {
displayText = @"";
} else {
displayText = displayText;
}
displayableText =
@ -336,70 +331,81 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
return displayableText;
}
- (nullable TSAttachment *)firstAttachmentIfAnyOfMessage:(TSMessage *)message
{
if (message.attachmentIds.count == 0) {
return nil;
}
NSString *_Nullable attachmentId = message.attachmentIds.firstObject;
if (attachmentId.length == 0) {
return nil;
}
return [TSAttachment fetchObjectWithUniqueID:attachmentId];
}
- (void)ensureViewState
{
OWSAssert([self.interaction isKindOfClass:[TSMessage class]]);
OWSAssert([self.interaction isKindOfClass:[TSOutgoingMessage class]] ||
[self.interaction isKindOfClass:[TSIncomingMessage class]]);
if (self.hasViewState) {
return;
}
self.hasViewState = YES;
TSMessage *interaction = (TSMessage *)self.interaction;
if (interaction.body != nil) {
self.messageCellType = OWSMessageCellType_TextMessage;
self.displayableText = [self displayableTextForText:interaction.body interactionId:interaction.uniqueId];
return;
} else {
NSString *_Nullable attachmentId = interaction.attachmentIds.firstObject;
if (attachmentId.length > 0) {
TSAttachment *_Nullable attachment = [TSAttachment fetchObjectWithUniqueID:attachmentId];
if ([attachment isKindOfClass:[TSAttachmentStream class]]) {
self.attachmentStream = (TSAttachmentStream *)attachment;
TSMessage *message = (TSMessage *)self.interaction;
TSAttachment *_Nullable attachment = [self firstAttachmentIfAnyOfMessage:message];
if (attachment) {
if ([attachment isKindOfClass:[TSAttachmentStream class]]) {
self.attachmentStream = (TSAttachmentStream *)attachment;
if ([attachment.contentType isEqualToString:OWSMimeTypeOversizeTextMessage]) {
self.messageCellType = OWSMessageCellType_OversizeTextMessage;
self.displayableText = [self displayableTextForAttachmentStream:self.attachmentStream
interactionId:interaction.uniqueId];
return;
} else if ([self.attachmentStream isAnimated] || [self.attachmentStream isImage] ||
[self.attachmentStream isVideo]) {
if ([self.attachmentStream isAnimated]) {
self.messageCellType = OWSMessageCellType_AnimatedImage;
} else if ([self.attachmentStream isImage]) {
self.messageCellType = OWSMessageCellType_StillImage;
} else if ([self.attachmentStream isVideo]) {
self.messageCellType = OWSMessageCellType_Video;
} else {
OWSFail(@"%@ unexpected attachment type.", self.tag);
self.messageCellType = OWSMessageCellType_GenericAttachment;
return;
}
self.contentSize = [self.attachmentStream imageSizeWithoutTransaction];
if (self.contentSize.width <= 0 || self.contentSize.height <= 0) {
self.messageCellType = OWSMessageCellType_GenericAttachment;
}
return;
} else if ([self.attachmentStream isAudio]) {
self.messageCellType = OWSMessageCellType_Audio;
return;
if ([attachment.contentType isEqualToString:OWSMimeTypeOversizeTextMessage]) {
self.messageCellType = OWSMessageCellType_OversizeTextMessage;
self.displayableText =
[self displayableTextForAttachmentStream:self.attachmentStream interactionId:message.uniqueId];
return;
} else if ([self.attachmentStream isAnimated] || [self.attachmentStream isImage] ||
[self.attachmentStream isVideo]) {
if ([self.attachmentStream isAnimated]) {
self.messageCellType = OWSMessageCellType_AnimatedImage;
} else if ([self.attachmentStream isImage]) {
self.messageCellType = OWSMessageCellType_StillImage;
} else if ([self.attachmentStream isVideo]) {
self.messageCellType = OWSMessageCellType_Video;
} else {
OWSFail(@"%@ unexpected attachment type.", self.tag);
self.messageCellType = OWSMessageCellType_GenericAttachment;
return;
}
} else if ([attachment isKindOfClass:[TSAttachmentPointer class]]) {
self.messageCellType = OWSMessageCellType_DownloadingAttachment;
self.attachmentPointer = (TSAttachmentPointer *)attachment;
self.contentSize = [self.attachmentStream imageSizeWithoutTransaction];
if (self.contentSize.width <= 0 || self.contentSize.height <= 0) {
self.messageCellType = OWSMessageCellType_GenericAttachment;
}
return;
} else if ([self.attachmentStream isAudio]) {
self.messageCellType = OWSMessageCellType_Audio;
return;
} else {
OWSFail(@"%@ Unknown attachment type", self.tag);
self.messageCellType = OWSMessageCellType_GenericAttachment;
return;
}
} else if ([attachment isKindOfClass:[TSAttachmentPointer class]]) {
self.messageCellType = OWSMessageCellType_DownloadingAttachment;
self.attachmentPointer = (TSAttachmentPointer *)attachment;
return;
} else {
OWSFail(@"%@ Message has neither attachment nor body", self.tag);
OWSFail(@"%@ Unknown attachment type", self.tag);
}
} else if (message.body != nil) {
self.messageCellType = OWSMessageCellType_TextMessage;
self.displayableText = [self displayableTextForText:message.body interactionId:message.uniqueId];
OWSAssert(self.displayableText);
return;
} else {
OWSFail(@"%@ Message has neither attachment nor body", self.tag);
}
DDLogVerbose(@"%@ interaction: %@", self.tag, interaction.description);
DDLogVerbose(@"%@ message: %@", self.tag, message.description);
OWSFail(@"%@ Unknown cell type", self.tag);
self.messageCellType = OWSMessageCellType_Unknown;

View File

@ -10,7 +10,7 @@ enum MessageMetadataViewMode: UInt {
case focusOnMetadata
}
class MessageDetailViewController: OWSViewController {
class MessageDetailViewController: OWSViewController, UIScrollViewDelegate {
static let TAG = "[MessageDetailViewController]"
let TAG = "[MessageDetailViewController]"
@ -30,6 +30,13 @@ class MessageDetailViewController: OWSViewController {
var mediaMessageView: MediaMessageView?
// See comments on updateTextLayout.
var messageTextView: UITextView?
var messageTextProxyView: UIView?
var messageTextTopConstraint: NSLayoutConstraint?
var messageTextHeightLayoutConstraint: NSLayoutConstraint?
var messageTextProxyViewHeightConstraint: NSLayoutConstraint?
var scrollView: UIScrollView?
var contentView: UIView?
@ -89,6 +96,8 @@ class MessageDetailViewController: OWSViewController {
super.viewWillAppear(animated)
mediaMessageView?.viewWillAppear(animated)
updateTextLayout()
}
override func viewWillDisappear(_ animated: Bool) {
@ -103,6 +112,7 @@ class MessageDetailViewController: OWSViewController {
view.backgroundColor = UIColor.white
let scrollView = UIScrollView()
scrollView.delegate = self
self.scrollView = scrollView
view.addSubview(scrollView)
scrollView.autoPinWidthToSuperview(withMargin: 0)
@ -305,23 +315,34 @@ class MessageDetailViewController: OWSViewController {
// on the size of its backing buffer, especially when we're
// embedding it "full-size' within a UIScrollView as we do in this view.
//
// TODO: We could use CoreText instead, or we could dynamically
// manipulate the size/position of our UITextView to
// reflect scroll state.
let bodyLabel = UITextView()
bodyLabel.font = UIFont.ows_dynamicTypeBody()
bodyLabel.backgroundColor = UIColor.clear
bodyLabel.isOpaque = false
bodyLabel.isEditable = false
bodyLabel.isSelectable = true
bodyLabel.textContainerInset = UIEdgeInsets.zero
bodyLabel.contentInset = UIEdgeInsets.zero
bodyLabel.isScrollEnabled = false
bodyLabel.textColor = isIncoming ? UIColor.black : UIColor.white
bodyLabel.text = messageBody
// 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 = isIncoming ? bubbleFactory.incoming : bubbleFactory.outgoing
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 leadingMargin: CGFloat = isIncoming ? 15 : 10
let trailingMargin: CGFloat = isIncoming ? 10 : 15
@ -329,11 +350,12 @@ class MessageDetailViewController: OWSViewController {
self.bubbleView = bubbleView
bubbleView.layer.cornerRadius = 10
bubbleView.addSubview(bodyLabel)
bubbleView.addSubview(messageTextProxyView)
bodyLabel.autoPinEdge(toSuperviewEdge: .leading, withInset: leadingMargin)
bodyLabel.autoPinEdge(toSuperviewEdge: .trailing, withInset: trailingMargin)
bodyLabel.autoPinHeightToSuperview(withMargin: 10)
messageTextProxyView.autoPinEdge(toSuperviewEdge: .leading, withInset: leadingMargin)
messageTextProxyView.autoPinEdge(toSuperviewEdge: .trailing, withInset: trailingMargin)
messageTextProxyView.autoPinHeightToSuperview(withMargin: 10)
self.messageTextProxyViewHeightConstraint = messageTextProxyView.autoSetDimension(.height, toSize:0)
let row = UIView()
row.addSubview(bubbleView)
@ -567,4 +589,93 @@ class MessageDetailViewController: OWSViewController {
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 messageTextTopConstraint = messageTextTopConstraint else {
owsFail("\(TAG) Missing messageTextProxyView")
return
}
guard let messageTextHeightLayoutConstraint = messageTextHeightLayoutConstraint else {
owsFail("\(TAG) Missing messageTextProxyView")
return
}
guard let messageTextProxyViewHeightConstraint = messageTextProxyViewHeightConstraint 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
}
if messageTextView.width() != messageTextProxyView.width() {
owsFail("\(TAG) messageTextView.width \(messageTextView.width) != messageTextProxyView.width \(messageTextProxyView.width)")
}
// Measure the total text size.
let textSize = messageTextView.sizeThatFits(CGSize(width:messageTextView.width(), 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 text proxy should always be sized large enough to hold the
// entire text content.
let messageTextProxyViewHeight = textSize.height
messageTextProxyViewHeightConstraint.constant = messageTextProxyViewHeight
// 2. 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 = min(textSize.height, scrollViewSize.height)
messageTextHeightLayoutConstraint.constant = messageTextViewHeight
// 3. 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
// 4. 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()
}
}

View File

@ -24,7 +24,7 @@ import Foundation
@objc
class func displayableText(_ text: String?) -> String? {
guard let text = text else {
guard let text = text?.ows_stripped() else {
return nil
}