From 0dfa9cac7bb3b7216eaffafc489ee566f06cc665 Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Fri, 23 Mar 2018 09:40:49 -0400 Subject: [PATCH 01/31] Elaborate debug UI for messages. --- .../AppSettings/AppSettingsViewController.m | 4 + .../ViewControllers/DebugUI/DebugUIMessages.m | 387 +++++++----------- .../src/ViewControllers/HomeViewController.m | 4 + 3 files changed, 145 insertions(+), 250 deletions(-) diff --git a/Signal/src/ViewControllers/AppSettings/AppSettingsViewController.m b/Signal/src/ViewControllers/AppSettings/AppSettingsViewController.m index 97b37ab0ff..494217f087 100644 --- a/Signal/src/ViewControllers/AppSettings/AppSettingsViewController.m +++ b/Signal/src/ViewControllers/AppSettings/AppSettingsViewController.m @@ -91,6 +91,10 @@ self.title = NSLocalizedString(@"SETTINGS_NAV_BAR_TITLE", @"Title for settings activity"); [self updateTableContents]; + + dispatch_async(dispatch_get_main_queue(), ^{ + [self showDebugUI]; + }); } - (void)viewWillAppear:(BOOL)animated diff --git a/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m b/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m index b7f38ac453..54d8e2cd75 100644 --- a/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m +++ b/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m @@ -26,6 +26,14 @@ NS_ASSUME_NONNULL_BEGIN +typedef NS_ENUM(NSUInteger, DebugMediaType) { + DebugMediaType_Gif, + DebugMediaType_Jpeg, + DebugMediaType_Mp3, + DebugMediaType_Mp4, + // A mix of the above options. + DebugMediaType_Random, +}; @interface TSOutgoingMessage (PostDatingDebug) @@ -187,65 +195,9 @@ NS_ASSUME_NONNULL_BEGIN actionBlock:^{ [DebugUIMessages sendRandomAttachment:thread uti:(NSString *)kUTTypePDF]; }], - [OWSTableItem itemWithTitle:@"Send 1 Random GIF (1/sec.)" + [OWSTableItem itemWithTitle:@"Send N Random Media (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]; + [DebugUIMessages sendRandomMediaInThread:thread]; }], [OWSTableItem itemWithTitle:@"Create all system messages" actionBlock:^{ @@ -452,46 +404,136 @@ NS_ASSUME_NONNULL_BEGIN success(); } -+ (void)ensureRandomGifWithSuccess:(nullable void (^)(NSString *filePath))success - failure:(nullable void (^)(void))failure +#pragma mark - Media + ++ (NSArray *)allMediaTypes { - [self ensureRandomFileWithURL:@"https://s3.amazonaws.com/ows-data/example_attachment_media/random-gif.gif" - filename:@"random-gif.gif" - success:success - failure:failure]; + return @[ + @(DebugMediaType_Gif), + @(DebugMediaType_Jpeg), + @(DebugMediaType_Mp3), + @(DebugMediaType_Mp4), + @(DebugMediaType_Random), + ]; } -+ (void)sendRandomGifInThread:(TSThread *)thread - success:(nullable void (^)(void))success - failure:(nullable void (^)(void))failure ++ (void)sendRandomMediaInThread:(TSThread *)thread { - [self ensureRandomGifWithSuccess:^(NSString *filePath) { - [self sendAttachment:filePath thread:thread success:success failure:failure]; + OWSAssertIsOnMainThread() OWSAssert(thread); + + UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"Select Media Type" + message:nil + preferredStyle:UIAlertControllerStyleActionSheet]; + for (NSNumber *value in self.allMediaTypes) { + DebugMediaType mediaType = (DebugMediaType)value.intValue; + NSString *label; + switch (mediaType) { + case DebugMediaType_Gif: + label = @"Gif"; + break; + case DebugMediaType_Jpeg: + label = @"Jpeg"; + break; + case DebugMediaType_Mp3: + label = @"Mp3"; + break; + case DebugMediaType_Mp4: + label = @"Mp4"; + break; + case DebugMediaType_Random: + label = @"Random"; + break; + } + + [alert addAction:[UIAlertAction actionWithTitle:label + style:UIAlertActionStyleDefault + handler:^(UIAlertAction *action) { + [self sendRandomMedia:mediaType thread:thread]; + }]]; } - failure:failure]; + + [alert addAction:[OWSAlerts cancelAction]]; + + UIViewController *fromViewController = [[UIApplication sharedApplication] frontmostViewController]; + [fromViewController presentViewController:alert animated:YES completion:nil]; } -+ (void)sendRandomGifs:(int)count thread:(TSThread *)thread ++ (void)sendRandomMedia:(DebugMediaType)mediaType thread:(TSThread *)thread { + OWSAssertIsOnMainThread() OWSAssert(thread); + + UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"How many to send?" + 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 *action) { + [self sendRandomMedia:mediaType + count:countValue.unsignedIntegerValue + thread:thread]; + }]]; + } + + [alert addAction:[OWSAlerts cancelAction]]; + UIViewController *fromViewController = [[UIApplication sharedApplication] frontmostViewController]; + [fromViewController presentViewController:alert animated:YES completion:nil]; +} + ++ (void)sendRandomMedia:(DebugMediaType)mediaType count:(NSUInteger)count thread:(TSThread *)thread +{ + OWSAssert(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:^{ - }]; + + void (^completion)(void) = ^{ + if (count <= 1) { + return; + } + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)1.f * NSEC_PER_SEC), dispatch_get_main_queue(), ^{ + [self sendRandomMedia:mediaType count:count - 1 thread:thread]; + }); + }; + void (^sendMessage)(NSString *) = ^(NSString *filePath) { + [self sendAttachment:filePath + thread:thread + success:completion + failure:^{ + }]; + }; + DebugMediaType mediaTypeToSend = mediaType; + while (mediaTypeToSend == DebugMediaType_Random) { + NSArray *allMediaTypes = self.allMediaTypes; + NSNumber *value = allMediaTypes[arc4random_uniform((uint32_t)allMediaTypes.count)]; + mediaTypeToSend = (DebugMediaType)value.intValue; + } + + switch (mediaTypeToSend) { + case DebugMediaType_Gif: + return [self ensureRandomGifWithSuccess:sendMessage + failure:^{ + }]; + case DebugMediaType_Jpeg: + return [self ensureRandomJpegWithSuccess:sendMessage + failure:^{ + }]; + case DebugMediaType_Mp3: + return [self ensureRandomMp3WithSuccess:sendMessage + failure:^{ + }]; + case DebugMediaType_Mp4: + return [self ensureRandomMp4WithSuccess:sendMessage + failure:^{ + }]; + case DebugMediaType_Random: + OWSFail(@"%@ Invalid value.", self.logTag); + break; } - failure:^{ - }]; } + (void)ensureRandomJpegWithSuccess:(nullable void (^)(NSString *filePath))success @@ -503,39 +545,6 @@ NS_ASSUME_NONNULL_BEGIN 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 { @@ -545,39 +554,6 @@ NS_ASSUME_NONNULL_BEGIN 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 { @@ -587,102 +563,13 @@ NS_ASSUME_NONNULL_BEGIN failure:failure]; } -+ (void)sendRandomMp4InThread:(TSThread *)thread - success:(nullable void (^)(void))success - failure:(nullable void (^)(void))failure ++ (void)ensureRandomGifWithSuccess:(nullable void (^)(NSString *filePath))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]; - }); - }; - - switch (arc4random_uniform(4)) { - case 0: { - [self ensureRandomGifWithSuccess:^(NSString *filePath) { - [self sendAttachment:filePath - thread:thread - success:success - failure:^{ - }]; - } - failure:^{ - }]; - break; - } - case 1: { - [self ensureRandomJpegWithSuccess:^(NSString *filePath) { - [self sendAttachment:filePath - thread:thread - success:success - failure:^{ - }]; - } - 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; - } - } + [self ensureRandomFileWithURL:@"https://s3.amazonaws.com/ows-data/example_attachment_media/random-gif.gif" + filename:@"random-gif.gif" + success:success + failure:failure]; } + (void)sendOversizeTextMessage:(TSThread *)thread @@ -713,7 +600,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 diff --git a/Signal/src/ViewControllers/HomeViewController.m b/Signal/src/ViewControllers/HomeViewController.m index dd48ef95ef..1a37cccd71 100644 --- a/Signal/src/ViewControllers/HomeViewController.m +++ b/Signal/src/ViewControllers/HomeViewController.m @@ -284,6 +284,10 @@ typedef NS_ENUM(NSInteger, CellState) { kArchiveState, kInboxState }; } [self updateBarButtonItems]; + + dispatch_async(dispatch_get_main_queue(), ^{ + [self settingsButtonPressed:nil]; + }); } - (void)viewDidAppear:(BOOL)animated From 68f3334e77f8f842ac0071bfd0025c6d458211d9 Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Fri, 23 Mar 2018 09:57:22 -0400 Subject: [PATCH 02/31] Elaborate debug UI for messages. --- .../ViewControllers/DebugUI/DebugUIMessages.m | 122 ++++++++++-------- 1 file changed, 67 insertions(+), 55 deletions(-) diff --git a/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m b/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m index 54d8e2cd75..f565a99271 100644 --- a/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m +++ b/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m @@ -35,6 +35,10 @@ typedef NS_ENUM(NSUInteger, DebugMediaType) { DebugMediaType_Random, }; +typedef void (^ActionSucceesBlock)(void); +typedef void (^ActionFailureBlock)(void); +typedef void (^ActionBlock)(ActionSucceesBlock success, ActionFailureBlock failure); + @interface TSOutgoingMessage (PostDatingDebug) - (void)setReceivedAtTimestamp:(uint64_t)value; @@ -445,11 +449,13 @@ typedef NS_ENUM(NSUInteger, DebugMediaType) { break; } - [alert addAction:[UIAlertAction actionWithTitle:label - style:UIAlertActionStyleDefault - handler:^(UIAlertAction *action) { - [self sendRandomMedia:mediaType thread:thread]; - }]]; + [alert addAction:[UIAlertAction + actionWithTitle:label + style:UIAlertActionStyleDefault + handler:^(UIAlertAction *action) { + [self performActionNTimes:[self sendRandomMediaAction:mediaType thread:thread] + thread:thread]; + }]]; } [alert addAction:[OWSAlerts cancelAction]]; @@ -458,14 +464,54 @@ typedef NS_ENUM(NSUInteger, DebugMediaType) { [fromViewController presentViewController:alert animated:YES completion:nil]; } -+ (void)sendRandomMedia:(DebugMediaType)mediaType thread:(TSThread *)thread ++ (ActionBlock)sendRandomMediaAction:(DebugMediaType)mediaType thread:(TSThread *)thread +{ + OWSAssert(thread); + + return ^(ActionSucceesBlock success, ActionFailureBlock failure) { + void (^sendMessage)(NSString *) = ^(NSString *filePath) { + [self sendAttachment:filePath thread:thread success:success failure:failure]; + }; + DebugMediaType mediaTypeToSend = mediaType; + while (mediaTypeToSend == DebugMediaType_Random) { + NSArray *allMediaTypes = self.allMediaTypes; + NSNumber *value = allMediaTypes[arc4random_uniform((uint32_t)allMediaTypes.count)]; + mediaTypeToSend = (DebugMediaType)value.intValue; + } + + switch (mediaTypeToSend) { + case DebugMediaType_Gif: + return [self ensureRandomGifWithSuccess:sendMessage + failure:^{ + }]; + case DebugMediaType_Jpeg: + return [self ensureRandomJpegWithSuccess:sendMessage + failure:^{ + }]; + case DebugMediaType_Mp3: + return [self ensureRandomMp3WithSuccess:sendMessage + failure:^{ + }]; + case DebugMediaType_Mp4: + return [self ensureRandomMp4WithSuccess:sendMessage + failure:^{ + }]; + case DebugMediaType_Random: + OWSFail(@"%@ Invalid value.", self.logTag); + break; + } + }; +} + ++ (void)performActionNTimes:(ActionBlock)action thread:(TSThread *)thread { OWSAssertIsOnMainThread() OWSAssert(thread); - UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"How many to send?" + UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"How many?" message:nil preferredStyle:UIAlertControllerStyleActionSheet]; for (NSNumber *countValue in @[ + @(0), @(1), @(10), @(100), @@ -474,10 +520,8 @@ typedef NS_ENUM(NSUInteger, DebugMediaType) { ]) { [alert addAction:[UIAlertAction actionWithTitle:countValue.stringValue style:UIAlertActionStyleDefault - handler:^(UIAlertAction *action) { - [self sendRandomMedia:mediaType - count:countValue.unsignedIntegerValue - thread:thread]; + handler:^(UIAlertAction *ignore) { + [self performAction:action count:countValue.unsignedIntegerValue]; }]]; } @@ -486,54 +530,22 @@ typedef NS_ENUM(NSUInteger, DebugMediaType) { [fromViewController presentViewController:alert animated:YES completion:nil]; } -+ (void)sendRandomMedia:(DebugMediaType)mediaType count:(NSUInteger)count thread:(TSThread *)thread ++ (void)performAction:(ActionBlock)action count:(NSUInteger)count { - OWSAssert(thread); + OWSAssert(action); OWSAssert(count > 0); - void (^completion)(void) = ^{ - if (count <= 1) { - return; - } - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)1.f * NSEC_PER_SEC), dispatch_get_main_queue(), ^{ - [self sendRandomMedia:mediaType count:count - 1 thread:thread]; + action( + ^{ + if (count <= 1) { + return; + } + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)1.f * NSEC_PER_SEC), dispatch_get_main_queue(), ^{ + [self performAction:action count:count - 1]; + }); + }, + ^{ }); - }; - void (^sendMessage)(NSString *) = ^(NSString *filePath) { - [self sendAttachment:filePath - thread:thread - success:completion - failure:^{ - }]; - }; - DebugMediaType mediaTypeToSend = mediaType; - while (mediaTypeToSend == DebugMediaType_Random) { - NSArray *allMediaTypes = self.allMediaTypes; - NSNumber *value = allMediaTypes[arc4random_uniform((uint32_t)allMediaTypes.count)]; - mediaTypeToSend = (DebugMediaType)value.intValue; - } - - switch (mediaTypeToSend) { - case DebugMediaType_Gif: - return [self ensureRandomGifWithSuccess:sendMessage - failure:^{ - }]; - case DebugMediaType_Jpeg: - return [self ensureRandomJpegWithSuccess:sendMessage - failure:^{ - }]; - case DebugMediaType_Mp3: - return [self ensureRandomMp3WithSuccess:sendMessage - failure:^{ - }]; - case DebugMediaType_Mp4: - return [self ensureRandomMp4WithSuccess:sendMessage - failure:^{ - }]; - case DebugMediaType_Random: - OWSFail(@"%@ Invalid value.", self.logTag); - break; - } } + (void)ensureRandomJpegWithSuccess:(nullable void (^)(NSString *filePath))success From c2e31540d7605cca303a5009dd5379214fe60e87 Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Fri, 23 Mar 2018 10:19:04 -0400 Subject: [PATCH 03/31] Elaborate debug UI for messages. --- .../ConversationViewController.m | 4 + .../ViewControllers/DebugUI/DebugUIMessages.m | 153 +++++++++--------- .../src/ViewControllers/HomeViewController.m | 8 +- SignalServiceKit/src/Util/NSDate+OWS.h | 2 +- 4 files changed, 87 insertions(+), 80 deletions(-) diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m index 241a1c1ca3..da0e1bff89 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m @@ -490,6 +490,10 @@ typedef enum : NSUInteger { [self createBackButton]; [self addNotificationListeners]; [self loadDraftInCompose]; + + dispatch_async(dispatch_get_main_queue(), ^{ + [DebugUITableViewController presentDebugUIForThread:self.thread fromViewController:self]; + }); } - (void)loadView diff --git a/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m b/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m index f565a99271..0f2a09a681 100644 --- a/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m +++ b/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m @@ -35,9 +35,9 @@ typedef NS_ENUM(NSUInteger, DebugMediaType) { DebugMediaType_Random, }; -typedef void (^ActionSucceesBlock)(void); +typedef void (^ActionSuccessBlock)(void); typedef void (^ActionFailureBlock)(void); -typedef void (^ActionBlock)(ActionSucceesBlock success, ActionFailureBlock failure); +typedef void (^ActionBlock)(NSUInteger index, ActionSuccessBlock success, ActionFailureBlock failure); @interface TSOutgoingMessage (PostDatingDebug) @@ -59,6 +59,20 @@ typedef void (^ActionBlock)(ActionSucceesBlock success, ActionFailureBlock failu OWSAssert(thread); NSMutableArray *items = [@[ + +#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 sendNRandomMediaInThread:thread]; + }], + +#pragma mark - Misc. + [OWSTableItem itemWithTitle:@"Perform 100 random actions" actionBlock:^{ [DebugUIMessages performRandomActions:100 thread:thread]; @@ -67,22 +81,6 @@ typedef void (^ActionBlock)(ActionSucceesBlock success, ActionFailureBlock failu 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]; @@ -199,10 +197,6 @@ typedef void (^ActionBlock)(ActionSucceesBlock success, ActionFailureBlock failu actionBlock:^{ [DebugUIMessages sendRandomAttachment:thread uti:(NSString *)kUTTypePDF]; }], - [OWSTableItem itemWithTitle:@"Send N Random Media (1/sec.)" - actionBlock:^{ - [DebugUIMessages sendRandomMediaInThread:thread]; - }], [OWSTableItem itemWithTitle:@"Create all system messages" actionBlock:^{ [DebugUIMessages createSystemMessagesInThread:thread]; @@ -291,17 +285,17 @@ typedef void (^ActionBlock)(ActionSucceesBlock success, ActionFailureBlock failu 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 performAction:[self sendTextMessagesActionInThread:contactThread] count: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]; @@ -311,18 +305,24 @@ typedef void (^ActionBlock)(ActionSucceesBlock success, ActionFailureBlock failu 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 ++ (ActionBlock)sendTextMessagesActionInThread:(TSThread *)thread +{ + + OWSAssert(thread); + + return ^(NSUInteger index, ActionSuccessBlock success, ActionFailureBlock failure) { + [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]; @@ -330,7 +330,7 @@ typedef void (^ActionBlock)(ActionSucceesBlock success, ActionFailureBlock failu [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; @@ -421,7 +421,7 @@ typedef void (^ActionBlock)(ActionSucceesBlock success, ActionFailureBlock failu ]; } -+ (void)sendRandomMediaInThread:(TSThread *)thread ++ (void)sendNRandomMediaInThread:(TSThread *)thread { OWSAssertIsOnMainThread() OWSAssert(thread); @@ -449,13 +449,12 @@ typedef void (^ActionBlock)(ActionSucceesBlock success, ActionFailureBlock failu break; } - [alert addAction:[UIAlertAction - actionWithTitle:label - style:UIAlertActionStyleDefault - handler:^(UIAlertAction *action) { - [self performActionNTimes:[self sendRandomMediaAction:mediaType thread:thread] - thread:thread]; - }]]; + [alert addAction:[UIAlertAction actionWithTitle:label + style:UIAlertActionStyleDefault + handler:^(UIAlertAction *action) { + [self performActionNTimes:[self sendRandomMediaAction:mediaType + thread:thread]]; + }]]; } [alert addAction:[OWSAlerts cancelAction]]; @@ -468,7 +467,7 @@ typedef void (^ActionBlock)(ActionSucceesBlock success, ActionFailureBlock failu { OWSAssert(thread); - return ^(ActionSucceesBlock success, ActionFailureBlock failure) { + return ^(NSUInteger index, ActionSuccessBlock success, ActionFailureBlock failure) { void (^sendMessage)(NSString *) = ^(NSString *filePath) { [self sendAttachment:filePath thread:thread success:success failure:failure]; }; @@ -503,15 +502,14 @@ typedef void (^ActionBlock)(ActionSucceesBlock success, ActionFailureBlock failu }; } -+ (void)performActionNTimes:(ActionBlock)action thread:(TSThread *)thread ++ (void)performActionNTimes:(ActionBlock)action { - OWSAssertIsOnMainThread() OWSAssert(thread); + OWSAssertIsOnMainThread(); UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"How many?" message:nil preferredStyle:UIAlertControllerStyleActionSheet]; for (NSNumber *countValue in @[ - @(0), @(1), @(10), @(100), @@ -535,7 +533,7 @@ typedef void (^ActionBlock)(ActionSucceesBlock success, ActionFailureBlock failu OWSAssert(action); OWSAssert(count > 0); - action( + action(count, ^{ if (count <= 1) { return; @@ -588,7 +586,7 @@ typedef void (^ActionBlock)(ActionSucceesBlock success, ActionFailureBlock failu { 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 " @@ -824,7 +822,7 @@ typedef void (^ActionBlock)(ActionSucceesBlock success, ActionFailureBlock failu }]; } -+ (void)sendTextAndSystemMessages:(int)counter thread:(TSThread *)thread ++ (void)sendTextAndSystemMessages:(NSUInteger)counter thread:(TSThread *)thread { if (counter < 1) { return; @@ -878,10 +876,10 @@ typedef void (^ActionBlock)(ActionSucceesBlock success, ActionFailureBlock failu 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 @@ -1170,7 +1168,7 @@ typedef void (^ActionBlock)(ActionSucceesBlock success, ActionFailureBlock failu } } -+ (void)createFakeLargeOutgoingAttachments:(int)counter thread:(TSThread *)thread ++ (void)createFakeLargeOutgoingAttachments:(NSUInteger)counter thread:(TSThread *)thread { if (counter < 1) { return; @@ -1208,7 +1206,7 @@ typedef void (^ActionBlock)(ActionSucceesBlock success, ActionFailureBlock failu }); } -+ (void)sendTinyAttachments:(int)counter thread:(TSThread *)thread ++ (void)sendTinyAttachments:(NSUInteger)counter thread:(TSThread *)thread { if (counter < 1) { return; @@ -1229,7 +1227,7 @@ typedef void (^ActionBlock)(ActionSucceesBlock success, ActionFailureBlock failu }); } -+ (void)createNewGroups:(int)counter recipientId:(NSString *)recipientId ++ (void)createNewGroups:(NSUInteger)counter recipientId:(NSString *)recipientId { if (counter < 1) { return; @@ -1272,24 +1270,24 @@ typedef void (^ActionBlock)(ActionSucceesBlock success, ActionFailureBlock failu }]; } -+ (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]; @@ -1335,7 +1333,7 @@ typedef void (^ActionBlock)(ActionSucceesBlock success, ActionFailureBlock failu }]; } -+ (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(), @@ -1347,8 +1345,7 @@ typedef void (^ActionBlock)(ActionSucceesBlock success, ActionFailureBlock failu }); } -+ (void)performRandomActionInThread:(TSThread *)thread - counter:(int)counter ++ (void)performRandomActionInThread:(TSThread *)thread counter:(NSUInteger)counter { typedef void (^ActionBlock)(YapDatabaseReadWriteTransaction *transaction); NSArray *actionBlocks = @[ @@ -1394,8 +1391,8 @@ typedef void (^ActionBlock)(ActionSucceesBlock success, ActionFailureBlock failu }, ]; [OWSPrimaryStorage.dbReadWriteConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { - int actionCount = 1 + (int)arc4random_uniform(3); - for (int actionIdx = 0; actionIdx < actionCount; actionIdx++) { + NSUInteger actionCount = 1 + (NSUInteger)arc4random_uniform(3); + for (NSUInteger actionIdx = 0; actionIdx < actionCount; actionIdx++) { ActionBlock actionBlock = actionBlocks[(NSUInteger)arc4random_uniform((uint32_t)actionBlocks.count)]; actionBlock(transaction); } @@ -1609,21 +1606,21 @@ typedef void (^ActionBlock)(ActionSucceesBlock success, ActionFailureBlock failu { 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]]; diff --git a/Signal/src/ViewControllers/HomeViewController.m b/Signal/src/ViewControllers/HomeViewController.m index 1a37cccd71..4aef807335 100644 --- a/Signal/src/ViewControllers/HomeViewController.m +++ b/Signal/src/ViewControllers/HomeViewController.m @@ -286,7 +286,13 @@ typedef NS_ENUM(NSInteger, CellState) { kArchiveState, kInboxState }; [self updateBarButtonItems]; dispatch_async(dispatch_get_main_queue(), ^{ - [self settingsButtonPressed:nil]; + TSThread *_Nullable thread = [self threadForIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]]; + if (!thread) { + return; + } + [self presentThread:thread keyboardOnViewAppearing:NO callOnViewAppearing:NO]; + + // [self settingsButtonPressed:nil]; }); } 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) From 24cc6ec1130390797a4ec2058029c9d6bfa26574 Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Fri, 23 Mar 2018 11:14:07 -0400 Subject: [PATCH 04/31] Elaborate debug UI for messages. --- .../ViewControllers/DebugUI/DebugUIMessages.m | 447 +++++++++++++----- 1 file changed, 319 insertions(+), 128 deletions(-) diff --git a/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m b/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m index 0f2a09a681..b291374eee 100644 --- a/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m +++ b/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m @@ -26,25 +26,173 @@ NS_ASSUME_NONNULL_BEGIN -typedef NS_ENUM(NSUInteger, DebugMediaType) { - DebugMediaType_Gif, - DebugMediaType_Jpeg, - DebugMediaType_Mp3, - DebugMediaType_Mp4, - // A mix of the above options. - DebugMediaType_Random, -}; +// typedef NS_ENUM(NSUInteger, DebugMediaType) { +// DebugMediaType_Gif, +// DebugMediaType_Jpeg, +// DebugMediaType_Mp3, +// DebugMediaType_Mp4, +// // A mix of the above options. +// DebugMediaType_Random, +//}; typedef void (^ActionSuccessBlock)(void); typedef void (^ActionFailureBlock)(void); +typedef void (^ActionPrepareBlock)(ActionSuccessBlock success, ActionFailureBlock failure); typedef void (^ActionBlock)(NSUInteger index, ActionSuccessBlock success, ActionFailureBlock failure); +@interface DebugUIMessagesAction : NSObject + +@property (nonatomic) NSString *label; +@property (nonatomic) ActionPrepareBlock prepareBlock; +@property (nonatomic) ActionBlock actionBlock; + +//@property (nonatomic, nullable) NSArray *subactions; + +@end + +#pragma mark - + +@implementation DebugUIMessagesAction + ++ (DebugUIMessagesAction *)actionWithLabel:(NSString *)label actionBlock:(ActionBlock)actionBlock +{ + OWSAssert(label.length > 0); + OWSAssert(actionBlock); + + DebugUIMessagesAction *instance = [DebugUIMessagesAction new]; + instance.label = label; + instance.actionBlock = actionBlock; + return instance; +} + ++ (DebugUIMessagesAction *)actionWithLabel:(NSString *)label + actionBlock:(ActionBlock)actionBlock + prepareBlock:(ActionPrepareBlock)prepareBlock +{ + OWSAssert(label.length > 0); + OWSAssert(actionBlock); + OWSAssert(prepareBlock); + + DebugUIMessagesAction *instance = [DebugUIMessagesAction new]; + instance.label = label; + instance.actionBlock = actionBlock; + instance.prepareBlock = prepareBlock; + return instance; +} + ++ (DebugUIMessagesAction *)groupActionWithLabel:(NSString *)label + subactions:(NSArray *)subactions +{ + OWSAssert(label.length > 0); + OWSAssert(subactions.count > 0); + + DebugUIMessagesAction *instance = [DebugUIMessagesAction new]; + instance.label = label; + instance.actionBlock = ^(NSUInteger index, ActionSuccessBlock success, ActionFailureBlock failure) { + DebugUIMessagesAction *subaction = subactions[arc4random_uniform((uint32_t)subactions.count)]; + [subaction justPerformNTimes:1]; + }; + instance.prepareBlock = ^(ActionSuccessBlock success, ActionFailureBlock failure) { + [DebugUIMessagesAction prepareSubactions:[subactions mutableCopy] success:success failure:failure]; + }; + return instance; +} + +- (void)prepareAndPerformNTimes:(NSUInteger)count +{ + __weak DebugUIMessagesAction *weakSelf = self; + [self prepare:^{ + [weakSelf justPerformNTimes:count]; + } + failure:^{ + }]; +} + +- (void)justPerformNTimes:(NSUInteger)count +{ + if (count < 1) { + return; + } + + __weak DebugUIMessagesAction *weakSelf = self; + self.actionBlock(count, + ^{ + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)1.f * NSEC_PER_SEC), dispatch_get_main_queue(), ^{ + [weakSelf justPerformNTimes:count - 1]; + }); + }, + ^{ + // Do nothing. + }); +} + +- (void)prepare:(ActionSuccessBlock)success failure:(ActionFailureBlock)failure +{ + OWSAssert(success); + OWSAssert(failure); + + if (self.prepareBlock) { + self.prepareBlock(success, failure); + } else { + success(); + } +} + ++ (void)prepareSubactions:(NSMutableArray *)unpreparedSubactions + success:(ActionSuccessBlock)success + failure:(ActionFailureBlock)failure +{ + OWSAssert(unpreparedSubactions); + OWSAssert(success); + OWSAssert(failure); + + if (unpreparedSubactions.count < 1) { + return success(); + } + + DebugUIMessagesAction *nextAction = unpreparedSubactions.lastObject; + [unpreparedSubactions removeLastObject]; + [nextAction prepare:^{ + [self prepareSubactions:unpreparedSubactions success:success failure:failure]; + } + failure:^{ + }]; +} +// actionBlock:^(NSUInteger index, ActionSuccessBlock success, ActionFailureBlock failure) { +// OWSAssert(filePath.length > 0); +// +// DebugUIMessagesAction *action = actions[arc4random_uniform((uint32_t) actions.count)]; +// [action performNTimes:index]; +//} prepareBlock:^(ActionSuccessBlock success, ActionFailureBlock failure) { +// void (^prepareNextAction)(void); +// prepareNextAction = ^{ +// +// }; +// if (unpreparedActions.count < 1) { +// +// } +// return [self ensureRandomJpegWithSuccess: +// ^(NSString *ensuredFilePath) { +// OWSAssert(ensuredFilePath.length > 0); +// filePath = ensuredFilePath; +// } +// failure:^{ +// }]; +//}]; + + +@end + +#pragma mark - + @interface TSOutgoingMessage (PostDatingDebug) - (void)setReceivedAtTimestamp:(uint64_t)value; @end +#pragma mark - + @implementation DebugUIMessages #pragma mark - Factory Methods @@ -289,7 +437,7 @@ typedef void (^ActionBlock)(NSUInteger index, ActionSuccessBlock success, Action { for (NSString *recipientId in groupThread.groupModel.groupMemberIds) { TSContactThread *contactThread = [TSContactThread getOrCreateThreadWithContactId:recipientId]; - [self performAction:[self sendTextMessagesActionInThread:contactThread] count:count]; + [[self sendTextMessagesActionInThread:contactThread] prepareAndPerformNTimes:count]; } } @@ -310,16 +458,17 @@ typedef void (^ActionBlock)(NSUInteger index, ActionSuccessBlock success, Action [self performActionNTimes:[self sendTextMessagesActionInThread:thread]]; } -+ (ActionBlock)sendTextMessagesActionInThread:(TSThread *)thread ++ (DebugUIMessagesAction *)sendTextMessagesActionInThread:(TSThread *)thread { - OWSAssert(thread); - return ^(NSUInteger index, ActionSuccessBlock success, ActionFailureBlock failure) { - [self sendTextMessageInThread:thread counter:index]; - // TODO: - success(); - }; + return [DebugUIMessagesAction + actionWithLabel:@"Send Text Message" + actionBlock:^(NSUInteger index, ActionSuccessBlock success, ActionFailureBlock failure) { + [self sendTextMessageInThread:thread counter:index]; + // TODO: + success(); + }]; } + (void)sendTinyTextMessageInThread:(TSThread *)thread counter:(NSUInteger)counter @@ -408,103 +557,12 @@ typedef void (^ActionBlock)(NSUInteger index, ActionSuccessBlock success, Action success(); } -#pragma mark - Media +#pragma mark - Infrastructure -+ (NSArray *)allMediaTypes -{ - return @[ - @(DebugMediaType_Gif), - @(DebugMediaType_Jpeg), - @(DebugMediaType_Mp3), - @(DebugMediaType_Mp4), - @(DebugMediaType_Random), - ]; -} - -+ (void)sendNRandomMediaInThread:(TSThread *)thread -{ - OWSAssertIsOnMainThread() OWSAssert(thread); - - UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"Select Media Type" - message:nil - preferredStyle:UIAlertControllerStyleActionSheet]; - for (NSNumber *value in self.allMediaTypes) { - DebugMediaType mediaType = (DebugMediaType)value.intValue; - NSString *label; - switch (mediaType) { - case DebugMediaType_Gif: - label = @"Gif"; - break; - case DebugMediaType_Jpeg: - label = @"Jpeg"; - break; - case DebugMediaType_Mp3: - label = @"Mp3"; - break; - case DebugMediaType_Mp4: - label = @"Mp4"; - break; - case DebugMediaType_Random: - label = @"Random"; - break; - } - - [alert addAction:[UIAlertAction actionWithTitle:label - style:UIAlertActionStyleDefault - handler:^(UIAlertAction *action) { - [self performActionNTimes:[self sendRandomMediaAction:mediaType - thread:thread]]; - }]]; - } - - [alert addAction:[OWSAlerts cancelAction]]; - - UIViewController *fromViewController = [[UIApplication sharedApplication] frontmostViewController]; - [fromViewController presentViewController:alert animated:YES completion:nil]; -} - -+ (ActionBlock)sendRandomMediaAction:(DebugMediaType)mediaType thread:(TSThread *)thread -{ - OWSAssert(thread); - - return ^(NSUInteger index, ActionSuccessBlock success, ActionFailureBlock failure) { - void (^sendMessage)(NSString *) = ^(NSString *filePath) { - [self sendAttachment:filePath thread:thread success:success failure:failure]; - }; - DebugMediaType mediaTypeToSend = mediaType; - while (mediaTypeToSend == DebugMediaType_Random) { - NSArray *allMediaTypes = self.allMediaTypes; - NSNumber *value = allMediaTypes[arc4random_uniform((uint32_t)allMediaTypes.count)]; - mediaTypeToSend = (DebugMediaType)value.intValue; - } - - switch (mediaTypeToSend) { - case DebugMediaType_Gif: - return [self ensureRandomGifWithSuccess:sendMessage - failure:^{ - }]; - case DebugMediaType_Jpeg: - return [self ensureRandomJpegWithSuccess:sendMessage - failure:^{ - }]; - case DebugMediaType_Mp3: - return [self ensureRandomMp3WithSuccess:sendMessage - failure:^{ - }]; - case DebugMediaType_Mp4: - return [self ensureRandomMp4WithSuccess:sendMessage - failure:^{ - }]; - case DebugMediaType_Random: - OWSFail(@"%@ Invalid value.", self.logTag); - break; - } - }; -} - -+ (void)performActionNTimes:(ActionBlock)action ++ (void)performActionNTimes:(DebugUIMessagesAction *)action { OWSAssertIsOnMainThread(); + OWSAssert(action); UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"How many?" message:nil @@ -519,7 +577,7 @@ typedef void (^ActionBlock)(NSUInteger index, ActionSuccessBlock success, Action [alert addAction:[UIAlertAction actionWithTitle:countValue.stringValue style:UIAlertActionStyleDefault handler:^(UIAlertAction *ignore) { - [self performAction:action count:countValue.unsignedIntegerValue]; + [action prepareAndPerformNTimes:countValue.unsignedIntegerValue]; }]]; } @@ -528,22 +586,140 @@ typedef void (^ActionBlock)(NSUInteger index, ActionSuccessBlock success, Action [fromViewController presentViewController:alert animated:YES completion:nil]; } -+ (void)performAction:(ActionBlock)action count:(NSUInteger)count -{ - OWSAssert(action); - OWSAssert(count > 0); +#pragma mark - Media - action(count, - ^{ - if (count <= 1) { - return; +//+ (NSArray *)allMediaTypes +//{ +// return @[ +// @(DebugMediaType_Gif), +// @(DebugMediaType_Jpeg), +// @(DebugMediaType_Mp3), +// @(DebugMediaType_Mp4), +// @(DebugMediaType_Random), +// ]; +//} + ++ (void)sendNRandomMediaInThread:(TSThread *)thread +{ + OWSAssertIsOnMainThread() OWSAssert(thread); + + NSArray *actions = @[ + [self sendJpegAction:thread], + [self sendGifAction:thread], + [self sendMp3Action:thread], + [self sendMp4Action:thread], + [self sendRandomMediaAction:thread], + ]; + + UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"Select Media Type" + 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]; +} + ++ (DebugUIMessagesAction *)sendRandomMediaAction:(TSThread *)thread +{ + OWSAssert(thread); + + NSArray *actions = @[ + [self sendJpegAction:thread], + [self sendGifAction:thread], + [self sendMp3Action:thread], + [self sendMp4Action:thread], + ]; + + return [DebugUIMessagesAction groupActionWithLabel:@"Send Random Media" subactions:actions]; +} + ++ (DebugUIMessagesAction *)sendJpegAction:(TSThread *)thread +{ + OWSAssert(thread); + + __block NSString *_Nullable filePath = nil; + return [DebugUIMessagesAction actionWithLabel:@"Send Jpeg" + actionBlock:^(NSUInteger index, ActionSuccessBlock success, ActionFailureBlock failure) { + OWSAssert(filePath.length > 0); + [self sendAttachment:filePath thread:thread success:success failure:failure]; + } + prepareBlock:^(ActionSuccessBlock success, ActionFailureBlock failure) { + return [self ensureRandomJpegWithSuccess:^(NSString *ensuredFilePath) { + OWSAssert(ensuredFilePath.length > 0); + filePath = ensuredFilePath; } - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)1.f * NSEC_PER_SEC), dispatch_get_main_queue(), ^{ - [self performAction:action count:count - 1]; - }); - }, - ^{ - }); + failure:^{ + }]; + }]; +} + ++ (DebugUIMessagesAction *)sendGifAction:(TSThread *)thread +{ + OWSAssert(thread); + + __block NSString *_Nullable filePath = nil; + return [DebugUIMessagesAction actionWithLabel:@"Send Gif" + actionBlock:^(NSUInteger index, ActionSuccessBlock success, ActionFailureBlock failure) { + OWSAssert(filePath.length > 0); + [self sendAttachment:filePath thread:thread success:success failure:failure]; + } + prepareBlock:^(ActionSuccessBlock success, ActionFailureBlock failure) { + return [self ensureRandomGifWithSuccess:^(NSString *ensuredFilePath) { + OWSAssert(ensuredFilePath.length > 0); + filePath = ensuredFilePath; + } + failure:^{ + }]; + }]; +} + ++ (DebugUIMessagesAction *)sendMp3Action:(TSThread *)thread +{ + OWSAssert(thread); + + __block NSString *_Nullable filePath = nil; + return [DebugUIMessagesAction actionWithLabel:@"Send Mp3" + actionBlock:^(NSUInteger index, ActionSuccessBlock success, ActionFailureBlock failure) { + OWSAssert(filePath.length > 0); + [self sendAttachment:filePath thread:thread success:success failure:failure]; + } + prepareBlock:^(ActionSuccessBlock success, ActionFailureBlock failure) { + return [self ensureRandomMp3WithSuccess:^(NSString *ensuredFilePath) { + OWSAssert(ensuredFilePath.length > 0); + filePath = ensuredFilePath; + } + failure:^{ + }]; + }]; +} + ++ (DebugUIMessagesAction *)sendMp4Action:(TSThread *)thread +{ + OWSAssert(thread); + + __block NSString *_Nullable filePath = nil; + return [DebugUIMessagesAction actionWithLabel:@"Send Mp4" + actionBlock:^(NSUInteger index, ActionSuccessBlock success, ActionFailureBlock failure) { + OWSAssert(filePath.length > 0); + [self sendAttachment:filePath thread:thread success:success failure:failure]; + } + prepareBlock:^(ActionSuccessBlock success, ActionFailureBlock failure) { + return [self ensureRandomMp4WithSuccess:^(NSString *ensuredFilePath) { + OWSAssert(ensuredFilePath.length > 0); + filePath = ensuredFilePath; + } + failure:^{ + }]; + }]; } + (void)ensureRandomJpegWithSuccess:(nullable void (^)(NSString *filePath))success @@ -1345,10 +1521,25 @@ typedef void (^ActionBlock)(NSUInteger index, ActionSuccessBlock success, Action }); } +//+ (ActionBlock)sendTextMessagesActionInThread:(TSThread *)thread +//{ +// OWSAssert(thread); +// +// return ^(NSUInteger index, ActionSuccessBlock success, ActionFailureBlock failure) { +// [self sendTextMessageInThread:thread counter:index]; +// // TODO: +// success(); +// }; +//} +// +//+ (void)performRandomActionInThread:(TSThread *)thread counter:(NSUInteger)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(), ^{ @@ -1393,7 +1584,7 @@ typedef void (^ActionBlock)(NSUInteger index, ActionSuccessBlock success, Action [OWSPrimaryStorage.dbReadWriteConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { NSUInteger actionCount = 1 + (NSUInteger)arc4random_uniform(3); for (NSUInteger actionIdx = 0; actionIdx < actionCount; actionIdx++) { - ActionBlock actionBlock = actionBlocks[(NSUInteger)arc4random_uniform((uint32_t)actionBlocks.count)]; + TransactionBlock actionBlock = actionBlocks[(NSUInteger)arc4random_uniform((uint32_t)actionBlocks.count)]; actionBlock(transaction); } }]; From e874503f8d5679b4f711459b760531788f081d2e Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Fri, 23 Mar 2018 11:37:29 -0400 Subject: [PATCH 05/31] Elaborate debug UI for messages. --- .../ViewControllers/DebugUI/DebugUIMessages.m | 92 ++++++++----------- 1 file changed, 39 insertions(+), 53 deletions(-) diff --git a/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m b/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m index b291374eee..c311f953d2 100644 --- a/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m +++ b/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m @@ -26,15 +26,6 @@ NS_ASSUME_NONNULL_BEGIN -// typedef NS_ENUM(NSUInteger, DebugMediaType) { -// DebugMediaType_Gif, -// DebugMediaType_Jpeg, -// DebugMediaType_Mp3, -// DebugMediaType_Mp4, -// // A mix of the above options. -// DebugMediaType_Random, -//}; - typedef void (^ActionSuccessBlock)(void); typedef void (^ActionFailureBlock)(void); typedef void (^ActionPrepareBlock)(ActionSuccessBlock success, ActionFailureBlock failure); @@ -46,8 +37,6 @@ typedef void (^ActionBlock)(NSUInteger index, ActionSuccessBlock success, Action @property (nonatomic) ActionPrepareBlock prepareBlock; @property (nonatomic) ActionBlock actionBlock; -//@property (nonatomic, nullable) NSArray *subactions; - @end #pragma mark - @@ -90,7 +79,7 @@ typedef void (^ActionBlock)(NSUInteger index, ActionSuccessBlock success, Action instance.label = label; instance.actionBlock = ^(NSUInteger index, ActionSuccessBlock success, ActionFailureBlock failure) { DebugUIMessagesAction *subaction = subactions[arc4random_uniform((uint32_t)subactions.count)]; - [subaction justPerformNTimes:1]; + [subaction performWithIndex:index success:success failure:failure]; }; instance.prepareBlock = ^(ActionSuccessBlock success, ActionFailureBlock failure) { [DebugUIMessagesAction prepareSubactions:[subactions mutableCopy] success:success failure:failure]; @@ -102,28 +91,47 @@ typedef void (^ActionBlock)(NSUInteger index, ActionSuccessBlock success, Action { __weak DebugUIMessagesAction *weakSelf = self; [self prepare:^{ - [weakSelf justPerformNTimes:count]; + [weakSelf performNTimes:count + success:^{ + } + failure:^{ + }]; } failure:^{ }]; } -- (void)justPerformNTimes:(NSUInteger)count +- (void)performNTimes:(NSUInteger)count success:(ActionSuccessBlock)success failure:(ActionFailureBlock)failure { + OWSAssert(success); + OWSAssert(failure); + + DDLogInfo(@"%@ %@ performNTimes: %zd", self.logTag, self.label, count); + [DDLog flushLog]; + if (count < 1) { + success(); return; } __weak DebugUIMessagesAction *weakSelf = self; - self.actionBlock(count, - ^{ - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)1.f * NSEC_PER_SEC), dispatch_get_main_queue(), ^{ - [weakSelf justPerformNTimes:count - 1]; - }); - }, - ^{ - // Do nothing. - }); + [self performWithIndex:count + success:^{ + 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); + [weakSelf performNTimes:count - 1 success:success failure:failure]; + }); + } + failure:failure]; +} + +- (void)performWithIndex:(NSUInteger)index success:(ActionSuccessBlock)success failure:(ActionFailureBlock)failure +{ + OWSAssert(success); + OWSAssert(failure); + + self.actionBlock(index, success, failure); } - (void)prepare:(ActionSuccessBlock)success failure:(ActionFailureBlock)failure @@ -158,28 +166,6 @@ typedef void (^ActionBlock)(NSUInteger index, ActionSuccessBlock success, Action failure:^{ }]; } -// actionBlock:^(NSUInteger index, ActionSuccessBlock success, ActionFailureBlock failure) { -// OWSAssert(filePath.length > 0); -// -// DebugUIMessagesAction *action = actions[arc4random_uniform((uint32_t) actions.count)]; -// [action performNTimes:index]; -//} prepareBlock:^(ActionSuccessBlock success, ActionFailureBlock failure) { -// void (^prepareNextAction)(void); -// prepareNextAction = ^{ -// -// }; -// if (unpreparedActions.count < 1) { -// -// } -// return [self ensureRandomJpegWithSuccess: -// ^(NSString *ensuredFilePath) { -// OWSAssert(ensuredFilePath.length > 0); -// filePath = ensuredFilePath; -// } -// failure:^{ -// }]; -//}]; - @end @@ -656,9 +642,9 @@ typedef void (^ActionBlock)(NSUInteger index, ActionSuccessBlock success, Action return [self ensureRandomJpegWithSuccess:^(NSString *ensuredFilePath) { OWSAssert(ensuredFilePath.length > 0); filePath = ensuredFilePath; + success(); } - failure:^{ - }]; + failure:failure]; }]; } @@ -676,9 +662,9 @@ typedef void (^ActionBlock)(NSUInteger index, ActionSuccessBlock success, Action return [self ensureRandomGifWithSuccess:^(NSString *ensuredFilePath) { OWSAssert(ensuredFilePath.length > 0); filePath = ensuredFilePath; + success(); } - failure:^{ - }]; + failure:failure]; }]; } @@ -696,9 +682,9 @@ typedef void (^ActionBlock)(NSUInteger index, ActionSuccessBlock success, Action return [self ensureRandomMp3WithSuccess:^(NSString *ensuredFilePath) { OWSAssert(ensuredFilePath.length > 0); filePath = ensuredFilePath; + success(); } - failure:^{ - }]; + failure:failure]; }]; } @@ -716,9 +702,9 @@ typedef void (^ActionBlock)(NSUInteger index, ActionSuccessBlock success, Action return [self ensureRandomMp4WithSuccess:^(NSString *ensuredFilePath) { OWSAssert(ensuredFilePath.length > 0); filePath = ensuredFilePath; + success(); } - failure:^{ - }]; + failure:failure]; }]; } From 66a454ce4ffd9737da0f716a856159575f916c44 Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Fri, 23 Mar 2018 17:49:46 -0400 Subject: [PATCH 06/31] Elaborate debug UI for messages. --- .../ViewControllers/DebugUI/DebugUIMessages.m | 1455 ++++++++++++----- 1 file changed, 1069 insertions(+), 386 deletions(-) diff --git a/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m b/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m index c311f953d2..66d39e6092 100644 --- a/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m +++ b/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m @@ -29,13 +29,160 @@ NS_ASSUME_NONNULL_BEGIN typedef void (^ActionSuccessBlock)(void); typedef void (^ActionFailureBlock)(void); typedef void (^ActionPrepareBlock)(ActionSuccessBlock success, ActionFailureBlock failure); -typedef void (^ActionBlock)(NSUInteger index, ActionSuccessBlock success, ActionFailureBlock failure); +typedef void (^StaggeredActionBlock)(NSUInteger index, + YapDatabaseReadWriteTransaction *transaction, + ActionSuccessBlock success, + ActionFailureBlock failure); +typedef void (^UnstaggeredActionBlock)(NSUInteger index, YapDatabaseReadWriteTransaction *transaction); + +typedef NS_ENUM(NSUInteger, SubactionMode) { + SubactionMode_Random = 0, + SubactionMode_Ordered, +}; + +@interface FakeAssetLoader : NSObject + +@property (nonatomic) NSString *fileUrl; +@property (nonatomic) NSString *filename; +@property (nonatomic) NSString *mimeType; + +@property (nonatomic, nullable) NSString *filePath; + +@end + +#pragma mark - + +@implementation FakeAssetLoader + +- (void)ensureAssetLoaded:(ActionSuccessBlock)success failure:(ActionFailureBlock)failure +{ + OWSAssert(success); + OWSAssert(failure); + OWSAssert(self.fileUrl.length > 0); + OWSAssert(self.filename.length > 0); + OWSAssert(self.mimeType.length > 0); + + if (self.filePath) { + success(); + return; + } + + NSString *filePath = [OWSFileSystem temporaryFilePathWithFileExtension:self.filename.pathExtension]; + + // NSFileManager *fileManager = [NSFileManager defaultManager]; + // [fileManager] + // NSURL *documentDirectoryURL = + // [[fileManager URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject]; + // NSString *randomFilesDirectoryPath = + // [[documentDirectoryURL path] stringByAppendingPathComponent:@"cached_random_files"]; + // [OWSFileSystem ensureDirectoryExists:randomFilesDirectoryPath]; + // NSString *filePath = [randomFilesDirectoryPath stringByAppendingPathComponent:self.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:self.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 [%@]: %@", self.fileUrl, filePath); + failure(); + } + } + failure:^(NSURLSessionDataTask *_Nullable task, NSError *requestError) { + OWSFail(@"Error downloading url[%@]: %@", self.fileUrl, requestError); + failure(); + }]; +} + ++ (instancetype)jpegInstance +{ + static FakeAssetLoader *instance = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + instance = [self new]; + instance.fileUrl = @"https://s3.amazonaws.com/ows-data/example_attachment_media/random-jpg.JPG"; + instance.filename = @"random-jpg.jpg"; + instance.mimeType = @"image/jpeg"; + }); + return instance; +} + ++ (instancetype)gifInstance +{ + static FakeAssetLoader *instance = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + instance = [self new]; + instance.fileUrl = @"https://s3.amazonaws.com/ows-data/example_attachment_media/random-gif.gif"; + instance.filename = @"random-gif.gif"; + instance.mimeType = @"image/gif"; + }); + return instance; +} + ++ (instancetype)mp3Instance +{ + static FakeAssetLoader *instance = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + instance = [self new]; + instance.fileUrl = @"https://s3.amazonaws.com/ows-data/example_attachment_media/random-mp3.mp3"; + instance.filename = @"random-mp3.mp3"; + instance.mimeType = @"audio/mpeg"; + }); + return instance; +} + ++ (instancetype)mp4Instance +{ + static FakeAssetLoader *instance = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + instance = [self new]; + instance.fileUrl = @"https://s3.amazonaws.com/ows-data/example_attachment_media/random-mp4.mp4"; + instance.filename = @"random-mp4.mp4"; + instance.mimeType = @"video/mp4"; + }); + return instance; +} + +- (ActionPrepareBlock)prepareBlock +{ + __weak FakeAssetLoader *weakSelf = self; + return ^(ActionSuccessBlock success, ActionFailureBlock failure) { + [weakSelf ensureAssetLoaded:success failure:failure]; + }; +} + +@end + +#pragma mark - + +@class DebugUIMessagesSingleAction; @interface DebugUIMessagesAction : NSObject @property (nonatomic) NSString *label; -@property (nonatomic) ActionPrepareBlock prepareBlock; -@property (nonatomic) ActionBlock actionBlock; + +@end + +#pragma mark - + +@interface DebugUIMessagesSingleAction : DebugUIMessagesAction + +@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 @@ -43,95 +190,153 @@ typedef void (^ActionBlock)(NSUInteger index, ActionSuccessBlock success, Action @implementation DebugUIMessagesAction -+ (DebugUIMessagesAction *)actionWithLabel:(NSString *)label actionBlock:(ActionBlock)actionBlock +- (DebugUIMessagesSingleAction *)nextActionToPerform { - OWSAssert(label.length > 0); - OWSAssert(actionBlock); - - DebugUIMessagesAction *instance = [DebugUIMessagesAction new]; - instance.label = label; - instance.actionBlock = actionBlock; - return instance; + return (DebugUIMessagesSingleAction *)self; } -+ (DebugUIMessagesAction *)actionWithLabel:(NSString *)label - actionBlock:(ActionBlock)actionBlock - prepareBlock:(ActionPrepareBlock)prepareBlock +- (void)prepare:(ActionSuccessBlock)success failure:(ActionFailureBlock)failure { - OWSAssert(label.length > 0); - OWSAssert(actionBlock); - OWSAssert(prepareBlock); + OWSAssert(success); + OWSAssert(failure); - DebugUIMessagesAction *instance = [DebugUIMessagesAction new]; - instance.label = label; - instance.actionBlock = actionBlock; - instance.prepareBlock = prepareBlock; - return instance; -} + OWS_ABSTRACT_METHOD(); -+ (DebugUIMessagesAction *)groupActionWithLabel:(NSString *)label - subactions:(NSArray *)subactions -{ - OWSAssert(label.length > 0); - OWSAssert(subactions.count > 0); - - DebugUIMessagesAction *instance = [DebugUIMessagesAction new]; - instance.label = label; - instance.actionBlock = ^(NSUInteger index, ActionSuccessBlock success, ActionFailureBlock failure) { - DebugUIMessagesAction *subaction = subactions[arc4random_uniform((uint32_t)subactions.count)]; - [subaction performWithIndex:index success:success failure:failure]; - }; - instance.prepareBlock = ^(ActionSuccessBlock success, ActionFailureBlock failure) { - [DebugUIMessagesAction prepareSubactions:[subactions mutableCopy] success:success failure:failure]; - }; - return instance; + success(); } - (void)prepareAndPerformNTimes:(NSUInteger)count { - __weak DebugUIMessagesAction *weakSelf = self; + // __weak DebugUIMessagesAction *weakSelf = self; [self prepare:^{ - [weakSelf performNTimes:count - success:^{ - } - failure:^{ - }]; + [self performNTimes:count + success:^{ + } + failure:^{ + }]; } failure:^{ }]; } -- (void)performNTimes:(NSUInteger)count success:(ActionSuccessBlock)success failure:(ActionFailureBlock)failure +- (void)performNTimes:(NSUInteger)countParam success:(ActionSuccessBlock)success failure:(ActionFailureBlock)failure { OWSAssert(success); OWSAssert(failure); - DDLogInfo(@"%@ %@ performNTimes: %zd", self.logTag, self.label, count); + DDLogInfo(@"%@ %@ performNTimes: %zd", self.logTag, self.label, countParam); [DDLog flushLog]; - if (count < 1) { + if (countParam < 1) { success(); return; } - __weak DebugUIMessagesAction *weakSelf = self; - [self performWithIndex:count - success:^{ - 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); - [weakSelf performNTimes:count - 1 success:success failure:failure]; - }); - } - failure:failure]; + __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--; + } + } + }]; } -- (void)performWithIndex:(NSUInteger)index success:(ActionSuccessBlock)success failure:(ActionFailureBlock)failure -{ - OWSAssert(success); - OWSAssert(failure); +@end - self.actionBlock(index, success, failure); +#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 @@ -146,11 +351,60 @@ typedef void (^ActionBlock)(NSUInteger index, ActionSuccessBlock success, Action } } +@end + +#pragma mark - + +@interface DebugUIMessagesGroupAction : DebugUIMessagesAction + +// "Group" actions should have these properties. +@property (nonatomic) SubactionMode subactionMode; +@property (nonatomic, nullable) NSArray *subactions; +@property (nonatomic) NSUInteger subactionIndex; + +//@property (nonatomic, nullable) MessageActionBlock actionBlock; +//@property (nonatomic, nullable) StaggeredActionBlock staggeredActionBlock; +//@property (nonatomic, nullable) BulkActionBlock staggeredActionBlock; +//@property (nonatomic, nullable) BulkActionBlock bulkActionBlock; +//@property (nonatomic) BOOL isStaggered; + +@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(unpreparedSubactions); OWSAssert(success); OWSAssert(failure); @@ -167,6 +421,52 @@ typedef void (^ActionBlock)(NSUInteger index, ActionSuccessBlock success, Action }]; } +// Given a group of subactions, perform a single random subaction each time. ++ (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; + // instance.staggeredActionBlock = ^(NSUInteger index, YapDatabaseReadWriteTransaction *transaction, + // ActionSuccessBlock success, ActionFailureBlock failure) { + // [self performRandomStaggeredSubaction:subactions index:index transaction:transaction success:success + // failure:failure]; + // }; + // instance.prepareBlock = ^(ActionSuccessBlock success, ActionFailureBlock failure) { + // [DebugUIMessagesAction prepareSubactions:[subactions mutableCopy] success:success failure:failure]; + // }; + return instance; +} + +// Given a group of subactions, perform all of the subactions each time. ++ (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; + // instance.bulkActionBlock = ^(NSUInteger count, YapDatabaseReadWriteTransaction *transaction, + // ActionSuccessBlock success, ActionFailureBlock failure) { + // for (NSUInteger index = 0; index < count; index++) { + // [self performAllUnstaggeredSubactions:[subactions mutableCopy] index:index transaction:transaction + // success:success failure:failure]; + // } + // }; + // instance.prepareBlock = ^(ActionSuccessBlock success, ActionFailureBlock failure) { + // [DebugUIMessagesAction prepareSubactions:[subactions mutableCopy] success:success failure:failure]; + // }; + return instance; +} + @end #pragma mark - @@ -192,7 +492,42 @@ typedef void (^ActionBlock)(NSUInteger index, ActionSuccessBlock success, Action { OWSAssert(thread); - NSMutableArray *items = [@[ + NSMutableArray *items = [NSMutableArray new]; + + for (DebugUIMessagesAction *action in @[ + [DebugUIMessages sendMessageVariationsAction:thread], + // Send Media + [DebugUIMessages sendJpegAction:thread], + [DebugUIMessages sendGifAction:thread], + [DebugUIMessages sendMp3Action:thread], + [DebugUIMessages sendMp4Action:thread], + [DebugUIMessages sendAllMediaAction:thread], + [DebugUIMessages sendRandomMediaAction:thread], + // Fake Media + [DebugUIMessages fakeAllMediaAction:thread], + [DebugUIMessages fakeRandomMediaAction:thread], + // Fake Text + [DebugUIMessages fakeAllTextAction:thread], + [DebugUIMessages fakeRandomTextAction:thread], + // Exemplary + [DebugUIMessages allExemplaryAction: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 @@ -200,10 +535,10 @@ typedef void (^ActionBlock)(NSUInteger index, ActionSuccessBlock success, Action actionBlock:^{ [DebugUIMessages sendNTextMessagesInThread:thread]; }], - [OWSTableItem itemWithTitle:@"Send N Random Media (1/sec.)" - actionBlock:^{ - [DebugUIMessages sendNRandomMediaInThread:thread]; - }], + // [OWSTableItem itemWithTitle:@"Send N Random Media (1/sec.)" + // actionBlock:^{ + // [DebugUIMessages sendSelectedMediaTypeInThread:thread]; + // }], #pragma mark - Misc. @@ -267,18 +602,6 @@ typedef void (^ActionBlock)(NSUInteger index, ActionSuccessBlock success, Action 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]; @@ -392,7 +715,8 @@ typedef void (^ActionBlock)(NSUInteger index, ActionSuccessBlock success, Action actionBlock:^{ [DebugUIMessages testDirectionalFilenamesInThread:thread]; }], - ] mutableCopy]; + ]]; + if ([thread isKindOfClass:[TSContactThread class]]) { TSContactThread *contactThread = (TSContactThread *)thread; NSString *recipientId = contactThread.contactIdentifier; @@ -448,13 +772,17 @@ typedef void (^ActionBlock)(NSUInteger index, ActionSuccessBlock success, Action { OWSAssert(thread); - return [DebugUIMessagesAction - actionWithLabel:@"Send Text Message" - actionBlock:^(NSUInteger index, ActionSuccessBlock success, ActionFailureBlock failure) { - [self sendTextMessageInThread:thread counter:index]; - // TODO: - success(); - }]; + 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 @@ -476,43 +804,43 @@ typedef void (^ActionBlock)(NSUInteger index, ActionSuccessBlock success, Action }); } -+ (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)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 @@ -572,49 +900,38 @@ typedef void (^ActionBlock)(NSUInteger index, ActionSuccessBlock success, Action [fromViewController presentViewController:alert animated:YES completion:nil]; } -#pragma mark - Media +#pragma mark - Send Media -//+ (NSArray *)allMediaTypes +//+ (void)sendSelectedMediaTypeInThread:(TSThread *)thread //{ -// return @[ -// @(DebugMediaType_Gif), -// @(DebugMediaType_Jpeg), -// @(DebugMediaType_Mp3), -// @(DebugMediaType_Mp4), -// @(DebugMediaType_Random), +// OWSAssertIsOnMainThread() OWSAssert(thread); +// +// NSArray *actions = @[ +// [self sendJpegAction:thread], +// [self sendGifAction:thread], +// [self sendMp3Action:thread], +// [self sendMp4Action:thread], +// [self sendRandomMediaAction:thread], // ]; +// +// UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"Select Media Type" +// 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]; //} -+ (void)sendNRandomMediaInThread:(TSThread *)thread -{ - OWSAssertIsOnMainThread() OWSAssert(thread); - - NSArray *actions = @[ - [self sendJpegAction:thread], - [self sendGifAction:thread], - [self sendMp3Action:thread], - [self sendMp4Action:thread], - [self sendRandomMediaAction:thread], - ]; - - UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"Select Media Type" - 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]; -} - -+ (DebugUIMessagesAction *)sendRandomMediaAction:(TSThread *)thread ++ (NSArray *)allSendMediaActions:(TSThread *)thread { OWSAssert(thread); @@ -624,126 +941,632 @@ typedef void (^ActionBlock)(NSUInteger index, ActionSuccessBlock success, Action [self sendMp3Action:thread], [self sendMp4Action:thread], ]; - - return [DebugUIMessagesAction groupActionWithLabel:@"Send Random Media" subactions:actions]; + return actions; } + (DebugUIMessagesAction *)sendJpegAction:(TSThread *)thread { OWSAssert(thread); - __block NSString *_Nullable filePath = nil; - return [DebugUIMessagesAction actionWithLabel:@"Send Jpeg" - actionBlock:^(NSUInteger index, ActionSuccessBlock success, ActionFailureBlock failure) { - OWSAssert(filePath.length > 0); - [self sendAttachment:filePath thread:thread success:success failure:failure]; - } - prepareBlock:^(ActionSuccessBlock success, ActionFailureBlock failure) { - return [self ensureRandomJpegWithSuccess:^(NSString *ensuredFilePath) { - OWSAssert(ensuredFilePath.length > 0); - filePath = ensuredFilePath; - success(); - } - failure:failure]; - }]; + return [self sendMediaAction:@"Send Jpeg" fakeAssetLoader:[FakeAssetLoader jpegInstance] thread:thread]; } + (DebugUIMessagesAction *)sendGifAction:(TSThread *)thread { OWSAssert(thread); - __block NSString *_Nullable filePath = nil; - return [DebugUIMessagesAction actionWithLabel:@"Send Gif" - actionBlock:^(NSUInteger index, ActionSuccessBlock success, ActionFailureBlock failure) { - OWSAssert(filePath.length > 0); - [self sendAttachment:filePath thread:thread success:success failure:failure]; - } - prepareBlock:^(ActionSuccessBlock success, ActionFailureBlock failure) { - return [self ensureRandomGifWithSuccess:^(NSString *ensuredFilePath) { - OWSAssert(ensuredFilePath.length > 0); - filePath = ensuredFilePath; - success(); - } - failure:failure]; - }]; + return [self sendMediaAction:@"Send Gif" fakeAssetLoader:[FakeAssetLoader gifInstance] thread:thread]; } + (DebugUIMessagesAction *)sendMp3Action:(TSThread *)thread { OWSAssert(thread); - __block NSString *_Nullable filePath = nil; - return [DebugUIMessagesAction actionWithLabel:@"Send Mp3" - actionBlock:^(NSUInteger index, ActionSuccessBlock success, ActionFailureBlock failure) { - OWSAssert(filePath.length > 0); - [self sendAttachment:filePath thread:thread success:success failure:failure]; - } - prepareBlock:^(ActionSuccessBlock success, ActionFailureBlock failure) { - return [self ensureRandomMp3WithSuccess:^(NSString *ensuredFilePath) { - OWSAssert(ensuredFilePath.length > 0); - filePath = ensuredFilePath; - success(); - } - failure:failure]; - }]; + return [self sendMediaAction:@"Send Mp3" fakeAssetLoader:[FakeAssetLoader mp3Instance] thread:thread]; } + (DebugUIMessagesAction *)sendMp4Action:(TSThread *)thread { OWSAssert(thread); - __block NSString *_Nullable filePath = nil; - return [DebugUIMessagesAction actionWithLabel:@"Send Mp4" - actionBlock:^(NSUInteger index, ActionSuccessBlock success, ActionFailureBlock failure) { - OWSAssert(filePath.length > 0); - [self sendAttachment:filePath thread:thread success:success failure:failure]; + return [self sendMediaAction:@"Send Mp4" fakeAssetLoader:[FakeAssetLoader mp4Instance] thread:thread]; +} + ++ (DebugUIMessagesAction *)sendMediaAction:(NSString *)label + fakeAssetLoader:(FakeAssetLoader *)fakeAssetLoader + thread:(TSThread *)thread +{ + OWSAssert(label.length > 0); + OWSAssert(fakeAssetLoader); + OWSAssert(thread); + + 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 success:success failure:failure]; + }); } - prepareBlock:^(ActionSuccessBlock success, ActionFailureBlock failure) { - return [self ensureRandomMp4WithSuccess:^(NSString *ensuredFilePath) { - OWSAssert(ensuredFilePath.length > 0); - filePath = ensuredFilePath; - success(); - } - failure:failure]; + prepareBlock:fakeAssetLoader.prepareBlock]; +} + ++ (DebugUIMessagesAction *)sendAllMediaAction:(TSThread *)thread +{ + OWSAssert(thread); + + return [DebugUIMessagesGroupAction allGroupActionWithLabel:@"Send All Media" + subactions:[self allSendMediaActions:thread]]; +} + ++ (DebugUIMessagesAction *)sendRandomMediaAction:(TSThread *)thread +{ + OWSAssert(thread); + + return [DebugUIMessagesGroupAction randomGroupActionWithLabel:@"Send Random Media" + subactions:[self allSendMediaActions:thread]]; +} + +#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:[FakeAssetLoader 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:[FakeAssetLoader gifInstance] + 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:[FakeAssetLoader 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:[FakeAssetLoader mp4Instance] + thread:thread]; +} + ++ (DebugUIMessagesAction *)fakeOutgoingMediaAction:(NSString *)labelParam + messageState:(TSOutgoingMessageState)messageState + hasCaption:(BOOL)hasCaption + fakeAssetLoader:(FakeAssetLoader *)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:(FakeAssetLoader *)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) { + messageBody = [[@(index).stringValue stringByAppendingString:@" "] stringByAppendingString:[self randomText]]; + + 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) + } + } + + 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]; + + DataSource *dataSource = [DataSourcePath dataSourceWithFilePath:fakeAssetLoader.filePath]; + NSString *filename = dataSource.sourceFilename; + TSAttachmentStream *attachmentStream = [[TSAttachmentStream alloc] initWithContentType:fakeAssetLoader.mimeType + byteCount:(UInt32)dataSource.dataLength + 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:[FakeAssetLoader 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:[FakeAssetLoader gifInstance] + 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:[FakeAssetLoader 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:[FakeAssetLoader mp4Instance] + thread:thread]; +} + ++ (DebugUIMessagesAction *)fakeIncomingMediaAction:(NSString *)labelParam + isAttachmentDownloaded:(BOOL)isAttachmentDownloaded + hasCaption:(BOOL)hasCaption + fakeAssetLoader:(FakeAssetLoader *)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:(FakeAssetLoader *)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) { + messageBody = [[@(index).stringValue stringByAppendingString:@" "] stringByAppendingString:[self randomText]]; + + if (hasCaption) { + messageBody = [messageBody stringByAppendingString:@" 🔤"]; + } + + if (isAttachmentDownloaded) { + messageBody = [messageBody stringByAppendingString:@" 👍"]; + } + } + NSString *attachmentId; + if (isAttachmentDownloaded) { + DataSource *dataSource = [DataSourcePath dataSourceWithFilePath:fakeAssetLoader.filePath]; + NSString *filename = dataSource.sourceFilename; + TSAttachmentStream *attachmentStream = + [[TSAttachmentStream alloc] initWithContentType:fakeAssetLoader.mimeType + byteCount:(UInt32)dataSource.dataLength + 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 +{ + OWSAssert(thread); + + NSArray *actions = @[ + // Outgoing + [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], + // Don't bother with multiple GIF states. + [self fakeOutgoingGifAction:thread messageState:TSOutgoingMessageStateSentToService hasCaption:NO], + // + [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], + // + [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], + + // Incoming + [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], + // Don't bother with multiple GIF states. + [self fakeIncomingGifAction:thread isAttachmentDownloaded:YES hasCaption:NO], + // + [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], + // + [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], + ]; + return actions; +} + ++ (DebugUIMessagesAction *)fakeAllMediaAction:(TSThread *)thread +{ + OWSAssert(thread); + + return [DebugUIMessagesGroupAction allGroupActionWithLabel:@"Fake All Media" + subactions:[self allFakeMediaActions:thread]]; +} + ++ (DebugUIMessagesAction *)fakeRandomMediaAction:(TSThread *)thread +{ + OWSAssert(thread); + + return [DebugUIMessagesGroupAction randomGroupActionWithLabel:@"Fake Random Media" + subactions:[self allFakeMediaActions:thread]]; +} + +#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]; }]; } -+ (void)ensureRandomJpegWithSuccess:(nullable void (^)(NSString *filePath))success - failure:(nullable void (^)(void))failure ++ (DebugUIMessagesAction *)fakeIncomingTextMessageAction:(TSThread *)thread text:(NSString *)text { - [self ensureRandomFileWithURL:@"https://s3.amazonaws.com/ows-data/example_attachment_media/random-jpg.JPG" - filename:@"random-jpg.jpg" - success:success - failure:failure]; + OWSAssert(thread); + + return [DebugUIMessagesSingleAction + actionWithLabel:[NSString stringWithFormat:@"Fake Incoming Text Message (%@)", text] + unstaggeredActionBlock:^(NSUInteger index, YapDatabaseReadWriteTransaction *transaction) { + TSIncomingMessage *message = [[TSIncomingMessage alloc] initWithTimestamp:[NSDate ows_millisecondTimeStamp] + inThread:thread + authorId:@"+19174054215" + sourceDeviceId:0 + messageBody:text]; + [message markAsReadWithTransaction:transaction sendReadReceipt:NO updateExpiration:NO]; + }]; } -+ (void)ensureRandomMp3WithSuccess:(nullable void (^)(NSString *filePath))success - failure:(nullable void (^)(void))failure ++ (DebugUIMessagesAction *)fakeOutgoingTextMessageAction:(TSThread *)thread + messageState:(TSOutgoingMessageState)messageState + text:(NSString *)text { - [self ensureRandomFileWithURL:@"https://s3.amazonaws.com/ows-data/example_attachment_media/random-mp3.mp3" - filename:@"random-mp3.mp3" - success:success - failure:failure]; + OWSAssert(thread); + + return [DebugUIMessagesSingleAction + actionWithLabel:[NSString stringWithFormat:@"Fake Incoming Text Message (%@)", text] + unstaggeredActionBlock:^(NSUInteger index, YapDatabaseReadWriteTransaction *transaction) { + TSOutgoingMessage *message = [[TSOutgoingMessage alloc] initWithTimestamp:[NSDate ows_millisecondTimeStamp] + inThread:thread + messageBody:text]; + [message saveWithTransaction:transaction]; + [message updateWithMessageState:messageState transaction:transaction]; + }]; } -+ (void)ensureRandomMp4WithSuccess:(nullable void (^)(NSString *filePath))success - failure:(nullable void (^)(void))failure ++ (DebugUIMessagesAction *)fakeShortOutgoingTextMessageAction:(TSThread *)thread + messageState:(TSOutgoingMessageState)messageState { - [self ensureRandomFileWithURL:@"https://s3.amazonaws.com/ows-data/example_attachment_media/random-mp4.mp4" - filename:@"random-mp4.mp4" - success:success - failure:failure]; + OWSAssert(thread); + + return [DebugUIMessagesSingleAction + actionWithLabel:@"Fake Short Incoming Text Message" + unstaggeredActionBlock:^(NSUInteger index, YapDatabaseReadWriteTransaction *transaction) { + NSString *messageBody = + [[@(index).stringValue stringByAppendingString:@" "] stringByAppendingString:[self randomText]]; + TSOutgoingMessage *message = [[TSOutgoingMessage alloc] initWithTimestamp:[NSDate ows_millisecondTimeStamp] + inThread:thread + messageBody:messageBody]; + [message saveWithTransaction:transaction]; + [message updateWithMessageState:messageState transaction:transaction]; + }]; } -+ (void)ensureRandomGifWithSuccess:(nullable void (^)(NSString *filePath))success - failure:(nullable void (^)(void))failure ++ (NSArray *)allFakeTextActions:(TSThread *)thread { - [self ensureRandomFileWithURL:@"https://s3.amazonaws.com/ows-data/example_attachment_media/random-gif.gif" - filename:@"random-gif.gif" - success:success - failure:failure]; + OWSAssert(thread); + + NSArray *actions = @[ + [self fakeShortIncomingTextMessageAction:thread], + [self fakeIncomingTextMessageAction:thread text:@"Hi"], + [self fakeIncomingTextMessageAction:thread text:@"1️⃣"], + [self fakeIncomingTextMessageAction:thread text:@"1️⃣2️⃣"], + [self fakeIncomingTextMessageAction:thread text:@"1️⃣2️⃣3️⃣"], + [self fakeIncomingTextMessageAction:thread text:@"落"], + [self fakeIncomingTextMessageAction:thread text:@"﷽"], + + [self fakeShortOutgoingTextMessageAction:thread messageState:TSOutgoingMessageStateUnsent], + [self fakeShortOutgoingTextMessageAction:thread messageState:TSOutgoingMessageStateAttemptingOut], + [self fakeShortOutgoingTextMessageAction:thread messageState:TSOutgoingMessageStateSentToService], + [self fakeOutgoingTextMessageAction:thread messageState:TSOutgoingMessageStateSentToService text:@"Hi"], + [self fakeOutgoingTextMessageAction:thread messageState:TSOutgoingMessageStateSentToService text:@"1️⃣"], + [self fakeOutgoingTextMessageAction:thread + messageState:TSOutgoingMessageStateSentToService + text:@"1️⃣2️⃣"], + [self fakeOutgoingTextMessageAction:thread + messageState:TSOutgoingMessageStateSentToService + text:@"1️⃣2️⃣3️⃣"], + [self fakeOutgoingTextMessageAction:thread messageState:TSOutgoingMessageStateSentToService text:@"1️⃣"], + [self fakeOutgoingTextMessageAction:thread messageState:TSOutgoingMessageStateSentToService text:@"落"], + [self fakeOutgoingTextMessageAction:thread messageState:TSOutgoingMessageStateSentToService text:@"﷽"], + ]; + return actions; } ++ (DebugUIMessagesAction *)fakeAllTextAction:(TSThread *)thread +{ + OWSAssert(thread); + + return [DebugUIMessagesGroupAction allGroupActionWithLabel:@"Fake All Text" + subactions:[self allFakeTextActions:thread]]; +} + ++ (DebugUIMessagesAction *)fakeRandomTextAction:(TSThread *)thread +{ + OWSAssert(thread); + + return [DebugUIMessagesGroupAction randomGroupActionWithLabel:@"Fake Random Text" + subactions:[self allFakeTextActions:thread]]; +} + +#pragma mark - Exemplary + ++ (DebugUIMessagesAction *)allExemplaryAction:(TSThread *)thread +{ + OWSAssert(thread); + + NSMutableArray *actions = [NSMutableArray new]; + [actions addObjectsFromArray:[self allFakeMediaActions:thread]]; + [actions addObjectsFromArray:[self allFakeTextActions:thread]]; + + return [DebugUIMessagesGroupAction allGroupActionWithLabel:@"Exemplary Permutations" subactions:actions]; +} + +#pragma mark - + + (void)sendOversizeTextMessage:(TSThread *)thread { OWSMessageSender *messageSender = [Environment current].messageSender; @@ -804,6 +1627,7 @@ typedef void (^ActionBlock)(NSUInteger index, ActionSuccessBlock success, Action ignoreErrors:YES completion:nil]; } + + (OWSSignalServiceProtosEnvelope *)createEnvelopeForThread:(TSThread *)thread { OWSAssert(thread); @@ -1187,148 +2011,7 @@ typedef void (^ActionBlock)(NSUInteger index, ActionSuccessBlock success, Action } } -+ (void)sendFakeMediaMessages:(NSUInteger)counter thread:(TSThread *)thread -{ - // Download media - void (^failureBlock)(void) = ^void(void) { - OWSFail(@"%@ Failed to download example media.", self.logTag); - return; - }; - - [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]; - } -} +#pragma mark - + (void)createFakeLargeOutgoingAttachments:(NSUInteger)counter thread:(TSThread *)thread { From a130760085ded83b18b5eabc9b9e9e84d29f7b85 Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Mon, 26 Mar 2018 09:19:49 -0400 Subject: [PATCH 07/31] Elaborate debug UI for messages. --- .../ViewControllers/DebugUI/DebugUIMessages.m | 518 ++++++++++++++++-- 1 file changed, 479 insertions(+), 39 deletions(-) diff --git a/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m b/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m index 66d39e6092..af0413c08c 100644 --- a/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m +++ b/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m @@ -7,13 +7,17 @@ #import "OWSTableViewController.h" #import "Signal-Swift.h" #import "ThreadUtil.h" +#import +#import #import #import #import +#import #import #import #import #import +#import #import #import #import @@ -26,6 +30,9 @@ NS_ASSUME_NONNULL_BEGIN +// Delivered/Read +// Fake image contents +// Oversize text messages. typedef void (^ActionSuccessBlock)(void); typedef void (^ActionFailureBlock)(void); typedef void (^ActionPrepareBlock)(ActionSuccessBlock success, ActionFailureBlock failure); @@ -42,10 +49,11 @@ typedef NS_ENUM(NSUInteger, SubactionMode) { @interface FakeAssetLoader : NSObject -@property (nonatomic) NSString *fileUrl; @property (nonatomic) NSString *filename; @property (nonatomic) NSString *mimeType; +@property (nonatomic) ActionPrepareBlock prepareBlock; + @property (nonatomic, nullable) NSString *filePath; @end @@ -54,11 +62,25 @@ typedef NS_ENUM(NSUInteger, SubactionMode) { @implementation FakeAssetLoader -- (void)ensureAssetLoaded:(ActionSuccessBlock)success failure:(ActionFailureBlock)failure ++ (FakeAssetLoader *)fakeAssetLoaderWithUrl:(NSString *)fileUrl mimeType:(NSString *)mimeType +{ + OWSAssert(fileUrl.length > 0); + OWSAssert(mimeType.length > 0); + + FakeAssetLoader *instance = [FakeAssetLoader new]; + instance.mimeType = mimeType; + instance.filename = [NSURL URLWithString:fileUrl].lastPathComponent; + __weak FakeAssetLoader *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.fileUrl.length > 0); OWSAssert(self.filename.length > 0); OWSAssert(self.mimeType.length > 0); @@ -67,24 +89,21 @@ typedef NS_ENUM(NSUInteger, SubactionMode) { return; } - NSString *filePath = [OWSFileSystem temporaryFilePathWithFileExtension:self.filename.pathExtension]; + // 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(); + } - // NSFileManager *fileManager = [NSFileManager defaultManager]; - // [fileManager] - // NSURL *documentDirectoryURL = - // [[fileManager URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject]; - // NSString *randomFilesDirectoryPath = - // [[documentDirectoryURL path] stringByAppendingPathComponent:@"cached_random_files"]; - // [OWSFileSystem ensureDirectoryExists:randomFilesDirectoryPath]; - // NSString *filePath = [randomFilesDirectoryPath stringByAppendingPathComponent:self.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:self.fileUrl + [sessionManager GET:fileUrl parameters:nil progress:nil success:^(NSURLSessionDataTask *task, NSData *_Nullable responseObject) { @@ -93,25 +112,146 @@ typedef NS_ENUM(NSUInteger, SubactionMode) { OWSAssert([NSFileManager.defaultManager fileExistsAtPath:filePath]); success(); } else { - OWSFail(@"Error write url response [%@]: %@", self.fileUrl, filePath); + OWSFail(@"Error write url response [%@]: %@", fileUrl, filePath); failure(); } } failure:^(NSURLSessionDataTask *_Nullable task, NSError *requestError) { - OWSFail(@"Error downloading url[%@]: %@", self.fileUrl, requestError); + OWSFail(@"Error downloading url[%@]: %@", fileUrl, requestError); failure(); }]; } +#pragma mark - + ++ (FakeAssetLoader *)fakePngAssetLoaderWithImageSize:(CGSize)imageSize imageColor:(UIColor *)imageColor +{ + OWSAssert(imageSize.width > 0); + OWSAssert(imageSize.height > 0); + + FakeAssetLoader *instance = [FakeAssetLoader new]; + instance.mimeType = OWSMimeTypeImagePng; + instance.filename = @"image.png"; + __weak FakeAssetLoader *weakSelf = instance; + instance.prepareBlock = ^(ActionSuccessBlock success, ActionFailureBlock failure) { + [weakSelf ensurePngAssetLoaded:imageSize imageColor:imageColor success:success failure:failure]; + }; + return instance; +} + +- (void)ensurePngAssetLoaded:(CGSize)imageSize + imageColor:(UIColor *)imageColor + success:(ActionSuccessBlock)success + failure:(ActionFailureBlock)failure +{ + OWSAssert(success); + OWSAssert(failure); + OWSAssert(self.filename.length > 0); + OWSAssert(self.mimeType.length > 0); + + NSString *filePath = [OWSFileSystem temporaryFilePathWithFileExtension:@"png"]; + UIImage *image = [UIImage imageWithColor:imageColor size:imageSize]; + NSData *pngData = UIImagePNGRepresentation(image); + [pngData writeToFile:filePath atomically:YES]; + self.filePath = filePath; + OWSAssert([NSFileManager.defaultManager fileExistsAtPath:filePath]); + success(); +} + +#pragma mark - + ++ (FakeAssetLoader *)fakeRandomAssetLoaderWithLength:(NSUInteger)dataLength mimeType:(NSString *)mimeType +{ + OWSAssert(dataLength > 0); + OWSAssert(mimeType.length > 0); + + FakeAssetLoader *instance = [FakeAssetLoader new]; + instance.mimeType = mimeType; + NSString *fileExtension = [MIMETypeUtil fileExtensionForMIMEType:mimeType]; + OWSAssert(fileExtension.length > 0); + instance.filename = [@"attachment" stringByAppendingPathExtension:fileExtension]; + __weak FakeAssetLoader *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; + } + + 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]); +} + +#pragma mark - + ++ (FakeAssetLoader *)fakeMissingAssetLoaderWithMimeType:(NSString *)mimeType +{ + OWSAssert(mimeType.length > 0); + + FakeAssetLoader *instance = [FakeAssetLoader new]; + instance.mimeType = mimeType; + NSString *fileExtension = [MIMETypeUtil fileExtensionForMIMEType:mimeType]; + OWSAssert(fileExtension.length > 0); + instance.filename = [@"attachment" stringByAppendingPathExtension:fileExtension]; + __weak FakeAssetLoader *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]); +} + +#pragma mark - + + (instancetype)jpegInstance { static FakeAssetLoader *instance = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ - instance = [self new]; - instance.fileUrl = @"https://s3.amazonaws.com/ows-data/example_attachment_media/random-jpg.JPG"; - instance.filename = @"random-jpg.jpg"; - instance.mimeType = @"image/jpeg"; + instance = [FakeAssetLoader + fakeAssetLoaderWithUrl:@"https://s3.amazonaws.com/ows-data/example_attachment_media/random-jpg.JPG" + mimeType:@"image/jpeg"]; }); return instance; } @@ -121,10 +261,9 @@ typedef NS_ENUM(NSUInteger, SubactionMode) { static FakeAssetLoader *instance = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ - instance = [self new]; - instance.fileUrl = @"https://s3.amazonaws.com/ows-data/example_attachment_media/random-gif.gif"; - instance.filename = @"random-gif.gif"; - instance.mimeType = @"image/gif"; + instance = [FakeAssetLoader + fakeAssetLoaderWithUrl:@"https://s3.amazonaws.com/ows-data/example_attachment_media/random-gif.gif" + mimeType:@"image/gif"]; }); return instance; } @@ -134,10 +273,9 @@ typedef NS_ENUM(NSUInteger, SubactionMode) { static FakeAssetLoader *instance = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ - instance = [self new]; - instance.fileUrl = @"https://s3.amazonaws.com/ows-data/example_attachment_media/random-mp3.mp3"; - instance.filename = @"random-mp3.mp3"; - instance.mimeType = @"audio/mpeg"; + instance = [FakeAssetLoader + fakeAssetLoaderWithUrl:@"https://s3.amazonaws.com/ows-data/example_attachment_media/random-mp3.mp3" + mimeType:@"audio/mpeg"]; }); return instance; } @@ -147,20 +285,83 @@ typedef NS_ENUM(NSUInteger, SubactionMode) { static FakeAssetLoader *instance = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ - instance = [self new]; - instance.fileUrl = @"https://s3.amazonaws.com/ows-data/example_attachment_media/random-mp4.mp4"; - instance.filename = @"random-mp4.mp4"; - instance.mimeType = @"video/mp4"; + instance = [FakeAssetLoader + fakeAssetLoaderWithUrl:@"https://s3.amazonaws.com/ows-data/example_attachment_media/random-mp4.mp4" + mimeType:@"video/mp4"]; }); return instance; } -- (ActionPrepareBlock)prepareBlock ++ (instancetype)portraitPngInstance { - __weak FakeAssetLoader *weakSelf = self; - return ^(ActionSuccessBlock success, ActionFailureBlock failure) { - [weakSelf ensureAssetLoaded:success failure:failure]; - }; + static FakeAssetLoader *instance = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + instance = [FakeAssetLoader fakePngAssetLoaderWithImageSize:CGSizeMake(10, 100) imageColor:[UIColor blueColor]]; + }); + return instance; +} + ++ (instancetype)landscapePngInstance +{ + static FakeAssetLoader *instance = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + instance = + [FakeAssetLoader fakePngAssetLoaderWithImageSize:CGSizeMake(100, 10) imageColor:[UIColor greenColor]]; + }); + return instance; +} + ++ (instancetype)largePngInstance +{ + static FakeAssetLoader *instance = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + instance = + [FakeAssetLoader fakePngAssetLoaderWithImageSize:CGSizeMake(4000, 4000) imageColor:[UIColor greenColor]]; + }); + return instance; +} + ++ (instancetype)tinyPdfInstance +{ + static FakeAssetLoader *instance = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + instance = [FakeAssetLoader fakeRandomAssetLoaderWithLength:256 mimeType:@"application/pdf"]; + }); + return instance; +} + ++ (instancetype)largePdfInstance +{ + static FakeAssetLoader *instance = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + instance = [FakeAssetLoader fakeRandomAssetLoaderWithLength:256 mimeType:@"application/pdf"]; + }); + return instance; +} + ++ (instancetype)missingPngInstance +{ + static FakeAssetLoader *instance = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + instance = [FakeAssetLoader fakeMissingAssetLoaderWithMimeType:OWSMimeTypeImagePng]; + }); + return instance; +} + ++ (instancetype)missingPdfInstance +{ + static FakeAssetLoader *instance = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + instance = [FakeAssetLoader fakeMissingAssetLoaderWithMimeType:@"application/pdf"]; + }); + return instance; } @end @@ -1064,6 +1265,97 @@ typedef NS_ENUM(NSUInteger, SubactionMode) { thread:thread]; } ++ (DebugUIMessagesAction *)fakeOutgoingPortraitPngAction:(TSThread *)thread + messageState:(TSOutgoingMessageState)messageState + hasCaption:(BOOL)hasCaption +{ + OWSAssert(thread); + + return [self fakeOutgoingMediaAction:@"Fake Outgoing Portrait Png" + messageState:messageState + hasCaption:hasCaption + fakeAssetLoader:[FakeAssetLoader landscapePngInstance] + thread:thread]; +} + ++ (DebugUIMessagesAction *)fakeOutgoingLandscapePngAction:(TSThread *)thread + messageState:(TSOutgoingMessageState)messageState + hasCaption:(BOOL)hasCaption +{ + OWSAssert(thread); + + return [self fakeOutgoingMediaAction:@"Fake Outgoing Landscape Png" + messageState:messageState + hasCaption:hasCaption + fakeAssetLoader:[FakeAssetLoader portraitPngInstance] + 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:[FakeAssetLoader largePngInstance] + 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:[FakeAssetLoader 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:[FakeAssetLoader largePdfInstance] + thread:thread]; +} + ++ (DebugUIMessagesAction *)fakeOutgoingMissingPngAction:(TSThread *)thread + messageState:(TSOutgoingMessageState)messageState + hasCaption:(BOOL)hasCaption +{ + OWSAssert(thread); + + return [self fakeOutgoingMediaAction:@"Fake Missing Png" + messageState:messageState + hasCaption:hasCaption + fakeAssetLoader:[FakeAssetLoader missingPngInstance] + thread:thread]; +} + ++ (DebugUIMessagesAction *)fakeOutgoingMissingPdfAction:(TSThread *)thread + messageState:(TSOutgoingMessageState)messageState + hasCaption:(BOOL)hasCaption +{ + OWSAssert(thread); + + return [self fakeOutgoingMediaAction:@"Fake Missing Pdf" + messageState:messageState + hasCaption:hasCaption + fakeAssetLoader:[FakeAssetLoader missingPdfInstance] + thread:thread]; +} + + (DebugUIMessagesAction *)fakeOutgoingMediaAction:(NSString *)labelParam messageState:(TSOutgoingMessageState)messageState hasCaption:(BOOL)hasCaption @@ -1220,6 +1512,97 @@ typedef NS_ENUM(NSUInteger, SubactionMode) { thread:thread]; } ++ (DebugUIMessagesAction *)fakeIncomingPortraitPngAction:(TSThread *)thread + isAttachmentDownloaded:(BOOL)isAttachmentDownloaded + hasCaption:(BOOL)hasCaption +{ + OWSAssert(thread); + + return [self fakeIncomingMediaAction:@"Fake Incoming Portrait Png" + isAttachmentDownloaded:isAttachmentDownloaded + hasCaption:hasCaption + fakeAssetLoader:[FakeAssetLoader portraitPngInstance] + thread:thread]; +} + ++ (DebugUIMessagesAction *)fakeIncomingLandscapePngAction:(TSThread *)thread + isAttachmentDownloaded:(BOOL)isAttachmentDownloaded + hasCaption:(BOOL)hasCaption +{ + OWSAssert(thread); + + return [self fakeIncomingMediaAction:@"Fake Incoming Landscape Png" + isAttachmentDownloaded:isAttachmentDownloaded + hasCaption:hasCaption + fakeAssetLoader:[FakeAssetLoader landscapePngInstance] + 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:[FakeAssetLoader largePngInstance] + 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:[FakeAssetLoader 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:[FakeAssetLoader 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:[FakeAssetLoader 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:[FakeAssetLoader missingPdfInstance] + thread:thread]; +} + + (DebugUIMessagesAction *)fakeIncomingMediaAction:(NSString *)labelParam isAttachmentDownloaded:(BOOL)isAttachmentDownloaded hasCaption:(BOOL)hasCaption @@ -1352,6 +1735,35 @@ typedef NS_ENUM(NSUInteger, SubactionMode) { [self fakeOutgoingMp4Action:thread messageState:TSOutgoingMessageStateUnsent hasCaption:YES], [self fakeOutgoingMp4Action:thread messageState:TSOutgoingMessageStateSentToService hasCaption:NO], [self fakeOutgoingMp4Action:thread messageState:TSOutgoingMessageStateSentToService hasCaption:YES], + // + [self fakeOutgoingLandscapePngAction:thread messageState:TSOutgoingMessageStateAttemptingOut hasCaption:NO], + [self fakeOutgoingLandscapePngAction:thread messageState:TSOutgoingMessageStateAttemptingOut hasCaption:YES], + [self fakeOutgoingLandscapePngAction:thread messageState:TSOutgoingMessageStateUnsent hasCaption:NO], + [self fakeOutgoingLandscapePngAction:thread messageState:TSOutgoingMessageStateUnsent hasCaption:YES], + [self fakeOutgoingLandscapePngAction:thread messageState:TSOutgoingMessageStateSentToService hasCaption:NO], + [self fakeOutgoingLandscapePngAction:thread messageState:TSOutgoingMessageStateSentToService hasCaption:YES], + // + [self fakeOutgoingPortraitPngAction:thread messageState:TSOutgoingMessageStateAttemptingOut hasCaption:NO], + [self fakeOutgoingPortraitPngAction:thread messageState:TSOutgoingMessageStateAttemptingOut hasCaption:YES], + [self fakeOutgoingPortraitPngAction:thread messageState:TSOutgoingMessageStateUnsent hasCaption:NO], + [self fakeOutgoingPortraitPngAction:thread messageState:TSOutgoingMessageStateUnsent hasCaption:YES], + [self fakeOutgoingPortraitPngAction:thread messageState:TSOutgoingMessageStateSentToService hasCaption:NO], + [self fakeOutgoingPortraitPngAction:thread messageState:TSOutgoingMessageStateSentToService hasCaption:YES], + // + [self fakeOutgoingLargePngAction:thread messageState:TSOutgoingMessageStateSentToService hasCaption:YES], + // + [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], + // + [self fakeOutgoingLargePdfAction:thread messageState:TSOutgoingMessageStateUnsent hasCaption:NO], + // + [self fakeOutgoingMissingPngAction:thread messageState:TSOutgoingMessageStateUnsent hasCaption:NO], + // + [self fakeOutgoingMissingPdfAction:thread messageState:TSOutgoingMessageStateUnsent hasCaption:NO], // Incoming [self fakeIncomingJpegAction:thread isAttachmentDownloaded:NO hasCaption:NO], @@ -1370,6 +1782,32 @@ typedef NS_ENUM(NSUInteger, SubactionMode) { [self fakeIncomingMp4Action:thread isAttachmentDownloaded:YES hasCaption:NO], [self fakeIncomingMp4Action:thread isAttachmentDownloaded:NO hasCaption:YES], [self fakeIncomingMp4Action:thread isAttachmentDownloaded:YES hasCaption:YES], + // + [self fakeIncomingLandscapePngAction:thread isAttachmentDownloaded:NO hasCaption:NO], + [self fakeIncomingLandscapePngAction:thread isAttachmentDownloaded:YES hasCaption:NO], + [self fakeIncomingLandscapePngAction:thread isAttachmentDownloaded:NO hasCaption:YES], + [self fakeIncomingLandscapePngAction:thread isAttachmentDownloaded:YES hasCaption:YES], + // + [self fakeIncomingPortraitPngAction:thread isAttachmentDownloaded:NO hasCaption:NO], + [self fakeIncomingPortraitPngAction:thread isAttachmentDownloaded:YES hasCaption:NO], + [self fakeIncomingPortraitPngAction:thread isAttachmentDownloaded:NO hasCaption:YES], + [self fakeIncomingPortraitPngAction:thread isAttachmentDownloaded:YES hasCaption:YES], + // + [self fakeIncomingLargePngAction:thread isAttachmentDownloaded:YES hasCaption:NO], + [self fakeIncomingLargePngAction:thread isAttachmentDownloaded:YES hasCaption:YES], + // + [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], + // + [self fakeIncomingLargePdfAction:thread isAttachmentDownloaded:YES hasCaption:NO], + // + [self fakeIncomingMissingPngAction:thread isAttachmentDownloaded:YES hasCaption:NO], + [self fakeIncomingMissingPngAction:thread isAttachmentDownloaded:YES hasCaption:YES], + // + [self fakeIncomingMissingPdfAction:thread isAttachmentDownloaded:YES hasCaption:NO], + [self fakeIncomingMissingPdfAction:thread isAttachmentDownloaded:YES hasCaption:YES], ]; return actions; } @@ -1634,6 +2072,8 @@ typedef NS_ENUM(NSUInteger, SubactionMode) { OWSSignalServiceProtosEnvelopeBuilder *builder = [OWSSignalServiceProtosEnvelopeBuilder new]; + [builder setTimestamp:[NSDate ows_millisecondTimeStamp]]; + if ([thread isKindOfClass:[TSGroupThread class]]) { TSGroupThread *gThread = (TSGroupThread *)thread; [builder setSource:gThread.groupModel.groupMemberIds[0]]; From 8542a18f33d0d77f367e7c85c0974961fae5ee40 Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Mon, 26 Mar 2018 09:33:23 -0400 Subject: [PATCH 08/31] Elaborate debug UI for messages. --- .../ViewControllers/DebugUI/DebugUIMessages.m | 130 +++++++++++++++++- 1 file changed, 126 insertions(+), 4 deletions(-) diff --git a/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m b/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m index af0413c08c..6260148217 100644 --- a/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m +++ b/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m @@ -30,9 +30,7 @@ NS_ASSUME_NONNULL_BEGIN -// Delivered/Read // Fake image contents -// Oversize text messages. typedef void (^ActionSuccessBlock)(void); typedef void (^ActionFailureBlock)(void); typedef void (^ActionPrepareBlock)(ActionSuccessBlock success, ActionFailureBlock failure); @@ -244,6 +242,57 @@ typedef NS_ENUM(NSUInteger, SubactionMode) { #pragma mark - ++ (FakeAssetLoader *)fakeOversizeTextAssetLoader +{ + FakeAssetLoader *instance = [FakeAssetLoader new]; + instance.mimeType = OWSMimeTypeOversizeTextMessage; + instance.filename = @"attachment.txt"; + __weak FakeAssetLoader *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]); +} + +#pragma mark - + + (instancetype)jpegInstance { static FakeAssetLoader *instance = nil; @@ -364,6 +413,16 @@ typedef NS_ENUM(NSUInteger, SubactionMode) { return instance; } ++ (instancetype)oversizeTextInstance +{ + static FakeAssetLoader *instance = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + instance = [FakeAssetLoader fakeOversizeTextAssetLoader]; + }); + return instance; +} + @end #pragma mark - @@ -1336,7 +1395,7 @@ typedef NS_ENUM(NSUInteger, SubactionMode) { { OWSAssert(thread); - return [self fakeOutgoingMediaAction:@"Fake Missing Png" + return [self fakeOutgoingMediaAction:@"Fake Outgoing Missing Png" messageState:messageState hasCaption:hasCaption fakeAssetLoader:[FakeAssetLoader missingPngInstance] @@ -1349,13 +1408,26 @@ typedef NS_ENUM(NSUInteger, SubactionMode) { { OWSAssert(thread); - return [self fakeOutgoingMediaAction:@"Fake Missing Pdf" + return [self fakeOutgoingMediaAction:@"Fake Outgoing Missing Pdf" messageState:messageState hasCaption:hasCaption fakeAssetLoader:[FakeAssetLoader 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:[FakeAssetLoader oversizeTextInstance] + thread:thread]; +} + + (DebugUIMessagesAction *)fakeOutgoingMediaAction:(NSString *)labelParam messageState:(TSOutgoingMessageState)messageState hasCaption:(BOOL)hasCaption @@ -1603,6 +1675,19 @@ typedef NS_ENUM(NSUInteger, SubactionMode) { 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:[FakeAssetLoader oversizeTextInstance] + thread:thread]; +} + + (DebugUIMessagesAction *)fakeIncomingMediaAction:(NSString *)labelParam isAttachmentDownloaded:(BOOL)isAttachmentDownloaded hasCaption:(BOOL)hasCaption @@ -1764,6 +1849,10 @@ typedef NS_ENUM(NSUInteger, SubactionMode) { [self fakeOutgoingMissingPngAction:thread messageState:TSOutgoingMessageStateUnsent hasCaption:NO], // [self fakeOutgoingMissingPdfAction:thread messageState:TSOutgoingMessageStateUnsent hasCaption:NO], + // + [self fakeOutgoingOversizeTextAction:thread messageState:TSOutgoingMessageStateUnsent hasCaption:NO], + [self fakeOutgoingOversizeTextAction:thread messageState:TSOutgoingMessageStateAttemptingOut hasCaption:NO], + [self fakeOutgoingOversizeTextAction:thread messageState:TSOutgoingMessageStateSentToService hasCaption:NO], // Incoming [self fakeIncomingJpegAction:thread isAttachmentDownloaded:NO hasCaption:NO], @@ -1808,6 +1897,9 @@ typedef NS_ENUM(NSUInteger, SubactionMode) { // [self fakeIncomingMissingPdfAction:thread isAttachmentDownloaded:YES hasCaption:NO], [self fakeIncomingMissingPdfAction:thread isAttachmentDownloaded:YES hasCaption:YES], + // + [self fakeIncomingOversizeTextAction:thread isAttachmentDownloaded:NO hasCaption:NO], + [self fakeIncomingOversizeTextAction:thread isAttachmentDownloaded:YES hasCaption:NO], ]; return actions; } @@ -1927,6 +2019,14 @@ typedef NS_ENUM(NSUInteger, SubactionMode) { + (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 { OWSAssert(thread); @@ -1940,6 +2040,20 @@ typedef NS_ENUM(NSUInteger, SubactionMode) { 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]; + } + if (isRead) { + NSString *_Nullable recipientId = thread.recipientIdentifiers.lastObject; + OWSAssert(recipientId.length > 0); + [message updateWithReadRecipientId:recipientId + readTimestamp:[NSDate ows_millisecondTimeStamp] + transaction:transaction]; + } }]; } @@ -1959,6 +2073,14 @@ typedef NS_ENUM(NSUInteger, SubactionMode) { [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], [self fakeOutgoingTextMessageAction:thread messageState:TSOutgoingMessageStateSentToService text:@"Hi"], [self fakeOutgoingTextMessageAction:thread messageState:TSOutgoingMessageStateSentToService text:@"1️⃣"], [self fakeOutgoingTextMessageAction:thread From 469fb2644f41ff69d32e3c5edd90753a2414f7e2 Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Mon, 26 Mar 2018 10:20:38 -0400 Subject: [PATCH 09/31] Elaborate debug UI for messages. --- .../ViewControllers/DebugUI/DebugUIMessages.m | 214 +++++++++++++----- 1 file changed, 152 insertions(+), 62 deletions(-) diff --git a/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m b/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m index 6260148217..adbde937d4 100644 --- a/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m +++ b/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m @@ -30,7 +30,6 @@ NS_ASSUME_NONNULL_BEGIN -// Fake image contents typedef void (^ActionSuccessBlock)(void); typedef void (^ActionFailureBlock)(void); typedef void (^ActionPrepareBlock)(ActionSuccessBlock success, ActionFailureBlock failure); @@ -122,23 +121,36 @@ typedef NS_ENUM(NSUInteger, SubactionMode) { #pragma mark - -+ (FakeAssetLoader *)fakePngAssetLoaderWithImageSize:(CGSize)imageSize imageColor:(UIColor *)imageColor ++ (FakeAssetLoader *)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); FakeAssetLoader *instance = [FakeAssetLoader new]; instance.mimeType = OWSMimeTypeImagePng; instance.filename = @"image.png"; __weak FakeAssetLoader *weakSelf = instance; instance.prepareBlock = ^(ActionSuccessBlock success, ActionFailureBlock failure) { - [weakSelf ensurePngAssetLoaded:imageSize imageColor:imageColor success:success failure:failure]; + [weakSelf ensurePngAssetLoaded:imageSize + backgroundColor:backgroundColor + textColor:textColor + label:label + success:success + failure:failure]; }; return instance; } - (void)ensurePngAssetLoaded:(CGSize)imageSize - imageColor:(UIColor *)imageColor + backgroundColor:(UIColor *)backgroundColor + textColor:(UIColor *)textColor + label:(NSString *)label success:(ActionSuccessBlock)success failure:(ActionFailureBlock)failure { @@ -146,9 +158,19 @@ typedef NS_ENUM(NSUInteger, SubactionMode) { 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; + } NSString *filePath = [OWSFileSystem temporaryFilePathWithFileExtension:@"png"]; - UIImage *image = [UIImage imageWithColor:imageColor size:imageSize]; + UIImage *image = + [self createRandomPngWithSize:imageSize backgroundColor:backgroundColor textColor:textColor label:label]; NSData *pngData = UIImagePNGRepresentation(image); [pngData writeToFile:filePath atomically:YES]; self.filePath = filePath; @@ -156,6 +178,43 @@ typedef NS_ENUM(NSUInteger, SubactionMode) { 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); + + 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 - + (FakeAssetLoader *)fakeRandomAssetLoaderWithLength:(NSUInteger)dataLength mimeType:(NSString *)mimeType @@ -199,6 +258,8 @@ typedef NS_ENUM(NSUInteger, SubactionMode) { OWSAssert(didWrite); self.filePath = filePath; OWSAssert([NSFileManager.defaultManager fileExistsAtPath:filePath]); + + success(); } #pragma mark - @@ -238,6 +299,8 @@ typedef NS_ENUM(NSUInteger, SubactionMode) { OWSAssert(didCreate); self.filePath = filePath; OWSAssert([NSFileManager.defaultManager fileExistsAtPath:filePath]); + + success(); } #pragma mark - @@ -289,6 +352,8 @@ typedef NS_ENUM(NSUInteger, SubactionMode) { OWSAssert(didWrite); self.filePath = filePath; OWSAssert([NSFileManager.defaultManager fileExistsAtPath:filePath]); + + success(); } #pragma mark - @@ -346,7 +411,10 @@ typedef NS_ENUM(NSUInteger, SubactionMode) { static FakeAssetLoader *instance = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ - instance = [FakeAssetLoader fakePngAssetLoaderWithImageSize:CGSizeMake(10, 100) imageColor:[UIColor blueColor]]; + instance = [FakeAssetLoader fakePngAssetLoaderWithImageSize:CGSizeMake(10, 100) + backgroundColor:[UIColor blueColor] + textColor:[UIColor whiteColor] + label:@"P"]; }); return instance; } @@ -356,8 +424,10 @@ typedef NS_ENUM(NSUInteger, SubactionMode) { static FakeAssetLoader *instance = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ - instance = - [FakeAssetLoader fakePngAssetLoaderWithImageSize:CGSizeMake(100, 10) imageColor:[UIColor greenColor]]; + instance = [FakeAssetLoader fakePngAssetLoaderWithImageSize:CGSizeMake(100, 10) + backgroundColor:[UIColor greenColor] + textColor:[UIColor whiteColor] + label:@"L"]; }); return instance; } @@ -367,8 +437,10 @@ typedef NS_ENUM(NSUInteger, SubactionMode) { static FakeAssetLoader *instance = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ - instance = - [FakeAssetLoader fakePngAssetLoaderWithImageSize:CGSizeMake(4000, 4000) imageColor:[UIColor greenColor]]; + instance = [FakeAssetLoader fakePngAssetLoaderWithImageSize:CGSizeMake(4000, 4000) + backgroundColor:[UIColor redColor] + textColor:[UIColor whiteColor] + label:@"B"]; }); return instance; } @@ -467,7 +539,9 @@ typedef NS_ENUM(NSUInteger, SubactionMode) { - (void)prepareAndPerformNTimes:(NSUInteger)count { - // __weak DebugUIMessagesAction *weakSelf = self; + DDLogInfo(@"%@ %@ prepareAndPerformNTimes: %zd", self.logTag, self.label, count); + [DDLog flushLog]; + [self prepare:^{ [self performNTimes:count success:^{ @@ -622,12 +696,6 @@ typedef NS_ENUM(NSUInteger, SubactionMode) { @property (nonatomic, nullable) NSArray *subactions; @property (nonatomic) NSUInteger subactionIndex; -//@property (nonatomic, nullable) MessageActionBlock actionBlock; -//@property (nonatomic, nullable) StaggeredActionBlock staggeredActionBlock; -//@property (nonatomic, nullable) BulkActionBlock staggeredActionBlock; -//@property (nonatomic, nullable) BulkActionBlock bulkActionBlock; -//@property (nonatomic) BOOL isStaggered; - @end #pragma mark - @@ -674,6 +742,8 @@ typedef NS_ENUM(NSUInteger, SubactionMode) { DebugUIMessagesAction *nextAction = unpreparedSubactions.lastObject; [unpreparedSubactions removeLastObject]; + DDLogInfo(@"%@ preparing: %@", self.logTag, nextAction.label); + [DDLog flushLog]; [nextAction prepare:^{ [self prepareSubactions:unpreparedSubactions success:success failure:failure]; } @@ -692,14 +762,6 @@ typedef NS_ENUM(NSUInteger, SubactionMode) { instance.label = label; instance.subactions = subactions; instance.subactionMode = SubactionMode_Random; - // instance.staggeredActionBlock = ^(NSUInteger index, YapDatabaseReadWriteTransaction *transaction, - // ActionSuccessBlock success, ActionFailureBlock failure) { - // [self performRandomStaggeredSubaction:subactions index:index transaction:transaction success:success - // failure:failure]; - // }; - // instance.prepareBlock = ^(ActionSuccessBlock success, ActionFailureBlock failure) { - // [DebugUIMessagesAction prepareSubactions:[subactions mutableCopy] success:success failure:failure]; - // }; return instance; } @@ -714,16 +776,6 @@ typedef NS_ENUM(NSUInteger, SubactionMode) { instance.label = label; instance.subactions = subactions; instance.subactionMode = SubactionMode_Ordered; - // instance.bulkActionBlock = ^(NSUInteger count, YapDatabaseReadWriteTransaction *transaction, - // ActionSuccessBlock success, ActionFailureBlock failure) { - // for (NSUInteger index = 0; index < count; index++) { - // [self performAllUnstaggeredSubactions:[subactions mutableCopy] index:index transaction:transaction - // success:success failure:failure]; - // } - // }; - // instance.prepareBlock = ^(ActionSuccessBlock success, ActionFailureBlock failure) { - // [DebugUIMessagesAction prepareSubactions:[subactions mutableCopy] success:success failure:failure]; - // }; return instance; } @@ -795,10 +847,14 @@ typedef NS_ENUM(NSUInteger, SubactionMode) { actionBlock:^{ [DebugUIMessages sendNTextMessagesInThread:thread]; }], - // [OWSTableItem itemWithTitle:@"Send N Random Media (1/sec.)" - // actionBlock:^{ - // [DebugUIMessages sendSelectedMediaTypeInThread:thread]; - // }], + // [OWSTableItem itemWithTitle:@"Send N Random Media (1/sec.)" + // actionBlock:^{ + // [DebugUIMessages sendSelectedMediaTypeInThread:thread]; + // }], + [OWSTableItem itemWithTitle:@"Select Action" + actionBlock:^{ + [DebugUIMessages selectExemplaryAction:thread]; + }], #pragma mark - Misc. @@ -1514,8 +1570,11 @@ typedef NS_ENUM(NSUInteger, SubactionMode) { 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:(UInt32)dataSource.dataLength + byteCount:nominalDataLength sourceFilename:filename]; NSError *error; BOOL success = [attachmentStream writeData:dataSource.data error:&error]; @@ -1753,10 +1812,12 @@ typedef NS_ENUM(NSUInteger, SubactionMode) { if (isAttachmentDownloaded) { DataSource *dataSource = [DataSourcePath dataSourceWithFilePath:fakeAssetLoader.filePath]; NSString *filename = dataSource.sourceFilename; - TSAttachmentStream *attachmentStream = - [[TSAttachmentStream alloc] initWithContentType:fakeAssetLoader.mimeType - byteCount:(UInt32)dataSource.dataLength - sourceFilename:filename]; + // 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); @@ -2030,8 +2091,25 @@ typedef NS_ENUM(NSUInteger, SubactionMode) { { 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:@"Fake Short Incoming Text Message" + actionWithLabel:label unstaggeredActionBlock:^(NSUInteger index, YapDatabaseReadWriteTransaction *transaction) { NSString *messageBody = [[@(index).stringValue stringByAppendingString:@" "] stringByAppendingString:[self randomText]]; @@ -2114,15 +2192,42 @@ typedef NS_ENUM(NSUInteger, SubactionMode) { #pragma mark - Exemplary -+ (DebugUIMessagesAction *)allExemplaryAction:(TSThread *)thread ++ (NSArray *)allExemplaryActions:(TSThread *)thread { OWSAssert(thread); NSMutableArray *actions = [NSMutableArray new]; [actions addObjectsFromArray:[self allFakeMediaActions:thread]]; [actions addObjectsFromArray:[self allFakeTextActions:thread]]; + return actions; +} - return [DebugUIMessagesGroupAction allGroupActionWithLabel:@"Exemplary Permutations" subactions:actions]; ++ (DebugUIMessagesAction *)allExemplaryAction:(TSThread *)thread +{ + OWSAssert(thread); + + return [DebugUIMessagesGroupAction allGroupActionWithLabel:@"Exemplary Permutations" + subactions:[self allExemplaryActions:thread]]; +} + ++ (void)selectExemplaryAction:(TSThread *)thread +{ + OWSAssertIsOnMainThread() OWSAssert(thread); + UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"Select Action" + message:nil + preferredStyle:UIAlertControllerStyleActionSheet]; + for (DebugUIMessagesAction *action in [self allExemplaryActions:thread]) { + [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 - @@ -2752,21 +2857,6 @@ typedef NS_ENUM(NSUInteger, SubactionMode) { }); } -//+ (ActionBlock)sendTextMessagesActionInThread:(TSThread *)thread -//{ -// OWSAssert(thread); -// -// return ^(NSUInteger index, ActionSuccessBlock success, ActionFailureBlock failure) { -// [self sendTextMessageInThread:thread counter:index]; -// // TODO: -// success(); -// }; -//} -// -//+ (void)performRandomActionInThread:(TSThread *)thread counter:(NSUInteger)counter -//{ -//} - + (void)performRandomActionInThread:(TSThread *)thread counter:(NSUInteger)counter { typedef void (^TransactionBlock)(YapDatabaseReadWriteTransaction *transaction); From 041b28dd79fe5540d050eb0be28f09e5aa1688c8 Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Mon, 26 Mar 2018 10:45:52 -0400 Subject: [PATCH 10/31] Elaborate debug UI for messages. --- Signal.xcodeproj/project.pbxproj | 14 + .../ViewControllers/DebugUI/DebugUIMessages.h | 4 +- .../ViewControllers/DebugUI/DebugUIMessages.m | 835 +----------------- .../DebugUI/DebugUIMessagesAction.h | 62 ++ .../DebugUI/DebugUIMessagesAction.m | 288 ++++++ .../DebugUI/DebugUIMessagesAssetLoader.h | 35 + .../DebugUI/DebugUIMessagesAssetLoader.m | 454 ++++++++++ .../DebugUI/DebugUIMessagesUtils.h | 18 + 8 files changed, 908 insertions(+), 802 deletions(-) create mode 100644 Signal/src/ViewControllers/DebugUI/DebugUIMessagesAction.h create mode 100644 Signal/src/ViewControllers/DebugUI/DebugUIMessagesAction.m create mode 100644 Signal/src/ViewControllers/DebugUI/DebugUIMessagesAssetLoader.h create mode 100644 Signal/src/ViewControllers/DebugUI/DebugUIMessagesAssetLoader.m create mode 100644 Signal/src/ViewControllers/DebugUI/DebugUIMessagesUtils.h diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index 6ee946dd62..3906643d05 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -206,6 +206,8 @@ 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 */; }; @@ -814,6 +816,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 = ""; }; @@ -1676,6 +1683,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 */, @@ -3137,6 +3149,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 */, @@ -3246,6 +3259,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/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 adbde937d4..6302c9665e 100644 --- a/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m +++ b/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m @@ -4,785 +4,22 @@ #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 #import +#import #import NS_ASSUME_NONNULL_BEGIN -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); - -typedef NS_ENUM(NSUInteger, SubactionMode) { - SubactionMode_Random = 0, - SubactionMode_Ordered, -}; - -@interface FakeAssetLoader : NSObject - -@property (nonatomic) NSString *filename; -@property (nonatomic) NSString *mimeType; - -@property (nonatomic) ActionPrepareBlock prepareBlock; - -@property (nonatomic, nullable) NSString *filePath; - -@end - -#pragma mark - - -@implementation FakeAssetLoader - -+ (FakeAssetLoader *)fakeAssetLoaderWithUrl:(NSString *)fileUrl mimeType:(NSString *)mimeType -{ - OWSAssert(fileUrl.length > 0); - OWSAssert(mimeType.length > 0); - - FakeAssetLoader *instance = [FakeAssetLoader new]; - instance.mimeType = mimeType; - instance.filename = [NSURL URLWithString:fileUrl].lastPathComponent; - __weak FakeAssetLoader *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 - - -+ (FakeAssetLoader *)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); - - FakeAssetLoader *instance = [FakeAssetLoader new]; - instance.mimeType = OWSMimeTypeImagePng; - instance.filename = @"image.png"; - __weak FakeAssetLoader *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; - } - - 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); - - 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 - - -+ (FakeAssetLoader *)fakeRandomAssetLoaderWithLength:(NSUInteger)dataLength mimeType:(NSString *)mimeType -{ - OWSAssert(dataLength > 0); - OWSAssert(mimeType.length > 0); - - FakeAssetLoader *instance = [FakeAssetLoader new]; - instance.mimeType = mimeType; - NSString *fileExtension = [MIMETypeUtil fileExtensionForMIMEType:mimeType]; - OWSAssert(fileExtension.length > 0); - instance.filename = [@"attachment" stringByAppendingPathExtension:fileExtension]; - __weak FakeAssetLoader *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; - } - - 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 - - -+ (FakeAssetLoader *)fakeMissingAssetLoaderWithMimeType:(NSString *)mimeType -{ - OWSAssert(mimeType.length > 0); - - FakeAssetLoader *instance = [FakeAssetLoader new]; - instance.mimeType = mimeType; - NSString *fileExtension = [MIMETypeUtil fileExtensionForMIMEType:mimeType]; - OWSAssert(fileExtension.length > 0); - instance.filename = [@"attachment" stringByAppendingPathExtension:fileExtension]; - __weak FakeAssetLoader *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 - - -+ (FakeAssetLoader *)fakeOversizeTextAssetLoader -{ - FakeAssetLoader *instance = [FakeAssetLoader new]; - instance.mimeType = OWSMimeTypeOversizeTextMessage; - instance.filename = @"attachment.txt"; - __weak FakeAssetLoader *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 FakeAssetLoader *instance = nil; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - instance = [FakeAssetLoader - fakeAssetLoaderWithUrl:@"https://s3.amazonaws.com/ows-data/example_attachment_media/random-jpg.JPG" - mimeType:@"image/jpeg"]; - }); - return instance; -} - -+ (instancetype)gifInstance -{ - static FakeAssetLoader *instance = nil; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - instance = [FakeAssetLoader - fakeAssetLoaderWithUrl:@"https://s3.amazonaws.com/ows-data/example_attachment_media/random-gif.gif" - mimeType:@"image/gif"]; - }); - return instance; -} - -+ (instancetype)mp3Instance -{ - static FakeAssetLoader *instance = nil; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - instance = [FakeAssetLoader - fakeAssetLoaderWithUrl:@"https://s3.amazonaws.com/ows-data/example_attachment_media/random-mp3.mp3" - mimeType:@"audio/mpeg"]; - }); - return instance; -} - -+ (instancetype)mp4Instance -{ - static FakeAssetLoader *instance = nil; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - instance = [FakeAssetLoader - fakeAssetLoaderWithUrl:@"https://s3.amazonaws.com/ows-data/example_attachment_media/random-mp4.mp4" - mimeType:@"video/mp4"]; - }); - return instance; -} - -+ (instancetype)portraitPngInstance -{ - static FakeAssetLoader *instance = nil; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - instance = [FakeAssetLoader fakePngAssetLoaderWithImageSize:CGSizeMake(10, 100) - backgroundColor:[UIColor blueColor] - textColor:[UIColor whiteColor] - label:@"P"]; - }); - return instance; -} - -+ (instancetype)landscapePngInstance -{ - static FakeAssetLoader *instance = nil; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - instance = [FakeAssetLoader fakePngAssetLoaderWithImageSize:CGSizeMake(100, 10) - backgroundColor:[UIColor greenColor] - textColor:[UIColor whiteColor] - label:@"L"]; - }); - return instance; -} - -+ (instancetype)largePngInstance -{ - static FakeAssetLoader *instance = nil; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - instance = [FakeAssetLoader fakePngAssetLoaderWithImageSize:CGSizeMake(4000, 4000) - backgroundColor:[UIColor redColor] - textColor:[UIColor whiteColor] - label:@"B"]; - }); - return instance; -} - -+ (instancetype)tinyPdfInstance -{ - static FakeAssetLoader *instance = nil; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - instance = [FakeAssetLoader fakeRandomAssetLoaderWithLength:256 mimeType:@"application/pdf"]; - }); - return instance; -} - -+ (instancetype)largePdfInstance -{ - static FakeAssetLoader *instance = nil; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - instance = [FakeAssetLoader fakeRandomAssetLoaderWithLength:256 mimeType:@"application/pdf"]; - }); - return instance; -} - -+ (instancetype)missingPngInstance -{ - static FakeAssetLoader *instance = nil; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - instance = [FakeAssetLoader fakeMissingAssetLoaderWithMimeType:OWSMimeTypeImagePng]; - }); - return instance; -} - -+ (instancetype)missingPdfInstance -{ - static FakeAssetLoader *instance = nil; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - instance = [FakeAssetLoader fakeMissingAssetLoaderWithMimeType:@"application/pdf"]; - }); - return instance; -} - -+ (instancetype)oversizeTextInstance -{ - static FakeAssetLoader *instance = nil; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - instance = [FakeAssetLoader fakeOversizeTextAssetLoader]; - }); - return instance; -} - -@end - -#pragma mark - - -@class DebugUIMessagesSingleAction; - -@interface DebugUIMessagesAction : NSObject - -@property (nonatomic) NSString *label; - -@end - -#pragma mark - - -@interface DebugUIMessagesSingleAction : DebugUIMessagesAction - -@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 : DebugUIMessagesAction - -// "Group" actions should have these properties. -@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:^{ - }]; -} - -// Given a group of subactions, perform a single random subaction each time. -+ (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; -} - -// Given a group of subactions, perform all of the subactions each time. -+ (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 - -#pragma mark - - @interface TSOutgoingMessage (PostDatingDebug) - (void)setReceivedAtTimestamp:(uint64_t)value; @@ -1264,32 +501,32 @@ typedef NS_ENUM(NSUInteger, SubactionMode) { { OWSAssert(thread); - return [self sendMediaAction:@"Send Jpeg" fakeAssetLoader:[FakeAssetLoader jpegInstance] thread:thread]; + return [self sendMediaAction:@"Send Jpeg" fakeAssetLoader:[DebugUIMessagesAssetLoader jpegInstance] thread:thread]; } + (DebugUIMessagesAction *)sendGifAction:(TSThread *)thread { OWSAssert(thread); - return [self sendMediaAction:@"Send Gif" fakeAssetLoader:[FakeAssetLoader gifInstance] thread:thread]; + return [self sendMediaAction:@"Send Gif" fakeAssetLoader:[DebugUIMessagesAssetLoader gifInstance] thread:thread]; } + (DebugUIMessagesAction *)sendMp3Action:(TSThread *)thread { OWSAssert(thread); - return [self sendMediaAction:@"Send Mp3" fakeAssetLoader:[FakeAssetLoader mp3Instance] thread:thread]; + return [self sendMediaAction:@"Send Mp3" fakeAssetLoader:[DebugUIMessagesAssetLoader mp3Instance] thread:thread]; } + (DebugUIMessagesAction *)sendMp4Action:(TSThread *)thread { OWSAssert(thread); - return [self sendMediaAction:@"Send Mp4" fakeAssetLoader:[FakeAssetLoader mp4Instance] thread:thread]; + return [self sendMediaAction:@"Send Mp4" fakeAssetLoader:[DebugUIMessagesAssetLoader mp4Instance] thread:thread]; } + (DebugUIMessagesAction *)sendMediaAction:(NSString *)label - fakeAssetLoader:(FakeAssetLoader *)fakeAssetLoader + fakeAssetLoader:(DebugUIMessagesAssetLoader *)fakeAssetLoader thread:(TSThread *)thread { OWSAssert(label.length > 0); @@ -1337,7 +574,7 @@ typedef NS_ENUM(NSUInteger, SubactionMode) { return [self fakeOutgoingMediaAction:@"Fake Outgoing Jpeg" messageState:messageState hasCaption:hasCaption - fakeAssetLoader:[FakeAssetLoader jpegInstance] + fakeAssetLoader:[DebugUIMessagesAssetLoader jpegInstance] thread:thread]; } @@ -1350,7 +587,7 @@ typedef NS_ENUM(NSUInteger, SubactionMode) { return [self fakeOutgoingMediaAction:@"Fake Outgoing Gif" messageState:messageState hasCaption:hasCaption - fakeAssetLoader:[FakeAssetLoader gifInstance] + fakeAssetLoader:[DebugUIMessagesAssetLoader gifInstance] thread:thread]; } @@ -1363,7 +600,7 @@ typedef NS_ENUM(NSUInteger, SubactionMode) { return [self fakeOutgoingMediaAction:@"Fake Outgoing Mp3" messageState:messageState hasCaption:hasCaption - fakeAssetLoader:[FakeAssetLoader mp3Instance] + fakeAssetLoader:[DebugUIMessagesAssetLoader mp3Instance] thread:thread]; } @@ -1376,7 +613,7 @@ typedef NS_ENUM(NSUInteger, SubactionMode) { return [self fakeOutgoingMediaAction:@"Fake Outgoing Mp4" messageState:messageState hasCaption:hasCaption - fakeAssetLoader:[FakeAssetLoader mp4Instance] + fakeAssetLoader:[DebugUIMessagesAssetLoader mp4Instance] thread:thread]; } @@ -1389,7 +626,7 @@ typedef NS_ENUM(NSUInteger, SubactionMode) { return [self fakeOutgoingMediaAction:@"Fake Outgoing Portrait Png" messageState:messageState hasCaption:hasCaption - fakeAssetLoader:[FakeAssetLoader landscapePngInstance] + fakeAssetLoader:[DebugUIMessagesAssetLoader landscapePngInstance] thread:thread]; } @@ -1402,7 +639,7 @@ typedef NS_ENUM(NSUInteger, SubactionMode) { return [self fakeOutgoingMediaAction:@"Fake Outgoing Landscape Png" messageState:messageState hasCaption:hasCaption - fakeAssetLoader:[FakeAssetLoader portraitPngInstance] + fakeAssetLoader:[DebugUIMessagesAssetLoader portraitPngInstance] thread:thread]; } @@ -1415,7 +652,7 @@ typedef NS_ENUM(NSUInteger, SubactionMode) { return [self fakeOutgoingMediaAction:@"Fake Outgoing Large Png" messageState:messageState hasCaption:hasCaption - fakeAssetLoader:[FakeAssetLoader largePngInstance] + fakeAssetLoader:[DebugUIMessagesAssetLoader largePngInstance] thread:thread]; } @@ -1428,7 +665,7 @@ typedef NS_ENUM(NSUInteger, SubactionMode) { return [self fakeOutgoingMediaAction:@"Fake Outgoing Tiny Pdf" messageState:messageState hasCaption:hasCaption - fakeAssetLoader:[FakeAssetLoader tinyPdfInstance] + fakeAssetLoader:[DebugUIMessagesAssetLoader tinyPdfInstance] thread:thread]; } @@ -1441,7 +678,7 @@ typedef NS_ENUM(NSUInteger, SubactionMode) { return [self fakeOutgoingMediaAction:@"Fake Outgoing Large Pdf" messageState:messageState hasCaption:hasCaption - fakeAssetLoader:[FakeAssetLoader largePdfInstance] + fakeAssetLoader:[DebugUIMessagesAssetLoader largePdfInstance] thread:thread]; } @@ -1454,7 +691,7 @@ typedef NS_ENUM(NSUInteger, SubactionMode) { return [self fakeOutgoingMediaAction:@"Fake Outgoing Missing Png" messageState:messageState hasCaption:hasCaption - fakeAssetLoader:[FakeAssetLoader missingPngInstance] + fakeAssetLoader:[DebugUIMessagesAssetLoader missingPngInstance] thread:thread]; } @@ -1467,7 +704,7 @@ typedef NS_ENUM(NSUInteger, SubactionMode) { return [self fakeOutgoingMediaAction:@"Fake Outgoing Missing Pdf" messageState:messageState hasCaption:hasCaption - fakeAssetLoader:[FakeAssetLoader missingPdfInstance] + fakeAssetLoader:[DebugUIMessagesAssetLoader missingPdfInstance] thread:thread]; } @@ -1480,14 +717,14 @@ typedef NS_ENUM(NSUInteger, SubactionMode) { return [self fakeOutgoingMediaAction:@"Fake Outgoing Oversize Text" messageState:messageState hasCaption:hasCaption - fakeAssetLoader:[FakeAssetLoader oversizeTextInstance] + fakeAssetLoader:[DebugUIMessagesAssetLoader oversizeTextInstance] thread:thread]; } + (DebugUIMessagesAction *)fakeOutgoingMediaAction:(NSString *)labelParam messageState:(TSOutgoingMessageState)messageState hasCaption:(BOOL)hasCaption - fakeAssetLoader:(FakeAssetLoader *)fakeAssetLoader + fakeAssetLoader:(DebugUIMessagesAssetLoader *)fakeAssetLoader thread:(TSThread *)thread { OWSAssert(labelParam.length > 0); @@ -1525,7 +762,7 @@ typedef NS_ENUM(NSUInteger, SubactionMode) { + (void)createFakeOutgoingMedia:(NSUInteger)index messageState:(TSOutgoingMessageState)messageState hasCaption:(BOOL)hasCaption - fakeAssetLoader:(FakeAssetLoader *)fakeAssetLoader + fakeAssetLoader:(DebugUIMessagesAssetLoader *)fakeAssetLoader thread:(TSThread *)thread transaction:(YapDatabaseReadWriteTransaction *)transaction { @@ -1600,7 +837,7 @@ typedef NS_ENUM(NSUInteger, SubactionMode) { return [self fakeIncomingMediaAction:@"Fake Incoming Jpeg" isAttachmentDownloaded:isAttachmentDownloaded hasCaption:hasCaption - fakeAssetLoader:[FakeAssetLoader jpegInstance] + fakeAssetLoader:[DebugUIMessagesAssetLoader jpegInstance] thread:thread]; } @@ -1613,7 +850,7 @@ typedef NS_ENUM(NSUInteger, SubactionMode) { return [self fakeIncomingMediaAction:@"Fake Incoming Gif" isAttachmentDownloaded:isAttachmentDownloaded hasCaption:hasCaption - fakeAssetLoader:[FakeAssetLoader gifInstance] + fakeAssetLoader:[DebugUIMessagesAssetLoader gifInstance] thread:thread]; } @@ -1626,7 +863,7 @@ typedef NS_ENUM(NSUInteger, SubactionMode) { return [self fakeIncomingMediaAction:@"Fake Incoming Mp3" isAttachmentDownloaded:isAttachmentDownloaded hasCaption:hasCaption - fakeAssetLoader:[FakeAssetLoader mp3Instance] + fakeAssetLoader:[DebugUIMessagesAssetLoader mp3Instance] thread:thread]; } @@ -1639,7 +876,7 @@ typedef NS_ENUM(NSUInteger, SubactionMode) { return [self fakeIncomingMediaAction:@"Fake Incoming Mp4" isAttachmentDownloaded:isAttachmentDownloaded hasCaption:hasCaption - fakeAssetLoader:[FakeAssetLoader mp4Instance] + fakeAssetLoader:[DebugUIMessagesAssetLoader mp4Instance] thread:thread]; } @@ -1652,7 +889,7 @@ typedef NS_ENUM(NSUInteger, SubactionMode) { return [self fakeIncomingMediaAction:@"Fake Incoming Portrait Png" isAttachmentDownloaded:isAttachmentDownloaded hasCaption:hasCaption - fakeAssetLoader:[FakeAssetLoader portraitPngInstance] + fakeAssetLoader:[DebugUIMessagesAssetLoader portraitPngInstance] thread:thread]; } @@ -1665,7 +902,7 @@ typedef NS_ENUM(NSUInteger, SubactionMode) { return [self fakeIncomingMediaAction:@"Fake Incoming Landscape Png" isAttachmentDownloaded:isAttachmentDownloaded hasCaption:hasCaption - fakeAssetLoader:[FakeAssetLoader landscapePngInstance] + fakeAssetLoader:[DebugUIMessagesAssetLoader landscapePngInstance] thread:thread]; } @@ -1678,7 +915,7 @@ typedef NS_ENUM(NSUInteger, SubactionMode) { return [self fakeIncomingMediaAction:@"Fake Incoming Large Png" isAttachmentDownloaded:isAttachmentDownloaded hasCaption:hasCaption - fakeAssetLoader:[FakeAssetLoader largePngInstance] + fakeAssetLoader:[DebugUIMessagesAssetLoader largePngInstance] thread:thread]; } @@ -1691,7 +928,7 @@ typedef NS_ENUM(NSUInteger, SubactionMode) { return [self fakeIncomingMediaAction:@"Fake Incoming Tiny Pdf" isAttachmentDownloaded:isAttachmentDownloaded hasCaption:hasCaption - fakeAssetLoader:[FakeAssetLoader tinyPdfInstance] + fakeAssetLoader:[DebugUIMessagesAssetLoader tinyPdfInstance] thread:thread]; } @@ -1704,7 +941,7 @@ typedef NS_ENUM(NSUInteger, SubactionMode) { return [self fakeIncomingMediaAction:@"Fake Incoming Large Pdf" isAttachmentDownloaded:isAttachmentDownloaded hasCaption:hasCaption - fakeAssetLoader:[FakeAssetLoader largePdfInstance] + fakeAssetLoader:[DebugUIMessagesAssetLoader largePdfInstance] thread:thread]; } @@ -1717,7 +954,7 @@ typedef NS_ENUM(NSUInteger, SubactionMode) { return [self fakeIncomingMediaAction:@"Fake Incoming Missing Png" isAttachmentDownloaded:isAttachmentDownloaded hasCaption:hasCaption - fakeAssetLoader:[FakeAssetLoader missingPngInstance] + fakeAssetLoader:[DebugUIMessagesAssetLoader missingPngInstance] thread:thread]; } @@ -1730,7 +967,7 @@ typedef NS_ENUM(NSUInteger, SubactionMode) { return [self fakeIncomingMediaAction:@"Fake Incoming Missing Pdf" isAttachmentDownloaded:isAttachmentDownloaded hasCaption:hasCaption - fakeAssetLoader:[FakeAssetLoader missingPdfInstance] + fakeAssetLoader:[DebugUIMessagesAssetLoader missingPdfInstance] thread:thread]; } @@ -1743,14 +980,14 @@ typedef NS_ENUM(NSUInteger, SubactionMode) { return [self fakeIncomingMediaAction:@"Fake Incoming Oversize Text" isAttachmentDownloaded:isAttachmentDownloaded hasCaption:hasCaption - fakeAssetLoader:[FakeAssetLoader oversizeTextInstance] + fakeAssetLoader:[DebugUIMessagesAssetLoader oversizeTextInstance] thread:thread]; } + (DebugUIMessagesAction *)fakeIncomingMediaAction:(NSString *)labelParam isAttachmentDownloaded:(BOOL)isAttachmentDownloaded hasCaption:(BOOL)hasCaption - fakeAssetLoader:(FakeAssetLoader *)fakeAssetLoader + fakeAssetLoader:(DebugUIMessagesAssetLoader *)fakeAssetLoader thread:(TSThread *)thread { OWSAssert(labelParam.length > 0); @@ -1783,7 +1020,7 @@ typedef NS_ENUM(NSUInteger, SubactionMode) { + (void)createFakeIncomingMedia:(NSUInteger)index isAttachmentDownloaded:(BOOL)isAttachmentDownloaded hasCaption:(BOOL)hasCaption - fakeAssetLoader:(FakeAssetLoader *)fakeAssetLoader + fakeAssetLoader:(DebugUIMessagesAssetLoader *)fakeAssetLoader thread:(TSThread *)thread transaction:(YapDatabaseReadWriteTransaction *)transaction { 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..a16ca58e95 --- /dev/null +++ b/Signal/src/ViewControllers/DebugUI/DebugUIMessagesAssetLoader.h @@ -0,0 +1,35 @@ +// +// 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)mp3Instance; ++ (instancetype)mp4Instance; ++ (instancetype)portraitPngInstance; ++ (instancetype)landscapePngInstance; ++ (instancetype)largePngInstance; ++ (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..03ce21a75f --- /dev/null +++ b/Signal/src/ViewControllers/DebugUI/DebugUIMessagesAssetLoader.m @@ -0,0 +1,454 @@ +// +// 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; + } + + 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); + + 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; + } + + 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)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/mpeg"]; + }); + 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)portraitPngInstance +{ + static DebugUIMessagesAssetLoader *instance = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + instance = [DebugUIMessagesAssetLoader fakePngAssetLoaderWithImageSize:CGSizeMake(10, 100) + backgroundColor:[UIColor blueColor] + textColor:[UIColor whiteColor] + label:@"P"]; + }); + return instance; +} + ++ (instancetype)landscapePngInstance +{ + static DebugUIMessagesAssetLoader *instance = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + instance = [DebugUIMessagesAssetLoader fakePngAssetLoaderWithImageSize:CGSizeMake(100, 10) + backgroundColor:[UIColor greenColor] + 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 redColor] + textColor:[UIColor whiteColor] + label:@"B"]; + }); + return instance; +} + ++ (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:256 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 From 3a5ba15d21333394274e8d7613a490819b38a275 Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Mon, 26 Mar 2018 10:54:43 -0400 Subject: [PATCH 11/31] Elaborate debug UI for messages. --- .../AppSettings/AppSettingsViewController.m | 4 -- .../ConversationViewController.m | 4 -- .../ViewControllers/DebugUI/DebugUIMessages.m | 38 ------------------- .../src/ViewControllers/HomeViewController.m | 10 ----- 4 files changed, 56 deletions(-) diff --git a/Signal/src/ViewControllers/AppSettings/AppSettingsViewController.m b/Signal/src/ViewControllers/AppSettings/AppSettingsViewController.m index 494217f087..97b37ab0ff 100644 --- a/Signal/src/ViewControllers/AppSettings/AppSettingsViewController.m +++ b/Signal/src/ViewControllers/AppSettings/AppSettingsViewController.m @@ -91,10 +91,6 @@ self.title = NSLocalizedString(@"SETTINGS_NAV_BAR_TITLE", @"Title for settings activity"); [self updateTableContents]; - - dispatch_async(dispatch_get_main_queue(), ^{ - [self showDebugUI]; - }); } - (void)viewWillAppear:(BOOL)animated diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m index da0e1bff89..241a1c1ca3 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m @@ -490,10 +490,6 @@ typedef enum : NSUInteger { [self createBackButton]; [self addNotificationListeners]; [self loadDraftInCompose]; - - dispatch_async(dispatch_get_main_queue(), ^{ - [DebugUITableViewController presentDebugUIForThread:self.thread fromViewController:self]; - }); } - (void)loadView diff --git a/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m b/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m index 6302c9665e..9e054de877 100644 --- a/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m +++ b/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m @@ -357,44 +357,6 @@ 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 success:(nullable void (^)(void))success diff --git a/Signal/src/ViewControllers/HomeViewController.m b/Signal/src/ViewControllers/HomeViewController.m index 4aef807335..dd48ef95ef 100644 --- a/Signal/src/ViewControllers/HomeViewController.m +++ b/Signal/src/ViewControllers/HomeViewController.m @@ -284,16 +284,6 @@ typedef NS_ENUM(NSInteger, CellState) { kArchiveState, kInboxState }; } [self updateBarButtonItems]; - - dispatch_async(dispatch_get_main_queue(), ^{ - TSThread *_Nullable thread = [self threadForIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]]; - if (!thread) { - return; - } - [self presentThread:thread keyboardOnViewAppearing:NO callOnViewAppearing:NO]; - - // [self settingsButtonPressed:nil]; - }); } - (void)viewDidAppear:(BOOL)animated From d0cddfd22c9d203026c755f51fd7fe556e95bdd7 Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Mon, 26 Mar 2018 10:55:03 -0400 Subject: [PATCH 12/31] Elaborate debug UI for messages. --- .../ViewControllers/DebugUI/DebugUIMessages.m | 29 ------------------- 1 file changed, 29 deletions(-) diff --git a/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m b/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m index 9e054de877..bae44a2970 100644 --- a/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m +++ b/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m @@ -417,35 +417,6 @@ NS_ASSUME_NONNULL_BEGIN #pragma mark - Send Media -//+ (void)sendSelectedMediaTypeInThread:(TSThread *)thread -//{ -// OWSAssertIsOnMainThread() OWSAssert(thread); -// -// NSArray *actions = @[ -// [self sendJpegAction:thread], -// [self sendGifAction:thread], -// [self sendMp3Action:thread], -// [self sendMp4Action:thread], -// [self sendRandomMediaAction:thread], -// ]; -// -// UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"Select Media Type" -// 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]; -//} - + (NSArray *)allSendMediaActions:(TSThread *)thread { OWSAssert(thread); From 75177ef00fb556ad57a38b141a1a4f5e7f9ed372 Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Fri, 17 Nov 2017 10:49:34 -0500 Subject: [PATCH 13/31] Bubble collapse. --- .../ConversationView/Cells/OWSMessageCell.m | 1867 +++++++++++++---- .../ConversationView/ConversationViewItem.m | 34 +- .../src/ViewControllers/HomeViewController.m | 5 + .../utils/OWSMessagesBubbleImageFactory.swift | 18 + .../Messages/Attachments/TSAttachmentStream.m | 28 +- 5 files changed, 1542 insertions(+), 410 deletions(-) diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m index 14469c223a..aca0422cb6 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m @@ -14,80 +14,484 @@ #import "UIColor+OWS.h" #import #import +#import NS_ASSUME_NONNULL_BEGIN +// TODO: Choose best thorn. +// TODO: Review all comments. + +CG_INLINE CGSize CGSizeCeil(CGSize size) +{ + return CGSizeMake((CGFloat)ceil(size.width), (CGFloat)ceil(size.height)); +} + // This approximates the curve of our message bubbles, which makes the animation feel a little smoother. const CGFloat OWSMessageCellCornerRadius = 17; -@interface BubbleMaskingView : UIView +// TODO: We could make the bubble shape respond to dynamic text. +static const CGFloat kBubbleVRounding = 8.5f; +static const CGFloat kBubbleHRounding = 10.f; +//static const CGFloat kBubbleThornSideInset = 3.f; +//static const CGFloat kBubbleThornVInset = 3.f; +//static const CGFloat kBubbleThornSideInset = 6.f; +//static const CGFloat kBubbleThornVInset = 0.f; +static const CGFloat kBubbleThornSideInset = kBubbleHRounding * 0.3f; +static const CGFloat kBubbleThornVInset = kBubbleVRounding * 0.3f; +static const CGFloat kBubbleTextHInset = 6.f; +static const CGFloat kBubbleTextVInset = 6.f; + +@interface OWSBubbleView : UIView @property (nonatomic) BOOL isOutgoing; @property (nonatomic) BOOL hideTail; -@property (nonatomic, nullable, weak) UIView *maskedSubview; +//@property (nonatomic, nullable, weak) UIView *maskedSubview; +@property (nonatomic) CAShapeLayer *maskLayer; + +//@property (nonatomic) BOOL isOutgoing; +@property (nonatomic) CAShapeLayer *shapeLayer; +@property (nonatomic) UIColor *bubbleColor; @end #pragma mark - -@implementation BubbleMaskingView +@implementation OWSBubbleView + +- (void)setIsOutgoing:(BOOL)isOutgoing { + BOOL didChange = _isOutgoing != isOutgoing; + + _isOutgoing = isOutgoing; + + if (didChange || !self.shapeLayer) { + [self updateMask]; + } +} - (void)setFrame:(CGRect)frame { - BOOL didSizeChange = !CGSizeEqualToSize(self.frame.size, frame.size); + BOOL didChange = !CGSizeEqualToSize(self.frame.size, frame.size); [super setFrame:frame]; - if (didSizeChange) { + if (didChange || !self.shapeLayer) { [self updateMask]; } } - (void)setBounds:(CGRect)bounds { - BOOL didSizeChange = !CGSizeEqualToSize(self.bounds.size, bounds.size); + BOOL didChange = !CGSizeEqualToSize(self.bounds.size, bounds.size); [super setBounds:bounds]; - if (didSizeChange) { + if (didChange || !self.shapeLayer) { [self updateMask]; } } -- (void)updateMask +- (void)setBubbleColor:(UIColor *)bubbleColor { - 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; + _bubbleColor = bubbleColor; - // 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]; + if (!self.shapeLayer) { + [self updateMask]; } + self.shapeLayer.fillColor = bubbleColor.CGColor; } +- (void)updateMask +{ + if (!self.shapeLayer) { + self.shapeLayer = [CAShapeLayer new]; + [self.layer addSublayer:self.shapeLayer]; + } + if (!self.maskLayer) { + self.maskLayer = [CAShapeLayer new]; + self.layer.mask = self.maskLayer; + } + + UIBezierPath *bezierPath = [self.class maskPathForSize:self.bounds.size + isOutgoing:self.isOutgoing + isRTL:self.isRTL]; + + self.shapeLayer.fillColor = self.bubbleColor.CGColor; + self.shapeLayer.path = bezierPath.CGPath; + + self.maskLayer.path = bezierPath.CGPath; +} + +//- (void)updateMask +//{ +// UIView *_Nullable maskedSubview = self.maskedSubview; +// if (!maskedSubview) { +// return; +// } +// maskedSubview.frame = self.bounds; +// //<<<<<<< HEAD +// // // 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]; +// // } +// //||||||| merged common ancestors +// // // The JSQ masks are not RTL-safe, so we need to invert the +// // // mask orientation manually. +// // BOOL hasOutgoingMask = self.isOutgoing ^ self.isRTL; +// // [JSQMessagesMediaViewBubbleImageMasker applyBubbleImageMaskToMediaView:maskedSubview +// isOutgoing:hasOutgoingMask]; +// //======= +// +// +// UIBezierPath *bezierPath = [BubbleFillView maskPathForSize:self.bounds.size +// isOutgoing:self.isOutgoing +// isRTL:self.isRTL]; +// self.maskLayer.path = bezierPath.CGPath; +// maskedSubview.layer.mask = self.maskLayer; +// //>>>>>>> SQUASHED +//} + +//- (void)setIsOutgoing:(BOOL)isOutgoing { +// if (_isOutgoing == isOutgoing) { +// return; +// } +// _isOutgoing = isOutgoing; +// [self updateMask]; +//} +// +//- (void)setFrame:(CGRect)frame +//{ +// BOOL didSizeChange = !CGSizeEqualToSize(self.frame.size, frame.size); +// +// [super setFrame:frame]; +// +// if (didSizeChange || !self.shapeLayer) { +// [self updateMask]; +// } +//} +// +//- (void)setBounds:(CGRect)bounds +//{ +// BOOL didSizeChange = !CGSizeEqualToSize(self.bounds.size, bounds.size); +// +// [super setBounds:bounds]; +// +// if (didSizeChange || !self.shapeLayer) { +// [self updateMask]; +// } +//} +// +//- (void)updateMask +//{ +// if (!self.shapeLayer) { +// self.shapeLayer = [CAShapeLayer new]; +// [self.layer addSublayer:self.shapeLayer]; +// } +// +// UIBezierPath *bezierPath = [self.class maskPathForSize:self.bounds.size +// isOutgoing:self.isOutgoing +// isRTL:self.isRTL]; +// +// self.shapeLayer.fillColor = self.bubbleColor.CGColor; +// self.shapeLayer.path = bezierPath.CGPath; +//} + ++ (UIBezierPath *)maskPathForSize:(CGSize)size + isOutgoing:(BOOL)isOutgoing + 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)]; + + // Thorn Tip + CGPoint thornTip = CGPointMake(size.width, + size.height); + CGPoint thornA = CGPointMake(bubbleRight - kBubbleHRounding, bubbleBottom); + CGPoint thornB = CGPointMake(bubbleRight, bubbleBottom - kBubbleVRounding); + [bezierPath moveToPoint:thornTip]; + // [bezierPath addLineToPoint:CGPointMake(bubbleRight - kBubbleHRounding * 0.85f, bubbleBottom)]; + // [bezierPath addLineToPoint:thornA]; + // [bezierPath addLineToPoint:thornB]; + // [bezierPath addLineToPoint:CGPointMake(bubbleRight, bubbleBottom - kBubbleVRounding * 0.5f)]; + [bezierPath addQuadCurveToPoint:CGPointMake(bubbleRight - kBubbleHRounding * 0.8f, bubbleBottom) + controlPoint:CGPointMake(bubbleRight - kBubbleHRounding * 0.4f, bubbleBottom)]; + [bezierPath addLineToPoint:thornA]; + [bezierPath addLineToPoint:thornB]; + [bezierPath addLineToPoint:CGPointMake(bubbleRight, bubbleBottom - kBubbleVRounding * 0.7f)]; + [bezierPath addQuadCurveToPoint:thornTip + controlPoint:CGPointMake(bubbleRight, bubbleBottom - kBubbleVRounding * 0.3f)]; + + // // Thorn Tip + // [bezierPath moveToPoint:CGPointMake(bubbleRight - kBubbleHRounding, bubbleBottom)]; + // [bezierPath addLineToPoint:CGPointMake(bubbleRight, bubbleBottom - kBubbleVRounding)]; + // [bezierPath addQuadCurveToPoint:CGPointMake(bubbleRight + kBubbleThornSideInset, bubbleBottom - 0.f) + // controlPoint:CGPointMake(bubbleRight, bubbleBottom)]; + // // [bezierPath addQuadCurveToPoint:CGPointMake(bubbleRight + kBubbleThornSideInset - 1.f, bubbleBottom - + // 0.5f) + // // controlPoint:CGPointMake(bubbleRight, bubbleBottom)]; + // // [bezierPath addQuadCurveToPoint:CGPointMake(bubbleRight + kBubbleThornSideInset, bubbleBottom) + // // controlPoint:CGPointMake(bubbleRight + kBubbleThornSideInset, bubbleBottom - 0.5f)]; + // [bezierPath addLineToPoint:CGPointMake(bubbleRight + kBubbleThornSideInset, bubbleBottom)]; + + // // Thorn Tip + // CGFloat kThornPinchingA = 0.f; + // CGFloat kThornPinchingB = 3.5f; + // CGPoint thornTip = CGPointMake(self.width, + // self.height); + // CGPoint thornA = CGPointMake(bubbleRight - kBubbleHRounding, bubbleBottom - kThornPinchingA); + // CGPoint thornB = CGPointMake(bubbleRight - kThornPinchingB, bubbleBottom - kBubbleVRounding); + // [bezierPath moveToPoint:thornTip]; + // [bezierPath addQuadCurveToPoint:thornA + // controlPoint:CGPointMake(bubbleRight - kBubbleHRounding, bubbleBottom - kThornPinchingA)]; + // [bezierPath addLineToPoint:thornB]; + // [bezierPath addQuadCurveToPoint:thornTip + // controlPoint:CGPointMake(bubbleRight - kThornPinchingB, bubbleBottom - kBubbleVRounding * 0.1f)]; + + // Thorn Tip + // CGFloat kThornPinchingA = 0.f; + // CGFloat kThornPinchingB = 3.5f; + // CGPoint thornA = CGPointMake(bubbleRight, bubbleBottom - kBubbleVRounding * 1.65f); + // CGPoint thornB = CGPointMake(bubbleRight, bubbleBottom - kBubbleVRounding * 1.f); + + // CGPoint thornA = CGPointMake(bubbleRight, bubbleTop + kBubbleVRounding * 1.f); + // CGPoint thornB = CGPointMake(bubbleRight, bubbleTop + kBubbleVRounding * 1.65f); + // CGPoint thornTip = CGPointMake(bubbleRight + kBubbleThornSideInset * 0.85f, + // (thornA.y + thornB.y) * 0.5f); + // [bezierPath moveToPoint:thornTip]; + // [bezierPath addLineToPoint:thornA]; + // [bezierPath addLineToPoint:thornB]; + + // [bezierPath addQuadCurveToPoint:thornA + // controlPoint:CGPointMake(bubbleRight - kBubbleHRounding, bubbleBottom - kThornPinchingA)]; + // [bezierPath addLineToPoint:thornB]; + // [bezierPath addQuadCurveToPoint:thornTip + // controlPoint:CGPointMake(bubbleRight - kThornPinchingB, bubbleBottom - kBubbleVRounding * 0.1f)]; + + // 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; +} + +//- (void)setBubbleColor:(UIColor *)bubbleColor { +// _bubbleColor = bubbleColor; +// +// self.shapeLayer.fillColor = bubbleColor.CGColor; +//} + @end #pragma mark - +//@interface BubbleFillView : UIView +// +////@property (nonatomic) BOOL isOutgoing; +////@property (nonatomic) CAShapeLayer *shapeLayer; +////@property (nonatomic) UIColor *bubbleColor; +// +//@end +// +//#pragma mark - +// +//@implementation BubbleFillView +// +////- (void)setIsOutgoing:(BOOL)isOutgoing { +//// if (_isOutgoing == isOutgoing) { +//// return; +//// } +//// _isOutgoing = isOutgoing; +//// [self updateMask]; +////} +//// +////- (void)setFrame:(CGRect)frame +////{ +//// BOOL didSizeChange = !CGSizeEqualToSize(self.frame.size, frame.size); +//// +//// [super setFrame:frame]; +//// +//// if (didSizeChange || !self.shapeLayer) { +//// [self updateMask]; +//// } +////} +//// +////- (void)setBounds:(CGRect)bounds +////{ +//// BOOL didSizeChange = !CGSizeEqualToSize(self.bounds.size, bounds.size); +//// +//// [super setBounds:bounds]; +//// +//// if (didSizeChange || !self.shapeLayer) { +//// [self updateMask]; +//// } +////} +//// +////- (void)updateMask +////{ +//// if (!self.shapeLayer) { +//// self.shapeLayer = [CAShapeLayer new]; +//// [self.layer addSublayer:self.shapeLayer]; +//// } +//// +//// UIBezierPath *bezierPath = [self.class maskPathForSize:self.bounds.size +//// isOutgoing:self.isOutgoing +//// isRTL:self.isRTL]; +//// +//// self.shapeLayer.fillColor = self.bubbleColor.CGColor; +//// self.shapeLayer.path = bezierPath.CGPath; +////} +// +////- (void)setBubbleColor:(UIColor *)bubbleColor { +//// _bubbleColor = bubbleColor; +//// +//// self.shapeLayer.fillColor = bubbleColor.CGColor; +////} +// +//@end +// +//#pragma mark - +// +//@interface BubbleMaskingView : UIView +// +//@property (nonatomic) BOOL isOutgoing; +//@property (nonatomic) BOOL hideTail; +//@property (nonatomic, nullable, weak) UIView *maskedSubview; +//@property (nonatomic) CAShapeLayer *maskLayer; +// +//@end +// +//#pragma mark - +// +//@implementation BubbleMaskingView +// +//- (void)setMaskedSubview:(UIView * _Nullable)maskedSubview { +// if (_maskedSubview == maskedSubview) { +// return; +// } +// _maskedSubview = maskedSubview; +// [self updateMask]; +//} +// +//- (void)setIsOutgoing:(BOOL)isOutgoing { +// if (_isOutgoing == isOutgoing) { +// return; +// } +// _isOutgoing = isOutgoing; +// [self updateMask]; +//} +// +//- (void)setFrame:(CGRect)frame +//{ +// BOOL didSizeChange = !CGSizeEqualToSize(self.frame.size, frame.size); +// +// [super setFrame:frame]; +// +// if (didSizeChange) { +// [self updateMask]; +// } +//} +// +//- (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; +// //<<<<<<< HEAD +// // // 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]; +// // } +// //||||||| merged common ancestors +// // // The JSQ masks are not RTL-safe, so we need to invert the +// // // mask orientation manually. +// // BOOL hasOutgoingMask = self.isOutgoing ^ self.isRTL; +// // [JSQMessagesMediaViewBubbleImageMasker applyBubbleImageMaskToMediaView:maskedSubview +// isOutgoing:hasOutgoingMask]; +// //======= +// +// if (!self.maskLayer) { +// self.maskLayer = [CAShapeLayer new]; +// } +// +// UIBezierPath *bezierPath = [OWSBubbleView maskPathForSize:self.bounds.size +// isOutgoing:self.isOutgoing +// isRTL:self.isRTL]; +// self.maskLayer.path = bezierPath.CGPath; +// maskedSubview.layer.mask = self.maskLayer; +// //>>>>>>> SQUASHED +//} +// +//@end + +#pragma mark - + @interface OWSMessageTextView : UITextView @property (nonatomic) BOOL shouldIgnoreEvents; @@ -159,29 +563,62 @@ const CGFloat OWSMessageCellCornerRadius = 17; // 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 contentView contains: +// +// * MessageView (message) +// * dateHeaderLabel (above message) +// * footerView (below message) +// * failedSendBadgeView (beside message) + +@property (nonatomic) OWSBubbleView *bubbleView; + +//@property (nonatomic) UIView *messageWrapperView; + + +//@property (nonatomic) UIView *payloadView; +//@property (nonatomic) BubbleMaskingView *bubbleView; +////@property (nonatomic) BubbleMaskingView *mediaMaskingView; @property (nonatomic) UILabel *dateHeaderLabel; -@property (nonatomic) OWSMessageTextView *textView; +@property (nonatomic) OWSMessageTextView *bodyTextViewCached; @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, nullable) UILabel *tapForMoreLabel; +////<<<<<<< HEAD +////@property (nonatomic, nullable) UIImageView *textBubbleImageView; +////||||||| merged common ancestors +////@property (nonatomic, nullable) UIImageView *bubbleImageView; +////======= +////@property (nonatomic, nullable) BubbleFillView *bubbleFillView; +//////@property (nonatomic, nullable) UIImageView *bubbleImageView; +////>>>>>>> SQUASHED +//@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) UIImageView *lastImageView; + +// 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; + +// TODO: Review +// TODO: Rename to cellcont +@property (nonatomic, nullable) NSArray *cellContentConstraints; @property (nonatomic, nullable) NSArray *dateHeaderConstraints; -@property (nonatomic, nullable) NSMutableArray *contentConstraints; +@property (nonatomic, nullable) NSMutableArray *bubbleContentConstraints; @property (nonatomic, nullable) NSArray *footerConstraints; @property (nonatomic) BOOL isPresentingMenuController; + @end @implementation OWSMessageCell @@ -198,20 +635,27 @@ const CGFloat OWSMessageCellCornerRadius = 17; - (void)commontInit { - OWSAssert(!self.textView); + OWSAssert(!self.bodyTextViewCached); - _contentConstraints = [NSMutableArray new]; + _bubbleContentConstraints = [NSMutableArray new]; self.layoutMargins = UIEdgeInsetsZero; self.contentView.layoutMargins = UIEdgeInsetsZero; - self.payloadView = [UIView new]; - self.payloadView.layoutMargins = UIEdgeInsetsZero; - [self.contentView addSubview:self.payloadView]; + self.bubbleView = [OWSBubbleView new]; + self.bubbleView.layoutMargins = UIEdgeInsetsZero; + [self.contentView addSubview:self.bubbleView]; - self.mediaMaskingView = [BubbleMaskingView new]; - self.mediaMaskingView.layoutMargins = UIEdgeInsetsZero; - [self.payloadView addSubview:self.mediaMaskingView]; + // self.messageWrapperView = [UIView new]; + // self.messageWrapperView.layoutMargins = UIEdgeInsetsZero; + // [self.messageWrapperView addSubview:self.payloadView]; + // 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.footerView = [UIView containerView]; [self.contentView addSubview:self.footerView]; @@ -222,25 +666,50 @@ 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); + //<<<<<<< HEAD + // 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]; + // + //||||||| merged common ancestors + // self.bubbleImageView = [UIImageView new]; + // self.bubbleImageView.layoutMargins = UIEdgeInsetsZero; + // // Enable userInteractionEnabled so that links in textView work. + // self.bubbleImageView.userInteractionEnabled = YES; + // [self.payloadView addSubview:self.bubbleImageView]; + // [self.bubbleImageView autoPinToSuperviewEdges]; + // + //======= + // self.bubbleFillView = [BubbleFillView new]; + // self.bubbleFillView.layoutMargins = UIEdgeInsetsZero; + // // TODO: + //// // Enable userInteractionEnabled so that links in textView work. + // self.bubbleFillView.userInteractionEnabled = YES; + // [self.payloadView addSubview:self.bubbleFillView]; + // [self.bubbleFillView autoPinToSuperviewEdges]; + // + //// self.bubbleImageView = [UIImageView new]; + //// self.bubbleImageView.layoutMargins = UIEdgeInsetsZero; + //// // Enable userInteractionEnabled so that links in textView work. + //// self.bubbleImageView.userInteractionEnabled = YES; + //// [self.payloadView addSubview:self.bubbleImageView]; + //// [self.bubbleImageView autoPinToSuperviewEdges]; + // + //>>>>>>> SQUASHED + self.bodyTextViewCached = [self newTextView]; + //<<<<<<< HEAD + // + // [self.textBubbleImageView addSubview:self.textView]; + // + //||||||| merged common ancestors + // [self.bubbleImageView addSubview:self.textView]; + //======= + // [self.bubbleFillView addSubview:self.textView]; + //>>>>>>> SQUASHED + // OWSAssert(self.textView.superview); self.footerLabel = [UILabel new]; self.footerLabel.font = [UIFont ows_regularFontWithSize:12.f]; @@ -248,43 +717,67 @@ const CGFloat OWSMessageCellCornerRadius = 17; [self.footerView addSubview:self.footerLabel]; // Hide these views by default. - self.textBubbleImageView.hidden = YES; - self.textView.hidden = YES; + //<<<<<<< HEAD + // self.textBubbleImageView.hidden = YES; + //||||||| merged common ancestors + // self.bubbleImageView.hidden = YES; + //======= + // self.bubbleFillView.hidden = YES; + //// self.bubbleImageView.hidden = YES; + //>>>>>>> SQUASHED + self.bodyTextViewCached.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.bubbleView autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:self.dateHeaderLabel]; + [self.footerView autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:self.bubbleView]; - [self.mediaMaskingView autoPinEdgeToSuperviewEdge:ALEdgeTop]; + // [self.payloadView autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:self.dateHeaderLabel]; + // [self.footerView autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:self.bubbleView]; - [self.textBubbleImageView autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:self.mediaMaskingView]; - [self.textBubbleImageView autoPinEdgeToSuperviewEdge:ALEdgeBottom]; + // [self.mediaMaskingView autoPinEdgeToSuperviewEdge:ALEdgeTop]; + // + // [self.textBubbleImageView autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:self.mediaMaskingView]; + // [self.textBubbleImageView autoPinEdgeToSuperviewEdge:ALEdgeBottom]; [self.footerView autoPinEdgeToSuperviewEdge:ALEdgeBottom]; [self.footerView autoPinWidthToSuperview]; - UITapGestureRecognizer *mediaTap = - [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleMediaTapGesture:)]; - [self.mediaMaskingView addGestureRecognizer:mediaTap]; + // UITapGestureRecognizer *mediaTap = + // [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleMediaTapGesture:)]; + // [self.mediaMaskingView addGestureRecognizer:mediaTap]; + // + // UITapGestureRecognizer *textTap = + // [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTextTapGesture:)]; + // [self.textBubbleImageView addGestureRecognizer:textTap]; + // + // UILongPressGestureRecognizer *mediaLongPress = + // [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleMediaLongPressGesture:)]; + // [self.mediaMaskingView addGestureRecognizer:mediaLongPress]; + // + // 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:)]; + // [self addGestureRecognizer:panGesture]; +} - UITapGestureRecognizer *textTap = - [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTextTapGesture:)]; - [self.textBubbleImageView addGestureRecognizer:textTap]; - - UILongPressGestureRecognizer *mediaLongPress = - [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleMediaLongPressGesture:)]; - [self.mediaMaskingView addGestureRecognizer:mediaLongPress]; - - 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:)]; - [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.scrollEnabled = NO; + return textView; } + (NSString *)cellReuseIdentifier @@ -400,6 +893,43 @@ 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.hasText; + } +} + +- (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.hasText; + } +} + +#pragma mark - Load + - (void)loadForDisplay { OWSAssert(self.viewItem); @@ -413,77 +943,262 @@ 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.cellContentConstraints = @[ + [self.bubbleView autoPinLeadingToSuperview], + [self.failedSendBadgeView autoPinLeadingToTrailingOfView:self.bubbleView], + [self.failedSendBadgeView autoAlignAxis:ALAxisHorizontal toSameAxisOfView:self.bubbleView], + // [self.payloadView autoPinLeadingToSuperview], + // [self.failedSendBadgeView autoPinLeadingToTrailingOfView:self.payloadView], [self.failedSendBadgeView autoPinTrailingToSuperview], - [self.failedSendBadgeView autoAlignAxis:ALAxisHorizontal toSameAxisOfView:self.payloadView], + // [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.cellContentConstraints = @[ + [self.bubbleView autoPinLeadingToSuperview], + [self.bubbleView autoPinTrailingToSuperview], + // [self.bubbleView autoPinLeadingToSuperview], + // [self.bubbleView autoPinWidthToSuperview], + ]; + // self.cellContentConstraints = [self.payloadView autoPinWidthToSuperview]; } - JSQMessagesBubbleImage *_Nullable bubbleImageData; - if ([self.viewItem.interaction isKindOfClass:[TSMessage class]]) { + // JSQMessagesBubbleImage *_Nullable bubbleImageData; + if ([self.viewItem.interaction isKindOfClass:[TSMessage class]] && self.hasNonImageBodyContent) { TSMessage *message = (TSMessage *)self.viewItem.interaction; - bubbleImageData = [self.bubbleFactory bubbleWithMessage:message]; + // bubbleImageData = [self.bubbleFactory bubbleWithMessage:message]; + // TODO: + self.bubbleView.bubbleColor = [self.bubbleFactory bubbleColorWithMessage:message]; + // self.bubbleFillView.bubbleColor = [self.bubbleFactory bubbleColorWithMessage:message]; + } else { + OWSFail(@"%@ Unknown interaction type: %@", self.logTag, self.viewItem.interaction.class); } - self.textBubbleImageView.image = bubbleImageData.messageBubbleImage; + //<<<<<<< HEAD + // self.textBubbleImageView.image = bubbleImageData.messageBubbleImage; + //||||||| merged common ancestors + // self.bubbleImageView.image = bubbleImageData.messageBubbleImage; + //======= + // // TODO: + //// self.bubbleImageView.image = bubbleImageData.messageBubbleImage; + // self.bubbleFillView.isOutgoing = self.isOutgoing; + //>>>>>>> SQUASHED [self updateDateHeader]; [self updateFooter]; + // NSMutableArray *contentViews = [NSMutableArray new]; + // + // 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]; + // break; + // case OWSMessageCellType_AnimatedImage: + // OWSAssert(self.viewItem.attachmentStream); + // [self loadForAnimatedImageDisplay]; + // break; + // case OWSMessageCellType_Audio: + // OWSAssert(self.viewItem.attachmentStream); + // [self loadForAudioDisplay]; + // break; + // case OWSMessageCellType_Video: + // OWSAssert(self.viewItem.attachmentStream); + // [self loadForVideoDisplay]; + // break; + // case OWSMessageCellType_GenericAttachment: { + // OWSAssert(self.viewItem.attachmentStream); + // OWSGenericAttachmentView *attachmentView = + // [[OWSGenericAttachmentView alloc] initWithAttachment:self.attachmentStream + // isIncoming:self.isIncoming]; + // [attachmentView createContents]; + // [self setMediaView:attachmentView]; + // [self addAttachmentUploadViewIfNecessary:attachmentView]; + // [self addCaptionIfNecessary]; + // break; + // } + // case OWSMessageCellType_DownloadingAttachment: { + // [self loadForDownloadingAttachment]; + // [self addCaptionIfNecessary]; + // break; + // } + // } + + // [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.textBottomMargin], [self.tapForMoreLabel + // autoSetDimension:ALDimensionHeight + // toSize:self.tapForMoreHeight], + + + // Do we need to pin the bubble size? + { + // - (CGSize)cellSizeForViewWidth:(int)viewWidth contentWidth:(int)contentWidth + // CGSize mediaSize = [self bodyMediaSizeForContentWidth:self.contentWidth]; + // TODO: + // [self.bubbleContentConstraints addObjectsFromArray:[self.mediaMaskingView + // autoSetDimensionsToSize:mediaSize]]; + } + + UIView *_Nullable lastSubview = nil; + CGFloat bottomMargin = 0; + // for (UIView *subview in contentViews) { + // [self.bubbleView addSubview:subview]; + // if (last + // lastSubview = subview; + // } + + 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_DownloadingAttachment: { - [self loadForDownloadingAttachment]; - [self addCaptionIfNecessary]; + case OWSMessageCellType_GenericAttachment: + bodyMediaView = [self loadViewForGenericAttachment]; + bodyMediaViewHasGreedyWidth = YES; + break; + case OWSMessageCellType_DownloadingAttachment: + bodyMediaView = [self loadViewForDownloadingAttachment]; + bodyMediaViewHasGreedyWidth = YES; break; - } } - [self ensureViewMediaState]; + if (bodyMediaView) { + OWSAssert(self.loadCellContentBlock); + OWSAssert(self.unloadCellContentBlock); + OWSAssert(!lastSubview); + + bodyMediaView.userInteractionEnabled = NO; + if (self.isMediaBeingSent) { + bodyMediaView.layer.opacity = 0.75f; + } + + // TODO: Is this right? + CGFloat contentWidth = self.width; + // Sometimes we want the media to "try to fill" the message bubble. + // For example, when the message bubble contains just an image, we + // want the image's bounds to exactly fill the message bubble. + // + // In other situations, we want to center the media within the message + // bubble. + // For example, when a message has a tall portrait image and a long + // caption. + CGSize bodyMediaSize = [self bodyMediaSizeForContentWidth:(int)contentWidth]; + BOOL hasValidMediaSize = bodyMediaSize.width > 0.01 && bodyMediaSize.height > 0.01; + if (!hasValidMediaSize) { + bodyMediaViewHasGreedyWidth = YES; + } else if (!self.hasText) { + bodyMediaViewHasGreedyWidth = YES; + } + + [self.bubbleView addSubview:bodyMediaView]; + if (bodyMediaViewHasGreedyWidth) { + [self.bubbleContentConstraints addObjectsFromArray:@[ + [bodyMediaView autoPinLeadingToSuperviewWithMargin:0], + [bodyMediaView autoPinTrailingToSuperviewWithMargin:0], + ]]; + } else { + CGFloat aspectRatio = bodyMediaSize.width / bodyMediaSize.height; + [self.bubbleContentConstraints addObjectsFromArray:@[ + [bodyMediaView autoHCenterInSuperview], + [bodyMediaView autoPinToAspectRatio:aspectRatio], + ]]; + } + if (lastSubview) { + [self.bubbleContentConstraints + addObject:[bodyMediaView autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:lastSubview withOffset:0]]; + } else { + [self.bubbleContentConstraints addObject:[bodyMediaView autoPinEdgeToSuperviewEdge:ALEdgeTop withInset:0]]; + } + lastSubview = bodyMediaView; + bottomMargin = 0; + } + + UIView *_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.hasText || !bodyMediaView) { + bodyTextView = [self createBodyTextViewIfNecessary]; + } + if (bodyTextView) { + [self.bubbleView addSubview:bodyTextView]; + [self.bubbleContentConstraints addObjectsFromArray:@[ + [bodyTextView autoPinLeadingToSuperviewWithMargin:self.textLeadingMargin], + [bodyTextView autoPinTrailingToSuperviewWithMargin:self.textTrailingMargin], + ]]; + if (lastSubview) { + [self.bubbleContentConstraints addObject:[bodyTextView autoPinEdge:ALEdgeTop + toEdge:ALEdgeBottom + ofView:lastSubview + withOffset:self.textTopMargin]]; + } else { + [self.bubbleContentConstraints + 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.bubbleContentConstraints 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.bubbleContentConstraints 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. @@ -516,78 +1231,85 @@ const CGFloat OWSMessageCellCornerRadius = 17; self.viewItem.didCellMediaFailToLoad = YES; [mediaView removeFromSuperview]; // TODO: We need to hide/remove the media view. - [self showAttachmentErrorView]; + [self showAttachmentErrorView: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]]; + // CGSize mediaSize = [self bodyMediaSizeForContentWidth:self.contentWidth]; + // TODO: + // [self.bubbleContentConstraints addObjectsFromArray:[self.mediaMaskingView autoSetDimensionsToSize:mediaSize]]; if (!self.isCellVisible) { // Eagerly unload. - self.stillImageView.image = nil; - self.animatedImageView.image = nil; + if (self.unloadCellContentBlock) { + self.unloadCellContentBlock(); + } return; + } else { + // Lazy load. + if (self.loadCellContentBlock) { + self.loadCellContentBlock(); + } } - switch (self.cellType) { - case OWSMessageCellType_StillImage: { - if (self.stillImageView.image) { - return; - } - self.stillImageView.image = [self tryToLoadCellMedia:^{ - OWSAssert([self.attachmentStream isImage]); - return self.attachmentStream.image; - } - mediaView:self.stillImageView - cacheKey:self.attachmentStream.uniqueId]; - break; - } - case OWSMessageCellType_AnimatedImage: { - if (self.animatedImageView.image) { - return; - } - self.animatedImageView.image = [self tryToLoadCellMedia:^{ - OWSAssert([self.attachmentStream isAnimated]); - - NSString *_Nullable filePath = [self.attachmentStream filePath]; - YYImage *_Nullable animatedImage = nil; - if (filePath && [NSData ows_isValidImageAtPath:filePath]) { - animatedImage = [YYImage imageWithContentsOfFile:filePath]; - } - return animatedImage; - } - mediaView:self.animatedImageView - cacheKey:self.attachmentStream.uniqueId]; - break; - } - case OWSMessageCellType_Video: { - if (self.stillImageView.image) { - return; - } - self.stillImageView.image = [self tryToLoadCellMedia:^{ - OWSAssert([self.attachmentStream isVideo]); - - return self.attachmentStream.image; - } - mediaView:self.stillImageView - cacheKey:self.attachmentStream.uniqueId]; - break; - } - case OWSMessageCellType_TextMessage: - case OWSMessageCellType_OversizeTextMessage: - case OWSMessageCellType_GenericAttachment: - case OWSMessageCellType_DownloadingAttachment: - case OWSMessageCellType_Audio: - case OWSMessageCellType_Unknown: - // Inexpensive cell types don't need to lazy-load or eagerly-unload. - break; - } + // switch (self.cellType) { + // case OWSMessageCellType_StillImage: { + // if (self.stillImageView.image) { + // return; + // } + // self.stillImageView.image = [self tryToLoadCellMedia:^{ + // OWSAssert([self.attachmentStream isImage]); + // return self.attachmentStream.image; + // } + // mediaView:self.stillImageView + // cacheKey:self.attachmentStream.uniqueId]; + // break; + // } + // case OWSMessageCellType_AnimatedImage: { + // if (self.animatedImageView.image) { + // return; + // } + // self.animatedImageView.image = [self tryToLoadCellMedia:^{ + // OWSAssert([self.attachmentStream isAnimated]); + // + // NSString *_Nullable filePath = [self.attachmentStream filePath]; + // YYImage *_Nullable animatedImage = nil; + // if (filePath && [NSData ows_isValidImageAtPath:filePath]) { + // animatedImage = [YYImage imageWithContentsOfFile:filePath]; + // } + // return animatedImage; + // } + // mediaView:self.animatedImageView + // cacheKey:self.attachmentStream.uniqueId]; + // break; + // } + // case OWSMessageCellType_Video: { + // if (self.stillImageView.image) { + // return; + // } + // self.stillImageView.image = [self tryToLoadCellMedia:^{ + // OWSAssert([self.attachmentStream isVideo]); + // + // return self.attachmentStream.image; + // } + // mediaView:self.stillImageView + // cacheKey:self.attachmentStream.uniqueId]; + // break; + // } + // case OWSMessageCellType_TextMessage: + // case OWSMessageCellType_OversizeTextMessage: + // case OWSMessageCellType_GenericAttachment: + // case OWSMessageCellType_DownloadingAttachment: + // case OWSMessageCellType_Audio: + // case OWSMessageCellType_Unknown: + // // Inexpensive cell types don't need to lazy-load or eagerly-unload. + // break; + // } } - (void)updateDateHeader @@ -762,197 +1484,480 @@ 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; - } - - OWSAssert(self.contentWidth); - CGSize textBubbleSize = [self textBubbleSizeForContentWidth:self.contentWidth]; - - 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]; - - [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], - ]]; - } 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], - ]]; + shouldIgnoreEvents = outgoingMessage.messageState != TSOutgoingMessageStateSentToService; } + [self.class loadForTextDisplay:self.bodyTextViewCached + text:self.displayableText.displayText + textColor:self.textColor + font:self.textMessageFont + shouldIgnoreEvents:shouldIgnoreEvents]; + return self.bodyTextViewCached; } -- (void)loadForStillImageDisplay ++ (void)loadForTextDisplay:(OWSMessageTextView *)textView + text:(NSString *)text + textColor:(UIColor *)textColor + font:(UIFont *)font + shouldIgnoreEvents:(BOOL)shouldIgnoreEvents +{ + //<<<<<<< HEAD + // self.textBubbleImageView.hidden = NO; + //||||||| merged common ancestors + // self.bubbleImageView.hidden = NO; + //======= + // self.bubbleFillView.hidden = NO; + //// self.bubbleImageView.hidden = NO; + //>>>>>>> SQUASHED + textView.hidden = NO; + textView.text = text; + textView.textColor = textColor; + + // Honor dynamic type in the message bodies. + textView.font = font; + textView.linkTextAttributes = @{ + NSForegroundColorAttributeName : textColor, + NSUnderlineStyleAttributeName : @(NSUnderlineStyleSingle | NSUnderlinePatternSolid) + }; + textView.dataDetectorTypes = (UIDataDetectorTypeLink | UIDataDetectorTypeAddress | UIDataDetectorTypeCalendarEvent); + textView.shouldIgnoreEvents = shouldIgnoreEvents; + + // OWSAssert(self.contentWidth); + // CGSize textBubbleSize = [self textBubbleSizeForContentWidth:self.contentWidth]; + // + // 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]; + // <<<<<<< HEAD + // [self.textBubbleImageView addSubview:self.tapForMoreLabel]; + // ||||||| merged common ancestors + // [self.bubbleImageView addSubview:self.tapForMoreLabel]; + // ======= + // [self.bubbleFillView addSubview:self.tapForMoreLabel]; + // >>>>>>> SQUASHED + // + // [self.bubbleContentConstraints 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.textTopMargin], + // + // [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.textBottomMargin], [self.tapForMoreLabel + // autoSetDimension:ALDimensionHeight + // toSize:self.tapForMoreHeight], + // ]]; + // } else { + // [self.bubbleContentConstraints 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], + // <<<<<<< HEAD + // [self.textView autoPinEdgeToSuperviewEdge:ALEdgeTop + // withInset:self.textVMargin], [self.textView + // autoPinEdgeToSuperviewEdge:ALEdgeBottom + // withInset:self.textVMargin], + // ]]; + // ||||||| merged common ancestors + // [self.textView autoPinEdgeToSuperviewEdge:ALEdgeTop withInset:self.textVMargin], + // [self.textView autoPinEdgeToSuperviewEdge:ALEdgeBottom withInset:self.textVMargin], + // ]; + // ======= + // [self.textView autoPinEdgeToSuperviewEdge:ALEdgeTop withInset:self.textTopMargin], + // [self.textView autoPinEdgeToSuperviewEdge:ALEdgeBottom withInset:self.textBottomMargin], + // ]; + // >>>>>>> SQUASHED + // } +} + +- (nullable UIView *)createTapForMoreLabelIfNecessary +{ + // //<<<<<<< HEAD + // // self.textBubbleImageView.hidden = NO; + // //||||||| merged common ancestors + // // self.bubbleImageView.hidden = NO; + // //======= + // // self.bubbleFillView.hidden = NO; + // //// self.bubbleImageView.hidden = NO; + // //>>>>>>> SQUASHED + // textView.hidden = NO; + // textView.text = text; + // textView.textColor = textColor; + // + // // Honor dynamic type in the message bodies. + // textView.font = font; + // textView.linkTextAttributes = @{ + // NSForegroundColorAttributeName : textColor, + // NSUnderlineStyleAttributeName : @(NSUnderlineStyleSingle | + // NSUnderlinePatternSolid) + // }; + // textView.dataDetectorTypes + // = (UIDataDetectorTypeLink | UIDataDetectorTypeAddress | UIDataDetectorTypeCalendarEvent); + // + // 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; + // } + // + // OWSAssert(self.contentWidth); + // CGSize textBubbleSize = [self textBubbleSizeForContentWidth:self.contentWidth]; + + if (!self.hasText) { + return nil; + } + if (!self.displayableText.isTextTruncated) { + 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]; + // <<<<<<< HEAD + // [self.textBubbleImageView addSubview:self.tapForMoreLabel]; + // ||||||| merged common ancestors + // [self.bubbleImageView addSubview:self.tapForMoreLabel]; + // ======= + // [self.bubbleFillView addSubview:self.tapForMoreLabel]; + // >>>>>>> SQUASHED + // + // [self.bubbleContentConstraints 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.textTopMargin], + // + // [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.textBottomMargin], [self.tapForMoreLabel + // autoSetDimension:ALDimensionHeight + // toSize:self.tapForMoreHeight], + // ]]; + // } else { + // [self.bubbleContentConstraints 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], + // <<<<<<< HEAD + // [self.textView autoPinEdgeToSuperviewEdge:ALEdgeTop + // withInset:self.textVMargin], [self.textView + // autoPinEdgeToSuperviewEdge:ALEdgeBottom + // withInset:self.textVMargin], + // ]]; + // ||||||| merged common ancestors + // [self.textView autoPinEdgeToSuperviewEdge:ALEdgeTop withInset:self.textVMargin], + // [self.textView autoPinEdgeToSuperviewEdge:ALEdgeBottom withInset:self.textVMargin], + // ]; + // ======= + // [self.textView autoPinEdgeToSuperviewEdge:ALEdgeTop withInset:self.textTopMargin], + // [self.textView autoPinEdgeToSuperviewEdge:ALEdgeBottom withInset:self.textBottomMargin], + // ]; + // >>>>>>> SQUASHED + // } + + 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 setMediaView:self.stillImageView]; + [self addAttachmentUploadViewIfNecessary:stillImageView]; + // [self addCaptionIfNecessary]; + + __weak OWSMessageCell *weakSelf = self; + self.loadCellContentBlock = ^{ + OWSMessageCell *strongSelf = weakSelf; + if (!strongSelf) { + return; + } + if (stillImageView.image) { + return; + } + stillImageView.image = [strongSelf tryToLoadCellMedia:^{ + OWSCAssert([strongSelf.attachmentStream isImage]); + return strongSelf.attachmentStream.image; + } + mediaView:stillImageView + cacheKey:strongSelf.attachmentStream.uniqueId]; + }; + self.unloadCellContentBlock = ^{ + stillImageView.image = nil; + }; + self.lastImageView = stillImageView; + + 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 setMediaView:self.animatedImageView]; + [self addAttachmentUploadViewIfNecessary:animatedImageView]; + // [self addCaptionIfNecessary]; + + __weak OWSMessageCell *weakSelf = self; + self.loadCellContentBlock = ^{ + OWSMessageCell *strongSelf = weakSelf; + if (!strongSelf) { + return; + } + 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]; + }; + self.unloadCellContentBlock = ^{ + animatedImageView.image = nil; + }; + self.lastImageView = animatedImageView; + + return animatedImageView; } -- (void)addCaptionIfNecessary -{ - if (self.hasText) { - [self loadForTextDisplay]; - } else { - [self.contentConstraints addObject:[self.textBubbleImageView autoSetDimension:ALDimensionHeight toSize:0]]; - } -} +//// TODO: +//- (void)addCaptionIfNecessary +//{ +// if (self.hasText) { +// [self loadForTextDisplay]; +// } else { +// [self.bubbleContentConstraints 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 setMediaView:self.audioMessageView]; + [self addAttachmentUploadViewIfNecessary:audioMessageView]; + // [self addCaptionIfNecessary]; + + 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; + // [self setMediaView:stillImageView]; 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]; + // [self addCaptionIfNecessary]; + + __weak OWSMessageCell *weakSelf = self; + self.loadCellContentBlock = ^{ + OWSMessageCell *strongSelf = weakSelf; + if (!strongSelf) { + return; + } + if (stillImageView.image) { + return; + } + stillImageView.image = [strongSelf tryToLoadCellMedia:^{ + OWSCAssert([strongSelf.attachmentStream isVideo]); + + return strongSelf.attachmentStream.image; + } + mediaView:stillImageView + cacheKey:strongSelf.attachmentStream.uniqueId]; + }; + self.unloadCellContentBlock = ^{ + stillImageView.image = nil; + }; + self.lastImageView = stillImageView; + + return stillImageView; } -- (void)loadForDownloadingAttachment +- (UIView *)loadViewForGenericAttachment +{ + OWSAssert(self.viewItem.attachmentStream); + OWSGenericAttachmentView *attachmentView = + [[OWSGenericAttachmentView alloc] initWithAttachment:self.attachmentStream isIncoming:self.isIncoming]; + [attachmentView createContents]; + // [self setMediaView:attachmentView]; + [self addAttachmentUploadViewIfNecessary:attachmentView]; + // [self addCaptionIfNecessary]; + + 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 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]; + // [self addCaptionIfNecessary]; + + self.loadCellContentBlock = ^{ + // Do nothing. + }; + self.unloadCellContentBlock = ^{ + // Do nothing. + }; + + return customView; } -- (void)setMediaView:(UIView *)view -{ - OWSAssert(view); - - 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; - } -} +//- (void)setMediaView:(UIView *)view +//{ +// OWSAssert(view); +// +// view.userInteractionEnabled = NO; +// [self.mediaMaskingView addSubview:view]; +// +// [self.bubbleContentConstraints +// addObject:[self.mediaMaskingView +// autoPinEdgeToSuperviewEdge:(self.isIncoming ? ALEdgeLeading : ALEdgeTrailing)]]; +// +// [self.bubbleContentConstraints addObjectsFromArray:[view autoPinEdgesToSuperviewMargins]]; +// +// [self cropMediaViewToBubbbleShape:view]; +// if (self.isMediaBeingSent) { +// view.layer.opacity = 0.75f; +// } +//} - (void)addAttachmentUploadViewIfNecessary:(UIView *)attachmentView { @@ -970,38 +1975,44 @@ const CGFloat OWSMessageCellCornerRadius = 17; if (self.isOutgoing) { if (!self.attachmentStream.isUploaded) { - self.attachmentUploadView = [[AttachmentUploadView alloc] initWithAttachment:self.attachmentStream - superview:attachmentView - attachmentStateCallback:attachmentStateCallback]; + __unused AttachmentUploadView *attachmentUploadView = + // self.attachmentUploadView = + // This view will be added to attachmentView which will retain a strong reference to it. + [[AttachmentUploadView alloc] initWithAttachment:self.attachmentStream + superview:attachmentView + attachmentStateCallback:attachmentStateCallback]; } } } -- (void)cropMediaViewToBubbbleShape:(UIView *)view -{ - OWSAssert(view); - OWSAssert(view.superview == self.mediaMaskingView); +//- (void)cropMediaViewToBubbbleShape:(UIView *)view +//{ +// 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]; +//} - 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 +- (void)showAttachmentErrorView:(UIView *)mediaView { - 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 *customView = [UIView new]; + customView.backgroundColor = [UIColor colorWithWhite:0.85f alpha:1.f]; + customView.userInteractionEnabled = NO; + [mediaView addSubview:customView]; + [customView autoPinEdgesToSuperviewEdges]; } -- (CGSize)textBubbleSizeForContentWidth:(int)contentWidth +#pragma mark - Measurement + +// Size of "message body" text, not quoted reply text. +- (CGSize)bodyTextSizeForContentWidth:(int)contentWidth { if (!self.hasText) { return CGSizeZero; @@ -1010,23 +2021,48 @@ const CGFloat OWSMessageCellCornerRadius = 17; 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.bodyTextViewCached.text = self.displayableText.displayText; // Honor dynamic type in the message bodies. - self.textView.font = [self textMessageFont]; - CGSize textSize = [self.textView sizeThatFits:CGSizeMake(maxTextWidth, CGFLOAT_MAX)]; + self.bodyTextViewCached.font = [self textMessageFont]; + CGSize textSize = CGSizeCeil([self.bodyTextViewCached 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)); + CGSize textViewSize = CGSizeCeil(CGSizeMake(textSize.width + leftMargin + rightMargin, + textSize.height + self.textTopMargin + self.textBottomMargin + tapForMoreHeight)); return textViewSize; } +// +//+ (CGSize)textSizeForContentWidth:(int)contentWidth +// displayableText:(DisplayableText *)displayableText +// textView:(OWSMessageTextView *)textView +//{ +// OWSAssert(text); +// OWSAssert(textView); +// +// BOOL isRTL = self.isRTL; +// CGFloat leftMargin = isRTL ? self.textTrailingMargin : self.textLeadingMargin; +// CGFloat rightMargin = isRTL ? self.textLeadingMargin : self.textTrailingMargin; +// +// const int maxMessageWidth = [self maxMessageWidthForContentWidth:contentWidth]; +// const int maxTextWidth = (int)floor(maxMessageWidth - (leftMargin + rightMargin)); +// +// textView.text = displayableText.displayText; +// // Honor dynamic type in the message bodies. +// textView.font = [self textMessageFont]; +// CGSize textSize = CGSizeCeil([textView sizeThatFits:CGSizeMake(maxTextWidth, CGFLOAT_MAX)]); +// CGFloat tapForMoreHeight = (displayableText.isTextTruncated ? [self tapForMoreHeight] : 0.f); +// CGSize textViewSize = CGSizeCeil(CGSizeMake(textSize.width + leftMargin + rightMargin, +// textSize.height + self.textTopMargin + self.textBottomMargin + +// tapForMoreHeight)); +// +// return textViewSize; +//} -- (CGSize)mediaBubbleSizeForContentWidth:(int)contentWidth +- (CGSize)bodyMediaSizeForContentWidth:(int)contentWidth { const int maxMessageWidth = [self maxMessageWidthForContentWidth:contentWidth]; @@ -1082,8 +2118,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]; CGFloat cellContentWidth = fmax(mediaContentSize.width, textContentSize.width); CGFloat cellContentHeight = mediaContentSize.height + textContentSize.height; @@ -1114,6 +2150,8 @@ const CGFloat OWSMessageCellCornerRadius = 17; } } +#pragma mark - + - (BOOL)isIncoming { return self.viewItem.interaction.interactionType == OWSInteractionType_IncomingMessage; @@ -1126,19 +2164,41 @@ const CGFloat OWSMessageCellCornerRadius = 17; - (CGFloat)textLeadingMargin { - return self.isIncoming ? 15 : 10; + return (self.isIncoming ? kBubbleTextHInset + kBubbleThornSideInset : kBubbleTextHInset); + + // static const CGFloat kBubbleVRounding = 8.f; + // static const CGFloat kBubbleHRounding = 10.f; + // static const CGFloat kBubbleThornSideInset = 3.f; + // static const CGFloat kBubbleThornVInset = 3.f; + // static const CGFloat kBubbleTextHInset = 10.f; + // static const CGFloat kBubbleTextVInset = 10.f; + // static const CGFloat kBubbleTextTopInset = kBubbleTextVInset; + // static const CGFloat kBubbleTextBottomInset = kBubbleThornVInset + kBubbleTextVInset; + + // return self.isIncoming ? 15 : 10; } - (CGFloat)textTrailingMargin { - return self.isIncoming ? 10 : 15; + return (self.isIncoming ? kBubbleTextHInset : kBubbleTextHInset + kBubbleThornSideInset); + // return self.isIncoming ? 10 : 15; } -- (CGFloat)textVMargin +- (CGFloat)textTopMargin { - return 10; + return kBubbleTextVInset; } +- (CGFloat)textBottomMargin +{ + return kBubbleTextVInset + kBubbleThornVInset; +} + +//- (CGFloat)textVMargin +//{ +// return 10; +//} + - (UIColor *)textColor { return self.isIncoming ? [UIColor blackColor] : [UIColor whiteColor]; @@ -1168,10 +2228,10 @@ const CGFloat OWSMessageCellCornerRadius = 17; { [super prepareForReuse]; - [NSLayoutConstraint deactivateConstraints:self.payloadConstraints]; - self.payloadConstraints = nil; - [NSLayoutConstraint deactivateConstraints:self.contentConstraints]; - self.contentConstraints = [NSMutableArray new]; + [NSLayoutConstraint deactivateConstraints:self.cellContentConstraints]; + self.cellContentConstraints = nil; + [NSLayoutConstraint deactivateConstraints:self.bubbleContentConstraints]; + self.bubbleContentConstraints = [NSMutableArray new]; [NSLayoutConstraint deactivateConstraints:self.dateHeaderConstraints]; self.dateHeaderConstraints = nil; [NSLayoutConstraint deactivateConstraints:self.footerConstraints]; @@ -1179,39 +2239,67 @@ const CGFloat OWSMessageCellCornerRadius = 17; self.dateHeaderLabel.text = nil; self.dateHeaderLabel.hidden = YES; - self.textView.text = nil; - self.textView.hidden = YES; - self.textView.dataDetectorTypes = UIDataDetectorTypeNone; + [self.bodyTextViewCached removeFromSuperview]; + self.bodyTextViewCached.text = nil; + self.bodyTextViewCached.hidden = YES; + self.bodyTextViewCached.dataDetectorTypes = UIDataDetectorTypeNone; [self.failedSendBadgeView removeFromSuperview]; self.failedSendBadgeView = nil; - [self.tapForMoreLabel removeFromSuperview]; - self.tapForMoreLabel = 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; + // TODO: + self.bubbleView.hidden = YES; + //<<<<<<< HEAD + // self.textBubbleImageView.image = nil; + // self.textBubbleImageView.hidden = YES; + // self.mediaMaskingView.maskedSubview = nil; + // self.mediaMaskingView.hideTail = NO; + // self.mediaMaskingView.layoutMargins = UIEdgeInsetsZero; + //||||||| merged common ancestors + // self.bubbleImageView.image = nil; + // self.bubbleImageView.hidden = YES; + // self.payloadView.maskedSubview = nil; + //======= + // self.bubbleFillView.hidden = YES; + //// self.bubbleImageView.image = nil; + //// self.bubbleImageView.hidden = YES; + // self.payloadView.maskedSubview = nil; + //>>>>>>> SQUASHED + + for (UIView *subview in self.bubbleView.subviews) { + [subview removeFromSuperview]; + } + + if (self.unloadCellContentBlock) { + self.unloadCellContentBlock(); + } + self.loadCellContentBlock = nil; + self.unloadCellContentBlock = nil; + + // [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.expirationTimerView clearAnimations]; [self.expirationTimerView removeFromSuperview]; self.expirationTimerView = nil; + [self.lastImageView removeFromSuperview]; + self.lastImageView = nil; + [self hideMenuControllerIfNecessary]; } @@ -1226,7 +2314,7 @@ const CGFloat OWSMessageCellCornerRadius = 17; return; } - [self ensureViewMediaState]; + [self ensureMediaLoadState]; if (isCellVisible) { if (self.message.shouldStartExpireTimer) { @@ -1300,22 +2388,25 @@ const CGFloat OWSMessageCellCornerRadius = 17; } break; case OWSMessageCellType_StillImage: + OWSAssert(self.lastImageView); [self.delegate didTapImageViewItem:self.viewItem attachmentStream:self.attachmentStream - imageView:self.stillImageView]; + imageView:self.lastImageView]; break; case OWSMessageCellType_AnimatedImage: + OWSAssert(self.lastImageView); [self.delegate didTapImageViewItem:self.viewItem attachmentStream:self.attachmentStream - imageView:self.animatedImageView]; + imageView:self.lastImageView]; break; case OWSMessageCellType_Audio: [self.delegate didTapAudioViewItem:self.viewItem attachmentStream:self.attachmentStream]; return; case OWSMessageCellType_Video: + OWSAssert(self.lastImageView); [self.delegate didTapVideoViewItem:self.viewItem attachmentStream:self.attachmentStream - imageView:self.stillImageView]; + imageView:self.lastImageView]; return; case OWSMessageCellType_GenericAttachment: [AttachmentSharing showShareUIForAttachment:self.attachmentStream]; diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewItem.m b/Signal/src/ViewControllers/ConversationView/ConversationViewItem.m index 4797175936..13fa7740c4 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewItem.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewItem.m @@ -55,11 +55,13 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType) @property (nonatomic) BOOL hasViewState; @property (nonatomic) OWSMessageCellType messageCellType; +// TODO: Rename to displayableBodyText. @property (nonatomic, nullable) DisplayableText *displayableText; @property (nonatomic, nullable) TSAttachmentStream *attachmentStream; @property (nonatomic, nullable) TSAttachmentPointer *attachmentPointer; @property (nonatomic) CGSize mediaSize; -@property (nonatomic) BOOL hasText; +// TODO: Remove in favor of displayableBodyText nil test. +//@property (nonatomic) BOOL hasText; @end @@ -84,6 +86,8 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType) [self ensureViewState:transaction]; + // OWSAssert(self.hasViewState); + return self; } @@ -103,6 +107,13 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType) [self clearCachedLayoutState]; [self ensureViewState:transaction]; + + // OWSAssert(self.hasViewState); +} + +- (BOOL)hasText +{ + return _displayableText != nil; } - (void)setShouldShowDate:(BOOL)shouldShowDate @@ -287,8 +298,8 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType) }]; } -- (DisplayableText *)displayableTextForAttachmentStream:(TSAttachmentStream *)attachmentStream - interactionId:(NSString *)interactionId +- (DisplayableText *)displayableTextForOversizeTextAttachment:(TSAttachmentStream *)attachmentStream + interactionId:(NSString *)interactionId { OWSAssert(attachmentStream); OWSAssert(interactionId.length > 0); @@ -353,9 +364,9 @@ 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.displayableText = [self displayableTextForOversizeTextAttachment:self.attachmentStream + interactionId:message.uniqueId]; + // self.hasText = YES; } else if ([self.attachmentStream isAnimated] || [self.attachmentStream isImage] || [self.attachmentStream isVideo]) { if ([self.attachmentStream isAnimated]) { @@ -392,8 +403,13 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType) } } + // Ignore message body for oversize text attachments. if (message.body.length > 0) { - self.hasText = YES; + if (self.hasText) { + OWSFail(@"%@ oversize text message has unexpected caption.", self.logTag); + } + + // self.hasText = YES; // 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) { @@ -401,7 +417,7 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType) self.messageCellType = OWSMessageCellType_TextMessage; } self.displayableText = [self displayableTextForText:message.body interactionId:message.uniqueId]; - OWSAssert(self.displayableText); + OWSAssert(self.displayableText); } if (self.messageCellType == OWSMessageCellType_Unknown) { @@ -409,7 +425,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.hasText = YES; self.displayableText = [[DisplayableText alloc] initWithFullText:@"" displayText:@"" isTextTruncated:NO]; } } diff --git a/Signal/src/ViewControllers/HomeViewController.m b/Signal/src/ViewControllers/HomeViewController.m index dd48ef95ef..9c3f41dc14 100644 --- a/Signal/src/ViewControllers/HomeViewController.m +++ b/Signal/src/ViewControllers/HomeViewController.m @@ -284,6 +284,11 @@ typedef NS_ENUM(NSInteger, CellState) { kArchiveState, kInboxState }; } [self updateBarButtonItems]; + + dispatch_async(dispatch_get_main_queue(), ^{ + TSThread *thread = [self threadForIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]]; + [self presentThread:thread keyboardOnViewAppearing:NO callOnViewAppearing:NO]; + }); } - (void)viewDidAppear:(BOOL)animated diff --git a/SignalMessaging/utils/OWSMessagesBubbleImageFactory.swift b/SignalMessaging/utils/OWSMessagesBubbleImageFactory.swift index 48192996eb..6791a1a016 100644 --- a/SignalMessaging/utils/OWSMessagesBubbleImageFactory.swift +++ b/SignalMessaging/utils/OWSMessagesBubbleImageFactory.swift @@ -58,6 +58,24 @@ public class OWSMessagesBubbleImageFactory: NSObject { } } + public func bubbleColor(message: TSMessage) -> UIColor { + if message is TSIncomingMessage { + return UIColor.jsq_messageBubbleLightGray()! + } else if let outgoingMessage = message as? TSOutgoingMessage { + switch outgoingMessage.messageState { + case .unsent: + return UIColor.gray + case .attemptingOut: + return UIColor.ows_fadedBlue + default: + return UIColor.ows_materialBlue + } + } 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/Messages/Attachments/TSAttachmentStream.m b/SignalServiceKit/src/Messages/Attachments/TSAttachmentStream.m index 15ca2fbd9f..727175897f 100644 --- a/SignalServiceKit/src/Messages/Attachments/TSAttachmentStream.m +++ b/SignalServiceKit/src/Messages/Attachments/TSAttachmentStream.m @@ -572,19 +572,21 @@ NS_ASSUME_NONNULL_BEGIN OWSAssertIsOnMainThread(); OWSAssert([self isAudio]); - NSError *error; - AVAudioPlayer *audioPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:self.mediaURL error:&error]; - if (error && [error.domain isEqualToString:NSOSStatusErrorDomain] - && (error.code == kAudioFileInvalidFileError || error.code == kAudioFileStreamError_InvalidFile)) { - // Ignore "invalid audio file" errors. - return 0.f; - } - if (!error) { - return (CGFloat)[audioPlayer duration]; - } else { - OWSFail(@"Could not find audio duration: %@", self.mediaURL); - return 0; - } + return 0; + + // NSError *error; + // AVAudioPlayer *audioPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:self.mediaURL error:&error]; + // if (error && [error.domain isEqualToString:NSOSStatusErrorDomain] + // && (error.code == kAudioFileInvalidFileError || error.code == kAudioFileStreamError_InvalidFile)) { + // // Ignore "invalid audio file" errors. + // return 0.f; + // } + // if (!error) { + // return (CGFloat)[audioPlayer duration]; + // } else { + // OWSFail(@"Could not find audio duration: %@", self.mediaURL); + // return 0; + // } } - (CGFloat)audioDurationSeconds From 6525ccdb05c0cab763508f68fa65289696449d3c Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Tue, 27 Mar 2018 13:25:02 -0400 Subject: [PATCH 14/31] Bubble collapse. --- .../ConversationView/Cells/OWSMessageCell.m | 361 ++++-------------- 1 file changed, 78 insertions(+), 283 deletions(-) diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m index aca0422cb6..5697316b6e 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m @@ -50,7 +50,7 @@ static const CGFloat kBubbleTextVInset = 6.f; //@property (nonatomic) BOOL isOutgoing; @property (nonatomic) CAShapeLayer *shapeLayer; -@property (nonatomic) UIColor *bubbleColor; +@property (nonatomic, nullable) UIColor *bubbleColor; @end @@ -90,7 +90,7 @@ static const CGFloat kBubbleTextVInset = 6.f; } } -- (void)setBubbleColor:(UIColor *)bubbleColor +- (void)setBubbleColor:(nullable UIColor *)bubbleColor { _bubbleColor = bubbleColor; @@ -121,97 +121,6 @@ static const CGFloat kBubbleTextVInset = 6.f; self.maskLayer.path = bezierPath.CGPath; } -//- (void)updateMask -//{ -// UIView *_Nullable maskedSubview = self.maskedSubview; -// if (!maskedSubview) { -// return; -// } -// maskedSubview.frame = self.bounds; -// //<<<<<<< HEAD -// // // 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]; -// // } -// //||||||| merged common ancestors -// // // The JSQ masks are not RTL-safe, so we need to invert the -// // // mask orientation manually. -// // BOOL hasOutgoingMask = self.isOutgoing ^ self.isRTL; -// // [JSQMessagesMediaViewBubbleImageMasker applyBubbleImageMaskToMediaView:maskedSubview -// isOutgoing:hasOutgoingMask]; -// //======= -// -// -// UIBezierPath *bezierPath = [BubbleFillView maskPathForSize:self.bounds.size -// isOutgoing:self.isOutgoing -// isRTL:self.isRTL]; -// self.maskLayer.path = bezierPath.CGPath; -// maskedSubview.layer.mask = self.maskLayer; -// //>>>>>>> SQUASHED -//} - -//- (void)setIsOutgoing:(BOOL)isOutgoing { -// if (_isOutgoing == isOutgoing) { -// return; -// } -// _isOutgoing = isOutgoing; -// [self updateMask]; -//} -// -//- (void)setFrame:(CGRect)frame -//{ -// BOOL didSizeChange = !CGSizeEqualToSize(self.frame.size, frame.size); -// -// [super setFrame:frame]; -// -// if (didSizeChange || !self.shapeLayer) { -// [self updateMask]; -// } -//} -// -//- (void)setBounds:(CGRect)bounds -//{ -// BOOL didSizeChange = !CGSizeEqualToSize(self.bounds.size, bounds.size); -// -// [super setBounds:bounds]; -// -// if (didSizeChange || !self.shapeLayer) { -// [self updateMask]; -// } -//} -// -//- (void)updateMask -//{ -// if (!self.shapeLayer) { -// self.shapeLayer = [CAShapeLayer new]; -// [self.layer addSublayer:self.shapeLayer]; -// } -// -// UIBezierPath *bezierPath = [self.class maskPathForSize:self.bounds.size -// isOutgoing:self.isOutgoing -// isRTL:self.isRTL]; -// -// self.shapeLayer.fillColor = self.bubbleColor.CGColor; -// self.shapeLayer.path = bezierPath.CGPath; -//} - + (UIBezierPath *)maskPathForSize:(CGSize)size isOutgoing:(BOOL)isOutgoing isRTL:(BOOL)isRTL @@ -311,187 +220,10 @@ static const CGFloat kBubbleTextVInset = 6.f; return bezierPath; } -//- (void)setBubbleColor:(UIColor *)bubbleColor { -// _bubbleColor = bubbleColor; -// -// self.shapeLayer.fillColor = bubbleColor.CGColor; -//} - @end #pragma mark - -//@interface BubbleFillView : UIView -// -////@property (nonatomic) BOOL isOutgoing; -////@property (nonatomic) CAShapeLayer *shapeLayer; -////@property (nonatomic) UIColor *bubbleColor; -// -//@end -// -//#pragma mark - -// -//@implementation BubbleFillView -// -////- (void)setIsOutgoing:(BOOL)isOutgoing { -//// if (_isOutgoing == isOutgoing) { -//// return; -//// } -//// _isOutgoing = isOutgoing; -//// [self updateMask]; -////} -//// -////- (void)setFrame:(CGRect)frame -////{ -//// BOOL didSizeChange = !CGSizeEqualToSize(self.frame.size, frame.size); -//// -//// [super setFrame:frame]; -//// -//// if (didSizeChange || !self.shapeLayer) { -//// [self updateMask]; -//// } -////} -//// -////- (void)setBounds:(CGRect)bounds -////{ -//// BOOL didSizeChange = !CGSizeEqualToSize(self.bounds.size, bounds.size); -//// -//// [super setBounds:bounds]; -//// -//// if (didSizeChange || !self.shapeLayer) { -//// [self updateMask]; -//// } -////} -//// -////- (void)updateMask -////{ -//// if (!self.shapeLayer) { -//// self.shapeLayer = [CAShapeLayer new]; -//// [self.layer addSublayer:self.shapeLayer]; -//// } -//// -//// UIBezierPath *bezierPath = [self.class maskPathForSize:self.bounds.size -//// isOutgoing:self.isOutgoing -//// isRTL:self.isRTL]; -//// -//// self.shapeLayer.fillColor = self.bubbleColor.CGColor; -//// self.shapeLayer.path = bezierPath.CGPath; -////} -// -////- (void)setBubbleColor:(UIColor *)bubbleColor { -//// _bubbleColor = bubbleColor; -//// -//// self.shapeLayer.fillColor = bubbleColor.CGColor; -////} -// -//@end -// -//#pragma mark - -// -//@interface BubbleMaskingView : UIView -// -//@property (nonatomic) BOOL isOutgoing; -//@property (nonatomic) BOOL hideTail; -//@property (nonatomic, nullable, weak) UIView *maskedSubview; -//@property (nonatomic) CAShapeLayer *maskLayer; -// -//@end -// -//#pragma mark - -// -//@implementation BubbleMaskingView -// -//- (void)setMaskedSubview:(UIView * _Nullable)maskedSubview { -// if (_maskedSubview == maskedSubview) { -// return; -// } -// _maskedSubview = maskedSubview; -// [self updateMask]; -//} -// -//- (void)setIsOutgoing:(BOOL)isOutgoing { -// if (_isOutgoing == isOutgoing) { -// return; -// } -// _isOutgoing = isOutgoing; -// [self updateMask]; -//} -// -//- (void)setFrame:(CGRect)frame -//{ -// BOOL didSizeChange = !CGSizeEqualToSize(self.frame.size, frame.size); -// -// [super setFrame:frame]; -// -// if (didSizeChange) { -// [self updateMask]; -// } -//} -// -//- (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; -// //<<<<<<< HEAD -// // // 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]; -// // } -// //||||||| merged common ancestors -// // // The JSQ masks are not RTL-safe, so we need to invert the -// // // mask orientation manually. -// // BOOL hasOutgoingMask = self.isOutgoing ^ self.isRTL; -// // [JSQMessagesMediaViewBubbleImageMasker applyBubbleImageMaskToMediaView:maskedSubview -// isOutgoing:hasOutgoingMask]; -// //======= -// -// if (!self.maskLayer) { -// self.maskLayer = [CAShapeLayer new]; -// } -// -// UIBezierPath *bezierPath = [OWSBubbleView maskPathForSize:self.bounds.size -// isOutgoing:self.isOutgoing -// isRTL:self.isRTL]; -// self.maskLayer.path = bezierPath.CGPath; -// maskedSubview.layer.mask = self.maskLayer; -// //>>>>>>> SQUASHED -//} -// -//@end - -#pragma mark - - @interface OWSMessageTextView : UITextView @property (nonatomic) BOOL shouldIgnoreEvents; @@ -602,7 +334,7 @@ static const CGFloat kBubbleTextVInset = 6.f; @property (nonatomic) UILabel *footerLabel; @property (nonatomic, nullable) OWSExpirationTimerView *expirationTimerView; -@property (nonatomic, nullable) UIImageView *lastImageView; +@property (nonatomic, nullable) UIView *lastBodyMediaView; // Should lazy-load expensive view contents (images, etc.). // Should do nothing if view is already loaded. @@ -743,6 +475,22 @@ static const CGFloat kBubbleTextVInset = 6.f; [self.footerView autoPinEdgeToSuperviewEdge:ALEdgeBottom]; [self.footerView autoPinWidthToSuperview]; + self.bubbleView.userInteractionEnabled = YES; + + UITapGestureRecognizer *tap = + [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTapGesture:)]; + [self.bubbleView addGestureRecognizer:tap]; + + + UILongPressGestureRecognizer *longPress = + [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleLongPressGesture:)]; + [self.bubbleView addGestureRecognizer:longPress]; + + PanDirectionGestureRecognizer *panGesture = [[PanDirectionGestureRecognizer alloc] + initWithDirection:(self.isRTL ? PanDirectionLeft : PanDirectionRight)target:self + action:@selector(handlePanGesture:)]; + [self addGestureRecognizer:panGesture]; + // UITapGestureRecognizer *mediaTap = // [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleMediaTapGesture:)]; // [self.mediaMaskingView addGestureRecognizer:mediaTap]; @@ -972,7 +720,8 @@ static const CGFloat kBubbleTextVInset = 6.f; self.bubbleView.bubbleColor = [self.bubbleFactory bubbleColorWithMessage:message]; // self.bubbleFillView.bubbleColor = [self.bubbleFactory bubbleColorWithMessage:message]; } else { - OWSFail(@"%@ Unknown interaction type: %@", self.logTag, self.viewItem.interaction.class); + // Media-only messages should have no background color; they will fill the bubble's bounds + // and we don't want artifacts at the edges. } //<<<<<<< HEAD @@ -1104,6 +853,7 @@ static const CGFloat kBubbleTextVInset = 6.f; OWSAssert(self.unloadCellContentBlock); OWSAssert(!lastSubview); + self.lastBodyMediaView = bodyMediaView; bodyMediaView.userInteractionEnabled = NO; if (self.isMediaBeingSent) { bodyMediaView.layer.opacity = 0.75f; @@ -1750,7 +1500,6 @@ static const CGFloat kBubbleTextVInset = 6.f; self.unloadCellContentBlock = ^{ stillImageView.image = nil; }; - self.lastImageView = stillImageView; return stillImageView; } @@ -1793,7 +1542,6 @@ static const CGFloat kBubbleTextVInset = 6.f; self.unloadCellContentBlock = ^{ animatedImageView.image = nil; }; - self.lastImageView = animatedImageView; return animatedImageView; } @@ -1878,7 +1626,6 @@ static const CGFloat kBubbleTextVInset = 6.f; self.unloadCellContentBlock = ^{ stillImageView.image = nil; }; - self.lastImageView = stillImageView; return stillImageView; } @@ -2252,6 +1999,7 @@ static const CGFloat kBubbleTextVInset = 6.f; // TODO: self.bubbleView.hidden = YES; + self.bubbleView.bubbleColor = nil; //<<<<<<< HEAD // self.textBubbleImageView.image = nil; // self.textBubbleImageView.hidden = YES; @@ -2297,8 +2045,8 @@ static const CGFloat kBubbleTextVInset = 6.f; [self.expirationTimerView removeFromSuperview]; self.expirationTimerView = nil; - [self.lastImageView removeFromSuperview]; - self.lastImageView = nil; + [self.lastBodyMediaView removeFromSuperview]; + self.lastBodyMediaView = nil; [self hideMenuControllerIfNecessary]; } @@ -2331,6 +2079,30 @@ static const CGFloat kBubbleTextVInset = 6.f; #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); @@ -2388,25 +2160,25 @@ static const CGFloat kBubbleTextVInset = 6.f; } break; case OWSMessageCellType_StillImage: - OWSAssert(self.lastImageView); + OWSAssert(self.lastBodyMediaView); [self.delegate didTapImageViewItem:self.viewItem attachmentStream:self.attachmentStream - imageView:self.lastImageView]; + imageView:self.lastBodyMediaView]; break; case OWSMessageCellType_AnimatedImage: - OWSAssert(self.lastImageView); + OWSAssert(self.lastBodyMediaView); [self.delegate didTapImageViewItem:self.viewItem attachmentStream:self.attachmentStream - imageView:self.lastImageView]; + imageView:self.lastBodyMediaView]; break; case OWSMessageCellType_Audio: [self.delegate didTapAudioViewItem:self.viewItem attachmentStream:self.attachmentStream]; return; case OWSMessageCellType_Video: - OWSAssert(self.lastImageView); + OWSAssert(self.lastBodyMediaView); [self.delegate didTapVideoViewItem:self.viewItem attachmentStream:self.attachmentStream - imageView:self.lastImageView]; + imageView:self.lastBodyMediaView]; return; case OWSMessageCellType_GenericAttachment: [AttachmentSharing showShareUIForAttachment:self.attachmentStream]; @@ -2421,6 +2193,29 @@ static const CGFloat kBubbleTextVInset = 6.f; } } +- (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); From cb00b2287013834cc9423b9fa30792aa42469783 Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Wed, 28 Mar 2018 10:01:01 -0400 Subject: [PATCH 15/31] Bubble collapse. --- Signal.xcodeproj/project.pbxproj | 12 + Signal/src/Signal-Bridging-Header.h | 1 + .../ConversationView/Cells/OWSBubbleView.h | 28 ++ .../ConversationView/Cells/OWSBubbleView.m | 130 +++++++++ .../ConversationView/Cells/OWSMessageCell.h | 2 - .../ConversationView/Cells/OWSMessageCell.m | 274 +----------------- .../Cells/OWSMessageTextView.h | 13 + .../Cells/OWSMessageTextView.m | 65 +++++ .../MediaGalleryViewController.swift | 4 +- 9 files changed, 259 insertions(+), 270 deletions(-) create mode 100644 Signal/src/ViewControllers/ConversationView/Cells/OWSBubbleView.h create mode 100644 Signal/src/ViewControllers/ConversationView/Cells/OWSBubbleView.m create mode 100644 Signal/src/ViewControllers/ConversationView/Cells/OWSMessageTextView.h create mode 100644 Signal/src/ViewControllers/ConversationView/Cells/OWSMessageTextView.m diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index 3906643d05..c10d5fb815 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -214,6 +214,8 @@ 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 */; }; 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 */; }; @@ -835,6 +837,10 @@ 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 = ""; }; 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 = ""; }; @@ -1645,6 +1651,8 @@ 34D1F0971F867BFC0066283D /* ConversationViewCell.m */, 34D1F0B81F8800D90066283D /* OWSAudioMessageView.h */, 34D1F0B91F8800D90066283D /* OWSAudioMessageView.m */, + 34DBF002206BD5A500025978 /* OWSBubbleView.h */, + 34DBF001206BD5A500025978 /* OWSBubbleView.m */, 34D1F09A1F867BFC0066283D /* OWSContactOffersCell.h */, 34D1F09B1F867BFC0066283D /* OWSContactOffersCell.m */, 34D1F09C1F867BFC0066283D /* OWSExpirableMessageView.h */, @@ -1654,6 +1662,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 */, @@ -3213,6 +3223,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 */, @@ -3244,6 +3255,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 */, 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/OWSBubbleView.h b/Signal/src/ViewControllers/ConversationView/Cells/OWSBubbleView.h new file mode 100644 index 0000000000..a1f5436aa6 --- /dev/null +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSBubbleView.h @@ -0,0 +1,28 @@ +// +// 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; + +@interface OWSBubbleView : UIView + +@property (nonatomic) BOOL isOutgoing; +@property (nonatomic) BOOL hideTail; + +@property (nonatomic) CAShapeLayer *maskLayer; +@property (nonatomic) CAShapeLayer *shapeLayer; + +@property (nonatomic, nullable) UIColor *bubbleColor; + +@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..0bde2233ab --- /dev/null +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSBubbleView.m @@ -0,0 +1,130 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "OWSBubbleView.h" +#import + +NS_ASSUME_NONNULL_BEGIN + +// This approximates the curve of our message bubbles, which makes the animation feel a little smoother. +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; + +@implementation OWSBubbleView + +- (void)setIsOutgoing:(BOOL)isOutgoing +{ + BOOL didChange = _isOutgoing != isOutgoing; + + _isOutgoing = isOutgoing; + + if (didChange || !self.shapeLayer) { + [self updateMask]; + } +} + +- (void)setFrame:(CGRect)frame +{ + BOOL didChange = !CGSizeEqualToSize(self.frame.size, frame.size); + + [super setFrame:frame]; + + if (didChange || !self.shapeLayer) { + [self updateMask]; + } +} + +- (void)setBounds:(CGRect)bounds +{ + BOOL didChange = !CGSizeEqualToSize(self.bounds.size, bounds.size); + + [super setBounds:bounds]; + + if (didChange || !self.shapeLayer) { + [self updateMask]; + } +} + +- (void)setBubbleColor:(nullable UIColor *)bubbleColor +{ + _bubbleColor = bubbleColor; + + if (!self.shapeLayer) { + [self updateMask]; + } + self.shapeLayer.fillColor = bubbleColor.CGColor; +} + +- (void)updateMask +{ + if (!self.shapeLayer) { + self.shapeLayer = [CAShapeLayer new]; + [self.layer addSublayer:self.shapeLayer]; + } + if (!self.maskLayer) { + self.maskLayer = [CAShapeLayer new]; + self.layer.mask = self.maskLayer; + } + + UIBezierPath *bezierPath = + [self.class maskPathForSize:self.bounds.size isOutgoing:self.isOutgoing isRTL:self.isRTL]; + + self.shapeLayer.fillColor = self.bubbleColor.CGColor; + self.shapeLayer.path = bezierPath.CGPath; + + self.maskLayer.path = bezierPath.CGPath; +} + ++ (UIBezierPath *)maskPathForSize:(CGSize)size isOutgoing:(BOOL)isOutgoing 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)]; + + // 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 5697316b6e..c2fc47ba01 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m @@ -8,8 +8,10 @@ #import "ConversationViewItem.h" #import "NSAttributedString+OWS.h" #import "OWSAudioMessageView.h" +#import "OWSBubbleView.h" #import "OWSExpirationTimerView.h" #import "OWSGenericAttachmentView.h" +#import "OWSMessageTextView.h" #import "Signal-Swift.h" #import "UIColor+OWS.h" #import @@ -18,7 +20,6 @@ NS_ASSUME_NONNULL_BEGIN -// TODO: Choose best thorn. // TODO: Review all comments. CG_INLINE CGSize CGSizeCeil(CGSize size) @@ -26,270 +27,6 @@ CG_INLINE CGSize CGSizeCeil(CGSize size) return CGSizeMake((CGFloat)ceil(size.width), (CGFloat)ceil(size.height)); } -// This approximates the curve of our message bubbles, which makes the animation feel a little smoother. -const CGFloat OWSMessageCellCornerRadius = 17; - -// TODO: We could make the bubble shape respond to dynamic text. -static const CGFloat kBubbleVRounding = 8.5f; -static const CGFloat kBubbleHRounding = 10.f; -//static const CGFloat kBubbleThornSideInset = 3.f; -//static const CGFloat kBubbleThornVInset = 3.f; -//static const CGFloat kBubbleThornSideInset = 6.f; -//static const CGFloat kBubbleThornVInset = 0.f; -static const CGFloat kBubbleThornSideInset = kBubbleHRounding * 0.3f; -static const CGFloat kBubbleThornVInset = kBubbleVRounding * 0.3f; -static const CGFloat kBubbleTextHInset = 6.f; -static const CGFloat kBubbleTextVInset = 6.f; - -@interface OWSBubbleView : UIView - -@property (nonatomic) BOOL isOutgoing; -@property (nonatomic) BOOL hideTail; -//@property (nonatomic, nullable, weak) UIView *maskedSubview; -@property (nonatomic) CAShapeLayer *maskLayer; - -//@property (nonatomic) BOOL isOutgoing; -@property (nonatomic) CAShapeLayer *shapeLayer; -@property (nonatomic, nullable) UIColor *bubbleColor; - -@end - -#pragma mark - - -@implementation OWSBubbleView - -- (void)setIsOutgoing:(BOOL)isOutgoing { - BOOL didChange = _isOutgoing != isOutgoing; - - _isOutgoing = isOutgoing; - - if (didChange || !self.shapeLayer) { - [self updateMask]; - } -} - -- (void)setFrame:(CGRect)frame -{ - BOOL didChange = !CGSizeEqualToSize(self.frame.size, frame.size); - - [super setFrame:frame]; - - if (didChange || !self.shapeLayer) { - [self updateMask]; - } -} - -- (void)setBounds:(CGRect)bounds -{ - BOOL didChange = !CGSizeEqualToSize(self.bounds.size, bounds.size); - - [super setBounds:bounds]; - - if (didChange || !self.shapeLayer) { - [self updateMask]; - } -} - -- (void)setBubbleColor:(nullable UIColor *)bubbleColor -{ - _bubbleColor = bubbleColor; - - if (!self.shapeLayer) { - [self updateMask]; - } - self.shapeLayer.fillColor = bubbleColor.CGColor; -} - -- (void)updateMask -{ - if (!self.shapeLayer) { - self.shapeLayer = [CAShapeLayer new]; - [self.layer addSublayer:self.shapeLayer]; - } - if (!self.maskLayer) { - self.maskLayer = [CAShapeLayer new]; - self.layer.mask = self.maskLayer; - } - - UIBezierPath *bezierPath = [self.class maskPathForSize:self.bounds.size - isOutgoing:self.isOutgoing - isRTL:self.isRTL]; - - self.shapeLayer.fillColor = self.bubbleColor.CGColor; - self.shapeLayer.path = bezierPath.CGPath; - - self.maskLayer.path = bezierPath.CGPath; -} - -+ (UIBezierPath *)maskPathForSize:(CGSize)size - isOutgoing:(BOOL)isOutgoing - 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)]; - - // Thorn Tip - CGPoint thornTip = CGPointMake(size.width, - size.height); - CGPoint thornA = CGPointMake(bubbleRight - kBubbleHRounding, bubbleBottom); - CGPoint thornB = CGPointMake(bubbleRight, bubbleBottom - kBubbleVRounding); - [bezierPath moveToPoint:thornTip]; - // [bezierPath addLineToPoint:CGPointMake(bubbleRight - kBubbleHRounding * 0.85f, bubbleBottom)]; - // [bezierPath addLineToPoint:thornA]; - // [bezierPath addLineToPoint:thornB]; - // [bezierPath addLineToPoint:CGPointMake(bubbleRight, bubbleBottom - kBubbleVRounding * 0.5f)]; - [bezierPath addQuadCurveToPoint:CGPointMake(bubbleRight - kBubbleHRounding * 0.8f, bubbleBottom) - controlPoint:CGPointMake(bubbleRight - kBubbleHRounding * 0.4f, bubbleBottom)]; - [bezierPath addLineToPoint:thornA]; - [bezierPath addLineToPoint:thornB]; - [bezierPath addLineToPoint:CGPointMake(bubbleRight, bubbleBottom - kBubbleVRounding * 0.7f)]; - [bezierPath addQuadCurveToPoint:thornTip - controlPoint:CGPointMake(bubbleRight, bubbleBottom - kBubbleVRounding * 0.3f)]; - - // // Thorn Tip - // [bezierPath moveToPoint:CGPointMake(bubbleRight - kBubbleHRounding, bubbleBottom)]; - // [bezierPath addLineToPoint:CGPointMake(bubbleRight, bubbleBottom - kBubbleVRounding)]; - // [bezierPath addQuadCurveToPoint:CGPointMake(bubbleRight + kBubbleThornSideInset, bubbleBottom - 0.f) - // controlPoint:CGPointMake(bubbleRight, bubbleBottom)]; - // // [bezierPath addQuadCurveToPoint:CGPointMake(bubbleRight + kBubbleThornSideInset - 1.f, bubbleBottom - - // 0.5f) - // // controlPoint:CGPointMake(bubbleRight, bubbleBottom)]; - // // [bezierPath addQuadCurveToPoint:CGPointMake(bubbleRight + kBubbleThornSideInset, bubbleBottom) - // // controlPoint:CGPointMake(bubbleRight + kBubbleThornSideInset, bubbleBottom - 0.5f)]; - // [bezierPath addLineToPoint:CGPointMake(bubbleRight + kBubbleThornSideInset, bubbleBottom)]; - - // // Thorn Tip - // CGFloat kThornPinchingA = 0.f; - // CGFloat kThornPinchingB = 3.5f; - // CGPoint thornTip = CGPointMake(self.width, - // self.height); - // CGPoint thornA = CGPointMake(bubbleRight - kBubbleHRounding, bubbleBottom - kThornPinchingA); - // CGPoint thornB = CGPointMake(bubbleRight - kThornPinchingB, bubbleBottom - kBubbleVRounding); - // [bezierPath moveToPoint:thornTip]; - // [bezierPath addQuadCurveToPoint:thornA - // controlPoint:CGPointMake(bubbleRight - kBubbleHRounding, bubbleBottom - kThornPinchingA)]; - // [bezierPath addLineToPoint:thornB]; - // [bezierPath addQuadCurveToPoint:thornTip - // controlPoint:CGPointMake(bubbleRight - kThornPinchingB, bubbleBottom - kBubbleVRounding * 0.1f)]; - - // Thorn Tip - // CGFloat kThornPinchingA = 0.f; - // CGFloat kThornPinchingB = 3.5f; - // CGPoint thornA = CGPointMake(bubbleRight, bubbleBottom - kBubbleVRounding * 1.65f); - // CGPoint thornB = CGPointMake(bubbleRight, bubbleBottom - kBubbleVRounding * 1.f); - - // CGPoint thornA = CGPointMake(bubbleRight, bubbleTop + kBubbleVRounding * 1.f); - // CGPoint thornB = CGPointMake(bubbleRight, bubbleTop + kBubbleVRounding * 1.65f); - // CGPoint thornTip = CGPointMake(bubbleRight + kBubbleThornSideInset * 0.85f, - // (thornA.y + thornB.y) * 0.5f); - // [bezierPath moveToPoint:thornTip]; - // [bezierPath addLineToPoint:thornA]; - // [bezierPath addLineToPoint:thornB]; - - // [bezierPath addQuadCurveToPoint:thornA - // controlPoint:CGPointMake(bubbleRight - kBubbleHRounding, bubbleBottom - kThornPinchingA)]; - // [bezierPath addLineToPoint:thornB]; - // [bezierPath addQuadCurveToPoint:thornTip - // controlPoint:CGPointMake(bubbleRight - kThornPinchingB, bubbleBottom - kBubbleVRounding * 0.1f)]; - - // 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 - -#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. @@ -481,7 +218,6 @@ static const CGFloat kBubbleTextVInset = 6.f; [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTapGesture:)]; [self.bubbleView addGestureRecognizer:tap]; - UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleLongPressGesture:)]; [self.bubbleView addGestureRecognizer:longPress]; @@ -684,6 +420,11 @@ static const CGFloat kBubbleTextVInset = 6.f; OWSAssert(self.viewItem.interaction); OWSAssert([self.viewItem.interaction isKindOfClass:[TSMessage class]]); + // TODO: We might not need to hide it. + self.bubbleView.hidden = NO; + self.bubbleView.isOutgoing = self.isOutgoing; + // TODO: Hide tails/thorns here? + if (self.shouldHaveFailedSendBadge) { self.failedSendBadgeView = [UIImageView new]; self.failedSendBadgeView.image = @@ -722,6 +463,7 @@ static const CGFloat kBubbleTextVInset = 6.f; } 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; } //<<<<<<< HEAD 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/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 From e1e6606783873711053ce95e5198f35f2c8d8d59 Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Wed, 28 Mar 2018 10:11:01 -0400 Subject: [PATCH 16/31] Bubble collapse. --- .../ConversationView/Cells/OWSMessageCell.m | 506 ++---------------- 1 file changed, 53 insertions(+), 453 deletions(-) diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m index c2fc47ba01..f164a521bb 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m @@ -33,40 +33,18 @@ CG_INLINE CGSize CGSizeCeil(CGSize size) // The non-nullable properties are so frequently used that it's easier // to always keep one around. -// The contentView contains: +// The cell's contentView contains: // // * MessageView (message) // * dateHeaderLabel (above message) // * footerView (below message) -// * failedSendBadgeView (beside message) +// * failedSendBadgeView ("trailing" beside message) @property (nonatomic) OWSBubbleView *bubbleView; -//@property (nonatomic) UIView *messageWrapperView; - - -//@property (nonatomic) UIView *payloadView; -//@property (nonatomic) BubbleMaskingView *bubbleView; -////@property (nonatomic) BubbleMaskingView *mediaMaskingView; @property (nonatomic) UILabel *dateHeaderLabel; @property (nonatomic) OWSMessageTextView *bodyTextViewCached; @property (nonatomic, nullable) UIImageView *failedSendBadgeView; -//@property (nonatomic, nullable) UILabel *tapForMoreLabel; -////<<<<<<< HEAD -////@property (nonatomic, nullable) UIImageView *textBubbleImageView; -////||||||| merged common ancestors -////@property (nonatomic, nullable) UIImageView *bubbleImageView; -////======= -////@property (nonatomic, nullable) BubbleFillView *bubbleFillView; -//////@property (nonatomic, nullable) UIImageView *bubbleImageView; -////>>>>>>> SQUASHED -//@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; @@ -79,12 +57,7 @@ CG_INLINE CGSize CGSizeCeil(CGSize size) // Should unload all expensive view contents (images, etc.). @property (nonatomic, nullable) dispatch_block_t unloadCellContentBlock; -// TODO: Review -// TODO: Rename to cellcont -@property (nonatomic, nullable) NSArray *cellContentConstraints; -@property (nonatomic, nullable) NSArray *dateHeaderConstraints; -@property (nonatomic, nullable) NSMutableArray *bubbleContentConstraints; -@property (nonatomic, nullable) NSArray *footerConstraints; +@property (nonatomic, nullable) NSMutableArray *viewConstraints; @property (nonatomic) BOOL isPresentingMenuController; @@ -106,7 +79,7 @@ CG_INLINE CGSize CGSizeCeil(CGSize size) { OWSAssert(!self.bodyTextViewCached); - _bubbleContentConstraints = [NSMutableArray new]; + _viewConstraints = [NSMutableArray new]; self.layoutMargins = UIEdgeInsetsZero; self.contentView.layoutMargins = UIEdgeInsetsZero; @@ -432,119 +405,39 @@ CG_INLINE CGSize CGSizeCeil(CGSize size) self.failedSendBadgeView.tintColor = [UIColor ows_destructiveRedColor]; [self.contentView addSubview:self.failedSendBadgeView]; - self.cellContentConstraints = @[ + [self.viewConstraints addObjectsFromArray:@[ [self.bubbleView autoPinLeadingToSuperview], [self.failedSendBadgeView autoPinLeadingToTrailingOfView:self.bubbleView], [self.failedSendBadgeView autoAlignAxis:ALAxisHorizontal toSameAxisOfView:self.bubbleView], - // [self.payloadView autoPinLeadingToSuperview], - // [self.failedSendBadgeView autoPinLeadingToTrailingOfView:self.payloadView], [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.cellContentConstraints = @[ + [self.viewConstraints addObjectsFromArray:@[ [self.bubbleView autoPinLeadingToSuperview], [self.bubbleView autoPinTrailingToSuperview], - // [self.bubbleView autoPinLeadingToSuperview], - // [self.bubbleView autoPinWidthToSuperview], - ]; - // self.cellContentConstraints = [self.payloadView autoPinWidthToSuperview]; + ]]; } - // JSQMessagesBubbleImage *_Nullable bubbleImageData; if ([self.viewItem.interaction isKindOfClass:[TSMessage class]] && self.hasNonImageBodyContent) { TSMessage *message = (TSMessage *)self.viewItem.interaction; - // bubbleImageData = [self.bubbleFactory bubbleWithMessage:message]; - // TODO: self.bubbleView.bubbleColor = [self.bubbleFactory bubbleColorWithMessage:message]; - // self.bubbleFillView.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; } - //<<<<<<< HEAD - // self.textBubbleImageView.image = bubbleImageData.messageBubbleImage; - //||||||| merged common ancestors - // self.bubbleImageView.image = bubbleImageData.messageBubbleImage; - //======= - // // TODO: - //// self.bubbleImageView.image = bubbleImageData.messageBubbleImage; - // self.bubbleFillView.isOutgoing = self.isOutgoing; - //>>>>>>> SQUASHED - [self updateDateHeader]; [self updateFooter]; - // NSMutableArray *contentViews = [NSMutableArray new]; - // - // 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]; - // break; - // case OWSMessageCellType_AnimatedImage: - // OWSAssert(self.viewItem.attachmentStream); - // [self loadForAnimatedImageDisplay]; - // break; - // case OWSMessageCellType_Audio: - // OWSAssert(self.viewItem.attachmentStream); - // [self loadForAudioDisplay]; - // break; - // case OWSMessageCellType_Video: - // OWSAssert(self.viewItem.attachmentStream); - // [self loadForVideoDisplay]; - // break; - // case OWSMessageCellType_GenericAttachment: { - // OWSAssert(self.viewItem.attachmentStream); - // OWSGenericAttachmentView *attachmentView = - // [[OWSGenericAttachmentView alloc] initWithAttachment:self.attachmentStream - // isIncoming:self.isIncoming]; - // [attachmentView createContents]; - // [self setMediaView:attachmentView]; - // [self addAttachmentUploadViewIfNecessary:attachmentView]; - // [self addCaptionIfNecessary]; - // break; - // } - // case OWSMessageCellType_DownloadingAttachment: { - // [self loadForDownloadingAttachment]; - // [self addCaptionIfNecessary]; - // break; - // } - // } - - // [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.textBottomMargin], [self.tapForMoreLabel - // autoSetDimension:ALDimensionHeight - // toSize:self.tapForMoreHeight], - - - // Do we need to pin the bubble size? + // TODO: Do we need to pin the bubble size? { // - (CGSize)cellSizeForViewWidth:(int)viewWidth contentWidth:(int)contentWidth // CGSize mediaSize = [self bodyMediaSizeForContentWidth:self.contentWidth]; // TODO: - // [self.bubbleContentConstraints addObjectsFromArray:[self.mediaMaskingView + // [self.viewConstraints addObjectsFromArray:[self.mediaMaskingView // autoSetDimensionsToSize:mediaSize]]; } @@ -621,22 +514,22 @@ CG_INLINE CGSize CGSizeCeil(CGSize size) [self.bubbleView addSubview:bodyMediaView]; if (bodyMediaViewHasGreedyWidth) { - [self.bubbleContentConstraints addObjectsFromArray:@[ + [self.viewConstraints addObjectsFromArray:@[ [bodyMediaView autoPinLeadingToSuperviewWithMargin:0], [bodyMediaView autoPinTrailingToSuperviewWithMargin:0], ]]; } else { CGFloat aspectRatio = bodyMediaSize.width / bodyMediaSize.height; - [self.bubbleContentConstraints addObjectsFromArray:@[ + [self.viewConstraints addObjectsFromArray:@[ [bodyMediaView autoHCenterInSuperview], [bodyMediaView autoPinToAspectRatio:aspectRatio], ]]; } if (lastSubview) { - [self.bubbleContentConstraints + [self.viewConstraints addObject:[bodyMediaView autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:lastSubview withOffset:0]]; } else { - [self.bubbleContentConstraints addObject:[bodyMediaView autoPinEdgeToSuperviewEdge:ALEdgeTop withInset:0]]; + [self.viewConstraints addObject:[bodyMediaView autoPinEdgeToSuperviewEdge:ALEdgeTop withInset:0]]; } lastSubview = bodyMediaView; bottomMargin = 0; @@ -650,17 +543,17 @@ CG_INLINE CGSize CGSizeCeil(CGSize size) } if (bodyTextView) { [self.bubbleView addSubview:bodyTextView]; - [self.bubbleContentConstraints addObjectsFromArray:@[ + [self.viewConstraints addObjectsFromArray:@[ [bodyTextView autoPinLeadingToSuperviewWithMargin:self.textLeadingMargin], [bodyTextView autoPinTrailingToSuperviewWithMargin:self.textTrailingMargin], ]]; if (lastSubview) { - [self.bubbleContentConstraints addObject:[bodyTextView autoPinEdge:ALEdgeTop - toEdge:ALEdgeBottom - ofView:lastSubview - withOffset:self.textTopMargin]]; + [self.viewConstraints addObject:[bodyTextView autoPinEdge:ALEdgeTop + toEdge:ALEdgeBottom + ofView:lastSubview + withOffset:self.textTopMargin]]; } else { - [self.bubbleContentConstraints + [self.viewConstraints addObject:[bodyTextView autoPinEdgeToSuperviewEdge:ALEdgeTop withInset:self.textTopMargin]]; } lastSubview = bodyTextView; @@ -672,7 +565,7 @@ CG_INLINE CGSize CGSizeCeil(CGSize size) OWSAssert(lastSubview); OWSAssert(lastSubview == bodyTextView); [self.bubbleView addSubview:tapForMoreLabel]; - [self.bubbleContentConstraints addObjectsFromArray:@[ + [self.viewConstraints addObjectsFromArray:@[ [tapForMoreLabel autoPinLeadingToSuperviewWithMargin:self.textLeadingMargin], [tapForMoreLabel autoPinTrailingToSuperviewWithMargin:self.textTrailingMargin], [tapForMoreLabel autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:lastSubview], @@ -683,7 +576,7 @@ CG_INLINE CGSize CGSizeCeil(CGSize size) } OWSAssert(lastSubview); - [self.bubbleContentConstraints addObjectsFromArray:@[ + [self.viewConstraints addObjectsFromArray:@[ [lastSubview autoPinEdgeToSuperviewEdge:ALEdgeBottom withInset:bottomMargin], ]]; @@ -734,7 +627,7 @@ CG_INLINE CGSize CGSizeCeil(CGSize size) { // CGSize mediaSize = [self bodyMediaSizeForContentWidth:self.contentWidth]; // TODO: - // [self.bubbleContentConstraints addObjectsFromArray:[self.mediaMaskingView autoSetDimensionsToSize:mediaSize]]; + // [self.viewConstraints addObjectsFromArray:[self.mediaMaskingView autoSetDimensionsToSize:mediaSize]]; if (!self.isCellVisible) { // Eagerly unload. @@ -748,60 +641,6 @@ CG_INLINE CGSize CGSizeCeil(CGSize size) self.loadCellContentBlock(); } } - - // switch (self.cellType) { - // case OWSMessageCellType_StillImage: { - // if (self.stillImageView.image) { - // return; - // } - // self.stillImageView.image = [self tryToLoadCellMedia:^{ - // OWSAssert([self.attachmentStream isImage]); - // return self.attachmentStream.image; - // } - // mediaView:self.stillImageView - // cacheKey:self.attachmentStream.uniqueId]; - // break; - // } - // case OWSMessageCellType_AnimatedImage: { - // if (self.animatedImageView.image) { - // return; - // } - // self.animatedImageView.image = [self tryToLoadCellMedia:^{ - // OWSAssert([self.attachmentStream isAnimated]); - // - // NSString *_Nullable filePath = [self.attachmentStream filePath]; - // YYImage *_Nullable animatedImage = nil; - // if (filePath && [NSData ows_isValidImageAtPath:filePath]) { - // animatedImage = [YYImage imageWithContentsOfFile:filePath]; - // } - // return animatedImage; - // } - // mediaView:self.animatedImageView - // cacheKey:self.attachmentStream.uniqueId]; - // break; - // } - // case OWSMessageCellType_Video: { - // if (self.stillImageView.image) { - // return; - // } - // self.stillImageView.image = [self tryToLoadCellMedia:^{ - // OWSAssert([self.attachmentStream isVideo]); - // - // return self.attachmentStream.image; - // } - // mediaView:self.stillImageView - // cacheKey:self.attachmentStream.uniqueId]; - // break; - // } - // case OWSMessageCellType_TextMessage: - // case OWSMessageCellType_OversizeTextMessage: - // case OWSMessageCellType_GenericAttachment: - // case OWSMessageCellType_DownloadingAttachment: - // case OWSMessageCellType_Audio: - // case OWSMessageCellType_Unknown: - // // Inexpensive cell types don't need to lazy-load or eagerly-unload. - // break; - // } } - (void)updateDateHeader @@ -852,7 +691,7 @@ CG_INLINE CGSize CGSizeCeil(CGSize size) 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], @@ -860,13 +699,13 @@ CG_INLINE CGSize CGSizeCeil(CGSize size) : [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], - ]; + ]]; } } @@ -914,9 +753,9 @@ CG_INLINE CGSize CGSizeCeil(CGSize size) 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; } @@ -934,33 +773,29 @@ CG_INLINE CGSize CGSizeCeil(CGSize size) 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); } @@ -1019,118 +854,10 @@ CG_INLINE CGSize CGSizeCeil(CGSize size) }; textView.dataDetectorTypes = (UIDataDetectorTypeLink | UIDataDetectorTypeAddress | UIDataDetectorTypeCalendarEvent); textView.shouldIgnoreEvents = shouldIgnoreEvents; - - // OWSAssert(self.contentWidth); - // CGSize textBubbleSize = [self textBubbleSizeForContentWidth:self.contentWidth]; - // - // 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]; - // <<<<<<< HEAD - // [self.textBubbleImageView addSubview:self.tapForMoreLabel]; - // ||||||| merged common ancestors - // [self.bubbleImageView addSubview:self.tapForMoreLabel]; - // ======= - // [self.bubbleFillView addSubview:self.tapForMoreLabel]; - // >>>>>>> SQUASHED - // - // [self.bubbleContentConstraints 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.textTopMargin], - // - // [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.textBottomMargin], [self.tapForMoreLabel - // autoSetDimension:ALDimensionHeight - // toSize:self.tapForMoreHeight], - // ]]; - // } else { - // [self.bubbleContentConstraints 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], - // <<<<<<< HEAD - // [self.textView autoPinEdgeToSuperviewEdge:ALEdgeTop - // withInset:self.textVMargin], [self.textView - // autoPinEdgeToSuperviewEdge:ALEdgeBottom - // withInset:self.textVMargin], - // ]]; - // ||||||| merged common ancestors - // [self.textView autoPinEdgeToSuperviewEdge:ALEdgeTop withInset:self.textVMargin], - // [self.textView autoPinEdgeToSuperviewEdge:ALEdgeBottom withInset:self.textVMargin], - // ]; - // ======= - // [self.textView autoPinEdgeToSuperviewEdge:ALEdgeTop withInset:self.textTopMargin], - // [self.textView autoPinEdgeToSuperviewEdge:ALEdgeBottom withInset:self.textBottomMargin], - // ]; - // >>>>>>> SQUASHED - // } } - (nullable UIView *)createTapForMoreLabelIfNecessary { - // //<<<<<<< HEAD - // // self.textBubbleImageView.hidden = NO; - // //||||||| merged common ancestors - // // self.bubbleImageView.hidden = NO; - // //======= - // // self.bubbleFillView.hidden = NO; - // //// self.bubbleImageView.hidden = NO; - // //>>>>>>> SQUASHED - // textView.hidden = NO; - // textView.text = text; - // textView.textColor = textColor; - // - // // Honor dynamic type in the message bodies. - // textView.font = font; - // textView.linkTextAttributes = @{ - // NSForegroundColorAttributeName : textColor, - // NSUnderlineStyleAttributeName : @(NSUnderlineStyleSingle | - // NSUnderlinePatternSolid) - // }; - // textView.dataDetectorTypes - // = (UIDataDetectorTypeLink | UIDataDetectorTypeAddress | UIDataDetectorTypeCalendarEvent); - // - // 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; - // } - // - // OWSAssert(self.contentWidth); - // CGSize textBubbleSize = [self textBubbleSizeForContentWidth:self.contentWidth]; - if (!self.hasText) { return nil; } @@ -1144,64 +871,6 @@ CG_INLINE CGSize CGSizeCeil(CGSize size) tapForMoreLabel.font = [self tapForMoreFont]; tapForMoreLabel.textColor = [self.textColor colorWithAlphaComponent:0.85]; tapForMoreLabel.textAlignment = [tapForMoreLabel textAlignmentUnnatural]; - // <<<<<<< HEAD - // [self.textBubbleImageView addSubview:self.tapForMoreLabel]; - // ||||||| merged common ancestors - // [self.bubbleImageView addSubview:self.tapForMoreLabel]; - // ======= - // [self.bubbleFillView addSubview:self.tapForMoreLabel]; - // >>>>>>> SQUASHED - // - // [self.bubbleContentConstraints 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.textTopMargin], - // - // [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.textBottomMargin], [self.tapForMoreLabel - // autoSetDimension:ALDimensionHeight - // toSize:self.tapForMoreHeight], - // ]]; - // } else { - // [self.bubbleContentConstraints 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], - // <<<<<<< HEAD - // [self.textView autoPinEdgeToSuperviewEdge:ALEdgeTop - // withInset:self.textVMargin], [self.textView - // autoPinEdgeToSuperviewEdge:ALEdgeBottom - // withInset:self.textVMargin], - // ]]; - // ||||||| merged common ancestors - // [self.textView autoPinEdgeToSuperviewEdge:ALEdgeTop withInset:self.textVMargin], - // [self.textView autoPinEdgeToSuperviewEdge:ALEdgeBottom withInset:self.textVMargin], - // ]; - // ======= - // [self.textView autoPinEdgeToSuperviewEdge:ALEdgeTop withInset:self.textTopMargin], - // [self.textView autoPinEdgeToSuperviewEdge:ALEdgeBottom withInset:self.textBottomMargin], - // ]; - // >>>>>>> SQUASHED - // } return tapForMoreLabel; } @@ -1288,17 +957,6 @@ CG_INLINE CGSize CGSizeCeil(CGSize size) return animatedImageView; } -//// TODO: -//- (void)addCaptionIfNecessary -//{ -// if (self.hasText) { -// [self loadForTextDisplay]; -// } else { -// [self.bubbleContentConstraints addObject:[self.textBubbleImageView autoSetDimension:ALDimensionHeight -// toSize:0]]; -// } -//} - - (UIView *)loadViewForAudio { OWSAssert(self.attachmentStream); @@ -1429,25 +1087,6 @@ CG_INLINE CGSize CGSizeCeil(CGSize size) return customView; } -//- (void)setMediaView:(UIView *)view -//{ -// OWSAssert(view); -// -// view.userInteractionEnabled = NO; -// [self.mediaMaskingView addSubview:view]; -// -// [self.bubbleContentConstraints -// addObject:[self.mediaMaskingView -// autoPinEdgeToSuperviewEdge:(self.isIncoming ? ALEdgeLeading : ALEdgeTrailing)]]; -// -// [self.bubbleContentConstraints addObjectsFromArray:[view autoPinEdgesToSuperviewMargins]]; -// -// [self cropMediaViewToBubbbleShape:view]; -// if (self.isMediaBeingSent) { -// view.layer.opacity = 0.75f; -// } -//} - - (void)addAttachmentUploadViewIfNecessary:(UIView *)attachmentView { [self addAttachmentUploadViewIfNecessary:attachmentView @@ -1717,14 +1356,8 @@ CG_INLINE CGSize CGSizeCeil(CGSize size) { [super prepareForReuse]; - [NSLayoutConstraint deactivateConstraints:self.cellContentConstraints]; - self.cellContentConstraints = nil; - [NSLayoutConstraint deactivateConstraints:self.bubbleContentConstraints]; - self.bubbleContentConstraints = [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; @@ -1734,30 +1367,11 @@ CG_INLINE CGSize CGSizeCeil(CGSize size) self.bodyTextViewCached.dataDetectorTypes = UIDataDetectorTypeNone; [self.failedSendBadgeView removeFromSuperview]; self.failedSendBadgeView = nil; - // [self.tapForMoreLabel removeFromSuperview]; - // self.tapForMoreLabel = nil; self.footerLabel.text = nil; self.footerLabel.hidden = YES; - // TODO: self.bubbleView.hidden = YES; self.bubbleView.bubbleColor = nil; - //<<<<<<< HEAD - // self.textBubbleImageView.image = nil; - // self.textBubbleImageView.hidden = YES; - // self.mediaMaskingView.maskedSubview = nil; - // self.mediaMaskingView.hideTail = NO; - // self.mediaMaskingView.layoutMargins = UIEdgeInsetsZero; - //||||||| merged common ancestors - // self.bubbleImageView.image = nil; - // self.bubbleImageView.hidden = YES; - // self.payloadView.maskedSubview = nil; - //======= - // self.bubbleFillView.hidden = YES; - //// self.bubbleImageView.image = nil; - //// self.bubbleImageView.hidden = YES; - // self.payloadView.maskedSubview = nil; - //>>>>>>> SQUASHED for (UIView *subview in self.bubbleView.subviews) { [subview removeFromSuperview]; @@ -1769,20 +1383,6 @@ CG_INLINE CGSize CGSizeCeil(CGSize size) self.loadCellContentBlock = nil; self.unloadCellContentBlock = nil; - // [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.expirationTimerView clearAnimations]; [self.expirationTimerView removeFromSuperview]; self.expirationTimerView = nil; From 11819d9b8a5894fb13b540d2f1ea81ca9b66c66d Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Wed, 28 Mar 2018 10:33:17 -0400 Subject: [PATCH 17/31] Bubble collapse. --- .../ViewControllers/DebugUI/DebugUIMessages.m | 201 ++++++++++++++---- .../DebugUI/DebugUIMessagesAssetLoader.h | 7 +- .../DebugUI/DebugUIMessagesAssetLoader.m | 49 ++++- .../utils/OWSMessagesBubbleImageFactory.swift | 1 - 4 files changed, 214 insertions(+), 44 deletions(-) diff --git a/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m b/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m index bae44a2970..145c46bd7c 100644 --- a/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m +++ b/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m @@ -550,29 +550,55 @@ NS_ASSUME_NONNULL_BEGIN thread:thread]; } -+ (DebugUIMessagesAction *)fakeOutgoingPortraitPngAction:(TSThread *)thread - messageState:(TSOutgoingMessageState)messageState - hasCaption:(BOOL)hasCaption ++ (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 landscapePngInstance] + fakeAssetLoader:[DebugUIMessagesAssetLoader compactLandscapePngInstance] thread:thread]; } -+ (DebugUIMessagesAction *)fakeOutgoingLandscapePngAction:(TSThread *)thread - messageState:(TSOutgoingMessageState)messageState - hasCaption:(BOOL)hasCaption ++ (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 portraitPngInstance] + 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]; } @@ -589,6 +615,19 @@ NS_ASSUME_NONNULL_BEGIN 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 *)fakeOutgoingTinyPdfAction:(TSThread *)thread messageState:(TSOutgoingMessageState)messageState hasCaption:(BOOL)hasCaption @@ -813,29 +852,55 @@ NS_ASSUME_NONNULL_BEGIN thread:thread]; } -+ (DebugUIMessagesAction *)fakeIncomingPortraitPngAction:(TSThread *)thread - isAttachmentDownloaded:(BOOL)isAttachmentDownloaded - hasCaption:(BOOL)hasCaption ++ (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 portraitPngInstance] + fakeAssetLoader:[DebugUIMessagesAssetLoader compactPortraitPngInstance] thread:thread]; } -+ (DebugUIMessagesAction *)fakeIncomingLandscapePngAction:(TSThread *)thread - isAttachmentDownloaded:(BOOL)isAttachmentDownloaded - hasCaption:(BOOL)hasCaption ++ (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 landscapePngInstance] + 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]; } @@ -852,6 +917,19 @@ NS_ASSUME_NONNULL_BEGIN 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 *)fakeIncomingTinyPdfAction:(TSThread *)thread isAttachmentDownloaded:(BOOL)isAttachmentDownloaded hasCaption:(BOOL)hasCaption @@ -1052,22 +1130,60 @@ NS_ASSUME_NONNULL_BEGIN [self fakeOutgoingMp4Action:thread messageState:TSOutgoingMessageStateSentToService hasCaption:NO], [self fakeOutgoingMp4Action:thread messageState:TSOutgoingMessageStateSentToService hasCaption:YES], // - [self fakeOutgoingLandscapePngAction:thread messageState:TSOutgoingMessageStateAttemptingOut hasCaption:NO], - [self fakeOutgoingLandscapePngAction:thread messageState:TSOutgoingMessageStateAttemptingOut hasCaption:YES], - [self fakeOutgoingLandscapePngAction:thread messageState:TSOutgoingMessageStateUnsent hasCaption:NO], - [self fakeOutgoingLandscapePngAction:thread messageState:TSOutgoingMessageStateUnsent hasCaption:YES], - [self fakeOutgoingLandscapePngAction:thread messageState:TSOutgoingMessageStateSentToService hasCaption:NO], - [self fakeOutgoingLandscapePngAction:thread messageState:TSOutgoingMessageStateSentToService hasCaption:YES], + [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], // - [self fakeOutgoingPortraitPngAction:thread messageState:TSOutgoingMessageStateAttemptingOut hasCaption:NO], - [self fakeOutgoingPortraitPngAction:thread messageState:TSOutgoingMessageStateAttemptingOut hasCaption:YES], - [self fakeOutgoingPortraitPngAction:thread messageState:TSOutgoingMessageStateUnsent hasCaption:NO], - [self fakeOutgoingPortraitPngAction:thread messageState:TSOutgoingMessageStateUnsent hasCaption:YES], - [self fakeOutgoingPortraitPngAction:thread messageState:TSOutgoingMessageStateSentToService hasCaption:NO], - [self fakeOutgoingPortraitPngAction:thread messageState:TSOutgoingMessageStateSentToService hasCaption:YES], + [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], // + [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], + // + [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], + // + [self fakeOutgoingLargePngAction:thread messageState:TSOutgoingMessageStateSentToService hasCaption:NO], [self fakeOutgoingLargePngAction:thread messageState:TSOutgoingMessageStateSentToService hasCaption:YES], // + [self fakeOutgoingTinyPngAction:thread messageState:TSOutgoingMessageStateSentToService hasCaption:NO], + [self fakeOutgoingTinyPngAction:thread messageState:TSOutgoingMessageStateSentToService hasCaption:YES], + // [self fakeOutgoingTinyPdfAction:thread messageState:TSOutgoingMessageStateAttemptingOut hasCaption:NO], [self fakeOutgoingTinyPdfAction:thread messageState:TSOutgoingMessageStateAttemptingOut hasCaption:YES], [self fakeOutgoingTinyPdfAction:thread messageState:TSOutgoingMessageStateUnsent hasCaption:NO], @@ -1103,19 +1219,32 @@ NS_ASSUME_NONNULL_BEGIN [self fakeIncomingMp4Action:thread isAttachmentDownloaded:NO hasCaption:YES], [self fakeIncomingMp4Action:thread isAttachmentDownloaded:YES hasCaption:YES], // - [self fakeIncomingLandscapePngAction:thread isAttachmentDownloaded:NO hasCaption:NO], - [self fakeIncomingLandscapePngAction:thread isAttachmentDownloaded:YES hasCaption:NO], - [self fakeIncomingLandscapePngAction:thread isAttachmentDownloaded:NO hasCaption:YES], - [self fakeIncomingLandscapePngAction:thread isAttachmentDownloaded:YES hasCaption:YES], + [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], // - [self fakeIncomingPortraitPngAction:thread isAttachmentDownloaded:NO hasCaption:NO], - [self fakeIncomingPortraitPngAction:thread isAttachmentDownloaded:YES hasCaption:NO], - [self fakeIncomingPortraitPngAction:thread isAttachmentDownloaded:NO hasCaption:YES], - [self fakeIncomingPortraitPngAction:thread isAttachmentDownloaded:YES hasCaption:YES], + [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], + // + [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], + // + [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], // [self fakeIncomingLargePngAction:thread isAttachmentDownloaded:YES hasCaption:NO], [self fakeIncomingLargePngAction:thread isAttachmentDownloaded:YES hasCaption:YES], // + [self fakeIncomingTinyPngAction:thread isAttachmentDownloaded:YES hasCaption:NO], + [self fakeIncomingTinyPngAction:thread isAttachmentDownloaded:YES hasCaption:YES], + // [self fakeIncomingTinyPdfAction:thread isAttachmentDownloaded:NO hasCaption:NO], [self fakeIncomingTinyPdfAction:thread isAttachmentDownloaded:YES hasCaption:NO], [self fakeIncomingTinyPdfAction:thread isAttachmentDownloaded:NO hasCaption:YES], diff --git a/Signal/src/ViewControllers/DebugUI/DebugUIMessagesAssetLoader.h b/Signal/src/ViewControllers/DebugUI/DebugUIMessagesAssetLoader.h index a16ca58e95..d6ce5dbdbd 100644 --- a/Signal/src/ViewControllers/DebugUI/DebugUIMessagesAssetLoader.h +++ b/Signal/src/ViewControllers/DebugUI/DebugUIMessagesAssetLoader.h @@ -21,9 +21,12 @@ NS_ASSUME_NONNULL_BEGIN + (instancetype)gifInstance; + (instancetype)mp3Instance; + (instancetype)mp4Instance; -+ (instancetype)portraitPngInstance; -+ (instancetype)landscapePngInstance; ++ (instancetype)compactPortraitPngInstance; ++ (instancetype)compactLandscapePngInstance; ++ (instancetype)tallPortraitPngInstance; ++ (instancetype)wideLandscapePngInstance; + (instancetype)largePngInstance; ++ (instancetype)tinyPngInstance; + (instancetype)tinyPdfInstance; + (instancetype)largePdfInstance; + (instancetype)missingPngInstance; diff --git a/Signal/src/ViewControllers/DebugUI/DebugUIMessagesAssetLoader.m b/Signal/src/ViewControllers/DebugUI/DebugUIMessagesAssetLoader.m index 03ce21a75f..d16ced0ca6 100644 --- a/Signal/src/ViewControllers/DebugUI/DebugUIMessagesAssetLoader.m +++ b/Signal/src/ViewControllers/DebugUI/DebugUIMessagesAssetLoader.m @@ -360,12 +360,12 @@ NS_ASSUME_NONNULL_BEGIN return instance; } -+ (instancetype)portraitPngInstance ++ (instancetype)compactPortraitPngInstance { static DebugUIMessagesAssetLoader *instance = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ - instance = [DebugUIMessagesAssetLoader fakePngAssetLoaderWithImageSize:CGSizeMake(10, 100) + instance = [DebugUIMessagesAssetLoader fakePngAssetLoaderWithImageSize:CGSizeMake(60, 100) backgroundColor:[UIColor blueColor] textColor:[UIColor whiteColor] label:@"P"]; @@ -373,13 +373,39 @@ NS_ASSUME_NONNULL_BEGIN return instance; } -+ (instancetype)landscapePngInstance ++ (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 greenColor] + backgroundColor:[UIColor purpleColor] textColor:[UIColor whiteColor] label:@"L"]; }); @@ -392,13 +418,26 @@ NS_ASSUME_NONNULL_BEGIN static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ instance = [DebugUIMessagesAssetLoader fakePngAssetLoaderWithImageSize:CGSizeMake(4000, 4000) - backgroundColor:[UIColor redColor] + 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)tinyPdfInstance { static DebugUIMessagesAssetLoader *instance = nil; diff --git a/SignalMessaging/utils/OWSMessagesBubbleImageFactory.swift b/SignalMessaging/utils/OWSMessagesBubbleImageFactory.swift index 6791a1a016..9a3907bce9 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 { From 4a4e9d1ce2af8e09db773d56f93ef0e3bf9018d0 Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Wed, 28 Mar 2018 10:42:48 -0400 Subject: [PATCH 18/31] Bubble collapse. --- .../src/ViewControllers/DebugUI/DebugUIMessages.m | 13 +++++++++++++ SignalServiceKit/src/Contacts/TSThread.h | 4 +++- SignalServiceKit/src/Contacts/TSThread.m | 7 ++++++- 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m b/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m index 145c46bd7c..004d19b6fd 100644 --- a/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m +++ b/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m @@ -43,6 +43,11 @@ NS_ASSUME_NONNULL_BEGIN 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 @@ -2589,6 +2594,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/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. // From e0e8eafb5d2706dae0240d9dc08e31da948ab9c2 Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Wed, 28 Mar 2018 13:34:33 -0400 Subject: [PATCH 19/31] Bubble collapse. --- .../ConversationView/Cells/OWSMessageCell.m | 287 +++++------------- .../ConversationView/ConversationViewItem.m | 6 + .../ViewControllers/DebugUI/DebugUIMessages.m | 22 +- SignalMessaging/categories/UIView+OWS.h | 18 ++ SignalMessaging/categories/UIView+OWS.m | 40 +++ 5 files changed, 144 insertions(+), 229 deletions(-) diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m index f164a521bb..711fa86b16 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m @@ -60,7 +60,6 @@ CG_INLINE CGSize CGSizeCeil(CGSize size) @property (nonatomic, nullable) NSMutableArray *viewConstraints; @property (nonatomic) BOOL isPresentingMenuController; - @end @implementation OWSMessageCell @@ -88,17 +87,6 @@ CG_INLINE CGSize CGSizeCeil(CGSize size) self.bubbleView.layoutMargins = UIEdgeInsetsZero; [self.contentView addSubview:self.bubbleView]; - // self.messageWrapperView = [UIView new]; - // self.messageWrapperView.layoutMargins = UIEdgeInsetsZero; - // [self.messageWrapperView addSubview:self.payloadView]; - // 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.footerView = [UIView containerView]; [self.contentView addSubview:self.footerView]; @@ -108,50 +96,7 @@ CG_INLINE CGSize CGSizeCeil(CGSize size) self.dateHeaderLabel.textColor = [UIColor lightGrayColor]; [self.contentView addSubview:self.dateHeaderLabel]; - //<<<<<<< HEAD - // 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]; - // - //||||||| merged common ancestors - // self.bubbleImageView = [UIImageView new]; - // self.bubbleImageView.layoutMargins = UIEdgeInsetsZero; - // // Enable userInteractionEnabled so that links in textView work. - // self.bubbleImageView.userInteractionEnabled = YES; - // [self.payloadView addSubview:self.bubbleImageView]; - // [self.bubbleImageView autoPinToSuperviewEdges]; - // - //======= - // self.bubbleFillView = [BubbleFillView new]; - // self.bubbleFillView.layoutMargins = UIEdgeInsetsZero; - // // TODO: - //// // Enable userInteractionEnabled so that links in textView work. - // self.bubbleFillView.userInteractionEnabled = YES; - // [self.payloadView addSubview:self.bubbleFillView]; - // [self.bubbleFillView autoPinToSuperviewEdges]; - // - //// self.bubbleImageView = [UIImageView new]; - //// self.bubbleImageView.layoutMargins = UIEdgeInsetsZero; - //// // Enable userInteractionEnabled so that links in textView work. - //// self.bubbleImageView.userInteractionEnabled = YES; - //// [self.payloadView addSubview:self.bubbleImageView]; - //// [self.bubbleImageView autoPinToSuperviewEdges]; - // - //>>>>>>> SQUASHED self.bodyTextViewCached = [self newTextView]; - //<<<<<<< HEAD - // - // [self.textBubbleImageView addSubview:self.textView]; - // - //||||||| merged common ancestors - // [self.bubbleImageView addSubview:self.textView]; - //======= - // [self.bubbleFillView addSubview:self.textView]; - //>>>>>>> SQUASHED - // OWSAssert(self.textView.superview); self.footerLabel = [UILabel new]; self.footerLabel.font = [UIFont ows_regularFontWithSize:12.f]; @@ -159,14 +104,6 @@ CG_INLINE CGSize CGSizeCeil(CGSize size) [self.footerView addSubview:self.footerLabel]; // Hide these views by default. - //<<<<<<< HEAD - // self.textBubbleImageView.hidden = YES; - //||||||| merged common ancestors - // self.bubbleImageView.hidden = YES; - //======= - // self.bubbleFillView.hidden = YES; - //// self.bubbleImageView.hidden = YES; - //>>>>>>> SQUASHED self.bodyTextViewCached.hidden = YES; self.dateHeaderLabel.hidden = YES; self.footerLabel.hidden = YES; @@ -174,14 +111,6 @@ CG_INLINE CGSize CGSizeCeil(CGSize size) [self.bubbleView autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:self.dateHeaderLabel]; [self.footerView autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:self.bubbleView]; - // [self.payloadView autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:self.dateHeaderLabel]; - // [self.footerView autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:self.bubbleView]; - - // [self.mediaMaskingView autoPinEdgeToSuperviewEdge:ALEdgeTop]; - // - // [self.textBubbleImageView autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:self.mediaMaskingView]; - // [self.textBubbleImageView autoPinEdgeToSuperviewEdge:ALEdgeBottom]; - [self.footerView autoPinEdgeToSuperviewEdge:ALEdgeBottom]; [self.footerView autoPinWidthToSuperview]; @@ -199,29 +128,6 @@ CG_INLINE CGSize CGSizeCeil(CGSize size) initWithDirection:(self.isRTL ? PanDirectionLeft : PanDirectionRight)target:self action:@selector(handlePanGesture:)]; [self addGestureRecognizer:panGesture]; - - // UITapGestureRecognizer *mediaTap = - // [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleMediaTapGesture:)]; - // [self.mediaMaskingView addGestureRecognizer:mediaTap]; - // - // UITapGestureRecognizer *textTap = - // [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTextTapGesture:)]; - // [self.textBubbleImageView addGestureRecognizer:textTap]; - // - // UILongPressGestureRecognizer *mediaLongPress = - // [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleMediaLongPressGesture:)]; - // [self.mediaMaskingView addGestureRecognizer:mediaLongPress]; - // - // 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:)]; - // [self addGestureRecognizer:panGesture]; } - (OWSMessageTextView *)newTextView @@ -233,6 +139,7 @@ CG_INLINE CGSize CGSizeCeil(CGSize size) textView.selectable = YES; textView.textContainerInset = UIEdgeInsetsZero; textView.contentInset = UIEdgeInsetsZero; + textView.textContainer.lineFragmentPadding = 0; textView.scrollEnabled = NO; return textView; } @@ -392,6 +299,10 @@ CG_INLINE CGSize CGSizeCeil(CGSize size) 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; @@ -443,12 +354,6 @@ CG_INLINE CGSize CGSizeCeil(CGSize size) UIView *_Nullable lastSubview = nil; CGFloat bottomMargin = 0; - // for (UIView *subview in contentViews) { - // [self.bubbleView addSubview:subview]; - // if (last - // lastSubview = subview; - // } - UIView *_Nullable bodyMediaView = nil; BOOL bodyMediaViewHasGreedyWidth = NO; switch (self.cellType) { @@ -488,43 +393,31 @@ CG_INLINE CGSize CGSizeCeil(CGSize size) OWSAssert(self.unloadCellContentBlock); OWSAssert(!lastSubview); + bodyMediaView.clipsToBounds = YES; + self.lastBodyMediaView = bodyMediaView; bodyMediaView.userInteractionEnabled = NO; if (self.isMediaBeingSent) { bodyMediaView.layer.opacity = 0.75f; } - // TODO: Is this right? - CGFloat contentWidth = self.width; - // Sometimes we want the media to "try to fill" the message bubble. - // For example, when the message bubble contains just an image, we - // want the image's bounds to exactly fill the message bubble. - // - // In other situations, we want to center the media within the message - // bubble. - // For example, when a message has a tall portrait image and a long - // caption. - CGSize bodyMediaSize = [self bodyMediaSizeForContentWidth:(int)contentWidth]; - BOOL hasValidMediaSize = bodyMediaSize.width > 0.01 && bodyMediaSize.height > 0.01; - if (!hasValidMediaSize) { - bodyMediaViewHasGreedyWidth = YES; - } else if (!self.hasText) { - bodyMediaViewHasGreedyWidth = YES; - } - [self.bubbleView addSubview:bodyMediaView]; - if (bodyMediaViewHasGreedyWidth) { - [self.viewConstraints addObjectsFromArray:@[ - [bodyMediaView autoPinLeadingToSuperviewWithMargin:0], - [bodyMediaView autoPinTrailingToSuperviewWithMargin:0], - ]]; - } else { - CGFloat aspectRatio = bodyMediaSize.width / bodyMediaSize.height; - [self.viewConstraints addObjectsFromArray:@[ - [bodyMediaView autoHCenterInSuperview], - [bodyMediaView autoPinToAspectRatio:aspectRatio], - ]]; - } + // 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], + ]]; + [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]]; @@ -535,7 +428,7 @@ CG_INLINE CGSize CGSizeCeil(CGSize size) bottomMargin = 0; } - UIView *_Nullable bodyTextView = nil; + 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.hasText || !bodyMediaView) { @@ -547,6 +440,12 @@ CG_INLINE CGSize CGSizeCeil(CGSize size) [bodyTextView autoPinLeadingToSuperviewWithMargin:self.textLeadingMargin], [bodyTextView autoPinTrailingToSuperviewWithMargin:self.textTrailingMargin], ]]; + [NSLayoutConstraint + autoSetPriority:UILayoutPriorityDefaultLow + forConstraints:^{ + [self.viewConstraints + addObject:[bodyTextView autoSetDimension:ALDimensionHeight toSize:bodyTextContentSize.height]]; + }]; if (lastSubview) { [self.viewConstraints addObject:[bodyTextView autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom @@ -560,6 +459,22 @@ CG_INLINE CGSize CGSizeCeil(CGSize size) bottomMargin = self.textBottomMargin; } +#ifdef DEBUG + DDLogVerbose( + @"%@ --- bodyMediaView: %@ [%@].", self.logTag, bodyMediaView, NSStringFromCGSize(bodyMediaContentSize)); + DDLogVerbose(@"%@ --- bodyTextView: %@ (%zd) [%@].", + self.logTag, + bodyTextView.text, + bodyTextView.text.length, + NSStringFromCGSize(bodyTextContentSize)); + [bodyMediaView logFrameLaterWithLabel:@"bodyMediaView"]; + [bodyTextView logFrameLaterWithLabel:@"bodyTextView"]; + [self.bubbleView logFrameLaterWithLabel:@"bubbleView"]; + [self.contentView logFrameLaterWithLabel:@"contentView"]; +// [bodyMediaView addRedBorder]; +// [bodyTextView addRedBorder]; +#endif + UIView *_Nullable tapForMoreLabel = [self createTapForMoreLabelIfNecessary]; if (tapForMoreLabel) { OWSAssert(lastSubview); @@ -725,9 +640,7 @@ CG_INLINE CGSize CGSizeCeil(CGSize size) 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 @@ -834,14 +747,6 @@ CG_INLINE CGSize CGSizeCeil(CGSize size) font:(UIFont *)font shouldIgnoreEvents:(BOOL)shouldIgnoreEvents { - //<<<<<<< HEAD - // self.textBubbleImageView.hidden = NO; - //||||||| merged common ancestors - // self.bubbleImageView.hidden = NO; - //======= - // self.bubbleFillView.hidden = NO; - //// self.bubbleImageView.hidden = NO; - //>>>>>>> SQUASHED textView.hidden = NO; textView.text = text; textView.textColor = textColor; @@ -888,9 +793,7 @@ CG_INLINE CGSize CGSizeCeil(CGSize size) // some performance cost. stillImageView.layer.minificationFilter = kCAFilterTrilinear; stillImageView.layer.magnificationFilter = kCAFilterTrilinear; - // [self setMediaView:self.stillImageView]; [self addAttachmentUploadViewIfNecessary:stillImageView]; - // [self addCaptionIfNecessary]; __weak OWSMessageCell *weakSelf = self; self.loadCellContentBlock = ^{ @@ -924,9 +827,7 @@ CG_INLINE CGSize CGSizeCeil(CGSize size) // We need to specify a contentMode since the size of the image // might not match the aspect ratio of the view. animatedImageView.contentMode = UIViewContentModeScaleAspectFill; - // [self setMediaView:self.animatedImageView]; [self addAttachmentUploadViewIfNecessary:animatedImageView]; - // [self addCaptionIfNecessary]; __weak OWSMessageCell *weakSelf = self; self.loadCellContentBlock = ^{ @@ -967,9 +868,7 @@ CG_INLINE CGSize CGSizeCeil(CGSize size) viewItem:self.viewItem]; self.viewItem.lastAudioMessageView = audioMessageView; [audioMessageView createContents]; - // [self setMediaView:self.audioMessageView]; [self addAttachmentUploadViewIfNecessary:audioMessageView]; - // [self addCaptionIfNecessary]; self.loadCellContentBlock = ^{ // Do nothing. @@ -994,7 +893,6 @@ CG_INLINE CGSize CGSizeCeil(CGSize size) // some performance cost. stillImageView.layer.minificationFilter = kCAFilterTrilinear; stillImageView.layer.magnificationFilter = kCAFilterTrilinear; - // [self setMediaView:stillImageView]; UIImage *videoPlayIcon = [UIImage imageNamed:@"play_button"]; UIImageView *videoPlayButton = [[UIImageView alloc] initWithImage:videoPlayIcon]; @@ -1004,7 +902,6 @@ CG_INLINE CGSize CGSizeCeil(CGSize size) attachmentStateCallback:^(BOOL isAttachmentReady) { videoPlayButton.hidden = !isAttachmentReady; }]; - // [self addCaptionIfNecessary]; __weak OWSMessageCell *weakSelf = self; self.loadCellContentBlock = ^{ @@ -1036,9 +933,7 @@ CG_INLINE CGSize CGSizeCeil(CGSize size) OWSGenericAttachmentView *attachmentView = [[OWSGenericAttachmentView alloc] initWithAttachment:self.attachmentStream isIncoming:self.isIncoming]; [attachmentView createContents]; - // [self setMediaView:attachmentView]; [self addAttachmentUploadViewIfNecessary:attachmentView]; - // [self addCaptionIfNecessary]; self.loadCellContentBlock = ^{ // Do nothing. @@ -1068,14 +963,12 @@ CG_INLINE CGSize CGSizeCeil(CGSize size) customView.backgroundColor = [UIColor grayColor]; break; } - // [self setMediaView:self.customView]; AttachmentPointerView *attachmentPointerView = [[AttachmentPointerView alloc] initWithAttachmentPointer:self.attachmentPointer isIncoming:self.isIncoming]; [customView addSubview:attachmentPointerView]; [attachmentPointerView autoPinWidthToSuperviewWithMargin:20.f]; [attachmentPointerView autoVCenterInSuperview]; - // [self addCaptionIfNecessary]; self.loadCellContentBlock = ^{ // Do nothing. @@ -1113,18 +1006,6 @@ CG_INLINE CGSize CGSizeCeil(CGSize size) } } -//- (void)cropMediaViewToBubbbleShape:(UIView *)view -//{ -// 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:(UIView *)mediaView { OWSAssert(mediaView); @@ -1140,7 +1021,7 @@ CG_INLINE CGSize CGSizeCeil(CGSize size) #pragma mark - Measurement // Size of "message body" text, not quoted reply text. -- (CGSize)bodyTextSizeForContentWidth:(int)contentWidth +- (CGSize)bodyTextSizeForContentWidth:(int)contentWidth includeMargins:(BOOL)includeMargins { if (!self.hasText) { return CGSizeZero; @@ -1157,38 +1038,15 @@ CG_INLINE CGSize CGSizeCeil(CGSize size) // Honor dynamic type in the message bodies. self.bodyTextViewCached.font = [self textMessageFont]; CGSize textSize = CGSizeCeil([self.bodyTextViewCached sizeThatFits:CGSizeMake(maxTextWidth, CGFLOAT_MAX)]); - CGFloat tapForMoreHeight = (self.displayableText.isTextTruncated ? [self tapForMoreHeight] : 0.f); - CGSize textViewSize = CGSizeCeil(CGSizeMake(textSize.width + leftMargin + rightMargin, - textSize.height + self.textTopMargin + self.textBottomMargin + tapForMoreHeight)); + CGSize textViewSize = textSize; - return textViewSize; + if (includeMargins) { + textViewSize.width += leftMargin + rightMargin; + textViewSize.height += self.textTopMargin + self.textBottomMargin; + } + + return CGSizeCeil(textViewSize); } -// -//+ (CGSize)textSizeForContentWidth:(int)contentWidth -// displayableText:(DisplayableText *)displayableText -// textView:(OWSMessageTextView *)textView -//{ -// OWSAssert(text); -// OWSAssert(textView); -// -// BOOL isRTL = self.isRTL; -// CGFloat leftMargin = isRTL ? self.textTrailingMargin : self.textLeadingMargin; -// CGFloat rightMargin = isRTL ? self.textLeadingMargin : self.textTrailingMargin; -// -// const int maxMessageWidth = [self maxMessageWidthForContentWidth:contentWidth]; -// const int maxTextWidth = (int)floor(maxMessageWidth - (leftMargin + rightMargin)); -// -// textView.text = displayableText.displayText; -// // Honor dynamic type in the message bodies. -// textView.font = [self textMessageFont]; -// CGSize textSize = CGSizeCeil([textView sizeThatFits:CGSizeMake(maxTextWidth, CGFLOAT_MAX)]); -// CGFloat tapForMoreHeight = (displayableText.isTextTruncated ? [self tapForMoreHeight] : 0.f); -// CGSize textViewSize = CGSizeCeil(CGSizeMake(textSize.width + leftMargin + rightMargin, -// textSize.height + self.textTopMargin + self.textBottomMargin + -// tapForMoreHeight)); -// -// return textViewSize; -//} - (CGSize)bodyMediaSizeForContentWidth:(int)contentWidth { @@ -1247,7 +1105,7 @@ CG_INLINE CGSize CGSizeCeil(CGSize size) OWSAssert([self.viewItem.interaction isKindOfClass:[TSMessage class]]); CGSize mediaContentSize = [self bodyMediaSizeForContentWidth:contentWidth]; - CGSize textContentSize = [self bodyTextSizeForContentWidth:contentWidth]; + CGSize textContentSize = [self bodyTextSizeForContentWidth:contentWidth includeMargins:YES]; CGFloat cellContentWidth = fmax(mediaContentSize.width, textContentSize.width); CGFloat cellContentHeight = mediaContentSize.height + textContentSize.height; @@ -1257,13 +1115,23 @@ CG_INLINE CGSize CGSizeCeil(CGSize size) cellSize.height += self.dateHeaderHeight; cellSize.height += self.footerHeight; + if (self.hasText && self.displayableText.isTextTruncated) { + cellSize.height += self.tapForMoreHeight; + } if (self.shouldHaveFailedSendBadge) { cellSize.width += self.failedSendBadgeSize; } - cellSize.width = ceil(cellSize.width); - cellSize.height = ceil(cellSize.height); + cellSize = CGSizeCeil(cellSize); + + DDLogVerbose(@"%@ --- cellSizeForViewWidth: %@ + %@ + %f + %f = %@.", + self.logTag, + NSStringFromCGSize(mediaContentSize), + NSStringFromCGSize(textContentSize), + self.dateHeaderHeight, + self.footerHeight, + NSStringFromCGSize(cellSize)); return cellSize; } @@ -1272,7 +1140,7 @@ CG_INLINE CGSize CGSizeCeil(CGSize size) { 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; } @@ -1293,23 +1161,11 @@ CG_INLINE CGSize CGSizeCeil(CGSize size) - (CGFloat)textLeadingMargin { return (self.isIncoming ? kBubbleTextHInset + kBubbleThornSideInset : kBubbleTextHInset); - - // static const CGFloat kBubbleVRounding = 8.f; - // static const CGFloat kBubbleHRounding = 10.f; - // static const CGFloat kBubbleThornSideInset = 3.f; - // static const CGFloat kBubbleThornVInset = 3.f; - // static const CGFloat kBubbleTextHInset = 10.f; - // static const CGFloat kBubbleTextVInset = 10.f; - // static const CGFloat kBubbleTextTopInset = kBubbleTextVInset; - // static const CGFloat kBubbleTextBottomInset = kBubbleThornVInset + kBubbleTextVInset; - - // return self.isIncoming ? 15 : 10; } - (CGFloat)textTrailingMargin { return (self.isIncoming ? kBubbleTextHInset : kBubbleTextHInset + kBubbleThornSideInset); - // return self.isIncoming ? 10 : 15; } - (CGFloat)textTopMargin @@ -1322,11 +1178,6 @@ CG_INLINE CGSize CGSizeCeil(CGSize size) return kBubbleTextVInset + kBubbleThornVInset; } -//- (CGFloat)textVMargin -//{ -// return 10; -//} - - (UIColor *)textColor { return self.isIncoming ? [UIColor blackColor] : [UIColor whiteColor]; diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewItem.m b/Signal/src/ViewControllers/ConversationView/ConversationViewItem.m index 13fa7740c4..9bde6eaf78 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewItem.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewItem.m @@ -403,6 +403,12 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType) } } + DDLogVerbose(@"%@ --- message. hasAttachments: %d, body: %@ (%zd).", + self.logTag, + message.hasAttachments, + message.body, + message.body.length); + // Ignore message body for oversize text attachments. if (message.body.length > 0) { if (self.hasText) { diff --git a/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m b/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m index 004d19b6fd..1ec0f7e08b 100644 --- a/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m +++ b/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m @@ -51,10 +51,6 @@ NS_ASSUME_NONNULL_BEGIN for (DebugUIMessagesAction *action in @[ [DebugUIMessages sendMessageVariationsAction:thread], // Send Media - [DebugUIMessages sendJpegAction:thread], - [DebugUIMessages sendGifAction:thread], - [DebugUIMessages sendMp3Action:thread], - [DebugUIMessages sendMp4Action:thread], [DebugUIMessages sendAllMediaAction:thread], [DebugUIMessages sendRandomMediaAction:thread], // Fake Media @@ -755,7 +751,11 @@ NS_ASSUME_NONNULL_BEGIN NSString *messageBody = nil; if (hasCaption) { - messageBody = [[@(index).stringValue stringByAppendingString:@" "] stringByAppendingString:[self randomText]]; + // 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:@" 🔤"]; @@ -771,13 +771,9 @@ NS_ASSUME_NONNULL_BEGIN } } - NSString *_Nullable randomCaption; - if (arc4random() % 2 == 2) { - randomCaption = [self randomText]; - } TSOutgoingMessage *message = [[TSOutgoingMessage alloc] initWithTimestamp:timestamp inThread:thread - messageBody:randomCaption + messageBody:messageBody isVoiceMessage:NO expiresInSeconds:0]; [message setReceivedAtTimestamp:timestamp]; @@ -1051,7 +1047,11 @@ NS_ASSUME_NONNULL_BEGIN NSString *messageBody = nil; if (hasCaption) { - messageBody = [[@(index).stringValue stringByAppendingString:@" "] stringByAppendingString:[self randomText]]; + // 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:@" 🔤"]; diff --git a/SignalMessaging/categories/UIView+OWS.h b/SignalMessaging/categories/UIView+OWS.h index 095624c387..5479c85841 100644 --- a/SignalMessaging/categories/UIView+OWS.h +++ b/SignalMessaging/categories/UIView+OWS.h @@ -125,4 +125,22 @@ CGFloat ScaleFromIPhone5(CGFloat iPhone5Value); @end +//#pragma mark - +// +//@interface NSLayoutConstraint (OWS) +// +//- (NSLayoutConstraint *)withLowPriority; +//- (NSLayoutConstraint *)withHighPriority; +// +//@end +// +//#pragma mark - +// +//@interface NSArray (OWSLayout) +// +//- (NSArray *)withLowPriorities; +//- (NSArray *)withHighPriorities; +// +//@end + NS_ASSUME_NONNULL_END diff --git a/SignalMessaging/categories/UIView+OWS.m b/SignalMessaging/categories/UIView+OWS.m index 2297497ff7..c83533372a 100644 --- a/SignalMessaging/categories/UIView+OWS.m +++ b/SignalMessaging/categories/UIView+OWS.m @@ -487,4 +487,44 @@ CGFloat ScaleFromIPhone5(CGFloat iPhone5Value) @end +//#pragma mark - +// +//@implementation NSLayoutConstraint (OWS) +// +//- (NSLayoutConstraint *)withLowPriority +//{ +// self.priority = UILayoutPriorityDefaultLow; +// return self; +//} +// +//- (NSLayoutConstraint *)withHighPriority +//{ +// self.priority = UILayoutPriorityDefaultHigh; +// return self; +//} +// +//@end +// +//#pragma mark - +// +//@implementation NSArray (OWSLayout) +// +//- (instancetype)withLowPriorities +//{ +// for (NSLayoutConstraint *constraint in self) { +// constraint.priority = UILayoutPriorityDefaultLow; +// } +// return self; +//} +// +//- (instancetype)withHighPriorities +//{ +// for (NSLayoutConstraint *constraint in self) { +// constraint.priority = UILayoutPriorityDefaultHigh; +// } +// return self; +//} +// +//@end + NS_ASSUME_NONNULL_END From 8a74e10208ffd6d82d1b6fcddd3c688aaa5fa4f6 Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Wed, 28 Mar 2018 14:46:16 -0400 Subject: [PATCH 20/31] Bubble collapse. --- .../ConversationView/Cells/OWSMessageCell.m | 6 +- .../ViewControllers/DebugUI/DebugUIMessages.m | 297 ++++++++++++++---- .../DebugUI/DebugUIMessagesAssetLoader.m | 83 ++--- 3 files changed, 283 insertions(+), 103 deletions(-) diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m index 711fa86b16..9e673e5261 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m @@ -114,15 +114,15 @@ CG_INLINE CGSize CGSizeCeil(CGSize size) [self.footerView autoPinEdgeToSuperviewEdge:ALEdgeBottom]; [self.footerView autoPinWidthToSuperview]; - self.bubbleView.userInteractionEnabled = YES; + self.contentView.userInteractionEnabled = YES; UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTapGesture:)]; - [self.bubbleView addGestureRecognizer:tap]; + [self.contentView addGestureRecognizer:tap]; UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleLongPressGesture:)]; - [self.bubbleView addGestureRecognizer:longPress]; + [self.contentView addGestureRecognizer:longPress]; PanDirectionGestureRecognizer *panGesture = [[PanDirectionGestureRecognizer alloc] initWithDirection:(self.isRTL ? PanDirectionLeft : PanDirectionRight)target:self diff --git a/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m b/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m index 1ec0f7e08b..db6e9d9bfe 100644 --- a/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m +++ b/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m @@ -1106,35 +1106,66 @@ NS_ASSUME_NONNULL_BEGIN #pragma mark - Fake Media -+ (NSArray *)allFakeMediaActions:(TSThread *)thread ++ (NSArray *)allFakeMediaActions:(TSThread *)thread includeLabels:(BOOL)includeLabels { OWSAssert(thread); - NSArray *actions = @[ - // Outgoing + 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], - // + ]]; + 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], @@ -1149,7 +1180,13 @@ NS_ASSUME_NONNULL_BEGIN [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], @@ -1164,7 +1201,13 @@ NS_ASSUME_NONNULL_BEGIN [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 @@ -1175,97 +1218,211 @@ NS_ASSUME_NONNULL_BEGIN [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 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 + // 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], - // Don't bother with multiple GIF states. + ]]; + if (includeLabels) { + [actions addObject:[self fakeIncomingTextMessageAction:thread text:@"⚠️ Incoming Gif ⚠️"]]; + } + [actions addObjectsFromArray:@[ [self fakeIncomingGifAction: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 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; } @@ -1274,7 +1431,7 @@ NS_ASSUME_NONNULL_BEGIN OWSAssert(thread); return [DebugUIMessagesGroupAction allGroupActionWithLabel:@"Fake All Media" - subactions:[self allFakeMediaActions:thread]]; + subactions:[self allFakeMediaActions:thread includeLabels:YES]]; } + (DebugUIMessagesAction *)fakeRandomMediaAction:(TSThread *)thread @@ -1282,7 +1439,7 @@ NS_ASSUME_NONNULL_BEGIN OWSAssert(thread); return [DebugUIMessagesGroupAction randomGroupActionWithLabel:@"Fake Random Media" - subactions:[self allFakeMediaActions:thread]]; + subactions:[self allFakeMediaActions:thread includeLabels:NO]]; } #pragma mark - Send Text Messages @@ -1439,19 +1596,35 @@ NS_ASSUME_NONNULL_BEGIN }]; } -+ (NSArray *)allFakeTextActions:(TSThread *)thread ++ (NSArray *)allFakeTextActions:(TSThread *)thread includeLabels:(BOOL)includeLabels { OWSAssert(thread); - NSArray *actions = @[ - [self fakeShortIncomingTextMessageAction:thread], - [self fakeIncomingTextMessageAction:thread text:@"Hi"], - [self fakeIncomingTextMessageAction:thread text:@"1️⃣"], - [self fakeIncomingTextMessageAction:thread text:@"1️⃣2️⃣"], - [self fakeIncomingTextMessageAction:thread text:@"1️⃣2️⃣3️⃣"], - [self fakeIncomingTextMessageAction:thread text:@"落"], - [self fakeIncomingTextMessageAction:thread text:@"﷽"], + 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], @@ -1463,18 +1636,18 @@ NS_ASSUME_NONNULL_BEGIN messageState:TSOutgoingMessageStateSentToService isDelivered:YES isRead:YES], - [self fakeOutgoingTextMessageAction:thread messageState:TSOutgoingMessageStateSentToService text:@"Hi"], - [self fakeOutgoingTextMessageAction:thread messageState:TSOutgoingMessageStateSentToService text:@"1️⃣"], - [self fakeOutgoingTextMessageAction:thread - messageState:TSOutgoingMessageStateSentToService - text:@"1️⃣2️⃣"], - [self fakeOutgoingTextMessageAction:thread - messageState:TSOutgoingMessageStateSentToService - text:@"1️⃣2️⃣3️⃣"], - [self fakeOutgoingTextMessageAction:thread messageState:TSOutgoingMessageStateSentToService text:@"1️⃣"], - [self fakeOutgoingTextMessageAction:thread messageState:TSOutgoingMessageStateSentToService text:@"落"], - [self fakeOutgoingTextMessageAction:thread messageState:TSOutgoingMessageStateSentToService text:@"﷽"], - ]; + ]]; + + 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; } @@ -1483,7 +1656,7 @@ NS_ASSUME_NONNULL_BEGIN OWSAssert(thread); return [DebugUIMessagesGroupAction allGroupActionWithLabel:@"Fake All Text" - subactions:[self allFakeTextActions:thread]]; + subactions:[self allFakeTextActions:thread includeLabels:YES]]; } + (DebugUIMessagesAction *)fakeRandomTextAction:(TSThread *)thread @@ -1491,18 +1664,18 @@ NS_ASSUME_NONNULL_BEGIN OWSAssert(thread); return [DebugUIMessagesGroupAction randomGroupActionWithLabel:@"Fake Random Text" - subactions:[self allFakeTextActions:thread]]; + subactions:[self allFakeTextActions:thread includeLabels:NO]]; } #pragma mark - Exemplary -+ (NSArray *)allExemplaryActions:(TSThread *)thread ++ (NSArray *)allExemplaryActions:(TSThread *)thread includeLabels:(BOOL)includeLabels { OWSAssert(thread); NSMutableArray *actions = [NSMutableArray new]; - [actions addObjectsFromArray:[self allFakeMediaActions:thread]]; - [actions addObjectsFromArray:[self allFakeTextActions:thread]]; + [actions addObjectsFromArray:[self allFakeMediaActions:thread includeLabels:includeLabels]]; + [actions addObjectsFromArray:[self allFakeTextActions:thread includeLabels:includeLabels]]; return actions; } @@ -1511,7 +1684,7 @@ NS_ASSUME_NONNULL_BEGIN OWSAssert(thread); return [DebugUIMessagesGroupAction allGroupActionWithLabel:@"Exemplary Permutations" - subactions:[self allExemplaryActions:thread]]; + subactions:[self allExemplaryActions:thread includeLabels:YES]]; } + (void)selectExemplaryAction:(TSThread *)thread @@ -1520,7 +1693,7 @@ NS_ASSUME_NONNULL_BEGIN UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"Select Action" message:nil preferredStyle:UIAlertControllerStyleActionSheet]; - for (DebugUIMessagesAction *action in [self allExemplaryActions:thread]) { + for (DebugUIMessagesAction *action in [self allExemplaryActions:thread includeLabels:NO]) { [alert addAction:[UIAlertAction actionWithTitle:action.label style:UIAlertActionStyleDefault handler:^(UIAlertAction *ignore) { diff --git a/Signal/src/ViewControllers/DebugUI/DebugUIMessagesAssetLoader.m b/Signal/src/ViewControllers/DebugUI/DebugUIMessagesAssetLoader.m index d16ced0ca6..0731710317 100644 --- a/Signal/src/ViewControllers/DebugUI/DebugUIMessagesAssetLoader.m +++ b/Signal/src/ViewControllers/DebugUI/DebugUIMessagesAssetLoader.m @@ -122,14 +122,16 @@ NS_ASSUME_NONNULL_BEGIN return; } - 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(); + @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 @@ -142,31 +144,33 @@ NS_ASSUME_NONNULL_BEGIN OWSAssert(textColor); OWSAssert(label.length > 0); - 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 }; + @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]; + CGRect textFrame = + [label boundingRectWithSize:frame.size + options:(NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading) + attributes:textAttributes + context:nil]; - UIGraphicsBeginImageContextWithOptions(frame.size, NO, [UIScreen mainScreen].scale); - CGContextRef context = UIGraphicsGetCurrentContext(); + 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]; + CGContextSetFillColorWithColor(context, backgroundColor.CGColor); + CGContextFillRect(context, frame); + [label drawAtPoint:CGPointMake(CGRectGetMidX(frame) - CGRectGetMidX(textFrame), + CGRectGetMidY(frame) - CGRectGetMidY(textFrame)) + withAttributes:textAttributes]; - UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); - UIGraphicsEndImageContext(); + UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); - return image; + return image; + } } #pragma mark - @@ -203,15 +207,17 @@ NS_ASSUME_NONNULL_BEGIN return; } - 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]); + @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(); } @@ -453,7 +459,8 @@ NS_ASSUME_NONNULL_BEGIN static DebugUIMessagesAssetLoader *instance = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ - instance = [DebugUIMessagesAssetLoader fakeRandomAssetLoaderWithLength:256 mimeType:@"application/pdf"]; + instance = + [DebugUIMessagesAssetLoader fakeRandomAssetLoaderWithLength:4 * 1024 * 1024 mimeType:@"application/pdf"]; }); return instance; } From 8d74c68f9de6f9d422f1aa53905704149ab8132b Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Wed, 28 Mar 2018 16:10:35 -0400 Subject: [PATCH 21/31] Bubble collapse. --- .../ConversationView/Cells/OWSMessageCell.m | 20 ++- .../ConversationView/ConversationViewItem.m | 4 - .../ViewControllers/DebugUI/DebugUIMessages.m | 120 +++++++++++++----- .../DebugUI/DebugUIMessagesAssetLoader.m | 2 +- .../Messages/Attachments/TSAttachmentStream.m | 28 ++-- 5 files changed, 118 insertions(+), 56 deletions(-) diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m index 9e673e5261..c0b4fb018d 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m @@ -505,6 +505,7 @@ CG_INLINE CGSize CGSizeCeil(CGSize size) - (nullable id)tryToLoadCellMedia:(nullable id (^)(void))loadCellMediaBlock mediaView:(UIView *)mediaView cacheKey:(NSString *)cacheKey + shouldSkipCache:(BOOL)shouldSkipCache { OWSAssert(self.attachmentStream); OWSAssert(mediaView); @@ -525,7 +526,9 @@ CG_INLINE CGSize CGSizeCeil(CGSize size) 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; @@ -804,12 +807,17 @@ CG_INLINE CGSize CGSizeCeil(CGSize size) if (stillImageView.image) { return; } + // Don't cache large still images. + 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]; + cacheKey:strongSelf.attachmentStream.uniqueId + shouldSkipCache:shouldSkipCache]; }; self.unloadCellContentBlock = ^{ stillImageView.image = nil; @@ -849,7 +857,8 @@ CG_INLINE CGSize CGSizeCeil(CGSize size) return animatedImage; } mediaView:animatedImageView - cacheKey:strongSelf.attachmentStream.uniqueId]; + cacheKey:strongSelf.attachmentStream.uniqueId + shouldSkipCache:NO]; }; self.unloadCellContentBlock = ^{ animatedImageView.image = nil; @@ -918,7 +927,8 @@ CG_INLINE CGSize CGSizeCeil(CGSize size) return strongSelf.attachmentStream.image; } mediaView:stillImageView - cacheKey:strongSelf.attachmentStream.uniqueId]; + cacheKey:strongSelf.attachmentStream.uniqueId + shouldSkipCache:NO]; }; self.unloadCellContentBlock = ^{ stillImageView.image = nil; @@ -1071,7 +1081,7 @@ CG_INLINE CGSize CGSizeCeil(CGSize size) 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)); diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewItem.m b/Signal/src/ViewControllers/ConversationView/ConversationViewItem.m index 9bde6eaf78..a26eac86f8 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewItem.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewItem.m @@ -86,8 +86,6 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType) [self ensureViewState:transaction]; - // OWSAssert(self.hasViewState); - return self; } @@ -107,8 +105,6 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType) [self clearCachedLayoutState]; [self ensureViewState:transaction]; - - // OWSAssert(self.hasViewState); } - (BOOL)hasText diff --git a/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m b/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m index db6e9d9bfe..016a83c0ac 100644 --- a/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m +++ b/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m @@ -60,7 +60,7 @@ NS_ASSUME_NONNULL_BEGIN [DebugUIMessages fakeAllTextAction:thread], [DebugUIMessages fakeRandomTextAction:thread], // Exemplary - [DebugUIMessages allExemplaryAction:thread], + [DebugUIMessages allFakeAction:thread], ]) { [items addObject:[OWSTableItem itemWithTitle:action.label actionBlock:^{ @@ -89,9 +89,13 @@ NS_ASSUME_NONNULL_BEGIN // actionBlock:^{ // [DebugUIMessages sendSelectedMediaTypeInThread:thread]; // }], - [OWSTableItem itemWithTitle:@"Select Action" + [OWSTableItem itemWithTitle:@"Select Fake" actionBlock:^{ - [DebugUIMessages selectExemplaryAction:thread]; + [DebugUIMessages selectFakeAction:thread]; + }], + [OWSTableItem itemWithTitle:@"Select Send Media" + actionBlock:^{ + [DebugUIMessages selectSendMediaAction:thread]; }], #pragma mark - Misc. @@ -360,6 +364,8 @@ NS_ASSUME_NONNULL_BEGIN + (void)sendAttachment:(NSString *)filePath thread:(TSThread *)thread + label:(NSString *)label + hasCaption:(BOOL)hasCaption success:(nullable void (^)(void))success failure:(nullable void (^)(void))failure { @@ -373,9 +379,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]) { @@ -423,50 +438,72 @@ NS_ASSUME_NONNULL_BEGIN OWSAssert(thread); NSArray *actions = @[ - [self sendJpegAction:thread], - [self sendGifAction:thread], - [self sendMp3Action:thread], - [self sendMp4Action:thread], + [self sendJpegAction:thread hasCaption:NO], + [self sendJpegAction:thread hasCaption:YES], + [self sendGifAction:thread hasCaption:NO], + [self sendGifAction: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 ++ (DebugUIMessagesAction *)sendJpegAction:(TSThread *)thread hasCaption:(BOOL)hasCaption { OWSAssert(thread); - return [self sendMediaAction:@"Send Jpeg" fakeAssetLoader:[DebugUIMessagesAssetLoader jpegInstance] thread:thread]; + return [self sendMediaAction:@"Send Jpeg" + hasCaption:hasCaption + fakeAssetLoader:[DebugUIMessagesAssetLoader jpegInstance] + thread:thread]; } -+ (DebugUIMessagesAction *)sendGifAction:(TSThread *)thread ++ (DebugUIMessagesAction *)sendGifAction:(TSThread *)thread hasCaption:(BOOL)hasCaption { OWSAssert(thread); - return [self sendMediaAction:@"Send Gif" fakeAssetLoader:[DebugUIMessagesAssetLoader gifInstance] thread:thread]; + return [self sendMediaAction:@"Send Gif" + hasCaption:hasCaption + fakeAssetLoader:[DebugUIMessagesAssetLoader gifInstance] + thread:thread]; } -+ (DebugUIMessagesAction *)sendMp3Action:(TSThread *)thread ++ (DebugUIMessagesAction *)sendMp3Action:(TSThread *)thread hasCaption:(BOOL)hasCaption { OWSAssert(thread); - return [self sendMediaAction:@"Send Mp3" fakeAssetLoader:[DebugUIMessagesAssetLoader mp3Instance] thread:thread]; + return [self sendMediaAction:@"Send Mp3" + hasCaption:hasCaption + fakeAssetLoader:[DebugUIMessagesAssetLoader mp3Instance] + thread:thread]; } -+ (DebugUIMessagesAction *)sendMp4Action:(TSThread *)thread ++ (DebugUIMessagesAction *)sendMp4Action:(TSThread *)thread hasCaption:(BOOL)hasCaption { OWSAssert(thread); - return [self sendMediaAction:@"Send Mp4" fakeAssetLoader:[DebugUIMessagesAssetLoader mp4Instance] thread:thread]; + return [self sendMediaAction:@"Send Mp4" + hasCaption:hasCaption + fakeAssetLoader:[DebugUIMessagesAssetLoader mp4Instance] + thread:thread]; } -+ (DebugUIMessagesAction *)sendMediaAction:(NSString *)label ++ (DebugUIMessagesAction *)sendMediaAction:(NSString *)labelParam + hasCaption:(BOOL)hasCaption fakeAssetLoader:(DebugUIMessagesAssetLoader *)fakeAssetLoader thread:(TSThread *)thread { - OWSAssert(label.length > 0); + OWSAssert(labelParam.length > 0); OWSAssert(fakeAssetLoader); OWSAssert(thread); + NSString *label = labelParam; + if (hasCaption) { + label = [label stringByAppendingString:@" 🔤"]; + } + return [DebugUIMessagesSingleAction actionWithLabel:label staggeredActionBlock:^(NSUInteger index, @@ -475,7 +512,12 @@ NS_ASSUME_NONNULL_BEGIN ActionFailureBlock failure) { dispatch_async(dispatch_get_main_queue(), ^{ OWSAssert(fakeAssetLoader.filePath.length > 0); - [self sendAttachment:fakeAssetLoader.filePath thread:thread success:success failure:failure]; + [self sendAttachment:fakeAssetLoader.filePath + thread:thread + label:label + hasCaption:hasCaption + success:success + failure:failure]; }); } prepareBlock:fakeAssetLoader.prepareBlock]; @@ -485,7 +527,7 @@ NS_ASSUME_NONNULL_BEGIN { OWSAssert(thread); - return [DebugUIMessagesGroupAction allGroupActionWithLabel:@"Send All Media" + return [DebugUIMessagesGroupAction allGroupActionWithLabel:@"All Send Media" subactions:[self allSendMediaActions:thread]]; } @@ -493,10 +535,17 @@ NS_ASSUME_NONNULL_BEGIN { OWSAssert(thread); - return [DebugUIMessagesGroupAction randomGroupActionWithLabel:@"Send Random Media" + 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 @@ -1669,7 +1718,7 @@ NS_ASSUME_NONNULL_BEGIN #pragma mark - Exemplary -+ (NSArray *)allExemplaryActions:(TSThread *)thread includeLabels:(BOOL)includeLabels ++ (NSArray *)allFakeActions:(TSThread *)thread includeLabels:(BOOL)includeLabels { OWSAssert(thread); @@ -1679,21 +1728,28 @@ NS_ASSUME_NONNULL_BEGIN return actions; } -+ (DebugUIMessagesAction *)allExemplaryAction:(TSThread *)thread ++ (DebugUIMessagesAction *)allFakeAction:(TSThread *)thread { OWSAssert(thread); - return [DebugUIMessagesGroupAction allGroupActionWithLabel:@"Exemplary Permutations" - subactions:[self allExemplaryActions:thread includeLabels:YES]]; + return [DebugUIMessagesGroupAction allGroupActionWithLabel:@"All Fake" + subactions:[self allFakeActions:thread includeLabels:YES]]; } -+ (void)selectExemplaryAction:(TSThread *)thread ++ (void)selectFakeAction:(TSThread *)thread { - OWSAssertIsOnMainThread() OWSAssert(thread); - UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"Select Action" - message:nil - preferredStyle:UIAlertControllerStyleActionSheet]; - for (DebugUIMessagesAction *action in [self allExemplaryActions:thread includeLabels:NO]) { + 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) { diff --git a/Signal/src/ViewControllers/DebugUI/DebugUIMessagesAssetLoader.m b/Signal/src/ViewControllers/DebugUI/DebugUIMessagesAssetLoader.m index 0731710317..079854474b 100644 --- a/Signal/src/ViewControllers/DebugUI/DebugUIMessagesAssetLoader.m +++ b/Signal/src/ViewControllers/DebugUI/DebugUIMessagesAssetLoader.m @@ -349,7 +349,7 @@ NS_ASSUME_NONNULL_BEGIN dispatch_once(&onceToken, ^{ instance = [DebugUIMessagesAssetLoader fakeAssetLoaderWithUrl:@"https://s3.amazonaws.com/ows-data/example_attachment_media/random-mp3.mp3" - mimeType:@"audio/mpeg"]; + mimeType:@"audio/mp3"]; }); return instance; } diff --git a/SignalServiceKit/src/Messages/Attachments/TSAttachmentStream.m b/SignalServiceKit/src/Messages/Attachments/TSAttachmentStream.m index 727175897f..33bcd2141d 100644 --- a/SignalServiceKit/src/Messages/Attachments/TSAttachmentStream.m +++ b/SignalServiceKit/src/Messages/Attachments/TSAttachmentStream.m @@ -572,21 +572,21 @@ NS_ASSUME_NONNULL_BEGIN OWSAssertIsOnMainThread(); OWSAssert([self isAudio]); - return 0; + // return 0; - // NSError *error; - // AVAudioPlayer *audioPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:self.mediaURL error:&error]; - // if (error && [error.domain isEqualToString:NSOSStatusErrorDomain] - // && (error.code == kAudioFileInvalidFileError || error.code == kAudioFileStreamError_InvalidFile)) { - // // Ignore "invalid audio file" errors. - // return 0.f; - // } - // if (!error) { - // return (CGFloat)[audioPlayer duration]; - // } else { - // OWSFail(@"Could not find audio duration: %@", self.mediaURL); - // return 0; - // } + NSError *error; + AVAudioPlayer *audioPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:self.mediaURL error:&error]; + if (error && [error.domain isEqualToString:NSOSStatusErrorDomain] + && (error.code == kAudioFileInvalidFileError || error.code == kAudioFileStreamError_InvalidFile)) { + // Ignore "invalid audio file" errors. + return 0.f; + } + if (!error) { + return (CGFloat)[audioPlayer duration]; + } else { + OWSFail(@"Could not find audio duration: %@", self.mediaURL); + return 0; + } } - (CGFloat)audioDurationSeconds From 643c6385b54ecb4d8c4e4d68ca91cafb7376449e Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Wed, 28 Mar 2018 16:23:56 -0400 Subject: [PATCH 22/31] Bubble collapse. --- .../ViewControllers/DebugUI/DebugUIMessages.m | 40 +++++++++++++++++++ .../DebugUI/DebugUIMessagesAssetLoader.h | 1 + .../DebugUI/DebugUIMessagesAssetLoader.m | 12 ++++++ 3 files changed, 53 insertions(+) diff --git a/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m b/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m index 016a83c0ac..61c3cbe561 100644 --- a/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m +++ b/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m @@ -442,6 +442,8 @@ NS_ASSUME_NONNULL_BEGIN [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], @@ -470,6 +472,16 @@ NS_ASSUME_NONNULL_BEGIN 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); @@ -574,6 +586,19 @@ NS_ASSUME_NONNULL_BEGIN 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 @@ -876,6 +901,19 @@ NS_ASSUME_NONNULL_BEGIN 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 @@ -1182,6 +1220,7 @@ NS_ASSUME_NONNULL_BEGIN [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 @@ -1363,6 +1402,7 @@ NS_ASSUME_NONNULL_BEGIN } [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 ⚠️"]]; diff --git a/Signal/src/ViewControllers/DebugUI/DebugUIMessagesAssetLoader.h b/Signal/src/ViewControllers/DebugUI/DebugUIMessagesAssetLoader.h index d6ce5dbdbd..909c604317 100644 --- a/Signal/src/ViewControllers/DebugUI/DebugUIMessagesAssetLoader.h +++ b/Signal/src/ViewControllers/DebugUI/DebugUIMessagesAssetLoader.h @@ -19,6 +19,7 @@ NS_ASSUME_NONNULL_BEGIN + (instancetype)jpegInstance; + (instancetype)gifInstance; ++ (instancetype)largeGifInstance; + (instancetype)mp3Instance; + (instancetype)mp4Instance; + (instancetype)compactPortraitPngInstance; diff --git a/Signal/src/ViewControllers/DebugUI/DebugUIMessagesAssetLoader.m b/Signal/src/ViewControllers/DebugUI/DebugUIMessagesAssetLoader.m index 079854474b..39800f2914 100644 --- a/Signal/src/ViewControllers/DebugUI/DebugUIMessagesAssetLoader.m +++ b/Signal/src/ViewControllers/DebugUI/DebugUIMessagesAssetLoader.m @@ -342,6 +342,18 @@ NS_ASSUME_NONNULL_BEGIN 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; From 3d07dc7c5be7ab14cdbdf625b35061b38b294712 Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Wed, 28 Mar 2018 16:38:48 -0400 Subject: [PATCH 23/31] Bubble collapse. --- .../ConversationView/ConversationViewController.m | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m index 241a1c1ca3..293e883d0f 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; From c8012d3891f556fbd7baa47776b1eab188262bea Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Thu, 29 Mar 2018 10:59:16 -0400 Subject: [PATCH 24/31] Bubble collapse. --- Signal.xcodeproj/project.pbxproj | 6 + .../Cells/OWSBubbleStrokeView.h | 20 ++ .../Cells/OWSBubbleStrokeView.m | 114 ++++++++++ .../ConversationView/Cells/OWSBubbleView.h | 9 +- .../ConversationView/Cells/OWSBubbleView.m | 44 +++- .../ConversationView/Cells/OWSMessageCell.m | 18 ++ .../ViewControllers/DebugUI/DebugUIMessages.m | 194 +++++++++++++++++- .../DebugUI/DebugUIMessagesAssetLoader.h | 4 + .../DebugUI/DebugUIMessagesAssetLoader.m | 11 + .../utils/OWSMessagesBubbleImageFactory.swift | 16 +- 10 files changed, 418 insertions(+), 18 deletions(-) create mode 100644 Signal/src/ViewControllers/ConversationView/Cells/OWSBubbleStrokeView.h create mode 100644 Signal/src/ViewControllers/ConversationView/Cells/OWSBubbleStrokeView.m diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index c10d5fb815..079b71d8fe 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -216,6 +216,7 @@ 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 */; }; @@ -841,6 +842,8 @@ 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 = ""; }; @@ -1651,6 +1654,8 @@ 34D1F0971F867BFC0066283D /* ConversationViewCell.m */, 34D1F0B81F8800D90066283D /* OWSAudioMessageView.h */, 34D1F0B91F8800D90066283D /* OWSAudioMessageView.m */, + 34DBF005206C3CB100025978 /* OWSBubbleStrokeView.h */, + 34DBF006206C3CB200025978 /* OWSBubbleStrokeView.m */, 34DBF002206BD5A500025978 /* OWSBubbleView.h */, 34DBF001206BD5A500025978 /* OWSBubbleView.m */, 34D1F09A1F867BFC0066283D /* OWSContactOffersCell.h */, @@ -3135,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 */, 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..c96c1f4ffa --- /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]; + + return self; +} + +- (void)setStrokeColor:(UIColor *)strokeColor +{ + _strokeColor = strokeColor; + + [self updateLayers]; +} + +- (void)setStrokeThickness:(CGFloat)strokeThickness +{ + _strokeThickness = strokeThickness; + + [self updateLayers]; +} + +- (void)setFrame:(CGRect)frame +{ + BOOL didChange = !CGSizeEqualToSize(self.frame.size, frame.size); + + [super setFrame:frame]; + + if (didChange || !self.shapeLayer) { + [self updateLayers]; + } +} + +- (void)setBounds:(CGRect)bounds +{ + BOOL didChange = !CGSizeEqualToSize(self.bounds.size, bounds.size); + + [super setBounds:bounds]; + + if (didChange || !self.shapeLayer) { + [self updateLayers]; + } +} + +- (void)setCenter:(CGPoint)center +{ + [super setCenter:center]; + + [self updateLayers]; +} + +- (void)updateLayers +{ + if (!self.shapeLayer) { + self.shapeLayer = [CAShapeLayer new]; + [self.layer addSublayer: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 index a1f5436aa6..7184fc3154 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSBubbleView.h +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSBubbleView.h @@ -13,16 +13,19 @@ 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) CAShapeLayer *maskLayer; -@property (nonatomic) CAShapeLayer *shapeLayer; - @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 index 0bde2233ab..0ee1379bc2 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSBubbleView.m +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSBubbleView.m @@ -3,6 +3,7 @@ // #import "OWSBubbleView.h" +#import "OWSBubbleStrokeView.h" #import NS_ASSUME_NONNULL_BEGIN @@ -17,6 +18,15 @@ 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 - (void)setIsOutgoing:(BOOL)isOutgoing @@ -26,7 +36,7 @@ const CGFloat kBubbleTextVInset = 10.f; _isOutgoing = isOutgoing; if (didChange || !self.shapeLayer) { - [self updateMask]; + [self updateLayers]; } } @@ -37,8 +47,11 @@ const CGFloat kBubbleTextVInset = 10.f; [super setFrame:frame]; if (didChange || !self.shapeLayer) { - [self updateMask]; + [self updateLayers]; } + + // We need to inform the "bubble stroke view" (if any) any time our frame changes. + [self.bubbleStrokeView updateLayers]; } - (void)setBounds:(CGRect)bounds @@ -48,8 +61,19 @@ const CGFloat kBubbleTextVInset = 10.f; [super setBounds:bounds]; if (didChange || !self.shapeLayer) { - [self updateMask]; + [self updateLayers]; } + + // We need to inform the "bubble stroke view" (if any) any time our frame changes. + [self.bubbleStrokeView updateLayers]; +} + +- (void)setCenter:(CGPoint)center +{ + [super setCenter:center]; + + // We need to inform the "bubble stroke view" (if any) any time our frame changes. + [self.bubbleStrokeView updateLayers]; } - (void)setBubbleColor:(nullable UIColor *)bubbleColor @@ -57,12 +81,12 @@ const CGFloat kBubbleTextVInset = 10.f; _bubbleColor = bubbleColor; if (!self.shapeLayer) { - [self updateMask]; + [self updateLayers]; } self.shapeLayer.fillColor = bubbleColor.CGColor; } -- (void)updateMask +- (void)updateLayers { if (!self.shapeLayer) { self.shapeLayer = [CAShapeLayer new]; @@ -73,15 +97,21 @@ const CGFloat kBubbleTextVInset = 10.f; self.layer.mask = self.maskLayer; } - UIBezierPath *bezierPath = - [self.class maskPathForSize:self.bounds.size isOutgoing:self.isOutgoing isRTL:self.isRTL]; + UIBezierPath *bezierPath = [self maskPath]; self.shapeLayer.fillColor = self.bubbleColor.CGColor; + // self.shapeLayer.bounds = self.bounds; self.shapeLayer.path = bezierPath.CGPath; + // self.maskLayer.bounds = self.bounds; self.maskLayer.path = bezierPath.CGPath; } +- (UIBezierPath *)maskPath +{ + return [self.class maskPathForSize:self.bounds.size isOutgoing:self.isOutgoing isRTL:self.isRTL]; +} + + (UIBezierPath *)maskPathForSize:(CGSize)size isOutgoing:(BOOL)isOutgoing isRTL:(BOOL)isRTL { UIBezierPath *bezierPath = [UIBezierPath new]; diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m index c0b4fb018d..260d1d7dc4 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m @@ -8,6 +8,7 @@ #import "ConversationViewItem.h" #import "NSAttributedString+OWS.h" #import "OWSAudioMessageView.h" +#import "OWSBubbleStrokeView.h" #import "OWSBubbleView.h" #import "OWSExpirationTimerView.h" #import "OWSGenericAttachmentView.h" @@ -426,6 +427,22 @@ CG_INLINE CGSize CGSizeCeil(CGSize size) } 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); + } } OWSMessageTextView *_Nullable bodyTextView = nil; @@ -1233,6 +1250,7 @@ CG_INLINE CGSize CGSizeCeil(CGSize size) self.bubbleView.hidden = YES; self.bubbleView.bubbleColor = nil; + self.bubbleView.bubbleStrokeView = nil; for (UIView *subview in self.bubbleView.subviews) { [subview removeFromSuperview]; diff --git a/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m b/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m index 61c3cbe561..08b531e5ca 100644 --- a/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m +++ b/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m @@ -703,6 +703,27 @@ NS_ASSUME_NONNULL_BEGIN 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 @@ -1018,6 +1039,27 @@ NS_ASSUME_NONNULL_BEGIN 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 @@ -1338,6 +1380,112 @@ NS_ASSUME_NONNULL_BEGIN [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 @@ -1476,6 +1624,44 @@ NS_ASSUME_NONNULL_BEGIN [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 ⚠️"]]; } @@ -1519,7 +1705,7 @@ NS_ASSUME_NONNULL_BEGIN { OWSAssert(thread); - return [DebugUIMessagesGroupAction allGroupActionWithLabel:@"Fake All Media" + return [DebugUIMessagesGroupAction allGroupActionWithLabel:@"All Fake Media" subactions:[self allFakeMediaActions:thread includeLabels:YES]]; } @@ -1527,7 +1713,7 @@ NS_ASSUME_NONNULL_BEGIN { OWSAssert(thread); - return [DebugUIMessagesGroupAction randomGroupActionWithLabel:@"Fake Random Media" + return [DebugUIMessagesGroupAction randomGroupActionWithLabel:@"Random Fake Media" subactions:[self allFakeMediaActions:thread includeLabels:NO]]; } @@ -1744,7 +1930,7 @@ NS_ASSUME_NONNULL_BEGIN { OWSAssert(thread); - return [DebugUIMessagesGroupAction allGroupActionWithLabel:@"Fake All Text" + return [DebugUIMessagesGroupAction allGroupActionWithLabel:@"All Fake Text" subactions:[self allFakeTextActions:thread includeLabels:YES]]; } @@ -1752,7 +1938,7 @@ NS_ASSUME_NONNULL_BEGIN { OWSAssert(thread); - return [DebugUIMessagesGroupAction randomGroupActionWithLabel:@"Fake Random Text" + return [DebugUIMessagesGroupAction randomGroupActionWithLabel:@"Random Fake Text" subactions:[self allFakeTextActions:thread includeLabels:NO]]; } diff --git a/Signal/src/ViewControllers/DebugUI/DebugUIMessagesAssetLoader.h b/Signal/src/ViewControllers/DebugUI/DebugUIMessagesAssetLoader.h index 909c604317..dc35286d57 100644 --- a/Signal/src/ViewControllers/DebugUI/DebugUIMessagesAssetLoader.h +++ b/Signal/src/ViewControllers/DebugUI/DebugUIMessagesAssetLoader.h @@ -28,6 +28,10 @@ NS_ASSUME_NONNULL_BEGIN + (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; diff --git a/Signal/src/ViewControllers/DebugUI/DebugUIMessagesAssetLoader.m b/Signal/src/ViewControllers/DebugUI/DebugUIMessagesAssetLoader.m index 39800f2914..6781d3450f 100644 --- a/Signal/src/ViewControllers/DebugUI/DebugUIMessagesAssetLoader.m +++ b/Signal/src/ViewControllers/DebugUI/DebugUIMessagesAssetLoader.m @@ -456,6 +456,17 @@ NS_ASSUME_NONNULL_BEGIN 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; diff --git a/SignalMessaging/utils/OWSMessagesBubbleImageFactory.swift b/SignalMessaging/utils/OWSMessagesBubbleImageFactory.swift index 9a3907bce9..eb8bccdc64 100644 --- a/SignalMessaging/utils/OWSMessagesBubbleImageFactory.swift +++ b/SignalMessaging/utils/OWSMessagesBubbleImageFactory.swift @@ -57,17 +57,25 @@ 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 UIColor.jsq_messageBubbleLightGray()! + return OWSMessagesBubbleImageFactory.bubbleColorIncoming } else if let outgoingMessage = message as? TSOutgoingMessage { switch outgoingMessage.messageState { case .unsent: - return UIColor.gray + return OWSMessagesBubbleImageFactory.bubbleColorOutgoingUnsent case .attemptingOut: - return UIColor.ows_fadedBlue + return OWSMessagesBubbleImageFactory.bubbleColorOutgoingSending default: - return UIColor.ows_materialBlue + return OWSMessagesBubbleImageFactory.bubbleColorOutgoingSent } } else { owsFail("Unexpected message type: \(message)") From 578f40d7919bf5ed87c775ba9348ae410a2a5904 Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Thu, 29 Mar 2018 11:25:19 -0400 Subject: [PATCH 25/31] Bubble collapse. --- .../ConversationView/Cells/OWSMessageCell.m | 36 +++---- .../ConversationView/ConversationViewItem.h | 4 +- .../ConversationView/ConversationViewItem.m | 99 ++++++++++--------- .../MessageDetailViewController.swift | 4 +- 4 files changed, 72 insertions(+), 71 deletions(-) diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m index 260d1d7dc4..55c79529e8 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m @@ -155,7 +155,7 @@ CG_INLINE CGSize CGSizeCeil(CGSize size) OWSAssert(DisplayableText.kMaxJumbomojiCount == 5); CGFloat basePointSize = [UIFont ows_dynamicTypeBodyFont].pointSize; - switch (self.displayableText.jumbomojiCount) { + switch (self.displayableBodyText.jumbomojiCount) { case 0: break; case 1: @@ -167,7 +167,7 @@ CG_INLINE CGSize CGSizeCeil(CGSize size) 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; } @@ -211,20 +211,20 @@ CG_INLINE CGSize CGSizeCeil(CGSize size) 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 @@ -271,7 +271,7 @@ CG_INLINE CGSize CGSizeCeil(CGSize size) case OWSMessageCellType_AnimatedImage: case OWSMessageCellType_Audio: case OWSMessageCellType_Video: - return self.hasText; + return self.hasBodyText; } } @@ -289,7 +289,7 @@ CG_INLINE CGSize CGSizeCeil(CGSize size) case OWSMessageCellType_Audio: case OWSMessageCellType_Video: // Is there a caption? - return self.hasText; + return self.hasBodyText; } } @@ -448,7 +448,7 @@ CG_INLINE CGSize CGSizeCeil(CGSize size) 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.hasText || !bodyMediaView) { + if (self.hasBodyText || !bodyMediaView) { bodyTextView = [self createBodyTextViewIfNecessary]; } if (bodyTextView) { @@ -754,7 +754,7 @@ CG_INLINE CGSize CGSizeCeil(CGSize size) shouldIgnoreEvents = outgoingMessage.messageState != TSOutgoingMessageStateSentToService; } [self.class loadForTextDisplay:self.bodyTextViewCached - text:self.displayableText.displayText + text:self.displayableBodyText.displayText textColor:self.textColor font:self.textMessageFont shouldIgnoreEvents:shouldIgnoreEvents]; @@ -783,10 +783,10 @@ CG_INLINE CGSize CGSizeCeil(CGSize size) - (nullable UIView *)createTapForMoreLabelIfNecessary { - if (!self.hasText) { + if (!self.hasBodyText) { return nil; } - if (!self.displayableText.isTextTruncated) { + if (!self.displayableBodyText.isTextTruncated) { return nil; } @@ -1050,7 +1050,7 @@ CG_INLINE CGSize CGSizeCeil(CGSize size) // Size of "message body" text, not quoted reply text. - (CGSize)bodyTextSizeForContentWidth:(int)contentWidth includeMargins:(BOOL)includeMargins { - if (!self.hasText) { + if (!self.hasBodyText) { return CGSizeZero; } @@ -1061,7 +1061,7 @@ CG_INLINE CGSize CGSizeCeil(CGSize size) const int maxMessageWidth = [self maxMessageWidthForContentWidth:contentWidth]; const int maxTextWidth = (int)floor(maxMessageWidth - (leftMargin + rightMargin)); - self.bodyTextViewCached.text = self.displayableText.displayText; + self.bodyTextViewCached.text = self.displayableBodyText.displayText; // Honor dynamic type in the message bodies. self.bodyTextViewCached.font = [self textMessageFont]; CGSize textSize = CGSizeCeil([self.bodyTextViewCached sizeThatFits:CGSizeMake(maxTextWidth, CGFLOAT_MAX)]); @@ -1142,7 +1142,7 @@ CG_INLINE CGSize CGSizeCeil(CGSize size) cellSize.height += self.dateHeaderHeight; cellSize.height += self.footerHeight; - if (self.hasText && self.displayableText.isTextTruncated) { + if (self.hasBodyText && self.displayableBodyText.isTextTruncated) { cellSize.height += self.tapForMoreHeight; } @@ -1344,7 +1344,7 @@ CG_INLINE CGSize CGSizeCeil(CGSize size) } } - if (self.hasText && self.displayableText.isTextTruncated) { + if (self.hasBodyText && self.displayableBodyText.isTextTruncated) { [self.delegate didTapTruncatedTextMessage:self.viewItem]; return; } @@ -1375,7 +1375,7 @@ CG_INLINE CGSize CGSizeCeil(CGSize size) break; case OWSMessageCellType_TextMessage: case OWSMessageCellType_OversizeTextMessage: - if (self.displayableText.isTextTruncated) { + if (self.displayableBodyText.isTextTruncated) { [self.delegate didTapTruncatedTextMessage:self.viewItem]; return; } diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewItem.h b/Signal/src/ViewControllers/ConversationView/ConversationViewItem.h index 8f7e345c81..1777bf38d9 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewItem.h +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewItem.h @@ -43,7 +43,7 @@ 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; @@ -79,7 +79,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 a26eac86f8..0c6de073d3 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewItem.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewItem.m @@ -56,12 +56,12 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType) @property (nonatomic) BOOL hasViewState; @property (nonatomic) OWSMessageCellType messageCellType; // TODO: Rename to displayableBodyText. -@property (nonatomic, nullable) DisplayableText *displayableText; +@property (nonatomic, nullable) DisplayableText *displayableBodyText; @property (nonatomic, nullable) TSAttachmentStream *attachmentStream; @property (nonatomic, nullable) TSAttachmentPointer *attachmentPointer; @property (nonatomic) CGSize mediaSize; // TODO: Remove in favor of displayableBodyText nil test. -//@property (nonatomic) BOOL hasText; +//@property (nonatomic) BOOL hasBodyText; @end @@ -97,7 +97,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; @@ -107,9 +107,9 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType) [self ensureViewState:transaction]; } -- (BOOL)hasText +- (BOOL)hasBodyText { - return _displayableText != nil; + return _displayableBodyText != nil; } - (void)setShouldShowDate:(BOOL)shouldShowDate @@ -271,7 +271,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; @@ -283,44 +283,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 *)displayableTextForOversizeTextAttachment:(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 @@ -360,9 +361,9 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType) if ([attachment.contentType isEqualToString:OWSMimeTypeOversizeTextMessage]) { self.messageCellType = OWSMessageCellType_OversizeTextMessage; - self.displayableText = [self displayableTextForOversizeTextAttachment:self.attachmentStream - interactionId:message.uniqueId]; - // self.hasText = YES; + self.displayableBodyText = [self displayableBodyTextForOversizeTextAttachment:self.attachmentStream + interactionId:message.uniqueId]; + // self.hasBodyText = YES; } else if ([self.attachmentStream isAnimated] || [self.attachmentStream isImage] || [self.attachmentStream isVideo]) { if ([self.attachmentStream isAnimated]) { @@ -407,19 +408,19 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType) // Ignore message body for oversize text attachments. if (message.body.length > 0) { - if (self.hasText) { + if (self.hasBodyText) { OWSFail(@"%@ oversize text message has unexpected caption.", self.logTag); } - // self.hasText = YES; + // self.hasBodyText = YES; // 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) { @@ -427,8 +428,8 @@ 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.hasBodyText = YES; + self.displayableBodyText = [[DisplayableText alloc] initWithFullText:@"" displayText:@"" isTextTruncated:NO]; } } @@ -439,16 +440,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 @@ -551,13 +552,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) { @@ -579,8 +580,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: { @@ -638,8 +639,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: { @@ -746,9 +747,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/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 From 3ca2c08b06a1296dad5569ccfc4ca5e74d28047c Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Thu, 29 Mar 2018 11:49:02 -0400 Subject: [PATCH 26/31] Bubble collapse. --- .../ConversationView/Cells/OWSBubbleView.m | 42 ++++++++++++------- .../ConversationView/Cells/OWSMessageCell.m | 34 ++++++--------- .../ConversationViewController.m | 14 ++++++- .../ConversationView/ConversationViewItem.h | 1 + .../ConversationView/ConversationViewItem.m | 11 +++++ 5 files changed, 65 insertions(+), 37 deletions(-) diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSBubbleView.m b/Signal/src/ViewControllers/ConversationView/Cells/OWSBubbleView.m index 0ee1379bc2..15565b9dc3 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSBubbleView.m +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSBubbleView.m @@ -40,6 +40,17 @@ const CGFloat kBubbleTextVInset = 10.f; } } +- (void)setHideTail:(BOOL)hideTail +{ + BOOL didChange = _hideTail != hideTail; + + _hideTail = hideTail; + + if (didChange || !self.shapeLayer) { + [self updateLayers]; + } +} + - (void)setFrame:(CGRect)frame { BOOL didChange = !CGSizeEqualToSize(self.frame.size, frame.size); @@ -100,24 +111,25 @@ const CGFloat kBubbleTextVInset = 10.f; UIBezierPath *bezierPath = [self maskPath]; self.shapeLayer.fillColor = self.bubbleColor.CGColor; - // self.shapeLayer.bounds = self.bounds; self.shapeLayer.path = bezierPath.CGPath; - // self.maskLayer.bounds = self.bounds; self.maskLayer.path = bezierPath.CGPath; } - (UIBezierPath *)maskPath { - return [self.class maskPathForSize:self.bounds.size isOutgoing:self.isOutgoing isRTL:self.isRTL]; + return [self.class maskPathForSize:self.bounds.size + isOutgoing:self.isOutgoing + hideTail:self.hideTail + isRTL:self.isRTL]; } -+ (UIBezierPath *)maskPathForSize:(CGSize)size isOutgoing:(BOOL)isOutgoing isRTL:(BOOL)isRTL ++ (UIBezierPath *)maskPathForSize:(CGSize)size isOutgoing:(BOOL)isOutgoing hideTail:(BOOL)hideTail isRTL:(BOOL)isRTL { UIBezierPath *bezierPath = [UIBezierPath new]; CGFloat bubbleLeft = 0.f; - CGFloat bubbleRight = size.width - kBubbleThornSideInset; + CGFloat bubbleRight = size.width - (hideTail ? 0.f : kBubbleThornSideInset); CGFloat bubbleTop = 0.f; CGFloat bubbleBottom = size.height - kBubbleThornVInset; @@ -135,15 +147,17 @@ const CGFloat kBubbleTextVInset = 10.f; [bezierPath addQuadCurveToPoint:CGPointMake(bubbleLeft + kBubbleHRounding, bubbleTop) controlPoint:CGPointMake(bubbleLeft, bubbleTop)]; - // 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]; + 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; diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m index 55c79529e8..a6af7c801e 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m @@ -21,8 +21,6 @@ NS_ASSUME_NONNULL_BEGIN -// TODO: Review all comments. - CG_INLINE CGSize CGSizeCeil(CGSize size) { return CGSizeMake((CGFloat)ceil(size.width), (CGFloat)ceil(size.height)); @@ -308,7 +306,7 @@ CG_INLINE CGSize CGSizeCeil(CGSize size) // TODO: We might not need to hide it. self.bubbleView.hidden = NO; self.bubbleView.isOutgoing = self.isOutgoing; - // TODO: Hide tails/thorns here? + self.bubbleView.hideTail = self.viewItem.shouldHideBubbleTail; if (self.shouldHaveFailedSendBadge) { self.failedSendBadgeView = [UIImageView new]; @@ -344,15 +342,6 @@ CG_INLINE CGSize CGSizeCeil(CGSize size) [self updateDateHeader]; [self updateFooter]; - // TODO: Do we need to pin the bubble size? - { - // - (CGSize)cellSizeForViewWidth:(int)viewWidth contentWidth:(int)contentWidth - // CGSize mediaSize = [self bodyMediaSizeForContentWidth:self.contentWidth]; - // TODO: - // [self.viewConstraints addObjectsFromArray:[self.mediaMaskingView - // autoSetDimensionsToSize:mediaSize]]; - } - UIView *_Nullable lastSubview = nil; CGFloat bottomMargin = 0; UIView *_Nullable bodyMediaView = nil; @@ -549,8 +538,7 @@ CG_INLINE CGSize CGSizeCeil(CGSize size) } 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. + // TODO: Do we need to hide/remove the media view? [self showAttachmentErrorView:mediaView]; } return cellMedia; @@ -560,10 +548,6 @@ CG_INLINE CGSize CGSizeCeil(CGSize size) // * If cell is not visible, eagerly unload view contents. - (void)ensureMediaLoadState { - // CGSize mediaSize = [self bodyMediaSizeForContentWidth:self.contentWidth]; - // TODO: - // [self.viewConstraints addObjectsFromArray:[self.mediaMaskingView autoSetDimensionsToSize:mediaSize]]; - if (!self.isCellVisible) { // Eagerly unload. if (self.unloadCellContentBlock) { @@ -1092,8 +1076,6 @@ CG_INLINE CGSize CGSizeCeil(CGSize size) 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 @@ -1187,12 +1169,20 @@ CG_INLINE CGSize CGSizeCeil(CGSize size) - (CGFloat)textLeadingMargin { - return (self.isIncoming ? kBubbleTextHInset + kBubbleThornSideInset : kBubbleTextHInset); + CGFloat result = kBubbleTextHInset; + if (self.isIncoming && !self.viewItem.shouldHideBubbleTail) { + result += kBubbleThornSideInset; + } + return result; } - (CGFloat)textTrailingMargin { - return (self.isIncoming ? kBubbleTextHInset : kBubbleTextHInset + kBubbleThornSideInset); + CGFloat result = kBubbleTextHInset; + if (!self.isIncoming && !self.viewItem.shouldHideBubbleTail) { + result += kBubbleThornSideInset; + } + return result; } - (CGFloat)textTopMargin diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m index 293e883d0f..8854eb3f62 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m @@ -4463,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) { @@ -4473,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; @@ -4491,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 1777bf38d9..0ecf54be2f 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewItem.h +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewItem.h @@ -46,6 +46,7 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType); @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 diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewItem.m b/Signal/src/ViewControllers/ConversationView/ConversationViewItem.m index 0c6de073d3..32210f6503 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewItem.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewItem.m @@ -134,6 +134,17 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType) [self clearCachedLayoutState]; } +- (void)setShouldHideBubbleTail:(BOOL)shouldHideBubbleTail +{ + if (_shouldHideBubbleTail == shouldHideBubbleTail) { + return; + } + + _shouldHideBubbleTail = shouldHideBubbleTail; + + [self clearCachedLayoutState]; +} + - (void)clearCachedLayoutState { self.cachedCellSize = nil; From 4f9085a76141f0b268d51b586c8909663392a255 Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Thu, 29 Mar 2018 13:32:07 -0400 Subject: [PATCH 27/31] Bubble collapse. --- .../ConversationView/Cells/OWSBubbleView.h | 1 + .../ConversationView/Cells/OWSBubbleView.m | 20 ++- .../ConversationView/Cells/OWSMessageCell.m | 48 ++---- .../ConversationView/ConversationViewItem.m | 6 - .../ViewControllers/DebugUI/DebugUIMessages.m | 158 +++++++++++++++++- 5 files changed, 189 insertions(+), 44 deletions(-) diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSBubbleView.h b/Signal/src/ViewControllers/ConversationView/Cells/OWSBubbleView.h index 7184fc3154..b7ea6b8718 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSBubbleView.h +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSBubbleView.h @@ -21,6 +21,7 @@ extern const CGFloat kBubbleTextVInset; @property (nonatomic) BOOL isOutgoing; @property (nonatomic) BOOL hideTail; +@property (nonatomic) BOOL isTruncated; @property (nonatomic, nullable) UIColor *bubbleColor; diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSBubbleView.m b/Signal/src/ViewControllers/ConversationView/Cells/OWSBubbleView.m index 15565b9dc3..829464cd83 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSBubbleView.m +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSBubbleView.m @@ -51,6 +51,17 @@ const CGFloat kBubbleTextVInset = 10.f; } } +- (void)setIsTruncated:(BOOL)isTruncated +{ + BOOL didChange = _isTruncated != isTruncated; + + _isTruncated = isTruncated; + + if (didChange || !self.shapeLayer) { + [self updateLayers]; + } +} + - (void)setFrame:(CGRect)frame { BOOL didChange = !CGSizeEqualToSize(self.frame.size, frame.size); @@ -121,15 +132,20 @@ const CGFloat kBubbleTextVInset = 10.f; 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 isRTL:(BOOL)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 - (hideTail ? 0.f : kBubbleThornSideInset); + CGFloat bubbleRight = size.width - kBubbleThornSideInset; CGFloat bubbleTop = 0.f; CGFloat bubbleBottom = size.height - kBubbleThornVInset; diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m index a6af7c801e..30f9154a3f 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m @@ -465,22 +465,6 @@ CG_INLINE CGSize CGSizeCeil(CGSize size) bottomMargin = self.textBottomMargin; } -#ifdef DEBUG - DDLogVerbose( - @"%@ --- bodyMediaView: %@ [%@].", self.logTag, bodyMediaView, NSStringFromCGSize(bodyMediaContentSize)); - DDLogVerbose(@"%@ --- bodyTextView: %@ (%zd) [%@].", - self.logTag, - bodyTextView.text, - bodyTextView.text.length, - NSStringFromCGSize(bodyTextContentSize)); - [bodyMediaView logFrameLaterWithLabel:@"bodyMediaView"]; - [bodyTextView logFrameLaterWithLabel:@"bodyTextView"]; - [self.bubbleView logFrameLaterWithLabel:@"bubbleView"]; - [self.contentView logFrameLaterWithLabel:@"contentView"]; -// [bodyMediaView addRedBorder]; -// [bodyTextView addRedBorder]; -#endif - UIView *_Nullable tapForMoreLabel = [self createTapForMoreLabelIfNecessary]; if (tapForMoreLabel) { OWSAssert(lastSubview); @@ -765,12 +749,20 @@ CG_INLINE CGSize CGSizeCeil(CGSize size) textView.shouldIgnoreEvents = shouldIgnoreEvents; } -- (nullable UIView *)createTapForMoreLabelIfNecessary +- (BOOL)hasTapForMore { if (!self.hasBodyText) { - return nil; + return NO; + } else if (!self.displayableBodyText.isTextTruncated) { + return NO; + } else { + return YES; } - if (!self.displayableBodyText.isTextTruncated) { +} + +- (nullable UIView *)createTapForMoreLabelIfNecessary +{ + if (!self.hasTapForMore) { return nil; } @@ -1124,7 +1116,7 @@ CG_INLINE CGSize CGSizeCeil(CGSize size) cellSize.height += self.dateHeaderHeight; cellSize.height += self.footerHeight; - if (self.hasBodyText && self.displayableBodyText.isTextTruncated) { + if (self.hasTapForMore) { cellSize.height += self.tapForMoreHeight; } @@ -1134,14 +1126,6 @@ CG_INLINE CGSize CGSizeCeil(CGSize size) cellSize = CGSizeCeil(cellSize); - DDLogVerbose(@"%@ --- cellSizeForViewWidth: %@ + %@ + %f + %f = %@.", - self.logTag, - NSStringFromCGSize(mediaContentSize), - NSStringFromCGSize(textContentSize), - self.dateHeaderHeight, - self.footerHeight, - NSStringFromCGSize(cellSize)); - return cellSize; } @@ -1170,7 +1154,7 @@ CG_INLINE CGSize CGSizeCeil(CGSize size) - (CGFloat)textLeadingMargin { CGFloat result = kBubbleTextHInset; - if (self.isIncoming && !self.viewItem.shouldHideBubbleTail) { + if (self.isIncoming) { result += kBubbleThornSideInset; } return result; @@ -1179,7 +1163,7 @@ CG_INLINE CGSize CGSizeCeil(CGSize size) - (CGFloat)textTrailingMargin { CGFloat result = kBubbleTextHInset; - if (!self.isIncoming && !self.viewItem.shouldHideBubbleTail) { + if (!self.isIncoming) { result += kBubbleThornSideInset; } return result; @@ -1334,7 +1318,7 @@ CG_INLINE CGSize CGSizeCeil(CGSize size) } } - if (self.hasBodyText && self.displayableBodyText.isTextTruncated) { + if (self.hasTapForMore) { [self.delegate didTapTruncatedTextMessage:self.viewItem]; return; } @@ -1365,7 +1349,7 @@ CG_INLINE CGSize CGSizeCeil(CGSize size) break; case OWSMessageCellType_TextMessage: case OWSMessageCellType_OversizeTextMessage: - if (self.displayableBodyText.isTextTruncated) { + if (self.hasTapForMore) { [self.delegate didTapTruncatedTextMessage:self.viewItem]; return; } diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewItem.m b/Signal/src/ViewControllers/ConversationView/ConversationViewItem.m index 32210f6503..cd55621b27 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewItem.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewItem.m @@ -411,12 +411,6 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType) } } - DDLogVerbose(@"%@ --- message. hasAttachments: %d, body: %@ (%zd).", - self.logTag, - message.hasAttachments, - message.body, - message.body.length); - // Ignore message body for oversize text attachments. if (message.body.length > 0) { if (self.hasBodyText) { diff --git a/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m b/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m index 08b531e5ca..486414d2b0 100644 --- a/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m +++ b/Signal/src/ViewControllers/DebugUI/DebugUIMessages.m @@ -59,6 +59,8 @@ NS_ASSUME_NONNULL_BEGIN // Fake Text [DebugUIMessages fakeAllTextAction:thread], [DebugUIMessages fakeRandomTextAction:thread], + // Sequences + [DebugUIMessages allFakeSequencesAction:thread], // Exemplary [DebugUIMessages allFakeAction:thread], ]) { @@ -1788,11 +1790,12 @@ NS_ASSUME_NONNULL_BEGIN 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:text]; + messageBody:messageBody]; [message markAsReadWithTransaction:transaction sendReadReceipt:NO updateExpiration:NO]; }]; } @@ -1806,9 +1809,10 @@ NS_ASSUME_NONNULL_BEGIN 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:text]; + messageBody:messageBody]; [message saveWithTransaction:transaction]; [message updateWithMessageState:messageState transaction:transaction]; }]; @@ -1824,6 +1828,19 @@ NS_ASSUME_NONNULL_BEGIN 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); @@ -1847,8 +1864,7 @@ NS_ASSUME_NONNULL_BEGIN return [DebugUIMessagesSingleAction actionWithLabel:label unstaggeredActionBlock:^(NSUInteger index, YapDatabaseReadWriteTransaction *transaction) { - NSString *messageBody = - [[@(index).stringValue stringByAppendingString:@" "] stringByAppendingString:[self randomText]]; + NSString *messageBody = [[@(index).stringValue stringByAppendingString:@" "] stringByAppendingString:text]; TSOutgoingMessage *message = [[TSOutgoingMessage alloc] initWithTimestamp:[NSDate ows_millisecondTimeStamp] inThread:thread messageBody:messageBody]; @@ -1951,6 +1967,7 @@ NS_ASSUME_NONNULL_BEGIN 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; } @@ -1989,6 +2006,139 @@ NS_ASSUME_NONNULL_BEGIN [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 From 12bcf887c150e4fd6160eb8e7af21e884af76975 Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Thu, 29 Mar 2018 13:44:14 -0400 Subject: [PATCH 28/31] Bubble collapse. --- .../ViewControllers/ConversationView/ConversationViewItem.m | 6 ------ 1 file changed, 6 deletions(-) diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewItem.m b/Signal/src/ViewControllers/ConversationView/ConversationViewItem.m index cd55621b27..da1d4badc8 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationViewItem.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationViewItem.m @@ -55,13 +55,10 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType) @property (nonatomic) BOOL hasViewState; @property (nonatomic) OWSMessageCellType messageCellType; -// TODO: Rename to displayableBodyText. @property (nonatomic, nullable) DisplayableText *displayableBodyText; @property (nonatomic, nullable) TSAttachmentStream *attachmentStream; @property (nonatomic, nullable) TSAttachmentPointer *attachmentPointer; @property (nonatomic) CGSize mediaSize; -// TODO: Remove in favor of displayableBodyText nil test. -//@property (nonatomic) BOOL hasBodyText; @end @@ -374,7 +371,6 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType) self.messageCellType = OWSMessageCellType_OversizeTextMessage; self.displayableBodyText = [self displayableBodyTextForOversizeTextAttachment:self.attachmentStream interactionId:message.uniqueId]; - // self.hasBodyText = YES; } else if ([self.attachmentStream isAnimated] || [self.attachmentStream isImage] || [self.attachmentStream isVideo]) { if ([self.attachmentStream isAnimated]) { @@ -417,7 +413,6 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType) OWSFail(@"%@ oversize text message has unexpected caption.", self.logTag); } - // self.hasBodyText = YES; // 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) { @@ -433,7 +428,6 @@ 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.hasBodyText = YES; self.displayableBodyText = [[DisplayableText alloc] initWithFullText:@"" displayText:@"" isTextTruncated:NO]; } } From 31f062ed118e321bb6dc831763422eec0f6b890f Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Thu, 29 Mar 2018 13:47:30 -0400 Subject: [PATCH 29/31] Bubble collapse. --- .../src/ViewControllers/HomeViewController.m | 5 --- SignalMessaging/categories/UIView+OWS.h | 18 --------- SignalMessaging/categories/UIView+OWS.m | 40 ------------------- .../Messages/Attachments/TSAttachmentStream.m | 2 - 4 files changed, 65 deletions(-) diff --git a/Signal/src/ViewControllers/HomeViewController.m b/Signal/src/ViewControllers/HomeViewController.m index 9c3f41dc14..dd48ef95ef 100644 --- a/Signal/src/ViewControllers/HomeViewController.m +++ b/Signal/src/ViewControllers/HomeViewController.m @@ -284,11 +284,6 @@ typedef NS_ENUM(NSInteger, CellState) { kArchiveState, kInboxState }; } [self updateBarButtonItems]; - - dispatch_async(dispatch_get_main_queue(), ^{ - TSThread *thread = [self threadForIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]]; - [self presentThread:thread keyboardOnViewAppearing:NO callOnViewAppearing:NO]; - }); } - (void)viewDidAppear:(BOOL)animated diff --git a/SignalMessaging/categories/UIView+OWS.h b/SignalMessaging/categories/UIView+OWS.h index 5479c85841..095624c387 100644 --- a/SignalMessaging/categories/UIView+OWS.h +++ b/SignalMessaging/categories/UIView+OWS.h @@ -125,22 +125,4 @@ CGFloat ScaleFromIPhone5(CGFloat iPhone5Value); @end -//#pragma mark - -// -//@interface NSLayoutConstraint (OWS) -// -//- (NSLayoutConstraint *)withLowPriority; -//- (NSLayoutConstraint *)withHighPriority; -// -//@end -// -//#pragma mark - -// -//@interface NSArray (OWSLayout) -// -//- (NSArray *)withLowPriorities; -//- (NSArray *)withHighPriorities; -// -//@end - NS_ASSUME_NONNULL_END diff --git a/SignalMessaging/categories/UIView+OWS.m b/SignalMessaging/categories/UIView+OWS.m index c83533372a..2297497ff7 100644 --- a/SignalMessaging/categories/UIView+OWS.m +++ b/SignalMessaging/categories/UIView+OWS.m @@ -487,44 +487,4 @@ CGFloat ScaleFromIPhone5(CGFloat iPhone5Value) @end -//#pragma mark - -// -//@implementation NSLayoutConstraint (OWS) -// -//- (NSLayoutConstraint *)withLowPriority -//{ -// self.priority = UILayoutPriorityDefaultLow; -// return self; -//} -// -//- (NSLayoutConstraint *)withHighPriority -//{ -// self.priority = UILayoutPriorityDefaultHigh; -// return self; -//} -// -//@end -// -//#pragma mark - -// -//@implementation NSArray (OWSLayout) -// -//- (instancetype)withLowPriorities -//{ -// for (NSLayoutConstraint *constraint in self) { -// constraint.priority = UILayoutPriorityDefaultLow; -// } -// return self; -//} -// -//- (instancetype)withHighPriorities -//{ -// for (NSLayoutConstraint *constraint in self) { -// constraint.priority = UILayoutPriorityDefaultHigh; -// } -// return self; -//} -// -//@end - NS_ASSUME_NONNULL_END diff --git a/SignalServiceKit/src/Messages/Attachments/TSAttachmentStream.m b/SignalServiceKit/src/Messages/Attachments/TSAttachmentStream.m index 33bcd2141d..15ca2fbd9f 100644 --- a/SignalServiceKit/src/Messages/Attachments/TSAttachmentStream.m +++ b/SignalServiceKit/src/Messages/Attachments/TSAttachmentStream.m @@ -572,8 +572,6 @@ NS_ASSUME_NONNULL_BEGIN OWSAssertIsOnMainThread(); OWSAssert([self isAudio]); - // return 0; - NSError *error; AVAudioPlayer *audioPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:self.mediaURL error:&error]; if (error && [error.domain isEqualToString:NSOSStatusErrorDomain] From f98c45603ccbdf3692e45fe5f263924f1cef4edd Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Mon, 2 Apr 2018 11:13:11 -0400 Subject: [PATCH 30/31] Respond to CR. --- .../Cells/AttachmentUploadView.h | 3 +- .../Cells/AttachmentUploadView.m | 7 +- .../Cells/OWSBubbleStrokeView.m | 12 +-- .../ConversationView/Cells/OWSBubbleView.m | 49 +++++++----- .../ConversationView/Cells/OWSMessageCell.m | 75 ++++++++++++------- .../utils/OWSMessagesBubbleImageFactory.swift | 8 +- 6 files changed, 94 insertions(+), 60 deletions(-) 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.m b/Signal/src/ViewControllers/ConversationView/Cells/OWSBubbleStrokeView.m index c96c1f4ffa..81feef6e47 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSBubbleStrokeView.m +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSBubbleStrokeView.m @@ -28,6 +28,9 @@ NS_ASSUME_NONNULL_BEGIN self.opaque = NO; self.backgroundColor = [UIColor clearColor]; + self.shapeLayer = [CAShapeLayer new]; + [self.layer addSublayer:self.shapeLayer]; + return self; } @@ -47,7 +50,7 @@ NS_ASSUME_NONNULL_BEGIN - (void)setFrame:(CGRect)frame { - BOOL didChange = !CGSizeEqualToSize(self.frame.size, frame.size); + BOOL didChange = !CGRectEqualToRect(self.frame, frame); [super setFrame:frame]; @@ -58,7 +61,7 @@ NS_ASSUME_NONNULL_BEGIN - (void)setBounds:(CGRect)bounds { - BOOL didChange = !CGSizeEqualToSize(self.bounds.size, bounds.size); + BOOL didChange = !CGRectEqualToRect(self.bounds, bounds); [super setBounds:bounds]; @@ -76,10 +79,7 @@ NS_ASSUME_NONNULL_BEGIN - (void)updateLayers { - if (!self.shapeLayer) { - self.shapeLayer = [CAShapeLayer new]; - [self.layer addSublayer:self.shapeLayer]; - } + OWSAssert(self.shapeLayer); // Don't fill the shape layer; we just want a stroke around the border. self.shapeLayer.fillColor = [UIColor clearColor].CGColor; diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSBubbleView.m b/Signal/src/ViewControllers/ConversationView/Cells/OWSBubbleView.m index 829464cd83..a020da0e19 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSBubbleView.m +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSBubbleView.m @@ -8,7 +8,6 @@ NS_ASSUME_NONNULL_BEGIN -// This approximates the curve of our message bubbles, which makes the animation feel a little smoother. const CGFloat kOWSMessageCellCornerRadius = 17; const CGFloat kBubbleVRounding = kOWSMessageCellCornerRadius; @@ -29,6 +28,22 @@ const CGFloat kBubbleTextVInset = 10.f; @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; @@ -64,29 +79,35 @@ const CGFloat kBubbleTextVInset = 10.f; - (void)setFrame:(CGRect)frame { - BOOL didChange = !CGSizeEqualToSize(self.frame.size, frame.size); + // 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 (didChange || !self.shapeLayer) { + if (didChangeSize || !self.shapeLayer) { [self updateLayers]; } - // We need to inform the "bubble stroke view" (if any) any time our frame changes. + // 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 { - BOOL didChange = !CGSizeEqualToSize(self.bounds.size, bounds.size); + // 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 (didChange || !self.shapeLayer) { + if (didChangeSize || !self.shapeLayer) { [self updateLayers]; } - // We need to inform the "bubble stroke view" (if any) any time our frame changes. + // 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]; } @@ -94,7 +115,8 @@ const CGFloat kBubbleTextVInset = 10.f; { [super setCenter:center]; - // We need to inform the "bubble stroke view" (if any) any time our frame changes. + // 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]; } @@ -110,20 +132,13 @@ const CGFloat kBubbleTextVInset = 10.f; - (void)updateLayers { - if (!self.shapeLayer) { - self.shapeLayer = [CAShapeLayer new]; - [self.layer addSublayer:self.shapeLayer]; - } - if (!self.maskLayer) { - self.maskLayer = [CAShapeLayer new]; - self.layer.mask = self.maskLayer; - } + 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; } diff --git a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m index 30f9154a3f..0f0eedb258 100644 --- a/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m +++ b/Signal/src/ViewControllers/ConversationView/Cells/OWSMessageCell.m @@ -42,7 +42,7 @@ CG_INLINE CGSize CGSizeCeil(CGSize size) @property (nonatomic) OWSBubbleView *bubbleView; @property (nonatomic) UILabel *dateHeaderLabel; -@property (nonatomic) OWSMessageTextView *bodyTextViewCached; +@property (nonatomic) OWSMessageTextView *bodyTextView; @property (nonatomic, nullable) UIImageView *failedSendBadgeView; @property (nonatomic) UIView *footerView; @property (nonatomic) UILabel *footerLabel; @@ -75,7 +75,7 @@ CG_INLINE CGSize CGSizeCeil(CGSize size) - (void)commontInit { - OWSAssert(!self.bodyTextViewCached); + OWSAssert(!self.bodyTextView); _viewConstraints = [NSMutableArray new]; @@ -95,7 +95,7 @@ CG_INLINE CGSize CGSizeCeil(CGSize size) self.dateHeaderLabel.textColor = [UIColor lightGrayColor]; [self.contentView addSubview:self.dateHeaderLabel]; - self.bodyTextViewCached = [self newTextView]; + self.bodyTextView = [self newTextView]; self.footerLabel = [UILabel new]; self.footerLabel.font = [UIFont ows_regularFontWithSize:12.f]; @@ -103,7 +103,7 @@ CG_INLINE CGSize CGSizeCeil(CGSize size) [self.footerView addSubview:self.footerLabel]; // Hide these views by default. - self.bodyTextViewCached.hidden = YES; + self.bodyTextView.hidden = YES; self.dateHeaderLabel.hidden = YES; self.footerLabel.hidden = YES; @@ -140,6 +140,8 @@ CG_INLINE CGSize CGSizeCeil(CGSize size) 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; } @@ -401,6 +403,9 @@ CG_INLINE CGSize CGSizeCeil(CGSize size) [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:^{ @@ -446,6 +451,9 @@ CG_INLINE CGSize CGSizeCeil(CGSize size) [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:^{ @@ -523,7 +531,7 @@ CG_INLINE CGSize CGSizeCeil(CGSize size) DDLogError(@"%@ Failed to load cell media: %@", [self logTag], [self.attachmentStream mediaURL]); self.viewItem.didCellMediaFailToLoad = YES; // TODO: Do we need to hide/remove the media view? - [self showAttachmentErrorView:mediaView]; + [self showAttachmentErrorViewWithMediaView:mediaView]; } return cellMedia; } @@ -721,12 +729,12 @@ CG_INLINE CGSize CGSizeCeil(CGSize size) TSOutgoingMessage *outgoingMessage = (TSOutgoingMessage *)self.viewItem.interaction; shouldIgnoreEvents = outgoingMessage.messageState != TSOutgoingMessageStateSentToService; } - [self.class loadForTextDisplay:self.bodyTextViewCached + [self.class loadForTextDisplay:self.bodyTextView text:self.displayableBodyText.displayText textColor:self.textColor font:self.textMessageFont shouldIgnoreEvents:shouldIgnoreEvents]; - return self.bodyTextViewCached; + return self.bodyTextView; } + (void)loadForTextDisplay:(OWSMessageTextView *)textView @@ -745,7 +753,6 @@ CG_INLINE CGSize CGSizeCeil(CGSize size) NSForegroundColorAttributeName : textColor, NSUnderlineStyleAttributeName : @(NSUnderlineStyleSingle | NSUnderlinePatternSolid) }; - textView.dataDetectorTypes = (UIDataDetectorTypeLink | UIDataDetectorTypeAddress | UIDataDetectorTypeCalendarEvent); textView.shouldIgnoreEvents = shouldIgnoreEvents; } @@ -797,10 +804,13 @@ CG_INLINE CGSize CGSizeCeil(CGSize size) 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; @@ -813,6 +823,11 @@ CG_INLINE CGSize CGSizeCeil(CGSize size) shouldSkipCache:shouldSkipCache]; }; self.unloadCellContentBlock = ^{ + OWSMessageCell *strongSelf = weakSelf; + if (!strongSelf) { + return; + } + OWSCAssert(strongSelf.lastBodyMediaView == stillImageView); stillImageView.image = nil; }; @@ -836,6 +851,7 @@ CG_INLINE CGSize CGSizeCeil(CGSize size) if (!strongSelf) { return; } + OWSCAssert(strongSelf.lastBodyMediaView == animatedImageView); if (animatedImageView.image) { return; } @@ -854,6 +870,11 @@ CG_INLINE CGSize CGSizeCeil(CGSize size) shouldSkipCache:NO]; }; self.unloadCellContentBlock = ^{ + OWSMessageCell *strongSelf = weakSelf; + if (!strongSelf) { + return; + } + OWSCAssert(strongSelf.lastBodyMediaView == animatedImageView); animatedImageView.image = nil; }; @@ -911,6 +932,7 @@ CG_INLINE CGSize CGSizeCeil(CGSize size) if (!strongSelf) { return; } + OWSCAssert(strongSelf.lastBodyMediaView == stillImageView); if (stillImageView.image) { return; } @@ -924,6 +946,11 @@ CG_INLINE CGSize CGSizeCeil(CGSize size) shouldSkipCache:NO]; }; self.unloadCellContentBlock = ^{ + OWSMessageCell *strongSelf = weakSelf; + if (!strongSelf) { + return; + } + OWSCAssert(strongSelf.lastBodyMediaView == stillImageView); stillImageView.image = nil; }; @@ -999,26 +1026,25 @@ CG_INLINE CGSize CGSizeCeil(CGSize size) if (self.isOutgoing) { if (!self.attachmentStream.isUploaded) { - __unused AttachmentUploadView *attachmentUploadView = - // self.attachmentUploadView = - // This view will be added to attachmentView which will retain a strong reference to it. + AttachmentUploadView *attachmentUploadView = [[AttachmentUploadView alloc] initWithAttachment:self.attachmentStream - superview:attachmentView attachmentStateCallback:attachmentStateCallback]; + [attachmentView addSubview:attachmentUploadView]; + [attachmentUploadView autoPinToSuperviewEdges]; } } } -- (void)showAttachmentErrorView:(UIView *)mediaView +- (void)showAttachmentErrorViewWithMediaView:(UIView *)mediaView { OWSAssert(mediaView); // TODO: We could do a better job of indicating that the media could not be loaded. - UIView *customView = [UIView new]; - customView.backgroundColor = [UIColor colorWithWhite:0.85f alpha:1.f]; - customView.userInteractionEnabled = NO; - [mediaView addSubview:customView]; - [customView autoPinEdgesToSuperviewEdges]; + UIView *errorView = [UIView new]; + errorView.backgroundColor = [UIColor colorWithWhite:0.85f alpha:1.f]; + errorView.userInteractionEnabled = NO; + [mediaView addSubview:errorView]; + [errorView autoPinEdgesToSuperviewEdges]; } #pragma mark - Measurement @@ -1037,10 +1063,10 @@ CG_INLINE CGSize CGSizeCeil(CGSize size) const int maxMessageWidth = [self maxMessageWidthForContentWidth:contentWidth]; const int maxTextWidth = (int)floor(maxMessageWidth - (leftMargin + rightMargin)); - self.bodyTextViewCached.text = self.displayableBodyText.displayText; + self.bodyTextView.text = self.displayableBodyText.displayText; // Honor dynamic type in the message bodies. - self.bodyTextViewCached.font = [self textMessageFont]; - CGSize textSize = CGSizeCeil([self.bodyTextViewCached sizeThatFits:CGSizeMake(maxTextWidth, CGFLOAT_MAX)]); + self.bodyTextView.font = [self textMessageFont]; + CGSize textSize = CGSizeCeil([self.bodyTextView sizeThatFits:CGSizeMake(maxTextWidth, CGFLOAT_MAX)]); CGSize textViewSize = textSize; if (includeMargins) { @@ -1213,10 +1239,9 @@ CG_INLINE CGSize CGSizeCeil(CGSize size) self.dateHeaderLabel.text = nil; self.dateHeaderLabel.hidden = YES; - [self.bodyTextViewCached removeFromSuperview]; - self.bodyTextViewCached.text = nil; - self.bodyTextViewCached.hidden = YES; - self.bodyTextViewCached.dataDetectorTypes = UIDataDetectorTypeNone; + [self.bodyTextView removeFromSuperview]; + self.bodyTextView.text = nil; + self.bodyTextView.hidden = YES; [self.failedSendBadgeView removeFromSuperview]; self.failedSendBadgeView = nil; self.footerLabel.text = nil; diff --git a/SignalMessaging/utils/OWSMessagesBubbleImageFactory.swift b/SignalMessaging/utils/OWSMessagesBubbleImageFactory.swift index eb8bccdc64..7f9c65d384 100644 --- a/SignalMessaging/utils/OWSMessagesBubbleImageFactory.swift +++ b/SignalMessaging/utils/OWSMessagesBubbleImageFactory.swift @@ -20,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) }() From 52b238c499824c460510dfe38128afa33d93c41d Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Mon, 2 Apr 2018 11:14:07 -0400 Subject: [PATCH 31/31] Fix flush of registration connections. --- .../src/Storage/OWSPrimaryStorage.m | 36 +++++++++++++------ SignalServiceKit/src/Storage/OWSStorage.h | 22 +++++++++++- SignalServiceKit/src/Storage/OWSStorage.m | 30 +++++++++++----- 3 files changed, 69 insertions(+), 19 deletions(-) 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 -