// // Copyright (c) 2017 Open Whisper Systems. All rights reserved. // #import "OWSMessagesBubblesSizeCalculator.h" #import "OWSCall.h" #import "OWSContactOffersCell.h" #import "OWSContactOffersInteraction.h" #import "OWSSystemMessageCell.h" #import "OWSUnreadIndicatorCell.h" #import "TSGenericAttachmentAdapter.h" #import "TSMessageAdapter.h" #import "TSUnreadIndicatorInteraction.h" #import "UIFont+OWS.h" #import "UIView+OWS.h" #import "tgmath.h" // generic math allows fmax to handle CGFLoat correctly on 32 & 64bit. #import #import NS_ASSUME_NONNULL_BEGIN /** * We use some private method to size our info messages. */ @interface OWSMessagesBubblesSizeCalculator (JSQPrivateMethods) @property (strong, nonatomic, readonly) NSCache *cache; @property (assign, nonatomic, readonly) NSUInteger minimumBubbleWidth; @property (assign, nonatomic, readonly) BOOL usesFixedWidthBubbles; @property (assign, nonatomic, readonly) NSInteger additionalInset; @property (assign, nonatomic) CGFloat layoutWidthForFixedWidthBubbles; - (CGSize)jsq_avatarSizeForMessageData:(id)messageData withLayout:(JSQMessagesCollectionViewFlowLayout *)layout; - (CGFloat)textBubbleWidthForLayout:(JSQMessagesCollectionViewFlowLayout *)layout; @end #pragma mark - @interface OWSMessagesBubblesSizeCalculator () @property (nonatomic, readonly) OWSSystemMessageCell *referenceSystemMessageCell; @property (nonatomic, readonly) OWSUnreadIndicatorCell *referenceUnreadIndicatorCell; @property (nonatomic, readonly) OWSContactOffersCell *referenceContactOffersCell; @end #pragma mark - @implementation OWSMessagesBubblesSizeCalculator - (instancetype)init { if (self = [super init]) { _referenceSystemMessageCell = [OWSSystemMessageCell new]; _referenceUnreadIndicatorCell = [OWSUnreadIndicatorCell new]; _referenceContactOffersCell = [OWSContactOffersCell new]; // Calculating message size is relatively expensive, so unbound the size of the cache. self.cache.countLimit = 0; self.cache.totalCostLimit = 0; } return self; } /** * Computes and returns the size of the `messageBubbleImageView` property * of a `JSQMessagesCollectionViewCell` for the specified messageData at indexPath. * * @param messageData A message data object. * @param indexPath The index path at which messageData is located. * @param layout The layout object asking for this information. * * @return A sizes that specifies the required dimensions to display the entire message contents. * Note, this is *not* the entire cell, but only its message bubble. */ - (CGSize)messageBubbleSizeForMessageData:(id)messageData atIndexPath:(NSIndexPath *)indexPath withLayout:(JSQMessagesCollectionViewFlowLayout *)layout { id cacheKey = [self cacheKeyForMessageData:messageData]; NSValue *cachedSize = [self.cache objectForKey:cacheKey]; if (cachedSize != nil) { return [cachedSize CGSizeValue]; } CGSize result = [self calculateMessageBubbleSizeForMessageData:messageData atIndexPath:indexPath withLayout:layout]; [self.cache setObject:[NSValue valueWithCGSize:result] forKey:cacheKey]; return result; } - (CGSize)calculateMessageBubbleSizeForMessageData:(id)messageData atIndexPath:(NSIndexPath *)indexPath withLayout:(JSQMessagesCollectionViewFlowLayout *)layout { if ([messageData isKindOfClass:[TSMessageAdapter class]]) { TSMessageAdapter *message = (TSMessageAdapter *)messageData; switch (message.messageType) { case TSCallAdapter: case TSInfoMessageAdapter: case TSErrorMessageAdapter: { TSInteraction *interaction = ((TSMessageAdapter *)messageData).interaction; return [self sizeForSystemMessage:interaction layout:layout]; } case TSUnreadIndicatorAdapter: { TSUnreadIndicatorInteraction *interaction = (TSUnreadIndicatorInteraction *)((TSMessageAdapter *)messageData).interaction; return [self sizeForUnreadIndicator:interaction layout:layout]; } case OWSContactOffersAdapter: { OWSContactOffersInteraction *interaction = (OWSContactOffersInteraction *)((TSMessageAdapter *)messageData).interaction; return [self sizeForContactOffers:interaction layout:layout]; } case TSIncomingMessageAdapter: case TSOutgoingMessageAdapter: break; default: OWSFail(@"Unknown sizing interaction: %@", [((TSMessageAdapter *)messageData).interaction class]); break; } } else if ([messageData isKindOfClass:[OWSCall class]]) { TSInteraction *interaction = ((OWSCall *)messageData).interaction; return [self sizeForSystemMessage:interaction layout:layout]; } else { OWSFail(@"Can't size unknown message data type: %@", [messageData class]); } // BEGIN HACK iOS10EmojiBug see: https://github.com/WhisperSystems/Signal-iOS/issues/1368 if ([self shouldApplyiOS10EmojiFixToString:messageData.text font:layout.messageBubbleFont]) { return [self withiOS10EmojiFixSuperMessageBubbleSizeForMessageData:messageData atIndexPath:indexPath withLayout:layout]; } else { // END HACK iOS10EmojiBug see: https://github.com/WhisperSystems/Signal-iOS/issues/1368 return [super messageBubbleSizeForMessageData:messageData atIndexPath:indexPath withLayout:layout]; } } - (CGSize)sizeForSystemMessage:(TSInteraction *)interaction layout:(JSQMessagesCollectionViewFlowLayout *)layout { OWSAssert([NSThread isMainThread]); OWSAssert(interaction); return [self.referenceSystemMessageCell bubbleSizeForInteraction:interaction collectionViewWidth:layout.collectionView.width]; } - (CGSize)sizeForUnreadIndicator:(TSUnreadIndicatorInteraction *)interaction layout:(JSQMessagesCollectionViewFlowLayout *)layout { OWSAssert(interaction); return [self.referenceUnreadIndicatorCell bubbleSizeForInteraction:interaction collectionViewWidth:layout.collectionView.width]; } - (CGSize)sizeForContactOffers:(OWSContactOffersInteraction *)interaction layout:(JSQMessagesCollectionViewFlowLayout *)layout { OWSAssert(interaction); return [self.referenceContactOffersCell bubbleSizeForInteraction:interaction collectionViewWidth:layout.collectionView.width]; } /** * Emoji sizing bug only affects iOS10. Unfortunately the "fix" for emoji font breaks some other fonts, so it's * important * to only apply it when emoji is actually present. */ - (BOOL)shouldApplyiOS10EmojiFixToString:(NSString *)string font:(UIFont *)font { if (!string) { return NO; } BOOL isIOS10OrGreater = [[NSProcessInfo processInfo] isOperatingSystemAtLeastVersion:(NSOperatingSystemVersion){.majorVersion = 10 }]; if (!isIOS10OrGreater) { return NO; } __block BOOL foundEmoji = NO; NSDictionary *attributes = @{ NSFontAttributeName : font }; NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:string attributes:attributes]; [attributedString fixAttributesInRange:NSMakeRange(0, string.length)]; [attributedString enumerateAttribute:NSFontAttributeName inRange:NSMakeRange(0, string.length) options:0 usingBlock:^(id _Nullable value, NSRange range, BOOL *_Nonnull stop) { UIFont *rangeFont = (UIFont *)value; if ([rangeFont.fontName isEqualToString:@".AppleColorEmojiUI"]) { DDLogVerbose(@"Detected Emoji at location: %lu, for length: %lu", (unsigned long)range.location, (unsigned long)range.length); foundEmoji = YES; *stop = YES; } }]; return foundEmoji; } /** * HACK iOS10EmojiBug see: https://github.com/WhisperSystems/Signal-iOS/issues/1368 * As of iOS10.0 the UIEmoji font doesn't present proper line heights. In some cases this causes the last line in a * message to get cropped off. */ - (CGSize)withiOS10EmojiFixSuperMessageBubbleSizeForMessageData:(id)messageData atIndexPath:(NSIndexPath *)indexPath withLayout:(JSQMessagesCollectionViewFlowLayout *)layout { UIFont *emojiFont = [UIFont fontWithName:@".AppleColorEmojiUI" size:layout.messageBubbleFont.pointSize]; CGSize superSize = [super messageBubbleSizeForMessageData:messageData atIndexPath:indexPath withLayout:layout]; int lines = (int)floor(superSize.height / emojiFont.lineHeight); // Add an extra pixel per line to fit the emoji. // This is a crappy solution. Long messages with only one line of emoji will have an extra pixel per line. return CGSizeMake(superSize.width, superSize.height + (CGFloat)1.5 * lines); } - (id)cacheKeyForMessageData:(id)messageData { OWSAssert(messageData); OWSAssert([messageData conformsToProtocol:@protocol(OWSMessageData)]); OWSAssert(((id)messageData).interaction); OWSAssert(((id)messageData).interaction.uniqueId); return @([messageData messageHash]); } #pragma mark - Logging + (NSString *)tag { return [NSString stringWithFormat:@"[%@]", self.class]; } - (NSString *)tag { return self.class.tag; } @end NS_ASSUME_NONNULL_END