// // Copyright (c) 2019 Open Whisper Systems. All rights reserved. // #import "OWSQuotedMessageView.h" #import "ConversationViewItem.h" #import "Environment.h" #import "OWSBubbleView.h" #import "Session-Swift.h" #import #import #import #import #import #import #import NS_ASSUME_NONNULL_BEGIN const CGFloat kRemotelySourcedContentGlyphLength = 16; const CGFloat kRemotelySourcedContentRowMargin = 4; const CGFloat kRemotelySourcedContentRowSpacing = 4; @interface OWSQuotedMessageView () @property (nonatomic, readonly) OWSQuotedReplyModel *quotedMessage; @property (nonatomic, nullable, readonly) DisplayableText *displayableQuotedText; @property (nonatomic, readonly) ConversationStyle *conversationStyle; @property (nonatomic, readonly) BOOL isForPreview; @property (nonatomic, readonly) BOOL isOutgoing; @property (nonatomic, readonly) OWSDirectionalRectCorner sharpCorners; @property (nonatomic, readonly) UILabel *quotedAuthorLabel; @property (nonatomic, readonly) UILabel *quotedTextLabel; @property (nonatomic, readonly) UILabel *quoteContentSourceLabel; @end #pragma mark - @implementation OWSQuotedMessageView + (OWSQuotedMessageView *)quotedMessageViewForConversation:(OWSQuotedReplyModel *)quotedMessage displayableQuotedText:(nullable DisplayableText *)displayableQuotedText conversationStyle:(ConversationStyle *)conversationStyle isOutgoing:(BOOL)isOutgoing sharpCorners:(OWSDirectionalRectCorner)sharpCorners { OWSAssertDebug(quotedMessage); return [[OWSQuotedMessageView alloc] initWithQuotedMessage:quotedMessage displayableQuotedText:displayableQuotedText conversationStyle:conversationStyle isForPreview:NO isOutgoing:isOutgoing sharpCorners:sharpCorners]; } + (OWSQuotedMessageView *)quotedMessageViewForPreview:(OWSQuotedReplyModel *)quotedMessage conversationStyle:(ConversationStyle *)conversationStyle { OWSAssertDebug(quotedMessage); DisplayableText *_Nullable displayableQuotedText = nil; if (quotedMessage.body.length > 0) { displayableQuotedText = [DisplayableText displayableText:quotedMessage.body]; } OWSQuotedMessageView *instance = [[OWSQuotedMessageView alloc] initWithQuotedMessage:quotedMessage displayableQuotedText:displayableQuotedText conversationStyle:conversationStyle isForPreview:YES isOutgoing:YES sharpCorners:(OWSDirectionalRectCornerBottomLeading | OWSDirectionalRectCornerBottomTrailing)]; [instance createContents]; return instance; } - (instancetype)initWithQuotedMessage:(OWSQuotedReplyModel *)quotedMessage displayableQuotedText:(nullable DisplayableText *)displayableQuotedText conversationStyle:(ConversationStyle *)conversationStyle isForPreview:(BOOL)isForPreview isOutgoing:(BOOL)isOutgoing sharpCorners:(OWSDirectionalRectCorner)sharpCorners { self = [super init]; if (!self) { return self; } OWSAssertDebug(quotedMessage); _quotedMessage = quotedMessage; _displayableQuotedText = displayableQuotedText; _isForPreview = isForPreview; _conversationStyle = conversationStyle; _isOutgoing = isOutgoing; _sharpCorners = sharpCorners; _quotedAuthorLabel = [UILabel new]; _quotedTextLabel = [UILabel new]; _quoteContentSourceLabel = [UILabel new]; return self; } - (BOOL)hasQuotedAttachment { return (self.quotedMessage.contentType.length > 0 && ![OWSMimeTypeOversizeTextMessage isEqualToString:self.quotedMessage.contentType]); } - (BOOL)hasQuotedAttachmentThumbnailImage { return (self.quotedMessage.contentType.length > 0 && ![OWSMimeTypeOversizeTextMessage isEqualToString:self.quotedMessage.contentType] && [TSAttachmentStream hasThumbnailForMimeType:self.quotedMessage.contentType]); } - (UIColor *)highlightColor { BOOL isQuotingSelf = [NSObject isNullableObject:self.quotedMessage.authorId equalTo:TSAccountManager.localNumber]; return (isQuotingSelf ? [self.conversationStyle bubbleColorWithIsIncoming:NO] : [self.conversationStyle quotingSelfHighlightColor]); } #pragma mark - - (CGFloat)bubbleHMargin { return (self.isForPreview ? 0.f : 20.f); } - (CGFloat)hSpacing { return 8.f; } - (CGFloat)vSpacing { return 4.f; } - (CGFloat)stripeThickness { return LKValues.accentLineThickness; } - (UIColor *)quoteBubbleBackgroundColor { return [self.conversationStyle quotedReplyBubbleColorWithIsIncoming:!self.isOutgoing]; } - (void)createContents { // Ensure only called once. OWSAssertDebug(self.subviews.count < 1); self.userInteractionEnabled = YES; self.layoutMargins = UIEdgeInsetsZero; self.clipsToBounds = YES; CAShapeLayer *maskLayer = [CAShapeLayer new]; OWSDirectionalRectCorner sharpCorners = self.sharpCorners; OWSLayerView *innerBubbleView = [[OWSLayerView alloc] initWithFrame:CGRectZero layoutCallback:^(UIView *layerView) { CGRect layerFrame = layerView.bounds; const CGFloat bubbleLeft = 0.f; const CGFloat bubbleRight = layerFrame.size.width; const CGFloat bubbleTop = 0.f; const CGFloat bubbleBottom = layerFrame.size.height; const CGFloat sharpCornerRadius = 2; const CGFloat wideCornerRadius = self.isForPreview ? 14 : 4; UIBezierPath *bezierPath = [OWSBubbleView roundedBezierRectWithBubbleTop:bubbleTop bubbleLeft:bubbleLeft bubbleBottom:bubbleBottom bubbleRight:bubbleRight sharpCornerRadius:sharpCornerRadius wideCornerRadius:wideCornerRadius sharpCorners:sharpCorners]; maskLayer.path = bezierPath.CGPath; }]; innerBubbleView.layer.mask = maskLayer; if (self.isForPreview) { NSString *userHexEncodedPublicKey = OWSIdentityManager.sharedManager.identityKeyPair.hexEncodedPublicKey; BOOL wasSentByUser = [self.quotedMessage.authorId isEqual:userHexEncodedPublicKey]; innerBubbleView.backgroundColor = [self.conversationStyle quotedReplyBubbleColorWithIsIncoming:wasSentByUser]; } else { innerBubbleView.backgroundColor = self.quoteBubbleBackgroundColor; } [self addSubview:innerBubbleView]; [innerBubbleView autoPinLeadingToSuperviewMarginWithInset:self.bubbleHMargin]; [innerBubbleView autoPinTrailingToSuperviewMarginWithInset:self.bubbleHMargin]; [innerBubbleView autoPinTopToSuperviewMargin]; [innerBubbleView autoPinBottomToSuperviewMargin]; [innerBubbleView setContentHuggingHorizontalLow]; [innerBubbleView setCompressionResistanceHorizontalLow]; UIStackView *hStackView = [UIStackView new]; hStackView.axis = UILayoutConstraintAxisHorizontal; hStackView.spacing = self.hSpacing; UIView *stripeView = [UIView new]; if (self.isForPreview) { stripeView.backgroundColor = LKColors.accent; } else { stripeView.backgroundColor = [self.conversationStyle quotedReplyStripeColorWithIsIncoming:!self.isOutgoing]; } [stripeView autoSetDimension:ALDimensionWidth toSize:self.stripeThickness]; [stripeView setContentHuggingHigh]; [stripeView setCompressionResistanceHigh]; [hStackView addArrangedSubview:stripeView]; UIStackView *vStackView = [UIStackView new]; vStackView.axis = UILayoutConstraintAxisVertical; vStackView.layoutMargins = UIEdgeInsetsMake(self.textVMargin, 0, self.textVMargin, 0); vStackView.layoutMarginsRelativeArrangement = YES; vStackView.spacing = self.vSpacing; [vStackView setContentHuggingHorizontalLow]; [vStackView setCompressionResistanceHorizontalLow]; [hStackView addArrangedSubview:vStackView]; UILabel *quotedAuthorLabel = [self configureQuotedAuthorLabel]; [vStackView addArrangedSubview:quotedAuthorLabel]; [quotedAuthorLabel autoSetDimension:ALDimensionHeight toSize:self.quotedAuthorHeight]; [quotedAuthorLabel setContentHuggingVerticalHigh]; [quotedAuthorLabel setContentHuggingHorizontalLow]; [quotedAuthorLabel setCompressionResistanceHorizontalLow]; UILabel *quotedTextLabel = [self configureQuotedTextLabel]; [vStackView addArrangedSubview:quotedTextLabel]; [quotedTextLabel setContentHuggingHorizontalLow]; [quotedTextLabel setCompressionResistanceHorizontalLow]; [quotedTextLabel setCompressionResistanceVerticalHigh]; if (self.hasQuotedAttachment) { UIView *_Nullable quotedAttachmentView = nil; UIImage *_Nullable thumbnailImage = [self tryToLoadThumbnailImage]; if (thumbnailImage) { quotedAttachmentView = [self imageViewForImage:thumbnailImage]; quotedAttachmentView.clipsToBounds = YES; quotedAttachmentView.backgroundColor = [UIColor whiteColor]; if (self.isVideoAttachment) { UIImage *contentIcon = [UIImage imageNamed:@"Play"]; contentIcon = [contentIcon imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; UIImageView *contentImageView = [self imageViewForImage:contentIcon]; contentImageView.tintColor = LKColors.text; [quotedAttachmentView addSubview:contentImageView]; [contentImageView autoSetDimension:ALDimensionWidth toSize:16]; [contentImageView autoSetDimension:ALDimensionHeight toSize:16]; [contentImageView autoCenterInSuperview]; } } else if (self.quotedMessage.thumbnailDownloadFailed) { // TODO design review icon and color UIImage *contentIcon = [[UIImage imageNamed:@"btnRefresh--white"] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; UIImageView *contentImageView = [self imageViewForImage:contentIcon]; contentImageView.contentMode = UIViewContentModeScaleAspectFit; contentImageView.tintColor = UIColor.whiteColor; quotedAttachmentView = [UIView containerView]; [quotedAttachmentView addSubview:contentImageView]; quotedAttachmentView.backgroundColor = self.highlightColor; [contentImageView autoCenterInSuperview]; [contentImageView autoSetDimensionsToSize:CGSizeMake(self.quotedAttachmentSize * 0.5f, self.quotedAttachmentSize * 0.5f)]; UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(didTapFailedThumbnailDownload:)]; [quotedAttachmentView addGestureRecognizer:tapGesture]; quotedAttachmentView.userInteractionEnabled = YES; } else { UIImage *contentIcon = [UIImage imageNamed:@"generic-attachment"]; UIImageView *contentImageView = [self imageViewForImage:contentIcon]; contentImageView.contentMode = UIViewContentModeScaleAspectFit; UIView *wrapper = [UIView containerView]; [wrapper addSubview:contentImageView]; [contentImageView autoCenterInSuperview]; [contentImageView autoSetDimension:ALDimensionWidth toSize:self.quotedAttachmentSize * 0.5f]; quotedAttachmentView = wrapper; } [quotedAttachmentView autoPinToSquareAspectRatio]; [quotedAttachmentView setContentHuggingHigh]; [quotedAttachmentView setCompressionResistanceHigh]; [hStackView addArrangedSubview:quotedAttachmentView]; } else { // If there's no attachment, add an empty view so that // the stack view's spacing serves as a margin between // the text views and the trailing edge. UIView *emptyView = [UIView containerView]; [hStackView addArrangedSubview:emptyView]; [emptyView setContentHuggingHigh]; [emptyView autoSetDimension:ALDimensionWidth toSize:0.f]; } UIView *contentView = hStackView; [contentView setContentHuggingHorizontalLow]; [contentView setCompressionResistanceHorizontalLow]; if (self.quotedMessage.isRemotelySourced) { UIStackView *quoteSourceWrapper = [[UIStackView alloc] initWithArrangedSubviews:@[ contentView, [self buildRemoteContentSourceView], ]]; quoteSourceWrapper.axis = UILayoutConstraintAxisVertical; contentView = quoteSourceWrapper; [contentView setContentHuggingHorizontalLow]; [contentView setCompressionResistanceHorizontalLow]; } if (self.isForPreview) { UIButton *cancelButton = [UIButton buttonWithType:UIButtonTypeCustom]; UIColor *tintColor = [LKAppModeUtilities isLightMode] ? UIColor.blackColor : UIColor.whiteColor; UIImage *cancelIcon = [[UIImage imageNamed:@"X"] asTintedImageWithColor:tintColor]; [cancelButton setImage:cancelIcon forState:UIControlStateNormal]; cancelButton.contentMode = UIViewContentModeScaleAspectFit; [cancelButton addTarget:self action:@selector(didTapCancel) forControlEvents:UIControlEventTouchUpInside]; [cancelButton autoSetDimension:ALDimensionWidth toSize:14.f]; [cancelButton autoSetDimension:ALDimensionHeight toSize:14.f]; UIStackView *cancelStack = [[UIStackView alloc] initWithArrangedSubviews:@[ cancelButton ]]; cancelStack.axis = UILayoutConstraintAxisHorizontal; cancelStack.alignment = UIStackViewAlignmentTop; cancelStack.layoutMarginsRelativeArrangement = YES; CGFloat hMarginLeading = 8; CGFloat hMarginTrailing = 8; cancelStack.layoutMargins = UIEdgeInsetsMake(8, CurrentAppContext().isRTL ? hMarginTrailing : hMarginLeading, 0, CurrentAppContext().isRTL ? hMarginLeading : hMarginTrailing); UIStackView *cancelWrapper = [[UIStackView alloc] initWithArrangedSubviews:@[ contentView, cancelStack, ]]; cancelWrapper.axis = UILayoutConstraintAxisHorizontal; contentView = cancelWrapper; [contentView setContentHuggingHorizontalLow]; [contentView setCompressionResistanceHorizontalLow]; } [innerBubbleView addSubview:contentView]; [contentView ows_autoPinToSuperviewEdges]; } - (UIView *)buildRemoteContentSourceView { UIImage *glyphImage = [[UIImage imageNamed:@"ic_broken_link"] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; OWSAssertDebug(glyphImage); OWSAssertDebug(CGSizeEqualToSize( CGSizeMake(kRemotelySourcedContentGlyphLength, kRemotelySourcedContentGlyphLength), glyphImage.size)); UIImageView *glyphView = [[UIImageView alloc] initWithImage:glyphImage]; glyphView.tintColor = LKColors.text; [glyphView autoSetDimensionsToSize:CGSizeMake(kRemotelySourcedContentGlyphLength, kRemotelySourcedContentGlyphLength)]; UILabel *label = [self configureQuoteContentSourceLabel]; UIStackView *sourceRow = [[UIStackView alloc] initWithArrangedSubviews:@[ glyphView, label ]]; sourceRow.axis = UILayoutConstraintAxisHorizontal; sourceRow.alignment = UIStackViewAlignmentCenter; // TODO verify spacing w/ design sourceRow.spacing = kRemotelySourcedContentRowSpacing; sourceRow.layoutMarginsRelativeArrangement = YES; const CGFloat leftMargin = 4; sourceRow.layoutMargins = UIEdgeInsetsMake(kRemotelySourcedContentRowMargin, leftMargin, kRemotelySourcedContentRowMargin, kRemotelySourcedContentRowMargin); UIColor *backgroundColor = LKAppModeUtilities.isLightMode ? [UIColor.whiteColor colorWithAlphaComponent:LKValues.unimportantElementOpacity] : [LKColors.text colorWithAlphaComponent:LKValues.unimportantElementOpacity]; [sourceRow addBackgroundViewWithBackgroundColor:backgroundColor]; return sourceRow; } - (void)didTapFailedThumbnailDownload:(UITapGestureRecognizer *)gestureRecognizer { OWSLogDebug(@"in didTapFailedThumbnailDownload"); if (!self.quotedMessage.thumbnailDownloadFailed) { OWSFailDebug(@"thumbnailDownloadFailed was unexpectedly false"); return; } if (!self.quotedMessage.thumbnailAttachmentPointer) { OWSFailDebug(@"thumbnailAttachmentPointer was unexpectedly nil"); return; } [self.delegate didTapQuotedReply:self.quotedMessage failedThumbnailDownloadAttachmentPointer:self.quotedMessage.thumbnailAttachmentPointer]; } - (nullable UIImage *)tryToLoadThumbnailImage { if (!self.hasQuotedAttachmentThumbnailImage) { return nil; } // TODO: Possibly ignore data that is too large. UIImage *_Nullable image = self.quotedMessage.thumbnailImage; // TODO: Possibly ignore images that are too large. return image; } - (UIImageView *)imageViewForImage:(UIImage *)image { OWSAssertDebug(image); UIImageView *imageView = [UIImageView new]; imageView.image = image; // We need to specify a contentMode since the size of the image // might not match the aspect ratio of the view. imageView.contentMode = UIViewContentModeScaleAspectFill; // Use trilinear filters for better scaling quality at // some performance cost. imageView.layer.minificationFilter = kCAFilterTrilinear; imageView.layer.magnificationFilter = kCAFilterTrilinear; return imageView; } - (UILabel *)configureQuotedTextLabel { OWSAssertDebug(self.quotedTextLabel); UIColor *textColor = self.quotedTextColor; SUPPRESS_DEADSTORE_WARNING(textColor); UIFont *font = self.quotedTextFont; SUPPRESS_DEADSTORE_WARNING(font); NSString *text = @""; NSString *_Nullable fileTypeForSnippet = [self fileTypeForSnippet]; NSString *_Nullable sourceFilename = [self.quotedMessage.sourceFilename filterStringForDisplay]; if (self.displayableQuotedText.displayText.length > 0) { text = self.displayableQuotedText.displayText; textColor = self.quotedTextColor; font = self.quotedTextFont; } else if (fileTypeForSnippet) { text = fileTypeForSnippet; textColor = self.fileTypeTextColor; font = self.fileTypeFont; } else if (sourceFilename) { text = sourceFilename; textColor = self.filenameTextColor; font = self.filenameFont; } else { text = NSLocalizedString( @"QUOTED_REPLY_TYPE_ATTACHMENT", @"Indicates this message is a quoted reply to an attachment of unknown type."); textColor = self.fileTypeTextColor; font = self.fileTypeFont; } self.quotedTextLabel.numberOfLines = self.isForPreview ? 1 : 2; self.quotedTextLabel.lineBreakMode = NSLineBreakByTruncatingTail; self.quotedTextLabel.text = text; self.quotedTextLabel.textColor = textColor; self.quotedTextLabel.font = font; return self.quotedTextLabel; } - (UILabel *)configureQuoteContentSourceLabel { OWSAssertDebug(self.quoteContentSourceLabel); self.quoteContentSourceLabel.font = [UIFont systemFontOfSize:LKValues.smallFontSize]; self.quoteContentSourceLabel.textColor = LKColors.text; self.quoteContentSourceLabel.text = NSLocalizedString(@"QUOTED_REPLY_CONTENT_FROM_REMOTE_SOURCE", @"Footer label that appears below quoted messages when the quoted content was not derived locally. When the " @"local user doesn't have a copy of the message being quoted, e.g. if it had since been deleted, we instead " @"show the content specified by the sender."); return self.quoteContentSourceLabel; } - (nullable NSString *)fileTypeForSnippet { // TODO: Are we going to use the filename? For all mimetypes? NSString *_Nullable contentType = self.quotedMessage.contentType; if (contentType.length < 1) { return nil; } if ([MIMETypeUtil isAudio:contentType]) { return NSLocalizedString( @"QUOTED_REPLY_TYPE_AUDIO", @"Indicates this message is a quoted reply to an audio file."); } else if ([MIMETypeUtil isVideo:contentType]) { return NSLocalizedString( @"QUOTED_REPLY_TYPE_VIDEO", @"Indicates this message is a quoted reply to a video file."); } else if ([MIMETypeUtil isImage:contentType]) { return NSLocalizedString( @"QUOTED_REPLY_TYPE_IMAGE", @"Indicates this message is a quoted reply to an image file."); } else if ([MIMETypeUtil isAnimated:contentType]) { return NSLocalizedString( @"QUOTED_REPLY_TYPE_GIF", @"Indicates this message is a quoted reply to animated GIF file."); } return nil; } - (BOOL)isAudioAttachment { // TODO: Are we going to use the filename? For all mimetypes? NSString *_Nullable contentType = self.quotedMessage.contentType; if (contentType.length < 1) { return NO; } return [MIMETypeUtil isAudio:contentType]; } - (BOOL)isVideoAttachment { // TODO: Are we going to use the filename? For all mimetypes? NSString *_Nullable contentType = self.quotedMessage.contentType; if (contentType.length < 1) { return NO; } return [MIMETypeUtil isVideo:contentType]; } - (UILabel *)configureQuotedAuthorLabel { OWSAssertDebug(self.quotedAuthorLabel); NSString *_Nullable localNumber = [TSAccountManager localNumber]; NSString *quotedAuthorText; if ([localNumber isEqualToString:self.quotedMessage.authorId]) { if (self.isOutgoing) { quotedAuthorText = NSLocalizedString(@"You", @""); } else { quotedAuthorText = NSLocalizedString(@"You", @""); } } else { __block NSString *quotedAuthor = [SSKEnvironment.shared.profileManager profileNameForRecipientWithID:self.quotedMessage.authorId] ?: self.quotedMessage.authorId; if (quotedAuthor == self.quotedMessage.authorId) { SNOpenGroup *openGroup = [LKStorage.shared getOpenGroupForThreadID:self.quotedMessage.threadId]; [OWSPrimaryStorage.sharedManager.dbReadConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) { if (openGroup != nil) { quotedAuthor = [LKUserDisplayNameUtilities getPublicChatDisplayNameFor:self.quotedMessage.authorId in:openGroup.channel on:openGroup.server using:transaction]; } }]; } quotedAuthorText = quotedAuthor; } self.quotedAuthorLabel.text = quotedAuthorText; self.quotedAuthorLabel.font = self.quotedAuthorFont; // TODO: self.quotedAuthorLabel.textColor = [self quotedAuthorColor]; self.quotedAuthorLabel.lineBreakMode = NSLineBreakByTruncatingTail; self.quotedAuthorLabel.numberOfLines = 1; return self.quotedAuthorLabel; } #pragma mark - Measurement - (CGFloat)textVMargin { return 7.f; } - (CGSize)sizeForMaxWidth:(CGFloat)maxWidth { CGSize result = CGSizeZero; result.width += self.bubbleHMargin * 2 + self.stripeThickness + self.hSpacing * 2; CGFloat thumbnailHeight = 0.f; if (self.hasQuotedAttachment) { result.width += self.quotedAttachmentSize; thumbnailHeight += self.quotedAttachmentSize; } // Quoted Author CGFloat textWidth = 0.f; CGFloat maxTextWidth = maxWidth - result.width; CGFloat textHeight = self.textVMargin * 2 + self.quotedAuthorHeight + self.vSpacing; { UILabel *quotedAuthorLabel = [self configureQuotedAuthorLabel]; CGSize quotedAuthorSize = CGSizeCeil([quotedAuthorLabel sizeThatFits:CGSizeMake(maxTextWidth, CGFLOAT_MAX)]); textWidth = quotedAuthorSize.width; } { UILabel *quotedTextLabel = [self configureQuotedTextLabel]; CGSize textSize = CGSizeCeil([quotedTextLabel sizeThatFits:CGSizeMake(maxTextWidth, CGFLOAT_MAX)]); textWidth = MAX(textWidth, textSize.width); textHeight += textSize.height; } if (self.quotedMessage.isRemotelySourced) { UILabel *quoteContentSourceLabel = [self configureQuoteContentSourceLabel]; CGSize textSize = CGSizeCeil([quoteContentSourceLabel sizeThatFits:CGSizeMake(maxTextWidth, CGFLOAT_MAX)]); CGFloat sourceStackViewHeight = MAX(kRemotelySourcedContentGlyphLength, textSize.height); textWidth = MAX(textWidth, textSize.width + kRemotelySourcedContentGlyphLength + kRemotelySourcedContentRowSpacing); result.height += kRemotelySourcedContentRowMargin * 2 + sourceStackViewHeight; } textWidth = MIN(textWidth, maxTextWidth); result.width += textWidth; result.height += MAX(textHeight, thumbnailHeight); return CGSizeCeil(result); } - (UIFont *)quotedAuthorFont { return [UIFont boldSystemFontOfSize:LKValues.smallFontSize]; } - (UIColor *)quotedAuthorColor { return [self.conversationStyle quotedReplyAuthorColor]; } - (UIColor *)quotedTextColor { return [self.conversationStyle quotedReplyTextColor]; } - (UIFont *)quotedTextFont { return [UIFont systemFontOfSize:LKValues.smallFontSize]; } - (UIColor *)fileTypeTextColor { return [self.conversationStyle quotedReplyAttachmentColor]; } - (UIFont *)fileTypeFont { return self.quotedTextFont; } - (UIColor *)filenameTextColor { return [self.conversationStyle quotedReplyAttachmentColor]; } - (UIFont *)filenameFont { return self.quotedTextFont; } - (CGFloat)quotedAuthorHeight { return (CGFloat)ceil([self quotedAuthorFont].lineHeight * 1.f); } - (CGFloat)quotedAttachmentSize { return 54.f; } #pragma mark - - (CGSize)sizeThatFits:(CGSize)size { return [self sizeForMaxWidth:CGFLOAT_MAX]; } - (void)didTapCancel { [self.delegate didCancelQuotedReply]; } @end NS_ASSUME_NONNULL_END