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 20f83a0a61..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 - @@ -54,9 +58,22 @@ 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. +// +// 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; -- (void)loadForDisplay:(int)contentWidth; +// The width of the collection view. +@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/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..04a2f1e0a5 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSAudioMessageView.m +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSAudioMessageView.m @@ -193,34 +193,45 @@ 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]]; + NSString *topText = [[filename stringByDeletingPathExtension] ows_stripped]; if (topText.length < 1) { topText = [MIMETypeUtil fileExtensionForMIMEType:self.attachmentStream.contentType].uppercaseString; } @@ -235,13 +246,18 @@ 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]; + [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 +265,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]; - - 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); + [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/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/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..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" @@ -98,32 +99,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,22 +157,13 @@ 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]]; + NSString *topText = [self.attachmentStream.sourceFilename ows_stripped]; if (topText.length < 1) { topText = [MIMETypeUtil fileExtensionForMIMEType:self.attachmentStream.contentType].uppercaseString; } @@ -172,8 +175,9 @@ 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]; + [labelsView addSubview:topLabel]; + [topLabel autoPinEdgeToSuperviewEdge:ALEdgeTop]; + [topLabel autoPinWidthToSuperview]; NSError *error; unsigned long long fileSize = @@ -185,21 +189,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 739b55c307..4a1db8aa8d 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) { @@ -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,8 +265,113 @@ NS_ASSUME_NONNULL_BEGIN // }); } -- (void)updateDateHeader:(int)contentWidth +// 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 + cacheKey:(NSString *)cacheKey { + OWSAssert(self.attachmentStream); + OWSAssert(mediaView); + OWSAssert(cacheKey); + + if (self.viewItem.didCellMediaFailToLoad) { + return nil; + } + + NSCache *cellMediaCache = self.delegate.cellMediaCache; + OWSAssert(cellMediaCache); + + id _Nullable cellMedia = [cellMediaCache objectForKey:cacheKey]; + if (cellMedia) { + DDLogVerbose(@"%@ cell media cache hit", self.logTag); + return cellMedia; + } + cellMedia = loadCellMediaBlock(); + if (cellMedia) { + DDLogVerbose(@"%@ cell media cache miss", self.logTag); + [cellMediaCache setObject:cellMedia forKey:cacheKey]; + } 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. + self.stillImageView.image = nil; + self.animatedImageView.image = nil; + return; + } + + switch (self.cellType) { + case OWSMessageCellType_StillImage: { + if (self.stillImageView.image) { + return; + } + self.stillImageView.image = [self tryToLoadCellMedia:^{ + OWSAssert([self.attachmentStream isImage]); + return self.attachmentStream.image; + } + mediaView:self.stillImageView + cacheKey:self.attachmentStream.uniqueId]; + break; + } + case OWSMessageCellType_AnimatedImage: { + if (self.animatedImageView.image) { + return; + } + 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 + cacheKey:self.attachmentStream.uniqueId]; + break; + } + case OWSMessageCellType_Video: { + if (self.stillImageView.image) { + return; + } + self.stillImageView.image = [self tryToLoadCellMedia:^{ + OWSAssert([self.attachmentStream isVideo]); + + return self.attachmentStream.image; + } + mediaView:self.stillImageView + cacheKey:self.attachmentStream.uniqueId]; + break; + } + case OWSMessageCellType_TextMessage: + 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; + } +} + +- (void)updateDateHeader +{ + OWSAssert(self.contentWidth > 0); + static NSDateFormatter *dateHeaderDateFormatter = nil; static NSDateFormatter *dateHeaderTimeFormatter = nil; static dispatch_once_t onceToken; @@ -312,7 +419,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], @@ -452,14 +559,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; @@ -476,19 +576,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; @@ -505,7 +593,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]; } @@ -515,16 +603,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; @@ -614,25 +693,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; @@ -860,6 +925,8 @@ NS_ASSUME_NONNULL_BEGIN return; } + [self ensureViewMediaState]; + if (isCellVisible) { if (self.message.shouldStartExpireTimer) { [self.expirationTimerView ensureAnimations]; 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/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/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 591a216943..0455faf9bc 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" @@ -165,7 +166,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) NSCache *cellMediaCache; @property (nonatomic) ConversationHeaderView *navigationBarTitleView; @property (nonatomic) UILabel *navigationBarTitleLabel; @property (nonatomic) UILabel *navigationBarSubtitleLabel; @@ -392,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]; @@ -526,6 +531,7 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) { self.hasClearedUnreadMessagesIndicator = NO; [self.dynamicInteractions clearUnreadIndicatorState]; } + [self.cellMediaCache removeAllObjects]; } - (void)applicationWillResignActive:(NSNotification *)notification @@ -943,6 +949,7 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) { - (void)cancelReadTimer { [self.readTimer invalidate]; + self.readTimer = nil; } - (void)viewDidAppear:(BOOL)animated @@ -986,6 +993,7 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) { [self saveDraft]; [self markVisibleMessagesAsRead]; [self cancelVoiceMemo]; + [self.cellMediaCache removeAllObjects]; self.isUserScrolling = NO; } @@ -3630,7 +3638,7 @@ typedef NS_ENUM(NSInteger, MessagesRangeSizeMode) { return; } - text = [text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; + text = [text ows_stripped]; if (text.length < 1) { return; @@ -4025,8 +4033,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; } diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewItem.h b/Signal/src/ViewControllers/ConversationView/ConversationViewItem.h index e48a1f12fd..03e29bdfc2 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewItem.h +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewItem.h @@ -86,6 +86,10 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType); - (nullable TSAttachmentPointer *)attachmentPointer; - (CGSize)contentSize; +// 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: //// Cells will request that this adapter clear its cached media views, //// but the adapter should only honor requests from the last cell to 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]; }