diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m index ee0a5196a1..844610a445 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m @@ -96,6 +96,9 @@ CG_INLINE CGSize CGSizeCeil(CGSize size) [self.contentView addSubview:self.dateHeaderLabel]; self.bodyTextView = [self newTextView]; + // Setting dataDetectorTypes is expensive. Do it just once. + self.bodyTextView.dataDetectorTypes + = (UIDataDetectorTypeLink | UIDataDetectorTypeAddress | UIDataDetectorTypeCalendarEvent); self.footerLabel = [UILabel new]; self.footerLabel.font = [UIFont ows_regularFontWithSize:12.f]; @@ -140,8 +143,6 @@ CG_INLINE CGSize CGSizeCeil(CGSize size) textView.contentInset = UIEdgeInsetsZero; textView.textContainer.lineFragmentPadding = 0; textView.scrollEnabled = NO; - // Setting dataDetectorTypes is expensive. Do it just once. - textView.dataDetectorTypes = (UIDataDetectorTypeLink | UIDataDetectorTypeAddress | UIDataDetectorTypeCalendarEvent); return textView; } @@ -174,16 +175,6 @@ CG_INLINE CGSize CGSizeCeil(CGSize size) return [UIFont ows_dynamicTypeBodyFont]; } -- (UIFont *)tapForMoreFont -{ - return [UIFont ows_regularFontWithSize:12.f]; -} - -- (CGFloat)tapForMoreHeight -{ - return (CGFloat)ceil([self tapForMoreFont].lineHeight * 1.25); -} - - (BOOL)shouldHaveFailedSendBadge { if (![self.viewItem.interaction isKindOfClass:[TSOutgoingMessage class]]) { @@ -251,6 +242,47 @@ CG_INLINE CGSize CGSizeCeil(CGSize size) return self.viewItem.mediaSize; } +- (BOOL)isQuotedReply +{ + // This should always be valid for the appropriate cell types. + OWSAssert(self.viewItem); + + return self.viewItem.isQuotedReply; +} + +- (BOOL)hasQuotedText +{ + // This should always be valid for the appropriate cell types. + OWSAssert(self.viewItem); + + return self.viewItem.hasQuotedText; +} + +- (BOOL)hasQuotedAttachment +{ + // This should always be valid for the appropriate cell types. + OWSAssert(self.viewItem); + + return self.viewItem.hasQuotedAttachment; +} + +- (BOOL)hasQuotedAttachmentThumbnail +{ + // This should always be valid for the appropriate cell types. + OWSAssert(self.viewItem); + + return (self.viewItem.hasQuotedAttachment && + [TSAttachmentStream hasThumbnailForMimeType:self.viewItem.quotedAttachmentMimetype]); +} + +- (nullable DisplayableText *)displayableQuotedText +{ + // This should always be valid for the appropriate cell types. + OWSAssert(self.viewItem.displayableQuotedText); + + return self.viewItem.displayableQuotedText; +} + - (TSMessage *)message { OWSAssert([self.viewItem.interaction isKindOfClass:[TSMessage class]]); @@ -346,6 +378,40 @@ CG_INLINE CGSize CGSizeCeil(CGSize size) UIView *_Nullable lastSubview = nil; CGFloat bottomMargin = 0; + + if (self.isQuotedReply) { + OWSAssert(!lastSubview); + + UIView *quotedMessageView = [self createQuotedMessageView]; + + [self.bubbleView addSubview:quotedMessageView]; + + CGFloat leadingMargin = self.quotedBubbleLeadingMargin; + CGFloat trailingMargin = self.quotedBubbleTrailingMargin; + + [self.viewConstraints addObjectsFromArray:@[ + [quotedMessageView autoPinLeadingToSuperviewMarginWithInset:leadingMargin], + [quotedMessageView autoPinTrailingToSuperviewMarginWithInset:trailingMargin], + ]]; + + if (lastSubview) { + [self.viewConstraints addObject:[quotedMessageView autoPinEdge:ALEdgeTop + toEdge:ALEdgeBottom + ofView:lastSubview + withOffset:self.quotedMessageTopInset]]; + } else { + [self.viewConstraints addObject:[quotedMessageView autoPinEdgeToSuperviewEdge:ALEdgeTop + withInset:self.quotedMessageTopInset]]; + } + lastSubview = quotedMessageView; + bottomMargin = 0; + + [self.bubbleView logFrameLaterWithLabel:@"bubbleView"]; + [quotedMessageView logFrameLaterWithLabel:@"quotedMessageView"]; + + // TODO: Consider stroking the quoted thumbnail. + } + UIView *_Nullable bodyMediaView = nil; BOOL bodyMediaViewHasGreedyWidth = NO; switch (self.cellType) { @@ -443,7 +509,7 @@ CG_INLINE CGSize CGSizeCeil(CGSize size) // We render malformed messages as "empty text" messages, // so create a text view if there is no body media view. if (self.hasBodyText || !bodyMediaView) { - bodyTextView = [self createBodyTextViewIfNecessary]; + bodyTextView = [self configureBodyTextView]; } if (bodyTextView) { [self.bubbleView addSubview:bodyTextView]; @@ -720,8 +786,23 @@ CG_INLINE CGSize CGSizeCeil(CGSize size) return [UIFont systemFontOfSize:12.0f]; } -- (OWSMessageTextView *)createBodyTextViewIfNecessary +- (UILabel *)createQuotedTextLabel { + UILabel *quotedTextLabel = [UILabel new]; + quotedTextLabel.numberOfLines = 3; + quotedTextLabel.lineBreakMode = NSLineBreakByWordWrapping; + quotedTextLabel.text = self.displayableQuotedText.displayText; + quotedTextLabel.textColor = self.quotedTextColor; + + // Honor dynamic type in the message bodies. + quotedTextLabel.font = self.textMessageFont; + return quotedTextLabel; +} + +- (OWSMessageTextView *)configureBodyTextView +{ + OWSAssert(self.hasBodyText); + BOOL shouldIgnoreEvents = NO; if (self.viewItem.interaction.interactionType == OWSInteractionType_OutgoingMessage) { // Ignore taps on links in outgoing messages that haven't been sent yet, as @@ -731,7 +812,7 @@ CG_INLINE CGSize CGSizeCeil(CGSize size) } [self.class loadForTextDisplay:self.bodyTextView text:self.displayableBodyText.displayText - textColor:self.textColor + textColor:self.bodyTextColor font:self.textMessageFont shouldIgnoreEvents:shouldIgnoreEvents]; return self.bodyTextView; @@ -777,12 +858,84 @@ CG_INLINE CGSize CGSizeCeil(CGSize size) tapForMoreLabel.text = NSLocalizedString(@"CONVERSATION_VIEW_OVERSIZE_TEXT_TAP_FOR_MORE", @"Indicator on truncated text messages that they can be tapped to see the entire text message."); tapForMoreLabel.font = [self tapForMoreFont]; - tapForMoreLabel.textColor = [self.textColor colorWithAlphaComponent:0.85]; + tapForMoreLabel.textColor = [self.bodyTextColor colorWithAlphaComponent:0.85]; tapForMoreLabel.textAlignment = [tapForMoreLabel textAlignmentUnnatural]; return tapForMoreLabel; } +- (UIView *)createQuotedMessageView +{ + OWSAssert(self.isQuotedReply); + + UIView *quotedMessageView = [UIView containerView]; + quotedMessageView.userInteractionEnabled = NO; + quotedMessageView.clipsToBounds = YES; + // TODO: + quotedMessageView.layer.cornerRadius = 3.f; + quotedMessageView.backgroundColor = [UIColor colorWithRGBHex:0xe2f7fa]; + + UIView *quoteStripView = [UIView containerView]; + quoteStripView.backgroundColor = (self.isIncoming ? [UIColor whiteColor] : [UIColor colorWithRGBHex:0x007884]); + quoteStripView.userInteractionEnabled = NO; + [quotedMessageView addSubview:quoteStripView]; + [quoteStripView autoPinHeightToSuperview]; + [quoteStripView autoPinLeadingToSuperviewMargin]; + [quoteStripView autoSetDimension:ALDimensionWidth toSize:self.quotedReplyStripeThickness]; + + UIView *_Nullable quotedThumbnailView = nil; + if (self.hasQuotedAttachmentThumbnail) { + // TODO: + quotedThumbnailView = [UIView containerView]; + quotedThumbnailView.userInteractionEnabled = NO; + quotedThumbnailView.backgroundColor = [UIColor redColor]; + [quotedMessageView addSubview:quotedThumbnailView]; + [quotedThumbnailView autoPinTopToSuperviewMargin]; + [quotedThumbnailView autoPinTrailingToSuperviewMargin]; + [quotedThumbnailView autoSetDimension:ALDimensionWidth toSize:self.quotedThumbnailSize]; + [quotedThumbnailView autoSetDimension:ALDimensionHeight toSize:self.quotedThumbnailSize]; + } + + OWSContactsManager *contactsManager = Environment.current.contactsManager; + NSString *quotedAuthor = [contactsManager displayNameForPhoneIdentifier:self.viewItem.quotedRecipientId]; + + UILabel *quotedAuthorLabel = [UILabel new]; + quotedAuthorLabel.text = quotedAuthor; + quotedAuthorLabel.font = self.quotedAuthorFont; + quotedAuthorLabel.textColor + = (self.isIncoming ? [UIColor colorWithRGBHex:0xd84315] : [UIColor colorWithRGBHex:0x007884]); + quotedAuthorLabel.numberOfLines = 1; + quotedAuthorLabel.lineBreakMode = NSLineBreakByTruncatingTail; + [quotedMessageView addSubview:quotedAuthorLabel]; + [quotedAuthorLabel autoPinEdgeToSuperviewEdge:ALEdgeTop withInset:self.quotedContentTopInset]; + [quotedAuthorLabel autoPinLeadingToTrailingEdgeOfView:quoteStripView offset:self.quotedReplyStripeHSpacing]; + if (quotedThumbnailView) { + [quotedAuthorLabel autoPinTrailingToEdgeOfView:quotedThumbnailView offset:self.quotedThumbnailHSpacing]; + } else { + [quotedAuthorLabel autoPinTrailingToSuperviewMarginWithInset:self.quotedContentTrailingMargin]; + } + + if (self.hasQuotedText) { + UILabel *quotedTextLabel = [self createQuotedTextLabel]; + + [quotedMessageView addSubview:quotedTextLabel]; + [quotedTextLabel autoPinEdge:ALEdgeTop + toEdge:ALEdgeBottom + ofView:quotedAuthorLabel + withOffset:self.quotedAuthorBottomSpacing]; + [quotedTextLabel autoPinLeadingToTrailingEdgeOfView:quoteStripView offset:self.quotedReplyStripeHSpacing]; + if (quotedThumbnailView) { + [quotedTextLabel autoPinLeadingToTrailingEdgeOfView:quotedThumbnailView + offset:self.quotedThumbnailHSpacing]; + } else { + [quotedTextLabel autoPinTrailingToSuperviewMarginWithInset:self.quotedContentTrailingMargin]; + } + [quotedTextLabel autoPinBottomToSuperviewMarginWithInset:self.quotedContentBottomInset]; + } + + return quotedMessageView; +} + - (UIView *)loadViewForStillImage { OWSAssert(self.attachmentStream); @@ -1063,10 +1216,8 @@ CG_INLINE CGSize CGSizeCeil(CGSize size) const int maxMessageWidth = [self maxMessageWidthForContentWidth:contentWidth]; const int maxTextWidth = (int)floor(maxMessageWidth - (leftMargin + rightMargin)); - self.bodyTextView.text = self.displayableBodyText.displayText; - // Honor dynamic type in the message bodies. - self.bodyTextView.font = [self textMessageFont]; - CGSize textSize = CGSizeCeil([self.bodyTextView sizeThatFits:CGSizeMake(maxTextWidth, CGFLOAT_MAX)]); + OWSMessageTextView *bodyTextView = [self configureBodyTextView]; + CGSize textSize = CGSizeCeil([bodyTextView sizeThatFits:CGSizeMake(maxTextWidth, CGFLOAT_MAX)]); CGSize textViewSize = textSize; if (includeMargins) { @@ -1126,17 +1277,103 @@ CG_INLINE CGSize CGSizeCeil(CGSize size) return (int)floor(contentWidth * 0.8f); } +- (CGSize)quotedMessageSizeForViewWidth:(int)viewWidth + contentWidth:(int)contentWidth + includeMargins:(BOOL)includeMargins +{ + OWSAssert(self.viewItem); + OWSAssert([self.viewItem.interaction isKindOfClass:[TSMessage class]]); + + CGSize result = CGSizeZero; + + if (!self.isQuotedReply) { + return result; + } + + result.width += self.quotedMessageHInset; + result.width += self.quotedReplyStripeThickness; + result.width += self.quotedReplyStripeHSpacing; + + result.height += self.quotedMessageTopInset; + + CGFloat thumbnailHeight = 0.f; + if (self.hasQuotedAttachmentThumbnail) { + result.width += self.quotedThumbnailHSpacing; + result.width += self.quotedThumbnailSize; + + thumbnailHeight = self.quotedThumbnailSize; + } else { + result.width += self.quotedContentTrailingMargin; + } + + result.width += self.quotedMessageHInset; + + // Once we've determined everything _except_ the size of the text + // content (i.e. the quoted author and the quoted text (if any)), + // we can determine the size of the text content. + const int maxMessageWidth = [self maxMessageWidthForContentWidth:contentWidth]; + CGFloat maxTextWidth = (maxMessageWidth - (self.textTrailingMargin + self.textLeadingMargin + result.width)); + CGFloat textWidth = 0.f; + + // Author + { + OWSContactsManager *contactsManager = Environment.current.contactsManager; + NSString *quotedAuthor = [contactsManager displayNameForPhoneIdentifier:self.viewItem.quotedRecipientId]; + + UILabel *quotedAuthorLabel = [UILabel new]; + quotedAuthorLabel.text = quotedAuthor; + quotedAuthorLabel.font = self.quotedAuthorFont; + quotedAuthorLabel.lineBreakMode = NSLineBreakByTruncatingTail; + quotedAuthorLabel.numberOfLines = 1; + + CGSize quotedAuthorSize = CGSizeCeil([quotedAuthorLabel sizeThatFits:CGSizeMake(maxTextWidth, CGFLOAT_MAX)]); + + textWidth = MAX(textWidth, quotedAuthorSize.width); + result.height += self.quotedContentTopInset; + result.height += self.quotedAuthorHeight; + } + + if (self.hasQuotedText) { + UILabel *quotedTextLabel = [self createQuotedTextLabel]; + + CGSize textSize = CGSizeCeil([quotedTextLabel sizeThatFits:CGSizeMake(maxTextWidth, CGFLOAT_MAX)]); + + textWidth = MAX(textWidth, textSize.width); + result.height += self.quotedAuthorBottomSpacing; + result.height += textSize.height; + } + + result.width += textWidth; + result.height += self.quotedContentBottomInset; + + result.height = MAX(result.height, thumbnailHeight); + + if (includeMargins) { + result.width += kBubbleThornSideInset; + } + + return result; +} + - (CGSize)cellSizeForViewWidth:(int)viewWidth contentWidth:(int)contentWidth { OWSAssert(self.viewItem); OWSAssert([self.viewItem.interaction isKindOfClass:[TSMessage class]]); - CGSize mediaContentSize = [self bodyMediaSizeForContentWidth:contentWidth]; - CGSize textContentSize = [self bodyTextSizeForContentWidth:contentWidth includeMargins:YES]; + CGSize cellSize = CGSizeZero; - CGFloat cellContentWidth = fmax(mediaContentSize.width, textContentSize.width); - CGFloat cellContentHeight = mediaContentSize.height + textContentSize.height; - CGSize cellSize = CGSizeMake(cellContentWidth, cellContentHeight); + CGSize quotedMessageSize = + [self quotedMessageSizeForViewWidth:viewWidth contentWidth:contentWidth includeMargins:YES]; + cellSize.width = MAX(cellSize.width, quotedMessageSize.width); + cellSize.height += quotedMessageSize.height; + + CGSize mediaContentSize = [self bodyMediaSizeForContentWidth:contentWidth]; + cellSize.width = MAX(cellSize.width, mediaContentSize.width); + cellSize.height += mediaContentSize.height; + + CGSize textContentSize = [self bodyTextSizeForContentWidth:contentWidth includeMargins:YES]; + cellSize.width = MAX(cellSize.width, textContentSize.width); + cellSize.height += textContentSize.height; OWSAssert(cellSize.width > 0 && cellSize.height > 0); @@ -1165,6 +1402,92 @@ CG_INLINE CGSize CGSizeCeil(CGSize size) } } +- (UIFont *)tapForMoreFont +{ + return [UIFont ows_regularFontWithSize:12.f]; +} + +- (CGFloat)tapForMoreHeight +{ + return (CGFloat)ceil([self tapForMoreFont].lineHeight * 1.25); +} + +// TODO: +- (UIFont *)quotedAuthorFont +{ + return [UIFont ows_regularFontWithSize:10.f]; +} + +// TODO: +- (CGFloat)quotedAuthorHeight +{ + return (CGFloat)ceil([self quotedAuthorFont].lineHeight * 1.25); +} + +// TODO: +- (CGFloat)quotedAuthorBottomSpacing +{ + return 2.f; +} + +// TODO: +- (CGFloat)quotedContentTopInset +{ + return 3.f; +} + +// TODO: +- (CGFloat)quotedContentBottomInset +{ + return 3.f; +} + +// Distance from top edge of "quoted message" bubble to top of message bubble. +// TODO: +- (CGFloat)quotedMessageTopInset +{ + return 3.f; +} + +// Distance from side of "quoted message" bubble to side of message bubble. +// TODO: +- (CGFloat)quotedMessageHInset +{ + return 3.f; +} + +// TODO: +- (CGFloat)quotedReplyStripeThickness +{ + return 3.f; +} + +// The spacing between the vertical "quoted reply stripe" +// and the quoted message content. +// TODO: +- (CGFloat)quotedReplyStripeHSpacing +{ + return 10.f; +} + +// TODO: +- (CGFloat)quotedThumbnailSize +{ + return 30.f; +} + +// TODO: +- (CGFloat)quotedThumbnailHSpacing +{ + return 10.f; +} + +// TODO: +- (CGFloat)quotedContentTrailingMargin +{ + return 10.f; +} + #pragma mark - - (BOOL)isIncoming @@ -1195,6 +1518,24 @@ CG_INLINE CGSize CGSizeCeil(CGSize size) return result; } +- (CGFloat)quotedBubbleLeadingMargin +{ + CGFloat result = self.quotedMessageHInset; + if (self.isIncoming) { + result += kBubbleThornSideInset; + } + return result; +} + +- (CGFloat)quotedBubbleTrailingMargin +{ + CGFloat result = self.quotedMessageHInset; + if (!self.isIncoming) { + result += kBubbleThornSideInset; + } + return result; +} + - (CGFloat)textTopMargin { return kBubbleTextVInset; @@ -1205,11 +1546,16 @@ CG_INLINE CGSize CGSizeCeil(CGSize size) return kBubbleTextVInset + kBubbleThornVInset; } -- (UIColor *)textColor +- (UIColor *)bodyTextColor { return self.isIncoming ? [UIColor blackColor] : [UIColor whiteColor]; } +- (UIColor *)quotedTextColor +{ + return [UIColor blackColor]; +} + - (BOOL)isMediaBeingSent { if (self.isIncoming) { diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewItem.h b/Signal/src/ViewControllers/ConversationView/ConversationViewItem.h index 0ecf54be2f..0732492310 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewItem.h +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewItem.h @@ -43,7 +43,13 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType); @property (nonatomic, readonly) TSInteraction *interaction; @property (nonatomic, readonly) BOOL isGroupThread; + @property (nonatomic, readonly) BOOL hasBodyText; + +@property (nonatomic, readonly) BOOL isQuotedReply; +@property (nonatomic, readonly) BOOL hasQuotedAttachment; +@property (nonatomic, readonly) BOOL hasQuotedText; + @property (nonatomic) BOOL shouldShowDate; @property (nonatomic) BOOL shouldHideRecipientStatus; @property (nonatomic) BOOL shouldHideBubbleTail; @@ -85,6 +91,10 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType); - (nullable TSAttachmentPointer *)attachmentPointer; - (CGSize)mediaSize; +- (nullable DisplayableText *)displayableQuotedText; +- (nullable NSString *)quotedAttachmentMimetype; +- (nullable NSString *)quotedRecipientId; + // We don't want to try to load the media for this item (if any) // if a load has previously failed. @property (nonatomic) BOOL didCellMediaFailToLoad; diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewItem.m b/Signal/src/ViewControllers/ConversationView/ConversationViewItem.m index 3581ac47d0..a32338cdca 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewItem.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewItem.m @@ -56,6 +56,9 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType) @property (nonatomic) BOOL hasViewState; @property (nonatomic) OWSMessageCellType messageCellType; @property (nonatomic, nullable) DisplayableText *displayableBodyText; +@property (nonatomic, nullable) DisplayableText *displayableQuotedText; +@property (nonatomic, nullable) NSString *quotedAttachmentMimetype; +@property (nonatomic, nullable) NSString *quotedRecipientId; @property (nonatomic, nullable) TSAttachmentStream *attachmentStream; @property (nonatomic, nullable) TSAttachmentPointer *attachmentPointer; @property (nonatomic) CGSize mediaSize; @@ -99,6 +102,10 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType) self.attachmentPointer = nil; self.mediaSize = CGSizeZero; + self.displayableQuotedText = nil; + self.quotedRecipientId = nil; + self.quotedAttachmentMimetype = nil; + [self clearCachedLayoutState]; [self ensureViewState:transaction]; @@ -109,6 +116,21 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType) return _displayableBodyText != nil; } +- (BOOL)hasQuotedText +{ + return _displayableQuotedText != nil; +} + +- (BOOL)hasQuotedAttachment +{ + return self.quotedAttachmentMimetype.length > 0; +} + +- (BOOL)isQuotedReply +{ + return self.hasQuotedAttachment || self.hasQuotedText; +} + - (void)setShouldShowDate:(BOOL)shouldShowDate { if (_shouldShowDate == shouldShowDate) { @@ -275,11 +297,11 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType) [self.lastAudioMessageView updateContents]; } -#pragma mark - View State +#pragma mark - Displayable Text // TODO: Now that we're caching the displayable text on the view items, // I don't think we need this cache any more. -- (NSCache *)displayableBodyTextCache +- (NSCache *)displayableTextCache { static NSCache *cache = nil; static dispatch_once_t onceToken; @@ -296,10 +318,12 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType) OWSAssert(text); OWSAssert(interactionId.length > 0); - return [self displayableBodyTextForInteractionId:interactionId - textBlock:^{ - return text; - }]; + NSString *displayableTextCacheKey = [@"body-" stringByAppendingString:interactionId]; + + return [self displayableTextForCacheKey:displayableTextCacheKey + textBlock:^{ + return text; + }]; } - (DisplayableText *)displayableBodyTextForOversizeTextAttachment:(TSAttachmentStream *)attachmentStream @@ -308,30 +332,46 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType) OWSAssert(attachmentStream); OWSAssert(interactionId.length > 0); - return - [self displayableBodyTextForInteractionId:interactionId - textBlock:^{ - NSData *textData = [NSData dataWithContentsOfURL:attachmentStream.mediaURL]; - NSString *text = - [[NSString alloc] initWithData:textData encoding:NSUTF8StringEncoding]; - return text; - }]; + NSString *displayableTextCacheKey = [@"oversize-body-" stringByAppendingString:interactionId]; + + return [self displayableTextForCacheKey:displayableTextCacheKey + textBlock:^{ + NSData *textData = [NSData dataWithContentsOfURL:attachmentStream.mediaURL]; + NSString *text = + [[NSString alloc] initWithData:textData encoding:NSUTF8StringEncoding]; + return text; + }]; } -- (DisplayableText *)displayableBodyTextForInteractionId:(NSString *)interactionId - textBlock:(NSString * (^_Nonnull)(void))textBlock +- (DisplayableText *)displayableQuotedTextForText:(NSString *)text interactionId:(NSString *)interactionId { + OWSAssert(text); OWSAssert(interactionId.length > 0); - DisplayableText *_Nullable displayableBodyText = [[self displayableBodyTextCache] objectForKey:interactionId]; - if (!displayableBodyText) { - NSString *text = textBlock(); - displayableBodyText = [DisplayableText displayableText:text]; - [[self displayableBodyTextCache] setObject:displayableBodyText forKey:interactionId]; - } - return displayableBodyText; + NSString *displayableTextCacheKey = [@"quoted-" stringByAppendingString:interactionId]; + + return [self displayableTextForCacheKey:displayableTextCacheKey + textBlock:^{ + return text; + }]; } +- (DisplayableText *)displayableTextForCacheKey:(NSString *)displayableTextCacheKey + textBlock:(NSString * (^_Nonnull)(void))textBlock +{ + OWSAssert(displayableTextCacheKey.length > 0); + + DisplayableText *_Nullable displayableText = [[self displayableTextCache] objectForKey:displayableTextCacheKey]; + if (!displayableText) { + NSString *text = textBlock(); + displayableText = [DisplayableText displayableText:text]; + [[self displayableTextCache] setObject:displayableText forKey:displayableTextCacheKey]; + } + return displayableText; +} + +#pragma mark - View State + - (nullable TSAttachment *)firstAttachmentIfAnyOfMessage:(TSMessage *)message transaction:(YapDatabaseReadTransaction *)transaction { @@ -430,6 +470,16 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType) self.messageCellType = OWSMessageCellType_TextMessage; self.displayableBodyText = [[DisplayableText alloc] initWithFullText:@"" displayText:@"" isTextTruncated:NO]; } + + if (message.quotedMessage && message.quotedMessage.authorId.length > 0 + && (message.quotedMessage.body.length > 0 || message.quotedMessage.contentType.length > 0)) { + if (message.quotedMessage.body.length > 0) { + self.displayableQuotedText = + [self displayableQuotedTextForText:message.quotedMessage.body interactionId:message.uniqueId]; + } + self.quotedAttachmentMimetype = message.quotedMessage.contentType; + self.quotedRecipientId = message.quotedMessage.authorId; + } } - (OWSMessageCellType)messageCellType @@ -475,6 +525,18 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType) return _mediaSize; } +- (nullable DisplayableText *)displayableQuotedText +{ + OWSAssertIsOnMainThread(); + OWSAssert(self.hasViewState); + + OWSAssert(_displayableQuotedText); + OWSAssert(_displayableQuotedText.displayText); + OWSAssert(_displayableQuotedText.fullText); + + return _displayableQuotedText; +} + #pragma mark - UIMenuController - (NSArray *)textMenuControllerItems @@ -569,6 +631,7 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType) } } +// TODO: Update for quoted text. - (void)copyTextAction { switch (self.messageCellType) { @@ -628,6 +691,7 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType) } } +// TODO: Update for quoted text. - (void)shareTextAction { switch (self.messageCellType) { diff --git a/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m b/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m index cdeb528bf8..846d4b9dd4 100644 --- a/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m +++ b/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m @@ -87,6 +87,7 @@ NS_ASSUME_NONNULL_BEGIN [DebugUIMessages performActionNTimes:action]; }]]; } + [items addObjectsFromArray:@[ #pragma mark - Actions @@ -103,6 +104,10 @@ NS_ASSUME_NONNULL_BEGIN actionBlock:^{ [DebugUIMessages selectSendMediaAction:thread]; }], + [OWSTableItem itemWithTitle:@"Select Quoted Reply" + actionBlock:^{ + [DebugUIMessages selectQuotedReplyAction:thread]; + }], #pragma mark - Misc. @@ -2464,6 +2469,14 @@ isQuotedMessageAttachmentDownloaded:(BOOL)isQuotedMessageAttachmentDownloaded subactions:[self allFakeQuotedReplyActions:thread includeLabels:YES]]; } ++ (void)selectQuotedReplyAction:(TSThread *)thread +{ + OWSAssertIsOnMainThread(); + OWSAssert(thread); + + [self selectActionUI:[self allFakeQuotedReplyActions:thread includeLabels:NO] label:@"Select QuotedReply"]; +} + + (DebugUIMessagesAction *)randomQuotedReplyAction:(TSThread *)thread { OWSAssert(thread); @@ -2483,6 +2496,7 @@ isQuotedMessageAttachmentDownloaded:(BOOL)isQuotedMessageAttachmentDownloaded [actions addObjectsFromArray:[self allFakeMediaActions:thread includeLabels:includeLabels]]; [actions addObjectsFromArray:[self allFakeTextActions:thread includeLabels:includeLabels]]; [actions addObjectsFromArray:[self allFakeSequenceActions:thread includeLabels:includeLabels]]; + [actions addObjectsFromArray:[self allFakeQuotedReplyActions:thread includeLabels:includeLabels]]; return actions; } diff --git a/SignalServiceKit/src/Messages/Attachments/TSAttachmentStream.h b/SignalServiceKit/src/Messages/Attachments/TSAttachmentStream.h index 97c73e47c7..22e6fe54b4 100644 --- a/SignalServiceKit/src/Messages/Attachments/TSAttachmentStream.h +++ b/SignalServiceKit/src/Messages/Attachments/TSAttachmentStream.h @@ -46,6 +46,8 @@ NS_ASSUME_NONNULL_BEGIN - (BOOL)isAudio; - (nullable NSURL *)mediaURL; ++ (BOOL)hasThumbnailForMimeType:(NSString *)contentType; + - (nullable NSString *)filePath; - (nullable NSString *)thumbnailPath; diff --git a/SignalServiceKit/src/Messages/Attachments/TSAttachmentStream.m b/SignalServiceKit/src/Messages/Attachments/TSAttachmentStream.m index 15ca2fbd9f..51fadbaa46 100644 --- a/SignalServiceKit/src/Messages/Attachments/TSAttachmentStream.m +++ b/SignalServiceKit/src/Messages/Attachments/TSAttachmentStream.m @@ -331,6 +331,12 @@ NS_ASSUME_NONNULL_BEGIN } } ++ (BOOL)hasThumbnailForMimeType:(NSString *)contentType +{ + return ([MIMETypeUtil isVideo:contentType] || [MIMETypeUtil isImage:contentType] || + [MIMETypeUtil isAnimated:contentType]); +} + - (nullable UIImage *)thumbnailImage { NSString *thumbnailPath = self.thumbnailPath; diff --git a/SignalServiceKit/src/Messages/Interactions/TSQuotedMessage.h b/SignalServiceKit/src/Messages/Interactions/TSQuotedMessage.h index 0d9ae2d53f..51acde44f9 100644 --- a/SignalServiceKit/src/Messages/Interactions/TSQuotedMessage.h +++ b/SignalServiceKit/src/Messages/Interactions/TSQuotedMessage.h @@ -11,16 +11,17 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, readonly) uint64_t timestamp; @property (nonatomic, readonly) NSString *authorId; -// This property should be set IFF we are quoting a text message. +// This property should be set IFF we are quoting a text message +// or attachment with caption. @property (nullable, nonatomic, readonly) NSString *body; -// This property should be set IFF we are quoting a attachment message. +// This property should be set IFF we are quoting an attachment message. @property (nullable, nonatomic, readonly) NSString *sourceFilename; -// This property can be set IFF we are quoting a attachment message, but it is optional. +// This property can be set IFF we are quoting an attachment message, but it is optional. @property (nullable, nonatomic, readonly) NSData *thumbnailData; // This is a MIME type. // -// This property should be set IFF we are quoting a attachment message. +// This property should be set IFF we are quoting an attachment message. @property (nullable, nonatomic, readonly) NSString *contentType; - (instancetype)init NS_UNAVAILABLE;