diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index 6ee946dd62..079b71d8fe 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -206,12 +206,17 @@ 34D2CCD4206294B900CB1A14 /* OWSScreenLock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34D2CCD3206294B900CB1A14 /* OWSScreenLock.swift */; }; 34D2CCDA2062E7D000CB1A14 /* OWSScreenLockUI.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D2CCD92062E7D000CB1A14 /* OWSScreenLockUI.m */; }; 34D2CCD220618B3000CB1A14 /* OWSBackupLazyRestoreJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34D2CCD120618B2F00CB1A14 /* OWSBackupLazyRestoreJob.swift */; }; + 34D2CCDF206939B400CB1A14 /* DebugUIMessagesAction.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D2CCDB206939B100CB1A14 /* DebugUIMessagesAction.m */; }; + 34D2CCE0206939B400CB1A14 /* DebugUIMessagesAssetLoader.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D2CCDC206939B200CB1A14 /* DebugUIMessagesAssetLoader.m */; }; 34D5CCA91EAE3D30005515DB /* AvatarViewHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D5CCA81EAE3D30005515DB /* AvatarViewHelper.m */; }; 34D8C0271ED3673300188D7C /* DebugUIMessages.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D8C0241ED3673300188D7C /* DebugUIMessages.m */; }; 34D8C0281ED3673300188D7C /* DebugUITableViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D8C0261ED3673300188D7C /* DebugUITableViewController.m */; }; 34D8C02B1ED3685800188D7C /* DebugUIContacts.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D8C02A1ED3685800188D7C /* DebugUIContacts.m */; }; 34D99C931F2937CC00D284D6 /* OWSAnalytics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34D99C911F2937CC00D284D6 /* OWSAnalytics.swift */; }; 34DB0BED2011548B007B313F /* OWSDatabaseConverterTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 34DB0BEC2011548B007B313F /* OWSDatabaseConverterTest.m */; }; + 34DBF003206BD5A500025978 /* OWSMessageTextView.m in Sources */ = {isa = PBXBuildFile; fileRef = 34DBEFFF206BD5A400025978 /* OWSMessageTextView.m */; }; + 34DBF004206BD5A500025978 /* OWSBubbleView.m in Sources */ = {isa = PBXBuildFile; fileRef = 34DBF001206BD5A500025978 /* OWSBubbleView.m */; }; + 34DBF007206C3CB200025978 /* OWSBubbleStrokeView.m in Sources */ = {isa = PBXBuildFile; fileRef = 34DBF006206C3CB200025978 /* OWSBubbleStrokeView.m */; }; 34E3E5681EC4B19400495BAC /* AudioProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34E3E5671EC4B19400495BAC /* AudioProgressView.swift */; }; 34E3EF0D1EFC235B007F6822 /* DebugUIDiskUsage.m in Sources */ = {isa = PBXBuildFile; fileRef = 34E3EF0C1EFC235B007F6822 /* DebugUIDiskUsage.m */; }; 34E3EF101EFC2684007F6822 /* DebugUIPage.m in Sources */ = {isa = PBXBuildFile; fileRef = 34E3EF0F1EFC2684007F6822 /* DebugUIPage.m */; }; @@ -814,6 +819,11 @@ 34D2CCD82062E7D000CB1A14 /* OWSScreenLockUI.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSScreenLockUI.h; sourceTree = ""; }; 34D2CCD92062E7D000CB1A14 /* OWSScreenLockUI.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSScreenLockUI.m; sourceTree = ""; }; 34D2CCD120618B2F00CB1A14 /* OWSBackupLazyRestoreJob.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OWSBackupLazyRestoreJob.swift; sourceTree = ""; }; + 34D2CCDB206939B100CB1A14 /* DebugUIMessagesAction.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DebugUIMessagesAction.m; sourceTree = ""; }; + 34D2CCDC206939B200CB1A14 /* DebugUIMessagesAssetLoader.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DebugUIMessagesAssetLoader.m; sourceTree = ""; }; + 34D2CCDD206939B200CB1A14 /* DebugUIMessagesAction.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DebugUIMessagesAction.h; sourceTree = ""; }; + 34D2CCDE206939B400CB1A14 /* DebugUIMessagesAssetLoader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DebugUIMessagesAssetLoader.h; sourceTree = ""; }; + 34D2CCE220693A1700CB1A14 /* DebugUIMessagesUtils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DebugUIMessagesUtils.h; sourceTree = ""; }; 34D5CCA71EAE3D30005515DB /* AvatarViewHelper.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AvatarViewHelper.h; sourceTree = ""; }; 34D5CCA81EAE3D30005515DB /* AvatarViewHelper.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AvatarViewHelper.m; sourceTree = ""; }; 34D8C0231ED3673300188D7C /* DebugUIMessages.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DebugUIMessages.h; sourceTree = ""; }; @@ -828,6 +838,12 @@ 34D99C911F2937CC00D284D6 /* OWSAnalytics.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OWSAnalytics.swift; sourceTree = ""; }; 34DB0BEB2011548A007B313F /* OWSDatabaseConverterTest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSDatabaseConverterTest.h; sourceTree = ""; }; 34DB0BEC2011548B007B313F /* OWSDatabaseConverterTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSDatabaseConverterTest.m; sourceTree = ""; }; + 34DBEFFF206BD5A400025978 /* OWSMessageTextView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSMessageTextView.m; sourceTree = ""; }; + 34DBF000206BD5A400025978 /* OWSMessageTextView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSMessageTextView.h; sourceTree = ""; }; + 34DBF001206BD5A500025978 /* OWSBubbleView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSBubbleView.m; sourceTree = ""; }; + 34DBF002206BD5A500025978 /* OWSBubbleView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSBubbleView.h; sourceTree = ""; }; + 34DBF005206C3CB100025978 /* OWSBubbleStrokeView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSBubbleStrokeView.h; sourceTree = ""; }; + 34DBF006206C3CB200025978 /* OWSBubbleStrokeView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSBubbleStrokeView.m; sourceTree = ""; }; 34E3E5671EC4B19400495BAC /* AudioProgressView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AudioProgressView.swift; sourceTree = ""; }; 34E3EF0B1EFC235B007F6822 /* DebugUIDiskUsage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DebugUIDiskUsage.h; sourceTree = ""; }; 34E3EF0C1EFC235B007F6822 /* DebugUIDiskUsage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DebugUIDiskUsage.m; sourceTree = ""; }; @@ -1638,6 +1654,10 @@ 34D1F0971F867BFC0066283D /* ConversationViewCell.m */, 34D1F0B81F8800D90066283D /* OWSAudioMessageView.h */, 34D1F0B91F8800D90066283D /* OWSAudioMessageView.m */, + 34DBF005206C3CB100025978 /* OWSBubbleStrokeView.h */, + 34DBF006206C3CB200025978 /* OWSBubbleStrokeView.m */, + 34DBF002206BD5A500025978 /* OWSBubbleView.h */, + 34DBF001206BD5A500025978 /* OWSBubbleView.m */, 34D1F09A1F867BFC0066283D /* OWSContactOffersCell.h */, 34D1F09B1F867BFC0066283D /* OWSContactOffersCell.m */, 34D1F09C1F867BFC0066283D /* OWSExpirableMessageView.h */, @@ -1647,6 +1667,8 @@ 34D1F0B61F87F8850066283D /* OWSGenericAttachmentView.m */, 34D1F0A11F867BFC0066283D /* OWSMessageCell.h */, 34D1F0A21F867BFC0066283D /* OWSMessageCell.m */, + 34DBF000206BD5A400025978 /* OWSMessageTextView.h */, + 34DBEFFF206BD5A400025978 /* OWSMessageTextView.m */, 34D1F0A51F867BFC0066283D /* OWSSystemMessageCell.h */, 34D1F0A61F867BFC0066283D /* OWSSystemMessageCell.m */, 34D1F0A71F867BFC0066283D /* OWSUnreadIndicatorCell.h */, @@ -1676,6 +1698,11 @@ 45B27B852037FFB400A539DF /* DebugUIFileBrowser.swift */, 34D8C0231ED3673300188D7C /* DebugUIMessages.h */, 34D8C0241ED3673300188D7C /* DebugUIMessages.m */, + 34D2CCDD206939B200CB1A14 /* DebugUIMessagesAction.h */, + 34D2CCDB206939B100CB1A14 /* DebugUIMessagesAction.m */, + 34D2CCDE206939B400CB1A14 /* DebugUIMessagesAssetLoader.h */, + 34D2CCDC206939B200CB1A14 /* DebugUIMessagesAssetLoader.m */, + 34D2CCE220693A1700CB1A14 /* DebugUIMessagesUtils.h */, 341F2C0D1F2B8AE700D07D6B /* DebugUIMisc.h */, 341F2C0E1F2B8AE700D07D6B /* DebugUIMisc.m */, 457C87B72032645C008D52D6 /* DebugUINotifications.swift */, @@ -3113,6 +3140,7 @@ 3461293E1FD1D72B00532771 /* ExperienceUpgradeFinder.swift in Sources */, 34D1F0BD1F8D108C0066283D /* AttachmentUploadView.m in Sources */, 452EC6DF205E9E30000E787C /* MediaGalleryViewController.swift in Sources */, + 34DBF007206C3CB200025978 /* OWSBubbleStrokeView.m in Sources */, 34D1F0BA1F8800D90066283D /* OWSAudioMessageView.m in Sources */, 34D8C02B1ED3685800188D7C /* DebugUIContacts.m in Sources */, 45C9DEB81DF4E35A0065CA84 /* WebRTCCallMessageHandler.swift in Sources */, @@ -3137,6 +3165,7 @@ 34CCAF381F0C0599004084F4 /* AppUpdateNag.m in Sources */, EF764C351DB67CC5000D9A87 /* UIViewController+Permissions.m in Sources */, 45CD81EF1DC030E7004C9430 /* SyncPushTokensJob.swift in Sources */, + 34D2CCE0206939B400CB1A14 /* DebugUIMessagesAssetLoader.m in Sources */, 45794E861E00620000066731 /* CallUIAdapter.swift in Sources */, 340FC8BA204DAC8D007AEB0F /* FingerprintViewScanController.m in Sources */, 4585C4681ED8F8D200896AEA /* SafetyNumberConfirmationAlert.swift in Sources */, @@ -3200,6 +3229,7 @@ 45F32C222057297A00A300D5 /* MediaDetailViewController.m in Sources */, 34B3F8851E8DF1700035BE1A /* NewGroupViewController.m in Sources */, 34D8C0271ED3673300188D7C /* DebugUIMessages.m in Sources */, + 34DBF003206BD5A500025978 /* OWSMessageTextView.m in Sources */, 34D1F0B41F86D31D0066283D /* ConversationCollectionView.m in Sources */, 34B3F8821E8DF1700035BE1A /* NewContactThreadViewController.m in Sources */, 45D308AD2049A439000189E4 /* PinEntryView.m in Sources */, @@ -3231,6 +3261,7 @@ 458DE9D61DEE3FD00071BB03 /* PeerConnectionClient.swift in Sources */, 45F32C242057297A00A300D5 /* MessageDetailViewController.swift in Sources */, 34D1F0841F8678AA0066283D /* ConversationInputToolbar.m in Sources */, + 34DBF004206BD5A500025978 /* OWSBubbleView.m in Sources */, FCC81A981A44558300DFEC7D /* UIDevice+TSHardwareVersion.m in Sources */, 76EB054018170B33006006FC /* AppDelegate.m in Sources */, 34D1F0831F8678AA0066283D /* ConversationInputTextView.m in Sources */, @@ -3246,6 +3277,7 @@ 3461299C1FD1EA9E00532771 /* NotificationsManager.m in Sources */, 4521C3C01F59F3BA00B4C582 /* TextFieldHelper.swift in Sources */, 34B3F87E1E8DF1700035BE1A /* InboxTableViewCell.m in Sources */, + 34D2CCDF206939B400CB1A14 /* DebugUIMessagesAction.m in Sources */, 340FC8AC204DAC8D007AEB0F /* PrivacySettingsTableViewController.m in Sources */, 34D2CCD4206294B900CB1A14 /* OWSScreenLock.swift in Sources */, 340FC8C5204DE223007AEB0F /* DebugUIBackup.m in Sources */, diff --git a/Signal/src/Signal-Bridging-Header.h b/Signal/src/Signal-Bridging-Header.h index a950f6f371..15ef23f82c 100644 --- a/Signal/src/Signal-Bridging-Header.h +++ b/Signal/src/Signal-Bridging-Header.h @@ -22,6 +22,7 @@ #import "OWSBackup.h" #import "OWSBackupIO.h" #import "OWSBezierPathView.h" +#import "OWSBubbleView.h" #import "OWSCallNotificationsAdaptee.h" #import "OWSDatabaseMigration.h" #import "OWSMessageCell.h" diff --git a/Signal/src/ViewControllers/ConversationView/Cells/AttachmentUploadView.h b/Signal/src/ViewControllers/ConversationView/Cells/AttachmentUploadView.h index 507fc6beb2..312676e89c 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/AttachmentUploadView.h +++ b/Signal/src/ViewControllers/ConversationView/Cells/AttachmentUploadView.h @@ -1,5 +1,5 @@ // -// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. // NS_ASSUME_NONNULL_BEGIN @@ -19,7 +19,6 @@ typedef void (^AttachmentStateBlock)(BOOL isAttachmentReady); @interface AttachmentUploadView : UIView - (instancetype)initWithAttachment:(TSAttachmentStream *)attachment - superview:(UIView *)superview attachmentStateCallback:(AttachmentStateBlock _Nullable)attachmentStateCallback; @end diff --git a/Signal/src/ViewControllers/ConversationView/Cells/AttachmentUploadView.m b/Signal/src/ViewControllers/ConversationView/Cells/AttachmentUploadView.m index 14a4358285..16254cd6d0 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/AttachmentUploadView.m +++ b/Signal/src/ViewControllers/ConversationView/Cells/AttachmentUploadView.m @@ -1,5 +1,5 @@ // -// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. // #import "AttachmentUploadView.h" @@ -32,21 +32,16 @@ NS_ASSUME_NONNULL_BEGIN @implementation AttachmentUploadView - (instancetype)initWithAttachment:(TSAttachmentStream *)attachment - superview:(UIView *)superview attachmentStateCallback:(AttachmentStateBlock _Nullable)attachmentStateCallback { self = [super init]; if (self) { OWSAssert(attachment); - OWSAssert(superview); self.attachment = attachment; self.attachmentStateCallback = attachmentStateCallback; - [superview addSubview:self]; - [self autoPinToSuperviewEdges]; - _bezierPathView = [OWSBezierPathView new]; self.bezierPathView.configureShapeLayerBlock = ^(CAShapeLayer *layer, CGRect bounds) { layer.path = [UIBezierPath bezierPathWithRect:bounds].CGPath; diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSBubbleStrokeView.h b/Signal/src/ViewControllers/ConversationView/Cells/OWSBubbleStrokeView.h new file mode 100644 index 0000000000..c2ddcc92ba --- /dev/null +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSBubbleStrokeView.h @@ -0,0 +1,20 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +NS_ASSUME_NONNULL_BEGIN + +@class OWSBubbleView; + +@interface OWSBubbleStrokeView : UIView + +@property (nonatomic, weak) OWSBubbleView *bubbleView; + +@property (nonatomic) UIColor *strokeColor; +@property (nonatomic) CGFloat strokeThickness; + +- (void)updateLayers; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSBubbleStrokeView.m b/Signal/src/ViewControllers/ConversationView/Cells/OWSBubbleStrokeView.m new file mode 100644 index 0000000000..81feef6e47 --- /dev/null +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSBubbleStrokeView.m @@ -0,0 +1,114 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "OWSBubbleStrokeView.h" +#import "OWSBubbleView.h" +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface OWSBubbleStrokeView () + +@property (nonatomic) CAShapeLayer *shapeLayer; + +@end + +#pragma mark - + +@implementation OWSBubbleStrokeView + +- (instancetype)init +{ + self = [super init]; + if (!self) { + return self; + } + + self.opaque = NO; + self.backgroundColor = [UIColor clearColor]; + + self.shapeLayer = [CAShapeLayer new]; + [self.layer addSublayer:self.shapeLayer]; + + return self; +} + +- (void)setStrokeColor:(UIColor *)strokeColor +{ + _strokeColor = strokeColor; + + [self updateLayers]; +} + +- (void)setStrokeThickness:(CGFloat)strokeThickness +{ + _strokeThickness = strokeThickness; + + [self updateLayers]; +} + +- (void)setFrame:(CGRect)frame +{ + BOOL didChange = !CGRectEqualToRect(self.frame, frame); + + [super setFrame:frame]; + + if (didChange || !self.shapeLayer) { + [self updateLayers]; + } +} + +- (void)setBounds:(CGRect)bounds +{ + BOOL didChange = !CGRectEqualToRect(self.bounds, bounds); + + [super setBounds:bounds]; + + if (didChange || !self.shapeLayer) { + [self updateLayers]; + } +} + +- (void)setCenter:(CGPoint)center +{ + [super setCenter:center]; + + [self updateLayers]; +} + +- (void)updateLayers +{ + OWSAssert(self.shapeLayer); + + // Don't fill the shape layer; we just want a stroke around the border. + self.shapeLayer.fillColor = [UIColor clearColor].CGColor; + + self.clipsToBounds = YES; + + if (!self.bubbleView) { + return; + } + + self.shapeLayer.strokeColor = self.strokeColor.CGColor; + self.shapeLayer.lineWidth = self.strokeThickness; + self.shapeLayer.zPosition = 100.f; + + UIBezierPath *bezierPath = [UIBezierPath new]; + + UIBezierPath *boundsBezierPath = [UIBezierPath bezierPathWithRect:self.bounds]; + [bezierPath appendPath:boundsBezierPath]; + + UIBezierPath *bubbleBezierPath = [self.bubbleView maskPath]; + // We need to convert between coordinate systems using layers, not views. + CGPoint bubbleOffset = [self.layer convertPoint:CGPointZero fromLayer:self.bubbleView.layer]; + CGAffineTransform transform = CGAffineTransformMakeTranslation(bubbleOffset.x, bubbleOffset.y); + [bubbleBezierPath applyTransform:transform]; + [bezierPath appendPath:bubbleBezierPath]; + + self.shapeLayer.path = bezierPath.CGPath; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSBubbleView.h b/Signal/src/ViewControllers/ConversationView/Cells/OWSBubbleView.h new file mode 100644 index 0000000000..b7ea6b8718 --- /dev/null +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSBubbleView.h @@ -0,0 +1,32 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +NS_ASSUME_NONNULL_BEGIN + +extern const CGFloat kOWSMessageCellCornerRadius; + +extern const CGFloat kBubbleVRounding; +extern const CGFloat kBubbleHRounding; +extern const CGFloat kBubbleThornSideInset; +extern const CGFloat kBubbleThornVInset; +extern const CGFloat kBubbleTextHInset; +extern const CGFloat kBubbleTextVInset; + +@class OWSBubbleStrokeView; + +@interface OWSBubbleView : UIView + +@property (nonatomic, weak, nullable) OWSBubbleStrokeView *bubbleStrokeView; + +@property (nonatomic) BOOL isOutgoing; +@property (nonatomic) BOOL hideTail; +@property (nonatomic) BOOL isTruncated; + +@property (nonatomic, nullable) UIColor *bubbleColor; + +- (UIBezierPath *)maskPath; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSBubbleView.m b/Signal/src/ViewControllers/ConversationView/Cells/OWSBubbleView.m new file mode 100644 index 0000000000..a020da0e19 --- /dev/null +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSBubbleView.m @@ -0,0 +1,205 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "OWSBubbleView.h" +#import "OWSBubbleStrokeView.h" +#import + +NS_ASSUME_NONNULL_BEGIN + +const CGFloat kOWSMessageCellCornerRadius = 17; + +const CGFloat kBubbleVRounding = kOWSMessageCellCornerRadius; +const CGFloat kBubbleHRounding = kOWSMessageCellCornerRadius; +const CGFloat kBubbleThornSideInset = 5.f; +const CGFloat kBubbleThornVInset = 0; +const CGFloat kBubbleTextHInset = 10.f; +const CGFloat kBubbleTextVInset = 10.f; + +@interface OWSBubbleView () + +@property (nonatomic) CAShapeLayer *maskLayer; +@property (nonatomic) CAShapeLayer *shapeLayer; + +@end + +#pragma mark - + +@implementation OWSBubbleView + +- (instancetype)init +{ + self = [super init]; + if (!self) { + return self; + } + + self.shapeLayer = [CAShapeLayer new]; + [self.layer addSublayer:self.shapeLayer]; + + self.maskLayer = [CAShapeLayer new]; + self.layer.mask = self.maskLayer; + + return self; +} + +- (void)setIsOutgoing:(BOOL)isOutgoing +{ + BOOL didChange = _isOutgoing != isOutgoing; + + _isOutgoing = isOutgoing; + + if (didChange || !self.shapeLayer) { + [self updateLayers]; + } +} + +- (void)setHideTail:(BOOL)hideTail +{ + BOOL didChange = _hideTail != hideTail; + + _hideTail = hideTail; + + if (didChange || !self.shapeLayer) { + [self updateLayers]; + } +} + +- (void)setIsTruncated:(BOOL)isTruncated +{ + BOOL didChange = _isTruncated != isTruncated; + + _isTruncated = isTruncated; + + if (didChange || !self.shapeLayer) { + [self updateLayers]; + } +} + +- (void)setFrame:(CGRect)frame +{ + // We only need to update our layers if the _size_ of this view + // changes since the contents of the layers are in local coordinates. + BOOL didChangeSize = !CGSizeEqualToSize(self.frame.size, frame.size); + + [super setFrame:frame]; + + if (didChangeSize || !self.shapeLayer) { + [self updateLayers]; + } + + // We always need to inform the "bubble stroke view" (if any) if our + // frame/bounds/center changes. Its contents are not in local coordinates. + [self.bubbleStrokeView updateLayers]; +} + +- (void)setBounds:(CGRect)bounds +{ + // We only need to update our layers if the _size_ of this view + // changes since the contents of the layers are in local coordinates. + BOOL didChangeSize = !CGSizeEqualToSize(self.bounds.size, bounds.size); + + [super setBounds:bounds]; + + if (didChangeSize || !self.shapeLayer) { + [self updateLayers]; + } + + // We always need to inform the "bubble stroke view" (if any) if our + // frame/bounds/center changes. Its contents are not in local coordinates. + [self.bubbleStrokeView updateLayers]; +} + +- (void)setCenter:(CGPoint)center +{ + [super setCenter:center]; + + // We always need to inform the "bubble stroke view" (if any) if our + // frame/bounds/center changes. Its contents are not in local coordinates. + [self.bubbleStrokeView updateLayers]; +} + +- (void)setBubbleColor:(nullable UIColor *)bubbleColor +{ + _bubbleColor = bubbleColor; + + if (!self.shapeLayer) { + [self updateLayers]; + } + self.shapeLayer.fillColor = bubbleColor.CGColor; +} + +- (void)updateLayers +{ + OWSAssert(self.maskLayer); + OWSAssert(self.shapeLayer); + + UIBezierPath *bezierPath = [self maskPath]; + + self.shapeLayer.fillColor = self.bubbleColor.CGColor; + self.shapeLayer.path = bezierPath.CGPath; + self.maskLayer.path = bezierPath.CGPath; +} + +- (UIBezierPath *)maskPath +{ + return [self.class maskPathForSize:self.bounds.size + isOutgoing:self.isOutgoing + hideTail:self.hideTail + isTruncated:self.isTruncated + isRTL:self.isRTL]; +} + ++ (UIBezierPath *)maskPathForSize:(CGSize)size + isOutgoing:(BOOL)isOutgoing + hideTail:(BOOL)hideTail + isTruncated:(BOOL)isTruncated + isRTL:(BOOL)isRTL +{ + UIBezierPath *bezierPath = [UIBezierPath new]; + + CGFloat bubbleLeft = 0.f; + CGFloat bubbleRight = size.width - kBubbleThornSideInset; + CGFloat bubbleTop = 0.f; + CGFloat bubbleBottom = size.height - kBubbleThornVInset; + + [bezierPath moveToPoint:CGPointMake(bubbleLeft + kBubbleHRounding, bubbleTop)]; + [bezierPath addLineToPoint:CGPointMake(bubbleRight - kBubbleHRounding, bubbleTop)]; + [bezierPath addQuadCurveToPoint:CGPointMake(bubbleRight, bubbleTop + kBubbleVRounding) + controlPoint:CGPointMake(bubbleRight, bubbleTop)]; + [bezierPath addLineToPoint:CGPointMake(bubbleRight, bubbleBottom - kBubbleVRounding)]; + [bezierPath addQuadCurveToPoint:CGPointMake(bubbleRight - kBubbleHRounding, bubbleBottom) + controlPoint:CGPointMake(bubbleRight, bubbleBottom)]; + [bezierPath addLineToPoint:CGPointMake(bubbleLeft + kBubbleHRounding, bubbleBottom)]; + [bezierPath addQuadCurveToPoint:CGPointMake(bubbleLeft, bubbleBottom - kBubbleVRounding) + controlPoint:CGPointMake(bubbleLeft, bubbleBottom)]; + [bezierPath addLineToPoint:CGPointMake(bubbleLeft, bubbleTop + kBubbleVRounding)]; + [bezierPath addQuadCurveToPoint:CGPointMake(bubbleLeft + kBubbleHRounding, bubbleTop) + controlPoint:CGPointMake(bubbleLeft, bubbleTop)]; + + if (!hideTail) { + // Thorn Tip + CGPoint thornTip = CGPointMake(size.width + 1, size.height); + CGPoint thornA = CGPointMake(bubbleRight - kBubbleHRounding * 0.5f, bubbleBottom - kBubbleVRounding); + CGPoint thornB = CGPointMake(bubbleRight, bubbleBottom - kBubbleVRounding); + [bezierPath moveToPoint:thornTip]; + [bezierPath addQuadCurveToPoint:thornA controlPoint:CGPointMake(thornA.x, bubbleBottom)]; + [bezierPath addLineToPoint:thornB]; + [bezierPath addQuadCurveToPoint:thornTip controlPoint:CGPointMake(thornB.x, bubbleBottom)]; + [bezierPath addLineToPoint:thornTip]; + } + + // Horizontal Flip If Necessary + BOOL shouldFlip = isOutgoing == isRTL; + if (shouldFlip) { + CGAffineTransform flipTransform = CGAffineTransformMakeTranslation(size.width, 0.0); + flipTransform = CGAffineTransformScale(flipTransform, -1.0, 1.0); + [bezierPath applyTransform:flipTransform]; + } + return bezierPath; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.h b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.h index 3116bce7e6..789b94fbb1 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.h +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.h @@ -6,8 +6,6 @@ NS_ASSUME_NONNULL_BEGIN -extern const CGFloat OWSMessageCellCornerRadius; - @interface OWSMessageCell : ConversationViewCell + (NSString *)cellReuseIdentifier; diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m index 14469c223a..0f0eedb258 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m @@ -8,178 +8,55 @@ #import "ConversationViewItem.h" #import "NSAttributedString+OWS.h" #import "OWSAudioMessageView.h" +#import "OWSBubbleStrokeView.h" +#import "OWSBubbleView.h" #import "OWSExpirationTimerView.h" #import "OWSGenericAttachmentView.h" +#import "OWSMessageTextView.h" #import "Signal-Swift.h" #import "UIColor+OWS.h" #import #import +#import NS_ASSUME_NONNULL_BEGIN -// This approximates the curve of our message bubbles, which makes the animation feel a little smoother. -const CGFloat OWSMessageCellCornerRadius = 17; - -@interface BubbleMaskingView : UIView - -@property (nonatomic) BOOL isOutgoing; -@property (nonatomic) BOOL hideTail; -@property (nonatomic, nullable, weak) UIView *maskedSubview; - -@end - -#pragma mark - - -@implementation BubbleMaskingView - -- (void)setFrame:(CGRect)frame +CG_INLINE CGSize CGSizeCeil(CGSize size) { - BOOL didSizeChange = !CGSizeEqualToSize(self.frame.size, frame.size); - - [super setFrame:frame]; - - if (didSizeChange) { - [self updateMask]; - } + return CGSizeMake((CGFloat)ceil(size.width), (CGFloat)ceil(size.height)); } -- (void)setBounds:(CGRect)bounds -{ - BOOL didSizeChange = !CGSizeEqualToSize(self.bounds.size, bounds.size); - - [super setBounds:bounds]; - - if (didSizeChange) { - [self updateMask]; - } -} - -- (void)updateMask -{ - UIView *_Nullable maskedSubview = self.maskedSubview; - if (!maskedSubview) { - return; - } - maskedSubview.frame = self.bounds; - // The JSQ masks are not RTL-safe, so we need to invert the - // mask orientation manually. - BOOL hasOutgoingMask = self.isOutgoing ^ self.isRTL; - - // Since the caption has it's own tail, the media bubble just above - // it looks better without a tail. - if (self.hideTail) { - if (hasOutgoingMask) { - self.layoutMargins = UIEdgeInsetsMake(0, 0, 2, 8); - } else { - self.layoutMargins = UIEdgeInsetsMake(0, 8, 2, 0); - } - maskedSubview.clipsToBounds = YES; - - // I arrived at this cornerRadius by superimposing the generated corner - // over that generated from the JSQMessagesMediaViewBubbleImageMasker - maskedSubview.layer.cornerRadius = 17; - } else { - [JSQMessagesMediaViewBubbleImageMasker applyBubbleImageMaskToMediaView:maskedSubview - isOutgoing:hasOutgoingMask]; - } -} - -@end - -#pragma mark - - -@interface OWSMessageTextView : UITextView - -@property (nonatomic) BOOL shouldIgnoreEvents; - -@end - -#pragma mark - - -@implementation OWSMessageTextView - -// Our message text views are never used for editing; -// suppress their ability to become first responder -// so that tapping on them doesn't hide keyboard. -- (BOOL)canBecomeFirstResponder -{ - return NO; -} - -// Ignore interactions with the text view _except_ taps on links. -// -// We want to disable "partial" selection of text in the message -// and we want to enable "tap to resend" by tapping on a message. -- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *_Nullable)event -{ - if (self.shouldIgnoreEvents) { - // We ignore all events for failed messages so that users - // can tap-to-resend even "all link" messages. - return NO; - } - - // Find the nearest text position to the event. - UITextPosition *_Nullable position = [self closestPositionToPoint:point]; - if (!position) { - return NO; - } - // Find the range of the character in the text which contains the event. - // - // Try every layout direction (this might not be necessary). - UITextRange *_Nullable range = nil; - for (NSNumber *textLayoutDirection in @[ - @(UITextLayoutDirectionLeft), - @(UITextLayoutDirectionRight), - @(UITextLayoutDirectionUp), - @(UITextLayoutDirectionDown), - ]) { - range = [self.tokenizer rangeEnclosingPosition:position - withGranularity:UITextGranularityCharacter - inDirection:(UITextDirection)textLayoutDirection.intValue]; - if (range) { - break; - } - } - if (!range) { - return NO; - } - // Ignore the event unless it occurred inside a link. - NSInteger startIndex = [self offsetFromPosition:self.beginningOfDocument toPosition:range.start]; - BOOL result = - [self.attributedText attribute:NSLinkAttributeName atIndex:(NSUInteger)startIndex effectiveRange:nil] != nil; - return result; -} - -@end - -#pragma mark - - @interface OWSMessageCell () // The nullable properties are created as needed. // The non-nullable properties are so frequently used that it's easier // to always keep one around. -@property (nonatomic) UIView *payloadView; -@property (nonatomic) BubbleMaskingView *mediaMaskingView; + +// The cell's contentView contains: +// +// * MessageView (message) +// * dateHeaderLabel (above message) +// * footerView (below message) +// * failedSendBadgeView ("trailing" beside message) + +@property (nonatomic) OWSBubbleView *bubbleView; + @property (nonatomic) UILabel *dateHeaderLabel; -@property (nonatomic) OWSMessageTextView *textView; +@property (nonatomic) OWSMessageTextView *bodyTextView; @property (nonatomic, nullable) UIImageView *failedSendBadgeView; -@property (nonatomic, nullable) UILabel *tapForMoreLabel; -@property (nonatomic, nullable) UIImageView *textBubbleImageView; -@property (nonatomic, nullable) AttachmentUploadView *attachmentUploadView; -@property (nonatomic, nullable) UIImageView *stillImageView; -@property (nonatomic, nullable) YYAnimatedImageView *animatedImageView; -@property (nonatomic, nullable) UIView *customView; -@property (nonatomic, nullable) AttachmentPointerView *attachmentPointerView; -@property (nonatomic, nullable) OWSGenericAttachmentView *attachmentView; -@property (nonatomic, nullable) OWSAudioMessageView *audioMessageView; @property (nonatomic) UIView *footerView; @property (nonatomic) UILabel *footerLabel; @property (nonatomic, nullable) OWSExpirationTimerView *expirationTimerView; -@property (nonatomic, nullable) NSArray *payloadConstraints; -@property (nonatomic, nullable) NSArray *dateHeaderConstraints; -@property (nonatomic, nullable) NSMutableArray *contentConstraints; -@property (nonatomic, nullable) NSArray *footerConstraints; + +@property (nonatomic, nullable) UIView *lastBodyMediaView; + +// Should lazy-load expensive view contents (images, etc.). +// Should do nothing if view is already loaded. +@property (nonatomic, nullable) dispatch_block_t loadCellContentBlock; +// Should unload all expensive view contents (images, etc.). +@property (nonatomic, nullable) dispatch_block_t unloadCellContentBlock; + +@property (nonatomic, nullable) NSMutableArray *viewConstraints; @property (nonatomic) BOOL isPresentingMenuController; @end @@ -198,20 +75,16 @@ const CGFloat OWSMessageCellCornerRadius = 17; - (void)commontInit { - OWSAssert(!self.textView); + OWSAssert(!self.bodyTextView); - _contentConstraints = [NSMutableArray new]; + _viewConstraints = [NSMutableArray new]; self.layoutMargins = UIEdgeInsetsZero; self.contentView.layoutMargins = UIEdgeInsetsZero; - self.payloadView = [UIView new]; - self.payloadView.layoutMargins = UIEdgeInsetsZero; - [self.contentView addSubview:self.payloadView]; - - self.mediaMaskingView = [BubbleMaskingView new]; - self.mediaMaskingView.layoutMargins = UIEdgeInsetsZero; - [self.payloadView addSubview:self.mediaMaskingView]; + self.bubbleView = [OWSBubbleView new]; + self.bubbleView.layoutMargins = UIEdgeInsetsZero; + [self.contentView addSubview:self.bubbleView]; self.footerView = [UIView containerView]; [self.contentView addSubview:self.footerView]; @@ -222,25 +95,7 @@ const CGFloat OWSMessageCellCornerRadius = 17; self.dateHeaderLabel.textColor = [UIColor lightGrayColor]; [self.contentView addSubview:self.dateHeaderLabel]; - self.textBubbleImageView = [UIImageView new]; - self.textBubbleImageView.layoutMargins = UIEdgeInsetsZero; - // Enable userInteractionEnabled so that links in textView work. - self.textBubbleImageView.userInteractionEnabled = YES; - - [self.payloadView addSubview:self.textBubbleImageView]; - - self.textView = [OWSMessageTextView new]; - self.textView.backgroundColor = [UIColor clearColor]; - self.textView.opaque = NO; - self.textView.editable = NO; - self.textView.selectable = YES; - self.textView.textContainerInset = UIEdgeInsetsZero; - self.textView.contentInset = UIEdgeInsetsZero; - self.textView.scrollEnabled = NO; - - [self.textBubbleImageView addSubview:self.textView]; - - OWSAssert(self.textView.superview); + self.bodyTextView = [self newTextView]; self.footerLabel = [UILabel new]; self.footerLabel.font = [UIFont ows_regularFontWithSize:12.f]; @@ -248,45 +103,48 @@ const CGFloat OWSMessageCellCornerRadius = 17; [self.footerView addSubview:self.footerLabel]; // Hide these views by default. - self.textBubbleImageView.hidden = YES; - self.textView.hidden = YES; + self.bodyTextView.hidden = YES; self.dateHeaderLabel.hidden = YES; self.footerLabel.hidden = YES; - [self.payloadView autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:self.dateHeaderLabel]; - [self.footerView autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:self.payloadView]; - - [self.mediaMaskingView autoPinEdgeToSuperviewEdge:ALEdgeTop]; - - [self.textBubbleImageView autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:self.mediaMaskingView]; - [self.textBubbleImageView autoPinEdgeToSuperviewEdge:ALEdgeBottom]; + [self.bubbleView autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:self.dateHeaderLabel]; + [self.footerView autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:self.bubbleView]; [self.footerView autoPinEdgeToSuperviewEdge:ALEdgeBottom]; [self.footerView autoPinWidthToSuperview]; - UITapGestureRecognizer *mediaTap = - [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleMediaTapGesture:)]; - [self.mediaMaskingView addGestureRecognizer:mediaTap]; + self.contentView.userInteractionEnabled = YES; - UITapGestureRecognizer *textTap = - [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTextTapGesture:)]; - [self.textBubbleImageView addGestureRecognizer:textTap]; + UITapGestureRecognizer *tap = + [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTapGesture:)]; + [self.contentView addGestureRecognizer:tap]; - UILongPressGestureRecognizer *mediaLongPress = - [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleMediaLongPressGesture:)]; - [self.mediaMaskingView addGestureRecognizer:mediaLongPress]; + UILongPressGestureRecognizer *longPress = + [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleLongPressGesture:)]; + [self.contentView addGestureRecognizer:longPress]; - UILongPressGestureRecognizer *textLongPress = - [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleTextLongPressGesture:)]; - [self.textBubbleImageView addGestureRecognizer:textLongPress]; - - PanDirectionGestureRecognizer *panGesture = - [[PanDirectionGestureRecognizer alloc] initWithDirection:(self.isRTL ? PanDirectionLeft : PanDirectionRight) - target:self - action:@selector(handlePanGesture:)]; + PanDirectionGestureRecognizer *panGesture = [[PanDirectionGestureRecognizer alloc] + initWithDirection:(self.isRTL ? PanDirectionLeft : PanDirectionRight)target:self + action:@selector(handlePanGesture:)]; [self addGestureRecognizer:panGesture]; } +- (OWSMessageTextView *)newTextView +{ + OWSMessageTextView *textView = [OWSMessageTextView new]; + textView.backgroundColor = [UIColor clearColor]; + textView.opaque = NO; + textView.editable = NO; + textView.selectable = YES; + textView.textContainerInset = UIEdgeInsetsZero; + textView.contentInset = UIEdgeInsetsZero; + textView.textContainer.lineFragmentPadding = 0; + textView.scrollEnabled = NO; + // Setting dataDetectorTypes is expensive. Do it just once. + textView.dataDetectorTypes = (UIDataDetectorTypeLink | UIDataDetectorTypeAddress | UIDataDetectorTypeCalendarEvent); + return textView; +} + + (NSString *)cellReuseIdentifier { return NSStringFromClass([self class]); @@ -297,7 +155,7 @@ const CGFloat OWSMessageCellCornerRadius = 17; OWSAssert(DisplayableText.kMaxJumbomojiCount == 5); CGFloat basePointSize = [UIFont ows_dynamicTypeBodyFont].pointSize; - switch (self.displayableText.jumbomojiCount) { + switch (self.displayableBodyText.jumbomojiCount) { case 0: break; case 1: @@ -309,7 +167,7 @@ const CGFloat OWSMessageCellCornerRadius = 17; case 5: return [UIFont ows_regularFontWithSize:basePointSize + 6.f]; default: - OWSFail(@"%@ Unexpected jumbomoji count: %zd", self.logTag, self.displayableText.jumbomojiCount); + OWSFail(@"%@ Unexpected jumbomoji count: %zd", self.logTag, self.displayableBodyText.jumbomojiCount); break; } @@ -353,20 +211,20 @@ const CGFloat OWSMessageCellCornerRadius = 17; return self.viewItem.messageCellType; } -- (BOOL)hasText +- (BOOL)hasBodyText { // This should always be valid for the appropriate cell types. OWSAssert(self.viewItem); - return self.viewItem.hasText; + return self.viewItem.hasBodyText; } -- (nullable DisplayableText *)displayableText +- (nullable DisplayableText *)displayableBodyText { // This should always be valid for the appropriate cell types. - OWSAssert(self.viewItem.displayableText); + OWSAssert(self.viewItem.displayableBodyText); - return self.viewItem.displayableText; + return self.viewItem.displayableBodyText; } - (nullable TSAttachmentStream *)attachmentStream @@ -400,11 +258,57 @@ const CGFloat OWSMessageCellCornerRadius = 17; return (TSMessage *)self.viewItem.interaction; } +- (BOOL)hasNonImageBodyContent +{ + switch (self.cellType) { + case OWSMessageCellType_Unknown: + case OWSMessageCellType_TextMessage: + case OWSMessageCellType_OversizeTextMessage: + case OWSMessageCellType_GenericAttachment: + case OWSMessageCellType_DownloadingAttachment: + return YES; + case OWSMessageCellType_StillImage: + case OWSMessageCellType_AnimatedImage: + case OWSMessageCellType_Audio: + case OWSMessageCellType_Video: + return self.hasBodyText; + } +} + +- (BOOL)hasBodyTextContent +{ + switch (self.cellType) { + case OWSMessageCellType_Unknown: + case OWSMessageCellType_TextMessage: + case OWSMessageCellType_OversizeTextMessage: + return YES; + case OWSMessageCellType_GenericAttachment: + case OWSMessageCellType_DownloadingAttachment: + case OWSMessageCellType_StillImage: + case OWSMessageCellType_AnimatedImage: + case OWSMessageCellType_Audio: + case OWSMessageCellType_Video: + // Is there a caption? + return self.hasBodyText; + } +} + +#pragma mark - Load + - (void)loadForDisplay { OWSAssert(self.viewItem); OWSAssert(self.viewItem.interaction); OWSAssert([self.viewItem.interaction isKindOfClass:[TSMessage class]]); + OWSAssert(self.contentWidth > 0); + + CGSize bodyMediaContentSize = [self bodyMediaSizeForContentWidth:self.contentWidth]; + CGSize bodyTextContentSize = [self bodyTextSizeForContentWidth:self.contentWidth includeMargins:NO]; + + // TODO: We might not need to hide it. + self.bubbleView.hidden = NO; + self.bubbleView.isOutgoing = self.isOutgoing; + self.bubbleView.hideTail = self.viewItem.shouldHideBubbleTail; if (self.shouldHaveFailedSendBadge) { self.failedSendBadgeView = [UIImageView new]; @@ -413,83 +317,193 @@ const CGFloat OWSMessageCellCornerRadius = 17; self.failedSendBadgeView.tintColor = [UIColor ows_destructiveRedColor]; [self.contentView addSubview:self.failedSendBadgeView]; - self.payloadConstraints = @[ - [self.payloadView autoPinLeadingToSuperview], - [self.failedSendBadgeView autoPinLeadingToTrailingOfView:self.payloadView], + [self.viewConstraints addObjectsFromArray:@[ + [self.bubbleView autoPinLeadingToSuperview], + [self.failedSendBadgeView autoPinLeadingToTrailingOfView:self.bubbleView], + [self.failedSendBadgeView autoAlignAxis:ALAxisHorizontal toSameAxisOfView:self.bubbleView], [self.failedSendBadgeView autoPinTrailingToSuperview], - [self.failedSendBadgeView autoAlignAxis:ALAxisHorizontal toSameAxisOfView:self.payloadView], [self.failedSendBadgeView autoSetDimension:ALDimensionWidth toSize:self.failedSendBadgeSize], [self.failedSendBadgeView autoSetDimension:ALDimensionHeight toSize:self.failedSendBadgeSize], - ]; + ]]; } else { - self.payloadConstraints = [self.payloadView autoPinWidthToSuperview]; + [self.viewConstraints addObjectsFromArray:@[ + [self.bubbleView autoPinLeadingToSuperview], + [self.bubbleView autoPinTrailingToSuperview], + ]]; } - JSQMessagesBubbleImage *_Nullable bubbleImageData; - if ([self.viewItem.interaction isKindOfClass:[TSMessage class]]) { + if ([self.viewItem.interaction isKindOfClass:[TSMessage class]] && self.hasNonImageBodyContent) { TSMessage *message = (TSMessage *)self.viewItem.interaction; - bubbleImageData = [self.bubbleFactory bubbleWithMessage:message]; + self.bubbleView.bubbleColor = [self.bubbleFactory bubbleColorWithMessage:message]; + } else { + // Media-only messages should have no background color; they will fill the bubble's bounds + // and we don't want artifacts at the edges. + self.bubbleView.bubbleColor = nil; } - self.textBubbleImageView.image = bubbleImageData.messageBubbleImage; - [self updateDateHeader]; [self updateFooter]; + UIView *_Nullable lastSubview = nil; + CGFloat bottomMargin = 0; + UIView *_Nullable bodyMediaView = nil; + BOOL bodyMediaViewHasGreedyWidth = NO; switch (self.cellType) { case OWSMessageCellType_Unknown: - OWSFail(@"Unknown cell type for viewItem: %@", self.viewItem); - break; case OWSMessageCellType_TextMessage: - [self loadForTextDisplay]; - break; case OWSMessageCellType_OversizeTextMessage: - OWSAssert(self.viewItem.attachmentStream); - [self loadForTextDisplay]; break; case OWSMessageCellType_StillImage: OWSAssert(self.viewItem.attachmentStream); - [self loadForStillImageDisplay]; + bodyMediaView = [self loadViewForStillImage]; break; case OWSMessageCellType_AnimatedImage: OWSAssert(self.viewItem.attachmentStream); - [self loadForAnimatedImageDisplay]; - break; - case OWSMessageCellType_Audio: - OWSAssert(self.viewItem.attachmentStream); - [self loadForAudioDisplay]; + bodyMediaView = [self loadViewForAnimatedImage]; break; case OWSMessageCellType_Video: OWSAssert(self.viewItem.attachmentStream); - [self loadForVideoDisplay]; + bodyMediaView = [self loadViewForVideo]; break; - case OWSMessageCellType_GenericAttachment: { + case OWSMessageCellType_Audio: OWSAssert(self.viewItem.attachmentStream); - self.attachmentView = - [[OWSGenericAttachmentView alloc] initWithAttachment:self.attachmentStream isIncoming:self.isIncoming]; - [self.attachmentView createContents]; - [self setMediaView:self.attachmentView]; - [self addAttachmentUploadViewIfNecessary:self.attachmentView]; - [self addCaptionIfNecessary]; + bodyMediaView = [self loadViewForAudio]; + bodyMediaViewHasGreedyWidth = YES; break; + case OWSMessageCellType_GenericAttachment: + bodyMediaView = [self loadViewForGenericAttachment]; + bodyMediaViewHasGreedyWidth = YES; + break; + case OWSMessageCellType_DownloadingAttachment: + bodyMediaView = [self loadViewForDownloadingAttachment]; + bodyMediaViewHasGreedyWidth = YES; + break; + } + + if (bodyMediaView) { + OWSAssert(self.loadCellContentBlock); + OWSAssert(self.unloadCellContentBlock); + OWSAssert(!lastSubview); + + bodyMediaView.clipsToBounds = YES; + + self.lastBodyMediaView = bodyMediaView; + bodyMediaView.userInteractionEnabled = NO; + if (self.isMediaBeingSent) { + bodyMediaView.layer.opacity = 0.75f; } - case OWSMessageCellType_DownloadingAttachment: { - [self loadForDownloadingAttachment]; - [self addCaptionIfNecessary]; - break; + + [self.bubbleView addSubview:bodyMediaView]; + // This layout can lead to extreme cropping of media content, + // e.g. a very tall portrait image + long caption. The media + // view will have "max width", so the image will be cropped to + // roughly a square. + // TODO: Myles is considering alternatives. + [self.viewConstraints addObjectsFromArray:@[ + [bodyMediaView autoPinLeadingToSuperviewWithMargin:0], + [bodyMediaView autoPinTrailingToSuperviewWithMargin:0], + ]]; + // We need constraints to control the vertical sizing of media and text views, but we use + // lower priority so that when a message only contains media it uses the exact bounds of + // the message view. + [NSLayoutConstraint + autoSetPriority:UILayoutPriorityDefaultLow + forConstraints:^{ + [self.viewConstraints + addObject:[bodyMediaView autoSetDimension:ALDimensionHeight toSize:bodyMediaContentSize.height]]; + }]; + + if (lastSubview) { + [self.viewConstraints + addObject:[bodyMediaView autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:lastSubview withOffset:0]]; + } else { + [self.viewConstraints addObject:[bodyMediaView autoPinEdgeToSuperviewEdge:ALEdgeTop withInset:0]]; + } + lastSubview = bodyMediaView; + bottomMargin = 0; + + BOOL shouldStrokeMediaView = [bodyMediaView isKindOfClass:[UIImageView class]]; + if (shouldStrokeMediaView) { + OWSBubbleStrokeView *bubbleStrokeView = [OWSBubbleStrokeView new]; + bubbleStrokeView.strokeThickness = 1.f; + bubbleStrokeView.strokeColor = [UIColor colorWithWhite:0.f alpha:0.1f]; + bubbleStrokeView.bubbleView = self.bubbleView; + + [self.bubbleView addSubview:bubbleStrokeView]; + [bubbleStrokeView autoPinEdge:ALEdgeTop toEdge:ALEdgeTop ofView:bodyMediaView]; + [bubbleStrokeView autoPinEdge:ALEdgeBottom toEdge:ALEdgeBottom ofView:bodyMediaView]; + [bubbleStrokeView autoPinEdge:ALEdgeLeft toEdge:ALEdgeLeft ofView:bodyMediaView]; + [bubbleStrokeView autoPinEdge:ALEdgeRight toEdge:ALEdgeRight ofView:bodyMediaView]; + self.bubbleView.bubbleStrokeView = bubbleStrokeView; + OWSAssert(self.bubbleView.bubbleStrokeView); } } - [self ensureViewMediaState]; + OWSMessageTextView *_Nullable bodyTextView = nil; + // We render malformed messages as "empty text" messages, + // so create a text view if there is no body media view. + if (self.hasBodyText || !bodyMediaView) { + bodyTextView = [self createBodyTextViewIfNecessary]; + } + if (bodyTextView) { + [self.bubbleView addSubview:bodyTextView]; + [self.viewConstraints addObjectsFromArray:@[ + [bodyTextView autoPinLeadingToSuperviewWithMargin:self.textLeadingMargin], + [bodyTextView autoPinTrailingToSuperviewWithMargin:self.textTrailingMargin], + ]]; + // We need constraints to control the vertical sizing of media and text views, but we use + // lower priority so that when a message only contains media it uses the exact bounds of + // the message view. + [NSLayoutConstraint + autoSetPriority:UILayoutPriorityDefaultLow + forConstraints:^{ + [self.viewConstraints + addObject:[bodyTextView autoSetDimension:ALDimensionHeight toSize:bodyTextContentSize.height]]; + }]; + if (lastSubview) { + [self.viewConstraints addObject:[bodyTextView autoPinEdge:ALEdgeTop + toEdge:ALEdgeBottom + ofView:lastSubview + withOffset:self.textTopMargin]]; + } else { + [self.viewConstraints + addObject:[bodyTextView autoPinEdgeToSuperviewEdge:ALEdgeTop withInset:self.textTopMargin]]; + } + lastSubview = bodyTextView; + bottomMargin = self.textBottomMargin; + } + + UIView *_Nullable tapForMoreLabel = [self createTapForMoreLabelIfNecessary]; + if (tapForMoreLabel) { + OWSAssert(lastSubview); + OWSAssert(lastSubview == bodyTextView); + [self.bubbleView addSubview:tapForMoreLabel]; + [self.viewConstraints addObjectsFromArray:@[ + [tapForMoreLabel autoPinLeadingToSuperviewWithMargin:self.textLeadingMargin], + [tapForMoreLabel autoPinTrailingToSuperviewWithMargin:self.textTrailingMargin], + [tapForMoreLabel autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:lastSubview], + [tapForMoreLabel autoSetDimension:ALDimensionHeight toSize:self.tapForMoreHeight], + ]]; + lastSubview = tapForMoreLabel; + bottomMargin = self.textBottomMargin; + } + + OWSAssert(lastSubview); + [self.viewConstraints addObjectsFromArray:@[ + [lastSubview autoPinEdgeToSuperviewEdge:ALEdgeBottom withInset:bottomMargin], + ]]; + + [self ensureMediaLoadState]; } -// We now eagerly create out view hierarchy (to do this exactly once per cell usage) +// We now eagerly create our 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 (^)(void))loadCellMediaBlock mediaView:(UIView *)mediaView cacheKey:(NSString *)cacheKey + shouldSkipCache:(BOOL)shouldSkipCache { OWSAssert(self.attachmentStream); OWSAssert(mediaView); @@ -510,83 +524,33 @@ const CGFloat OWSMessageCellCornerRadius = 17; cellMedia = loadCellMediaBlock(); if (cellMedia) { DDLogVerbose(@"%@ cell media cache miss", self.logTag); - [cellMediaCache setObject:cellMedia forKey:cacheKey]; + if (!shouldSkipCache) { + [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]; + // TODO: Do we need to hide/remove the media view? + [self showAttachmentErrorViewWithMediaView:mediaView]; } return cellMedia; } -// We want to lazy-load expensive view contents and eagerly unload if the -// cell is no longer visible. -- (void)ensureViewMediaState +// * If cell is visible, lazy-load (expensive) view contents. +// * If cell is not visible, eagerly unload view contents. +- (void)ensureMediaLoadState { - CGSize mediaSize = [self mediaBubbleSizeForContentWidth:self.contentWidth]; - [self.contentConstraints addObjectsFromArray:[self.mediaMaskingView autoSetDimensionsToSize:mediaSize]]; - if (!self.isCellVisible) { // Eagerly unload. - self.stillImageView.image = nil; - self.animatedImageView.image = nil; + if (self.unloadCellContentBlock) { + self.unloadCellContentBlock(); + } 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; + } else { + // Lazy load. + if (self.loadCellContentBlock) { + self.loadCellContentBlock(); } - 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: - case OWSMessageCellType_Unknown: - // Inexpensive cell types don't need to lazy-load or eagerly-unload. - break; } } @@ -638,7 +602,7 @@ const CGFloat OWSMessageCellCornerRadius = 17; self.dateHeaderLabel.attributedText = attributedText; self.dateHeaderLabel.hidden = NO; - self.dateHeaderConstraints = @[ + [self.viewConstraints addObjectsFromArray:@[ // 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:self.contentWidth], @@ -646,13 +610,13 @@ const CGFloat OWSMessageCellCornerRadius = 17; : [self.dateHeaderLabel autoPinEdgeToSuperviewEdge:ALEdgeTrailing]), [self.dateHeaderLabel autoPinEdgeToSuperviewEdge:ALEdgeTop], [self.dateHeaderLabel autoSetDimension:ALDimensionHeight toSize:self.dateHeaderHeight], - ]; + ]]; } else { self.dateHeaderLabel.hidden = YES; - self.dateHeaderConstraints = @[ + [self.viewConstraints addObjectsFromArray:@[ [self.dateHeaderLabel autoSetDimension:ALDimensionHeight toSize:0], [self.dateHeaderLabel autoPinEdgeToSuperviewEdge:ALEdgeTop], - ]; + ]]; } } @@ -672,9 +636,7 @@ const CGFloat OWSMessageCellCornerRadius = 17; showFooter = NO; } - return (showFooter ? MAX(kExpirationTimerViewSize, - self.footerLabel.font.lineHeight) - : 0.f); + return (showFooter ? (CGFloat)ceil(MAX(kExpirationTimerViewSize, self.footerLabel.font.lineHeight)) : 0.f); } - (void)updateFooter @@ -700,9 +662,9 @@ const CGFloat OWSMessageCellCornerRadius = 17; if (!hasExpirationTimer && !attributedText) { self.footerLabel.hidden = YES; - self.footerConstraints = @[ - [self.footerView autoSetDimension:ALDimensionHeight toSize:0], - ]; + [self.viewConstraints addObjectsFromArray:@[ + [self.footerView autoSetDimension:ALDimensionHeight toSize:0], + ]]; return; } @@ -720,33 +682,29 @@ const CGFloat OWSMessageCellCornerRadius = 17; if (hasExpirationTimer && attributedText) { - self.footerConstraints = @[ - [self.expirationTimerView autoVCenterInSuperview], - [self.footerLabel autoVCenterInSuperview], - (self.isIncoming - ? [self.expirationTimerView autoPinLeadingToSuperview] - : [self.expirationTimerView autoPinTrailingToSuperview]), - (self.isIncoming - ? [self.footerLabel autoPinLeadingToTrailingOfView:self.expirationTimerView margin:0.f] - : [self.footerLabel autoPinTrailingToLeadingOfView:self.expirationTimerView margin:0.f]), - [self.footerView autoSetDimension:ALDimensionHeight toSize:self.footerHeight], - ]; + [self.viewConstraints addObjectsFromArray:@[ + [self.expirationTimerView autoVCenterInSuperview], + [self.footerLabel autoVCenterInSuperview], + (self.isIncoming ? [self.expirationTimerView autoPinLeadingToSuperview] + : [self.expirationTimerView autoPinTrailingToSuperview]), + (self.isIncoming ? [self.footerLabel autoPinLeadingToTrailingOfView:self.expirationTimerView margin:0.f] + : [self.footerLabel autoPinTrailingToLeadingOfView:self.expirationTimerView margin:0.f]), + [self.footerView autoSetDimension:ALDimensionHeight toSize:self.footerHeight], + ]]; } else if (hasExpirationTimer) { - self.footerConstraints = @[ - [self.expirationTimerView autoVCenterInSuperview], - (self.isIncoming - ? [self.expirationTimerView autoPinLeadingToSuperview] - : [self.expirationTimerView autoPinTrailingToSuperview]), - [self.footerView autoSetDimension:ALDimensionHeight toSize:self.footerHeight], - ]; + [self.viewConstraints addObjectsFromArray:@[ + [self.expirationTimerView autoVCenterInSuperview], + (self.isIncoming ? [self.expirationTimerView autoPinLeadingToSuperview] + : [self.expirationTimerView autoPinTrailingToSuperview]), + [self.footerView autoSetDimension:ALDimensionHeight toSize:self.footerHeight], + ]]; } else if (attributedText) { - self.footerConstraints = @[ - [self.footerLabel autoVCenterInSuperview], - (self.isIncoming - ? [self.footerLabel autoPinLeadingToSuperview] - : [self.footerLabel autoPinTrailingToSuperview]), - [self.footerView autoSetDimension:ALDimensionHeight toSize:self.footerHeight], - ]; + [self.viewConstraints addObjectsFromArray:@[ + [self.footerLabel autoVCenterInSuperview], + (self.isIncoming ? [self.footerLabel autoPinLeadingToSuperview] + : [self.footerLabel autoPinTrailingToSuperview]), + [self.footerView autoSetDimension:ALDimensionHeight toSize:self.footerHeight], + ]]; } else { OWSFail(@"%@ Cell unexpectedly has neither expiration timer nor footer text.", self.logTag); } @@ -762,196 +720,294 @@ const CGFloat OWSMessageCellCornerRadius = 17; return [UIFont systemFontOfSize:12.0f]; } -- (void)loadForTextDisplay +- (OWSMessageTextView *)createBodyTextViewIfNecessary { - self.textBubbleImageView.hidden = NO; - self.textView.hidden = NO; - self.textView.text = self.displayableText.displayText; - self.textView.textColor = self.textColor; - - // Honor dynamic type in the message bodies. - self.textView.font = [self textMessageFont]; - self.textView.linkTextAttributes = @{ - NSForegroundColorAttributeName : self.textColor, - NSUnderlineStyleAttributeName : @(NSUnderlineStyleSingle | NSUnderlinePatternSolid) - }; - self.textView.dataDetectorTypes - = (UIDataDetectorTypeLink | UIDataDetectorTypeAddress | UIDataDetectorTypeCalendarEvent); - + BOOL shouldIgnoreEvents = NO; if (self.viewItem.interaction.interactionType == OWSInteractionType_OutgoingMessage) { // Ignore taps on links in outgoing messages that haven't been sent yet, as // this interferes with "tap to retry". TSOutgoingMessage *outgoingMessage = (TSOutgoingMessage *)self.viewItem.interaction; - self.textView.shouldIgnoreEvents = outgoingMessage.messageState != TSOutgoingMessageStateSentToService; - } else { - self.textView.shouldIgnoreEvents = NO; + shouldIgnoreEvents = outgoingMessage.messageState != TSOutgoingMessageStateSentToService; } + [self.class loadForTextDisplay:self.bodyTextView + text:self.displayableBodyText.displayText + textColor:self.textColor + font:self.textMessageFont + shouldIgnoreEvents:shouldIgnoreEvents]; + return self.bodyTextView; +} - OWSAssert(self.contentWidth); - CGSize textBubbleSize = [self textBubbleSizeForContentWidth:self.contentWidth]; ++ (void)loadForTextDisplay:(OWSMessageTextView *)textView + text:(NSString *)text + textColor:(UIColor *)textColor + font:(UIFont *)font + shouldIgnoreEvents:(BOOL)shouldIgnoreEvents +{ + textView.hidden = NO; + textView.text = text; + textView.textColor = textColor; - if (self.displayableText.isTextTruncated) { - self.tapForMoreLabel = [UILabel new]; - self.tapForMoreLabel.text = NSLocalizedString(@"CONVERSATION_VIEW_OVERSIZE_TEXT_TAP_FOR_MORE", - @"Indicator on truncated text messages that they can be tapped to see the entire text message."); - self.tapForMoreLabel.font = [self tapForMoreFont]; - self.tapForMoreLabel.textColor = [self.textColor colorWithAlphaComponent:0.85]; - self.tapForMoreLabel.textAlignment = [self.tapForMoreLabel textAlignmentUnnatural]; - [self.textBubbleImageView addSubview:self.tapForMoreLabel]; + // Honor dynamic type in the message bodies. + textView.font = font; + textView.linkTextAttributes = @{ + NSForegroundColorAttributeName : textColor, + NSUnderlineStyleAttributeName : @(NSUnderlineStyleSingle | NSUnderlinePatternSolid) + }; + textView.shouldIgnoreEvents = shouldIgnoreEvents; +} - [self.contentConstraints addObjectsFromArray:@[ - [self.textBubbleImageView autoSetDimension:ALDimensionWidth toSize:textBubbleSize.width], - [self.textBubbleImageView autoSetDimension:ALDimensionHeight toSize:textBubbleSize.height], - [self.textView autoPinLeadingToSuperviewWithMargin:self.textLeadingMargin], - [self.textView autoPinTrailingToSuperviewWithMargin:self.textTrailingMargin], - [self.textView autoPinEdgeToSuperviewEdge:ALEdgeTop withInset:self.textVMargin], - - [self.tapForMoreLabel autoPinLeadingToSuperviewWithMargin:self.textLeadingMargin], - [self.tapForMoreLabel autoPinTrailingToSuperviewWithMargin:self.textTrailingMargin], - [self.tapForMoreLabel autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:self.textView], - [self.tapForMoreLabel autoPinEdgeToSuperviewEdge:ALEdgeBottom withInset:self.textVMargin], - [self.tapForMoreLabel autoSetDimension:ALDimensionHeight toSize:self.tapForMoreHeight], - ]]; +- (BOOL)hasTapForMore +{ + if (!self.hasBodyText) { + return NO; + } else if (!self.displayableBodyText.isTextTruncated) { + return NO; } else { - [self.contentConstraints addObjectsFromArray:@[ - [self.textBubbleImageView autoSetDimension:ALDimensionWidth toSize:textBubbleSize.width], - [self.textBubbleImageView autoSetDimension:ALDimensionHeight toSize:textBubbleSize.height], - [self.textBubbleImageView autoPinEdgeToSuperviewEdge:(self.isIncoming ? ALEdgeLeading : ALEdgeTrailing)], - [self.textView autoPinLeadingToSuperviewWithMargin:self.textLeadingMargin], - [self.textView autoPinTrailingToSuperviewWithMargin:self.textTrailingMargin], - [self.textView autoPinEdgeToSuperviewEdge:ALEdgeTop withInset:self.textVMargin], - [self.textView autoPinEdgeToSuperviewEdge:ALEdgeBottom withInset:self.textVMargin], - ]]; + return YES; } } -- (void)loadForStillImageDisplay +- (nullable UIView *)createTapForMoreLabelIfNecessary +{ + if (!self.hasTapForMore) { + return nil; + } + + UILabel *tapForMoreLabel = [UILabel new]; + tapForMoreLabel.text = NSLocalizedString(@"CONVERSATION_VIEW_OVERSIZE_TEXT_TAP_FOR_MORE", + @"Indicator on truncated text messages that they can be tapped to see the entire text message."); + tapForMoreLabel.font = [self tapForMoreFont]; + tapForMoreLabel.textColor = [self.textColor colorWithAlphaComponent:0.85]; + tapForMoreLabel.textAlignment = [tapForMoreLabel textAlignmentUnnatural]; + + return tapForMoreLabel; +} + +- (UIView *)loadViewForStillImage { OWSAssert(self.attachmentStream); OWSAssert([self.attachmentStream isImage]); - self.stillImageView = [UIImageView new]; + UIImageView *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; + stillImageView.contentMode = UIViewContentModeScaleAspectFill; // Use trilinear filters for better scaling quality at // some performance cost. - self.stillImageView.layer.minificationFilter = kCAFilterTrilinear; - self.stillImageView.layer.magnificationFilter = kCAFilterTrilinear; - [self setMediaView:self.stillImageView]; - [self addAttachmentUploadViewIfNecessary:self.stillImageView]; - [self addCaptionIfNecessary]; + stillImageView.layer.minificationFilter = kCAFilterTrilinear; + stillImageView.layer.magnificationFilter = kCAFilterTrilinear; + [self addAttachmentUploadViewIfNecessary:stillImageView]; + + __weak OWSMessageCell *weakSelf = self; + self.loadCellContentBlock = ^{ + OWSMessageCell *strongSelf = weakSelf; + if (!strongSelf) { + return; + } + OWSCAssert(strongSelf.lastBodyMediaView == stillImageView); + if (stillImageView.image) { + return; + } + // Don't cache large still images. + // + // TODO: Don't use full size images in the message cells. + const NSUInteger kMaxCachableSize = 1024 * 1024; + BOOL shouldSkipCache = + [OWSFileSystem fileSizeOfPath:strongSelf.attachmentStream.filePath].unsignedIntegerValue < kMaxCachableSize; + stillImageView.image = [strongSelf tryToLoadCellMedia:^{ + OWSCAssert([strongSelf.attachmentStream isImage]); + return strongSelf.attachmentStream.image; + } + mediaView:stillImageView + cacheKey:strongSelf.attachmentStream.uniqueId + shouldSkipCache:shouldSkipCache]; + }; + self.unloadCellContentBlock = ^{ + OWSMessageCell *strongSelf = weakSelf; + if (!strongSelf) { + return; + } + OWSCAssert(strongSelf.lastBodyMediaView == stillImageView); + stillImageView.image = nil; + }; + + return stillImageView; } -- (void)loadForAnimatedImageDisplay +- (UIView *)loadViewForAnimatedImage { OWSAssert(self.attachmentStream); OWSAssert([self.attachmentStream isAnimated]); - self.animatedImageView = [[YYAnimatedImageView alloc] init]; + YYAnimatedImageView *animatedImageView = [[YYAnimatedImageView alloc] init]; // 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; - [self setMediaView:self.animatedImageView]; - [self addAttachmentUploadViewIfNecessary:self.animatedImageView]; - [self addCaptionIfNecessary]; + animatedImageView.contentMode = UIViewContentModeScaleAspectFill; + [self addAttachmentUploadViewIfNecessary:animatedImageView]; + + __weak OWSMessageCell *weakSelf = self; + self.loadCellContentBlock = ^{ + OWSMessageCell *strongSelf = weakSelf; + if (!strongSelf) { + return; + } + OWSCAssert(strongSelf.lastBodyMediaView == animatedImageView); + if (animatedImageView.image) { + return; + } + animatedImageView.image = [strongSelf tryToLoadCellMedia:^{ + OWSCAssert([strongSelf.attachmentStream isAnimated]); + + NSString *_Nullable filePath = [strongSelf.attachmentStream filePath]; + YYImage *_Nullable animatedImage = nil; + if (filePath && [NSData ows_isValidImageAtPath:filePath]) { + animatedImage = [YYImage imageWithContentsOfFile:filePath]; + } + return animatedImage; + } + mediaView:animatedImageView + cacheKey:strongSelf.attachmentStream.uniqueId + shouldSkipCache:NO]; + }; + self.unloadCellContentBlock = ^{ + OWSMessageCell *strongSelf = weakSelf; + if (!strongSelf) { + return; + } + OWSCAssert(strongSelf.lastBodyMediaView == animatedImageView); + animatedImageView.image = nil; + }; + + return animatedImageView; } -- (void)addCaptionIfNecessary -{ - if (self.hasText) { - [self loadForTextDisplay]; - } else { - [self.contentConstraints addObject:[self.textBubbleImageView autoSetDimension:ALDimensionHeight toSize:0]]; - } -} - -- (void)loadForAudioDisplay +- (UIView *)loadViewForAudio { OWSAssert(self.attachmentStream); OWSAssert([self.attachmentStream isAudio]); - self.audioMessageView = [[OWSAudioMessageView alloc] initWithAttachment:self.attachmentStream - isIncoming:self.isIncoming - viewItem:self.viewItem]; - self.viewItem.lastAudioMessageView = self.audioMessageView; - [self.audioMessageView createContents]; - [self setMediaView:self.audioMessageView]; - [self addAttachmentUploadViewIfNecessary:self.audioMessageView]; - [self addCaptionIfNecessary]; + OWSAudioMessageView *audioMessageView = [[OWSAudioMessageView alloc] initWithAttachment:self.attachmentStream + isIncoming:self.isIncoming + viewItem:self.viewItem]; + self.viewItem.lastAudioMessageView = audioMessageView; + [audioMessageView createContents]; + [self addAttachmentUploadViewIfNecessary:audioMessageView]; + + self.loadCellContentBlock = ^{ + // Do nothing. + }; + self.unloadCellContentBlock = ^{ + // Do nothing. + }; + + return audioMessageView; } -- (void)loadForVideoDisplay +- (UIView *)loadViewForVideo { OWSAssert(self.attachmentStream); OWSAssert([self.attachmentStream isVideo]); - self.stillImageView = [UIImageView new]; + UIImageView *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; + stillImageView.contentMode = UIViewContentModeScaleAspectFill; // Use trilinear filters for better scaling quality at // some performance cost. - self.stillImageView.layer.minificationFilter = kCAFilterTrilinear; - self.stillImageView.layer.magnificationFilter = kCAFilterTrilinear; - [self setMediaView:self.stillImageView]; + stillImageView.layer.minificationFilter = kCAFilterTrilinear; + stillImageView.layer.magnificationFilter = kCAFilterTrilinear; UIImage *videoPlayIcon = [UIImage imageNamed:@"play_button"]; UIImageView *videoPlayButton = [[UIImageView alloc] initWithImage:videoPlayIcon]; - [self.stillImageView addSubview:videoPlayButton]; + [stillImageView addSubview:videoPlayButton]; [videoPlayButton autoCenterInSuperview]; - [self addAttachmentUploadViewIfNecessary:self.stillImageView + [self addAttachmentUploadViewIfNecessary:stillImageView attachmentStateCallback:^(BOOL isAttachmentReady) { videoPlayButton.hidden = !isAttachmentReady; }]; - [self addCaptionIfNecessary]; + + __weak OWSMessageCell *weakSelf = self; + self.loadCellContentBlock = ^{ + OWSMessageCell *strongSelf = weakSelf; + if (!strongSelf) { + return; + } + OWSCAssert(strongSelf.lastBodyMediaView == stillImageView); + if (stillImageView.image) { + return; + } + stillImageView.image = [strongSelf tryToLoadCellMedia:^{ + OWSCAssert([strongSelf.attachmentStream isVideo]); + + return strongSelf.attachmentStream.image; + } + mediaView:stillImageView + cacheKey:strongSelf.attachmentStream.uniqueId + shouldSkipCache:NO]; + }; + self.unloadCellContentBlock = ^{ + OWSMessageCell *strongSelf = weakSelf; + if (!strongSelf) { + return; + } + OWSCAssert(strongSelf.lastBodyMediaView == stillImageView); + stillImageView.image = nil; + }; + + return stillImageView; } -- (void)loadForDownloadingAttachment +- (UIView *)loadViewForGenericAttachment +{ + OWSAssert(self.viewItem.attachmentStream); + OWSGenericAttachmentView *attachmentView = + [[OWSGenericAttachmentView alloc] initWithAttachment:self.attachmentStream isIncoming:self.isIncoming]; + [attachmentView createContents]; + [self addAttachmentUploadViewIfNecessary:attachmentView]; + + self.loadCellContentBlock = ^{ + // Do nothing. + }; + self.unloadCellContentBlock = ^{ + // Do nothing. + }; + + return attachmentView; +} + +- (UIView *)loadViewForDownloadingAttachment { OWSAssert(self.attachmentPointer); - self.customView = [UIView new]; + UIView *customView = [UIView new]; switch (self.attachmentPointer.state) { case TSAttachmentPointerStateEnqueued: - self.customView.backgroundColor + customView.backgroundColor = (self.isIncoming ? [UIColor jsq_messageBubbleLightGrayColor] : [UIColor ows_fadedBlueColor]); break; case TSAttachmentPointerStateDownloading: - self.customView.backgroundColor + customView.backgroundColor = (self.isIncoming ? [UIColor jsq_messageBubbleLightGrayColor] : [UIColor ows_fadedBlueColor]); break; case TSAttachmentPointerStateFailed: - self.customView.backgroundColor = [UIColor grayColor]; + customView.backgroundColor = [UIColor grayColor]; break; } - [self setMediaView:self.customView]; - self.attachmentPointerView = + AttachmentPointerView *attachmentPointerView = [[AttachmentPointerView alloc] initWithAttachmentPointer:self.attachmentPointer isIncoming:self.isIncoming]; - [self.customView addSubview:self.attachmentPointerView]; - [self.attachmentPointerView autoPinWidthToSuperviewWithMargin:20.f]; - [self.attachmentPointerView autoVCenterInSuperview]; - [self addCaptionIfNecessary]; -} + [customView addSubview:attachmentPointerView]; + [attachmentPointerView autoPinWidthToSuperviewWithMargin:20.f]; + [attachmentPointerView autoVCenterInSuperview]; -- (void)setMediaView:(UIView *)view -{ - OWSAssert(view); + self.loadCellContentBlock = ^{ + // Do nothing. + }; + self.unloadCellContentBlock = ^{ + // Do nothing. + }; - view.userInteractionEnabled = NO; - [self.mediaMaskingView addSubview:view]; - - [self.contentConstraints - addObject:[self.mediaMaskingView - autoPinEdgeToSuperviewEdge:(self.isIncoming ? ALEdgeLeading : ALEdgeTrailing)]]; - - [self.contentConstraints addObjectsFromArray:[view autoPinEdgesToSuperviewMargins]]; - - [self cropMediaViewToBubbbleShape:view]; - if (self.isMediaBeingSent) { - view.layer.opacity = 0.75f; - } + return customView; } - (void)addAttachmentUploadViewIfNecessary:(UIView *)attachmentView @@ -970,63 +1026,58 @@ const CGFloat OWSMessageCellCornerRadius = 17; if (self.isOutgoing) { if (!self.attachmentStream.isUploaded) { - self.attachmentUploadView = [[AttachmentUploadView alloc] initWithAttachment:self.attachmentStream - superview:attachmentView - attachmentStateCallback:attachmentStateCallback]; + AttachmentUploadView *attachmentUploadView = + [[AttachmentUploadView alloc] initWithAttachment:self.attachmentStream + attachmentStateCallback:attachmentStateCallback]; + [attachmentView addSubview:attachmentUploadView]; + [attachmentUploadView autoPinToSuperviewEdges]; } } } -- (void)cropMediaViewToBubbbleShape:(UIView *)view +- (void)showAttachmentErrorViewWithMediaView:(UIView *)mediaView { - OWSAssert(view); - OWSAssert(view.superview == self.mediaMaskingView); - - self.mediaMaskingView.isOutgoing = self.isOutgoing; - // Hide tail on attachments followed by a caption - self.mediaMaskingView.hideTail = self.hasText; - self.mediaMaskingView.maskedSubview = view; - [self.mediaMaskingView updateMask]; -} - -- (void)showAttachmentErrorView -{ - OWSAssert(!self.customView); + OWSAssert(mediaView); // 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; - [self.payloadView addSubview:self.customView]; - [self.contentConstraints addObjectsFromArray:[self.customView autoPinToSuperviewEdges]]; + UIView *errorView = [UIView new]; + errorView.backgroundColor = [UIColor colorWithWhite:0.85f alpha:1.f]; + errorView.userInteractionEnabled = NO; + [mediaView addSubview:errorView]; + [errorView autoPinEdgesToSuperviewEdges]; } -- (CGSize)textBubbleSizeForContentWidth:(int)contentWidth +#pragma mark - Measurement + +// Size of "message body" text, not quoted reply text. +- (CGSize)bodyTextSizeForContentWidth:(int)contentWidth includeMargins:(BOOL)includeMargins { - if (!self.hasText) { + if (!self.hasBodyText) { return CGSizeZero; } BOOL isRTL = self.isRTL; CGFloat leftMargin = isRTL ? self.textTrailingMargin : self.textLeadingMargin; CGFloat rightMargin = isRTL ? self.textLeadingMargin : self.textTrailingMargin; - CGFloat textVMargin = self.textVMargin; const int maxMessageWidth = [self maxMessageWidthForContentWidth:contentWidth]; const int maxTextWidth = (int)floor(maxMessageWidth - (leftMargin + rightMargin)); - self.textView.text = self.displayableText.displayText; + self.bodyTextView.text = self.displayableBodyText.displayText; // Honor dynamic type in the message bodies. - self.textView.font = [self textMessageFont]; - CGSize textSize = [self.textView sizeThatFits:CGSizeMake(maxTextWidth, CGFLOAT_MAX)]; - CGFloat tapForMoreHeight = (self.displayableText.isTextTruncated ? [self tapForMoreHeight] : 0.f); - CGSize textViewSize = CGSizeMake((CGFloat)ceil(textSize.width + leftMargin + rightMargin), - (CGFloat)ceil(textSize.height + textVMargin * 2 + tapForMoreHeight)); + self.bodyTextView.font = [self textMessageFont]; + CGSize textSize = CGSizeCeil([self.bodyTextView sizeThatFits:CGSizeMake(maxTextWidth, CGFLOAT_MAX)]); + CGSize textViewSize = textSize; - return textViewSize; + if (includeMargins) { + textViewSize.width += leftMargin + rightMargin; + textViewSize.height += self.textTopMargin + self.textBottomMargin; + } + + return CGSizeCeil(textViewSize); } -- (CGSize)mediaBubbleSizeForContentWidth:(int)contentWidth +- (CGSize)bodyMediaSizeForContentWidth:(int)contentWidth { const int maxMessageWidth = [self maxMessageWidthForContentWidth:contentWidth]; @@ -1043,13 +1094,11 @@ const CGFloat OWSMessageCellCornerRadius = 17; OWSAssert(self.mediaSize.height > 0); // TODO: Adjust this behavior. - // TODO: This behavior is a bit different than the old behavior defined - // in JSQMediaItem+OWS.h. Let's discuss. CGFloat contentAspectRatio = self.mediaSize.width / self.mediaSize.height; // Clamp the aspect ratio so that very thin/wide content is presented // in a reasonable way. - const CGFloat minAspectRatio = 0.25f; + const CGFloat minAspectRatio = 0.35f; const CGFloat maxAspectRatio = 1 / minAspectRatio; contentAspectRatio = MAX(minAspectRatio, MIN(maxAspectRatio, contentAspectRatio)); @@ -1082,8 +1131,8 @@ const CGFloat OWSMessageCellCornerRadius = 17; OWSAssert(self.viewItem); OWSAssert([self.viewItem.interaction isKindOfClass:[TSMessage class]]); - CGSize mediaContentSize = [self mediaBubbleSizeForContentWidth:contentWidth]; - CGSize textContentSize = [self textBubbleSizeForContentWidth:contentWidth]; + CGSize mediaContentSize = [self bodyMediaSizeForContentWidth:contentWidth]; + CGSize textContentSize = [self bodyTextSizeForContentWidth:contentWidth includeMargins:YES]; CGFloat cellContentWidth = fmax(mediaContentSize.width, textContentSize.width); CGFloat cellContentHeight = mediaContentSize.height + textContentSize.height; @@ -1093,13 +1142,15 @@ const CGFloat OWSMessageCellCornerRadius = 17; cellSize.height += self.dateHeaderHeight; cellSize.height += self.footerHeight; + if (self.hasTapForMore) { + cellSize.height += self.tapForMoreHeight; + } if (self.shouldHaveFailedSendBadge) { cellSize.width += self.failedSendBadgeSize; } - cellSize.width = ceil(cellSize.width); - cellSize.height = ceil(cellSize.height); + cellSize = CGSizeCeil(cellSize); return cellSize; } @@ -1108,12 +1159,14 @@ const CGFloat OWSMessageCellCornerRadius = 17; { if (self.viewItem.shouldShowDate) { // Add 5pt spacing above and below the date header. - return MAX(self.dateHeaderDateFont.lineHeight, self.dateHeaderTimeFont.lineHeight) + 10.f; + return (CGFloat)ceil(MAX(self.dateHeaderDateFont.lineHeight, self.dateHeaderTimeFont.lineHeight) + 10.f); } else { return 0.f; } } +#pragma mark - + - (BOOL)isIncoming { return self.viewItem.interaction.interactionType == OWSInteractionType_IncomingMessage; @@ -1126,17 +1179,30 @@ const CGFloat OWSMessageCellCornerRadius = 17; - (CGFloat)textLeadingMargin { - return self.isIncoming ? 15 : 10; + CGFloat result = kBubbleTextHInset; + if (self.isIncoming) { + result += kBubbleThornSideInset; + } + return result; } - (CGFloat)textTrailingMargin { - return self.isIncoming ? 10 : 15; + CGFloat result = kBubbleTextHInset; + if (!self.isIncoming) { + result += kBubbleThornSideInset; + } + return result; } -- (CGFloat)textVMargin +- (CGFloat)textTopMargin { - return 10; + return kBubbleTextVInset; +} + +- (CGFloat)textBottomMargin +{ + return kBubbleTextVInset + kBubbleThornVInset; } - (UIColor *)textColor @@ -1168,50 +1234,40 @@ const CGFloat OWSMessageCellCornerRadius = 17; { [super prepareForReuse]; - [NSLayoutConstraint deactivateConstraints:self.payloadConstraints]; - self.payloadConstraints = nil; - [NSLayoutConstraint deactivateConstraints:self.contentConstraints]; - self.contentConstraints = [NSMutableArray new]; - [NSLayoutConstraint deactivateConstraints:self.dateHeaderConstraints]; - self.dateHeaderConstraints = nil; - [NSLayoutConstraint deactivateConstraints:self.footerConstraints]; - self.footerConstraints = nil; + [NSLayoutConstraint deactivateConstraints:self.viewConstraints]; + self.viewConstraints = [NSMutableArray new]; self.dateHeaderLabel.text = nil; self.dateHeaderLabel.hidden = YES; - self.textView.text = nil; - self.textView.hidden = YES; - self.textView.dataDetectorTypes = UIDataDetectorTypeNone; + [self.bodyTextView removeFromSuperview]; + self.bodyTextView.text = nil; + self.bodyTextView.hidden = YES; [self.failedSendBadgeView removeFromSuperview]; self.failedSendBadgeView = nil; - [self.tapForMoreLabel removeFromSuperview]; - self.tapForMoreLabel = nil; self.footerLabel.text = nil; self.footerLabel.hidden = YES; - self.textBubbleImageView.image = nil; - self.textBubbleImageView.hidden = YES; - self.mediaMaskingView.maskedSubview = nil; - self.mediaMaskingView.hideTail = NO; - self.mediaMaskingView.layoutMargins = UIEdgeInsetsZero; - [self.stillImageView removeFromSuperview]; - self.stillImageView = nil; - [self.animatedImageView removeFromSuperview]; - self.animatedImageView = nil; - [self.customView removeFromSuperview]; - self.customView = nil; - [self.attachmentPointerView removeFromSuperview]; - self.attachmentPointerView = nil; - [self.attachmentView removeFromSuperview]; - self.attachmentView = nil; - [self.audioMessageView removeFromSuperview]; - self.audioMessageView = nil; - [self.attachmentUploadView removeFromSuperview]; - self.attachmentUploadView = nil; + self.bubbleView.hidden = YES; + self.bubbleView.bubbleColor = nil; + self.bubbleView.bubbleStrokeView = nil; + + for (UIView *subview in self.bubbleView.subviews) { + [subview removeFromSuperview]; + } + + if (self.unloadCellContentBlock) { + self.unloadCellContentBlock(); + } + self.loadCellContentBlock = nil; + self.unloadCellContentBlock = nil; + [self.expirationTimerView clearAnimations]; [self.expirationTimerView removeFromSuperview]; self.expirationTimerView = nil; + [self.lastBodyMediaView removeFromSuperview]; + self.lastBodyMediaView = nil; + [self hideMenuControllerIfNecessary]; } @@ -1226,7 +1282,7 @@ const CGFloat OWSMessageCellCornerRadius = 17; return; } - [self ensureViewMediaState]; + [self ensureMediaLoadState]; if (isCellVisible) { if (self.message.shouldStartExpireTimer) { @@ -1243,6 +1299,30 @@ const CGFloat OWSMessageCellCornerRadius = 17; #pragma mark - Gesture recognizers +- (void)handleTapGesture:(UITapGestureRecognizer *)sender +{ + OWSAssert(self.delegate); + + if (sender.state != UIGestureRecognizerStateRecognized) { + DDLogVerbose(@"%@ Ignoring tap on message: %@", self.logTag, self.viewItem.interaction.debugDescription); + return; + } + + if (self.lastBodyMediaView) { + // Treat this as a "body media" gesture if: + // + // * There is a "body media" view. + // * The gesture occured within or above the "body media" view. + CGPoint location = [sender locationInView:self.lastBodyMediaView]; + if (location.y <= self.lastBodyMediaView.height) { + [self handleMediaTapGesture:sender]; + return; + } + } + + [self handleTextTapGesture:sender]; +} + - (void)handleTextTapGesture:(UITapGestureRecognizer *)sender { OWSAssert(self.delegate); @@ -1263,7 +1343,7 @@ const CGFloat OWSMessageCellCornerRadius = 17; } } - if (self.hasText && self.displayableText.isTextTruncated) { + if (self.hasTapForMore) { [self.delegate didTapTruncatedTextMessage:self.viewItem]; return; } @@ -1294,28 +1374,31 @@ const CGFloat OWSMessageCellCornerRadius = 17; break; case OWSMessageCellType_TextMessage: case OWSMessageCellType_OversizeTextMessage: - if (self.displayableText.isTextTruncated) { + if (self.hasTapForMore) { [self.delegate didTapTruncatedTextMessage:self.viewItem]; return; } break; case OWSMessageCellType_StillImage: + OWSAssert(self.lastBodyMediaView); [self.delegate didTapImageViewItem:self.viewItem attachmentStream:self.attachmentStream - imageView:self.stillImageView]; + imageView:self.lastBodyMediaView]; break; case OWSMessageCellType_AnimatedImage: + OWSAssert(self.lastBodyMediaView); [self.delegate didTapImageViewItem:self.viewItem attachmentStream:self.attachmentStream - imageView:self.animatedImageView]; + imageView:self.lastBodyMediaView]; break; case OWSMessageCellType_Audio: [self.delegate didTapAudioViewItem:self.viewItem attachmentStream:self.attachmentStream]; return; case OWSMessageCellType_Video: + OWSAssert(self.lastBodyMediaView); [self.delegate didTapVideoViewItem:self.viewItem attachmentStream:self.attachmentStream - imageView:self.stillImageView]; + imageView:self.lastBodyMediaView]; return; case OWSMessageCellType_GenericAttachment: [AttachmentSharing showShareUIForAttachment:self.attachmentStream]; @@ -1330,6 +1413,29 @@ const CGFloat OWSMessageCellCornerRadius = 17; } } +- (void)handleLongPressGesture:(UILongPressGestureRecognizer *)sender +{ + OWSAssert(self.delegate); + + if (sender.state != UIGestureRecognizerStateBegan) { + return; + } + + if (self.lastBodyMediaView) { + // Treat this as a "body media" gesture if: + // + // * There is a "body media" view. + // * The gesture occured within or above the "body media" view. + CGPoint location = [sender locationInView:self.lastBodyMediaView]; + if (location.y <= self.lastBodyMediaView.height) { + [self handleMediaLongPressGesture:sender]; + return; + } + } + + [self handleTextLongPressGesture:sender]; +} + - (void)handleTextLongPressGesture:(UILongPressGestureRecognizer *)sender { OWSAssert(self.delegate); diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageTextView.h b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageTextView.h new file mode 100644 index 0000000000..6b7294f8c8 --- /dev/null +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageTextView.h @@ -0,0 +1,13 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +NS_ASSUME_NONNULL_BEGIN + +@interface OWSMessageTextView : UITextView + +@property (nonatomic) BOOL shouldIgnoreEvents; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageTextView.m b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageTextView.m new file mode 100644 index 0000000000..f172de9d51 --- /dev/null +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageTextView.m @@ -0,0 +1,65 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "OWSMessageTextView.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation OWSMessageTextView + +// Our message text views are never used for editing; +// suppress their ability to become first responder +// so that tapping on them doesn't hide keyboard. +- (BOOL)canBecomeFirstResponder +{ + return NO; +} + +// Ignore interactions with the text view _except_ taps on links. +// +// We want to disable "partial" selection of text in the message +// and we want to enable "tap to resend" by tapping on a message. +- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *_Nullable)event +{ + if (self.shouldIgnoreEvents) { + // We ignore all events for failed messages so that users + // can tap-to-resend even "all link" messages. + return NO; + } + + // Find the nearest text position to the event. + UITextPosition *_Nullable position = [self closestPositionToPoint:point]; + if (!position) { + return NO; + } + // Find the range of the character in the text which contains the event. + // + // Try every layout direction (this might not be necessary). + UITextRange *_Nullable range = nil; + for (NSNumber *textLayoutDirection in @[ + @(UITextLayoutDirectionLeft), + @(UITextLayoutDirectionRight), + @(UITextLayoutDirectionUp), + @(UITextLayoutDirectionDown), + ]) { + range = [self.tokenizer rangeEnclosingPosition:position + withGranularity:UITextGranularityCharacter + inDirection:(UITextDirection)textLayoutDirection.intValue]; + if (range) { + break; + } + } + if (!range) { + return NO; + } + // Ignore the event unless it occurred inside a link. + NSInteger startIndex = [self offsetFromPosition:self.beginningOfDocument toPosition:range.start]; + BOOL result = + [self.attributedText attribute:NSLinkAttributeName atIndex:(NSUInteger)startIndex effectiveRange:nil] != nil; + return result; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m index 241a1c1ca3..8854eb3f62 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m @@ -2274,6 +2274,14 @@ typedef enum : NSUInteger { - (void)scrollDownButtonTapped { +#ifdef DEBUG + CGPoint contentOffset = self.collectionView.contentOffset; + contentOffset.y += self.collectionView.height + - (self.collectionView.contentInset.top + self.collectionView.contentInset.bottom); + [self.collectionView setContentOffset:contentOffset animated:NO]; + return; +#endif + NSIndexPath *indexPathOfUnreadMessagesIndicator = [self indexPathOfUnreadMessagesIndicator]; if (indexPathOfUnreadMessagesIndicator != nil) { NSInteger unreadRow = indexPathOfUnreadMessagesIndicator.row; @@ -4455,8 +4463,10 @@ typedef enum : NSUInteger { // Update the "shouldShowDate" property of the view items. OWSInteractionType lastInteractionType = OWSInteractionType_Unknown; MessageRecipientStatus lastRecipientStatus = MessageRecipientStatusUploading; + NSString *_Nullable lastIncomingSenderId = nil; for (ConversationViewItem *viewItem in viewItems.reverseObjectEnumerator) { BOOL shouldHideRecipientStatus = NO; + BOOL shouldHideBubbleTail = NO; OWSInteractionType interactionType = viewItem.interaction.interactionType; if (interactionType == OWSInteractionType_OutgoingMessage) { @@ -4465,14 +4475,23 @@ typedef enum : NSUInteger { [MessageRecipientStatusUtils recipientStatusWithOutgoingMessage:outgoingMessage]; if (outgoingMessage.messageState == TSOutgoingMessageStateUnsent) { - // always sow "failed to send" status + // always show "failed to send" status shouldHideRecipientStatus = NO; } else { shouldHideRecipientStatus = (interactionType == lastInteractionType && recipientStatus == lastRecipientStatus); } + shouldHideBubbleTail = (interactionType == lastInteractionType && recipientStatus == lastRecipientStatus); + lastRecipientStatus = recipientStatus; + } else if (interactionType == OWSInteractionType_IncomingMessage) { + TSIncomingMessage *incomingMessage = (TSIncomingMessage *)viewItem.interaction; + NSString *incomingSenderId = incomingMessage.authorId; + OWSAssert(incomingSenderId.length > 0); + shouldHideBubbleTail = (interactionType == lastInteractionType && + [NSObject isNullableObject:lastIncomingSenderId equalTo:incomingSenderId]); + lastIncomingSenderId = incomingSenderId; } lastInteractionType = interactionType; @@ -4483,6 +4502,7 @@ typedef enum : NSUInteger { [rowsThatChangedSize addObject:@(viewItem.previousRow)]; } viewItem.shouldHideRecipientStatus = shouldHideRecipientStatus; + viewItem.shouldHideBubbleTail = shouldHideBubbleTail; } self.viewItems = viewItems; diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewItem.h b/Signal/src/ViewControllers/ConversationView/ConversationViewItem.h index 8f7e345c81..0ecf54be2f 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewItem.h +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewItem.h @@ -43,9 +43,10 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType); @property (nonatomic, readonly) TSInteraction *interaction; @property (nonatomic, readonly) BOOL isGroupThread; -@property (nonatomic, readonly) BOOL hasText; +@property (nonatomic, readonly) BOOL hasBodyText; @property (nonatomic) BOOL shouldShowDate; @property (nonatomic) BOOL shouldHideRecipientStatus; +@property (nonatomic) BOOL shouldHideBubbleTail; @property (nonatomic) NSInteger row; // During updates, we sometimes need the previous row index @@ -79,7 +80,7 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType); // These methods only apply to text & attachment messages. - (OWSMessageCellType)messageCellType; -- (nullable DisplayableText *)displayableText; +- (nullable DisplayableText *)displayableBodyText; - (nullable TSAttachmentStream *)attachmentStream; - (nullable TSAttachmentPointer *)attachmentPointer; - (CGSize)mediaSize; diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewItem.m b/Signal/src/ViewControllers/ConversationView/ConversationViewItem.m index 4797175936..da1d4badc8 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewItem.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewItem.m @@ -55,11 +55,10 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType) @property (nonatomic) BOOL hasViewState; @property (nonatomic) OWSMessageCellType messageCellType; -@property (nonatomic, nullable) DisplayableText *displayableText; +@property (nonatomic, nullable) DisplayableText *displayableBodyText; @property (nonatomic, nullable) TSAttachmentStream *attachmentStream; @property (nonatomic, nullable) TSAttachmentPointer *attachmentPointer; @property (nonatomic) CGSize mediaSize; -@property (nonatomic) BOOL hasText; @end @@ -95,7 +94,7 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType) self.hasViewState = NO; self.messageCellType = OWSMessageCellType_Unknown; - self.displayableText = nil; + self.displayableBodyText = nil; self.attachmentStream = nil; self.attachmentPointer = nil; self.mediaSize = CGSizeZero; @@ -105,6 +104,11 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType) [self ensureViewState:transaction]; } +- (BOOL)hasBodyText +{ + return _displayableBodyText != nil; +} + - (void)setShouldShowDate:(BOOL)shouldShowDate { if (_shouldShowDate == shouldShowDate) { @@ -127,6 +131,17 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType) [self clearCachedLayoutState]; } +- (void)setShouldHideBubbleTail:(BOOL)shouldHideBubbleTail +{ + if (_shouldHideBubbleTail == shouldHideBubbleTail) { + return; + } + + _shouldHideBubbleTail = shouldHideBubbleTail; + + [self clearCachedLayoutState]; +} + - (void)clearCachedLayoutState { self.cachedCellSize = nil; @@ -264,7 +279,7 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType) // TODO: Now that we're caching the displayable text on the view items, // I don't think we need this cache any more. -- (NSCache *)displayableTextCache +- (NSCache *)displayableBodyTextCache { static NSCache *cache = nil; static dispatch_once_t onceToken; @@ -276,44 +291,45 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType) return cache; } -- (DisplayableText *)displayableTextForText:(NSString *)text interactionId:(NSString *)interactionId +- (DisplayableText *)displayableBodyTextForText:(NSString *)text interactionId:(NSString *)interactionId { OWSAssert(text); OWSAssert(interactionId.length > 0); - return [self displayableTextForInteractionId:interactionId - textBlock:^{ - return text; - }]; + return [self displayableBodyTextForInteractionId:interactionId + textBlock:^{ + return text; + }]; } -- (DisplayableText *)displayableTextForAttachmentStream:(TSAttachmentStream *)attachmentStream - interactionId:(NSString *)interactionId +- (DisplayableText *)displayableBodyTextForOversizeTextAttachment:(TSAttachmentStream *)attachmentStream + interactionId:(NSString *)interactionId { OWSAssert(attachmentStream); OWSAssert(interactionId.length > 0); - return [self displayableTextForInteractionId:interactionId - textBlock:^{ - NSData *textData = [NSData dataWithContentsOfURL:attachmentStream.mediaURL]; - NSString *text = - [[NSString alloc] initWithData:textData encoding:NSUTF8StringEncoding]; - return text; - }]; + return + [self displayableBodyTextForInteractionId:interactionId + textBlock:^{ + NSData *textData = [NSData dataWithContentsOfURL:attachmentStream.mediaURL]; + NSString *text = + [[NSString alloc] initWithData:textData encoding:NSUTF8StringEncoding]; + return text; + }]; } -- (DisplayableText *)displayableTextForInteractionId:(NSString *)interactionId - textBlock:(NSString * (^_Nonnull)(void))textBlock +- (DisplayableText *)displayableBodyTextForInteractionId:(NSString *)interactionId + textBlock:(NSString * (^_Nonnull)(void))textBlock { OWSAssert(interactionId.length > 0); - DisplayableText *_Nullable displayableText = [[self displayableTextCache] objectForKey:interactionId]; - if (!displayableText) { + DisplayableText *_Nullable displayableBodyText = [[self displayableBodyTextCache] objectForKey:interactionId]; + if (!displayableBodyText) { NSString *text = textBlock(); - displayableText = [DisplayableText displayableText:text]; - [[self displayableTextCache] setObject:displayableText forKey:interactionId]; + displayableBodyText = [DisplayableText displayableText:text]; + [[self displayableBodyTextCache] setObject:displayableBodyText forKey:interactionId]; } - return displayableText; + return displayableBodyText; } - (nullable TSAttachment *)firstAttachmentIfAnyOfMessage:(TSMessage *)message @@ -353,9 +369,8 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType) if ([attachment.contentType isEqualToString:OWSMimeTypeOversizeTextMessage]) { self.messageCellType = OWSMessageCellType_OversizeTextMessage; - self.displayableText = - [self displayableTextForAttachmentStream:self.attachmentStream interactionId:message.uniqueId]; - self.hasText = YES; + self.displayableBodyText = [self displayableBodyTextForOversizeTextAttachment:self.attachmentStream + interactionId:message.uniqueId]; } else if ([self.attachmentStream isAnimated] || [self.attachmentStream isImage] || [self.attachmentStream isVideo]) { if ([self.attachmentStream isAnimated]) { @@ -392,16 +407,20 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType) } } + // Ignore message body for oversize text attachments. if (message.body.length > 0) { - self.hasText = YES; + if (self.hasBodyText) { + OWSFail(@"%@ oversize text message has unexpected caption.", self.logTag); + } + // If we haven't already assigned an attachment type at this point, message.body isn't a caption, // it's a stand-alone text message. if (self.messageCellType == OWSMessageCellType_Unknown) { OWSAssert(message.attachmentIds.count == 0); self.messageCellType = OWSMessageCellType_TextMessage; } - self.displayableText = [self displayableTextForText:message.body interactionId:message.uniqueId]; - OWSAssert(self.displayableText); + self.displayableBodyText = [self displayableBodyTextForText:message.body interactionId:message.uniqueId]; + OWSAssert(self.displayableBodyText); } if (self.messageCellType == OWSMessageCellType_Unknown) { @@ -409,8 +428,7 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType) // are rendered like empty text messages, but without any interactivity. DDLogWarn(@"%@ Treating unknown message as empty text message: %@", self.logTag, message.description); self.messageCellType = OWSMessageCellType_TextMessage; - self.hasText = YES; - self.displayableText = [[DisplayableText alloc] initWithFullText:@"" displayText:@"" isTextTruncated:NO]; + self.displayableBodyText = [[DisplayableText alloc] initWithFullText:@"" displayText:@"" isTextTruncated:NO]; } } @@ -421,16 +439,16 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType) return _messageCellType; } -- (nullable DisplayableText *)displayableText +- (nullable DisplayableText *)displayableBodyText { OWSAssertIsOnMainThread(); OWSAssert(self.hasViewState); - OWSAssert(_displayableText); - OWSAssert(_displayableText.displayText); - OWSAssert(_displayableText.fullText); + OWSAssert(_displayableBodyText); + OWSAssert(_displayableBodyText.displayText); + OWSAssert(_displayableBodyText.fullText); - return _displayableText; + return _displayableBodyText; } - (nullable TSAttachmentStream *)attachmentStream @@ -533,13 +551,13 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType) - (BOOL)canPerformAction:(SEL)action { if (action == self.copyTextActionSelector) { - return [self hasTextActionContent]; + return [self hasBodyTextActionContent]; } else if (action == self.copyMediaActionSelector) { return [self hasMediaActionContent]; } else if (action == self.saveMediaActionSelector) { return [self canSaveMedia]; } else if (action == self.shareTextActionSelector) { - return [self hasTextActionContent]; + return [self hasBodyTextActionContent]; } else if (action == self.shareMediaActionSelector) { return [self hasMediaActionContent]; } else if (action == self.deleteActionSelector) { @@ -561,8 +579,8 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType) case OWSMessageCellType_Audio: case OWSMessageCellType_Video: case OWSMessageCellType_GenericAttachment: { - OWSAssert(self.displayableText); - [UIPasteboard.generalPasteboard setString:self.displayableText.fullText]; + OWSAssert(self.displayableBodyText); + [UIPasteboard.generalPasteboard setString:self.displayableBodyText.fullText]; break; } case OWSMessageCellType_DownloadingAttachment: { @@ -620,8 +638,8 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType) case OWSMessageCellType_Audio: case OWSMessageCellType_Video: case OWSMessageCellType_GenericAttachment: { - OWSAssert(self.displayableText); - [AttachmentSharing showShareUIForText:self.displayableText.fullText]; + OWSAssert(self.displayableBodyText); + [AttachmentSharing showShareUIForText:self.displayableBodyText.fullText]; break; } case OWSMessageCellType_DownloadingAttachment: { @@ -728,9 +746,9 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType) [self.interaction remove]; } -- (BOOL)hasTextActionContent +- (BOOL)hasBodyTextActionContent { - return self.hasText && self.displayableText.fullText.length > 0; + return self.hasBodyText && self.displayableBodyText.fullText.length > 0; } - (BOOL)hasMediaActionContent diff --git a/Signal/src/ViewControllers/DebugUI/DebugUIMessages.h b/Signal/src/ViewControllers/DebugUI/DebugUIMessages.h index 3c4a297259..4ff187f13d 100644 --- a/Signal/src/ViewControllers/DebugUI/DebugUIMessages.h +++ b/Signal/src/ViewControllers/DebugUI/DebugUIMessages.h @@ -1,13 +1,11 @@ // -// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. // #import "DebugUIPage.h" NS_ASSUME_NONNULL_BEGIN -@class TSThread; - @interface DebugUIMessages : DebugUIPage @end diff --git a/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m b/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m index b7f38ac453..486414d2b0 100644 --- a/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m +++ b/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m @@ -4,35 +4,30 @@ #import "DebugUIMessages.h" #import "DebugUIContacts.h" +#import "DebugUIMessagesAction.h" +#import "DebugUIMessagesAssetLoader.h" #import "OWSTableViewController.h" #import "Signal-Swift.h" -#import "ThreadUtil.h" -#import #import -#import -#import -#import +#import #import -#import -#import #import #import -#import -#import -#import #import #import +#import #import NS_ASSUME_NONNULL_BEGIN - @interface TSOutgoingMessage (PostDatingDebug) - (void)setReceivedAtTimestamp:(uint64_t)value; @end +#pragma mark - + @implementation DebugUIMessages #pragma mark - Factory Methods @@ -46,7 +41,67 @@ NS_ASSUME_NONNULL_BEGIN { OWSAssert(thread); - NSMutableArray *items = [@[ + NSMutableArray *items = [NSMutableArray new]; + + [items addObject:[OWSTableItem itemWithTitle:@"Delete all messages in thread" + actionBlock:^{ + [DebugUIMessages deleteAllMessagesInThread:thread]; + }]]; + + for (DebugUIMessagesAction *action in @[ + [DebugUIMessages sendMessageVariationsAction:thread], + // Send Media + [DebugUIMessages sendAllMediaAction:thread], + [DebugUIMessages sendRandomMediaAction:thread], + // Fake Media + [DebugUIMessages fakeAllMediaAction:thread], + [DebugUIMessages fakeRandomMediaAction:thread], + // Fake Text + [DebugUIMessages fakeAllTextAction:thread], + [DebugUIMessages fakeRandomTextAction:thread], + // Sequences + [DebugUIMessages allFakeSequencesAction:thread], + // Exemplary + [DebugUIMessages allFakeAction:thread], + ]) { + [items addObject:[OWSTableItem itemWithTitle:action.label + actionBlock:^{ + // For "all in group" actions, do each subaction in the group + // exactly once, in a predictable order. + if ([action isKindOfClass:[DebugUIMessagesGroupAction class]]) { + DebugUIMessagesGroupAction *groupAction + = (DebugUIMessagesGroupAction *)action; + if (groupAction.subactionMode == SubactionMode_Ordered) { + [action prepareAndPerformNTimes:groupAction.subactions.count]; + return; + } + } + [DebugUIMessages performActionNTimes:action]; + }]]; + } + [items addObjectsFromArray:@[ + +#pragma mark - Actions + + [OWSTableItem itemWithTitle:@"Send N text messages (1/sec.)" + actionBlock:^{ + [DebugUIMessages sendNTextMessagesInThread:thread]; + }], + // [OWSTableItem itemWithTitle:@"Send N Random Media (1/sec.)" + // actionBlock:^{ + // [DebugUIMessages sendSelectedMediaTypeInThread:thread]; + // }], + [OWSTableItem itemWithTitle:@"Select Fake" + actionBlock:^{ + [DebugUIMessages selectFakeAction:thread]; + }], + [OWSTableItem itemWithTitle:@"Select Send Media" + actionBlock:^{ + [DebugUIMessages selectSendMediaAction:thread]; + }], + +#pragma mark - Misc. + [OWSTableItem itemWithTitle:@"Perform 100 random actions" actionBlock:^{ [DebugUIMessages performRandomActions:100 thread:thread]; @@ -55,22 +110,6 @@ NS_ASSUME_NONNULL_BEGIN actionBlock:^{ [DebugUIMessages performRandomActions:1000 thread:thread]; }], - [OWSTableItem itemWithTitle:@"Send 10 messages (1/sec.)" - actionBlock:^{ - [DebugUIMessages sendTextMessages:10 thread:thread]; - }], - [OWSTableItem itemWithTitle:@"Send 100 messages (1/sec.)" - actionBlock:^{ - [DebugUIMessages sendTextMessages:100 thread:thread]; - }], - [OWSTableItem itemWithTitle:@"Send 1,000 messages (1/sec.)" - actionBlock:^{ - [DebugUIMessages sendTextMessages:1000 thread:thread]; - }], - [OWSTableItem itemWithTitle:@"Send 3,000 messages (1/sec.)" - actionBlock:^{ - [DebugUIMessages sendTextMessages:3000 thread:thread]; - }], [OWSTableItem itemWithTitle:@"Send 10 tiny text messages (1/sec.)" actionBlock:^{ [DebugUIMessages sendTinyTextMessages:10 thread:thread]; @@ -123,18 +162,6 @@ NS_ASSUME_NONNULL_BEGIN actionBlock:^{ [DebugUIMessages createFakeThreads:1000 withFakeMessages:1]; }], - [OWSTableItem itemWithTitle:@"🖼 fake media messages: 5" - actionBlock:^{ - [DebugUIMessages sendFakeMediaMessages:5 thread:thread]; - }], - [OWSTableItem itemWithTitle:@"🖼 fake media messages: 100" - actionBlock:^{ - [DebugUIMessages sendFakeMediaMessages:100 thread:thread]; - }], - [OWSTableItem itemWithTitle:@"🖼 fake media messages: 1k" - actionBlock:^{ - [DebugUIMessages sendFakeMediaMessages:1000 thread:thread]; - }], [OWSTableItem itemWithTitle:@"Create 1k fake messages" actionBlock:^{ [DebugUIMessages sendFakeMessages:1000 thread:thread]; @@ -187,66 +214,6 @@ NS_ASSUME_NONNULL_BEGIN actionBlock:^{ [DebugUIMessages sendRandomAttachment:thread uti:(NSString *)kUTTypePDF]; }], - [OWSTableItem itemWithTitle:@"Send 1 Random GIF (1/sec.)" - actionBlock:^{ - [DebugUIMessages sendRandomGifs:1 thread:thread]; - }], - [OWSTableItem itemWithTitle:@"Send 10 Random GIF (1/sec.)" - actionBlock:^{ - [DebugUIMessages sendRandomGifs:10 thread:thread]; - }], - [OWSTableItem itemWithTitle:@"Send 100 Random GIF (1/sec.)" - actionBlock:^{ - [DebugUIMessages sendRandomGifs:100 thread:thread]; - }], - [OWSTableItem itemWithTitle:@"Send 1 Random JPEG (1/sec.)" - actionBlock:^{ - [DebugUIMessages sendRandomJpegs:1 thread:thread]; - }], - [OWSTableItem itemWithTitle:@"Send 10 Random JPEG (1/sec.)" - actionBlock:^{ - [DebugUIMessages sendRandomJpegs:10 thread:thread]; - }], - [OWSTableItem itemWithTitle:@"Send 100 Random JPEG (1/sec.)" - actionBlock:^{ - [DebugUIMessages sendRandomJpegs:100 thread:thread]; - }], - [OWSTableItem itemWithTitle:@"Send 1 Random Mp3 (1/sec.)" - actionBlock:^{ - [DebugUIMessages sendRandomMp3s:1 thread:thread]; - }], - [OWSTableItem itemWithTitle:@"Send 10 Random Mp3 (1/sec.)" - actionBlock:^{ - [DebugUIMessages sendRandomMp3s:10 thread:thread]; - }], - [OWSTableItem itemWithTitle:@"Send 100 Random Mp3 (1/sec.)" - actionBlock:^{ - [DebugUIMessages sendRandomMp3s:100 thread:thread]; - }], - [OWSTableItem itemWithTitle:@"Send 1 Random Mp4 (1/sec.)" - actionBlock:^{ - [DebugUIMessages sendRandomMp4s:1 thread:thread]; - }], - [OWSTableItem itemWithTitle:@"Send 10 Random Mp4 (1/sec.)" - actionBlock:^{ - [DebugUIMessages sendRandomMp4s:10 thread:thread]; - }], - [OWSTableItem itemWithTitle:@"Send 100 Random Mp4 (1/sec.)" - actionBlock:^{ - [DebugUIMessages sendRandomMp4s:100 thread:thread]; - }], - [OWSTableItem itemWithTitle:@"Send 10 media (1/sec.)" - actionBlock:^{ - [DebugUIMessages sendMediaAttachments:10 thread:thread]; - }], - [OWSTableItem itemWithTitle:@"Send 100 media (1/sec.)" - actionBlock:^{ - [DebugUIMessages sendMediaAttachments:100 thread:thread]; - }], - [OWSTableItem itemWithTitle:@"Send 1,000 media (1/sec.)" - actionBlock:^{ - [DebugUIMessages sendMediaAttachments:1000 thread:thread]; - }], [OWSTableItem itemWithTitle:@"Create all system messages" actionBlock:^{ [DebugUIMessages createSystemMessagesInThread:thread]; @@ -308,7 +275,8 @@ NS_ASSUME_NONNULL_BEGIN actionBlock:^{ [DebugUIMessages testDirectionalFilenamesInThread:thread]; }], - ] mutableCopy]; + ]]; + if ([thread isKindOfClass:[TSContactThread class]]) { TSContactThread *contactThread = (TSContactThread *)thread; NSString *recipientId = contactThread.contactIdentifier; @@ -335,17 +303,17 @@ NS_ASSUME_NONNULL_BEGIN return [OWSTableSection sectionWithTitle:self.name items:items]; } -+ (void)sendMessages:(int)counter toAllMembersOfGroup:(TSGroupThread *)groupThread ++ (void)sendMessages:(NSUInteger)count toAllMembersOfGroup:(TSGroupThread *)groupThread { for (NSString *recipientId in groupThread.groupModel.groupMemberIds) { TSContactThread *contactThread = [TSContactThread getOrCreateThreadWithContactId:recipientId]; - [DebugUIMessages sendTextMessages:counter thread:contactThread]; + [[self sendTextMessagesActionInThread:contactThread] prepareAndPerformNTimes:count]; } } -+ (void)sendTextMessageInThread:(TSThread *)thread counter:(int)counter ++ (void)sendTextMessageInThread:(TSThread *)thread counter:(NSUInteger)counter { - DDLogInfo(@"%@ sendTextMessageInThread: %d", self.logTag, counter); + DDLogInfo(@"%@ sendTextMessageInThread: %zd", self.logTag, counter); [DDLog flushLog]; NSString *randomText = [self randomText]; @@ -355,18 +323,29 @@ NS_ASSUME_NONNULL_BEGIN DDLogError(@"%@ sendTextMessageInThread timestamp: %llu.", self.logTag, message.timestamp); } -+ (void)sendTextMessages:(int)counter thread:(TSThread *)thread ++ (void)sendNTextMessagesInThread:(TSThread *)thread { - if (counter < 1) { - return; - } - [self sendTextMessageInThread:thread counter:counter]; - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)1.f * NSEC_PER_SEC), dispatch_get_main_queue(), ^{ - [self sendTextMessages:counter - 1 thread:thread]; - }); + [self performActionNTimes:[self sendTextMessagesActionInThread:thread]]; } -+ (void)sendTinyTextMessageInThread:(TSThread *)thread counter:(int)counter ++ (DebugUIMessagesAction *)sendTextMessagesActionInThread:(TSThread *)thread +{ + OWSAssert(thread); + + return [DebugUIMessagesSingleAction actionWithLabel:@"Send Text Message" + staggeredActionBlock:^(NSUInteger index, + YapDatabaseReadWriteTransaction *transaction, + ActionSuccessBlock success, + ActionFailureBlock failure) { + dispatch_async(dispatch_get_main_queue(), ^{ + [self sendTextMessageInThread:thread counter:index]; + // TODO: + success(); + }); + }]; +} + ++ (void)sendTinyTextMessageInThread:(TSThread *)thread counter:(NSUInteger)counter { NSString *randomText = [[self randomText] substringToIndex:arc4random_uniform(4)]; NSString *text = [[[@(counter) description] stringByAppendingString:@" "] stringByAppendingString:randomText]; @@ -374,7 +353,7 @@ NS_ASSUME_NONNULL_BEGIN [ThreadUtil sendMessageWithText:text inThread:thread messageSender:messageSender]; } -+ (void)sendTinyTextMessages:(int)counter thread:(TSThread *)thread ++ (void)sendTinyTextMessages:(NSUInteger)counter thread:(TSThread *)thread { if (counter < 1) { return; @@ -385,46 +364,10 @@ NS_ASSUME_NONNULL_BEGIN }); } -+ (void)ensureRandomFileWithURL:(NSString *)url - filename:(NSString *)filename - success:(nullable void (^)(NSString *filePath))success - failure:(nullable void (^)(void))failure -{ - NSFileManager *fileManager = [NSFileManager defaultManager]; - NSURL *documentDirectoryURL = - [[fileManager URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject]; - NSString *randomFilesDirectoryPath = - [[documentDirectoryURL path] stringByAppendingPathComponent:@"cached_random_files"]; - [OWSFileSystem ensureDirectoryExists:randomFilesDirectoryPath]; - NSString *filePath = [randomFilesDirectoryPath stringByAppendingPathComponent:filename]; - if ([fileManager fileExistsAtPath:filePath]) { - success(filePath); - } else { - NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration]; - AFHTTPSessionManager *sessionManager = - [[AFHTTPSessionManager alloc] initWithSessionConfiguration:configuration]; - sessionManager.responseSerializer = [AFHTTPResponseSerializer serializer]; - OWSAssert(sessionManager.responseSerializer); - [sessionManager GET:url - parameters:nil - progress:nil - success:^(NSURLSessionDataTask *task, NSData *_Nullable responseObject) { - if ([responseObject writeToFile:filePath atomically:YES]) { - success(filePath); - } else { - OWSFail(@"Error write url response [%@]: %@", url, filePath); - failure(); - } - } - failure:^(NSURLSessionDataTask *_Nullable task, NSError *requestError) { - OWSFail(@"Error downloading url[%@]: %@", url, requestError); - failure(); - }]; - } -} - + (void)sendAttachment:(NSString *)filePath thread:(TSThread *)thread + label:(NSString *)label + hasCaption:(BOOL)hasCaption success:(nullable void (^)(void))success failure:(nullable void (^)(void))failure { @@ -438,9 +381,18 @@ NS_ASSUME_NONNULL_BEGIN [dataSource setSourceFilename:filename]; SignalAttachment *attachment = [SignalAttachment attachmentWithDataSource:dataSource dataUTI:utiType imageQuality:TSImageQualityOriginal]; - if (arc4random_uniform(100) > 50) { - attachment.captionText = [self randomCaptionText]; + + NSString *messageBody = nil; + if (hasCaption) { + // We want a message body that is "more than one line on all devices, + // using all dynamic type sizes." + NSString *sampleText = @"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, " + @"consectetur adipiscing elit."; + messageBody = [[label stringByAppendingString:@" "] stringByAppendingString:sampleText]; + + messageBody = [messageBody stringByAppendingString:@" 🔤"]; } + attachment.captionText = messageBody; OWSAssert(attachment); if ([attachment hasError]) { @@ -452,244 +404,1748 @@ NS_ASSUME_NONNULL_BEGIN success(); } -+ (void)ensureRandomGifWithSuccess:(nullable void (^)(NSString *filePath))success - failure:(nullable void (^)(void))failure +#pragma mark - Infrastructure + ++ (void)performActionNTimes:(DebugUIMessagesAction *)action { - [self ensureRandomFileWithURL:@"https://s3.amazonaws.com/ows-data/example_attachment_media/random-gif.gif" - filename:@"random-gif.gif" - success:success - failure:failure]; + OWSAssertIsOnMainThread(); + OWSAssert(action); + + UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"How many?" + message:nil + preferredStyle:UIAlertControllerStyleActionSheet]; + for (NSNumber *countValue in @[ + @(1), + @(10), + @(100), + @(1 * 1000), + @(10 * 1000), + ]) { + [alert addAction:[UIAlertAction actionWithTitle:countValue.stringValue + style:UIAlertActionStyleDefault + handler:^(UIAlertAction *ignore) { + [action prepareAndPerformNTimes:countValue.unsignedIntegerValue]; + }]]; + } + + [alert addAction:[OWSAlerts cancelAction]]; + UIViewController *fromViewController = [[UIApplication sharedApplication] frontmostViewController]; + [fromViewController presentViewController:alert animated:YES completion:nil]; } -+ (void)sendRandomGifInThread:(TSThread *)thread - success:(nullable void (^)(void))success - failure:(nullable void (^)(void))failure +#pragma mark - Send Media + ++ (NSArray *)allSendMediaActions:(TSThread *)thread { - [self ensureRandomGifWithSuccess:^(NSString *filePath) { - [self sendAttachment:filePath thread:thread success:success failure:failure]; + OWSAssert(thread); + + NSArray *actions = @[ + [self sendJpegAction:thread hasCaption:NO], + [self sendJpegAction:thread hasCaption:YES], + [self sendGifAction:thread hasCaption:NO], + [self sendGifAction:thread hasCaption:YES], + [self sendLargeGifAction:thread hasCaption:NO], + [self sendLargeGifAction:thread hasCaption:YES], + [self sendMp3Action:thread hasCaption:NO], + [self sendMp3Action:thread hasCaption:YES], + [self sendMp4Action:thread hasCaption:NO], + [self sendMp4Action:thread hasCaption:YES], + ]; + return actions; +} + ++ (DebugUIMessagesAction *)sendJpegAction:(TSThread *)thread hasCaption:(BOOL)hasCaption +{ + OWSAssert(thread); + + return [self sendMediaAction:@"Send Jpeg" + hasCaption:hasCaption + fakeAssetLoader:[DebugUIMessagesAssetLoader jpegInstance] + thread:thread]; +} + ++ (DebugUIMessagesAction *)sendGifAction:(TSThread *)thread hasCaption:(BOOL)hasCaption +{ + OWSAssert(thread); + + return [self sendMediaAction:@"Send Gif" + hasCaption:hasCaption + fakeAssetLoader:[DebugUIMessagesAssetLoader gifInstance] + thread:thread]; +} + ++ (DebugUIMessagesAction *)sendLargeGifAction:(TSThread *)thread hasCaption:(BOOL)hasCaption +{ + OWSAssert(thread); + + return [self sendMediaAction:@"Send Large Gif" + hasCaption:hasCaption + fakeAssetLoader:[DebugUIMessagesAssetLoader largeGifInstance] + thread:thread]; +} + ++ (DebugUIMessagesAction *)sendMp3Action:(TSThread *)thread hasCaption:(BOOL)hasCaption +{ + OWSAssert(thread); + + return [self sendMediaAction:@"Send Mp3" + hasCaption:hasCaption + fakeAssetLoader:[DebugUIMessagesAssetLoader mp3Instance] + thread:thread]; +} + ++ (DebugUIMessagesAction *)sendMp4Action:(TSThread *)thread hasCaption:(BOOL)hasCaption +{ + OWSAssert(thread); + + return [self sendMediaAction:@"Send Mp4" + hasCaption:hasCaption + fakeAssetLoader:[DebugUIMessagesAssetLoader mp4Instance] + thread:thread]; +} + ++ (DebugUIMessagesAction *)sendMediaAction:(NSString *)labelParam + hasCaption:(BOOL)hasCaption + fakeAssetLoader:(DebugUIMessagesAssetLoader *)fakeAssetLoader + thread:(TSThread *)thread +{ + OWSAssert(labelParam.length > 0); + OWSAssert(fakeAssetLoader); + OWSAssert(thread); + + NSString *label = labelParam; + if (hasCaption) { + label = [label stringByAppendingString:@" 🔤"]; } + + return [DebugUIMessagesSingleAction + actionWithLabel:label + staggeredActionBlock:^(NSUInteger index, + YapDatabaseReadWriteTransaction *transaction, + ActionSuccessBlock success, + ActionFailureBlock failure) { + dispatch_async(dispatch_get_main_queue(), ^{ + OWSAssert(fakeAssetLoader.filePath.length > 0); + [self sendAttachment:fakeAssetLoader.filePath + thread:thread + label:label + hasCaption:hasCaption + success:success failure:failure]; -} - -+ (void)sendRandomGifs:(int)count thread:(TSThread *)thread -{ - OWSAssert(count > 0); - [self ensureRandomGifWithSuccess:^(NSString *filePath) { - [self - sendAttachment:filePath - thread:thread - success:^{ - if (count <= 1) { - return; - } - dispatch_after( - dispatch_time(DISPATCH_TIME_NOW, (int64_t)1.f * NSEC_PER_SEC), dispatch_get_main_queue(), ^{ - [self sendRandomGifs:count - 1 thread:thread]; - }); - } - failure:^{ - }]; - } - failure:^{ - }]; -} - -+ (void)ensureRandomJpegWithSuccess:(nullable void (^)(NSString *filePath))success - failure:(nullable void (^)(void))failure -{ - [self ensureRandomFileWithURL:@"https://s3.amazonaws.com/ows-data/example_attachment_media/random-jpg.JPG" - filename:@"random-jpg.jpg" - success:success - failure:failure]; -} - -+ (void)sendRandomJpegInThread:(TSThread *)thread - success:(nullable void (^)(void))success - failure:(nullable void (^)(void))failure -{ - [self ensureRandomJpegWithSuccess:^(NSString *filePath) { - [self sendAttachment:filePath thread:thread success:success failure:failure]; - } - failure:failure]; -} - -+ (void)sendRandomJpegs:(int)count thread:(TSThread *)thread -{ - OWSAssert(count > 0); - [self ensureRandomJpegWithSuccess:^(NSString *filePath) { - [self - sendAttachment:filePath - thread:thread - success:^{ - if (count <= 1) { - return; - } - dispatch_after( - dispatch_time(DISPATCH_TIME_NOW, (int64_t)1.f * NSEC_PER_SEC), dispatch_get_main_queue(), ^{ - [self sendRandomJpegs:count - 1 thread:thread]; - }); - } - failure:^{ - }]; - } - failure:^{ - }]; -} - -+ (void)ensureRandomMp3WithSuccess:(nullable void (^)(NSString *filePath))success - failure:(nullable void (^)(void))failure -{ - [self ensureRandomFileWithURL:@"https://s3.amazonaws.com/ows-data/example_attachment_media/random-mp3.mp3" - filename:@"random-mp3.mp3" - success:success - failure:failure]; -} - -+ (void)sendRandomMp3InThread:(TSThread *)thread - success:(nullable void (^)(void))success - failure:(nullable void (^)(void))failure -{ - [self ensureRandomMp3WithSuccess:^(NSString *filePath) { - [self sendAttachment:filePath thread:thread success:success failure:failure]; - } - failure:failure]; -} - -+ (void)sendRandomMp3s:(int)count thread:(TSThread *)thread -{ - OWSAssert(count > 0); - [self ensureRandomMp3WithSuccess:^(NSString *filePath) { - [self - sendAttachment:filePath - thread:thread - success:^{ - if (count <= 1) { - return; - } - dispatch_after( - dispatch_time(DISPATCH_TIME_NOW, (int64_t)1.f * NSEC_PER_SEC), dispatch_get_main_queue(), ^{ - [self sendRandomMp3s:count - 1 thread:thread]; - }); - } - failure:^{ - }]; - } - failure:^{ - }]; -} - -+ (void)ensureRandomMp4WithSuccess:(nullable void (^)(NSString *filePath))success - failure:(nullable void (^)(void))failure -{ - [self ensureRandomFileWithURL:@"https://s3.amazonaws.com/ows-data/example_attachment_media/random-mp4.mp4" - filename:@"random-mp4.mp4" - success:success - failure:failure]; -} - -+ (void)sendRandomMp4InThread:(TSThread *)thread - success:(nullable void (^)(void))success - failure:(nullable void (^)(void))failure -{ - [self ensureRandomMp4WithSuccess:^(NSString *filePath) { - [self sendAttachment:filePath thread:thread success:success failure:failure]; - } - failure:failure]; -} - -+ (void)sendRandomMp4s:(int)count thread:(TSThread *)thread -{ - OWSAssert(count > 0); - [self ensureRandomMp4WithSuccess:^(NSString *filePath) { - [self - sendAttachment:filePath - thread:thread - success:^{ - if (count <= 1) { - return; - } - dispatch_after( - dispatch_time(DISPATCH_TIME_NOW, (int64_t)1.f * NSEC_PER_SEC), dispatch_get_main_queue(), ^{ - [self sendRandomMp4s:count - 1 thread:thread]; - }); - } - failure:^{ - }]; - } - failure:^{ - }]; -} - -+ (void)sendMediaAttachments:(int)count thread:(TSThread *)thread -{ - OWSAssert(count > 0); - - void (^success)(void) = ^{ - if (count <= 1) { - return; + }); } - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)1.f * NSEC_PER_SEC), dispatch_get_main_queue(), ^{ - [self sendMediaAttachments:count - 1 thread:thread]; - }); - }; + prepareBlock:fakeAssetLoader.prepareBlock]; +} - switch (arc4random_uniform(4)) { - case 0: { - [self ensureRandomGifWithSuccess:^(NSString *filePath) { - [self sendAttachment:filePath - thread:thread - success:success - failure:^{ - }]; ++ (DebugUIMessagesAction *)sendAllMediaAction:(TSThread *)thread +{ + OWSAssert(thread); + + return [DebugUIMessagesGroupAction allGroupActionWithLabel:@"All Send Media" + subactions:[self allSendMediaActions:thread]]; +} + ++ (DebugUIMessagesAction *)sendRandomMediaAction:(TSThread *)thread +{ + OWSAssert(thread); + + return [DebugUIMessagesGroupAction randomGroupActionWithLabel:@"Random Send Media" + subactions:[self allSendMediaActions:thread]]; +} + ++ (void)selectSendMediaAction:(TSThread *)thread +{ + OWSAssert(thread); + + [self selectActionUI:[self allSendMediaActions:thread] label:@"Select Send Media"]; +} + +#pragma mark - Fake Outgoing Media + ++ (DebugUIMessagesAction *)fakeOutgoingJpegAction:(TSThread *)thread + messageState:(TSOutgoingMessageState)messageState + hasCaption:(BOOL)hasCaption +{ + OWSAssert(thread); + + return [self fakeOutgoingMediaAction:@"Fake Outgoing Jpeg" + messageState:messageState + hasCaption:hasCaption + fakeAssetLoader:[DebugUIMessagesAssetLoader jpegInstance] + thread:thread]; +} + ++ (DebugUIMessagesAction *)fakeOutgoingGifAction:(TSThread *)thread + messageState:(TSOutgoingMessageState)messageState + hasCaption:(BOOL)hasCaption +{ + OWSAssert(thread); + + return [self fakeOutgoingMediaAction:@"Fake Outgoing Gif" + messageState:messageState + hasCaption:hasCaption + fakeAssetLoader:[DebugUIMessagesAssetLoader gifInstance] + thread:thread]; +} + ++ (DebugUIMessagesAction *)fakeOutgoingLargeGifAction:(TSThread *)thread + messageState:(TSOutgoingMessageState)messageState + hasCaption:(BOOL)hasCaption +{ + OWSAssert(thread); + + return [self fakeOutgoingMediaAction:@"Fake Outgoing Large Gif" + messageState:messageState + hasCaption:hasCaption + fakeAssetLoader:[DebugUIMessagesAssetLoader largeGifInstance] + thread:thread]; +} + ++ (DebugUIMessagesAction *)fakeOutgoingMp3Action:(TSThread *)thread + messageState:(TSOutgoingMessageState)messageState + hasCaption:(BOOL)hasCaption +{ + OWSAssert(thread); + + return [self fakeOutgoingMediaAction:@"Fake Outgoing Mp3" + messageState:messageState + hasCaption:hasCaption + fakeAssetLoader:[DebugUIMessagesAssetLoader mp3Instance] + thread:thread]; +} + ++ (DebugUIMessagesAction *)fakeOutgoingMp4Action:(TSThread *)thread + messageState:(TSOutgoingMessageState)messageState + hasCaption:(BOOL)hasCaption +{ + OWSAssert(thread); + + return [self fakeOutgoingMediaAction:@"Fake Outgoing Mp4" + messageState:messageState + hasCaption:hasCaption + fakeAssetLoader:[DebugUIMessagesAssetLoader mp4Instance] + thread:thread]; +} + ++ (DebugUIMessagesAction *)fakeOutgoingCompactPortraitPngAction:(TSThread *)thread + messageState:(TSOutgoingMessageState)messageState + hasCaption:(BOOL)hasCaption +{ + OWSAssert(thread); + + return [self fakeOutgoingMediaAction:@"Fake Outgoing Portrait Png" + messageState:messageState + hasCaption:hasCaption + fakeAssetLoader:[DebugUIMessagesAssetLoader compactLandscapePngInstance] + thread:thread]; +} + ++ (DebugUIMessagesAction *)fakeOutgoingCompactLandscapePngAction:(TSThread *)thread + messageState:(TSOutgoingMessageState)messageState + hasCaption:(BOOL)hasCaption +{ + OWSAssert(thread); + + return [self fakeOutgoingMediaAction:@"Fake Outgoing Landscape Png" + messageState:messageState + hasCaption:hasCaption + fakeAssetLoader:[DebugUIMessagesAssetLoader compactPortraitPngInstance] + thread:thread]; +} + ++ (DebugUIMessagesAction *)fakeOutgoingTallPortraitPngAction:(TSThread *)thread + messageState:(TSOutgoingMessageState)messageState + hasCaption:(BOOL)hasCaption +{ + OWSAssert(thread); + + return [self fakeOutgoingMediaAction:@"Fake Outgoing Tall Portrait Png" + messageState:messageState + hasCaption:hasCaption + fakeAssetLoader:[DebugUIMessagesAssetLoader tallPortraitPngInstance] + thread:thread]; +} + ++ (DebugUIMessagesAction *)fakeOutgoingWideLandscapePngAction:(TSThread *)thread + messageState:(TSOutgoingMessageState)messageState + hasCaption:(BOOL)hasCaption +{ + OWSAssert(thread); + + return [self fakeOutgoingMediaAction:@"Fake Outgoing Wide Landscape Png" + messageState:messageState + hasCaption:hasCaption + fakeAssetLoader:[DebugUIMessagesAssetLoader wideLandscapePngInstance] + thread:thread]; +} + ++ (DebugUIMessagesAction *)fakeOutgoingLargePngAction:(TSThread *)thread + messageState:(TSOutgoingMessageState)messageState + hasCaption:(BOOL)hasCaption +{ + OWSAssert(thread); + + return [self fakeOutgoingMediaAction:@"Fake Outgoing Large Png" + messageState:messageState + hasCaption:hasCaption + fakeAssetLoader:[DebugUIMessagesAssetLoader largePngInstance] + thread:thread]; +} + ++ (DebugUIMessagesAction *)fakeOutgoingTinyPngAction:(TSThread *)thread + messageState:(TSOutgoingMessageState)messageState + hasCaption:(BOOL)hasCaption +{ + OWSAssert(thread); + + return [self fakeOutgoingMediaAction:@"Fake Outgoing Tiny Png" + messageState:messageState + hasCaption:hasCaption + fakeAssetLoader:[DebugUIMessagesAssetLoader tinyPngInstance] + thread:thread]; +} + ++ (DebugUIMessagesAction *)fakeOutgoingPngAction:(TSThread *)thread + actionLabel:(NSString *)actionLabel + imageSize:(CGSize)imageSize + backgroundColor:(UIColor *)backgroundColor + textColor:(UIColor *)textColor + imageLabel:(NSString *)imageLabel + messageState:(TSOutgoingMessageState)messageState + hasCaption:(BOOL)hasCaption +{ + OWSAssert(thread); + + return [self fakeOutgoingMediaAction:actionLabel + messageState:messageState + hasCaption:hasCaption + fakeAssetLoader:[DebugUIMessagesAssetLoader pngInstanceWithSize:imageSize + backgroundColor:backgroundColor + textColor:textColor + label:imageLabel] + thread:thread]; +} + ++ (DebugUIMessagesAction *)fakeOutgoingTinyPdfAction:(TSThread *)thread + messageState:(TSOutgoingMessageState)messageState + hasCaption:(BOOL)hasCaption +{ + OWSAssert(thread); + + return [self fakeOutgoingMediaAction:@"Fake Outgoing Tiny Pdf" + messageState:messageState + hasCaption:hasCaption + fakeAssetLoader:[DebugUIMessagesAssetLoader tinyPdfInstance] + thread:thread]; +} + ++ (DebugUIMessagesAction *)fakeOutgoingLargePdfAction:(TSThread *)thread + messageState:(TSOutgoingMessageState)messageState + hasCaption:(BOOL)hasCaption +{ + OWSAssert(thread); + + return [self fakeOutgoingMediaAction:@"Fake Outgoing Large Pdf" + messageState:messageState + hasCaption:hasCaption + fakeAssetLoader:[DebugUIMessagesAssetLoader largePdfInstance] + thread:thread]; +} + ++ (DebugUIMessagesAction *)fakeOutgoingMissingPngAction:(TSThread *)thread + messageState:(TSOutgoingMessageState)messageState + hasCaption:(BOOL)hasCaption +{ + OWSAssert(thread); + + return [self fakeOutgoingMediaAction:@"Fake Outgoing Missing Png" + messageState:messageState + hasCaption:hasCaption + fakeAssetLoader:[DebugUIMessagesAssetLoader missingPngInstance] + thread:thread]; +} + ++ (DebugUIMessagesAction *)fakeOutgoingMissingPdfAction:(TSThread *)thread + messageState:(TSOutgoingMessageState)messageState + hasCaption:(BOOL)hasCaption +{ + OWSAssert(thread); + + return [self fakeOutgoingMediaAction:@"Fake Outgoing Missing Pdf" + messageState:messageState + hasCaption:hasCaption + fakeAssetLoader:[DebugUIMessagesAssetLoader missingPdfInstance] + thread:thread]; +} + ++ (DebugUIMessagesAction *)fakeOutgoingOversizeTextAction:(TSThread *)thread + messageState:(TSOutgoingMessageState)messageState + hasCaption:(BOOL)hasCaption +{ + OWSAssert(thread); + + return [self fakeOutgoingMediaAction:@"Fake Outgoing Oversize Text" + messageState:messageState + hasCaption:hasCaption + fakeAssetLoader:[DebugUIMessagesAssetLoader oversizeTextInstance] + thread:thread]; +} + ++ (DebugUIMessagesAction *)fakeOutgoingMediaAction:(NSString *)labelParam + messageState:(TSOutgoingMessageState)messageState + hasCaption:(BOOL)hasCaption + fakeAssetLoader:(DebugUIMessagesAssetLoader *)fakeAssetLoader + thread:(TSThread *)thread +{ + OWSAssert(labelParam.length > 0); + OWSAssert(fakeAssetLoader); + OWSAssert(thread); + + NSString *label = labelParam; + if (hasCaption) { + label = [label stringByAppendingString:@" 🔤"]; + } + if (messageState == TSOutgoingMessageStateUnsent) { + label = [label stringByAppendingString:@" (Unsent)"]; + } else if (messageState == TSOutgoingMessageStateAttemptingOut) { + label = [label stringByAppendingString:@" (Sending)"]; + } else if (messageState == TSOutgoingMessageStateSentToService) { + label = [label stringByAppendingString:@" (Sent)"]; + } else { + OWSFail(@"%@ unknown message state.", self.logTag) + } + + return + [DebugUIMessagesSingleAction actionWithLabel:label + unstaggeredActionBlock:^(NSUInteger index, YapDatabaseReadWriteTransaction *transaction) { + OWSAssert(fakeAssetLoader.filePath.length > 0); + [self createFakeOutgoingMedia:index + messageState:messageState + hasCaption:hasCaption + fakeAssetLoader:fakeAssetLoader + thread:thread + transaction:transaction]; + } + prepareBlock:fakeAssetLoader.prepareBlock]; +} + ++ (void)createFakeOutgoingMedia:(NSUInteger)index + messageState:(TSOutgoingMessageState)messageState + hasCaption:(BOOL)hasCaption + fakeAssetLoader:(DebugUIMessagesAssetLoader *)fakeAssetLoader + thread:(TSThread *)thread + transaction:(YapDatabaseReadWriteTransaction *)transaction +{ + OWSAssert(thread); + OWSAssert(fakeAssetLoader.filePath); + OWSAssert(transaction); + + // Random time within last n years. Helpful for filling out a media gallery over time. + // double yearsMillis = 4.0 * kYearsInMs; + // uint64_t millisAgo = (uint64_t)(((double)arc4random() / ((double)0xffffffff)) * yearsMillis); + // uint64_t timestamp = [NSDate ows_millisecondTimeStamp] - millisAgo; + uint64_t timestamp = [NSDate ows_millisecondTimeStamp]; + + NSString *messageBody = nil; + if (hasCaption) { + // We want a message body that is "more than one line on all devices, + // using all dynamic type sizes." + NSString *sampleText = @"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, " + @"consectetur adipiscing elit."; + messageBody = [[@(index).stringValue stringByAppendingString:@" "] stringByAppendingString:sampleText]; + + if (hasCaption) { + messageBody = [messageBody stringByAppendingString:@" 🔤"]; + } + if (messageState == TSOutgoingMessageStateUnsent) { + messageBody = [messageBody stringByAppendingString:@" (Unsent)"]; + } else if (messageState == TSOutgoingMessageStateAttemptingOut) { + messageBody = [messageBody stringByAppendingString:@" (Sending)"]; + } else if (messageState == TSOutgoingMessageStateSentToService) { + messageBody = [messageBody stringByAppendingString:@" (Sent)"]; + } else { + OWSFail(@"%@ unknown message state.", self.logTag) + } + } + + TSOutgoingMessage *message = [[TSOutgoingMessage alloc] initWithTimestamp:timestamp + inThread:thread + messageBody:messageBody + isVoiceMessage:NO + expiresInSeconds:0]; + [message setReceivedAtTimestamp:timestamp]; + + DataSource *dataSource = [DataSourcePath dataSourceWithFilePath:fakeAssetLoader.filePath]; + NSString *filename = dataSource.sourceFilename; + // To support "fake missing" attachments, we sometimes lie about the + // length of the data. + UInt32 nominalDataLength = (UInt32)MAX((NSUInteger)1, dataSource.dataLength); + TSAttachmentStream *attachmentStream = [[TSAttachmentStream alloc] initWithContentType:fakeAssetLoader.mimeType + byteCount:nominalDataLength + sourceFilename:filename]; + NSError *error; + BOOL success = [attachmentStream writeData:dataSource.data error:&error]; + OWSAssert(success && !error); + + [attachmentStream saveWithTransaction:transaction]; + [message.attachmentIds addObject:attachmentStream.uniqueId]; + if (filename) { + message.attachmentFilenameMap[attachmentStream.uniqueId] = filename; + } + [message saveWithTransaction:transaction]; + [message updateWithMessageState:messageState transaction:transaction]; +} + +#pragma mark - Fake Incoming Media + ++ (DebugUIMessagesAction *)fakeIncomingJpegAction:(TSThread *)thread + isAttachmentDownloaded:(BOOL)isAttachmentDownloaded + hasCaption:(BOOL)hasCaption +{ + OWSAssert(thread); + + return [self fakeIncomingMediaAction:@"Fake Incoming Jpeg" + isAttachmentDownloaded:isAttachmentDownloaded + hasCaption:hasCaption + fakeAssetLoader:[DebugUIMessagesAssetLoader jpegInstance] + thread:thread]; +} + ++ (DebugUIMessagesAction *)fakeIncomingGifAction:(TSThread *)thread + isAttachmentDownloaded:(BOOL)isAttachmentDownloaded + hasCaption:(BOOL)hasCaption +{ + OWSAssert(thread); + + return [self fakeIncomingMediaAction:@"Fake Incoming Gif" + isAttachmentDownloaded:isAttachmentDownloaded + hasCaption:hasCaption + fakeAssetLoader:[DebugUIMessagesAssetLoader gifInstance] + thread:thread]; +} + ++ (DebugUIMessagesAction *)fakeIncomingLargeGifAction:(TSThread *)thread + isAttachmentDownloaded:(BOOL)isAttachmentDownloaded + hasCaption:(BOOL)hasCaption +{ + OWSAssert(thread); + + return [self fakeIncomingMediaAction:@"Fake Incoming Large Gif" + isAttachmentDownloaded:isAttachmentDownloaded + hasCaption:hasCaption + fakeAssetLoader:[DebugUIMessagesAssetLoader largeGifInstance] + thread:thread]; +} + ++ (DebugUIMessagesAction *)fakeIncomingMp3Action:(TSThread *)thread + isAttachmentDownloaded:(BOOL)isAttachmentDownloaded + hasCaption:(BOOL)hasCaption +{ + OWSAssert(thread); + + return [self fakeIncomingMediaAction:@"Fake Incoming Mp3" + isAttachmentDownloaded:isAttachmentDownloaded + hasCaption:hasCaption + fakeAssetLoader:[DebugUIMessagesAssetLoader mp3Instance] + thread:thread]; +} + ++ (DebugUIMessagesAction *)fakeIncomingMp4Action:(TSThread *)thread + isAttachmentDownloaded:(BOOL)isAttachmentDownloaded + hasCaption:(BOOL)hasCaption +{ + OWSAssert(thread); + + return [self fakeIncomingMediaAction:@"Fake Incoming Mp4" + isAttachmentDownloaded:isAttachmentDownloaded + hasCaption:hasCaption + fakeAssetLoader:[DebugUIMessagesAssetLoader mp4Instance] + thread:thread]; +} + ++ (DebugUIMessagesAction *)fakeIncomingCompactPortraitPngAction:(TSThread *)thread + isAttachmentDownloaded:(BOOL)isAttachmentDownloaded + hasCaption:(BOOL)hasCaption +{ + OWSAssert(thread); + + return [self fakeIncomingMediaAction:@"Fake Incoming Portrait Png" + isAttachmentDownloaded:isAttachmentDownloaded + hasCaption:hasCaption + fakeAssetLoader:[DebugUIMessagesAssetLoader compactPortraitPngInstance] + thread:thread]; +} + ++ (DebugUIMessagesAction *)fakeIncomingCompactLandscapePngAction:(TSThread *)thread + isAttachmentDownloaded:(BOOL)isAttachmentDownloaded + hasCaption:(BOOL)hasCaption +{ + OWSAssert(thread); + + return [self fakeIncomingMediaAction:@"Fake Incoming Landscape Png" + isAttachmentDownloaded:isAttachmentDownloaded + hasCaption:hasCaption + fakeAssetLoader:[DebugUIMessagesAssetLoader compactLandscapePngInstance] + thread:thread]; +} + ++ (DebugUIMessagesAction *)fakeIncomingTallPortraitPngAction:(TSThread *)thread + isAttachmentDownloaded:(BOOL)isAttachmentDownloaded + hasCaption:(BOOL)hasCaption +{ + OWSAssert(thread); + + return [self fakeIncomingMediaAction:@"Fake Incoming Tall Portrait Png" + isAttachmentDownloaded:isAttachmentDownloaded + hasCaption:hasCaption + fakeAssetLoader:[DebugUIMessagesAssetLoader tallPortraitPngInstance] + thread:thread]; +} + ++ (DebugUIMessagesAction *)fakeIncomingWideLandscapePngAction:(TSThread *)thread + isAttachmentDownloaded:(BOOL)isAttachmentDownloaded + hasCaption:(BOOL)hasCaption +{ + OWSAssert(thread); + + return [self fakeIncomingMediaAction:@"Fake Incoming Wide Landscape Png" + isAttachmentDownloaded:isAttachmentDownloaded + hasCaption:hasCaption + fakeAssetLoader:[DebugUIMessagesAssetLoader wideLandscapePngInstance] + thread:thread]; +} + ++ (DebugUIMessagesAction *)fakeIncomingLargePngAction:(TSThread *)thread + isAttachmentDownloaded:(BOOL)isAttachmentDownloaded + hasCaption:(BOOL)hasCaption +{ + OWSAssert(thread); + + return [self fakeIncomingMediaAction:@"Fake Incoming Large Png" + isAttachmentDownloaded:isAttachmentDownloaded + hasCaption:hasCaption + fakeAssetLoader:[DebugUIMessagesAssetLoader largePngInstance] + thread:thread]; +} + ++ (DebugUIMessagesAction *)fakeIncomingTinyPngAction:(TSThread *)thread + isAttachmentDownloaded:(BOOL)isAttachmentDownloaded + hasCaption:(BOOL)hasCaption +{ + OWSAssert(thread); + + return [self fakeIncomingMediaAction:@"Tiny Incoming Large Png" + isAttachmentDownloaded:isAttachmentDownloaded + hasCaption:hasCaption + fakeAssetLoader:[DebugUIMessagesAssetLoader tinyPngInstance] + thread:thread]; +} + ++ (DebugUIMessagesAction *)fakeIncomingPngAction:(TSThread *)thread + actionLabel:(NSString *)actionLabel + imageSize:(CGSize)imageSize + backgroundColor:(UIColor *)backgroundColor + textColor:(UIColor *)textColor + imageLabel:(NSString *)imageLabel + isAttachmentDownloaded:(BOOL)isAttachmentDownloaded + hasCaption:(BOOL)hasCaption +{ + OWSAssert(thread); + + return [self fakeIncomingMediaAction:actionLabel + isAttachmentDownloaded:isAttachmentDownloaded + hasCaption:hasCaption + fakeAssetLoader:[DebugUIMessagesAssetLoader pngInstanceWithSize:imageSize + backgroundColor:backgroundColor + textColor:textColor + label:imageLabel] + thread:thread]; +} + ++ (DebugUIMessagesAction *)fakeIncomingTinyPdfAction:(TSThread *)thread + isAttachmentDownloaded:(BOOL)isAttachmentDownloaded + hasCaption:(BOOL)hasCaption +{ + OWSAssert(thread); + + return [self fakeIncomingMediaAction:@"Fake Incoming Tiny Pdf" + isAttachmentDownloaded:isAttachmentDownloaded + hasCaption:hasCaption + fakeAssetLoader:[DebugUIMessagesAssetLoader tinyPdfInstance] + thread:thread]; +} + ++ (DebugUIMessagesAction *)fakeIncomingLargePdfAction:(TSThread *)thread + isAttachmentDownloaded:(BOOL)isAttachmentDownloaded + hasCaption:(BOOL)hasCaption +{ + OWSAssert(thread); + + return [self fakeIncomingMediaAction:@"Fake Incoming Large Pdf" + isAttachmentDownloaded:isAttachmentDownloaded + hasCaption:hasCaption + fakeAssetLoader:[DebugUIMessagesAssetLoader largePdfInstance] + thread:thread]; +} + ++ (DebugUIMessagesAction *)fakeIncomingMissingPngAction:(TSThread *)thread + isAttachmentDownloaded:(BOOL)isAttachmentDownloaded + hasCaption:(BOOL)hasCaption +{ + OWSAssert(thread); + + return [self fakeIncomingMediaAction:@"Fake Incoming Missing Png" + isAttachmentDownloaded:isAttachmentDownloaded + hasCaption:hasCaption + fakeAssetLoader:[DebugUIMessagesAssetLoader missingPngInstance] + thread:thread]; +} + ++ (DebugUIMessagesAction *)fakeIncomingMissingPdfAction:(TSThread *)thread + isAttachmentDownloaded:(BOOL)isAttachmentDownloaded + hasCaption:(BOOL)hasCaption +{ + OWSAssert(thread); + + return [self fakeIncomingMediaAction:@"Fake Incoming Missing Pdf" + isAttachmentDownloaded:isAttachmentDownloaded + hasCaption:hasCaption + fakeAssetLoader:[DebugUIMessagesAssetLoader missingPdfInstance] + thread:thread]; +} + ++ (DebugUIMessagesAction *)fakeIncomingOversizeTextAction:(TSThread *)thread + isAttachmentDownloaded:(BOOL)isAttachmentDownloaded + hasCaption:(BOOL)hasCaption +{ + OWSAssert(thread); + + return [self fakeIncomingMediaAction:@"Fake Incoming Oversize Text" + isAttachmentDownloaded:isAttachmentDownloaded + hasCaption:hasCaption + fakeAssetLoader:[DebugUIMessagesAssetLoader oversizeTextInstance] + thread:thread]; +} + ++ (DebugUIMessagesAction *)fakeIncomingMediaAction:(NSString *)labelParam + isAttachmentDownloaded:(BOOL)isAttachmentDownloaded + hasCaption:(BOOL)hasCaption + fakeAssetLoader:(DebugUIMessagesAssetLoader *)fakeAssetLoader + thread:(TSThread *)thread +{ + OWSAssert(labelParam.length > 0); + OWSAssert(fakeAssetLoader); + OWSAssert(thread); + + NSString *label = labelParam; + if (hasCaption) { + label = [label stringByAppendingString:@" 🔤"]; + } + + if (isAttachmentDownloaded) { + label = [label stringByAppendingString:@" 👍"]; + } + + return + [DebugUIMessagesSingleAction actionWithLabel:label + unstaggeredActionBlock:^(NSUInteger index, YapDatabaseReadWriteTransaction *transaction) { + OWSAssert(fakeAssetLoader.filePath.length > 0); + [self createFakeIncomingMedia:index + isAttachmentDownloaded:isAttachmentDownloaded + hasCaption:hasCaption + fakeAssetLoader:fakeAssetLoader + thread:thread + transaction:transaction]; + } + prepareBlock:fakeAssetLoader.prepareBlock]; +} + ++ (void)createFakeIncomingMedia:(NSUInteger)index + isAttachmentDownloaded:(BOOL)isAttachmentDownloaded + hasCaption:(BOOL)hasCaption + fakeAssetLoader:(DebugUIMessagesAssetLoader *)fakeAssetLoader + thread:(TSThread *)thread + transaction:(YapDatabaseReadWriteTransaction *)transaction +{ + OWSAssert(thread); + OWSAssert(fakeAssetLoader.filePath); + OWSAssert(transaction); + + // // Random time within last n years. Helpful for filling out a media gallery over time. + // double yearsMillis = 4.0 * kYearsInMs; + // uint64_t millisAgo = (uint64_t)(((double)arc4random() / ((double)0xffffffff)) * yearsMillis); + // uint64_t timestamp = [NSDate ows_millisecondTimeStamp] - millisAgo; + + NSString *messageBody = nil; + if (hasCaption) { + // We want a message body that is "more than one line on all devices, + // using all dynamic type sizes." + NSString *sampleText = @"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, " + @"consectetur adipiscing elit."; + messageBody = [[@(index).stringValue stringByAppendingString:@" "] stringByAppendingString:sampleText]; + + if (hasCaption) { + messageBody = [messageBody stringByAppendingString:@" 🔤"]; + } + + if (isAttachmentDownloaded) { + messageBody = [messageBody stringByAppendingString:@" 👍"]; + } + } + NSString *attachmentId; + if (isAttachmentDownloaded) { + DataSource *dataSource = [DataSourcePath dataSourceWithFilePath:fakeAssetLoader.filePath]; + NSString *filename = dataSource.sourceFilename; + // To support "fake missing" attachments, we sometimes lie about the + // length of the data. + UInt32 nominalDataLength = (UInt32)MAX((NSUInteger)1, dataSource.dataLength); + TSAttachmentStream *attachmentStream = [[TSAttachmentStream alloc] initWithContentType:fakeAssetLoader.mimeType + byteCount:nominalDataLength + sourceFilename:filename]; + NSError *error; + BOOL success = [attachmentStream writeData:dataSource.data error:&error]; + OWSAssert(success && !error); + + [attachmentStream saveWithTransaction:transaction]; + attachmentId = attachmentStream.uniqueId; + } else { + UInt32 filesize = 64; + TSAttachmentPointer *pointer = + [[TSAttachmentPointer alloc] initWithServerId:237391539706350548 + key:[self createRandomNSDataOfSize:filesize] + digest:nil + byteCount:filesize + contentType:fakeAssetLoader.mimeType + relay:@"" + sourceFilename:fakeAssetLoader.filename + attachmentType:TSAttachmentTypeDefault]; + pointer.state = TSAttachmentPointerStateFailed; + [pointer saveWithTransaction:transaction]; + attachmentId = pointer.uniqueId; + } + TSIncomingMessage *message = [[TSIncomingMessage alloc] initWithTimestamp:[NSDate ows_millisecondTimeStamp] + inThread:thread + authorId:@"+19174054215" + sourceDeviceId:0 + messageBody:messageBody + attachmentIds:@[ + attachmentId, + ] + expiresInSeconds:0]; + [message markAsReadWithTransaction:transaction sendReadReceipt:NO updateExpiration:NO]; +} + +#pragma mark - Fake Media + ++ (NSArray *)allFakeMediaActions:(TSThread *)thread includeLabels:(BOOL)includeLabels +{ + OWSAssert(thread); + + NSMutableArray *actions = [NSMutableArray new]; + + if (includeLabels) { + [actions addObject:[self fakeOutgoingTextMessageAction:thread + messageState:TSOutgoingMessageStateSentToService + text:@"⚠️ Outgoing Jpeg ⚠️"]]; + } + [actions addObjectsFromArray:@[ + [self fakeOutgoingJpegAction:thread messageState:TSOutgoingMessageStateUnsent hasCaption:NO], + [self fakeOutgoingJpegAction:thread messageState:TSOutgoingMessageStateUnsent hasCaption:YES], + [self fakeOutgoingJpegAction:thread messageState:TSOutgoingMessageStateAttemptingOut hasCaption:NO], + [self fakeOutgoingJpegAction:thread messageState:TSOutgoingMessageStateAttemptingOut hasCaption:YES], + [self fakeOutgoingJpegAction:thread messageState:TSOutgoingMessageStateSentToService hasCaption:NO], + [self fakeOutgoingJpegAction:thread messageState:TSOutgoingMessageStateSentToService hasCaption:YES], + ]]; + if (includeLabels) { + [actions addObject:[self fakeOutgoingTextMessageAction:thread + messageState:TSOutgoingMessageStateSentToService + text:@"⚠️ Outgoing Gif ⚠️"]]; + } + [actions addObjectsFromArray:@[ + // Don't bother with multiple GIF states. + [self fakeOutgoingGifAction:thread messageState:TSOutgoingMessageStateSentToService hasCaption:NO], + [self fakeOutgoingLargeGifAction:thread messageState:TSOutgoingMessageStateSentToService hasCaption:NO], + ]]; + if (includeLabels) { + [actions addObject:[self fakeOutgoingTextMessageAction:thread + messageState:TSOutgoingMessageStateSentToService + text:@"⚠️ Outgoing Mp3 ⚠️"]]; + } + [actions addObjectsFromArray:@[ + [self fakeOutgoingMp3Action:thread messageState:TSOutgoingMessageStateAttemptingOut hasCaption:YES], + [self fakeOutgoingMp3Action:thread messageState:TSOutgoingMessageStateAttemptingOut hasCaption:NO], + [self fakeOutgoingMp3Action:thread messageState:TSOutgoingMessageStateUnsent hasCaption:NO], + [self fakeOutgoingMp3Action:thread messageState:TSOutgoingMessageStateUnsent hasCaption:YES], + [self fakeOutgoingMp3Action:thread messageState:TSOutgoingMessageStateSentToService hasCaption:NO], + [self fakeOutgoingMp3Action:thread messageState:TSOutgoingMessageStateSentToService hasCaption:YES], + ]]; + if (includeLabels) { + [actions addObject:[self fakeOutgoingTextMessageAction:thread + messageState:TSOutgoingMessageStateSentToService + text:@"⚠️ Outgoing Mp4 ⚠️"]]; + } + [actions addObjectsFromArray:@[ + [self fakeOutgoingMp4Action:thread messageState:TSOutgoingMessageStateAttemptingOut hasCaption:NO], + [self fakeOutgoingMp4Action:thread messageState:TSOutgoingMessageStateAttemptingOut hasCaption:YES], + [self fakeOutgoingMp4Action:thread messageState:TSOutgoingMessageStateUnsent hasCaption:NO], + [self fakeOutgoingMp4Action:thread messageState:TSOutgoingMessageStateUnsent hasCaption:YES], + [self fakeOutgoingMp4Action:thread messageState:TSOutgoingMessageStateSentToService hasCaption:NO], + [self fakeOutgoingMp4Action:thread messageState:TSOutgoingMessageStateSentToService hasCaption:YES], + ]]; + if (includeLabels) { + [actions addObject:[self fakeOutgoingTextMessageAction:thread + messageState:TSOutgoingMessageStateSentToService + text:@"⚠️ Outgoing Compact Landscape Png ⚠️"]]; + } + [actions addObjectsFromArray:@[ + [self fakeOutgoingCompactLandscapePngAction:thread + messageState:TSOutgoingMessageStateAttemptingOut + hasCaption:NO], + [self fakeOutgoingCompactLandscapePngAction:thread + messageState:TSOutgoingMessageStateAttemptingOut + hasCaption:YES], + [self fakeOutgoingCompactLandscapePngAction:thread messageState:TSOutgoingMessageStateUnsent hasCaption:NO], + [self fakeOutgoingCompactLandscapePngAction:thread messageState:TSOutgoingMessageStateUnsent hasCaption:YES], + [self fakeOutgoingCompactLandscapePngAction:thread + messageState:TSOutgoingMessageStateSentToService + hasCaption:NO], + [self fakeOutgoingCompactLandscapePngAction:thread + messageState:TSOutgoingMessageStateSentToService + hasCaption:YES], + ]]; + if (includeLabels) { + [actions addObject:[self fakeOutgoingTextMessageAction:thread + messageState:TSOutgoingMessageStateSentToService + text:@"⚠️ Outgoing Compact Portrait Png ⚠️"]]; + } + [actions addObjectsFromArray:@[ + [self fakeOutgoingCompactPortraitPngAction:thread + messageState:TSOutgoingMessageStateAttemptingOut + hasCaption:NO], + [self fakeOutgoingCompactPortraitPngAction:thread + messageState:TSOutgoingMessageStateAttemptingOut + hasCaption:YES], + [self fakeOutgoingCompactPortraitPngAction:thread messageState:TSOutgoingMessageStateUnsent hasCaption:NO], + [self fakeOutgoingCompactPortraitPngAction:thread messageState:TSOutgoingMessageStateUnsent hasCaption:YES], + [self fakeOutgoingCompactPortraitPngAction:thread + messageState:TSOutgoingMessageStateSentToService + hasCaption:NO], + [self fakeOutgoingCompactPortraitPngAction:thread + messageState:TSOutgoingMessageStateSentToService + hasCaption:YES], + ]]; + if (includeLabels) { + [actions addObject:[self fakeOutgoingTextMessageAction:thread + messageState:TSOutgoingMessageStateSentToService + text:@"⚠️ Outgoing Wide Landscape Png ⚠️"]]; + } + [actions addObjectsFromArray:@[ + [self fakeOutgoingWideLandscapePngAction:thread messageState:TSOutgoingMessageStateAttemptingOut hasCaption:NO], + [self fakeOutgoingWideLandscapePngAction:thread + messageState:TSOutgoingMessageStateAttemptingOut + hasCaption:YES], + [self fakeOutgoingWideLandscapePngAction:thread messageState:TSOutgoingMessageStateUnsent hasCaption:NO], + [self fakeOutgoingWideLandscapePngAction:thread messageState:TSOutgoingMessageStateUnsent hasCaption:YES], + [self fakeOutgoingWideLandscapePngAction:thread messageState:TSOutgoingMessageStateSentToService hasCaption:NO], + [self fakeOutgoingWideLandscapePngAction:thread + messageState:TSOutgoingMessageStateSentToService + hasCaption:YES], + ]]; + if (includeLabels) { + [actions addObject:[self fakeOutgoingTextMessageAction:thread + messageState:TSOutgoingMessageStateSentToService + text:@"⚠️ Outgoing Tall Portrait Png ⚠️"]]; + } + [actions addObjectsFromArray:@[ + [self fakeOutgoingTallPortraitPngAction:thread messageState:TSOutgoingMessageStateAttemptingOut hasCaption:NO], + [self fakeOutgoingTallPortraitPngAction:thread messageState:TSOutgoingMessageStateAttemptingOut hasCaption:YES], + [self fakeOutgoingTallPortraitPngAction:thread messageState:TSOutgoingMessageStateUnsent hasCaption:NO], + [self fakeOutgoingTallPortraitPngAction:thread messageState:TSOutgoingMessageStateUnsent hasCaption:YES], + [self fakeOutgoingTallPortraitPngAction:thread messageState:TSOutgoingMessageStateSentToService hasCaption:NO], + [self fakeOutgoingTallPortraitPngAction:thread messageState:TSOutgoingMessageStateSentToService hasCaption:YES], + ]]; + if (includeLabels) { + [actions addObject:[self fakeOutgoingTextMessageAction:thread + messageState:TSOutgoingMessageStateSentToService + text:@"⚠️ Outgoing Large Png ⚠️"]]; + } + [actions addObjectsFromArray:@[ + [self fakeOutgoingLargePngAction:thread messageState:TSOutgoingMessageStateSentToService hasCaption:NO], + [self fakeOutgoingLargePngAction:thread messageState:TSOutgoingMessageStateSentToService hasCaption:YES], + ]]; + if (includeLabels) { + [actions addObject:[self fakeOutgoingTextMessageAction:thread + messageState:TSOutgoingMessageStateSentToService + text:@"⚠️ Outgoing Tiny Png ⚠️"]]; + } + [actions addObjectsFromArray:@[ + [self fakeOutgoingTinyPngAction:thread messageState:TSOutgoingMessageStateSentToService hasCaption:NO], + [self fakeOutgoingTinyPngAction:thread messageState:TSOutgoingMessageStateSentToService hasCaption:YES], + ]]; + if (includeLabels) { + [actions addObject:[self fakeOutgoingTextMessageAction:thread + messageState:TSOutgoingMessageStateSentToService + text:@"⚠️ Outgoing Reserved Color Png ⚠️"]]; + } + [actions addObjectsFromArray:@[ + [self fakeOutgoingPngAction:thread + actionLabel:@"Fake Outgoing White Png" + imageSize:CGSizeMake(200.f, 200.f) + backgroundColor:[UIColor whiteColor] + textColor:[UIColor ows_signalBrandBlueColor] + imageLabel:@"W" + messageState:TSOutgoingMessageStateUnsent + hasCaption:YES], + [self fakeOutgoingPngAction:thread + actionLabel:@"Fake Outgoing White Png" + imageSize:CGSizeMake(200.f, 200.f) + backgroundColor:[UIColor whiteColor] + textColor:[UIColor ows_signalBrandBlueColor] + imageLabel:@"W" + messageState:TSOutgoingMessageStateAttemptingOut + hasCaption:YES], + [self fakeOutgoingPngAction:thread + actionLabel:@"Fake Outgoing White Png" + imageSize:CGSizeMake(200.f, 200.f) + backgroundColor:[UIColor whiteColor] + textColor:[UIColor ows_signalBrandBlueColor] + imageLabel:@"W" + messageState:TSOutgoingMessageStateSentToService + hasCaption:YES], + + [self fakeOutgoingPngAction:thread + actionLabel:@"Fake Outgoing 'Outgoing Unsent' Png" + imageSize:CGSizeMake(200.f, 200.f) + backgroundColor:[OWSMessagesBubbleImageFactory bubbleColorOutgoingUnsent] + textColor:[UIColor whiteColor] + imageLabel:@"W" + messageState:TSOutgoingMessageStateUnsent + hasCaption:YES], + [self fakeOutgoingPngAction:thread + actionLabel:@"Fake Outgoing 'Outgoing Unsent' Png" + imageSize:CGSizeMake(200.f, 200.f) + backgroundColor:[OWSMessagesBubbleImageFactory bubbleColorOutgoingUnsent] + textColor:[UIColor whiteColor] + imageLabel:@"W" + messageState:TSOutgoingMessageStateAttemptingOut + hasCaption:YES], + [self fakeOutgoingPngAction:thread + actionLabel:@"Fake Outgoing 'Outgoing Unsent' Png" + imageSize:CGSizeMake(200.f, 200.f) + backgroundColor:[OWSMessagesBubbleImageFactory bubbleColorOutgoingUnsent] + textColor:[UIColor whiteColor] + imageLabel:@"W" + messageState:TSOutgoingMessageStateSentToService + hasCaption:YES], + + [self fakeOutgoingPngAction:thread + actionLabel:@"Fake Outgoing 'Outgoing Sending' Png" + imageSize:CGSizeMake(200.f, 200.f) + backgroundColor:[OWSMessagesBubbleImageFactory bubbleColorOutgoingSending] + textColor:[UIColor whiteColor] + imageLabel:@"W" + messageState:TSOutgoingMessageStateUnsent + hasCaption:YES], + [self fakeOutgoingPngAction:thread + actionLabel:@"Fake Outgoing 'Outgoing Sending' Png" + imageSize:CGSizeMake(200.f, 200.f) + backgroundColor:[OWSMessagesBubbleImageFactory bubbleColorOutgoingSending] + textColor:[UIColor whiteColor] + imageLabel:@"W" + messageState:TSOutgoingMessageStateAttemptingOut + hasCaption:YES], + [self fakeOutgoingPngAction:thread + actionLabel:@"Fake Outgoing 'Outgoing Sending' Png" + imageSize:CGSizeMake(200.f, 200.f) + backgroundColor:[OWSMessagesBubbleImageFactory bubbleColorOutgoingSending] + textColor:[UIColor whiteColor] + imageLabel:@"W" + messageState:TSOutgoingMessageStateSentToService + hasCaption:YES], + + [self fakeOutgoingPngAction:thread + actionLabel:@"Fake Outgoing 'Outgoing Sent' Png" + imageSize:CGSizeMake(200.f, 200.f) + backgroundColor:[OWSMessagesBubbleImageFactory bubbleColorOutgoingSent] + textColor:[UIColor whiteColor] + imageLabel:@"W" + messageState:TSOutgoingMessageStateUnsent + hasCaption:YES], + [self fakeOutgoingPngAction:thread + actionLabel:@"Fake Outgoing 'Outgoing Sent' Png" + imageSize:CGSizeMake(200.f, 200.f) + backgroundColor:[OWSMessagesBubbleImageFactory bubbleColorOutgoingSent] + textColor:[UIColor whiteColor] + imageLabel:@"W" + messageState:TSOutgoingMessageStateAttemptingOut + hasCaption:YES], + [self fakeOutgoingPngAction:thread + actionLabel:@"Fake Outgoing 'Outgoing Sent' Png" + imageSize:CGSizeMake(200.f, 200.f) + backgroundColor:[OWSMessagesBubbleImageFactory bubbleColorOutgoingSent] + textColor:[UIColor whiteColor] + imageLabel:@"W" + messageState:TSOutgoingMessageStateSentToService + hasCaption:YES], + ]]; + if (includeLabels) { + [actions addObject:[self fakeOutgoingTextMessageAction:thread + messageState:TSOutgoingMessageStateSentToService + text:@"⚠️ Outgoing Tiny Pdf ⚠️"]]; + } + [actions addObjectsFromArray:@[ + [self fakeOutgoingTinyPdfAction:thread messageState:TSOutgoingMessageStateAttemptingOut hasCaption:NO], + [self fakeOutgoingTinyPdfAction:thread messageState:TSOutgoingMessageStateAttemptingOut hasCaption:YES], + [self fakeOutgoingTinyPdfAction:thread messageState:TSOutgoingMessageStateUnsent hasCaption:NO], + [self fakeOutgoingTinyPdfAction:thread messageState:TSOutgoingMessageStateUnsent hasCaption:YES], + [self fakeOutgoingTinyPdfAction:thread messageState:TSOutgoingMessageStateSentToService hasCaption:NO], + [self fakeOutgoingTinyPdfAction:thread messageState:TSOutgoingMessageStateSentToService hasCaption:YES], + ]]; + if (includeLabels) { + [actions addObject:[self fakeOutgoingTextMessageAction:thread + messageState:TSOutgoingMessageStateSentToService + text:@"⚠️ Outgoing Large Pdf ⚠️"]]; + } + [actions addObjectsFromArray:@[ + [self fakeOutgoingLargePdfAction:thread messageState:TSOutgoingMessageStateUnsent hasCaption:NO], + ]]; + if (includeLabels) { + [actions addObject:[self fakeOutgoingTextMessageAction:thread + messageState:TSOutgoingMessageStateSentToService + text:@"⚠️ Outgoing Missing Png ⚠️"]]; + } + [actions addObjectsFromArray:@[ + [self fakeOutgoingMissingPngAction:thread messageState:TSOutgoingMessageStateUnsent hasCaption:NO], + ]]; + if (includeLabels) { + [actions addObject:[self fakeOutgoingTextMessageAction:thread + messageState:TSOutgoingMessageStateSentToService + text:@"⚠️ Outgoing Large Pdf ⚠️"]]; + } + [actions addObjectsFromArray:@[ + [self fakeOutgoingMissingPdfAction:thread messageState:TSOutgoingMessageStateUnsent hasCaption:NO], + ]]; + if (includeLabels) { + [actions addObject:[self fakeOutgoingTextMessageAction:thread + messageState:TSOutgoingMessageStateSentToService + text:@"⚠️ Outgoing Oversize Text ⚠️"]]; + } + [actions addObjectsFromArray:@[ + [self fakeOutgoingOversizeTextAction:thread messageState:TSOutgoingMessageStateUnsent hasCaption:NO], + [self fakeOutgoingOversizeTextAction:thread messageState:TSOutgoingMessageStateAttemptingOut hasCaption:NO], + [self fakeOutgoingOversizeTextAction:thread messageState:TSOutgoingMessageStateSentToService hasCaption:NO], + ]]; + + // Incoming + + if (includeLabels) { + [actions addObject:[self fakeIncomingTextMessageAction:thread text:@"⚠️ Incoming Jpg ⚠️"]]; + } + [actions addObjectsFromArray:@[ + [self fakeIncomingJpegAction:thread isAttachmentDownloaded:NO hasCaption:NO], + [self fakeIncomingJpegAction:thread isAttachmentDownloaded:YES hasCaption:NO], + [self fakeIncomingJpegAction:thread isAttachmentDownloaded:NO hasCaption:YES], + [self fakeIncomingJpegAction:thread isAttachmentDownloaded:YES hasCaption:YES], + ]]; + if (includeLabels) { + [actions addObject:[self fakeIncomingTextMessageAction:thread text:@"⚠️ Incoming Gif ⚠️"]]; + } + [actions addObjectsFromArray:@[ + [self fakeIncomingGifAction:thread isAttachmentDownloaded:YES hasCaption:NO], + [self fakeIncomingLargeGifAction:thread isAttachmentDownloaded:YES hasCaption:NO], + ]]; + if (includeLabels) { + [actions addObject:[self fakeIncomingTextMessageAction:thread text:@"⚠️ Incoming Mp3 ⚠️"]]; + } + [actions addObjectsFromArray:@[ + [self fakeIncomingMp3Action:thread isAttachmentDownloaded:NO hasCaption:NO], + [self fakeIncomingMp3Action:thread isAttachmentDownloaded:YES hasCaption:NO], + [self fakeIncomingMp3Action:thread isAttachmentDownloaded:NO hasCaption:YES], + [self fakeIncomingMp3Action:thread isAttachmentDownloaded:YES hasCaption:YES], + ]]; + if (includeLabels) { + [actions addObject:[self fakeIncomingTextMessageAction:thread text:@"⚠️ Incoming Mp4 ⚠️"]]; + } + [actions addObjectsFromArray:@[ + [self fakeIncomingMp4Action:thread isAttachmentDownloaded:NO hasCaption:NO], + [self fakeIncomingMp4Action:thread isAttachmentDownloaded:YES hasCaption:NO], + [self fakeIncomingMp4Action:thread isAttachmentDownloaded:NO hasCaption:YES], + [self fakeIncomingMp4Action:thread isAttachmentDownloaded:YES hasCaption:YES], + ]]; + if (includeLabels) { + [actions + addObject:[self fakeIncomingTextMessageAction:thread text:@"⚠️ Incoming Compact Landscape Png ⚠️"]]; + } + [actions addObjectsFromArray:@[ + [self fakeIncomingCompactLandscapePngAction:thread isAttachmentDownloaded:NO hasCaption:NO], + [self fakeIncomingCompactLandscapePngAction:thread isAttachmentDownloaded:YES hasCaption:NO], + [self fakeIncomingCompactLandscapePngAction:thread isAttachmentDownloaded:NO hasCaption:YES], + [self fakeIncomingCompactLandscapePngAction:thread isAttachmentDownloaded:YES hasCaption:YES], + ]]; + if (includeLabels) { + [actions + addObject:[self fakeIncomingTextMessageAction:thread text:@"⚠️ Incoming Compact Portrait Png ⚠️"]]; + } + [actions addObjectsFromArray:@[ + [self fakeIncomingCompactPortraitPngAction:thread isAttachmentDownloaded:NO hasCaption:NO], + [self fakeIncomingCompactPortraitPngAction:thread isAttachmentDownloaded:YES hasCaption:NO], + [self fakeIncomingCompactPortraitPngAction:thread isAttachmentDownloaded:NO hasCaption:YES], + [self fakeIncomingCompactPortraitPngAction:thread isAttachmentDownloaded:YES hasCaption:YES], + ]]; + if (includeLabels) { + [actions + addObject:[self fakeIncomingTextMessageAction:thread text:@"⚠️ Incoming Wide Landscape Png ⚠️"]]; + } + [actions addObjectsFromArray:@[ + [self fakeIncomingWideLandscapePngAction:thread isAttachmentDownloaded:NO hasCaption:NO], + [self fakeIncomingWideLandscapePngAction:thread isAttachmentDownloaded:YES hasCaption:NO], + [self fakeIncomingWideLandscapePngAction:thread isAttachmentDownloaded:NO hasCaption:YES], + [self fakeIncomingWideLandscapePngAction:thread isAttachmentDownloaded:YES hasCaption:YES], + ]]; + if (includeLabels) { + [actions + addObject:[self fakeIncomingTextMessageAction:thread text:@"⚠️ Incoming Tall Portrait Png ⚠️"]]; + } + [actions addObjectsFromArray:@[ + [self fakeIncomingTallPortraitPngAction:thread isAttachmentDownloaded:NO hasCaption:NO], + [self fakeIncomingTallPortraitPngAction:thread isAttachmentDownloaded:YES hasCaption:NO], + [self fakeIncomingTallPortraitPngAction:thread isAttachmentDownloaded:NO hasCaption:YES], + [self fakeIncomingTallPortraitPngAction:thread isAttachmentDownloaded:YES hasCaption:YES], + ]]; + if (includeLabels) { + [actions addObject:[self fakeIncomingTextMessageAction:thread text:@"⚠️ Incoming Large Png ⚠️"]]; + } + [actions addObjectsFromArray:@[ + [self fakeIncomingLargePngAction:thread isAttachmentDownloaded:YES hasCaption:NO], + [self fakeIncomingLargePngAction:thread isAttachmentDownloaded:YES hasCaption:YES], + ]]; + if (includeLabels) { + [actions addObject:[self fakeIncomingTextMessageAction:thread text:@"⚠️ Incoming Tiny Png ⚠️"]]; + } + [actions addObjectsFromArray:@[ + [self fakeIncomingTinyPngAction:thread isAttachmentDownloaded:YES hasCaption:NO], + [self fakeIncomingTinyPngAction:thread isAttachmentDownloaded:YES hasCaption:YES], + ]]; + if (includeLabels) { + [actions + addObject:[self fakeIncomingTextMessageAction:thread text:@"⚠️ Incoming Reserved Color Png ⚠️"]]; + } + [actions addObjectsFromArray:@[ + [self fakeIncomingPngAction:thread + actionLabel:@"Fake Incoming White Png" + imageSize:CGSizeMake(200.f, 200.f) + backgroundColor:[UIColor whiteColor] + textColor:[UIColor ows_signalBrandBlueColor] + imageLabel:@"W" + isAttachmentDownloaded:YES + hasCaption:YES], + [self fakeIncomingPngAction:thread + actionLabel:@"Fake Incoming White Png" + imageSize:CGSizeMake(200.f, 200.f) + backgroundColor:[UIColor whiteColor] + textColor:[UIColor ows_signalBrandBlueColor] + imageLabel:@"W" + isAttachmentDownloaded:NO + hasCaption:YES], + [self fakeIncomingPngAction:thread + actionLabel:@"Fake Incoming 'Incoming' Png" + imageSize:CGSizeMake(200.f, 200.f) + backgroundColor:[OWSMessagesBubbleImageFactory bubbleColorIncoming] + textColor:[UIColor whiteColor] + imageLabel:@"W" + isAttachmentDownloaded:YES + hasCaption:YES], + [self fakeIncomingPngAction:thread + actionLabel:@"Fake Incoming 'Incoming' Png" + imageSize:CGSizeMake(200.f, 200.f) + backgroundColor:[OWSMessagesBubbleImageFactory bubbleColorIncoming] + textColor:[UIColor whiteColor] + imageLabel:@"W" + isAttachmentDownloaded:NO + hasCaption:YES], + ]]; + if (includeLabels) { + [actions addObject:[self fakeIncomingTextMessageAction:thread text:@"⚠️ Incoming Tiny Pdf ⚠️"]]; + } + [actions addObjectsFromArray:@[ + [self fakeIncomingTinyPdfAction:thread isAttachmentDownloaded:NO hasCaption:NO], + [self fakeIncomingTinyPdfAction:thread isAttachmentDownloaded:YES hasCaption:NO], + [self fakeIncomingTinyPdfAction:thread isAttachmentDownloaded:NO hasCaption:YES], + [self fakeIncomingTinyPdfAction:thread isAttachmentDownloaded:YES hasCaption:YES], + ]]; + if (includeLabels) { + [actions addObject:[self fakeIncomingTextMessageAction:thread text:@"⚠️ Incoming Large Pdf ⚠️"]]; + } + [actions addObjectsFromArray:@[ + [self fakeIncomingLargePdfAction:thread isAttachmentDownloaded:YES hasCaption:NO], + ]]; + if (includeLabels) { + [actions addObject:[self fakeIncomingTextMessageAction:thread text:@"⚠️ Incoming Missing Png ⚠️"]]; + } + [actions addObjectsFromArray:@[ + [self fakeIncomingMissingPngAction:thread isAttachmentDownloaded:YES hasCaption:NO], + [self fakeIncomingMissingPngAction:thread isAttachmentDownloaded:YES hasCaption:YES], + ]]; + if (includeLabels) { + [actions addObject:[self fakeIncomingTextMessageAction:thread text:@"⚠️ Incoming Missing Pdf ⚠️"]]; + } + [actions addObjectsFromArray:@[ + [self fakeIncomingMissingPdfAction:thread isAttachmentDownloaded:YES hasCaption:NO], + [self fakeIncomingMissingPdfAction:thread isAttachmentDownloaded:YES hasCaption:YES], + ]]; + if (includeLabels) { + [actions addObject:[self fakeIncomingTextMessageAction:thread text:@"⚠️ Incoming Oversize Text ⚠️"]]; + } + [actions addObjectsFromArray:@[ + [self fakeIncomingOversizeTextAction:thread isAttachmentDownloaded:NO hasCaption:NO], + [self fakeIncomingOversizeTextAction:thread isAttachmentDownloaded:YES hasCaption:NO], + ]]; + return actions; +} + ++ (DebugUIMessagesAction *)fakeAllMediaAction:(TSThread *)thread +{ + OWSAssert(thread); + + return [DebugUIMessagesGroupAction allGroupActionWithLabel:@"All Fake Media" + subactions:[self allFakeMediaActions:thread includeLabels:YES]]; +} + ++ (DebugUIMessagesAction *)fakeRandomMediaAction:(TSThread *)thread +{ + OWSAssert(thread); + + return [DebugUIMessagesGroupAction randomGroupActionWithLabel:@"Random Fake Media" + subactions:[self allFakeMediaActions:thread includeLabels:NO]]; +} + +#pragma mark - Send Text Messages + ++ (DebugUIMessagesAction *)sendShortTextMessageAction:(TSThread *)thread +{ + OWSAssert(thread); + + return [DebugUIMessagesSingleAction actionWithLabel:@"Send Short Text Message" + staggeredActionBlock:^(NSUInteger index, + YapDatabaseReadWriteTransaction *transaction, + ActionSuccessBlock success, + ActionFailureBlock failure) { + dispatch_async(dispatch_get_main_queue(), ^{ + [self sendTextMessageInThread:thread counter:index]; + }); + }]; +} + ++ (DebugUIMessagesAction *)sendOversizeTextMessageAction:(TSThread *)thread +{ + OWSAssert(thread); + + return [DebugUIMessagesSingleAction actionWithLabel:@"Send Oversize Text Message" + staggeredActionBlock:^(NSUInteger index, + YapDatabaseReadWriteTransaction *transaction, + ActionSuccessBlock success, + ActionFailureBlock failure) { + dispatch_async(dispatch_get_main_queue(), ^{ + [self sendOversizeTextMessage:thread]; + }); + }]; +} + ++ (DebugUIMessagesAction *)sendMessageVariationsAction:(TSThread *)thread +{ + OWSAssert(thread); + + NSArray *actions = @[ + [self sendShortTextMessageAction:thread], + [self sendOversizeTextMessageAction:thread], + ]; + + return [DebugUIMessagesGroupAction allGroupActionWithLabel:@"Send Conversation Cell Variations" subactions:actions]; +} + +#pragma mark - Fake Text Messages + ++ (DebugUIMessagesAction *)fakeShortIncomingTextMessageAction:(TSThread *)thread +{ + OWSAssert(thread); + + return [DebugUIMessagesSingleAction + actionWithLabel:@"Fake Short Incoming Text Message" + unstaggeredActionBlock:^(NSUInteger index, YapDatabaseReadWriteTransaction *transaction) { + NSString *messageBody = + [[@(index).stringValue stringByAppendingString:@" "] stringByAppendingString:[self randomText]]; + TSIncomingMessage *message = [[TSIncomingMessage alloc] initWithTimestamp:[NSDate ows_millisecondTimeStamp] + inThread:thread + authorId:@"+19174054215" + sourceDeviceId:0 + messageBody:messageBody]; + [message markAsReadWithTransaction:transaction sendReadReceipt:NO updateExpiration:NO]; + }]; +} + ++ (DebugUIMessagesAction *)fakeIncomingTextMessageAction:(TSThread *)thread text:(NSString *)text +{ + OWSAssert(thread); + + return [DebugUIMessagesSingleAction + actionWithLabel:[NSString stringWithFormat:@"Fake Incoming Text Message (%@)", text] + unstaggeredActionBlock:^(NSUInteger index, YapDatabaseReadWriteTransaction *transaction) { + NSString *messageBody = [[@(index).stringValue stringByAppendingString:@" "] stringByAppendingString:text]; + TSIncomingMessage *message = [[TSIncomingMessage alloc] initWithTimestamp:[NSDate ows_millisecondTimeStamp] + inThread:thread + authorId:@"+19174054215" + sourceDeviceId:0 + messageBody:messageBody]; + [message markAsReadWithTransaction:transaction sendReadReceipt:NO updateExpiration:NO]; + }]; +} + ++ (DebugUIMessagesAction *)fakeOutgoingTextMessageAction:(TSThread *)thread + messageState:(TSOutgoingMessageState)messageState + text:(NSString *)text +{ + OWSAssert(thread); + + return [DebugUIMessagesSingleAction + actionWithLabel:[NSString stringWithFormat:@"Fake Incoming Text Message (%@)", text] + unstaggeredActionBlock:^(NSUInteger index, YapDatabaseReadWriteTransaction *transaction) { + NSString *messageBody = [[@(index).stringValue stringByAppendingString:@" "] stringByAppendingString:text]; + TSOutgoingMessage *message = [[TSOutgoingMessage alloc] initWithTimestamp:[NSDate ows_millisecondTimeStamp] + inThread:thread + messageBody:messageBody]; + [message saveWithTransaction:transaction]; + [message updateWithMessageState:messageState transaction:transaction]; + }]; +} + ++ (DebugUIMessagesAction *)fakeShortOutgoingTextMessageAction:(TSThread *)thread + messageState:(TSOutgoingMessageState)messageState +{ + return [self fakeShortOutgoingTextMessageAction:thread messageState:messageState isDelivered:NO isRead:NO]; +} + ++ (DebugUIMessagesAction *)fakeShortOutgoingTextMessageAction:(TSThread *)thread + messageState:(TSOutgoingMessageState)messageState + isDelivered:(BOOL)isDelivered + isRead:(BOOL)isRead +{ + return [self fakeShortOutgoingTextMessageAction:(TSThread *)thread + text:[self randomText] + messageState:messageState + isDelivered:isDelivered + isRead:isRead]; +} + ++ (DebugUIMessagesAction *)fakeShortOutgoingTextMessageAction:(TSThread *)thread + text:(NSString *)text + messageState:(TSOutgoingMessageState)messageState + isDelivered:(BOOL)isDelivered + isRead:(BOOL)isRead +{ + OWSAssert(thread); + + NSString *label = @"Fake Short Incoming Text Message"; + if (messageState == TSOutgoingMessageStateUnsent) { + label = [label stringByAppendingString:@" (Unsent)"]; + } else if (messageState == TSOutgoingMessageStateAttemptingOut) { + label = [label stringByAppendingString:@" (Sending)"]; + } else if (messageState == TSOutgoingMessageStateSentToService) { + if (isRead) { + label = [label stringByAppendingString:@" (Read)"]; + } else if (isDelivered) { + label = [label stringByAppendingString:@" (Delivered)"]; + } else { + label = [label stringByAppendingString:@" (Sent)"]; + } + } else { + OWSFail(@"%@ unknown message state.", self.logTag) + } + + return [DebugUIMessagesSingleAction + actionWithLabel:label + unstaggeredActionBlock:^(NSUInteger index, YapDatabaseReadWriteTransaction *transaction) { + NSString *messageBody = [[@(index).stringValue stringByAppendingString:@" "] stringByAppendingString:text]; + TSOutgoingMessage *message = [[TSOutgoingMessage alloc] initWithTimestamp:[NSDate ows_millisecondTimeStamp] + inThread:thread + messageBody:messageBody]; + [message saveWithTransaction:transaction]; + [message updateWithMessageState:messageState transaction:transaction]; + if (isDelivered) { + NSString *_Nullable recipientId = thread.recipientIdentifiers.lastObject; + OWSAssert(recipientId.length > 0); + [message updateWithDeliveredToRecipientId:recipientId + deliveryTimestamp:@([NSDate ows_millisecondTimeStamp]) + transaction:transaction]; } - failure:^{ - }]; - break; - } - case 1: { - [self ensureRandomJpegWithSuccess:^(NSString *filePath) { - [self sendAttachment:filePath - thread:thread - success:success - failure:^{ - }]; + if (isRead) { + NSString *_Nullable recipientId = thread.recipientIdentifiers.lastObject; + OWSAssert(recipientId.length > 0); + [message updateWithReadRecipientId:recipientId + readTimestamp:[NSDate ows_millisecondTimeStamp] + transaction:transaction]; } - failure:^{ - }]; - break; - } - case 2: { - [self ensureRandomMp3WithSuccess:^(NSString *filePath) { - [self sendAttachment:filePath - thread:thread - success:success - failure:^{ - }]; - } - failure:^{ - }]; - break; - } - case 3: { - [self ensureRandomMp4WithSuccess:^(NSString *filePath) { - [self sendAttachment:filePath - thread:thread - success:success - failure:^{ - }]; - } - failure:^{ - }]; - break; - } - } + }]; } ++ (NSArray *)allFakeTextActions:(TSThread *)thread includeLabels:(BOOL)includeLabels +{ + OWSAssert(thread); + + NSArray *messageBodies = @[ + @"Hi", + @"1️⃣", + @"1️⃣2️⃣", + @"1️⃣2️⃣3️⃣", + @"落", + @"﷽", + ]; + + NSMutableArray *actions = [NSMutableArray new]; + + if (includeLabels) { + [actions addObject:[self fakeIncomingTextMessageAction:thread text:@"⚠️ Incoming Message Bodies ⚠️"]]; + } + [actions addObject:[self fakeShortIncomingTextMessageAction:thread]]; + for (NSString *messageBody in messageBodies) { + [actions addObject:[self fakeIncomingTextMessageAction:thread text:messageBody]]; + } + + if (includeLabels) { + [actions addObject:[self fakeOutgoingTextMessageAction:thread + messageState:TSOutgoingMessageStateSentToService + text:@"⚠️ Outgoing Statuses ⚠️"]]; + } + [actions addObjectsFromArray:@[ + [self fakeShortOutgoingTextMessageAction:thread messageState:TSOutgoingMessageStateUnsent], + [self fakeShortOutgoingTextMessageAction:thread messageState:TSOutgoingMessageStateAttemptingOut], + [self fakeShortOutgoingTextMessageAction:thread messageState:TSOutgoingMessageStateSentToService], + [self fakeShortOutgoingTextMessageAction:thread + messageState:TSOutgoingMessageStateSentToService + isDelivered:YES + isRead:NO], + [self fakeShortOutgoingTextMessageAction:thread + messageState:TSOutgoingMessageStateSentToService + isDelivered:YES + isRead:YES], + ]]; + + if (includeLabels) { + [actions addObject:[self fakeOutgoingTextMessageAction:thread + messageState:TSOutgoingMessageStateSentToService + text:@"⚠️ Outgoing Message Bodies ⚠️"]]; + } + for (NSString *messageBody in messageBodies) { + [actions addObject:[self fakeOutgoingTextMessageAction:thread + messageState:TSOutgoingMessageStateSentToService + text:messageBody]]; + } + return actions; +} + ++ (DebugUIMessagesAction *)fakeAllTextAction:(TSThread *)thread +{ + OWSAssert(thread); + + return [DebugUIMessagesGroupAction allGroupActionWithLabel:@"All Fake Text" + subactions:[self allFakeTextActions:thread includeLabels:YES]]; +} + ++ (DebugUIMessagesAction *)fakeRandomTextAction:(TSThread *)thread +{ + OWSAssert(thread); + + return [DebugUIMessagesGroupAction randomGroupActionWithLabel:@"Random Fake Text" + subactions:[self allFakeTextActions:thread includeLabels:NO]]; +} + +#pragma mark - Exemplary + ++ (NSArray *)allFakeActions:(TSThread *)thread includeLabels:(BOOL)includeLabels +{ + OWSAssert(thread); + + NSMutableArray *actions = [NSMutableArray new]; + [actions addObjectsFromArray:[self allFakeMediaActions:thread includeLabels:includeLabels]]; + [actions addObjectsFromArray:[self allFakeTextActions:thread includeLabels:includeLabels]]; + [actions addObjectsFromArray:[self allFakeSequenceActions:thread includeLabels:includeLabels]]; + return actions; +} + ++ (DebugUIMessagesAction *)allFakeAction:(TSThread *)thread +{ + OWSAssert(thread); + + return [DebugUIMessagesGroupAction allGroupActionWithLabel:@"All Fake" + subactions:[self allFakeActions:thread includeLabels:YES]]; +} + ++ (void)selectFakeAction:(TSThread *)thread +{ + OWSAssertIsOnMainThread(); + OWSAssert(thread); + + [self selectActionUI:[self allFakeActions:thread includeLabels:NO] label:@"Select Fake"]; +} + ++ (void)selectActionUI:(NSArray *)actions label:(NSString *)label +{ + OWSAssertIsOnMainThread(); + UIAlertController *alert = + [UIAlertController alertControllerWithTitle:label message:nil preferredStyle:UIAlertControllerStyleActionSheet]; + for (DebugUIMessagesAction *action in actions) { + [alert addAction:[UIAlertAction actionWithTitle:action.label + style:UIAlertActionStyleDefault + handler:^(UIAlertAction *ignore) { + [self performActionNTimes:action]; + }]]; + } + + [alert addAction:[OWSAlerts cancelAction]]; + + UIViewController *fromViewController = [[UIApplication sharedApplication] frontmostViewController]; + [fromViewController presentViewController:alert animated:YES completion:nil]; +} + +#pragma mark - Sequences + ++ (NSArray *)allFakeSequenceActions:(TSThread *)thread includeLabels:(BOOL)includeLabels +{ + OWSAssert(thread); + + NSMutableArray *actions = [NSMutableArray new]; + + if (includeLabels) { + [actions addObject:[self fakeOutgoingTextMessageAction:thread + messageState:TSOutgoingMessageStateSentToService + text:@"⚠️ Short Message Sequences ⚠️"]]; + } + + [actions addObject:[self fakeIncomingTextMessageAction:thread text:@"Incoming"]]; + [actions addObject:[self fakeOutgoingTextMessageAction:thread + messageState:TSOutgoingMessageStateSentToService + text:@"Outgoing"]]; + [actions addObject:[self fakeIncomingTextMessageAction:thread text:@"Incoming 1"]]; + [actions addObject:[self fakeIncomingTextMessageAction:thread text:@"Incoming 2"]]; + [actions addObject:[self fakeIncomingTextMessageAction:thread text:@"Incoming 3"]]; + [actions addObject:[self fakeOutgoingTextMessageAction:thread + messageState:TSOutgoingMessageStateUnsent + text:@"Outgoing Unsent 1"]]; + [actions addObject:[self fakeOutgoingTextMessageAction:thread + messageState:TSOutgoingMessageStateUnsent + text:@"Outgoing Unsent 2"]]; + [actions addObject:[self fakeOutgoingTextMessageAction:thread + messageState:TSOutgoingMessageStateAttemptingOut + text:@"Outgoing Sending 1"]]; + [actions addObject:[self fakeOutgoingTextMessageAction:thread + messageState:TSOutgoingMessageStateAttemptingOut + text:@"Outgoing Sending 2"]]; + [actions addObject:[self fakeOutgoingTextMessageAction:thread + messageState:TSOutgoingMessageStateSentToService + text:@"Outgoing Sent 1"]]; + [actions addObject:[self fakeOutgoingTextMessageAction:thread + messageState:TSOutgoingMessageStateSentToService + text:@"Outgoing Sent 2"]]; + [actions addObject:[self fakeShortOutgoingTextMessageAction:thread + text:@"Outgoing Delivered 1" + messageState:TSOutgoingMessageStateSentToService + isDelivered:YES + isRead:NO]]; + [actions addObject:[self fakeShortOutgoingTextMessageAction:thread + text:@"Outgoing Delivered 2" + messageState:TSOutgoingMessageStateSentToService + isDelivered:YES + isRead:NO]]; + [actions addObject:[self fakeShortOutgoingTextMessageAction:thread + text:@"Outgoing Read 1" + messageState:TSOutgoingMessageStateSentToService + isDelivered:YES + isRead:YES]]; + [actions addObject:[self fakeShortOutgoingTextMessageAction:thread + text:@"Outgoing Read 2" + messageState:TSOutgoingMessageStateSentToService + isDelivered:YES + isRead:YES]]; + [actions addObject:[self fakeIncomingTextMessageAction:thread text:@"Incoming"]]; + + if (includeLabels) { + [actions addObject:[self fakeOutgoingTextMessageAction:thread + messageState:TSOutgoingMessageStateSentToService + text:@"⚠️ Long Message Sequences ⚠️"]]; + } + + NSString *longText = @"\nLorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse rutrum, nulla " + @"vitae pretium hendrerit, tellus turpis pharetra libero..."; + + [actions addObject:[self fakeIncomingTextMessageAction:thread text:[@"Incoming" stringByAppendingString:longText]]]; + [actions addObject:[self fakeOutgoingTextMessageAction:thread + messageState:TSOutgoingMessageStateSentToService + text:[@"Outgoing" stringByAppendingString:longText]]]; + [actions + addObject:[self fakeIncomingTextMessageAction:thread text:[@"Incoming 1" stringByAppendingString:longText]]]; + [actions + addObject:[self fakeIncomingTextMessageAction:thread text:[@"Incoming 2" stringByAppendingString:longText]]]; + [actions + addObject:[self fakeIncomingTextMessageAction:thread text:[@"Incoming 3" stringByAppendingString:longText]]]; + [actions addObject:[self fakeOutgoingTextMessageAction:thread + messageState:TSOutgoingMessageStateUnsent + text:[@"Outgoing Unsent 1" stringByAppendingString:longText]]]; + [actions addObject:[self fakeOutgoingTextMessageAction:thread + messageState:TSOutgoingMessageStateUnsent + text:[@"Outgoing Unsent 2" stringByAppendingString:longText]]]; + [actions addObject:[self fakeOutgoingTextMessageAction:thread + messageState:TSOutgoingMessageStateAttemptingOut + text:[@"Outgoing Sending 1" stringByAppendingString:longText]]]; + [actions addObject:[self fakeOutgoingTextMessageAction:thread + messageState:TSOutgoingMessageStateAttemptingOut + text:[@"Outgoing Sending 2" stringByAppendingString:longText]]]; + [actions addObject:[self fakeOutgoingTextMessageAction:thread + messageState:TSOutgoingMessageStateSentToService + text:[@"Outgoing Sent 1" stringByAppendingString:longText]]]; + [actions addObject:[self fakeOutgoingTextMessageAction:thread + messageState:TSOutgoingMessageStateSentToService + text:[@"Outgoing Sent 2" stringByAppendingString:longText]]]; + [actions + addObject:[self fakeShortOutgoingTextMessageAction:thread + text:[@"Outgoing Delivered 1" stringByAppendingString:longText] + messageState:TSOutgoingMessageStateSentToService + isDelivered:YES + isRead:NO]]; + [actions + addObject:[self fakeShortOutgoingTextMessageAction:thread + text:[@"Outgoing Delivered 2" stringByAppendingString:longText] + messageState:TSOutgoingMessageStateSentToService + isDelivered:YES + isRead:NO]]; + [actions addObject:[self fakeShortOutgoingTextMessageAction:thread + text:[@"Outgoing Read 1" stringByAppendingString:longText] + messageState:TSOutgoingMessageStateSentToService + isDelivered:YES + isRead:YES]]; + [actions addObject:[self fakeShortOutgoingTextMessageAction:thread + text:[@"Outgoing Read 2" stringByAppendingString:longText] + messageState:TSOutgoingMessageStateSentToService + isDelivered:YES + isRead:YES]]; + [actions addObject:[self fakeIncomingTextMessageAction:thread text:[@"Incoming" stringByAppendingString:longText]]]; + + return actions; +} + ++ (DebugUIMessagesAction *)allFakeSequencesAction:(TSThread *)thread +{ + OWSAssert(thread); + + return [DebugUIMessagesGroupAction allGroupActionWithLabel:@"All Fake Sequences" + subactions:[self allFakeSequenceActions:thread includeLabels:YES]]; +} + +#pragma mark - + + (void)sendOversizeTextMessage:(TSThread *)thread { OWSMessageSender *messageSender = [Environment current].messageSender; NSMutableString *message = [NSMutableString new]; - for (int i = 0; i < 32; i++) { + for (NSUInteger i = 0; i < 32; i++) { [message appendString:@"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse rutrum, nulla " @"vitae pretium hendrerit, tellus turpis pharetra libero, vitae sodales tortor ante vel " @"sem. Fusce sed nisl a lorem gravida tincidunt. Suspendisse efficitur non quam ac " @@ -713,7 +2169,7 @@ NS_ASSUME_NONNULL_BEGIN { OWSAssert(size % 4 == 0); - return [Randomness generateRandomBytes:size]; + return [Randomness generateRandomBytes:(int)size]; } + (void)sendRandomAttachment:(TSThread *)thread uti:(NSString *)uti @@ -745,12 +2201,15 @@ NS_ASSUME_NONNULL_BEGIN ignoreErrors:YES completion:nil]; } + + (OWSSignalServiceProtosEnvelope *)createEnvelopeForThread:(TSThread *)thread { OWSAssert(thread); OWSSignalServiceProtosEnvelopeBuilder *builder = [OWSSignalServiceProtosEnvelopeBuilder new]; + [builder setTimestamp:[NSDate ows_millisecondTimeStamp]]; + if ([thread isKindOfClass:[TSGroupThread class]]) { TSGroupThread *gThread = (TSGroupThread *)thread; [builder setSource:gThread.groupModel.groupMemberIds[0]]; @@ -925,7 +2384,7 @@ NS_ASSUME_NONNULL_BEGIN }]; } -+ (void)sendTextAndSystemMessages:(int)counter thread:(TSThread *)thread ++ (void)sendTextAndSystemMessages:(NSUInteger)counter thread:(TSThread *)thread { if (counter < 1) { return; @@ -979,10 +2438,10 @@ NS_ASSUME_NONNULL_BEGIN return randomText; } -+ (void)createFakeUnreadMessages:(int)counter thread:(TSThread *)thread ++ (void)createFakeUnreadMessages:(NSUInteger)counter thread:(TSThread *)thread { [OWSPrimaryStorage.dbReadWriteConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { - for (int i = 0; i < counter; i++) { + for (NSUInteger i = 0; i < counter; i++) { NSString *randomText = [self randomText]; TSIncomingMessage *message = [[TSIncomingMessage alloc] initWithTimestamp:[NSDate ows_millisecondTimeStamp] inThread:thread @@ -1128,150 +2587,9 @@ NS_ASSUME_NONNULL_BEGIN } } -+ (void)sendFakeMediaMessages:(NSUInteger)counter thread:(TSThread *)thread -{ - // Download media - void (^failureBlock)(void) = ^void(void) { - OWSFail(@"%@ Failed to download example media.", self.logTag); - return; - }; +#pragma mark - - [self ensureRandomGifWithSuccess:^(NSString *_Nonnull gifFilePath) { - [self ensureRandomJpegWithSuccess:^(NSString *_Nonnull jpegFilePath) { - [self ensureRandomMp4WithSuccess:^(NSString *_Nonnull mp4FilePath) { - DataSource *gifDataSource = [DataSourcePath dataSourceWithFilePath:gifFilePath]; - DataSource *jpegDataSource = [DataSourcePath dataSourceWithFilePath:jpegFilePath]; - DataSource *mp4DataSource = [DataSourcePath dataSourceWithFilePath:mp4FilePath]; - - [self sendFakeMediaMessages:counter - thread:thread - gifDataSource:gifDataSource - jpegDataSource:jpegDataSource - mp4DataSource:mp4DataSource]; - } - failure:failureBlock]; - } - failure:failureBlock]; - } - failure:failureBlock]; -} - -+ (void)sendFakeMediaMessages:(NSUInteger)counter - thread:(TSThread *)thread - gifDataSource:(DataSource *)gifDataSource - jpegDataSource:(DataSource *)jpegDataSource - mp4DataSource:(DataSource *)mp4DataSource -{ - const NSUInteger kMaxBatchSize = 2500; - if (counter < kMaxBatchSize) { - [OWSPrimaryStorage.dbReadWriteConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { - [self sendFakeMediaMessages:counter - thread:thread - gifDataSource:gifDataSource - jpegDataSource:jpegDataSource - mp4DataSource:mp4DataSource - transaction:transaction]; - }]; - } else { - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - NSUInteger remainder = counter; - while (remainder > 0) { - NSUInteger batchSize = MIN(kMaxBatchSize, remainder); - [OWSPrimaryStorage.dbReadWriteConnection - readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { - [self sendFakeMediaMessages:batchSize - thread:thread - gifDataSource:gifDataSource - jpegDataSource:jpegDataSource - mp4DataSource:mp4DataSource - transaction:transaction]; - }]; - remainder -= batchSize; - DDLogInfo(@"%@ sendFakeMediaMessages %zd / %zd", self.logTag, counter - remainder, counter); - } - }); - } -} - -+ (void)sendFakeMediaMessages:(NSUInteger)counter - thread:(TSThread *)thread - gifDataSource:(DataSource *)gifDataSource - jpegDataSource:(DataSource *)jpegDataSource - mp4DataSource:(DataSource *)mp4DataSource - transaction:(YapDatabaseReadWriteTransaction *)transaction -{ - DDLogInfo(@"%@ sendFakeMediaMessages: %zd", self.logTag, counter); - - for (NSUInteger i = 0; i < counter; i++) { - - // Random time within last n years. Helpful for filling out a media gallery over time. - double yearsMillis = 4.0 * kYearsInMs; - uint64_t millisAgo = (uint64_t)(((double)arc4random() / ((double)0xffffffff)) * yearsMillis); - - uint64_t timestamp = [NSDate ows_millisecondTimeStamp] - millisAgo; - - - NSString *_Nullable randomCaption; - if (arc4random() % 2 == 2) { - randomCaption = [self randomText]; - } - TSOutgoingMessage *message = [[TSOutgoingMessage alloc] initWithTimestamp:timestamp - inThread:thread - messageBody:randomCaption - isVoiceMessage:NO - expiresInSeconds:0]; - [message setReceivedAtTimestamp:timestamp]; - - TSAttachmentStream *attachmentStream; - NSString *filename; - switch (arc4random_uniform(3)) { - case 0: { - DataSource *dataSource = gifDataSource; - filename = dataSource.sourceFilename; - attachmentStream = [[TSAttachmentStream alloc] initWithContentType:@"image/gif" - byteCount:(UInt32)dataSource.dataLength - sourceFilename:filename]; - NSError *error; - BOOL success = [attachmentStream writeData:dataSource.data error:&error]; - OWSAssert(success && !error); - break; - } - case 1: { - DataSource *dataSource = jpegDataSource; - filename = dataSource.sourceFilename; - attachmentStream = [[TSAttachmentStream alloc] initWithContentType:@"image/jpeg" - byteCount:(UInt32)dataSource.dataLength - sourceFilename:filename]; - NSError *error; - BOOL success = [attachmentStream writeData:dataSource.data error:&error]; - OWSAssert(success && !error); - break; - } - case 2: { - DataSource *dataSource = mp4DataSource; - filename = dataSource.sourceFilename; - attachmentStream = [[TSAttachmentStream alloc] initWithContentType:@"video/mp4" - byteCount:(UInt32)dataSource.dataLength - sourceFilename:filename]; - NSError *error; - BOOL success = [attachmentStream writeData:dataSource.data error:&error]; - OWSAssert(success && !error); - break; - } - } - - [attachmentStream saveWithTransaction:transaction]; - [message.attachmentIds addObject:attachmentStream.uniqueId]; - if (filename) { - message.attachmentFilenameMap[attachmentStream.uniqueId] = filename; - } - [message saveWithTransaction:transaction]; - - [message updateWithMessageState:TSOutgoingMessageStateSentToService transaction:transaction]; - } -} - -+ (void)createFakeLargeOutgoingAttachments:(int)counter thread:(TSThread *)thread ++ (void)createFakeLargeOutgoingAttachments:(NSUInteger)counter thread:(TSThread *)thread { if (counter < 1) { return; @@ -1309,7 +2627,7 @@ NS_ASSUME_NONNULL_BEGIN }); } -+ (void)sendTinyAttachments:(int)counter thread:(TSThread *)thread ++ (void)sendTinyAttachments:(NSUInteger)counter thread:(TSThread *)thread { if (counter < 1) { return; @@ -1330,7 +2648,7 @@ NS_ASSUME_NONNULL_BEGIN }); } -+ (void)createNewGroups:(int)counter recipientId:(NSString *)recipientId ++ (void)createNewGroups:(NSUInteger)counter recipientId:(NSString *)recipientId { if (counter < 1) { return; @@ -1373,24 +2691,24 @@ NS_ASSUME_NONNULL_BEGIN }]; } -+ (void)injectFakeIncomingMessages:(int)counter thread:(TSThread *)thread ++ (void)injectFakeIncomingMessages:(NSUInteger)counter thread:(TSThread *)thread { // Wait 5 seconds so debug user has time to navigate to another // view before message processing occurs. dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5.f * NSEC_PER_SEC)), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - for (int i = 0; i < counter; i++) { + for (NSUInteger i = 0; i < counter; i++) { [self injectIncomingMessageInThread:thread counter:counter - i]; } }); } -+ (void)injectIncomingMessageInThread:(TSThread *)thread counter:(int)counter ++ (void)injectIncomingMessageInThread:(TSThread *)thread counter:(NSUInteger)counter { OWSAssert(thread); - DDLogInfo(@"%@ injectIncomingMessageInThread: %d", self.logTag, counter); + DDLogInfo(@"%@ injectIncomingMessageInThread: %zd", self.logTag, counter); NSString *randomText = [self randomText]; NSString *text = [[[@(counter) description] stringByAppendingString:@" "] stringByAppendingString:randomText]; @@ -1436,7 +2754,7 @@ NS_ASSUME_NONNULL_BEGIN }]; } -+ (void)performRandomActions:(int)counter thread:(TSThread *)thread ++ (void)performRandomActions:(NSUInteger)counter thread:(TSThread *)thread { dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.f * NSEC_PER_SEC)), dispatch_get_main_queue(), @@ -1448,11 +2766,10 @@ NS_ASSUME_NONNULL_BEGIN }); } -+ (void)performRandomActionInThread:(TSThread *)thread - counter:(int)counter ++ (void)performRandomActionInThread:(TSThread *)thread counter:(NSUInteger)counter { - typedef void (^ActionBlock)(YapDatabaseReadWriteTransaction *transaction); - NSArray *actionBlocks = @[ + typedef void (^TransactionBlock)(YapDatabaseReadWriteTransaction *transaction); + NSArray *actionBlocks = @[ ^(YapDatabaseReadWriteTransaction *transaction) { // injectIncomingMessageInThread doesn't take a transaction. dispatch_async(dispatch_get_main_queue(), ^{ @@ -1495,9 +2812,9 @@ NS_ASSUME_NONNULL_BEGIN }, ]; [OWSPrimaryStorage.dbReadWriteConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { - int actionCount = 1 + (int)arc4random_uniform(3); - for (int actionIdx = 0; actionIdx < actionCount; actionIdx++) { - ActionBlock actionBlock = actionBlocks[(NSUInteger)arc4random_uniform((uint32_t)actionBlocks.count)]; + NSUInteger actionCount = 1 + (NSUInteger)arc4random_uniform(3); + for (NSUInteger actionIdx = 0; actionIdx < actionCount; actionIdx++) { + TransactionBlock actionBlock = actionBlocks[(NSUInteger)arc4random_uniform((uint32_t)actionBlocks.count)]; actionBlock(transaction); } }]; @@ -1710,21 +3027,21 @@ NS_ASSUME_NONNULL_BEGIN { OWSAssert(thread); - uint64_t now = [NSDate ows_millisecondTimeStamp]; + long long now = (long long)[NSDate ows_millisecondTimeStamp]; NSArray *timestamps = @[ - @(now + 1 * kHourInMs), + @(now + 1 * (long long)kHourInMs), @(now), - @(now - 1 * kHourInMs), - @(now - 12 * kHourInMs), - @(now - 1 * kDayInMs), - @(now - 2 * kDayInMs), - @(now - 3 * kDayInMs), - @(now - 6 * kDayInMs), - @(now - 7 * kDayInMs), - @(now - 8 * kDayInMs), - @(now - 2 * kWeekInMs), - @(now - 1 * kMonthInMs), - @(now - 2 * kMonthInMs), + @(now - 1 * (long long)kHourInMs), + @(now - 12 * (long long)kHourInMs), + @(now - 1 * (long long)kDayInMs), + @(now - 2 * (long long)kDayInMs), + @(now - 3 * (long long)kDayInMs), + @(now - 6 * (long long)kDayInMs), + @(now - 7 * (long long)kDayInMs), + @(now - 8 * (long long)kDayInMs), + @(now - 2 * (long long)kWeekInMs), + @(now - 1 * (long long)kMonthInMs), + @(now - 2 * (long long)kMonthInMs), ]; NSMutableArray *recipientIds = [thread.recipientIdentifiers mutableCopy]; [recipientIds removeObject:[TSAccountManager localNumber]]; @@ -1882,6 +3199,14 @@ NS_ASSUME_NONNULL_BEGIN }; } ++ (void)deleteAllMessagesInThread:(TSThread *)thread +{ + [OWSPrimaryStorage.sharedManager.newDatabaseConnection + readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { + [thread removeAllThreadInteractionsWithTransaction:transaction]; + }]; +} + @end NS_ASSUME_NONNULL_END diff --git a/Signal/src/ViewControllers/DebugUI/DebugUIMessagesAction.h b/Signal/src/ViewControllers/DebugUI/DebugUIMessagesAction.h new file mode 100644 index 0000000000..44f69c54f1 --- /dev/null +++ b/Signal/src/ViewControllers/DebugUI/DebugUIMessagesAction.h @@ -0,0 +1,62 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "DebugUIMessagesUtils.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface DebugUIMessagesAction : NSObject + +@property (nonatomic) NSString *label; + +- (void)prepareAndPerformNTimes:(NSUInteger)count; + +@end + +#pragma mark - + +@interface DebugUIMessagesSingleAction : DebugUIMessagesAction + ++ (DebugUIMessagesAction *)actionWithLabel:(NSString *)label + staggeredActionBlock:(StaggeredActionBlock)staggeredActionBlock; + ++ (DebugUIMessagesAction *)actionWithLabel:(NSString *)label + unstaggeredActionBlock:(UnstaggeredActionBlock)unstaggeredActionBlock; + ++ (DebugUIMessagesAction *)actionWithLabel:(NSString *)label + staggeredActionBlock:(StaggeredActionBlock)staggeredActionBlock + prepareBlock:(ActionPrepareBlock)prepareBlock; + ++ (DebugUIMessagesAction *)actionWithLabel:(NSString *)label + unstaggeredActionBlock:(UnstaggeredActionBlock)unstaggeredActionBlock + prepareBlock:(ActionPrepareBlock)prepareBlock; + +@end + +#pragma mark - + +typedef NS_ENUM(NSUInteger, SubactionMode) { + SubactionMode_Random = 0, + SubactionMode_Ordered, +}; + +@interface DebugUIMessagesGroupAction : DebugUIMessagesAction + +@property (nonatomic, readonly) SubactionMode subactionMode; +@property (nonatomic, readonly, nullable) NSArray *subactions; + +// Given a group of subactions, perform a single random subaction each time. ++ (DebugUIMessagesAction *)randomGroupActionWithLabel:(NSString *)label + subactions:(NSArray *)subactions; + +// Given a group of subactions, perform the subactions in order. +// +// If prepareAndPerformNTimes: is called with count == subactions.count, all of the subactions +// are performed exactly once. ++ (DebugUIMessagesAction *)allGroupActionWithLabel:(NSString *)label + subactions:(NSArray *)subactions; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Signal/src/ViewControllers/DebugUI/DebugUIMessagesAction.m b/Signal/src/ViewControllers/DebugUI/DebugUIMessagesAction.m new file mode 100644 index 0000000000..2f54d609f0 --- /dev/null +++ b/Signal/src/ViewControllers/DebugUI/DebugUIMessagesAction.m @@ -0,0 +1,288 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "DebugUIMessagesAction.h" +#import + +NS_ASSUME_NONNULL_BEGIN + +@class DebugUIMessagesSingleAction; + +@interface DebugUIMessagesAction () + +@end + +#pragma mark - + +@interface DebugUIMessagesSingleAction () + +@property (nonatomic, nullable) ActionPrepareBlock prepareBlock; + +// "Single" actions should have exactly one "staggered" or "unstaggered" action block. +@property (nonatomic, nullable) StaggeredActionBlock staggeredActionBlock; +@property (nonatomic, nullable) UnstaggeredActionBlock unstaggeredActionBlock; + +@end + +#pragma mark - + +@implementation DebugUIMessagesAction + +- (DebugUIMessagesSingleAction *)nextActionToPerform +{ + return (DebugUIMessagesSingleAction *)self; +} + +- (void)prepare:(ActionSuccessBlock)success failure:(ActionFailureBlock)failure +{ + OWSAssert(success); + OWSAssert(failure); + + OWS_ABSTRACT_METHOD(); + + success(); +} + +- (void)prepareAndPerformNTimes:(NSUInteger)count +{ + DDLogInfo(@"%@ %@ prepareAndPerformNTimes: %zd", self.logTag, self.label, count); + [DDLog flushLog]; + + [self prepare:^{ + [self performNTimes:count + success:^{ + } + failure:^{ + }]; + } + failure:^{ + }]; +} + +- (void)performNTimes:(NSUInteger)countParam success:(ActionSuccessBlock)success failure:(ActionFailureBlock)failure +{ + OWSAssert(success); + OWSAssert(failure); + + DDLogInfo(@"%@ %@ performNTimes: %zd", self.logTag, self.label, countParam); + [DDLog flushLog]; + + if (countParam < 1) { + success(); + return; + } + + __block NSUInteger count = countParam; + [OWSPrimaryStorage.sharedManager.newDatabaseConnection readWriteWithBlock:^( + YapDatabaseReadWriteTransaction *transaction) { + NSUInteger batchSize = 0; + while (count > 0) { + NSUInteger index = count; + + DebugUIMessagesSingleAction *action = [self nextActionToPerform]; + OWSAssert([action isKindOfClass:[DebugUIMessagesSingleAction class]]); + + if (action.staggeredActionBlock) { + OWSAssert(!action.unstaggeredActionBlock); + action.staggeredActionBlock(index, + transaction, + ^{ + dispatch_after( + dispatch_time(DISPATCH_TIME_NOW, (int64_t)1.f * NSEC_PER_SEC), dispatch_get_main_queue(), ^{ + DDLogInfo(@"%@ %@ performNTimes success: %zd", self.logTag, self.label, count); + [self performNTimes:count - 1 success:success failure:failure]; + }); + }, + failure); + + break; + } else { + OWSAssert(action.unstaggeredActionBlock); + + // TODO: We could check result for failure. + action.unstaggeredActionBlock(index, transaction); + + const NSUInteger kMaxBatchSize = 2500; + batchSize++; + if (batchSize >= kMaxBatchSize) { + dispatch_after( + dispatch_time(DISPATCH_TIME_NOW, (int64_t)1.f * NSEC_PER_SEC), dispatch_get_main_queue(), ^{ + DDLogInfo(@"%@ %@ performNTimes success: %zd", self.logTag, self.label, count); + [self performNTimes:count - 1 success:success failure:failure]; + }); + + break; + } + count--; + } + } + }]; +} + +@end + +#pragma mark - + +@implementation DebugUIMessagesSingleAction + ++ (DebugUIMessagesAction *)actionWithLabel:(NSString *)label + staggeredActionBlock:(StaggeredActionBlock)staggeredActionBlock +{ + OWSAssert(label.length > 0); + OWSAssert(staggeredActionBlock); + + DebugUIMessagesSingleAction *instance = [DebugUIMessagesSingleAction new]; + instance.label = label; + instance.staggeredActionBlock = staggeredActionBlock; + return instance; +} + ++ (DebugUIMessagesAction *)actionWithLabel:(NSString *)label + unstaggeredActionBlock:(UnstaggeredActionBlock)unstaggeredActionBlock +{ + OWSAssert(label.length > 0); + OWSAssert(unstaggeredActionBlock); + + DebugUIMessagesSingleAction *instance = [DebugUIMessagesSingleAction new]; + instance.label = label; + instance.unstaggeredActionBlock = unstaggeredActionBlock; + return instance; +} + ++ (DebugUIMessagesAction *)actionWithLabel:(NSString *)label + staggeredActionBlock:(StaggeredActionBlock)staggeredActionBlock + prepareBlock:(ActionPrepareBlock)prepareBlock +{ + OWSAssert(label.length > 0); + OWSAssert(staggeredActionBlock); + OWSAssert(prepareBlock); + + DebugUIMessagesSingleAction *instance = [DebugUIMessagesSingleAction new]; + instance.label = label; + instance.staggeredActionBlock = staggeredActionBlock; + instance.prepareBlock = prepareBlock; + return instance; +} + ++ (DebugUIMessagesAction *)actionWithLabel:(NSString *)label + unstaggeredActionBlock:(UnstaggeredActionBlock)unstaggeredActionBlock + prepareBlock:(ActionPrepareBlock)prepareBlock +{ + OWSAssert(label.length > 0); + OWSAssert(unstaggeredActionBlock); + OWSAssert(prepareBlock); + + DebugUIMessagesSingleAction *instance = [DebugUIMessagesSingleAction new]; + instance.label = label; + instance.unstaggeredActionBlock = unstaggeredActionBlock; + instance.prepareBlock = prepareBlock; + return instance; +} + +- (void)prepare:(ActionSuccessBlock)success failure:(ActionFailureBlock)failure +{ + OWSAssert(success); + OWSAssert(failure); + + if (self.prepareBlock) { + self.prepareBlock(success, failure); + } else { + success(); + } +} + +@end + +#pragma mark - + +@interface DebugUIMessagesGroupAction () + +@property (nonatomic) SubactionMode subactionMode; +@property (nonatomic, nullable) NSArray *subactions; +@property (nonatomic) NSUInteger subactionIndex; + +@end + +#pragma mark - + +@implementation DebugUIMessagesGroupAction + +- (DebugUIMessagesSingleAction *)nextActionToPerform +{ + OWSAssert(self.subactions.count > 0); + + switch (self.subactionMode) { + case SubactionMode_Random: { + DebugUIMessagesAction *subaction = self.subactions[arc4random_uniform((uint32_t)self.subactions.count)]; + OWSAssert(subaction); + return subaction.nextActionToPerform; + } + case SubactionMode_Ordered: { + DebugUIMessagesAction *subaction = self.subactions[self.subactionIndex]; + OWSAssert(subaction); + self.subactionIndex = (self.subactionIndex + 1) % self.subactions.count; + return subaction.nextActionToPerform; + } + } +} + +- (void)prepare:(ActionSuccessBlock)success failure:(ActionFailureBlock)failure +{ + OWSAssert(success); + OWSAssert(failure); + + [DebugUIMessagesGroupAction prepareSubactions:[self.subactions mutableCopy] success:success failure:failure]; +} + ++ (void)prepareSubactions:(NSMutableArray *)unpreparedSubactions + success:(ActionSuccessBlock)success + failure:(ActionFailureBlock)failure +{ + OWSAssert(success); + OWSAssert(failure); + + if (unpreparedSubactions.count < 1) { + return success(); + } + + DebugUIMessagesAction *nextAction = unpreparedSubactions.lastObject; + [unpreparedSubactions removeLastObject]; + DDLogInfo(@"%@ preparing: %@", self.logTag, nextAction.label); + [DDLog flushLog]; + [nextAction prepare:^{ + [self prepareSubactions:unpreparedSubactions success:success failure:failure]; + } + failure:^{ + }]; +} + ++ (DebugUIMessagesAction *)randomGroupActionWithLabel:(NSString *)label + subactions:(NSArray *)subactions +{ + OWSAssert(label.length > 0); + OWSAssert(subactions.count > 0); + + DebugUIMessagesGroupAction *instance = [DebugUIMessagesGroupAction new]; + instance.label = label; + instance.subactions = subactions; + instance.subactionMode = SubactionMode_Random; + return instance; +} + ++ (DebugUIMessagesAction *)allGroupActionWithLabel:(NSString *)label + subactions:(NSArray *)subactions +{ + OWSAssert(label.length > 0); + OWSAssert(subactions.count > 0); + + DebugUIMessagesGroupAction *instance = [DebugUIMessagesGroupAction new]; + instance.label = label; + instance.subactions = subactions; + instance.subactionMode = SubactionMode_Ordered; + return instance; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Signal/src/ViewControllers/DebugUI/DebugUIMessagesAssetLoader.h b/Signal/src/ViewControllers/DebugUI/DebugUIMessagesAssetLoader.h new file mode 100644 index 0000000000..dc35286d57 --- /dev/null +++ b/Signal/src/ViewControllers/DebugUI/DebugUIMessagesAssetLoader.h @@ -0,0 +1,43 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "DebugUIMessagesUtils.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface DebugUIMessagesAssetLoader : NSObject + +@property (nonatomic) NSString *filename; +@property (nonatomic) NSString *mimeType; + +@property (nonatomic) ActionPrepareBlock prepareBlock; + +@property (nonatomic, nullable) NSString *filePath; + +#pragma mark - + ++ (instancetype)jpegInstance; ++ (instancetype)gifInstance; ++ (instancetype)largeGifInstance; ++ (instancetype)mp3Instance; ++ (instancetype)mp4Instance; ++ (instancetype)compactPortraitPngInstance; ++ (instancetype)compactLandscapePngInstance; ++ (instancetype)tallPortraitPngInstance; ++ (instancetype)wideLandscapePngInstance; ++ (instancetype)largePngInstance; ++ (instancetype)tinyPngInstance; ++ (instancetype)pngInstanceWithSize:(CGSize)size + backgroundColor:(UIColor *)backgroundColor + textColor:(UIColor *)textColor + label:(NSString *)label; ++ (instancetype)tinyPdfInstance; ++ (instancetype)largePdfInstance; ++ (instancetype)missingPngInstance; ++ (instancetype)missingPdfInstance; ++ (instancetype)oversizeTextInstance; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Signal/src/ViewControllers/DebugUI/DebugUIMessagesAssetLoader.m b/Signal/src/ViewControllers/DebugUI/DebugUIMessagesAssetLoader.m new file mode 100644 index 0000000000..6781d3450f --- /dev/null +++ b/Signal/src/ViewControllers/DebugUI/DebugUIMessagesAssetLoader.m @@ -0,0 +1,523 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "DebugUIMessagesAssetLoader.h" +#import +#import +#import +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@implementation DebugUIMessagesAssetLoader + ++ (DebugUIMessagesAssetLoader *)fakeAssetLoaderWithUrl:(NSString *)fileUrl mimeType:(NSString *)mimeType +{ + OWSAssert(fileUrl.length > 0); + OWSAssert(mimeType.length > 0); + + DebugUIMessagesAssetLoader *instance = [DebugUIMessagesAssetLoader new]; + instance.mimeType = mimeType; + instance.filename = [NSURL URLWithString:fileUrl].lastPathComponent; + __weak DebugUIMessagesAssetLoader *weakSelf = instance; + instance.prepareBlock = ^(ActionSuccessBlock success, ActionFailureBlock failure) { + [weakSelf ensureURLAssetLoaded:fileUrl success:success failure:failure]; + }; + return instance; +} + +- (void)ensureURLAssetLoaded:(NSString *)fileUrl success:(ActionSuccessBlock)success failure:(ActionFailureBlock)failure +{ + OWSAssert(success); + OWSAssert(failure); + OWSAssert(self.filename.length > 0); + OWSAssert(self.mimeType.length > 0); + + if (self.filePath) { + success(); + return; + } + + // Use a predictable file path so that we reuse the cache between app launches. + NSString *temporaryDirectory = NSTemporaryDirectory(); + NSString *cacheDirectory = [temporaryDirectory stringByAppendingPathComponent:@"cached_random_files"]; + [OWSFileSystem ensureDirectoryExists:cacheDirectory]; + NSString *filePath = [cacheDirectory stringByAppendingPathComponent:self.filename]; + if ([NSFileManager.defaultManager fileExistsAtPath:filePath]) { + self.filePath = filePath; + return success(); + } + + NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration]; + AFHTTPSessionManager *sessionManager = [[AFHTTPSessionManager alloc] initWithSessionConfiguration:configuration]; + sessionManager.responseSerializer = [AFHTTPResponseSerializer serializer]; + OWSAssert(sessionManager.responseSerializer); + [sessionManager GET:fileUrl + parameters:nil + progress:nil + success:^(NSURLSessionDataTask *task, NSData *_Nullable responseObject) { + if ([responseObject writeToFile:filePath atomically:YES]) { + self.filePath = filePath; + OWSAssert([NSFileManager.defaultManager fileExistsAtPath:filePath]); + success(); + } else { + OWSFail(@"Error write url response [%@]: %@", fileUrl, filePath); + failure(); + } + } + failure:^(NSURLSessionDataTask *_Nullable task, NSError *requestError) { + OWSFail(@"Error downloading url[%@]: %@", fileUrl, requestError); + failure(); + }]; +} + +#pragma mark - + ++ (DebugUIMessagesAssetLoader *)fakePngAssetLoaderWithImageSize:(CGSize)imageSize + backgroundColor:(UIColor *)backgroundColor + textColor:(UIColor *)textColor + label:(NSString *)label +{ + OWSAssert(imageSize.width > 0); + OWSAssert(imageSize.height > 0); + OWSAssert(backgroundColor); + OWSAssert(textColor); + OWSAssert(label.length > 0); + + DebugUIMessagesAssetLoader *instance = [DebugUIMessagesAssetLoader new]; + instance.mimeType = OWSMimeTypeImagePng; + instance.filename = @"image.png"; + __weak DebugUIMessagesAssetLoader *weakSelf = instance; + instance.prepareBlock = ^(ActionSuccessBlock success, ActionFailureBlock failure) { + [weakSelf ensurePngAssetLoaded:imageSize + backgroundColor:backgroundColor + textColor:textColor + label:label + success:success + failure:failure]; + }; + return instance; +} + +- (void)ensurePngAssetLoaded:(CGSize)imageSize + backgroundColor:(UIColor *)backgroundColor + textColor:(UIColor *)textColor + label:(NSString *)label + success:(ActionSuccessBlock)success + failure:(ActionFailureBlock)failure +{ + OWSAssert(success); + OWSAssert(failure); + OWSAssert(self.filename.length > 0); + OWSAssert(self.mimeType.length > 0); + OWSAssert(imageSize.width > 0 && imageSize.height > 0); + OWSAssert(backgroundColor); + OWSAssert(textColor); + OWSAssert(label.length > 0); + + if (self.filePath) { + success(); + return; + } + + @autoreleasepool { + NSString *filePath = [OWSFileSystem temporaryFilePathWithFileExtension:@"png"]; + UIImage *image = + [self createRandomPngWithSize:imageSize backgroundColor:backgroundColor textColor:textColor label:label]; + NSData *pngData = UIImagePNGRepresentation(image); + [pngData writeToFile:filePath atomically:YES]; + self.filePath = filePath; + OWSAssert([NSFileManager.defaultManager fileExistsAtPath:filePath]); + success(); + } +} + +- (nullable UIImage *)createRandomPngWithSize:(CGSize)imageSize + backgroundColor:(UIColor *)backgroundColor + textColor:(UIColor *)textColor + label:(NSString *)label +{ + OWSAssert(imageSize.width > 0 && imageSize.height > 0); + OWSAssert(backgroundColor); + OWSAssert(textColor); + OWSAssert(label.length > 0); + + @autoreleasepool { + CGRect frame = CGRectZero; + frame.size = imageSize; + CGFloat smallDimension = MIN(imageSize.width, imageSize.height); + UIFont *font = [UIFont boldSystemFontOfSize:smallDimension * 0.5f]; + NSDictionary *textAttributes = @{ NSFontAttributeName : font, NSForegroundColorAttributeName : textColor }; + + CGRect textFrame = + [label boundingRectWithSize:frame.size + options:(NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading) + attributes:textAttributes + context:nil]; + + UIGraphicsBeginImageContextWithOptions(frame.size, NO, [UIScreen mainScreen].scale); + CGContextRef context = UIGraphicsGetCurrentContext(); + + CGContextSetFillColorWithColor(context, backgroundColor.CGColor); + CGContextFillRect(context, frame); + [label drawAtPoint:CGPointMake(CGRectGetMidX(frame) - CGRectGetMidX(textFrame), + CGRectGetMidY(frame) - CGRectGetMidY(textFrame)) + withAttributes:textAttributes]; + + UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + + return image; + } +} + +#pragma mark - + ++ (DebugUIMessagesAssetLoader *)fakeRandomAssetLoaderWithLength:(NSUInteger)dataLength mimeType:(NSString *)mimeType +{ + OWSAssert(dataLength > 0); + OWSAssert(mimeType.length > 0); + + DebugUIMessagesAssetLoader *instance = [DebugUIMessagesAssetLoader new]; + instance.mimeType = mimeType; + NSString *fileExtension = [MIMETypeUtil fileExtensionForMIMEType:mimeType]; + OWSAssert(fileExtension.length > 0); + instance.filename = [@"attachment" stringByAppendingPathExtension:fileExtension]; + __weak DebugUIMessagesAssetLoader *weakSelf = instance; + instance.prepareBlock = ^(ActionSuccessBlock success, ActionFailureBlock failure) { + [weakSelf ensureRandomAssetLoaded:dataLength success:success failure:failure]; + }; + return instance; +} + +- (void)ensureRandomAssetLoaded:(NSUInteger)dataLength + success:(ActionSuccessBlock)success + failure:(ActionFailureBlock)failure +{ + OWSAssert(dataLength > 0); + OWSAssert(success); + OWSAssert(failure); + OWSAssert(self.filename.length > 0); + OWSAssert(self.mimeType.length > 0); + + if (self.filePath) { + success(); + return; + } + + @autoreleasepool { + NSString *fileExtension = [MIMETypeUtil fileExtensionForMIMEType:self.mimeType]; + OWSAssert(fileExtension.length > 0); + NSData *data = [Randomness generateRandomBytes:(int)dataLength]; + OWSAssert(data); + NSString *filePath = [OWSFileSystem temporaryFilePathWithFileExtension:fileExtension]; + BOOL didWrite = [data writeToFile:filePath atomically:YES]; + OWSAssert(didWrite); + self.filePath = filePath; + OWSAssert([NSFileManager.defaultManager fileExistsAtPath:filePath]); + } + + success(); +} + +#pragma mark - + ++ (DebugUIMessagesAssetLoader *)fakeMissingAssetLoaderWithMimeType:(NSString *)mimeType +{ + OWSAssert(mimeType.length > 0); + + DebugUIMessagesAssetLoader *instance = [DebugUIMessagesAssetLoader new]; + instance.mimeType = mimeType; + NSString *fileExtension = [MIMETypeUtil fileExtensionForMIMEType:mimeType]; + OWSAssert(fileExtension.length > 0); + instance.filename = [@"attachment" stringByAppendingPathExtension:fileExtension]; + __weak DebugUIMessagesAssetLoader *weakSelf = instance; + instance.prepareBlock = ^(ActionSuccessBlock success, ActionFailureBlock failure) { + [weakSelf ensureMissingAssetLoaded:success failure:failure]; + }; + return instance; +} + +- (void)ensureMissingAssetLoaded:(ActionSuccessBlock)success failure:(ActionFailureBlock)failure +{ + OWSAssert(success); + OWSAssert(failure); + OWSAssert(self.filename.length > 0); + OWSAssert(self.mimeType.length > 0); + + if (self.filePath) { + success(); + return; + } + + NSString *fileExtension = [MIMETypeUtil fileExtensionForMIMEType:self.mimeType]; + OWSAssert(fileExtension.length > 0); + NSString *filePath = [OWSFileSystem temporaryFilePathWithFileExtension:fileExtension]; + BOOL didCreate = [NSFileManager.defaultManager createFileAtPath:filePath contents:nil attributes:nil]; + OWSAssert(didCreate); + self.filePath = filePath; + OWSAssert([NSFileManager.defaultManager fileExistsAtPath:filePath]); + + success(); +} + +#pragma mark - + ++ (DebugUIMessagesAssetLoader *)fakeOversizeTextAssetLoader +{ + DebugUIMessagesAssetLoader *instance = [DebugUIMessagesAssetLoader new]; + instance.mimeType = OWSMimeTypeOversizeTextMessage; + instance.filename = @"attachment.txt"; + __weak DebugUIMessagesAssetLoader *weakSelf = instance; + instance.prepareBlock = ^(ActionSuccessBlock success, ActionFailureBlock failure) { + [weakSelf ensureOversizeTextAssetLoaded:success failure:failure]; + }; + return instance; +} + +- (void)ensureOversizeTextAssetLoaded:(ActionSuccessBlock)success failure:(ActionFailureBlock)failure +{ + OWSAssert(success); + OWSAssert(failure); + OWSAssert(self.filename.length > 0); + OWSAssert(self.mimeType.length > 0); + + if (self.filePath) { + success(); + return; + } + + NSMutableString *message = [NSMutableString new]; + for (NSUInteger i = 0; i < 32; i++) { + [message appendString:@"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse rutrum, nulla " + @"vitae pretium hendrerit, tellus turpis pharetra libero, vitae sodales tortor ante vel " + @"sem. Fusce sed nisl a lorem gravida tincidunt. Suspendisse efficitur non quam ac " + @"sodales. Aenean ut velit maximus, posuere sem a, accumsan nunc. Donec ullamcorper " + @"turpis lorem. Quisque dignissim purus eu placerat ultricies. Proin at urna eget mi " + @"semper congue. Aenean non elementum ex. Praesent pharetra quam at sem vestibulum, " + @"vestibulum ornare dolor elementum. Vestibulum massa tortor, scelerisque sit amet " + @"pulvinar a, rhoncus vitae nisl. Sed mi nunc, tempus at varius in, malesuada vitae " + @"dui. Vivamus efficitur pulvinar erat vitae congue. Proin vehicula turpis non felis " + @"congue facilisis. Nullam aliquet dapibus ligula ac mollis. Etiam sit amet posuere " + @"lorem, in rhoncus nisi.\n\n"]; + } + + NSString *fileExtension = @"txt"; + NSString *filePath = [OWSFileSystem temporaryFilePathWithFileExtension:fileExtension]; + NSData *data = [message dataUsingEncoding:NSUTF8StringEncoding]; + OWSAssert(data); + BOOL didWrite = [data writeToFile:filePath atomically:YES]; + OWSAssert(didWrite); + self.filePath = filePath; + OWSAssert([NSFileManager.defaultManager fileExistsAtPath:filePath]); + + success(); +} + +#pragma mark - + ++ (instancetype)jpegInstance +{ + static DebugUIMessagesAssetLoader *instance = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + instance = [DebugUIMessagesAssetLoader + fakeAssetLoaderWithUrl:@"https://s3.amazonaws.com/ows-data/example_attachment_media/random-jpg.JPG" + mimeType:@"image/jpeg"]; + }); + return instance; +} + ++ (instancetype)gifInstance +{ + static DebugUIMessagesAssetLoader *instance = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + instance = [DebugUIMessagesAssetLoader + fakeAssetLoaderWithUrl:@"https://s3.amazonaws.com/ows-data/example_attachment_media/random-gif.gif" + mimeType:@"image/gif"]; + }); + return instance; +} + ++ (instancetype)largeGifInstance +{ + static DebugUIMessagesAssetLoader *instance = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + instance = + [DebugUIMessagesAssetLoader fakeAssetLoaderWithUrl:@"https://i.giphy.com/media/LTw0F3GAdaao8/source.gif" + mimeType:@"image/gif"]; + }); + return instance; +} + ++ (instancetype)mp3Instance +{ + static DebugUIMessagesAssetLoader *instance = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + instance = [DebugUIMessagesAssetLoader + fakeAssetLoaderWithUrl:@"https://s3.amazonaws.com/ows-data/example_attachment_media/random-mp3.mp3" + mimeType:@"audio/mp3"]; + }); + return instance; +} + ++ (instancetype)mp4Instance +{ + static DebugUIMessagesAssetLoader *instance = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + instance = [DebugUIMessagesAssetLoader + fakeAssetLoaderWithUrl:@"https://s3.amazonaws.com/ows-data/example_attachment_media/random-mp4.mp4" + mimeType:@"video/mp4"]; + }); + return instance; +} + ++ (instancetype)compactPortraitPngInstance +{ + static DebugUIMessagesAssetLoader *instance = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + instance = [DebugUIMessagesAssetLoader fakePngAssetLoaderWithImageSize:CGSizeMake(60, 100) + backgroundColor:[UIColor blueColor] + textColor:[UIColor whiteColor] + label:@"P"]; + }); + return instance; +} + ++ (instancetype)compactLandscapePngInstance +{ + static DebugUIMessagesAssetLoader *instance = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + instance = [DebugUIMessagesAssetLoader fakePngAssetLoaderWithImageSize:CGSizeMake(100, 60) + backgroundColor:[UIColor greenColor] + textColor:[UIColor whiteColor] + label:@"L"]; + }); + return instance; +} + ++ (instancetype)tallPortraitPngInstance +{ + static DebugUIMessagesAssetLoader *instance = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + instance = [DebugUIMessagesAssetLoader fakePngAssetLoaderWithImageSize:CGSizeMake(10, 100) + backgroundColor:[UIColor yellowColor] + textColor:[UIColor whiteColor] + label:@"P"]; + }); + return instance; +} + ++ (instancetype)wideLandscapePngInstance +{ + static DebugUIMessagesAssetLoader *instance = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + instance = [DebugUIMessagesAssetLoader fakePngAssetLoaderWithImageSize:CGSizeMake(100, 10) + backgroundColor:[UIColor purpleColor] + textColor:[UIColor whiteColor] + label:@"L"]; + }); + return instance; +} + ++ (instancetype)largePngInstance +{ + static DebugUIMessagesAssetLoader *instance = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + instance = [DebugUIMessagesAssetLoader fakePngAssetLoaderWithImageSize:CGSizeMake(4000, 4000) + backgroundColor:[UIColor brownColor] + textColor:[UIColor whiteColor] + label:@"B"]; + }); + return instance; +} + ++ (instancetype)tinyPngInstance +{ + static DebugUIMessagesAssetLoader *instance = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + instance = [DebugUIMessagesAssetLoader fakePngAssetLoaderWithImageSize:CGSizeMake(2, 2) + backgroundColor:[UIColor cyanColor] + textColor:[UIColor whiteColor] + label:@"T"]; + }); + return instance; +} + ++ (instancetype)pngInstanceWithSize:(CGSize)size + backgroundColor:(UIColor *)backgroundColor + textColor:(UIColor *)textColor + label:(NSString *)label +{ + return [DebugUIMessagesAssetLoader fakePngAssetLoaderWithImageSize:size + backgroundColor:backgroundColor + textColor:textColor + label:label]; +} + ++ (instancetype)tinyPdfInstance +{ + static DebugUIMessagesAssetLoader *instance = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + instance = [DebugUIMessagesAssetLoader fakeRandomAssetLoaderWithLength:256 mimeType:@"application/pdf"]; + }); + return instance; +} + ++ (instancetype)largePdfInstance +{ + static DebugUIMessagesAssetLoader *instance = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + instance = + [DebugUIMessagesAssetLoader fakeRandomAssetLoaderWithLength:4 * 1024 * 1024 mimeType:@"application/pdf"]; + }); + return instance; +} + ++ (instancetype)missingPngInstance +{ + static DebugUIMessagesAssetLoader *instance = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + instance = [DebugUIMessagesAssetLoader fakeMissingAssetLoaderWithMimeType:OWSMimeTypeImagePng]; + }); + return instance; +} + ++ (instancetype)missingPdfInstance +{ + static DebugUIMessagesAssetLoader *instance = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + instance = [DebugUIMessagesAssetLoader fakeMissingAssetLoaderWithMimeType:@"application/pdf"]; + }); + return instance; +} + ++ (instancetype)oversizeTextInstance +{ + static DebugUIMessagesAssetLoader *instance = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + instance = [DebugUIMessagesAssetLoader fakeOversizeTextAssetLoader]; + }); + return instance; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Signal/src/ViewControllers/DebugUI/DebugUIMessagesUtils.h b/Signal/src/ViewControllers/DebugUI/DebugUIMessagesUtils.h new file mode 100644 index 0000000000..261b8587b6 --- /dev/null +++ b/Signal/src/ViewControllers/DebugUI/DebugUIMessagesUtils.h @@ -0,0 +1,18 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +NS_ASSUME_NONNULL_BEGIN + +@class YapDatabaseReadWriteTransaction; + +typedef void (^ActionSuccessBlock)(void); +typedef void (^ActionFailureBlock)(void); +typedef void (^ActionPrepareBlock)(ActionSuccessBlock success, ActionFailureBlock failure); +typedef void (^StaggeredActionBlock)(NSUInteger index, + YapDatabaseReadWriteTransaction *transaction, + ActionSuccessBlock success, + ActionFailureBlock failure); +typedef void (^UnstaggeredActionBlock)(NSUInteger index, YapDatabaseReadWriteTransaction *transaction); + +NS_ASSUME_NONNULL_END diff --git a/Signal/src/ViewControllers/MediaGalleryViewController.swift b/Signal/src/ViewControllers/MediaGalleryViewController.swift index c866da1a8e..0a8c1e0150 100644 --- a/Signal/src/ViewControllers/MediaGalleryViewController.swift +++ b/Signal/src/ViewControllers/MediaGalleryViewController.swift @@ -325,7 +325,7 @@ class MediaGalleryViewController: UINavigationController, MediaGalleryDataSource detailView.backgroundColor = .clear self.view.backgroundColor = .clear - self.presentationView.layer.cornerRadius = OWSMessageCellCornerRadius + self.presentationView.layer.cornerRadius = kOWSMessageCellCornerRadius fromViewController.present(self, animated: false) { @@ -478,7 +478,7 @@ class MediaGalleryViewController: UINavigationController, MediaGalleryDataSource if changedItems { self.presentationView.alpha = 0 } else { - self.presentationView.layer.cornerRadius = OWSMessageCellCornerRadius + self.presentationView.layer.cornerRadius = kOWSMessageCellCornerRadius } }, completion: { (_: Bool) in diff --git a/Signal/src/ViewControllers/MessageDetailViewController.swift b/Signal/src/ViewControllers/MessageDetailViewController.swift index 1f9e964cbe..f657d9af6f 100644 --- a/Signal/src/ViewControllers/MessageDetailViewController.swift +++ b/Signal/src/ViewControllers/MessageDetailViewController.swift @@ -307,10 +307,10 @@ class MessageDetailViewController: OWSViewController, UIScrollViewDelegate, Medi } private func displayableTextIfText() -> String? { - guard viewItem.hasText else { + guard viewItem.hasBodyText else { return nil } - guard let displayableText = viewItem.displayableText() else { + guard let displayableText = viewItem.displayableBodyText() else { return nil } let messageBody = displayableText.fullText diff --git a/SignalMessaging/utils/OWSMessagesBubbleImageFactory.swift b/SignalMessaging/utils/OWSMessagesBubbleImageFactory.swift index 48192996eb..7f9c65d384 100644 --- a/SignalMessaging/utils/OWSMessagesBubbleImageFactory.swift +++ b/SignalMessaging/utils/OWSMessagesBubbleImageFactory.swift @@ -5,7 +5,6 @@ import Foundation import JSQMessagesViewController import SignalServiceKit -import SignalMessaging @objc public class OWSMessagesBubbleImageFactory: NSObject { @@ -21,22 +20,22 @@ public class OWSMessagesBubbleImageFactory: NSObject { }() public lazy var incoming: JSQMessagesBubbleImage = { - let color = UIColor.jsq_messageBubbleLightGray()! + let color = bubbleColorIncoming return self.incoming(color: color) }() public lazy var outgoing: JSQMessagesBubbleImage = { - let color = UIColor.ows_materialBlue + let color = bubbleColorOutgoingSent return self.outgoing(color: color) }() public lazy var currentlyOutgoing: JSQMessagesBubbleImage = { - let color = UIColor.ows_fadedBlue + let color = bubbleColorOutgoingSending return self.outgoing(color: color) }() public lazy var outgoingFailed: JSQMessagesBubbleImage = { - let color = UIColor.gray + let color = bubbleColorOutgoingUnsent return self.outgoing(color: color) }() @@ -58,6 +57,32 @@ public class OWSMessagesBubbleImageFactory: NSObject { } } + public static let bubbleColorIncoming = UIColor.jsq_messageBubbleLightGray()! + + public static let bubbleColorOutgoingUnsent = UIColor.gray + + public static let bubbleColorOutgoingSending = UIColor.ows_fadedBlue + + public static let bubbleColorOutgoingSent = UIColor.ows_materialBlue + + public func bubbleColor(message: TSMessage) -> UIColor { + if message is TSIncomingMessage { + return OWSMessagesBubbleImageFactory.bubbleColorIncoming + } else if let outgoingMessage = message as? TSOutgoingMessage { + switch outgoingMessage.messageState { + case .unsent: + return OWSMessagesBubbleImageFactory.bubbleColorOutgoingUnsent + case .attemptingOut: + return OWSMessagesBubbleImageFactory.bubbleColorOutgoingSending + default: + return OWSMessagesBubbleImageFactory.bubbleColorOutgoingSent + } + } else { + owsFail("Unexpected message type: \(message)") + return UIColor.ows_materialBlue + } + } + private func outgoing(color: UIColor) -> JSQMessagesBubbleImage { if isRTL { return jsqFactory.incomingMessagesBubbleImage(with: color) diff --git a/SignalServiceKit/src/Contacts/TSThread.h b/SignalServiceKit/src/Contacts/TSThread.h index f56e65fd7b..8ed22a61fd 100644 --- a/SignalServiceKit/src/Contacts/TSThread.h +++ b/SignalServiceKit/src/Contacts/TSThread.h @@ -1,5 +1,5 @@ // -// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. // #import "TSYapDatabaseObject.h" @@ -133,6 +133,8 @@ NS_ASSUME_NONNULL_BEGIN */ - (void)unarchiveThreadWithTransaction:(YapDatabaseReadWriteTransaction *)transaction; +- (void)removeAllThreadInteractionsWithTransaction:(YapDatabaseReadWriteTransaction *)transaction; + #pragma mark Drafts /** diff --git a/SignalServiceKit/src/Contacts/TSThread.m b/SignalServiceKit/src/Contacts/TSThread.m index 0adb101ce1..b4798cdfa6 100644 --- a/SignalServiceKit/src/Contacts/TSThread.m +++ b/SignalServiceKit/src/Contacts/TSThread.m @@ -49,8 +49,13 @@ NS_ASSUME_NONNULL_BEGIN - (void)removeWithTransaction:(YapDatabaseReadWriteTransaction *)transaction { - [super removeWithTransaction:transaction]; + [self removeAllThreadInteractionsWithTransaction:transaction]; + [super removeWithTransaction:transaction]; +} + +- (void)removeAllThreadInteractionsWithTransaction:(YapDatabaseReadWriteTransaction *)transaction +{ // We can't safely delete interactions while enumerating them, so // we collect and delete separately. // diff --git a/SignalServiceKit/src/Storage/OWSPrimaryStorage.m b/SignalServiceKit/src/Storage/OWSPrimaryStorage.m index ddff2a59a3..307ebe66c7 100644 --- a/SignalServiceKit/src/Storage/OWSPrimaryStorage.m +++ b/SignalServiceKit/src/Storage/OWSPrimaryStorage.m @@ -132,26 +132,42 @@ void runAsyncRegistrationsForStorage(OWSStorage *storage) { OWSAssert(completion); + [((OWSDatabase *)self.database)collectRegistrationConnections]; + runAsyncRegistrationsForStorage(self); DDLogVerbose(@"%@ async registrations enqueued.", self.logTag); // Block until all async registrations are complete. // - // NOTE: This has to happen on the "registration connection" for this + // NOTE: This has to happen on the "registration connections" for this // database. - YapDatabaseConnection *dbConnection = self.registrationConnection; - OWSAssert(self.registrationConnection); - [dbConnection flushTransactionsWithCompletionQueue:dispatch_get_main_queue() - completionBlock:^{ - OWSAssert(!self.areAsyncRegistrationsComplete); + NSMutableSet *pendingRegistrationConnectionSet = + [[((OWSDatabase *)self.database)clearCollectedRegistrationConnections] mutableCopy]; + DDLogVerbose(@"%@ flushing registration connections: %zd.", self.logTag, pendingRegistrationConnectionSet.count); - DDLogVerbose(@"%@ async registrations complete.", self.logTag); + dispatch_async(dispatch_get_main_queue(), ^{ + for (YapDatabaseConnection *dbConnection in pendingRegistrationConnectionSet) { + [dbConnection + flushTransactionsWithCompletionQueue:dispatch_get_main_queue() + completionBlock:^{ + OWSAssertIsOnMainThread(); + OWSAssert(!self.areAsyncRegistrationsComplete); - self.areAsyncRegistrationsComplete = YES; + [pendingRegistrationConnectionSet removeObject:dbConnection]; + if (pendingRegistrationConnectionSet.count > 0) { + DDLogVerbose(@"%@ registration connection flushed.", self.logTag); + return; + } - completion(); - }]; + DDLogVerbose(@"%@ async registrations complete.", self.logTag); + + self.areAsyncRegistrationsComplete = YES; + + completion(); + }]; + } + }); } + (void)protectFiles diff --git a/SignalServiceKit/src/Storage/OWSStorage.h b/SignalServiceKit/src/Storage/OWSStorage.h index 75f710db16..5beab984ef 100644 --- a/SignalServiceKit/src/Storage/OWSStorage.h +++ b/SignalServiceKit/src/Storage/OWSStorage.h @@ -2,7 +2,7 @@ // Copyright (c) 2018 Open Whisper Systems. All rights reserved. // -#import +#import NS_ASSUME_NONNULL_BEGIN @@ -34,6 +34,26 @@ extern NSString *const StorageIsReadyNotification; #pragma mark - +@interface OWSDatabase : YapDatabase + +- (instancetype)init NS_UNAVAILABLE; + +- (id)initWithPath:(NSString *)inPath + serializer:(nullable YapDatabaseSerializer)inSerializer + deserializer:(YapDatabaseDeserializer)inDeserializer + options:(YapDatabaseOptions *)inOptions + delegate:(id)delegate NS_DESIGNATED_INITIALIZER; + +// Starts collecting references to the registration connections. +- (void)collectRegistrationConnections; +// Stops collecting references to the registration connections and returns +// all collected connections. +- (NSSet *)clearCollectedRegistrationConnections; + +@end + +#pragma mark - + @interface OWSStorage : NSObject - (instancetype)init NS_UNAVAILABLE; diff --git a/SignalServiceKit/src/Storage/OWSStorage.m b/SignalServiceKit/src/Storage/OWSStorage.m index 73fc3680db..e79cc80697 100644 --- a/SignalServiceKit/src/Storage/OWSStorage.m +++ b/SignalServiceKit/src/Storage/OWSStorage.m @@ -130,17 +130,11 @@ typedef NSData *_Nullable (^CreateDatabaseMetadataBlock)(void); #pragma mark - -@interface OWSDatabase : YapDatabase +@interface OWSDatabase () @property (atomic, weak) id delegate; -- (instancetype)init NS_UNAVAILABLE; - -- (id)initWithPath:(NSString *)inPath - serializer:(nullable YapDatabaseSerializer)inSerializer - deserializer:(YapDatabaseDeserializer)inDeserializer - options:(YapDatabaseOptions *)inOptions - delegate:(id)delegate NS_DESIGNATED_INITIALIZER; +@property (nonatomic, readonly, nullable) NSMutableSet *registrationConnectionSet; @end @@ -191,9 +185,29 @@ typedef NSData *_Nullable (^CreateDatabaseMetadataBlock)(void); ((OWSDatabaseConnection *)connection).canWriteBeforeStorageReady = YES; #endif + [self.registrationConnectionSet addObject:connection]; + return connection; } +- (void)collectRegistrationConnections +{ + OWSAssert(!self.registrationConnectionSet); + + _registrationConnectionSet = [NSMutableSet set]; +} + +- (NSSet *)clearCollectedRegistrationConnections +{ + OWSAssert(self.registrationConnectionSet); + + NSSet *registrationConnectionSetCopy = [self.registrationConnectionSet copy]; + + _registrationConnectionSet = nil; + + return registrationConnectionSetCopy; +} + @end #pragma mark - diff --git a/SignalServiceKit/src/Util/NSDate+OWS.h b/SignalServiceKit/src/Util/NSDate+OWS.h index e48a6f5709..2455b74392 100755 --- a/SignalServiceKit/src/Util/NSDate+OWS.h +++ b/SignalServiceKit/src/Util/NSDate+OWS.h @@ -12,7 +12,7 @@ extern const NSTimeInterval kDayInterval; extern const NSTimeInterval kWeekInterval; extern const NSTimeInterval kMonthInterval; -#define kSecondInMs 1000 +#define kSecondInMs ((uint64_t)1000) #define kMinuteInMs (kSecondInMs * 60) #define kHourInMs (kMinuteInMs * 60) #define kDayInMs (kHourInMs * 24)