Fix edge cases around oversize test messages.

// FREEBIE
pull/1/head
Matthew Chen 8 years ago
parent e1526876a6
commit 8cb3e5d35d

@ -303,15 +303,12 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
if (!displayableText) { if (!displayableText) {
NSString *text = textBlock(); NSString *text = textBlock();
// Only show up to 2kb of text. // Only show up to N characters of text.
const NSUInteger kMaxTextDisplayLength = 2 * 1024; const NSUInteger kMaxTextDisplayLength = 1024;
text = [text ows_stripped];
NSString *_Nullable fullText = [DisplayableText displayableText:text]; NSString *_Nullable fullText = [DisplayableText displayableText:text];
BOOL isTextTruncated = NO; BOOL isTextTruncated = NO;
if (!fullText) { if (!fullText) {
fullText = @""; fullText = @"";
} else {
fullText = fullText;
} }
NSString *_Nullable displayText = fullText; NSString *_Nullable displayText = fullText;
if (displayText.length > kMaxTextDisplayLength) { if (displayText.length > kMaxTextDisplayLength) {
@ -324,8 +321,6 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
} }
if (!displayText) { if (!displayText) {
displayText = @""; displayText = @"";
} else {
displayText = displayText;
} }
displayableText = displayableText =
@ -336,31 +331,38 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
return displayableText; 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 - (void)ensureViewState
{ {
OWSAssert([self.interaction isKindOfClass:[TSMessage class]]); OWSAssert([self.interaction isKindOfClass:[TSOutgoingMessage class]] ||
[self.interaction isKindOfClass:[TSIncomingMessage class]]);
if (self.hasViewState) { if (self.hasViewState) {
return; return;
} }
self.hasViewState = YES; self.hasViewState = YES;
TSMessage *interaction = (TSMessage *)self.interaction; TSMessage *message = (TSMessage *)self.interaction;
if (interaction.body != nil) { TSAttachment *_Nullable attachment = [self firstAttachmentIfAnyOfMessage:message];
self.messageCellType = OWSMessageCellType_TextMessage; if (attachment) {
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]]) { if ([attachment isKindOfClass:[TSAttachmentStream class]]) {
self.attachmentStream = (TSAttachmentStream *)attachment; self.attachmentStream = (TSAttachmentStream *)attachment;
if ([attachment.contentType isEqualToString:OWSMimeTypeOversizeTextMessage]) { if ([attachment.contentType isEqualToString:OWSMimeTypeOversizeTextMessage]) {
self.messageCellType = OWSMessageCellType_OversizeTextMessage; self.messageCellType = OWSMessageCellType_OversizeTextMessage;
self.displayableText = [self displayableTextForAttachmentStream:self.attachmentStream self.displayableText =
interactionId:interaction.uniqueId]; [self displayableTextForAttachmentStream:self.attachmentStream interactionId:message.uniqueId];
return; return;
} else if ([self.attachmentStream isAnimated] || [self.attachmentStream isImage] || } else if ([self.attachmentStream isAnimated] || [self.attachmentStream isImage] ||
[self.attachmentStream isVideo]) { [self.attachmentStream isVideo]) {
@ -394,12 +396,16 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
} else { } else {
OWSFail(@"%@ Unknown attachment type", 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 { } else {
OWSFail(@"%@ Message has neither attachment nor body", self.tag); 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); OWSFail(@"%@ Unknown cell type", self.tag);
self.messageCellType = OWSMessageCellType_Unknown; self.messageCellType = OWSMessageCellType_Unknown;

@ -10,7 +10,7 @@ enum MessageMetadataViewMode: UInt {
case focusOnMetadata case focusOnMetadata
} }
class MessageDetailViewController: OWSViewController { class MessageDetailViewController: OWSViewController, UIScrollViewDelegate {
static let TAG = "[MessageDetailViewController]" static let TAG = "[MessageDetailViewController]"
let TAG = "[MessageDetailViewController]" let TAG = "[MessageDetailViewController]"
@ -30,6 +30,13 @@ class MessageDetailViewController: OWSViewController {
var mediaMessageView: MediaMessageView? 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 scrollView: UIScrollView?
var contentView: UIView? var contentView: UIView?
@ -89,6 +96,8 @@ class MessageDetailViewController: OWSViewController {
super.viewWillAppear(animated) super.viewWillAppear(animated)
mediaMessageView?.viewWillAppear(animated) mediaMessageView?.viewWillAppear(animated)
updateTextLayout()
} }
override func viewWillDisappear(_ animated: Bool) { override func viewWillDisappear(_ animated: Bool) {
@ -103,6 +112,7 @@ class MessageDetailViewController: OWSViewController {
view.backgroundColor = UIColor.white view.backgroundColor = UIColor.white
let scrollView = UIScrollView() let scrollView = UIScrollView()
scrollView.delegate = self
self.scrollView = scrollView self.scrollView = scrollView
view.addSubview(scrollView) view.addSubview(scrollView)
scrollView.autoPinWidthToSuperview(withMargin: 0) scrollView.autoPinWidthToSuperview(withMargin: 0)
@ -305,23 +315,34 @@ class MessageDetailViewController: OWSViewController {
// on the size of its backing buffer, especially when we're // on the size of its backing buffer, especially when we're
// embedding it "full-size' within a UIScrollView as we do in this view. // embedding it "full-size' within a UIScrollView as we do in this view.
// //
// TODO: We could use CoreText instead, or we could dynamically // Therefore we're doing something unusual here.
// manipulate the size/position of our UITextView to // See comments on updateTextLayout.
// reflect scroll state. let messageTextView = UITextView()
let bodyLabel = UITextView() self.messageTextView = messageTextView
bodyLabel.font = UIFont.ows_dynamicTypeBody() messageTextView.font = UIFont.ows_dynamicTypeBody()
bodyLabel.backgroundColor = UIColor.clear messageTextView.backgroundColor = UIColor.clear
bodyLabel.isOpaque = false messageTextView.isOpaque = false
bodyLabel.isEditable = false messageTextView.isEditable = false
bodyLabel.isSelectable = true messageTextView.isSelectable = true
bodyLabel.textContainerInset = UIEdgeInsets.zero messageTextView.textContainerInset = UIEdgeInsets.zero
bodyLabel.contentInset = UIEdgeInsets.zero messageTextView.contentInset = UIEdgeInsets.zero
bodyLabel.isScrollEnabled = false messageTextView.isScrollEnabled = true
bodyLabel.textColor = isIncoming ? UIColor.black : UIColor.white messageTextView.showsHorizontalScrollIndicator = false
bodyLabel.text = messageBody messageTextView.showsVerticalScrollIndicator = false
messageTextView.isUserInteractionEnabled = false
messageTextView.textColor = isIncoming ? UIColor.black : UIColor.white
messageTextView.text = messageBody
let bubbleImageData = isIncoming ? bubbleFactory.incoming : bubbleFactory.outgoing 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 leadingMargin: CGFloat = isIncoming ? 15 : 10
let trailingMargin: CGFloat = isIncoming ? 10 : 15 let trailingMargin: CGFloat = isIncoming ? 10 : 15
@ -329,11 +350,12 @@ class MessageDetailViewController: OWSViewController {
self.bubbleView = bubbleView self.bubbleView = bubbleView
bubbleView.layer.cornerRadius = 10 bubbleView.layer.cornerRadius = 10
bubbleView.addSubview(bodyLabel) bubbleView.addSubview(messageTextProxyView)
bodyLabel.autoPinEdge(toSuperviewEdge: .leading, withInset: leadingMargin) messageTextProxyView.autoPinEdge(toSuperviewEdge: .leading, withInset: leadingMargin)
bodyLabel.autoPinEdge(toSuperviewEdge: .trailing, withInset: trailingMargin) messageTextProxyView.autoPinEdge(toSuperviewEdge: .trailing, withInset: trailingMargin)
bodyLabel.autoPinHeightToSuperview(withMargin: 10) messageTextProxyView.autoPinHeightToSuperview(withMargin: 10)
self.messageTextProxyViewHeightConstraint = messageTextProxyView.autoSetDimension(.height, toSize:0)
let row = UIView() let row = UIView()
row.addSubview(bubbleView) row.addSubview(bubbleView)
@ -567,4 +589,93 @@ class MessageDetailViewController: OWSViewController {
comment: "Status label for messages which are 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 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()
}
} }

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

Loading…
Cancel
Save