From 8751a876415fa03343a7d868e6443a6aaced2d01 Mon Sep 17 00:00:00 2001 From: nielsandriesse Date: Wed, 5 May 2021 15:49:03 +1000 Subject: [PATCH 01/13] Prep for new share extension --- Session.xcodeproj/project.pbxproj | 18 +- .../Base.lproj/MainInterface.storyboard | 15 +- .../SAEFailedViewController.swift | 2 +- .../SAELoadViewController.swift | 2 +- .../ShareViewController.swift | 16 +- .../SAEScreenLockViewController.m | 7 +- SessionShareExtension/ShareVC.swift | 172 ++++++++++++++++++ 7 files changed, 208 insertions(+), 24 deletions(-) rename SessionShareExtension/{ => Deprecated}/SAEFailedViewController.swift (98%) rename SessionShareExtension/{ => Deprecated}/SAELoadViewController.swift (98%) rename SessionShareExtension/{ => Deprecated}/ShareViewController.swift (98%) create mode 100644 SessionShareExtension/ShareVC.swift diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 8ebd3a890..3c80e5db9 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -689,6 +689,7 @@ C3AABDDF2553ECF00042FF4C /* Array+Description.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5D12553860800C340D1 /* Array+Description.swift */; }; C3AAFFE825AE975D0089E6DD /* ConfigurationMessage+Convenience.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3AAFFDE25AE96FF0089E6DD /* ConfigurationMessage+Convenience.swift */; }; C3AAFFF225AE99710089E6DD /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3AAFFF125AE99710089E6DD /* AppDelegate.swift */; }; + C3ADC66126426688005F1414 /* ShareVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3ADC66026426688005F1414 /* ShareVC.swift */; }; C3BBE0762554CDA60050F1E3 /* Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3BBE0752554CDA60050F1E3 /* Configuration.swift */; }; C3BBE0802554CDD70050F1E3 /* Storage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3BBE07F2554CDD70050F1E3 /* Storage.swift */; }; C3BBE0A72554D4DE0050F1E3 /* Promise+Retrying.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5D62553860B00C340D1 /* Promise+Retrying.swift */; }; @@ -1690,6 +1691,7 @@ C3AAFFCB25AE92150089E6DD /* OpenGroupManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenGroupManager.swift; sourceTree = ""; }; C3AAFFDE25AE96FF0089E6DD /* ConfigurationMessage+Convenience.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ConfigurationMessage+Convenience.swift"; sourceTree = ""; }; C3AAFFF125AE99710089E6DD /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + C3ADC66026426688005F1414 /* ShareVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareVC.swift; sourceTree = ""; }; C3AECBEA24EF5244005743DE /* fa */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fa; path = fa.lproj/Localizable.strings; sourceTree = ""; }; C3B7845C25649DA600ADB2E7 /* TSIncomingMessage+Conversion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TSIncomingMessage+Conversion.swift"; sourceTree = ""; }; C3BBE0752554CDA60050F1E3 /* Configuration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Configuration.swift; sourceTree = ""; }; @@ -1981,14 +1983,13 @@ isa = PBXGroup; children = ( C31C21A4255BCA4800EC2D66 /* Meta */, + C3ADC65F264265D9005F1414 /* Deprecated */, 4535186C1FC635DD00210559 /* MainInterface.storyboard */, - 347850561FD86544007B8332 /* SAEFailedViewController.swift */, - 3461284A1FD0B93F00532771 /* SAELoadViewController.swift */, 34641E1D2088DA6C00E2EDE5 /* SAEScreenLockViewController.h */, 34641E1E2088DA6D00E2EDE5 /* SAEScreenLockViewController.m */, - 4535186A1FC635DD00210559 /* ShareViewController.swift */, 34480B341FD0929200BC14EF /* ShareAppExtensionContext.h */, 34480B351FD0929200BC14EF /* ShareAppExtensionContext.m */, + C3ADC66026426688005F1414 /* ShareVC.swift */, ); path = SessionShareExtension; sourceTree = ""; @@ -3154,6 +3155,16 @@ path = "File Server"; sourceTree = ""; }; + C3ADC65F264265D9005F1414 /* Deprecated */ = { + isa = PBXGroup; + children = ( + 347850561FD86544007B8332 /* SAEFailedViewController.swift */, + 3461284A1FD0B93F00532771 /* SAELoadViewController.swift */, + 4535186A1FC635DD00210559 /* ShareViewController.swift */, + ); + path = Deprecated; + sourceTree = ""; + }; C3BBE0B32554F0D30050F1E3 /* Utilities */ = { isa = PBXGroup; children = ( @@ -4407,6 +4418,7 @@ files = ( 4535186B1FC635DD00210559 /* ShareViewController.swift in Sources */, 34480B361FD0929200BC14EF /* ShareAppExtensionContext.m in Sources */, + C3ADC66126426688005F1414 /* ShareVC.swift in Sources */, 34641E1F2088DA6D00E2EDE5 /* SAEScreenLockViewController.m in Sources */, 3461284B1FD0B94000532771 /* SAELoadViewController.swift in Sources */, 347850571FD86544007B8332 /* SAEFailedViewController.swift in Sources */, diff --git a/SessionShareExtension/Base.lproj/MainInterface.storyboard b/SessionShareExtension/Base.lproj/MainInterface.storyboard index 2e59abc17..5182a5f6f 100644 --- a/SessionShareExtension/Base.lproj/MainInterface.storyboard +++ b/SessionShareExtension/Base.lproj/MainInterface.storyboard @@ -1,28 +1,27 @@ - - - - + + - + - + - + - + + diff --git a/SessionShareExtension/SAEFailedViewController.swift b/SessionShareExtension/Deprecated/SAEFailedViewController.swift similarity index 98% rename from SessionShareExtension/SAEFailedViewController.swift rename to SessionShareExtension/Deprecated/SAEFailedViewController.swift index 27f5be14d..767a65d0c 100644 --- a/SessionShareExtension/SAEFailedViewController.swift +++ b/SessionShareExtension/Deprecated/SAEFailedViewController.swift @@ -10,7 +10,7 @@ protocol SAEFailedViewDelegate: class { func shareViewWasCancelled() } -class SAEFailedViewController: UIViewController { +class SAEFailedViewControllerOld: UIViewController { weak var delegate: SAEFailedViewDelegate? diff --git a/SessionShareExtension/SAELoadViewController.swift b/SessionShareExtension/Deprecated/SAELoadViewController.swift similarity index 98% rename from SessionShareExtension/SAELoadViewController.swift rename to SessionShareExtension/Deprecated/SAELoadViewController.swift index c86abbeb2..6102c9b7b 100644 --- a/SessionShareExtension/SAELoadViewController.swift +++ b/SessionShareExtension/Deprecated/SAELoadViewController.swift @@ -5,7 +5,7 @@ import UIKit import PureLayout -class SAELoadViewController: UIViewController { +class SAELoadViewControllerOld: UIViewController { weak var delegate: ShareViewDelegate? diff --git a/SessionShareExtension/ShareViewController.swift b/SessionShareExtension/Deprecated/ShareViewController.swift similarity index 98% rename from SessionShareExtension/ShareViewController.swift rename to SessionShareExtension/Deprecated/ShareViewController.swift index 5491e73dd..09d353f44 100644 --- a/SessionShareExtension/ShareViewController.swift +++ b/SessionShareExtension/Deprecated/ShareViewController.swift @@ -10,7 +10,7 @@ import SessionUIKit import CoreServices @objc -public class ShareViewController: UIViewController, ShareViewDelegate, SAEFailedViewDelegate, AppModeManagerDelegate { +public class ShareViewControllerOld: UIViewController, ShareViewDelegate, SAEFailedViewDelegate, AppModeManagerDelegate { // MARK: - Dependencies @@ -32,7 +32,7 @@ public class ShareViewController: UIViewController, ShareViewDelegate, SAEFailed private var areVersionMigrationsComplete = false private var progressPoller: ProgressPoller? - var loadViewController: SAELoadViewController? + var loadViewController: SAELoadViewControllerOld? private var shareViewNavigationController: OWSNavigationController? @@ -96,7 +96,7 @@ public class ShareViewController: UIViewController, ShareViewDelegate, SAEFailed let shareViewNavigationController = OWSNavigationController() self.shareViewNavigationController = shareViewNavigationController - let loadViewController = SAELoadViewController(delegate: self) + let loadViewController = SAELoadViewControllerOld(delegate: self) self.loadViewController = loadViewController // Don't display load screen immediately, in hopes that we can avoid it altogether. @@ -393,7 +393,7 @@ public class ShareViewController: UIViewController, ShareViewDelegate, SAEFailed private func showErrorView(title: String, message: String) { AssertIsOnMainThread() - let viewController = SAEFailedViewController(delegate: self, title: title, message: message) + let viewController = SAEFailedViewControllerOld(delegate: self, title: title, message: message) self.showPrimaryViewController(viewController) } @@ -466,7 +466,7 @@ public class ShareViewController: UIViewController, ShareViewDelegate, SAEFailed // If no view has been presented yet, do nothing. return } - if let _ = firstViewController as? SAEFailedViewController { + if let _ = firstViewController as? SAEFailedViewControllerOld { // If root view is an error view, do nothing. return } @@ -708,7 +708,7 @@ public class ShareViewController: UIViewController, ShareViewDelegate, SAEFailed Logger.error("invalid inputItem \(inputItemRaw)") continue } - if let itemProviders = ShareViewController.preferredItemProviders(inputItem: inputItem) { + if let itemProviders = ShareViewControllerOld.preferredItemProviders(inputItem: inputItem) { return Promise.value(itemProviders) } } @@ -753,7 +753,7 @@ public class ShareViewController: UIViewController, ShareViewDelegate, SAEFailed // * UTIs aren't very descriptive (there are far more MIME types than UTI types) // so in the case of file attachments we try to refine the attachment type // using the file extension. - guard let srcUtiType = ShareViewController.utiType(itemProvider: itemProvider) else { + guard let srcUtiType = ShareViewControllerOld.utiType(itemProvider: itemProvider) else { let error = ShareViewControllerError.unsupportedMedia return Promise(error: error) } @@ -880,7 +880,7 @@ public class ShareViewController: UIViewController, ShareViewDelegate, SAEFailed Logger.debug("building DataSource with url: \(url), utiType: \(utiType)") - guard let dataSource = ShareViewController.createDataSource(utiType: utiType, url: url, customFileName: loadedItem.customFileName) else { + guard let dataSource = ShareViewControllerOld.createDataSource(utiType: utiType, url: url, customFileName: loadedItem.customFileName) else { let error = ShareViewControllerError.assertionError(description: "Unable to read attachment data") return Promise(error: error) } diff --git a/SessionShareExtension/SAEScreenLockViewController.m b/SessionShareExtension/SAEScreenLockViewController.m index 70d5e6696..06e8f553b 100644 --- a/SessionShareExtension/SAEScreenLockViewController.m +++ b/SessionShareExtension/SAEScreenLockViewController.m @@ -43,7 +43,7 @@ NS_ASSUME_NONNULL_BEGIN UIView.appearance.tintColor = LKColors.text; - // Loki: Set gradient background + // Gradient background self.view.backgroundColor = UIColor.clearColor; CAGradientLayer *layer = [CAGradientLayer new]; layer.frame = UIScreen.mainScreen.bounds; @@ -52,20 +52,21 @@ NS_ASSUME_NONNULL_BEGIN layer.colors = @[ (id)gradientStartColor.CGColor, (id)gradientEndColor.CGColor ]; [self.view.layer insertSublayer:layer atIndex:0]; - // Loki: Set navigation bar background color + // 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; - // Loki: Customize title + // Title UILabel *titleLabel = [UILabel new]; titleLabel.text = NSLocalizedString(@"Share to Session", @""); 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; diff --git a/SessionShareExtension/ShareVC.swift b/SessionShareExtension/ShareVC.swift new file mode 100644 index 000000000..3454146be --- /dev/null +++ b/SessionShareExtension/ShareVC.swift @@ -0,0 +1,172 @@ +import SessionUIKit + +final class ShareVC : UIViewController, AppModeManagerDelegate { + private var areVersionMigrationsComplete = false + + // MARK: Lifecycle + override func loadView() { + super.loadView() + + // This should be the first thing we do. + let appContext = ShareAppExtensionContext(rootViewController: self) + SetCurrentAppContext(appContext) + + AppModeManager.configure(delegate: self) + + DebugLogger.shared().enableTTYLogging() + if _isDebugAssertConfiguration() { + DebugLogger.shared().enableFileLogging() + } else if OWSPreferences.isLoggingEnabled() { + DebugLogger.shared().enableFileLogging() + } + + Logger.info("") + + _ = AppVersion.sharedInstance() + + Cryptography.seedRandom() + + // We don't need to use DeviceSleepManager in the SAE. + + // We don't need to use applySignalAppearence in the SAE. + + if CurrentAppContext().isRunningTests { + // TODO: Do we need to implement isRunningTests in the SAE context? + return + } + + AppSetup.setupEnvironment(appSpecificSingletonBlock: { + SSKEnvironment.shared.notificationsManager = NoopNotificationsManager() + }, migrationCompletion: { [weak self] in + AssertIsOnMainThread() + + guard let strongSelf = self else { return } + + // performUpdateCheck must be invoked after Environment has been initialized because + // upgrade process may depend on Environment. + strongSelf.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) + } + + @objc + func versionMigrationsDidComplete() { + AssertIsOnMainThread() + + Logger.debug("") + + areVersionMigrationsComplete = true + + checkIsAppReady() + } + + @objc + func storageIsReady() { + AssertIsOnMainThread() + + Logger.debug("") + + checkIsAppReady() + } + + @objc + func checkIsAppReady() { + 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 !AppReadiness.isAppReady() else { + // Only mark the app as ready once. + return + } + + SignalUtilitiesKit.Configuration.performMainSetup() + + Logger.debug("") + + // TODO: Once "app ready" logic is moved into AppSetup, move this line there. + OWSProfileManager.shared().ensureLocalProfileCached() + + // Note that this does much more than set a flag; + // it will also run all deferred blocks. + 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() + + setUpViewHierarchy() + + // We don't need to use OWSMessageReceiver in the SAE. + // We don't need to use OWSBatchMessageProcessor in the SAE. + + OWSProfileManager.shared().ensureLocalProfileCached() + + // 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() + } + + private func setUpViewHierarchy() { + + } + + @objc + public func applicationDidEnterBackground() { + AssertIsOnMainThread() + + 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) + } + } + } + + deinit { + NotificationCenter.default.removeObserver(self) + + // Share extensions reside in a process that may be reused between usages. + // That isn't safe; the codebase is full of statics (e.g. singletons) which + // we can't easily clean up. + ExitShareExtension() + } + + // 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 + } + + public func setCurrentAppMode(to appMode: AppMode) { + return // Not applicable to share extensions + } +} From 69a9e2c76f0b23cbe534ca1e2fa20f35e409bfc4 Mon Sep 17 00:00:00 2001 From: Niels Andriesse Date: Thu, 6 May 2021 09:43:02 +1000 Subject: [PATCH 02/13] Set up share extension launch screen --- .../Base.lproj/MainInterface.storyboard | 21 ++++++++++++++++++- SessionShareExtension/ShareVC.swift | 1 + 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/SessionShareExtension/Base.lproj/MainInterface.storyboard b/SessionShareExtension/Base.lproj/MainInterface.storyboard index 5182a5f6f..f88a45417 100644 --- a/SessionShareExtension/Base.lproj/MainInterface.storyboard +++ b/SessionShareExtension/Base.lproj/MainInterface.storyboard @@ -15,13 +15,32 @@ + + + + + + + + + - + + + + + + + + + + + diff --git a/SessionShareExtension/ShareVC.swift b/SessionShareExtension/ShareVC.swift index 3454146be..4e9963c6c 100644 --- a/SessionShareExtension/ShareVC.swift +++ b/SessionShareExtension/ShareVC.swift @@ -1,6 +1,7 @@ import SessionUIKit final class ShareVC : UIViewController, AppModeManagerDelegate { + @IBOutlet private var logoImageView: UIImageView! private var areVersionMigrationsComplete = false // MARK: Lifecycle From 4e487dd368cb77dfd17e277b302beeffd559e101 Mon Sep 17 00:00:00 2001 From: Niels Andriesse Date: Thu, 6 May 2021 10:02:05 +1000 Subject: [PATCH 03/13] Display all threads --- Session.xcodeproj/project.pbxproj | 4 + SessionShareExtension/ShareVC.swift | 88 +++++++++++++++++- .../SimplifiedConversationCell.swift | 92 +++++++++++++++++++ 3 files changed, 180 insertions(+), 4 deletions(-) create mode 100644 SessionShareExtension/SimplifiedConversationCell.swift diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 3c80e5db9..226ce3aef 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -165,6 +165,7 @@ B8041A9525C8FA1D003C2166 /* MediaLoaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8041A9425C8FA1D003C2166 /* MediaLoaderView.swift */; }; B8041AA725C90927003C2166 /* TypingIndicatorCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8041AA625C90927003C2166 /* TypingIndicatorCell.swift */; }; B80A579F23DFF1F300876683 /* NewClosedGroupVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = B80A579E23DFF1F300876683 /* NewClosedGroupVC.swift */; }; + B817AD9A26436593009DF825 /* SimplifiedConversationCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B817AD9926436593009DF825 /* SimplifiedConversationCell.swift */; }; B81D25C426157F40004D1FE1 /* storage-seed-3.crt in Resources */ = {isa = PBXBuildFile; fileRef = B81D25B926157F20004D1FE1 /* storage-seed-3.crt */; }; B81D25C526157F40004D1FE1 /* storage-seed-1.crt in Resources */ = {isa = PBXBuildFile; fileRef = B81D25B726157F20004D1FE1 /* storage-seed-1.crt */; }; B81D25C626157F40004D1FE1 /* public-loki-foundation.crt in Resources */ = {isa = PBXBuildFile; fileRef = B81D25B826157F20004D1FE1 /* public-loki-foundation.crt */; }; @@ -1157,6 +1158,7 @@ B8041A9425C8FA1D003C2166 /* MediaLoaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaLoaderView.swift; sourceTree = ""; }; B8041AA625C90927003C2166 /* TypingIndicatorCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypingIndicatorCell.swift; sourceTree = ""; }; B80A579E23DFF1F300876683 /* NewClosedGroupVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewClosedGroupVC.swift; sourceTree = ""; }; + B817AD9926436593009DF825 /* SimplifiedConversationCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimplifiedConversationCell.swift; sourceTree = ""; }; B81D25B726157F20004D1FE1 /* storage-seed-1.crt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "storage-seed-1.crt"; sourceTree = ""; }; B81D25B826157F20004D1FE1 /* public-loki-foundation.crt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "public-loki-foundation.crt"; sourceTree = ""; }; B81D25B926157F20004D1FE1 /* storage-seed-3.crt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "storage-seed-3.crt"; sourceTree = ""; }; @@ -1990,6 +1992,7 @@ 34480B341FD0929200BC14EF /* ShareAppExtensionContext.h */, 34480B351FD0929200BC14EF /* ShareAppExtensionContext.m */, C3ADC66026426688005F1414 /* ShareVC.swift */, + B817AD9926436593009DF825 /* SimplifiedConversationCell.swift */, ); path = SessionShareExtension; sourceTree = ""; @@ -4416,6 +4419,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + B817AD9A26436593009DF825 /* SimplifiedConversationCell.swift in Sources */, 4535186B1FC635DD00210559 /* ShareViewController.swift in Sources */, 34480B361FD0929200BC14EF /* ShareAppExtensionContext.m in Sources */, C3ADC66126426688005F1414 /* ShareVC.swift in Sources */, diff --git a/SessionShareExtension/ShareVC.swift b/SessionShareExtension/ShareVC.swift index 4e9963c6c..2b7591b1a 100644 --- a/SessionShareExtension/ShareVC.swift +++ b/SessionShareExtension/ShareVC.swift @@ -1,9 +1,30 @@ import SessionUIKit -final class ShareVC : UIViewController, AppModeManagerDelegate { +final class ShareVC : UIViewController, UITableViewDataSource, AppModeManagerDelegate { @IBOutlet private var logoImageView: UIImageView! private var areVersionMigrationsComplete = false - + private var threads: YapDatabaseViewMappings! + private var threadViewModelCache: [String:ThreadViewModel] = [:] // Thread ID to ThreadViewModel + + private var threadCount: UInt { + threads.numberOfItems(inGroup: TSInboxGroup) + } + + private lazy var dbConnection: YapDatabaseConnection = { + let result = OWSPrimaryStorage.shared().newDatabaseConnection() + result.objectCacheLimit = 500 + return result + }() + + 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 + }() + // MARK: Lifecycle override func loadView() { super.loadView() @@ -130,7 +151,21 @@ final class ShareVC : UIViewController, AppModeManagerDelegate { } private func setUpViewHierarchy() { - + // 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 + threads.setIsReversed(true, forGroup: TSInboxGroup) + dbConnection.read { transaction in + self.threads.update(with: transaction) // Perform the initial update + } + // Logo + logoImageView.alpha = 0 + // Table view + tableView.dataSource = self + view.addSubview(tableView) + tableView.pin(to: view) + // Reload + reload() } @objc @@ -159,7 +194,6 @@ final class ShareVC : UIViewController, AppModeManagerDelegate { } // MARK: App Mode - public func getCurrentAppMode() -> AppMode { guard let window = self.view.window else { return .light } let userInterfaceStyle = window.traitCollection.userInterfaceStyle @@ -170,4 +204,50 @@ final class ShareVC : UIViewController, AppModeManagerDelegate { public func setCurrentAppMode(to appMode: AppMode) { return // Not applicable to share extensions } + + // 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 + cell.threadViewModel = threadViewModel(at: indexPath.row) + return cell + } + + // MARK: Updating + private func reload() { + AssertIsOnMainThread() + dbConnection.beginLongLivedReadTransaction() // Jump to the latest commit + dbConnection.read { transaction in + self.threads.update(with: transaction) + } + threadViewModelCache.removeAll() + tableView.reloadData() + } + + // MARK: Convenience + private func thread(at index: Int) -> TSThread? { + var thread: TSThread? = nil + dbConnection.read { transaction in + let ext = transaction.ext(TSThreadDatabaseViewExtensionName) as! YapDatabaseViewTransaction + thread = ext.object(atRow: UInt(index), inSection: 0, with: self.threads) as! TSThread? + } + return thread + } + + 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 { + var threadViewModel: ThreadViewModel? = nil + dbConnection.read { transaction in + threadViewModel = ThreadViewModel(thread: thread, transaction: transaction) + } + threadViewModelCache[thread.uniqueId!] = threadViewModel + return threadViewModel + } + } } diff --git a/SessionShareExtension/SimplifiedConversationCell.swift b/SessionShareExtension/SimplifiedConversationCell.swift new file mode 100644 index 000000000..8b45d3fc2 --- /dev/null +++ b/SessionShareExtension/SimplifiedConversationCell.swift @@ -0,0 +1,92 @@ +import UIKit +import SessionUIKit + +final class SimplifiedConversationCell : UITableViewCell { + var threadViewModel: ThreadViewModel! { didSet { update() } } + + static let reuseIdentifier = "SimplifiedConversationCell" + + // MARK: UI Components + private lazy var accentLineView: UIView = { + let result = UIView() + result.backgroundColor = Colors.destructive + return result + }() + + private lazy var profilePictureView = ProfilePictureView() + + 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() + } + + private func setUpViewHierarchy() { + // Background color + backgroundColor = Colors.cellBackground + // Highlight color + let selectedBackgroundView = UIView() + selectedBackgroundView.backgroundColor = Colors.cellSelected + self.selectedBackgroundView = selectedBackgroundView + // Accent line view + 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 ]) + stackView.axis = .horizontal + stackView.alignment = .center + stackView.spacing = Values.mediumSpacing + addSubview(stackView) + stackView.pin(to: self) + } + + // MARK: Updating + 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 + profilePictureView.update(for: thread) + displayNameLabel.text = getDisplayName() + } + + private func getDisplayName() -> String { + if threadViewModel.isGroupThread { + if threadViewModel.name.isEmpty { + 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 + } + } + } +} From 86af5f1d99f892f608430b554b926dbe1917aede Mon Sep 17 00:00:00 2001 From: Niels Andriesse Date: Thu, 6 May 2021 10:12:57 +1000 Subject: [PATCH 04/13] Add share extension title --- SessionShareExtension/ShareVC.swift | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/SessionShareExtension/ShareVC.swift b/SessionShareExtension/ShareVC.swift index 2b7591b1a..5e4b78d8a 100644 --- a/SessionShareExtension/ShareVC.swift +++ b/SessionShareExtension/ShareVC.swift @@ -151,6 +151,10 @@ final class ShareVC : UIViewController, UITableViewDataSource, AppModeManagerDel } private func setUpViewHierarchy() { + // Gradient + view.backgroundColor = .clear + let gradient = Gradients.defaultBackground + view.setGradient(gradient) // 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 @@ -160,10 +164,21 @@ final class ShareVC : UIViewController, UITableViewDataSource, AppModeManagerDel } // Logo logoImageView.alpha = 0 + // Fake nav bar + let fakeNavBar = UIView() + fakeNavBar.set(.height, to: 64) + view.addSubview(fakeNavBar) + fakeNavBar.pin([ UIView.HorizontalEdge.left, UIView.VerticalEdge.top, UIView.HorizontalEdge.right ], to: view) + let titleLabel = UILabel() + titleLabel.text = NSLocalizedString("share", comment: "") + titleLabel.font = .boldSystemFont(ofSize: Values.veryLargeFontSize) + fakeNavBar.addSubview(titleLabel) + titleLabel.center(in: fakeNavBar) // Table view tableView.dataSource = self view.addSubview(tableView) - tableView.pin(to: view) + tableView.pin(.top, to: .bottom, of: fakeNavBar) + tableView.pin([ UIView.HorizontalEdge.left, UIView.HorizontalEdge.right, UIView.VerticalEdge.bottom ], to: view) // Reload reload() } From 1a11476b852fe72cc178cdfdfef9649ac13bacb1 Mon Sep 17 00:00:00 2001 From: Niels Andriesse Date: Thu, 6 May 2021 10:36:58 +1000 Subject: [PATCH 05/13] Fix screen lock handling --- Session.xcodeproj/project.pbxproj | 4 + .../Translations/de.lproj/Localizable.strings | 1 + .../Translations/en.lproj/Localizable.strings | 1 + .../Translations/es.lproj/Localizable.strings | 1 + .../Translations/fa.lproj/Localizable.strings | 1 + .../Translations/fr.lproj/Localizable.strings | 1 + .../id-ID.lproj/Localizable.strings | 1 + .../Translations/it.lproj/Localizable.strings | 1 + .../Translations/ja.lproj/Localizable.strings | 1 + .../Translations/pl.lproj/Localizable.strings | 1 + .../pt_BR.lproj/Localizable.strings | 1 + .../Translations/ru.lproj/Localizable.strings | 1 + .../Translations/sk.lproj/Localizable.strings | 1 + .../vi-VN.lproj/Localizable.strings | 1 + .../zh_CN.lproj/Localizable.strings | 1 + .../Base.lproj/MainInterface.storyboard | 3 - .../SAEScreenLockViewController.m | 2 +- SessionShareExtension/ShareVC.swift | 125 +++++------------- SessionShareExtension/ThreadPickerVC.swift | 99 ++++++++++++++ 19 files changed, 152 insertions(+), 95 deletions(-) create mode 100644 SessionShareExtension/ThreadPickerVC.swift diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 226ce3aef..cdf8f3411 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -166,6 +166,7 @@ B8041AA725C90927003C2166 /* TypingIndicatorCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8041AA625C90927003C2166 /* TypingIndicatorCell.swift */; }; B80A579F23DFF1F300876683 /* NewClosedGroupVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = B80A579E23DFF1F300876683 /* NewClosedGroupVC.swift */; }; B817AD9A26436593009DF825 /* SimplifiedConversationCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B817AD9926436593009DF825 /* SimplifiedConversationCell.swift */; }; + B817AD9C26436F73009DF825 /* ThreadPickerVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = B817AD9B26436F73009DF825 /* ThreadPickerVC.swift */; }; B81D25C426157F40004D1FE1 /* storage-seed-3.crt in Resources */ = {isa = PBXBuildFile; fileRef = B81D25B926157F20004D1FE1 /* storage-seed-3.crt */; }; B81D25C526157F40004D1FE1 /* storage-seed-1.crt in Resources */ = {isa = PBXBuildFile; fileRef = B81D25B726157F20004D1FE1 /* storage-seed-1.crt */; }; B81D25C626157F40004D1FE1 /* public-loki-foundation.crt in Resources */ = {isa = PBXBuildFile; fileRef = B81D25B826157F20004D1FE1 /* public-loki-foundation.crt */; }; @@ -1159,6 +1160,7 @@ B8041AA625C90927003C2166 /* TypingIndicatorCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypingIndicatorCell.swift; sourceTree = ""; }; B80A579E23DFF1F300876683 /* NewClosedGroupVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewClosedGroupVC.swift; sourceTree = ""; }; B817AD9926436593009DF825 /* SimplifiedConversationCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimplifiedConversationCell.swift; sourceTree = ""; }; + B817AD9B26436F73009DF825 /* ThreadPickerVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadPickerVC.swift; sourceTree = ""; }; B81D25B726157F20004D1FE1 /* storage-seed-1.crt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "storage-seed-1.crt"; sourceTree = ""; }; B81D25B826157F20004D1FE1 /* public-loki-foundation.crt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "public-loki-foundation.crt"; sourceTree = ""; }; B81D25B926157F20004D1FE1 /* storage-seed-3.crt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "storage-seed-3.crt"; sourceTree = ""; }; @@ -1992,6 +1994,7 @@ 34480B341FD0929200BC14EF /* ShareAppExtensionContext.h */, 34480B351FD0929200BC14EF /* ShareAppExtensionContext.m */, C3ADC66026426688005F1414 /* ShareVC.swift */, + B817AD9B26436F73009DF825 /* ThreadPickerVC.swift */, B817AD9926436593009DF825 /* SimplifiedConversationCell.swift */, ); path = SessionShareExtension; @@ -4425,6 +4428,7 @@ C3ADC66126426688005F1414 /* ShareVC.swift in Sources */, 34641E1F2088DA6D00E2EDE5 /* SAEScreenLockViewController.m in Sources */, 3461284B1FD0B94000532771 /* SAELoadViewController.swift in Sources */, + B817AD9C26436F73009DF825 /* ThreadPickerVC.swift in Sources */, 347850571FD86544007B8332 /* SAEFailedViewController.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Session/Meta/Translations/de.lproj/Localizable.strings b/Session/Meta/Translations/de.lproj/Localizable.strings index ab8f65d23..a195d3971 100644 --- a/Session/Meta/Translations/de.lproj/Localizable.strings +++ b/Session/Meta/Translations/de.lproj/Localizable.strings @@ -489,3 +489,4 @@ "vc_qr_code_view_scan_qr_code_tab_title" = "QR-Code scannen"; "vc_qr_code_view_scan_qr_code_explanation" = "Scannen Sie den QR-Code einer Person, um ein Gespräch mit ihr zu beginnen."; "vc_view_my_qr_code_explanation" = "Das ist Ihr QR-Code. Andere Benutzer können ihn scannen, um eine Session mit Ihnen zu starten."; +"vc_share_title" = "Share to Session"; diff --git a/Session/Meta/Translations/en.lproj/Localizable.strings b/Session/Meta/Translations/en.lproj/Localizable.strings index b24891931..d5b2cb8ac 100644 --- a/Session/Meta/Translations/en.lproj/Localizable.strings +++ b/Session/Meta/Translations/en.lproj/Localizable.strings @@ -520,3 +520,4 @@ "modal_link_previews_title" = "Enable Link Previews?"; "modal_link_previews_explanation" = "Enabling link previews will show previews for URLs you send and receive. This can be useful, but Session will need to contact linked websites to generate previews. You can always disable link previews in Session's settings."; "modal_link_previews_button_title" = "Enable"; +"vc_share_title" = "Share to Session"; diff --git a/Session/Meta/Translations/es.lproj/Localizable.strings b/Session/Meta/Translations/es.lproj/Localizable.strings index cd73fb1c7..2748ca8a1 100644 --- a/Session/Meta/Translations/es.lproj/Localizable.strings +++ b/Session/Meta/Translations/es.lproj/Localizable.strings @@ -489,3 +489,4 @@ "vc_qr_code_view_scan_qr_code_tab_title" = "Escanear código QR"; "vc_qr_code_view_scan_qr_code_explanation" = "Escanea el código QR de una persona para comenzar una conversación con ella"; "vc_view_my_qr_code_explanation" = "Este es tu código QR. Otros usuarios pueden escanearlo para empezar una Session contigo."; +"vc_share_title" = "Share to Session"; diff --git a/Session/Meta/Translations/fa.lproj/Localizable.strings b/Session/Meta/Translations/fa.lproj/Localizable.strings index eb3084dd2..3e69b9d44 100644 --- a/Session/Meta/Translations/fa.lproj/Localizable.strings +++ b/Session/Meta/Translations/fa.lproj/Localizable.strings @@ -489,3 +489,4 @@ "vc_qr_code_view_scan_qr_code_tab_title" = "اسکن کد QR"; "vc_qr_code_view_scan_qr_code_explanation" = "برای شروع مکالمه با دیگران، کد QR شخصی را اسکن کنید"; "vc_view_my_qr_code_explanation" = "این کد QR شماست. سایر کاربران می‌توانند برای شروع Session با شما آن را اسکن کنند."; +"vc_share_title" = "Share to Session"; diff --git a/Session/Meta/Translations/fr.lproj/Localizable.strings b/Session/Meta/Translations/fr.lproj/Localizable.strings index ef56450e5..2ec2d983d 100644 --- a/Session/Meta/Translations/fr.lproj/Localizable.strings +++ b/Session/Meta/Translations/fr.lproj/Localizable.strings @@ -489,3 +489,4 @@ "vc_qr_code_view_scan_qr_code_tab_title" = "Scanner le code QR"; "vc_qr_code_view_scan_qr_code_explanation" = "Scannez le code QR d'un autre utilisateur pour démarrer une session"; "vc_view_my_qr_code_explanation" = "Ceci est votre code QR. Les autres utilisateurs peuvent le scanner pour démarrer une session avec vous."; +"vc_share_title" = "Share to Session"; diff --git a/Session/Meta/Translations/id-ID.lproj/Localizable.strings b/Session/Meta/Translations/id-ID.lproj/Localizable.strings index 7349629a5..bea443195 100644 --- a/Session/Meta/Translations/id-ID.lproj/Localizable.strings +++ b/Session/Meta/Translations/id-ID.lproj/Localizable.strings @@ -490,3 +490,4 @@ "vc_qr_code_view_scan_qr_code_tab_title" = "Pindai kode QR"; "vc_qr_code_view_scan_qr_code_explanation" = "Pindai kode QR pengguna lain untuk memulai percakapan"; "vc_view_my_qr_code_explanation" = "Ini adalah kode QR anda. Pengguna lain bisa memindainya untuk memulai percakapan dengan anda"; +"vc_share_title" = "Share to Session"; diff --git a/Session/Meta/Translations/it.lproj/Localizable.strings b/Session/Meta/Translations/it.lproj/Localizable.strings index 0e845962d..ec9fe9b5f 100644 --- a/Session/Meta/Translations/it.lproj/Localizable.strings +++ b/Session/Meta/Translations/it.lproj/Localizable.strings @@ -489,3 +489,4 @@ "vc_qr_code_view_scan_qr_code_tab_title" = "Scansiona il codice QR"; "vc_qr_code_view_scan_qr_code_explanation" = "Scansiona il codice QR di un utente per iniziare una conversazione con questa persona"; "vc_view_my_qr_code_explanation" = "Questo è il tuo codice QR. Altri utenti possono scansionarlo per iniziare una sessione con te."; +"vc_share_title" = "Share to Session"; diff --git a/Session/Meta/Translations/ja.lproj/Localizable.strings b/Session/Meta/Translations/ja.lproj/Localizable.strings index 033a835f6..2f8bb85a3 100644 --- a/Session/Meta/Translations/ja.lproj/Localizable.strings +++ b/Session/Meta/Translations/ja.lproj/Localizable.strings @@ -490,3 +490,4 @@ "vc_qr_code_view_scan_qr_code_tab_title" = "QR コードをスキャンする"; "vc_qr_code_view_scan_qr_code_explanation" = "誰かの QR コードをスキャンして、会話を始めましょう"; "vc_view_my_qr_code_explanation" = "これはあなたの QR コードです。他のユーザーはそれをスキャンして、あなたとの Session を開始できます。"; +"vc_share_title" = "Share to Session"; diff --git a/Session/Meta/Translations/pl.lproj/Localizable.strings b/Session/Meta/Translations/pl.lproj/Localizable.strings index af4c18f54..6a849a738 100644 --- a/Session/Meta/Translations/pl.lproj/Localizable.strings +++ b/Session/Meta/Translations/pl.lproj/Localizable.strings @@ -489,3 +489,4 @@ "vc_qr_code_view_scan_qr_code_tab_title" = "Skanowania QR code"; "vc_qr_code_view_scan_qr_code_explanation" = "Zeskanuj czyjś kod QR, aby rozpocząć z nim rozmowę"; "vc_view_my_qr_code_explanation" = "To jest twój kod QR. Inni użytkownicy mogą go zeskanować, aby rozpocząć z tobą sesję."; +"vc_share_title" = "Share to Session"; diff --git a/Session/Meta/Translations/pt_BR.lproj/Localizable.strings b/Session/Meta/Translations/pt_BR.lproj/Localizable.strings index b0bcead0f..96528b44b 100644 --- a/Session/Meta/Translations/pt_BR.lproj/Localizable.strings +++ b/Session/Meta/Translations/pt_BR.lproj/Localizable.strings @@ -489,3 +489,4 @@ "vc_qr_code_view_scan_qr_code_tab_title" = "Escanear código QR"; "vc_qr_code_view_scan_qr_code_explanation" = "Escaneie o código QR de alguém para iniciar uma conversa com essa pessoa"; "vc_view_my_qr_code_explanation" = "Este é o seu código QR. Outros usuários podem escaneá-lo para iniciar uma sessão com você."; +"vc_share_title" = "Share to Session"; diff --git a/Session/Meta/Translations/ru.lproj/Localizable.strings b/Session/Meta/Translations/ru.lproj/Localizable.strings index 0f5ba7190..9044606d5 100644 --- a/Session/Meta/Translations/ru.lproj/Localizable.strings +++ b/Session/Meta/Translations/ru.lproj/Localizable.strings @@ -520,3 +520,4 @@ "modal_link_previews_title" = "Включить предварительный просмотр ссылок?"; "modal_link_previews_explanation" = "Включение предпросмотра ссылок покажет превью для отправляемых и получаемых ссылок. Это может быть полезно, но Session нужно будет соединиться с сайтами, связанными с ссылками, чтобы сгенерировать предпросмотр. Вы всегда можете отключить предпросмотр ссылок в настройках Session."; "modal_link_previews_button_title" = "Включить"; +"vc_share_title" = "Share to Session"; diff --git a/Session/Meta/Translations/sk.lproj/Localizable.strings b/Session/Meta/Translations/sk.lproj/Localizable.strings index 03090c32b..fb8a95a89 100644 --- a/Session/Meta/Translations/sk.lproj/Localizable.strings +++ b/Session/Meta/Translations/sk.lproj/Localizable.strings @@ -520,3 +520,4 @@ "modal_link_previews_title" = "Povoliť náhľad odkazov?"; "modal_link_previews_explanation" = "Enabling link previews will show previews for URLs you send and receive. This can be useful, but Session will need to contact linked websites to generate previews. You can always disable link previews in Session's settings."; "modal_link_previews_button_title" = "Povoliť"; +"vc_share_title" = "Share to Session"; diff --git a/Session/Meta/Translations/vi-VN.lproj/Localizable.strings b/Session/Meta/Translations/vi-VN.lproj/Localizable.strings index 9a574a392..3f764bf8b 100644 --- a/Session/Meta/Translations/vi-VN.lproj/Localizable.strings +++ b/Session/Meta/Translations/vi-VN.lproj/Localizable.strings @@ -496,3 +496,4 @@ "vc_qr_code_view_scan_qr_code_tab_title" = "Quét mã QR"; "vc_qr_code_view_scan_qr_code_explanation" = "Quét mã QR của ai đó để bắt đầu trò chuyện với họ"; "vc_view_my_qr_code_explanation" = "Đây là mã QR của bạn. Những người dùng khác có thể quét mã này và bắt đầu session với bạn."; +"vc_share_title" = "Share to Session"; diff --git a/Session/Meta/Translations/zh_CN.lproj/Localizable.strings b/Session/Meta/Translations/zh_CN.lproj/Localizable.strings index 12e6a82d1..fac8be8d3 100644 --- a/Session/Meta/Translations/zh_CN.lproj/Localizable.strings +++ b/Session/Meta/Translations/zh_CN.lproj/Localizable.strings @@ -489,3 +489,4 @@ "vc_qr_code_view_scan_qr_code_tab_title" = "扫描二维码"; "vc_qr_code_view_scan_qr_code_explanation" = "扫描对方的二维码以发起对话"; "vc_view_my_qr_code_explanation" = "这是您的二维码。其他用户可以对其进行扫描以发起与您的对话。"; +"vc_share_title" = "Share to Session"; diff --git a/SessionShareExtension/Base.lproj/MainInterface.storyboard b/SessionShareExtension/Base.lproj/MainInterface.storyboard index f88a45417..b2f1bc5ef 100644 --- a/SessionShareExtension/Base.lproj/MainInterface.storyboard +++ b/SessionShareExtension/Base.lproj/MainInterface.storyboard @@ -31,9 +31,6 @@ - - - diff --git a/SessionShareExtension/SAEScreenLockViewController.m b/SessionShareExtension/SAEScreenLockViewController.m index 06e8f553b..9cae51ad6 100644 --- a/SessionShareExtension/SAEScreenLockViewController.m +++ b/SessionShareExtension/SAEScreenLockViewController.m @@ -61,7 +61,7 @@ NS_ASSUME_NONNULL_BEGIN // Title UILabel *titleLabel = [UILabel new]; - titleLabel.text = NSLocalizedString(@"Share to Session", @""); + titleLabel.text = NSLocalizedString(@"vc_share_title", @""); titleLabel.textColor = LKColors.text; titleLabel.font = [UIFont boldSystemFontOfSize:LKValues.veryLargeFontSize]; self.navigationItem.titleView = titleLabel; diff --git a/SessionShareExtension/ShareVC.swift b/SessionShareExtension/ShareVC.swift index 5e4b78d8a..939e26e75 100644 --- a/SessionShareExtension/ShareVC.swift +++ b/SessionShareExtension/ShareVC.swift @@ -1,29 +1,7 @@ import SessionUIKit -final class ShareVC : UIViewController, UITableViewDataSource, AppModeManagerDelegate { - @IBOutlet private var logoImageView: UIImageView! +final class ShareVC : UINavigationController, ShareViewDelegate, AppModeManagerDelegate { private var areVersionMigrationsComplete = false - private var threads: YapDatabaseViewMappings! - private var threadViewModelCache: [String:ThreadViewModel] = [:] // Thread ID to ThreadViewModel - - private var threadCount: UInt { - threads.numberOfItems(inGroup: TSInboxGroup) - } - - private lazy var dbConnection: YapDatabaseConnection = { - let result = OWSPrimaryStorage.shared().newDatabaseConnection() - result.objectCacheLimit = 500 - return result - }() - - 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 - }() // MARK: Lifecycle override func loadView() { @@ -136,7 +114,7 @@ final class ShareVC : UIViewController, UITableViewDataSource, AppModeManagerDel AppVersion.sharedInstance().saeLaunchDidComplete() - setUpViewHierarchy() + showLockScreenOrMainContent() // We don't need to use OWSMessageReceiver in the SAE. // We don't need to use OWSBatchMessageProcessor in the SAE. @@ -149,38 +127,14 @@ final class ShareVC : UIViewController, UITableViewDataSource, AppModeManagerDel OWSReadReceiptManager.shared().prepareCachedValues() } - - private func setUpViewHierarchy() { - // Gradient - view.backgroundColor = .clear - let gradient = Gradients.defaultBackground - view.setGradient(gradient) - // 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 - threads.setIsReversed(true, forGroup: TSInboxGroup) - dbConnection.read { transaction in - self.threads.update(with: transaction) // Perform the initial update + + override func viewDidLoad() { + super.viewDidLoad() + AppReadiness.runNowOrWhenAppDidBecomeReady { [weak self] in + AssertIsOnMainThread() + guard let strongSelf = self else { return } + strongSelf.showLockScreenOrMainContent() } - // Logo - logoImageView.alpha = 0 - // Fake nav bar - let fakeNavBar = UIView() - fakeNavBar.set(.height, to: 64) - view.addSubview(fakeNavBar) - fakeNavBar.pin([ UIView.HorizontalEdge.left, UIView.VerticalEdge.top, UIView.HorizontalEdge.right ], to: view) - let titleLabel = UILabel() - titleLabel.text = NSLocalizedString("share", comment: "") - titleLabel.font = .boldSystemFont(ofSize: Values.veryLargeFontSize) - fakeNavBar.addSubview(titleLabel) - titleLabel.center(in: fakeNavBar) - // Table view - tableView.dataSource = self - view.addSubview(tableView) - tableView.pin(.top, to: .bottom, of: fakeNavBar) - tableView.pin([ UIView.HorizontalEdge.left, UIView.HorizontalEdge.right, UIView.VerticalEdge.bottom ], to: view) - // Reload - reload() } @objc @@ -220,49 +174,38 @@ final class ShareVC : UIViewController, UITableViewDataSource, AppModeManagerDel return // Not applicable to share extensions } - // MARK: Table View Data Source - func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return Int(threadCount) + // MARK: Updating + private func showLockScreenOrMainContent() { + if OWSScreenLock.shared.isScreenLockEnabled() { + showLockScreen() + } else { + showMainContent() + } + } + + private func showLockScreen() { + let screenLockVC = SAEScreenLockViewController(shareViewDelegate: self) + setViewControllers([ screenLockVC ], animated: false) } - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let cell = tableView.dequeueReusableCell(withIdentifier: SimplifiedConversationCell.reuseIdentifier) as! SimplifiedConversationCell - cell.threadViewModel = threadViewModel(at: indexPath.row) - return cell + private func showMainContent() { + let threadPickerVC = ThreadPickerVC() + setViewControllers([ threadPickerVC ], animated: false) } - // MARK: Updating - private func reload() { - AssertIsOnMainThread() - dbConnection.beginLongLivedReadTransaction() // Jump to the latest commit - dbConnection.read { transaction in - self.threads.update(with: transaction) - } - threadViewModelCache.removeAll() - tableView.reloadData() + func shareViewWasUnlocked() { + showMainContent() } - // MARK: Convenience - private func thread(at index: Int) -> TSThread? { - var thread: TSThread? = nil - dbConnection.read { transaction in - let ext = transaction.ext(TSThreadDatabaseViewExtensionName) as! YapDatabaseViewTransaction - thread = ext.object(atRow: UInt(index), inSection: 0, with: self.threads) as! TSThread? - } - return thread + func shareViewWasCompleted() { + print("completed") } - 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 { - var threadViewModel: ThreadViewModel? = nil - dbConnection.read { transaction in - threadViewModel = ThreadViewModel(thread: thread, transaction: transaction) - } - threadViewModelCache[thread.uniqueId!] = threadViewModel - return threadViewModel - } + func shareViewWasCancelled() { + print("canceled") + } + + func shareViewFailed(error: Error) { + print("failed") } } diff --git a/SessionShareExtension/ThreadPickerVC.swift b/SessionShareExtension/ThreadPickerVC.swift new file mode 100644 index 000000000..3312c6bbb --- /dev/null +++ b/SessionShareExtension/ThreadPickerVC.swift @@ -0,0 +1,99 @@ +import SessionUIKit + +final class ThreadPickerVC : UIViewController, UITableViewDataSource { + private var threads: YapDatabaseViewMappings! + private var threadViewModelCache: [String:ThreadViewModel] = [:] // Thread ID to ThreadViewModel + + private var threadCount: UInt { + threads.numberOfItems(inGroup: TSInboxGroup) + } + + private lazy var dbConnection: YapDatabaseConnection = { + let result = OWSPrimaryStorage.shared().newDatabaseConnection() + result.objectCacheLimit = 500 + return result + }() + + 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 + }() + + // MARK: Lifecycle + override func viewDidLoad() { + super.viewDidLoad() + // Gradient + view.backgroundColor = .clear + let gradient = Gradients.defaultBackground + view.setGradient(gradient) + // 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 + threads.setIsReversed(true, forGroup: TSInboxGroup) + 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 + view.addSubview(tableView) + tableView.pin(to: view) + // Reload + reload() + } + + // 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 + cell.threadViewModel = threadViewModel(at: indexPath.row) + return cell + } + + // MARK: Updating + private func reload() { + AssertIsOnMainThread() + dbConnection.beginLongLivedReadTransaction() // Jump to the latest commit + dbConnection.read { transaction in + self.threads.update(with: transaction) + } + threadViewModelCache.removeAll() + tableView.reloadData() + } + + // MARK: Convenience + private func thread(at index: Int) -> TSThread? { + var thread: TSThread? = nil + dbConnection.read { transaction in + let ext = transaction.ext(TSThreadDatabaseViewExtensionName) as! YapDatabaseViewTransaction + thread = ext.object(atRow: UInt(index), inSection: 0, with: self.threads) as! TSThread? + } + return thread + } + + 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 { + var threadViewModel: ThreadViewModel? = nil + dbConnection.read { transaction in + threadViewModel = ThreadViewModel(thread: thread, transaction: transaction) + } + threadViewModelCache[thread.uniqueId!] = threadViewModel + return threadViewModel + } + } +} From 012daf83a577c24864202281f656655065164cc1 Mon Sep 17 00:00:00 2001 From: Niels Andriesse Date: Thu, 6 May 2021 10:45:30 +1000 Subject: [PATCH 06/13] Add back attachment prep logic --- SessionShareExtension/ShareVC.swift | 440 +++++++++++++++++++++ SessionShareExtension/ThreadPickerVC.swift | 7 + 2 files changed, 447 insertions(+) diff --git a/SessionShareExtension/ShareVC.swift b/SessionShareExtension/ShareVC.swift index 939e26e75..d8677aa44 100644 --- a/SessionShareExtension/ShareVC.swift +++ b/SessionShareExtension/ShareVC.swift @@ -1,8 +1,18 @@ +import CoreServices +import PromiseKit import SessionUIKit final class ShareVC : UINavigationController, ShareViewDelegate, AppModeManagerDelegate { private var areVersionMigrationsComplete = false + // MARK: Error + enum ShareViewControllerError: Error { + case assertionError(description: String) + case unsupportedMedia + case notRegistered + case obsoleteShare + } + // MARK: Lifecycle override func loadView() { super.loadView() @@ -208,4 +218,434 @@ final class ShareVC : UINavigationController, ShareViewDelegate, AppModeManagerD func shareViewFailed(error: Error) { print("failed") } + + // MARK: Attachment Prep + private class func itemMatchesSpecificUtiType(itemProvider: NSItemProvider, utiType: String) -> Bool { + // URLs, contacts and other special items have to be detected separately. + // Many shares (e.g. pdfs) will register many UTI types and/or conform to kUTTypeData. + guard itemProvider.registeredTypeIdentifiers.count == 1 else { + return false + } + guard let firstUtiType = itemProvider.registeredTypeIdentifiers.first else { + return false + } + return firstUtiType == utiType + } + + private class func isVisualMediaItem(itemProvider: NSItemProvider) -> Bool { + 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) + } + + private class func isContactItem(itemProvider: NSItemProvider) -> Bool { + return itemMatchesSpecificUtiType(itemProvider: itemProvider, + utiType: kUTTypeContact as String) + } + + private class func utiType(itemProvider: NSItemProvider) -> String? { + Logger.info("utiTypeForItem: \(itemProvider.registeredTypeIdentifiers)") + + if isUrlItem(itemProvider: itemProvider) { + return kUTTypeURL as String + } else if isContactItem(itemProvider: itemProvider) { + return kUTTypeContact as String + } + + // Use the first UTI that conforms to "data". + let matchingUtiType = itemProvider.registeredTypeIdentifiers.first { (utiType: String) -> Bool in + UTTypeConformsTo(utiType as CFString, kUTTypeData) + } + return matchingUtiType + } + + private class func createDataSource(utiType: String, url: URL, customFileName: String?) -> DataSource? { + if utiType == (kUTTypeURL as String) { + // Share URLs as oversize text messages whose text content is the URL. + // + // NOTE: SharingThreadPickerViewController will try to unpack them + // 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) { + // 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 + } + } + + private class func preferredItemProviders(inputItem: NSExtensionItem) -> [NSItemProvider]? { + 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 { + hasNonVisualMedia = true + } + } + // Only allow multiple-attachment sends if all attachments + // are visual media. + if visualMediaItemProviders.count > 0 && !hasNonVisualMedia { + return visualMediaItemProviders + } + + // A single inputItem can have multiple attachments, e.g. sharing from Firefox gives + // one url attachment and another text attachment, where the the url would be https://some-news.com/articles/123-cat-stuck-in-tree + // and the text attachment would be something like "Breaking news - cat stuck in tree" + // + // FIXME: For now, we prefer the URL provider and discard the text provider, since it's more useful to share the URL than the caption + // but we *should* include both. This will be a bigger change though since our share extension is currently heavily predicated + // on one itemProvider per share. + + // Prefer a URL provider if available + if let preferredAttachment = attachments.first(where: { (attachment: Any) -> Bool in + guard let itemProvider = attachment as? NSItemProvider else { + return false + } + return isUrlItem(itemProvider: itemProvider) + }) { + return [preferredAttachment] + } + + // else return whatever is available + if let itemProvider = inputItem.attachments?.first { + return [itemProvider] + } else { + owsFailDebug("Missing attachment.") + } + return [] + } + + private func selectItemProviders() -> Promise<[NSItemProvider]> { + guard let inputItems = self.extensionContext?.inputItems else { + let error = ShareViewControllerError.assertionError(description: "no input item") + return Promise(error: error) + } + + for inputItemRaw in inputItems { + guard let inputItem = inputItemRaw as? NSExtensionItem else { + Logger.error("invalid inputItem \(inputItemRaw)") + continue + } + if let itemProviders = ShareVC.preferredItemProviders(inputItem: inputItem) { + return Promise.value(itemProviders) + } + } + let error = ShareViewControllerError.assertionError(description: "no input item") + return Promise(error: error) + } + + private + struct LoadedItem { + let itemProvider: NSItemProvider + let itemUrl: URL + let utiType: String + + var customFileName: String? + var isConvertibleToTextMessage = false + var isConvertibleToContactShare = false + + init(itemProvider: NSItemProvider, + itemUrl: URL, + utiType: String, + customFileName: String? = nil, + isConvertibleToTextMessage: Bool = false, + isConvertibleToContactShare: Bool = false) { + self.itemProvider = itemProvider + self.itemUrl = itemUrl + self.utiType = utiType + self.customFileName = customFileName + self.isConvertibleToTextMessage = isConvertibleToTextMessage + self.isConvertibleToContactShare = isConvertibleToContactShare + } + } + + private func loadItemProvider(itemProvider: NSItemProvider) -> Promise { + Logger.info("attachment: \(itemProvider)") + + // We need to be very careful about which UTI type we use. + // + // * In the case of "textual" shares (e.g. web URLs and text snippets), we want to + // coerce the UTI type to kUTTypeURL or kUTTypeText. + // * We want to treat shared files as file attachments. Therefore we do not + // want to treat file URLs like web URLs. + // * UTIs aren't very descriptive (there are far more MIME types than UTI types) + // so in the case of file attachments we try to refine the attachment type + // using the file extension. + guard let srcUtiType = ShareVC.utiType(itemProvider: itemProvider) else { + let error = ShareViewControllerError.unsupportedMedia + return Promise(error: error) + } + Logger.debug("matched utiType: \(srcUtiType)") + + let (promise, resolver) = Promise.pending() + + let loadCompletion: NSItemProvider.CompletionHandler = { [weak self] + (value, error) in + + guard let _ = self else { return } + guard error == nil else { + resolver.reject(error!) + return + } + + guard let value = value else { + let missingProviderError = ShareViewControllerError.assertionError(description: "missing item provider") + resolver.reject(missingProviderError) + return + } + + Logger.info("value type: \(type(of: value))") + + if let data = value as? Data { + let customFileName = "Contact.vcf" + + let customFileExtension = MIMETypeUtil.fileExtension(forUTIType: srcUtiType) + guard let tempFilePath = OWSFileSystem.writeData(toTemporaryFile: data, fileExtension: customFileExtension) else { + let writeError = ShareViewControllerError.assertionError(description: "Error writing item data: \(String(describing: error))") + resolver.reject(writeError) + return + } + let fileUrl = URL(fileURLWithPath: tempFilePath) + resolver.fulfill(LoadedItem(itemProvider: itemProvider, + itemUrl: fileUrl, + utiType: srcUtiType, + customFileName: customFileName, + isConvertibleToContactShare: false)) + } else if let string = value as? String { + Logger.debug("string provider: \(string)") + guard let data = string.filterStringForDisplay().data(using: String.Encoding.utf8) else { + let writeError = ShareViewControllerError.assertionError(description: "Error writing item data: \(String(describing: error))") + resolver.reject(writeError) + return + } + guard let tempFilePath = OWSFileSystem.writeData(toTemporaryFile: data, fileExtension: "txt") else { + let writeError = ShareViewControllerError.assertionError(description: "Error writing item data: \(String(describing: error))") + resolver.reject(writeError) + return + } + + let fileUrl = URL(fileURLWithPath: tempFilePath) + + let isConvertibleToTextMessage = !itemProvider.registeredTypeIdentifiers.contains(kUTTypeFileURL as String) + + if UTTypeConformsTo(srcUtiType as CFString, kUTTypeText) { + resolver.fulfill(LoadedItem(itemProvider: itemProvider, + itemUrl: fileUrl, + utiType: srcUtiType, + isConvertibleToTextMessage: isConvertibleToTextMessage)) + } else { + resolver.fulfill(LoadedItem(itemProvider: itemProvider, + itemUrl: fileUrl, + utiType: kUTTypeText as String, + isConvertibleToTextMessage: isConvertibleToTextMessage)) + } + } else if let url = value as? URL { + // If the share itself is a URL (e.g. a link from Safari), try to send this as a text message. + let isConvertibleToTextMessage = (itemProvider.registeredTypeIdentifiers.contains(kUTTypeURL as String) && + !itemProvider.registeredTypeIdentifiers.contains(kUTTypeFileURL as String)) + if isConvertibleToTextMessage { + resolver.fulfill(LoadedItem(itemProvider: itemProvider, + itemUrl: url, + utiType: kUTTypeURL as String, + isConvertibleToTextMessage: isConvertibleToTextMessage)) + } else { + resolver.fulfill(LoadedItem(itemProvider: itemProvider, + itemUrl: url, + utiType: srcUtiType, + isConvertibleToTextMessage: isConvertibleToTextMessage)) + } + } else if let image = value as? UIImage { + if let data = image.pngData() { + let tempFilePath = OWSFileSystem.temporaryFilePath(withFileExtension: "png") + do { + let url = NSURL.fileURL(withPath: tempFilePath) + try data.write(to: url) + resolver.fulfill(LoadedItem(itemProvider: itemProvider, itemUrl: url, + utiType: srcUtiType)) + } catch { + resolver.reject(ShareViewControllerError.assertionError(description: "couldn't write UIImage: \(String(describing: error))")) + } + } else { + resolver.reject(ShareViewControllerError.assertionError(description: "couldn't convert UIImage to PNG: \(String(describing: error))")) + } + } else { + // It's unavoidable that we may sometimes receives data types that we + // don't know how to handle. + let unexpectedTypeError = ShareViewControllerError.assertionError(description: "unexpected value: \(String(describing: value))") + resolver.reject(unexpectedTypeError) + } + } + + itemProvider.loadItem(forTypeIdentifier: srcUtiType, options: nil, completionHandler: loadCompletion) + + return promise + } + + private func buildAttachment(forLoadedItem loadedItem: LoadedItem) -> Promise { + let itemProvider = loadedItem.itemProvider + let itemUrl = loadedItem.itemUrl + let utiType = loadedItem.utiType + + var url = itemUrl + do { + if isVideoNeedingRelocation(itemProvider: itemProvider, itemUrl: itemUrl) { + url = try SignalAttachment.copyToVideoTempDir(url: itemUrl) + } + } catch { + let error = ShareViewControllerError.assertionError(description: "Could not copy video") + return Promise(error: error) + } + + Logger.debug("building DataSource with url: \(url), utiType: \(utiType)") + + guard let dataSource = ShareVC.createDataSource(utiType: utiType, url: url, customFileName: loadedItem.customFileName) else { + let error = ShareViewControllerError.assertionError(description: "Unable to read attachment data") + return Promise(error: error) + } + + // start with base utiType, but it might be something generic like "image" + var specificUTIType = utiType + if utiType == (kUTTypeURL as String) { + // Use kUTTypeURL for URLs. + } else if UTTypeConformsTo(utiType as CFString, kUTTypeText) { + // Use kUTTypeText for text. + } else if url.pathExtension.count > 0 { + // Determine a more specific utiType based on file extension + if let typeExtension = MIMETypeUtil.utiType(forFileExtension: url.pathExtension) { + Logger.debug("utiType based on extension: \(typeExtension)") + specificUTIType = typeExtension + } + } + + guard !SignalAttachment.isInvalidVideo(dataSource: dataSource, dataUTI: specificUTIType) else { + // This can happen, e.g. when sharing a quicktime-video from iCloud drive. + + let (promise, exportSession) = SignalAttachment.compressVideoAsMp4(dataSource: dataSource, dataUTI: specificUTIType) + + // TODO: How can we move waiting for this export to the end of the share flow rather than having to do it up front? + // Ideally we'd be able to start it here, and not block the UI on conversion unless there's still work to be done + // when the user hits "send". + if let exportSession = exportSession { + let progressPoller = ProgressPoller(timeInterval: 0.1, ratioCompleteBlock: { return exportSession.progress }) + self.progressPoller = progressPoller + progressPoller.startPolling() + + guard let loadViewController = self.loadViewController else { + owsFailDebug("load view controller was unexpectedly nil") + return promise + } + + DispatchQueue.main.async { + loadViewController.progress = progressPoller.progress + } + } + + return promise + } + + let attachment = SignalAttachment.attachment(dataSource: dataSource, dataUTI: specificUTIType, imageQuality: .medium) + if loadedItem.isConvertibleToContactShare { + Logger.info("isConvertibleToContactShare") + attachment.isConvertibleToContactShare = true + } else if loadedItem.isConvertibleToTextMessage { + Logger.info("isConvertibleToTextMessage") + attachment.isConvertibleToTextMessage = true + } + return Promise.value(attachment) + } + + private func buildAttachments() -> Promise<[SignalAttachment]> { + return selectItemProviders().then { [weak self] (itemProviders) -> Promise<[SignalAttachment]> in + guard let strongSelf = self else { + let error = ShareViewControllerError.assertionError(description: "expired") + return Promise(error: error) + } + + var loadPromises = [Promise]() + + for itemProvider in itemProviders.prefix(SignalAttachment.maxAttachmentsAllowed) { + let loadPromise = strongSelf.loadItemProvider(itemProvider: itemProvider) + .then({ (loadedItem) -> Promise in + return strongSelf.buildAttachment(forLoadedItem: loadedItem) + }) + + loadPromises.append(loadPromise) + } + return when(fulfilled: loadPromises) + }.map { (signalAttachments) -> [SignalAttachment] in + guard signalAttachments.count > 0 else { + let error = ShareViewControllerError.assertionError(description: "no valid attachments") + throw error + } + return signalAttachments + } + } + + // Some host apps (e.g. iOS Photos.app) sometimes auto-converts some video formats (e.g. com.apple.quicktime-movie) + // into mp4s as part of the NSItemProvider `loadItem` API. (Some files the Photo's app doesn't auto-convert) + // + // However, when using this url to the converted item, AVFoundation operations such as generating a + // preview image and playing the url in the AVMoviePlayer fails with an unhelpful error: "The operation could not be completed" + // + // We can work around this by first copying the media into our container. + // + // I don't understand why this is, and I haven't found any relevant documentation in the NSItemProvider + // or AVFoundation docs. + // + // Notes: + // + // These operations succeed when sending a video which initially existed on disk as an mp4. + // (e.g. Alice sends a video to Bob through the main app, which ensures it's an mp4. Bob saves it, then re-shares it) + // + // I *did* verify that the size and SHA256 sum of the original url matches that of the copied url. So there + // is no difference between the contents of the file, yet one works one doesn't. + // Perhaps the AVFoundation APIs require some extra file system permssion we don't have in the + // passed through URL. + private func isVideoNeedingRelocation(itemProvider: NSItemProvider, itemUrl: URL) -> Bool { + let pathExtension = itemUrl.pathExtension + guard pathExtension.count > 0 else { + Logger.verbose("item URL has no file extension: \(itemUrl).") + return false + } + guard let utiTypeForURL = MIMETypeUtil.utiType(forFileExtension: pathExtension) else { + Logger.verbose("item has unknown UTI type: \(itemUrl).") + return false + } + Logger.verbose("utiTypeForURL: \(utiTypeForURL)") + guard utiTypeForURL == kUTTypeMPEG4 as String else { + // Either it's not a video or it was a video which was not auto-converted to mp4. + // Not affected by the issue. + return false + } + + // If video file already existed on disk as an mp4, then the host app didn't need to + // apply any conversion, so no need to relocate the app. + return !itemProvider.registeredTypeIdentifiers.contains(kUTTypeMPEG4 as String) + } } diff --git a/SessionShareExtension/ThreadPickerVC.swift b/SessionShareExtension/ThreadPickerVC.swift index 3312c6bbb..7c9dd852a 100644 --- a/SessionShareExtension/ThreadPickerVC.swift +++ b/SessionShareExtension/ThreadPickerVC.swift @@ -73,6 +73,13 @@ final class ThreadPickerVC : UIViewController, UITableViewDataSource { tableView.reloadData() } + // MARK: Interaction + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + guard let thread = self.thread(at: indexPath.row) else { return } + // TODO: Send the attachment + tableView.deselectRow(at: indexPath, animated: true) + } + // MARK: Convenience private func thread(at index: Int) -> TSThread? { var thread: TSThread? = nil From e09b0ebd3fc5ec107881ce32f7ecb31eb4be2e23 Mon Sep 17 00:00:00 2001 From: Niels Andriesse Date: Thu, 6 May 2021 10:49:32 +1000 Subject: [PATCH 07/13] Hook more things up --- SessionShareExtension/ShareVC.swift | 3 +++ SessionShareExtension/ThreadPickerVC.swift | 14 ++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/SessionShareExtension/ShareVC.swift b/SessionShareExtension/ShareVC.swift index d8677aa44..63e89d268 100644 --- a/SessionShareExtension/ShareVC.swift +++ b/SessionShareExtension/ShareVC.swift @@ -201,6 +201,7 @@ final class ShareVC : UINavigationController, ShareViewDelegate, AppModeManagerD private func showMainContent() { let threadPickerVC = ThreadPickerVC() setViewControllers([ threadPickerVC ], animated: false) + buildAttachments().retainUntilComplete() } func shareViewWasUnlocked() { @@ -548,6 +549,7 @@ final class ShareVC : UINavigationController, ShareViewDelegate, AppModeManagerD let (promise, exportSession) = SignalAttachment.compressVideoAsMp4(dataSource: dataSource, dataUTI: specificUTIType) + /* // TODO: How can we move waiting for this export to the end of the share flow rather than having to do it up front? // Ideally we'd be able to start it here, and not block the UI on conversion unless there's still work to be done // when the user hits "send". @@ -565,6 +567,7 @@ final class ShareVC : UINavigationController, ShareViewDelegate, AppModeManagerD loadViewController.progress = progressPoller.progress } } + */ return promise } diff --git a/SessionShareExtension/ThreadPickerVC.swift b/SessionShareExtension/ThreadPickerVC.swift index 7c9dd852a..73fdbc035 100644 --- a/SessionShareExtension/ThreadPickerVC.swift +++ b/SessionShareExtension/ThreadPickerVC.swift @@ -23,6 +23,14 @@ final class ThreadPickerVC : UIViewController, UITableViewDataSource { return result }() + private lazy var fadeView: UIView = { + let result = UIView() + let gradient = Gradients.homeVCFade + result.setGradient(gradient) + result.isUserInteractionEnabled = false + return result + }() + // MARK: Lifecycle override func viewDidLoad() { super.viewDidLoad() @@ -47,6 +55,12 @@ final class ThreadPickerVC : UIViewController, UITableViewDataSource { tableView.dataSource = 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) // Reload reload() } From 0cbcf0b1696ce512b12767535f777d83054a8c44 Mon Sep 17 00:00:00 2001 From: Niels Andriesse Date: Thu, 6 May 2021 11:09:54 +1000 Subject: [PATCH 08/13] Implement basic sending logic --- SessionShareExtension/ShareVC.swift | 27 +++---------------- SessionShareExtension/ThreadPickerVC.swift | 31 +++++++++++++++++++--- 2 files changed, 31 insertions(+), 27 deletions(-) diff --git a/SessionShareExtension/ShareVC.swift b/SessionShareExtension/ShareVC.swift index 63e89d268..ce226e0af 100644 --- a/SessionShareExtension/ShareVC.swift +++ b/SessionShareExtension/ShareVC.swift @@ -4,6 +4,7 @@ import SessionUIKit final class ShareVC : UINavigationController, ShareViewDelegate, AppModeManagerDelegate { private var areVersionMigrationsComplete = false + public static var attachmentPrepPromise: Promise<[SignalAttachment]>? // MARK: Error enum ShareViewControllerError: Error { @@ -201,7 +202,7 @@ final class ShareVC : UINavigationController, ShareViewDelegate, AppModeManagerD private func showMainContent() { let threadPickerVC = ThreadPickerVC() setViewControllers([ threadPickerVC ], animated: false) - buildAttachments().retainUntilComplete() + ShareVC.attachmentPrepPromise = buildAttachments() } func shareViewWasUnlocked() { @@ -546,29 +547,7 @@ final class ShareVC : UINavigationController, ShareViewDelegate, AppModeManagerD guard !SignalAttachment.isInvalidVideo(dataSource: dataSource, dataUTI: specificUTIType) else { // This can happen, e.g. when sharing a quicktime-video from iCloud drive. - - let (promise, exportSession) = SignalAttachment.compressVideoAsMp4(dataSource: dataSource, dataUTI: specificUTIType) - - /* - // TODO: How can we move waiting for this export to the end of the share flow rather than having to do it up front? - // Ideally we'd be able to start it here, and not block the UI on conversion unless there's still work to be done - // when the user hits "send". - if let exportSession = exportSession { - let progressPoller = ProgressPoller(timeInterval: 0.1, ratioCompleteBlock: { return exportSession.progress }) - self.progressPoller = progressPoller - progressPoller.startPolling() - - guard let loadViewController = self.loadViewController else { - owsFailDebug("load view controller was unexpectedly nil") - return promise - } - - DispatchQueue.main.async { - loadViewController.progress = progressPoller.progress - } - } - */ - + let (promise, _) = SignalAttachment.compressVideoAsMp4(dataSource: dataSource, dataUTI: specificUTIType) return promise } diff --git a/SessionShareExtension/ThreadPickerVC.swift b/SessionShareExtension/ThreadPickerVC.swift index 73fdbc035..2f68d1736 100644 --- a/SessionShareExtension/ThreadPickerVC.swift +++ b/SessionShareExtension/ThreadPickerVC.swift @@ -1,8 +1,9 @@ import SessionUIKit -final class ThreadPickerVC : UIViewController, UITableViewDataSource { +final class ThreadPickerVC : UIViewController, UITableViewDataSource, UITableViewDelegate, AttachmentApprovalViewControllerDelegate { private var threads: YapDatabaseViewMappings! private var threadViewModelCache: [String:ThreadViewModel] = [:] // Thread ID to ThreadViewModel + private var selectedThread: TSThread? private var threadCount: UInt { threads.numberOfItems(inGroup: TSInboxGroup) @@ -53,6 +54,7 @@ final class ThreadPickerVC : UIViewController, UITableViewDataSource { navigationItem.titleView = titleLabel // Table view tableView.dataSource = self + tableView.delegate = self view.addSubview(tableView) tableView.pin(to: view) view.addSubview(fadeView) @@ -89,9 +91,32 @@ final class ThreadPickerVC : UIViewController, UITableViewDataSource { // MARK: Interaction func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - guard let thread = self.thread(at: indexPath.row) else { return } - // TODO: Send the attachment + guard let thread = self.thread(at: indexPath.row), let attachments = ShareVC.attachmentPrepPromise?.value else { return } + self.selectedThread = thread tableView.deselectRow(at: indexPath, animated: true) + let approvalVC = AttachmentApprovalViewController.wrappedInNavController(attachments: attachments, approvalDelegate: self) + navigationController!.present(approvalVC, animated: true, completion: nil) + } + + func attachmentApproval(_ attachmentApproval: AttachmentApprovalViewController, didApproveAttachments attachments: [SignalAttachment], messageText: String?) { + 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) + } + Storage.write { transaction in + MessageSender.sendNonDurably(message, with: attachments, in: self.selectedThread!, using: transaction).retainUntilComplete() + } + } + + func attachmentApprovalDidCancel(_ attachmentApproval: AttachmentApprovalViewController) { + + } + + func attachmentApproval(_ attachmentApproval: AttachmentApprovalViewController, didChangeMessageText newMessageText: String?) { + } // MARK: Convenience From 54684ba565f64b95f5ad678186e0dbbd9dff71f8 Mon Sep 17 00:00:00 2001 From: Niels Andriesse Date: Thu, 6 May 2021 11:22:33 +1000 Subject: [PATCH 09/13] Handle completion --- SessionShareExtension/ShareVC.swift | 24 +++++++++++++++---- .../SimplifiedConversationCell.swift | 2 +- SessionShareExtension/ThreadPickerVC.swift | 16 ++++++++++--- 3 files changed, 34 insertions(+), 8 deletions(-) diff --git a/SessionShareExtension/ShareVC.swift b/SessionShareExtension/ShareVC.swift index ce226e0af..f3f7900e7 100644 --- a/SessionShareExtension/ShareVC.swift +++ b/SessionShareExtension/ShareVC.swift @@ -201,8 +201,16 @@ final class ShareVC : UINavigationController, ShareViewDelegate, AppModeManagerD private func showMainContent() { let threadPickerVC = ThreadPickerVC() + threadPickerVC.shareVC = self setViewControllers([ threadPickerVC ], animated: false) - ShareVC.attachmentPrepPromise = buildAttachments() + showLoader() + let promise = buildAttachments() + promise.done { [weak self] _ in + self?.hideLoader() + }.catch { [weak self] _ in + self?.hideLoader() + } + ShareVC.attachmentPrepPromise = promise } func shareViewWasUnlocked() { @@ -210,15 +218,23 @@ final class ShareVC : UINavigationController, ShareViewDelegate, AppModeManagerD } func shareViewWasCompleted() { - print("completed") + extensionContext!.completeRequest(returningItems: [], completionHandler: nil) } func shareViewWasCancelled() { - print("canceled") + extensionContext!.completeRequest(returningItems: [], completionHandler: nil) } func shareViewFailed(error: Error) { - print("failed") + extensionContext!.cancelRequest(withError: error) + } + + func showLoader() { + + } + + func hideLoader() { + } // MARK: Attachment Prep diff --git a/SessionShareExtension/SimplifiedConversationCell.swift b/SessionShareExtension/SimplifiedConversationCell.swift index 8b45d3fc2..b03404392 100644 --- a/SessionShareExtension/SimplifiedConversationCell.swift +++ b/SessionShareExtension/SimplifiedConversationCell.swift @@ -50,7 +50,7 @@ final class SimplifiedConversationCell : UITableViewCell { profilePictureView.set(.height, to: profilePictureViewSize) profilePictureView.size = profilePictureViewSize // Main stack view - let stackView = UIStackView(arrangedSubviews: [ accentLineView, profilePictureView, displayNameLabel ]) + let stackView = UIStackView(arrangedSubviews: [ accentLineView, profilePictureView, displayNameLabel, UIView.hSpacer(0) ]) stackView.axis = .horizontal stackView.alignment = .center stackView.spacing = Values.mediumSpacing diff --git a/SessionShareExtension/ThreadPickerVC.swift b/SessionShareExtension/ThreadPickerVC.swift index 2f68d1736..384243322 100644 --- a/SessionShareExtension/ThreadPickerVC.swift +++ b/SessionShareExtension/ThreadPickerVC.swift @@ -4,6 +4,7 @@ final class ThreadPickerVC : UIViewController, UITableViewDataSource, UITableVie private var threads: YapDatabaseViewMappings! private var threadViewModelCache: [String:ThreadViewModel] = [:] // Thread ID to ThreadViewModel private var selectedThread: TSThread? + var shareVC: ShareVC? private var threadCount: UInt { threads.numberOfItems(inGroup: TSInboxGroup) @@ -99,6 +100,7 @@ final class ThreadPickerVC : UIViewController, UITableViewDataSource, UITableVie } func attachmentApproval(_ attachmentApproval: AttachmentApprovalViewController, didApproveAttachments attachments: [SignalAttachment], messageText: String?) { + shareVC?.showLoader() let message = VisibleMessage() message.sentTimestamp = NSDate.millisecondTimestamp() message.text = messageText @@ -107,16 +109,24 @@ final class ThreadPickerVC : UIViewController, UITableViewDataSource, UITableVie tsMessage.save(with: transaction) } Storage.write { transaction in - MessageSender.sendNonDurably(message, with: attachments, in: self.selectedThread!, using: transaction).retainUntilComplete() + MessageSender.sendNonDurably(message, with: attachments, in: self.selectedThread!, using: transaction).done { [weak self] _ in + guard let self = self else { return } + self.shareVC?.hideLoader() + self.shareVC?.shareViewWasCompleted() + }.catch { [weak self] error in + guard let self = self else { return } + self.shareVC?.hideLoader() + self.shareVC?.shareViewFailed(error: error) + } } } func attachmentApprovalDidCancel(_ attachmentApproval: AttachmentApprovalViewController) { - + // Do nothing } func attachmentApproval(_ attachmentApproval: AttachmentApprovalViewController, didChangeMessageText newMessageText: String?) { - + // Do nothing } // MARK: Convenience From 33a16df602ee5b5ef0873b44f6f7607ab0580535 Mon Sep 17 00:00:00 2001 From: Niels Andriesse Date: Thu, 6 May 2021 11:36:13 +1000 Subject: [PATCH 10/13] Minor refactoring --- SessionShareExtension/ShareVC.swift | 21 ++++++---------- SessionShareExtension/ThreadPickerVC.swift | 29 ++++++++++++---------- 2 files changed, 23 insertions(+), 27 deletions(-) diff --git a/SessionShareExtension/ShareVC.swift b/SessionShareExtension/ShareVC.swift index f3f7900e7..0ecfe0aa9 100644 --- a/SessionShareExtension/ShareVC.swift +++ b/SessionShareExtension/ShareVC.swift @@ -201,14 +201,15 @@ final class ShareVC : UINavigationController, ShareViewDelegate, AppModeManagerD private func showMainContent() { let threadPickerVC = ThreadPickerVC() - threadPickerVC.shareVC = self + threadPickerVC.shareDelegate = self setViewControllers([ threadPickerVC ], animated: false) - showLoader() let promise = buildAttachments() - promise.done { [weak self] _ in - self?.hideLoader() - }.catch { [weak self] _ in - self?.hideLoader() + ModalActivityIndicatorViewController.present(fromViewController: self, canCancel: false) { activityIndicator in + promise.done { _ in + activityIndicator.dismiss { } + }.catch { _ in + activityIndicator.dismiss { } + } } ShareVC.attachmentPrepPromise = promise } @@ -229,14 +230,6 @@ final class ShareVC : UINavigationController, ShareViewDelegate, AppModeManagerD extensionContext!.cancelRequest(withError: error) } - func showLoader() { - - } - - func hideLoader() { - - } - // MARK: Attachment Prep private class func itemMatchesSpecificUtiType(itemProvider: NSItemProvider, utiType: String) -> Bool { // URLs, contacts and other special items have to be detected separately. diff --git a/SessionShareExtension/ThreadPickerVC.swift b/SessionShareExtension/ThreadPickerVC.swift index 384243322..254237727 100644 --- a/SessionShareExtension/ThreadPickerVC.swift +++ b/SessionShareExtension/ThreadPickerVC.swift @@ -4,7 +4,7 @@ final class ThreadPickerVC : UIViewController, UITableViewDataSource, UITableVie private var threads: YapDatabaseViewMappings! private var threadViewModelCache: [String:ThreadViewModel] = [:] // Thread ID to ThreadViewModel private var selectedThread: TSThread? - var shareVC: ShareVC? + var shareDelegate: ShareViewDelegate? private var threadCount: UInt { threads.numberOfItems(inGroup: TSInboxGroup) @@ -100,7 +100,6 @@ final class ThreadPickerVC : UIViewController, UITableViewDataSource, UITableVie } func attachmentApproval(_ attachmentApproval: AttachmentApprovalViewController, didApproveAttachments attachments: [SignalAttachment], messageText: String?) { - shareVC?.showLoader() let message = VisibleMessage() message.sentTimestamp = NSDate.millisecondTimestamp() message.text = messageText @@ -108,17 +107,21 @@ final class ThreadPickerVC : UIViewController, UITableViewDataSource, UITableVie Storage.write { transaction in tsMessage.save(with: transaction) } - Storage.write { transaction in - MessageSender.sendNonDurably(message, with: attachments, in: self.selectedThread!, using: transaction).done { [weak self] _ in - guard let self = self else { return } - self.shareVC?.hideLoader() - self.shareVC?.shareViewWasCompleted() - }.catch { [weak self] error in - guard let self = self else { return } - self.shareVC?.hideLoader() - self.shareVC?.shareViewFailed(error: error) - } - } +// DispatchQueue.main.async { +// ModalActivityIndicatorViewController.present(fromViewController: self.navigationController!, canCancel: false) { activityIndicator in + Storage.write { transaction in + MessageSender.sendNonDurably(message, with: attachments, in: self.selectedThread!, using: transaction).done { [weak self] _ in + guard let self = self else { return } +// activityIndicator.dismiss { } + self.shareDelegate?.shareViewWasCompleted() + }.catch { [weak self] error in + guard let self = self else { return } +// activityIndicator.dismiss { } + self.shareDelegate?.shareViewFailed(error: error) + } + } +// } +// } } func attachmentApprovalDidCancel(_ attachmentApproval: AttachmentApprovalViewController) { From 57206c4a5c22dc7f5b81f2db2e12a9e764f6a69a Mon Sep 17 00:00:00 2001 From: Niels Andriesse Date: Thu, 6 May 2021 12:48:49 +1000 Subject: [PATCH 11/13] Fix loader --- .../Translations/de.lproj/Localizable.strings | 2 ++ .../Translations/en.lproj/Localizable.strings | 2 ++ .../Translations/es.lproj/Localizable.strings | 2 ++ .../Translations/fa.lproj/Localizable.strings | 2 ++ .../Translations/fr.lproj/Localizable.strings | 2 ++ .../id-ID.lproj/Localizable.strings | 2 ++ .../Translations/it.lproj/Localizable.strings | 2 ++ .../Translations/ja.lproj/Localizable.strings | 2 ++ .../Translations/pl.lproj/Localizable.strings | 2 ++ .../pt_BR.lproj/Localizable.strings | 2 ++ .../Translations/ru.lproj/Localizable.strings | 2 ++ .../Translations/sk.lproj/Localizable.strings | 2 ++ .../vi-VN.lproj/Localizable.strings | 2 ++ .../zh_CN.lproj/Localizable.strings | 2 ++ SessionShareExtension/ShareVC.swift | 4 +-- SessionShareExtension/ThreadPickerVC.swift | 29 +++++++++---------- ...ModalActivityIndicatorViewController.swift | 1 - 17 files changed, 44 insertions(+), 18 deletions(-) diff --git a/Session/Meta/Translations/de.lproj/Localizable.strings b/Session/Meta/Translations/de.lproj/Localizable.strings index a195d3971..5fdff2003 100644 --- a/Session/Meta/Translations/de.lproj/Localizable.strings +++ b/Session/Meta/Translations/de.lproj/Localizable.strings @@ -490,3 +490,5 @@ "vc_qr_code_view_scan_qr_code_explanation" = "Scannen Sie den QR-Code einer Person, um ein Gespräch mit ihr zu beginnen."; "vc_view_my_qr_code_explanation" = "Das ist Ihr QR-Code. Andere Benutzer können ihn scannen, um eine Session mit Ihnen zu starten."; "vc_share_title" = "Share to Session"; +"vc_share_loading_message" = "Preparing attachments..."; +"vc_share_sending_message" = "Sending..."; diff --git a/Session/Meta/Translations/en.lproj/Localizable.strings b/Session/Meta/Translations/en.lproj/Localizable.strings index d5b2cb8ac..51cba59c8 100644 --- a/Session/Meta/Translations/en.lproj/Localizable.strings +++ b/Session/Meta/Translations/en.lproj/Localizable.strings @@ -521,3 +521,5 @@ "modal_link_previews_explanation" = "Enabling link previews will show previews for URLs you send and receive. This can be useful, but Session will need to contact linked websites to generate previews. You can always disable link previews in Session's settings."; "modal_link_previews_button_title" = "Enable"; "vc_share_title" = "Share to Session"; +"vc_share_loading_message" = "Preparing attachments..."; +"vc_share_sending_message" = "Sending..."; diff --git a/Session/Meta/Translations/es.lproj/Localizable.strings b/Session/Meta/Translations/es.lproj/Localizable.strings index 2748ca8a1..d86b91cbe 100644 --- a/Session/Meta/Translations/es.lproj/Localizable.strings +++ b/Session/Meta/Translations/es.lproj/Localizable.strings @@ -490,3 +490,5 @@ "vc_qr_code_view_scan_qr_code_explanation" = "Escanea el código QR de una persona para comenzar una conversación con ella"; "vc_view_my_qr_code_explanation" = "Este es tu código QR. Otros usuarios pueden escanearlo para empezar una Session contigo."; "vc_share_title" = "Share to Session"; +"vc_share_loading_message" = "Preparing attachments..."; +"vc_share_sending_message" = "Sending..."; diff --git a/Session/Meta/Translations/fa.lproj/Localizable.strings b/Session/Meta/Translations/fa.lproj/Localizable.strings index 3e69b9d44..6c1ef49eb 100644 --- a/Session/Meta/Translations/fa.lproj/Localizable.strings +++ b/Session/Meta/Translations/fa.lproj/Localizable.strings @@ -490,3 +490,5 @@ "vc_qr_code_view_scan_qr_code_explanation" = "برای شروع مکالمه با دیگران، کد QR شخصی را اسکن کنید"; "vc_view_my_qr_code_explanation" = "این کد QR شماست. سایر کاربران می‌توانند برای شروع Session با شما آن را اسکن کنند."; "vc_share_title" = "Share to Session"; +"vc_share_loading_message" = "Preparing attachments..."; +"vc_share_sending_message" = "Sending..."; diff --git a/Session/Meta/Translations/fr.lproj/Localizable.strings b/Session/Meta/Translations/fr.lproj/Localizable.strings index 2ec2d983d..a2adacf98 100644 --- a/Session/Meta/Translations/fr.lproj/Localizable.strings +++ b/Session/Meta/Translations/fr.lproj/Localizable.strings @@ -490,3 +490,5 @@ "vc_qr_code_view_scan_qr_code_explanation" = "Scannez le code QR d'un autre utilisateur pour démarrer une session"; "vc_view_my_qr_code_explanation" = "Ceci est votre code QR. Les autres utilisateurs peuvent le scanner pour démarrer une session avec vous."; "vc_share_title" = "Share to Session"; +"vc_share_loading_message" = "Preparing attachments..."; +"vc_share_sending_message" = "Sending..."; diff --git a/Session/Meta/Translations/id-ID.lproj/Localizable.strings b/Session/Meta/Translations/id-ID.lproj/Localizable.strings index bea443195..0490fd401 100644 --- a/Session/Meta/Translations/id-ID.lproj/Localizable.strings +++ b/Session/Meta/Translations/id-ID.lproj/Localizable.strings @@ -491,3 +491,5 @@ "vc_qr_code_view_scan_qr_code_explanation" = "Pindai kode QR pengguna lain untuk memulai percakapan"; "vc_view_my_qr_code_explanation" = "Ini adalah kode QR anda. Pengguna lain bisa memindainya untuk memulai percakapan dengan anda"; "vc_share_title" = "Share to Session"; +"vc_share_loading_message" = "Preparing attachments..."; +"vc_share_sending_message" = "Sending..."; diff --git a/Session/Meta/Translations/it.lproj/Localizable.strings b/Session/Meta/Translations/it.lproj/Localizable.strings index ec9fe9b5f..a7a345cc2 100644 --- a/Session/Meta/Translations/it.lproj/Localizable.strings +++ b/Session/Meta/Translations/it.lproj/Localizable.strings @@ -490,3 +490,5 @@ "vc_qr_code_view_scan_qr_code_explanation" = "Scansiona il codice QR di un utente per iniziare una conversazione con questa persona"; "vc_view_my_qr_code_explanation" = "Questo è il tuo codice QR. Altri utenti possono scansionarlo per iniziare una sessione con te."; "vc_share_title" = "Share to Session"; +"vc_share_loading_message" = "Preparing attachments..."; +"vc_share_sending_message" = "Sending..."; diff --git a/Session/Meta/Translations/ja.lproj/Localizable.strings b/Session/Meta/Translations/ja.lproj/Localizable.strings index 2f8bb85a3..d40049750 100644 --- a/Session/Meta/Translations/ja.lproj/Localizable.strings +++ b/Session/Meta/Translations/ja.lproj/Localizable.strings @@ -491,3 +491,5 @@ "vc_qr_code_view_scan_qr_code_explanation" = "誰かの QR コードをスキャンして、会話を始めましょう"; "vc_view_my_qr_code_explanation" = "これはあなたの QR コードです。他のユーザーはそれをスキャンして、あなたとの Session を開始できます。"; "vc_share_title" = "Share to Session"; +"vc_share_loading_message" = "Preparing attachments..."; +"vc_share_sending_message" = "Sending..."; diff --git a/Session/Meta/Translations/pl.lproj/Localizable.strings b/Session/Meta/Translations/pl.lproj/Localizable.strings index 6a849a738..a269b46f3 100644 --- a/Session/Meta/Translations/pl.lproj/Localizable.strings +++ b/Session/Meta/Translations/pl.lproj/Localizable.strings @@ -490,3 +490,5 @@ "vc_qr_code_view_scan_qr_code_explanation" = "Zeskanuj czyjś kod QR, aby rozpocząć z nim rozmowę"; "vc_view_my_qr_code_explanation" = "To jest twój kod QR. Inni użytkownicy mogą go zeskanować, aby rozpocząć z tobą sesję."; "vc_share_title" = "Share to Session"; +"vc_share_loading_message" = "Preparing attachments..."; +"vc_share_sending_message" = "Sending..."; diff --git a/Session/Meta/Translations/pt_BR.lproj/Localizable.strings b/Session/Meta/Translations/pt_BR.lproj/Localizable.strings index 96528b44b..0551e8706 100644 --- a/Session/Meta/Translations/pt_BR.lproj/Localizable.strings +++ b/Session/Meta/Translations/pt_BR.lproj/Localizable.strings @@ -490,3 +490,5 @@ "vc_qr_code_view_scan_qr_code_explanation" = "Escaneie o código QR de alguém para iniciar uma conversa com essa pessoa"; "vc_view_my_qr_code_explanation" = "Este é o seu código QR. Outros usuários podem escaneá-lo para iniciar uma sessão com você."; "vc_share_title" = "Share to Session"; +"vc_share_loading_message" = "Preparing attachments..."; +"vc_share_sending_message" = "Sending..."; diff --git a/Session/Meta/Translations/ru.lproj/Localizable.strings b/Session/Meta/Translations/ru.lproj/Localizable.strings index 9044606d5..2254fb24f 100644 --- a/Session/Meta/Translations/ru.lproj/Localizable.strings +++ b/Session/Meta/Translations/ru.lproj/Localizable.strings @@ -521,3 +521,5 @@ "modal_link_previews_explanation" = "Включение предпросмотра ссылок покажет превью для отправляемых и получаемых ссылок. Это может быть полезно, но Session нужно будет соединиться с сайтами, связанными с ссылками, чтобы сгенерировать предпросмотр. Вы всегда можете отключить предпросмотр ссылок в настройках Session."; "modal_link_previews_button_title" = "Включить"; "vc_share_title" = "Share to Session"; +"vc_share_loading_message" = "Preparing attachments..."; +"vc_share_sending_message" = "Sending..."; diff --git a/Session/Meta/Translations/sk.lproj/Localizable.strings b/Session/Meta/Translations/sk.lproj/Localizable.strings index fb8a95a89..38bbe71d9 100644 --- a/Session/Meta/Translations/sk.lproj/Localizable.strings +++ b/Session/Meta/Translations/sk.lproj/Localizable.strings @@ -521,3 +521,5 @@ "modal_link_previews_explanation" = "Enabling link previews will show previews for URLs you send and receive. This can be useful, but Session will need to contact linked websites to generate previews. You can always disable link previews in Session's settings."; "modal_link_previews_button_title" = "Povoliť"; "vc_share_title" = "Share to Session"; +"vc_share_loading_message" = "Preparing attachments..."; +"vc_share_sending_message" = "Sending..."; diff --git a/Session/Meta/Translations/vi-VN.lproj/Localizable.strings b/Session/Meta/Translations/vi-VN.lproj/Localizable.strings index 3f764bf8b..8b1009fc2 100644 --- a/Session/Meta/Translations/vi-VN.lproj/Localizable.strings +++ b/Session/Meta/Translations/vi-VN.lproj/Localizable.strings @@ -497,3 +497,5 @@ "vc_qr_code_view_scan_qr_code_explanation" = "Quét mã QR của ai đó để bắt đầu trò chuyện với họ"; "vc_view_my_qr_code_explanation" = "Đây là mã QR của bạn. Những người dùng khác có thể quét mã này và bắt đầu session với bạn."; "vc_share_title" = "Share to Session"; +"vc_share_loading_message" = "Preparing attachments..."; +"vc_share_sending_message" = "Sending..."; diff --git a/Session/Meta/Translations/zh_CN.lproj/Localizable.strings b/Session/Meta/Translations/zh_CN.lproj/Localizable.strings index fac8be8d3..53dd66628 100644 --- a/Session/Meta/Translations/zh_CN.lproj/Localizable.strings +++ b/Session/Meta/Translations/zh_CN.lproj/Localizable.strings @@ -490,3 +490,5 @@ "vc_qr_code_view_scan_qr_code_explanation" = "扫描对方的二维码以发起对话"; "vc_view_my_qr_code_explanation" = "这是您的二维码。其他用户可以对其进行扫描以发起与您的对话。"; "vc_share_title" = "Share to Session"; +"vc_share_loading_message" = "Preparing attachments..."; +"vc_share_sending_message" = "Sending..."; diff --git a/SessionShareExtension/ShareVC.swift b/SessionShareExtension/ShareVC.swift index 0ecfe0aa9..ae65a0a4a 100644 --- a/SessionShareExtension/ShareVC.swift +++ b/SessionShareExtension/ShareVC.swift @@ -201,10 +201,10 @@ final class ShareVC : UINavigationController, ShareViewDelegate, AppModeManagerD private func showMainContent() { let threadPickerVC = ThreadPickerVC() - threadPickerVC.shareDelegate = self + threadPickerVC.shareVC = self setViewControllers([ threadPickerVC ], animated: false) let promise = buildAttachments() - ModalActivityIndicatorViewController.present(fromViewController: self, canCancel: false) { activityIndicator in + ModalActivityIndicatorViewController.present(fromViewController: self, canCancel: false, message: NSLocalizedString("vc_share_loading_message", comment: "")) { activityIndicator in promise.done { _ in activityIndicator.dismiss { } }.catch { _ in diff --git a/SessionShareExtension/ThreadPickerVC.swift b/SessionShareExtension/ThreadPickerVC.swift index 254237727..939a67594 100644 --- a/SessionShareExtension/ThreadPickerVC.swift +++ b/SessionShareExtension/ThreadPickerVC.swift @@ -4,7 +4,7 @@ final class ThreadPickerVC : UIViewController, UITableViewDataSource, UITableVie private var threads: YapDatabaseViewMappings! private var threadViewModelCache: [String:ThreadViewModel] = [:] // Thread ID to ThreadViewModel private var selectedThread: TSThread? - var shareDelegate: ShareViewDelegate? + var shareVC: ShareVC? private var threadCount: UInt { threads.numberOfItems(inGroup: TSInboxGroup) @@ -107,21 +107,20 @@ final class ThreadPickerVC : UIViewController, UITableViewDataSource, UITableVie Storage.write { transaction in tsMessage.save(with: transaction) } -// DispatchQueue.main.async { -// ModalActivityIndicatorViewController.present(fromViewController: self.navigationController!, canCancel: false) { activityIndicator in - Storage.write { transaction in - MessageSender.sendNonDurably(message, with: attachments, in: self.selectedThread!, using: transaction).done { [weak self] _ in - guard let self = self else { return } -// activityIndicator.dismiss { } - self.shareDelegate?.shareViewWasCompleted() - }.catch { [weak self] error in - guard let self = self else { return } -// activityIndicator.dismiss { } - self.shareDelegate?.shareViewFailed(error: error) - } + shareVC!.dismiss(animated: true, completion: nil) + ModalActivityIndicatorViewController.present(fromViewController: shareVC!, canCancel: false, message: NSLocalizedString("vc_share_sending_message", comment: "")) { activityIndicator in + Storage.write { transaction in + MessageSender.sendNonDurably(message, with: attachments, in: self.selectedThread!, using: transaction).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) } -// } -// } + } + } } func attachmentApprovalDidCancel(_ attachmentApproval: AttachmentApprovalViewController) { diff --git a/SignalUtilitiesKit/Shared View Controllers/ModalActivityIndicatorViewController.swift b/SignalUtilitiesKit/Shared View Controllers/ModalActivityIndicatorViewController.swift index b491f8cb5..c99a28310 100644 --- a/SignalUtilitiesKit/Shared View Controllers/ModalActivityIndicatorViewController.swift +++ b/SignalUtilitiesKit/Shared View Controllers/ModalActivityIndicatorViewController.swift @@ -54,7 +54,6 @@ public class ModalActivityIndicatorViewController: OWSViewController { fromViewController.present(view, animated: false) { DispatchQueue.global().async { backgroundBlock(view) - } } } From 15810dea9c965052750fee6585a578e172a6c150 Mon Sep 17 00:00:00 2001 From: Niels Andriesse Date: Thu, 6 May 2021 12:54:05 +1000 Subject: [PATCH 12/13] Handle errors --- SessionShareExtension/ShareVC.swift | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/SessionShareExtension/ShareVC.swift b/SessionShareExtension/ShareVC.swift index ae65a0a4a..6c62e83e7 100644 --- a/SessionShareExtension/ShareVC.swift +++ b/SessionShareExtension/ShareVC.swift @@ -227,7 +227,11 @@ final class ShareVC : UINavigationController, ShareViewDelegate, AppModeManagerD } func shareViewFailed(error: Error) { - extensionContext!.cancelRequest(withError: error) + let alert = UIAlertController(title: "Session", message: error.localizedDescription, preferredStyle: .alert) + alert.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .default, handler: { _ in + self.extensionContext!.cancelRequest(withError: error) + })) + present(alert, animated: true, completion: nil) } // MARK: Attachment Prep From 94902f715357005d940f8ed14f69c05237092bf8 Mon Sep 17 00:00:00 2001 From: Niels Andriesse Date: Thu, 6 May 2021 12:54:48 +1000 Subject: [PATCH 13/13] Delete old implementation --- Session.xcodeproj/project.pbxproj | 20 - .../Deprecated/SAEFailedViewController.swift | 96 -- .../Deprecated/SAELoadViewController.swift | 110 -- .../Deprecated/ShareViewController.swift | 1063 ----------------- 4 files changed, 1289 deletions(-) delete mode 100644 SessionShareExtension/Deprecated/SAEFailedViewController.swift delete mode 100644 SessionShareExtension/Deprecated/SAELoadViewController.swift delete mode 100644 SessionShareExtension/Deprecated/ShareViewController.swift diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index cdf8f3411..c07b58381 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -26,7 +26,6 @@ 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 */; }; - 3461284B1FD0B94000532771 /* SAELoadViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3461284A1FD0B93F00532771 /* SAELoadViewController.swift */; }; 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 */; }; @@ -36,7 +35,6 @@ 347850331FD7494A007B8332 /* fontawesome-webfont.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 34330A591E7875FB00DF2FB9 /* fontawesome-webfont.ttf */; }; 3478504C1FD7496D007B8332 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B66DBF4919D5BBC8006EA940 /* Images.xcassets */; }; 347850551FD749C0007B8332 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = B6F509951AA53F760068F56A /* Localizable.strings */; }; - 347850571FD86544007B8332 /* SAEFailedViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 347850561FD86544007B8332 /* SAEFailedViewController.swift */; }; 3488F9362191CC4000E524CC /* MediaView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3488F9352191CC4000E524CC /* MediaView.swift */; }; 3496744F2076ACD000080B5F /* LongTextViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3496744E2076ACCE00080B5F /* LongTextViewController.swift */; }; 3496955C219B605E00DCFE74 /* ImagePickerController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34969559219B605E00DCFE74 /* ImagePickerController.swift */; }; @@ -81,7 +79,6 @@ 4520D8D51D417D8E00123472 /* Photos.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4520D8D41D417D8E00123472 /* Photos.framework */; }; 4521C3C01F59F3BA00B4C582 /* TextFieldHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4521C3BF1F59F3BA00B4C582 /* TextFieldHelper.swift */; }; 452EC6DF205E9E30000E787C /* MediaGalleryViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 452EC6DE205E9E30000E787C /* MediaGalleryViewController.swift */; }; - 4535186B1FC635DD00210559 /* ShareViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4535186A1FC635DD00210559 /* ShareViewController.swift */; }; 4535186E1FC635DD00210559 /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 4535186C1FC635DD00210559 /* MainInterface.storyboard */; }; 453518721FC635DD00210559 /* SessionShareExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 453518681FC635DD00210559 /* SessionShareExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 4539B5861F79348F007141FF /* PushRegistrationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4539B5851F79348F007141FF /* PushRegistrationManager.swift */; }; @@ -977,14 +974,12 @@ 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 = ""; }; - 3461284A1FD0B93F00532771 /* SAELoadViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SAELoadViewController.swift; 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 = ""; }; - 347850561FD86544007B8332 /* SAEFailedViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SAEFailedViewController.swift; sourceTree = ""; }; 3488F9352191CC4000E524CC /* MediaView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaView.swift; sourceTree = ""; }; 3496744E2076ACCE00080B5F /* LongTextViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LongTextViewController.swift; sourceTree = ""; }; 34969559219B605E00DCFE74 /* ImagePickerController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImagePickerController.swift; sourceTree = ""; }; @@ -1045,7 +1040,6 @@ 4521C3BF1F59F3BA00B4C582 /* TextFieldHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextFieldHelper.swift; sourceTree = ""; }; 452EC6DE205E9E30000E787C /* MediaGalleryViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaGalleryViewController.swift; sourceTree = ""; }; 453518681FC635DD00210559 /* SessionShareExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = SessionShareExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; - 4535186A1FC635DD00210559 /* ShareViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareViewController.swift; sourceTree = ""; }; 4535186D1FC635DD00210559 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = ""; }; 4535186F1FC635DD00210559 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 4539B5851F79348F007141FF /* PushRegistrationManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PushRegistrationManager.swift; sourceTree = ""; }; @@ -1987,7 +1981,6 @@ isa = PBXGroup; children = ( C31C21A4255BCA4800EC2D66 /* Meta */, - C3ADC65F264265D9005F1414 /* Deprecated */, 4535186C1FC635DD00210559 /* MainInterface.storyboard */, 34641E1D2088DA6C00E2EDE5 /* SAEScreenLockViewController.h */, 34641E1E2088DA6D00E2EDE5 /* SAEScreenLockViewController.m */, @@ -3161,16 +3154,6 @@ path = "File Server"; sourceTree = ""; }; - C3ADC65F264265D9005F1414 /* Deprecated */ = { - isa = PBXGroup; - children = ( - 347850561FD86544007B8332 /* SAEFailedViewController.swift */, - 3461284A1FD0B93F00532771 /* SAELoadViewController.swift */, - 4535186A1FC635DD00210559 /* ShareViewController.swift */, - ); - path = Deprecated; - sourceTree = ""; - }; C3BBE0B32554F0D30050F1E3 /* Utilities */ = { isa = PBXGroup; children = ( @@ -4423,13 +4406,10 @@ buildActionMask = 2147483647; files = ( B817AD9A26436593009DF825 /* SimplifiedConversationCell.swift in Sources */, - 4535186B1FC635DD00210559 /* ShareViewController.swift in Sources */, 34480B361FD0929200BC14EF /* ShareAppExtensionContext.m in Sources */, C3ADC66126426688005F1414 /* ShareVC.swift in Sources */, 34641E1F2088DA6D00E2EDE5 /* SAEScreenLockViewController.m in Sources */, - 3461284B1FD0B94000532771 /* SAELoadViewController.swift in Sources */, B817AD9C26436F73009DF825 /* ThreadPickerVC.swift in Sources */, - 347850571FD86544007B8332 /* SAEFailedViewController.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/SessionShareExtension/Deprecated/SAEFailedViewController.swift b/SessionShareExtension/Deprecated/SAEFailedViewController.swift deleted file mode 100644 index 767a65d0c..000000000 --- a/SessionShareExtension/Deprecated/SAEFailedViewController.swift +++ /dev/null @@ -1,96 +0,0 @@ -// -// Copyright (c) 2018 Open Whisper Systems. All rights reserved. -// - -import UIKit -import PureLayout - -// All Observer methods will be invoked from the main thread. -protocol SAEFailedViewDelegate: class { - func shareViewWasCancelled() -} - -class SAEFailedViewControllerOld: UIViewController { - - weak var delegate: SAEFailedViewDelegate? - - let failureTitle: String - let failureMessage: String - - // MARK: Initializers and Factory Methods - - init(delegate: SAEFailedViewDelegate, title: String, message: String) { - self.delegate = delegate - self.failureTitle = title - self.failureMessage = message - super.init(nibName: nil, bundle: nil) - } - - @available(*, unavailable, message:"use other constructor instead.") - required init?(coder aDecoder: NSCoder) { - notImplemented() - } - - override func loadView() { - super.loadView() - - self.navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, - target: self, - action: #selector(cancelPressed)) - self.navigationItem.title = "Session" - - self.view.backgroundColor = UIColor.ows_signalBrandBlue - - let logoImage = UIImage(named: "logoSignal") - let logoImageView = UIImageView(image: logoImage) - self.view.addSubview(logoImageView) - logoImageView.autoCenterInSuperview() - let logoSize = CGFloat(120) - logoImageView.autoSetDimension(.width, toSize: logoSize) - logoImageView.autoSetDimension(.height, toSize: logoSize) - - let titleLabel = UILabel() - titleLabel.textColor = UIColor.white - titleLabel.font = UIFont.ows_mediumFont(withSize: 18) - titleLabel.text = failureTitle - titleLabel.textAlignment = .center - titleLabel.numberOfLines = 0 - titleLabel.lineBreakMode = .byWordWrapping - self.view.addSubview(titleLabel) - titleLabel.autoPinEdge(toSuperviewEdge: .leading, withInset: 20) - titleLabel.autoPinEdge(toSuperviewEdge: .trailing, withInset: 20) - titleLabel.autoPinEdge(.top, to: .bottom, of: logoImageView, withOffset: 25) - - let messageLabel = UILabel() - messageLabel.textColor = UIColor.white - messageLabel.font = UIFont.ows_regularFont(withSize: 14) - messageLabel.text = failureMessage - messageLabel.textAlignment = .center - messageLabel.numberOfLines = 0 - messageLabel.lineBreakMode = .byWordWrapping - self.view.addSubview(messageLabel) - messageLabel.autoPinEdge(toSuperviewEdge: .leading, withInset: 20) - messageLabel.autoPinEdge(toSuperviewEdge: .trailing, withInset: 20) - messageLabel.autoPinEdge(.top, to: .bottom, of: titleLabel, withOffset: 10) - } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - - self.navigationController?.isNavigationBarHidden = false - } - - override func viewDidDisappear(_ animated: Bool) { - super.viewDidDisappear(animated) - } - - // MARK: - Event Handlers - - @objc func cancelPressed(sender: UIButton) { - guard let delegate = delegate else { - owsFailDebug("missing delegate") - return - } - delegate.shareViewWasCancelled() - } -} diff --git a/SessionShareExtension/Deprecated/SAELoadViewController.swift b/SessionShareExtension/Deprecated/SAELoadViewController.swift deleted file mode 100644 index 6102c9b7b..000000000 --- a/SessionShareExtension/Deprecated/SAELoadViewController.swift +++ /dev/null @@ -1,110 +0,0 @@ -// -// Copyright (c) 2019 Open Whisper Systems. All rights reserved. -// - -import UIKit -import PureLayout - -class SAELoadViewControllerOld: UIViewController { - - weak var delegate: ShareViewDelegate? - - var activityIndicator: UIActivityIndicatorView! - var progressView: UIProgressView! - - var progress: Progress? { - didSet { - guard progressView != nil else { - return - } - - updateProgressViewVisability() - progressView.observedProgress = progress - } - } - - func updateProgressViewVisability() { - guard progressView != nil, activityIndicator != nil else { - return - } - - // Prefer to show progress view when progress is present - if self.progress == nil { - activityIndicator.startAnimating() - self.progressView.isHidden = true - self.activityIndicator.isHidden = false - } else { - activityIndicator.stopAnimating() - self.progressView.isHidden = false - self.activityIndicator.isHidden = true - } - } - - // MARK: Initializers and Factory Methods - - init(delegate: ShareViewDelegate) { - self.delegate = delegate - super.init(nibName: nil, bundle: nil) - } - - @available(*, unavailable, message:"use other constructor instead.") - required init?(coder aDecoder: NSCoder) { - notImplemented() - } - - override func loadView() { - super.loadView() - - self.navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, - target: self, - action: #selector(cancelPressed)) - self.navigationItem.title = "Session" - - self.view.backgroundColor = UIColor.ows_signalBrandBlue - - let activityIndicator = UIActivityIndicatorView(style: .whiteLarge) - self.activityIndicator = activityIndicator - self.view.addSubview(activityIndicator) - activityIndicator.autoCenterInSuperview() - - progressView = UIProgressView(progressViewStyle: .default) - progressView.observedProgress = progress - - self.view.addSubview(progressView) - progressView.autoVCenterInSuperview() - progressView.autoPinWidthToSuperview(withMargin: ScaleFromIPhone5(30)) - progressView.progressTintColor = UIColor.white - - updateProgressViewVisability() - - let label = UILabel() - label.textColor = UIColor.white - label.font = UIFont.ows_mediumFont(withSize: 18) - label.text = NSLocalizedString("SHARE_EXTENSION_LOADING", - comment: "Indicates that the share extension is still loading.") - self.view.addSubview(label) - label.autoHCenterInSuperview() - label.autoPinEdge(.top, to: .bottom, of: activityIndicator, withOffset: 25) - } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - - self.navigationController?.isNavigationBarHidden = false - } - - override func viewDidDisappear(_ animated: Bool) { - super.viewDidDisappear(animated) - - } - - // MARK: - Event Handlers - - @objc func cancelPressed(sender: UIButton) { - guard let delegate = delegate else { - owsFailDebug("missing delegate") - return - } - delegate.shareViewWasCancelled() - } -} diff --git a/SessionShareExtension/Deprecated/ShareViewController.swift b/SessionShareExtension/Deprecated/ShareViewController.swift deleted file mode 100644 index 09d353f44..000000000 --- a/SessionShareExtension/Deprecated/ShareViewController.swift +++ /dev/null @@ -1,1063 +0,0 @@ -// -// Copyright (c) 2019 Open Whisper Systems. All rights reserved. -// - -import UIKit - -import PureLayout -import PromiseKit -import SessionUIKit -import CoreServices - -@objc -public class ShareViewControllerOld: UIViewController, ShareViewDelegate, SAEFailedViewDelegate, AppModeManagerDelegate { - - // MARK: - Dependencies - - private var tsAccountManager: TSAccountManager { - return TSAccountManager.sharedInstance() - } - - // MARK: - - - enum ShareViewControllerError: Error { - case assertionError(description: String) - case unsupportedMedia - case notRegistered - case obsoleteShare - } - - private var hasInitialRootViewController = false - private var isReadyForAppExtensions = false - private var areVersionMigrationsComplete = false - - private var progressPoller: ProgressPoller? - var loadViewController: SAELoadViewControllerOld? - - private var shareViewNavigationController: OWSNavigationController? - - override open func loadView() { - super.loadView() - - // This should be the first thing we do. - let appContext = ShareAppExtensionContext(rootViewController: self) - SetCurrentAppContext(appContext) - - AppModeManager.configure(delegate: self) - - DebugLogger.shared().enableTTYLogging() - if _isDebugAssertConfiguration() { - DebugLogger.shared().enableFileLogging() - } else if OWSPreferences.isLoggingEnabled() { - DebugLogger.shared().enableFileLogging() - } - - Logger.info("") - - _ = AppVersion.sharedInstance() - - startupLogging() - - Cryptography.seedRandom() - - // We don't need to use DeviceSleepManager in the SAE. - - // We don't need to use applySignalAppearence in the SAE. - - if CurrentAppContext().isRunningTests { - // TODO: Do we need to implement isRunningTests in the SAE context? - return - } - - // If we haven't migrated the database file to the shared data - // directory we can't load it, and therefore can't init TSSPrimaryStorage, - // and therefore don't want to setup most of our machinery (Environment, - // most of the singletons, etc.). We just want to show an error view and - // abort. - isReadyForAppExtensions = OWSPreferences.isReadyForAppExtensions() - guard isReadyForAppExtensions else { - showNotReadyView() - return - } - - // We shouldn't set up our environment until after we've consulted isReadyForAppExtensions. - AppSetup.setupEnvironment(appSpecificSingletonBlock: { - SSKEnvironment.shared.notificationsManager = NoopNotificationsManager() - }, migrationCompletion: { [weak self] in - AssertIsOnMainThread() - - guard let strongSelf = self else { return } - - // performUpdateCheck must be invoked after Environment has been initialized because - // upgrade process may depend on Environment. - strongSelf.versionMigrationsDidComplete() - }) - - let shareViewNavigationController = OWSNavigationController() - self.shareViewNavigationController = shareViewNavigationController - - let loadViewController = SAELoadViewControllerOld(delegate: self) - self.loadViewController = loadViewController - - // Don't display load screen immediately, in hopes that we can avoid it altogether. - after(seconds: 0.5).done { [weak self] in - AssertIsOnMainThread() - - guard let strongSelf = self else { return } - guard strongSelf.presentedViewController == nil else { - Logger.debug("setup completed quickly, no need to present load view controller.") - return - } - - Logger.debug("setup is slow - showing loading screen") - strongSelf.showPrimaryViewController(loadViewController) - }.retainUntilComplete() - - // 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(registrationStateDidChange), - name: .RegistrationStateDidChange, - object: nil) - NotificationCenter.default.addObserver(self, - selector: #selector(owsApplicationWillEnterForeground), - name: .OWSApplicationWillEnterForeground, - object: nil) - NotificationCenter.default.addObserver(self, - selector: #selector(applicationDidEnterBackground), - name: .OWSApplicationDidEnterBackground, - object: nil) - - Logger.info("completed.") - } - - deinit { - Logger.info("deinit") - NotificationCenter.default.removeObserver(self) - - // Share extensions reside in a process that may be reused between usages. - // That isn't safe; the codebase is full of statics (e.g. singletons) which - // we can't easily clean up. - ExitShareExtension() - } - - @objc - public func applicationDidEnterBackground() { - AssertIsOnMainThread() - - Logger.info("") - - if OWSScreenLock.shared.isScreenLockEnabled() { - - Logger.info("dismissing.") - - self.dismiss(animated: false) { [weak self] in - AssertIsOnMainThread() - guard let strongSelf = self else { return } - strongSelf.extensionContext!.completeRequest(returningItems: [], completionHandler: nil) - } - } - } - - private func activate() { - AssertIsOnMainThread() - - Logger.debug("") - - // We don't need to use "screen protection" in the SAE. - - ensureRootViewController() - - // We don't need to use RTCInitializeSSL() in the SAE. - - if tsAccountManager.isRegistered() { - // At this point, potentially lengthy DB locking migrations could be running. - // Avoid blocking app launch by putting all further possible DB access in async block - DispatchQueue.global().async { [weak self] in - guard let _ = self else { return } - Logger.info("running post launch block for registered user: \(TSAccountManager.localNumber()!)") - - // We don't need to use OWSDisappearingMessagesJob in the SAE. - - // We don't need to use OWSFailedMessagesJob in the SAE. - - // We don't need to use OWSFailedAttachmentDownloadsJob in the SAE. - } - } else { - Logger.info("running post launch block for unregistered user.") - - // We don't need to update the app icon badge number in the SAE. - - // We don't need to prod the TSSocketManager in the SAE. - } - - if tsAccountManager.isRegistered() { - DispatchQueue.main.async { [weak self] in - guard let _ = self else { return } - Logger.info("running post launch block for registered user: \(TSAccountManager.localNumber()!)") - - // We don't need to use the TSSocketManager in the SAE. - - // We don't need to fetch messages in the SAE. - - // We don't need to use OWSSyncPushTokensJob in the SAE. - } - } - } - - @objc - func versionMigrationsDidComplete() { - AssertIsOnMainThread() - - Logger.debug("") - - areVersionMigrationsComplete = true - - checkIsAppReady() - } - - @objc - func storageIsReady() { - AssertIsOnMainThread() - - Logger.debug("") - - checkIsAppReady() - } - - @objc - func checkIsAppReady() { - 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 !AppReadiness.isAppReady() else { - // Only mark the app as ready once. - return - } - - SignalUtilitiesKit.Configuration.performMainSetup() - - Logger.debug("") - - // TODO: Once "app ready" logic is moved into AppSetup, move this line there. - OWSProfileManager.shared().ensureLocalProfileCached() - - // Note that this does much more than set a flag; - // it will also run all deferred blocks. - AppReadiness.setAppIsReady() - - if tsAccountManager.isRegistered() { - Logger.info("localNumber: \(TSAccountManager.localNumber()!)") - - // 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() - - ensureRootViewController() - - // We don't need to use OWSMessageReceiver in the SAE. - // We don't need to use OWSBatchMessageProcessor in the SAE. - - OWSProfileManager.shared().ensureLocalProfileCached() - - // 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() - } - - @objc - func registrationStateDidChange() { - AssertIsOnMainThread() - - Logger.debug("") - - if tsAccountManager.isRegistered() { - Logger.info("localNumber: \(TSAccountManager.localNumber()!)") - - // We don't need to use ExperienceUpgradeFinder in the SAE. - - // We don't need to use OWSDisappearingMessagesJob in the SAE. - - OWSProfileManager.shared().ensureLocalProfileCached() - } - } - - private func ensureRootViewController() { - AssertIsOnMainThread() - - Logger.debug("") - - guard AppReadiness.isAppReady() else { - return - } - guard !hasInitialRootViewController else { - return - } - hasInitialRootViewController = true - - Logger.info("Presenting initial root view controller") - - if OWSScreenLock.shared.isScreenLockEnabled() { - presentScreenLock() - } else { - presentContentView() - } - } - - private func presentContentView() { - AssertIsOnMainThread() - - Logger.debug("") - - Logger.info("Presenting content view") - - if !tsAccountManager.isRegistered() { - showNotRegisteredView() - } else if !OWSProfileManager.shared().localProfileExists() { - // This is a rare edge case, but we want to ensure that the user - // is has already saved their local profile key in the main app. - showNotReadyView() - } else { - DispatchQueue.main.async { [weak self] in - guard let strongSelf = self else { return } - strongSelf.buildAttachmentsAndPresentConversationPicker() - } - } - - // We don't use the AppUpdateNag in the SAE. - } - - func startupLogging() { - Logger.info("iOS Version: \(UIDevice.current.systemVersion)}") - - let locale = NSLocale.current as NSLocale - if let localeIdentifier = locale.object(forKey: NSLocale.Key.identifier) as? String, - localeIdentifier.count > 0 { - Logger.info("Locale Identifier: \(localeIdentifier)") - } else { - owsFailDebug("Locale Identifier: Unknown") - } - if let countryCode = locale.object(forKey: NSLocale.Key.countryCode) as? String, - countryCode.count > 0 { - Logger.info("Country Code: \(countryCode)") - } else { - owsFailDebug("Country Code: Unknown") - } - if let languageCode = locale.object(forKey: NSLocale.Key.languageCode) as? String, - languageCode.count > 0 { - Logger.info("Language Code: \(languageCode)") - } else { - owsFailDebug("Language Code: Unknown") - } - } - - // MARK: Error Views - - private func showNotReadyView() { - AssertIsOnMainThread() - - let failureTitle = NSLocalizedString("SHARE_EXTENSION_NOT_YET_MIGRATED_TITLE", - comment: "Title indicating that the share extension cannot be used until the main app has been launched at least once.") - let failureMessage = NSLocalizedString("SHARE_EXTENSION_NOT_YET_MIGRATED_MESSAGE", - comment: "Message indicating that the share extension cannot be used until the main app has been launched at least once.") - showErrorView(title: failureTitle, message: failureMessage) - } - - private func showNotRegisteredView() { - AssertIsOnMainThread() - - let failureTitle = NSLocalizedString("SHARE_EXTENSION_NOT_REGISTERED_TITLE", - comment: "Title indicating that the share extension cannot be used until the user has registered in the main app.") - let failureMessage = NSLocalizedString("SHARE_EXTENSION_NOT_REGISTERED_MESSAGE", - comment: "Message indicating that the share extension cannot be used until the user has registered in the main app.") - showErrorView(title: failureTitle, message: failureMessage) - } - - private func showErrorView(title: String, message: String) { - AssertIsOnMainThread() - - let viewController = SAEFailedViewControllerOld(delegate: self, title: title, message: message) - self.showPrimaryViewController(viewController) - } - - // MARK: View Lifecycle - - override open func viewDidLoad() { - super.viewDidLoad() - - Logger.debug("") - - if isReadyForAppExtensions { - AppReadiness.runNowOrWhenAppDidBecomeReady { [weak self] in - AssertIsOnMainThread() - guard let strongSelf = self else { return } - strongSelf.activate() - } - } - } - - override open func viewWillAppear(_ animated: Bool) { - Logger.debug("") - - super.viewWillAppear(animated) - } - - override open func viewDidAppear(_ animated: Bool) { - Logger.debug("") - - super.viewDidAppear(animated) - } - - override open func viewWillDisappear(_ animated: Bool) { - Logger.debug("") - - super.viewWillDisappear(animated) - - Logger.flush() - } - - override open func viewDidDisappear(_ animated: Bool) { - Logger.debug("") - - super.viewDidDisappear(animated) - - Logger.flush() - - // Share extensions reside in a process that may be reused between usages. - // That isn't safe; the codebase is full of statics (e.g. singletons) which - // we can't easily clean up. - ExitShareExtension() - } - - @objc - func owsApplicationWillEnterForeground() throws { - AssertIsOnMainThread() - - Logger.debug("") - - // If a user unregisters in the main app, the SAE should shut down - // immediately. - guard !tsAccountManager.isRegistered() else { - // If user is registered, do nothing. - return - } - guard let shareViewNavigationController = shareViewNavigationController else { - owsFailDebug("Missing shareViewNavigationController") - return - } - guard let firstViewController = shareViewNavigationController.viewControllers.first else { - // If no view has been presented yet, do nothing. - return - } - if let _ = firstViewController as? SAEFailedViewControllerOld { - // If root view is an error view, do nothing. - return - } - throw ShareViewControllerError.notRegistered - } - - // MARK: ShareViewDelegate, SAEFailedViewDelegate - - public func shareViewWasUnlocked() { - Logger.info("") - - presentContentView() - } - - public func shareViewWasCompleted() { - Logger.info("") - - self.dismiss(animated: true) { [weak self] in - AssertIsOnMainThread() - guard let strongSelf = self else { return } - strongSelf.extensionContext!.completeRequest(returningItems: [], completionHandler: nil) - } - } - - public func shareViewWasCancelled() { - Logger.info("") - - self.dismiss(animated: true) { [weak self] in - AssertIsOnMainThread() - guard let strongSelf = self else { return } - strongSelf.extensionContext!.completeRequest(returningItems: [], completionHandler: nil) - } - } - - public func shareViewFailed(error: Error) { - Logger.info("") - - self.dismiss(animated: true) { [weak self] in - AssertIsOnMainThread() - guard let strongSelf = self else { return } - strongSelf.extensionContext!.cancelRequest(withError: error) - } - } - - // MARK: Helpers - - // This view controller is not visible to the user. It exists to intercept touches, set up the - // extensions dependencies, and eventually present a visible view to the user. - // For speed of presentation, we only present a single modal, and if it's already been presented - // we swap out the contents. - // e.g. if loading is taking a while, the user will see the load screen presented with a modal - // animation. Next, when loading completes, the load view will be switched out for the contact - // picker view. - private func showPrimaryViewController(_ viewController: UIViewController) { - AssertIsOnMainThread() - - guard let shareViewNavigationController = shareViewNavigationController else { - owsFailDebug("Missing shareViewNavigationController") - return - } - shareViewNavigationController.setViewControllers([viewController], animated: false) - if self.presentedViewController == nil { - Logger.debug("presenting modally: \(viewController)") - self.present(shareViewNavigationController, animated: true) - } else { - Logger.debug("modal already presented. swapping modal content for: \(viewController)") - assert(self.presentedViewController == shareViewNavigationController) - } - } - - private func buildAttachmentsAndPresentConversationPicker() { - AssertIsOnMainThread() - - self.buildAttachments().map { [weak self] attachments in - AssertIsOnMainThread() - guard let strongSelf = self else { return } - - strongSelf.progressPoller = nil - strongSelf.loadViewController = nil - - let conversationPicker = SharingThreadPickerViewController(shareViewDelegate: strongSelf) - Logger.debug("presentConversationPicker: \(conversationPicker)") - conversationPicker.attachments = attachments - strongSelf.showPrimaryViewController(conversationPicker) - Logger.info("showing picker with attachments: \(attachments)") - }.catch { [weak self] error in - AssertIsOnMainThread() - guard let strongSelf = self else { return } - - let alertTitle = NSLocalizedString("SHARE_EXTENSION_UNABLE_TO_BUILD_ATTACHMENT_ALERT_TITLE", - comment: "Shown when trying to share content to a Signal user for the share extension. Followed by failure details.") - OWSAlerts.showAlert(title: alertTitle, - message: error.localizedDescription, - buttonTitle: CommonStrings.cancelButton) { _ in - strongSelf.shareViewWasCancelled() - } - owsFailDebug("building attachment failed with error: \(error)") - }.retainUntilComplete() - } - - private func presentScreenLock() { - AssertIsOnMainThread() - - let screenLockUI = SAEScreenLockViewController(shareViewDelegate: self) - Logger.debug("presentScreenLock: \(screenLockUI)") - showPrimaryViewController(screenLockUI) - Logger.info("showing screen lock") - } - - private class func itemMatchesSpecificUtiType(itemProvider: NSItemProvider, utiType: String) -> Bool { - // URLs, contacts and other special items have to be detected separately. - // Many shares (e.g. pdfs) will register many UTI types and/or conform to kUTTypeData. - guard itemProvider.registeredTypeIdentifiers.count == 1 else { - return false - } - guard let firstUtiType = itemProvider.registeredTypeIdentifiers.first else { - return false - } - return firstUtiType == utiType - } - - private class func isVisualMediaItem(itemProvider: NSItemProvider) -> Bool { - 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) - } - - private class func isContactItem(itemProvider: NSItemProvider) -> Bool { - return itemMatchesSpecificUtiType(itemProvider: itemProvider, - utiType: kUTTypeContact as String) - } - - private class func utiType(itemProvider: NSItemProvider) -> String? { - Logger.info("utiTypeForItem: \(itemProvider.registeredTypeIdentifiers)") - - if isUrlItem(itemProvider: itemProvider) { - return kUTTypeURL as String - } else if isContactItem(itemProvider: itemProvider) { - return kUTTypeContact as String - } - - // Use the first UTI that conforms to "data". - let matchingUtiType = itemProvider.registeredTypeIdentifiers.first { (utiType: String) -> Bool in - UTTypeConformsTo(utiType as CFString, kUTTypeData) - } - return matchingUtiType - } - - private class func createDataSource(utiType: String, url: URL, customFileName: String?) -> DataSource? { - if utiType == (kUTTypeURL as String) { - // Share URLs as oversize text messages whose text content is the URL. - // - // NOTE: SharingThreadPickerViewController will try to unpack them - // 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) { - // 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 - } - } - - private class func preferredItemProviders(inputItem: NSExtensionItem) -> [NSItemProvider]? { - 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 { - hasNonVisualMedia = true - } - } - // Only allow multiple-attachment sends if all attachments - // are visual media. - if visualMediaItemProviders.count > 0 && !hasNonVisualMedia { - return visualMediaItemProviders - } - - // A single inputItem can have multiple attachments, e.g. sharing from Firefox gives - // one url attachment and another text attachment, where the the url would be https://some-news.com/articles/123-cat-stuck-in-tree - // and the text attachment would be something like "Breaking news - cat stuck in tree" - // - // FIXME: For now, we prefer the URL provider and discard the text provider, since it's more useful to share the URL than the caption - // but we *should* include both. This will be a bigger change though since our share extension is currently heavily predicated - // on one itemProvider per share. - - // Prefer a URL provider if available - if let preferredAttachment = attachments.first(where: { (attachment: Any) -> Bool in - guard let itemProvider = attachment as? NSItemProvider else { - return false - } - return isUrlItem(itemProvider: itemProvider) - }) { - return [preferredAttachment] - } - - // else return whatever is available - if let itemProvider = inputItem.attachments?.first { - return [itemProvider] - } else { - owsFailDebug("Missing attachment.") - } - return [] - } - - private func selectItemProviders() -> Promise<[NSItemProvider]> { - guard let inputItems = self.extensionContext?.inputItems else { - let error = ShareViewControllerError.assertionError(description: "no input item") - return Promise(error: error) - } - - for inputItemRaw in inputItems { - guard let inputItem = inputItemRaw as? NSExtensionItem else { - Logger.error("invalid inputItem \(inputItemRaw)") - continue - } - if let itemProviders = ShareViewControllerOld.preferredItemProviders(inputItem: inputItem) { - return Promise.value(itemProviders) - } - } - let error = ShareViewControllerError.assertionError(description: "no input item") - return Promise(error: error) - } - - private - struct LoadedItem { - let itemProvider: NSItemProvider - let itemUrl: URL - let utiType: String - - var customFileName: String? - var isConvertibleToTextMessage = false - var isConvertibleToContactShare = false - - init(itemProvider: NSItemProvider, - itemUrl: URL, - utiType: String, - customFileName: String? = nil, - isConvertibleToTextMessage: Bool = false, - isConvertibleToContactShare: Bool = false) { - self.itemProvider = itemProvider - self.itemUrl = itemUrl - self.utiType = utiType - self.customFileName = customFileName - self.isConvertibleToTextMessage = isConvertibleToTextMessage - self.isConvertibleToContactShare = isConvertibleToContactShare - } - } - - private func loadItemProvider(itemProvider: NSItemProvider) -> Promise { - Logger.info("attachment: \(itemProvider)") - - // We need to be very careful about which UTI type we use. - // - // * In the case of "textual" shares (e.g. web URLs and text snippets), we want to - // coerce the UTI type to kUTTypeURL or kUTTypeText. - // * We want to treat shared files as file attachments. Therefore we do not - // want to treat file URLs like web URLs. - // * UTIs aren't very descriptive (there are far more MIME types than UTI types) - // so in the case of file attachments we try to refine the attachment type - // using the file extension. - guard let srcUtiType = ShareViewControllerOld.utiType(itemProvider: itemProvider) else { - let error = ShareViewControllerError.unsupportedMedia - return Promise(error: error) - } - Logger.debug("matched utiType: \(srcUtiType)") - - let (promise, resolver) = Promise.pending() - - let loadCompletion: NSItemProvider.CompletionHandler = { [weak self] - (value, error) in - - guard let _ = self else { return } - guard error == nil else { - resolver.reject(error!) - return - } - - guard let value = value else { - let missingProviderError = ShareViewControllerError.assertionError(description: "missing item provider") - resolver.reject(missingProviderError) - return - } - - Logger.info("value type: \(type(of: value))") - - if let data = value as? Data { - let customFileName = "Contact.vcf" - - let customFileExtension = MIMETypeUtil.fileExtension(forUTIType: srcUtiType) - guard let tempFilePath = OWSFileSystem.writeData(toTemporaryFile: data, fileExtension: customFileExtension) else { - let writeError = ShareViewControllerError.assertionError(description: "Error writing item data: \(String(describing: error))") - resolver.reject(writeError) - return - } - let fileUrl = URL(fileURLWithPath: tempFilePath) - resolver.fulfill(LoadedItem(itemProvider: itemProvider, - itemUrl: fileUrl, - utiType: srcUtiType, - customFileName: customFileName, - isConvertibleToContactShare: false)) - } else if let string = value as? String { - Logger.debug("string provider: \(string)") - guard let data = string.filterStringForDisplay().data(using: String.Encoding.utf8) else { - let writeError = ShareViewControllerError.assertionError(description: "Error writing item data: \(String(describing: error))") - resolver.reject(writeError) - return - } - guard let tempFilePath = OWSFileSystem.writeData(toTemporaryFile: data, fileExtension: "txt") else { - let writeError = ShareViewControllerError.assertionError(description: "Error writing item data: \(String(describing: error))") - resolver.reject(writeError) - return - } - - let fileUrl = URL(fileURLWithPath: tempFilePath) - - let isConvertibleToTextMessage = !itemProvider.registeredTypeIdentifiers.contains(kUTTypeFileURL as String) - - if UTTypeConformsTo(srcUtiType as CFString, kUTTypeText) { - resolver.fulfill(LoadedItem(itemProvider: itemProvider, - itemUrl: fileUrl, - utiType: srcUtiType, - isConvertibleToTextMessage: isConvertibleToTextMessage)) - } else { - resolver.fulfill(LoadedItem(itemProvider: itemProvider, - itemUrl: fileUrl, - utiType: kUTTypeText as String, - isConvertibleToTextMessage: isConvertibleToTextMessage)) - } - } else if let url = value as? URL { - // If the share itself is a URL (e.g. a link from Safari), try to send this as a text message. - let isConvertibleToTextMessage = (itemProvider.registeredTypeIdentifiers.contains(kUTTypeURL as String) && - !itemProvider.registeredTypeIdentifiers.contains(kUTTypeFileURL as String)) - if isConvertibleToTextMessage { - resolver.fulfill(LoadedItem(itemProvider: itemProvider, - itemUrl: url, - utiType: kUTTypeURL as String, - isConvertibleToTextMessage: isConvertibleToTextMessage)) - } else { - resolver.fulfill(LoadedItem(itemProvider: itemProvider, - itemUrl: url, - utiType: srcUtiType, - isConvertibleToTextMessage: isConvertibleToTextMessage)) - } - } else if let image = value as? UIImage { - if let data = image.pngData() { - let tempFilePath = OWSFileSystem.temporaryFilePath(withFileExtension: "png") - do { - let url = NSURL.fileURL(withPath: tempFilePath) - try data.write(to: url) - resolver.fulfill(LoadedItem(itemProvider: itemProvider, itemUrl: url, - utiType: srcUtiType)) - } catch { - resolver.reject(ShareViewControllerError.assertionError(description: "couldn't write UIImage: \(String(describing: error))")) - } - } else { - resolver.reject(ShareViewControllerError.assertionError(description: "couldn't convert UIImage to PNG: \(String(describing: error))")) - } - } else { - // It's unavoidable that we may sometimes receives data types that we - // don't know how to handle. - let unexpectedTypeError = ShareViewControllerError.assertionError(description: "unexpected value: \(String(describing: value))") - resolver.reject(unexpectedTypeError) - } - } - - itemProvider.loadItem(forTypeIdentifier: srcUtiType, options: nil, completionHandler: loadCompletion) - - return promise - } - - private func buildAttachment(forLoadedItem loadedItem: LoadedItem) -> Promise { - let itemProvider = loadedItem.itemProvider - let itemUrl = loadedItem.itemUrl - let utiType = loadedItem.utiType - - var url = itemUrl - do { - if isVideoNeedingRelocation(itemProvider: itemProvider, itemUrl: itemUrl) { - url = try SignalAttachment.copyToVideoTempDir(url: itemUrl) - } - } catch { - let error = ShareViewControllerError.assertionError(description: "Could not copy video") - return Promise(error: error) - } - - Logger.debug("building DataSource with url: \(url), utiType: \(utiType)") - - guard let dataSource = ShareViewControllerOld.createDataSource(utiType: utiType, url: url, customFileName: loadedItem.customFileName) else { - let error = ShareViewControllerError.assertionError(description: "Unable to read attachment data") - return Promise(error: error) - } - - // start with base utiType, but it might be something generic like "image" - var specificUTIType = utiType - if utiType == (kUTTypeURL as String) { - // Use kUTTypeURL for URLs. - } else if UTTypeConformsTo(utiType as CFString, kUTTypeText) { - // Use kUTTypeText for text. - } else if url.pathExtension.count > 0 { - // Determine a more specific utiType based on file extension - if let typeExtension = MIMETypeUtil.utiType(forFileExtension: url.pathExtension) { - Logger.debug("utiType based on extension: \(typeExtension)") - specificUTIType = typeExtension - } - } - - guard !SignalAttachment.isInvalidVideo(dataSource: dataSource, dataUTI: specificUTIType) else { - // This can happen, e.g. when sharing a quicktime-video from iCloud drive. - - let (promise, exportSession) = SignalAttachment.compressVideoAsMp4(dataSource: dataSource, dataUTI: specificUTIType) - - // TODO: How can we move waiting for this export to the end of the share flow rather than having to do it up front? - // Ideally we'd be able to start it here, and not block the UI on conversion unless there's still work to be done - // when the user hits "send". - if let exportSession = exportSession { - let progressPoller = ProgressPoller(timeInterval: 0.1, ratioCompleteBlock: { return exportSession.progress }) - self.progressPoller = progressPoller - progressPoller.startPolling() - - guard let loadViewController = self.loadViewController else { - owsFailDebug("load view controller was unexpectedly nil") - return promise - } - - DispatchQueue.main.async { - loadViewController.progress = progressPoller.progress - } - } - - return promise - } - - let attachment = SignalAttachment.attachment(dataSource: dataSource, dataUTI: specificUTIType, imageQuality: .medium) - if loadedItem.isConvertibleToContactShare { - Logger.info("isConvertibleToContactShare") - attachment.isConvertibleToContactShare = true - } else if loadedItem.isConvertibleToTextMessage { - Logger.info("isConvertibleToTextMessage") - attachment.isConvertibleToTextMessage = true - } - return Promise.value(attachment) - } - - private func buildAttachments() -> Promise<[SignalAttachment]> { - return selectItemProviders().then { [weak self] (itemProviders) -> Promise<[SignalAttachment]> in - guard let strongSelf = self else { - let error = ShareViewControllerError.assertionError(description: "expired") - return Promise(error: error) - } - - var loadPromises = [Promise]() - - for itemProvider in itemProviders.prefix(SignalAttachment.maxAttachmentsAllowed) { - let loadPromise = strongSelf.loadItemProvider(itemProvider: itemProvider) - .then({ (loadedItem) -> Promise in - return strongSelf.buildAttachment(forLoadedItem: loadedItem) - }) - - loadPromises.append(loadPromise) - } - return when(fulfilled: loadPromises) - }.map { (signalAttachments) -> [SignalAttachment] in - guard signalAttachments.count > 0 else { - let error = ShareViewControllerError.assertionError(description: "no valid attachments") - throw error - } - return signalAttachments - } - } - - // Some host apps (e.g. iOS Photos.app) sometimes auto-converts some video formats (e.g. com.apple.quicktime-movie) - // into mp4s as part of the NSItemProvider `loadItem` API. (Some files the Photo's app doesn't auto-convert) - // - // However, when using this url to the converted item, AVFoundation operations such as generating a - // preview image and playing the url in the AVMoviePlayer fails with an unhelpful error: "The operation could not be completed" - // - // We can work around this by first copying the media into our container. - // - // I don't understand why this is, and I haven't found any relevant documentation in the NSItemProvider - // or AVFoundation docs. - // - // Notes: - // - // These operations succeed when sending a video which initially existed on disk as an mp4. - // (e.g. Alice sends a video to Bob through the main app, which ensures it's an mp4. Bob saves it, then re-shares it) - // - // I *did* verify that the size and SHA256 sum of the original url matches that of the copied url. So there - // is no difference between the contents of the file, yet one works one doesn't. - // Perhaps the AVFoundation APIs require some extra file system permssion we don't have in the - // passed through URL. - private func isVideoNeedingRelocation(itemProvider: NSItemProvider, itemUrl: URL) -> Bool { - let pathExtension = itemUrl.pathExtension - guard pathExtension.count > 0 else { - Logger.verbose("item URL has no file extension: \(itemUrl).") - return false - } - guard let utiTypeForURL = MIMETypeUtil.utiType(forFileExtension: pathExtension) else { - Logger.verbose("item has unknown UTI type: \(itemUrl).") - return false - } - Logger.verbose("utiTypeForURL: \(utiTypeForURL)") - guard utiTypeForURL == kUTTypeMPEG4 as String else { - // Either it's not a video or it was a video which was not auto-converted to mp4. - // Not affected by the issue. - return false - } - - // If video file already existed on disk as an mp4, then the host app didn't need to - // apply any conversion, so no need to relocate the app. - return !itemProvider.registeredTypeIdentifiers.contains(kUTTypeMPEG4 as String) - } - - // 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 - } - - public func setCurrentAppMode(to appMode: AppMode) { - return // Not applicable to share extensions - } -} - -// Exposes a Progress object, whose progress is updated by polling the return of a given block -private class ProgressPoller: NSObject { - - let progress: Progress - private(set) var timer: Timer? - - // Higher number offers higher ganularity - let progressTotalUnitCount: Int64 = 10000 - private let timeInterval: Double - private let ratioCompleteBlock: () -> Float - - init(timeInterval: TimeInterval, ratioCompleteBlock: @escaping () -> Float) { - self.timeInterval = timeInterval - self.ratioCompleteBlock = ratioCompleteBlock - - self.progress = Progress() - - progress.totalUnitCount = progressTotalUnitCount - progress.completedUnitCount = Int64(ratioCompleteBlock() * Float(progressTotalUnitCount)) - } - - func startPolling() { - guard self.timer == nil else { - owsFailDebug("already started timer") - return - } - - self.timer = WeakTimer.scheduledTimer(timeInterval: timeInterval, target: self, userInfo: nil, repeats: true) { [weak self] (timer) in - guard let strongSelf = self else { - return - } - - let completedUnitCount = Int64(strongSelf.ratioCompleteBlock() * Float(strongSelf.progressTotalUnitCount)) - strongSelf.progress.completedUnitCount = completedUnitCount - - if completedUnitCount == strongSelf.progressTotalUnitCount { - Logger.debug("progress complete") - timer.invalidate() - } - } - } -}