diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index 37a8f3f76..21e816027 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -65,7 +65,7 @@ 34B3F88D1E8DF1700035BE1A /* OWSQRCodeScanningViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 34B3F8631E8DF1700035BE1A /* OWSQRCodeScanningViewController.m */; }; 34B3F88E1E8DF1700035BE1A /* PrivacySettingsTableViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 34B3F8651E8DF1700035BE1A /* PrivacySettingsTableViewController.m */; }; 34B3F88F1E8DF1710035BE1A /* RegistrationViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 34B3F8671E8DF1700035BE1A /* RegistrationViewController.m */; }; - 34B3F8901E8DF1710035BE1A /* SettingsTableViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 34B3F8691E8DF1700035BE1A /* SettingsTableViewController.m */; }; + 34B3F8901E8DF1710035BE1A /* AppSettingsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 34B3F8691E8DF1700035BE1A /* AppSettingsViewController.m */; }; 34B3F8911E8DF1710035BE1A /* ShowGroupMembersViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 34B3F86B1E8DF1700035BE1A /* ShowGroupMembersViewController.m */; }; 34B3F8921E8DF1710035BE1A /* SignalAttachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34B3F86C1E8DF1700035BE1A /* SignalAttachment.swift */; }; 34B3F8931E8DF1710035BE1A /* SignalsNavigationController.m in Sources */ = {isa = PBXBuildFile; fileRef = 34B3F86E1E8DF1700035BE1A /* SignalsNavigationController.m */; }; @@ -76,8 +76,9 @@ 34B3F8A21E8EA6040035BE1A /* ViewControllerUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = 34B3F8A11E8EA6040035BE1A /* ViewControllerUtils.m */; }; 34CCAF381F0C0599004084F4 /* AppUpdateNag.m in Sources */ = {isa = PBXBuildFile; fileRef = 34CCAF371F0C0599004084F4 /* AppUpdateNag.m */; }; 34CCAF3B1F0C2748004084F4 /* OWSAddToContactViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 34CCAF3A1F0C2748004084F4 /* OWSAddToContactViewController.m */; }; + 34CE88E71F2FB9A10098030F /* ProfileViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 34CE88E61F2FB9A10098030F /* ProfileViewController.m */; }; 34D5CC961EA6AFAD005515DB /* OWSContactsSyncing.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D5CC951EA6AFAD005515DB /* OWSContactsSyncing.m */; }; - 34D5CCA91EAE3D30005515DB /* GroupViewHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D5CCA81EAE3D30005515DB /* GroupViewHelper.m */; }; + 34D5CCA91EAE3D30005515DB /* AvatarViewHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D5CCA81EAE3D30005515DB /* AvatarViewHelper.m */; }; 34D5CCB11EAE7E7F005515DB /* SelectRecipientViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D5CCB01EAE7E7F005515DB /* SelectRecipientViewController.m */; }; 34D8C0271ED3673300188D7C /* DebugUIMessages.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D8C0241ED3673300188D7C /* DebugUIMessages.m */; }; 34D8C0281ED3673300188D7C /* DebugUITableViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D8C0261ED3673300188D7C /* DebugUITableViewController.m */; }; @@ -483,8 +484,8 @@ 34B3F8651E8DF1700035BE1A /* PrivacySettingsTableViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PrivacySettingsTableViewController.m; sourceTree = ""; }; 34B3F8661E8DF1700035BE1A /* RegistrationViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RegistrationViewController.h; sourceTree = ""; }; 34B3F8671E8DF1700035BE1A /* RegistrationViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RegistrationViewController.m; sourceTree = ""; }; - 34B3F8681E8DF1700035BE1A /* SettingsTableViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SettingsTableViewController.h; sourceTree = ""; }; - 34B3F8691E8DF1700035BE1A /* SettingsTableViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SettingsTableViewController.m; sourceTree = ""; }; + 34B3F8681E8DF1700035BE1A /* AppSettingsViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppSettingsViewController.h; sourceTree = ""; }; + 34B3F8691E8DF1700035BE1A /* AppSettingsViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppSettingsViewController.m; sourceTree = ""; }; 34B3F86A1E8DF1700035BE1A /* ShowGroupMembersViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ShowGroupMembersViewController.h; sourceTree = ""; }; 34B3F86B1E8DF1700035BE1A /* ShowGroupMembersViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ShowGroupMembersViewController.m; sourceTree = ""; }; 34B3F86C1E8DF1700035BE1A /* SignalAttachment.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SignalAttachment.swift; sourceTree = ""; }; @@ -503,12 +504,14 @@ 34CCAF371F0C0599004084F4 /* AppUpdateNag.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppUpdateNag.m; sourceTree = ""; }; 34CCAF391F0C2748004084F4 /* OWSAddToContactViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSAddToContactViewController.h; sourceTree = ""; }; 34CCAF3A1F0C2748004084F4 /* OWSAddToContactViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSAddToContactViewController.m; sourceTree = ""; }; + 34CE88E51F2FB9A10098030F /* ProfileViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ProfileViewController.h; sourceTree = ""; }; + 34CE88E61F2FB9A10098030F /* ProfileViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ProfileViewController.m; sourceTree = ""; }; 34D5CC941EA6AFAD005515DB /* OWSContactsSyncing.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSContactsSyncing.h; sourceTree = ""; }; 34D5CC951EA6AFAD005515DB /* OWSContactsSyncing.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSContactsSyncing.m; sourceTree = ""; }; 34D5CC981EA6EB79005515DB /* OWSMessageCollectionViewCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSMessageCollectionViewCell.h; sourceTree = ""; }; 34D5CC9B1EA6ED17005515DB /* OWSMessageMediaAdapter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSMessageMediaAdapter.h; sourceTree = ""; }; - 34D5CCA71EAE3D30005515DB /* GroupViewHelper.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GroupViewHelper.h; sourceTree = ""; }; - 34D5CCA81EAE3D30005515DB /* GroupViewHelper.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GroupViewHelper.m; 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 = ""; }; 34D5CCAB1EAE7136005515DB /* OWSConversationSettingsViewDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSConversationSettingsViewDelegate.h; sourceTree = ""; }; 34D5CCAF1EAE7E7F005515DB /* SelectRecipientViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SelectRecipientViewController.h; sourceTree = ""; }; 34D5CCB01EAE7E7F005515DB /* SelectRecipientViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SelectRecipientViewController.m; sourceTree = ""; }; @@ -950,9 +953,13 @@ 3472229E1EB22FFE00E53955 /* AddToGroupViewController.m */, 34B3F8361E8DF1700035BE1A /* AdvancedSettingsTableViewController.h */, 34B3F8371E8DF1700035BE1A /* AdvancedSettingsTableViewController.m */, + 34B3F8681E8DF1700035BE1A /* AppSettingsViewController.h */, + 34B3F8691E8DF1700035BE1A /* AppSettingsViewController.m */, 34B3F8381E8DF1700035BE1A /* AttachmentApprovalViewController.swift */, 34B3F8391E8DF1700035BE1A /* AttachmentSharing.h */, 34B3F83A1E8DF1700035BE1A /* AttachmentSharing.m */, + 34D5CCA71EAE3D30005515DB /* AvatarViewHelper.h */, + 34D5CCA81EAE3D30005515DB /* AvatarViewHelper.m */, 343D3D991E9283F100165CA4 /* BlockListUIUtils.h */, 343D3D9A1E9283F100165CA4 /* BlockListUIUtils.m */, 34B3F89A1E8DF3270035BE1A /* BlockListViewController.h */, @@ -977,8 +984,6 @@ 34E8BF371EE9E2FD00F5F4CA /* FingerprintViewScanController.m */, 34B3F8471E8DF1700035BE1A /* FullImageViewController.h */, 34B3F8481E8DF1700035BE1A /* FullImageViewController.m */, - 34D5CCA71EAE3D30005515DB /* GroupViewHelper.h */, - 34D5CCA81EAE3D30005515DB /* GroupViewHelper.m */, 34B3F8491E8DF1700035BE1A /* InboxTableViewCell.h */, 34B3F84A1E8DF1700035BE1A /* InboxTableViewCell.m */, 34B3F84C1E8DF1700035BE1A /* InviteFlow.swift */, @@ -1017,6 +1022,8 @@ 34D99C8B1F27B13B00D284D6 /* OWSViewController.m */, 34B3F8641E8DF1700035BE1A /* PrivacySettingsTableViewController.h */, 34B3F8651E8DF1700035BE1A /* PrivacySettingsTableViewController.m */, + 34CE88E51F2FB9A10098030F /* ProfileViewController.h */, + 34CE88E61F2FB9A10098030F /* ProfileViewController.m */, 34B3F8661E8DF1700035BE1A /* RegistrationViewController.h */, 34B3F8671E8DF1700035BE1A /* RegistrationViewController.m */, 4585C4671ED8F8D200896AEA /* SafetyNumberConfirmationAlert.swift */, @@ -1026,8 +1033,6 @@ 3400C7951EAF99F4008A8584 /* SelectThreadViewController.m */, 3400C7901EAF89CD008A8584 /* SendExternalFileViewController.h */, 3400C7911EAF89CD008A8584 /* SendExternalFileViewController.m */, - 34B3F8681E8DF1700035BE1A /* SettingsTableViewController.h */, - 34B3F8691E8DF1700035BE1A /* SettingsTableViewController.m */, 34B3F86A1E8DF1700035BE1A /* ShowGroupMembersViewController.h */, 34B3F86B1E8DF1700035BE1A /* ShowGroupMembersViewController.m */, 34B3F86C1E8DF1700035BE1A /* SignalAttachment.swift */, @@ -2161,12 +2166,13 @@ 34D5CCB11EAE7E7F005515DB /* SelectRecipientViewController.m in Sources */, 34B3F88F1E8DF1710035BE1A /* RegistrationViewController.m in Sources */, 3448BFCD1EDF0EA7005B2D69 /* OWSMessagesInputToolbar.m in Sources */, - 34B3F8901E8DF1710035BE1A /* SettingsTableViewController.m in Sources */, + 34B3F8901E8DF1710035BE1A /* AppSettingsViewController.m in Sources */, 34FD93701E3BD43A00109093 /* OWSAnyTouchGestureRecognizer.m in Sources */, 343D3D9B1E9283F100165CA4 /* BlockListUIUtils.m in Sources */, 34B3F8931E8DF1710035BE1A /* SignalsNavigationController.m in Sources */, 76EB063A18170B33006006FC /* FunctionalUtil.m in Sources */, 34F308A21ECB469700BB7697 /* OWSBezierPathView.m in Sources */, + 34CE88E71F2FB9A10098030F /* ProfileViewController.m in Sources */, 348F2EAE1F0D21BC00D4ECE0 /* DeviceSleepManager.swift in Sources */, 34E3EF101EFC2684007F6822 /* DebugUIPage.m in Sources */, 76EB058A18170B33006006FC /* Release.m in Sources */, @@ -2216,7 +2222,7 @@ 34B3F8881E8DF1700035BE1A /* OversizeTextMessageViewController.swift in Sources */, 34330AA31E79686200DF2FB9 /* OWSProgressView.m in Sources */, 34B3F8A21E8EA6040035BE1A /* ViewControllerUtils.m in Sources */, - 34D5CCA91EAE3D30005515DB /* GroupViewHelper.m in Sources */, + 34D5CCA91EAE3D30005515DB /* AvatarViewHelper.m in Sources */, 453D28BA1D332DB100D523F0 /* OWSMessagesBubblesSizeCalculator.m in Sources */, 45F170AC1E2F0351003FC1F2 /* CallAudioSession.swift in Sources */, 34B3F8801E8DF1700035BE1A /* InviteFlow.swift in Sources */, diff --git a/Signal/Images.xcassets/profile_avatar_default.imageset/Contents.json b/Signal/Images.xcassets/profile_avatar_default.imageset/Contents.json new file mode 100644 index 000000000..f7587e86c --- /dev/null +++ b/Signal/Images.xcassets/profile_avatar_default.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "profile_avatar_default@1x.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "profile_avatar_default@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "profile_avatar_default@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Signal/Images.xcassets/profile_avatar_default.imageset/profile_avatar_default@1x.png b/Signal/Images.xcassets/profile_avatar_default.imageset/profile_avatar_default@1x.png new file mode 100644 index 000000000..850526de9 Binary files /dev/null and b/Signal/Images.xcassets/profile_avatar_default.imageset/profile_avatar_default@1x.png differ diff --git a/Signal/Images.xcassets/profile_avatar_default.imageset/profile_avatar_default@2x.png b/Signal/Images.xcassets/profile_avatar_default.imageset/profile_avatar_default@2x.png new file mode 100644 index 000000000..22695ce7f Binary files /dev/null and b/Signal/Images.xcassets/profile_avatar_default.imageset/profile_avatar_default@2x.png differ diff --git a/Signal/Images.xcassets/profile_avatar_default.imageset/profile_avatar_default@3x.png b/Signal/Images.xcassets/profile_avatar_default.imageset/profile_avatar_default@3x.png new file mode 100644 index 000000000..c460d495f Binary files /dev/null and b/Signal/Images.xcassets/profile_avatar_default.imageset/profile_avatar_default@3x.png differ diff --git a/Signal/src/AppDelegate.m b/Signal/src/AppDelegate.m index 6748639f3..79438d5ee 100644 --- a/Signal/src/AppDelegate.m +++ b/Signal/src/AppDelegate.m @@ -28,6 +28,7 @@ #import #import #import +#import #import #import #import @@ -160,6 +161,7 @@ static NSString *const kURLHostVerifyPrefix = @"verify"; DDLogInfo(@"%@ application: didFinishLaunchingWithOptions completed.", self.tag); [OWSAnalytics appLaunchDidBegin]; + [OWSProfilesManager.sharedManager appLaunchDidBegin]; return YES; } diff --git a/Signal/src/Signal-Bridging-Header.h b/Signal/src/Signal-Bridging-Header.h index 3b0467780..00d707a04 100644 --- a/Signal/src/Signal-Bridging-Header.h +++ b/Signal/src/Signal-Bridging-Header.h @@ -4,6 +4,7 @@ #import +#import "AppSettingsViewController.h" #import "AttachmentSharing.h" #import "Environment.h" #import "FLAnimatedImage.h" @@ -23,7 +24,6 @@ #import "PrivacySettingsTableViewController.h" #import "PropertyListPreferences.h" #import "PushManager.h" -#import "SettingsTableViewController.h" #import "SignalsViewController.h" #import "TSMessageAdapter.h" #import "UIColor+OWS.h" diff --git a/Signal/src/ViewControllers/SettingsTableViewController.h b/Signal/src/ViewControllers/AppSettingsViewController.h similarity index 64% rename from Signal/src/ViewControllers/SettingsTableViewController.h rename to Signal/src/ViewControllers/AppSettingsViewController.h index f9bb8bf3c..b2550cee6 100644 --- a/Signal/src/ViewControllers/SettingsTableViewController.h +++ b/Signal/src/ViewControllers/AppSettingsViewController.h @@ -4,6 +4,6 @@ #import "OWSTableViewController.h" -@interface SettingsTableViewController : OWSTableViewController +@interface AppSettingsViewController : OWSTableViewController @end diff --git a/Signal/src/ViewControllers/SettingsTableViewController.m b/Signal/src/ViewControllers/AppSettingsViewController.m similarity index 93% rename from Signal/src/ViewControllers/SettingsTableViewController.m rename to Signal/src/ViewControllers/AppSettingsViewController.m index 113e10ee0..5eeae8abc 100644 --- a/Signal/src/ViewControllers/SettingsTableViewController.m +++ b/Signal/src/ViewControllers/AppSettingsViewController.m @@ -2,7 +2,7 @@ // Copyright (c) 2017 Open Whisper Systems. All rights reserved. // -#import "SettingsTableViewController.h" +#import "AppSettingsViewController.h" #import "AboutTableViewController.h" #import "AdvancedSettingsTableViewController.h" #import "DebugUITableViewController.h" @@ -11,6 +11,7 @@ #import "OWSContactsManager.h" #import "OWSLinkedDevicesTableViewController.h" #import "PrivacySettingsTableViewController.h" +#import "ProfileViewController.h" #import "PropertyListPreferences.h" #import "PushManager.h" #import "Signal-Swift.h" @@ -18,7 +19,7 @@ #import #import -@interface SettingsTableViewController () +@interface AppSettingsViewController () @property (nonatomic, readonly) OWSContactsManager *contactsManager; @@ -26,7 +27,7 @@ #pragma mark - -@implementation SettingsTableViewController +@implementation AppSettingsViewController - (instancetype)init { @@ -72,7 +73,7 @@ [self observeNotifications]; self.title = NSLocalizedString(@"SETTINGS_NAV_BAR_TITLE", @"Title for settings activity"); - + [self updateTableContents]; } @@ -83,7 +84,8 @@ [self updateTableContents]; } -- (void)dealloc { +- (void)dealloc +{ [[NSNotificationCenter defaultCenter] removeObserver:self]; } @@ -94,7 +96,7 @@ OWSTableContents *contents = [OWSTableContents new]; OWSTableSection *section = [OWSTableSection new]; - __weak SettingsTableViewController *weakSelf = self; + __weak AppSettingsViewController *weakSelf = self; [section addItem:[OWSTableItem itemWithCustomCellBlock:^{ UITableViewCell *cell = [UITableViewCell new]; cell.preservesSuperviewLayoutMargins = YES; @@ -168,6 +170,13 @@ } actionBlock:nil]]; } + + [section addItem:[OWSTableItem + disclosureItemWithText:NSLocalizedString(@"PROFILE_VIEW_TITLE", @"Title for the profile view.") + actionBlock:^{ + [weakSelf showProfile]; + }]]; + [section addItem:[OWSTableItem disclosureItemWithText:NSLocalizedString(@"SETTINGS_INVITE_TITLE", @"Settings table view cell label") actionBlock:^{ @@ -225,7 +234,7 @@ actionBlock:nil]]; [contents addSection:section]; - + self.contents = contents; } @@ -255,6 +264,12 @@ [self.navigationController pushViewController:vc animated:YES]; } +- (void)showProfile +{ + ProfileViewController *vc = [[ProfileViewController alloc] init]; + [self.navigationController pushViewController:vc animated:YES]; +} + - (void)showAdvanced { AdvancedSettingsTableViewController *vc = [[AdvancedSettingsTableViewController alloc] init]; @@ -288,7 +303,7 @@ [alertController addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"PROCEED_BUTTON", @"") style:UIAlertActionStyleDestructive handler:^(UIAlertAction *action) { - [self proceedToUnregistration]; + [self proceedToUnregistration]; }]]; [alertController addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"TXT_CANCEL_TITLE", @"") style:UIAlertActionStyleCancel @@ -297,7 +312,8 @@ [self presentViewController:alertController animated:YES completion:nil]; } -- (void)proceedToUnregistration { +- (void)proceedToUnregistration +{ [TSAccountManager unregisterTextSecureWithSuccess:^{ [Environment resetAppData]; } @@ -316,9 +332,10 @@ object:nil]; } -- (void)socketStateDidChange { +- (void)socketStateDidChange +{ OWSAssert([NSThread isMainThread]); - + [self updateTableContents]; } diff --git a/Signal/src/ViewControllers/AvatarViewHelper.h b/Signal/src/ViewControllers/AvatarViewHelper.h new file mode 100644 index 000000000..928164279 --- /dev/null +++ b/Signal/src/ViewControllers/AvatarViewHelper.h @@ -0,0 +1,36 @@ +// +// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@class SignalAccount; +@class AvatarViewHelper; +@class OWSContactsManager; +@class TSThread; + +@protocol AvatarViewHelperDelegate + +- (NSString *)avatarActionSheetTitle; + +- (void)avatarDidChange:(UIImage *)image; + +- (UIViewController *)fromViewController; + +@end + +#pragma mark - + +typedef void (^AvatarViewSuccessBlock)(); + +@interface AvatarViewHelper : NSObject + +@property (nonatomic, weak) id delegate; + +- (void)showChangeAvatarUI; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Signal/src/ViewControllers/GroupViewHelper.m b/Signal/src/ViewControllers/AvatarViewHelper.m similarity index 89% rename from Signal/src/ViewControllers/GroupViewHelper.m rename to Signal/src/ViewControllers/AvatarViewHelper.m index d980e1f70..b88b4a00e 100644 --- a/Signal/src/ViewControllers/GroupViewHelper.m +++ b/Signal/src/ViewControllers/AvatarViewHelper.m @@ -2,7 +2,7 @@ // Copyright (c) 2017 Open Whisper Systems. All rights reserved. // -#import "GroupViewHelper.h" +#import "AvatarViewHelper.h" #import "OWSContactsManager.h" #import "UIUtil.h" #import @@ -13,24 +13,23 @@ NS_ASSUME_NONNULL_BEGIN -@interface GroupViewHelper () +@interface AvatarViewHelper () @end #pragma mark - -@implementation GroupViewHelper +@implementation AvatarViewHelper -#pragma mark - Group Avatar +#pragma mark - Avatar Avatar -- (void)showChangeGroupAvatarUI +- (void)showChangeAvatarUI { OWSAssert([NSThread isMainThread]); OWSAssert(self.delegate); UIAlertController *actionSheetController = - [UIAlertController alertControllerWithTitle:NSLocalizedString(@"NEW_GROUP_ADD_PHOTO_ACTION", - @"Action Sheet title prompting the user for a group avatar") + [UIAlertController alertControllerWithTitle:self.delegate.avatarActionSheetTitle message:nil preferredStyle:UIAlertControllerStyleActionSheet]; UIAlertAction *dismissAction = [UIAlertAction actionWithTitle:NSLocalizedString(@"TXT_CANCEL_TITLE", @"") @@ -119,7 +118,7 @@ NS_ASSUME_NONNULL_BEGIN // // See: GroupCreateActivity.java in Signal-Android.java. UIImage *resizedAvatar = [rawAvatar resizedImageToFillPixelSize:CGSizeMake(210, 210)]; - [self.delegate groupAvatarDidChange:resizedAvatar]; + [self.delegate avatarDidChange:resizedAvatar]; } [self.delegate.fromViewController dismissViewControllerAnimated:YES completion:nil]; diff --git a/Signal/src/ViewControllers/CallViewController.swift b/Signal/src/ViewControllers/CallViewController.swift index 1f1adcd73..f5541c3fe 100644 --- a/Signal/src/ViewControllers/CallViewController.swift +++ b/Signal/src/ViewControllers/CallViewController.swift @@ -900,7 +900,7 @@ class CallViewController: OWSViewController, CallObserver, CallServiceObserver, assert(fromViewController != nil) // Construct the "settings" view & push the "privacy settings" view. - let navigationController = UINavigationController(rootViewController:SettingsTableViewController()) + let navigationController = UINavigationController(rootViewController:AppSettingsViewController()) navigationController.pushViewController(PrivacySettingsTableViewController(), animated:false) fromViewController?.present(navigationController, animated: true, completion: nil) diff --git a/Signal/src/ViewControllers/ExperienceUpgradesPageViewController.swift b/Signal/src/ViewControllers/ExperienceUpgradesPageViewController.swift index b7c7d99ac..309d38cde 100644 --- a/Signal/src/ViewControllers/ExperienceUpgradesPageViewController.swift +++ b/Signal/src/ViewControllers/ExperienceUpgradesPageViewController.swift @@ -38,7 +38,7 @@ private class CallKitExperienceUpgradeViewController: ExperienceUpgradeViewContr assert(fromViewController != nil) // Construct the "settings" view & push the "privacy settings" view. - let navigationController = UINavigationController(rootViewController:SettingsTableViewController()) + let navigationController = UINavigationController(rootViewController:AppSettingsViewController()) navigationController.pushViewController(PrivacySettingsTableViewController(), animated:false) fromViewController?.present(navigationController, animated: true, completion: nil) diff --git a/Signal/src/ViewControllers/GroupViewHelper.h b/Signal/src/ViewControllers/GroupViewHelper.h deleted file mode 100644 index 932f6d543..000000000 --- a/Signal/src/ViewControllers/GroupViewHelper.h +++ /dev/null @@ -1,34 +0,0 @@ -// -// Copyright (c) 2017 Open Whisper Systems. All rights reserved. -// - -#import - -NS_ASSUME_NONNULL_BEGIN - -@class SignalAccount; -@class GroupViewHelper; -@class OWSContactsManager; -@class TSThread; - -@protocol GroupViewHelperDelegate - -- (void)groupAvatarDidChange:(UIImage *)image; - -- (UIViewController *)fromViewController; - -@end - -#pragma mark - - -typedef void (^GroupViewSuccessBlock)(); - -@interface GroupViewHelper : NSObject - -@property (nonatomic, weak) id delegate; - -- (void)showChangeGroupAvatarUI; - -@end - -NS_ASSUME_NONNULL_END diff --git a/Signal/src/ViewControllers/NewGroupViewController.m b/Signal/src/ViewControllers/NewGroupViewController.m index ab6236dab..83eb7f09f 100644 --- a/Signal/src/ViewControllers/NewGroupViewController.m +++ b/Signal/src/ViewControllers/NewGroupViewController.m @@ -4,11 +4,11 @@ #import "NewGroupViewController.h" #import "AddToGroupViewController.h" +#import "AvatarViewHelper.h" #import "BlockListUIUtils.h" #import "ContactTableViewCell.h" #import "ContactsViewHelper.h" #import "Environment.h" -#import "GroupViewHelper.h" #import "OWSContactsManager.h" #import "OWSTableViewController.h" #import "Signal-Swift.h" @@ -31,14 +31,14 @@ const NSUInteger kNewGroupViewControllerAvatarWidth = 68; @interface NewGroupViewController () @property (nonatomic, readonly) OWSMessageSender *messageSender; @property (nonatomic, readonly) ContactsViewHelper *contactsViewHelper; -@property (nonatomic, readonly) GroupViewHelper *groupViewHelper; +@property (nonatomic, readonly) AvatarViewHelper *avatarViewHelper; @property (nonatomic, readonly) OWSTableViewController *tableViewController; @property (nonatomic, readonly) AvatarImageView *avatarView; @@ -84,8 +84,8 @@ const NSUInteger kNewGroupViewControllerAvatarWidth = 68; { _messageSender = [Environment getCurrent].messageSender; _contactsViewHelper = [[ContactsViewHelper alloc] initWithDelegate:self]; - _groupViewHelper = [GroupViewHelper new]; - _groupViewHelper.delegate = self; + _avatarViewHelper = [AvatarViewHelper new]; + _avatarViewHelper.delegate = self; self.memberRecipientIds = [NSMutableSet new]; } @@ -181,7 +181,7 @@ const NSUInteger kNewGroupViewControllerAvatarWidth = 68; - (void)avatarTouched:(UIGestureRecognizer *)sender { if (sender.state == UIGestureRecognizerStateRecognized) { - [self showChangeGroupAvatarUI]; + [self showChangeAvatarUI]; } } @@ -520,9 +520,9 @@ const NSUInteger kNewGroupViewControllerAvatarWidth = 68; #pragma mark - Group Avatar -- (void)showChangeGroupAvatarUI +- (void)showChangeAvatarUI { - [self.groupViewHelper showChangeGroupAvatarUI]; + [self.avatarViewHelper showChangeAvatarUI]; } - (void)setGroupAvatar:(nullable UIImage *)groupAvatar @@ -606,9 +606,15 @@ const NSUInteger kNewGroupViewControllerAvatarWidth = 68; return YES; } -#pragma mark - GroupViewHelperDelegate +#pragma mark - AvatarViewHelperDelegate -- (void)groupAvatarDidChange:(UIImage *)image +- (NSString *)avatarActionSheetTitle +{ + return NSLocalizedString( + @"NEW_GROUP_ADD_PHOTO_ACTION", @"Action Sheet title prompting the user for a group avatar"); +} + +- (void)avatarDidChange:(UIImage *)image { OWSAssert(image); diff --git a/Signal/src/ViewControllers/OWSLinkDeviceViewController.m b/Signal/src/ViewControllers/OWSLinkDeviceViewController.m index 73cd9812a..650bc6679 100644 --- a/Signal/src/ViewControllers/OWSLinkDeviceViewController.m +++ b/Signal/src/ViewControllers/OWSLinkDeviceViewController.m @@ -5,7 +5,6 @@ #import "OWSLinkDeviceViewController.h" #import "OWSDeviceProvisioningURLParser.h" #import "OWSLinkedDevicesTableViewController.h" -#import "SettingsTableViewController.h" #import #import #import diff --git a/Signal/src/ViewControllers/ProfileViewController.h b/Signal/src/ViewControllers/ProfileViewController.h new file mode 100644 index 000000000..16471c80f --- /dev/null +++ b/Signal/src/ViewControllers/ProfileViewController.h @@ -0,0 +1,13 @@ +// +// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// + +#import "OWSTableViewController.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface ProfileViewController : OWSTableViewController + +@end + +NS_ASSUME_NONNULL_END diff --git a/Signal/src/ViewControllers/ProfileViewController.m b/Signal/src/ViewControllers/ProfileViewController.m new file mode 100644 index 000000000..18383a749 --- /dev/null +++ b/Signal/src/ViewControllers/ProfileViewController.m @@ -0,0 +1,306 @@ +// +// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// + +#import "ProfileViewController.h" +#import "AvatarViewHelper.h" +#import "Signal-Swift.h" +#import "UIColor+OWS.h" +#import "UIFont+OWS.h" +#import "UIView+OWS.h" +#import "UIViewController+OWS.h" +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface ProfileViewController () + +@property (nonatomic, readonly) AvatarViewHelper *avatarViewHelper; + +@property (nonatomic) UITextField *nameTextField; + +@property (nonatomic) AvatarImageView *avatarView; + +@property (nonatomic) UILabel *avatarLabel; + +@property (nonatomic, nullable) UIImage *avatar; + +@property (nonatomic) BOOL hasUnsavedChanges; + +@end + +#pragma mark - + +@implementation ProfileViewController + +- (void)loadView +{ + [super loadView]; + + self.view.backgroundColor = [UIColor whiteColor]; + [self.navigationController.navigationBar setTranslucent:NO]; + self.title = NSLocalizedString(@"PROFILE_VIEW_TITLE", @"Title for the profile view."); + self.navigationItem.leftBarButtonItem = + [self createOWSBackButtonWithTarget:self selector:@selector(backButtonPressed:)]; + + _avatarViewHelper = [AvatarViewHelper new]; + _avatarViewHelper.delegate = self; + + _avatar = [OWSProfilesManager.sharedManager localProfileAvatarImage]; + + [self createViews]; +} + +- (void)createViews +{ + _nameTextField = [UITextField new]; + _nameTextField.font = [UIFont ows_mediumFontWithSize:18.f]; + _nameTextField.textColor = [UIColor ows_materialBlueColor]; + _nameTextField.placeholder = NSLocalizedString( + @"PROFILE_VIEW_NAME_DEFAULT_TEXT", @"Default text for the profile name field of the profile view."); + _nameTextField.delegate = self; + _nameTextField.text = [OWSProfilesManager.sharedManager localProfileName]; + [_nameTextField addTarget:self action:@selector(textFieldDidChange:) forControlEvents:UIControlEventEditingChanged]; + + _avatarView = [AvatarImageView new]; + + _avatarLabel = [UILabel new]; + _avatarLabel.font = [UIFont ows_regularFontWithSize:14.f]; + _avatarLabel.textColor = [UIColor ows_materialBlueColor]; + // TODO: Copy. + _avatarLabel.text + = NSLocalizedString(@"PROFILE_VIEW_AVATAR_INSTRUCTIONS", @"Instructions for how to change the profile avatar."); + [_avatarLabel sizeToFit]; + + [self updateTableContents]; +} + +#pragma mark - Table Contents + +- (void)updateTableContents +{ + OWSTableContents *contents = [OWSTableContents new]; + + __weak ProfileViewController *weakSelf = self; + + // Profile Avatar + OWSTableSection *avatarSection = [OWSTableSection new]; + avatarSection.headerTitle = NSLocalizedString( + @"PROFILE_VIEW_AVATAR_SECTION_HEADER", @"Header title for the profile avatar field of the profile view."); + const CGFloat kAvatarSizePoints = 100.f; + const CGFloat kAvatarTopMargin = 10.f; + const CGFloat kAvatarBottomMargin = 10.f; + const CGFloat kAvatarVSpacing = 10.f; + CGFloat avatarCellHeight + = round(kAvatarSizePoints + kAvatarTopMargin + kAvatarBottomMargin + kAvatarVSpacing + self.avatarLabel.height); + [avatarSection addItem:[OWSTableItem itemWithCustomCellBlock:^{ + UITableViewCell *cell = [UITableViewCell new]; + cell.preservesSuperviewLayoutMargins = YES; + cell.contentView.preservesSuperviewLayoutMargins = YES; + + AvatarImageView *avatarView = weakSelf.avatarView; + [weakSelf updateAvatarView]; + [cell.contentView addSubview:avatarView]; + [avatarView autoHCenterInSuperview]; + [avatarView autoPinEdgeToSuperviewEdge:ALEdgeTop withInset:kAvatarTopMargin]; + [avatarView autoSetDimension:ALDimensionWidth toSize:kAvatarSizePoints]; + [avatarView autoSetDimension:ALDimensionHeight toSize:kAvatarSizePoints]; + + UILabel *avatarLabel = weakSelf.avatarLabel; + [cell.contentView addSubview:avatarLabel]; + [avatarLabel autoHCenterInSuperview]; + [avatarLabel autoPinEdgeToSuperviewEdge:ALEdgeBottom withInset:kAvatarBottomMargin]; + + cell.userInteractionEnabled = YES; + [cell + addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(avatarTapped:)]]; + + cell.selectionStyle = UITableViewCellSelectionStyleNone; + return cell; + } + customRowHeight:avatarCellHeight + actionBlock:nil]]; + [contents addSection:avatarSection]; + + // Profile Name + OWSTableSection *nameSection = [OWSTableSection new]; + nameSection.headerTitle = NSLocalizedString( + @"PROFILE_VIEW_NAME_SECTION_HEADER", @"Label for the profile name field of the profile view."); + [nameSection + addItem: + [OWSTableItem + itemWithCustomCellBlock:^{ + UITableViewCell *cell = [UITableViewCell new]; + cell.preservesSuperviewLayoutMargins = YES; + cell.contentView.preservesSuperviewLayoutMargins = YES; + + UITextField *nameTextField = weakSelf.nameTextField; + [cell.contentView addSubview:nameTextField]; + [nameTextField autoPinLeadingToSuperView]; + [nameTextField autoPinTrailingToSuperView]; + [nameTextField autoVCenterInSuperview]; + + cell.selectionStyle = UITableViewCellSelectionStyleNone; + return cell; + } + actionBlock:nil]]; + [contents addSection:nameSection]; + + self.contents = contents; +} + +#pragma mark - Event Handling + +- (void)backButtonPressed:(id)sender +{ + [self.nameTextField resignFirstResponder]; + + if (!self.hasUnsavedChanges) { + // If user made no changes, return to conversation settings view. + [self.navigationController popViewControllerAnimated:YES]; + return; + } + + UIAlertController *controller = [UIAlertController + alertControllerWithTitle: + NSLocalizedString(@"NEW_GROUP_VIEW_UNSAVED_CHANGES_TITLE", + @"The alert title if user tries to exit the new group view without saving changes.") + message: + NSLocalizedString(@"NEW_GROUP_VIEW_UNSAVED_CHANGES_MESSAGE", + @"The alert message if user tries to exit the new group view without saving changes.") + preferredStyle:UIAlertControllerStyleAlert]; + [controller + addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"ALERT_DISCARD_BUTTON", + @"The label for the 'discard' button in alerts and action sheets.") + style:UIAlertActionStyleDestructive + handler:^(UIAlertAction *action) { + [self.navigationController popViewControllerAnimated:YES]; + }]]; + [controller addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"TXT_CANCEL_TITLE", nil) + style:UIAlertActionStyleCancel + handler:nil]]; + [self presentViewController:controller animated:YES completion:nil]; +} + +- (void)avatarTapped:(UIGestureRecognizer *)sender +{ + if (sender.state == UIGestureRecognizerStateRecognized) { + [self.avatarViewHelper showChangeAvatarUI]; + } +} + +- (void)setHasUnsavedChanges:(BOOL)hasUnsavedChanges +{ + _hasUnsavedChanges = hasUnsavedChanges; + + if (hasUnsavedChanges) { + self.navigationItem.rightBarButtonItem = (self.hasUnsavedChanges + ? [[UIBarButtonItem alloc] initWithTitle:NSLocalizedString(@"EDIT_GROUP_UPDATE_BUTTON", + @"The title for the 'update group' button.") + style:UIBarButtonItemStylePlain + target:self + action:@selector(updatePressed)] + : nil); + } +} + +- (void)updatePressed +{ + [self updateProfile]; +} + +- (void)updateProfile +{ + __weak ProfileViewController *weakSelf = self; + [OWSProfilesManager.sharedManager + updateLocalProfileName:self.nameTextField.text + localProfileAvatarImage:self.avatar + success:^{ + [weakSelf.navigationController popViewControllerAnimated:YES]; + } + failure:^{ + // <#code#> + }]; +} + +#pragma mark - UITextFieldDelegate + +// TODO: This logic resides in both RegistrationViewController and here. +// We should refactor it out into a utility function. +- (BOOL)textField:(UITextField *)textField + shouldChangeCharactersInRange:(NSRange)range + replacementString:(NSString *)insertionText +{ + // TODO: Possibly filter invalid input. + // TODO: Possibly prevent user from typing overlong name. + return YES; +} + +- (BOOL)textFieldShouldReturn:(UITextField *)textField +{ + [self updateProfile]; + return NO; +} + +- (void)textFieldDidChange:(id)sender +{ + self.hasUnsavedChanges = YES; + + // TODO: Update length warning. +} + +#pragma mark - Avatar + +- (void)setAvatar:(nullable UIImage *)avatar +{ + OWSAssert([NSThread isMainThread]); + + _avatar = avatar; + + self.hasUnsavedChanges = YES; + + [self updateAvatarView]; +} + +- (void)updateAvatarView +{ + self.avatarView.image = (self.avatar ?: [UIImage imageNamed:@"profile_avatar_default"]); +} + +#pragma mark - AvatarViewHelperDelegate + +- (NSString *)avatarActionSheetTitle +{ + return NSLocalizedString( + @"PROFILE_AVATAR_ACTIONSHEET_TITLE", @"Action Sheet title prompting the user for a profile avatar"); +} + +- (void)avatarDidChange:(UIImage *)image +{ + OWSAssert(image); + + // TODO: Crop to square and possible resize. + + self.avatar = image; +} + +- (UIViewController *)fromViewController +{ + return self; +} + +#pragma mark - Logging + ++ (NSString *)tag +{ + return [NSString stringWithFormat:@"[%@]", self.class]; +} + +- (NSString *)tag +{ + return self.class.tag; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Signal/src/ViewControllers/SignalsViewController.m b/Signal/src/ViewControllers/SignalsViewController.m index 8758a42ed..6dba98273 100644 --- a/Signal/src/ViewControllers/SignalsViewController.m +++ b/Signal/src/ViewControllers/SignalsViewController.m @@ -4,6 +4,7 @@ #import "SignalsViewController.h" #import "AppDelegate.h" +#import "AppSettingsViewController.h" #import "InboxTableViewCell.h" #import "MessageComposeTableViewController.h" #import "MessagesViewController.h" @@ -11,7 +12,6 @@ #import "OWSContactsManager.h" #import "PropertyListPreferences.h" #import "PushManager.h" -#import "SettingsTableViewController.h" #import "Signal-Swift.h" #import "TSAccountManager.h" #import "TSDatabaseView.h" @@ -305,7 +305,7 @@ typedef NS_ENUM(NSInteger, CellState) { kArchiveState, kInboxState }; } - (void)settingsButtonPressed:(id)sender { - SettingsTableViewController *vc = [SettingsTableViewController new]; + AppSettingsViewController *vc = [AppSettingsViewController new]; UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController:vc]; [self presentViewController:navigationController animated:YES completion:nil]; } diff --git a/Signal/src/ViewControllers/UpdateGroupViewController.m b/Signal/src/ViewControllers/UpdateGroupViewController.m index 0c4d67ced..6b3f1eb93 100644 --- a/Signal/src/ViewControllers/UpdateGroupViewController.m +++ b/Signal/src/ViewControllers/UpdateGroupViewController.m @@ -4,11 +4,11 @@ #import "UpdateGroupViewController.h" #import "AddToGroupViewController.h" +#import "AvatarViewHelper.h" #import "BlockListUIUtils.h" #import "ContactTableViewCell.h" #import "ContactsViewHelper.h" #import "Environment.h" -#import "GroupViewHelper.h" #import "OWSContactsManager.h" #import "OWSTableViewController.h" #import "Signal-Swift.h" @@ -30,14 +30,14 @@ NS_ASSUME_NONNULL_BEGIN @interface UpdateGroupViewController () @property (nonatomic, readonly) OWSMessageSender *messageSender; @property (nonatomic, readonly) ContactsViewHelper *contactsViewHelper; -@property (nonatomic, readonly) GroupViewHelper *groupViewHelper; +@property (nonatomic, readonly) AvatarViewHelper *avatarViewHelper; @property (nonatomic, readonly) OWSTableViewController *tableViewController; @property (nonatomic, readonly) AvatarImageView *avatarView; @@ -83,8 +83,8 @@ NS_ASSUME_NONNULL_BEGIN { _messageSender = [Environment getCurrent].messageSender; _contactsViewHelper = [[ContactsViewHelper alloc] initWithDelegate:self]; - _groupViewHelper = [GroupViewHelper new]; - _groupViewHelper.delegate = self; + _avatarViewHelper = [AvatarViewHelper new]; + _avatarViewHelper.delegate = self; self.memberRecipientIds = [NSMutableSet new]; } @@ -158,7 +158,7 @@ NS_ASSUME_NONNULL_BEGIN [self.groupNameTextField becomeFirstResponder]; break; case UpdateGroupMode_EditGroupAvatar: - [self showChangeGroupAvatarUI]; + [self showChangeAvatarUI]; break; default: break; @@ -228,7 +228,7 @@ NS_ASSUME_NONNULL_BEGIN - (void)avatarTouched:(UIGestureRecognizer *)sender { if (sender.state == UIGestureRecognizerStateRecognized) { - [self showChangeGroupAvatarUI]; + [self showChangeAvatarUI]; } } @@ -384,11 +384,11 @@ NS_ASSUME_NONNULL_BEGIN #pragma mark - Group Avatar -- (void)showChangeGroupAvatarUI +- (void)showChangeAvatarUI { [self.groupNameTextField resignFirstResponder]; - [self.groupViewHelper showChangeGroupAvatarUI]; + [self.avatarViewHelper showChangeAvatarUI]; } - (void)setGroupAvatar:(nullable UIImage *)groupAvatar @@ -487,9 +487,15 @@ NS_ASSUME_NONNULL_BEGIN return YES; } -#pragma mark - GroupViewHelperDelegate +#pragma mark - AvatarViewHelperDelegate -- (void)groupAvatarDidChange:(UIImage *)image +- (NSString *)avatarActionSheetTitle +{ + return NSLocalizedString( + @"NEW_GROUP_ADD_PHOTO_ACTION", @"Action Sheet title prompting the user for a group avatar"); +} + +- (void)avatarDidChange:(UIImage *)image { OWSAssert(image); diff --git a/Signal/translations/en.lproj/Localizable.strings b/Signal/translations/en.lproj/Localizable.strings index 0de7d0989..01fe53dc4 100644 --- a/Signal/translations/en.lproj/Localizable.strings +++ b/Signal/translations/en.lproj/Localizable.strings @@ -1036,6 +1036,24 @@ /* No comment provided by engineer. */ "PROCEED_BUTTON" = "Proceed"; +/* Action Sheet title prompting the user for a profile avatar */ +"PROFILE_AVATAR_ACTIONSHEET_TITLE" = "Set Profile Avatar"; + +/* Instructions for how to change the profile avatar. */ +"PROFILE_VIEW_AVATAR_INSTRUCTIONS" = "Tap to Select Avatar"; + +/* Header title for the profile avatar field of the profile view. */ +"PROFILE_VIEW_AVATAR_SECTION_HEADER" = "Avatar"; + +/* Default text for the profile name field of the profile view. */ +"PROFILE_VIEW_NAME_DEFAULT_TEXT" = "Enter your name."; + +/* Label for the profile name field of the profile view. */ +"PROFILE_VIEW_NAME_SECTION_HEADER" = "Profile Name"; + +/* Title for the profile view. */ +"PROFILE_VIEW_TITLE" = "Profile"; + /* No comment provided by engineer. */ "PUSH_MANAGER_MARKREAD" = "Mark as Read"; diff --git a/SignalServiceKit/src/Account/TSAccountManager.m b/SignalServiceKit/src/Account/TSAccountManager.m index 894e492ff..560516067 100644 --- a/SignalServiceKit/src/Account/TSAccountManager.m +++ b/SignalServiceKit/src/Account/TSAccountManager.m @@ -314,7 +314,7 @@ NSString *const kNSNotificationName_LocalNumberDidChange = @"kNSNotificationName DDLogInfo(@"%@ Successfully unregistered", self.tag); success(); - // This is called from `[SettingsTableViewController proceedToUnregistration]` whose + // This is called from `[AppSettingsViewController proceedToUnregistration]` whose // success handler calls `[Environment resetAppData]`. // This method, after calling that success handler, fires // `kNSNotificationName_RegistrationStateDidChange` which is only safe to fire after diff --git a/SignalServiceKit/src/Messages/Attachments/TSAttachmentStream.m b/SignalServiceKit/src/Messages/Attachments/TSAttachmentStream.m index 27c5db7bb..74bc26eda 100644 --- a/SignalServiceKit/src/Messages/Attachments/TSAttachmentStream.m +++ b/SignalServiceKit/src/Messages/Attachments/TSAttachmentStream.m @@ -167,7 +167,7 @@ NS_ASSUME_NONNULL_BEGIN dispatch_once(&onceToken, ^{ NSString *documentsPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject]; - attachmentsFolder = [documentsPath stringByAppendingFormat:@"/Attachments"]; + attachmentsFolder = [documentsPath stringByAppendingPathComponent:@"Attachments"]; BOOL isDirectory; BOOL exists = [[NSFileManager defaultManager] fileExistsAtPath:attachmentsFolder isDirectory:&isDirectory]; diff --git a/SignalServiceKit/src/Profiles/OWSProfilesManager.h b/SignalServiceKit/src/Profiles/OWSProfilesManager.h index 139bb2728..bddbcae48 100644 --- a/SignalServiceKit/src/Profiles/OWSProfilesManager.h +++ b/SignalServiceKit/src/Profiles/OWSProfilesManager.h @@ -4,6 +4,8 @@ NS_ASSUME_NONNULL_BEGIN +extern NSString *const kNSNotificationName_LocalProfileDidChange; + // This class can be safely accessed and used from any thread. @interface OWSProfilesManager : NSObject @@ -11,6 +13,19 @@ NS_ASSUME_NONNULL_BEGIN + (instancetype)sharedManager; +@property (atomic, nullable, readonly) NSString *localProfileName; +@property (atomic, nullable, readonly) UIImage *localProfileAvatarImage; + +// This method is used to update the "local profile" state on the client +// and the service. Client state is only updated if service state is +// successfully updated. +- (void)updateLocalProfileName:(nullable NSString *)localProfileName + localProfileAvatarImage:(nullable UIImage *)localProfileAvatarImage + success:(void (^)())successBlock + failure:(void (^)())failureBlock; + +- (void)appLaunchDidBegin; + @end NS_ASSUME_NONNULL_END diff --git a/SignalServiceKit/src/Profiles/OWSProfilesManager.m b/SignalServiceKit/src/Profiles/OWSProfilesManager.m index a46c7f437..7eda6dd54 100644 --- a/SignalServiceKit/src/Profiles/OWSProfilesManager.m +++ b/SignalServiceKit/src/Profiles/OWSProfilesManager.m @@ -8,11 +8,79 @@ #import "TSStorageManager.h" #import "TextSecureKitEnv.h" +#import "TSYapDatabaseObject.h" + NS_ASSUME_NONNULL_BEGIN +@class TSThread; + +@interface AvatarMetadata : TSYapDatabaseObject + +// This filename is relative to OWSProfilesManager.profileAvatarsDirPath. +@property (nonatomic, readonly) NSString *fileName; +@property (nonatomic, readonly) NSString *avatarUrl; +@property (nonatomic, readonly) NSString *avatarDigest; + +- (instancetype)init NS_UNAVAILABLE; + +@end + +#pragma mark - + +@implementation AvatarMetadata + ++ (NSString *)collection +{ + return @"AvatarMetadata"; +} + +- (instancetype)initWithFileName:(NSString *)fileName + avatarUrl:(NSString *)avatarUrl + avatarDigest:(NSString *)avatarDigest +{ + // TODO: Local filenames for avatars are guaranteed to be unique. + self = [super initWithUniqueId:fileName]; + + if (!self) { + return self; + } + + OWSAssert(fileName.length > 0); + OWSAssert(avatarUrl.length > 0); + OWSAssert(avatarDigest.length > 0); + _fileName = fileName; + _avatarUrl = avatarUrl; + _avatarDigest = avatarDigest; + + return self; +} + + +#pragma mark - NSObject + +- (BOOL)isEqual:(AvatarMetadata *)other +{ + return ([other isKindOfClass:[AvatarMetadata class]] && [self.fileName isEqualToString:other.fileName] && + [self.avatarUrl isEqualToString:other.avatarUrl] && [self.avatarDigest isEqualToString:other.avatarDigest]); +} + +- (NSUInteger)hash +{ + return self.fileName.hash ^ self.avatarUrl.hash ^ self.avatarDigest.hash; +} + +@end + +#pragma mark - + +NSString *const kNSNotificationName_LocalProfileDidChange = @"kNSNotificationName_LocalProfileDidChange"; + NSString *const kOWSProfilesManager_Collection = @"kOWSProfilesManager_Collection"; // This key is used to persist the local user's profile key. -NSString *const kOWSProfilesManager_LocalProfileKey = @"kOWSProfilesManager_LocalProfileKey"; +NSString *const kOWSProfilesManager_LocalProfileSecretKey = @"kOWSProfilesManager_LocalProfileSecretKey"; +NSString *const kOWSProfilesManager_LocalProfileNameKey = @"kOWSProfilesManager_LocalProfileNameKey"; +NSString *const kOWSProfilesManager_LocalProfileAvatarMetadataKey + = @"kOWSProfilesManager_LocalProfileAvatarMetadataKey"; // TODO: static const NSInteger kProfileKeyLength = 16; @@ -22,7 +90,13 @@ static const NSInteger kProfileKeyLength = 16; @property (nonatomic, readonly) TSStorageManager *storageManager; @property (nonatomic, readonly) OWSMessageSender *messageSender; -@property (nonatomic, readonly, nullable) NSData *localProfileKey; +@property (atomic, readonly, nullable) NSData *localProfileKey; + +// These properties should only be mutated on the main thread, +// but they may be accessed on other threads. +@property (atomic, nullable) NSString *localProfileName; +@property (atomic, nullable) UIImage *localProfileAvatarImage; +@property (atomic, nullable) AvatarMetadata *localProfileAvatarMetadata; @end @@ -72,18 +146,20 @@ static const NSInteger kProfileKeyLength = 16; [messageSender setProfilesManager:self]; // Try to load. - _localProfileKey = [self.storageManager objectForKey:kOWSProfilesManager_LocalProfileKey + _localProfileKey = [self.storageManager objectForKey:kOWSProfilesManager_LocalProfileSecretKey inCollection:kOWSProfilesManager_Collection]; if (!_localProfileKey) { // Generate _localProfileKey = [OWSProfilesManager generateLocalProfileKey]; // Persist [self.storageManager setObject:_localProfileKey - forKey:kOWSProfilesManager_LocalProfileKey + forKey:kOWSProfilesManager_LocalProfileSecretKey inCollection:kOWSProfilesManager_Collection]; } OWSAssert(_localProfileKey.length == kProfileKeyLength); + [self loadLocalProfileAsync]; + return self; } @@ -100,19 +176,271 @@ static const NSInteger kProfileKeyLength = 16; object:nil]; } +- (void)appLaunchDidBegin +{ + // Do nothing; we only want to make sure this singleton is created on startup. +} + #pragma mark - Local Profile Key + (NSData *)generateLocalProfileKey { // TODO: - OWSFail(@"Profile key generation is not yet implemented."); + DDLogVerbose(@"%@ Profile key generation is not yet implemented.", self.tag); return [SecurityUtils generateRandomBytes:kProfileKeyLength]; } -- (nullable NSData *)localProfileKey +#pragma mark - Local Profile + +// This method is use to update client "local profile" state. +- (void)updateLocalProfileName:(nullable NSString *)localProfileName + localProfileAvatarImage:(nullable UIImage *)localProfileAvatarImage + localProfileAvatarMetadata:(nullable AvatarMetadata *)localProfileAvatarMetadata { - OWSAssert(_localProfileKey.length == kProfileKeyLength); - return _localProfileKey; + OWSAssert([NSThread isMainThread]); + + // The avatar image and filename should both be set, or neither should be set. + if (!localProfileAvatarMetadata && localProfileAvatarImage) { + OWSFail(@"Missing avatar metadata."); + localProfileAvatarImage = nil; + } + if (localProfileAvatarMetadata && !localProfileAvatarImage) { + OWSFail(@"Missing avatar image."); + localProfileAvatarMetadata = nil; + } + + self.localProfileName = localProfileName; + self.localProfileAvatarImage = localProfileAvatarImage; + self.localProfileAvatarMetadata = localProfileAvatarMetadata; + + if (localProfileName) { + [self.storageManager setObject:localProfileName + forKey:kOWSProfilesManager_LocalProfileNameKey + inCollection:kOWSProfilesManager_Collection]; + } else { + [self.storageManager removeObjectForKey:kOWSProfilesManager_LocalProfileNameKey + inCollection:kOWSProfilesManager_Collection]; + } + if (localProfileAvatarMetadata) { + [self.storageManager setObject:localProfileAvatarMetadata + forKey:kOWSProfilesManager_LocalProfileAvatarMetadataKey + inCollection:kOWSProfilesManager_Collection]; + } else { + [self.storageManager removeObjectForKey:kOWSProfilesManager_LocalProfileAvatarMetadataKey + inCollection:kOWSProfilesManager_Collection]; + } + + [[NSNotificationCenter defaultCenter] postNotificationName:kNSNotificationName_LocalProfileDidChange + object:nil + userInfo:nil]; +} + +- (void)updateLocalProfileName:(nullable NSString *)localProfileName + localProfileAvatarImage:(nullable UIImage *)localProfileAvatarImage + success:(void (^)())successBlock + failure:(void (^)())failureBlockParameter +{ + OWSAssert([NSThread isMainThread]); + OWSAssert(successBlock); + OWSAssert(failureBlockParameter); + + // Ensure that the failure block is called on the main thread. + void (^failureBlock)() = ^{ + dispatch_async(dispatch_get_main_queue(), ^{ + failureBlockParameter(); + }); + }; + + // The final steps are to: + // + // * Try to update the service. + // * Update client state on success. + void (^tryToUpdateService)(AvatarMetadata *_Nullable) = ^(AvatarMetadata *_Nullable avatarMetadata) { + [self updateProfileOnService:localProfileName + avatarMetadata:avatarMetadata + success:^{ + dispatch_async(dispatch_get_main_queue(), ^{ + [self updateLocalProfileName:localProfileName + localProfileAvatarImage:localProfileAvatarImage + localProfileAvatarMetadata:avatarMetadata]; + successBlock(); + }); + } + failure:^{ + failureBlock(); + }]; + }; + + // If we have a new avatar image, we must first: + // + // * Encode it to JPEG. + // * Write it to disk. + // * Upload it to service. + if (localProfileAvatarImage) { + if (self.localProfileAvatarMetadata && self.localProfileAvatarImage == localProfileAvatarImage) { + DDLogVerbose(@"%@ Updating local profile on service with unchanged avatar.", self.tag); + // If the avatar hasn't changed, reuse the existing metadata. + tryToUpdateService(self.localProfileAvatarMetadata); + } else { + DDLogVerbose(@"%@ Updating local profile on service with new avatar.", self.tag); + [self writeAvatarToDisk:localProfileAvatarImage + success:^(NSData *data, NSString *fileName) { + [self uploadAvatarToService:data + fileName:fileName + success:^(AvatarMetadata *avatarMetadata) { + tryToUpdateService(avatarMetadata); + } + failure:^{ + failureBlock(); + }]; + } + failure:^{ + failureBlock(); + }]; + } + } else { + DDLogVerbose(@"%@ Updating local profile on service with no avatar.", self.tag); + tryToUpdateService(nil); + } +} + +- (void)writeAvatarToDisk:(UIImage *)avatar + success:(void (^)(NSData *data, NSString *fileName))successBlock + failure:(void (^)())failureBlock +{ + OWSAssert(avatar); + OWSAssert(successBlock); + OWSAssert(failureBlock); + + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + if (avatar) { + NSData *_Nullable data = UIImageJPEGRepresentation(avatar, 1.f); + OWSAssert(data); + if (data) { + NSString *fileName = [[NSUUID UUID].UUIDString stringByAppendingPathExtension:@"jpg"]; + NSString *filePath = [self.profileAvatarsDirPath stringByAppendingPathComponent:fileName]; + BOOL success = [data writeToFile:filePath atomically:YES]; + OWSAssert(success); + if (success) { + successBlock(data, fileName); + return; + } + } + } + failureBlock(); + }); +} + +// TODO: The exact API & encryption scheme for avatars is not yet settled. +- (void)uploadAvatarToService:(NSData *)data + fileName:(NSString *)fileName + success:(void (^)(AvatarMetadata *avatarMetadata))successBlock + failure:(void (^)())failureBlock +{ + OWSAssert(data.length > 0); + OWSAssert(fileName.length > 0); + OWSAssert(successBlock); + OWSAssert(failureBlock); + + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + // TODO: + NSString *avatarUrl = @"avatarUrl"; + NSString *avatarDigest = @"digest"; + AvatarMetadata *avatarMetadata = + [[AvatarMetadata alloc] initWithFileName:fileName avatarUrl:avatarUrl avatarDigest:avatarDigest]; + if (YES) { + successBlock(avatarMetadata); + return; + } + failureBlock(); + }); +} + +// TODO: The exact API & encryption scheme for profiles is not yet settled. +- (void)updateProfileOnService:(nullable NSString *)localProfileName + avatarMetadata:(nullable AvatarMetadata *)avatarMetadata + success:(void (^)())successBlock + failure:(void (^)())failureBlock +{ + OWSAssert(successBlock); + OWSAssert(failureBlock); + + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + // TODO: + if (YES) { + successBlock(); + return; + } + failureBlock(); + }); +} + +- (void)loadLocalProfileAsync +{ + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + NSString *_Nullable localProfileName = [self.storageManager objectForKey:kOWSProfilesManager_LocalProfileNameKey + inCollection:kOWSProfilesManager_Collection]; + AvatarMetadata *_Nullable localProfileAvatarMetadata = + [self.storageManager objectForKey:kOWSProfilesManager_LocalProfileAvatarMetadataKey + inCollection:kOWSProfilesManager_Collection]; + UIImage *_Nullable localProfileAvatarImage = nil; + if (localProfileAvatarMetadata) { + localProfileAvatarImage = [self loadProfileAvatarWithFilename:localProfileAvatarMetadata.fileName]; + if (!localProfileAvatarImage) { + localProfileAvatarMetadata = nil; + } + } + + dispatch_async(dispatch_get_main_queue(), ^{ + self.localProfileName = localProfileName; + self.localProfileAvatarImage = localProfileAvatarImage; + self.localProfileAvatarMetadata = localProfileAvatarMetadata; + + [[NSNotificationCenter defaultCenter] postNotificationName:kNSNotificationName_LocalProfileDidChange + object:nil + userInfo:nil]; + }); + }); +} + +#pragma mark - Avatar Disk Cache + +- (nullable UIImage *)loadProfileAvatarWithFilename:(NSString *)fileName +{ + OWSAssert(fileName.length > 0); + + NSString *filePath = [self.profileAvatarsDirPath stringByAppendingPathComponent:fileName]; + UIImage *_Nullable image = [UIImage imageWithContentsOfFile:filePath]; + return image; +} + +- (NSString *)profileAvatarsDirPath +{ + static NSString *profileAvatarsDirPath = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + NSString *documentsPath = + [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject]; + profileAvatarsDirPath = [documentsPath stringByAppendingPathComponent:@"ProfileAvatars"]; + + BOOL isDirectory; + BOOL exists = [[NSFileManager defaultManager] fileExistsAtPath:profileAvatarsDirPath isDirectory:&isDirectory]; + if (exists) { + OWSAssert(isDirectory); + + DDLogInfo(@"Profile avatars directory already exists"); + } else { + NSError *error = nil; + [[NSFileManager defaultManager] createDirectoryAtPath:profileAvatarsDirPath + withIntermediateDirectories:YES + attributes:nil + error:&error]; + if (error) { + DDLogError(@"Failed to create profile avatars directory: %@", error); + } + } + }); + return profileAvatarsDirPath; } #pragma mark - Notifications diff --git a/SignalServiceKit/src/Storage/TSStorageManager.m b/SignalServiceKit/src/Storage/TSStorageManager.m index c52489fdd..f030b36b9 100644 --- a/SignalServiceKit/src/Storage/TSStorageManager.m +++ b/SignalServiceKit/src/Storage/TSStorageManager.m @@ -277,7 +277,7 @@ static NSString *keychainDBPassAccount = @"TSDatabasePass"; #if TARGET_OS_IPHONE NSURL *fileURL = [[fileManager URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject]; NSString *path = [fileURL path]; - databasePath = [path stringByAppendingFormat:@"/%@", databaseName]; + databasePath = [path stringByAppendingPathComponent:databaseName]; #elif TARGET_OS_MAC NSString *bundleID = [[NSBundle mainBundle] bundleIdentifier]; @@ -289,7 +289,7 @@ static NSString *keychainDBPassAccount = @"TSDatabasePass"; [fileManager createDirectoryAtURL:appDirectory withIntermediateDirectories:NO attributes:nil error:nil]; } - databasePath = [appDirectory.filePathURL.absoluteString stringByAppendingFormat:@"/%@", databaseName]; + databasePath = [appDirectory.filePathURL.absoluteString stringByAppendingPathComponent:databaseName]; #endif return databasePath; diff --git a/SignalServiceKit/src/Util/MIMETypeUtil.m b/SignalServiceKit/src/Util/MIMETypeUtil.m index 5c77a0002..dfde27b72 100644 --- a/SignalServiceKit/src/Util/MIMETypeUtil.m +++ b/SignalServiceKit/src/Util/MIMETypeUtil.m @@ -364,36 +364,41 @@ NSString *const OWSMimeTypeUnknownForTests = @"unknown/mimetype"; } + (NSString *)filePathForImage:(NSString *)uniqueId ofMIMEType:(NSString *)contentType inFolder:(NSString *)folder { - return [[folder stringByAppendingFormat:@"/%@", uniqueId] - stringByAppendingPathExtension:[self getSupportedExtensionFromImageMIMEType:contentType]]; + return [self filePathForData:uniqueId + withFileExtension:[self getSupportedExtensionFromImageMIMEType:contentType] + inFolder:folder]; } + (NSString *)filePathForVideo:(NSString *)uniqueId ofMIMEType:(NSString *)contentType inFolder:(NSString *)folder { - return [[folder stringByAppendingFormat:@"/%@", uniqueId] - stringByAppendingPathExtension:[self getSupportedExtensionFromVideoMIMEType:contentType]]; + return [self filePathForData:uniqueId + withFileExtension:[self getSupportedExtensionFromVideoMIMEType:contentType] + inFolder:folder]; } + (NSString *)filePathForAudio:(NSString *)uniqueId ofMIMEType:(NSString *)contentType inFolder:(NSString *)folder { - return [[folder stringByAppendingFormat:@"/%@", uniqueId] - stringByAppendingPathExtension:[self getSupportedExtensionFromAudioMIMEType:contentType]]; + return [self filePathForData:uniqueId + withFileExtension:[self getSupportedExtensionFromAudioMIMEType:contentType] + inFolder:folder]; } + (NSString *)filePathForAnimated:(NSString *)uniqueId ofMIMEType:(NSString *)contentType inFolder:(NSString *)folder { - return [[folder stringByAppendingFormat:@"/%@", uniqueId] - stringByAppendingPathExtension:[self getSupportedExtensionFromAnimatedMIMEType:contentType]]; + return [self filePathForData:uniqueId + withFileExtension:[self getSupportedExtensionFromAnimatedMIMEType:contentType] + inFolder:folder]; } + (NSString *)filePathForBinaryData:(NSString *)uniqueId ofMIMEType:(NSString *)contentType inFolder:(NSString *)folder { - return [[folder stringByAppendingFormat:@"/%@", uniqueId] - stringByAppendingPathExtension:[self getSupportedExtensionFromBinaryDataMIMEType:contentType]]; + return [self filePathForData:uniqueId + withFileExtension:[self getSupportedExtensionFromBinaryDataMIMEType:contentType] + inFolder:folder]; } + (NSString *)filePathForData:(NSString *)uniqueId withFileExtension:(NSString *)fileExtension inFolder:(NSString *)folder { - return [[folder stringByAppendingFormat:@"/%@", uniqueId] stringByAppendingPathExtension:fileExtension]; + return [folder stringByAppendingPathComponent:[uniqueId stringByAppendingPathExtension:fileExtension]]; } + (nullable NSString *)utiTypeForMIMEType:(NSString *)mimeType