mirror of
https://github.com/oxen-io/session-ios.git
synced 2023-12-13 21:30:14 +01:00
Merge branch 'charles/longTextMessageDetails'
This commit is contained in:
commit
1e7d15841d
|
@ -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;
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue