From 4f3faa28bcc7677af9e47988248bd3830c627b66 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Tue, 11 Jan 2022 12:38:19 +1100 Subject: [PATCH] Refactored SessionShareExtension code to Swift --- Session.xcodeproj/project.pbxproj | 32 ++- .../SignalShareExtension-Bridging-Header.h | 2 - .../SAEScreenLockViewController.h | 18 -- .../SAEScreenLockViewController.m | 206 --------------- .../SAEScreenLockViewController.swift | 206 +++++++++++++++ .../ShareAppExtensionContext.h | 18 -- .../ShareAppExtensionContext.m | 240 ------------------ .../ShareAppExtensionContext.swift | 204 +++++++++++++++ SessionShareExtension/ShareVC.swift | 175 +++++++------ .../SimplifiedConversationCell.swift | 107 +++++--- SessionShareExtension/ThreadPickerVC.swift | 129 ++++++---- .../General/ReusableView.swift | 16 ++ .../General/String+Localization.swift | 13 + .../General/UITableView+ReusableView.swift | 23 ++ .../Utilities/ShareViewDelegate.swift | 2 +- 15 files changed, 741 insertions(+), 650 deletions(-) delete mode 100644 SessionShareExtension/SAEScreenLockViewController.h delete mode 100644 SessionShareExtension/SAEScreenLockViewController.m create mode 100644 SessionShareExtension/SAEScreenLockViewController.swift delete mode 100644 SessionShareExtension/ShareAppExtensionContext.h delete mode 100644 SessionShareExtension/ShareAppExtensionContext.m create mode 100644 SessionShareExtension/ShareAppExtensionContext.swift create mode 100644 SessionUtilitiesKit/General/ReusableView.swift create mode 100644 SessionUtilitiesKit/General/String+Localization.swift create mode 100644 SessionUtilitiesKit/General/UITableView+ReusableView.swift diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 97ce6a4b5..a224cb1f3 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -21,10 +21,8 @@ 34330AA31E79686200DF2FB9 /* OWSProgressView.m in Sources */ = {isa = PBXBuildFile; fileRef = 34330AA21E79686200DF2FB9 /* OWSProgressView.m */; }; 34386A54207D271D009F5D9C /* NeverClearView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34386A53207D271C009F5D9C /* NeverClearView.swift */; }; 3441FD9F21A3604F00BB9542 /* BackupRestoreViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3441FD9E21A3604F00BB9542 /* BackupRestoreViewController.swift */; }; - 34480B361FD0929200BC14EF /* ShareAppExtensionContext.m in Sources */ = {isa = PBXBuildFile; fileRef = 34480B351FD0929200BC14EF /* ShareAppExtensionContext.m */; }; 344825C6211390C800DB4BD8 /* OWSOrphanDataCleaner.m in Sources */ = {isa = PBXBuildFile; fileRef = 344825C5211390C800DB4BD8 /* OWSOrphanDataCleaner.m */; }; 346129991FD1E4DA00532771 /* SignalApp.m in Sources */ = {isa = PBXBuildFile; fileRef = 346129971FD1E4D900532771 /* SignalApp.m */; }; - 34641E1F2088DA6D00E2EDE5 /* SAEScreenLockViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 34641E1E2088DA6D00E2EDE5 /* SAEScreenLockViewController.m */; }; 34661FB820C1C0D60056EDD6 /* message_sent.aiff in Resources */ = {isa = PBXBuildFile; fileRef = 34661FB720C1C0D60056EDD6 /* message_sent.aiff */; }; 346B66311F4E29B200E5122F /* CropScaleImageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 346B66301F4E29B200E5122F /* CropScaleImageViewController.swift */; }; 3478504C1FD7496D007B8332 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B66DBF4919D5BBC8006EA940 /* Images.xcassets */; }; @@ -775,6 +773,11 @@ FAD6392E0205566D11AA9E48 /* Pods_Session.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CB3724C70247A916D43271FE /* Pods_Session.framework */; }; FC3BD9881A30A790005B96BB /* Social.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FC3BD9871A30A790005B96BB /* Social.framework */; }; FCB11D8C1A129A76002F93FB /* CoreMedia.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FCB11D8B1A129A76002F93FB /* CoreMedia.framework */; }; + FD705A8C278CDB5600F16121 /* SAEScreenLockViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD705A8B278CDB5600F16121 /* SAEScreenLockViewController.swift */; }; + FD705A8E278CE29800F16121 /* String+Localization.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD705A8D278CE29800F16121 /* String+Localization.swift */; }; + FD705A90278CEBBC00F16121 /* ShareAppExtensionContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD705A8F278CEBBC00F16121 /* ShareAppExtensionContext.swift */; }; + FD705A92278D051200F16121 /* ReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD705A91278D051200F16121 /* ReusableView.swift */; }; + FD705A94278D052B00F16121 /* UITableView+ReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD705A93278D052B00F16121 /* UITableView+ReusableView.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -956,16 +959,12 @@ 34330AA21E79686200DF2FB9 /* OWSProgressView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSProgressView.m; sourceTree = ""; }; 34386A53207D271C009F5D9C /* NeverClearView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NeverClearView.swift; sourceTree = ""; }; 3441FD9E21A3604F00BB9542 /* BackupRestoreViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BackupRestoreViewController.swift; sourceTree = ""; }; - 34480B341FD0929200BC14EF /* ShareAppExtensionContext.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ShareAppExtensionContext.h; sourceTree = ""; }; - 34480B351FD0929200BC14EF /* ShareAppExtensionContext.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ShareAppExtensionContext.m; sourceTree = ""; }; 34480B371FD092A900BC14EF /* SignalShareExtension-Bridging-Header.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "SignalShareExtension-Bridging-Header.h"; sourceTree = ""; }; 34480B381FD092E300BC14EF /* SessionShareExtension-Prefix.pch */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "SessionShareExtension-Prefix.pch"; sourceTree = ""; }; 344825C4211390C700DB4BD8 /* OWSOrphanDataCleaner.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSOrphanDataCleaner.h; sourceTree = ""; }; 344825C5211390C800DB4BD8 /* OWSOrphanDataCleaner.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSOrphanDataCleaner.m; sourceTree = ""; }; 346129971FD1E4D900532771 /* SignalApp.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SignalApp.m; sourceTree = ""; }; 346129981FD1E4DA00532771 /* SignalApp.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SignalApp.h; sourceTree = ""; }; - 34641E1D2088DA6C00E2EDE5 /* SAEScreenLockViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SAEScreenLockViewController.h; sourceTree = ""; }; - 34641E1E2088DA6D00E2EDE5 /* SAEScreenLockViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SAEScreenLockViewController.m; sourceTree = ""; }; 34661FB720C1C0D60056EDD6 /* message_sent.aiff */ = {isa = PBXFileReference; lastKnownFileType = audio.aiff; name = message_sent.aiff; path = Session/Meta/AudioFiles/message_sent.aiff; sourceTree = SOURCE_ROOT; }; 346B66301F4E29B200E5122F /* CropScaleImageViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CropScaleImageViewController.swift; sourceTree = ""; }; 3488F9352191CC4000E524CC /* MediaView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaView.swift; sourceTree = ""; }; @@ -1780,6 +1779,11 @@ FB523C549815DE935E98151E /* Pods_SessionMessagingKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_SessionMessagingKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; FC3BD9871A30A790005B96BB /* Social.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Social.framework; path = System/Library/Frameworks/Social.framework; sourceTree = SDKROOT; }; FCB11D8B1A129A76002F93FB /* CoreMedia.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreMedia.framework; path = System/Library/Frameworks/CoreMedia.framework; sourceTree = SDKROOT; }; + FD705A8B278CDB5600F16121 /* SAEScreenLockViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SAEScreenLockViewController.swift; sourceTree = ""; }; + FD705A8D278CE29800F16121 /* String+Localization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Localization.swift"; sourceTree = ""; }; + FD705A8F278CEBBC00F16121 /* ShareAppExtensionContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareAppExtensionContext.swift; sourceTree = ""; }; + FD705A91278D051200F16121 /* ReusableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReusableView.swift; sourceTree = ""; }; + FD705A93278D052B00F16121 /* UITableView+ReusableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UITableView+ReusableView.swift"; sourceTree = ""; }; FF9BA33D021B115B1F5B4E46 /* Pods-SessionMessagingKit.app store release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SessionMessagingKit.app store release.xcconfig"; path = "Pods/Target Support Files/Pods-SessionMessagingKit/Pods-SessionMessagingKit.app store release.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ @@ -1971,10 +1975,8 @@ children = ( C31C21A4255BCA4800EC2D66 /* Meta */, 4535186C1FC635DD00210559 /* MainInterface.storyboard */, - 34641E1D2088DA6C00E2EDE5 /* SAEScreenLockViewController.h */, - 34641E1E2088DA6D00E2EDE5 /* SAEScreenLockViewController.m */, - 34480B341FD0929200BC14EF /* ShareAppExtensionContext.h */, - 34480B351FD0929200BC14EF /* ShareAppExtensionContext.m */, + FD705A8B278CDB5600F16121 /* SAEScreenLockViewController.swift */, + FD705A8F278CEBBC00F16121 /* ShareAppExtensionContext.swift */, C3ADC66026426688005F1414 /* ShareVC.swift */, B817AD9B26436F73009DF825 /* ThreadPickerVC.swift */, B817AD9926436593009DF825 /* SimplifiedConversationCell.swift */, @@ -2282,8 +2284,11 @@ C33FDB6B255A580F00E217F9 /* SNUserDefaults.swift */, C33FDB3F255A580C00E217F9 /* String+SSK.swift */, C3C2AC2D2553CBEB00C340D1 /* String+Trimming.swift */, + FD705A8D278CE29800F16121 /* String+Localization.swift */, C38EF237255B6D65007E1867 /* UIDevice+featureSupport.swift */, C35D0DB425AE5F1200B6BF49 /* UIEdgeInsets.swift */, + FD705A91278D051200F16121 /* ReusableView.swift */, + FD705A93278D052B00F16121 /* UITableView+ReusableView.swift */, C38EF23D255B6D66007E1867 /* UIView+OWS.h */, C38EF23E255B6D66007E1867 /* UIView+OWS.m */, C38EF2EF255B6DBB007E1867 /* Weak.swift */, @@ -4330,9 +4335,9 @@ buildActionMask = 2147483647; files = ( B817AD9A26436593009DF825 /* SimplifiedConversationCell.swift in Sources */, - 34480B361FD0929200BC14EF /* ShareAppExtensionContext.m in Sources */, C3ADC66126426688005F1414 /* ShareVC.swift in Sources */, - 34641E1F2088DA6D00E2EDE5 /* SAEScreenLockViewController.m in Sources */, + FD705A90278CEBBC00F16121 /* ShareAppExtensionContext.swift in Sources */, + FD705A8C278CDB5600F16121 /* SAEScreenLockViewController.swift in Sources */, B817AD9C26436F73009DF825 /* ThreadPickerVC.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -4549,6 +4554,7 @@ C3D9E4C02567767F0040E4F3 /* DataSource.m in Sources */, C3D9E43125676D3D0040E4F3 /* Configuration.swift in Sources */, C32C5DD2256DD9E5003C73A2 /* LRUCache.swift in Sources */, + FD705A94278D052B00F16121 /* UITableView+ReusableView.swift in Sources */, C3A7211A2558BCA10043A11F /* DiffieHellman.swift in Sources */, C32C5FA1256DFED5003C73A2 /* NSArray+Functional.m in Sources */, C3A7225E2558C38D0043A11F /* Promise+Retaining.swift in Sources */, @@ -4561,6 +4567,7 @@ C32C5B48256DC211003C73A2 /* NSNotificationCenter+OWS.m in Sources */, C3D9E3C925676AF30040E4F3 /* TSYapDatabaseObject.m in Sources */, C352A3A62557B60D00338F3E /* TSRequest.m in Sources */, + FD705A92278D051200F16121 /* ReusableView.swift in Sources */, B8AE75A425A6C6A6001A84D2 /* Data+Trimming.swift in Sources */, B8856DE6256F15F2001CE70E /* String+SSK.swift in Sources */, C3471ED42555386B00297E91 /* AESGCM.swift in Sources */, @@ -4584,6 +4591,7 @@ C300A60D2554B31900555489 /* Logging.swift in Sources */, B8FF8EA625C11FEF004D1F22 /* IPv4.swift in Sources */, C3D9E35525675EE10040E4F3 /* MIMETypeUtil.m in Sources */, + FD705A8E278CE29800F16121 /* String+Localization.swift in Sources */, C3A7219A2558C1660043A11F /* AnyPromise+Conversion.swift in Sources */, C300A6322554B6D100555489 /* NSDate+Timestamp.mm in Sources */, ); diff --git a/SessionShareExtension/Meta/SignalShareExtension-Bridging-Header.h b/SessionShareExtension/Meta/SignalShareExtension-Bridging-Header.h index 92486ea73..29c93b1b9 100644 --- a/SessionShareExtension/Meta/SignalShareExtension-Bridging-Header.h +++ b/SessionShareExtension/Meta/SignalShareExtension-Bridging-Header.h @@ -6,8 +6,6 @@ #import // Separate iOS Frameworks from other imports. -#import "SAEScreenLockViewController.h" -#import "ShareAppExtensionContext.h" #import #import #import diff --git a/SessionShareExtension/SAEScreenLockViewController.h b/SessionShareExtension/SAEScreenLockViewController.h deleted file mode 100644 index 1feaf140e..000000000 --- a/SessionShareExtension/SAEScreenLockViewController.h +++ /dev/null @@ -1,18 +0,0 @@ -// -// Copyright (c) 2018 Open Whisper Systems. All rights reserved. -// - -#import -#import - -NS_ASSUME_NONNULL_BEGIN - -@protocol ShareViewDelegate; - -@interface SAEScreenLockViewController : ScreenLockViewController - -- (instancetype)initWithShareViewDelegate:(id)shareViewDelegate; - -@end - -NS_ASSUME_NONNULL_END diff --git a/SessionShareExtension/SAEScreenLockViewController.m b/SessionShareExtension/SAEScreenLockViewController.m deleted file mode 100644 index 9cae51ad6..000000000 --- a/SessionShareExtension/SAEScreenLockViewController.m +++ /dev/null @@ -1,206 +0,0 @@ -// -// Copyright (c) 2018 Open Whisper Systems. All rights reserved. -// - -#import "SAEScreenLockViewController.h" -#import "UIColor+OWS.h" -#import -#import - -NS_ASSUME_NONNULL_BEGIN - -@interface SAEScreenLockViewController () - -@property (nonatomic, readonly, weak) id shareViewDelegate; - -@property (nonatomic) BOOL hasShownAuthUIOnce; - -@property (nonatomic) BOOL isShowingAuthUI; - -@end - -#pragma mark - - -@implementation SAEScreenLockViewController - -- (instancetype)initWithShareViewDelegate:(id)shareViewDelegate -{ - self = [super init]; - if (!self) { - return self; - } - - _shareViewDelegate = shareViewDelegate; - - self.delegate = self; - - return self; -} - -- (void)loadView -{ - [super loadView]; - - UIView.appearance.tintColor = LKColors.text; - - // Gradient background - self.view.backgroundColor = UIColor.clearColor; - CAGradientLayer *layer = [CAGradientLayer new]; - layer.frame = UIScreen.mainScreen.bounds; - UIColor *gradientStartColor = LKAppModeUtilities.isLightMode ? [UIColor colorWithRGBHex:0xFCFCFC] : [UIColor colorWithRGBHex:0x171717]; - UIColor *gradientEndColor = LKAppModeUtilities.isLightMode ? [UIColor colorWithRGBHex:0xFFFFFF] : [UIColor colorWithRGBHex:0x121212]; - layer.colors = @[ (id)gradientStartColor.CGColor, (id)gradientEndColor.CGColor ]; - [self.view.layer insertSublayer:layer atIndex:0]; - - // Navigation bar background color - UINavigationBar *navigationBar = self.navigationController.navigationBar; - [navigationBar setBackgroundImage:[UIImage new] forBarMetrics:UIBarMetricsDefault]; - navigationBar.shadowImage = [UIImage new]; - [navigationBar setTranslucent:NO]; - navigationBar.barTintColor = LKColors.navigationBarBackground; - - // Title - UILabel *titleLabel = [UILabel new]; - titleLabel.text = NSLocalizedString(@"vc_share_title", @""); - titleLabel.textColor = LKColors.text; - titleLabel.font = [UIFont boldSystemFontOfSize:LKValues.veryLargeFontSize]; - self.navigationItem.titleView = titleLabel; - - // Close button - UIBarButtonItem *closeButton = [[UIBarButtonItem alloc] initWithImage:[UIImage imageNamed:@"X"] style:UIBarButtonItemStylePlain target:self action:@selector(dismissPressed:)]; - closeButton.tintColor = LKColors.text; - self.navigationItem.leftBarButtonItem = closeButton; -} - -- (void)viewWillAppear:(BOOL)animated -{ - [super viewWillAppear:animated]; - - [self ensureUI]; -} - -- (void)viewDidAppear:(BOOL)animated -{ - [super viewDidAppear:animated]; - - [self ensureUI]; - - // Auto-show the auth UI f - if (!self.hasShownAuthUIOnce) { - self.hasShownAuthUIOnce = YES; - - [self tryToPresentAuthUIToUnlockScreenLock]; - } -} - -- (void)dealloc -{ - // Surface memory leaks by logging the deallocation of view controllers. - OWSLogVerbose(@"Dealloc: %@", self.class); -} - -- (void)tryToPresentAuthUIToUnlockScreenLock -{ - OWSAssertIsOnMainThread(); - - if (self.isShowingAuthUI) { - // We're already showing the auth UI; abort. - return; - } - OWSLogInfo(@"try to unlock screen lock"); - - self.isShowingAuthUI = YES; - - [OWSScreenLock.sharedManager tryToUnlockScreenLockWithSuccess:^{ - OWSAssertIsOnMainThread(); - - OWSLogInfo(@"unlock screen lock succeeded."); - - self.isShowingAuthUI = NO; - - [self.shareViewDelegate shareViewWasUnlocked]; - } - failure:^(NSError *error) { - OWSAssertIsOnMainThread(); - - OWSLogInfo(@"unlock screen lock failed."); - - self.isShowingAuthUI = NO; - - [self ensureUI]; - - [self showScreenLockFailureAlertWithMessage:error.localizedDescription]; - } - unexpectedFailure:^(NSError *error) { - OWSAssertIsOnMainThread(); - - OWSLogInfo(@"unlock screen lock unexpectedly failed."); - - self.isShowingAuthUI = NO; - - // Local Authentication isn't working properly. - // This isn't covered by the docs or the forums but in practice - // it appears to be effective to retry again after waiting a bit. - dispatch_async(dispatch_get_main_queue(), ^{ - [self ensureUI]; - }); - } - cancel:^{ - OWSAssertIsOnMainThread(); - - OWSLogInfo(@"unlock screen lock cancelled."); - - self.isShowingAuthUI = NO; - - [self ensureUI]; - }]; - - [self ensureUI]; -} - -- (void)ensureUI -{ - [self updateUIWithState:ScreenLockUIStateScreenLock isLogoAtTop:NO animated:NO]; -} - -- (void)showScreenLockFailureAlertWithMessage:(NSString *)message -{ - OWSAssertIsOnMainThread(); - - [OWSAlerts showAlertWithTitle:NSLocalizedString(@"SCREEN_LOCK_UNLOCK_FAILED", - @"Title for alert indicating that screen lock could not be unlocked.") - message:message - buttonTitle:nil - buttonAction:^(UIAlertAction *action) { - // After the alert, update the UI. - [self ensureUI]; - } - fromViewController:self]; -} - -- (void)dismissPressed:(id)sender -{ - OWSLogDebug(@"tapped dismiss share button"); - - [self cancelShareExperience]; -} - -- (void)cancelShareExperience -{ - [self.shareViewDelegate shareViewWasCancelled]; -} - -#pragma mark - ScreenLockViewDelegate - -- (void)unlockButtonWasTapped -{ - OWSAssertIsOnMainThread(); - - OWSLogInfo(@"unlockButtonWasTapped"); - - [self tryToPresentAuthUIToUnlockScreenLock]; -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/SessionShareExtension/SAEScreenLockViewController.swift b/SessionShareExtension/SAEScreenLockViewController.swift new file mode 100644 index 000000000..dd8bd11d4 --- /dev/null +++ b/SessionShareExtension/SAEScreenLockViewController.swift @@ -0,0 +1,206 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import UIKit +import PromiseKit +import SignalCoreKit +import SignalUtilitiesKit +import SessionUIKit +import SessionUtilitiesKit + +final class SAEScreenLockViewController: ScreenLockViewController, ScreenLockViewDelegate { + private var hasShownAuthUIOnce: Bool = false + private var isShowingAuthUI: Bool = false + + private weak var shareViewDelegate: ShareViewDelegate? + + // MARK: - Initialization + + init(shareViewDelegate: ShareViewDelegate) { + super.init(nibName: nil, bundle: nil) + + self.shareViewDelegate = shareViewDelegate + self.delegate = self + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + OWSLogger.verbose("Dealloc: \(type(of: self))") + } + + // MARK: - UI + + private lazy var gradientBackground: CAGradientLayer = { + let layer: CAGradientLayer = CAGradientLayer() + + let gradientStartColor: UIColor = (LKAppModeUtilities.isLightMode ? + UIColor(rgbHex: 0xFCFCFC) : + UIColor(rgbHex: 0x171717) + ) + let gradientEndColor: UIColor = (LKAppModeUtilities.isLightMode ? + UIColor(rgbHex: 0xFFFFFF) : + UIColor(rgbHex: 0x121212) + ) + layer.colors = [gradientStartColor.cgColor, gradientEndColor.cgColor] + + return layer + }() + + private lazy var titleLabel: UILabel = { + let titleLabel: UILabel = UILabel() + titleLabel.font = UIFont.boldSystemFont(ofSize: Values.veryLargeFontSize) + titleLabel.text = "vc_share_title".localized() + titleLabel.textColor = Colors.text + + return titleLabel + }() + + private lazy var closeButton: UIBarButtonItem = { + let closeButton: UIBarButtonItem = UIBarButtonItem(image: UIImage(named: "X"), style: .plain, target: self, action: #selector(dismissPressed)) + closeButton.tintColor = Colors.text + + return closeButton + }() + + // MARK: - Lifecycle + + override func loadView() { + super.loadView() + + UIView.appearance().tintColor = Colors.text + + self.view.backgroundColor = UIColor.clear + self.view.layer.insertSublayer(gradientBackground, at: 0) + + self.navigationController?.navigationBar.setBackgroundImage(UIImage(), for: .default) + self.navigationController?.navigationBar.shadowImage = UIImage() + self.navigationController?.navigationBar.isTranslucent = false + self.navigationController?.navigationBar.tintColor = Colors.navigationBarBackground + + self.navigationItem.titleView = titleLabel + self.navigationItem.leftBarButtonItem = closeButton + + setupLayout() + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + self.ensureUI() + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + self.ensureUI() + + // Auto-show the auth UI f + if !hasShownAuthUIOnce { + hasShownAuthUIOnce = true + + self.tryToPresentAuthUIToUnlockScreenLock() + } + } + + // MARK: - Layout + + private func setupLayout() { + gradientBackground.frame = UIScreen.main.bounds + } + + // MARK: - Functions + + private func tryToPresentAuthUIToUnlockScreenLock() { + AssertIsOnMainThread() + + // If we're already showing the auth UI; abort. + if self.isShowingAuthUI { return } + + OWSLogger.info("try to unlock screen lock") + + isShowingAuthUI = true + + OWSScreenLock.shared.tryToUnlockScreenLock( + success: { [weak self] in + AssertIsOnMainThread() + OWSLogger.info("unlock screen lock succeeded.") + + self?.isShowingAuthUI = false + self?.shareViewDelegate?.shareViewWasUnlocked() + }, + failure: { [weak self] error in + AssertIsOnMainThread() + OWSLogger.info("unlock screen lock failed.") + + self?.isShowingAuthUI = false + self?.ensureUI() + self?.showScreenLockFailureAlert(message: error.localizedDescription) + }, + unexpectedFailure: { [weak self] error in + AssertIsOnMainThread() + OWSLogger.info("unlock screen lock unexpectedly failed.") + + self?.isShowingAuthUI = false + + // Local Authentication isn't working properly. + // This isn't covered by the docs or the forums but in practice + // it appears to be effective to retry again after waiting a bit. + DispatchQueue.main.async { + self?.ensureUI() + } + }, + cancel: { [weak self] in + AssertIsOnMainThread() + OWSLogger.info("unlock screen lock cancelled.") + + self?.isShowingAuthUI = false + self?.ensureUI() + } + ) + + self.ensureUI() + } + + private func ensureUI() { + self.updateUI(with: .screenLock, isLogoAtTop: false, animated: false) + } + + private func showScreenLockFailureAlert(message: String) { + AssertIsOnMainThread() + + OWSAlerts.showAlert( + // Title for alert indicating that screen lock could not be unlocked. + title: "SCREEN_LOCK_UNLOCK_FAILED".localized(), + message: message, + buttonTitle: nil, + buttonAction: { [weak self] action in + // After the alert, update the UI + self?.ensureUI() + }, + fromViewController: self + ) + } + + // MARK: - Transitions + + @objc private func dismissPressed() { + OWSLogger.debug("unlock screen lock cancelled.") + + self.cancelShareExperience() + } + + private func cancelShareExperience() { + self.shareViewDelegate?.shareViewWasCancelled() + } + + // MARK: - ScreenLockViewDelegate + + func unlockButtonWasTapped() { + AssertIsOnMainThread() + OWSLogger.info("unlockButtonWasTapped") + + self.tryToPresentAuthUIToUnlockScreenLock() + } +} diff --git a/SessionShareExtension/ShareAppExtensionContext.h b/SessionShareExtension/ShareAppExtensionContext.h deleted file mode 100644 index 8e53d724d..000000000 --- a/SessionShareExtension/ShareAppExtensionContext.h +++ /dev/null @@ -1,18 +0,0 @@ -// -// Copyright (c) 2017 Open Whisper Systems. All rights reserved. -// - -#import - -NS_ASSUME_NONNULL_BEGIN - -// This is _NOT_ a singleton and will be instantiated each time that the SAE is used. -@interface ShareAppExtensionContext : NSObject - -- (instancetype)init NS_UNAVAILABLE; - -- (instancetype)initWithRootViewController:(UIViewController *)rootViewController; - -@end - -NS_ASSUME_NONNULL_END diff --git a/SessionShareExtension/ShareAppExtensionContext.m b/SessionShareExtension/ShareAppExtensionContext.m deleted file mode 100644 index 52e3621df..000000000 --- a/SessionShareExtension/ShareAppExtensionContext.m +++ /dev/null @@ -1,240 +0,0 @@ -// -// Copyright (c) 2019 Open Whisper Systems. All rights reserved. -// - -#import "ShareAppExtensionContext.h" -#import -#import -#import -#import -#import - -NS_ASSUME_NONNULL_BEGIN - -@interface ShareAppExtensionContext () - -@property (nonatomic) UIViewController *rootViewController; - -@property (atomic) UIApplicationState reportedApplicationState; - -@end - -#pragma mark - - -@implementation ShareAppExtensionContext - -@synthesize mainWindow = _mainWindow; -@synthesize appLaunchTime = _appLaunchTime; - -- (instancetype)initWithRootViewController:(UIViewController *)rootViewController -{ - self = [super init]; - - if (!self) { - return self; - } - - OWSAssertDebug(rootViewController); - - _rootViewController = rootViewController; - - self.reportedApplicationState = UIApplicationStateActive; - - _appLaunchTime = [NSDate new]; - - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(extensionHostDidBecomeActive:) - name:NSExtensionHostDidBecomeActiveNotification - object:nil]; - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(extensionHostWillResignActive:) - name:NSExtensionHostWillResignActiveNotification - object:nil]; - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(extensionHostDidEnterBackground:) - name:NSExtensionHostDidEnterBackgroundNotification - object:nil]; - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(extensionHostWillEnterForeground:) - name:NSExtensionHostWillEnterForegroundNotification - object:nil]; - - return self; -} - -- (void)dealloc -{ - [[NSNotificationCenter defaultCenter] removeObserver:self]; -} - -#pragma mark - Notifications - -- (void)extensionHostDidBecomeActive:(NSNotification *)notification -{ - OWSAssertIsOnMainThread(); - - OWSLogInfo(@""); - - self.reportedApplicationState = UIApplicationStateActive; - - [NSNotificationCenter.defaultCenter postNotificationName:OWSApplicationDidBecomeActiveNotification object:nil]; -} - -- (void)extensionHostWillResignActive:(NSNotification *)notification -{ - OWSAssertIsOnMainThread(); - - self.reportedApplicationState = UIApplicationStateInactive; - - OWSLogInfo(@""); - [DDLog flushLog]; - - [NSNotificationCenter.defaultCenter postNotificationName:OWSApplicationWillResignActiveNotification object:nil]; -} - -- (void)extensionHostDidEnterBackground:(NSNotification *)notification -{ - OWSAssertIsOnMainThread(); - - OWSLogInfo(@""); - [DDLog flushLog]; - - self.reportedApplicationState = UIApplicationStateBackground; - - [NSNotificationCenter.defaultCenter postNotificationName:OWSApplicationDidEnterBackgroundNotification object:nil]; -} - -- (void)extensionHostWillEnterForeground:(NSNotification *)notification -{ - OWSAssertIsOnMainThread(); - - OWSLogInfo(@""); - - self.reportedApplicationState = UIApplicationStateInactive; - - [NSNotificationCenter.defaultCenter postNotificationName:OWSApplicationWillEnterForegroundNotification object:nil]; -} - -#pragma mark - - -- (BOOL)isMainApp -{ - return NO; -} - -- (BOOL)isMainAppAndActive -{ - return NO; -} - -- (BOOL)isRTL -{ - static BOOL isRTL = NO; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - // Borrowed from PureLayout's AppExtension compatible RTL support. - // App Extensions may not access -[UIApplication sharedApplication]; fall back to checking the bundle's - // preferred localization character direction - isRTL = [NSLocale characterDirectionForLanguage:[[NSBundle mainBundle] preferredLocalizations][0]] - == NSLocaleLanguageDirectionRightToLeft; - }); - return isRTL; -} - -- (void)setStatusBarHidden:(BOOL)isHidden animated:(BOOL)isAnimated -{ - OWSLogInfo(@"Ignoring request to show/hide status bar since we're in an app extension"); -} - -- (CGFloat)statusBarHeight -{ - return 20; -} - -- (BOOL)isInBackground -{ - return self.reportedApplicationState == UIApplicationStateBackground; -} - -- (BOOL)isAppForegroundAndActive -{ - return self.reportedApplicationState == UIApplicationStateActive; -} - -- (UIBackgroundTaskIdentifier)beginBackgroundTaskWithExpirationHandler: - (BackgroundTaskExpirationHandler)expirationHandler -{ - return UIBackgroundTaskInvalid; -} - -- (void)endBackgroundTask:(UIBackgroundTaskIdentifier)backgroundTaskIdentifier -{ - OWSAssertDebug(backgroundTaskIdentifier == UIBackgroundTaskInvalid); -} - -- (void)ensureSleepBlocking:(BOOL)shouldBeBlocking blockingObjects:(NSArray *)blockingObjects -{ - OWSLogDebug(@"Ignoring request to block sleep."); -} - -- (void)setMainAppBadgeNumber:(NSInteger)value -{ - OWSFailDebug(@""); -} - -- (nullable UIViewController *)frontmostViewController -{ - OWSAssertDebug(self.rootViewController); - - return [self.rootViewController findFrontmostViewController:YES]; -} - -- (nullable UIAlertAction *)openSystemSettingsAction -{ - return nil; -} - -- (BOOL)isRunningTests -{ - // We don't need to distinguish this in the SAE. - return NO; -} - -- (void)setNetworkActivityIndicatorVisible:(BOOL)value -{ - OWSFailDebug(@""); -} - -- (void)runNowOrWhenMainAppIsActive:(AppActiveBlock)block -{ - OWSFailDebug(@"cannot run main app active blocks in share extension."); -} - -- (id)keychainStorage -{ - return [SSKDefaultKeychainStorage shared]; -} - -- (NSString *)appDocumentDirectoryPath -{ - NSFileManager *fileManager = [NSFileManager defaultManager]; - NSURL *documentDirectoryURL = - [[fileManager URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject]; - return [documentDirectoryURL path]; -} - -- (NSString *)appSharedDataDirectoryPath -{ - NSURL *groupContainerDirectoryURL = - [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:SignalApplicationGroup]; - return [groupContainerDirectoryURL path]; -} - -- (NSUserDefaults *)appUserDefaults -{ - return [[NSUserDefaults alloc] initWithSuiteName:SignalApplicationGroup]; -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/SessionShareExtension/ShareAppExtensionContext.swift b/SessionShareExtension/ShareAppExtensionContext.swift new file mode 100644 index 000000000..51c80bd95 --- /dev/null +++ b/SessionShareExtension/ShareAppExtensionContext.swift @@ -0,0 +1,204 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import UIKit +import SignalUtilitiesKit +import SessionUtilitiesKit +import SessionMessagingKit + +/// This is _NOT_ a singleton and will be instantiated each time that the SAE is used. +final class ShareAppExtensionContext: NSObject, AppContext { + var rootViewController: UIViewController + var reportedApplicationState: UIApplication.State + + let appLaunchTime = Date() + let isMainApp = false + let isMainAppAndActive = false + + var mainWindow: UIWindow? + var wasWokenUpByPushNotification: Bool = false + + private static var _isRTL: Bool = { + // Borrowed from PureLayout's AppExtension compatible RTL support. + // App Extensions may not access -[UIApplication sharedApplication]; fall back + // to checking the bundle's preferred localization character direction + return ( + Locale.characterDirection( + forLanguage: (Bundle.main.preferredLocalizations.first ?? "") + ) == Locale.LanguageDirection.rightToLeft + ) + }() + + var isRTL: Bool { return ShareAppExtensionContext._isRTL } + var isRunningTests: Bool { return false } // We don't need to distinguish this in the SAE + + var statusBarHeight: CGFloat { return 20 } + var openSystemSettingsAction: UIAlertAction? + + // MARK: - Initialization + + init(rootViewController: UIViewController) { + self.rootViewController = rootViewController + self.reportedApplicationState = .active + + super.init() + + NotificationCenter.default.addObserver( + self, + selector: #selector(extensionHostDidBecomeActive(notification:)), + name: .NSExtensionHostDidBecomeActive, + object: nil + ) + NotificationCenter.default.addObserver( + self, + selector: #selector(extensionHostWillResignActive(notification:)), + name: .NSExtensionHostWillResignActive, + object: nil + ) + NotificationCenter.default.addObserver( + self, + selector: #selector(extensionHostDidEnterBackground(notification:)), + name: .NSExtensionHostDidEnterBackground, + object: nil + ) + NotificationCenter.default.addObserver( + self, + selector: #selector(extensionHostWillEnterForeground(notification:)), + name: .NSExtensionHostWillEnterForeground, + object: nil + ) + } + + deinit { + NotificationCenter.default.removeObserver(self) + } + + // MARK: - Notifications + + @objc private func extensionHostDidBecomeActive(notification: NSNotification) { + AssertIsOnMainThread() + OWSLogger.info("") + + self.reportedApplicationState = .active + + NotificationCenter.default.post( + name: .OWSApplicationDidBecomeActive, + object: nil + ) + } + + @objc private func extensionHostWillResignActive(notification: NSNotification) { + AssertIsOnMainThread() + + self.reportedApplicationState = .inactive + + OWSLogger.info("") + DDLog.flushLog() + + NotificationCenter.default.post( + name: .OWSApplicationWillResignActive, + object: nil + ) + } + + @objc private func extensionHostDidEnterBackground(notification: NSNotification) { + AssertIsOnMainThread() + + OWSLogger.info("") + DDLog.flushLog() + + self.reportedApplicationState = .background + + NotificationCenter.default.post( + name: .OWSApplicationDidEnterBackground, + object: nil + ) + } + + @objc private func extensionHostWillEnterForeground(notification: NSNotification) { + AssertIsOnMainThread() + + OWSLogger.info("") + + self.reportedApplicationState = .inactive + + NotificationCenter.default.post( + name: .OWSApplicationWillEnterForeground, + object: nil + ) + } + + // MARK: - AppContext Functions + + func isAppForegroundAndActive() -> Bool { + return (reportedApplicationState == .active) + } + + func isInBackground() -> Bool { + return (reportedApplicationState == .background) + } + + func frontmostViewController() -> UIViewController? { + return rootViewController.findFrontmostViewController(true) + } + + func keychainStorage() -> SSKKeychainStorage { + return SSKDefaultKeychainStorage.shared + } + + func appDocumentDirectoryPath() -> String { + let targetPath: String? = FileManager.default + .urls( + for: .documentDirectory, + in: .userDomainMask + ) + .last? + .path + owsAssertDebug(targetPath != nil) + + return (targetPath ?? "") + } + + func appSharedDataDirectoryPath() -> String { + let targetPath: String? = FileManager.default + .containerURL(forSecurityApplicationGroupIdentifier: SignalApplicationGroup)? + .path + owsAssertDebug(targetPath != nil) + + return (targetPath ?? "") + } + + func appUserDefaults() -> UserDefaults { + let targetUserDefaults: UserDefaults? = UserDefaults(suiteName: SignalApplicationGroup) + owsAssertDebug(targetUserDefaults != nil) + + return (targetUserDefaults ?? UserDefaults.standard) + } + + func setStatusBarHidden(_ isHidden: Bool, animated isAnimated: Bool) { + OWSLogger.info("Ignoring request to show/hide status bar since we're in an app extension") + } + + func beginBackgroundTask(expirationHandler: @escaping BackgroundTaskExpirationHandler) -> UIBackgroundTaskIdentifier { + return .invalid + } + + func endBackgroundTask(_ backgroundTaskIdentifier: UIBackgroundTaskIdentifier) { + owsAssertDebug(backgroundTaskIdentifier == .invalid) + } + + func ensureSleepBlocking(_ shouldBeBlocking: Bool, blockingObjects: [Any]) { + OWSLogger.debug("Ignoring request to block sleep.") + } + + func setMainAppBadgeNumber(_ value: Int) { + owsFailDebug("") + } + + func setNetworkActivityIndicatorVisible(_ value: Bool) { + owsFailDebug("") + } + + func runNowOr(whenMainAppIsActive block: @escaping AppActiveBlock) { + owsFailDebug("cannot run main app active blocks in share extension.") + } +} diff --git a/SessionShareExtension/ShareVC.swift b/SessionShareExtension/ShareVC.swift index 6de48c8d9..749b2f0c7 100644 --- a/SessionShareExtension/ShareVC.swift +++ b/SessionShareExtension/ShareVC.swift @@ -1,12 +1,14 @@ import CoreServices import PromiseKit +import SignalUtilitiesKit import SessionUIKit final class ShareVC : UINavigationController, ShareViewDelegate, AppModeManagerDelegate { private var areVersionMigrationsComplete = false public static var attachmentPrepPromise: Promise<[SignalAttachment]>? - // MARK: Error + // MARK: - Error + enum ShareViewControllerError: Error { case assertionError(description: String) case unsupportedMedia @@ -14,7 +16,8 @@ final class ShareVC : UINavigationController, ShareViewDelegate, AppModeManagerD case obsoleteShare } - // MARK: Lifecycle + // MARK: - Lifecycle + override func loadView() { super.loadView() @@ -39,28 +42,35 @@ final class ShareVC : UINavigationController, ShareViewDelegate, AppModeManagerD return } - AppSetup.setupEnvironment(appSpecificSingletonBlock: { - SSKEnvironment.shared.notificationsManager = NoopNotificationsManager() - }, migrationCompletion: { [weak self] in - AssertIsOnMainThread() - - guard let strongSelf = self else { return } + AppSetup.setupEnvironment( + appSpecificSingletonBlock: { + SSKEnvironment.shared.notificationsManager = NoopNotificationsManager() + }, + migrationCompletion: { [weak self] in + AssertIsOnMainThread() + + self?.versionMigrationsDidComplete() - // performUpdateCheck must be invoked after Environment has been initialized because - // upgrade process may depend on Environment. - strongSelf.versionMigrationsDidComplete() - }) + // performUpdateCheck must be invoked after Environment has been initialized because + // upgrade process may depend on Environment. + self?.versionMigrationsDidComplete() + } + ) // We don't need to use "screen protection" in the SAE. - NotificationCenter.default.addObserver(self, - selector: #selector(storageIsReady), - name: .StorageIsReady, - object: nil) - NotificationCenter.default.addObserver(self, - selector: #selector(applicationDidEnterBackground), - name: .OWSApplicationDidEnterBackground, - object: nil) + NotificationCenter.default.addObserver( + self, + selector: #selector(storageIsReady), + name: .StorageIsReady, + object: nil + ) + NotificationCenter.default.addObserver( + self, + selector: #selector(applicationDidEnterBackground), + name: .OWSApplicationDidEnterBackground, + object: nil + ) } @objc @@ -88,12 +98,8 @@ final class ShareVC : UINavigationController, ShareViewDelegate, AppModeManagerD AssertIsOnMainThread() // App isn't ready until storage is ready AND all version migrations are complete. - guard areVersionMigrationsComplete else { - return - } - guard OWSStorage.isStorageReady() else { - return - } + guard areVersionMigrationsComplete else { return } + guard OWSStorage.isStorageReady() else { return } guard !AppReadiness.isAppReady() else { // Only mark the app as ready once. return @@ -108,9 +114,7 @@ final class ShareVC : UINavigationController, ShareViewDelegate, AppModeManagerD AppReadiness.setAppIsReady() // We don't need to use messageFetcherJob in the SAE. - // We don't need to use SyncPushTokensJob in the SAE. - // We don't need to use DeviceSleepManager in the SAE. AppVersion.sharedInstance().saeLaunchDidComplete() @@ -119,9 +123,7 @@ final class ShareVC : UINavigationController, ShareViewDelegate, AppModeManagerD // We don't need to use OWSMessageReceiver in the SAE. // We don't need to use OWSBatchMessageProcessor in the SAE. - // We don't need to use OWSOrphanDataCleaner in the SAE. - // We don't need to fetch the local profile in the SAE OWSReadReceiptManager.shared().prepareCachedValues() @@ -129,10 +131,10 @@ final class ShareVC : UINavigationController, ShareViewDelegate, AppModeManagerD override func viewDidLoad() { super.viewDidLoad() + AppReadiness.runNowOrWhenAppDidBecomeReady { [weak self] in AssertIsOnMainThread() - guard let strongSelf = self else { return } - strongSelf.showLockScreenOrMainContent() + self?.showLockScreenOrMainContent() } } @@ -143,11 +145,9 @@ final class ShareVC : UINavigationController, ShareViewDelegate, AppModeManagerD Logger.info("") if OWSScreenLock.shared.isScreenLockEnabled() { - self.dismiss(animated: false) { [weak self] in AssertIsOnMainThread() - guard let strongSelf = self else { return } - strongSelf.extensionContext!.completeRequest(returningItems: [], completionHandler: nil) + self?.extensionContext?.completeRequest(returningItems: [], completionHandler: nil) } } } @@ -161,12 +161,15 @@ final class ShareVC : UINavigationController, ShareViewDelegate, AppModeManagerD ExitShareExtension() } - // MARK: App Mode + // MARK: - App Mode + public func getCurrentAppMode() -> AppMode { guard let window = self.view.window else { return .light } + let userInterfaceStyle = window.traitCollection.userInterfaceStyle let isLightMode = (userInterfaceStyle == .light || userInterfaceStyle == .unspecified) - return isLightMode ? .light : .dark + + return (isLightMode ? .light : .dark) } public func setCurrentAppMode(to appMode: AppMode) { @@ -181,7 +184,8 @@ final class ShareVC : UINavigationController, ShareViewDelegate, AppModeManagerD private func showLockScreenOrMainContent() { if OWSScreenLock.shared.isScreenLockEnabled() { showLockScreen() - } else { + } + else { showMainContent() } } @@ -192,16 +196,23 @@ final class ShareVC : UINavigationController, ShareViewDelegate, AppModeManagerD } private func showMainContent() { - let threadPickerVC = ThreadPickerVC() + let threadPickerVC: ThreadPickerVC = ThreadPickerVC() threadPickerVC.shareVC = self + setViewControllers([ threadPickerVC ], animated: false) + let promise = buildAttachments() - ModalActivityIndicatorViewController.present(fromViewController: self, canCancel: false, message: NSLocalizedString("vc_share_loading_message", comment: "")) { activityIndicator in - promise.done { _ in - activityIndicator.dismiss { } - }.catch { _ in - activityIndicator.dismiss { } - } + ModalActivityIndicatorViewController.present( + fromViewController: self, + canCancel: false, + message: "vc_share_loading_message".localized()) { activityIndicator in + promise + .done { _ in + activityIndicator.dismiss { } + } + .catch { _ in + activityIndicator.dismiss { } + } } ShareVC.attachmentPrepPromise = promise } @@ -220,7 +231,7 @@ final class ShareVC : UINavigationController, ShareViewDelegate, AppModeManagerD func shareViewFailed(error: Error) { let alert = UIAlertController(title: "Session", message: error.localizedDescription, preferredStyle: .alert) - alert.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .default, handler: { _ in + alert.addAction(UIAlertAction(title: "OK".localized(), style: .default, handler: { _ in self.extensionContext!.cancelRequest(withError: error) })) present(alert, animated: true, completion: nil) @@ -236,22 +247,29 @@ final class ShareVC : UINavigationController, ShareViewDelegate, AppModeManagerD guard let firstUtiType = itemProvider.registeredTypeIdentifiers.first else { return false } - return firstUtiType == utiType + + return (firstUtiType == utiType) } private class func isVisualMediaItem(itemProvider: NSItemProvider) -> Bool { - return (itemProvider.hasItemConformingToTypeIdentifier(kUTTypeImage as String) || - itemProvider.hasItemConformingToTypeIdentifier(kUTTypeMovie as String)) + return ( + itemProvider.hasItemConformingToTypeIdentifier(kUTTypeImage as String) || + itemProvider.hasItemConformingToTypeIdentifier(kUTTypeMovie as String) + ) } private class func isUrlItem(itemProvider: NSItemProvider) -> Bool { - return itemMatchesSpecificUtiType(itemProvider: itemProvider, - utiType: kUTTypeURL as String) + return itemMatchesSpecificUtiType( + itemProvider: itemProvider, + utiType: kUTTypeURL as String + ) } private class func isContactItem(itemProvider: NSItemProvider) -> Bool { - return itemMatchesSpecificUtiType(itemProvider: itemProvider, - utiType: kUTTypeContact as String) + return itemMatchesSpecificUtiType( + itemProvider: itemProvider, + utiType: kUTTypeContact as String + ) } private class func utiType(itemProvider: NSItemProvider) -> String? { @@ -259,7 +277,8 @@ final class ShareVC : UINavigationController, ShareViewDelegate, AppModeManagerD if isUrlItem(itemProvider: itemProvider) { return kUTTypeURL as String - } else if isContactItem(itemProvider: itemProvider) { + } + else if isContactItem(itemProvider: itemProvider) { return kUTTypeContact as String } @@ -278,43 +297,43 @@ final class ShareVC : UINavigationController, ShareViewDelegate, AppModeManagerD // and send them as normal text messages if possible. let urlString = url.absoluteString return DataSourceValue.dataSource(withOversizeText: urlString) - } else if UTTypeConformsTo(utiType as CFString, kUTTypeText) { + } + else if UTTypeConformsTo(utiType as CFString, kUTTypeText) { // Share text as oversize text messages. // // NOTE: SharingThreadPickerViewController will try to unpack them // and send them as normal text messages if possible. - return DataSourcePath.dataSource(with: url, - shouldDeleteOnDeallocation: false) - } else { - guard let dataSource = DataSourcePath.dataSource(with: url, - shouldDeleteOnDeallocation: false) else { - return nil - } - - if let customFileName = customFileName { - dataSource.sourceFilename = customFileName - } else { - // Ignore the filename for URLs. - dataSource.sourceFilename = url.lastPathComponent - } - return dataSource + return DataSourcePath.dataSource( + with: url, + shouldDeleteOnDeallocation: false + ) } + + guard let dataSource = DataSourcePath.dataSource(with: url, shouldDeleteOnDeallocation: false) else { + return nil + } + + // Fallback to the last part of the URL + dataSource.sourceFilename = (customFileName ?? url.lastPathComponent) + + return dataSource } private class func preferredItemProviders(inputItem: NSExtensionItem) -> [NSItemProvider]? { - guard let attachments = inputItem.attachments else { - return nil - } + guard let attachments = inputItem.attachments else { return nil } var visualMediaItemProviders = [NSItemProvider]() var hasNonVisualMedia = false + for attachment in attachments { if isVisualMediaItem(itemProvider: attachment) { visualMediaItemProviders.append(attachment) - } else { + } + else { hasNonVisualMedia = true } } + // Only allow multiple-attachment sends if all attachments // are visual media. if visualMediaItemProviders.count > 0 && !hasNonVisualMedia { @@ -334,6 +353,7 @@ final class ShareVC : UINavigationController, ShareViewDelegate, AppModeManagerD guard let itemProvider = attachment as? NSItemProvider else { return false } + return isUrlItem(itemProvider: itemProvider) }) { return [preferredAttachment] @@ -342,9 +362,11 @@ final class ShareVC : UINavigationController, ShareViewDelegate, AppModeManagerD // else return whatever is available if let itemProvider = inputItem.attachments?.first { return [itemProvider] - } else { + } + else { owsFailDebug("Missing attachment.") } + return [] } @@ -359,6 +381,7 @@ final class ShareVC : UINavigationController, ShareViewDelegate, AppModeManagerD Logger.error("invalid inputItem \(inputItemRaw)") continue } + if let itemProviders = ShareVC.preferredItemProviders(inputItem: inputItem) { return Promise.value(itemProviders) } @@ -366,6 +389,8 @@ final class ShareVC : UINavigationController, ShareViewDelegate, AppModeManagerD let error = ShareViewControllerError.assertionError(description: "no input item") return Promise(error: error) } + + // MARK: - LoadedItem private struct LoadedItem { diff --git a/SessionShareExtension/SimplifiedConversationCell.swift b/SessionShareExtension/SimplifiedConversationCell.swift index b03404392..b23f02b43 100644 --- a/SessionShareExtension/SimplifiedConversationCell.swift +++ b/SessionShareExtension/SimplifiedConversationCell.swift @@ -4,71 +4,102 @@ import SessionUIKit final class SimplifiedConversationCell : UITableViewCell { var threadViewModel: ThreadViewModel! { didSet { update() } } - static let reuseIdentifier = "SimplifiedConversationCell" + // MARK: - Initialization + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + setUpViewHierarchy() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + setUpViewHierarchy() + } + + // MARK: - UI + + private lazy var stackView: UIStackView = { + let stackView: UIStackView = UIStackView() + stackView.translatesAutoresizingMaskIntoConstraints = false + stackView.axis = .horizontal + stackView.alignment = .center + stackView.spacing = Values.mediumSpacing + + return stackView + }() - // MARK: UI Components private lazy var accentLineView: UIView = { let result = UIView() + result.translatesAutoresizingMaskIntoConstraints = false result.backgroundColor = Colors.destructive + return result }() - private lazy var profilePictureView = ProfilePictureView() + private lazy var profilePictureView: ProfilePictureView = { + let view: ProfilePictureView = ProfilePictureView() + view.translatesAutoresizingMaskIntoConstraints = false + + return view + }() private lazy var displayNameLabel: UILabel = { let result = UILabel() result.font = .boldSystemFont(ofSize: Values.mediumFontSize) result.textColor = Colors.text result.lineBreakMode = .byTruncatingTail + return result }() - // MARK: Initialization - override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - setUpViewHierarchy() - } - - required init?(coder: NSCoder) { - super.init(coder: coder) - setUpViewHierarchy() - } + // MARK: - Initialization private func setUpViewHierarchy() { - // Background color backgroundColor = Colors.cellBackground - // Highlight color + let selectedBackgroundView = UIView() selectedBackgroundView.backgroundColor = Colors.cellSelected self.selectedBackgroundView = selectedBackgroundView - // Accent line view + + addSubview(stackView) + + stackView.addArrangedSubview(accentLineView) + stackView.addArrangedSubview(profilePictureView) + stackView.addArrangedSubview(displayNameLabel) + stackView.addArrangedSubview(UIView.hSpacer(0)) + + setupLayout() + } + + // MARK: - Layout + + private func setupLayout() { accentLineView.set(.width, to: Values.accentLineThickness) accentLineView.set(.height, to: 68) - // Profile picture view + let profilePictureViewSize = Values.mediumProfilePictureSize profilePictureView.set(.width, to: profilePictureViewSize) profilePictureView.set(.height, to: profilePictureViewSize) profilePictureView.size = profilePictureViewSize - // Main stack view - let stackView = UIStackView(arrangedSubviews: [ accentLineView, profilePictureView, displayNameLabel, UIView.hSpacer(0) ]) - stackView.axis = .horizontal - stackView.alignment = .center - stackView.spacing = Values.mediumSpacing - addSubview(stackView) + stackView.pin(to: self) } - // MARK: Updating + // MARK: - Content + private func update() { AssertIsOnMainThread() + guard let thread = threadViewModel?.threadRecord else { return } + let isBlocked: Bool if let thread = thread as? TSContactThread { isBlocked = SSKEnvironment.shared.blockingManager.isRecipientIdBlocked(thread.contactSessionID()) } else { isBlocked = false } - accentLineView.alpha = isBlocked ? 1 : 0 + + accentLineView.alpha = (isBlocked ? 1 : 0) profilePictureView.update(for: thread) displayNameLabel.text = getDisplayName() } @@ -76,17 +107,25 @@ final class SimplifiedConversationCell : UITableViewCell { private func getDisplayName() -> String { if threadViewModel.isGroupThread { if threadViewModel.name.isEmpty { + // TODO: Localization return "Unknown Group" - } else { - return threadViewModel.name - } - } else { - if threadViewModel.threadRecord.isNoteToSelf() { - return NSLocalizedString("NOTE_TO_SELF", comment: "") - } else { - let hexEncodedPublicKey = threadViewModel.contactSessionID! - return Storage.shared.getContact(with: hexEncodedPublicKey)?.displayName(for: .regular) ?? hexEncodedPublicKey } + + return threadViewModel.name + } + + if threadViewModel.threadRecord.isNoteToSelf() { + return "NOTE_TO_SELF".localized() + } + + guard let hexEncodedPublicKey: String = threadViewModel.contactSessionID else { + // TODO: Localization + return "Unknown" } + + return ( + Storage.shared.getContact(with: hexEncodedPublicKey)?.displayName(for: .regular) ?? + hexEncodedPublicKey + ) } } diff --git a/SessionShareExtension/ThreadPickerVC.swift b/SessionShareExtension/ThreadPickerVC.swift index 75355d23a..414036bad 100644 --- a/SessionShareExtension/ThreadPickerVC.swift +++ b/SessionShareExtension/ThreadPickerVC.swift @@ -1,8 +1,12 @@ +import UIKit +import SignalUtilitiesKit import SessionUIKit +import SessionMessagingKit +import SessionUtilitiesKit -final class ThreadPickerVC : UIViewController, UITableViewDataSource, UITableViewDelegate, AttachmentApprovalViewControllerDelegate { +final class ThreadPickerVC: UIViewController, UITableViewDataSource, UITableViewDelegate, AttachmentApprovalViewControllerDelegate { private var threads: YapDatabaseViewMappings! - private var threadViewModelCache: [String:ThreadViewModel] = [:] // Thread ID to ThreadViewModel + private var threadViewModelCache: [String: ThreadViewModel] = [:] // Thread ID to ThreadViewModel private var selectedThread: TSThread? var shareVC: ShareVC? @@ -15,32 +19,50 @@ final class ThreadPickerVC : UIViewController, UITableViewDataSource, UITableVie result.objectCacheLimit = 500 return result }() + + // MARK: - UI + + private lazy var titleLabel: UILabel = { + let titleLabel: UILabel = UILabel() + titleLabel.text = "vc_share_title".localized() + titleLabel.textColor = Colors.text + titleLabel.font = .boldSystemFont(ofSize: Values.veryLargeFontSize) + + return titleLabel + }() private lazy var tableView: UITableView = { - let result = UITableView() - result.backgroundColor = .clear - result.separatorStyle = .none - result.register(SimplifiedConversationCell.self, forCellReuseIdentifier: SimplifiedConversationCell.reuseIdentifier) - result.showsVerticalScrollIndicator = false - return result + let tableView: UITableView = UITableView() + tableView.backgroundColor = .clear + tableView.separatorStyle = .none + tableView.register(view: SimplifiedConversationCell.self) + tableView.showsVerticalScrollIndicator = false + tableView.dataSource = self + tableView.delegate = self + + return tableView }() private lazy var fadeView: UIView = { - let result = UIView() + let view = UIView() let gradient = Gradients.homeVCFade - result.setGradient(gradient) - result.isUserInteractionEnabled = false - return result + view.setGradient(gradient) + view.isUserInteractionEnabled = false + + return view }() - // MARK: Lifecycle + // MARK: - Lifecycle + override func viewDidLoad() { super.viewDidLoad() + setupNavBar() + // Gradient view.backgroundColor = .clear - let gradient = Gradients.defaultBackground - view.setGradient(gradient) + view.setGradient(Gradients.defaultBackground) + // Threads dbConnection.beginLongLivedReadTransaction() // Freeze the connection for use on the main thread (this gives us a stable data source that doesn't change until we tell it to) threads = YapDatabaseViewMappings(groups: [ TSInboxGroup ], view: TSThreadDatabaseViewExtensionName) // The extension should be registered at this point @@ -48,23 +70,16 @@ final class ThreadPickerVC : UIViewController, UITableViewDataSource, UITableVie dbConnection.read { transaction in self.threads.update(with: transaction) // Perform the initial update } + // Title - let titleLabel = UILabel() - titleLabel.text = NSLocalizedString("vc_share_title", comment: "") - titleLabel.textColor = Colors.text - titleLabel.font = .boldSystemFont(ofSize: Values.veryLargeFontSize) navigationItem.titleView = titleLabel + // Table view - tableView.dataSource = self - tableView.delegate = self + view.addSubview(tableView) - tableView.pin(to: view) view.addSubview(fadeView) - fadeView.pin(.leading, to: .leading, of: view) - let topInset = 0.15 * view.height() - fadeView.pin(.top, to: .top, of: view, withInset: topInset) - fadeView.pin(.trailing, to: .trailing, of: view) - fadeView.pin(.bottom, to: .bottom, of: view) + + setupLayout() // Reload reload() } @@ -80,18 +95,32 @@ final class ThreadPickerVC : UIViewController, UITableViewDataSource, UITableVie } } + // MARK: Layout + + private func setupLayout() { + let topInset = 0.15 * view.height() + + tableView.pin(to: view) + fadeView.pin(.leading, to: .leading, of: view) + fadeView.pin(.top, to: .top, of: view, withInset: topInset) + fadeView.pin(.trailing, to: .trailing, of: view) + fadeView.pin(.bottom, to: .bottom, of: view) + } + // MARK: Table View Data Source func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return Int(threadCount) } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let cell = tableView.dequeueReusableCell(withIdentifier: SimplifiedConversationCell.reuseIdentifier) as! SimplifiedConversationCell + let cell: SimplifiedConversationCell = tableView.dequeue(type: SimplifiedConversationCell.self, for: indexPath) cell.threadViewModel = threadViewModel(at: indexPath.row) + return cell } - // MARK: Updating + // MARK: - Updating + private func reload() { AssertIsOnMainThread() dbConnection.beginLongLivedReadTransaction() // Jump to the latest commit @@ -102,11 +131,17 @@ final class ThreadPickerVC : UIViewController, UITableViewDataSource, UITableVie tableView.reloadData() } - // MARK: Interaction + // MARK: - Interaction + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - guard let thread = self.thread(at: indexPath.row), let attachments = ShareVC.attachmentPrepPromise?.value else { return } - self.selectedThread = thread tableView.deselectRow(at: indexPath, animated: true) + + guard let thread = self.thread(at: indexPath.row), let attachments = ShareVC.attachmentPrepPromise?.value else { + return + } + + self.selectedThread = thread + let approvalVC = AttachmentApprovalViewController.wrappedInNavController(attachments: attachments, approvalDelegate: self) navigationController!.present(approvalVC, animated: true, completion: nil) } @@ -115,21 +150,24 @@ final class ThreadPickerVC : UIViewController, UITableViewDataSource, UITableVie let message = VisibleMessage() message.sentTimestamp = NSDate.millisecondTimestamp() message.text = messageText + let tsMessage = TSOutgoingMessage.from(message, associatedWith: selectedThread!) Storage.write { transaction in tsMessage.save(with: transaction) } + shareVC!.dismiss(animated: true, completion: nil) - ModalActivityIndicatorViewController.present(fromViewController: shareVC!, canCancel: false, message: NSLocalizedString("vc_share_sending_message", comment: "")) { activityIndicator in - MessageSender.sendNonDurably(message, with: attachments, in: self.selectedThread!).done { [weak self] _ in - guard let self = self else { return } - activityIndicator.dismiss { } - self.shareVC!.shareViewWasCompleted() - }.catch { [weak self] error in - guard let self = self else { return } - activityIndicator.dismiss { } - self.shareVC!.shareViewFailed(error: error) - } + + ModalActivityIndicatorViewController.present(fromViewController: shareVC!, canCancel: false, message: "vc_share_sending_message".localized()) { activityIndicator in + MessageSender.sendNonDurably(message, with: attachments, in: self.selectedThread!) + .done { [weak self] _ in + activityIndicator.dismiss { } + self?.shareVC?.shareViewWasCompleted() + } + .catch { [weak self] error in + activityIndicator.dismiss { } + self?.shareVC?.shareViewFailed(error: error) + } } } @@ -141,7 +179,8 @@ final class ThreadPickerVC : UIViewController, UITableViewDataSource, UITableVie // Do nothing } - // MARK: Convenience + // MARK: - Convenience + private func thread(at index: Int) -> TSThread? { var thread: TSThread? = nil dbConnection.read { transaction in @@ -153,9 +192,11 @@ final class ThreadPickerVC : UIViewController, UITableViewDataSource, UITableVie private func threadViewModel(at index: Int) -> ThreadViewModel? { guard let thread = thread(at: index) else { return nil } + if let cachedThreadViewModel = threadViewModelCache[thread.uniqueId!] { return cachedThreadViewModel - } else { + } + else { var threadViewModel: ThreadViewModel? = nil dbConnection.read { transaction in threadViewModel = ThreadViewModel(thread: thread, transaction: transaction) diff --git a/SessionUtilitiesKit/General/ReusableView.swift b/SessionUtilitiesKit/General/ReusableView.swift new file mode 100644 index 000000000..4a33f2e65 --- /dev/null +++ b/SessionUtilitiesKit/General/ReusableView.swift @@ -0,0 +1,16 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import UIKit + +public protocol ReusableView: AnyObject { + static var defaultReuseIdentifier: String { get } +} + +public extension ReusableView where Self: UIView { + static var defaultReuseIdentifier: String { + return String(describing: self.self) + } +} + +extension UITableViewCell: ReusableView {} +extension UITableViewHeaderFooterView: ReusableView {} diff --git a/SessionUtilitiesKit/General/String+Localization.swift b/SessionUtilitiesKit/General/String+Localization.swift new file mode 100644 index 000000000..2468d1d25 --- /dev/null +++ b/SessionUtilitiesKit/General/String+Localization.swift @@ -0,0 +1,13 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import SignalCoreKit + +public extension String { + func localized() -> String { + // If the localized string matches the key provided then the localisation failed + let localizedString = NSLocalizedString(self, comment: "") + owsAssertDebug(localizedString != self, "Key \"\(self)\" is not set in Localizable.strings") + + return localizedString + } +} diff --git a/SessionUtilitiesKit/General/UITableView+ReusableView.swift b/SessionUtilitiesKit/General/UITableView+ReusableView.swift new file mode 100644 index 000000000..725faa6b4 --- /dev/null +++ b/SessionUtilitiesKit/General/UITableView+ReusableView.swift @@ -0,0 +1,23 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import UIKit + +public extension UITableView { + func register(view: View.Type) where View: UITableViewCell { + register(view.self, forCellReuseIdentifier: view.defaultReuseIdentifier) + } + + func registerHeaderFooterView(view: View.Type) where View: UITableViewHeaderFooterView { + register(view.self, forHeaderFooterViewReuseIdentifier: view.defaultReuseIdentifier) + } + + func dequeue(type: T.Type, for indexPath: IndexPath) -> T where T: UITableViewCell { + let reuseIdentifier = T.defaultReuseIdentifier + return dequeueReusableCell(withIdentifier: reuseIdentifier, for: indexPath) as! T + } + + func dequeueHeaderFooterView(type: T.Type) -> T where T: UITableViewHeaderFooterView { + let reuseIdentifier = T.defaultReuseIdentifier + return dequeueReusableHeaderFooterView(withIdentifier: reuseIdentifier) as! T + } +} diff --git a/SignalUtilitiesKit/Utilities/ShareViewDelegate.swift b/SignalUtilitiesKit/Utilities/ShareViewDelegate.swift index 6be9361d8..887dabcda 100644 --- a/SignalUtilitiesKit/Utilities/ShareViewDelegate.swift +++ b/SignalUtilitiesKit/Utilities/ShareViewDelegate.swift @@ -5,7 +5,7 @@ import Foundation // All Observer methods will be invoked from the main thread. @objc -public protocol ShareViewDelegate: class { +public protocol ShareViewDelegate: AnyObject { func shareViewWasUnlocked() func shareViewWasCompleted() func shareViewWasCancelled()