From e77292c2a95f09dac936e40d39b06b3ed5d9851f Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Mon, 16 Oct 2017 08:41:46 -0400 Subject: [PATCH 1/6] Add contentWidth property to ConversationViewCell. // FREEBIE --- .../ConversationView/Cells/ConversationViewCell.h | 8 +++++++- .../ConversationView/Cells/ConversationViewCell.m | 3 ++- .../ConversationView/Cells/OWSContactOffersCell.m | 2 +- .../ConversationView/Cells/OWSMessageCell.m | 10 ++++++---- .../ConversationView/Cells/OWSSystemMessageCell.m | 2 +- .../ConversationView/Cells/OWSUnreadIndicatorCell.m | 2 +- .../ConversationView/ConversationViewController.m | 3 ++- 7 files changed, 20 insertions(+), 10 deletions(-) diff --git a/Signal/src/ViewControllers/ConversationView/Cells/ConversationViewCell.h b/Signal/src/ViewControllers/ConversationView/Cells/ConversationViewCell.h index 20f83a0a61..0986a688cb 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/ConversationViewCell.h +++ b/Signal/src/ViewControllers/ConversationView/Cells/ConversationViewCell.h @@ -54,9 +54,15 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, nullable) ConversationViewItem *viewItem; +// Cells are prefetched but expensive cells (e.g. media) should only load +// when visible and unload when no longer visible. Non-visible cells can +// cache their contents on their ConversationViewItem, but that cache may +// be evacuated before the cell becomes visible again. @property (nonatomic) BOOL isCellVisible; -- (void)loadForDisplay:(int)contentWidth; +@property (nonatomic) int contentWidth; + +- (void)loadForDisplay; - (CGSize)cellSizeForViewWidth:(int)viewWidth contentWidth:(int)contentWidth; diff --git a/Signal/src/ViewControllers/ConversationView/Cells/ConversationViewCell.m b/Signal/src/ViewControllers/ConversationView/Cells/ConversationViewCell.m index a168467bc9..ea9852c0aa 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/ConversationViewCell.m +++ b/Signal/src/ViewControllers/ConversationView/Cells/ConversationViewCell.m @@ -16,9 +16,10 @@ NS_ASSUME_NONNULL_BEGIN self.viewItem = nil; self.delegate = nil; self.isCellVisible = NO; + self.contentWidth = 0; } -- (void)loadForDisplay:(int)contentWidth +- (void)loadForDisplay { OWSFail(@"%@ This method should be overridden.", self.logTag); } diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSContactOffersCell.m b/Signal/src/ViewControllers/ConversationView/Cells/OWSContactOffersCell.m index 5dcd6171e0..dfc21c6f15 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSContactOffersCell.m +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSContactOffersCell.m @@ -87,7 +87,7 @@ NS_ASSUME_NONNULL_BEGIN return NSStringFromClass([self class]); } -- (void)loadForDisplay:(int)contentWidth +- (void)loadForDisplay { OWSAssert(self.viewItem); OWSAssert([self.viewItem.interaction isKindOfClass:[OWSContactOffersInteraction class]]); diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m index 739b55c307..a2fcaa804b 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m @@ -207,7 +207,7 @@ NS_ASSUME_NONNULL_BEGIN return (TSMessage *)self.viewItem.interaction; } -- (void)loadForDisplay:(int)contentWidth +- (void)loadForDisplay { OWSAssert(self.viewItem); OWSAssert(self.viewItem.interaction); @@ -220,7 +220,7 @@ NS_ASSUME_NONNULL_BEGIN = isIncoming ? [self.bubbleFactory incoming] : [self.bubbleFactory outgoing]; self.bubbleImageView.image = bubbleImageData.messageBubbleImage; - [self updateDateHeader:contentWidth]; + [self updateDateHeader]; [self updateFooter]; switch (self.cellType) { @@ -263,8 +263,10 @@ NS_ASSUME_NONNULL_BEGIN // }); } -- (void)updateDateHeader:(int)contentWidth +- (void)updateDateHeader { + OWSAssert(self.contentWidth > 0); + static NSDateFormatter *dateHeaderDateFormatter = nil; static NSDateFormatter *dateHeaderTimeFormatter = nil; static dispatch_once_t onceToken; @@ -312,7 +314,7 @@ NS_ASSUME_NONNULL_BEGIN self.dateHeaderConstraints = @[ // Date headers should be visually centered within the conversation view, // so they need to extend outside the cell's boundaries. - [self.dateHeaderLabel autoSetDimension:ALDimensionWidth toSize:contentWidth], + [self.dateHeaderLabel autoSetDimension:ALDimensionWidth toSize:self.contentWidth], (self.isIncoming ? [self.dateHeaderLabel autoPinEdgeToSuperviewEdge:ALEdgeLeading] : [self.dateHeaderLabel autoPinEdgeToSuperviewEdge:ALEdgeTrailing]), [self.dateHeaderLabel autoPinEdgeToSuperviewEdge:ALEdgeTop], diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSSystemMessageCell.m b/Signal/src/ViewControllers/ConversationView/Cells/OWSSystemMessageCell.m index f0a1cdc68d..9b811758fb 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSSystemMessageCell.m +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSSystemMessageCell.m @@ -85,7 +85,7 @@ NS_ASSUME_NONNULL_BEGIN return NSStringFromClass([self class]); } -- (void)loadForDisplay:(int)contentWidth +- (void)loadForDisplay { OWSAssert(self.viewItem); diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSUnreadIndicatorCell.m b/Signal/src/ViewControllers/ConversationView/Cells/OWSUnreadIndicatorCell.m index b078d29664..e97d32e08b 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSUnreadIndicatorCell.m +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSUnreadIndicatorCell.m @@ -84,7 +84,7 @@ NS_ASSUME_NONNULL_BEGIN return NSStringFromClass([self class]); } -- (void)loadForDisplay:(int)contentWidth +- (void)loadForDisplay { OWSAssert(self.viewItem); OWSAssert([self.viewItem.interaction isKindOfClass:[TSUnreadIndicatorInteraction class]]); diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m index 591a216943..502dbbaec2 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m @@ -4025,8 +4025,9 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) { } cell.viewItem = viewItem; cell.delegate = self; + cell.contentWidth = self.layout.contentWidth; - [cell loadForDisplay:self.layout.contentWidth]; + [cell loadForDisplay]; return cell; } From 65efa7f8366bffd252e51a3369b1a8c80e050f64 Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Mon, 16 Oct 2017 10:39:52 -0400 Subject: [PATCH 2/6] Lazy load, eagerly unload & cache cell media. // FREEBIE --- .../Cells/OWSAudioMessageView.h | 2 +- .../Cells/OWSAudioMessageView.m | 74 ++++---- .../Cells/OWSGenericAttachmentView.h | 2 +- .../Cells/OWSGenericAttachmentView.m | 72 ++++---- .../ConversationView/Cells/OWSMessageCell.m | 170 +++++++++++++----- .../ConversationView/ConversationViewItem.h | 7 + 6 files changed, 202 insertions(+), 125 deletions(-) diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSAudioMessageView.h b/Signal/src/ViewControllers/ConversationView/Cells/OWSAudioMessageView.h index fc7465fbb0..7200788e33 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSAudioMessageView.h +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSAudioMessageView.h @@ -15,7 +15,7 @@ NS_ASSUME_NONNULL_BEGIN isIncoming:(BOOL)isIncoming viewItem:(ConversationViewItem *)viewItem; -- (void)createContentsForSize:(CGSize)viewSize; +- (void)createContents; + (CGFloat)bubbleHeight; diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSAudioMessageView.m b/Signal/src/ViewControllers/ConversationView/Cells/OWSAudioMessageView.m index 11e0aa03ed..d4120221a1 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSAudioMessageView.m +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSAudioMessageView.m @@ -193,34 +193,46 @@ NS_ASSUME_NONNULL_BEGIN return (self.attachmentStream.isVoiceMessage || self.attachmentStream.sourceFilename.length < 1); } -- (void)createContentsForSize:(CGSize)viewSize +- (void)createContents { UIColor *textColor = [self audioTextColor]; self.backgroundColor = self.bubbleBackgroundColor; + self.layoutMargins = UIEdgeInsetsZero; + // TODO: Verify that this layout works in RTL. const CGFloat kBubbleTailWidth = 6.f; - CGRect contentFrame = CGRectMake(self.isIncoming ? kBubbleTailWidth : 0.f, - self.audioIconVMargin, - viewSize.width - kBubbleTailWidth - self.audioIconHMargin, - viewSize.height - self.audioIconVMargin * 2); - CGRect iconFrame = CGRectMake((CGFloat)round(contentFrame.origin.x + self.audioIconHMargin), - (CGFloat)round(contentFrame.origin.y + (contentFrame.size.height - self.iconSize) * 0.5f), - self.iconSize, - self.iconSize); - _audioPlayPauseButton = [[UIButton alloc] initWithFrame:iconFrame]; - _audioPlayPauseButton.enabled = NO; - [self addSubview:_audioPlayPauseButton]; + UIView *contentView = [UIView containerView]; + [self addSubview:contentView]; + [contentView autoPinLeadingToSuperviewWithMargin:self.isIncoming ? kBubbleTailWidth : 0.f]; + [contentView autoPinTrailingToSuperviewWithMargin:self.isIncoming ? 0.f : kBubbleTailWidth]; + [contentView autoPinEdgeToSuperviewEdge:ALEdgeTop withInset:self.audioIconVMargin]; + [contentView autoPinEdgeToSuperviewEdge:ALEdgeBottom withInset:self.audioIconVMargin]; + + _audioPlayPauseButton = [UIButton buttonWithType:UIButtonTypeCustom]; + self.audioPlayPauseButton.enabled = NO; + [contentView addSubview:self.audioPlayPauseButton]; + [self.audioPlayPauseButton autoPinEdgeToSuperviewEdge:ALEdgeLeft withInset:self.audioIconHMargin]; + [self.audioPlayPauseButton autoVCenterInSuperview]; + [self.audioPlayPauseButton autoSetDimension:ALDimensionWidth toSize:self.iconSize]; + [self.audioPlayPauseButton autoSetDimension:ALDimensionHeight toSize:self.iconSize]; const CGFloat kLabelHSpacing = self.audioIconHSpacing; + + UIView *labelsView = [UIView containerView]; + [contentView addSubview:labelsView]; + [labelsView autoPinLeadingToTrailingOfView:self.audioPlayPauseButton margin:kLabelHSpacing]; + [labelsView autoPinEdgeToSuperviewEdge:ALEdgeRight]; + [labelsView autoVCenterInSuperview]; + const CGFloat kLabelVSpacing = 2; NSString *filename = self.attachmentStream.sourceFilename; if (!filename) { filename = [[self.attachmentStream filePath] lastPathComponent]; } NSString *topText = [[filename stringByDeletingPathExtension] - stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; + stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; if (topText.length < 1) { topText = [MIMETypeUtil fileExtensionForMIMEType:self.attachmentStream.contentType].uppercaseString; } @@ -235,13 +247,19 @@ NS_ASSUME_NONNULL_BEGIN topLabel.textColor = [textColor colorWithAlphaComponent:0.85f]; topLabel.lineBreakMode = NSLineBreakByTruncatingMiddle; topLabel.font = [UIFont ows_regularFontWithSize:ScaleFromIPhone5To7Plus(11.f, 13.f)]; - [topLabel sizeToFit]; - [self addSubview:topLabel]; + topLabel.textAlignment = NSTextAlignmentLeft; + [labelsView addSubview:topLabel]; + [topLabel autoPinEdgeToSuperviewEdge:ALEdgeTop]; + [topLabel autoPinWidthToSuperview]; + const CGFloat kAudioProgressViewHeight = 12.f; AudioProgressView *audioProgressView = [AudioProgressView new]; self.audioProgressView = audioProgressView; [self updateAudioProgressView]; - [self addSubview:audioProgressView]; + [labelsView addSubview:audioProgressView]; + [audioProgressView autoPinWidthToSuperview]; + [audioProgressView autoSetDimension:ALDimensionHeight toSize:kAudioProgressViewHeight]; + [audioProgressView autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:topLabel withOffset:kLabelVSpacing]; UILabel *bottomLabel = [UILabel new]; self.audioBottomLabel = bottomLabel; @@ -249,25 +267,11 @@ NS_ASSUME_NONNULL_BEGIN bottomLabel.textColor = [textColor colorWithAlphaComponent:0.85f]; bottomLabel.lineBreakMode = NSLineBreakByTruncatingMiddle; bottomLabel.font = [UIFont ows_regularFontWithSize:ScaleFromIPhone5To7Plus(11.f, 13.f)]; - [bottomLabel sizeToFit]; - [self addSubview:bottomLabel]; - - const CGFloat topLabelHeight = (CGFloat)ceil(topLabel.font.lineHeight); - const CGFloat kAudioProgressViewHeight = 12.f; - const CGFloat bottomLabelHeight = (CGFloat)ceil(bottomLabel.font.lineHeight); - CGRect labelsBounds = CGRectZero; - labelsBounds.origin.x = (CGFloat)round(iconFrame.origin.x + iconFrame.size.width + kLabelHSpacing); - labelsBounds.size.width = contentFrame.origin.x + contentFrame.size.width - labelsBounds.origin.x; - labelsBounds.size.height = topLabelHeight + kAudioProgressViewHeight + bottomLabelHeight + kLabelVSpacing * 2; - labelsBounds.origin.y - = (CGFloat)round(contentFrame.origin.y + (contentFrame.size.height - labelsBounds.size.height) * 0.5f); - - CGFloat y = labelsBounds.origin.y; - topLabel.frame = CGRectMake(labelsBounds.origin.x, labelsBounds.origin.y, labelsBounds.size.width, topLabelHeight); - y += topLabelHeight + kLabelVSpacing; - audioProgressView.frame = CGRectMake(labelsBounds.origin.x, y, labelsBounds.size.width, kAudioProgressViewHeight); - y += kAudioProgressViewHeight + kLabelVSpacing; - bottomLabel.frame = CGRectMake(labelsBounds.origin.x, y, labelsBounds.size.width, bottomLabelHeight); + bottomLabel.textAlignment = NSTextAlignmentLeft; + [labelsView addSubview:bottomLabel]; + [bottomLabel autoPinWidthToSuperview]; + [bottomLabel autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:audioProgressView withOffset:kLabelVSpacing]; + [bottomLabel autoPinEdgeToSuperviewEdge:ALEdgeBottom]; [self updateContents]; } diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSGenericAttachmentView.h b/Signal/src/ViewControllers/ConversationView/Cells/OWSGenericAttachmentView.h index f3b0957099..73b9bcaba0 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSGenericAttachmentView.h +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSGenericAttachmentView.h @@ -10,7 +10,7 @@ NS_ASSUME_NONNULL_BEGIN - (instancetype)initWithAttachment:(TSAttachmentStream *)attachmentStream isIncoming:(BOOL)isIncoming; -- (void)createContentsForSize:(CGSize)viewSize; +- (void)createContents; + (CGFloat)bubbleHeight; diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSGenericAttachmentView.m b/Signal/src/ViewControllers/ConversationView/Cells/OWSGenericAttachmentView.m index f6831a6fac..97544e6b29 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSGenericAttachmentView.m +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSGenericAttachmentView.m @@ -98,32 +98,43 @@ NS_ASSUME_NONNULL_BEGIN return [self.textColor blendWithColor:self.bubbleBackgroundColor alpha:alpha]; } -- (void)createContentsForSize:(CGSize)viewSize +- (void)createContents { UIColor *textColor = (self.isIncoming ? [UIColor colorWithWhite:0.2 alpha:1.f] : [UIColor whiteColor]); self.backgroundColor = self.bubbleBackgroundColor; + self.layoutMargins = UIEdgeInsetsZero; + // TODO: Verify that this layout works in RTL. const CGFloat kBubbleTailWidth = 6.f; - CGRect contentFrame = CGRectMake(self.isIncoming ? kBubbleTailWidth : 0.f, - self.vMargin, - viewSize.width - kBubbleTailWidth - self.iconHMargin, - viewSize.height - self.vMargin * 2); + + UIView *contentView = [UIView containerView]; + [self addSubview:contentView]; + [contentView autoPinLeadingToSuperviewWithMargin:self.isIncoming ? kBubbleTailWidth : 0.f]; + [contentView autoPinTrailingToSuperviewWithMargin:self.isIncoming ? 0.f : kBubbleTailWidth]; + [contentView autoPinEdgeToSuperviewEdge:ALEdgeTop withInset:self.vMargin]; + [contentView autoPinEdgeToSuperviewEdge:ALEdgeBottom withInset:self.vMargin]; UIImage *image = [UIImage imageNamed:@"generic-attachment-small"]; OWSAssert(image); UIImageView *imageView = [UIImageView new]; - CGRect iconFrame = CGRectMake(round(contentFrame.origin.x + self.iconHMargin), - round(contentFrame.origin.y + (contentFrame.size.height - self.iconSize) * 0.5f), - self.iconSize, - self.iconSize); - imageView.frame = iconFrame; imageView.image = [image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; imageView.tintColor = self.bubbleBackgroundColor; imageView.backgroundColor = (self.isIncoming ? [UIColor colorWithRGBHex:0x9e9e9e] : [self foregroundColorWithOpacity:0.15f]); imageView.layer.cornerRadius = MIN(imageView.bounds.size.width, imageView.bounds.size.height) * 0.5f; - [self addSubview:imageView]; + [contentView addSubview:imageView]; + [imageView autoPinEdgeToSuperviewEdge:ALEdgeLeft withInset:self.iconHMargin]; + [imageView autoVCenterInSuperview]; + [imageView autoSetDimension:ALDimensionWidth toSize:self.iconSize]; + [imageView autoSetDimension:ALDimensionHeight toSize:self.iconSize]; + + const CGFloat kLabelHSpacing = self.iconHSpacing; + UIView *labelsView = [UIView containerView]; + [contentView addSubview:labelsView]; + [labelsView autoPinLeadingToTrailingOfView:imageView margin:kLabelHSpacing]; + [labelsView autoPinEdgeToSuperviewEdge:ALEdgeRight]; + [labelsView autoVCenterInSuperview]; NSString *filename = self.attachmentStream.sourceFilename; if (!filename) { @@ -145,19 +156,11 @@ NS_ASSUME_NONNULL_BEGIN fileTypeLabel.font = [UIFont ows_mediumFontWithSize:20.f]; fileTypeLabel.adjustsFontSizeToFitWidth = YES; fileTypeLabel.textAlignment = NSTextAlignmentCenter; - CGRect fileTypeLabelFrame = CGRectZero; - fileTypeLabelFrame.size = [fileTypeLabel sizeThatFits:CGSizeZero]; - // This dimension depends on the space within the icon boundaries. - fileTypeLabelFrame.size.width = 15.f; // Center on icon. - fileTypeLabelFrame.origin.x - = round(iconFrame.origin.x + (iconFrame.size.width - fileTypeLabelFrame.size.width) * 0.5f); - fileTypeLabelFrame.origin.y - = round(iconFrame.origin.y + (iconFrame.size.height - fileTypeLabelFrame.size.height) * 0.5f); - fileTypeLabel.frame = fileTypeLabelFrame; - [self addSubview:fileTypeLabel]; + [imageView addSubview:fileTypeLabel]; + [imageView autoCenterInSuperview]; + [imageView autoSetDimension:ALDimensionWidth toSize:15.f]; - const CGFloat kLabelHSpacing = self.iconHSpacing; const CGFloat kLabelVSpacing = 2; NSString *topText = [self.attachmentStream.sourceFilename stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; @@ -172,8 +175,10 @@ NS_ASSUME_NONNULL_BEGIN topLabel.textColor = textColor; topLabel.lineBreakMode = NSLineBreakByTruncatingMiddle; topLabel.font = [UIFont ows_regularFontWithSize:ScaleFromIPhone5To7Plus(13.f, 15.f)]; - [topLabel sizeToFit]; - [self addSubview:topLabel]; + topLabel.textAlignment = NSTextAlignmentLeft; + [labelsView addSubview:topLabel]; + [topLabel autoPinEdgeToSuperviewEdge:ALEdgeTop]; + [topLabel autoPinWidthToSuperview]; NSError *error; unsigned long long fileSize = @@ -185,21 +190,10 @@ NS_ASSUME_NONNULL_BEGIN bottomLabel.textColor = [textColor colorWithAlphaComponent:0.85f]; bottomLabel.lineBreakMode = NSLineBreakByTruncatingMiddle; bottomLabel.font = [UIFont ows_regularFontWithSize:ScaleFromIPhone5To7Plus(11.f, 13.f)]; - [bottomLabel sizeToFit]; - [self addSubview:bottomLabel]; - - CGRect topLabelFrame = CGRectZero; - topLabelFrame.size = topLabel.bounds.size; - topLabelFrame.origin.x = round(iconFrame.origin.x + iconFrame.size.width + kLabelHSpacing); - topLabelFrame.origin.y = round(contentFrame.origin.y - + (contentFrame.size.height - (topLabel.frame.size.height + bottomLabel.frame.size.height + kLabelVSpacing)) - * 0.5f); - topLabelFrame.size.width = round((contentFrame.origin.x + contentFrame.size.width) - topLabelFrame.origin.x); - topLabel.frame = topLabelFrame; - - CGRect bottomLabelFrame = topLabelFrame; - bottomLabelFrame.origin.y += topLabelFrame.size.height + kLabelVSpacing; - bottomLabel.frame = bottomLabelFrame; + [labelsView addSubview:bottomLabel]; + [bottomLabel autoPinWidthToSuperview]; + [bottomLabel autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:topLabel withOffset:kLabelVSpacing]; + [bottomLabel autoPinEdgeToSuperviewEdge:ALEdgeBottom]; } @end diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m index a2fcaa804b..001ca4789c 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m @@ -243,7 +243,7 @@ NS_ASSUME_NONNULL_BEGIN case OWSMessageCellType_GenericAttachment: { self.attachmentView = [[OWSGenericAttachmentView alloc] initWithAttachment:self.attachmentStream isIncoming:self.isIncoming]; - [self.attachmentView createContentsForSize:self.bounds.size]; + [self.attachmentView createContents]; [self replaceBubbleWithView:self.attachmentView]; [self addAttachmentUploadViewIfNecessary:self.attachmentView]; break; @@ -254,6 +254,8 @@ NS_ASSUME_NONNULL_BEGIN } } + [self ensureViewMediaState]; + // dispatch_async(dispatch_get_main_queue(), ^{ // NSLog(@"---- %@", self.viewItem.interaction.debugDescription); // NSLog(@"cell: %@", NSStringFromCGRect(self.frame)); @@ -263,6 +265,116 @@ NS_ASSUME_NONNULL_BEGIN // }); } +- (nullable id)tryToLoadCellMedia:(nullable id (^)())loadCellMediaBlock mediaView:(UIView *)mediaView +{ + OWSAssert(self.attachmentStream); + OWSAssert(mediaView); + + if (self.viewItem.didCellMediaFailToLoad) { + return nil; + } + id _Nullable cellMedia = self.viewItem.cachedCellMedia; + if (cellMedia) { + DDLogVerbose(@"%@ cell media cache hit", self.logTag); + return cellMedia; + } + cellMedia = loadCellMediaBlock(); + if (cellMedia) { + DDLogVerbose(@"%@ cell media cache miss", self.logTag); + self.viewItem.cachedCellMedia = cellMedia; + } else { + DDLogError(@"%@ Failed to load cell media: %@", [self logTag], [self.attachmentStream mediaURL]); + self.viewItem.didCellMediaFailToLoad = YES; + [mediaView removeFromSuperview]; + // TODO: We need to hide/remove the media view. + [self showAttachmentErrorView]; + } + return cellMedia; +} + +// We want to lazy-load expensive view contents and eagerly unload if the +// cell is no longer visible. +- (void)ensureViewMediaState +{ + if (!self.isCellVisible) { + // Eagerly unload. + if (self.stillImageView.image || self.animatedImageView.image) { + DDLogError(@"%@ ---- ensureViewMediaState unloading[%zd]: %@", + self.logTag, + self.viewItem.row, + self.viewItem.interaction.description); + } + self.stillImageView.image = nil; + self.animatedImageView.image = nil; + return; + } + + switch (self.cellType) { + case OWSMessageCellType_StillImage: { + if (self.stillImageView.image) { + return; + } + DDLogError(@"%@ ---- ensureViewMediaState loading[%zd]: %@", + self.logTag, + self.viewItem.row, + self.viewItem.interaction.description); + self.stillImageView.image = [self tryToLoadCellMedia:^{ + OWSAssert([self.attachmentStream isImage]); + return self.attachmentStream.image; + } + mediaView:self.stillImageView]; + break; + } + case OWSMessageCellType_AnimatedImage: { + if (self.animatedImageView.image) { + return; + } + DDLogError(@"%@ ---- ensureViewMediaState loading[%zd]: %@", + self.logTag, + self.viewItem.row, + self.viewItem.interaction.description); + self.animatedImageView.image = [self tryToLoadCellMedia:^{ + OWSAssert([self.attachmentStream isAnimated]); + + NSString *_Nullable filePath = [self.attachmentStream filePath]; + YYImage *_Nullable animatedImage = nil; + if (filePath && [NSData ows_isValidImageAtPath:filePath]) { + animatedImage = [YYImage imageWithContentsOfFile:filePath]; + } + return animatedImage; + } + mediaView:self.animatedImageView]; + break; + } + case OWSMessageCellType_Audio: + // TODO: Lazy load audio length in audio cells. + // [self loadForAudioDisplay]; + break; + case OWSMessageCellType_Video: { + if (self.stillImageView.image) { + return; + } + DDLogError(@"%@ ---- ensureViewMediaState loading[%zd]: %@", + self.logTag, + self.viewItem.row, + self.viewItem.interaction.description); + self.stillImageView.image = [self tryToLoadCellMedia:^{ + OWSAssert([self.attachmentStream isVideo]); + + return self.attachmentStream.image; + } + mediaView:self.stillImageView]; + break; + } + case OWSMessageCellType_TextMessage: + case OWSMessageCellType_OversizeTextMessage: + case OWSMessageCellType_GenericAttachment: + case OWSMessageCellType_DownloadingAttachment: + // Inexpensive cell types don't need to lazy-load or eagerly-unload. + break; + } +} + - (void)updateDateHeader { OWSAssert(self.contentWidth > 0); @@ -454,14 +566,7 @@ NS_ASSUME_NONNULL_BEGIN OWSAssert(self.attachmentStream); OWSAssert([self.attachmentStream isImage]); - UIImage *_Nullable image = self.attachmentStream.image; - if (!image) { - DDLogError(@"%@ Could not load image: %@", [self logTag], [self.attachmentStream mediaURL]); - [self showAttachmentErrorView]; - return; - } - - self.stillImageView = [[UIImageView alloc] initWithImage:image]; + self.stillImageView = [UIImageView new]; // We need to specify a contentMode since the size of the image // might not match the aspect ratio of the view. self.stillImageView.contentMode = UIViewContentModeScaleAspectFill; @@ -478,19 +583,7 @@ NS_ASSUME_NONNULL_BEGIN OWSAssert(self.attachmentStream); OWSAssert([self.attachmentStream isAnimated]); - NSString *_Nullable filePath = [self.attachmentStream filePath]; - YYImage *_Nullable animatedImage = nil; - if (filePath && [NSData ows_isValidImageAtPath:filePath]) { - animatedImage = [YYImage imageWithContentsOfFile:filePath]; - } - if (!animatedImage) { - DDLogError(@"%@ Could not load animated image: %@", [self logTag], [self.attachmentStream mediaURL]); - [self showAttachmentErrorView]; - return; - } - self.animatedImageView = [[YYAnimatedImageView alloc] init]; - self.animatedImageView.image = animatedImage; // We need to specify a contentMode since the size of the image // might not match the aspect ratio of the view. self.animatedImageView.contentMode = UIViewContentModeScaleAspectFill; @@ -507,7 +600,7 @@ NS_ASSUME_NONNULL_BEGIN isIncoming:self.isIncoming viewItem:self.viewItem]; self.viewItem.lastAudioMessageView = self.audioMessageView; - [self.audioMessageView createContentsForSize:self.bounds.size]; + [self.audioMessageView createContents]; [self replaceBubbleWithView:self.audioMessageView]; [self addAttachmentUploadViewIfNecessary:self.audioMessageView]; } @@ -517,16 +610,7 @@ NS_ASSUME_NONNULL_BEGIN OWSAssert(self.attachmentStream); OWSAssert([self.attachmentStream isVideo]); - // CGSize size = [self mediaViewDisplaySize]; - - UIImage *_Nullable image = self.attachmentStream.image; - if (!image) { - DDLogError(@"%@ Could not load image: %@", [self logTag], [self.attachmentStream mediaURL]); - [self showAttachmentErrorView]; - return; - } - - self.stillImageView = [[UIImageView alloc] initWithImage:image]; + self.stillImageView = [UIImageView new]; // We need to specify a contentMode since the size of the image // might not match the aspect ratio of the view. self.stillImageView.contentMode = UIViewContentModeScaleAspectFill; @@ -616,25 +700,11 @@ NS_ASSUME_NONNULL_BEGIN [self.payloadView updateMask]; } -//// TODO: -//- (void)setFrame:(CGRect)frame { -// [super setFrame:frame]; -// -// DDLogError(@"setFrame: %@ %@ %@", self.viewItem.interaction.uniqueId, self.viewItem.interaction.description, -// NSStringFromCGRect(frame)); -//} -// -//// TODO: -//- (void)setBounds:(CGRect)bounds { -// [super setBounds:bounds]; -// -// DDLogError(@"setBounds: %@ %@ %@", self.viewItem.interaction.uniqueId, self.viewItem.interaction.description, -// NSStringFromCGRect(bounds)); -//} - - (void)showAttachmentErrorView { - // TODO: We could do a better job of indicating that the image could not be loaded. + OWSAssert(!self.customView); + + // TODO: We could do a better job of indicating that the media could not be loaded. self.customView = [UIView new]; self.customView.backgroundColor = [UIColor colorWithWhite:0.85f alpha:1.f]; self.customView.userInteractionEnabled = NO; @@ -862,6 +932,8 @@ NS_ASSUME_NONNULL_BEGIN return; } + [self ensureViewMediaState]; + if (isCellVisible) { if (self.message.shouldStartExpireTimer) { [self.expirationTimerView ensureAnimations]; diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewItem.h b/Signal/src/ViewControllers/ConversationView/ConversationViewItem.h index e48a1f12fd..1827a7a6db 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewItem.h +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewItem.h @@ -86,6 +86,13 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType); - (nullable TSAttachmentPointer *)attachmentPointer; - (CGSize)contentSize; +// A generic property that cells can use to cache their loaded +// media. This cache is volatile and will get evacuated based +// on scroll state, so that we only retain state for a sliding +// window of cells that are almost on-screen. +@property (nonatomic) id cachedCellMedia; +@property (nonatomic) BOOL didCellMediaFailToLoad; + // TODO: //// Cells will request that this adapter clear its cached media views, //// but the adapter should only honor requests from the last cell to From 257f8249bf9458707be2d509ff00fe8d77aae7e7 Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Mon, 16 Oct 2017 11:01:13 -0400 Subject: [PATCH 3/6] Cull cached cell media outside a load window. // FREEBIE --- .../ConversationViewController.m | 70 ++++++++++++++++++- .../ConversationView/ConversationViewItem.h | 2 +- 2 files changed, 70 insertions(+), 2 deletions(-) diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m index 502dbbaec2..81f1af6de5 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m @@ -165,7 +165,8 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) { @property (nonatomic, nullable) OWSAudioAttachmentPlayer *audioAttachmentPlayer; @property (nonatomic, nullable) NSUUID *voiceMessageUUID; -@property (nonatomic) NSTimer *readTimer; +@property (nonatomic, nullable) NSTimer *readTimer; +@property (nonatomic, nullable) NSTimer *cellMediaCachesTimer; @property (nonatomic) ConversationHeaderView *navigationBarTitleView; @property (nonatomic) UILabel *navigationBarTitleLabel; @property (nonatomic) UILabel *navigationBarSubtitleLabel; @@ -526,6 +527,7 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) { self.hasClearedUnreadMessagesIndicator = NO; [self.dynamicInteractions clearUnreadIndicatorState]; } + [self clearCellMediaCaches]; } - (void)applicationWillResignActive:(NSNotification *)notification @@ -943,6 +945,69 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) { - (void)cancelReadTimer { [self.readTimer invalidate]; + self.readTimer = nil; +} + +- (void)startCellMediaCachesTimerIfNecessary +{ + if (self.cellMediaCachesTimer) { + return; + } + self.cellMediaCachesTimer = [NSTimer weakScheduledTimerWithTimeInterval:3.f + target:self + selector:@selector(auditCellMediaCaches) + userInfo:nil + repeats:NO]; +} + +- (void)auditCellMediaCaches +{ + [self.cellMediaCachesTimer invalidate]; + self.cellMediaCachesTimer = nil; + + NSIndexPath *_Nullable firstVisibleIndexPath = nil; + NSIndexPath *_Nullable lastVisibleIndexPath = nil; + + for (NSIndexPath *indexPath in self.collectionView.indexPathsForVisibleItems) { + if (!firstVisibleIndexPath || firstVisibleIndexPath.row > indexPath.row) { + firstVisibleIndexPath = indexPath; + } + if (!lastVisibleIndexPath || lastVisibleIndexPath.row < indexPath.row) { + lastVisibleIndexPath = indexPath; + } + } + + if (!firstVisibleIndexPath || !lastVisibleIndexPath) { + [self clearCellMediaCaches]; + return; + } + + // Only retain the cached cell media for N cells at a time. + const NSInteger kCellMediaCacheWindowSize = 24; + NSInteger centerRow = (firstVisibleIndexPath.row + lastVisibleIndexPath.row) / 2; + // Determine the first and law rows (inclusive) of the cached cell media load window. + // Note these row values may not correspond to actual rows in the collection view. + // + // Always retain the cached cell media for any visible cells. + NSInteger firstLoadWindowRow = MIN(firstVisibleIndexPath.row, centerRow - kCellMediaCacheWindowSize / 2); + NSInteger lastLoadWindowRow = MAX(lastVisibleIndexPath.row, centerRow + kCellMediaCacheWindowSize / 2); + + // Cull cached cell media outside the load window. + for (ConversationViewItem *viewItem in self.viewItems) { + if (viewItem.row < firstLoadWindowRow || viewItem.row > lastLoadWindowRow) { + viewItem.cachedCellMedia = nil; + } + } +} + +- (void)clearCellMediaCaches +{ + [self.cellMediaCachesTimer invalidate]; + self.cellMediaCachesTimer = nil; + + for (ConversationViewItem *viewItem in self.viewItems) { + viewItem.cachedCellMedia = nil; + } } - (void)viewDidAppear:(BOOL)animated @@ -986,6 +1051,7 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) { [self saveDraft]; [self markVisibleMessagesAsRead]; [self cancelVoiceMemo]; + [self clearCellMediaCaches]; self.isUserScrolling = NO; } @@ -3531,6 +3597,8 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) { - (void)scrollViewDidScroll:(UIScrollView *)scrollView { [self updateLastVisibleTimestamp]; + + [self startCellMediaCachesTimerIfNecessary]; } - (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewItem.h b/Signal/src/ViewControllers/ConversationView/ConversationViewItem.h index 1827a7a6db..75212d1d86 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewItem.h +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewItem.h @@ -90,7 +90,7 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType); // media. This cache is volatile and will get evacuated based // on scroll state, so that we only retain state for a sliding // window of cells that are almost on-screen. -@property (nonatomic) id cachedCellMedia; +@property (nonatomic, nullable) id cachedCellMedia; @property (nonatomic) BOOL didCellMediaFailToLoad; // TODO: From 88c874e4e661d87aea6df125749e3ddbebf7e9b7 Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Wed, 18 Oct 2017 10:26:54 -0700 Subject: [PATCH 4/6] Clean up ahead of PR. // FREEBIE --- .../Cells/ConversationViewCell.h | 1 + .../ConversationView/Cells/OWSMessageCell.m | 19 +++++-------------- 2 files changed, 6 insertions(+), 14 deletions(-) diff --git a/Signal/src/ViewControllers/ConversationView/Cells/ConversationViewCell.h b/Signal/src/ViewControllers/ConversationView/Cells/ConversationViewCell.h index 0986a688cb..df6695f70c 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/ConversationViewCell.h +++ b/Signal/src/ViewControllers/ConversationView/Cells/ConversationViewCell.h @@ -60,6 +60,7 @@ NS_ASSUME_NONNULL_BEGIN // be evacuated before the cell becomes visible again. @property (nonatomic) BOOL isCellVisible; +// The width of the collection view. @property (nonatomic) int contentWidth; - (void)loadForDisplay; diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m index 001ca4789c..95f62c4787 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m @@ -265,6 +265,10 @@ NS_ASSUME_NONNULL_BEGIN // }); } +// We now eagerly create out view hierarchy (to do this exactly once per cell usage) +// but lazy-load any expensive media (photo, gif, etc.) used in those views. Note that +// this lazy-load can fail, in which case we modify the view hierarchy to use an "error" +// state. The didCellMediaFailToLoad reflects media load fails. - (nullable id)tryToLoadCellMedia:(nullable id (^)())loadCellMediaBlock mediaView:(UIView *)mediaView { OWSAssert(self.attachmentStream); @@ -298,12 +302,6 @@ NS_ASSUME_NONNULL_BEGIN { if (!self.isCellVisible) { // Eagerly unload. - if (self.stillImageView.image || self.animatedImageView.image) { - DDLogError(@"%@ ---- ensureViewMediaState unloading[%zd]: %@", - self.logTag, - self.viewItem.row, - self.viewItem.interaction.description); - } self.stillImageView.image = nil; self.animatedImageView.image = nil; return; @@ -314,10 +312,6 @@ NS_ASSUME_NONNULL_BEGIN if (self.stillImageView.image) { return; } - DDLogError(@"%@ ---- ensureViewMediaState loading[%zd]: %@", - self.logTag, - self.viewItem.row, - self.viewItem.interaction.description); self.stillImageView.image = [self tryToLoadCellMedia:^{ OWSAssert([self.attachmentStream isImage]); return self.attachmentStream.image; @@ -346,10 +340,6 @@ NS_ASSUME_NONNULL_BEGIN mediaView:self.animatedImageView]; break; } - case OWSMessageCellType_Audio: - // TODO: Lazy load audio length in audio cells. - // [self loadForAudioDisplay]; - break; case OWSMessageCellType_Video: { if (self.stillImageView.image) { return; @@ -370,6 +360,7 @@ NS_ASSUME_NONNULL_BEGIN case OWSMessageCellType_OversizeTextMessage: case OWSMessageCellType_GenericAttachment: case OWSMessageCellType_DownloadingAttachment: + case OWSMessageCellType_Audio: // Inexpensive cell types don't need to lazy-load or eagerly-unload. break; } From 3b945a9da28b982351ec59704f3152ae8f932ea9 Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Wed, 18 Oct 2017 11:53:31 -0700 Subject: [PATCH 5/6] Respond to CR. // FREEBIE --- Signal/src/ViewControllers/ContactsViewHelper.m | 3 ++- .../ConversationView/Cells/ConversationViewCell.h | 6 ++++++ .../ConversationView/Cells/OWSAudioMessageView.m | 5 +---- .../ConversationView/Cells/OWSGenericAttachmentView.m | 5 ++--- .../ConversationView/ConversationInputTextView.m | 3 ++- .../ConversationView/ConversationViewController.m | 5 +++-- .../ConversationView/ConversationViewItem.m | 9 ++++----- Signal/src/ViewControllers/CountryCodeViewController.m | 4 ++-- Signal/src/ViewControllers/NewGroupViewController.m | 4 ++-- Signal/src/ViewControllers/ProfileViewController.m | 3 ++- Signal/src/ViewControllers/RegistrationViewController.m | 4 ++-- Signal/src/ViewControllers/SelectThreadViewController.m | 4 ++-- .../src/ViewControllers/SendExternalFileViewController.m | 4 ++-- Signal/src/ViewControllers/UpdateGroupViewController.m | 7 +++---- Signal/src/contact/OWSContactsSearcher.m | 3 ++- Signal/src/environment/NotificationsManager.m | 4 ++-- 16 files changed, 39 insertions(+), 34 deletions(-) diff --git a/Signal/src/ViewControllers/ContactsViewHelper.m b/Signal/src/ViewControllers/ContactsViewHelper.m index cfdc92ba5e..2d58f82bb7 100644 --- a/Signal/src/ViewControllers/ContactsViewHelper.m +++ b/Signal/src/ViewControllers/ContactsViewHelper.m @@ -5,6 +5,7 @@ #import "ContactsViewHelper.h" #import "ContactTableViewCell.h" #import "Environment.h" +#import "NSString+OWS.h" #import "OWSProfileManager.h" #import "Signal-Swift.h" #import @@ -205,7 +206,7 @@ NS_ASSUME_NONNULL_BEGIN - (NSArray *)searchTermsForSearchString:(NSString *)searchText { - return [[[searchText stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]] + return [[[searchText ows_stripped] componentsSeparatedByCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]] filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(NSString *_Nullable searchTerm, NSDictionary *_Nullable bindings) { diff --git a/Signal/src/ViewControllers/ConversationView/Cells/ConversationViewCell.h b/Signal/src/ViewControllers/ConversationView/Cells/ConversationViewCell.h index df6695f70c..6bf29fc59f 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/ConversationViewCell.h +++ b/Signal/src/ViewControllers/ConversationView/Cells/ConversationViewCell.h @@ -58,6 +58,12 @@ NS_ASSUME_NONNULL_BEGIN // when visible and unload when no longer visible. Non-visible cells can // cache their contents on their ConversationViewItem, but that cache may // be evacuated before the cell becomes visible again. +// +// ConversationViewController also uses this property to evacuate the cell's +// meda views when: +// +// * App enters background. +// * Users enters another view (e.g. conversation settings view, call screen, etc.). @property (nonatomic) BOOL isCellVisible; // The width of the collection view. diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSAudioMessageView.m b/Signal/src/ViewControllers/ConversationView/Cells/OWSAudioMessageView.m index d4120221a1..04a2f1e0a5 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSAudioMessageView.m +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSAudioMessageView.m @@ -231,8 +231,7 @@ NS_ASSUME_NONNULL_BEGIN if (!filename) { filename = [[self.attachmentStream filePath] lastPathComponent]; } - NSString *topText = [[filename stringByDeletingPathExtension] - stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; + NSString *topText = [[filename stringByDeletingPathExtension] ows_stripped]; if (topText.length < 1) { topText = [MIMETypeUtil fileExtensionForMIMEType:self.attachmentStream.contentType].uppercaseString; } @@ -247,7 +246,6 @@ NS_ASSUME_NONNULL_BEGIN topLabel.textColor = [textColor colorWithAlphaComponent:0.85f]; topLabel.lineBreakMode = NSLineBreakByTruncatingMiddle; topLabel.font = [UIFont ows_regularFontWithSize:ScaleFromIPhone5To7Plus(11.f, 13.f)]; - topLabel.textAlignment = NSTextAlignmentLeft; [labelsView addSubview:topLabel]; [topLabel autoPinEdgeToSuperviewEdge:ALEdgeTop]; [topLabel autoPinWidthToSuperview]; @@ -267,7 +265,6 @@ NS_ASSUME_NONNULL_BEGIN bottomLabel.textColor = [textColor colorWithAlphaComponent:0.85f]; bottomLabel.lineBreakMode = NSLineBreakByTruncatingMiddle; bottomLabel.font = [UIFont ows_regularFontWithSize:ScaleFromIPhone5To7Plus(11.f, 13.f)]; - bottomLabel.textAlignment = NSTextAlignmentLeft; [labelsView addSubview:bottomLabel]; [bottomLabel autoPinWidthToSuperview]; [bottomLabel autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:audioProgressView withOffset:kLabelVSpacing]; diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSGenericAttachmentView.m b/Signal/src/ViewControllers/ConversationView/Cells/OWSGenericAttachmentView.m index 97544e6b29..2c0d869108 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSGenericAttachmentView.m +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSGenericAttachmentView.m @@ -3,6 +3,7 @@ // #import "OWSGenericAttachmentView.h" +#import "NSString+OWS.h" #import "UIColor+JSQMessages.h" #import "UIColor+OWS.h" #import "UIFont+OWS.h" @@ -162,8 +163,7 @@ NS_ASSUME_NONNULL_BEGIN [imageView autoSetDimension:ALDimensionWidth toSize:15.f]; const CGFloat kLabelVSpacing = 2; - NSString *topText = - [self.attachmentStream.sourceFilename stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; + NSString *topText = [self.attachmentStream.sourceFilename ows_stripped]; if (topText.length < 1) { topText = [MIMETypeUtil fileExtensionForMIMEType:self.attachmentStream.contentType].uppercaseString; } @@ -175,7 +175,6 @@ NS_ASSUME_NONNULL_BEGIN topLabel.textColor = textColor; topLabel.lineBreakMode = NSLineBreakByTruncatingMiddle; topLabel.font = [UIFont ows_regularFontWithSize:ScaleFromIPhone5To7Plus(13.f, 15.f)]; - topLabel.textAlignment = NSTextAlignmentLeft; [labelsView addSubview:topLabel]; [topLabel autoPinEdgeToSuperviewEdge:ALEdgeTop]; [topLabel autoPinWidthToSuperview]; diff --git a/Signal/src/ViewControllers/ConversationView/ConversationInputTextView.m b/Signal/src/ViewControllers/ConversationView/ConversationInputTextView.m index 10be79193c..535ac1fbb5 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationInputTextView.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationInputTextView.m @@ -3,6 +3,7 @@ // #import "ConversationInputTextView.h" +#import "NSString+OWS.h" #import "Signal-Swift.h" NS_ASSUME_NONNULL_BEGIN @@ -121,7 +122,7 @@ NS_ASSUME_NONNULL_BEGIN - (NSString *)trimmedText { - return [self.text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; + return [self.text ows_stripped]; } // TODO: diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m index 81f1af6de5..b08730e041 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m @@ -20,6 +20,7 @@ #import "FingerprintViewController.h" #import "FullImageViewController.h" #import "NSAttributedString+OWS.h" +#import "NSString+OWS.h" #import "NewGroupViewController.h" #import "OWSAudioAttachmentPlayer.h" #import "OWSContactOffersCell.h" @@ -985,7 +986,7 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) { // Only retain the cached cell media for N cells at a time. const NSInteger kCellMediaCacheWindowSize = 24; NSInteger centerRow = (firstVisibleIndexPath.row + lastVisibleIndexPath.row) / 2; - // Determine the first and law rows (inclusive) of the cached cell media load window. + // Determine the first and last rows (inclusive) of the cached cell media load window. // Note these row values may not correspond to actual rows in the collection view. // // Always retain the cached cell media for any visible cells. @@ -3698,7 +3699,7 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) { return; } - text = [text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; + text = [text ows_stripped]; if (text.length < 1) { return; diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewItem.m b/Signal/src/ViewControllers/ConversationView/ConversationViewItem.m index 863dc6ffe2..6ca452bc7f 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewItem.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewItem.m @@ -3,6 +3,7 @@ // #import "ConversationViewItem.h" +#import "NSString+OWS.h" #import "OWSAudioMessageView.h" #import "OWSContactOffersCell.h" #import "OWSMessageCell.h" @@ -274,14 +275,12 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType) if (!displayableText) { // Only show up to 2kb of text. const NSUInteger kMaxTextDisplayLength = 2 * 1024; - text = [text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; + text = [text ows_stripped]; displayableText = [[DisplayableTextFilter new] displayableText:text]; if (displayableText.length > kMaxTextDisplayLength) { // Trim whitespace before _AND_ after slicing the snipper from the string. - NSString *snippet = - [[[displayableText stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]] - substringWithRange:NSMakeRange(0, kMaxTextDisplayLength)] - stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; + NSString *snippet = [ + [[displayableText ows_stripped] substringWithRange:NSMakeRange(0, kMaxTextDisplayLength)] ows_stripped]; displayableText = [NSString stringWithFormat:NSLocalizedString(@"OVERSIZE_TEXT_DISPLAY_FORMAT", @"A display format for oversize text messages."), snippet]; diff --git a/Signal/src/ViewControllers/CountryCodeViewController.m b/Signal/src/ViewControllers/CountryCodeViewController.m index 94771136b2..c202436cbd 100644 --- a/Signal/src/ViewControllers/CountryCodeViewController.m +++ b/Signal/src/ViewControllers/CountryCodeViewController.m @@ -3,6 +3,7 @@ // #import "CountryCodeViewController.h" +#import "NSString+OWS.h" #import "PhoneNumberUtil.h" #import "UIColor+OWS.h" #import "UIFont+OWS.h" @@ -142,8 +143,7 @@ - (void)searchTextDidChange { - NSString *searchText = - [self.searchBar.text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; + NSString *searchText = [self.searchBar.text ows_stripped]; self.countryCodes = [PhoneNumberUtil countryCodesForSearchTerm:searchText]; diff --git a/Signal/src/ViewControllers/NewGroupViewController.m b/Signal/src/ViewControllers/NewGroupViewController.m index 7e8d9babf3..9ff7b37299 100644 --- a/Signal/src/ViewControllers/NewGroupViewController.m +++ b/Signal/src/ViewControllers/NewGroupViewController.m @@ -9,6 +9,7 @@ #import "ContactTableViewCell.h" #import "ContactsViewHelper.h" #import "Environment.h" +#import "NSString+OWS.h" #import "OWSContactsManager.h" #import "OWSNavigationController.h" #import "OWSTableViewController.h" @@ -515,8 +516,7 @@ const NSUInteger kNewGroupViewControllerAvatarWidth = 68; - (TSGroupModel *)makeGroup { - NSString *groupName = [self.groupNameTextField.text - stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; + NSString *groupName = [self.groupNameTextField.text ows_stripped]; NSMutableArray *recipientIds = [self.memberRecipientIds.allObjects mutableCopy]; [recipientIds addObject:[self.contactsViewHelper localNumber]]; NSData *groupId = [SecurityUtils generateRandomBytes:16]; diff --git a/Signal/src/ViewControllers/ProfileViewController.m b/Signal/src/ViewControllers/ProfileViewController.m index 4804e9625f..cda6d75a9a 100644 --- a/Signal/src/ViewControllers/ProfileViewController.m +++ b/Signal/src/ViewControllers/ProfileViewController.m @@ -6,6 +6,7 @@ #import "AppDelegate.h" #import "AvatarViewHelper.h" #import "HomeViewController.h" +#import "NSString+OWS.h" #import "OWSNavigationController.h" #import "OWSProfileManager.h" #import "Signal-Swift.h" @@ -410,7 +411,7 @@ NSString *const kProfileView_LastPresentedDate = @"kProfileView_LastPresentedDat - (NSString *)normalizedProfileName { - return [self.nameTextField.text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; + return [self.nameTextField.text ows_stripped]; } - (void)updateProfileCompleted diff --git a/Signal/src/ViewControllers/RegistrationViewController.m b/Signal/src/ViewControllers/RegistrationViewController.m index e86d640798..86eb0409d3 100644 --- a/Signal/src/ViewControllers/RegistrationViewController.m +++ b/Signal/src/ViewControllers/RegistrationViewController.m @@ -6,6 +6,7 @@ #import "CodeVerificationViewController.h" #import "CountryCodeViewController.h" #import "Environment.h" +#import "NSString+OWS.h" #import "PhoneNumber.h" #import "PhoneNumberUtil.h" #import "Signal-Swift.h" @@ -280,8 +281,7 @@ NSString *const kKeychainKey_LastRegisteredPhoneNumber = @"kKeychainKey_LastRegi - (void)sendCodeAction { - NSString *phoneNumberText = - [_phoneNumberTextField.text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; + NSString *phoneNumberText = [_phoneNumberTextField.text ows_stripped]; if (phoneNumberText.length < 1) { [OWSAlerts showAlertWithTitle:NSLocalizedString(@"REGISTRATION_VIEW_NO_PHONE_NUMBER_ALERT_TITLE", diff --git a/Signal/src/ViewControllers/SelectThreadViewController.m b/Signal/src/ViewControllers/SelectThreadViewController.m index 268ffc1ca0..a16fb5df5a 100644 --- a/Signal/src/ViewControllers/SelectThreadViewController.m +++ b/Signal/src/ViewControllers/SelectThreadViewController.m @@ -7,6 +7,7 @@ #import "ContactTableViewCell.h" #import "ContactsViewHelper.h" #import "Environment.h" +#import "NSString+OWS.h" #import "OWSContactsManager.h" #import "OWSContactsSearcher.h" #import "OWSTableViewController.h" @@ -223,8 +224,7 @@ NS_ASSUME_NONNULL_BEGIN { NSArray *threads = self.threadViewHelper.threads; - NSString *searchTerm = - [[self.searchBar text] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; + NSString *searchTerm = [[self.searchBar text] ows_stripped]; if ([searchTerm isEqualToString:@""]) { return threads; diff --git a/Signal/src/ViewControllers/SendExternalFileViewController.m b/Signal/src/ViewControllers/SendExternalFileViewController.m index ad31813b81..264a18539c 100644 --- a/Signal/src/ViewControllers/SendExternalFileViewController.m +++ b/Signal/src/ViewControllers/SendExternalFileViewController.m @@ -4,6 +4,7 @@ #import "SendExternalFileViewController.h" #import "Environment.h" +#import "NSString+OWS.h" #import "Signal-Swift.h" #import "ThreadUtil.h" #import "UIColor+OWS.h" @@ -141,8 +142,7 @@ NS_ASSUME_NONNULL_BEGIN // TODO: If we reuse this VC, for example to offer a "forward attachment to other thread", // feature, this assumption would no longer apply. OWSAssert(self.attachment); - NSString *filename = - [self.attachment.sourceFilename stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; + NSString *filename = [self.attachment.sourceFilename ows_stripped]; OWSAssert(filename.length > 0); const NSUInteger kMaxFilenameLength = 20; if (filename.length > kMaxFilenameLength) { diff --git a/Signal/src/ViewControllers/UpdateGroupViewController.m b/Signal/src/ViewControllers/UpdateGroupViewController.m index eaced2e568..63471c2d0f 100644 --- a/Signal/src/ViewControllers/UpdateGroupViewController.m +++ b/Signal/src/ViewControllers/UpdateGroupViewController.m @@ -9,6 +9,7 @@ #import "ContactTableViewCell.h" #import "ContactsViewHelper.h" #import "Environment.h" +#import "NSString+OWS.h" #import "OWSContactsManager.h" #import "OWSNavigationController.h" #import "OWSTableViewController.h" @@ -196,8 +197,7 @@ NS_ASSUME_NONNULL_BEGIN UITextField *groupNameTextField = [UITextField new]; _groupNameTextField = groupNameTextField; - self.groupNameTextField.text = [self.thread.groupModel.groupName - stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; + self.groupNameTextField.text = [self.thread.groupModel.groupName ows_stripped]; groupNameTextField.textColor = [UIColor blackColor]; groupNameTextField.font = [UIFont ows_dynamicTypeTitle2Font]; groupNameTextField.placeholder @@ -373,8 +373,7 @@ NS_ASSUME_NONNULL_BEGIN { OWSAssert(self.conversationSettingsViewDelegate); - NSString *groupName = [self.groupNameTextField.text - stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; + NSString *groupName = [self.groupNameTextField.text ows_stripped]; TSGroupModel *groupModel = [[TSGroupModel alloc] initWithTitle:groupName memberIds:[self.memberRecipientIds.allObjects mutableCopy] image:self.groupAvatar diff --git a/Signal/src/contact/OWSContactsSearcher.m b/Signal/src/contact/OWSContactsSearcher.m index f776e444d0..774f1038c8 100644 --- a/Signal/src/contact/OWSContactsSearcher.m +++ b/Signal/src/contact/OWSContactsSearcher.m @@ -3,6 +3,7 @@ // #import "OWSContactsSearcher.h" +#import "NSString+OWS.h" #import @interface OWSContactsSearcher () @@ -22,7 +23,7 @@ } - (NSArray *)filterWithString:(NSString *)string { - NSString *searchTerm = [string stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; + NSString *searchTerm = [string ows_stripped]; if ([searchTerm isEqualToString:@""]) { return self.contacts; diff --git a/Signal/src/environment/NotificationsManager.m b/Signal/src/environment/NotificationsManager.m index 30a9ad50ef..e5315b0f81 100644 --- a/Signal/src/environment/NotificationsManager.m +++ b/Signal/src/environment/NotificationsManager.m @@ -4,6 +4,7 @@ #import "NotificationsManager.h" #import "Environment.h" +#import "NSString+OWS.h" #import "OWSContactsManager.h" #import "OWSPreferences.h" #import "PushManager.h" @@ -272,8 +273,7 @@ NSString *const kNotificationsManagerNewMesssageSoundName = @"NewMessage.aifc"; BOOL shouldPlaySound = [self shouldPlaySoundForNotification]; NSString *senderName = [contactsManager displayNameForPhoneIdentifier:message.authorId]; - NSString *groupName = - [thread.name stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; + NSString *groupName = [thread.name ows_stripped]; if (groupName.length < 1) { groupName = [MessageStrings newGroupDefaultTitle]; } From c47573334b665066a1e17987a1e2e8385fd01092 Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Wed, 18 Oct 2017 12:11:19 -0700 Subject: [PATCH 6/6] Respond to CR. // FREEBIE --- .../Cells/ConversationViewCell.h | 4 + .../ConversationView/Cells/OWSMessageCell.m | 30 ++++---- .../ConversationCollectionView.m | 30 ++++++++ .../ConversationViewController.m | 73 ++----------------- .../ConversationView/ConversationViewItem.h | 7 +- 5 files changed, 58 insertions(+), 86 deletions(-) diff --git a/Signal/src/ViewControllers/ConversationView/Cells/ConversationViewCell.h b/Signal/src/ViewControllers/ConversationView/Cells/ConversationViewCell.h index 6bf29fc59f..0ba624c847 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/ConversationViewCell.h +++ b/Signal/src/ViewControllers/ConversationView/Cells/ConversationViewCell.h @@ -43,6 +43,10 @@ NS_ASSUME_NONNULL_BEGIN - (NSAttributedString *)attributedContactOrProfileNameForPhoneIdentifier:(NSString *)recipientId; +#pragma mark - Caching + +- (NSCache *)cellMediaCache; + @end #pragma mark - diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m index 95f62c4787..4a1db8aa8d 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m @@ -269,15 +269,22 @@ NS_ASSUME_NONNULL_BEGIN // but lazy-load any expensive media (photo, gif, etc.) used in those views. Note that // this lazy-load can fail, in which case we modify the view hierarchy to use an "error" // state. The didCellMediaFailToLoad reflects media load fails. -- (nullable id)tryToLoadCellMedia:(nullable id (^)())loadCellMediaBlock mediaView:(UIView *)mediaView +- (nullable id)tryToLoadCellMedia:(nullable id (^)())loadCellMediaBlock + mediaView:(UIView *)mediaView + cacheKey:(NSString *)cacheKey { OWSAssert(self.attachmentStream); OWSAssert(mediaView); + OWSAssert(cacheKey); if (self.viewItem.didCellMediaFailToLoad) { return nil; } - id _Nullable cellMedia = self.viewItem.cachedCellMedia; + + NSCache *cellMediaCache = self.delegate.cellMediaCache; + OWSAssert(cellMediaCache); + + id _Nullable cellMedia = [cellMediaCache objectForKey:cacheKey]; if (cellMedia) { DDLogVerbose(@"%@ cell media cache hit", self.logTag); return cellMedia; @@ -285,7 +292,7 @@ NS_ASSUME_NONNULL_BEGIN cellMedia = loadCellMediaBlock(); if (cellMedia) { DDLogVerbose(@"%@ cell media cache miss", self.logTag); - self.viewItem.cachedCellMedia = cellMedia; + [cellMediaCache setObject:cellMedia forKey:cacheKey]; } else { DDLogError(@"%@ Failed to load cell media: %@", [self logTag], [self.attachmentStream mediaURL]); self.viewItem.didCellMediaFailToLoad = YES; @@ -316,17 +323,14 @@ NS_ASSUME_NONNULL_BEGIN OWSAssert([self.attachmentStream isImage]); return self.attachmentStream.image; } - mediaView:self.stillImageView]; + mediaView:self.stillImageView + cacheKey:self.attachmentStream.uniqueId]; break; } case OWSMessageCellType_AnimatedImage: { if (self.animatedImageView.image) { return; } - DDLogError(@"%@ ---- ensureViewMediaState loading[%zd]: %@", - self.logTag, - self.viewItem.row, - self.viewItem.interaction.description); self.animatedImageView.image = [self tryToLoadCellMedia:^{ OWSAssert([self.attachmentStream isAnimated]); @@ -337,23 +341,21 @@ NS_ASSUME_NONNULL_BEGIN } return animatedImage; } - mediaView:self.animatedImageView]; + mediaView:self.animatedImageView + cacheKey:self.attachmentStream.uniqueId]; break; } case OWSMessageCellType_Video: { if (self.stillImageView.image) { return; } - DDLogError(@"%@ ---- ensureViewMediaState loading[%zd]: %@", - self.logTag, - self.viewItem.row, - self.viewItem.interaction.description); self.stillImageView.image = [self tryToLoadCellMedia:^{ OWSAssert([self.attachmentStream isVideo]); return self.attachmentStream.image; } - mediaView:self.stillImageView]; + mediaView:self.stillImageView + cacheKey:self.attachmentStream.uniqueId]; break; } case OWSMessageCellType_TextMessage: diff --git a/Signal/src/ViewControllers/ConversationView/ConversationCollectionView.m b/Signal/src/ViewControllers/ConversationView/ConversationCollectionView.m index 5f02b528db..b973ae637f 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationCollectionView.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationCollectionView.m @@ -57,6 +57,36 @@ NS_ASSUME_NONNULL_BEGIN [super setContentOffset:contentOffset]; } +- (void)reloadData +{ + DDLogVerbose(@"%@ reloadData", self.logTag); + [super reloadData]; +} + +- (void)reloadSections:(NSIndexSet *)sections +{ + DDLogVerbose(@"%@ reloadSections", self.logTag); + [super reloadSections:sections]; +} + +- (void)reloadItemsAtIndexPaths:(NSArray *)indexPaths +{ + DDLogVerbose(@"%@ reloadItemsAtIndexPaths", self.logTag); + [super reloadItemsAtIndexPaths:indexPaths]; +} + +#pragma mark - Logging + ++ (NSString *)logTag +{ + return [NSString stringWithFormat:@"[%@]", self.class]; +} + +- (NSString *)logTag +{ + return self.class.logTag; +} + @end NS_ASSUME_NONNULL_END diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m index b08730e041..0455faf9bc 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m @@ -167,7 +167,7 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) { @property (nonatomic, nullable) NSUUID *voiceMessageUUID; @property (nonatomic, nullable) NSTimer *readTimer; -@property (nonatomic, nullable) NSTimer *cellMediaCachesTimer; +@property (nonatomic) NSCache *cellMediaCache; @property (nonatomic) ConversationHeaderView *navigationBarTitleView; @property (nonatomic) UILabel *navigationBarTitleLabel; @property (nonatomic) UILabel *navigationBarSubtitleLabel; @@ -394,6 +394,9 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) { _isGroupConversation = [self.thread isKindOfClass:[TSGroupThread class]]; _composeOnOpen = keyboardOnViewAppearing; _callOnOpen = callOnViewAppearing; + _cellMediaCache = [NSCache new]; + // Cache the cell media for ~24 cells. + self.cellMediaCache.countLimit = 24; [self.uiDatabaseConnection beginLongLivedReadTransaction]; @@ -528,7 +531,7 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) { self.hasClearedUnreadMessagesIndicator = NO; [self.dynamicInteractions clearUnreadIndicatorState]; } - [self clearCellMediaCaches]; + [self.cellMediaCache removeAllObjects]; } - (void)applicationWillResignActive:(NSNotification *)notification @@ -949,68 +952,6 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) { self.readTimer = nil; } -- (void)startCellMediaCachesTimerIfNecessary -{ - if (self.cellMediaCachesTimer) { - return; - } - self.cellMediaCachesTimer = [NSTimer weakScheduledTimerWithTimeInterval:3.f - target:self - selector:@selector(auditCellMediaCaches) - userInfo:nil - repeats:NO]; -} - -- (void)auditCellMediaCaches -{ - [self.cellMediaCachesTimer invalidate]; - self.cellMediaCachesTimer = nil; - - NSIndexPath *_Nullable firstVisibleIndexPath = nil; - NSIndexPath *_Nullable lastVisibleIndexPath = nil; - - for (NSIndexPath *indexPath in self.collectionView.indexPathsForVisibleItems) { - if (!firstVisibleIndexPath || firstVisibleIndexPath.row > indexPath.row) { - firstVisibleIndexPath = indexPath; - } - if (!lastVisibleIndexPath || lastVisibleIndexPath.row < indexPath.row) { - lastVisibleIndexPath = indexPath; - } - } - - if (!firstVisibleIndexPath || !lastVisibleIndexPath) { - [self clearCellMediaCaches]; - return; - } - - // Only retain the cached cell media for N cells at a time. - const NSInteger kCellMediaCacheWindowSize = 24; - NSInteger centerRow = (firstVisibleIndexPath.row + lastVisibleIndexPath.row) / 2; - // Determine the first and last rows (inclusive) of the cached cell media load window. - // Note these row values may not correspond to actual rows in the collection view. - // - // Always retain the cached cell media for any visible cells. - NSInteger firstLoadWindowRow = MIN(firstVisibleIndexPath.row, centerRow - kCellMediaCacheWindowSize / 2); - NSInteger lastLoadWindowRow = MAX(lastVisibleIndexPath.row, centerRow + kCellMediaCacheWindowSize / 2); - - // Cull cached cell media outside the load window. - for (ConversationViewItem *viewItem in self.viewItems) { - if (viewItem.row < firstLoadWindowRow || viewItem.row > lastLoadWindowRow) { - viewItem.cachedCellMedia = nil; - } - } -} - -- (void)clearCellMediaCaches -{ - [self.cellMediaCachesTimer invalidate]; - self.cellMediaCachesTimer = nil; - - for (ConversationViewItem *viewItem in self.viewItems) { - viewItem.cachedCellMedia = nil; - } -} - - (void)viewDidAppear:(BOOL)animated { [super viewDidAppear:animated]; @@ -1052,7 +993,7 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) { [self saveDraft]; [self markVisibleMessagesAsRead]; [self cancelVoiceMemo]; - [self clearCellMediaCaches]; + [self.cellMediaCache removeAllObjects]; self.isUserScrolling = NO; } @@ -3598,8 +3539,6 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) { - (void)scrollViewDidScroll:(UIScrollView *)scrollView { [self updateLastVisibleTimestamp]; - - [self startCellMediaCachesTimerIfNecessary]; } - (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewItem.h b/Signal/src/ViewControllers/ConversationView/ConversationViewItem.h index 75212d1d86..03e29bdfc2 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewItem.h +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewItem.h @@ -86,11 +86,8 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType); - (nullable TSAttachmentPointer *)attachmentPointer; - (CGSize)contentSize; -// A generic property that cells can use to cache their loaded -// media. This cache is volatile and will get evacuated based -// on scroll state, so that we only retain state for a sliding -// window of cells that are almost on-screen. -@property (nonatomic, nullable) id cachedCellMedia; +// 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; // TODO: