From 4f3faa28bcc7677af9e47988248bd3830c627b66 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Tue, 11 Jan 2022 12:38:19 +1100 Subject: [PATCH 01/26] Refactored SessionShareExtension code to Swift --- Session.xcodeproj/project.pbxproj | 32 ++- .../SignalShareExtension-Bridging-Header.h | 2 - .../SAEScreenLockViewController.h | 18 -- .../SAEScreenLockViewController.m | 206 --------------- .../SAEScreenLockViewController.swift | 206 +++++++++++++++ .../ShareAppExtensionContext.h | 18 -- .../ShareAppExtensionContext.m | 240 ------------------ .../ShareAppExtensionContext.swift | 204 +++++++++++++++ SessionShareExtension/ShareVC.swift | 177 +++++++------ .../SimplifiedConversationCell.swift | 119 ++++++--- SessionShareExtension/ThreadPickerVC.swift | 129 ++++++---- .../General/ReusableView.swift | 16 ++ .../General/String+Localization.swift | 13 + .../General/UITableView+ReusableView.swift | 23 ++ .../Utilities/ShareViewDelegate.swift | 2 +- 15 files changed, 748 insertions(+), 657 deletions(-) delete mode 100644 SessionShareExtension/SAEScreenLockViewController.h delete mode 100644 SessionShareExtension/SAEScreenLockViewController.m create mode 100644 SessionShareExtension/SAEScreenLockViewController.swift delete mode 100644 SessionShareExtension/ShareAppExtensionContext.h delete mode 100644 SessionShareExtension/ShareAppExtensionContext.m create mode 100644 SessionShareExtension/ShareAppExtensionContext.swift create mode 100644 SessionUtilitiesKit/General/ReusableView.swift create mode 100644 SessionUtilitiesKit/General/String+Localization.swift create mode 100644 SessionUtilitiesKit/General/UITableView+ReusableView.swift diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 97ce6a4b5..a224cb1f3 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -21,10 +21,8 @@ 34330AA31E79686200DF2FB9 /* OWSProgressView.m in Sources */ = {isa = PBXBuildFile; fileRef = 34330AA21E79686200DF2FB9 /* OWSProgressView.m */; }; 34386A54207D271D009F5D9C /* NeverClearView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34386A53207D271C009F5D9C /* NeverClearView.swift */; }; 3441FD9F21A3604F00BB9542 /* BackupRestoreViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3441FD9E21A3604F00BB9542 /* BackupRestoreViewController.swift */; }; - 34480B361FD0929200BC14EF /* ShareAppExtensionContext.m in Sources */ = {isa = PBXBuildFile; fileRef = 34480B351FD0929200BC14EF /* ShareAppExtensionContext.m */; }; 344825C6211390C800DB4BD8 /* OWSOrphanDataCleaner.m in Sources */ = {isa = PBXBuildFile; fileRef = 344825C5211390C800DB4BD8 /* OWSOrphanDataCleaner.m */; }; 346129991FD1E4DA00532771 /* SignalApp.m in Sources */ = {isa = PBXBuildFile; fileRef = 346129971FD1E4D900532771 /* SignalApp.m */; }; - 34641E1F2088DA6D00E2EDE5 /* SAEScreenLockViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 34641E1E2088DA6D00E2EDE5 /* SAEScreenLockViewController.m */; }; 34661FB820C1C0D60056EDD6 /* message_sent.aiff in Resources */ = {isa = PBXBuildFile; fileRef = 34661FB720C1C0D60056EDD6 /* message_sent.aiff */; }; 346B66311F4E29B200E5122F /* CropScaleImageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 346B66301F4E29B200E5122F /* CropScaleImageViewController.swift */; }; 3478504C1FD7496D007B8332 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B66DBF4919D5BBC8006EA940 /* Images.xcassets */; }; @@ -775,6 +773,11 @@ FAD6392E0205566D11AA9E48 /* Pods_Session.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CB3724C70247A916D43271FE /* Pods_Session.framework */; }; FC3BD9881A30A790005B96BB /* Social.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FC3BD9871A30A790005B96BB /* Social.framework */; }; FCB11D8C1A129A76002F93FB /* CoreMedia.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FCB11D8B1A129A76002F93FB /* CoreMedia.framework */; }; + FD705A8C278CDB5600F16121 /* SAEScreenLockViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD705A8B278CDB5600F16121 /* SAEScreenLockViewController.swift */; }; + FD705A8E278CE29800F16121 /* String+Localization.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD705A8D278CE29800F16121 /* String+Localization.swift */; }; + FD705A90278CEBBC00F16121 /* ShareAppExtensionContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD705A8F278CEBBC00F16121 /* ShareAppExtensionContext.swift */; }; + FD705A92278D051200F16121 /* ReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD705A91278D051200F16121 /* ReusableView.swift */; }; + FD705A94278D052B00F16121 /* UITableView+ReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD705A93278D052B00F16121 /* UITableView+ReusableView.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -956,16 +959,12 @@ 34330AA21E79686200DF2FB9 /* OWSProgressView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSProgressView.m; sourceTree = ""; }; 34386A53207D271C009F5D9C /* NeverClearView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NeverClearView.swift; sourceTree = ""; }; 3441FD9E21A3604F00BB9542 /* BackupRestoreViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BackupRestoreViewController.swift; sourceTree = ""; }; - 34480B341FD0929200BC14EF /* ShareAppExtensionContext.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ShareAppExtensionContext.h; sourceTree = ""; }; - 34480B351FD0929200BC14EF /* ShareAppExtensionContext.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ShareAppExtensionContext.m; sourceTree = ""; }; 34480B371FD092A900BC14EF /* SignalShareExtension-Bridging-Header.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "SignalShareExtension-Bridging-Header.h"; sourceTree = ""; }; 34480B381FD092E300BC14EF /* SessionShareExtension-Prefix.pch */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "SessionShareExtension-Prefix.pch"; sourceTree = ""; }; 344825C4211390C700DB4BD8 /* OWSOrphanDataCleaner.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSOrphanDataCleaner.h; sourceTree = ""; }; 344825C5211390C800DB4BD8 /* OWSOrphanDataCleaner.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSOrphanDataCleaner.m; sourceTree = ""; }; 346129971FD1E4D900532771 /* SignalApp.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SignalApp.m; sourceTree = ""; }; 346129981FD1E4DA00532771 /* SignalApp.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SignalApp.h; sourceTree = ""; }; - 34641E1D2088DA6C00E2EDE5 /* SAEScreenLockViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SAEScreenLockViewController.h; sourceTree = ""; }; - 34641E1E2088DA6D00E2EDE5 /* SAEScreenLockViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SAEScreenLockViewController.m; sourceTree = ""; }; 34661FB720C1C0D60056EDD6 /* message_sent.aiff */ = {isa = PBXFileReference; lastKnownFileType = audio.aiff; name = message_sent.aiff; path = Session/Meta/AudioFiles/message_sent.aiff; sourceTree = SOURCE_ROOT; }; 346B66301F4E29B200E5122F /* CropScaleImageViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CropScaleImageViewController.swift; sourceTree = ""; }; 3488F9352191CC4000E524CC /* MediaView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaView.swift; sourceTree = ""; }; @@ -1780,6 +1779,11 @@ FB523C549815DE935E98151E /* Pods_SessionMessagingKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_SessionMessagingKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; FC3BD9871A30A790005B96BB /* Social.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Social.framework; path = System/Library/Frameworks/Social.framework; sourceTree = SDKROOT; }; FCB11D8B1A129A76002F93FB /* CoreMedia.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreMedia.framework; path = System/Library/Frameworks/CoreMedia.framework; sourceTree = SDKROOT; }; + FD705A8B278CDB5600F16121 /* SAEScreenLockViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SAEScreenLockViewController.swift; sourceTree = ""; }; + FD705A8D278CE29800F16121 /* String+Localization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Localization.swift"; sourceTree = ""; }; + FD705A8F278CEBBC00F16121 /* ShareAppExtensionContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareAppExtensionContext.swift; sourceTree = ""; }; + FD705A91278D051200F16121 /* ReusableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReusableView.swift; sourceTree = ""; }; + FD705A93278D052B00F16121 /* UITableView+ReusableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UITableView+ReusableView.swift"; sourceTree = ""; }; FF9BA33D021B115B1F5B4E46 /* Pods-SessionMessagingKit.app store release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SessionMessagingKit.app store release.xcconfig"; path = "Pods/Target Support Files/Pods-SessionMessagingKit/Pods-SessionMessagingKit.app store release.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ @@ -1971,10 +1975,8 @@ children = ( C31C21A4255BCA4800EC2D66 /* Meta */, 4535186C1FC635DD00210559 /* MainInterface.storyboard */, - 34641E1D2088DA6C00E2EDE5 /* SAEScreenLockViewController.h */, - 34641E1E2088DA6D00E2EDE5 /* SAEScreenLockViewController.m */, - 34480B341FD0929200BC14EF /* ShareAppExtensionContext.h */, - 34480B351FD0929200BC14EF /* ShareAppExtensionContext.m */, + FD705A8B278CDB5600F16121 /* SAEScreenLockViewController.swift */, + FD705A8F278CEBBC00F16121 /* ShareAppExtensionContext.swift */, C3ADC66026426688005F1414 /* ShareVC.swift */, B817AD9B26436F73009DF825 /* ThreadPickerVC.swift */, B817AD9926436593009DF825 /* SimplifiedConversationCell.swift */, @@ -2282,8 +2284,11 @@ C33FDB6B255A580F00E217F9 /* SNUserDefaults.swift */, C33FDB3F255A580C00E217F9 /* String+SSK.swift */, C3C2AC2D2553CBEB00C340D1 /* String+Trimming.swift */, + FD705A8D278CE29800F16121 /* String+Localization.swift */, C38EF237255B6D65007E1867 /* UIDevice+featureSupport.swift */, C35D0DB425AE5F1200B6BF49 /* UIEdgeInsets.swift */, + FD705A91278D051200F16121 /* ReusableView.swift */, + FD705A93278D052B00F16121 /* UITableView+ReusableView.swift */, C38EF23D255B6D66007E1867 /* UIView+OWS.h */, C38EF23E255B6D66007E1867 /* UIView+OWS.m */, C38EF2EF255B6DBB007E1867 /* Weak.swift */, @@ -4330,9 +4335,9 @@ buildActionMask = 2147483647; files = ( B817AD9A26436593009DF825 /* SimplifiedConversationCell.swift in Sources */, - 34480B361FD0929200BC14EF /* ShareAppExtensionContext.m in Sources */, C3ADC66126426688005F1414 /* ShareVC.swift in Sources */, - 34641E1F2088DA6D00E2EDE5 /* SAEScreenLockViewController.m in Sources */, + FD705A90278CEBBC00F16121 /* ShareAppExtensionContext.swift in Sources */, + FD705A8C278CDB5600F16121 /* SAEScreenLockViewController.swift in Sources */, B817AD9C26436F73009DF825 /* ThreadPickerVC.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -4549,6 +4554,7 @@ C3D9E4C02567767F0040E4F3 /* DataSource.m in Sources */, C3D9E43125676D3D0040E4F3 /* Configuration.swift in Sources */, C32C5DD2256DD9E5003C73A2 /* LRUCache.swift in Sources */, + FD705A94278D052B00F16121 /* UITableView+ReusableView.swift in Sources */, C3A7211A2558BCA10043A11F /* DiffieHellman.swift in Sources */, C32C5FA1256DFED5003C73A2 /* NSArray+Functional.m in Sources */, C3A7225E2558C38D0043A11F /* Promise+Retaining.swift in Sources */, @@ -4561,6 +4567,7 @@ C32C5B48256DC211003C73A2 /* NSNotificationCenter+OWS.m in Sources */, C3D9E3C925676AF30040E4F3 /* TSYapDatabaseObject.m in Sources */, C352A3A62557B60D00338F3E /* TSRequest.m in Sources */, + FD705A92278D051200F16121 /* ReusableView.swift in Sources */, B8AE75A425A6C6A6001A84D2 /* Data+Trimming.swift in Sources */, B8856DE6256F15F2001CE70E /* String+SSK.swift in Sources */, C3471ED42555386B00297E91 /* AESGCM.swift in Sources */, @@ -4584,6 +4591,7 @@ C300A60D2554B31900555489 /* Logging.swift in Sources */, B8FF8EA625C11FEF004D1F22 /* IPv4.swift in Sources */, C3D9E35525675EE10040E4F3 /* MIMETypeUtil.m in Sources */, + FD705A8E278CE29800F16121 /* String+Localization.swift in Sources */, C3A7219A2558C1660043A11F /* AnyPromise+Conversion.swift in Sources */, C300A6322554B6D100555489 /* NSDate+Timestamp.mm in Sources */, ); diff --git a/SessionShareExtension/Meta/SignalShareExtension-Bridging-Header.h b/SessionShareExtension/Meta/SignalShareExtension-Bridging-Header.h index 92486ea73..29c93b1b9 100644 --- a/SessionShareExtension/Meta/SignalShareExtension-Bridging-Header.h +++ b/SessionShareExtension/Meta/SignalShareExtension-Bridging-Header.h @@ -6,8 +6,6 @@ #import // Separate iOS Frameworks from other imports. -#import "SAEScreenLockViewController.h" -#import "ShareAppExtensionContext.h" #import #import #import diff --git a/SessionShareExtension/SAEScreenLockViewController.h b/SessionShareExtension/SAEScreenLockViewController.h deleted file mode 100644 index 1feaf140e..000000000 --- a/SessionShareExtension/SAEScreenLockViewController.h +++ /dev/null @@ -1,18 +0,0 @@ -// -// Copyright (c) 2018 Open Whisper Systems. All rights reserved. -// - -#import -#import - -NS_ASSUME_NONNULL_BEGIN - -@protocol ShareViewDelegate; - -@interface SAEScreenLockViewController : ScreenLockViewController - -- (instancetype)initWithShareViewDelegate:(id)shareViewDelegate; - -@end - -NS_ASSUME_NONNULL_END diff --git a/SessionShareExtension/SAEScreenLockViewController.m b/SessionShareExtension/SAEScreenLockViewController.m deleted file mode 100644 index 9cae51ad6..000000000 --- a/SessionShareExtension/SAEScreenLockViewController.m +++ /dev/null @@ -1,206 +0,0 @@ -// -// Copyright (c) 2018 Open Whisper Systems. All rights reserved. -// - -#import "SAEScreenLockViewController.h" -#import "UIColor+OWS.h" -#import -#import - -NS_ASSUME_NONNULL_BEGIN - -@interface SAEScreenLockViewController () - -@property (nonatomic, readonly, weak) id shareViewDelegate; - -@property (nonatomic) BOOL hasShownAuthUIOnce; - -@property (nonatomic) BOOL isShowingAuthUI; - -@end - -#pragma mark - - -@implementation SAEScreenLockViewController - -- (instancetype)initWithShareViewDelegate:(id)shareViewDelegate -{ - self = [super init]; - if (!self) { - return self; - } - - _shareViewDelegate = shareViewDelegate; - - self.delegate = self; - - return self; -} - -- (void)loadView -{ - [super loadView]; - - UIView.appearance.tintColor = LKColors.text; - - // Gradient background - self.view.backgroundColor = UIColor.clearColor; - CAGradientLayer *layer = [CAGradientLayer new]; - layer.frame = UIScreen.mainScreen.bounds; - UIColor *gradientStartColor = LKAppModeUtilities.isLightMode ? [UIColor colorWithRGBHex:0xFCFCFC] : [UIColor colorWithRGBHex:0x171717]; - UIColor *gradientEndColor = LKAppModeUtilities.isLightMode ? [UIColor colorWithRGBHex:0xFFFFFF] : [UIColor colorWithRGBHex:0x121212]; - layer.colors = @[ (id)gradientStartColor.CGColor, (id)gradientEndColor.CGColor ]; - [self.view.layer insertSublayer:layer atIndex:0]; - - // Navigation bar background color - UINavigationBar *navigationBar = self.navigationController.navigationBar; - [navigationBar setBackgroundImage:[UIImage new] forBarMetrics:UIBarMetricsDefault]; - navigationBar.shadowImage = [UIImage new]; - [navigationBar setTranslucent:NO]; - navigationBar.barTintColor = LKColors.navigationBarBackground; - - // Title - UILabel *titleLabel = [UILabel new]; - titleLabel.text = NSLocalizedString(@"vc_share_title", @""); - titleLabel.textColor = LKColors.text; - titleLabel.font = [UIFont boldSystemFontOfSize:LKValues.veryLargeFontSize]; - self.navigationItem.titleView = titleLabel; - - // Close button - UIBarButtonItem *closeButton = [[UIBarButtonItem alloc] initWithImage:[UIImage imageNamed:@"X"] style:UIBarButtonItemStylePlain target:self action:@selector(dismissPressed:)]; - closeButton.tintColor = LKColors.text; - self.navigationItem.leftBarButtonItem = closeButton; -} - -- (void)viewWillAppear:(BOOL)animated -{ - [super viewWillAppear:animated]; - - [self ensureUI]; -} - -- (void)viewDidAppear:(BOOL)animated -{ - [super viewDidAppear:animated]; - - [self ensureUI]; - - // Auto-show the auth UI f - if (!self.hasShownAuthUIOnce) { - self.hasShownAuthUIOnce = YES; - - [self tryToPresentAuthUIToUnlockScreenLock]; - } -} - -- (void)dealloc -{ - // Surface memory leaks by logging the deallocation of view controllers. - OWSLogVerbose(@"Dealloc: %@", self.class); -} - -- (void)tryToPresentAuthUIToUnlockScreenLock -{ - OWSAssertIsOnMainThread(); - - if (self.isShowingAuthUI) { - // We're already showing the auth UI; abort. - return; - } - OWSLogInfo(@"try to unlock screen lock"); - - self.isShowingAuthUI = YES; - - [OWSScreenLock.sharedManager tryToUnlockScreenLockWithSuccess:^{ - OWSAssertIsOnMainThread(); - - OWSLogInfo(@"unlock screen lock succeeded."); - - self.isShowingAuthUI = NO; - - [self.shareViewDelegate shareViewWasUnlocked]; - } - failure:^(NSError *error) { - OWSAssertIsOnMainThread(); - - OWSLogInfo(@"unlock screen lock failed."); - - self.isShowingAuthUI = NO; - - [self ensureUI]; - - [self showScreenLockFailureAlertWithMessage:error.localizedDescription]; - } - unexpectedFailure:^(NSError *error) { - OWSAssertIsOnMainThread(); - - OWSLogInfo(@"unlock screen lock unexpectedly failed."); - - self.isShowingAuthUI = NO; - - // Local Authentication isn't working properly. - // This isn't covered by the docs or the forums but in practice - // it appears to be effective to retry again after waiting a bit. - dispatch_async(dispatch_get_main_queue(), ^{ - [self ensureUI]; - }); - } - cancel:^{ - OWSAssertIsOnMainThread(); - - OWSLogInfo(@"unlock screen lock cancelled."); - - self.isShowingAuthUI = NO; - - [self ensureUI]; - }]; - - [self ensureUI]; -} - -- (void)ensureUI -{ - [self updateUIWithState:ScreenLockUIStateScreenLock isLogoAtTop:NO animated:NO]; -} - -- (void)showScreenLockFailureAlertWithMessage:(NSString *)message -{ - OWSAssertIsOnMainThread(); - - [OWSAlerts showAlertWithTitle:NSLocalizedString(@"SCREEN_LOCK_UNLOCK_FAILED", - @"Title for alert indicating that screen lock could not be unlocked.") - message:message - buttonTitle:nil - buttonAction:^(UIAlertAction *action) { - // After the alert, update the UI. - [self ensureUI]; - } - fromViewController:self]; -} - -- (void)dismissPressed:(id)sender -{ - OWSLogDebug(@"tapped dismiss share button"); - - [self cancelShareExperience]; -} - -- (void)cancelShareExperience -{ - [self.shareViewDelegate shareViewWasCancelled]; -} - -#pragma mark - ScreenLockViewDelegate - -- (void)unlockButtonWasTapped -{ - OWSAssertIsOnMainThread(); - - OWSLogInfo(@"unlockButtonWasTapped"); - - [self tryToPresentAuthUIToUnlockScreenLock]; -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/SessionShareExtension/SAEScreenLockViewController.swift b/SessionShareExtension/SAEScreenLockViewController.swift new file mode 100644 index 000000000..dd8bd11d4 --- /dev/null +++ b/SessionShareExtension/SAEScreenLockViewController.swift @@ -0,0 +1,206 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import UIKit +import PromiseKit +import SignalCoreKit +import SignalUtilitiesKit +import SessionUIKit +import SessionUtilitiesKit + +final class SAEScreenLockViewController: ScreenLockViewController, ScreenLockViewDelegate { + private var hasShownAuthUIOnce: Bool = false + private var isShowingAuthUI: Bool = false + + private weak var shareViewDelegate: ShareViewDelegate? + + // MARK: - Initialization + + init(shareViewDelegate: ShareViewDelegate) { + super.init(nibName: nil, bundle: nil) + + self.shareViewDelegate = shareViewDelegate + self.delegate = self + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + OWSLogger.verbose("Dealloc: \(type(of: self))") + } + + // MARK: - UI + + private lazy var gradientBackground: CAGradientLayer = { + let layer: CAGradientLayer = CAGradientLayer() + + let gradientStartColor: UIColor = (LKAppModeUtilities.isLightMode ? + UIColor(rgbHex: 0xFCFCFC) : + UIColor(rgbHex: 0x171717) + ) + let gradientEndColor: UIColor = (LKAppModeUtilities.isLightMode ? + UIColor(rgbHex: 0xFFFFFF) : + UIColor(rgbHex: 0x121212) + ) + layer.colors = [gradientStartColor.cgColor, gradientEndColor.cgColor] + + return layer + }() + + private lazy var titleLabel: UILabel = { + let titleLabel: UILabel = UILabel() + titleLabel.font = UIFont.boldSystemFont(ofSize: Values.veryLargeFontSize) + titleLabel.text = "vc_share_title".localized() + titleLabel.textColor = Colors.text + + return titleLabel + }() + + private lazy var closeButton: UIBarButtonItem = { + let closeButton: UIBarButtonItem = UIBarButtonItem(image: UIImage(named: "X"), style: .plain, target: self, action: #selector(dismissPressed)) + closeButton.tintColor = Colors.text + + return closeButton + }() + + // MARK: - Lifecycle + + override func loadView() { + super.loadView() + + UIView.appearance().tintColor = Colors.text + + self.view.backgroundColor = UIColor.clear + self.view.layer.insertSublayer(gradientBackground, at: 0) + + self.navigationController?.navigationBar.setBackgroundImage(UIImage(), for: .default) + self.navigationController?.navigationBar.shadowImage = UIImage() + self.navigationController?.navigationBar.isTranslucent = false + self.navigationController?.navigationBar.tintColor = Colors.navigationBarBackground + + self.navigationItem.titleView = titleLabel + self.navigationItem.leftBarButtonItem = closeButton + + setupLayout() + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + self.ensureUI() + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + self.ensureUI() + + // Auto-show the auth UI f + if !hasShownAuthUIOnce { + hasShownAuthUIOnce = true + + self.tryToPresentAuthUIToUnlockScreenLock() + } + } + + // MARK: - Layout + + private func setupLayout() { + gradientBackground.frame = UIScreen.main.bounds + } + + // MARK: - Functions + + private func tryToPresentAuthUIToUnlockScreenLock() { + AssertIsOnMainThread() + + // If we're already showing the auth UI; abort. + if self.isShowingAuthUI { return } + + OWSLogger.info("try to unlock screen lock") + + isShowingAuthUI = true + + OWSScreenLock.shared.tryToUnlockScreenLock( + success: { [weak self] in + AssertIsOnMainThread() + OWSLogger.info("unlock screen lock succeeded.") + + self?.isShowingAuthUI = false + self?.shareViewDelegate?.shareViewWasUnlocked() + }, + failure: { [weak self] error in + AssertIsOnMainThread() + OWSLogger.info("unlock screen lock failed.") + + self?.isShowingAuthUI = false + self?.ensureUI() + self?.showScreenLockFailureAlert(message: error.localizedDescription) + }, + unexpectedFailure: { [weak self] error in + AssertIsOnMainThread() + OWSLogger.info("unlock screen lock unexpectedly failed.") + + self?.isShowingAuthUI = false + + // Local Authentication isn't working properly. + // This isn't covered by the docs or the forums but in practice + // it appears to be effective to retry again after waiting a bit. + DispatchQueue.main.async { + self?.ensureUI() + } + }, + cancel: { [weak self] in + AssertIsOnMainThread() + OWSLogger.info("unlock screen lock cancelled.") + + self?.isShowingAuthUI = false + self?.ensureUI() + } + ) + + self.ensureUI() + } + + private func ensureUI() { + self.updateUI(with: .screenLock, isLogoAtTop: false, animated: false) + } + + private func showScreenLockFailureAlert(message: String) { + AssertIsOnMainThread() + + OWSAlerts.showAlert( + // Title for alert indicating that screen lock could not be unlocked. + title: "SCREEN_LOCK_UNLOCK_FAILED".localized(), + message: message, + buttonTitle: nil, + buttonAction: { [weak self] action in + // After the alert, update the UI + self?.ensureUI() + }, + fromViewController: self + ) + } + + // MARK: - Transitions + + @objc private func dismissPressed() { + OWSLogger.debug("unlock screen lock cancelled.") + + self.cancelShareExperience() + } + + private func cancelShareExperience() { + self.shareViewDelegate?.shareViewWasCancelled() + } + + // MARK: - ScreenLockViewDelegate + + func unlockButtonWasTapped() { + AssertIsOnMainThread() + OWSLogger.info("unlockButtonWasTapped") + + self.tryToPresentAuthUIToUnlockScreenLock() + } +} diff --git a/SessionShareExtension/ShareAppExtensionContext.h b/SessionShareExtension/ShareAppExtensionContext.h deleted file mode 100644 index 8e53d724d..000000000 --- a/SessionShareExtension/ShareAppExtensionContext.h +++ /dev/null @@ -1,18 +0,0 @@ -// -// Copyright (c) 2017 Open Whisper Systems. All rights reserved. -// - -#import - -NS_ASSUME_NONNULL_BEGIN - -// This is _NOT_ a singleton and will be instantiated each time that the SAE is used. -@interface ShareAppExtensionContext : NSObject - -- (instancetype)init NS_UNAVAILABLE; - -- (instancetype)initWithRootViewController:(UIViewController *)rootViewController; - -@end - -NS_ASSUME_NONNULL_END diff --git a/SessionShareExtension/ShareAppExtensionContext.m b/SessionShareExtension/ShareAppExtensionContext.m deleted file mode 100644 index 52e3621df..000000000 --- a/SessionShareExtension/ShareAppExtensionContext.m +++ /dev/null @@ -1,240 +0,0 @@ -// -// Copyright (c) 2019 Open Whisper Systems. All rights reserved. -// - -#import "ShareAppExtensionContext.h" -#import -#import -#import -#import -#import - -NS_ASSUME_NONNULL_BEGIN - -@interface ShareAppExtensionContext () - -@property (nonatomic) UIViewController *rootViewController; - -@property (atomic) UIApplicationState reportedApplicationState; - -@end - -#pragma mark - - -@implementation ShareAppExtensionContext - -@synthesize mainWindow = _mainWindow; -@synthesize appLaunchTime = _appLaunchTime; - -- (instancetype)initWithRootViewController:(UIViewController *)rootViewController -{ - self = [super init]; - - if (!self) { - return self; - } - - OWSAssertDebug(rootViewController); - - _rootViewController = rootViewController; - - self.reportedApplicationState = UIApplicationStateActive; - - _appLaunchTime = [NSDate new]; - - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(extensionHostDidBecomeActive:) - name:NSExtensionHostDidBecomeActiveNotification - object:nil]; - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(extensionHostWillResignActive:) - name:NSExtensionHostWillResignActiveNotification - object:nil]; - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(extensionHostDidEnterBackground:) - name:NSExtensionHostDidEnterBackgroundNotification - object:nil]; - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(extensionHostWillEnterForeground:) - name:NSExtensionHostWillEnterForegroundNotification - object:nil]; - - return self; -} - -- (void)dealloc -{ - [[NSNotificationCenter defaultCenter] removeObserver:self]; -} - -#pragma mark - Notifications - -- (void)extensionHostDidBecomeActive:(NSNotification *)notification -{ - OWSAssertIsOnMainThread(); - - OWSLogInfo(@""); - - self.reportedApplicationState = UIApplicationStateActive; - - [NSNotificationCenter.defaultCenter postNotificationName:OWSApplicationDidBecomeActiveNotification object:nil]; -} - -- (void)extensionHostWillResignActive:(NSNotification *)notification -{ - OWSAssertIsOnMainThread(); - - self.reportedApplicationState = UIApplicationStateInactive; - - OWSLogInfo(@""); - [DDLog flushLog]; - - [NSNotificationCenter.defaultCenter postNotificationName:OWSApplicationWillResignActiveNotification object:nil]; -} - -- (void)extensionHostDidEnterBackground:(NSNotification *)notification -{ - OWSAssertIsOnMainThread(); - - OWSLogInfo(@""); - [DDLog flushLog]; - - self.reportedApplicationState = UIApplicationStateBackground; - - [NSNotificationCenter.defaultCenter postNotificationName:OWSApplicationDidEnterBackgroundNotification object:nil]; -} - -- (void)extensionHostWillEnterForeground:(NSNotification *)notification -{ - OWSAssertIsOnMainThread(); - - OWSLogInfo(@""); - - self.reportedApplicationState = UIApplicationStateInactive; - - [NSNotificationCenter.defaultCenter postNotificationName:OWSApplicationWillEnterForegroundNotification object:nil]; -} - -#pragma mark - - -- (BOOL)isMainApp -{ - return NO; -} - -- (BOOL)isMainAppAndActive -{ - return NO; -} - -- (BOOL)isRTL -{ - static BOOL isRTL = NO; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - // Borrowed from PureLayout's AppExtension compatible RTL support. - // App Extensions may not access -[UIApplication sharedApplication]; fall back to checking the bundle's - // preferred localization character direction - isRTL = [NSLocale characterDirectionForLanguage:[[NSBundle mainBundle] preferredLocalizations][0]] - == NSLocaleLanguageDirectionRightToLeft; - }); - return isRTL; -} - -- (void)setStatusBarHidden:(BOOL)isHidden animated:(BOOL)isAnimated -{ - OWSLogInfo(@"Ignoring request to show/hide status bar since we're in an app extension"); -} - -- (CGFloat)statusBarHeight -{ - return 20; -} - -- (BOOL)isInBackground -{ - return self.reportedApplicationState == UIApplicationStateBackground; -} - -- (BOOL)isAppForegroundAndActive -{ - return self.reportedApplicationState == UIApplicationStateActive; -} - -- (UIBackgroundTaskIdentifier)beginBackgroundTaskWithExpirationHandler: - (BackgroundTaskExpirationHandler)expirationHandler -{ - return UIBackgroundTaskInvalid; -} - -- (void)endBackgroundTask:(UIBackgroundTaskIdentifier)backgroundTaskIdentifier -{ - OWSAssertDebug(backgroundTaskIdentifier == UIBackgroundTaskInvalid); -} - -- (void)ensureSleepBlocking:(BOOL)shouldBeBlocking blockingObjects:(NSArray *)blockingObjects -{ - OWSLogDebug(@"Ignoring request to block sleep."); -} - -- (void)setMainAppBadgeNumber:(NSInteger)value -{ - OWSFailDebug(@""); -} - -- (nullable UIViewController *)frontmostViewController -{ - OWSAssertDebug(self.rootViewController); - - return [self.rootViewController findFrontmostViewController:YES]; -} - -- (nullable UIAlertAction *)openSystemSettingsAction -{ - return nil; -} - -- (BOOL)isRunningTests -{ - // We don't need to distinguish this in the SAE. - return NO; -} - -- (void)setNetworkActivityIndicatorVisible:(BOOL)value -{ - OWSFailDebug(@""); -} - -- (void)runNowOrWhenMainAppIsActive:(AppActiveBlock)block -{ - OWSFailDebug(@"cannot run main app active blocks in share extension."); -} - -- (id)keychainStorage -{ - return [SSKDefaultKeychainStorage shared]; -} - -- (NSString *)appDocumentDirectoryPath -{ - NSFileManager *fileManager = [NSFileManager defaultManager]; - NSURL *documentDirectoryURL = - [[fileManager URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject]; - return [documentDirectoryURL path]; -} - -- (NSString *)appSharedDataDirectoryPath -{ - NSURL *groupContainerDirectoryURL = - [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:SignalApplicationGroup]; - return [groupContainerDirectoryURL path]; -} - -- (NSUserDefaults *)appUserDefaults -{ - return [[NSUserDefaults alloc] initWithSuiteName:SignalApplicationGroup]; -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/SessionShareExtension/ShareAppExtensionContext.swift b/SessionShareExtension/ShareAppExtensionContext.swift new file mode 100644 index 000000000..51c80bd95 --- /dev/null +++ b/SessionShareExtension/ShareAppExtensionContext.swift @@ -0,0 +1,204 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import UIKit +import SignalUtilitiesKit +import SessionUtilitiesKit +import SessionMessagingKit + +/// This is _NOT_ a singleton and will be instantiated each time that the SAE is used. +final class ShareAppExtensionContext: NSObject, AppContext { + var rootViewController: UIViewController + var reportedApplicationState: UIApplication.State + + let appLaunchTime = Date() + let isMainApp = false + let isMainAppAndActive = false + + var mainWindow: UIWindow? + var wasWokenUpByPushNotification: Bool = false + + private static var _isRTL: Bool = { + // Borrowed from PureLayout's AppExtension compatible RTL support. + // App Extensions may not access -[UIApplication sharedApplication]; fall back + // to checking the bundle's preferred localization character direction + return ( + Locale.characterDirection( + forLanguage: (Bundle.main.preferredLocalizations.first ?? "") + ) == Locale.LanguageDirection.rightToLeft + ) + }() + + var isRTL: Bool { return ShareAppExtensionContext._isRTL } + var isRunningTests: Bool { return false } // We don't need to distinguish this in the SAE + + var statusBarHeight: CGFloat { return 20 } + var openSystemSettingsAction: UIAlertAction? + + // MARK: - Initialization + + init(rootViewController: UIViewController) { + self.rootViewController = rootViewController + self.reportedApplicationState = .active + + super.init() + + NotificationCenter.default.addObserver( + self, + selector: #selector(extensionHostDidBecomeActive(notification:)), + name: .NSExtensionHostDidBecomeActive, + object: nil + ) + NotificationCenter.default.addObserver( + self, + selector: #selector(extensionHostWillResignActive(notification:)), + name: .NSExtensionHostWillResignActive, + object: nil + ) + NotificationCenter.default.addObserver( + self, + selector: #selector(extensionHostDidEnterBackground(notification:)), + name: .NSExtensionHostDidEnterBackground, + object: nil + ) + NotificationCenter.default.addObserver( + self, + selector: #selector(extensionHostWillEnterForeground(notification:)), + name: .NSExtensionHostWillEnterForeground, + object: nil + ) + } + + deinit { + NotificationCenter.default.removeObserver(self) + } + + // MARK: - Notifications + + @objc private func extensionHostDidBecomeActive(notification: NSNotification) { + AssertIsOnMainThread() + OWSLogger.info("") + + self.reportedApplicationState = .active + + NotificationCenter.default.post( + name: .OWSApplicationDidBecomeActive, + object: nil + ) + } + + @objc private func extensionHostWillResignActive(notification: NSNotification) { + AssertIsOnMainThread() + + self.reportedApplicationState = .inactive + + OWSLogger.info("") + DDLog.flushLog() + + NotificationCenter.default.post( + name: .OWSApplicationWillResignActive, + object: nil + ) + } + + @objc private func extensionHostDidEnterBackground(notification: NSNotification) { + AssertIsOnMainThread() + + OWSLogger.info("") + DDLog.flushLog() + + self.reportedApplicationState = .background + + NotificationCenter.default.post( + name: .OWSApplicationDidEnterBackground, + object: nil + ) + } + + @objc private func extensionHostWillEnterForeground(notification: NSNotification) { + AssertIsOnMainThread() + + OWSLogger.info("") + + self.reportedApplicationState = .inactive + + NotificationCenter.default.post( + name: .OWSApplicationWillEnterForeground, + object: nil + ) + } + + // MARK: - AppContext Functions + + func isAppForegroundAndActive() -> Bool { + return (reportedApplicationState == .active) + } + + func isInBackground() -> Bool { + return (reportedApplicationState == .background) + } + + func frontmostViewController() -> UIViewController? { + return rootViewController.findFrontmostViewController(true) + } + + func keychainStorage() -> SSKKeychainStorage { + return SSKDefaultKeychainStorage.shared + } + + func appDocumentDirectoryPath() -> String { + let targetPath: String? = FileManager.default + .urls( + for: .documentDirectory, + in: .userDomainMask + ) + .last? + .path + owsAssertDebug(targetPath != nil) + + return (targetPath ?? "") + } + + func appSharedDataDirectoryPath() -> String { + let targetPath: String? = FileManager.default + .containerURL(forSecurityApplicationGroupIdentifier: SignalApplicationGroup)? + .path + owsAssertDebug(targetPath != nil) + + return (targetPath ?? "") + } + + func appUserDefaults() -> UserDefaults { + let targetUserDefaults: UserDefaults? = UserDefaults(suiteName: SignalApplicationGroup) + owsAssertDebug(targetUserDefaults != nil) + + return (targetUserDefaults ?? UserDefaults.standard) + } + + func setStatusBarHidden(_ isHidden: Bool, animated isAnimated: Bool) { + OWSLogger.info("Ignoring request to show/hide status bar since we're in an app extension") + } + + func beginBackgroundTask(expirationHandler: @escaping BackgroundTaskExpirationHandler) -> UIBackgroundTaskIdentifier { + return .invalid + } + + func endBackgroundTask(_ backgroundTaskIdentifier: UIBackgroundTaskIdentifier) { + owsAssertDebug(backgroundTaskIdentifier == .invalid) + } + + func ensureSleepBlocking(_ shouldBeBlocking: Bool, blockingObjects: [Any]) { + OWSLogger.debug("Ignoring request to block sleep.") + } + + func setMainAppBadgeNumber(_ value: Int) { + owsFailDebug("") + } + + func setNetworkActivityIndicatorVisible(_ value: Bool) { + owsFailDebug("") + } + + func runNowOr(whenMainAppIsActive block: @escaping AppActiveBlock) { + owsFailDebug("cannot run main app active blocks in share extension.") + } +} diff --git a/SessionShareExtension/ShareVC.swift b/SessionShareExtension/ShareVC.swift index 6de48c8d9..749b2f0c7 100644 --- a/SessionShareExtension/ShareVC.swift +++ b/SessionShareExtension/ShareVC.swift @@ -1,12 +1,14 @@ import CoreServices import PromiseKit +import SignalUtilitiesKit import SessionUIKit final class ShareVC : UINavigationController, ShareViewDelegate, AppModeManagerDelegate { private var areVersionMigrationsComplete = false public static var attachmentPrepPromise: Promise<[SignalAttachment]>? - // MARK: Error + // MARK: - Error + enum ShareViewControllerError: Error { case assertionError(description: String) case unsupportedMedia @@ -14,7 +16,8 @@ final class ShareVC : UINavigationController, ShareViewDelegate, AppModeManagerD case obsoleteShare } - // MARK: Lifecycle + // MARK: - Lifecycle + override func loadView() { super.loadView() @@ -39,28 +42,35 @@ final class ShareVC : UINavigationController, ShareViewDelegate, AppModeManagerD return } - AppSetup.setupEnvironment(appSpecificSingletonBlock: { - SSKEnvironment.shared.notificationsManager = NoopNotificationsManager() - }, migrationCompletion: { [weak self] in - AssertIsOnMainThread() + AppSetup.setupEnvironment( + appSpecificSingletonBlock: { + SSKEnvironment.shared.notificationsManager = NoopNotificationsManager() + }, + migrationCompletion: { [weak self] in + AssertIsOnMainThread() + + self?.versionMigrationsDidComplete() - guard let strongSelf = self else { return } - - // performUpdateCheck must be invoked after Environment has been initialized because - // upgrade process may depend on Environment. - strongSelf.versionMigrationsDidComplete() - }) + // performUpdateCheck must be invoked after Environment has been initialized because + // upgrade process may depend on Environment. + self?.versionMigrationsDidComplete() + } + ) // We don't need to use "screen protection" in the SAE. - NotificationCenter.default.addObserver(self, - selector: #selector(storageIsReady), - name: .StorageIsReady, - object: nil) - NotificationCenter.default.addObserver(self, - selector: #selector(applicationDidEnterBackground), - name: .OWSApplicationDidEnterBackground, - object: nil) + NotificationCenter.default.addObserver( + self, + selector: #selector(storageIsReady), + name: .StorageIsReady, + object: nil + ) + NotificationCenter.default.addObserver( + self, + selector: #selector(applicationDidEnterBackground), + name: .OWSApplicationDidEnterBackground, + object: nil + ) } @objc @@ -88,12 +98,8 @@ final class ShareVC : UINavigationController, ShareViewDelegate, AppModeManagerD AssertIsOnMainThread() // App isn't ready until storage is ready AND all version migrations are complete. - guard areVersionMigrationsComplete else { - return - } - guard OWSStorage.isStorageReady() else { - return - } + guard areVersionMigrationsComplete else { return } + guard OWSStorage.isStorageReady() else { return } guard !AppReadiness.isAppReady() else { // Only mark the app as ready once. return @@ -108,9 +114,7 @@ final class ShareVC : UINavigationController, ShareViewDelegate, AppModeManagerD AppReadiness.setAppIsReady() // We don't need to use messageFetcherJob in the SAE. - // We don't need to use SyncPushTokensJob in the SAE. - // We don't need to use DeviceSleepManager in the SAE. AppVersion.sharedInstance().saeLaunchDidComplete() @@ -119,9 +123,7 @@ final class ShareVC : UINavigationController, ShareViewDelegate, AppModeManagerD // We don't need to use OWSMessageReceiver in the SAE. // We don't need to use OWSBatchMessageProcessor in the SAE. - // We don't need to use OWSOrphanDataCleaner in the SAE. - // We don't need to fetch the local profile in the SAE OWSReadReceiptManager.shared().prepareCachedValues() @@ -129,10 +131,10 @@ final class ShareVC : UINavigationController, ShareViewDelegate, AppModeManagerD override func viewDidLoad() { super.viewDidLoad() + AppReadiness.runNowOrWhenAppDidBecomeReady { [weak self] in AssertIsOnMainThread() - guard let strongSelf = self else { return } - strongSelf.showLockScreenOrMainContent() + self?.showLockScreenOrMainContent() } } @@ -143,11 +145,9 @@ final class ShareVC : UINavigationController, ShareViewDelegate, AppModeManagerD Logger.info("") if OWSScreenLock.shared.isScreenLockEnabled() { - self.dismiss(animated: false) { [weak self] in AssertIsOnMainThread() - guard let strongSelf = self else { return } - strongSelf.extensionContext!.completeRequest(returningItems: [], completionHandler: nil) + self?.extensionContext?.completeRequest(returningItems: [], completionHandler: nil) } } } @@ -161,12 +161,15 @@ final class ShareVC : UINavigationController, ShareViewDelegate, AppModeManagerD ExitShareExtension() } - // MARK: App Mode + // MARK: - App Mode + public func getCurrentAppMode() -> AppMode { guard let window = self.view.window else { return .light } + let userInterfaceStyle = window.traitCollection.userInterfaceStyle let isLightMode = (userInterfaceStyle == .light || userInterfaceStyle == .unspecified) - return isLightMode ? .light : .dark + + return (isLightMode ? .light : .dark) } public func setCurrentAppMode(to appMode: AppMode) { @@ -181,7 +184,8 @@ final class ShareVC : UINavigationController, ShareViewDelegate, AppModeManagerD private func showLockScreenOrMainContent() { if OWSScreenLock.shared.isScreenLockEnabled() { showLockScreen() - } else { + } + else { showMainContent() } } @@ -192,16 +196,23 @@ final class ShareVC : UINavigationController, ShareViewDelegate, AppModeManagerD } private func showMainContent() { - let threadPickerVC = ThreadPickerVC() + let threadPickerVC: ThreadPickerVC = ThreadPickerVC() threadPickerVC.shareVC = self + setViewControllers([ threadPickerVC ], animated: false) + let promise = buildAttachments() - ModalActivityIndicatorViewController.present(fromViewController: self, canCancel: false, message: NSLocalizedString("vc_share_loading_message", comment: "")) { activityIndicator in - promise.done { _ in - activityIndicator.dismiss { } - }.catch { _ in - activityIndicator.dismiss { } - } + ModalActivityIndicatorViewController.present( + fromViewController: self, + canCancel: false, + message: "vc_share_loading_message".localized()) { activityIndicator in + promise + .done { _ in + activityIndicator.dismiss { } + } + .catch { _ in + activityIndicator.dismiss { } + } } ShareVC.attachmentPrepPromise = promise } @@ -220,7 +231,7 @@ final class ShareVC : UINavigationController, ShareViewDelegate, AppModeManagerD func shareViewFailed(error: Error) { let alert = UIAlertController(title: "Session", message: error.localizedDescription, preferredStyle: .alert) - alert.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .default, handler: { _ in + alert.addAction(UIAlertAction(title: "OK".localized(), style: .default, handler: { _ in self.extensionContext!.cancelRequest(withError: error) })) present(alert, animated: true, completion: nil) @@ -236,22 +247,29 @@ final class ShareVC : UINavigationController, ShareViewDelegate, AppModeManagerD guard let firstUtiType = itemProvider.registeredTypeIdentifiers.first else { return false } - return firstUtiType == utiType + + return (firstUtiType == utiType) } private class func isVisualMediaItem(itemProvider: NSItemProvider) -> Bool { - return (itemProvider.hasItemConformingToTypeIdentifier(kUTTypeImage as String) || - itemProvider.hasItemConformingToTypeIdentifier(kUTTypeMovie as String)) + return ( + itemProvider.hasItemConformingToTypeIdentifier(kUTTypeImage as String) || + itemProvider.hasItemConformingToTypeIdentifier(kUTTypeMovie as String) + ) } private class func isUrlItem(itemProvider: NSItemProvider) -> Bool { - return itemMatchesSpecificUtiType(itemProvider: itemProvider, - utiType: kUTTypeURL as String) + return itemMatchesSpecificUtiType( + itemProvider: itemProvider, + utiType: kUTTypeURL as String + ) } private class func isContactItem(itemProvider: NSItemProvider) -> Bool { - return itemMatchesSpecificUtiType(itemProvider: itemProvider, - utiType: kUTTypeContact as String) + return itemMatchesSpecificUtiType( + itemProvider: itemProvider, + utiType: kUTTypeContact as String + ) } private class func utiType(itemProvider: NSItemProvider) -> String? { @@ -259,7 +277,8 @@ final class ShareVC : UINavigationController, ShareViewDelegate, AppModeManagerD if isUrlItem(itemProvider: itemProvider) { return kUTTypeURL as String - } else if isContactItem(itemProvider: itemProvider) { + } + else if isContactItem(itemProvider: itemProvider) { return kUTTypeContact as String } @@ -278,43 +297,43 @@ final class ShareVC : UINavigationController, ShareViewDelegate, AppModeManagerD // and send them as normal text messages if possible. let urlString = url.absoluteString return DataSourceValue.dataSource(withOversizeText: urlString) - } else if UTTypeConformsTo(utiType as CFString, kUTTypeText) { + } + else if UTTypeConformsTo(utiType as CFString, kUTTypeText) { // Share text as oversize text messages. // // NOTE: SharingThreadPickerViewController will try to unpack them // and send them as normal text messages if possible. - return DataSourcePath.dataSource(with: url, - shouldDeleteOnDeallocation: false) - } else { - guard let dataSource = DataSourcePath.dataSource(with: url, - shouldDeleteOnDeallocation: false) else { - return nil - } - - if let customFileName = customFileName { - dataSource.sourceFilename = customFileName - } else { - // Ignore the filename for URLs. - dataSource.sourceFilename = url.lastPathComponent - } - return dataSource + return DataSourcePath.dataSource( + with: url, + shouldDeleteOnDeallocation: false + ) } - } - - private class func preferredItemProviders(inputItem: NSExtensionItem) -> [NSItemProvider]? { - guard let attachments = inputItem.attachments else { + + guard let dataSource = DataSourcePath.dataSource(with: url, shouldDeleteOnDeallocation: false) else { return nil } + // Fallback to the last part of the URL + dataSource.sourceFilename = (customFileName ?? url.lastPathComponent) + + return dataSource + } + + private class func preferredItemProviders(inputItem: NSExtensionItem) -> [NSItemProvider]? { + guard let attachments = inputItem.attachments else { return nil } + var visualMediaItemProviders = [NSItemProvider]() var hasNonVisualMedia = false + for attachment in attachments { if isVisualMediaItem(itemProvider: attachment) { visualMediaItemProviders.append(attachment) - } else { + } + else { hasNonVisualMedia = true } } + // Only allow multiple-attachment sends if all attachments // are visual media. if visualMediaItemProviders.count > 0 && !hasNonVisualMedia { @@ -334,6 +353,7 @@ final class ShareVC : UINavigationController, ShareViewDelegate, AppModeManagerD guard let itemProvider = attachment as? NSItemProvider else { return false } + return isUrlItem(itemProvider: itemProvider) }) { return [preferredAttachment] @@ -342,9 +362,11 @@ final class ShareVC : UINavigationController, ShareViewDelegate, AppModeManagerD // else return whatever is available if let itemProvider = inputItem.attachments?.first { return [itemProvider] - } else { + } + else { owsFailDebug("Missing attachment.") } + return [] } @@ -359,6 +381,7 @@ final class ShareVC : UINavigationController, ShareViewDelegate, AppModeManagerD Logger.error("invalid inputItem \(inputItemRaw)") continue } + if let itemProviders = ShareVC.preferredItemProviders(inputItem: inputItem) { return Promise.value(itemProviders) } @@ -366,6 +389,8 @@ final class ShareVC : UINavigationController, ShareViewDelegate, AppModeManagerD let error = ShareViewControllerError.assertionError(description: "no input item") return Promise(error: error) } + + // MARK: - LoadedItem private struct LoadedItem { diff --git a/SessionShareExtension/SimplifiedConversationCell.swift b/SessionShareExtension/SimplifiedConversationCell.swift index b03404392..b23f02b43 100644 --- a/SessionShareExtension/SimplifiedConversationCell.swift +++ b/SessionShareExtension/SimplifiedConversationCell.swift @@ -4,26 +4,8 @@ import SessionUIKit final class SimplifiedConversationCell : UITableViewCell { var threadViewModel: ThreadViewModel! { didSet { update() } } - static let reuseIdentifier = "SimplifiedConversationCell" + // MARK: - Initialization - // 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() @@ -34,41 +16,90 @@ final class SimplifiedConversationCell : UITableViewCell { setUpViewHierarchy() } + // MARK: - UI + + private lazy var stackView: UIStackView = { + let stackView: UIStackView = UIStackView() + stackView.translatesAutoresizingMaskIntoConstraints = false + stackView.axis = .horizontal + stackView.alignment = .center + stackView.spacing = Values.mediumSpacing + + return stackView + }() + + private lazy var accentLineView: UIView = { + let result = UIView() + result.translatesAutoresizingMaskIntoConstraints = false + result.backgroundColor = Colors.destructive + + return result + }() + + private lazy var profilePictureView: ProfilePictureView = { + let view: ProfilePictureView = ProfilePictureView() + view.translatesAutoresizingMaskIntoConstraints = false + + return view + }() + + private lazy var displayNameLabel: UILabel = { + let result = UILabel() + result.font = .boldSystemFont(ofSize: Values.mediumFontSize) + result.textColor = Colors.text + result.lineBreakMode = .byTruncatingTail + + return result + }() + + // MARK: - Initialization + private func setUpViewHierarchy() { - // Background color backgroundColor = Colors.cellBackground - // Highlight color + let selectedBackgroundView = UIView() selectedBackgroundView.backgroundColor = Colors.cellSelected self.selectedBackgroundView = selectedBackgroundView - // Accent line view + + addSubview(stackView) + + stackView.addArrangedSubview(accentLineView) + stackView.addArrangedSubview(profilePictureView) + stackView.addArrangedSubview(displayNameLabel) + stackView.addArrangedSubview(UIView.hSpacer(0)) + + setupLayout() + } + + // MARK: - Layout + + private func setupLayout() { accentLineView.set(.width, to: Values.accentLineThickness) accentLineView.set(.height, to: 68) - // Profile picture view + let profilePictureViewSize = Values.mediumProfilePictureSize profilePictureView.set(.width, to: profilePictureViewSize) profilePictureView.set(.height, to: profilePictureViewSize) profilePictureView.size = profilePictureViewSize - // Main stack view - let stackView = UIStackView(arrangedSubviews: [ accentLineView, profilePictureView, displayNameLabel, UIView.hSpacer(0) ]) - stackView.axis = .horizontal - stackView.alignment = .center - stackView.spacing = Values.mediumSpacing - addSubview(stackView) + stackView.pin(to: self) } - // MARK: Updating + // MARK: - Content + private func update() { AssertIsOnMainThread() + guard let thread = threadViewModel?.threadRecord else { return } + let isBlocked: Bool if let thread = thread as? TSContactThread { isBlocked = SSKEnvironment.shared.blockingManager.isRecipientIdBlocked(thread.contactSessionID()) } else { isBlocked = false } - accentLineView.alpha = isBlocked ? 1 : 0 + + accentLineView.alpha = (isBlocked ? 1 : 0) profilePictureView.update(for: thread) displayNameLabel.text = getDisplayName() } @@ -76,17 +107,25 @@ final class SimplifiedConversationCell : UITableViewCell { private func getDisplayName() -> String { if threadViewModel.isGroupThread { if threadViewModel.name.isEmpty { + // TODO: Localization return "Unknown Group" - } else { - return threadViewModel.name - } - } else { - if threadViewModel.threadRecord.isNoteToSelf() { - return NSLocalizedString("NOTE_TO_SELF", comment: "") - } else { - let hexEncodedPublicKey = threadViewModel.contactSessionID! - return Storage.shared.getContact(with: hexEncodedPublicKey)?.displayName(for: .regular) ?? hexEncodedPublicKey } + + return threadViewModel.name } + + if threadViewModel.threadRecord.isNoteToSelf() { + return "NOTE_TO_SELF".localized() + } + + guard let hexEncodedPublicKey: String = threadViewModel.contactSessionID else { + // TODO: Localization + return "Unknown" + } + + return ( + Storage.shared.getContact(with: hexEncodedPublicKey)?.displayName(for: .regular) ?? + hexEncodedPublicKey + ) } } diff --git a/SessionShareExtension/ThreadPickerVC.swift b/SessionShareExtension/ThreadPickerVC.swift index 75355d23a..414036bad 100644 --- a/SessionShareExtension/ThreadPickerVC.swift +++ b/SessionShareExtension/ThreadPickerVC.swift @@ -1,8 +1,12 @@ +import UIKit +import SignalUtilitiesKit import SessionUIKit +import SessionMessagingKit +import SessionUtilitiesKit -final class ThreadPickerVC : UIViewController, UITableViewDataSource, UITableViewDelegate, AttachmentApprovalViewControllerDelegate { +final class ThreadPickerVC: UIViewController, UITableViewDataSource, UITableViewDelegate, AttachmentApprovalViewControllerDelegate { private var threads: YapDatabaseViewMappings! - private var threadViewModelCache: [String:ThreadViewModel] = [:] // Thread ID to ThreadViewModel + private var threadViewModelCache: [String: ThreadViewModel] = [:] // Thread ID to ThreadViewModel private var selectedThread: TSThread? var shareVC: ShareVC? @@ -15,32 +19,50 @@ final class ThreadPickerVC : UIViewController, UITableViewDataSource, UITableVie result.objectCacheLimit = 500 return result }() + + // MARK: - UI + + private lazy var titleLabel: UILabel = { + let titleLabel: UILabel = UILabel() + titleLabel.text = "vc_share_title".localized() + titleLabel.textColor = Colors.text + titleLabel.font = .boldSystemFont(ofSize: Values.veryLargeFontSize) + + return titleLabel + }() private lazy var tableView: UITableView = { - let result = UITableView() - result.backgroundColor = .clear - result.separatorStyle = .none - result.register(SimplifiedConversationCell.self, forCellReuseIdentifier: SimplifiedConversationCell.reuseIdentifier) - result.showsVerticalScrollIndicator = false - return result + let tableView: UITableView = UITableView() + tableView.backgroundColor = .clear + tableView.separatorStyle = .none + tableView.register(view: SimplifiedConversationCell.self) + tableView.showsVerticalScrollIndicator = false + tableView.dataSource = self + tableView.delegate = self + + return tableView }() private lazy var fadeView: UIView = { - let result = UIView() + let view = UIView() let gradient = Gradients.homeVCFade - result.setGradient(gradient) - result.isUserInteractionEnabled = false - return result + view.setGradient(gradient) + view.isUserInteractionEnabled = false + + return view }() - // MARK: Lifecycle + // MARK: - Lifecycle + override func viewDidLoad() { super.viewDidLoad() + setupNavBar() + // Gradient view.backgroundColor = .clear - let gradient = Gradients.defaultBackground - view.setGradient(gradient) + view.setGradient(Gradients.defaultBackground) + // Threads dbConnection.beginLongLivedReadTransaction() // Freeze the connection for use on the main thread (this gives us a stable data source that doesn't change until we tell it to) threads = YapDatabaseViewMappings(groups: [ TSInboxGroup ], view: TSThreadDatabaseViewExtensionName) // The extension should be registered at this point @@ -48,23 +70,16 @@ final class ThreadPickerVC : UIViewController, UITableViewDataSource, UITableVie dbConnection.read { transaction in self.threads.update(with: transaction) // Perform the initial update } + // Title - let titleLabel = UILabel() - titleLabel.text = NSLocalizedString("vc_share_title", comment: "") - titleLabel.textColor = Colors.text - titleLabel.font = .boldSystemFont(ofSize: Values.veryLargeFontSize) navigationItem.titleView = titleLabel + // Table view - tableView.dataSource = self - tableView.delegate = self + view.addSubview(tableView) - tableView.pin(to: view) view.addSubview(fadeView) - fadeView.pin(.leading, to: .leading, of: view) - let topInset = 0.15 * view.height() - fadeView.pin(.top, to: .top, of: view, withInset: topInset) - fadeView.pin(.trailing, to: .trailing, of: view) - fadeView.pin(.bottom, to: .bottom, of: view) + + setupLayout() // Reload reload() } @@ -80,18 +95,32 @@ final class ThreadPickerVC : UIViewController, UITableViewDataSource, UITableVie } } + // MARK: Layout + + private func setupLayout() { + let topInset = 0.15 * view.height() + + tableView.pin(to: view) + fadeView.pin(.leading, to: .leading, of: view) + fadeView.pin(.top, to: .top, of: view, withInset: topInset) + fadeView.pin(.trailing, to: .trailing, of: view) + fadeView.pin(.bottom, to: .bottom, of: view) + } + // MARK: Table View Data Source func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return Int(threadCount) } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let cell = tableView.dequeueReusableCell(withIdentifier: SimplifiedConversationCell.reuseIdentifier) as! SimplifiedConversationCell + let cell: SimplifiedConversationCell = tableView.dequeue(type: SimplifiedConversationCell.self, for: indexPath) cell.threadViewModel = threadViewModel(at: indexPath.row) + return cell } - // MARK: Updating + // MARK: - Updating + private func reload() { AssertIsOnMainThread() dbConnection.beginLongLivedReadTransaction() // Jump to the latest commit @@ -102,11 +131,17 @@ final class ThreadPickerVC : UIViewController, UITableViewDataSource, UITableVie tableView.reloadData() } - // MARK: Interaction + // MARK: - Interaction + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - guard let thread = self.thread(at: indexPath.row), let attachments = ShareVC.attachmentPrepPromise?.value else { return } - self.selectedThread = thread tableView.deselectRow(at: indexPath, animated: true) + + guard let thread = self.thread(at: indexPath.row), let attachments = ShareVC.attachmentPrepPromise?.value else { + return + } + + self.selectedThread = thread + let approvalVC = AttachmentApprovalViewController.wrappedInNavController(attachments: attachments, approvalDelegate: self) navigationController!.present(approvalVC, animated: true, completion: nil) } @@ -115,21 +150,24 @@ final class ThreadPickerVC : UIViewController, UITableViewDataSource, UITableVie let message = VisibleMessage() message.sentTimestamp = NSDate.millisecondTimestamp() message.text = messageText + let tsMessage = TSOutgoingMessage.from(message, associatedWith: selectedThread!) Storage.write { transaction in tsMessage.save(with: transaction) } + shareVC!.dismiss(animated: true, completion: nil) - ModalActivityIndicatorViewController.present(fromViewController: shareVC!, canCancel: false, message: NSLocalizedString("vc_share_sending_message", comment: "")) { activityIndicator in - MessageSender.sendNonDurably(message, with: attachments, in: self.selectedThread!).done { [weak self] _ in - guard let self = self else { return } - activityIndicator.dismiss { } - self.shareVC!.shareViewWasCompleted() - }.catch { [weak self] error in - guard let self = self else { return } - activityIndicator.dismiss { } - self.shareVC!.shareViewFailed(error: error) - } + + ModalActivityIndicatorViewController.present(fromViewController: shareVC!, canCancel: false, message: "vc_share_sending_message".localized()) { activityIndicator in + MessageSender.sendNonDurably(message, with: attachments, in: self.selectedThread!) + .done { [weak self] _ in + activityIndicator.dismiss { } + self?.shareVC?.shareViewWasCompleted() + } + .catch { [weak self] error in + activityIndicator.dismiss { } + self?.shareVC?.shareViewFailed(error: error) + } } } @@ -141,7 +179,8 @@ final class ThreadPickerVC : UIViewController, UITableViewDataSource, UITableVie // Do nothing } - // MARK: Convenience + // MARK: - Convenience + private func thread(at index: Int) -> TSThread? { var thread: TSThread? = nil dbConnection.read { transaction in @@ -153,9 +192,11 @@ final class ThreadPickerVC : UIViewController, UITableViewDataSource, UITableVie private func threadViewModel(at index: Int) -> ThreadViewModel? { guard let thread = thread(at: index) else { return nil } + if let cachedThreadViewModel = threadViewModelCache[thread.uniqueId!] { return cachedThreadViewModel - } else { + } + else { var threadViewModel: ThreadViewModel? = nil dbConnection.read { transaction in threadViewModel = ThreadViewModel(thread: thread, transaction: transaction) diff --git a/SessionUtilitiesKit/General/ReusableView.swift b/SessionUtilitiesKit/General/ReusableView.swift new file mode 100644 index 000000000..4a33f2e65 --- /dev/null +++ b/SessionUtilitiesKit/General/ReusableView.swift @@ -0,0 +1,16 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import UIKit + +public protocol ReusableView: AnyObject { + static var defaultReuseIdentifier: String { get } +} + +public extension ReusableView where Self: UIView { + static var defaultReuseIdentifier: String { + return String(describing: self.self) + } +} + +extension UITableViewCell: ReusableView {} +extension UITableViewHeaderFooterView: ReusableView {} diff --git a/SessionUtilitiesKit/General/String+Localization.swift b/SessionUtilitiesKit/General/String+Localization.swift new file mode 100644 index 000000000..2468d1d25 --- /dev/null +++ b/SessionUtilitiesKit/General/String+Localization.swift @@ -0,0 +1,13 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import SignalCoreKit + +public extension String { + func localized() -> String { + // If the localized string matches the key provided then the localisation failed + let localizedString = NSLocalizedString(self, comment: "") + owsAssertDebug(localizedString != self, "Key \"\(self)\" is not set in Localizable.strings") + + return localizedString + } +} diff --git a/SessionUtilitiesKit/General/UITableView+ReusableView.swift b/SessionUtilitiesKit/General/UITableView+ReusableView.swift new file mode 100644 index 000000000..725faa6b4 --- /dev/null +++ b/SessionUtilitiesKit/General/UITableView+ReusableView.swift @@ -0,0 +1,23 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import UIKit + +public extension UITableView { + func register(view: View.Type) where View: UITableViewCell { + register(view.self, forCellReuseIdentifier: view.defaultReuseIdentifier) + } + + func registerHeaderFooterView(view: View.Type) where View: UITableViewHeaderFooterView { + register(view.self, forHeaderFooterViewReuseIdentifier: view.defaultReuseIdentifier) + } + + func dequeue(type: T.Type, for indexPath: IndexPath) -> T where T: UITableViewCell { + let reuseIdentifier = T.defaultReuseIdentifier + return dequeueReusableCell(withIdentifier: reuseIdentifier, for: indexPath) as! T + } + + func dequeueHeaderFooterView(type: T.Type) -> T where T: UITableViewHeaderFooterView { + let reuseIdentifier = T.defaultReuseIdentifier + return dequeueReusableHeaderFooterView(withIdentifier: reuseIdentifier) as! T + } +} diff --git a/SignalUtilitiesKit/Utilities/ShareViewDelegate.swift b/SignalUtilitiesKit/Utilities/ShareViewDelegate.swift index 6be9361d8..887dabcda 100644 --- a/SignalUtilitiesKit/Utilities/ShareViewDelegate.swift +++ b/SignalUtilitiesKit/Utilities/ShareViewDelegate.swift @@ -5,7 +5,7 @@ import Foundation // All Observer methods will be invoked from the main thread. @objc -public protocol ShareViewDelegate: class { +public protocol ShareViewDelegate: AnyObject { func shareViewWasUnlocked() func shareViewWasCompleted() func shareViewWasCancelled() From ba1a0a2ac67c170ef8a08aff79eb2e8348bba8b3 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Tue, 11 Jan 2022 14:37:05 +1100 Subject: [PATCH 02/26] Fixed M1 build issues Included the Podfile changes for building on an M1 Mac (Need to shift from personal to session fork). Fixed some additional build issues due to unwrapping a non-optional type (Swift version?). --- Podfile | 23 ++--- Podfile.lock | 86 +++++++++---------- .../Utilities/ProofOfWork.swift | 2 +- SessionSnodeKit/SnodeAPI.swift | 11 +-- 4 files changed, 63 insertions(+), 59 deletions(-) diff --git a/Podfile b/Podfile index 5452d4e81..a87bceaf8 100644 --- a/Podfile +++ b/Podfile @@ -11,7 +11,7 @@ target 'Session' do pod 'PromiseKit', :inhibit_warnings => true pod 'PureLayout', '~> 3.1.8', :inhibit_warnings => true pod 'Reachability', :inhibit_warnings => true - pod 'Sodium', '~> 0.8.0', :inhibit_warnings => true + pod 'Sodium', '~> 0.9.1', :inhibit_warnings => true pod 'YapDatabase/SQLCipher', :git => 'https://github.com/loki-project/session-ios-yap-database.git', branch: 'signal-release', :inhibit_warnings => true pod 'YYImage', git: 'https://github.com/signalapp/YYImage', :inhibit_warnings => true pod 'ZXingObjC', :inhibit_warnings => true @@ -22,15 +22,18 @@ target 'SessionShareExtension' do pod 'CryptoSwift', :inhibit_warnings => true pod 'Curve25519Kit', git: 'https://github.com/signalapp/Curve25519Kit.git', :inhibit_warnings => true pod 'Mantle', git: 'https://github.com/signalapp/Mantle', branch: 'signal-master', :inhibit_warnings => true + pod 'NVActivityIndicatorView', :inhibit_warnings => true pod 'PromiseKit', :inhibit_warnings => true pod 'PureLayout', '~> 3.1.8', :inhibit_warnings => true - pod 'SignalCoreKit', git: 'https://github.com/signalapp/SignalCoreKit.git', :inhibit_warnings => true + pod 'Sodium', '~> 0.9.1', :inhibit_warnings => true + pod 'SignalCoreKit', git: 'https://github.com/mpretty-cyro/SignalCoreKit.git', branch: 'session-version', :inhibit_warnings => true pod 'YapDatabase/SQLCipher', :git => 'https://github.com/loki-project/session-ios-yap-database.git', branch: 'signal-release', :inhibit_warnings => true end target 'SessionNotificationServiceExtension' do pod 'Curve25519Kit', git: 'https://github.com/signalapp/Curve25519Kit.git', :inhibit_warnings => true - pod 'SignalCoreKit', git: 'https://github.com/signalapp/SignalCoreKit.git', :inhibit_warnings => true + pod 'Sodium', '~> 0.9.1', :inhibit_warnings => true + pod 'SignalCoreKit', git: 'https://github.com/mpretty-cyro/SignalCoreKit.git', branch: 'session-version', :inhibit_warnings => true pod 'YapDatabase/SQLCipher', :git => 'https://github.com/loki-project/session-ios-yap-database.git', branch: 'signal-release', :inhibit_warnings => true end @@ -38,15 +41,15 @@ target 'SignalUtilitiesKit' do pod 'AFNetworking', inhibit_warnings: true pod 'CryptoSwift', :inhibit_warnings => true pod 'Curve25519Kit', git: 'https://github.com/signalapp/Curve25519Kit.git', :inhibit_warnings => true - pod 'GRKOpenSSLFramework', :inhibit_warnings => true pod 'HKDFKit', :inhibit_warnings => true pod 'Mantle', git: 'https://github.com/signalapp/Mantle', branch: 'signal-master', :inhibit_warnings => true pod 'NVActivityIndicatorView', :inhibit_warnings => true pod 'PromiseKit', :inhibit_warnings => true pod 'PureLayout', '~> 3.1.8', :inhibit_warnings => true pod 'Reachability', :inhibit_warnings => true + pod 'Sodium', '~> 0.9.1', :inhibit_warnings => true pod 'SAMKeychain', :inhibit_warnings => true - pod 'SignalCoreKit', git: 'https://github.com/signalapp/SignalCoreKit.git', :inhibit_warnings => true + pod 'SignalCoreKit', git: 'https://github.com/mpretty-cyro/SignalCoreKit.git', branch: 'session-version', :inhibit_warnings => true pod 'SwiftProtobuf', '~> 1.5.0', :inhibit_warnings => true pod 'YapDatabase/SQLCipher', :git => 'https://github.com/loki-project/session-ios-yap-database.git', branch: 'signal-release', :inhibit_warnings => true pod 'YYImage', git: 'https://github.com/signalapp/YYImage', :inhibit_warnings => true @@ -66,8 +69,8 @@ target 'SessionMessagingKit' do pod 'PureLayout', '~> 3.1.8', :inhibit_warnings => true pod 'Reachability', :inhibit_warnings => true pod 'SAMKeychain', :inhibit_warnings => true - pod 'SignalCoreKit', git: 'https://github.com/signalapp/SignalCoreKit.git', :inhibit_warnings => true - pod 'Sodium', '~> 0.8.0', :inhibit_warnings => true + pod 'SignalCoreKit', git: 'https://github.com/mpretty-cyro/SignalCoreKit.git', branch: 'session-version', :inhibit_warnings => true + pod 'Sodium', '~> 0.9.1', :inhibit_warnings => true pod 'SwiftProtobuf', '~> 1.5.0', :inhibit_warnings => true pod 'YapDatabase/SQLCipher', :git => 'https://github.com/loki-project/session-ios-yap-database.git', branch: 'signal-release', :inhibit_warnings => true end @@ -76,8 +79,8 @@ target 'SessionSnodeKit' do pod 'CryptoSwift', :inhibit_warnings => true pod 'Curve25519Kit', git: 'https://github.com/signalapp/Curve25519Kit.git', :inhibit_warnings => true pod 'PromiseKit', :inhibit_warnings => true - pod 'SignalCoreKit', git: 'https://github.com/signalapp/SignalCoreKit.git', :inhibit_warnings => true - pod 'Sodium', '~> 0.8.0', :inhibit_warnings => true + pod 'SignalCoreKit', git: 'https://github.com/mpretty-cyro/SignalCoreKit.git', branch: 'session-version', :inhibit_warnings => true + pod 'Sodium', '~> 0.9.1', :inhibit_warnings => true pod 'YapDatabase/SQLCipher', :git => 'https://github.com/loki-project/session-ios-yap-database.git', branch: 'signal-release', :inhibit_warnings => true end @@ -89,7 +92,7 @@ target 'SessionUtilitiesKit' do pod 'PromiseKit', :inhibit_warnings => true pod 'PureLayout', '~> 3.1.8', :inhibit_warnings => true pod 'SAMKeychain', :inhibit_warnings => true - pod 'SignalCoreKit', git: 'https://github.com/signalapp/SignalCoreKit.git', :inhibit_warnings => true + pod 'SignalCoreKit', git: 'https://github.com/mpretty-cyro/SignalCoreKit.git', branch: 'session-version', :inhibit_warnings => true pod 'YapDatabase/SQLCipher', :git => 'https://github.com/loki-project/session-ios-yap-database.git', branch: 'signal-release', :inhibit_warnings => true end diff --git a/Podfile.lock b/Podfile.lock index 804a148c7..0e031ba47 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -14,41 +14,41 @@ PODS: - AFNetworking/Serialization (4.0.1) - AFNetworking/UIKit (4.0.1): - AFNetworking/NSURLSession - - CocoaLumberjack (3.6.2): - - CocoaLumberjack/Core (= 3.6.2) - - CocoaLumberjack/Core (3.6.2) - - CryptoSwift (1.3.2) + - CocoaLumberjack (3.7.4): + - CocoaLumberjack/Core (= 3.7.4) + - CocoaLumberjack/Core (3.7.4) + - CryptoSwift (1.4.2) - Curve25519Kit (2.1.0): - CocoaLumberjack - SignalCoreKit - - GRKOpenSSLFramework (1.0.2.20) - HKDFKit (0.0.3) - Mantle (2.1.0): - Mantle/extobjc (= 2.1.0) - Mantle/extobjc (2.1.0) - - NVActivityIndicatorView (5.0.1): - - NVActivityIndicatorView/Base (= 5.0.1) - - NVActivityIndicatorView/Base (5.0.1) - - PromiseKit (6.13.1): - - PromiseKit/CorePromise (= 6.13.1) - - PromiseKit/Foundation (= 6.13.1) - - PromiseKit/UIKit (= 6.13.1) - - PromiseKit/CorePromise (6.13.1) - - PromiseKit/Foundation (6.13.1): + - NVActivityIndicatorView (5.1.1): + - NVActivityIndicatorView/Base (= 5.1.1) + - NVActivityIndicatorView/Base (5.1.1) + - OpenSSL-Universal (1.1.1200) + - PromiseKit (6.15.3): + - PromiseKit/CorePromise (= 6.15.3) + - PromiseKit/Foundation (= 6.15.3) + - PromiseKit/UIKit (= 6.15.3) + - PromiseKit/CorePromise (6.15.3) + - PromiseKit/Foundation (6.15.3): - PromiseKit/CorePromise - - PromiseKit/UIKit (6.13.1): + - PromiseKit/UIKit (6.15.3): - PromiseKit/CorePromise - - PureLayout (3.1.8) + - PureLayout (3.1.9) - Reachability (3.2) - SAMKeychain (1.5.3) - SignalCoreKit (1.0.0): - CocoaLumberjack - - GRKOpenSSLFramework - - Sodium (0.8.0) - - SQLCipher (4.4.0): - - SQLCipher/standard (= 4.4.0) - - SQLCipher/common (4.4.0) - - SQLCipher/standard (4.4.0): + - OpenSSL-Universal + - Sodium (0.9.1) + - SQLCipher (4.5.0): + - SQLCipher/standard (= 4.5.0) + - SQLCipher/common (4.5.0) + - SQLCipher/standard (4.5.0): - SQLCipher/common - SwiftProtobuf (1.5.0) - YapDatabase/SQLCipher (3.1.1): @@ -124,7 +124,6 @@ DEPENDENCIES: - AFNetworking - CryptoSwift - Curve25519Kit (from `https://github.com/signalapp/Curve25519Kit.git`) - - GRKOpenSSLFramework - HKDFKit - Mantle (from `https://github.com/signalapp/Mantle`, branch `signal-master`) - NVActivityIndicatorView @@ -132,8 +131,8 @@ DEPENDENCIES: - PureLayout (~> 3.1.8) - Reachability - SAMKeychain - - SignalCoreKit (from `https://github.com/signalapp/SignalCoreKit.git`) - - Sodium (~> 0.8.0) + - SignalCoreKit (from `https://github.com/mpretty-cyro/SignalCoreKit.git`, branch `session-version`) + - Sodium (~> 0.9.1) - SwiftProtobuf (~> 1.5.0) - YapDatabase/SQLCipher (from `https://github.com/loki-project/session-ios-yap-database.git`, branch `signal-release`) - YYImage (from `https://github.com/signalapp/YYImage`) @@ -144,9 +143,9 @@ SPEC REPOS: - AFNetworking - CocoaLumberjack - CryptoSwift - - GRKOpenSSLFramework - HKDFKit - NVActivityIndicatorView + - OpenSSL-Universal - PromiseKit - PureLayout - Reachability @@ -163,7 +162,8 @@ EXTERNAL SOURCES: :branch: signal-master :git: https://github.com/signalapp/Mantle SignalCoreKit: - :git: https://github.com/signalapp/SignalCoreKit.git + :branch: session-version + :git: https://github.com/mpretty-cyro/SignalCoreKit.git YapDatabase: :branch: signal-release :git: https://github.com/loki-project/session-ios-yap-database.git @@ -175,39 +175,39 @@ CHECKOUT OPTIONS: :commit: 4fc1c10e98fff2534b5379a9bb587430fdb8e577 :git: https://github.com/signalapp/Curve25519Kit.git Mantle: - :commit: b72c2d1e6132501db906de2cffa8ded7803c54f4 + :commit: e7e46253bb01ce39525d90aa69ed9e85e758bfc4 :git: https://github.com/signalapp/Mantle SignalCoreKit: - :commit: 21c092e94b307690957b50f2305e5e65d28fa89e - :git: https://github.com/signalapp/SignalCoreKit.git + :commit: b6ff159ca01679d5d9f206ede8475caeb0dc3225 + :git: https://github.com/mpretty-cyro/SignalCoreKit.git YapDatabase: :commit: 5806f6b6e0b34124ee09283a9eca9ce7e6eaf14e :git: https://github.com/loki-project/session-ios-yap-database.git YYImage: - :commit: d91910e6f313a255febbf69795198e74259bd51c + :commit: 62a4cede20bcf31da73d18163408e46a92f171c6 :git: https://github.com/signalapp/YYImage SPEC CHECKSUMS: AFNetworking: 7864c38297c79aaca1500c33288e429c3451fdce - CocoaLumberjack: bd155f2dd06c0e0b03f876f7a3ee55693122ec94 - CryptoSwift: 093499be1a94b0cae36e6c26b70870668cb56060 + CocoaLumberjack: 543c79c114dadc3b1aba95641d8738b06b05b646 + CryptoSwift: a532e74ed010f8c95f611d00b8bbae42e9fe7c17 Curve25519Kit: e63f9859ede02438ae3defc5e1a87e09d1ec7ee6 - GRKOpenSSLFramework: dc635b0a9d4cd8af2a9ff80a61e779e21b69dfd8 HKDFKit: c058305d6f64b84f28c50bd7aa89574625bcb62a Mantle: 2fa750afa478cd625a94230fbf1c13462f29395b - NVActivityIndicatorView: 738e843cb8924e9e4fc3e559d0728031624bf860 - PromiseKit: 28fda91c973cc377875d8c0ea4f973013c05b6db - PureLayout: a4afb3d79dd958564ce33d22c89f407280d8e6a8 + NVActivityIndicatorView: 1f6c5687f1171810aa27a3296814dc2d7dec3667 + OpenSSL-Universal: 3b8c0d6268fbd5d3ac44f97338e2fd16a73d9dbf + PromiseKit: 3b2b6995e51a954c46dbc550ce3da44fbfb563c5 + PureLayout: 5fb5e5429519627d60d079ccb1eaa7265ce7cf88 Reachability: 33e18b67625424e47b6cde6d202dce689ad7af96 SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c - SignalCoreKit: 4562b2bbd9830077439ca003f952a798457d4ea5 - Sodium: 63c0ca312a932e6da481689537d4b35568841bdc - SQLCipher: e434ed542b24f38ea7b36468a13f9765e1b5c072 + SignalCoreKit: 1fbd8732163ef76de16cd1107d1fa3684b607e5d + Sodium: 23d11554ecd556196d313cf6130d406dfe7ac6da + SQLCipher: 98dc22f27c0b1790d39e710d440f22a466ebdb59 SwiftProtobuf: 241400280f912735c1e1b9fe675fdd2c6c4d42e2 YapDatabase: b418a4baa6906e8028748938f9159807fd039af4 - YYImage: 6db68da66f20d9f169ceb94dfb9947c3867b9665 + YYImage: f1ddd15ac032a58b78bbed1e012b50302d318331 ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb -PODFILE CHECKSUM: 50e6a35c838ba28d2ee02bc6018fdd297c04e55f +PODFILE CHECKSUM: 62496725424703f6390b7c9862c114cee36b26a1 -COCOAPODS: 1.10.1 +COCOAPODS: 1.11.2 diff --git a/SessionMessagingKit/Utilities/ProofOfWork.swift b/SessionMessagingKit/Utilities/ProofOfWork.swift index d82640bce..4cb500c97 100644 --- a/SessionMessagingKit/Utilities/ProofOfWork.swift +++ b/SessionMessagingKit/Utilities/ProofOfWork.swift @@ -29,7 +29,7 @@ enum ProofOfWork { value = newValue } // Encode as base 64 - let base64EncodedNonce = nonce.bigEndianBytes.toBase64()! + let base64EncodedNonce = nonce.bigEndianBytes.toBase64() // Return return (timestamp, base64EncodedNonce) } diff --git a/SessionSnodeKit/SnodeAPI.swift b/SessionSnodeKit/SnodeAPI.swift index 6512ffbfc..954230953 100644 --- a/SessionSnodeKit/SnodeAPI.swift +++ b/SessionSnodeKit/SnodeAPI.swift @@ -304,8 +304,9 @@ public final class SnodeAPI : NSObject { let onsName = onsName.lowercased() // Hash the ONS name using BLAKE2b let nameAsData = [UInt8](onsName.data(using: String.Encoding.utf8)!) - guard let nameHash = sodium.genericHash.hash(message: nameAsData), - let base64EncodedNameHash = nameHash.toBase64() else { return Promise(error: Error.hashingFailed) } + guard let nameHash = sodium.genericHash.hash(message: nameAsData) else { return Promise(error: Error.hashingFailed) } + + let base64EncodedNameHash = nameHash.toBase64() // Ask 3 different snodes for the Session ID associated with the given name hash let parameters: [String:Any] = [ "endpoint" : "ons_resolve", @@ -433,7 +434,7 @@ public final class SnodeAPI : NSObject { "lastHash" : lastHash, // "timestamp" : timestamp, // "pubkey_ed25519" : ed25519PublicKey, -// "signature" : signature.toBase64()! +// "signature" : signature.toBase64() ] return invoke(.getMessages, on: snode, associatedWith: publicKey, parameters: parameters) } @@ -473,7 +474,7 @@ public final class SnodeAPI : NSObject { "pubkey" : userX25519PublicKey, "pubkey_ed25519" : userED25519KeyPair.publicKey.toHexString(), "messages": serverHashes, - "signature": signature.toBase64()! + "signature": signature.toBase64() ] return attempt(maxRetryCount: maxRetryCount, recoveringOn: Threading.workQueue) { invoke(.deleteMessage, on: snode, associatedWith: publicKey, parameters: parameters).map2{ rawResponse -> [String:Bool] in @@ -520,7 +521,7 @@ public final class SnodeAPI : NSObject { "pubkey" : userX25519PublicKey, "pubkey_ed25519" : userED25519KeyPair.publicKey.toHexString(), "timestamp" : timestamp, - "signature" : signature.toBase64()! + "signature" : signature.toBase64() ] return attempt(maxRetryCount: maxRetryCount, recoveringOn: Threading.workQueue) { invoke(.clearAllData, on: snode, parameters: parameters).map2 { rawResponse -> [String:Bool] in From ab9f2a0c7b0fc443af87c6e330f78a6330fdf1a2 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Tue, 11 Jan 2022 14:37:55 +1100 Subject: [PATCH 03/26] Minor formatting and code cleanup of attachments --- .../AttachmentApprovalViewController.swift | 518 ++++++++++-------- 1 file changed, 279 insertions(+), 239 deletions(-) diff --git a/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentApprovalViewController.swift b/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentApprovalViewController.swift index 66d0dc51e..bb72c1cc4 100644 --- a/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentApprovalViewController.swift +++ b/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentApprovalViewController.swift @@ -10,17 +10,25 @@ import SessionUIKit import CoreServices @objc -public protocol AttachmentApprovalViewControllerDelegate: class { - func attachmentApproval(_ attachmentApproval: AttachmentApprovalViewController, - didApproveAttachments attachments: [SignalAttachment], messageText: String?) +public protocol AttachmentApprovalViewControllerDelegate: AnyObject { + func attachmentApproval( + _ attachmentApproval: AttachmentApprovalViewController, + didApproveAttachments attachments: [SignalAttachment], + messageText: String? + ) func attachmentApprovalDidCancel(_ attachmentApproval: AttachmentApprovalViewController) - func attachmentApproval(_ attachmentApproval: AttachmentApprovalViewController, - didChangeMessageText newMessageText: String?) + func attachmentApproval( + _ attachmentApproval: AttachmentApprovalViewController, + didChangeMessageText newMessageText: String? + ) @objc - optional func attachmentApproval(_ attachmentApproval: AttachmentApprovalViewController, didRemoveAttachment attachment: SignalAttachment) + optional func attachmentApproval( + _ attachmentApproval: AttachmentApprovalViewController, + didRemoveAttachment attachment: SignalAttachment + ) @objc optional func attachmentApprovalDidTapAddMore(_ attachmentApproval: AttachmentApprovalViewController) @@ -38,19 +46,72 @@ public enum AttachmentApprovalViewControllerMode: UInt { @objc public class AttachmentApprovalViewController: UIPageViewController, UIPageViewControllerDataSource, UIPageViewControllerDelegate { + @objc public enum Mode: UInt { + case modal + case sharedNavigation + } // MARK: - Properties - private let mode: AttachmentApprovalViewControllerMode + private let mode: Mode private let isAddMoreVisible: Bool public weak var approvalDelegate: AttachmentApprovalViewControllerDelegate? public var isEditingCaptions = false { - didSet { - updateContents() + didSet { updateContents() } + } + + let attachmentItemCollection: AttachmentItemCollection + + var attachmentItems: [SignalAttachmentItem] { + return attachmentItemCollection.attachmentItems + } + + var attachments: [SignalAttachment] { + return attachmentItems.map { (attachmentItem) in + autoreleasepool { + return self.processedAttachment(forAttachmentItem: attachmentItem) + } } } + + public var pageViewControllers: [AttachmentPrepViewController]? { + return viewControllers?.compactMap { $0 as? AttachmentPrepViewController } + } + + public var currentPageViewController: AttachmentPrepViewController? { + return pageViewControllers?.first + } + + var currentItem: SignalAttachmentItem? { + get { return currentPageViewController?.attachmentItem } + set { setCurrentItem(newValue, direction: .forward, animated: false) } + } + + private var cachedPages: [SignalAttachmentItem: AttachmentPrepViewController] = [:] + + public var shouldHideControls: Bool { + guard let pageViewController: AttachmentPrepViewController = pageViewControllers?.first else { + return false + } + + return pageViewController.shouldHideControls + } + + override public var inputAccessoryView: UIView? { + bottomToolView.layoutIfNeeded() + return bottomToolView + } + + override public var canBecomeFirstResponder: Bool { + return !shouldHideControls + } + + public var messageText: String? { + get { return bottomToolView.attachmentTextToolbar.messageText } + set { bottomToolView.attachmentTextToolbar.messageText = newValue } + } // MARK: - Initializers @@ -59,29 +120,34 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC notImplemented() } - let kSpacingBetweenItems: CGFloat = 20 - @objc - required public init(mode: AttachmentApprovalViewControllerMode, - attachments: [SignalAttachment]) { + required public init( + mode: Mode, + attachments: [SignalAttachment] + ) { assert(attachments.count > 0) self.mode = mode let attachmentItems = attachments.map { SignalAttachmentItem(attachment: $0 )} - self.isAddMoreVisible = mode == .sharedNavigation + self.isAddMoreVisible = (mode == .sharedNavigation) self.attachmentItemCollection = AttachmentItemCollection(attachmentItems: attachmentItems, isAddMoreVisible: isAddMoreVisible) - let options: [UIPageViewController.OptionsKey: Any] = [.interPageSpacing: kSpacingBetweenItems] - super.init(transitionStyle: .scroll, - navigationOrientation: .horizontal, - options: options) + super.init( + transitionStyle: .scroll, + navigationOrientation: .horizontal, + options: [ + .interPageSpacing: kSpacingBetweenItems + ] + ) self.dataSource = self self.delegate = self - NotificationCenter.default.addObserver(self, - selector: #selector(didBecomeActive), - name: NSNotification.Name.OWSApplicationDidBecomeActive, - object: nil) + NotificationCenter.default.addObserver( + self, + selector: #selector(didBecomeActive), + name: .OWSApplicationDidBecomeActive, + object: nil + ) } deinit { @@ -89,62 +155,78 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC } @objc - public class func wrappedInNavController(attachments: [SignalAttachment], approvalDelegate: AttachmentApprovalViewControllerDelegate) -> OWSNavigationController { + public class func wrappedInNavController( + attachments: [SignalAttachment], + approvalDelegate: AttachmentApprovalViewControllerDelegate + ) -> OWSNavigationController { let vc = AttachmentApprovalViewController(mode: .modal, attachments: attachments) vc.approvalDelegate = approvalDelegate + let navController = OWSNavigationController(rootViewController: vc) navController.ows_prefersStatusBarHidden = true + return navController } - // MARK: - Notifications - - @objc func didBecomeActive() { - AssertIsOnMainThread() - - updateContents() - } - - // MARK: - Subviews - - var galleryRailView: GalleryRailView { - return bottomToolView.galleryRailView - } - - var attachmentTextToolbar: AttachmentTextToolbar { - return bottomToolView.attachmentTextToolbar - } - - lazy var bottomToolView: AttachmentApprovalInputAccessoryView = { + // MARK: - UI + + private let kSpacingBetweenItems: CGFloat = 20 + + public override var prefersStatusBarHidden: Bool { return true } + + private lazy var bottomToolView: AttachmentApprovalInputAccessoryView = { let bottomToolView = AttachmentApprovalInputAccessoryView() bottomToolView.delegate = self + bottomToolView.attachmentTextToolbar.attachmentTextToolbarDelegate = self + bottomToolView.galleryRailView.delegate = self return bottomToolView }() - lazy var touchInterceptorView = UIView() + private var galleryRailView: GalleryRailView { return bottomToolView.galleryRailView } - // MARK: - View Lifecycle + private lazy var touchInterceptorView: UIView = { + let view: UIView = UIView() + view.translatesAutoresizingMaskIntoConstraints = false + view.isHidden = true + + let tapGesture = UITapGestureRecognizer( + target: self, + action: #selector(didTapTouchInterceptorView(gesture:)) + ) + view.addGestureRecognizer(tapGesture) + + return view + }() - public override var prefersStatusBarHidden: Bool { - return true - } + private lazy var pagerScrollView: UIScrollView? = { + // This is kind of a hack. Since we don't have first class access to the superview's `scrollView` + // we traverse the view hierarchy until we find it. + let pagerScrollView = view.subviews.first { $0 is UIScrollView } as? UIScrollView + assert(pagerScrollView != nil) + + return pagerScrollView + }() + + // MARK: - Lifecycle override public func viewDidLoad() { super.viewDidLoad() self.view.backgroundColor = Colors.navigationBarBackground - - // avoid an unpleasant "bounce" which doesn't make sense in the context of a single item. - pagerScrollView?.isScrollEnabled = attachmentItems.count > 1 - - // Bottom Toolbar - galleryRailView.delegate = self - attachmentTextToolbar.attachmentTextToolbarDelegate = self - - // Navigation - + + let backgroundImage: UIImage = UIImage(color: Colors.navigationBarBackground) self.navigationItem.title = nil + self.navigationController?.navigationBar.setBackgroundImage(UIImage(), for: .default) + self.navigationController?.navigationBar.shadowImage = UIImage() + self.navigationController?.navigationBar.isTranslucent = false + self.navigationController?.navigationBar.barTintColor = Colors.navigationBarBackground + (self.navigationController?.navigationBar as? OWSNavigationBar)?.respectsTheme = true + self.navigationController?.navigationBar.backgroundColor = Colors.navigationBarBackground + self.navigationController?.navigationBar.setBackgroundImage(backgroundImage, for: .default) + + // Avoid an unpleasant "bounce" which doesn't make sense in the context of a single item. + pagerScrollView?.isScrollEnabled = (attachmentItems.count > 1) guard let firstItem = attachmentItems.first else { owsFailDebug("firstItem was unexpectedly nil") @@ -152,36 +234,21 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC } self.setCurrentItem(firstItem, direction: .forward, animated: false) + + view.addSubview(touchInterceptorView) // layout immediately to avoid animating the layout process during the transition - self.currentPageViewController.view.layoutIfNeeded() + UIView.performWithoutAnimation { + self.currentPageViewController?.view.layoutIfNeeded() + } - view.addSubview(touchInterceptorView) - touchInterceptorView.autoPinEdgesToSuperviewEdges() - touchInterceptorView.isHidden = true - let tapGesture = UITapGestureRecognizer(target: self, action: #selector(didTapTouchInterceptorView(gesture:))) - touchInterceptorView.addGestureRecognizer(tapGesture) + setupLayout() } override public func viewWillAppear(_ animated: Bool) { Logger.debug("") super.viewWillAppear(animated) - guard let navigationBar = navigationController?.navigationBar as? OWSNavigationBar else { - owsFailDebug("navigationBar was nil or unexpected class") - return - } - - // Loki: Set navigation bar background color - navigationBar.setBackgroundImage(UIImage(), for: UIBarMetrics.default) - navigationBar.shadowImage = UIImage() - navigationBar.isTranslucent = false - navigationBar.barTintColor = Colors.navigationBarBackground - navigationBar.respectsTheme = true - navigationBar.backgroundColor = Colors.navigationBarBackground - let backgroundImage = UIImage(color: Colors.navigationBarBackground) - navigationBar.setBackgroundImage(backgroundImage, for: .default) - updateContents() } @@ -197,7 +264,23 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC Logger.debug("") super.viewWillDisappear(animated) } + + // MARK: - Layout + + private func setupLayout() { + touchInterceptorView.autoPinEdgesToSuperviewEdges() + } + + // MARK: - Notifications + @objc func didBecomeActive() { + AssertIsOnMainThread() + + updateContents() + } + + // MARK: - Contents + private func updateContents() { updateNavigationBar() updateInputAccessory() @@ -207,40 +290,26 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC // MARK: - Input Accessory - override public var inputAccessoryView: UIView? { - bottomToolView.layoutIfNeeded() - return bottomToolView - } - - override public var canBecomeFirstResponder: Bool { - return !shouldHideControls - } - public func updateInputAccessory() { var currentPageViewController: AttachmentPrepViewController? - if pageViewControllers.count == 1 { - currentPageViewController = pageViewControllers.first + + if pageViewControllers?.count == 1 { + currentPageViewController = pageViewControllers?.first } let currentAttachmentItem: SignalAttachmentItem? = currentPageViewController?.attachmentItem - let hasPresentedView = self.presentedViewController != nil + let hasPresentedView = (self.presentedViewController != nil) let isToolbarFirstResponder = bottomToolView.hasFirstResponder + if !shouldHideControls, !isFirstResponder, !hasPresentedView, !isToolbarFirstResponder { becomeFirstResponder() } - bottomToolView.update(isEditingCaptions: isEditingCaptions, - currentAttachmentItem: currentAttachmentItem, - shouldHideControls: shouldHideControls) - } - - public var messageText: String? { - get { - return attachmentTextToolbar.messageText - } - set { - attachmentTextToolbar.messageText = newValue - } + bottomToolView.update( + isEditingCaptions: isEditingCaptions, + currentAttachmentItem: currentAttachmentItem, + shouldHideControls: shouldHideControls + ) } // MARK: - Navigation Bar @@ -254,10 +323,18 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC guard !isEditingCaptions else { // Hide all navigation bar items while the caption view is open. - self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: NSLocalizedString("ATTACHMENT_APPROVAL_CAPTION_TITLE", comment: "Title for 'caption' mode of the attachment approval view."), style: .plain, target: nil, action: nil) + self.navigationItem.leftBarButtonItem = UIBarButtonItem( + //"Title for 'caption' mode of the attachment approval view." + title: "ATTACHMENT_APPROVAL_CAPTION_TITLE".localized(), + style: .plain, + target: nil, + action: nil + ) - let doneButton = navigationBarButton(imageName: "image_editor_checkmark_full", - selector: #selector(didTapCaptionDone(sender:))) + let doneButton = navigationBarButton( + imageName: "image_editor_checkmark_full", + selector: #selector(didTapCaptionDone(sender:)) + ) let navigationBarItems = [doneButton] updateNavigationBar(navigationBarItems: navigationBarItems) return @@ -265,29 +342,23 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC var navigationBarItems = [UIView]() - if let viewControllers = viewControllers, - viewControllers.count == 1, - let firstViewController = viewControllers.first as? AttachmentPrepViewController { + if viewControllers?.count == 1, let firstViewController: AttachmentPrepViewController = viewControllers?.first as? AttachmentPrepViewController { navigationBarItems = firstViewController.navigationBarItems() // Show the caption UI if there's more than one attachment // OR if the attachment already has a caption. - let attachmentCount = attachmentItemCollection.count - var shouldShowCaptionUI = attachmentCount > 0 - if let captionText = firstViewController.attachmentItem.captionText, captionText.count > 0 { - shouldShowCaptionUI = true - } - if shouldShowCaptionUI { - let captionButton = navigationBarButton(imageName: "image_editor_caption", - selector: #selector(didTapCaption(sender:))) + if attachmentItemCollection.count > 0, (firstViewController.attachmentItem.captionText?.count ?? 0) > 0 { + let captionButton = navigationBarButton( + imageName: "image_editor_caption", + selector: #selector(didTapCaption(sender:)) + ) navigationBarItems.append(captionButton) } } updateNavigationBar(navigationBarItems: navigationBarItems) - let hasCancel = (mode != .sharedNavigation) - if hasCancel { + if mode != .sharedNavigation { // Mimic a UIBarButtonItem of type .cancel, but with a shadow. let cancelButton = OWSButton(title: CommonStrings.cancelButton) { [weak self] in self?.cancelPressed() @@ -300,10 +371,11 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC } cancelButton.sizeToFit() navigationItem.leftBarButtonItem = UIBarButtonItem(customView: cancelButton) - } else { + } + else { // Mimic a conventional back button, but with a shadow. let isRTL = CurrentAppContext().isRTL - let imageName = isRTL ? "NavBarBackRTL" : "NavBarBack" + let imageName = (isRTL ? "NavBarBackRTL" : "NavBarBack") let backButton = OWSButton(imageName: imageName, tintColor: Colors.text) { [weak self] in self?.navigationController?.popViewController(animated: true) } @@ -328,40 +400,44 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC // Default back button is 1.5 pixel lower than our extracted image. let kTopInsetPadding: CGFloat = 1.5 - backButton.imageEdgeInsets = UIEdgeInsets(top: kTopInsetPadding, left: kExtraLeftPadding, bottom: 0, right: 0) + backButton.imageEdgeInsets = UIEdgeInsets( + top: kTopInsetPadding, + left: kExtraLeftPadding, + bottom: 0, + right: 0 + ) var backImageSize = CGSize.zero if let backImage = UIImage(named: imageName) { backImageSize = backImage.size - } else { + } + else { owsFailDebug("Missing backImage.") } - backButton.frame = CGRect(origin: .zero, size: CGSize(width: backImageSize.width + kExtraRightPadding, - height: backImageSize.height + kExtraHeightPadding)) + backButton.frame = CGRect( + origin: .zero, + size: CGSize( + width: backImageSize.width + kExtraRightPadding, + height: backImageSize.height + kExtraHeightPadding + ) + ) // Note: using a custom leftBarButtonItem breaks the interactive pop gesture. navigationItem.leftBarButtonItem = UIBarButtonItem(customView: backButton) } } - // MARK: - Control Visibility - - public var shouldHideControls: Bool { - guard let pageViewController = pageViewControllers.first else { - return false - } - return pageViewController.shouldHideControls - } - // MARK: - View Helpers func remove(attachmentItem: SignalAttachmentItem) { if attachmentItem == currentItem { if let nextItem = attachmentItemCollection.itemAfter(item: attachmentItem) { setCurrentItem(nextItem, direction: .forward, animated: true) - } else if let prevItem = attachmentItemCollection.itemBefore(item: attachmentItem) { + } + else if let prevItem = attachmentItemCollection.itemBefore(item: attachmentItem) { setCurrentItem(prevItem, direction: .reverse, animated: true) - } else { + } + else { owsFailDebug("removing last item shouldn't be possible because rail should not be visible") return } @@ -372,30 +448,27 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC return } - UIView.animate(withDuration: 0.2, - animations: { - // shrink stack view item until it disappears - cell.isHidden = true - - // simultaneously fade out - cell.alpha = 0 - }, - completion: { _ in - self.attachmentItemCollection.remove(item: attachmentItem) - self.approvalDelegate?.attachmentApproval?(self, didRemoveAttachment: attachmentItem.attachment) - self.updateMediaRail() - }) + UIView.animate( + withDuration: 0.2, + animations: { + // shrink stack view item until it disappears + cell.isHidden = true + + // simultaneously fade out + cell.alpha = 0 + }, + completion: { [weak self] _ in + self?.attachmentItemCollection.remove(item: attachmentItem) + + if let strongSelf: AttachmentApprovalViewController = self { + self?.approvalDelegate?.attachmentApproval?(strongSelf, didRemoveAttachment: attachmentItem.attachment) + } + + self?.updateMediaRail() + } + ) } - lazy var pagerScrollView: UIScrollView? = { - // This is kind of a hack. Since we don't have first class access to the superview's `scrollView` - // we traverse the view hierarchy until we find it. - let pagerScrollView = view.subviews.first { $0 is UIScrollView } as? UIScrollView - assert(pagerScrollView != nil) - - return pagerScrollView - }() - // MARK: - UIPageViewControllerDelegate public func pageViewController(_ pageViewController: UIPageViewController, willTransitionTo pendingViewControllers: [UIViewController]) { @@ -440,10 +513,7 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC } let currentItem = currentViewController.attachmentItem - guard let previousItem = attachmentItem(before: currentItem) else { - return nil - } - + guard let previousItem = attachmentItem(before: currentItem) else { return nil } guard let previousPage: AttachmentPrepViewController = buildPage(item: previousItem) else { return nil } @@ -460,10 +530,7 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC } let currentItem = currentViewController.attachmentItem - guard let nextItem = attachmentItem(after: currentItem) else { - return nil - } - + guard let nextItem = attachmentItem(after: currentItem) else { return nil } guard let nextPage: AttachmentPrepViewController = buildPage(item: nextItem) else { return nil } @@ -471,38 +538,19 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC return nextPage } - public var currentPageViewController: AttachmentPrepViewController { - return pageViewControllers.first! - } - - public var pageViewControllers: [AttachmentPrepViewController] { - return super.viewControllers!.map { $0 as! AttachmentPrepViewController } - } - @objc public override func setViewControllers(_ viewControllers: [UIViewController]?, direction: UIPageViewController.NavigationDirection, animated: Bool, completion: ((Bool) -> Void)? = nil) { - super.setViewControllers(viewControllers, - direction: direction, - animated: animated) { [weak self] (finished) in - if let completion = completion { - completion(finished) - } - self?.updateContents() + super.setViewControllers( + viewControllers, + direction: direction, + animated: animated + ) { [weak self] finished in + completion?(finished) + self?.updateContents() } } - var currentItem: SignalAttachmentItem! { - get { - return currentPageViewController.attachmentItem - } - set { - setCurrentItem(newValue, direction: .forward, animated: false) - } - } - - private var cachedPages: [SignalAttachmentItem: AttachmentPrepViewController] = [:] private func buildPage(item: SignalAttachmentItem) -> AttachmentPrepViewController? { - if let cachedPage = cachedPages[item] { Logger.debug("cache hit.") return cachedPage @@ -516,8 +564,8 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC return viewController } - private func setCurrentItem(_ item: SignalAttachmentItem, direction: UIPageViewController.NavigationDirection, animated isAnimated: Bool) { - guard let page = self.buildPage(item: item) else { + private func setCurrentItem(_ item: SignalAttachmentItem?, direction: UIPageViewController.NavigationDirection, animated isAnimated: Bool) { + guard let item: SignalAttachmentItem = item, let page = self.buildPage(item: item) else { owsFailDebug("unexpectedly unable to build new page") return } @@ -536,42 +584,34 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC let cellViewBuilder: (GalleryRailItem) -> GalleryRailCellView = { [weak self] railItem in switch railItem { - case is AddMoreRailItem: - return GalleryRailCellView() - case is SignalAttachmentItem: - let cell = ApprovalRailCellView() - cell.approvalRailCellDelegate = self - return cell - default: - owsFailDebug("unexpted rail item type: \(railItem)") - return GalleryRailCellView() + case is AddMoreRailItem: + return GalleryRailCellView() + + case is SignalAttachmentItem: + let cell = ApprovalRailCellView() + cell.approvalRailCellDelegate = self + return cell + + default: + owsFailDebug("unexpted rail item type: \(railItem)") + return GalleryRailCellView() } } - galleryRailView.configureCellViews(itemProvider: attachmentItemCollection, - focusedItem: currentItem, - cellViewBuilder: cellViewBuilder) + galleryRailView.configureCellViews( + itemProvider: attachmentItemCollection, + focusedItem: currentItem, + cellViewBuilder: cellViewBuilder + ) if isAddMoreVisible { galleryRailView.isHidden = false - } else if attachmentItemCollection.attachmentItems.count > 1 { - galleryRailView.isHidden = false - } else { - galleryRailView.isHidden = true } - } - - let attachmentItemCollection: AttachmentItemCollection - - var attachmentItems: [SignalAttachmentItem] { - return attachmentItemCollection.attachmentItems - } - - var attachments: [SignalAttachment] { - return attachmentItems.map { (attachmentItem) in - autoreleasepool { - return self.processedAttachment(forAttachmentItem: attachmentItem) - } + else if attachmentItemCollection.attachmentItems.count > 1 { + galleryRailView.isHidden = false + } + else { + galleryRailView.isHidden = true } } @@ -596,18 +636,24 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC return attachmentItem.attachment } var dataUTI = kUTTypeImage as String - guard let dstData: Data = { - let isLossy: Bool = attachmentItem.attachment.mimeType.caseInsensitiveCompare(OWSMimeTypeImageJpeg) == .orderedSame + let maybeDstData: Data? = { + let isLossy: Bool = ( + attachmentItem.attachment.mimeType.caseInsensitiveCompare(OWSMimeTypeImageJpeg) == .orderedSame + ) + if isLossy { dataUTI = kUTTypeJPEG as String return dstImage.jpegData(compressionQuality: 0.9) - } else { + } + else { dataUTI = kUTTypePNG as String return dstImage.pngData() } - }() else { - owsFailDebug("Could not export for output.") - return attachmentItem.attachment + }() + + guard let dstData: Data = maybeDstData else { + owsFailDebug("Could not export for output.") + return attachmentItem.attachment } guard let dataSource = DataSourceValue.dataSource(with: dstData, utiType: dataUTI) else { owsFailDebug("Could not prepare data source for output.") @@ -693,18 +739,18 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC extension AttachmentApprovalViewController: AttachmentTextToolbarDelegate { func attachmentTextToolbarDidBeginEditing(_ attachmentTextToolbar: AttachmentTextToolbar) { - currentPageViewController.setAttachmentViewScale(.compact, animated: true) + currentPageViewController?.setAttachmentViewScale(.compact, animated: true) } func attachmentTextToolbarDidEndEditing(_ attachmentTextToolbar: AttachmentTextToolbar) { - currentPageViewController.setAttachmentViewScale(.fullsize, animated: true) + currentPageViewController?.setAttachmentViewScale(.fullsize, animated: true) } func attachmentTextToolbarDidTapSend(_ attachmentTextToolbar: AttachmentTextToolbar) { // Toolbar flickers in and out if there are errors // and remains visible momentarily after share extension is dismissed. // It's easiest to just hide it at this point since we're done with it. - currentPageViewController.shouldAllowAttachmentViewResizing = false + currentPageViewController?.shouldAllowAttachmentViewResizing = false attachmentTextToolbar.isUserInteractionEnabled = false attachmentTextToolbar.isHidden = true @@ -769,7 +815,7 @@ extension AttachmentApprovalViewController: GalleryRailViewDelegate { return } - guard let currentIndex = attachmentItems.firstIndex(of: currentItem) else { + guard let currentItem: SignalAttachmentItem = currentItem, let currentIndex = attachmentItems.firstIndex(of: currentItem) else { owsFailDebug("currentIndex was unexpectedly nil") return } @@ -779,7 +825,7 @@ extension AttachmentApprovalViewController: GalleryRailViewDelegate { return } - let direction: UIPageViewController.NavigationDirection = currentIndex < targetIndex ? .forward : .reverse + let direction: UIPageViewController.NavigationDirection = (currentIndex < targetIndex ? .forward : .reverse) self.setCurrentItem(targetItem, direction: direction, animated: true) } @@ -787,12 +833,6 @@ extension AttachmentApprovalViewController: GalleryRailViewDelegate { // MARK: - -enum KeyboardScenario { - case hidden, editingMessage, editingCaption -} - -// MARK: - - extension AttachmentApprovalViewController: ApprovalRailCellViewDelegate { func approvalRailCellView(_ approvalRailCellView: ApprovalRailCellView, didRemoveItem attachmentItem: SignalAttachmentItem) { remove(attachmentItem: attachmentItem) From a9718a7be5f8cb9f94c09594ed09a4d35a0e6a41 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Tue, 11 Jan 2022 14:38:42 +1100 Subject: [PATCH 04/26] Fixed a nav button issue Fixed a bug where the Cancel button on the attachment approval screen when sharing into the app wasn't working. --- SessionShareExtension/ThreadPickerVC.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SessionShareExtension/ThreadPickerVC.swift b/SessionShareExtension/ThreadPickerVC.swift index 414036bad..e11a2d6e0 100644 --- a/SessionShareExtension/ThreadPickerVC.swift +++ b/SessionShareExtension/ThreadPickerVC.swift @@ -172,7 +172,7 @@ final class ThreadPickerVC: UIViewController, UITableViewDataSource, UITableView } func attachmentApprovalDidCancel(_ attachmentApproval: AttachmentApprovalViewController) { - // Do nothing + dismiss(animated: true, completion: nil) } func attachmentApproval(_ attachmentApproval: AttachmentApprovalViewController, didChangeMessageText newMessageText: String?) { From dd9eeb5d617b7ee7f85be6444b66f92965c48c7c Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Wed, 12 Jan 2022 09:40:53 +1100 Subject: [PATCH 05/26] Added initial support for sharing URLs and text Updated the share extension to load URL previews. Updated the ThreadPickerVC to send plain text & URLs in the same way they are sent for normal messages. --- Session/Meta/MainAppContext.m | 4 + .../Attachments/SignalAttachment.swift | 12 ++ .../NotificationServiceExtensionContext.swift | 1 + .../ShareAppExtensionContext.swift | 1 + SessionShareExtension/ThreadPickerVC.swift | 33 +++- SessionUtilitiesKit/General/AppContext.h | 1 + .../Networking/ProxiedContentDownloader.swift | 2 +- .../AttachmentApprovalViewController.swift | 6 + .../MediaMessageView.swift | 146 ++++++++++++++++++ 9 files changed, 201 insertions(+), 5 deletions(-) diff --git a/Session/Meta/MainAppContext.m b/Session/Meta/MainAppContext.m index af13c0326..897281679 100644 --- a/Session/Meta/MainAppContext.m +++ b/Session/Meta/MainAppContext.m @@ -160,6 +160,10 @@ NSString *const ReportedApplicationStateDidChangeNotification = @"ReportedApplic return [UIApplication sharedApplication].applicationState == UIApplicationStateActive; } +- (BOOL)isShareExtension { + return NO; +} + - (BOOL)isRTL { static BOOL isRTL = NO; diff --git a/SessionMessagingKit/Sending & Receiving/Attachments/SignalAttachment.swift b/SessionMessagingKit/Sending & Receiving/Attachments/SignalAttachment.swift index 6ef9d9598..f65d8640c 100644 --- a/SessionMessagingKit/Sending & Receiving/Attachments/SignalAttachment.swift +++ b/SessionMessagingKit/Sending & Receiving/Attachments/SignalAttachment.swift @@ -114,6 +114,9 @@ public class SignalAttachment: NSObject { @objc public var captionText: String? + + @objc + public var linkPreviewDraft: OWSLinkPreviewDraft? @objc public var data: Data { @@ -292,6 +295,15 @@ public class SignalAttachment: NSObject { return nil } } + + @objc + public func text() -> String? { + guard let text = String(data: dataSource.data(), encoding: .utf8) else { + return nil + } + + return text + } // Returns the MIME type for this attachment or nil if no MIME type // can be identified. diff --git a/SessionNotificationServiceExtension/NotificationServiceExtensionContext.swift b/SessionNotificationServiceExtension/NotificationServiceExtensionContext.swift index b5b4dfe50..469f280e1 100644 --- a/SessionNotificationServiceExtension/NotificationServiceExtensionContext.swift +++ b/SessionNotificationServiceExtension/NotificationServiceExtensionContext.swift @@ -9,6 +9,7 @@ final class NotificationServiceExtensionContext : NSObject, AppContext { let appLaunchTime = Date() let isMainApp = false let isMainAppAndActive = false + var isShareExtension: Bool = false var openSystemSettingsAction: UIAlertAction? var wasWokenUpByPushNotification = true diff --git a/SessionShareExtension/ShareAppExtensionContext.swift b/SessionShareExtension/ShareAppExtensionContext.swift index 51c80bd95..e25e91f47 100644 --- a/SessionShareExtension/ShareAppExtensionContext.swift +++ b/SessionShareExtension/ShareAppExtensionContext.swift @@ -13,6 +13,7 @@ final class ShareAppExtensionContext: NSObject, AppContext { let appLaunchTime = Date() let isMainApp = false let isMainAppAndActive = false + var isShareExtension: Bool = true var mainWindow: UIWindow? var wasWokenUpByPushNotification: Bool = false diff --git a/SessionShareExtension/ThreadPickerVC.swift b/SessionShareExtension/ThreadPickerVC.swift index e11a2d6e0..0eef72d5f 100644 --- a/SessionShareExtension/ThreadPickerVC.swift +++ b/SessionShareExtension/ThreadPickerVC.swift @@ -147,19 +147,44 @@ final class ThreadPickerVC: UIViewController, UITableViewDataSource, UITableView } func attachmentApproval(_ attachmentApproval: AttachmentApprovalViewController, didApproveAttachments attachments: [SignalAttachment], messageText: String?) { + // Sharing a URL or plain text will populate the 'messageText' field so in those + // cases we should ignore the attachments + let isSharingUrl: Bool = (attachments.count == 1 && attachments[0].isUrl) + let isSharingText: Bool = (attachments.count == 1 && attachments[0].isText) + let finalAttachments: [SignalAttachment] = (isSharingUrl || isSharingText ? [] : attachments) + 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( + with: { transaction in + if isSharingUrl { + message.linkPreview = VisibleMessage.LinkPreview.from( + attachments[0].linkPreviewDraft, + using: transaction + ) + } + else { + tsMessage.save(with: transaction) + } + }, + completion: { + if isSharingUrl { + tsMessage.linkPreview = OWSLinkPreview.from(message.linkPreview) + + Storage.write { transaction in + tsMessage.save(with: transaction) + } + } + } + ) shareVC!.dismiss(animated: true, completion: nil) ModalActivityIndicatorViewController.present(fromViewController: shareVC!, canCancel: false, message: "vc_share_sending_message".localized()) { activityIndicator in - MessageSender.sendNonDurably(message, with: attachments, in: self.selectedThread!) + MessageSender.sendNonDurably(message, with: finalAttachments, in: self.selectedThread!) .done { [weak self] _ in activityIndicator.dismiss { } self?.shareVC?.shareViewWasCompleted() diff --git a/SessionUtilitiesKit/General/AppContext.h b/SessionUtilitiesKit/General/AppContext.h index 50305e99e..77d6ee718 100755 --- a/SessionUtilitiesKit/General/AppContext.h +++ b/SessionUtilitiesKit/General/AppContext.h @@ -35,6 +35,7 @@ NSString *NSStringForUIApplicationState(UIApplicationState value); @property (nonatomic, readonly) BOOL isMainApp; @property (nonatomic, readonly) BOOL isMainAppAndActive; +@property (nonatomic, readonly) BOOL isShareExtension; /// Whether the app was woken up by a silent push notification. This is important for determining whether attachments should be downloaded or not. @property (nonatomic) BOOL wasWokenUpByPushNotification; diff --git a/SessionUtilitiesKit/Networking/ProxiedContentDownloader.swift b/SessionUtilitiesKit/Networking/ProxiedContentDownloader.swift index 307efcdc5..326760296 100644 --- a/SessionUtilitiesKit/Networking/ProxiedContentDownloader.swift +++ b/SessionUtilitiesKit/Networking/ProxiedContentDownloader.swift @@ -594,7 +594,7 @@ open class ProxiedContentDownloader: NSObject, URLSessionTaskDelegate, URLSessio removeAssetRequestFromQueue(assetRequest: assetRequest) return } - guard CurrentAppContext().isMainAppAndActive else { + guard CurrentAppContext().isMainAppAndActive || CurrentAppContext().isShareExtension else { // If app is not active, fail the asset request. assetRequest.state = .failed assetRequestDidFail(assetRequest: assetRequest) diff --git a/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentApprovalViewController.swift b/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentApprovalViewController.swift index bb72c1cc4..04ce2726a 100644 --- a/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentApprovalViewController.swift +++ b/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentApprovalViewController.swift @@ -241,6 +241,12 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC UIView.performWithoutAnimation { self.currentPageViewController?.view.layoutIfNeeded() } + + // If the first item is just text, or is a URL and LinkPreviews are disabled + // then just fill the 'message' box with it + if firstItem.attachment.isText || (firstItem.attachment.isUrl && OWSLinkPreview.previewURL(forRawBodyText: firstItem.attachment.text()) == nil) { + bottomToolView.attachmentTextToolbar.messageText = firstItem.attachment.text() + } setupLayout() } diff --git a/SignalUtilitiesKit/Media Viewing & Editing/MediaMessageView.swift b/SignalUtilitiesKit/Media Viewing & Editing/MediaMessageView.swift index 75e41f937..9c294582c 100644 --- a/SignalUtilitiesKit/Media Viewing & Editing/MediaMessageView.swift +++ b/SignalUtilitiesKit/Media Viewing & Editing/MediaMessageView.swift @@ -5,6 +5,7 @@ import Foundation import MediaPlayer import YYImage +import NVActivityIndicatorView import SessionUIKit @@ -52,6 +53,8 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate { @objc public var contentView: UIView? + + private var linkPreviewInfo: (url: String, draft: OWSLinkPreviewDraft?)? // MARK: Initializers @@ -89,6 +92,10 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate { createVideoPreview() } else if attachment.isAudio { createAudioPreview() + } else if attachment.isUrl { + createUrlPreview() + } else if attachment.isText { + // Do nothing as we will just put the text in the 'message' input } else { createGenericPreview() } @@ -118,6 +125,31 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate { return stackView } + + private func wrapViewsInHorizontalStack(subviews: [UIView]) -> UIView { + assert(subviews.count > 0) + + let stackView = UIView() + + var lastView: UIView? + for subview in subviews { + + stackView.addSubview(subview) + subview.autoVCenterInSuperview() + + if lastView == nil { + subview.autoPinEdge(toSuperviewEdge: .left) + } else { + subview.autoPinEdge(.left, to: .right, of: lastView!, withOffset: stackSpacing()) + } + + lastView = subview + } + + lastView?.autoPinEdge(toSuperviewEdge: .right) + + return stackView + } private func stackSpacing() -> CGFloat { switch mode { @@ -265,6 +297,120 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate { videoPlayButton.autoSetDimension(.height, toSize: 72) } } + + private func createUrlPreview() { + // If link previews aren't enabled then use a fallback state + guard let linkPreviewURL: String = OWSLinkPreview.previewURL(forRawBodyText: attachment.text()) else { + createGenericPreview() + return + } + + linkPreviewInfo = (url: linkPreviewURL, draft: nil) + + var subviews = [UIView]() + + let color: UIColor = isLightMode ? .black : .white + let loadingView = NVActivityIndicatorView(frame: CGRect.zero, type: .circleStrokeSpin, color: color, padding: nil) + loadingView.set(.width, to: 24) + loadingView.set(.height, to: 24) + loadingView.startAnimating() + subviews.append(loadingView) + + let imageViewContainer = UIView() + imageViewContainer.clipsToBounds = true + imageViewContainer.contentMode = .center + imageViewContainer.alpha = 0 + imageViewContainer.layer.cornerRadius = 8 + subviews.append(imageViewContainer) + + let imageView = createHeroImageView(imageName: "FileLarge") + imageViewContainer.addSubview(imageView) + imageView.pin(to: imageViewContainer) + + let titleLabel = UILabel() + titleLabel.text = linkPreviewURL + titleLabel.textColor = controlTintColor + titleLabel.font = labelFont() + titleLabel.textAlignment = .center + titleLabel.lineBreakMode = .byTruncatingMiddle + subviews.append(titleLabel) + + let stackView = wrapViewsInVerticalStack(subviews: subviews) + self.addSubview(stackView) + + titleLabel.autoPinWidthToSuperview(withMargin: 32) + + NSLayoutConstraint.activate([ + imageView.widthAnchor.constraint(equalToConstant: 80), + imageView.heightAnchor.constraint(equalToConstant: 80) + ]) + + // Build the link preview + OWSLinkPreview.tryToBuildPreviewInfo(previewUrl: linkPreviewURL).done { [weak self] draft in + // Loader + loadingView.alpha = 0 + loadingView.stopAnimating() + + self?.linkPreviewInfo = (url: linkPreviewURL, draft: draft) + + // TODO: Look at refactoring this behaviour to consolidate attachment mutations + self?.attachment.linkPreviewDraft = draft + + let image: UIImage? + + if let jpegImageData: Data = draft.jpegImageData, let loadedImage: UIImage = UIImage(data: jpegImageData) { + image = loadedImage + imageView.contentMode = .scaleAspectFill + } + else { + image = UIImage(named: "Link")?.withTint(isLightMode ? .black : .white) + imageView.contentMode = .center + } + + // Image view + (imageView as? UIImageView)?.image = image + imageViewContainer.alpha = 1 + imageViewContainer.backgroundColor = isDarkMode ? .black : UIColor.black.withAlphaComponent(0.06) + + // Title + if let title = draft.title { + titleLabel.font = .boldSystemFont(ofSize: Values.smallFontSize) + titleLabel.text = title + titleLabel.textAlignment = .left + titleLabel.numberOfLines = 2 + } + + guard let hStackView = self?.wrapViewsInHorizontalStack(subviews: subviews) else { + // TODO: Fallback + return + } + stackView.removeFromSuperview() + self?.addSubview(hStackView) + + // We want to center the stackView in it's superview while also ensuring + // it's superview is big enough to contain it. + hStackView.autoPinWidthToSuperview(withMargin: 32) + hStackView.autoVCenterInSuperview() + NSLayoutConstraint.autoSetPriority(UILayoutPriority.defaultLow) { + hStackView.autoPinHeightToSuperview() + } + hStackView.autoPinEdge(toSuperviewEdge: .top, withInset: 0, relation: .greaterThanOrEqual) + hStackView.autoPinEdge(toSuperviewEdge: .bottom, withInset: 0, relation: .greaterThanOrEqual) + }.catch { _ in + // TODO: Fallback + loadingView.stopAnimating() + }.retainUntilComplete() + + // We want to center the stackView in it's superview while also ensuring + // it's superview is big enough to contain it. + stackView.autoPinWidthToSuperview() + stackView.autoVCenterInSuperview() + NSLayoutConstraint.autoSetPriority(UILayoutPriority.defaultLow) { + stackView.autoPinHeightToSuperview() + } + stackView.autoPinEdge(toSuperviewEdge: .top, withInset: 0, relation: .greaterThanOrEqual) + stackView.autoPinEdge(toSuperviewEdge: .bottom, withInset: 0, relation: .greaterThanOrEqual) + } private func createGenericPreview() { var subviews = [UIView]() From 61f809caeeb230450d736b8af24f557e374b14a0 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Wed, 12 Jan 2022 16:57:04 +1100 Subject: [PATCH 06/26] Fixed a couple of bugs and started UI refactoring Refactored the UI creation and layout code in the attachments UI. Started refactoring the UI in the MediaMessageView (converting the existing stuff and will then consolidate when done). Fixed a bug where playing a video attachment would result in the zoom continually getting reset. Fixed a bug where the attachment zoom scale would randomly change causing odd behaviours. --- Session.xcodeproj/project.pbxproj | 4 + .../AttachmentPrepViewController.swift | 436 ++++++++-------- .../AttachmentTextToolbar.swift | 23 +- .../MediaMessageView.swift | 472 ++++++++++++------ .../Utilities/UIColor+Extensions.swift | 43 ++ 5 files changed, 617 insertions(+), 361 deletions(-) create mode 100644 SignalUtilitiesKit/Utilities/UIColor+Extensions.swift diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index a224cb1f3..221a6a919 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -778,6 +778,7 @@ FD705A90278CEBBC00F16121 /* ShareAppExtensionContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD705A8F278CEBBC00F16121 /* ShareAppExtensionContext.swift */; }; FD705A92278D051200F16121 /* ReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD705A91278D051200F16121 /* ReusableView.swift */; }; FD705A94278D052B00F16121 /* UITableView+ReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD705A93278D052B00F16121 /* UITableView+ReusableView.swift */; }; + FD705A98278E9F4D00F16121 /* UIColor+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD705A97278E9F4D00F16121 /* UIColor+Extensions.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -1784,6 +1785,7 @@ FD705A8F278CEBBC00F16121 /* ShareAppExtensionContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareAppExtensionContext.swift; sourceTree = ""; }; FD705A91278D051200F16121 /* ReusableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReusableView.swift; sourceTree = ""; }; FD705A93278D052B00F16121 /* UITableView+ReusableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UITableView+ReusableView.swift"; sourceTree = ""; }; + FD705A97278E9F4D00F16121 /* UIColor+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIColor+Extensions.swift"; sourceTree = ""; }; FF9BA33D021B115B1F5B4E46 /* Pods-SessionMessagingKit.app store release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SessionMessagingKit.app store release.xcconfig"; path = "Pods/Target Support Files/Pods-SessionMessagingKit/Pods-SessionMessagingKit.app store release.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ @@ -3321,6 +3323,7 @@ C38EF2B1255B6D9C007E1867 /* UIViewController+Utilities.swift */, C38EF307255B6DBE007E1867 /* UIGestureRecognizer+OWS.swift */, C38EF2F3255B6DBC007E1867 /* UIImage+OWS.swift */, + FD705A97278E9F4D00F16121 /* UIColor+Extensions.swift */, C38EF30A255B6DBE007E1867 /* UIUtil.h */, C38EF300255B6DBD007E1867 /* UIUtil.m */, C38EF239255B6D66007E1867 /* UIFont+OWS.h */, @@ -4445,6 +4448,7 @@ C38EF248255B6D67007E1867 /* UIViewController+OWS.m in Sources */, C38EF272255B6D7A007E1867 /* OWSResaveCollectionDBMigration.m in Sources */, C33FDD3A255A582000E217F9 /* Notification+Loki.swift in Sources */, + FD705A98278E9F4D00F16121 /* UIColor+Extensions.swift in Sources */, C38EF276255B6D7A007E1867 /* OWSDatabaseMigration.m in Sources */, C38EF370255B6DCC007E1867 /* OWSNavigationController.m in Sources */, C38EF24E255B6D67007E1867 /* Collection+OWS.swift in Sources */, diff --git a/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentPrepViewController.swift b/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentPrepViewController.swift index dea19759d..d249a63ef 100644 --- a/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentPrepViewController.swift +++ b/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentPrepViewController.swift @@ -7,7 +7,7 @@ import UIKit import AVFoundation import SessionUIKit -protocol AttachmentPrepViewControllerDelegate: class { +protocol AttachmentPrepViewControllerDelegate: AnyObject { func prepViewControllerUpdateNavigationBar() func prepViewControllerUpdateControls() @@ -31,13 +31,102 @@ public class AttachmentPrepViewController: OWSViewController, PlayerProgressBarD return attachmentItem.attachment } - private var videoPlayer: OWSVideoPlayer? + private lazy var videoPlayer: OWSVideoPlayer? = { + guard let videoURL = attachment.dataUrl else { + owsFailDebug("Missing videoURL") + return nil + } - private(set) var mediaMessageView: MediaMessageView! - private(set) var scrollView: UIScrollView! - private(set) var contentContainer: UIView! - private(set) var playVideoButton: UIView? - private var imageEditorView: ImageEditorView? + let player: OWSVideoPlayer = OWSVideoPlayer(url: videoURL) + player.delegate = self + + return player + }() + + // MARK: - UI + + private lazy var scrollView: UIScrollView = { + // Scroll View - used to zoom/pan on images and video + let scrollView: UIScrollView = UIScrollView() + scrollView.translatesAutoresizingMaskIntoConstraints = false + scrollView.delegate = self + scrollView.showsHorizontalScrollIndicator = false + scrollView.showsVerticalScrollIndicator = false + + // Panning should stop pretty soon after the user stops scrolling + scrollView.decelerationRate = UIScrollView.DecelerationRate.fast + + // If the content isn't zoomable then inset the content so it appears centered + if !isZoomable { + scrollView.isScrollEnabled = false + scrollView.contentInset = UIEdgeInsets( + top: 0, + leading: 0, + bottom: (AttachmentTextToolbar.kMinTextViewHeight + (AttachmentTextToolbar.kToolbarMargin * 2)), + trailing: 0 + ) + } + + return scrollView + }() + + private lazy var contentContainerView: UIView = { + // Anything that should be shrunk when user pops keyboard lives in the contentContainer. + let view: UIView = UIView() + view.translatesAutoresizingMaskIntoConstraints = false + + return view + }() + + private lazy var mediaMessageView: MediaMessageView = { + let view: MediaMessageView = MediaMessageView(attachment: attachment, mode: .attachmentApproval) + view.translatesAutoresizingMaskIntoConstraints = false + view.isHidden = (imageEditorView != nil) + + return view + }() + + private lazy var imageEditorView: ImageEditorView? = { + guard let imageEditorModel = attachmentItem.imageEditorModel else { return nil } + + let view: ImageEditorView = ImageEditorView(model: imageEditorModel, delegate: self) + view.translatesAutoresizingMaskIntoConstraints = false + + guard view.configureSubviews() else { return nil } + + return view + }() + + private lazy var videoPlayerView: VideoPlayerView? = { + guard let videoPlayer: OWSVideoPlayer = videoPlayer else { return nil } + + let view: VideoPlayerView = VideoPlayerView() + view.translatesAutoresizingMaskIntoConstraints = false + view.player = videoPlayer.avPlayer + + let pauseGesture = UITapGestureRecognizer(target: self, action: #selector(didTapPlayerView(_:))) + view.addGestureRecognizer(pauseGesture) + + return view + }() + + private lazy var progressBar: PlayerProgressBar = { + let progressBar: PlayerProgressBar = PlayerProgressBar() + progressBar.player = videoPlayer?.avPlayer + progressBar.delegate = self + + return progressBar + }() + + private lazy var playVideoButton: UIButton = { + let button: UIButton = UIButton() + button.translatesAutoresizingMaskIntoConstraints = false + button.contentMode = .scaleAspectFit + button.setBackgroundImage(#imageLiteral(resourceName: "CirclePlay"), for: .normal) + button.addTarget(self, action: #selector(playButtonTapped), for: .touchUpInside) + + return button + }() public var shouldHideControls: Bool { guard let imageEditorView = imageEditorView else { @@ -61,143 +150,120 @@ public class AttachmentPrepViewController: OWSViewController, PlayerProgressBarD } // MARK: - View Lifecycle + + public override func viewDidLoad() { + super.viewDidLoad() + + view.backgroundColor = Colors.navigationBarBackground - override public func loadView() { - self.view = UIView() - - self.mediaMessageView = MediaMessageView(attachment: attachment, mode: .attachmentApproval) - - // Anything that should be shrunk when user pops keyboard lives in the contentContainer. - let contentContainer = UIView() - self.contentContainer = contentContainer - view.addSubview(contentContainer) - contentContainer.autoPinEdgesToSuperviewEdges() - - // Scroll View - used to zoom/pan on images and video - scrollView = UIScrollView() - contentContainer.addSubview(scrollView) - scrollView.delegate = self - scrollView.showsHorizontalScrollIndicator = false - scrollView.showsVerticalScrollIndicator = false - - // Panning should stop pretty soon after the user stops scrolling - scrollView.decelerationRate = UIScrollView.DecelerationRate.fast - - // We want scroll view content up and behind the system status bar content - // but we want other content (e.g. bar buttons) to respect the top layout guide. - self.automaticallyAdjustsScrollViewInsets = false - - scrollView.autoPinEdgesToSuperviewEdges() - - let backgroundColor = Colors.navigationBarBackground - self.view.backgroundColor = backgroundColor - - // Create full screen container view so the scrollView - // can compute an appropriate content size in which to center - // our media view. - let containerView = UIView.container() - scrollView.addSubview(containerView) - containerView.autoPinEdgesToSuperviewEdges() - containerView.autoMatch(.height, to: .height, of: self.view) - containerView.autoMatch(.width, to: .width, of: self.view) - - containerView.addSubview(mediaMessageView) - mediaMessageView.autoPinEdgesToSuperviewEdges() - - if let imageEditorModel = attachmentItem.imageEditorModel { - - let imageEditorView = ImageEditorView(model: imageEditorModel, delegate: self) - if imageEditorView.configureSubviews() { - self.imageEditorView = imageEditorView - - mediaMessageView.isHidden = true - - view.addSubview(imageEditorView) - imageEditorView.autoPinEdgesToSuperviewEdges() - - imageEditorUpdateNavigationBar() - } + view.addSubview(contentContainerView) + + contentContainerView.addSubview(scrollView) + scrollView.addSubview(mediaMessageView) + + if let editorView: ImageEditorView = imageEditorView { + view.addSubview(editorView) + + imageEditorUpdateNavigationBar() } // Hide the play button embedded in the MediaView and replace it with our own. // This allows us to zoom in on the media view without zooming in on the button - if attachment.isVideo { - - guard let videoURL = attachment.dataUrl else { - owsFailDebug("Missing videoURL") - return - } - - let player = OWSVideoPlayer(url: videoURL) - self.videoPlayer = player - player.delegate = self - - let playerView = VideoPlayerView() - playerView.player = player.avPlayer - self.mediaMessageView.addSubview(playerView) - playerView.autoPinEdgesToSuperviewEdges() - - let pauseGesture = UITapGestureRecognizer(target: self, action: #selector(didTapPlayerView(_:))) - playerView.addGestureRecognizer(pauseGesture) - - let progressBar = PlayerProgressBar() - progressBar.player = player.avPlayer - progressBar.delegate = self - + // TODO: This for both Audio and Video? + if attachment.isVideo, let playerView: VideoPlayerView = videoPlayerView { + mediaMessageView.videoPlayButton.isHidden = true + mediaMessageView.addSubview(playerView) + // we don't want the progress bar to zoom during "pinch-to-zoom" // but we do want it to shrink with the media content when the user // pops the keyboard. - contentContainer.addSubview(progressBar) - - progressBar.autoPinEdge(.top, to: .top, of: view) - progressBar.autoPinWidthToSuperview() - progressBar.autoSetDimension(.height, toSize: 44) - - self.mediaMessageView.videoPlayButton?.isHidden = true - let playButton = UIButton() - self.playVideoButton = playButton - playButton.accessibilityLabel = NSLocalizedString("PLAY_BUTTON_ACCESSABILITY_LABEL", comment: "Accessibility label for button to start media playback") - playButton.setBackgroundImage(#imageLiteral(resourceName: "CirclePlay"), for: .normal) - playButton.contentMode = .scaleAspectFit - playButton.autoSetDimension(.width, toSize: 72) - playButton.autoSetDimension(.height, toSize: 72) - - let playButtonWidth = ScaleFromIPhone5(70) - playButton.autoSetDimensions(to: CGSize(width: playButtonWidth, height: playButtonWidth)) - self.contentContainer.addSubview(playButton) - - playButton.addTarget(self, action: #selector(playButtonTapped), for: .touchUpInside) - playButton.autoCenterInSuperview() + contentContainerView.addSubview(progressBar) + contentContainerView.addSubview(playVideoButton) } + + setupLayout() } override public func viewWillAppear(_ animated: Bool) { - Logger.debug("") - super.viewWillAppear(animated) - + prepDelegate?.prepViewControllerUpdateNavigationBar() prepDelegate?.prepViewControllerUpdateControls() } override public func viewDidAppear(_ animated: Bool) { - Logger.debug("") - super.viewDidAppear(animated) prepDelegate?.prepViewControllerUpdateNavigationBar() prepDelegate?.prepViewControllerUpdateControls() } - + override public func viewWillLayoutSubviews() { - Logger.debug("") super.viewWillLayoutSubviews() - - // e.g. if flipping to/from landscape - updateMinZoomScaleForSize(view.bounds.size) - + + setupZoomScale() ensureAttachmentViewScale(animated: false) } + + public override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + + // Note: Need to do this here to ensure it's based on the final sizing + // otherwise the offsets will be slightly off + resetContentInset() + } + + // MARK: - Layout + + private func setupLayout() { + NSLayoutConstraint.activate([ + contentContainerView.topAnchor.constraint(equalTo: view.topAnchor), + contentContainerView.leftAnchor.constraint(equalTo: view.leftAnchor), + contentContainerView.rightAnchor.constraint(equalTo: view.rightAnchor), + contentContainerView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + + scrollView.topAnchor.constraint(equalTo: contentContainerView.topAnchor), + scrollView.leftAnchor.constraint(equalTo: contentContainerView.leftAnchor), + scrollView.rightAnchor.constraint(equalTo: contentContainerView.rightAnchor), + scrollView.bottomAnchor.constraint(equalTo: contentContainerView.bottomAnchor), + + mediaMessageView.topAnchor.constraint(equalTo: scrollView.topAnchor), + mediaMessageView.leftAnchor.constraint(equalTo: scrollView.leftAnchor), + mediaMessageView.rightAnchor.constraint(equalTo: scrollView.rightAnchor), + mediaMessageView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor), + mediaMessageView.widthAnchor.constraint(equalTo: view.widthAnchor), + mediaMessageView.heightAnchor.constraint(equalTo: view.heightAnchor) + ]) + + if let editorView: ImageEditorView = imageEditorView { + NSLayoutConstraint.activate([ + editorView.topAnchor.constraint(equalTo: view.topAnchor), + editorView.leftAnchor.constraint(equalTo: view.leftAnchor), + editorView.rightAnchor.constraint(equalTo: view.rightAnchor), + editorView.bottomAnchor.constraint(equalTo: view.bottomAnchor) + ]) + } + + if attachment.isVideo, let playerView: VideoPlayerView = videoPlayerView { + let playButtonSize: CGFloat = ScaleFromIPhone5(70) + + NSLayoutConstraint.activate([ + playerView.topAnchor.constraint(equalTo: mediaMessageView.topAnchor), + playerView.leftAnchor.constraint(equalTo: mediaMessageView.leftAnchor), + playerView.rightAnchor.constraint(equalTo: mediaMessageView.rightAnchor), + playerView.bottomAnchor.constraint(equalTo: mediaMessageView.bottomAnchor), + + progressBar.topAnchor.constraint(equalTo: view.topAnchor), + progressBar.widthAnchor.constraint(equalTo: contentContainerView.widthAnchor), + progressBar.heightAnchor.constraint(equalToConstant: 44), + + playVideoButton.centerXAnchor.constraint(equalTo: contentContainerView.centerXAnchor), + playVideoButton.centerYAnchor.constraint(equalTo: contentContainerView.centerYAnchor), + playVideoButton.widthAnchor.constraint(equalToConstant: playButtonSize), + playVideoButton.heightAnchor.constraint(equalToConstant: playButtonSize), + ]) + } + } // MARK: - Navigation Bar @@ -205,39 +271,33 @@ public class AttachmentPrepViewController: OWSViewController, PlayerProgressBarD guard let imageEditorView = imageEditorView else { return [] } + return imageEditorView.navigationBarItems() } // MARK: - Event Handlers - @objc - public func didTapPlayerView(_ gestureRecognizer: UIGestureRecognizer) { + @objc public func didTapPlayerView(_ gestureRecognizer: UIGestureRecognizer) { assert(self.videoPlayer != nil) self.pauseVideo() } - @objc - public func playButtonTapped() { + @objc public func playButtonTapped() { self.playVideo() } // MARK: - Video private func playVideo() { - Logger.info("") - guard let videoPlayer = self.videoPlayer else { owsFailDebug("video player was unexpectedly nil") return } - guard let playVideoButton = self.playVideoButton else { - owsFailDebug("playVideoButton was unexpectedly nil") - return - } - UIView.animate(withDuration: 0.1) { - playVideoButton.alpha = 0.0 + UIView.animate(withDuration: 0.1) { [weak self] in + self?.playVideoButton.alpha = 0.0 } + videoPlayer.play() } @@ -248,24 +308,15 @@ public class AttachmentPrepViewController: OWSViewController, PlayerProgressBarD } videoPlayer.pause() - guard let playVideoButton = self.playVideoButton else { - owsFailDebug("playVideoButton was unexpectedly nil") - return - } - UIView.animate(withDuration: 0.1) { - playVideoButton.alpha = 1.0 + + UIView.animate(withDuration: 0.1) { [weak self] in + self?.playVideoButton.alpha = 1.0 } } - @objc - public func videoPlayerDidPlayToCompletion(_ videoPlayer: OWSVideoPlayer) { - guard let playVideoButton = self.playVideoButton else { - owsFailDebug("playVideoButton was unexpectedly nil") - return - } - - UIView.animate(withDuration: 0.1) { - playVideoButton.alpha = 1.0 + @objc public func videoPlayerDidPlayToCompletion(_ videoPlayer: OWSVideoPlayer) { + UIView.animate(withDuration: 0.1) { [weak self] in + self?.playVideoButton.alpha = 1.0 } } @@ -274,6 +325,7 @@ public class AttachmentPrepViewController: OWSViewController, PlayerProgressBarD owsFailDebug("video player was unexpectedly nil") return } + videoPlayer.pause() } @@ -315,6 +367,7 @@ public class AttachmentPrepViewController: OWSViewController, PlayerProgressBarD var shouldAllowAttachmentViewResizing: Bool = true var attachmentViewScale: AttachmentViewScale = .fullsize + public func setAttachmentViewScale(_ attachmentViewScale: AttachmentViewScale, animated: Bool) { self.attachmentViewScale = attachmentViewScale ensureAttachmentViewScale(animated: animated) @@ -323,9 +376,9 @@ public class AttachmentPrepViewController: OWSViewController, PlayerProgressBarD func ensureAttachmentViewScale(animated: Bool) { let animationDuration = animated ? 0.2 : 0 guard shouldAllowAttachmentViewResizing else { - if self.contentContainer.transform != CGAffineTransform.identity { + if self.contentContainerView.transform != CGAffineTransform.identity { UIView.animate(withDuration: animationDuration) { - self.contentContainer.transform = CGAffineTransform.identity + self.contentContainerView.transform = CGAffineTransform.identity } } return @@ -333,14 +386,14 @@ public class AttachmentPrepViewController: OWSViewController, PlayerProgressBarD switch attachmentViewScale { case .fullsize: - guard self.contentContainer.transform != .identity else { + guard self.contentContainerView.transform != .identity else { return } UIView.animate(withDuration: animationDuration) { - self.contentContainer.transform = CGAffineTransform.identity + self.contentContainerView.transform = CGAffineTransform.identity } case .compact: - guard self.contentContainer.transform == .identity else { + guard self.contentContainerView.transform == .identity else { return } UIView.animate(withDuration: animationDuration) { @@ -354,7 +407,7 @@ public class AttachmentPrepViewController: OWSViewController, PlayerProgressBarD let heightDelta = originalHeight * (1 - kScaleFactor) let translate = CGAffineTransform(translationX: 0, y: -heightDelta / 2) - self.contentContainer.transform = scale.concatenating(translate) + self.contentContainerView.transform = scale.concatenating(translate) } } } @@ -367,66 +420,55 @@ extension AttachmentPrepViewController: UIScrollViewDelegate { public func viewForZooming(in scrollView: UIScrollView) -> UIView? { if isZoomable { return mediaMessageView - } else { - // don't zoom for audio or generic attachments. - return nil } + + // Don't zoom for audio or generic attachments. + return nil + } + + public func scrollViewDidZoom(_ scrollView: UIScrollView) { + resetContentInset() } - fileprivate func updateMinZoomScaleForSize(_ size: CGSize) { - Logger.debug("") - + fileprivate func setupZoomScale() { + // We only want to setup the zoom scale once (otherwise we get glitchy behaviour + // when anything forces a re-layout) + guard abs(scrollView.maximumZoomScale - 1.0) <= CGFloat.leastNormalMagnitude else { + return + } + // Ensure bounds have been computed - mediaMessageView.layoutIfNeeded() guard mediaMessageView.bounds.width > 0, mediaMessageView.bounds.height > 0 else { Logger.warn("bad bounds") return } - let widthScale = size.width / mediaMessageView.bounds.width - let heightScale = size.height / mediaMessageView.bounds.height - let minScale = min(widthScale, heightScale) - scrollView.maximumZoomScale = minScale * 5.0 + let widthScale: CGFloat = (view.bounds.size.width / mediaMessageView.bounds.width) + let heightScale: CGFloat = (view.bounds.size.height / mediaMessageView.bounds.height) + let minScale: CGFloat = min(widthScale, heightScale) + scrollView.minimumZoomScale = minScale + scrollView.maximumZoomScale = (minScale * 5) scrollView.zoomScale = minScale } - - // Keep the media view centered within the scroll view as you zoom - public func scrollViewDidZoom(_ scrollView: UIScrollView) { - // The scroll view has zoomed, so you need to re-center the contents - let scrollViewSize = self.scrollViewVisibleSize - - // First assume that mediaMessageView center coincides with the contents center - // This is correct when the mediaMessageView is bigger than scrollView due to zoom - var contentCenter = CGPoint(x: (scrollView.contentSize.width / 2), y: (scrollView.contentSize.height / 2)) - - let scrollViewCenter = self.scrollViewCenter - - // if mediaMessageView is smaller than the scrollView visible size - fix the content center accordingly - if self.scrollView.contentSize.width < scrollViewSize.width { - contentCenter.x = scrollViewCenter.x + + // Allow the user to zoom out to 100% of the attachment size if it's smaller + // than the screen + fileprivate func resetContentInset() { + guard isZoomable else { + scrollView.contentOffset = CGPoint(x: 0, y: scrollView.contentInset.bottom) + return } - - if self.scrollView.contentSize.height < scrollViewSize.height { - contentCenter.y = scrollViewCenter.y - } - - self.mediaMessageView.center = contentCenter - } - - // return the scroll view center - private var scrollViewCenter: CGPoint { - let size = scrollViewVisibleSize - return CGPoint(x: (size.width / 2), y: (size.height / 2)) - } - - // Return scrollview size without the area overlapping with tab and nav bar. - private var scrollViewVisibleSize: CGSize { - let contentInset = scrollView.contentInset - let scrollViewSize = scrollView.bounds.standardized.size - let width = scrollViewSize.width - (contentInset.left + contentInset.right) - let height = scrollViewSize.height - (contentInset.top + contentInset.bottom) - return CGSize(width: width, height: height) + + let offsetX: CGFloat = max((scrollView.bounds.width - scrollView.contentSize.width) * 0.5, 0) + let offsetY: CGFloat = max((scrollView.bounds.height - scrollView.contentSize.height) * 0.5, 0) + + scrollView.contentInset = UIEdgeInsets( + top: offsetY, + left: offsetX, + bottom: 0, + right: 0 + ) } } diff --git a/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentTextToolbar.swift b/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentTextToolbar.swift index 8cdc30eb2..2907d0c64 100644 --- a/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentTextToolbar.swift +++ b/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentTextToolbar.swift @@ -32,8 +32,9 @@ class AttachmentTextToolbar: UIView, UITextViewDelegate { } // Layout Constants - - let kMinTextViewHeight: CGFloat = 40 + + static let kToolbarMargin: CGFloat = 8 + static let kMinTextViewHeight: CGFloat = 40 var maxTextViewHeight: CGFloat { // About ~4 lines in portrait and ~3 lines in landscape. // Otherwise we risk obscuring too much of the content. @@ -46,7 +47,7 @@ class AttachmentTextToolbar: UIView, UITextViewDelegate { init() { self.sendButton = UIButton(type: .system) - self.textViewHeight = kMinTextViewHeight + self.textViewHeight = AttachmentTextToolbar.kMinTextViewHeight super.init(frame: CGRect.zero) @@ -77,15 +78,19 @@ class AttachmentTextToolbar: UIView, UITextViewDelegate { contentView.autoPinEdgesToSuperviewEdges() // Layout - let kToolbarMargin: CGFloat = 8 // We have to wrap the toolbar items in a content view because iOS (at least on iOS10.3) assigns the inputAccessoryView.layoutMargins // when resigning first responder (verified by auditing with `layoutMarginsDidChange`). // The effect of this is that if we were to assign these margins to self.layoutMargins, they'd be blown away if the // user dismisses the keyboard, giving the input accessory view a wonky layout. - contentView.layoutMargins = UIEdgeInsets(top: kToolbarMargin, left: kToolbarMargin, bottom: kToolbarMargin, right: kToolbarMargin) + contentView.layoutMargins = UIEdgeInsets( + top: AttachmentTextToolbar.kToolbarMargin, + left: AttachmentTextToolbar.kToolbarMargin, + bottom: AttachmentTextToolbar.kToolbarMargin, + right: AttachmentTextToolbar.kToolbarMargin + ) - self.textViewHeightConstraint = textView.autoSetDimension(.height, toSize: kMinTextViewHeight) + self.textViewHeightConstraint = textView.autoSetDimension(.height, toSize: AttachmentTextToolbar.kMinTextViewHeight) // We pin all three edges explicitly rather than doing something like: // textView.autoPinEdges(toSuperviewMarginsExcludingEdge: .right) @@ -97,7 +102,7 @@ class AttachmentTextToolbar: UIView, UITextViewDelegate { textContainer.autoPinEdge(toSuperviewMargin: .bottom) textContainer.autoPinEdge(toSuperviewMargin: .left) - sendButton.autoPinEdge(.left, to: .right, of: textContainer, withOffset: kToolbarMargin) + sendButton.autoPinEdge(.left, to: .right, of: textContainer, withOffset: AttachmentTextToolbar.kToolbarMargin) sendButton.autoPinEdge(.bottom, to: .bottom, of: textContainer, withOffset: -3) sendButton.autoPinEdge(toSuperviewMargin: .right) @@ -170,7 +175,7 @@ class AttachmentTextToolbar: UIView, UITextViewDelegate { textContainer.layer.borderColor = UIColor.white.cgColor textContainer.layer.borderWidth = Values.separatorThickness - textContainer.layer.cornerRadius = kMinTextViewHeight / 2 + textContainer.layer.cornerRadius = (AttachmentTextToolbar.kMinTextViewHeight / 2) textContainer.clipsToBounds = true textContainer.addSubview(placeholderTextView) @@ -314,6 +319,6 @@ class AttachmentTextToolbar: UIView, UITextViewDelegate { private func clampedTextViewHeight(fixedWidth: CGFloat) -> CGFloat { let contentSize = textView.sizeThatFits(CGSize(width: fixedWidth, height: CGFloat.greatestFiniteMagnitude)) - return CGFloatClamp(contentSize.height, kMinTextViewHeight, maxTextViewHeight) + return CGFloatClamp(contentSize.height, AttachmentTextToolbar.kMinTextViewHeight, maxTextViewHeight) } } diff --git a/SignalUtilitiesKit/Media Viewing & Editing/MediaMessageView.swift b/SignalUtilitiesKit/Media Viewing & Editing/MediaMessageView.swift index 9c294582c..28b5cbac4 100644 --- a/SignalUtilitiesKit/Media Viewing & Editing/MediaMessageView.swift +++ b/SignalUtilitiesKit/Media Viewing & Editing/MediaMessageView.swift @@ -6,37 +6,25 @@ import Foundation import MediaPlayer import YYImage import NVActivityIndicatorView - import SessionUIKit -@objc -public enum MediaMessageViewMode: UInt { - case large - case small - case attachmentApproval -} - -@objc public class MediaMessageView: UIView, OWSAudioPlayerDelegate { + public enum Mode: UInt { + case large + case small + case attachmentApproval + } // MARK: Properties - @objc - public let mode: MediaMessageViewMode - - @objc + public let mode: Mode public let attachment: SignalAttachment - @objc public var audioPlayer: OWSAudioPlayer? + + private var linkPreviewInfo: (url: String, draft: OWSLinkPreviewDraft?)? + - @objc - public var audioPlayButton: UIButton? - - @objc - public var videoPlayButton: UIImageView? - - @objc public var playbackState = AudioPlaybackState.stopped { didSet { AssertIsOnMainThread() @@ -45,16 +33,12 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate { } } - @objc public var audioProgressSeconds: CGFloat = 0 - - @objc public var audioDurationSeconds: CGFloat = 0 - @objc public var contentView: UIView? - private var linkPreviewInfo: (url: String, draft: OWSLinkPreviewDraft?)? + // MARK: Initializers @@ -65,23 +49,119 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate { // Currently we only use one mode (AttachmentApproval), so we could simplify this class, but it's kind // of nice that it's written in a flexible way in case we'd want to use it elsewhere again in the future. - @objc - public required init(attachment: SignalAttachment, mode: MediaMessageViewMode) { - if attachment.hasError { - owsFailDebug(attachment.error.debugDescription) - } + public required init(attachment: SignalAttachment, mode: MediaMessageView.Mode) { + if attachment.hasError { owsFailDebug(attachment.error.debugDescription) } + self.attachment = attachment self.mode = mode + super.init(frame: CGRect.zero) createViews() + + backgroundColor = .red + + setupLayout() } deinit { NotificationCenter.default.removeObserver(self) } - - // MARK: - Create Views + + // MARK: - UI + + private lazy var stackView: UIStackView = { + let stackView: UIStackView = UIStackView() + stackView.translatesAutoresizingMaskIntoConstraints = false + stackView.axis = .vertical + stackView.alignment = .center + stackView.distribution = .equalSpacing + + switch mode { + case .large, .attachmentApproval: stackView.spacing = 10 + case .small: stackView.spacing = 5 + } + + return stackView + }() + + private lazy var imageView: UIImageView = { + let view: UIImageView = UIImageView() + view.translatesAutoresizingMaskIntoConstraints = false + view.contentMode = .scaleAspectFit + view.layer.minificationFilter = .trilinear + view.layer.magnificationFilter = .trilinear + + return view + }() + + private lazy var fileTypeImageView: UIImageView = { + let view: UIImageView = UIImageView() + view.translatesAutoresizingMaskIntoConstraints = false + view.layer.minificationFilter = .trilinear + view.layer.magnificationFilter = .trilinear + + return view + }() + + private lazy var animatedImageView: YYAnimatedImageView = { + let view: YYAnimatedImageView = YYAnimatedImageView() + view.translatesAutoresizingMaskIntoConstraints = false + + return view + }() + + lazy var videoPlayButton: UIImageView = { + let imageView: UIImageView = UIImageView(image: UIImage(named: "CirclePlay")) + imageView.translatesAutoresizingMaskIntoConstraints = false + imageView.contentMode = .scaleAspectFit + + return imageView + }() + + /// Note: This uses different assets from the `videoPlayButton` and has a 'Pause' state + private lazy var audioPlayPauseButton: UIButton = { + let button: UIButton = UIButton() + button.translatesAutoresizingMaskIntoConstraints = false + button.clipsToBounds = true + button.setBackgroundImage(UIColor.white.toImage(), for: .normal) + button.setBackgroundImage(UIColor.white.darken(by: 0.2).toImage(), for: .highlighted) + button.layer.cornerRadius = 30 + + button.addTarget(self, action: #selector(audioPlayPauseButtonPressed), for: .touchUpInside) + + return button + }() + + private lazy var titleLabel: UILabel = { + let label: UILabel = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.font = labelFont() + label.text = (formattedFileName() ?? formattedFileExtension()) + label.textColor = controlTintColor + label.textAlignment = .center + label.lineBreakMode = .byTruncatingMiddle + label.isHidden = ((label.text?.count ?? 0) == 0) + + return label + }() + + private lazy var fileSizeLabel: UILabel = { + let fileSize: UInt = attachment.dataLength + + let label: UILabel = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.font = labelFont() + // Format string for file size label in call interstitial view. + // Embeds: {{file size as 'N mb' or 'N kb'}}. + label.text = String(format: "ATTACHMENT_APPROVAL_FILE_SIZE_FORMAT".localized(), OWSFormat.formatFileSize(UInt(fileSize))) + label.textColor = controlTintColor + label.textAlignment = .center + + return label + }() + + // MARK: - Layout private func createViews() { if attachment.isAnimatedImage { @@ -100,7 +180,12 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate { createGenericPreview() } } + + private func setupLayout() { + // Bottom inset + } + // TODO: Any reason for not just using UIStackView private func wrapViewsInVerticalStack(subviews: [UIView]) -> UIView { assert(subviews.count > 0) @@ -115,7 +200,7 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate { if lastView == nil { subview.autoPinEdge(toSuperviewEdge: .top) } else { - subview.autoPinEdge(.top, to: .bottom, of: lastView!, withOffset: stackSpacing()) + subview.autoPinEdge(.top, to: .bottom, of: lastView!, withOffset: 10) } lastView = subview @@ -140,7 +225,7 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate { if lastView == nil { subview.autoPinEdge(toSuperviewEdge: .left) } else { - subview.autoPinEdge(.left, to: .right, of: lastView!, withOffset: stackSpacing()) + subview.autoPinEdge(.left, to: .right, of: lastView!, withOffset: 10) } lastView = subview @@ -151,14 +236,14 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate { return stackView } - private func stackSpacing() -> CGFloat { - switch mode { - case .large, .attachmentApproval: - return CGFloat(10) - case .small: - return CGFloat(5) - } - } +// private func stackSpacing() -> CGFloat { +// switch mode { +// case .large, .attachmentApproval: +// return CGFloat(10) +// case .small: +// return CGFloat(5) +// } +// } private func createAudioPreview() { guard let dataUrl = attachment.dataUrl else { @@ -167,41 +252,53 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate { } audioPlayer = OWSAudioPlayer(mediaUrl: dataUrl, audioBehavior: .playback, delegate: self) - - var subviews = [UIView]() - - let audioPlayButton = UIButton() - self.audioPlayButton = audioPlayButton + + imageView.image = UIImage(named: "FileLarge") + fileTypeImageView.image = UIImage(named: "table_ic_notification_sound") setAudioIconToPlay() - audioPlayButton.imageView?.layer.minificationFilter = .trilinear - audioPlayButton.imageView?.layer.magnificationFilter = .trilinear - audioPlayButton.addTarget(self, action: #selector(audioPlayButtonPressed), for: .touchUpInside) - let buttonSize = createHeroViewSize() - audioPlayButton.autoSetDimension(.width, toSize: buttonSize) - audioPlayButton.autoSetDimension(.height, toSize: buttonSize) - subviews.append(audioPlayButton) - - let fileNameLabel = createFileNameLabel() - if let fileNameLabel = fileNameLabel { - subviews.append(fileNameLabel) - } - - let fileSizeLabel = createFileSizeLabel() - subviews.append(fileSizeLabel) - - let stackView = wrapViewsInVerticalStack(subviews: subviews) + self.addSubview(stackView) - fileNameLabel?.autoPinWidthToSuperview(withMargin: 32) + self.addSubview(audioPlayPauseButton) + + stackView.addArrangedSubview(imageView) + stackView.addArrangedSubview(UIView.vSpacer(0)) + stackView.addArrangedSubview(titleLabel) + stackView.addArrangedSubview(fileSizeLabel) + + imageView.addSubview(fileTypeImageView) - // We want to center the stackView in it's superview while also ensuring - // it's superview is big enough to contain it. - stackView.autoPinWidthToSuperview() - stackView.autoVCenterInSuperview() - NSLayoutConstraint.autoSetPriority(UILayoutPriority.defaultLow) { - stackView.autoPinHeightToSuperview() - } - stackView.autoPinEdge(toSuperviewEdge: .top, withInset: 0, relation: .greaterThanOrEqual) - stackView.autoPinEdge(toSuperviewEdge: .bottom, withInset: 0, relation: .greaterThanOrEqual) + NSLayoutConstraint.activate([ + stackView.centerYAnchor.constraint(equalTo: centerYAnchor), + stackView.widthAnchor.constraint(equalTo: widthAnchor), + stackView.heightAnchor.constraint(lessThanOrEqualTo: heightAnchor), + + imageView.widthAnchor.constraint(equalToConstant: 150), + imageView.heightAnchor.constraint(equalToConstant: 150), + titleLabel.widthAnchor.constraint(equalTo: stackView.widthAnchor, constant: -(32 * 2)), + fileSizeLabel.widthAnchor.constraint(equalTo: stackView.widthAnchor, constant: -(32 * 2)), + + fileTypeImageView.centerXAnchor.constraint(equalTo: imageView.centerXAnchor), + fileTypeImageView.centerYAnchor.constraint( + equalTo: imageView.centerYAnchor, + constant: 25 + ), + fileTypeImageView.widthAnchor.constraint( + equalTo: fileTypeImageView.heightAnchor, + multiplier: ((fileTypeImageView.image?.size.width ?? 1) / (fileTypeImageView.image?.size.height ?? 1)) + ), + fileTypeImageView.widthAnchor.constraint( + equalTo: imageView.widthAnchor, constant: -75 + ), + + audioPlayPauseButton.centerXAnchor.constraint(equalTo: imageView.centerXAnchor), + audioPlayPauseButton.centerYAnchor.constraint(equalTo: imageView.centerYAnchor), + audioPlayPauseButton.widthAnchor.constraint( + equalToConstant: (audioPlayPauseButton.layer.cornerRadius * 2) + ), + audioPlayPauseButton.heightAnchor.constraint( + equalToConstant: (audioPlayPauseButton.layer.cornerRadius * 2) + ) + ]) } private func createAnimatedPreview() { @@ -221,25 +318,38 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate { createGenericPreview() return } - let animatedImageView = YYAnimatedImageView() animatedImageView.image = image - let aspectRatio = image.size.width / image.size.height - addSubviewWithScaleAspectFitLayout(view: animatedImageView, aspectRatio: aspectRatio) + let aspectRatio: CGFloat = (image.size.width / image.size.height) + let clampedRatio: CGFloat = CGFloatClamp(aspectRatio, 0.05, 95.0) + + addSubview(animatedImageView) +// addSubviewWithScaleAspectFitLayout(view: animatedImageView, aspectRatio: aspectRatio) contentView = animatedImageView + + NSLayoutConstraint.activate([ + animatedImageView.centerXAnchor.constraint(equalTo: centerXAnchor), + animatedImageView.centerYAnchor.constraint(equalTo: centerYAnchor), + animatedImageView.widthAnchor.constraint( + equalTo: animatedImageView.heightAnchor, + multiplier: clampedRatio + ), + animatedImageView.widthAnchor.constraint(lessThanOrEqualTo: widthAnchor), + animatedImageView.heightAnchor.constraint(lessThanOrEqualTo: heightAnchor) + ]) } - private func addSubviewWithScaleAspectFitLayout(view: UIView, aspectRatio: CGFloat) { - self.addSubview(view) - // This emulates the behavior of contentMode = .scaleAspectFit using - // iOS auto layout constraints. - // - // This allows ConversationInputToolbar to place the "cancel" button - // in the upper-right hand corner of the preview content. - view.autoCenterInSuperview() - view.autoPin(toAspectRatio: aspectRatio) - view.autoMatch(.width, to: .width, of: self, withMultiplier: 1.0, relation: .lessThanOrEqual) - view.autoMatch(.height, to: .height, of: self, withMultiplier: 1.0, relation: .lessThanOrEqual) - } +// private func addSubviewWithScaleAspectFitLayout(view: UIView, aspectRatio: CGFloat) { +// self.addSubview(view) +// // This emulates the behavior of contentMode = .scaleAspectFit using +// // iOS auto layout constraints. +// // +// // This allows ConversationInputToolbar to place the "cancel" button +// // in the upper-right hand corner of the preview content. +// view.autoCenterInSuperview() +// view.autoPin(toAspectRatio: aspectRatio) +// view.autoMatch(.width, to: .width, of: self, withMultiplier: 1.0, relation: .lessThanOrEqual) +// view.autoMatch(.height, to: .height, of: self, withMultiplier: 1.0, relation: .lessThanOrEqual) +// } private func createImagePreview() { guard attachment.isValidImage else { @@ -255,12 +365,26 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate { return } - let imageView = UIImageView(image: image) - imageView.layer.minificationFilter = .trilinear - imageView.layer.magnificationFilter = .trilinear + imageView.image = image +// imageView.layer.minificationFilter = .trilinear +// imageView.layer.magnificationFilter = .trilinear + let aspectRatio = image.size.width / image.size.height - addSubviewWithScaleAspectFitLayout(view: imageView, aspectRatio: aspectRatio) + let clampedRatio: CGFloat = CGFloatClamp(aspectRatio, 0.05, 95.0) + +// addSubviewWithScaleAspectFitLayout(view: imageView, aspectRatio: aspectRatio) contentView = imageView + + NSLayoutConstraint.activate([ + imageView.centerXAnchor.constraint(equalTo: centerXAnchor), + imageView.centerYAnchor.constraint(equalTo: centerYAnchor), + imageView.widthAnchor.constraint( + equalTo: imageView.heightAnchor, + multiplier: clampedRatio + ), + imageView.widthAnchor.constraint(lessThanOrEqualTo: widthAnchor), + imageView.heightAnchor.constraint(lessThanOrEqualTo: heightAnchor) + ]) } private func createVideoPreview() { @@ -277,30 +401,58 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate { return } - let imageView = UIImageView(image: image) - imageView.layer.minificationFilter = .trilinear - imageView.layer.magnificationFilter = .trilinear +// let imageView = UIImageView(image: image) + imageView.image = image +// imageView.layer.minificationFilter = .trilinear +// imageView.layer.magnificationFilter = .trilinear + self.addSubview(imageView) + let aspectRatio = image.size.width / image.size.height - addSubviewWithScaleAspectFitLayout(view: imageView, aspectRatio: aspectRatio) + let clampedRatio: CGFloat = CGFloatClamp(aspectRatio, 0.05, 95.0) + +// addSubviewWithScaleAspectFitLayout(view: imageView, aspectRatio: aspectRatio) contentView = imageView - - // attachment approval provides it's own play button to keep it + + // Attachment approval provides it's own play button to keep it // at the proper zoom scale. if mode != .attachmentApproval { - let videoPlayIcon = UIImage(named: "CirclePlay")! - let videoPlayButton = UIImageView(image: videoPlayIcon) - self.videoPlayButton = videoPlayButton - videoPlayButton.contentMode = .scaleAspectFit self.addSubview(videoPlayButton) - videoPlayButton.autoCenterInSuperview() - videoPlayButton.autoSetDimension(.width, toSize: 72) - videoPlayButton.autoSetDimension(.height, toSize: 72) + } + + NSLayoutConstraint.activate([ + imageView.centerXAnchor.constraint(equalTo: centerXAnchor), + imageView.centerYAnchor.constraint(equalTo: centerYAnchor), + imageView.widthAnchor.constraint( + equalTo: imageView.heightAnchor, + multiplier: clampedRatio + ), + imageView.widthAnchor.constraint(lessThanOrEqualTo: widthAnchor), + imageView.heightAnchor.constraint(lessThanOrEqualTo: heightAnchor) + ]) + + // Attachment approval provides it's own play button to keep it + // at the proper zoom scale. + if mode != .attachmentApproval { + self.addSubview(videoPlayButton) +// videoPlayButton.autoCenterInSuperview() +// videoPlayButton.autoSetDimension(.width, toSize: 72) +// videoPlayButton.autoSetDimension(.height, toSize: 72) + + NSLayoutConstraint.activate([ + videoPlayButton.centerXAnchor.constraint(equalTo: centerXAnchor), + videoPlayButton.centerYAnchor.constraint(equalTo: centerYAnchor), + imageView.widthAnchor.constraint(equalToConstant: 72), + imageView.heightAnchor.constraint(equalToConstant: 72) + ]) } } private func createUrlPreview() { // If link previews aren't enabled then use a fallback state guard let linkPreviewURL: String = OWSLinkPreview.previewURL(forRawBodyText: attachment.text()) else { +// "vc_share_link_previews_disabled_title" = "Link Previews Disabled"; +// "vc_share_link_previews_disabled_explanation" = "Enabling link previews will show previews for URLs you sshare. This can be useful, but Session will need to contact linked websites to generate previews. You can enable link previews in Session's settings."; +// TODO: Show "warning" about disabled link previews instead createGenericPreview() return } @@ -413,33 +565,40 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate { } private func createGenericPreview() { - var subviews = [UIView]() - - let imageView = createHeroImageView(imageName: "FileLarge") - imageView.contentMode = .center - subviews.append(imageView) - - let fileNameLabel = createFileNameLabel() - if let fileNameLabel = fileNameLabel { - subviews.append(fileNameLabel) - } - - let fileSizeLabel = createFileSizeLabel() - subviews.append(fileSizeLabel) - - let stackView = wrapViewsInVerticalStack(subviews: subviews) + imageView.image = UIImage(named: "FileLarge") + stackView.backgroundColor = .green self.addSubview(stackView) - fileNameLabel?.autoPinWidthToSuperview(withMargin: 32) + + stackView.addArrangedSubview(imageView) + stackView.addArrangedSubview(UIView.vSpacer(0)) + stackView.addArrangedSubview(titleLabel) + stackView.addArrangedSubview(fileSizeLabel) + + imageView.addSubview(fileTypeImageView) - // We want to center the stackView in it's superview while also ensuring - // it's superview is big enough to contain it. - stackView.autoPinWidthToSuperview() - stackView.autoVCenterInSuperview() - NSLayoutConstraint.autoSetPriority(UILayoutPriority.defaultLow) { - stackView.autoPinHeightToSuperview() - } - stackView.autoPinEdge(toSuperviewEdge: .top, withInset: 0, relation: .greaterThanOrEqual) - stackView.autoPinEdge(toSuperviewEdge: .bottom, withInset: 0, relation: .greaterThanOrEqual) + NSLayoutConstraint.activate([ + stackView.centerYAnchor.constraint(equalTo: centerYAnchor), + stackView.widthAnchor.constraint(equalTo: widthAnchor), + stackView.heightAnchor.constraint(lessThanOrEqualTo: heightAnchor), + + imageView.widthAnchor.constraint(equalToConstant: 150), + imageView.heightAnchor.constraint(equalToConstant: 150), + titleLabel.widthAnchor.constraint(equalTo: stackView.widthAnchor, constant: -(32 * 2)), + fileSizeLabel.widthAnchor.constraint(equalTo: stackView.widthAnchor, constant: -(32 * 2)), + + fileTypeImageView.centerXAnchor.constraint(equalTo: imageView.centerXAnchor), + fileTypeImageView.centerYAnchor.constraint( + equalTo: imageView.centerYAnchor, + constant: 25 + ), + fileTypeImageView.widthAnchor.constraint( + equalTo: fileTypeImageView.heightAnchor, + multiplier: ((fileTypeImageView.image?.size.width ?? 1) / (fileTypeImageView.image?.size.height ?? 1)) + ), + fileTypeImageView.widthAnchor.constraint( + equalTo: imageView.widthAnchor, constant: -75 + ) + ]) } private func createHeroViewSize() -> CGFloat { @@ -474,10 +633,10 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate { private func labelFont() -> UIFont { switch mode { - case .large, .attachmentApproval: - return UIFont.ows_regularFont(withSize: ScaleFromIPhone5To7Plus(18, 24)) - case .small: - return UIFont.ows_regularFont(withSize: ScaleFromIPhone5To7Plus(14, 14)) + case .large, .attachmentApproval: + return UIFont.ows_regularFont(withSize: ScaleFromIPhone5To7Plus(18, 24)) + case .small: + return UIFont.ows_regularFont(withSize: ScaleFromIPhone5To7Plus(14, 14)) } } @@ -495,19 +654,17 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate { return nil } - return String(format: NSLocalizedString("ATTACHMENT_APPROVAL_FILE_EXTENSION_FORMAT", - comment: "Format string for file extension label in call interstitial view"), - fileExtension.uppercased()) + //"Format string for file extension label in call interstitial view" + return String(format: "ATTACHMENT_APPROVAL_FILE_EXTENSION_FORMAT".localized(), fileExtension.uppercased()) } public func formattedFileName() -> String? { - guard let sourceFilename = attachment.sourceFilename else { - return nil - } + guard let sourceFilename = attachment.sourceFilename else { return nil } + let filename = sourceFilename.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) - guard filename.count > 0 else { - return nil - } + + guard filename.count > 0 else { return nil } + return filename } @@ -543,8 +700,7 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate { // MARK: - Event Handlers - @objc - func audioPlayButtonPressed(sender: UIButton) { + @objc func audioPlayPauseButtonPressed(sender: UIButton) { audioPlayer?.togglePlayState() } @@ -580,16 +736,22 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate { } private func setAudioIconToPlay() { - let image = UIImage(named: "audio_play_black_large")?.withRenderingMode(.alwaysTemplate) - assert(image != nil) - audioPlayButton?.setImage(image, for: .normal) - audioPlayButton?.imageView?.tintColor = controlTintColor + //attachment_audio +// let image = UIImage(named: "audio_play_black_large")?.withRenderingMode(.alwaysTemplate) +// assert(image != nil) +// audioPlayButton?.setImage(image, for: .normal) +// audioPlayButton?.imageView?.tintColor = controlTintColor + //let image = UIImage(named: "CirclePlay") + let image = UIImage(named: "Play") + audioPlayPauseButton.setImage(image, for: .normal) } private func setAudioIconToPause() { - let image = UIImage(named: "audio_pause_black_large")?.withRenderingMode(.alwaysTemplate) - assert(image != nil) - audioPlayButton?.setImage(image, for: .normal) - audioPlayButton?.imageView?.tintColor = controlTintColor +// let image = UIImage(named: "audio_pause_black_large")?.withRenderingMode(.alwaysTemplate) +// assert(image != nil) +// audioPlayButton?.setImage(image, for: .normal) +// audioPlayButton?.imageView?.tintColor = controlTintColor + let image = UIImage(named: "Pause") + audioPlayPauseButton.setImage(image, for: .normal) } } diff --git a/SignalUtilitiesKit/Utilities/UIColor+Extensions.swift b/SignalUtilitiesKit/Utilities/UIColor+Extensions.swift new file mode 100644 index 000000000..19aa376ce --- /dev/null +++ b/SignalUtilitiesKit/Utilities/UIColor+Extensions.swift @@ -0,0 +1,43 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import UIKit.UIColor + +public extension UIColor { + struct HSBA { + public var hue: CGFloat = 0 + public var saturation: CGFloat = 0 + public var brightness: CGFloat = 0 + public var alpha: CGFloat = 0 + + public init?(color: UIColor) { + // Note: Looks like as of iOS 10 devices use the kCGColorSpaceExtendedGray color + // space for grayscale colors which seems to be compatible with the RGB color space + // meaning we don'e need to check 'getWhite:alpha:' if the below method fails, for + // more info see: https://developer.apple.com/documentation/uikit/uicolor#overview + guard color.getHue(&hue, saturation: &saturation, brightness: &brightness, alpha: &alpha) else { + return nil + } + } + } + + var hsba: HSBA? { return HSBA(color: self) } + + // MARK: - Functions + + func toImage() -> UIImage { + let bounds: CGRect = CGRect(x: 0, y: 0, width: 1, height: 1) + let renderer: UIGraphicsImageRenderer = UIGraphicsImageRenderer(bounds: bounds) + + return renderer.image { rendererContext in + rendererContext.cgContext.setFillColor(self.cgColor) + rendererContext.cgContext.fill(bounds) + } + } + + func darken(by percentage: CGFloat) -> UIColor { + guard percentage != 0 else { return self } + guard let hsba: HSBA = self.hsba else { return self } + + return UIColor(hue: hsba.hue, saturation: hsba.saturation, brightness: (hsba.brightness - percentage), alpha: hsba.alpha) + } +} From 67ad9658591a5b1235b082dd4f21d286c3f68114 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 13 Jan 2022 10:05:42 +1100 Subject: [PATCH 07/26] Link Preview error state and UI standardisation Added a couple of error states for Link Preview loading. Standardised the UI creation code style. Removed some debug and redundant code. --- .../Translations/de.lproj/Localizable.strings | 3 + .../Translations/en.lproj/Localizable.strings | 3 + .../Translations/es.lproj/Localizable.strings | 3 + .../Translations/fa.lproj/Localizable.strings | 3 + .../Translations/fi.lproj/Localizable.strings | 3 + .../Translations/fr.lproj/Localizable.strings | 3 + .../Translations/hi.lproj/Localizable.strings | 3 + .../Translations/hr.lproj/Localizable.strings | 3 + .../id-ID.lproj/Localizable.strings | 3 + .../Translations/it.lproj/Localizable.strings | 3 + .../Translations/ja.lproj/Localizable.strings | 3 + .../Translations/nl.lproj/Localizable.strings | 3 + .../Translations/pl.lproj/Localizable.strings | 3 + .../pt_BR.lproj/Localizable.strings | 3 + .../Translations/ru.lproj/Localizable.strings | 3 + .../Translations/si.lproj/Localizable.strings | 3 + .../Translations/sk.lproj/Localizable.strings | 3 + .../Translations/sv.lproj/Localizable.strings | 3 + .../Translations/th.lproj/Localizable.strings | 3 + .../vi-VN.lproj/Localizable.strings | 3 + .../zh-Hant.lproj/Localizable.strings | 3 + .../zh_CN.lproj/Localizable.strings | 3 + .../MediaMessageView.swift | 427 ++++++++---------- .../Utilities/UIView+Utilities.swift | 7 + 24 files changed, 252 insertions(+), 248 deletions(-) diff --git a/Session/Meta/Translations/de.lproj/Localizable.strings b/Session/Meta/Translations/de.lproj/Localizable.strings index 8652ee572..1f1bc19a2 100644 --- a/Session/Meta/Translations/de.lproj/Localizable.strings +++ b/Session/Meta/Translations/de.lproj/Localizable.strings @@ -543,6 +543,9 @@ "vc_share_title" = "Mit Session teilen"; "vc_share_loading_message" = "Anlagen werden vorbereitet..."; "vc_share_sending_message" = "Wird gesendet ..."; +"vc_share_link_previews_error" = "Unable to load preview"; +"vc_share_link_previews_disabled_title" = "Link Previews Disabled"; +"vc_share_link_previews_disabled_explanation" = "Enabling link previews will show previews for URLs you share. This can be useful, but Session will need to contact linked websites to generate previews.\n\nYou can enable link previews in Session's settings."; "view_open_group_invitation_description" = "Gruppeneinladung öffnen"; "vc_conversation_settings_invite_button_title" = "Mitglieder hinzufügen"; "vc_settings_faq_button_title" = "FAQ"; diff --git a/Session/Meta/Translations/en.lproj/Localizable.strings b/Session/Meta/Translations/en.lproj/Localizable.strings index a58f90996..200895618 100644 --- a/Session/Meta/Translations/en.lproj/Localizable.strings +++ b/Session/Meta/Translations/en.lproj/Localizable.strings @@ -543,6 +543,9 @@ "vc_share_title" = "Share to Session"; "vc_share_loading_message" = "Preparing attachments..."; "vc_share_sending_message" = "Sending..."; +"vc_share_link_previews_error" = "Unable to load preview"; +"vc_share_link_previews_disabled_title" = "Link Previews Disabled"; +"vc_share_link_previews_disabled_explanation" = "Enabling link previews will show previews for URLs you share. This can be useful, but Session will need to contact linked websites to generate previews.\n\nYou can enable link previews in Session's settings."; "view_open_group_invitation_description" = "Open group invitation"; "vc_conversation_settings_invite_button_title" = "Add Members"; "vc_settings_faq_button_title" = "FAQ"; diff --git a/Session/Meta/Translations/es.lproj/Localizable.strings b/Session/Meta/Translations/es.lproj/Localizable.strings index 96dcb37e1..c5e93b6cd 100644 --- a/Session/Meta/Translations/es.lproj/Localizable.strings +++ b/Session/Meta/Translations/es.lproj/Localizable.strings @@ -543,6 +543,9 @@ "vc_share_title" = "Compartir en Session"; "vc_share_loading_message" = "Preparando archivos adjuntos..."; "vc_share_sending_message" = "Enviando..."; +"vc_share_link_previews_error" = "Unable to load preview"; +"vc_share_link_previews_disabled_title" = "Link Previews Disabled"; +"vc_share_link_previews_disabled_explanation" = "Enabling link previews will show previews for URLs you share. This can be useful, but Session will need to contact linked websites to generate previews.\n\nYou can enable link previews in Session's settings."; "view_open_group_invitation_description" = "Abrir invitación de grupo"; "vc_conversation_settings_invite_button_title" = "Añadir Miembros"; "vc_settings_faq_button_title" = "FAQ"; diff --git a/Session/Meta/Translations/fa.lproj/Localizable.strings b/Session/Meta/Translations/fa.lproj/Localizable.strings index 365e59a13..ce9f7462f 100644 --- a/Session/Meta/Translations/fa.lproj/Localizable.strings +++ b/Session/Meta/Translations/fa.lproj/Localizable.strings @@ -543,6 +543,9 @@ "vc_share_title" = "اشتراک گذاری با Session"; "vc_share_loading_message" = "آماده سازی پیوست‌ها..."; "vc_share_sending_message" = "در حال ارسال..."; +"vc_share_link_previews_error" = "Unable to load preview"; +"vc_share_link_previews_disabled_title" = "Link Previews Disabled"; +"vc_share_link_previews_disabled_explanation" = "Enabling link previews will show previews for URLs you share. This can be useful, but Session will need to contact linked websites to generate previews.\n\nYou can enable link previews in Session's settings."; "view_open_group_invitation_description" = "Open group invitation"; "vc_conversation_settings_invite_button_title" = "Add Members"; "vc_settings_faq_button_title" = "FAQ"; diff --git a/Session/Meta/Translations/fi.lproj/Localizable.strings b/Session/Meta/Translations/fi.lproj/Localizable.strings index e207a79c7..000500447 100644 --- a/Session/Meta/Translations/fi.lproj/Localizable.strings +++ b/Session/Meta/Translations/fi.lproj/Localizable.strings @@ -543,6 +543,9 @@ "vc_share_title" = "Jaa Sessioniin"; "vc_share_loading_message" = "Valmistellaan liitteitä..."; "vc_share_sending_message" = "Lähetetään..."; +"vc_share_link_previews_error" = "Unable to load preview"; +"vc_share_link_previews_disabled_title" = "Link Previews Disabled"; +"vc_share_link_previews_disabled_explanation" = "Enabling link previews will show previews for URLs you share. This can be useful, but Session will need to contact linked websites to generate previews.\n\nYou can enable link previews in Session's settings."; "view_open_group_invitation_description" = "Avaa ryhmäkutsu"; "vc_conversation_settings_invite_button_title" = "Lisää jäseniä"; "vc_settings_faq_button_title" = "FAQ"; diff --git a/Session/Meta/Translations/fr.lproj/Localizable.strings b/Session/Meta/Translations/fr.lproj/Localizable.strings index 0d8755eab..2866a93c1 100644 --- a/Session/Meta/Translations/fr.lproj/Localizable.strings +++ b/Session/Meta/Translations/fr.lproj/Localizable.strings @@ -543,6 +543,9 @@ "vc_share_title" = "Partager en Session"; "vc_share_loading_message" = "Préparation des pièces jointes ..."; "vc_share_sending_message" = "Envoi..."; +"vc_share_link_previews_error" = "Unable to load preview"; +"vc_share_link_previews_disabled_title" = "Link Previews Disabled"; +"vc_share_link_previews_disabled_explanation" = "Enabling link previews will show previews for URLs you share. This can be useful, but Session will need to contact linked websites to generate previews.\n\nYou can enable link previews in Session's settings."; "view_open_group_invitation_description" = "Invitation à un groupe ouvert"; "vc_conversation_settings_invite_button_title" = "Ajouter des membres"; "vc_settings_faq_button_title" = "FAQ"; diff --git a/Session/Meta/Translations/hi.lproj/Localizable.strings b/Session/Meta/Translations/hi.lproj/Localizable.strings index acceed3b6..bb5831abb 100644 --- a/Session/Meta/Translations/hi.lproj/Localizable.strings +++ b/Session/Meta/Translations/hi.lproj/Localizable.strings @@ -543,6 +543,9 @@ "vc_share_title" = "सत्र में साझा करें"; "vc_share_loading_message" = "अटैचमेंट तैयार किए जा रहे हैं..."; "vc_share_sending_message" = "भेजा जा रहा है..."; +"vc_share_link_previews_error" = "Unable to load preview"; +"vc_share_link_previews_disabled_title" = "Link Previews Disabled"; +"vc_share_link_previews_disabled_explanation" = "Enabling link previews will show previews for URLs you share. This can be useful, but Session will need to contact linked websites to generate previews.\n\nYou can enable link previews in Session's settings."; "view_open_group_invitation_description" = "ग्रुप आमंत्रण खोलें"; "vc_conversation_settings_invite_button_title" = "सदस्य जोड़ें"; "vc_settings_faq_button_title" = "FAQ"; diff --git a/Session/Meta/Translations/hr.lproj/Localizable.strings b/Session/Meta/Translations/hr.lproj/Localizable.strings index 56fb5a125..1b91a2189 100644 --- a/Session/Meta/Translations/hr.lproj/Localizable.strings +++ b/Session/Meta/Translations/hr.lproj/Localizable.strings @@ -543,6 +543,9 @@ "vc_share_title" = "Podijeli sa Session-om"; "vc_share_loading_message" = "Priprema privitaka..."; "vc_share_sending_message" = "Slanje..."; +"vc_share_link_previews_error" = "Unable to load preview"; +"vc_share_link_previews_disabled_title" = "Link Previews Disabled"; +"vc_share_link_previews_disabled_explanation" = "Enabling link previews will show previews for URLs you share. This can be useful, but Session will need to contact linked websites to generate previews.\n\nYou can enable link previews in Session's settings."; "view_open_group_invitation_description" = "Otvori pozivnicu za grupu"; "vc_conversation_settings_invite_button_title" = "Dodaj članove"; "vc_settings_faq_button_title" = "FAQ"; diff --git a/Session/Meta/Translations/id-ID.lproj/Localizable.strings b/Session/Meta/Translations/id-ID.lproj/Localizable.strings index 98a53450d..9c29ed382 100644 --- a/Session/Meta/Translations/id-ID.lproj/Localizable.strings +++ b/Session/Meta/Translations/id-ID.lproj/Localizable.strings @@ -543,6 +543,9 @@ "vc_share_title" = "Share to Session"; "vc_share_loading_message" = "Preparing attachments..."; "vc_share_sending_message" = "Sending..."; +"vc_share_link_previews_error" = "Unable to load preview"; +"vc_share_link_previews_disabled_title" = "Link Previews Disabled"; +"vc_share_link_previews_disabled_explanation" = "Enabling link previews will show previews for URLs you share. This can be useful, but Session will need to contact linked websites to generate previews.\n\nYou can enable link previews in Session's settings."; "view_open_group_invitation_description" = "Open group invitation"; "vc_conversation_settings_invite_button_title" = "Add Members"; "vc_settings_faq_button_title" = "FAQ"; diff --git a/Session/Meta/Translations/it.lproj/Localizable.strings b/Session/Meta/Translations/it.lproj/Localizable.strings index 117f44ebb..931f37a66 100644 --- a/Session/Meta/Translations/it.lproj/Localizable.strings +++ b/Session/Meta/Translations/it.lproj/Localizable.strings @@ -543,6 +543,9 @@ "vc_share_title" = "Condividi con Session"; "vc_share_loading_message" = "Preparazione allegati..."; "vc_share_sending_message" = "Invio..."; +"vc_share_link_previews_error" = "Unable to load preview"; +"vc_share_link_previews_disabled_title" = "Link Previews Disabled"; +"vc_share_link_previews_disabled_explanation" = "Enabling link previews will show previews for URLs you share. This can be useful, but Session will need to contact linked websites to generate previews.\n\nYou can enable link previews in Session's settings."; "view_open_group_invitation_description" = "Apri invito di gruppo"; "vc_conversation_settings_invite_button_title" = "Aggiungi membri"; "vc_settings_faq_button_title" = "FAQ"; diff --git a/Session/Meta/Translations/ja.lproj/Localizable.strings b/Session/Meta/Translations/ja.lproj/Localizable.strings index f70f9d905..99cf416d1 100644 --- a/Session/Meta/Translations/ja.lproj/Localizable.strings +++ b/Session/Meta/Translations/ja.lproj/Localizable.strings @@ -543,6 +543,9 @@ "vc_share_title" = "Sessionと共有"; "vc_share_loading_message" = "添付ファイルを準備しています..."; "vc_share_sending_message" = "送信中…"; +"vc_share_link_previews_error" = "Unable to load preview"; +"vc_share_link_previews_disabled_title" = "Link Previews Disabled"; +"vc_share_link_previews_disabled_explanation" = "Enabling link previews will show previews for URLs you share. This can be useful, but Session will need to contact linked websites to generate previews.\n\nYou can enable link previews in Session's settings."; "view_open_group_invitation_description" = "公開グループからの招待"; "vc_conversation_settings_invite_button_title" = "メンバーを追加する"; "vc_settings_faq_button_title" = "FAQ"; diff --git a/Session/Meta/Translations/nl.lproj/Localizable.strings b/Session/Meta/Translations/nl.lproj/Localizable.strings index 177d55e6a..88b53c6e5 100644 --- a/Session/Meta/Translations/nl.lproj/Localizable.strings +++ b/Session/Meta/Translations/nl.lproj/Localizable.strings @@ -543,6 +543,9 @@ "vc_share_title" = "Delen naar de Session"; "vc_share_loading_message" = "Bijlagen voorbereiden..."; "vc_share_sending_message" = "Aan het verzenden..."; +"vc_share_link_previews_error" = "Unable to load preview"; +"vc_share_link_previews_disabled_title" = "Link Previews Disabled"; +"vc_share_link_previews_disabled_explanation" = "Enabling link previews will show previews for URLs you share. This can be useful, but Session will need to contact linked websites to generate previews.\n\nYou can enable link previews in Session's settings."; "view_open_group_invitation_description" = "Open groepsuitnodiging"; "vc_conversation_settings_invite_button_title" = "Voeg deelnemers toe"; "vc_settings_faq_button_title" = "FAQ"; diff --git a/Session/Meta/Translations/pl.lproj/Localizable.strings b/Session/Meta/Translations/pl.lproj/Localizable.strings index bb96a0f00..9c8227b01 100644 --- a/Session/Meta/Translations/pl.lproj/Localizable.strings +++ b/Session/Meta/Translations/pl.lproj/Localizable.strings @@ -543,6 +543,9 @@ "vc_share_title" = "Udostępnij w Session"; "vc_share_loading_message" = "Przygotowywanie załączników..."; "vc_share_sending_message" = "Wysyłanie..."; +"vc_share_link_previews_error" = "Unable to load preview"; +"vc_share_link_previews_disabled_title" = "Link Previews Disabled"; +"vc_share_link_previews_disabled_explanation" = "Enabling link previews will show previews for URLs you share. This can be useful, but Session will need to contact linked websites to generate previews.\n\nYou can enable link previews in Session's settings."; "view_open_group_invitation_description" = "Otwórz zaproszenie do grupy"; "vc_conversation_settings_invite_button_title" = "Dodaj użytkowników"; "vc_settings_faq_button_title" = "FAQ"; diff --git a/Session/Meta/Translations/pt_BR.lproj/Localizable.strings b/Session/Meta/Translations/pt_BR.lproj/Localizable.strings index 55a396153..e72860d67 100644 --- a/Session/Meta/Translations/pt_BR.lproj/Localizable.strings +++ b/Session/Meta/Translations/pt_BR.lproj/Localizable.strings @@ -543,6 +543,9 @@ "vc_share_title" = "Compartilhar no Session"; "vc_share_loading_message" = "Preparando anexos..."; "vc_share_sending_message" = "Enviando..."; +"vc_share_link_previews_error" = "Unable to load preview"; +"vc_share_link_previews_disabled_title" = "Link Previews Disabled"; +"vc_share_link_previews_disabled_explanation" = "Enabling link previews will show previews for URLs you share. This can be useful, but Session will need to contact linked websites to generate previews.\n\nYou can enable link previews in Session's settings."; "view_open_group_invitation_description" = "Convite para grupo aberto"; "vc_conversation_settings_invite_button_title" = "Adicionar Membros"; "vc_settings_faq_button_title" = "FAQ"; diff --git a/Session/Meta/Translations/ru.lproj/Localizable.strings b/Session/Meta/Translations/ru.lproj/Localizable.strings index fa6bec9f5..9dfdb5e2f 100644 --- a/Session/Meta/Translations/ru.lproj/Localizable.strings +++ b/Session/Meta/Translations/ru.lproj/Localizable.strings @@ -543,6 +543,9 @@ "vc_share_title" = "Поделиться в Session"; "vc_share_loading_message" = "Подготовка вложений..."; "vc_share_sending_message" = "Отправка..."; +"vc_share_link_previews_error" = "Unable to load preview"; +"vc_share_link_previews_disabled_title" = "Link Previews Disabled"; +"vc_share_link_previews_disabled_explanation" = "Enabling link previews will show previews for URLs you share. This can be useful, but Session will need to contact linked websites to generate previews.\n\nYou can enable link previews in Session's settings."; "view_open_group_invitation_description" = "Открыть приглашение в группу"; "vc_conversation_settings_invite_button_title" = "Добавить участников"; "vc_settings_faq_button_title" = "FAQ"; diff --git a/Session/Meta/Translations/si.lproj/Localizable.strings b/Session/Meta/Translations/si.lproj/Localizable.strings index 3e0abe961..3a7997c7b 100644 --- a/Session/Meta/Translations/si.lproj/Localizable.strings +++ b/Session/Meta/Translations/si.lproj/Localizable.strings @@ -543,6 +543,9 @@ "vc_share_title" = "Share to Session"; "vc_share_loading_message" = "Preparing attachments..."; "vc_share_sending_message" = "Sending..."; +"vc_share_link_previews_error" = "Unable to load preview"; +"vc_share_link_previews_disabled_title" = "Link Previews Disabled"; +"vc_share_link_previews_disabled_explanation" = "Enabling link previews will show previews for URLs you share. This can be useful, but Session will need to contact linked websites to generate previews.\n\nYou can enable link previews in Session's settings."; "view_open_group_invitation_description" = "Open group invitation"; "vc_conversation_settings_invite_button_title" = "Add Members"; "vc_settings_faq_button_title" = "FAQ"; diff --git a/Session/Meta/Translations/sk.lproj/Localizable.strings b/Session/Meta/Translations/sk.lproj/Localizable.strings index f3b6cf878..f1fcfd831 100644 --- a/Session/Meta/Translations/sk.lproj/Localizable.strings +++ b/Session/Meta/Translations/sk.lproj/Localizable.strings @@ -543,6 +543,9 @@ "vc_share_title" = "Share to Session"; "vc_share_loading_message" = "Pripravujú sa prílohy..."; "vc_share_sending_message" = "Odosiela sa..."; +"vc_share_link_previews_error" = "Unable to load preview"; +"vc_share_link_previews_disabled_title" = "Link Previews Disabled"; +"vc_share_link_previews_disabled_explanation" = "Enabling link previews will show previews for URLs you share. This can be useful, but Session will need to contact linked websites to generate previews.\n\nYou can enable link previews in Session's settings."; "view_open_group_invitation_description" = "Otvoriť skupinovú pozvánku"; "vc_conversation_settings_invite_button_title" = "Pridať členov"; "vc_settings_faq_button_title" = "FAQ"; diff --git a/Session/Meta/Translations/sv.lproj/Localizable.strings b/Session/Meta/Translations/sv.lproj/Localizable.strings index ac958ab1e..78e621dd3 100644 --- a/Session/Meta/Translations/sv.lproj/Localizable.strings +++ b/Session/Meta/Translations/sv.lproj/Localizable.strings @@ -543,6 +543,9 @@ "vc_share_title" = "Dela i Session"; "vc_share_loading_message" = "Förbereder bilagor..."; "vc_share_sending_message" = "Skickar..."; +"vc_share_link_previews_error" = "Unable to load preview"; +"vc_share_link_previews_disabled_title" = "Link Previews Disabled"; +"vc_share_link_previews_disabled_explanation" = "Enabling link previews will show previews for URLs you share. This can be useful, but Session will need to contact linked websites to generate previews.\n\nYou can enable link previews in Session's settings."; "view_open_group_invitation_description" = "Öppen gruppinbjudan"; "vc_conversation_settings_invite_button_title" = "Lägg till medlemmar"; "vc_settings_faq_button_title" = "FAQ"; diff --git a/Session/Meta/Translations/th.lproj/Localizable.strings b/Session/Meta/Translations/th.lproj/Localizable.strings index a09a9c805..8386b8a94 100644 --- a/Session/Meta/Translations/th.lproj/Localizable.strings +++ b/Session/Meta/Translations/th.lproj/Localizable.strings @@ -543,6 +543,9 @@ "vc_share_title" = "เชิญมาใช้ Session"; "vc_share_loading_message" = "รวบรวมสิ่งแนบ..."; "vc_share_sending_message" = "กำลังส่ง..."; +"vc_share_link_previews_error" = "Unable to load preview"; +"vc_share_link_previews_disabled_title" = "Link Previews Disabled"; +"vc_share_link_previews_disabled_explanation" = "Enabling link previews will show previews for URLs you share. This can be useful, but Session will need to contact linked websites to generate previews.\n\nYou can enable link previews in Session's settings."; "view_open_group_invitation_description" = "การเชิญเข้าร่วมกลุ่ม"; "vc_conversation_settings_invite_button_title" = "เพิ่มสมาชิก"; "vc_settings_faq_button_title" = "FAQ"; diff --git a/Session/Meta/Translations/vi-VN.lproj/Localizable.strings b/Session/Meta/Translations/vi-VN.lproj/Localizable.strings index 5ff989a05..ca8781d2a 100644 --- a/Session/Meta/Translations/vi-VN.lproj/Localizable.strings +++ b/Session/Meta/Translations/vi-VN.lproj/Localizable.strings @@ -543,6 +543,9 @@ "vc_share_title" = "Share to Session"; "vc_share_loading_message" = "Preparing attachments..."; "vc_share_sending_message" = "Sending..."; +"vc_share_link_previews_error" = "Unable to load preview"; +"vc_share_link_previews_disabled_title" = "Link Previews Disabled"; +"vc_share_link_previews_disabled_explanation" = "Enabling link previews will show previews for URLs you share. This can be useful, but Session will need to contact linked websites to generate previews.\n\nYou can enable link previews in Session's settings."; "view_open_group_invitation_description" = "Open group invitation"; "vc_conversation_settings_invite_button_title" = "Add Members"; "vc_settings_faq_button_title" = "FAQ"; diff --git a/Session/Meta/Translations/zh-Hant.lproj/Localizable.strings b/Session/Meta/Translations/zh-Hant.lproj/Localizable.strings index b44d5dc07..38518449f 100644 --- a/Session/Meta/Translations/zh-Hant.lproj/Localizable.strings +++ b/Session/Meta/Translations/zh-Hant.lproj/Localizable.strings @@ -543,6 +543,9 @@ "vc_share_title" = "分享至 Session"; "vc_share_loading_message" = "準備附件中⋯"; "vc_share_sending_message" = "傳送中⋯"; +"vc_share_link_previews_error" = "Unable to load preview"; +"vc_share_link_previews_disabled_title" = "Link Previews Disabled"; +"vc_share_link_previews_disabled_explanation" = "Enabling link previews will show previews for URLs you share. This can be useful, but Session will need to contact linked websites to generate previews.\n\nYou can enable link previews in Session's settings."; "view_open_group_invitation_description" = "打開群組邀請"; "vc_conversation_settings_invite_button_title" = "新增成員"; "vc_settings_faq_button_title" = "FAQ"; diff --git a/Session/Meta/Translations/zh_CN.lproj/Localizable.strings b/Session/Meta/Translations/zh_CN.lproj/Localizable.strings index baaa6937c..391d1c159 100644 --- a/Session/Meta/Translations/zh_CN.lproj/Localizable.strings +++ b/Session/Meta/Translations/zh_CN.lproj/Localizable.strings @@ -543,6 +543,9 @@ "vc_share_title" = "分享到 Session"; "vc_share_loading_message" = "正在准备附件......"; "vc_share_sending_message" = "正在发送…"; +"vc_share_link_previews_error" = "Unable to load preview"; +"vc_share_link_previews_disabled_title" = "Link Previews Disabled"; +"vc_share_link_previews_disabled_explanation" = "Enabling link previews will show previews for URLs you share. This can be useful, but Session will need to contact linked websites to generate previews.\n\nYou can enable link previews in Session's settings."; "view_open_group_invitation_description" = "打开群组邀请"; "vc_conversation_settings_invite_button_title" = "添加成员"; "vc_settings_faq_button_title" = "FAQ"; diff --git a/SignalUtilitiesKit/Media Viewing & Editing/MediaMessageView.swift b/SignalUtilitiesKit/Media Viewing & Editing/MediaMessageView.swift index 28b5cbac4..f0d260c08 100644 --- a/SignalUtilitiesKit/Media Viewing & Editing/MediaMessageView.swift +++ b/SignalUtilitiesKit/Media Viewing & Editing/MediaMessageView.swift @@ -59,7 +59,6 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate { createViews() - backgroundColor = .red setupLayout() } @@ -78,13 +77,22 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate { stackView.distribution = .equalSpacing switch mode { - case .large, .attachmentApproval: stackView.spacing = 10 + case .attachmentApproval: stackView.spacing = 2 + case .large: stackView.spacing = 10 case .small: stackView.spacing = 5 } return stackView }() + private lazy var loadingView: NVActivityIndicatorView = { + let view: NVActivityIndicatorView = NVActivityIndicatorView(frame: CGRect.zero, type: .circleStrokeSpin, color: Colors.text, padding: nil) + view.translatesAutoresizingMaskIntoConstraints = false + view.isHidden = true + + return view + }() + private lazy var imageView: UIImageView = { let view: UIImageView = UIImageView() view.translatesAutoresizingMaskIntoConstraints = false @@ -98,8 +106,6 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate { private lazy var fileTypeImageView: UIImageView = { let view: UIImageView = UIImageView() view.translatesAutoresizingMaskIntoConstraints = false - view.layer.minificationFilter = .trilinear - view.layer.magnificationFilter = .trilinear return view }() @@ -126,8 +132,6 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate { button.clipsToBounds = true button.setBackgroundImage(UIColor.white.toImage(), for: .normal) button.setBackgroundImage(UIColor.white.darken(by: 0.2).toImage(), for: .highlighted) - button.layer.cornerRadius = 30 - button.addTarget(self, action: #selector(audioPlayPauseButtonPressed), for: .touchUpInside) return button @@ -136,13 +140,35 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate { private lazy var titleLabel: UILabel = { let label: UILabel = UILabel() label.translatesAutoresizingMaskIntoConstraints = false - label.font = labelFont() - label.text = (formattedFileName() ?? formattedFileExtension()) - label.textColor = controlTintColor label.textAlignment = .center label.lineBreakMode = .byTruncatingMiddle + + if let fileName: String = attachment.sourceFilename?.trimmingCharacters(in: .whitespacesAndNewlines), fileName.count > 0 { + label.text = fileName + } + else if let fileExtension: String = attachment.fileExtension { + label.text = String( + format: "ATTACHMENT_APPROVAL_FILE_EXTENSION_FORMAT".localized(), + fileExtension.uppercased() + ) + } + label.isHidden = ((label.text?.count ?? 0) == 0) + switch mode { + case .attachmentApproval: + label.font = UIFont.ows_boldFont(withSize: ScaleFromIPhone5To7Plus(16, 22)) + label.textColor = Colors.text + + case .large: + label.font = UIFont.ows_regularFont(withSize: ScaleFromIPhone5To7Plus(18, 24)) + label.textColor = Colors.accent + + case .small: + label.font = UIFont.ows_regularFont(withSize: ScaleFromIPhone5To7Plus(14, 14)) + label.textColor = Colors.accent + } + return label }() @@ -151,13 +177,25 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate { let label: UILabel = UILabel() label.translatesAutoresizingMaskIntoConstraints = false - label.font = labelFont() // Format string for file size label in call interstitial view. // Embeds: {{file size as 'N mb' or 'N kb'}}. label.text = String(format: "ATTACHMENT_APPROVAL_FILE_SIZE_FORMAT".localized(), OWSFormat.formatFileSize(UInt(fileSize))) - label.textColor = controlTintColor label.textAlignment = .center + switch mode { + case .attachmentApproval: + label.font = UIFont.ows_regularFont(withSize: ScaleFromIPhone5To7Plus(12, 18)) + label.textColor = Colors.pinIcon + + case .large: + label.font = UIFont.ows_regularFont(withSize: ScaleFromIPhone5To7Plus(18, 24)) + label.textColor = Colors.accent + + case .small: + label.font = UIFont.ows_regularFont(withSize: ScaleFromIPhone5To7Plus(14, 14)) + label.textColor = Colors.accent + } + return label }() @@ -253,8 +291,11 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate { audioPlayer = OWSAudioPlayer(mediaUrl: dataUrl, audioBehavior: .playback, delegate: self) - imageView.image = UIImage(named: "FileLarge") - fileTypeImageView.image = UIImage(named: "table_ic_notification_sound") + imageView.image = UIImage(named: "FileLarge")?.withRenderingMode(.alwaysTemplate) + imageView.tintColor = Colors.text + fileTypeImageView.image = UIImage(named: "table_ic_notification_sound")? + .withRenderingMode(.alwaysTemplate) + fileTypeImageView.tintColor = Colors.text setAudioIconToPlay() self.addSubview(stackView) @@ -266,38 +307,42 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate { stackView.addArrangedSubview(fileSizeLabel) imageView.addSubview(fileTypeImageView) - + + let imageSize: CGFloat = { + switch mode { + case .large: return 200 + case .attachmentApproval: return 150 + case .small: return 80 + } + }() + let audioButtonSize: CGFloat = (imageSize / 2.5) + audioPlayPauseButton.layer.cornerRadius = (audioButtonSize / 2) + NSLayoutConstraint.activate([ stackView.centerYAnchor.constraint(equalTo: centerYAnchor), stackView.widthAnchor.constraint(equalTo: widthAnchor), stackView.heightAnchor.constraint(lessThanOrEqualTo: heightAnchor), - imageView.widthAnchor.constraint(equalToConstant: 150), - imageView.heightAnchor.constraint(equalToConstant: 150), + imageView.widthAnchor.constraint(equalToConstant: imageSize), + imageView.heightAnchor.constraint(equalToConstant: imageSize), titleLabel.widthAnchor.constraint(equalTo: stackView.widthAnchor, constant: -(32 * 2)), fileSizeLabel.widthAnchor.constraint(equalTo: stackView.widthAnchor, constant: -(32 * 2)), fileTypeImageView.centerXAnchor.constraint(equalTo: imageView.centerXAnchor), fileTypeImageView.centerYAnchor.constraint( equalTo: imageView.centerYAnchor, - constant: 25 + constant: ceil(imageSize * 0.15) ), fileTypeImageView.widthAnchor.constraint( equalTo: fileTypeImageView.heightAnchor, multiplier: ((fileTypeImageView.image?.size.width ?? 1) / (fileTypeImageView.image?.size.height ?? 1)) ), - fileTypeImageView.widthAnchor.constraint( - equalTo: imageView.widthAnchor, constant: -75 - ), + fileTypeImageView.widthAnchor.constraint(equalTo: imageView.widthAnchor, multiplier: 0.5), audioPlayPauseButton.centerXAnchor.constraint(equalTo: imageView.centerXAnchor), audioPlayPauseButton.centerYAnchor.constraint(equalTo: imageView.centerYAnchor), - audioPlayPauseButton.widthAnchor.constraint( - equalToConstant: (audioPlayPauseButton.layer.cornerRadius * 2) - ), - audioPlayPauseButton.heightAnchor.constraint( - equalToConstant: (audioPlayPauseButton.layer.cornerRadius * 2) - ) + audioPlayPauseButton.widthAnchor.constraint(equalToConstant: audioButtonSize), + audioPlayPauseButton.heightAnchor.constraint(equalToConstant: audioButtonSize) ]) } @@ -401,16 +446,12 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate { return } -// let imageView = UIImageView(image: image) imageView.image = image -// imageView.layer.minificationFilter = .trilinear -// imageView.layer.magnificationFilter = .trilinear self.addSubview(imageView) let aspectRatio = image.size.width / image.size.height let clampedRatio: CGFloat = CGFloatClamp(aspectRatio, 0.05, 95.0) -// addSubviewWithScaleAspectFitLayout(view: imageView, aspectRatio: aspectRatio) contentView = imageView // Attachment approval provides it's own play button to keep it @@ -434,9 +475,6 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate { // at the proper zoom scale. if mode != .attachmentApproval { self.addSubview(videoPlayButton) -// videoPlayButton.autoCenterInSuperview() -// videoPlayButton.autoSetDimension(.width, toSize: 72) -// videoPlayButton.autoSetDimension(.height, toSize: 72) NSLayoutConstraint.activate([ videoPlayButton.centerXAnchor.constraint(equalTo: centerXAnchor), @@ -450,139 +488,142 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate { private func createUrlPreview() { // If link previews aren't enabled then use a fallback state guard let linkPreviewURL: String = OWSLinkPreview.previewURL(forRawBodyText: attachment.text()) else { -// "vc_share_link_previews_disabled_title" = "Link Previews Disabled"; -// "vc_share_link_previews_disabled_explanation" = "Enabling link previews will show previews for URLs you sshare. This can be useful, but Session will need to contact linked websites to generate previews. You can enable link previews in Session's settings."; -// TODO: Show "warning" about disabled link previews instead - createGenericPreview() + titleLabel.text = "vc_share_link_previews_disabled_title".localized() + titleLabel.isHidden = false + + fileSizeLabel.text = "vc_share_link_previews_disabled_explanation".localized() + fileSizeLabel.textColor = Colors.text + fileSizeLabel.numberOfLines = 0 + + self.addSubview(stackView) + + stackView.addArrangedSubview(titleLabel) + stackView.addArrangedSubview(UIView.vSpacer(10)) + stackView.addArrangedSubview(fileSizeLabel) + + NSLayoutConstraint.activate([ + stackView.centerXAnchor.constraint(equalTo: centerXAnchor), + stackView.centerYAnchor.constraint(equalTo: centerYAnchor), + stackView.widthAnchor.constraint(equalTo: widthAnchor, constant: -(32 * 2)), + stackView.heightAnchor.constraint(lessThanOrEqualTo: heightAnchor) + ]) return } linkPreviewInfo = (url: linkPreviewURL, draft: nil) - - var subviews = [UIView]() - - let color: UIColor = isLightMode ? .black : .white - let loadingView = NVActivityIndicatorView(frame: CGRect.zero, type: .circleStrokeSpin, color: color, padding: nil) - loadingView.set(.width, to: 24) - loadingView.set(.height, to: 24) - loadingView.startAnimating() - subviews.append(loadingView) - - let imageViewContainer = UIView() - imageViewContainer.clipsToBounds = true - imageViewContainer.contentMode = .center - imageViewContainer.alpha = 0 - imageViewContainer.layer.cornerRadius = 8 - subviews.append(imageViewContainer) - - let imageView = createHeroImageView(imageName: "FileLarge") - imageViewContainer.addSubview(imageView) - imageView.pin(to: imageViewContainer) - let titleLabel = UILabel() + stackView.axis = .horizontal + stackView.distribution = .fill + + imageView.clipsToBounds = true + imageView.image = UIImage(named: "Link")?.withTint(Colors.text) + imageView.alpha = 0 // Not 'isHidden' because we want it to take up space in the UIStackView + imageView.contentMode = .center + imageView.backgroundColor = (isDarkMode ? .black : UIColor.black.withAlphaComponent(0.06)) + imageView.layer.cornerRadius = 8 + + loadingView.isHidden = false + loadingView.startAnimating() + + titleLabel.font = .boldSystemFont(ofSize: Values.smallFontSize) titleLabel.text = linkPreviewURL - titleLabel.textColor = controlTintColor - titleLabel.font = labelFont() - titleLabel.textAlignment = .center - titleLabel.lineBreakMode = .byTruncatingMiddle - subviews.append(titleLabel) + titleLabel.textAlignment = .left + titleLabel.numberOfLines = 2 + titleLabel.isHidden = false - let stackView = wrapViewsInVerticalStack(subviews: subviews) self.addSubview(stackView) + self.addSubview(loadingView) - titleLabel.autoPinWidthToSuperview(withMargin: 32) + stackView.addArrangedSubview(imageView) + stackView.addArrangedSubview(UIView.vhSpacer(10, 0)) + stackView.addArrangedSubview(titleLabel) + + let imageSize: CGFloat = { + switch mode { + case .large: return 120 + case .attachmentApproval, .small: return 80 + } + }() NSLayoutConstraint.activate([ - imageView.widthAnchor.constraint(equalToConstant: 80), - imageView.heightAnchor.constraint(equalToConstant: 80) + stackView.centerXAnchor.constraint(equalTo: centerXAnchor), + stackView.centerYAnchor.constraint(equalTo: centerYAnchor), + stackView.widthAnchor.constraint(equalTo: widthAnchor, constant: -(32 * 2)), + stackView.heightAnchor.constraint(lessThanOrEqualTo: heightAnchor), + + imageView.widthAnchor.constraint(equalToConstant: imageSize), + imageView.heightAnchor.constraint(equalToConstant: imageSize), + + loadingView.centerXAnchor.constraint(equalTo: imageView.centerXAnchor), + loadingView.centerYAnchor.constraint(equalTo: imageView.centerYAnchor), + loadingView.widthAnchor.constraint(equalToConstant: ceil(imageSize / 3)), + loadingView.heightAnchor.constraint(equalToConstant: ceil(imageSize / 3)) ]) // Build the link preview - OWSLinkPreview.tryToBuildPreviewInfo(previewUrl: linkPreviewURL).done { [weak self] draft in - // Loader - loadingView.alpha = 0 - loadingView.stopAnimating() - - self?.linkPreviewInfo = (url: linkPreviewURL, draft: draft) - - // TODO: Look at refactoring this behaviour to consolidate attachment mutations - self?.attachment.linkPreviewDraft = draft - - let image: UIImage? - - if let jpegImageData: Data = draft.jpegImageData, let loadedImage: UIImage = UIImage(data: jpegImageData) { - image = loadedImage - imageView.contentMode = .scaleAspectFill + OWSLinkPreview.tryToBuildPreviewInfo(previewUrl: linkPreviewURL) + .done { [weak self] draft in + // TODO: Look at refactoring this behaviour to consolidate attachment mutations + self?.attachment.linkPreviewDraft = draft + self?.linkPreviewInfo = (url: linkPreviewURL, draft: draft) + + // Update the UI + self?.titleLabel.text = (draft.title ?? self?.titleLabel.text) + self?.loadingView.alpha = 0 + self?.loadingView.stopAnimating() + self?.imageView.alpha = 1 + + if let jpegImageData: Data = draft.jpegImageData, let loadedImage: UIImage = UIImage(data: jpegImageData) { + self?.imageView.image = loadedImage + self?.imageView.contentMode = .scaleAspectFill + } } - else { - image = UIImage(named: "Link")?.withTint(isLightMode ? .black : .white) - imageView.contentMode = .center + .catch { [weak self] _ in + self?.titleLabel.attributedText = NSMutableAttributedString(string: linkPreviewURL) + .rtlSafeAppend( + "\n\("vc_share_link_previews_error".localized())", + attributes: [ + NSAttributedString.Key.font: UIFont.ows_regularFont( + withSize: Values.verySmallFontSize + ), + NSAttributedString.Key.foregroundColor: self?.fileSizeLabel.textColor + ] + .compactMapValues { $0 } + ) + self?.loadingView.alpha = 0 + self?.loadingView.stopAnimating() + self?.imageView.alpha = 1 } - - // Image view - (imageView as? UIImageView)?.image = image - imageViewContainer.alpha = 1 - imageViewContainer.backgroundColor = isDarkMode ? .black : UIColor.black.withAlphaComponent(0.06) - - // Title - if let title = draft.title { - titleLabel.font = .boldSystemFont(ofSize: Values.smallFontSize) - titleLabel.text = title - titleLabel.textAlignment = .left - titleLabel.numberOfLines = 2 - } - - guard let hStackView = self?.wrapViewsInHorizontalStack(subviews: subviews) else { - // TODO: Fallback - return - } - stackView.removeFromSuperview() - self?.addSubview(hStackView) - - // We want to center the stackView in it's superview while also ensuring - // it's superview is big enough to contain it. - hStackView.autoPinWidthToSuperview(withMargin: 32) - hStackView.autoVCenterInSuperview() - NSLayoutConstraint.autoSetPriority(UILayoutPriority.defaultLow) { - hStackView.autoPinHeightToSuperview() - } - hStackView.autoPinEdge(toSuperviewEdge: .top, withInset: 0, relation: .greaterThanOrEqual) - hStackView.autoPinEdge(toSuperviewEdge: .bottom, withInset: 0, relation: .greaterThanOrEqual) - }.catch { _ in - // TODO: Fallback - loadingView.stopAnimating() - }.retainUntilComplete() - - // We want to center the stackView in it's superview while also ensuring - // it's superview is big enough to contain it. - stackView.autoPinWidthToSuperview() - stackView.autoVCenterInSuperview() - NSLayoutConstraint.autoSetPriority(UILayoutPriority.defaultLow) { - stackView.autoPinHeightToSuperview() - } - stackView.autoPinEdge(toSuperviewEdge: .top, withInset: 0, relation: .greaterThanOrEqual) - stackView.autoPinEdge(toSuperviewEdge: .bottom, withInset: 0, relation: .greaterThanOrEqual) + .retainUntilComplete() } private func createGenericPreview() { imageView.image = UIImage(named: "FileLarge") - stackView.backgroundColor = .green + self.addSubview(stackView) stackView.addArrangedSubview(imageView) - stackView.addArrangedSubview(UIView.vSpacer(0)) + stackView.addArrangedSubview(UIView.vSpacer(5)) stackView.addArrangedSubview(titleLabel) stackView.addArrangedSubview(fileSizeLabel) imageView.addSubview(fileTypeImageView) + + let imageSize: CGFloat = { + switch mode { + case .large: return 200 + case .attachmentApproval: return 150 + case .small: return 80 + } + }() NSLayoutConstraint.activate([ stackView.centerYAnchor.constraint(equalTo: centerYAnchor), stackView.widthAnchor.constraint(equalTo: widthAnchor), stackView.heightAnchor.constraint(lessThanOrEqualTo: heightAnchor), - imageView.widthAnchor.constraint(equalToConstant: 150), - imageView.heightAnchor.constraint(equalToConstant: 150), + imageView.widthAnchor.constraint(equalToConstant: imageSize), + imageView.heightAnchor.constraint(equalToConstant: imageSize), titleLabel.widthAnchor.constraint(equalTo: stackView.widthAnchor, constant: -(32 * 2)), fileSizeLabel.widthAnchor.constraint(equalTo: stackView.widthAnchor, constant: -(32 * 2)), @@ -601,103 +642,6 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate { ]) } - private func createHeroViewSize() -> CGFloat { - switch mode { - case .large: - return ScaleFromIPhone5To7Plus(175, 225) - case .attachmentApproval: - return ScaleFromIPhone5(100) - case .small: - return ScaleFromIPhone5To7Plus(80, 80) - } - } - - private func createHeroImageView(imageName: String) -> UIView { - let imageSize = createHeroViewSize() - - let image = UIImage(named: imageName) - assert(image != nil) - let imageView = UIImageView(image: image) - imageView.layer.minificationFilter = .trilinear - imageView.layer.magnificationFilter = .trilinear - imageView.layer.shadowColor = UIColor.black.cgColor - let shadowScaling = 5.0 - imageView.layer.shadowRadius = CGFloat(2.0 * shadowScaling) - imageView.layer.shadowOpacity = 0.25 - imageView.layer.shadowOffset = CGSize(width: 0.75 * shadowScaling, height: 0.75 * shadowScaling) - imageView.autoSetDimension(.width, toSize: imageSize) - imageView.autoSetDimension(.height, toSize: imageSize) - - return imageView - } - - private func labelFont() -> UIFont { - switch mode { - case .large, .attachmentApproval: - return UIFont.ows_regularFont(withSize: ScaleFromIPhone5To7Plus(18, 24)) - case .small: - return UIFont.ows_regularFont(withSize: ScaleFromIPhone5To7Plus(14, 14)) - } - } - - private var controlTintColor: UIColor { - switch mode { - case .small, .large: - return Colors.accent - case .attachmentApproval: - return Colors.text - } - } - - private func formattedFileExtension() -> String? { - guard let fileExtension = attachment.fileExtension else { - return nil - } - - //"Format string for file extension label in call interstitial view" - return String(format: "ATTACHMENT_APPROVAL_FILE_EXTENSION_FORMAT".localized(), fileExtension.uppercased()) - } - - public func formattedFileName() -> String? { - guard let sourceFilename = attachment.sourceFilename else { return nil } - - let filename = sourceFilename.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) - - guard filename.count > 0 else { return nil } - - return filename - } - - private func createFileNameLabel() -> UIView? { - let filename = formattedFileName() ?? formattedFileExtension() - - guard filename != nil else { - return nil - } - - let label = UILabel() - label.text = filename - label.textColor = controlTintColor - label.font = labelFont() - label.textAlignment = .center - label.lineBreakMode = .byTruncatingMiddle - return label - } - - private func createFileSizeLabel() -> UIView { - let label = UILabel() - let fileSize = attachment.dataLength - label.text = String(format: NSLocalizedString("ATTACHMENT_APPROVAL_FILE_SIZE_FORMAT", - comment: "Format string for file size label in call interstitial view. Embeds: {{file size as 'N mb' or 'N kb'}}."), - OWSFormat.formatFileSize(UInt(fileSize))) - - label.textColor = controlTintColor - label.font = labelFont() - label.textAlignment = .center - - return label - } - // MARK: - Event Handlers @objc func audioPlayPauseButtonPressed(sender: UIButton) { @@ -723,10 +667,9 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate { } private func ensureButtonState() { - if playbackState == .playing { - setAudioIconToPause() - } else { - setAudioIconToPlay() + switch playbackState { + case .playing: setAudioIconToPause() + default: setAudioIconToPlay() } } @@ -736,22 +679,10 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate { } private func setAudioIconToPlay() { - //attachment_audio -// let image = UIImage(named: "audio_play_black_large")?.withRenderingMode(.alwaysTemplate) -// assert(image != nil) -// audioPlayButton?.setImage(image, for: .normal) -// audioPlayButton?.imageView?.tintColor = controlTintColor - //let image = UIImage(named: "CirclePlay") - let image = UIImage(named: "Play") - audioPlayPauseButton.setImage(image, for: .normal) + audioPlayPauseButton.setImage(UIImage(named: "Play"), for: .normal) } private func setAudioIconToPause() { -// let image = UIImage(named: "audio_pause_black_large")?.withRenderingMode(.alwaysTemplate) -// assert(image != nil) -// audioPlayButton?.setImage(image, for: .normal) -// audioPlayButton?.imageView?.tintColor = controlTintColor - let image = UIImage(named: "Pause") - audioPlayPauseButton.setImage(image, for: .normal) + audioPlayPauseButton.setImage(UIImage(named: "Pause"), for: .normal) } } diff --git a/SignalUtilitiesKit/Utilities/UIView+Utilities.swift b/SignalUtilitiesKit/Utilities/UIView+Utilities.swift index 8856d2b91..41f9cc2c4 100644 --- a/SignalUtilitiesKit/Utilities/UIView+Utilities.swift +++ b/SignalUtilitiesKit/Utilities/UIView+Utilities.swift @@ -13,6 +13,13 @@ public extension UIView { result.set(.height, to: height) return result } + + static func vhSpacer(_ width: CGFloat, _ height: CGFloat) -> UIView { + let result = UIView() + result.set(.width, to: width) + result.set(.height, to: height) + return result + } static func separator() -> UIView { let result = UIView() From e6c90c5e18865ebb67d87f2356b9452c5b965077 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 13 Jan 2022 14:49:14 +1100 Subject: [PATCH 08/26] Finished off UI clean up Fixed a couple more vertical alignment issues with certain attachment types. Finished cleaning up the MediaMessageView UI code (removed old code). --- .../Attachments/SignalAttachment.swift | 25 + .../AttachmentPrepViewController.swift | 45 +- .../MediaMessageView.swift | 732 ++++++++---------- 3 files changed, 379 insertions(+), 423 deletions(-) diff --git a/SessionMessagingKit/Sending & Receiving/Attachments/SignalAttachment.swift b/SessionMessagingKit/Sending & Receiving/Attachments/SignalAttachment.swift index f65d8640c..e6222d59e 100644 --- a/SessionMessagingKit/Sending & Receiving/Attachments/SignalAttachment.swift +++ b/SessionMessagingKit/Sending & Receiving/Attachments/SignalAttachment.swift @@ -20,6 +20,18 @@ public enum SignalAttachmentError: Error { case couldNotResizeImage } +@objc +public enum SignalAttachmentType: Int { + case text + case oversizeText + case image + case animatedImage + case video + case audio + case url + case unknown +} + extension String { public var filenameWithoutExtension: String { return (self as NSString).deletingPathExtension @@ -434,6 +446,19 @@ public class SignalAttachment: NSObject { private class var mediaUTISet: Set { return audioUTISet.union(videoUTISet).union(animatedImageUTISet).union(inputImageUTISet) } + + @objc + public var fileType: SignalAttachmentType { + if isAnimatedImage { return .animatedImage } + if isImage { return .image } + if isVideo { return .video } + if isAudio { return .audio } + if isUrl { return .url } + if isOversizeText { return .oversizeText } + if isText { return .text } + + return .unknown + } @objc public var isImage: Bool { diff --git a/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentPrepViewController.swift b/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentPrepViewController.swift index d249a63ef..998d7c783 100644 --- a/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentPrepViewController.swift +++ b/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentPrepViewController.swift @@ -45,6 +45,10 @@ public class AttachmentPrepViewController: OWSViewController, PlayerProgressBarD // MARK: - UI + fileprivate static let verticalCenterOffset: CGFloat = ( + AttachmentTextToolbar.kMinTextViewHeight + (AttachmentTextToolbar.kToolbarMargin * 2) + ) + private lazy var scrollView: UIScrollView = { // Scroll View - used to zoom/pan on images and video let scrollView: UIScrollView = UIScrollView() @@ -56,17 +60,6 @@ public class AttachmentPrepViewController: OWSViewController, PlayerProgressBarD // Panning should stop pretty soon after the user stops scrolling scrollView.decelerationRate = UIScrollView.DecelerationRate.fast - // If the content isn't zoomable then inset the content so it appears centered - if !isZoomable { - scrollView.isScrollEnabled = false - scrollView.contentInset = UIEdgeInsets( - top: 0, - leading: 0, - bottom: (AttachmentTextToolbar.kMinTextViewHeight + (AttachmentTextToolbar.kToolbarMargin * 2)), - trailing: 0 - ) - } - return scrollView }() @@ -112,6 +105,7 @@ public class AttachmentPrepViewController: OWSViewController, PlayerProgressBarD private lazy var progressBar: PlayerProgressBar = { let progressBar: PlayerProgressBar = PlayerProgressBar() + progressBar.translatesAutoresizingMaskIntoConstraints = false progressBar.player = videoPlayer?.avPlayer progressBar.delegate = self @@ -161,7 +155,7 @@ public class AttachmentPrepViewController: OWSViewController, PlayerProgressBarD contentContainerView.addSubview(scrollView) scrollView.addSubview(mediaMessageView) - if let editorView: ImageEditorView = imageEditorView { + if attachment.isImage, let editorView: ImageEditorView = imageEditorView { view.addSubview(editorView) imageEditorUpdateNavigationBar() @@ -235,12 +229,20 @@ public class AttachmentPrepViewController: OWSViewController, PlayerProgressBarD mediaMessageView.heightAnchor.constraint(equalTo: view.heightAnchor) ]) - if let editorView: ImageEditorView = imageEditorView { + if attachment.isImage, let editorView: ImageEditorView = imageEditorView { + let size: CGSize = (attachment.image()?.size ?? CGSize.zero) + let isPortrait: Bool = (size.height > size.width) + NSLayoutConstraint.activate([ editorView.topAnchor.constraint(equalTo: view.topAnchor), editorView.leftAnchor.constraint(equalTo: view.leftAnchor), editorView.rightAnchor.constraint(equalTo: view.rightAnchor), - editorView.bottomAnchor.constraint(equalTo: view.bottomAnchor) + editorView.bottomAnchor.constraint( + equalTo: view.bottomAnchor, + // Don't offset portrait images as they look fine vertically aligned, horizontal + // ones need to be pushed up a bit though + constant: (isPortrait ? 0 : -AttachmentPrepViewController.verticalCenterOffset) + ) ]) } @@ -258,7 +260,10 @@ public class AttachmentPrepViewController: OWSViewController, PlayerProgressBarD progressBar.heightAnchor.constraint(equalToConstant: 44), playVideoButton.centerXAnchor.constraint(equalTo: contentContainerView.centerXAnchor), - playVideoButton.centerYAnchor.constraint(equalTo: contentContainerView.centerYAnchor), + playVideoButton.centerYAnchor.constraint( + equalTo: contentContainerView.centerYAnchor, + constant: -AttachmentPrepViewController.verticalCenterOffset + ), playVideoButton.widthAnchor.constraint(equalToConstant: playButtonSize), playVideoButton.heightAnchor.constraint(equalToConstant: playButtonSize), ]) @@ -455,8 +460,14 @@ extension AttachmentPrepViewController: UIScrollViewDelegate { // Allow the user to zoom out to 100% of the attachment size if it's smaller // than the screen fileprivate func resetContentInset() { + // If the content isn't zoomable then inset the content so it appears centered guard isZoomable else { - scrollView.contentOffset = CGPoint(x: 0, y: scrollView.contentInset.bottom) + scrollView.contentInset = UIEdgeInsets( + top: -AttachmentPrepViewController.verticalCenterOffset, + leading: 0, + bottom: 0, + trailing: 0 + ) return } @@ -464,7 +475,7 @@ extension AttachmentPrepViewController: UIScrollViewDelegate { let offsetY: CGFloat = max((scrollView.bounds.height - scrollView.contentSize.height) * 0.5, 0) scrollView.contentInset = UIEdgeInsets( - top: offsetY, + top: offsetY - AttachmentPrepViewController.verticalCenterOffset, left: offsetX, bottom: 0, right: 0 diff --git a/SignalUtilitiesKit/Media Viewing & Editing/MediaMessageView.swift b/SignalUtilitiesKit/Media Viewing & Editing/MediaMessageView.swift index f0d260c08..d5c9c2d64 100644 --- a/SignalUtilitiesKit/Media Viewing & Editing/MediaMessageView.swift +++ b/SignalUtilitiesKit/Media Viewing & Editing/MediaMessageView.swift @@ -20,11 +20,15 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate { public let mode: Mode public let attachment: SignalAttachment - public var audioPlayer: OWSAudioPlayer? + public lazy var audioPlayer: OWSAudioPlayer? = { + guard let dataUrl = attachment.dataUrl else { return nil } + + return OWSAudioPlayer(mediaUrl: dataUrl, audioBehavior: .playback, delegate: self) + }() - private var linkPreviewInfo: (url: String, draft: OWSLinkPreviewDraft?)? + public var audioProgressSeconds: CGFloat = 0 + public var audioDurationSeconds: CGFloat = 0 - public var playbackState = AudioPlaybackState.stopped { didSet { AssertIsOnMainThread() @@ -32,13 +36,51 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate { ensureButtonState() } } - - public var audioProgressSeconds: CGFloat = 0 - public var audioDurationSeconds: CGFloat = 0 - - public var contentView: UIView? - + private lazy var validImage: UIImage? = { + switch attachment.fileType { + case .image: + guard + attachment.isValidImage, + let image: UIImage = attachment.image(), + image.size.width > 0, + image.size.height > 0 + else { + return nil + } + + return image + + case .video: + guard + attachment.isValidVideo, + let image: UIImage = attachment.videoPreview(), + image.size.width > 0, + image.size.height > 0 + else { + return nil + } + + return image + + default: return nil + } + }() + private lazy var validAnimatedImage: YYImage? = { + guard + attachment.fileType == .animatedImage, + attachment.isValidImage, + let dataUrl: URL = attachment.dataUrl, + let image: YYImage = YYImage(contentsOfFile: dataUrl.path), + image.size.width > 0, + image.size.height > 0 + else { + return nil + } + + return image + }() + private var linkPreviewInfo: (url: String, draft: OWSLinkPreviewDraft?)? // MARK: Initializers @@ -55,11 +97,14 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate { self.attachment = attachment self.mode = mode + // Set the linkPreviewUrl if it's a url + if attachment.isUrl, let linkPreviewURL: String = OWSLinkPreview.previewURL(forRawBodyText: attachment.text()) { + self.linkPreviewInfo = (url: linkPreviewURL, draft: nil) + } + super.init(frame: CGRect.zero) - createViews() - - + setupViews() setupLayout() } @@ -74,7 +119,7 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate { stackView.translatesAutoresizingMaskIntoConstraints = false stackView.axis = .vertical stackView.alignment = .center - stackView.distribution = .equalSpacing + stackView.distribution = .fill switch mode { case .attachmentApproval: stackView.spacing = 2 @@ -97,8 +142,28 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate { let view: UIImageView = UIImageView() view.translatesAutoresizingMaskIntoConstraints = false view.contentMode = .scaleAspectFit - view.layer.minificationFilter = .trilinear - view.layer.magnificationFilter = .trilinear + view.image = UIImage(named: "FileLarge")?.withRenderingMode(.alwaysTemplate) + view.tintColor = Colors.text + view.isHidden = true + + // Override the image to the correct one + switch attachment.fileType { + case .image, .video: + if let validImage: UIImage = validImage { + view.layer.minificationFilter = .trilinear + view.layer.magnificationFilter = .trilinear + view.image = validImage + } + + case .url: + view.clipsToBounds = true + view.image = UIImage(named: "Link")?.withTint(Colors.text) + view.contentMode = .center + view.backgroundColor = (isDarkMode ? .black : UIColor.black.withAlphaComponent(0.06)) + view.layer.cornerRadius = 8 + + default: break + } return view }() @@ -106,6 +171,7 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate { private lazy var fileTypeImageView: UIImageView = { let view: UIImageView = UIImageView() view.translatesAutoresizingMaskIntoConstraints = false + view.isHidden = true return view }() @@ -113,16 +179,27 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate { private lazy var animatedImageView: YYAnimatedImageView = { let view: YYAnimatedImageView = YYAnimatedImageView() view.translatesAutoresizingMaskIntoConstraints = false + view.isHidden = true + + if let image: YYImage = validAnimatedImage { + view.image = image + } + else { + view.contentMode = .scaleAspectFit + view.image = UIImage(named: "FileLarge")?.withRenderingMode(.alwaysTemplate) + view.tintColor = Colors.text + } return view }() lazy var videoPlayButton: UIImageView = { - let imageView: UIImageView = UIImageView(image: UIImage(named: "CirclePlay")) - imageView.translatesAutoresizingMaskIntoConstraints = false - imageView.contentMode = .scaleAspectFit + let view: UIImageView = UIImageView(image: UIImage(named: "CirclePlay")) + view.translatesAutoresizingMaskIntoConstraints = false + view.contentMode = .scaleAspectFit + view.isHidden = true - return imageView + return view }() /// Note: This uses different assets from the `videoPlayButton` and has a 'Pause' state @@ -133,6 +210,7 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate { button.setBackgroundImage(UIColor.white.toImage(), for: .normal) button.setBackgroundImage(UIColor.white.darken(by: 0.2).toImage(), for: .highlighted) button.addTarget(self, action: #selector(audioPlayPauseButtonPressed), for: .touchUpInside) + button.isHidden = true return button }() @@ -140,21 +218,8 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate { private lazy var titleLabel: UILabel = { let label: UILabel = UILabel() label.translatesAutoresizingMaskIntoConstraints = false - label.textAlignment = .center - label.lineBreakMode = .byTruncatingMiddle - - if let fileName: String = attachment.sourceFilename?.trimmingCharacters(in: .whitespacesAndNewlines), fileName.count > 0 { - label.text = fileName - } - else if let fileExtension: String = attachment.fileExtension { - label.text = String( - format: "ATTACHMENT_APPROVAL_FILE_EXTENSION_FORMAT".localized(), - fileExtension.uppercased() - ) - } - - label.isHidden = ((label.text?.count ?? 0) == 0) + // Styling switch mode { case .attachmentApproval: label.font = UIFont.ows_boldFont(withSize: ScaleFromIPhone5To7Plus(16, 22)) @@ -169,19 +234,49 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate { label.textColor = Colors.accent } + // Content + switch attachment.fileType { + case .image, .animatedImage, .video: break // No title for these + + case .url: + // If we have no link preview info at this point then assume link previews are disabled + guard let linkPreviewURL: String = linkPreviewInfo?.url else { + label.text = "vc_share_link_previews_disabled_title".localized() + break + } + + label.font = .boldSystemFont(ofSize: Values.smallFontSize) + label.text = linkPreviewURL + label.textAlignment = .left + label.lineBreakMode = .byTruncatingTail + label.numberOfLines = 2 + + default: + if let fileName: String = attachment.sourceFilename?.trimmingCharacters(in: .whitespacesAndNewlines), fileName.count > 0 { + label.text = fileName + } + else if let fileExtension: String = attachment.fileExtension { + label.text = String( + format: "ATTACHMENT_APPROVAL_FILE_EXTENSION_FORMAT".localized(), + fileExtension.uppercased() + ) + } + + label.textAlignment = .center + label.lineBreakMode = .byTruncatingMiddle + } + + // Hide the label if it has no content + label.isHidden = ((label.text?.count ?? 0) == 0) + return label }() private lazy var fileSizeLabel: UILabel = { - let fileSize: UInt = attachment.dataLength - let label: UILabel = UILabel() label.translatesAutoresizingMaskIntoConstraints = false - // Format string for file size label in call interstitial view. - // Embeds: {{file size as 'N mb' or 'N kb'}}. - label.text = String(format: "ATTACHMENT_APPROVAL_FILE_SIZE_FORMAT".localized(), OWSFormat.formatFileSize(UInt(fileSize))) - label.textAlignment = .center + // Styling switch mode { case .attachmentApproval: label.font = UIFont.ows_regularFont(withSize: ScaleFromIPhone5To7Plus(12, 18)) @@ -196,137 +291,193 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate { label.textColor = Colors.accent } + // Content + switch attachment.fileType { + case .image, .animatedImage, .video: break // No size for these + + case .url: + // If we have no link preview info at this point then assume link previews are disabled + if linkPreviewInfo == nil { + label.text = "vc_share_link_previews_disabled_explanation".localized() + label.textColor = Colors.text + label.textAlignment = .center + label.numberOfLines = 0 + break + } + + default: + // Format string for file size label in call interstitial view. + // Embeds: {{file size as 'N mb' or 'N kb'}}. + let fileSize: UInt = attachment.dataLength + label.text = String(format: "ATTACHMENT_APPROVAL_FILE_SIZE_FORMAT".localized(), OWSFormat.formatFileSize(UInt(fileSize))) + label.textAlignment = .center + } + + // Hide the label if it has no content + label.isHidden = ((label.text?.count ?? 0) == 0) + return label }() // MARK: - Layout - private func createViews() { - if attachment.isAnimatedImage { - createAnimatedPreview() - } else if attachment.isImage { - createImagePreview() - } else if attachment.isVideo { - createVideoPreview() - } else if attachment.isAudio { - createAudioPreview() - } else if attachment.isUrl { - createUrlPreview() - } else if attachment.isText { - // Do nothing as we will just put the text in the 'message' input - } else { - createGenericPreview() - } - } - - private func setupLayout() { - // Bottom inset - } - - // TODO: Any reason for not just using UIStackView - private func wrapViewsInVerticalStack(subviews: [UIView]) -> UIView { - assert(subviews.count > 0) - - let stackView = UIView() - - var lastView: UIView? - for subview in subviews { - - stackView.addSubview(subview) - subview.autoHCenterInSuperview() - - if lastView == nil { - subview.autoPinEdge(toSuperviewEdge: .top) - } else { - subview.autoPinEdge(.top, to: .bottom, of: lastView!, withOffset: 10) - } - - lastView = subview - } - - lastView?.autoPinEdge(toSuperviewEdge: .bottom) - - return stackView - } - - private func wrapViewsInHorizontalStack(subviews: [UIView]) -> UIView { - assert(subviews.count > 0) - - let stackView = UIView() - - var lastView: UIView? - for subview in subviews { - - stackView.addSubview(subview) - subview.autoVCenterInSuperview() - - if lastView == nil { - subview.autoPinEdge(toSuperviewEdge: .left) - } else { - subview.autoPinEdge(.left, to: .right, of: lastView!, withOffset: 10) - } - - lastView = subview - } - - lastView?.autoPinEdge(toSuperviewEdge: .right) - - return stackView - } - -// private func stackSpacing() -> CGFloat { -// switch mode { -// case .large, .attachmentApproval: -// return CGFloat(10) -// case .small: -// return CGFloat(5) -// } -// } - - private func createAudioPreview() { - guard let dataUrl = attachment.dataUrl else { - createGenericPreview() - return - } - - audioPlayer = OWSAudioPlayer(mediaUrl: dataUrl, audioBehavior: .playback, delegate: self) + private func setupViews() { + // Plain text will just be put in the 'message' input so do nothing + guard attachment.fileType != .text && attachment.fileType != .oversizeText else { return } - imageView.image = UIImage(named: "FileLarge")?.withRenderingMode(.alwaysTemplate) - imageView.tintColor = Colors.text - fileTypeImageView.image = UIImage(named: "table_ic_notification_sound")? - .withRenderingMode(.alwaysTemplate) - fileTypeImageView.tintColor = Colors.text - setAudioIconToPlay() - - self.addSubview(stackView) - self.addSubview(audioPlayPauseButton) + // Setup the view hierarchy + addSubview(stackView) + addSubview(loadingView) + addSubview(videoPlayButton) stackView.addArrangedSubview(imageView) - stackView.addArrangedSubview(UIView.vSpacer(0)) + stackView.addArrangedSubview(animatedImageView) + if !titleLabel.isHidden { stackView.addArrangedSubview(UIView.vhSpacer(10, 10)) } stackView.addArrangedSubview(titleLabel) stackView.addArrangedSubview(fileSizeLabel) imageView.addSubview(fileTypeImageView) - let imageSize: CGFloat = { + // Type-specific configurations + switch attachment.fileType { + case .animatedImage: animatedImageView.isHidden = false + case .image: imageView.isHidden = false + + case .video: + // Note: The 'attachmentApproval' mode provides it's own play button to keep + // it at the proper scale when zooming + imageView.isHidden = false + videoPlayButton.isHidden = (mode == .attachmentApproval) + + case .audio: + // Hide the 'audioPlayPauseButton' if the 'audioPlayer' failed to get created + imageView.isHidden = false + audioPlayPauseButton.isHidden = (audioPlayer == nil) + setAudioIconToPlay() + + fileTypeImageView.image = UIImage(named: "table_ic_notification_sound")? + .withRenderingMode(.alwaysTemplate) + fileTypeImageView.tintColor = Colors.text + fileTypeImageView.isHidden = false + + // Note: There is an annoying bug where the MediaMessageView will fill the screen if the + // 'audioPlayPauseButton' is added anywhere within the view hierarchy causing issues with + // the min scale on 'image' and 'animatedImage' file types (assume it's actually any UIButton) + addSubview(audioPlayPauseButton) + + case .url: + imageView.isHidden = false + imageView.alpha = 0 // Not 'isHidden' because we want it to take up space in the UIStackView + loadingView.isHidden = false + + if let linkPreviewUrl: String = linkPreviewInfo?.url { + // Don't want to change the axis until we have a URL to start loading, otherwise the + // error message will be broken + stackView.axis = .horizontal + + loadLinkPreview(linkPreviewURL: linkPreviewUrl) + } + + default: imageView.isHidden = false + } + } + + private func setupLayout() { + // Sizing calculations + let clampedRatio: CGFloat = { + switch attachment.fileType { + case .url: return 1 + + case .image, .video, .audio, .unknown: + let imageSize: CGSize = (imageView.image?.size ?? CGSize(width: 1, height: 1)) + let aspectRatio: CGFloat = (imageSize.width / imageSize.height) + + return CGFloatClamp(aspectRatio, 0.05, 95.0) + + case .animatedImage: + let imageSize: CGSize = (animatedImageView.image?.size ?? CGSize(width: 1, height: 1)) + let aspectRatio: CGFloat = (imageSize.width / imageSize.height) + + return CGFloatClamp(aspectRatio, 0.05, 95.0) + + default: return 0 + } + }() + + let maybeImageSize: CGFloat? = { + switch attachment.fileType { + case .image, .video: + if validImage != nil { return nil } + + // If we don't have a valid image then use the 'generic' case + break + + case .animatedImage: + if validAnimatedImage != nil { return nil } + + // If we don't have a valid image then use the 'generic' case + break + + case .url: + switch mode { + case .large: return 120 + case .attachmentApproval, .small: return 80 + } + + // Use the 'generic' case for these + case .audio, .unknown: break + + default: return nil + } + + // Generic file size switch mode { case .large: return 200 - case .attachmentApproval: return 150 + case .attachmentApproval: return 120 case .small: return 80 } }() + + let imageSize: CGFloat = (maybeImageSize ?? 0) let audioButtonSize: CGFloat = (imageSize / 2.5) audioPlayPauseButton.layer.cornerRadius = (audioButtonSize / 2) + // Actual layout NSLayoutConstraint.activate([ + stackView.centerXAnchor.constraint(equalTo: centerXAnchor), stackView.centerYAnchor.constraint(equalTo: centerYAnchor), - stackView.widthAnchor.constraint(equalTo: widthAnchor), stackView.heightAnchor.constraint(lessThanOrEqualTo: heightAnchor), - imageView.widthAnchor.constraint(equalToConstant: imageSize), - imageView.heightAnchor.constraint(equalToConstant: imageSize), - titleLabel.widthAnchor.constraint(equalTo: stackView.widthAnchor, constant: -(32 * 2)), - fileSizeLabel.widthAnchor.constraint(equalTo: stackView.widthAnchor, constant: -(32 * 2)), + (maybeImageSize != nil ? + stackView.widthAnchor.constraint( + equalTo: widthAnchor, + constant: (attachment.isUrl ? -(32 * 2) : 0) // Inset stackView for urls + ) : + stackView.widthAnchor.constraint(lessThanOrEqualTo: widthAnchor) + ), + + imageView.widthAnchor.constraint( + equalTo: imageView.heightAnchor, + multiplier: clampedRatio + ), + animatedImageView.widthAnchor.constraint( + equalTo: animatedImageView.heightAnchor, + multiplier: clampedRatio + ), + + // Note: AnimatedImage, Image and Video types should allow zooming so be lessThanOrEqualTo + // the view size but some other types should have specific sizes + animatedImageView.widthAnchor.constraint(lessThanOrEqualTo: widthAnchor), + animatedImageView.heightAnchor.constraint(lessThanOrEqualTo: heightAnchor), + + (maybeImageSize != nil ? + imageView.widthAnchor.constraint(equalToConstant: imageSize) : + imageView.widthAnchor.constraint(lessThanOrEqualTo: widthAnchor) + ), + (maybeImageSize != nil ? + imageView.heightAnchor.constraint(equalToConstant: imageSize) : + imageView.heightAnchor.constraint(lessThanOrEqualTo: heightAnchor) + ), fileTypeImageView.centerXAnchor.constraint(equalTo: imageView.centerXAnchor), fileTypeImageView.centerYAnchor.constraint( @@ -339,228 +490,41 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate { ), fileTypeImageView.widthAnchor.constraint(equalTo: imageView.widthAnchor, multiplier: 0.5), - audioPlayPauseButton.centerXAnchor.constraint(equalTo: imageView.centerXAnchor), - audioPlayPauseButton.centerYAnchor.constraint(equalTo: imageView.centerYAnchor), - audioPlayPauseButton.widthAnchor.constraint(equalToConstant: audioButtonSize), - audioPlayPauseButton.heightAnchor.constraint(equalToConstant: audioButtonSize) - ]) - } + videoPlayButton.centerXAnchor.constraint(equalTo: centerXAnchor), + videoPlayButton.centerYAnchor.constraint(equalTo: centerYAnchor), - private func createAnimatedPreview() { - guard attachment.isValidImage else { - createGenericPreview() - return - } - guard let dataUrl = attachment.dataUrl else { - createGenericPreview() - return - } - guard let image = YYImage(contentsOfFile: dataUrl.path) else { - createGenericPreview() - return - } - guard image.size.width > 0 && image.size.height > 0 else { - createGenericPreview() - return - } - animatedImageView.image = image - let aspectRatio: CGFloat = (image.size.width / image.size.height) - let clampedRatio: CGFloat = CGFloatClamp(aspectRatio, 0.05, 95.0) - - addSubview(animatedImageView) -// addSubviewWithScaleAspectFitLayout(view: animatedImageView, aspectRatio: aspectRatio) - contentView = animatedImageView - - NSLayoutConstraint.activate([ - animatedImageView.centerXAnchor.constraint(equalTo: centerXAnchor), - animatedImageView.centerYAnchor.constraint(equalTo: centerYAnchor), - animatedImageView.widthAnchor.constraint( - equalTo: animatedImageView.heightAnchor, - multiplier: clampedRatio - ), - animatedImageView.widthAnchor.constraint(lessThanOrEqualTo: widthAnchor), - animatedImageView.heightAnchor.constraint(lessThanOrEqualTo: heightAnchor) - ]) - } - -// private func addSubviewWithScaleAspectFitLayout(view: UIView, aspectRatio: CGFloat) { -// self.addSubview(view) -// // This emulates the behavior of contentMode = .scaleAspectFit using -// // iOS auto layout constraints. -// // -// // This allows ConversationInputToolbar to place the "cancel" button -// // in the upper-right hand corner of the preview content. -// view.autoCenterInSuperview() -// view.autoPin(toAspectRatio: aspectRatio) -// view.autoMatch(.width, to: .width, of: self, withMultiplier: 1.0, relation: .lessThanOrEqual) -// view.autoMatch(.height, to: .height, of: self, withMultiplier: 1.0, relation: .lessThanOrEqual) -// } - - private func createImagePreview() { - guard attachment.isValidImage else { - createGenericPreview() - return - } - guard let image = attachment.image() else { - createGenericPreview() - return - } - guard image.size.width > 0 && image.size.height > 0 else { - createGenericPreview() - return - } - - imageView.image = image -// imageView.layer.minificationFilter = .trilinear -// imageView.layer.magnificationFilter = .trilinear - - let aspectRatio = image.size.width / image.size.height - let clampedRatio: CGFloat = CGFloatClamp(aspectRatio, 0.05, 95.0) - -// addSubviewWithScaleAspectFitLayout(view: imageView, aspectRatio: aspectRatio) - contentView = imageView - - NSLayoutConstraint.activate([ - imageView.centerXAnchor.constraint(equalTo: centerXAnchor), - imageView.centerYAnchor.constraint(equalTo: centerYAnchor), - imageView.widthAnchor.constraint( - equalTo: imageView.heightAnchor, - multiplier: clampedRatio - ), - imageView.widthAnchor.constraint(lessThanOrEqualTo: widthAnchor), - imageView.heightAnchor.constraint(lessThanOrEqualTo: heightAnchor) - ]) - } - - private func createVideoPreview() { - guard attachment.isValidVideo else { - createGenericPreview() - return - } - guard let image = attachment.videoPreview() else { - createGenericPreview() - return - } - guard image.size.width > 0 && image.size.height > 0 else { - createGenericPreview() - return - } - - imageView.image = image - self.addSubview(imageView) - - let aspectRatio = image.size.width / image.size.height - let clampedRatio: CGFloat = CGFloatClamp(aspectRatio, 0.05, 95.0) - - contentView = imageView - - // Attachment approval provides it's own play button to keep it - // at the proper zoom scale. - if mode != .attachmentApproval { - self.addSubview(videoPlayButton) - } - - NSLayoutConstraint.activate([ - imageView.centerXAnchor.constraint(equalTo: centerXAnchor), - imageView.centerYAnchor.constraint(equalTo: centerYAnchor), - imageView.widthAnchor.constraint( - equalTo: imageView.heightAnchor, - multiplier: clampedRatio - ), - imageView.widthAnchor.constraint(lessThanOrEqualTo: widthAnchor), - imageView.heightAnchor.constraint(lessThanOrEqualTo: heightAnchor) - ]) - - // Attachment approval provides it's own play button to keep it - // at the proper zoom scale. - if mode != .attachmentApproval { - self.addSubview(videoPlayButton) - - NSLayoutConstraint.activate([ - videoPlayButton.centerXAnchor.constraint(equalTo: centerXAnchor), - videoPlayButton.centerYAnchor.constraint(equalTo: centerYAnchor), - imageView.widthAnchor.constraint(equalToConstant: 72), - imageView.heightAnchor.constraint(equalToConstant: 72) - ]) - } - } - - private func createUrlPreview() { - // If link previews aren't enabled then use a fallback state - guard let linkPreviewURL: String = OWSLinkPreview.previewURL(forRawBodyText: attachment.text()) else { - titleLabel.text = "vc_share_link_previews_disabled_title".localized() - titleLabel.isHidden = false - - fileSizeLabel.text = "vc_share_link_previews_disabled_explanation".localized() - fileSizeLabel.textColor = Colors.text - fileSizeLabel.numberOfLines = 0 - - self.addSubview(stackView) - - stackView.addArrangedSubview(titleLabel) - stackView.addArrangedSubview(UIView.vSpacer(10)) - stackView.addArrangedSubview(fileSizeLabel) - - NSLayoutConstraint.activate([ - stackView.centerXAnchor.constraint(equalTo: centerXAnchor), - stackView.centerYAnchor.constraint(equalTo: centerYAnchor), - stackView.widthAnchor.constraint(equalTo: widthAnchor, constant: -(32 * 2)), - stackView.heightAnchor.constraint(lessThanOrEqualTo: heightAnchor) - ]) - return - } - - linkPreviewInfo = (url: linkPreviewURL, draft: nil) - - stackView.axis = .horizontal - stackView.distribution = .fill - - imageView.clipsToBounds = true - imageView.image = UIImage(named: "Link")?.withTint(Colors.text) - imageView.alpha = 0 // Not 'isHidden' because we want it to take up space in the UIStackView - imageView.contentMode = .center - imageView.backgroundColor = (isDarkMode ? .black : UIColor.black.withAlphaComponent(0.06)) - imageView.layer.cornerRadius = 8 - - loadingView.isHidden = false - loadingView.startAnimating() - - titleLabel.font = .boldSystemFont(ofSize: Values.smallFontSize) - titleLabel.text = linkPreviewURL - titleLabel.textAlignment = .left - titleLabel.numberOfLines = 2 - titleLabel.isHidden = false - - self.addSubview(stackView) - self.addSubview(loadingView) - - stackView.addArrangedSubview(imageView) - stackView.addArrangedSubview(UIView.vhSpacer(10, 0)) - stackView.addArrangedSubview(titleLabel) - - let imageSize: CGFloat = { - switch mode { - case .large: return 120 - case .attachmentApproval, .small: return 80 - } - }() - - NSLayoutConstraint.activate([ - stackView.centerXAnchor.constraint(equalTo: centerXAnchor), - stackView.centerYAnchor.constraint(equalTo: centerYAnchor), - stackView.widthAnchor.constraint(equalTo: widthAnchor, constant: -(32 * 2)), - stackView.heightAnchor.constraint(lessThanOrEqualTo: heightAnchor), - - imageView.widthAnchor.constraint(equalToConstant: imageSize), - imageView.heightAnchor.constraint(equalToConstant: imageSize), - loadingView.centerXAnchor.constraint(equalTo: imageView.centerXAnchor), loadingView.centerYAnchor.constraint(equalTo: imageView.centerYAnchor), loadingView.widthAnchor.constraint(equalToConstant: ceil(imageSize / 3)), loadingView.heightAnchor.constraint(equalToConstant: ceil(imageSize / 3)) ]) - // Build the link preview + // No inset for the text for URLs but there is for all other layouts + if (attachment.fileType != .url) { + NSLayoutConstraint.activate([ + titleLabel.widthAnchor.constraint(equalTo: stackView.widthAnchor, constant: -(32 * 2)), + fileSizeLabel.widthAnchor.constraint(equalTo: stackView.widthAnchor, constant: -(32 * 2)) + ]) + } + + // Note: There is an annoying bug where the MediaMessageView will fill the screen if the + // 'audioPlayPauseButton' is added anywhere within the view hierarchy causing issues with + // the min scale on 'image' and 'animatedImage' file types (assume it's actually any UIButton) + if attachment.fileType == .audio { + NSLayoutConstraint.activate([ + audioPlayPauseButton.centerXAnchor.constraint(equalTo: imageView.centerXAnchor), + audioPlayPauseButton.centerYAnchor.constraint(equalTo: imageView.centerYAnchor), + audioPlayPauseButton.widthAnchor.constraint(equalToConstant: audioButtonSize), + audioPlayPauseButton.heightAnchor.constraint(equalToConstant: audioButtonSize), + ]) + } + } + + // MARK: - Link Loading + + private func loadLinkPreview(linkPreviewURL: String) { + loadingView.startAnimating() + OWSLinkPreview.tryToBuildPreviewInfo(previewUrl: linkPreviewURL) .done { [weak self] draft in // TODO: Look at refactoring this behaviour to consolidate attachment mutations @@ -580,8 +544,9 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate { } .catch { [weak self] _ in self?.titleLabel.attributedText = NSMutableAttributedString(string: linkPreviewURL) + .rtlSafeAppend("\n") .rtlSafeAppend( - "\n\("vc_share_link_previews_error".localized())", + "vc_share_link_previews_error".localized(), attributes: [ NSAttributedString.Key.font: UIFont.ows_regularFont( withSize: Values.verySmallFontSize @@ -597,51 +562,6 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate { .retainUntilComplete() } - private func createGenericPreview() { - imageView.image = UIImage(named: "FileLarge") - - self.addSubview(stackView) - - stackView.addArrangedSubview(imageView) - stackView.addArrangedSubview(UIView.vSpacer(5)) - stackView.addArrangedSubview(titleLabel) - stackView.addArrangedSubview(fileSizeLabel) - - imageView.addSubview(fileTypeImageView) - - let imageSize: CGFloat = { - switch mode { - case .large: return 200 - case .attachmentApproval: return 150 - case .small: return 80 - } - }() - - NSLayoutConstraint.activate([ - stackView.centerYAnchor.constraint(equalTo: centerYAnchor), - stackView.widthAnchor.constraint(equalTo: widthAnchor), - stackView.heightAnchor.constraint(lessThanOrEqualTo: heightAnchor), - - imageView.widthAnchor.constraint(equalToConstant: imageSize), - imageView.heightAnchor.constraint(equalToConstant: imageSize), - titleLabel.widthAnchor.constraint(equalTo: stackView.widthAnchor, constant: -(32 * 2)), - fileSizeLabel.widthAnchor.constraint(equalTo: stackView.widthAnchor, constant: -(32 * 2)), - - fileTypeImageView.centerXAnchor.constraint(equalTo: imageView.centerXAnchor), - fileTypeImageView.centerYAnchor.constraint( - equalTo: imageView.centerYAnchor, - constant: 25 - ), - fileTypeImageView.widthAnchor.constraint( - equalTo: fileTypeImageView.heightAnchor, - multiplier: ((fileTypeImageView.image?.size.width ?? 1) / (fileTypeImageView.image?.size.height ?? 1)) - ), - fileTypeImageView.widthAnchor.constraint( - equalTo: imageView.widthAnchor, constant: -75 - ) - ]) - } - // MARK: - Event Handlers @objc func audioPlayPauseButtonPressed(sender: UIButton) { From 2018e94df81010035726c1dd7845ebb52e88ab97 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 13 Jan 2022 14:56:22 +1100 Subject: [PATCH 09/26] Updated audio attachments to allow scrubbing. --- .../AttachmentPrepViewController.swift | 45 ++++++++++++++++++- .../MediaMessageView.swift | 40 ++++++++++++++++- .../OWSVideoPlayer.swift | 6 +-- .../VideoPlayerView.swift | 38 +++++++++++++--- 4 files changed, 117 insertions(+), 12 deletions(-) diff --git a/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentPrepViewController.swift b/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentPrepViewController.swift index 998d7c783..0b352f6af 100644 --- a/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentPrepViewController.swift +++ b/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentPrepViewController.swift @@ -15,7 +15,7 @@ protocol AttachmentPrepViewControllerDelegate: AnyObject { // MARK: - -public class AttachmentPrepViewController: OWSViewController, PlayerProgressBarDelegate, OWSVideoPlayerDelegate { +public class AttachmentPrepViewController: OWSViewController, PlayerProgressBarDelegate, OWSVideoPlayerDelegate, MediaMessageViewAudioDelegate { // We sometimes shrink the attachment view so that it remains somewhat visible // when the keyboard is presented. public enum AttachmentViewScale { @@ -74,6 +74,7 @@ public class AttachmentPrepViewController: OWSViewController, PlayerProgressBarD private lazy var mediaMessageView: MediaMessageView = { let view: MediaMessageView = MediaMessageView(attachment: attachment, mode: .attachmentApproval) view.translatesAutoresizingMaskIntoConstraints = false + view.audioDelegate = self view.isHidden = (imageEditorView != nil) return view @@ -168,12 +169,15 @@ public class AttachmentPrepViewController: OWSViewController, PlayerProgressBarD mediaMessageView.videoPlayButton.isHidden = true mediaMessageView.addSubview(playerView) - // we don't want the progress bar to zoom during "pinch-to-zoom" + // We don't want the progress bar to zoom during "pinch-to-zoom" // but we do want it to shrink with the media content when the user // pops the keyboard. contentContainerView.addSubview(progressBar) contentContainerView.addSubview(playVideoButton) } + else if attachment.isAudio, mediaMessageView.audioPlayer != nil { + contentContainerView.addSubview(progressBar) + } setupLayout() } @@ -268,6 +272,13 @@ public class AttachmentPrepViewController: OWSViewController, PlayerProgressBarD playVideoButton.heightAnchor.constraint(equalToConstant: playButtonSize), ]) } + else if attachment.isAudio, mediaMessageView.audioPlayer != nil { + NSLayoutConstraint.activate([ + progressBar.topAnchor.constraint(equalTo: view.topAnchor), + progressBar.widthAnchor.constraint(equalTo: contentContainerView.widthAnchor), + progressBar.heightAnchor.constraint(equalToConstant: 44) + ]) + } } // MARK: - Navigation Bar @@ -326,6 +337,11 @@ public class AttachmentPrepViewController: OWSViewController, PlayerProgressBarD } public func playerProgressBarDidStartScrubbing(_ playerProgressBar: PlayerProgressBar) { + if attachment.isAudio { + mediaMessageView.pauseAudio() + return + } + guard let videoPlayer = self.videoPlayer else { owsFailDebug("video player was unexpectedly nil") return @@ -335,25 +351,50 @@ public class AttachmentPrepViewController: OWSViewController, PlayerProgressBarD } public func playerProgressBar(_ playerProgressBar: PlayerProgressBar, scrubbedToTime time: CMTime) { + if attachment.isAudio { + mediaMessageView.setAudioTime(currentTime: CMTimeGetSeconds(time)) + progressBar.manuallySetValue(CMTimeGetSeconds(time), durationSeconds: mediaMessageView.audioDurationSeconds) + return + } + guard let videoPlayer = self.videoPlayer else { owsFailDebug("video player was unexpectedly nil") return } videoPlayer.seek(to: time) + progressBar.updateState() } public func playerProgressBar(_ playerProgressBar: PlayerProgressBar, didFinishScrubbingAtTime time: CMTime, shouldResumePlayback: Bool) { + if attachment.isAudio { + mediaMessageView.setAudioTime(currentTime: CMTimeGetSeconds(time)) + progressBar.manuallySetValue(CMTimeGetSeconds(time), durationSeconds: mediaMessageView.audioDurationSeconds) + + if mediaMessageView.wasPlayingAudio { + mediaMessageView.playAudio() + } + return + } + guard let videoPlayer = self.videoPlayer else { owsFailDebug("video player was unexpectedly nil") return } videoPlayer.seek(to: time) + progressBar.updateState() + if (shouldResumePlayback) { videoPlayer.play() } } + + // MARK: - MediaMessageViewAudioDelegate + + public func progressChanged(_ progressSeconds: CGFloat, durationSeconds: CGFloat) { + progressBar.manuallySetValue(progressSeconds, durationSeconds: durationSeconds) + } // MARK: - Helpers diff --git a/SignalUtilitiesKit/Media Viewing & Editing/MediaMessageView.swift b/SignalUtilitiesKit/Media Viewing & Editing/MediaMessageView.swift index d5c9c2d64..50cec8afa 100644 --- a/SignalUtilitiesKit/Media Viewing & Editing/MediaMessageView.swift +++ b/SignalUtilitiesKit/Media Viewing & Editing/MediaMessageView.swift @@ -8,6 +8,10 @@ import YYImage import NVActivityIndicatorView import SessionUIKit +public protocol MediaMessageViewAudioDelegate: AnyObject { + func progressChanged(_ progressSeconds: CGFloat, durationSeconds: CGFloat) +} + public class MediaMessageView: UIView, OWSAudioPlayerDelegate { public enum Mode: UInt { case large @@ -26,8 +30,10 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate { return OWSAudioPlayer(mediaUrl: dataUrl, audioBehavior: .playback, delegate: self) }() + public var wasPlayingAudio: Bool = false public var audioProgressSeconds: CGFloat = 0 public var audioDurationSeconds: CGFloat = 0 + public weak var audioDelegate: MediaMessageViewAudioDelegate? public var playbackState = AudioPlaybackState.stopped { didSet { @@ -354,6 +360,7 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate { imageView.isHidden = false audioPlayPauseButton.isHidden = (audioPlayer == nil) setAudioIconToPlay() + setAudioProgress(0, duration: (audioPlayer?.duration ?? 0)) fileTypeImageView.image = UIImage(named: "table_ic_notification_sound")? .withRenderingMode(.alwaysTemplate) @@ -561,6 +568,32 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate { } .retainUntilComplete() } + + // MARK: - Functions + + public func playAudio() { + audioPlayer?.play() + ensureButtonState() + } + + public func pauseAudio() { + wasPlayingAudio = (audioPlayer?.isPlaying == true) + + // If the 'audioPlayer' has a duration of 0 then we probably haven't played previously which + // will result in the audioPlayer having a 'duration' of 0 breaking the progressBar. We play + // the audio to get it to properly load the file right before pausing it so the data is + // loaded correctly + if audioPlayer?.duration == 0 { + audioPlayer?.play() + } + + audioPlayer?.pause() + ensureButtonState() + } + + public func setAudioTime(currentTime: TimeInterval) { + audioPlayer?.setCurrentTime(currentTime) + } // MARK: - Event Handlers @@ -594,8 +627,13 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate { } public func setAudioProgress(_ progress: CGFloat, duration: CGFloat) { + // Note: When the OWSAudioPlayer stops it sets the duration to 0 (which we want to ignore so + // the UI doesn't look buggy) + let finalDuration: CGFloat = (duration > 0 ? duration : audioDurationSeconds) audioProgressSeconds = progress - audioDurationSeconds = duration + audioDurationSeconds = finalDuration + + audioDelegate?.progressChanged(progress, durationSeconds: finalDuration) } private func setAudioIconToPlay() { diff --git a/SignalUtilitiesKit/Media Viewing & Editing/OWSVideoPlayer.swift b/SignalUtilitiesKit/Media Viewing & Editing/OWSVideoPlayer.swift index 3fa8828e6..581b861cc 100644 --- a/SignalUtilitiesKit/Media Viewing & Editing/OWSVideoPlayer.swift +++ b/SignalUtilitiesKit/Media Viewing & Editing/OWSVideoPlayer.swift @@ -58,7 +58,7 @@ public class OWSVideoPlayer: NSObject { if item.currentTime() == item.duration { // Rewind for repeated plays, but only if it previously played to end. - avPlayer.seek(to: CMTime.zero) + avPlayer.seek(to: CMTime.zero, toleranceBefore: .zero, toleranceAfter: .zero) } avPlayer.play() @@ -67,13 +67,13 @@ public class OWSVideoPlayer: NSObject { @objc public func stop() { avPlayer.pause() - avPlayer.seek(to: CMTime.zero) + avPlayer.seek(to: CMTime.zero, toleranceBefore: .zero, toleranceAfter: .zero) audioSession.endAudioActivity(self.audioActivity) } @objc(seekToTime:) public func seek(to time: CMTime) { - avPlayer.seek(to: time) + avPlayer.seek(to: time, toleranceBefore: .zero, toleranceAfter: .zero) } // MARK: private diff --git a/SignalUtilitiesKit/Media Viewing & Editing/VideoPlayerView.swift b/SignalUtilitiesKit/Media Viewing & Editing/VideoPlayerView.swift index ff4e5637b..f9edd1de7 100644 --- a/SignalUtilitiesKit/Media Viewing & Editing/VideoPlayerView.swift +++ b/SignalUtilitiesKit/Media Viewing & Editing/VideoPlayerView.swift @@ -88,12 +88,16 @@ public class PlayerProgressBar: UIView { let duration: CMTime = item.asset.duration slider.maximumValue = Float(CMTimeGetSeconds(duration)) - // OPTIMIZE We need a high frequency observer for smooth slider updates, - // but could use a much less frequent observer for label updates - progressObserver = player?.addPeriodicTimeObserver(forInterval: CMTime(seconds: 0.01, preferredTimescale: kPreferredTimeScale), queue: nil, using: { [weak self] (_) in - self?.updateState() - }) as AnyObject updateState() + + // OPTIMIZE We need a high frequency observer for smooth slider updates while playing, + // but could use a much less frequent observer for label updates + progressObserver = player?.addPeriodicTimeObserver(forInterval: CMTime(seconds: 0.1, preferredTimescale: kPreferredTimeScale), queue: nil, using: { [weak self] _ in + // If it is playing update the time + if self?.player?.rate != 0 && self?.player?.error == nil { + self?.updateState() + } + }) as AnyObject } } @@ -182,7 +186,7 @@ public class PlayerProgressBar: UIView { // MARK: Render cycle - private func updateState() { + public func updateState() { guard let player = player else { owsFailDebug("player isn't set.") return @@ -219,4 +223,26 @@ public class PlayerProgressBar: UIView { let seconds: Double = Double(slider.value) return CMTime(seconds: seconds, preferredTimescale: kPreferredTimeScale) } + + // MARK: - Functions + + public func manuallySetValue(_ positionSeconds: CGFloat, durationSeconds: CGFloat) { + let remainingSeconds = (durationSeconds - positionSeconds) + + slider.minimumValue = 0 + slider.maximumValue = Float(durationSeconds) + + positionLabel.text = formatter.string(from: positionSeconds) + + guard let remainingString = formatter.string(from: remainingSeconds) else { + owsFailDebug("unable to format time remaining") + remainingLabel.text = "0:00" + return + } + + // show remaining time as negative + remainingLabel.text = "-\(remainingString)" + + slider.setValue(Float(positionSeconds), animated: false) + } } From f02f53fc49b7369d4ca99ad56c6e430dd4ec3c07 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 13 Jan 2022 16:08:23 +1100 Subject: [PATCH 10/26] Added code to show a HTTP LinkPreview error Added code to indicate the app won't load LinkPreviews for HTTP urls. Updated the title & subtitle layout to better handle LinkPreview errors. Missed an Objective C change in the last commit. --- .../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/fi.lproj/Localizable.strings | 1 + .../Translations/fr.lproj/Localizable.strings | 1 + .../Translations/hi.lproj/Localizable.strings | 1 + .../Translations/hr.lproj/Localizable.strings | 1 + .../id-ID.lproj/Localizable.strings | 1 + .../Translations/it.lproj/Localizable.strings | 1 + .../Translations/ja.lproj/Localizable.strings | 1 + .../Translations/nl.lproj/Localizable.strings | 1 + .../Translations/pl.lproj/Localizable.strings | 1 + .../pt_BR.lproj/Localizable.strings | 1 + .../Translations/ru.lproj/Localizable.strings | 1 + .../Translations/si.lproj/Localizable.strings | 1 + .../Translations/sk.lproj/Localizable.strings | 1 + .../Translations/sv.lproj/Localizable.strings | 1 + .../Translations/th.lproj/Localizable.strings | 1 + .../vi-VN.lproj/Localizable.strings | 1 + .../zh-Hant.lproj/Localizable.strings | 1 + .../zh_CN.lproj/Localizable.strings | 1 + .../Utilities/OWSAudioPlayer.h | 1 + .../Utilities/OWSAudioPlayer.m | 5 ++ .../MediaMessageView.swift | 66 ++++++++++++------- 25 files changed, 72 insertions(+), 22 deletions(-) diff --git a/Session/Meta/Translations/de.lproj/Localizable.strings b/Session/Meta/Translations/de.lproj/Localizable.strings index 1f1bc19a2..6491893af 100644 --- a/Session/Meta/Translations/de.lproj/Localizable.strings +++ b/Session/Meta/Translations/de.lproj/Localizable.strings @@ -543,6 +543,7 @@ "vc_share_title" = "Mit Session teilen"; "vc_share_loading_message" = "Anlagen werden vorbereitet..."; "vc_share_sending_message" = "Wird gesendet ..."; +"vc_share_link_previews_unsecure" = "Preview not loaded for unsecure link"; "vc_share_link_previews_error" = "Unable to load preview"; "vc_share_link_previews_disabled_title" = "Link Previews Disabled"; "vc_share_link_previews_disabled_explanation" = "Enabling link previews will show previews for URLs you share. This can be useful, but Session will need to contact linked websites to generate previews.\n\nYou can enable link previews in Session's settings."; diff --git a/Session/Meta/Translations/en.lproj/Localizable.strings b/Session/Meta/Translations/en.lproj/Localizable.strings index 200895618..0777460b8 100644 --- a/Session/Meta/Translations/en.lproj/Localizable.strings +++ b/Session/Meta/Translations/en.lproj/Localizable.strings @@ -543,6 +543,7 @@ "vc_share_title" = "Share to Session"; "vc_share_loading_message" = "Preparing attachments..."; "vc_share_sending_message" = "Sending..."; +"vc_share_link_previews_unsecure" = "Preview not loaded for unsecure link"; "vc_share_link_previews_error" = "Unable to load preview"; "vc_share_link_previews_disabled_title" = "Link Previews Disabled"; "vc_share_link_previews_disabled_explanation" = "Enabling link previews will show previews for URLs you share. This can be useful, but Session will need to contact linked websites to generate previews.\n\nYou can enable link previews in Session's settings."; diff --git a/Session/Meta/Translations/es.lproj/Localizable.strings b/Session/Meta/Translations/es.lproj/Localizable.strings index c5e93b6cd..6667e411a 100644 --- a/Session/Meta/Translations/es.lproj/Localizable.strings +++ b/Session/Meta/Translations/es.lproj/Localizable.strings @@ -543,6 +543,7 @@ "vc_share_title" = "Compartir en Session"; "vc_share_loading_message" = "Preparando archivos adjuntos..."; "vc_share_sending_message" = "Enviando..."; +"vc_share_link_previews_unsecure" = "Preview not loaded for unsecure link"; "vc_share_link_previews_error" = "Unable to load preview"; "vc_share_link_previews_disabled_title" = "Link Previews Disabled"; "vc_share_link_previews_disabled_explanation" = "Enabling link previews will show previews for URLs you share. This can be useful, but Session will need to contact linked websites to generate previews.\n\nYou can enable link previews in Session's settings."; diff --git a/Session/Meta/Translations/fa.lproj/Localizable.strings b/Session/Meta/Translations/fa.lproj/Localizable.strings index ce9f7462f..4043f5f66 100644 --- a/Session/Meta/Translations/fa.lproj/Localizable.strings +++ b/Session/Meta/Translations/fa.lproj/Localizable.strings @@ -543,6 +543,7 @@ "vc_share_title" = "اشتراک گذاری با Session"; "vc_share_loading_message" = "آماده سازی پیوست‌ها..."; "vc_share_sending_message" = "در حال ارسال..."; +"vc_share_link_previews_unsecure" = "Preview not loaded for unsecure link"; "vc_share_link_previews_error" = "Unable to load preview"; "vc_share_link_previews_disabled_title" = "Link Previews Disabled"; "vc_share_link_previews_disabled_explanation" = "Enabling link previews will show previews for URLs you share. This can be useful, but Session will need to contact linked websites to generate previews.\n\nYou can enable link previews in Session's settings."; diff --git a/Session/Meta/Translations/fi.lproj/Localizable.strings b/Session/Meta/Translations/fi.lproj/Localizable.strings index 000500447..286ec23f9 100644 --- a/Session/Meta/Translations/fi.lproj/Localizable.strings +++ b/Session/Meta/Translations/fi.lproj/Localizable.strings @@ -543,6 +543,7 @@ "vc_share_title" = "Jaa Sessioniin"; "vc_share_loading_message" = "Valmistellaan liitteitä..."; "vc_share_sending_message" = "Lähetetään..."; +"vc_share_link_previews_unsecure" = "Preview not loaded for unsecure link"; "vc_share_link_previews_error" = "Unable to load preview"; "vc_share_link_previews_disabled_title" = "Link Previews Disabled"; "vc_share_link_previews_disabled_explanation" = "Enabling link previews will show previews for URLs you share. This can be useful, but Session will need to contact linked websites to generate previews.\n\nYou can enable link previews in Session's settings."; diff --git a/Session/Meta/Translations/fr.lproj/Localizable.strings b/Session/Meta/Translations/fr.lproj/Localizable.strings index 2866a93c1..0e818d2ed 100644 --- a/Session/Meta/Translations/fr.lproj/Localizable.strings +++ b/Session/Meta/Translations/fr.lproj/Localizable.strings @@ -543,6 +543,7 @@ "vc_share_title" = "Partager en Session"; "vc_share_loading_message" = "Préparation des pièces jointes ..."; "vc_share_sending_message" = "Envoi..."; +"vc_share_link_previews_unsecure" = "Preview not loaded for unsecure link"; "vc_share_link_previews_error" = "Unable to load preview"; "vc_share_link_previews_disabled_title" = "Link Previews Disabled"; "vc_share_link_previews_disabled_explanation" = "Enabling link previews will show previews for URLs you share. This can be useful, but Session will need to contact linked websites to generate previews.\n\nYou can enable link previews in Session's settings."; diff --git a/Session/Meta/Translations/hi.lproj/Localizable.strings b/Session/Meta/Translations/hi.lproj/Localizable.strings index bb5831abb..daa874af6 100644 --- a/Session/Meta/Translations/hi.lproj/Localizable.strings +++ b/Session/Meta/Translations/hi.lproj/Localizable.strings @@ -543,6 +543,7 @@ "vc_share_title" = "सत्र में साझा करें"; "vc_share_loading_message" = "अटैचमेंट तैयार किए जा रहे हैं..."; "vc_share_sending_message" = "भेजा जा रहा है..."; +"vc_share_link_previews_unsecure" = "Preview not loaded for unsecure link"; "vc_share_link_previews_error" = "Unable to load preview"; "vc_share_link_previews_disabled_title" = "Link Previews Disabled"; "vc_share_link_previews_disabled_explanation" = "Enabling link previews will show previews for URLs you share. This can be useful, but Session will need to contact linked websites to generate previews.\n\nYou can enable link previews in Session's settings."; diff --git a/Session/Meta/Translations/hr.lproj/Localizable.strings b/Session/Meta/Translations/hr.lproj/Localizable.strings index 1b91a2189..207e01f7a 100644 --- a/Session/Meta/Translations/hr.lproj/Localizable.strings +++ b/Session/Meta/Translations/hr.lproj/Localizable.strings @@ -543,6 +543,7 @@ "vc_share_title" = "Podijeli sa Session-om"; "vc_share_loading_message" = "Priprema privitaka..."; "vc_share_sending_message" = "Slanje..."; +"vc_share_link_previews_unsecure" = "Preview not loaded for unsecure link"; "vc_share_link_previews_error" = "Unable to load preview"; "vc_share_link_previews_disabled_title" = "Link Previews Disabled"; "vc_share_link_previews_disabled_explanation" = "Enabling link previews will show previews for URLs you share. This can be useful, but Session will need to contact linked websites to generate previews.\n\nYou can enable link previews in Session's settings."; diff --git a/Session/Meta/Translations/id-ID.lproj/Localizable.strings b/Session/Meta/Translations/id-ID.lproj/Localizable.strings index 9c29ed382..46a837990 100644 --- a/Session/Meta/Translations/id-ID.lproj/Localizable.strings +++ b/Session/Meta/Translations/id-ID.lproj/Localizable.strings @@ -543,6 +543,7 @@ "vc_share_title" = "Share to Session"; "vc_share_loading_message" = "Preparing attachments..."; "vc_share_sending_message" = "Sending..."; +"vc_share_link_previews_unsecure" = "Preview not loaded for unsecure link"; "vc_share_link_previews_error" = "Unable to load preview"; "vc_share_link_previews_disabled_title" = "Link Previews Disabled"; "vc_share_link_previews_disabled_explanation" = "Enabling link previews will show previews for URLs you share. This can be useful, but Session will need to contact linked websites to generate previews.\n\nYou can enable link previews in Session's settings."; diff --git a/Session/Meta/Translations/it.lproj/Localizable.strings b/Session/Meta/Translations/it.lproj/Localizable.strings index 931f37a66..80ddd24a3 100644 --- a/Session/Meta/Translations/it.lproj/Localizable.strings +++ b/Session/Meta/Translations/it.lproj/Localizable.strings @@ -543,6 +543,7 @@ "vc_share_title" = "Condividi con Session"; "vc_share_loading_message" = "Preparazione allegati..."; "vc_share_sending_message" = "Invio..."; +"vc_share_link_previews_unsecure" = "Preview not loaded for unsecure link"; "vc_share_link_previews_error" = "Unable to load preview"; "vc_share_link_previews_disabled_title" = "Link Previews Disabled"; "vc_share_link_previews_disabled_explanation" = "Enabling link previews will show previews for URLs you share. This can be useful, but Session will need to contact linked websites to generate previews.\n\nYou can enable link previews in Session's settings."; diff --git a/Session/Meta/Translations/ja.lproj/Localizable.strings b/Session/Meta/Translations/ja.lproj/Localizable.strings index 99cf416d1..c5fa17a3d 100644 --- a/Session/Meta/Translations/ja.lproj/Localizable.strings +++ b/Session/Meta/Translations/ja.lproj/Localizable.strings @@ -543,6 +543,7 @@ "vc_share_title" = "Sessionと共有"; "vc_share_loading_message" = "添付ファイルを準備しています..."; "vc_share_sending_message" = "送信中…"; +"vc_share_link_previews_unsecure" = "Preview not loaded for unsecure link"; "vc_share_link_previews_error" = "Unable to load preview"; "vc_share_link_previews_disabled_title" = "Link Previews Disabled"; "vc_share_link_previews_disabled_explanation" = "Enabling link previews will show previews for URLs you share. This can be useful, but Session will need to contact linked websites to generate previews.\n\nYou can enable link previews in Session's settings."; diff --git a/Session/Meta/Translations/nl.lproj/Localizable.strings b/Session/Meta/Translations/nl.lproj/Localizable.strings index 88b53c6e5..596408937 100644 --- a/Session/Meta/Translations/nl.lproj/Localizable.strings +++ b/Session/Meta/Translations/nl.lproj/Localizable.strings @@ -543,6 +543,7 @@ "vc_share_title" = "Delen naar de Session"; "vc_share_loading_message" = "Bijlagen voorbereiden..."; "vc_share_sending_message" = "Aan het verzenden..."; +"vc_share_link_previews_unsecure" = "Preview not loaded for unsecure link"; "vc_share_link_previews_error" = "Unable to load preview"; "vc_share_link_previews_disabled_title" = "Link Previews Disabled"; "vc_share_link_previews_disabled_explanation" = "Enabling link previews will show previews for URLs you share. This can be useful, but Session will need to contact linked websites to generate previews.\n\nYou can enable link previews in Session's settings."; diff --git a/Session/Meta/Translations/pl.lproj/Localizable.strings b/Session/Meta/Translations/pl.lproj/Localizable.strings index 9c8227b01..5c1dd6c87 100644 --- a/Session/Meta/Translations/pl.lproj/Localizable.strings +++ b/Session/Meta/Translations/pl.lproj/Localizable.strings @@ -543,6 +543,7 @@ "vc_share_title" = "Udostępnij w Session"; "vc_share_loading_message" = "Przygotowywanie załączników..."; "vc_share_sending_message" = "Wysyłanie..."; +"vc_share_link_previews_unsecure" = "Preview not loaded for unsecure link"; "vc_share_link_previews_error" = "Unable to load preview"; "vc_share_link_previews_disabled_title" = "Link Previews Disabled"; "vc_share_link_previews_disabled_explanation" = "Enabling link previews will show previews for URLs you share. This can be useful, but Session will need to contact linked websites to generate previews.\n\nYou can enable link previews in Session's settings."; diff --git a/Session/Meta/Translations/pt_BR.lproj/Localizable.strings b/Session/Meta/Translations/pt_BR.lproj/Localizable.strings index e72860d67..1660b1d1f 100644 --- a/Session/Meta/Translations/pt_BR.lproj/Localizable.strings +++ b/Session/Meta/Translations/pt_BR.lproj/Localizable.strings @@ -543,6 +543,7 @@ "vc_share_title" = "Compartilhar no Session"; "vc_share_loading_message" = "Preparando anexos..."; "vc_share_sending_message" = "Enviando..."; +"vc_share_link_previews_unsecure" = "Preview not loaded for unsecure link"; "vc_share_link_previews_error" = "Unable to load preview"; "vc_share_link_previews_disabled_title" = "Link Previews Disabled"; "vc_share_link_previews_disabled_explanation" = "Enabling link previews will show previews for URLs you share. This can be useful, but Session will need to contact linked websites to generate previews.\n\nYou can enable link previews in Session's settings."; diff --git a/Session/Meta/Translations/ru.lproj/Localizable.strings b/Session/Meta/Translations/ru.lproj/Localizable.strings index 9dfdb5e2f..7f1c3b06d 100644 --- a/Session/Meta/Translations/ru.lproj/Localizable.strings +++ b/Session/Meta/Translations/ru.lproj/Localizable.strings @@ -543,6 +543,7 @@ "vc_share_title" = "Поделиться в Session"; "vc_share_loading_message" = "Подготовка вложений..."; "vc_share_sending_message" = "Отправка..."; +"vc_share_link_previews_unsecure" = "Preview not loaded for unsecure link"; "vc_share_link_previews_error" = "Unable to load preview"; "vc_share_link_previews_disabled_title" = "Link Previews Disabled"; "vc_share_link_previews_disabled_explanation" = "Enabling link previews will show previews for URLs you share. This can be useful, but Session will need to contact linked websites to generate previews.\n\nYou can enable link previews in Session's settings."; diff --git a/Session/Meta/Translations/si.lproj/Localizable.strings b/Session/Meta/Translations/si.lproj/Localizable.strings index 3a7997c7b..546914d38 100644 --- a/Session/Meta/Translations/si.lproj/Localizable.strings +++ b/Session/Meta/Translations/si.lproj/Localizable.strings @@ -543,6 +543,7 @@ "vc_share_title" = "Share to Session"; "vc_share_loading_message" = "Preparing attachments..."; "vc_share_sending_message" = "Sending..."; +"vc_share_link_previews_unsecure" = "Preview not loaded for unsecure link"; "vc_share_link_previews_error" = "Unable to load preview"; "vc_share_link_previews_disabled_title" = "Link Previews Disabled"; "vc_share_link_previews_disabled_explanation" = "Enabling link previews will show previews for URLs you share. This can be useful, but Session will need to contact linked websites to generate previews.\n\nYou can enable link previews in Session's settings."; diff --git a/Session/Meta/Translations/sk.lproj/Localizable.strings b/Session/Meta/Translations/sk.lproj/Localizable.strings index f1fcfd831..5bc10ae49 100644 --- a/Session/Meta/Translations/sk.lproj/Localizable.strings +++ b/Session/Meta/Translations/sk.lproj/Localizable.strings @@ -543,6 +543,7 @@ "vc_share_title" = "Share to Session"; "vc_share_loading_message" = "Pripravujú sa prílohy..."; "vc_share_sending_message" = "Odosiela sa..."; +"vc_share_link_previews_unsecure" = "Preview not loaded for unsecure link"; "vc_share_link_previews_error" = "Unable to load preview"; "vc_share_link_previews_disabled_title" = "Link Previews Disabled"; "vc_share_link_previews_disabled_explanation" = "Enabling link previews will show previews for URLs you share. This can be useful, but Session will need to contact linked websites to generate previews.\n\nYou can enable link previews in Session's settings."; diff --git a/Session/Meta/Translations/sv.lproj/Localizable.strings b/Session/Meta/Translations/sv.lproj/Localizable.strings index 78e621dd3..15185fc1f 100644 --- a/Session/Meta/Translations/sv.lproj/Localizable.strings +++ b/Session/Meta/Translations/sv.lproj/Localizable.strings @@ -543,6 +543,7 @@ "vc_share_title" = "Dela i Session"; "vc_share_loading_message" = "Förbereder bilagor..."; "vc_share_sending_message" = "Skickar..."; +"vc_share_link_previews_unsecure" = "Preview not loaded for unsecure link"; "vc_share_link_previews_error" = "Unable to load preview"; "vc_share_link_previews_disabled_title" = "Link Previews Disabled"; "vc_share_link_previews_disabled_explanation" = "Enabling link previews will show previews for URLs you share. This can be useful, but Session will need to contact linked websites to generate previews.\n\nYou can enable link previews in Session's settings."; diff --git a/Session/Meta/Translations/th.lproj/Localizable.strings b/Session/Meta/Translations/th.lproj/Localizable.strings index 8386b8a94..d57365cb9 100644 --- a/Session/Meta/Translations/th.lproj/Localizable.strings +++ b/Session/Meta/Translations/th.lproj/Localizable.strings @@ -543,6 +543,7 @@ "vc_share_title" = "เชิญมาใช้ Session"; "vc_share_loading_message" = "รวบรวมสิ่งแนบ..."; "vc_share_sending_message" = "กำลังส่ง..."; +"vc_share_link_previews_unsecure" = "Preview not loaded for unsecure link"; "vc_share_link_previews_error" = "Unable to load preview"; "vc_share_link_previews_disabled_title" = "Link Previews Disabled"; "vc_share_link_previews_disabled_explanation" = "Enabling link previews will show previews for URLs you share. This can be useful, but Session will need to contact linked websites to generate previews.\n\nYou can enable link previews in Session's settings."; diff --git a/Session/Meta/Translations/vi-VN.lproj/Localizable.strings b/Session/Meta/Translations/vi-VN.lproj/Localizable.strings index ca8781d2a..0d4da6bba 100644 --- a/Session/Meta/Translations/vi-VN.lproj/Localizable.strings +++ b/Session/Meta/Translations/vi-VN.lproj/Localizable.strings @@ -543,6 +543,7 @@ "vc_share_title" = "Share to Session"; "vc_share_loading_message" = "Preparing attachments..."; "vc_share_sending_message" = "Sending..."; +"vc_share_link_previews_unsecure" = "Preview not loaded for unsecure link"; "vc_share_link_previews_error" = "Unable to load preview"; "vc_share_link_previews_disabled_title" = "Link Previews Disabled"; "vc_share_link_previews_disabled_explanation" = "Enabling link previews will show previews for URLs you share. This can be useful, but Session will need to contact linked websites to generate previews.\n\nYou can enable link previews in Session's settings."; diff --git a/Session/Meta/Translations/zh-Hant.lproj/Localizable.strings b/Session/Meta/Translations/zh-Hant.lproj/Localizable.strings index 38518449f..efd1716a2 100644 --- a/Session/Meta/Translations/zh-Hant.lproj/Localizable.strings +++ b/Session/Meta/Translations/zh-Hant.lproj/Localizable.strings @@ -543,6 +543,7 @@ "vc_share_title" = "分享至 Session"; "vc_share_loading_message" = "準備附件中⋯"; "vc_share_sending_message" = "傳送中⋯"; +"vc_share_link_previews_unsecure" = "Preview not loaded for unsecure link"; "vc_share_link_previews_error" = "Unable to load preview"; "vc_share_link_previews_disabled_title" = "Link Previews Disabled"; "vc_share_link_previews_disabled_explanation" = "Enabling link previews will show previews for URLs you share. This can be useful, but Session will need to contact linked websites to generate previews.\n\nYou can enable link previews in Session's settings."; diff --git a/Session/Meta/Translations/zh_CN.lproj/Localizable.strings b/Session/Meta/Translations/zh_CN.lproj/Localizable.strings index 391d1c159..9f734e00d 100644 --- a/Session/Meta/Translations/zh_CN.lproj/Localizable.strings +++ b/Session/Meta/Translations/zh_CN.lproj/Localizable.strings @@ -543,6 +543,7 @@ "vc_share_title" = "分享到 Session"; "vc_share_loading_message" = "正在准备附件......"; "vc_share_sending_message" = "正在发送…"; +"vc_share_link_previews_unsecure" = "Preview not loaded for unsecure link"; "vc_share_link_previews_error" = "Unable to load preview"; "vc_share_link_previews_disabled_title" = "Link Previews Disabled"; "vc_share_link_previews_disabled_explanation" = "Enabling link previews will show previews for URLs you share. This can be useful, but Session will need to contact linked websites to generate previews.\n\nYou can enable link previews in Session's settings."; diff --git a/SessionMessagingKit/Utilities/OWSAudioPlayer.h b/SessionMessagingKit/Utilities/OWSAudioPlayer.h index 4e2c64854..09d207ad1 100644 --- a/SessionMessagingKit/Utilities/OWSAudioPlayer.h +++ b/SessionMessagingKit/Utilities/OWSAudioPlayer.h @@ -43,6 +43,7 @@ typedef NS_ENUM(NSUInteger, OWSAudioBehavior) { @property (nonatomic) BOOL isLooping; @property (nonatomic) BOOL isPlaying; @property (nonatomic) float playbackRate; +@property (nonatomic) NSTimeInterval duration; - (instancetype)initWithMediaUrl:(NSURL *)mediaUrl audioBehavior:(OWSAudioBehavior)audioBehavior; - (instancetype)initWithMediaUrl:(NSURL *)mediaUrl audioBehavior:(OWSAudioBehavior)audioBehavior delegate:(id)delegate; diff --git a/SessionMessagingKit/Utilities/OWSAudioPlayer.m b/SessionMessagingKit/Utilities/OWSAudioPlayer.m index d7b93e7c9..124c2483e 100644 --- a/SessionMessagingKit/Utilities/OWSAudioPlayer.m +++ b/SessionMessagingKit/Utilities/OWSAudioPlayer.m @@ -168,6 +168,11 @@ NS_ASSUME_NONNULL_BEGIN return self.audioPlayer.rate; } +- (NSTimeInterval)duration +{ + return [self.audioPlayer duration]; +} + - (void)setPlaybackRate:(float)rate { [self.audioPlayer setRate:rate]; diff --git a/SignalUtilitiesKit/Media Viewing & Editing/MediaMessageView.swift b/SignalUtilitiesKit/Media Viewing & Editing/MediaMessageView.swift index 50cec8afa..777f4c8ec 100644 --- a/SignalUtilitiesKit/Media Viewing & Editing/MediaMessageView.swift +++ b/SignalUtilitiesKit/Media Viewing & Editing/MediaMessageView.swift @@ -221,6 +221,22 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate { return button }() + private lazy var titleStackView: UIStackView = { + let stackView: UIStackView = UIStackView() + stackView.translatesAutoresizingMaskIntoConstraints = false + stackView.axis = .vertical + stackView.alignment = (attachment.fileType == .url ? .leading : .center) + stackView.distribution = .fill + + switch mode { + case .attachmentApproval: stackView.spacing = 2 + case .large: stackView.spacing = 10 + case .small: stackView.spacing = 5 + } + + return stackView + }() + private lazy var titleLabel: UILabel = { let label: UILabel = UILabel() label.translatesAutoresizingMaskIntoConstraints = false @@ -278,7 +294,7 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate { return label }() - private lazy var fileSizeLabel: UILabel = { + private lazy var subtitleLabel: UILabel = { let label: UILabel = UILabel() label.translatesAutoresizingMaskIntoConstraints = false @@ -303,7 +319,7 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate { case .url: // If we have no link preview info at this point then assume link previews are disabled - if linkPreviewInfo == nil { + guard let linkPreviewURL: String = linkPreviewInfo?.url else { label.text = "vc_share_link_previews_disabled_explanation".localized() label.textColor = Colors.text label.textAlignment = .center @@ -311,6 +327,13 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate { break } + // We only load Link Previews for HTTPS urls so append an explanation for not + if let targetUrl: URL = URL(string: linkPreviewURL), targetUrl.scheme?.lowercased() != "https" { + label.font = UIFont.ows_regularFont(withSize: Values.verySmallFontSize) + label.text = "vc_share_link_previews_unsecure".localized() + label.textColor = (mode == .attachmentApproval ? Colors.pinIcon : Colors.accent) + } + default: // Format string for file size label in call interstitial view. // Embeds: {{file size as 'N mb' or 'N kb'}}. @@ -339,8 +362,10 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate { stackView.addArrangedSubview(imageView) stackView.addArrangedSubview(animatedImageView) if !titleLabel.isHidden { stackView.addArrangedSubview(UIView.vhSpacer(10, 10)) } - stackView.addArrangedSubview(titleLabel) - stackView.addArrangedSubview(fileSizeLabel) + stackView.addArrangedSubview(titleStackView) + + titleStackView.addArrangedSubview(titleLabel) + titleStackView.addArrangedSubview(subtitleLabel) imageView.addSubview(fileTypeImageView) @@ -425,11 +450,7 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate { // If we don't have a valid image then use the 'generic' case break - case .url: - switch mode { - case .large: return 120 - case .attachmentApproval, .small: return 80 - } + case .url: return 80 // Use the 'generic' case for these case .audio, .unknown: break @@ -510,7 +531,7 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate { if (attachment.fileType != .url) { NSLayoutConstraint.activate([ titleLabel.widthAnchor.constraint(equalTo: stackView.widthAnchor, constant: -(32 * 2)), - fileSizeLabel.widthAnchor.constraint(equalTo: stackView.widthAnchor, constant: -(32 * 2)) + subtitleLabel.widthAnchor.constraint(equalTo: stackView.widthAnchor, constant: -(32 * 2)) ]) } @@ -550,21 +571,22 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate { } } .catch { [weak self] _ in - self?.titleLabel.attributedText = NSMutableAttributedString(string: linkPreviewURL) - .rtlSafeAppend("\n") - .rtlSafeAppend( - "vc_share_link_previews_error".localized(), - attributes: [ - NSAttributedString.Key.font: UIFont.ows_regularFont( - withSize: Values.verySmallFontSize - ), - NSAttributedString.Key.foregroundColor: self?.fileSizeLabel.textColor - ] - .compactMapValues { $0 } - ) self?.loadingView.alpha = 0 self?.loadingView.stopAnimating() self?.imageView.alpha = 1 + self?.titleLabel.numberOfLines = 1 // Truncates the URL at 1 line so the error is more readable + self?.subtitleLabel.isHidden = false + + // Set the error text appropriately + if let targetUrl: URL = URL(string: linkPreviewURL), targetUrl.scheme?.lowercased() != "https" { + // This error case is handled already in the 'subtitleLabel' creation + } + else { + self?.subtitleLabel.font = UIFont.ows_regularFont(withSize: Values.verySmallFontSize) + self?.subtitleLabel.text = "vc_share_link_previews_error".localized() + self?.subtitleLabel.textColor = (self?.mode == .attachmentApproval ? Colors.pinIcon : Colors.accent ) + self?.subtitleLabel.textAlignment = .left + } } .retainUntilComplete() } From 3b07be4eed110ed415af20ae31bddf03ea7338a8 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 13 Jan 2022 16:37:23 +1100 Subject: [PATCH 11/26] Fixed a couple of crashes Fixed an issue where sharing form Safari without adding comments would result in an invalid message getting sent. Fixed a crash when sharing plain text from safari. Fixed a crash when localising 'OK' (key and value can't be the same when using the new extension). --- Session/Closed Groups/EditClosedGroupVC.swift | 2 +- Session/Closed Groups/NewClosedGroupVC.swift | 4 ++-- .../Views & Modals/JoinOpenGroupModal.swift | 4 ++-- Session/DMs/NewDMVC.swift | 2 +- .../Meta/Translations/de.lproj/Localizable.strings | 2 +- .../Meta/Translations/en.lproj/Localizable.strings | 2 +- .../Meta/Translations/es.lproj/Localizable.strings | 2 +- .../Meta/Translations/fa.lproj/Localizable.strings | 2 +- .../Meta/Translations/fi.lproj/Localizable.strings | 2 +- .../Meta/Translations/fr.lproj/Localizable.strings | 2 +- .../Meta/Translations/hi.lproj/Localizable.strings | 2 +- .../Meta/Translations/hr.lproj/Localizable.strings | 2 +- .../Translations/id-ID.lproj/Localizable.strings | 2 +- .../Meta/Translations/it.lproj/Localizable.strings | 2 +- .../Meta/Translations/ja.lproj/Localizable.strings | 2 +- .../Meta/Translations/nl.lproj/Localizable.strings | 2 +- .../Meta/Translations/pl.lproj/Localizable.strings | 2 +- .../Translations/pt_BR.lproj/Localizable.strings | 2 +- .../Meta/Translations/ru.lproj/Localizable.strings | 2 +- .../Meta/Translations/si.lproj/Localizable.strings | 2 +- .../Meta/Translations/sk.lproj/Localizable.strings | 2 +- .../Meta/Translations/sv.lproj/Localizable.strings | 2 +- .../Meta/Translations/th.lproj/Localizable.strings | 2 +- .../Translations/vi-VN.lproj/Localizable.strings | 2 +- .../Translations/zh-Hant.lproj/Localizable.strings | 2 +- .../Translations/zh_CN.lproj/Localizable.strings | 2 +- Session/Onboarding/DisplayNameVC.swift | 2 +- Session/Onboarding/LinkDeviceVC.swift | 4 ++-- Session/Onboarding/PNModeVC.swift | 2 +- Session/Onboarding/RestoreVC.swift | 2 +- Session/Open Groups/JoinOpenGroupVC.swift | 2 +- Session/Settings/NukeDataModal.swift | 4 ++-- Session/Settings/QRCodeVC.swift | 2 +- Session/Settings/SettingsVC.swift | 4 ++-- SessionShareExtension/ShareVC.swift | 2 +- SessionShareExtension/ThreadPickerVC.swift | 12 ++++++++++-- .../AttachmentApprovalInputAccessoryView.swift | 1 + .../Media Viewing & Editing/MediaMessageView.swift | 3 +++ SignalUtilitiesKit/Messaging/BlockListUIUtils.m | 2 +- SignalUtilitiesKit/Utilities/OWSAlerts.swift | 4 ++-- 40 files changed, 57 insertions(+), 45 deletions(-) diff --git a/Session/Closed Groups/EditClosedGroupVC.swift b/Session/Closed Groups/EditClosedGroupVC.swift index 0e46f9f48..3a74dea3e 100644 --- a/Session/Closed Groups/EditClosedGroupVC.swift +++ b/Session/Closed Groups/EditClosedGroupVC.swift @@ -306,7 +306,7 @@ final class EditClosedGroupVC : BaseVC, UITableViewDataSource, UITableViewDelega // MARK: Convenience private func showError(title: String, message: String = "") { let alert = UIAlertController(title: title, message: message, preferredStyle: .alert) - alert.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .default, handler: nil)) + alert.addAction(UIAlertAction(title: NSLocalizedString("BUTTON_OK", comment: ""), style: .default, handler: nil)) presentAlert(alert) } } diff --git a/Session/Closed Groups/NewClosedGroupVC.swift b/Session/Closed Groups/NewClosedGroupVC.swift index 0489b556f..47fbbff43 100644 --- a/Session/Closed Groups/NewClosedGroupVC.swift +++ b/Session/Closed Groups/NewClosedGroupVC.swift @@ -151,7 +151,7 @@ final class NewClosedGroupVC : BaseVC, UITableViewDataSource, UITableViewDelegat @objc private func createClosedGroup() { func showError(title: String, message: String = "") { let alert = UIAlertController(title: title, message: message, preferredStyle: .alert) - alert.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .default, handler: nil)) + alert.addAction(UIAlertAction(title: NSLocalizedString("BUTTON_OK", comment: ""), style: .default, handler: nil)) presentAlert(alert) } guard let name = nameTextField.text?.trimmingCharacters(in: .whitespacesAndNewlines), name.count > 0 else { @@ -184,7 +184,7 @@ final class NewClosedGroupVC : BaseVC, UITableViewDataSource, UITableViewDelegat let title = "Couldn't Create Group" let message = "Please check your internet connection and try again." let alert = UIAlertController(title: title, message: message, preferredStyle: .alert) - alert.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .default, handler: nil)) + alert.addAction(UIAlertAction(title: NSLocalizedString("BUTTON_OK", comment: ""), style: .default, handler: nil)) self?.presentAlert(alert) } } diff --git a/Session/Conversations/Views & Modals/JoinOpenGroupModal.swift b/Session/Conversations/Views & Modals/JoinOpenGroupModal.swift index 0e319ae25..d6a47a02b 100644 --- a/Session/Conversations/Views & Modals/JoinOpenGroupModal.swift +++ b/Session/Conversations/Views & Modals/JoinOpenGroupModal.swift @@ -65,7 +65,7 @@ final class JoinOpenGroupModal : Modal { @objc private func joinOpenGroup() { guard let (room, server, publicKey) = OpenGroupManagerV2.parseV2OpenGroup(from: url) else { let alert = UIAlertController(title: "Couldn't Join", message: nil, preferredStyle: .alert) - alert.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .default, handler: nil)) + alert.addAction(UIAlertAction(title: NSLocalizedString("BUTTON_OK", comment: ""), style: .default, handler: nil)) return presentingViewController!.present(alert, animated: true, completion: nil) } presentingViewController!.dismiss(animated: true, completion: nil) @@ -77,7 +77,7 @@ final class JoinOpenGroupModal : Modal { } .catch(on: DispatchQueue.main) { error in let alert = UIAlertController(title: "Couldn't Join", message: error.localizedDescription, preferredStyle: .alert) - alert.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .default, handler: nil)) + alert.addAction(UIAlertAction(title: NSLocalizedString("BUTTON_OK", comment: ""), style: .default, handler: nil)) presentingViewController.present(alert, animated: true, completion: nil) } } diff --git a/Session/DMs/NewDMVC.swift b/Session/DMs/NewDMVC.swift index e0f2eaa40..e98a5b02e 100644 --- a/Session/DMs/NewDMVC.swift +++ b/Session/DMs/NewDMVC.swift @@ -158,7 +158,7 @@ final class NewDMVC : BaseVC, UIPageViewControllerDataSource, UIPageViewControll } let message = messageOrNil ?? "Please check the Session ID or ONS name and try again" let alert = UIAlertController(title: "Error", message: message, preferredStyle: .alert) - alert.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .default, handler: nil)) + alert.addAction(UIAlertAction(title: NSLocalizedString("BUTTON_OK", comment: ""), style: .default, handler: nil)) self?.presentAlert(alert) } } diff --git a/Session/Meta/Translations/de.lproj/Localizable.strings b/Session/Meta/Translations/de.lproj/Localizable.strings index 6491893af..ac1f796fe 100644 --- a/Session/Meta/Translations/de.lproj/Localizable.strings +++ b/Session/Meta/Translations/de.lproj/Localizable.strings @@ -269,7 +269,7 @@ /* No comment provided by engineer. */ "NOTIFICATIONS_SHOW" = "Anzeigen"; /* No comment provided by engineer. */ -"OK" = "Okay"; +"BUTTON_OK" = "Okay"; /* Info Message when {{other user}} disables or doesn't support disappearing messages */ "OTHER_DISABLED_DISAPPEARING_MESSAGES_CONFIGURATION" = "%@ hat verschwindende Nachrichten deaktiviert."; /* Info Message when {{other user}} updates message expiration to {{time amount}}, see the *_TIME_AMOUNT strings for context. */ diff --git a/Session/Meta/Translations/en.lproj/Localizable.strings b/Session/Meta/Translations/en.lproj/Localizable.strings index 0777460b8..cb912599d 100644 --- a/Session/Meta/Translations/en.lproj/Localizable.strings +++ b/Session/Meta/Translations/en.lproj/Localizable.strings @@ -269,7 +269,7 @@ /* No comment provided by engineer. */ "NOTIFICATIONS_SHOW" = "Show"; /* No comment provided by engineer. */ -"OK" = "OK"; +"BUTTON_OK" = "OK"; /* Info Message when {{other user}} disables or doesn't support disappearing messages */ "OTHER_DISABLED_DISAPPEARING_MESSAGES_CONFIGURATION" = "%@ disabled disappearing messages."; /* Info Message when {{other user}} updates message expiration to {{time amount}}, see the *_TIME_AMOUNT strings for context. */ diff --git a/Session/Meta/Translations/es.lproj/Localizable.strings b/Session/Meta/Translations/es.lproj/Localizable.strings index 6667e411a..ff1356af6 100644 --- a/Session/Meta/Translations/es.lproj/Localizable.strings +++ b/Session/Meta/Translations/es.lproj/Localizable.strings @@ -269,7 +269,7 @@ /* No comment provided by engineer. */ "NOTIFICATIONS_SHOW" = "Ver"; /* No comment provided by engineer. */ -"OK" = "OK"; +"BUTTON_OK" = "OK"; /* Info Message when {{other user}} disables or doesn't support disappearing messages */ "OTHER_DISABLED_DISAPPEARING_MESSAGES_CONFIGURATION" = "%@ ha desactivado la desaparición de mensajes."; /* Info Message when {{other user}} updates message expiration to {{time amount}}, see the *_TIME_AMOUNT strings for context. */ diff --git a/Session/Meta/Translations/fa.lproj/Localizable.strings b/Session/Meta/Translations/fa.lproj/Localizable.strings index 4043f5f66..50cac47b5 100644 --- a/Session/Meta/Translations/fa.lproj/Localizable.strings +++ b/Session/Meta/Translations/fa.lproj/Localizable.strings @@ -269,7 +269,7 @@ /* No comment provided by engineer. */ "NOTIFICATIONS_SHOW" = "نمایش"; /* No comment provided by engineer. */ -"OK" = "باشه"; +"BUTTON_OK" = "باشه"; /* Info Message when {{other user}} disables or doesn't support disappearing messages */ "OTHER_DISABLED_DISAPPEARING_MESSAGES_CONFIGURATION" = "%@ پیام‌های محوشونده را غیرفعال کرده است."; /* Info Message when {{other user}} updates message expiration to {{time amount}}, see the *_TIME_AMOUNT strings for context. */ diff --git a/Session/Meta/Translations/fi.lproj/Localizable.strings b/Session/Meta/Translations/fi.lproj/Localizable.strings index 286ec23f9..21457f99b 100644 --- a/Session/Meta/Translations/fi.lproj/Localizable.strings +++ b/Session/Meta/Translations/fi.lproj/Localizable.strings @@ -269,7 +269,7 @@ /* No comment provided by engineer. */ "NOTIFICATIONS_SHOW" = "Näytä"; /* No comment provided by engineer. */ -"OK" = "Ok"; +"BUTTON_OK" = "Ok"; /* Info Message when {{other user}} disables or doesn't support disappearing messages */ "OTHER_DISABLED_DISAPPEARING_MESSAGES_CONFIGURATION" = "%@ poisti katoavat viestit käytöstä."; /* Info Message when {{other user}} updates message expiration to {{time amount}}, see the *_TIME_AMOUNT strings for context. */ diff --git a/Session/Meta/Translations/fr.lproj/Localizable.strings b/Session/Meta/Translations/fr.lproj/Localizable.strings index 0e818d2ed..615fb8779 100644 --- a/Session/Meta/Translations/fr.lproj/Localizable.strings +++ b/Session/Meta/Translations/fr.lproj/Localizable.strings @@ -269,7 +269,7 @@ /* No comment provided by engineer. */ "NOTIFICATIONS_SHOW" = "Afficher"; /* No comment provided by engineer. */ -"OK" = "Valider"; +"BUTTON_OK" = "Valider"; /* Info Message when {{other user}} disables or doesn't support disappearing messages */ "OTHER_DISABLED_DISAPPEARING_MESSAGES_CONFIGURATION" = "%@ a désactivé les messages éphémères."; /* Info Message when {{other user}} updates message expiration to {{time amount}}, see the *_TIME_AMOUNT strings for context. */ diff --git a/Session/Meta/Translations/hi.lproj/Localizable.strings b/Session/Meta/Translations/hi.lproj/Localizable.strings index daa874af6..d53d15728 100644 --- a/Session/Meta/Translations/hi.lproj/Localizable.strings +++ b/Session/Meta/Translations/hi.lproj/Localizable.strings @@ -269,7 +269,7 @@ /* No comment provided by engineer. */ "NOTIFICATIONS_SHOW" = "दिखाएं"; /* No comment provided by engineer. */ -"OK" = "ठीक है"; +"BUTTON_OK" = "ठीक है"; /* Info Message when {{other user}} disables or doesn't support disappearing messages */ "OTHER_DISABLED_DISAPPEARING_MESSAGES_CONFIGURATION" = "%@ ने गायब संदेश अक्षम कर दिए हैं।"; /* Info Message when {{other user}} updates message expiration to {{time amount}}, see the *_TIME_AMOUNT strings for context. */ diff --git a/Session/Meta/Translations/hr.lproj/Localizable.strings b/Session/Meta/Translations/hr.lproj/Localizable.strings index 207e01f7a..befd956ac 100644 --- a/Session/Meta/Translations/hr.lproj/Localizable.strings +++ b/Session/Meta/Translations/hr.lproj/Localizable.strings @@ -269,7 +269,7 @@ /* No comment provided by engineer. */ "NOTIFICATIONS_SHOW" = "Prikaži"; /* No comment provided by engineer. */ -"OK" = "OK"; +"BUTTON_OK" = "OK"; /* Info Message when {{other user}} disables or doesn't support disappearing messages */ "OTHER_DISABLED_DISAPPEARING_MESSAGES_CONFIGURATION" = "%@ je onemogućio nestajuće poruke."; /* Info Message when {{other user}} updates message expiration to {{time amount}}, see the *_TIME_AMOUNT strings for context. */ diff --git a/Session/Meta/Translations/id-ID.lproj/Localizable.strings b/Session/Meta/Translations/id-ID.lproj/Localizable.strings index 46a837990..5cef83fe6 100644 --- a/Session/Meta/Translations/id-ID.lproj/Localizable.strings +++ b/Session/Meta/Translations/id-ID.lproj/Localizable.strings @@ -269,7 +269,7 @@ /* No comment provided by engineer. */ "NOTIFICATIONS_SHOW" = "Tampilkan"; /* No comment provided by engineer. */ -"OK" = "OK"; +"BUTTON_OK" = "OK"; /* Info Message when {{other user}} disables or doesn't support disappearing messages */ "OTHER_DISABLED_DISAPPEARING_MESSAGES_CONFIGURATION" = "%@menonaktifkan pesan tersembunyi"; /* Info Message when {{other user}} updates message expiration to {{time amount}}, see the *_TIME_AMOUNT strings for context. */ diff --git a/Session/Meta/Translations/it.lproj/Localizable.strings b/Session/Meta/Translations/it.lproj/Localizable.strings index 80ddd24a3..b185d7686 100644 --- a/Session/Meta/Translations/it.lproj/Localizable.strings +++ b/Session/Meta/Translations/it.lproj/Localizable.strings @@ -269,7 +269,7 @@ /* No comment provided by engineer. */ "NOTIFICATIONS_SHOW" = "Mostra"; /* No comment provided by engineer. */ -"OK" = "OK,"; +"BUTTON_OK" = "OK,"; /* Info Message when {{other user}} disables or doesn't support disappearing messages */ "OTHER_DISABLED_DISAPPEARING_MESSAGES_CONFIGURATION" = "%@ ha disabilitato la scomparsa dei messaggi."; /* Info Message when {{other user}} updates message expiration to {{time amount}}, see the *_TIME_AMOUNT strings for context. */ diff --git a/Session/Meta/Translations/ja.lproj/Localizable.strings b/Session/Meta/Translations/ja.lproj/Localizable.strings index c5fa17a3d..8667f8fe3 100644 --- a/Session/Meta/Translations/ja.lproj/Localizable.strings +++ b/Session/Meta/Translations/ja.lproj/Localizable.strings @@ -269,7 +269,7 @@ /* No comment provided by engineer. */ "NOTIFICATIONS_SHOW" = "表示内容"; /* No comment provided by engineer. */ -"OK" = "確定"; +"BUTTON_OK" = "確定"; /* Info Message when {{other user}} disables or doesn't support disappearing messages */ "OTHER_DISABLED_DISAPPEARING_MESSAGES_CONFIGURATION" = "%@がメッセージの消失をオフにしました"; /* Info Message when {{other user}} updates message expiration to {{time amount}}, see the *_TIME_AMOUNT strings for context. */ diff --git a/Session/Meta/Translations/nl.lproj/Localizable.strings b/Session/Meta/Translations/nl.lproj/Localizable.strings index 596408937..a90e162bb 100644 --- a/Session/Meta/Translations/nl.lproj/Localizable.strings +++ b/Session/Meta/Translations/nl.lproj/Localizable.strings @@ -269,7 +269,7 @@ /* No comment provided by engineer. */ "NOTIFICATIONS_SHOW" = "Tonen"; /* No comment provided by engineer. */ -"OK" = "OK"; +"BUTTON_OK" = "OK"; /* Info Message when {{other user}} disables or doesn't support disappearing messages */ "OTHER_DISABLED_DISAPPEARING_MESSAGES_CONFIGURATION" = "%@ heeft zelf-wissende berichten uitgeschakeld."; /* Info Message when {{other user}} updates message expiration to {{time amount}}, see the *_TIME_AMOUNT strings for context. */ diff --git a/Session/Meta/Translations/pl.lproj/Localizable.strings b/Session/Meta/Translations/pl.lproj/Localizable.strings index 5c1dd6c87..869fed251 100644 --- a/Session/Meta/Translations/pl.lproj/Localizable.strings +++ b/Session/Meta/Translations/pl.lproj/Localizable.strings @@ -269,7 +269,7 @@ /* No comment provided by engineer. */ "NOTIFICATIONS_SHOW" = "Pokaż"; /* No comment provided by engineer. */ -"OK" = "OK"; +"BUTTON_OK" = "OK"; /* Info Message when {{other user}} disables or doesn't support disappearing messages */ "OTHER_DISABLED_DISAPPEARING_MESSAGES_CONFIGURATION" = "%@ wyłączył(a) znikające wiadomości."; /* Info Message when {{other user}} updates message expiration to {{time amount}}, see the *_TIME_AMOUNT strings for context. */ diff --git a/Session/Meta/Translations/pt_BR.lproj/Localizable.strings b/Session/Meta/Translations/pt_BR.lproj/Localizable.strings index 1660b1d1f..3acb3060f 100644 --- a/Session/Meta/Translations/pt_BR.lproj/Localizable.strings +++ b/Session/Meta/Translations/pt_BR.lproj/Localizable.strings @@ -269,7 +269,7 @@ /* No comment provided by engineer. */ "NOTIFICATIONS_SHOW" = "Exibir"; /* No comment provided by engineer. */ -"OK" = "OK"; +"BUTTON_OK" = "OK"; /* Info Message when {{other user}} disables or doesn't support disappearing messages */ "OTHER_DISABLED_DISAPPEARING_MESSAGES_CONFIGURATION" = "%@ desabilitou mensagens efêmeras."; /* Info Message when {{other user}} updates message expiration to {{time amount}}, see the *_TIME_AMOUNT strings for context. */ diff --git a/Session/Meta/Translations/ru.lproj/Localizable.strings b/Session/Meta/Translations/ru.lproj/Localizable.strings index 7f1c3b06d..ae19b824a 100644 --- a/Session/Meta/Translations/ru.lproj/Localizable.strings +++ b/Session/Meta/Translations/ru.lproj/Localizable.strings @@ -269,7 +269,7 @@ /* No comment provided by engineer. */ "NOTIFICATIONS_SHOW" = "Показывать"; /* No comment provided by engineer. */ -"OK" = "Ок"; +"BUTTON_OK" = "Ок"; /* Info Message when {{other user}} disables or doesn't support disappearing messages */ "OTHER_DISABLED_DISAPPEARING_MESSAGES_CONFIGURATION" = "%@ отключил(а) исчезающие сообщения."; /* Info Message when {{other user}} updates message expiration to {{time amount}}, see the *_TIME_AMOUNT strings for context. */ diff --git a/Session/Meta/Translations/si.lproj/Localizable.strings b/Session/Meta/Translations/si.lproj/Localizable.strings index 546914d38..b2d4849c7 100644 --- a/Session/Meta/Translations/si.lproj/Localizable.strings +++ b/Session/Meta/Translations/si.lproj/Localizable.strings @@ -269,7 +269,7 @@ /* No comment provided by engineer. */ "NOTIFICATIONS_SHOW" = "Show"; /* No comment provided by engineer. */ -"OK" = "OK"; +"BUTTON_OK" = "OK"; /* Info Message when {{other user}} disables or doesn't support disappearing messages */ "OTHER_DISABLED_DISAPPEARING_MESSAGES_CONFIGURATION" = "%@ disabled disappearing messages."; /* Info Message when {{other user}} updates message expiration to {{time amount}}, see the *_TIME_AMOUNT strings for context. */ diff --git a/Session/Meta/Translations/sk.lproj/Localizable.strings b/Session/Meta/Translations/sk.lproj/Localizable.strings index 5bc10ae49..a225890fd 100644 --- a/Session/Meta/Translations/sk.lproj/Localizable.strings +++ b/Session/Meta/Translations/sk.lproj/Localizable.strings @@ -269,7 +269,7 @@ /* No comment provided by engineer. */ "NOTIFICATIONS_SHOW" = "Zobraziť"; /* No comment provided by engineer. */ -"OK" = "OK"; +"BUTTON_OK" = "OK"; /* Info Message when {{other user}} disables or doesn't support disappearing messages */ "OTHER_DISABLED_DISAPPEARING_MESSAGES_CONFIGURATION" = "%@ disabled disappearing messages."; /* Info Message when {{other user}} updates message expiration to {{time amount}}, see the *_TIME_AMOUNT strings for context. */ diff --git a/Session/Meta/Translations/sv.lproj/Localizable.strings b/Session/Meta/Translations/sv.lproj/Localizable.strings index 15185fc1f..e3e3fdb36 100644 --- a/Session/Meta/Translations/sv.lproj/Localizable.strings +++ b/Session/Meta/Translations/sv.lproj/Localizable.strings @@ -269,7 +269,7 @@ /* No comment provided by engineer. */ "NOTIFICATIONS_SHOW" = "Visa"; /* No comment provided by engineer. */ -"OK" = "OK"; +"BUTTON_OK" = "OK"; /* Info Message when {{other user}} disables or doesn't support disappearing messages */ "OTHER_DISABLED_DISAPPEARING_MESSAGES_CONFIGURATION" = "%@ inaktiverade försvinnande meddelanden."; /* Info Message when {{other user}} updates message expiration to {{time amount}}, see the *_TIME_AMOUNT strings for context. */ diff --git a/Session/Meta/Translations/th.lproj/Localizable.strings b/Session/Meta/Translations/th.lproj/Localizable.strings index d57365cb9..a4e943ecd 100644 --- a/Session/Meta/Translations/th.lproj/Localizable.strings +++ b/Session/Meta/Translations/th.lproj/Localizable.strings @@ -269,7 +269,7 @@ /* No comment provided by engineer. */ "NOTIFICATIONS_SHOW" = "แสดง"; /* No comment provided by engineer. */ -"OK" = "ตกลง"; +"BUTTON_OK" = "ตกลง"; /* Info Message when {{other user}} disables or doesn't support disappearing messages */ "OTHER_DISABLED_DISAPPEARING_MESSAGES_CONFIGURATION" = "%@ ได้ปิดใช้งานข้อความที่ลบตัวเองแล้ว"; /* Info Message when {{other user}} updates message expiration to {{time amount}}, see the *_TIME_AMOUNT strings for context. */ diff --git a/Session/Meta/Translations/vi-VN.lproj/Localizable.strings b/Session/Meta/Translations/vi-VN.lproj/Localizable.strings index 0d4da6bba..4c29a785f 100644 --- a/Session/Meta/Translations/vi-VN.lproj/Localizable.strings +++ b/Session/Meta/Translations/vi-VN.lproj/Localizable.strings @@ -269,7 +269,7 @@ /* No comment provided by engineer. */ "NOTIFICATIONS_SHOW" = "Show"; /* No comment provided by engineer. */ -"OK" = "OK"; +"BUTTON_OK" = "OK"; /* Info Message when {{other user}} disables or doesn't support disappearing messages */ "OTHER_DISABLED_DISAPPEARING_MESSAGES_CONFIGURATION" = "%@ disabled disappearing messages."; /* Info Message when {{other user}} updates message expiration to {{time amount}}, see the *_TIME_AMOUNT strings for context. */ diff --git a/Session/Meta/Translations/zh-Hant.lproj/Localizable.strings b/Session/Meta/Translations/zh-Hant.lproj/Localizable.strings index efd1716a2..d4b6922aa 100644 --- a/Session/Meta/Translations/zh-Hant.lproj/Localizable.strings +++ b/Session/Meta/Translations/zh-Hant.lproj/Localizable.strings @@ -269,7 +269,7 @@ /* No comment provided by engineer. */ "NOTIFICATIONS_SHOW" = "顯示"; /* No comment provided by engineer. */ -"OK" = "OK"; +"BUTTON_OK" = "OK"; /* Info Message when {{other user}} disables or doesn't support disappearing messages */ "OTHER_DISABLED_DISAPPEARING_MESSAGES_CONFIGURATION" = "%@ 取消了閱後即焚模式"; /* Info Message when {{other user}} updates message expiration to {{time amount}}, see the *_TIME_AMOUNT strings for context. */ diff --git a/Session/Meta/Translations/zh_CN.lproj/Localizable.strings b/Session/Meta/Translations/zh_CN.lproj/Localizable.strings index 9f734e00d..359a12684 100644 --- a/Session/Meta/Translations/zh_CN.lproj/Localizable.strings +++ b/Session/Meta/Translations/zh_CN.lproj/Localizable.strings @@ -269,7 +269,7 @@ /* No comment provided by engineer. */ "NOTIFICATIONS_SHOW" = "显示"; /* No comment provided by engineer. */ -"OK" = "好"; +"BUTTON_OK" = "好"; /* Info Message when {{other user}} disables or doesn't support disappearing messages */ "OTHER_DISABLED_DISAPPEARING_MESSAGES_CONFIGURATION" = "%@ 取消了阅后即焚。"; /* Info Message when {{other user}} updates message expiration to {{time amount}}, see the *_TIME_AMOUNT strings for context. */ diff --git a/Session/Onboarding/DisplayNameVC.swift b/Session/Onboarding/DisplayNameVC.swift index fd6e0ce28..0fdf23c90 100644 --- a/Session/Onboarding/DisplayNameVC.swift +++ b/Session/Onboarding/DisplayNameVC.swift @@ -124,7 +124,7 @@ final class DisplayNameVC : BaseVC { @objc private func register() { func showError(title: String, message: String = "") { let alert = UIAlertController(title: title, message: message, preferredStyle: .alert) - alert.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .default, handler: nil)) + alert.addAction(UIAlertAction(title: NSLocalizedString("BUTTON_OK", comment: ""), style: .default, handler: nil)) presentAlert(alert) } let displayName = displayNameTextField.text!.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) diff --git a/Session/Onboarding/LinkDeviceVC.swift b/Session/Onboarding/LinkDeviceVC.swift index e346bc325..e2defa126 100644 --- a/Session/Onboarding/LinkDeviceVC.swift +++ b/Session/Onboarding/LinkDeviceVC.swift @@ -124,7 +124,7 @@ final class LinkDeviceVC : BaseVC, UIPageViewControllerDataSource, UIPageViewCon func continueWithSeed(_ seed: Data) { if (seed.count != 16) { let alert = UIAlertController(title: NSLocalizedString("invalid_recovery_phrase", comment: ""), message: NSLocalizedString("Please check the Recovery Phrase and try again.", comment: ""), preferredStyle: .alert) - alert.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .default, handler: { _ in + alert.addAction(UIAlertAction(title: NSLocalizedString("BUTTON_OK", comment: ""), style: .default, handler: { _ in self.scanQRCodeWrapperVC.startCapture() })) presentAlert(alert) @@ -273,7 +273,7 @@ private final class RecoveryPhraseVC : UIViewController { @objc private func handleContinueButtonTapped() { func showError(title: String, message: String = "") { let alert = UIAlertController(title: title, message: message, preferredStyle: .alert) - alert.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .default, handler: nil)) + alert.addAction(UIAlertAction(title: NSLocalizedString("BUTTON_OK", comment: ""), style: .default, handler: nil)) presentAlert(alert) } let mnemonic = mnemonicTextView.text!.lowercased() diff --git a/Session/Onboarding/PNModeVC.swift b/Session/Onboarding/PNModeVC.swift index 474b5c975..56f2c9afd 100644 --- a/Session/Onboarding/PNModeVC.swift +++ b/Session/Onboarding/PNModeVC.swift @@ -90,7 +90,7 @@ final class PNModeVC : BaseVC, OptionViewDelegate { guard selectedOptionView != nil else { let title = NSLocalizedString("vc_pn_mode_no_option_picked_modal_title", comment: "") let alert = UIAlertController(title: title, message: nil, preferredStyle: .alert) - alert.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .default, handler: nil)) + alert.addAction(UIAlertAction(title: NSLocalizedString("BUTTON_OK", comment: ""), style: .default, handler: nil)) return present(alert, animated: true, completion: nil) } UserDefaults.standard[.isUsingFullAPNs] = (selectedOptionView == apnsOptionView) diff --git a/Session/Onboarding/RestoreVC.swift b/Session/Onboarding/RestoreVC.swift index 9e2543c75..ad4dbc4ff 100644 --- a/Session/Onboarding/RestoreVC.swift +++ b/Session/Onboarding/RestoreVC.swift @@ -157,7 +157,7 @@ final class RestoreVC : BaseVC { @objc private func restore() { func showError(title: String, message: String = "") { let alert = UIAlertController(title: title, message: message, preferredStyle: .alert) - alert.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .default, handler: nil)) + alert.addAction(UIAlertAction(title: NSLocalizedString("BUTTON_OK", comment: ""), style: .default, handler: nil)) presentAlert(alert) } let mnemonic = mnemonicTextView.text!.lowercased() diff --git a/Session/Open Groups/JoinOpenGroupVC.swift b/Session/Open Groups/JoinOpenGroupVC.swift index b1587be54..1036394f4 100644 --- a/Session/Open Groups/JoinOpenGroupVC.swift +++ b/Session/Open Groups/JoinOpenGroupVC.swift @@ -161,7 +161,7 @@ final class JoinOpenGroupVC : BaseVC, UIPageViewControllerDataSource, UIPageView // MARK: Convenience private func showError(title: String, message: String = "") { let alert = UIAlertController(title: title, message: message, preferredStyle: .alert) - alert.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .default, handler: nil)) + alert.addAction(UIAlertAction(title: NSLocalizedString("BUTTON_OK", comment: ""), style: .default, handler: nil)) presentAlert(alert) } } diff --git a/Session/Settings/NukeDataModal.swift b/Session/Settings/NukeDataModal.swift index 0246c7ae8..7c86d629c 100644 --- a/Session/Settings/NukeDataModal.swift +++ b/Session/Settings/NukeDataModal.swift @@ -146,13 +146,13 @@ final class NukeDataModal : Modal { message = String(format: NSLocalizedString("dialog_clear_all_data_deletion_failed_2", comment: ""), String(potentiallyMaliciousSnodes.count), potentiallyMaliciousSnodes.joined(separator: ", ")) } let alert = UIAlertController(title: "Error", message: message, preferredStyle: .alert) - alert.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .default, handler: nil)) + alert.addAction(UIAlertAction(title: NSLocalizedString("BUTTON_OK", comment: ""), style: .default, handler: nil)) self?.presentAlert(alert) } }.catch(on: DispatchQueue.main) { error in self?.dismiss(animated: true, completion: nil) // Dismiss the loader let alert = UIAlertController(title: "Error", message: error.localizedDescription, preferredStyle: .alert) - alert.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .default, handler: nil)) + alert.addAction(UIAlertAction(title: NSLocalizedString("BUTTON_OK", comment: ""), style: .default, handler: nil)) self?.presentAlert(alert) } } diff --git a/Session/Settings/QRCodeVC.swift b/Session/Settings/QRCodeVC.swift index 1658a9f73..158b88927 100644 --- a/Session/Settings/QRCodeVC.swift +++ b/Session/Settings/QRCodeVC.swift @@ -123,7 +123,7 @@ final class QRCodeVC : BaseVC, UIPageViewControllerDataSource, UIPageViewControl fileprivate func startNewPrivateChatIfPossible(with hexEncodedPublicKey: String) { if !ECKeyPair.isValidHexEncodedPublicKey(candidate: hexEncodedPublicKey) { let alert = UIAlertController(title: NSLocalizedString("invalid_session_id", comment: ""), message: NSLocalizedString("Please check the Session ID and try again.", comment: ""), preferredStyle: .alert) - alert.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .default, handler: nil)) + alert.addAction(UIAlertAction(title: NSLocalizedString("BUTTON_OK", comment: ""), style: .default, handler: nil)) presentAlert(alert) } else { let thread = TSContactThread.getOrCreateThread(contactSessionID: hexEncodedPublicKey) diff --git a/Session/Settings/SettingsVC.swift b/Session/Settings/SettingsVC.swift index 64046bb71..bb11ac90f 100644 --- a/Session/Settings/SettingsVC.swift +++ b/Session/Settings/SettingsVC.swift @@ -377,7 +377,7 @@ final class SettingsVC : BaseVC, AvatarViewHelperDelegate { let title = isMaxFileSizeExceeded ? "Maximum File Size Exceeded" : "Couldn't Update Profile" let message = isMaxFileSizeExceeded ? "Please select a smaller photo and try again" : "Please check your internet connection and try again" let alert = UIAlertController(title: title, message: message, preferredStyle: .alert) - alert.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .default, handler: nil)) + alert.addAction(UIAlertAction(title: NSLocalizedString("BUTTON_OK", comment: ""), style: .default, handler: nil)) self?.present(alert, animated: true, completion: nil) } } @@ -443,7 +443,7 @@ final class SettingsVC : BaseVC, AvatarViewHelperDelegate { @objc private func handleSaveDisplayNameButtonTapped() { func showError(title: String, message: String = "") { let alert = UIAlertController(title: title, message: message, preferredStyle: .alert) - alert.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .default, handler: nil)) + alert.addAction(UIAlertAction(title: NSLocalizedString("BUTTON_OK", comment: ""), style: .default, handler: nil)) presentAlert(alert) } let displayName = displayNameTextField.text!.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) diff --git a/SessionShareExtension/ShareVC.swift b/SessionShareExtension/ShareVC.swift index 749b2f0c7..1492f8d80 100644 --- a/SessionShareExtension/ShareVC.swift +++ b/SessionShareExtension/ShareVC.swift @@ -231,7 +231,7 @@ final class ShareVC : UINavigationController, ShareViewDelegate, AppModeManagerD func shareViewFailed(error: Error) { let alert = UIAlertController(title: "Session", message: error.localizedDescription, preferredStyle: .alert) - alert.addAction(UIAlertAction(title: "OK".localized(), style: .default, handler: { _ in + alert.addAction(UIAlertAction(title: "BUTTON_OK".localized(), style: .default, handler: { _ in self.extensionContext!.cancelRequest(withError: error) })) present(alert, animated: true, completion: nil) diff --git a/SessionShareExtension/ThreadPickerVC.swift b/SessionShareExtension/ThreadPickerVC.swift index 0eef72d5f..6e4df133a 100644 --- a/SessionShareExtension/ThreadPickerVC.swift +++ b/SessionShareExtension/ThreadPickerVC.swift @@ -155,8 +155,16 @@ final class ThreadPickerVC: UIViewController, UITableViewDataSource, UITableView let message = VisibleMessage() message.sentTimestamp = NSDate.millisecondTimestamp() - message.text = messageText - + message.text = (isSharingUrl && (messageText?.isEmpty == true || attachments[0].linkPreviewDraft == nil) ? + ( + (messageText?.isEmpty == true ? + attachments[0].text() : + "\(attachments[0].text() ?? "")\n\n\(messageText ?? "")" + ) + ) : + messageText + ) + let tsMessage = TSOutgoingMessage.from(message, associatedWith: selectedThread!) Storage.write( with: { transaction in diff --git a/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentApprovalInputAccessoryView.swift b/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentApprovalInputAccessoryView.swift index 4732da2d0..824a9008a 100644 --- a/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentApprovalInputAccessoryView.swift +++ b/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentApprovalInputAccessoryView.swift @@ -191,6 +191,7 @@ extension AttachmentApprovalInputAccessoryView: AttachmentCaptionToolbarDelegate return } + // TODO: Look at refactoring this behaviour to consolidate attachment mutations currentAttachmentItem.attachment.captionText = attachmentCaptionToolbar.textView.text delegate?.attachmentApprovalInputUpdateMediaRail() diff --git a/SignalUtilitiesKit/Media Viewing & Editing/MediaMessageView.swift b/SignalUtilitiesKit/Media Viewing & Editing/MediaMessageView.swift index 777f4c8ec..6743f556b 100644 --- a/SignalUtilitiesKit/Media Viewing & Editing/MediaMessageView.swift +++ b/SignalUtilitiesKit/Media Viewing & Editing/MediaMessageView.swift @@ -415,6 +415,9 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate { } private func setupLayout() { + // Plain text will just be put in the 'message' input so do nothing + guard attachment.fileType != .text && attachment.fileType != .oversizeText else { return } + // Sizing calculations let clampedRatio: CGFloat = { switch attachment.fileType { diff --git a/SignalUtilitiesKit/Messaging/BlockListUIUtils.m b/SignalUtilitiesKit/Messaging/BlockListUIUtils.m index 67bac3464..1c78ee13c 100644 --- a/SignalUtilitiesKit/Messaging/BlockListUIUtils.m +++ b/SignalUtilitiesKit/Messaging/BlockListUIUtils.m @@ -460,7 +460,7 @@ typedef void (^BlockAlertCompletionBlock)(UIAlertAction *action); UIAlertController *alert = [UIAlertController alertControllerWithTitle:title message:message preferredStyle:UIAlertControllerStyleAlert]; - UIAlertAction *okAction = [UIAlertAction actionWithTitle:NSLocalizedString(@"OK", nil) + UIAlertAction *okAction = [UIAlertAction actionWithTitle:NSLocalizedString(@"BUTTON_OK", nil) accessibilityIdentifier:ACCESSIBILITY_IDENTIFIER_WITH_NAME(self, @"ok") style:UIAlertActionStyleDefault handler:completionBlock]; diff --git a/SignalUtilitiesKit/Utilities/OWSAlerts.swift b/SignalUtilitiesKit/Utilities/OWSAlerts.swift index db7974a1f..5db9ecf7d 100644 --- a/SignalUtilitiesKit/Utilities/OWSAlerts.swift +++ b/SignalUtilitiesKit/Utilities/OWSAlerts.swift @@ -57,7 +57,7 @@ import Foundation let alert = UIAlertController(title: title, message: message, preferredStyle: .alert) - let actionTitle = buttonTitle ?? NSLocalizedString("OK", comment: "") + let actionTitle = buttonTitle ?? NSLocalizedString("BUTTON_OK", comment: "") let okAction = UIAlertAction(title: actionTitle, style: .default, handler: buttonAction) okAction.accessibilityIdentifier = "OWSAlerts.\("ok")" alert.addAction(okAction) @@ -71,7 +71,7 @@ import Foundation let alert = UIAlertController(title: title, message: message, preferredStyle: .alert) alert.addAction(self.cancelAction) - let actionTitle = proceedTitle ?? NSLocalizedString("OK", comment: "") + let actionTitle = proceedTitle ?? NSLocalizedString("BUTTON_OK", comment: "") let okAction = UIAlertAction(title: actionTitle, style: .default, handler: proceedAction) okAction.accessibilityIdentifier = "OWSAlerts.\("ok")" alert.addAction(okAction) From a7661da41af6bbbdf175978d220b76a190377b81 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Fri, 14 Jan 2022 12:51:18 +1100 Subject: [PATCH 12/26] Revert "Fixed M1 build issues" This reverts commit ba1a0a2ac67c170ef8a08aff79eb2e8348bba8b3. --- Podfile | 23 +++-- Podfile.lock | 86 +++++++++---------- .../Utilities/ProofOfWork.swift | 2 +- SessionSnodeKit/SnodeAPI.swift | 11 ++- 4 files changed, 59 insertions(+), 63 deletions(-) diff --git a/Podfile b/Podfile index a87bceaf8..5452d4e81 100644 --- a/Podfile +++ b/Podfile @@ -11,7 +11,7 @@ target 'Session' do pod 'PromiseKit', :inhibit_warnings => true pod 'PureLayout', '~> 3.1.8', :inhibit_warnings => true pod 'Reachability', :inhibit_warnings => true - pod 'Sodium', '~> 0.9.1', :inhibit_warnings => true + pod 'Sodium', '~> 0.8.0', :inhibit_warnings => true pod 'YapDatabase/SQLCipher', :git => 'https://github.com/loki-project/session-ios-yap-database.git', branch: 'signal-release', :inhibit_warnings => true pod 'YYImage', git: 'https://github.com/signalapp/YYImage', :inhibit_warnings => true pod 'ZXingObjC', :inhibit_warnings => true @@ -22,18 +22,15 @@ target 'SessionShareExtension' do pod 'CryptoSwift', :inhibit_warnings => true pod 'Curve25519Kit', git: 'https://github.com/signalapp/Curve25519Kit.git', :inhibit_warnings => true pod 'Mantle', git: 'https://github.com/signalapp/Mantle', branch: 'signal-master', :inhibit_warnings => true - pod 'NVActivityIndicatorView', :inhibit_warnings => true pod 'PromiseKit', :inhibit_warnings => true pod 'PureLayout', '~> 3.1.8', :inhibit_warnings => true - pod 'Sodium', '~> 0.9.1', :inhibit_warnings => true - pod 'SignalCoreKit', git: 'https://github.com/mpretty-cyro/SignalCoreKit.git', branch: 'session-version', :inhibit_warnings => true + pod 'SignalCoreKit', git: 'https://github.com/signalapp/SignalCoreKit.git', :inhibit_warnings => true pod 'YapDatabase/SQLCipher', :git => 'https://github.com/loki-project/session-ios-yap-database.git', branch: 'signal-release', :inhibit_warnings => true end target 'SessionNotificationServiceExtension' do pod 'Curve25519Kit', git: 'https://github.com/signalapp/Curve25519Kit.git', :inhibit_warnings => true - pod 'Sodium', '~> 0.9.1', :inhibit_warnings => true - pod 'SignalCoreKit', git: 'https://github.com/mpretty-cyro/SignalCoreKit.git', branch: 'session-version', :inhibit_warnings => true + pod 'SignalCoreKit', git: 'https://github.com/signalapp/SignalCoreKit.git', :inhibit_warnings => true pod 'YapDatabase/SQLCipher', :git => 'https://github.com/loki-project/session-ios-yap-database.git', branch: 'signal-release', :inhibit_warnings => true end @@ -41,15 +38,15 @@ target 'SignalUtilitiesKit' do pod 'AFNetworking', inhibit_warnings: true pod 'CryptoSwift', :inhibit_warnings => true pod 'Curve25519Kit', git: 'https://github.com/signalapp/Curve25519Kit.git', :inhibit_warnings => true + pod 'GRKOpenSSLFramework', :inhibit_warnings => true pod 'HKDFKit', :inhibit_warnings => true pod 'Mantle', git: 'https://github.com/signalapp/Mantle', branch: 'signal-master', :inhibit_warnings => true pod 'NVActivityIndicatorView', :inhibit_warnings => true pod 'PromiseKit', :inhibit_warnings => true pod 'PureLayout', '~> 3.1.8', :inhibit_warnings => true pod 'Reachability', :inhibit_warnings => true - pod 'Sodium', '~> 0.9.1', :inhibit_warnings => true pod 'SAMKeychain', :inhibit_warnings => true - pod 'SignalCoreKit', git: 'https://github.com/mpretty-cyro/SignalCoreKit.git', branch: 'session-version', :inhibit_warnings => true + pod 'SignalCoreKit', git: 'https://github.com/signalapp/SignalCoreKit.git', :inhibit_warnings => true pod 'SwiftProtobuf', '~> 1.5.0', :inhibit_warnings => true pod 'YapDatabase/SQLCipher', :git => 'https://github.com/loki-project/session-ios-yap-database.git', branch: 'signal-release', :inhibit_warnings => true pod 'YYImage', git: 'https://github.com/signalapp/YYImage', :inhibit_warnings => true @@ -69,8 +66,8 @@ target 'SessionMessagingKit' do pod 'PureLayout', '~> 3.1.8', :inhibit_warnings => true pod 'Reachability', :inhibit_warnings => true pod 'SAMKeychain', :inhibit_warnings => true - pod 'SignalCoreKit', git: 'https://github.com/mpretty-cyro/SignalCoreKit.git', branch: 'session-version', :inhibit_warnings => true - pod 'Sodium', '~> 0.9.1', :inhibit_warnings => true + pod 'SignalCoreKit', git: 'https://github.com/signalapp/SignalCoreKit.git', :inhibit_warnings => true + pod 'Sodium', '~> 0.8.0', :inhibit_warnings => true pod 'SwiftProtobuf', '~> 1.5.0', :inhibit_warnings => true pod 'YapDatabase/SQLCipher', :git => 'https://github.com/loki-project/session-ios-yap-database.git', branch: 'signal-release', :inhibit_warnings => true end @@ -79,8 +76,8 @@ target 'SessionSnodeKit' do pod 'CryptoSwift', :inhibit_warnings => true pod 'Curve25519Kit', git: 'https://github.com/signalapp/Curve25519Kit.git', :inhibit_warnings => true pod 'PromiseKit', :inhibit_warnings => true - pod 'SignalCoreKit', git: 'https://github.com/mpretty-cyro/SignalCoreKit.git', branch: 'session-version', :inhibit_warnings => true - pod 'Sodium', '~> 0.9.1', :inhibit_warnings => true + pod 'SignalCoreKit', git: 'https://github.com/signalapp/SignalCoreKit.git', :inhibit_warnings => true + pod 'Sodium', '~> 0.8.0', :inhibit_warnings => true pod 'YapDatabase/SQLCipher', :git => 'https://github.com/loki-project/session-ios-yap-database.git', branch: 'signal-release', :inhibit_warnings => true end @@ -92,7 +89,7 @@ target 'SessionUtilitiesKit' do pod 'PromiseKit', :inhibit_warnings => true pod 'PureLayout', '~> 3.1.8', :inhibit_warnings => true pod 'SAMKeychain', :inhibit_warnings => true - pod 'SignalCoreKit', git: 'https://github.com/mpretty-cyro/SignalCoreKit.git', branch: 'session-version', :inhibit_warnings => true + pod 'SignalCoreKit', git: 'https://github.com/signalapp/SignalCoreKit.git', :inhibit_warnings => true pod 'YapDatabase/SQLCipher', :git => 'https://github.com/loki-project/session-ios-yap-database.git', branch: 'signal-release', :inhibit_warnings => true end diff --git a/Podfile.lock b/Podfile.lock index 0e031ba47..804a148c7 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -14,41 +14,41 @@ PODS: - AFNetworking/Serialization (4.0.1) - AFNetworking/UIKit (4.0.1): - AFNetworking/NSURLSession - - CocoaLumberjack (3.7.4): - - CocoaLumberjack/Core (= 3.7.4) - - CocoaLumberjack/Core (3.7.4) - - CryptoSwift (1.4.2) + - CocoaLumberjack (3.6.2): + - CocoaLumberjack/Core (= 3.6.2) + - CocoaLumberjack/Core (3.6.2) + - CryptoSwift (1.3.2) - Curve25519Kit (2.1.0): - CocoaLumberjack - SignalCoreKit + - GRKOpenSSLFramework (1.0.2.20) - HKDFKit (0.0.3) - Mantle (2.1.0): - Mantle/extobjc (= 2.1.0) - Mantle/extobjc (2.1.0) - - NVActivityIndicatorView (5.1.1): - - NVActivityIndicatorView/Base (= 5.1.1) - - NVActivityIndicatorView/Base (5.1.1) - - OpenSSL-Universal (1.1.1200) - - PromiseKit (6.15.3): - - PromiseKit/CorePromise (= 6.15.3) - - PromiseKit/Foundation (= 6.15.3) - - PromiseKit/UIKit (= 6.15.3) - - PromiseKit/CorePromise (6.15.3) - - PromiseKit/Foundation (6.15.3): + - NVActivityIndicatorView (5.0.1): + - NVActivityIndicatorView/Base (= 5.0.1) + - NVActivityIndicatorView/Base (5.0.1) + - PromiseKit (6.13.1): + - PromiseKit/CorePromise (= 6.13.1) + - PromiseKit/Foundation (= 6.13.1) + - PromiseKit/UIKit (= 6.13.1) + - PromiseKit/CorePromise (6.13.1) + - PromiseKit/Foundation (6.13.1): - PromiseKit/CorePromise - - PromiseKit/UIKit (6.15.3): + - PromiseKit/UIKit (6.13.1): - PromiseKit/CorePromise - - PureLayout (3.1.9) + - PureLayout (3.1.8) - Reachability (3.2) - SAMKeychain (1.5.3) - SignalCoreKit (1.0.0): - CocoaLumberjack - - OpenSSL-Universal - - Sodium (0.9.1) - - SQLCipher (4.5.0): - - SQLCipher/standard (= 4.5.0) - - SQLCipher/common (4.5.0) - - SQLCipher/standard (4.5.0): + - GRKOpenSSLFramework + - Sodium (0.8.0) + - SQLCipher (4.4.0): + - SQLCipher/standard (= 4.4.0) + - SQLCipher/common (4.4.0) + - SQLCipher/standard (4.4.0): - SQLCipher/common - SwiftProtobuf (1.5.0) - YapDatabase/SQLCipher (3.1.1): @@ -124,6 +124,7 @@ DEPENDENCIES: - AFNetworking - CryptoSwift - Curve25519Kit (from `https://github.com/signalapp/Curve25519Kit.git`) + - GRKOpenSSLFramework - HKDFKit - Mantle (from `https://github.com/signalapp/Mantle`, branch `signal-master`) - NVActivityIndicatorView @@ -131,8 +132,8 @@ DEPENDENCIES: - PureLayout (~> 3.1.8) - Reachability - SAMKeychain - - SignalCoreKit (from `https://github.com/mpretty-cyro/SignalCoreKit.git`, branch `session-version`) - - Sodium (~> 0.9.1) + - SignalCoreKit (from `https://github.com/signalapp/SignalCoreKit.git`) + - Sodium (~> 0.8.0) - SwiftProtobuf (~> 1.5.0) - YapDatabase/SQLCipher (from `https://github.com/loki-project/session-ios-yap-database.git`, branch `signal-release`) - YYImage (from `https://github.com/signalapp/YYImage`) @@ -143,9 +144,9 @@ SPEC REPOS: - AFNetworking - CocoaLumberjack - CryptoSwift + - GRKOpenSSLFramework - HKDFKit - NVActivityIndicatorView - - OpenSSL-Universal - PromiseKit - PureLayout - Reachability @@ -162,8 +163,7 @@ EXTERNAL SOURCES: :branch: signal-master :git: https://github.com/signalapp/Mantle SignalCoreKit: - :branch: session-version - :git: https://github.com/mpretty-cyro/SignalCoreKit.git + :git: https://github.com/signalapp/SignalCoreKit.git YapDatabase: :branch: signal-release :git: https://github.com/loki-project/session-ios-yap-database.git @@ -175,39 +175,39 @@ CHECKOUT OPTIONS: :commit: 4fc1c10e98fff2534b5379a9bb587430fdb8e577 :git: https://github.com/signalapp/Curve25519Kit.git Mantle: - :commit: e7e46253bb01ce39525d90aa69ed9e85e758bfc4 + :commit: b72c2d1e6132501db906de2cffa8ded7803c54f4 :git: https://github.com/signalapp/Mantle SignalCoreKit: - :commit: b6ff159ca01679d5d9f206ede8475caeb0dc3225 - :git: https://github.com/mpretty-cyro/SignalCoreKit.git + :commit: 21c092e94b307690957b50f2305e5e65d28fa89e + :git: https://github.com/signalapp/SignalCoreKit.git YapDatabase: :commit: 5806f6b6e0b34124ee09283a9eca9ce7e6eaf14e :git: https://github.com/loki-project/session-ios-yap-database.git YYImage: - :commit: 62a4cede20bcf31da73d18163408e46a92f171c6 + :commit: d91910e6f313a255febbf69795198e74259bd51c :git: https://github.com/signalapp/YYImage SPEC CHECKSUMS: AFNetworking: 7864c38297c79aaca1500c33288e429c3451fdce - CocoaLumberjack: 543c79c114dadc3b1aba95641d8738b06b05b646 - CryptoSwift: a532e74ed010f8c95f611d00b8bbae42e9fe7c17 + CocoaLumberjack: bd155f2dd06c0e0b03f876f7a3ee55693122ec94 + CryptoSwift: 093499be1a94b0cae36e6c26b70870668cb56060 Curve25519Kit: e63f9859ede02438ae3defc5e1a87e09d1ec7ee6 + GRKOpenSSLFramework: dc635b0a9d4cd8af2a9ff80a61e779e21b69dfd8 HKDFKit: c058305d6f64b84f28c50bd7aa89574625bcb62a Mantle: 2fa750afa478cd625a94230fbf1c13462f29395b - NVActivityIndicatorView: 1f6c5687f1171810aa27a3296814dc2d7dec3667 - OpenSSL-Universal: 3b8c0d6268fbd5d3ac44f97338e2fd16a73d9dbf - PromiseKit: 3b2b6995e51a954c46dbc550ce3da44fbfb563c5 - PureLayout: 5fb5e5429519627d60d079ccb1eaa7265ce7cf88 + NVActivityIndicatorView: 738e843cb8924e9e4fc3e559d0728031624bf860 + PromiseKit: 28fda91c973cc377875d8c0ea4f973013c05b6db + PureLayout: a4afb3d79dd958564ce33d22c89f407280d8e6a8 Reachability: 33e18b67625424e47b6cde6d202dce689ad7af96 SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c - SignalCoreKit: 1fbd8732163ef76de16cd1107d1fa3684b607e5d - Sodium: 23d11554ecd556196d313cf6130d406dfe7ac6da - SQLCipher: 98dc22f27c0b1790d39e710d440f22a466ebdb59 + SignalCoreKit: 4562b2bbd9830077439ca003f952a798457d4ea5 + Sodium: 63c0ca312a932e6da481689537d4b35568841bdc + SQLCipher: e434ed542b24f38ea7b36468a13f9765e1b5c072 SwiftProtobuf: 241400280f912735c1e1b9fe675fdd2c6c4d42e2 YapDatabase: b418a4baa6906e8028748938f9159807fd039af4 - YYImage: f1ddd15ac032a58b78bbed1e012b50302d318331 + YYImage: 6db68da66f20d9f169ceb94dfb9947c3867b9665 ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb -PODFILE CHECKSUM: 62496725424703f6390b7c9862c114cee36b26a1 +PODFILE CHECKSUM: 50e6a35c838ba28d2ee02bc6018fdd297c04e55f -COCOAPODS: 1.11.2 +COCOAPODS: 1.10.1 diff --git a/SessionMessagingKit/Utilities/ProofOfWork.swift b/SessionMessagingKit/Utilities/ProofOfWork.swift index 4cb500c97..d82640bce 100644 --- a/SessionMessagingKit/Utilities/ProofOfWork.swift +++ b/SessionMessagingKit/Utilities/ProofOfWork.swift @@ -29,7 +29,7 @@ enum ProofOfWork { value = newValue } // Encode as base 64 - let base64EncodedNonce = nonce.bigEndianBytes.toBase64() + let base64EncodedNonce = nonce.bigEndianBytes.toBase64()! // Return return (timestamp, base64EncodedNonce) } diff --git a/SessionSnodeKit/SnodeAPI.swift b/SessionSnodeKit/SnodeAPI.swift index 954230953..6512ffbfc 100644 --- a/SessionSnodeKit/SnodeAPI.swift +++ b/SessionSnodeKit/SnodeAPI.swift @@ -304,9 +304,8 @@ public final class SnodeAPI : NSObject { let onsName = onsName.lowercased() // Hash the ONS name using BLAKE2b let nameAsData = [UInt8](onsName.data(using: String.Encoding.utf8)!) - guard let nameHash = sodium.genericHash.hash(message: nameAsData) else { return Promise(error: Error.hashingFailed) } - - let base64EncodedNameHash = nameHash.toBase64() + guard let nameHash = sodium.genericHash.hash(message: nameAsData), + let base64EncodedNameHash = nameHash.toBase64() else { return Promise(error: Error.hashingFailed) } // Ask 3 different snodes for the Session ID associated with the given name hash let parameters: [String:Any] = [ "endpoint" : "ons_resolve", @@ -434,7 +433,7 @@ public final class SnodeAPI : NSObject { "lastHash" : lastHash, // "timestamp" : timestamp, // "pubkey_ed25519" : ed25519PublicKey, -// "signature" : signature.toBase64() +// "signature" : signature.toBase64()! ] return invoke(.getMessages, on: snode, associatedWith: publicKey, parameters: parameters) } @@ -474,7 +473,7 @@ public final class SnodeAPI : NSObject { "pubkey" : userX25519PublicKey, "pubkey_ed25519" : userED25519KeyPair.publicKey.toHexString(), "messages": serverHashes, - "signature": signature.toBase64() + "signature": signature.toBase64()! ] return attempt(maxRetryCount: maxRetryCount, recoveringOn: Threading.workQueue) { invoke(.deleteMessage, on: snode, associatedWith: publicKey, parameters: parameters).map2{ rawResponse -> [String:Bool] in @@ -521,7 +520,7 @@ public final class SnodeAPI : NSObject { "pubkey" : userX25519PublicKey, "pubkey_ed25519" : userED25519KeyPair.publicKey.toHexString(), "timestamp" : timestamp, - "signature" : signature.toBase64() + "signature" : signature.toBase64()! ] return attempt(maxRetryCount: maxRetryCount, recoveringOn: Threading.workQueue) { invoke(.clearAllData, on: snode, parameters: parameters).map2 { rawResponse -> [String:Bool] in From 2fedba4ceae5bd1d9d0e9bbd3b74304679313ab1 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Fri, 14 Jan 2022 15:10:22 +1100 Subject: [PATCH 13/26] Addressed PR changes Added NVActivityIndicatorView to the SessionShareExtension. Removed the SignalAttachmentType. --- Podfile | 1 + Podfile.lock | 11 +- .../Attachments/SignalAttachment.swift | 25 -- .../MediaMessageView.swift | 319 +++++++++--------- 4 files changed, 161 insertions(+), 195 deletions(-) diff --git a/Podfile b/Podfile index 5452d4e81..7b71a5767 100644 --- a/Podfile +++ b/Podfile @@ -22,6 +22,7 @@ target 'SessionShareExtension' do pod 'CryptoSwift', :inhibit_warnings => true pod 'Curve25519Kit', git: 'https://github.com/signalapp/Curve25519Kit.git', :inhibit_warnings => true pod 'Mantle', git: 'https://github.com/signalapp/Mantle', branch: 'signal-master', :inhibit_warnings => true + pod 'NVActivityIndicatorView', :inhibit_warnings => true pod 'PromiseKit', :inhibit_warnings => true pod 'PureLayout', '~> 3.1.8', :inhibit_warnings => true pod 'SignalCoreKit', git: 'https://github.com/signalapp/SignalCoreKit.git', :inhibit_warnings => true diff --git a/Podfile.lock b/Podfile.lock index 804a148c7..2e195a17d 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -29,6 +29,7 @@ PODS: - NVActivityIndicatorView (5.0.1): - NVActivityIndicatorView/Base (= 5.0.1) - NVActivityIndicatorView/Base (5.0.1) + - OpenSSL-Universal (1.1.1200) - PromiseKit (6.13.1): - PromiseKit/CorePromise (= 6.13.1) - PromiseKit/Foundation (= 6.13.1) @@ -43,7 +44,7 @@ PODS: - SAMKeychain (1.5.3) - SignalCoreKit (1.0.0): - CocoaLumberjack - - GRKOpenSSLFramework + - OpenSSL-Universal - Sodium (0.8.0) - SQLCipher (4.4.0): - SQLCipher/standard (= 4.4.0) @@ -147,6 +148,7 @@ SPEC REPOS: - GRKOpenSSLFramework - HKDFKit - NVActivityIndicatorView + - OpenSSL-Universal - PromiseKit - PureLayout - Reachability @@ -196,11 +198,12 @@ SPEC CHECKSUMS: HKDFKit: c058305d6f64b84f28c50bd7aa89574625bcb62a Mantle: 2fa750afa478cd625a94230fbf1c13462f29395b NVActivityIndicatorView: 738e843cb8924e9e4fc3e559d0728031624bf860 + OpenSSL-Universal: 3b8c0d6268fbd5d3ac44f97338e2fd16a73d9dbf PromiseKit: 28fda91c973cc377875d8c0ea4f973013c05b6db PureLayout: a4afb3d79dd958564ce33d22c89f407280d8e6a8 Reachability: 33e18b67625424e47b6cde6d202dce689ad7af96 SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c - SignalCoreKit: 4562b2bbd9830077439ca003f952a798457d4ea5 + SignalCoreKit: 1fbd8732163ef76de16cd1107d1fa3684b607e5d Sodium: 63c0ca312a932e6da481689537d4b35568841bdc SQLCipher: e434ed542b24f38ea7b36468a13f9765e1b5c072 SwiftProtobuf: 241400280f912735c1e1b9fe675fdd2c6c4d42e2 @@ -208,6 +211,6 @@ SPEC CHECKSUMS: YYImage: 6db68da66f20d9f169ceb94dfb9947c3867b9665 ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb -PODFILE CHECKSUM: 50e6a35c838ba28d2ee02bc6018fdd297c04e55f +PODFILE CHECKSUM: e7aa4e20c8329a413eadd5e6f0d900340810f4c7 -COCOAPODS: 1.10.1 +COCOAPODS: 1.11.2 diff --git a/SessionMessagingKit/Sending & Receiving/Attachments/SignalAttachment.swift b/SessionMessagingKit/Sending & Receiving/Attachments/SignalAttachment.swift index e6222d59e..f65d8640c 100644 --- a/SessionMessagingKit/Sending & Receiving/Attachments/SignalAttachment.swift +++ b/SessionMessagingKit/Sending & Receiving/Attachments/SignalAttachment.swift @@ -20,18 +20,6 @@ public enum SignalAttachmentError: Error { case couldNotResizeImage } -@objc -public enum SignalAttachmentType: Int { - case text - case oversizeText - case image - case animatedImage - case video - case audio - case url - case unknown -} - extension String { public var filenameWithoutExtension: String { return (self as NSString).deletingPathExtension @@ -446,19 +434,6 @@ public class SignalAttachment: NSObject { private class var mediaUTISet: Set { return audioUTISet.union(videoUTISet).union(animatedImageUTISet).union(inputImageUTISet) } - - @objc - public var fileType: SignalAttachmentType { - if isAnimatedImage { return .animatedImage } - if isImage { return .image } - if isVideo { return .video } - if isAudio { return .audio } - if isUrl { return .url } - if isOversizeText { return .oversizeText } - if isText { return .text } - - return .unknown - } @objc public var isImage: Bool { diff --git a/SignalUtilitiesKit/Media Viewing & Editing/MediaMessageView.swift b/SignalUtilitiesKit/Media Viewing & Editing/MediaMessageView.swift index 6743f556b..2740baac8 100644 --- a/SignalUtilitiesKit/Media Viewing & Editing/MediaMessageView.swift +++ b/SignalUtilitiesKit/Media Viewing & Editing/MediaMessageView.swift @@ -44,37 +44,36 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate { } private lazy var validImage: UIImage? = { - switch attachment.fileType { - case .image: - guard - attachment.isValidImage, - let image: UIImage = attachment.image(), - image.size.width > 0, - image.size.height > 0 - else { - return nil - } - - return image - - case .video: - guard - attachment.isValidVideo, - let image: UIImage = attachment.videoPreview(), - image.size.width > 0, - image.size.height > 0 - else { - return nil - } - - return image + if attachment.isImage { + guard + attachment.isValidImage, + let image: UIImage = attachment.image(), + image.size.width > 0, + image.size.height > 0 + else { + return nil + } - default: return nil + return image } + else if attachment.isVideo { + guard + attachment.isValidVideo, + let image: UIImage = attachment.videoPreview(), + image.size.width > 0, + image.size.height > 0 + else { + return nil + } + + return image + } + + return nil }() private lazy var validAnimatedImage: YYImage? = { guard - attachment.fileType == .animatedImage, + attachment.isAnimatedImage, attachment.isValidImage, let dataUrl: URL = attachment.dataUrl, let image: YYImage = YYImage(contentsOfFile: dataUrl.path), @@ -153,22 +152,19 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate { view.isHidden = true // Override the image to the correct one - switch attachment.fileType { - case .image, .video: - if let validImage: UIImage = validImage { - view.layer.minificationFilter = .trilinear - view.layer.magnificationFilter = .trilinear - view.image = validImage - } - - case .url: - view.clipsToBounds = true - view.image = UIImage(named: "Link")?.withTint(Colors.text) - view.contentMode = .center - view.backgroundColor = (isDarkMode ? .black : UIColor.black.withAlphaComponent(0.06)) - view.layer.cornerRadius = 8 - - default: break + if attachment.isImage || attachment.isVideo { + if let validImage: UIImage = validImage { + view.layer.minificationFilter = .trilinear + view.layer.magnificationFilter = .trilinear + view.image = validImage + } + } + else if attachment.isUrl { + view.clipsToBounds = true + view.image = UIImage(named: "Link")?.withTint(Colors.text) + view.contentMode = .center + view.backgroundColor = (isDarkMode ? .black : UIColor.black.withAlphaComponent(0.06)) + view.layer.cornerRadius = 8 } return view @@ -225,7 +221,7 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate { let stackView: UIStackView = UIStackView() stackView.translatesAutoresizingMaskIntoConstraints = false stackView.axis = .vertical - stackView.alignment = (attachment.fileType == .url ? .leading : .center) + stackView.alignment = (attachment.isUrl ? .leading : .center) stackView.distribution = .fill switch mode { @@ -257,35 +253,33 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate { } // Content - switch attachment.fileType { - case .image, .animatedImage, .video: break // No title for these - - case .url: - // If we have no link preview info at this point then assume link previews are disabled - guard let linkPreviewURL: String = linkPreviewInfo?.url else { - label.text = "vc_share_link_previews_disabled_title".localized() - break - } - + if attachment.isUrl { + // If we have no link preview info at this point then assume link previews are disabled + if let linkPreviewURL: String = linkPreviewInfo?.url { label.font = .boldSystemFont(ofSize: Values.smallFontSize) label.text = linkPreviewURL label.textAlignment = .left label.lineBreakMode = .byTruncatingTail label.numberOfLines = 2 - - default: - if let fileName: String = attachment.sourceFilename?.trimmingCharacters(in: .whitespacesAndNewlines), fileName.count > 0 { - label.text = fileName - } - else if let fileExtension: String = attachment.fileExtension { - label.text = String( - format: "ATTACHMENT_APPROVAL_FILE_EXTENSION_FORMAT".localized(), - fileExtension.uppercased() - ) - } - - label.textAlignment = .center - label.lineBreakMode = .byTruncatingMiddle + } + else { + label.text = "vc_share_link_previews_disabled_title".localized() + } + } + // Title for everything except these types + else if !attachment.isImage && !attachment.isAnimatedImage && !attachment.isVideo { + if let fileName: String = attachment.sourceFilename?.trimmingCharacters(in: .whitespacesAndNewlines), fileName.count > 0 { + label.text = fileName + } + else if let fileExtension: String = attachment.fileExtension { + label.text = String( + format: "ATTACHMENT_APPROVAL_FILE_EXTENSION_FORMAT".localized(), + fileExtension.uppercased() + ) + } + + label.textAlignment = .center + label.lineBreakMode = .byTruncatingMiddle } // Hide the label if it has no content @@ -314,32 +308,30 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate { } // Content - switch attachment.fileType { - case .image, .animatedImage, .video: break // No size for these - - case .url: - // If we have no link preview info at this point then assume link previews are disabled - guard let linkPreviewURL: String = linkPreviewInfo?.url else { - label.text = "vc_share_link_previews_disabled_explanation".localized() - label.textColor = Colors.text - label.textAlignment = .center - label.numberOfLines = 0 - break - } - - // We only load Link Previews for HTTPS urls so append an explanation for not + if attachment.isUrl { + // We only load Link Previews for HTTPS urls so append an explanation for not + if let linkPreviewURL: String = linkPreviewInfo?.url { if let targetUrl: URL = URL(string: linkPreviewURL), targetUrl.scheme?.lowercased() != "https" { label.font = UIFont.ows_regularFont(withSize: Values.verySmallFontSize) label.text = "vc_share_link_previews_unsecure".localized() label.textColor = (mode == .attachmentApproval ? Colors.pinIcon : Colors.accent) } - - default: - // Format string for file size label in call interstitial view. - // Embeds: {{file size as 'N mb' or 'N kb'}}. - let fileSize: UInt = attachment.dataLength - label.text = String(format: "ATTACHMENT_APPROVAL_FILE_SIZE_FORMAT".localized(), OWSFormat.formatFileSize(UInt(fileSize))) + } + // If we have no link preview info at this point then assume link previews are disabled + else { + label.text = "vc_share_link_previews_disabled_explanation".localized() + label.textColor = Colors.text label.textAlignment = .center + label.numberOfLines = 0 + } + } + // Subtitle for everything else except these types + else if !attachment.isImage && !attachment.isAnimatedImage && !attachment.isVideo { + // Format string for file size label in call interstitial view. + // Embeds: {{file size as 'N mb' or 'N kb'}}. + let fileSize: UInt = attachment.dataLength + label.text = String(format: "ATTACHMENT_APPROVAL_FILE_SIZE_FORMAT".localized(), OWSFormat.formatFileSize(UInt(fileSize))) + label.textAlignment = .center } // Hide the label if it has no content @@ -352,7 +344,7 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate { private func setupViews() { // Plain text will just be put in the 'message' input so do nothing - guard attachment.fileType != .text && attachment.fileType != .oversizeText else { return } + guard !attachment.isText && !attachment.isOversizeText else { return } // Setup the view hierarchy addSubview(stackView) @@ -370,95 +362,90 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate { imageView.addSubview(fileTypeImageView) // Type-specific configurations - switch attachment.fileType { - case .animatedImage: animatedImageView.isHidden = false - case .image: imageView.isHidden = false + if attachment.isAnimatedImage { + animatedImageView.isHidden = false + } + else if attachment.isImage { + imageView.isHidden = false + } + else if attachment.isVideo { + // Note: The 'attachmentApproval' mode provides it's own play button to keep + // it at the proper scale when zooming + imageView.isHidden = false + videoPlayButton.isHidden = (mode == .attachmentApproval) + } + else if attachment.isAudio { + // Hide the 'audioPlayPauseButton' if the 'audioPlayer' failed to get created + imageView.isHidden = false + audioPlayPauseButton.isHidden = (audioPlayer == nil) + setAudioIconToPlay() + setAudioProgress(0, duration: (audioPlayer?.duration ?? 0)) + + fileTypeImageView.image = UIImage(named: "table_ic_notification_sound")? + .withRenderingMode(.alwaysTemplate) + fileTypeImageView.tintColor = Colors.text + fileTypeImageView.isHidden = false + + // Note: There is an annoying bug where the MediaMessageView will fill the screen if the + // 'audioPlayPauseButton' is added anywhere within the view hierarchy causing issues with + // the min scale on 'image' and 'animatedImage' file types (assume it's actually any UIButton) + addSubview(audioPlayPauseButton) + } + else if attachment.isUrl { + imageView.isHidden = false + imageView.alpha = 0 // Not 'isHidden' because we want it to take up space in the UIStackView + loadingView.isHidden = false + + if let linkPreviewUrl: String = linkPreviewInfo?.url { + // Don't want to change the axis until we have a URL to start loading, otherwise the + // error message will be broken + stackView.axis = .horizontal - case .video: - // Note: The 'attachmentApproval' mode provides it's own play button to keep - // it at the proper scale when zooming - imageView.isHidden = false - videoPlayButton.isHidden = (mode == .attachmentApproval) - - case .audio: - // Hide the 'audioPlayPauseButton' if the 'audioPlayer' failed to get created - imageView.isHidden = false - audioPlayPauseButton.isHidden = (audioPlayer == nil) - setAudioIconToPlay() - setAudioProgress(0, duration: (audioPlayer?.duration ?? 0)) - - fileTypeImageView.image = UIImage(named: "table_ic_notification_sound")? - .withRenderingMode(.alwaysTemplate) - fileTypeImageView.tintColor = Colors.text - fileTypeImageView.isHidden = false - - // Note: There is an annoying bug where the MediaMessageView will fill the screen if the - // 'audioPlayPauseButton' is added anywhere within the view hierarchy causing issues with - // the min scale on 'image' and 'animatedImage' file types (assume it's actually any UIButton) - addSubview(audioPlayPauseButton) - - case .url: - imageView.isHidden = false - imageView.alpha = 0 // Not 'isHidden' because we want it to take up space in the UIStackView - loadingView.isHidden = false - - if let linkPreviewUrl: String = linkPreviewInfo?.url { - // Don't want to change the axis until we have a URL to start loading, otherwise the - // error message will be broken - stackView.axis = .horizontal - - loadLinkPreview(linkPreviewURL: linkPreviewUrl) - } - - default: imageView.isHidden = false + loadLinkPreview(linkPreviewURL: linkPreviewUrl) + } + } + else { + imageView.isHidden = false } } private func setupLayout() { // Plain text will just be put in the 'message' input so do nothing - guard attachment.fileType != .text && attachment.fileType != .oversizeText else { return } + guard !attachment.isText && !attachment.isOversizeText else { return } // Sizing calculations let clampedRatio: CGFloat = { - switch attachment.fileType { - case .url: return 1 - - case .image, .video, .audio, .unknown: - let imageSize: CGSize = (imageView.image?.size ?? CGSize(width: 1, height: 1)) - let aspectRatio: CGFloat = (imageSize.width / imageSize.height) - - return CGFloatClamp(aspectRatio, 0.05, 95.0) - - case .animatedImage: - let imageSize: CGSize = (animatedImageView.image?.size ?? CGSize(width: 1, height: 1)) - let aspectRatio: CGFloat = (imageSize.width / imageSize.height) - - return CGFloatClamp(aspectRatio, 0.05, 95.0) - - default: return 0 + if attachment.isUrl { + return 1 } + + if attachment.isAnimatedImage { + let imageSize: CGSize = (animatedImageView.image?.size ?? CGSize(width: 1, height: 1)) + let aspectRatio: CGFloat = (imageSize.width / imageSize.height) + + return CGFloatClamp(aspectRatio, 0.05, 95.0) + } + + // All other types should maintain the ratio of the image in the 'imageView' + let imageSize: CGSize = (imageView.image?.size ?? CGSize(width: 1, height: 1)) + let aspectRatio: CGFloat = (imageSize.width / imageSize.height) + + return CGFloatClamp(aspectRatio, 0.05, 95.0) }() let maybeImageSize: CGFloat? = { - switch attachment.fileType { - case .image, .video: - if validImage != nil { return nil } - - // If we don't have a valid image then use the 'generic' case - break - - case .animatedImage: - if validAnimatedImage != nil { return nil } - - // If we don't have a valid image then use the 'generic' case - break - - case .url: return 80 - - // Use the 'generic' case for these - case .audio, .unknown: break - - default: return nil + if attachment.isImage || attachment.isVideo { + if validImage != nil { return nil } + + // If we don't have a valid image then use the 'generic' case + } + else if attachment.isAnimatedImage { + if validAnimatedImage != nil { return nil } + + // If we don't have a valid image then use the 'generic' case + } + else if attachment.isUrl { + return 80 } // Generic file size @@ -531,7 +518,7 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate { ]) // No inset for the text for URLs but there is for all other layouts - if (attachment.fileType != .url) { + if !attachment.isUrl { NSLayoutConstraint.activate([ titleLabel.widthAnchor.constraint(equalTo: stackView.widthAnchor, constant: -(32 * 2)), subtitleLabel.widthAnchor.constraint(equalTo: stackView.widthAnchor, constant: -(32 * 2)) @@ -541,7 +528,7 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate { // Note: There is an annoying bug where the MediaMessageView will fill the screen if the // 'audioPlayPauseButton' is added anywhere within the view hierarchy causing issues with // the min scale on 'image' and 'animatedImage' file types (assume it's actually any UIButton) - if attachment.fileType == .audio { + if attachment.isAudio { NSLayoutConstraint.activate([ audioPlayPauseButton.centerXAnchor.constraint(equalTo: imageView.centerXAnchor), audioPlayPauseButton.centerYAnchor.constraint(equalTo: imageView.centerYAnchor), From bcc5da75b6e7a791d1ed38df5db6ca5695c9565c Mon Sep 17 00:00:00 2001 From: Ryan Zhao Date: Fri, 28 Jan 2022 13:14:02 +1100 Subject: [PATCH 14/26] minor fix on debug mode to prevent crashing --- Session/Conversations/ConversationViewItem.m | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Session/Conversations/ConversationViewItem.m b/Session/Conversations/ConversationViewItem.m index a60beeab4..4ebdd168a 100644 --- a/Session/Conversations/ConversationViewItem.m +++ b/Session/Conversations/ConversationViewItem.m @@ -567,13 +567,13 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType) TSAttachment *_Nullable linkPreviewAttachment = [TSAttachment fetchObjectWithUniqueID:message.linkPreview.imageAttachmentId transaction:transaction]; if (!linkPreviewAttachment) { - OWSFailDebug(@"Could not load link preview image attachment."); + OWSLogDebug(@"Could not load link preview image attachment."); } else if (!linkPreviewAttachment.isImage) { - OWSFailDebug(@"Link preview attachment isn't an image."); + OWSLogDebug(@"Link preview attachment isn't an image."); } else if ([linkPreviewAttachment isKindOfClass:[TSAttachmentStream class]]) { TSAttachmentStream *attachmentStream = (TSAttachmentStream *)linkPreviewAttachment; if (!attachmentStream.isValidImage) { - OWSFailDebug(@"Link preview image attachment isn't valid."); + OWSLogDebug(@"Link preview image attachment isn't valid."); } else { self.linkPreviewAttachment = linkPreviewAttachment; } From d642d8a4ed4639d1e4700c43b1f7acb5b0b93fc5 Mon Sep 17 00:00:00 2001 From: Ryan Zhao Date: Mon, 20 Dec 2021 15:23:31 +1100 Subject: [PATCH 15/26] improve image message ratio --- .../Message Cells/VisibleMessageCell.swift | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/Session/Conversations/Message Cells/VisibleMessageCell.swift b/Session/Conversations/Message Cells/VisibleMessageCell.swift index c564a598e..86279801f 100644 --- a/Session/Conversations/Message Cells/VisibleMessageCell.swift +++ b/Session/Conversations/Message Cells/VisibleMessageCell.swift @@ -612,8 +612,15 @@ final class VisibleMessageCell : MessageCell, LinkPreviewViewDelegate { let maxAspectRatio = 1 / minAspectRatio aspectRatio = aspectRatio.clamp(minAspectRatio, maxAspectRatio) let maxSize = CGSize(width: maxMessageWidth, height: maxMessageWidth) - var width = with(maxSize.height * aspectRatio) { $0 > maxSize.width ? maxSize.width : $0 } - var height = (width > maxSize.width) ? (maxSize.width / aspectRatio) : maxSize.height + var width: CGFloat + var height: CGFloat + if aspectRatio > 1 { + width = maxSize.width + height = width / aspectRatio + } else { + height = maxSize.height + width = height * aspectRatio + } // Don't blow up small images unnecessarily let minSize: CGFloat = 150 let shortSourceDimension = min(size.width, size.height) From 6ae84d3e37c943f9bafff83908c5c7adf0d1001d Mon Sep 17 00:00:00 2001 From: Ryan Zhao Date: Fri, 28 Jan 2022 13:45:27 +1100 Subject: [PATCH 16/26] update outgoing message text colour in light mode --- Podfile.lock | 8 ++++---- .../Message Cells/VisibleMessageCell.swift | 1 + SessionUIKit/Style Guide/Colors.swift | 1 + .../session_grey.colorset/Contents.json | 20 +++++++++++++++++++ 4 files changed, 26 insertions(+), 4 deletions(-) create mode 100644 SessionUIKit/Style Guide/Colors.xcassets/session_grey.colorset/Contents.json diff --git a/Podfile.lock b/Podfile.lock index 24d6abeed..d8af51feb 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -132,7 +132,7 @@ DEPENDENCIES: - SignalCoreKit (from `https://github.com/oxen-io/session-ios-core-kit`, branch `session-version`) - Sodium (~> 0.9.1) - SwiftProtobuf (~> 1.5.0) - - YapDatabase/SQLCipher (from `https://github.com/loki-project/session-ios-yap-database.git`, branch `signal-release`) + - YapDatabase/SQLCipher (from `https://github.com/oxen-io/session-ios-yap-database.git`, branch `signal-release`) - YYImage (from `https://github.com/signalapp/YYImage`) - ZXingObjC @@ -163,7 +163,7 @@ EXTERNAL SOURCES: :git: https://github.com/oxen-io/session-ios-core-kit YapDatabase: :branch: signal-release - :git: https://github.com/loki-project/session-ios-yap-database.git + :git: https://github.com/oxen-io/session-ios-yap-database.git YYImage: :git: https://github.com/signalapp/YYImage @@ -179,7 +179,7 @@ CHECKOUT OPTIONS: :git: https://github.com/oxen-io/session-ios-core-kit YapDatabase: :commit: d84069e25e12a16ab4422e5258127a04b70489ad - :git: https://github.com/loki-project/session-ios-yap-database.git + :git: https://github.com/oxen-io/session-ios-yap-database.git YYImage: :commit: 62a4cede20bcf31da73d18163408e46a92f171c6 :git: https://github.com/signalapp/YYImage @@ -204,6 +204,6 @@ SPEC CHECKSUMS: YYImage: f1ddd15ac032a58b78bbed1e012b50302d318331 ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb -PODFILE CHECKSUM: 7722d8a03a0ebc6ecd0070d02b199bd748992f12 +PODFILE CHECKSUM: 7f961dc4934dd213f5a3277af57d54caef7a4442 COCOAPODS: 1.11.2 diff --git a/Session/Conversations/Message Cells/VisibleMessageCell.swift b/Session/Conversations/Message Cells/VisibleMessageCell.swift index 86279801f..ad3b33ea0 100644 --- a/Session/Conversations/Message Cells/VisibleMessageCell.swift +++ b/Session/Conversations/Message Cells/VisibleMessageCell.swift @@ -131,6 +131,7 @@ final class VisibleMessageCell : MessageCell, LinkPreviewViewDelegate { private var bodyLabelTextColor: UIColor { switch (direction, AppModeManager.shared.currentAppMode) { case (.outgoing, .dark), (.incoming, .light): return .black + case (.outgoing, .light): return Colors.grey default: return .white } } diff --git a/SessionUIKit/Style Guide/Colors.swift b/SessionUIKit/Style Guide/Colors.swift index 1a150ebc9..e2d45fb7f 100644 --- a/SessionUIKit/Style Guide/Colors.swift +++ b/SessionUIKit/Style Guide/Colors.swift @@ -13,6 +13,7 @@ import UIKit @objc(LKColors) public final class Colors : NSObject { + @objc public static var grey: UIColor { UIColor(named: "session_grey")! } @objc public static var accent: UIColor { UIColor(named: "session_accent")! } @objc public static var text: UIColor { UIColor(named: "session_text")! } @objc public static var destructive: UIColor { UIColor(named: "session_destructive")! } diff --git a/SessionUIKit/Style Guide/Colors.xcassets/session_grey.colorset/Contents.json b/SessionUIKit/Style Guide/Colors.xcassets/session_grey.colorset/Contents.json new file mode 100644 index 000000000..f347d967f --- /dev/null +++ b/SessionUIKit/Style Guide/Colors.xcassets/session_grey.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x30", + "green" : "0x2F", + "red" : "0x31" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} From d96d7468a420de3f08ca31bce11d16e50bf509e0 Mon Sep 17 00:00:00 2001 From: Ryan Zhao Date: Fri, 28 Jan 2022 14:04:41 +1100 Subject: [PATCH 17/26] fix read more button colour --- .../Message Cells/Content Views/MediaTextOverlayView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Session/Conversations/Message Cells/Content Views/MediaTextOverlayView.swift b/Session/Conversations/Message Cells/Content Views/MediaTextOverlayView.swift index 9be711311..a0c655a5f 100644 --- a/Session/Conversations/Message Cells/Content Views/MediaTextOverlayView.swift +++ b/Session/Conversations/Message Cells/Content Views/MediaTextOverlayView.swift @@ -56,7 +56,7 @@ final class MediaTextOverlayView : UIView { self.readMoreButton = readMoreButton readMoreButton.setTitle("Read More", for: UIControl.State.normal) readMoreButton.titleLabel!.font = .boldSystemFont(ofSize: Values.smallFontSize) - readMoreButton.setTitleColor(.white, for: UIControl.State.normal) + readMoreButton.setTitleColor(self.textColor, for: UIControl.State.normal) readMoreButton.addTarget(self, action: #selector(readMore), for: UIControl.Event.touchUpInside) addSubview(readMoreButton) readMoreButton.pin(.left, to: .left, of: self, withInset: inset) From 8ed77435d880dc7925e963b5b44ef38f4ec678a7 Mon Sep 17 00:00:00 2001 From: Ryan Zhao Date: Thu, 6 Jan 2022 10:12:54 +1100 Subject: [PATCH 18/26] update translation --- Session/Meta/Translations/en.lproj/Localizable.strings | 2 ++ .../DataExtractionNotificationInfoMessage.swift | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Session/Meta/Translations/en.lproj/Localizable.strings b/Session/Meta/Translations/en.lproj/Localizable.strings index a58f90996..0f7885322 100644 --- a/Session/Meta/Translations/en.lproj/Localizable.strings +++ b/Session/Meta/Translations/en.lproj/Localizable.strings @@ -579,3 +579,5 @@ "light_mode_theme" = "Light"; "PIN_BUTTON_TEXT" = "Pin"; "UNPIN_BUTTON_TEXT" = "Unpin"; +"meida_saved" = "Media saved by %@."; +"screenshot_taken" = "%@ took a screenshot."; diff --git a/SessionMessagingKit/Sending & Receiving/Data Extraction/DataExtractionNotificationInfoMessage.swift b/SessionMessagingKit/Sending & Receiving/Data Extraction/DataExtractionNotificationInfoMessage.swift index df2477e25..18ceef7f5 100644 --- a/SessionMessagingKit/Sending & Receiving/Data Extraction/DataExtractionNotificationInfoMessage.swift +++ b/SessionMessagingKit/Sending & Receiving/Data Extraction/DataExtractionNotificationInfoMessage.swift @@ -19,10 +19,10 @@ final class DataExtractionNotificationInfoMessage : TSInfoMessage { let sessionID = thread.contactSessionID() let displayName = Storage.shared.getContact(with: sessionID)?.displayName(for: .regular) ?? sessionID switch messageType { - case .screenshotNotification: return "\(displayName) took a screenshot." + case .screenshotNotification: return String(format: NSLocalizedString("screenshot_taken", comment: ""), displayName) case .mediaSavedNotification: // TODO: Use referencedAttachmentTimestamp to tell the user * which * media was saved - return "Media saved by \(displayName)." + return String(format: NSLocalizedString("meida_saved", comment: ""), displayName) default: preconditionFailure() } } From 70a562344c508f9c966023e40d4913248241df95 Mon Sep 17 00:00:00 2001 From: Ryan Zhao Date: Thu, 6 Jan 2022 10:08:59 +1100 Subject: [PATCH 19/26] minor issue fix --- .../Conversations/Message Cells/VisibleMessageCell.swift | 7 +++---- Session/Open Groups/JoinOpenGroupVC.swift | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/Session/Conversations/Message Cells/VisibleMessageCell.swift b/Session/Conversations/Message Cells/VisibleMessageCell.swift index ad3b33ea0..f54ea79c9 100644 --- a/Session/Conversations/Message Cells/VisibleMessageCell.swift +++ b/Session/Conversations/Message Cells/VisibleMessageCell.swift @@ -208,8 +208,7 @@ final class VisibleMessageCell : MessageCell, LinkPreviewViewDelegate { // MARK: Updating override func update() { guard let viewItem = viewItem, let message = viewItem.interaction as? TSMessage else { return } - let thread = message.thread - let isGroupThread = thread.isGroupThread() + let isGroupThread = viewItem.isGroupThread // Profile picture view profilePictureViewLeftConstraint.constant = isGroupThread ? VisibleMessageCell.groupThreadHSpacing : 0 profilePictureViewWidthConstraint.constant = isGroupThread ? VisibleMessageCell.profilePictureSize : 0 @@ -218,8 +217,8 @@ final class VisibleMessageCell : MessageCell, LinkPreviewViewDelegate { if let senderSessionID = senderSessionID { profilePictureView.update(for: senderSessionID) } - if let thread = thread as? TSGroupThread, thread.isOpenGroup, let senderSessionID = senderSessionID { - if let openGroupV2 = Storage.shared.getV2OpenGroup(for: thread.uniqueId!) { + if let senderSessionID = senderSessionID, message.isOpenGroupMessage { + if let openGroupV2 = Storage.shared.getV2OpenGroup(for: message.uniqueThreadId) { let isUserModerator = OpenGroupAPIV2.isUserModerator(senderSessionID, for: openGroupV2.room, on: openGroupV2.server) moderatorIconImageView.isHidden = !isUserModerator || profilePictureView.isHidden } else { diff --git a/Session/Open Groups/JoinOpenGroupVC.swift b/Session/Open Groups/JoinOpenGroupVC.swift index b1587be54..debad90e2 100644 --- a/Session/Open Groups/JoinOpenGroupVC.swift +++ b/Session/Open Groups/JoinOpenGroupVC.swift @@ -143,7 +143,7 @@ final class JoinOpenGroupVC : BaseVC, UIPageViewControllerDataSource, UIPageView Storage.shared.write { transaction in OpenGroupManagerV2.shared.add(room: room, server: server, publicKey: publicKey, using: transaction) .done(on: DispatchQueue.main) { [weak self] _ in - self?.presentingViewController!.dismiss(animated: true, completion: nil) + self?.presentingViewController?.dismiss(animated: true, completion: nil) let appDelegate = UIApplication.shared.delegate as! AppDelegate appDelegate.forceSyncConfigurationNowIfNeeded().retainUntilComplete() // FIXME: It's probably cleaner to do this inside addOpenGroup(...) } From 2c7dfe6a37ea7cf200fa70c13fd74c154166c0a0 Mon Sep 17 00:00:00 2001 From: Ryan Zhao Date: Mon, 20 Dec 2021 12:24:42 +1100 Subject: [PATCH 20/26] fix conversation screen scrolling to bottom issue --- Session/Conversations/ConversationVC.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Session/Conversations/ConversationVC.swift b/Session/Conversations/ConversationVC.swift index 0a58c831c..1cafba6dd 100644 --- a/Session/Conversations/ConversationVC.swift +++ b/Session/Conversations/ConversationVC.swift @@ -460,6 +460,10 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, OWSConversat func scrollToBottom(isAnimated: Bool) { guard !isUserScrolling else { return } + if let interactionID = viewItems.last?.interaction.uniqueId { + self.scrollToInteraction(with: interactionID, position: .top, isAnimated: isAnimated) + return + } // Ensure the view is fully up to date before we try to scroll to the bottom, since // we use the table view's bounds to determine where the bottom is. view.layoutIfNeeded() From 99022006da52e49e5081a89d3ac34f23457d6efc Mon Sep 17 00:00:00 2001 From: Ryan Zhao Date: Fri, 28 Jan 2022 14:21:54 +1100 Subject: [PATCH 21/26] Make the unread message count 4 digits instead of 2 --- Session/Conversations/ConversationVC.swift | 4 ++-- Session/Shared/ConversationCell.swift | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Session/Conversations/ConversationVC.swift b/Session/Conversations/ConversationVC.swift index 1cafba6dd..85063436c 100644 --- a/Session/Conversations/ConversationVC.swift +++ b/Session/Conversations/ConversationVC.swift @@ -494,8 +494,8 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, OWSConversat unreadViewItems.remove(at: index) } let unreadCount = unreadViewItems.count - unreadCountLabel.text = unreadCount < 100 ? "\(unreadCount)" : "99+" - let fontSize = (unreadCount < 100) ? Values.verySmallFontSize : 8 + unreadCountLabel.text = unreadCount < 10000 ? "\(unreadCount)" : "9999+" + let fontSize = (unreadCount < 10000) ? Values.verySmallFontSize : 8 unreadCountLabel.font = .boldSystemFont(ofSize: fontSize) unreadCountView.isHidden = (unreadCount == 0) } diff --git a/Session/Shared/ConversationCell.swift b/Session/Shared/ConversationCell.swift index 2a7edf38a..5f8a64c82 100644 --- a/Session/Shared/ConversationCell.swift +++ b/Session/Shared/ConversationCell.swift @@ -210,8 +210,8 @@ final class ConversationCell : UITableViewCell { isPinnedIcon.isHidden = !threadViewModel.isPinned unreadCountView.isHidden = !threadViewModel.hasUnreadMessages let unreadCount = threadViewModel.unreadCount - unreadCountLabel.text = unreadCount < 100 ? "\(unreadCount)" : "99+" - let fontSize = (unreadCount < 100) ? Values.verySmallFontSize : 8 + unreadCountLabel.text = unreadCount < 10000 ? "\(unreadCount)" : "9999+" + let fontSize = (unreadCount < 10000) ? Values.verySmallFontSize : 8 unreadCountLabel.font = .boldSystemFont(ofSize: fontSize) hasMentionView.isHidden = !(threadViewModel.hasUnreadMentions && thread.isGroupThread()) profilePictureView.update(for: thread) From c81e0e517d916ced49dd717455b1e597e703766e Mon Sep 17 00:00:00 2001 From: Ryan Zhao Date: Fri, 28 Jan 2022 15:03:19 +1100 Subject: [PATCH 22/26] fix ui issue after making unread message count 4 digits --- Session/Conversations/ConversationVC.swift | 7 +++++-- Session/Shared/ConversationCell.swift | 6 ++++-- SessionUIKit/Utilities/UIView+Constraints.swift | 13 +++++++++++++ 3 files changed, 22 insertions(+), 4 deletions(-) diff --git a/Session/Conversations/ConversationVC.swift b/Session/Conversations/ConversationVC.swift index 85063436c..c409b6112 100644 --- a/Session/Conversations/ConversationVC.swift +++ b/Session/Conversations/ConversationVC.swift @@ -112,7 +112,7 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, OWSConversat let result = UIView() result.backgroundColor = Colors.text.withAlphaComponent(Values.veryLowOpacity) let size = ConversationVC.unreadCountViewSize - result.set(.width, to: size) + result.set(.width, greaterThanOrEqualTo: size) result.set(.height, to: size) result.layer.masksToBounds = true result.layer.cornerRadius = size / 2 @@ -192,7 +192,10 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, OWSConversat // Unread count view view.addSubview(unreadCountView) unreadCountView.addSubview(unreadCountLabel) - unreadCountLabel.pin(to: unreadCountView) + unreadCountLabel.pin(.top, to: .top, of: unreadCountView) + unreadCountLabel.pin(.bottom, to: .bottom, of: unreadCountView) + unreadCountView.pin(.leading, to: .leading, of: unreadCountLabel, withInset: -4) + unreadCountView.pin(.trailing, to: .trailing, of: unreadCountLabel, withInset: 4) unreadCountView.centerYAnchor.constraint(equalTo: scrollButton.topAnchor).isActive = true unreadCountView.center(.horizontal, in: scrollButton) updateUnreadCountView() diff --git a/Session/Shared/ConversationCell.swift b/Session/Shared/ConversationCell.swift index 5f8a64c82..073bd8c28 100644 --- a/Session/Shared/ConversationCell.swift +++ b/Session/Shared/ConversationCell.swift @@ -23,7 +23,7 @@ final class ConversationCell : UITableViewCell { let result = UIView() result.backgroundColor = Colors.text.withAlphaComponent(Values.veryLowOpacity) let size = ConversationCell.unreadCountViewSize - result.set(.width, to: size) + result.set(.width, greaterThanOrEqualTo: size) result.set(.height, to: size) result.layer.masksToBounds = true result.layer.cornerRadius = size / 2 @@ -129,7 +129,9 @@ final class ConversationCell : UITableViewCell { profilePictureView.size = profilePictureViewSize // Unread count view unreadCountView.addSubview(unreadCountLabel) - unreadCountLabel.pin(to: unreadCountView) + unreadCountLabel.pin([ VerticalEdge.top, VerticalEdge.bottom ], to: unreadCountView) + unreadCountView.pin(.leading, to: .leading, of: unreadCountLabel, withInset: -4) + unreadCountView.pin(.trailing, to: .trailing, of: unreadCountLabel, withInset: 4) // Has mention view hasMentionView.addSubview(hasMentionLabel) hasMentionLabel.pin(to: hasMentionView) diff --git a/SessionUIKit/Utilities/UIView+Constraints.swift b/SessionUIKit/Utilities/UIView+Constraints.swift index 0dcad6829..e62e27940 100644 --- a/SessionUIKit/Utilities/UIView+Constraints.swift +++ b/SessionUIKit/Utilities/UIView+Constraints.swift @@ -95,4 +95,17 @@ public extension UIView { constraint.isActive = true return constraint } + + @discardableResult + func set(_ dimension: Dimension, greaterThanOrEqualTo size: CGFloat) -> NSLayoutConstraint { + translatesAutoresizingMaskIntoConstraints = false + let constraint: NSLayoutConstraint = { + switch dimension { + case .width: return widthAnchor.constraint(greaterThanOrEqualToConstant: size) + case .height: return heightAnchor.constraint(greaterThanOrEqualToConstant: size) + } + }() + constraint.isActive = true + return constraint + } } From ce83de5c46509b251ac665b84fc92ace70cf38fb Mon Sep 17 00:00:00 2001 From: Ryan Zhao Date: Mon, 31 Jan 2022 14:09:56 +1100 Subject: [PATCH 23/26] fix crash of dictionary.subscript.getter for closed groups --- .../Sending & Receiving/Pollers/ClosedGroupPoller.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/SessionMessagingKit/Sending & Receiving/Pollers/ClosedGroupPoller.swift b/SessionMessagingKit/Sending & Receiving/Pollers/ClosedGroupPoller.swift index 00148075a..1142ff4d8 100644 --- a/SessionMessagingKit/Sending & Receiving/Pollers/ClosedGroupPoller.swift +++ b/SessionMessagingKit/Sending & Receiving/Pollers/ClosedGroupPoller.swift @@ -44,7 +44,7 @@ public final class ClosedGroupPoller : NSObject { // Might be a race condition that the setUpPolling finishes too soon, // and the timer is not created, if we mark the group as is polling // after setUpPolling. So the poller may not work, thus misses messages. - isPolling[groupPublicKey] = true + internalQueue.sync{ isPolling[groupPublicKey] = true } setUpPolling(for: groupPublicKey) } @@ -55,7 +55,7 @@ public final class ClosedGroupPoller : NSObject { } public func stopPolling(for groupPublicKey: String) { - isPolling[groupPublicKey] = false + internalQueue.sync{ isPolling[groupPublicKey] = false } timers[groupPublicKey]?.invalidate() } From bee36423da39b46c18d87f9e1ded25412eff9d02 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Tue, 1 Feb 2022 14:55:03 +1100 Subject: [PATCH 24/26] Fixed a couple of issue with sharing large attachments Added back localized attachment error messages Fixed an issue where sending an attachment could fail and the screen would be dismissed before the user had a chance to read the error --- .../ConversationVC+Interaction.swift | 29 +++++++++++++------ .../Translations/de.lproj/Localizable.strings | 18 ++++++++++++ .../Translations/en.lproj/Localizable.strings | 18 ++++++++++++ .../Translations/es.lproj/Localizable.strings | 18 ++++++++++++ .../Translations/fa.lproj/Localizable.strings | 18 ++++++++++++ .../Translations/fi.lproj/Localizable.strings | 18 ++++++++++++ .../Translations/fr.lproj/Localizable.strings | 18 ++++++++++++ .../Translations/hi.lproj/Localizable.strings | 18 ++++++++++++ .../Translations/hr.lproj/Localizable.strings | 18 ++++++++++++ .../id-ID.lproj/Localizable.strings | 18 ++++++++++++ .../Translations/it.lproj/Localizable.strings | 18 ++++++++++++ .../Translations/ja.lproj/Localizable.strings | 18 ++++++++++++ .../Translations/nl.lproj/Localizable.strings | 18 ++++++++++++ .../Translations/pl.lproj/Localizable.strings | 18 ++++++++++++ .../pt_BR.lproj/Localizable.strings | 18 ++++++++++++ .../Translations/ru.lproj/Localizable.strings | 18 ++++++++++++ .../Translations/si.lproj/Localizable.strings | 18 ++++++++++++ .../Translations/sk.lproj/Localizable.strings | 18 ++++++++++++ .../Translations/sv.lproj/Localizable.strings | 18 ++++++++++++ .../Translations/th.lproj/Localizable.strings | 18 ++++++++++++ .../vi-VN.lproj/Localizable.strings | 18 ++++++++++++ .../zh-Hant.lproj/Localizable.strings | 18 ++++++++++++ .../zh_CN.lproj/Localizable.strings | 18 ++++++++++++ 23 files changed, 416 insertions(+), 9 deletions(-) diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index 61ff20d9f..4b0eb802a 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -1,6 +1,8 @@ +import UIKit import CoreServices import Photos import PhotosUI +import SignalUtilitiesKit extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuActionDelegate, ScrollToBottomButtonDelegate, SendMediaNavDelegate, UIDocumentPickerDelegate, AttachmentApprovalViewControllerDelegate, GifPickerViewControllerDelegate, @@ -79,11 +81,13 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc } func attachmentApproval(_ attachmentApproval: AttachmentApprovalViewController, didApproveAttachments attachments: [SignalAttachment], messageText: String?) { - sendAttachments(attachments, with: messageText ?? "") + sendAttachments(attachments, with: messageText ?? "") { [weak self] in + self?.dismiss(animated: true, completion: nil) + } + scrollToBottom(isAnimated: false) resetMentions() self.snInputView.text = "" - dismiss(animated: true) { } } func attachmentApprovalDidCancel(_ attachmentApproval: AttachmentApprovalViewController) { @@ -198,7 +202,7 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc if !attachment.hasError { self?.showAttachmentApprovalDialog(for: [ attachment ]) } else { - self?.showErrorAlert(for: attachment) + self?.showErrorAlert(for: attachment, onDismiss: nil) } } }.retainUntilComplete() @@ -248,11 +252,11 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc }) } - func sendAttachments(_ attachments: [SignalAttachment], with text: String) { + func sendAttachments(_ attachments: [SignalAttachment], with text: String, onComplete: (() -> ())? = nil) { guard !showBlockedModalIfNeeded() else { return } for attachment in attachments { if attachment.hasError { - return showErrorAlert(for: attachment) + return showErrorAlert(for: attachment, onDismiss: onComplete) } } let thread = self.thread @@ -272,6 +276,9 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc self?.scrollToBottom(isAnimated: false) }) self?.handleMessageSent() + + // Attachment successfully sent - dismiss the screen + onComplete?() }) } @@ -849,7 +856,7 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc dataSource.sourceFilename = fileName let attachment = SignalAttachment.voiceMessageAttachment(dataSource: dataSource, dataUTI: kUTTypeMPEG4Audio as String) guard !attachment.hasError else { - return showErrorAlert(for: attachment) + return showErrorAlert(for: attachment, onDismiss: nil) } // Send attachment sendAttachments([ attachment ], with: "") @@ -974,10 +981,14 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc } } - // MARK: Convenience - func showErrorAlert(for attachment: SignalAttachment) { + // MARK: - Convenience + + func showErrorAlert(for attachment: SignalAttachment, onDismiss: (() -> ())?) { let title = NSLocalizedString("ATTACHMENT_ERROR_ALERT_TITLE", comment: "") let message = attachment.localizedErrorDescription ?? SignalAttachment.missingDataErrorMessage - OWSAlerts.showAlert(title: title, message: message) + + OWSAlerts.showAlert(title: title, message: message, buttonTitle: nil) { _ in + onDismiss?() + } } } diff --git a/Session/Meta/Translations/de.lproj/Localizable.strings b/Session/Meta/Translations/de.lproj/Localizable.strings index ac1f796fe..0dded534f 100644 --- a/Session/Meta/Translations/de.lproj/Localizable.strings +++ b/Session/Meta/Translations/de.lproj/Localizable.strings @@ -26,6 +26,24 @@ "ATTACHMENT_DEFAULT_FILENAME" = "Anhang"; /* The title of the 'attachment error' alert. */ "ATTACHMENT_ERROR_ALERT_TITLE" = "Fehler beim Senden des Anhangs"; +/* Attachment error message for image attachments which could not be converted to JPEG */ +"ATTACHMENT_ERROR_COULD_NOT_CONVERT_TO_JPEG" = "Bild kann nicht konvertiert werden."; +/* Attachment error message for video attachments which could not be converted to MP4 */ +"ATTACHMENT_ERROR_COULD_NOT_CONVERT_TO_MP4" = "Video kann nicht verarbeitet werden."; +/* Attachment error message for image attachments which cannot be parsed */ +"ATTACHMENT_ERROR_COULD_NOT_PARSE_IMAGE" = "Bild kann nicht geparst werden."; +/* Attachment error message for image attachments in which metadata could not be removed */ +"ATTACHMENT_ERROR_COULD_NOT_REMOVE_METADATA" = "Metadaten können nicht aus dem Bild entfernt werden."; +/* Attachment error message for image attachments which could not be resized */ +"ATTACHMENT_ERROR_COULD_NOT_RESIZE_IMAGE" = "Bildgröße kann nicht geändert werden."; +/* Attachment error message for attachments whose data exceed file size limits */ +"ATTACHMENT_ERROR_FILE_SIZE_TOO_LARGE" = "Anhang ist zu groß."; +/* Attachment error message for attachments with invalid data */ +"ATTACHMENT_ERROR_INVALID_DATA" = "Anhang besitzt ungültigen Inhalt."; +/* Attachment error message for attachments with an invalid file format */ +"ATTACHMENT_ERROR_INVALID_FILE_FORMAT" = "Anhang besitzt ein ungültiges Dateiformat."; +/* Attachment error message for attachments without any data */ +"ATTACHMENT_ERROR_MISSING_DATA" = "Anhang ist leer."; /* Alert title when picking a document fails for an unknown reason */ "ATTACHMENT_PICKER_DOCUMENTS_FAILED_ALERT_TITLE" = "Auswählen des Dokuments gescheitert."; /* Alert body when picking a document fails because user picked a directory/bundle */ diff --git a/Session/Meta/Translations/en.lproj/Localizable.strings b/Session/Meta/Translations/en.lproj/Localizable.strings index cb912599d..3dd35931d 100644 --- a/Session/Meta/Translations/en.lproj/Localizable.strings +++ b/Session/Meta/Translations/en.lproj/Localizable.strings @@ -26,6 +26,24 @@ "ATTACHMENT_DEFAULT_FILENAME" = "Attachment"; /* The title of the 'attachment error' alert. */ "ATTACHMENT_ERROR_ALERT_TITLE" = "Error Sending Attachment"; +/* Attachment error message for image attachments which could not be converted to JPEG */ +"ATTACHMENT_ERROR_COULD_NOT_CONVERT_TO_JPEG" = "Unable to convert image."; +/* Attachment error message for video attachments which could not be converted to MP4 */ +"ATTACHMENT_ERROR_COULD_NOT_CONVERT_TO_MP4" = "Unable to process video."; +/* Attachment error message for image attachments which cannot be parsed */ +"ATTACHMENT_ERROR_COULD_NOT_PARSE_IMAGE" = "Unable to parse image."; +/* Attachment error message for image attachments in which metadata could not be removed */ +"ATTACHMENT_ERROR_COULD_NOT_REMOVE_METADATA" = "Unable to remove metadata from image."; +/* Attachment error message for image attachments which could not be resized */ +"ATTACHMENT_ERROR_COULD_NOT_RESIZE_IMAGE" = "Unable to resize image."; +/* Attachment error message for attachments whose data exceed file size limits */ +"ATTACHMENT_ERROR_FILE_SIZE_TOO_LARGE" = "Attachment is too large."; +/* Attachment error message for attachments with invalid data */ +"ATTACHMENT_ERROR_INVALID_DATA" = "Attachment includes invalid content."; +/* Attachment error message for attachments with an invalid file format */ +"ATTACHMENT_ERROR_INVALID_FILE_FORMAT" = "Attachment has an invalid file format."; +/* Attachment error message for attachments without any data */ +"ATTACHMENT_ERROR_MISSING_DATA" = "Attachment is empty."; /* Alert title when picking a document fails for an unknown reason */ "ATTACHMENT_PICKER_DOCUMENTS_FAILED_ALERT_TITLE" = "Failed to choose document."; /* Alert body when picking a document fails because user picked a directory/bundle */ diff --git a/Session/Meta/Translations/es.lproj/Localizable.strings b/Session/Meta/Translations/es.lproj/Localizable.strings index ff1356af6..51ab3036d 100644 --- a/Session/Meta/Translations/es.lproj/Localizable.strings +++ b/Session/Meta/Translations/es.lproj/Localizable.strings @@ -26,6 +26,24 @@ "ATTACHMENT_DEFAULT_FILENAME" = "Adjunto"; /* The title of the 'attachment error' alert. */ "ATTACHMENT_ERROR_ALERT_TITLE" = "Fallo al enviar archivo adjunto"; +/* Attachment error message for image attachments which could not be converted to JPEG */ +"ATTACHMENT_ERROR_COULD_NOT_CONVERT_TO_JPEG" = "Bild kann nicht konvertiert werden."; +/* Attachment error message for video attachments which could not be converted to MP4 */ +"ATTACHMENT_ERROR_COULD_NOT_CONVERT_TO_MP4" = "Video kann nicht verarbeitet werden."; +/* Attachment error message for image attachments which cannot be parsed */ +"ATTACHMENT_ERROR_COULD_NOT_PARSE_IMAGE" = "Bild kann nicht geparst werden."; +/* Attachment error message for image attachments in which metadata could not be removed */ +"ATTACHMENT_ERROR_COULD_NOT_REMOVE_METADATA" = "Metadaten können nicht aus dem Bild entfernt werden."; +/* Attachment error message for image attachments which could not be resized */ +"ATTACHMENT_ERROR_COULD_NOT_RESIZE_IMAGE" = "Bildgröße kann nicht geändert werden."; +/* Attachment error message for attachments whose data exceed file size limits */ +"ATTACHMENT_ERROR_FILE_SIZE_TOO_LARGE" = "Anhang ist zu groß."; +/* Attachment error message for attachments with invalid data */ +"ATTACHMENT_ERROR_INVALID_DATA" = "Anhang besitzt ungültigen Inhalt."; +/* Attachment error message for attachments with an invalid file format */ +"ATTACHMENT_ERROR_INVALID_FILE_FORMAT" = "Anhang besitzt ein ungültiges Dateiformat."; +/* Attachment error message for attachments without any data */ +"ATTACHMENT_ERROR_MISSING_DATA" = "Anhang ist leer."; /* Alert title when picking a document fails for an unknown reason */ "ATTACHMENT_PICKER_DOCUMENTS_FAILED_ALERT_TITLE" = "Fallo al seleccionar documento."; /* Alert body when picking a document fails because user picked a directory/bundle */ diff --git a/Session/Meta/Translations/fa.lproj/Localizable.strings b/Session/Meta/Translations/fa.lproj/Localizable.strings index 50cac47b5..c261e3af3 100644 --- a/Session/Meta/Translations/fa.lproj/Localizable.strings +++ b/Session/Meta/Translations/fa.lproj/Localizable.strings @@ -26,6 +26,24 @@ "ATTACHMENT_DEFAULT_FILENAME" = "ضميمه"; /* The title of the 'attachment error' alert. */ "ATTACHMENT_ERROR_ALERT_TITLE" = "خطا در ارسال فایل ضمیمه"; +/* Attachment error message for image attachments which could not be converted to JPEG */ +"ATTACHMENT_ERROR_COULD_NOT_CONVERT_TO_JPEG" = "Unable to convert image."; +/* Attachment error message for video attachments which could not be converted to MP4 */ +"ATTACHMENT_ERROR_COULD_NOT_CONVERT_TO_MP4" = "Unable to process video."; +/* Attachment error message for image attachments which cannot be parsed */ +"ATTACHMENT_ERROR_COULD_NOT_PARSE_IMAGE" = "Unable to parse image."; +/* Attachment error message for image attachments in which metadata could not be removed */ +"ATTACHMENT_ERROR_COULD_NOT_REMOVE_METADATA" = "Unable to remove metadata from image."; +/* Attachment error message for image attachments which could not be resized */ +"ATTACHMENT_ERROR_COULD_NOT_RESIZE_IMAGE" = "Unable to resize image."; +/* Attachment error message for attachments whose data exceed file size limits */ +"ATTACHMENT_ERROR_FILE_SIZE_TOO_LARGE" = "Attachment is too large."; +/* Attachment error message for attachments with invalid data */ +"ATTACHMENT_ERROR_INVALID_DATA" = "Attachment includes invalid content."; +/* Attachment error message for attachments with an invalid file format */ +"ATTACHMENT_ERROR_INVALID_FILE_FORMAT" = "Attachment has an invalid file format."; +/* Attachment error message for attachments without any data */ +"ATTACHMENT_ERROR_MISSING_DATA" = "Attachment is empty."; /* Alert title when picking a document fails for an unknown reason */ "ATTACHMENT_PICKER_DOCUMENTS_FAILED_ALERT_TITLE" = "خطا در انتخاب سند."; /* Alert body when picking a document fails because user picked a directory/bundle */ diff --git a/Session/Meta/Translations/fi.lproj/Localizable.strings b/Session/Meta/Translations/fi.lproj/Localizable.strings index 21457f99b..73808ff68 100644 --- a/Session/Meta/Translations/fi.lproj/Localizable.strings +++ b/Session/Meta/Translations/fi.lproj/Localizable.strings @@ -26,6 +26,24 @@ "ATTACHMENT_DEFAULT_FILENAME" = "Liite"; /* The title of the 'attachment error' alert. */ "ATTACHMENT_ERROR_ALERT_TITLE" = "Virhe liitteen lähettämisessä"; +/* Attachment error message for image attachments which could not be converted to JPEG */ +"ATTACHMENT_ERROR_COULD_NOT_CONVERT_TO_JPEG" = "Kuvaa ei voitu muuntaa."; +/* Attachment error message for video attachments which could not be converted to MP4 */ +"ATTACHMENT_ERROR_COULD_NOT_CONVERT_TO_MP4" = "Videon prosessointi ei onnistu."; +/* Attachment error message for image attachments which cannot be parsed */ +"ATTACHMENT_ERROR_COULD_NOT_PARSE_IMAGE" = "Kuvaa ei voitu jäsentää."; +/* Attachment error message for image attachments in which metadata could not be removed */ +"ATTACHMENT_ERROR_COULD_NOT_REMOVE_METADATA" = "Metatietojen poistaminen kuvasta ei onnistunut."; +/* Attachment error message for image attachments which could not be resized */ +"ATTACHMENT_ERROR_COULD_NOT_RESIZE_IMAGE" = "Kuvan kokoa ei voitu muuttaa."; +/* Attachment error message for attachments whose data exceed file size limits */ +"ATTACHMENT_ERROR_FILE_SIZE_TOO_LARGE" = "Liitetiedosto on liian suuri."; +/* Attachment error message for attachments with invalid data */ +"ATTACHMENT_ERROR_INVALID_DATA" = "Liitetiedoston sisältö on virheellinen."; +/* Attachment error message for attachments with an invalid file format */ +"ATTACHMENT_ERROR_INVALID_FILE_FORMAT" = "Liitetiedoston muoto on virheellinen."; +/* Attachment error message for attachments without any data */ +"ATTACHMENT_ERROR_MISSING_DATA" = "Liitetiedosto on tyhjä."; /* Alert title when picking a document fails for an unknown reason */ "ATTACHMENT_PICKER_DOCUMENTS_FAILED_ALERT_TITLE" = "Dokumentin valinta epäonnistui."; /* Alert body when picking a document fails because user picked a directory/bundle */ diff --git a/Session/Meta/Translations/fr.lproj/Localizable.strings b/Session/Meta/Translations/fr.lproj/Localizable.strings index 615fb8779..898f200e0 100644 --- a/Session/Meta/Translations/fr.lproj/Localizable.strings +++ b/Session/Meta/Translations/fr.lproj/Localizable.strings @@ -26,6 +26,24 @@ "ATTACHMENT_DEFAULT_FILENAME" = "Fichier joint"; /* The title of the 'attachment error' alert. */ "ATTACHMENT_ERROR_ALERT_TITLE" = "Erreur d’envoi du fichier joint"; +/* Attachment error message for image attachments which could not be converted to JPEG */ +"ATTACHMENT_ERROR_COULD_NOT_CONVERT_TO_JPEG" = "Impossible de convertir l’image."; +/* Attachment error message for video attachments which could not be converted to MP4 */ +"ATTACHMENT_ERROR_COULD_NOT_CONVERT_TO_MP4" = "Impossible de traiter la vidéo."; +/* Attachment error message for image attachments which cannot be parsed */ +"ATTACHMENT_ERROR_COULD_NOT_PARSE_IMAGE" = "Impossible d’analyser l’image."; +/* Attachment error message for image attachments in which metadata could not be removed */ +"ATTACHMENT_ERROR_COULD_NOT_REMOVE_METADATA" = "Impossible de supprimer les métadonnées de l’image."; +/* Attachment error message for image attachments which could not be resized */ +"ATTACHMENT_ERROR_COULD_NOT_RESIZE_IMAGE" = "Impossible de redimensionner l’image."; +/* Attachment error message for attachments whose data exceed file size limits */ +"ATTACHMENT_ERROR_FILE_SIZE_TOO_LARGE" = "Le fichier joint est trop volumineux."; +/* Attachment error message for attachments with invalid data */ +"ATTACHMENT_ERROR_INVALID_DATA" = "Le fichier joint comporte du contenu non valide."; +/* Attachment error message for attachments with an invalid file format */ +"ATTACHMENT_ERROR_INVALID_FILE_FORMAT" = "Le fichier joint présente un format de fichier invalide."; +/* Attachment error message for attachments without any data */ +"ATTACHMENT_ERROR_MISSING_DATA" = "Le fichier joint est vide."; /* Alert title when picking a document fails for an unknown reason */ "ATTACHMENT_PICKER_DOCUMENTS_FAILED_ALERT_TITLE" = "Échec de sélection du document."; /* Alert body when picking a document fails because user picked a directory/bundle */ diff --git a/Session/Meta/Translations/hi.lproj/Localizable.strings b/Session/Meta/Translations/hi.lproj/Localizable.strings index d53d15728..1aa6f2581 100644 --- a/Session/Meta/Translations/hi.lproj/Localizable.strings +++ b/Session/Meta/Translations/hi.lproj/Localizable.strings @@ -26,6 +26,24 @@ "ATTACHMENT_DEFAULT_FILENAME" = "अटैचमेंट"; /* The title of the 'attachment error' alert. */ "ATTACHMENT_ERROR_ALERT_TITLE" = "अनुलग्नक भेजने में त्रुटि"; +/* Attachment error message for image attachments which could not be converted to JPEG */ +"ATTACHMENT_ERROR_COULD_NOT_CONVERT_TO_JPEG" = "Unable to convert image."; +/* Attachment error message for video attachments which could not be converted to MP4 */ +"ATTACHMENT_ERROR_COULD_NOT_CONVERT_TO_MP4" = "Unable to process video."; +/* Attachment error message for image attachments which cannot be parsed */ +"ATTACHMENT_ERROR_COULD_NOT_PARSE_IMAGE" = "Unable to parse image."; +/* Attachment error message for image attachments in which metadata could not be removed */ +"ATTACHMENT_ERROR_COULD_NOT_REMOVE_METADATA" = "Unable to remove metadata from image."; +/* Attachment error message for image attachments which could not be resized */ +"ATTACHMENT_ERROR_COULD_NOT_RESIZE_IMAGE" = "Unable to resize image."; +/* Attachment error message for attachments whose data exceed file size limits */ +"ATTACHMENT_ERROR_FILE_SIZE_TOO_LARGE" = "Attachment is too large."; +/* Attachment error message for attachments with invalid data */ +"ATTACHMENT_ERROR_INVALID_DATA" = "Attachment includes invalid content."; +/* Attachment error message for attachments with an invalid file format */ +"ATTACHMENT_ERROR_INVALID_FILE_FORMAT" = "Attachment has an invalid file format."; +/* Attachment error message for attachments without any data */ +"ATTACHMENT_ERROR_MISSING_DATA" = "Attachment is empty."; /* Alert title when picking a document fails for an unknown reason */ "ATTACHMENT_PICKER_DOCUMENTS_FAILED_ALERT_TITLE" = "दस्तावेज़ चुनने में विफल."; /* Alert body when picking a document fails because user picked a directory/bundle */ diff --git a/Session/Meta/Translations/hr.lproj/Localizable.strings b/Session/Meta/Translations/hr.lproj/Localizable.strings index befd956ac..37658f8bb 100644 --- a/Session/Meta/Translations/hr.lproj/Localizable.strings +++ b/Session/Meta/Translations/hr.lproj/Localizable.strings @@ -26,6 +26,24 @@ "ATTACHMENT_DEFAULT_FILENAME" = "Privitak"; /* The title of the 'attachment error' alert. */ "ATTACHMENT_ERROR_ALERT_TITLE" = "Pogreška kod slanja privitka"; +/* Attachment error message for image attachments which could not be converted to JPEG */ +"ATTACHMENT_ERROR_COULD_NOT_CONVERT_TO_JPEG" = "Neuspješna pretvorba slike"; +/* Attachment error message for video attachments which could not be converted to MP4 */ +"ATTACHMENT_ERROR_COULD_NOT_CONVERT_TO_MP4" = "Neuspješna obrada videozapisa."; +/* Attachment error message for image attachments which cannot be parsed */ +"ATTACHMENT_ERROR_COULD_NOT_PARSE_IMAGE" = "Neuspješna analiza slike."; +/* Attachment error message for image attachments in which metadata could not be removed */ +"ATTACHMENT_ERROR_COULD_NOT_REMOVE_METADATA" = "Neuspješno uklanjanje metapodataka iz slike."; +/* Attachment error message for image attachments which could not be resized */ +"ATTACHMENT_ERROR_COULD_NOT_RESIZE_IMAGE" = "Neuspješna promjena veličine slike."; +/* Attachment error message for attachments whose data exceed file size limits */ +"ATTACHMENT_ERROR_FILE_SIZE_TOO_LARGE" = "Privitak je prevelik."; +/* Attachment error message for attachments with invalid data */ +"ATTACHMENT_ERROR_INVALID_DATA" = "Privitak sadrži nevažeći sadržaj."; +/* Attachment error message for attachments with an invalid file format */ +"ATTACHMENT_ERROR_INVALID_FILE_FORMAT" = "Privitak ima nevažeći format datoteke."; +/* Attachment error message for attachments without any data */ +"ATTACHMENT_ERROR_MISSING_DATA" = "Privitak je prazan."; /* Alert title when picking a document fails for an unknown reason */ "ATTACHMENT_PICKER_DOCUMENTS_FAILED_ALERT_TITLE" = "Odabir dokumenta neuspješan."; /* Alert body when picking a document fails because user picked a directory/bundle */ diff --git a/Session/Meta/Translations/id-ID.lproj/Localizable.strings b/Session/Meta/Translations/id-ID.lproj/Localizable.strings index 5cef83fe6..120c2bbcf 100644 --- a/Session/Meta/Translations/id-ID.lproj/Localizable.strings +++ b/Session/Meta/Translations/id-ID.lproj/Localizable.strings @@ -26,6 +26,24 @@ "ATTACHMENT_DEFAULT_FILENAME" = "Lampiran"; /* The title of the 'attachment error' alert. */ "ATTACHMENT_ERROR_ALERT_TITLE" = "Gagal Mengirim Lampiran"; +/* Attachment error message for image attachments which could not be converted to JPEG */ +"ATTACHMENT_ERROR_COULD_NOT_CONVERT_TO_JPEG" = "Tidak dapat mengonversi gambar."; +/* Attachment error message for video attachments which could not be converted to MP4 */ +"ATTACHMENT_ERROR_COULD_NOT_CONVERT_TO_MP4" = "Video tidak dapat diproses."; +/* Attachment error message for image attachments which cannot be parsed */ +"ATTACHMENT_ERROR_COULD_NOT_PARSE_IMAGE" = "Tidak dapat menguraikan gambar."; +/* Attachment error message for image attachments in which metadata could not be removed */ +"ATTACHMENT_ERROR_COULD_NOT_REMOVE_METADATA" = "Tidak dapat menghapus metadata dari gambar."; +/* Attachment error message for image attachments which could not be resized */ +"ATTACHMENT_ERROR_COULD_NOT_RESIZE_IMAGE" = "Tidak dapat mengubah ukuran gambar."; +/* Attachment error message for attachments whose data exceed file size limits */ +"ATTACHMENT_ERROR_FILE_SIZE_TOO_LARGE" = "Lampiran terlalu besar."; +/* Attachment error message for attachments with invalid data */ +"ATTACHMENT_ERROR_INVALID_DATA" = "Lampiran memuat konten yang tidak valid."; +/* Attachment error message for attachments with an invalid file format */ +"ATTACHMENT_ERROR_INVALID_FILE_FORMAT" = "Lampiran memiliki format berkas yang tidak valid."; +/* Attachment error message for attachments without any data */ +"ATTACHMENT_ERROR_MISSING_DATA" = "Lampiran kosong."; /* Alert title when picking a document fails for an unknown reason */ "ATTACHMENT_PICKER_DOCUMENTS_FAILED_ALERT_TITLE" = "Gagal memilih dokumen."; /* Alert body when picking a document fails because user picked a directory/bundle */ diff --git a/Session/Meta/Translations/it.lproj/Localizable.strings b/Session/Meta/Translations/it.lproj/Localizable.strings index b185d7686..bd197e62b 100644 --- a/Session/Meta/Translations/it.lproj/Localizable.strings +++ b/Session/Meta/Translations/it.lproj/Localizable.strings @@ -26,6 +26,24 @@ "ATTACHMENT_DEFAULT_FILENAME" = "Allegato"; /* The title of the 'attachment error' alert. */ "ATTACHMENT_ERROR_ALERT_TITLE" = "Errore invio allegato"; +/* Attachment error message for image attachments which could not be converted to JPEG */ +"ATTACHMENT_ERROR_COULD_NOT_CONVERT_TO_JPEG" = "Nepavyko kovertuoti paveikslo."; +/* Attachment error message for video attachments which could not be converted to MP4 */ +"ATTACHMENT_ERROR_COULD_NOT_CONVERT_TO_MP4" = "Nepavyko apdoroti vaizdo įrašo."; +/* Attachment error message for image attachments which cannot be parsed */ +"ATTACHMENT_ERROR_COULD_NOT_PARSE_IMAGE" = "Nepavyko išnagrinėti paveikslo."; +/* Attachment error message for image attachments in which metadata could not be removed */ +"ATTACHMENT_ERROR_COULD_NOT_REMOVE_METADATA" = "Nepavyko pašalinti metaduomenų iš paveikslo."; +/* Attachment error message for image attachments which could not be resized */ +"ATTACHMENT_ERROR_COULD_NOT_RESIZE_IMAGE" = "Nepavyko pakeisti paveikslo dydžio."; +/* Attachment error message for attachments whose data exceed file size limits */ +"ATTACHMENT_ERROR_FILE_SIZE_TOO_LARGE" = "Priedas yra per didelis."; +/* Attachment error message for attachments with invalid data */ +"ATTACHMENT_ERROR_INVALID_DATA" = "Priede yra neteisingas turinys."; +/* Attachment error message for attachments with an invalid file format */ +"ATTACHMENT_ERROR_INVALID_FILE_FORMAT" = "Priedas yra neteisingo failo formato."; +/* Attachment error message for attachments without any data */ +"ATTACHMENT_ERROR_MISSING_DATA" = "Priedas yra tuščias."; /* Alert title when picking a document fails for an unknown reason */ "ATTACHMENT_PICKER_DOCUMENTS_FAILED_ALERT_TITLE" = "Scelta del documento fallita."; /* Alert body when picking a document fails because user picked a directory/bundle */ diff --git a/Session/Meta/Translations/ja.lproj/Localizable.strings b/Session/Meta/Translations/ja.lproj/Localizable.strings index 8667f8fe3..cc2e1098d 100644 --- a/Session/Meta/Translations/ja.lproj/Localizable.strings +++ b/Session/Meta/Translations/ja.lproj/Localizable.strings @@ -26,6 +26,24 @@ "ATTACHMENT_DEFAULT_FILENAME" = "添付ファイル"; /* The title of the 'attachment error' alert. */ "ATTACHMENT_ERROR_ALERT_TITLE" = "添付ファイルの送信でエラー"; +/* Attachment error message for image attachments which could not be converted to JPEG */ +"ATTACHMENT_ERROR_COULD_NOT_CONVERT_TO_JPEG" = "画像を変換できません。"; +/* Attachment error message for video attachments which could not be converted to MP4 */ +"ATTACHMENT_ERROR_COULD_NOT_CONVERT_TO_MP4" = "動画を処理できません。"; +/* Attachment error message for image attachments which cannot be parsed */ +"ATTACHMENT_ERROR_COULD_NOT_PARSE_IMAGE" = "画像をパースできません。"; +/* Attachment error message for image attachments in which metadata could not be removed */ +"ATTACHMENT_ERROR_COULD_NOT_REMOVE_METADATA" = "画像からメタデータを消去できませんでした。"; +/* Attachment error message for image attachments which could not be resized */ +"ATTACHMENT_ERROR_COULD_NOT_RESIZE_IMAGE" = "画像のサイズを変更できません。"; +/* Attachment error message for attachments whose data exceed file size limits */ +"ATTACHMENT_ERROR_FILE_SIZE_TOO_LARGE" = "添付ファイルが大きすぎます。"; +/* Attachment error message for attachments with invalid data */ +"ATTACHMENT_ERROR_INVALID_DATA" = "添付ファイルが無効です。"; +/* Attachment error message for attachments with an invalid file format */ +"ATTACHMENT_ERROR_INVALID_FILE_FORMAT" = "添付ファイルのフォーマットが不正です。"; +/* Attachment error message for attachments without any data */ +"ATTACHMENT_ERROR_MISSING_DATA" = "添付ファイルの中身が空です。"; /* Alert title when picking a document fails for an unknown reason */ "ATTACHMENT_PICKER_DOCUMENTS_FAILED_ALERT_TITLE" = "ドキュメントの選択に失敗しました"; /* Alert body when picking a document fails because user picked a directory/bundle */ diff --git a/Session/Meta/Translations/nl.lproj/Localizable.strings b/Session/Meta/Translations/nl.lproj/Localizable.strings index a90e162bb..c3ba6a16f 100644 --- a/Session/Meta/Translations/nl.lproj/Localizable.strings +++ b/Session/Meta/Translations/nl.lproj/Localizable.strings @@ -26,6 +26,24 @@ "ATTACHMENT_DEFAULT_FILENAME" = "Bijlage"; /* The title of the 'attachment error' alert. */ "ATTACHMENT_ERROR_ALERT_TITLE" = "Versturen bijlage mislukt"; +/* Attachment error message for image attachments which could not be converted to JPEG */ +"ATTACHMENT_ERROR_COULD_NOT_CONVERT_TO_JPEG" = "Kan afbeelding niet naar JPEG converteren."; +/* Attachment error message for video attachments which could not be converted to MP4 */ +"ATTACHMENT_ERROR_COULD_NOT_CONVERT_TO_MP4" = "Kan video niet verwerken."; +/* Attachment error message for image attachments which cannot be parsed */ +"ATTACHMENT_ERROR_COULD_NOT_PARSE_IMAGE" = "Kan afbeelding niet verwerken."; +/* Attachment error message for image attachments in which metadata could not be removed */ +"ATTACHMENT_ERROR_COULD_NOT_REMOVE_METADATA" = "Kan metagegevens niet uit afbeelding verwijderen."; +/* Attachment error message for image attachments which could not be resized */ +"ATTACHMENT_ERROR_COULD_NOT_RESIZE_IMAGE" = "Afbeelding verkleinen mislukt."; +/* Attachment error message for attachments whose data exceed file size limits */ +"ATTACHMENT_ERROR_FILE_SIZE_TOO_LARGE" = "Bijlage is te groot."; +/* Attachment error message for attachments with invalid data */ +"ATTACHMENT_ERROR_INVALID_DATA" = "Bijlage bevat inhoud die niet wordt ondersteund of niet correct is geformatteerd."; +/* Attachment error message for attachments with an invalid file format */ +"ATTACHMENT_ERROR_INVALID_FILE_FORMAT" = "Bijlage is een bestandstype welke niet wordt ondersteund."; +/* Attachment error message for attachments without any data */ +"ATTACHMENT_ERROR_MISSING_DATA" = "Bijlage is leeg."; /* Alert title when picking a document fails for an unknown reason */ "ATTACHMENT_PICKER_DOCUMENTS_FAILED_ALERT_TITLE" = "Document kiezen mislukt."; /* Alert body when picking a document fails because user picked a directory/bundle */ diff --git a/Session/Meta/Translations/pl.lproj/Localizable.strings b/Session/Meta/Translations/pl.lproj/Localizable.strings index 869fed251..d91701ba2 100644 --- a/Session/Meta/Translations/pl.lproj/Localizable.strings +++ b/Session/Meta/Translations/pl.lproj/Localizable.strings @@ -26,6 +26,24 @@ "ATTACHMENT_DEFAULT_FILENAME" = "Załącznik"; /* The title of the 'attachment error' alert. */ "ATTACHMENT_ERROR_ALERT_TITLE" = "Wystąpił błąd podczas wysyłania załącznika"; +/* Attachment error message for image attachments which could not be converted to JPEG */ +"ATTACHMENT_ERROR_COULD_NOT_CONVERT_TO_JPEG" = "Nie można przekonwertować obrazu."; +/* Attachment error message for video attachments which could not be converted to MP4 */ +"ATTACHMENT_ERROR_COULD_NOT_CONVERT_TO_MP4" = "Nie można przetworzyć wideo."; +/* Attachment error message for image attachments which cannot be parsed */ +"ATTACHMENT_ERROR_COULD_NOT_PARSE_IMAGE" = "Nie można przetworzyć załączonej grafiki."; +/* Attachment error message for image attachments in which metadata could not be removed */ +"ATTACHMENT_ERROR_COULD_NOT_REMOVE_METADATA" = "Nie można usunąć metadanych z obrazu."; +/* Attachment error message for image attachments which could not be resized */ +"ATTACHMENT_ERROR_COULD_NOT_RESIZE_IMAGE" = "Nie można zmienić rozmiaru obrazu."; +/* Attachment error message for attachments whose data exceed file size limits */ +"ATTACHMENT_ERROR_FILE_SIZE_TOO_LARGE" = "Załącznik jest za duży."; +/* Attachment error message for attachments with invalid data */ +"ATTACHMENT_ERROR_INVALID_DATA" = "Załącznik zawiera nieprawidłową zawartość."; +/* Attachment error message for attachments with an invalid file format */ +"ATTACHMENT_ERROR_INVALID_FILE_FORMAT" = "Załącznik ma nieprawidłowy format pliku."; +/* Attachment error message for attachments without any data */ +"ATTACHMENT_ERROR_MISSING_DATA" = "Załącznik jest pusty."; /* Alert title when picking a document fails for an unknown reason */ "ATTACHMENT_PICKER_DOCUMENTS_FAILED_ALERT_TITLE" = "Nie udało się wybrać dokumentu."; /* Alert body when picking a document fails because user picked a directory/bundle */ diff --git a/Session/Meta/Translations/pt_BR.lproj/Localizable.strings b/Session/Meta/Translations/pt_BR.lproj/Localizable.strings index 3acb3060f..0d2e9985e 100644 --- a/Session/Meta/Translations/pt_BR.lproj/Localizable.strings +++ b/Session/Meta/Translations/pt_BR.lproj/Localizable.strings @@ -26,6 +26,24 @@ "ATTACHMENT_DEFAULT_FILENAME" = "Anexo"; /* The title of the 'attachment error' alert. */ "ATTACHMENT_ERROR_ALERT_TITLE" = "Erro ao Enviar Anexo"; +/* Attachment error message for image attachments which could not be converted to JPEG */ +"ATTACHMENT_ERROR_COULD_NOT_CONVERT_TO_JPEG" = "Impossível converter imagem."; +/* Attachment error message for video attachments which could not be converted to MP4 */ +"ATTACHMENT_ERROR_COULD_NOT_CONVERT_TO_MP4" = "Não foi possível processar o vídeo."; +/* Attachment error message for image attachments which cannot be parsed */ +"ATTACHMENT_ERROR_COULD_NOT_PARSE_IMAGE" = "Não foi possível analisar a imagem."; +/* Attachment error message for image attachments in which metadata could not be removed */ +"ATTACHMENT_ERROR_COULD_NOT_REMOVE_METADATA" = "Não foi possível suprimir os metadados da imagem."; +/* Attachment error message for image attachments which could not be resized */ +"ATTACHMENT_ERROR_COULD_NOT_RESIZE_IMAGE" = "Impossível redimensionar imagem."; +/* Attachment error message for attachments whose data exceed file size limits */ +"ATTACHMENT_ERROR_FILE_SIZE_TOO_LARGE" = "Anexo excede o tamanho possível."; +/* Attachment error message for attachments with invalid data */ +"ATTACHMENT_ERROR_INVALID_DATA" = "Anexo inclui conteúdo inválido."; +/* Attachment error message for attachments with an invalid file format */ +"ATTACHMENT_ERROR_INVALID_FILE_FORMAT" = "Anexo tem um formato de arquivo inválido."; +/* Attachment error message for attachments without any data */ +"ATTACHMENT_ERROR_MISSING_DATA" = "Anexo vazio."; /* Alert title when picking a document fails for an unknown reason */ "ATTACHMENT_PICKER_DOCUMENTS_FAILED_ALERT_TITLE" = "Falha ao selecionar o documento."; /* Alert body when picking a document fails because user picked a directory/bundle */ diff --git a/Session/Meta/Translations/ru.lproj/Localizable.strings b/Session/Meta/Translations/ru.lproj/Localizable.strings index ae19b824a..0a46af259 100644 --- a/Session/Meta/Translations/ru.lproj/Localizable.strings +++ b/Session/Meta/Translations/ru.lproj/Localizable.strings @@ -26,6 +26,24 @@ "ATTACHMENT_DEFAULT_FILENAME" = "Вложение"; /* The title of the 'attachment error' alert. */ "ATTACHMENT_ERROR_ALERT_TITLE" = "Ошибка отправки вложения"; +/* Attachment error message for image attachments which could not be converted to JPEG */ +"ATTACHMENT_ERROR_COULD_NOT_CONVERT_TO_JPEG" = "Unable to convert image."; +/* Attachment error message for video attachments which could not be converted to MP4 */ +"ATTACHMENT_ERROR_COULD_NOT_CONVERT_TO_MP4" = "Unable to process video."; +/* Attachment error message for image attachments which cannot be parsed */ +"ATTACHMENT_ERROR_COULD_NOT_PARSE_IMAGE" = "Unable to parse image."; +/* Attachment error message for image attachments in which metadata could not be removed */ +"ATTACHMENT_ERROR_COULD_NOT_REMOVE_METADATA" = "Unable to remove metadata from image."; +/* Attachment error message for image attachments which could not be resized */ +"ATTACHMENT_ERROR_COULD_NOT_RESIZE_IMAGE" = "Unable to resize image."; +/* Attachment error message for attachments whose data exceed file size limits */ +"ATTACHMENT_ERROR_FILE_SIZE_TOO_LARGE" = "Attachment is too large."; +/* Attachment error message for attachments with invalid data */ +"ATTACHMENT_ERROR_INVALID_DATA" = "Attachment includes invalid content."; +/* Attachment error message for attachments with an invalid file format */ +"ATTACHMENT_ERROR_INVALID_FILE_FORMAT" = "Attachment has an invalid file format."; +/* Attachment error message for attachments without any data */ +"ATTACHMENT_ERROR_MISSING_DATA" = "Attachment is empty."; /* Alert title when picking a document fails for an unknown reason */ "ATTACHMENT_PICKER_DOCUMENTS_FAILED_ALERT_TITLE" = "Не удалось выбрать документ."; /* Alert body when picking a document fails because user picked a directory/bundle */ diff --git a/Session/Meta/Translations/si.lproj/Localizable.strings b/Session/Meta/Translations/si.lproj/Localizable.strings index b2d4849c7..48940993f 100644 --- a/Session/Meta/Translations/si.lproj/Localizable.strings +++ b/Session/Meta/Translations/si.lproj/Localizable.strings @@ -26,6 +26,24 @@ "ATTACHMENT_DEFAULT_FILENAME" = "Attachment"; /* The title of the 'attachment error' alert. */ "ATTACHMENT_ERROR_ALERT_TITLE" = "Error Sending Attachment"; +/* Attachment error message for image attachments which could not be converted to JPEG */ +"ATTACHMENT_ERROR_COULD_NOT_CONVERT_TO_JPEG" = "Unable to convert image."; +/* Attachment error message for video attachments which could not be converted to MP4 */ +"ATTACHMENT_ERROR_COULD_NOT_CONVERT_TO_MP4" = "Unable to process video."; +/* Attachment error message for image attachments which cannot be parsed */ +"ATTACHMENT_ERROR_COULD_NOT_PARSE_IMAGE" = "Unable to parse image."; +/* Attachment error message for image attachments in which metadata could not be removed */ +"ATTACHMENT_ERROR_COULD_NOT_REMOVE_METADATA" = "Unable to remove metadata from image."; +/* Attachment error message for image attachments which could not be resized */ +"ATTACHMENT_ERROR_COULD_NOT_RESIZE_IMAGE" = "Unable to resize image."; +/* Attachment error message for attachments whose data exceed file size limits */ +"ATTACHMENT_ERROR_FILE_SIZE_TOO_LARGE" = "Attachment is too large."; +/* Attachment error message for attachments with invalid data */ +"ATTACHMENT_ERROR_INVALID_DATA" = "Attachment includes invalid content."; +/* Attachment error message for attachments with an invalid file format */ +"ATTACHMENT_ERROR_INVALID_FILE_FORMAT" = "Attachment has an invalid file format."; +/* Attachment error message for attachments without any data */ +"ATTACHMENT_ERROR_MISSING_DATA" = "Attachment is empty."; /* Alert title when picking a document fails for an unknown reason */ "ATTACHMENT_PICKER_DOCUMENTS_FAILED_ALERT_TITLE" = "Failed to choose document."; /* Alert body when picking a document fails because user picked a directory/bundle */ diff --git a/Session/Meta/Translations/sk.lproj/Localizable.strings b/Session/Meta/Translations/sk.lproj/Localizable.strings index a225890fd..18c696cd6 100644 --- a/Session/Meta/Translations/sk.lproj/Localizable.strings +++ b/Session/Meta/Translations/sk.lproj/Localizable.strings @@ -26,6 +26,24 @@ "ATTACHMENT_DEFAULT_FILENAME" = "Príloha"; /* The title of the 'attachment error' alert. */ "ATTACHMENT_ERROR_ALERT_TITLE" = "Chyba pri posielaní prílohy"; +/* Attachment error message for image attachments which could not be converted to JPEG */ +"ATTACHMENT_ERROR_COULD_NOT_CONVERT_TO_JPEG" = "Obrázok sa nepodarilo skonvertovať."; +/* Attachment error message for video attachments which could not be converted to MP4 */ +"ATTACHMENT_ERROR_COULD_NOT_CONVERT_TO_MP4" = "Video sa nepodarilo spracovať."; +/* Attachment error message for image attachments which cannot be parsed */ +"ATTACHMENT_ERROR_COULD_NOT_PARSE_IMAGE" = "Obrázok sa nepodarilo spracovať."; +/* Attachment error message for image attachments in which metadata could not be removed */ +"ATTACHMENT_ERROR_COULD_NOT_REMOVE_METADATA" = "Z obrázku sa nepodarilo odstrániť metadáta."; +/* Attachment error message for image attachments which could not be resized */ +"ATTACHMENT_ERROR_COULD_NOT_RESIZE_IMAGE" = "Nepodarilo sa zmeniť veľkosť obrázka."; +/* Attachment error message for attachments whose data exceed file size limits */ +"ATTACHMENT_ERROR_FILE_SIZE_TOO_LARGE" = "Príloha je priliš veľká."; +/* Attachment error message for attachments with invalid data */ +"ATTACHMENT_ERROR_INVALID_DATA" = "Príloha obsahuje neplatný obsah."; +/* Attachment error message for attachments with an invalid file format */ +"ATTACHMENT_ERROR_INVALID_FILE_FORMAT" = "Príloha má neplatný formát súboru."; +/* Attachment error message for attachments without any data */ +"ATTACHMENT_ERROR_MISSING_DATA" = "Príloha je prázdna."; /* Alert title when picking a document fails for an unknown reason */ "ATTACHMENT_PICKER_DOCUMENTS_FAILED_ALERT_TITLE" = "Nepodarilo sa vybrať dokument."; /* Alert body when picking a document fails because user picked a directory/bundle */ diff --git a/Session/Meta/Translations/sv.lproj/Localizable.strings b/Session/Meta/Translations/sv.lproj/Localizable.strings index e3e3fdb36..3fe6d5ffd 100644 --- a/Session/Meta/Translations/sv.lproj/Localizable.strings +++ b/Session/Meta/Translations/sv.lproj/Localizable.strings @@ -26,6 +26,24 @@ "ATTACHMENT_DEFAULT_FILENAME" = "Bilaga"; /* The title of the 'attachment error' alert. */ "ATTACHMENT_ERROR_ALERT_TITLE" = "Fel vid sändning av bilaga"; +/* Attachment error message for image attachments which could not be converted to JPEG */ +"ATTACHMENT_ERROR_COULD_NOT_CONVERT_TO_JPEG" = "Det går inte att konvertera bilden."; +/* Attachment error message for video attachments which could not be converted to MP4 */ +"ATTACHMENT_ERROR_COULD_NOT_CONVERT_TO_MP4" = "Det går inte att bearbeta videon."; +/* Attachment error message for image attachments which cannot be parsed */ +"ATTACHMENT_ERROR_COULD_NOT_PARSE_IMAGE" = "Det går inte att tolka bilden."; +/* Attachment error message for image attachments in which metadata could not be removed */ +"ATTACHMENT_ERROR_COULD_NOT_REMOVE_METADATA" = "Det går inte att ta bort metadata från bilden."; +/* Attachment error message for image attachments which could not be resized */ +"ATTACHMENT_ERROR_COULD_NOT_RESIZE_IMAGE" = "Det går inte att ändra storlek på bilden."; +/* Attachment error message for attachments whose data exceed file size limits */ +"ATTACHMENT_ERROR_FILE_SIZE_TOO_LARGE" = "Bilagan är för stor."; +/* Attachment error message for attachments with invalid data */ +"ATTACHMENT_ERROR_INVALID_DATA" = "Bilagan innehåller ogiltigt innehåll."; +/* Attachment error message for attachments with an invalid file format */ +"ATTACHMENT_ERROR_INVALID_FILE_FORMAT" = "Bilagan har ett ogiltigt filformat."; +/* Attachment error message for attachments without any data */ +"ATTACHMENT_ERROR_MISSING_DATA" = "Bilagan är tom."; /* Alert title when picking a document fails for an unknown reason */ "ATTACHMENT_PICKER_DOCUMENTS_FAILED_ALERT_TITLE" = "Det gick inte att välja dokument."; /* Alert body when picking a document fails because user picked a directory/bundle */ diff --git a/Session/Meta/Translations/th.lproj/Localizable.strings b/Session/Meta/Translations/th.lproj/Localizable.strings index a4e943ecd..4372e87c0 100644 --- a/Session/Meta/Translations/th.lproj/Localizable.strings +++ b/Session/Meta/Translations/th.lproj/Localizable.strings @@ -26,6 +26,24 @@ "ATTACHMENT_DEFAULT_FILENAME" = "ไฟล์แนบ"; /* The title of the 'attachment error' alert. */ "ATTACHMENT_ERROR_ALERT_TITLE" = "ส่งไฟล์แนบโดนผิดพลาด"; +/* Attachment error message for image attachments which could not be converted to JPEG */ +"ATTACHMENT_ERROR_COULD_NOT_CONVERT_TO_JPEG" = "Unable to convert image."; +/* Attachment error message for video attachments which could not be converted to MP4 */ +"ATTACHMENT_ERROR_COULD_NOT_CONVERT_TO_MP4" = "Unable to process video."; +/* Attachment error message for image attachments which cannot be parsed */ +"ATTACHMENT_ERROR_COULD_NOT_PARSE_IMAGE" = "Unable to parse image."; +/* Attachment error message for image attachments in which metadata could not be removed */ +"ATTACHMENT_ERROR_COULD_NOT_REMOVE_METADATA" = "Unable to remove metadata from image."; +/* Attachment error message for image attachments which could not be resized */ +"ATTACHMENT_ERROR_COULD_NOT_RESIZE_IMAGE" = "Unable to resize image."; +/* Attachment error message for attachments whose data exceed file size limits */ +"ATTACHMENT_ERROR_FILE_SIZE_TOO_LARGE" = "Attachment is too large."; +/* Attachment error message for attachments with invalid data */ +"ATTACHMENT_ERROR_INVALID_DATA" = "Attachment includes invalid content."; +/* Attachment error message for attachments with an invalid file format */ +"ATTACHMENT_ERROR_INVALID_FILE_FORMAT" = "Attachment has an invalid file format."; +/* Attachment error message for attachments without any data */ +"ATTACHMENT_ERROR_MISSING_DATA" = "Attachment is empty."; /* Alert title when picking a document fails for an unknown reason */ "ATTACHMENT_PICKER_DOCUMENTS_FAILED_ALERT_TITLE" = "เลือกไฟล์ไม่ได้สำเร็จ"; /* Alert body when picking a document fails because user picked a directory/bundle */ diff --git a/Session/Meta/Translations/vi-VN.lproj/Localizable.strings b/Session/Meta/Translations/vi-VN.lproj/Localizable.strings index 4c29a785f..efae3e3d2 100644 --- a/Session/Meta/Translations/vi-VN.lproj/Localizable.strings +++ b/Session/Meta/Translations/vi-VN.lproj/Localizable.strings @@ -26,6 +26,24 @@ "ATTACHMENT_DEFAULT_FILENAME" = "Attachment"; /* The title of the 'attachment error' alert. */ "ATTACHMENT_ERROR_ALERT_TITLE" = "Error Sending Attachment"; +/* Attachment error message for image attachments which could not be converted to JPEG */ +"ATTACHMENT_ERROR_COULD_NOT_CONVERT_TO_JPEG" = "Unable to convert image."; +/* Attachment error message for video attachments which could not be converted to MP4 */ +"ATTACHMENT_ERROR_COULD_NOT_CONVERT_TO_MP4" = "Unable to process video."; +/* Attachment error message for image attachments which cannot be parsed */ +"ATTACHMENT_ERROR_COULD_NOT_PARSE_IMAGE" = "Unable to parse image."; +/* Attachment error message for image attachments in which metadata could not be removed */ +"ATTACHMENT_ERROR_COULD_NOT_REMOVE_METADATA" = "Unable to remove metadata from image."; +/* Attachment error message for image attachments which could not be resized */ +"ATTACHMENT_ERROR_COULD_NOT_RESIZE_IMAGE" = "Unable to resize image."; +/* Attachment error message for attachments whose data exceed file size limits */ +"ATTACHMENT_ERROR_FILE_SIZE_TOO_LARGE" = "Attachment is too large."; +/* Attachment error message for attachments with invalid data */ +"ATTACHMENT_ERROR_INVALID_DATA" = "Attachment includes invalid content."; +/* Attachment error message for attachments with an invalid file format */ +"ATTACHMENT_ERROR_INVALID_FILE_FORMAT" = "Attachment has an invalid file format."; +/* Attachment error message for attachments without any data */ +"ATTACHMENT_ERROR_MISSING_DATA" = "Attachment is empty."; /* Alert title when picking a document fails for an unknown reason */ "ATTACHMENT_PICKER_DOCUMENTS_FAILED_ALERT_TITLE" = "Failed to choose document."; /* Alert body when picking a document fails because user picked a directory/bundle */ diff --git a/Session/Meta/Translations/zh-Hant.lproj/Localizable.strings b/Session/Meta/Translations/zh-Hant.lproj/Localizable.strings index d4b6922aa..2f7d6e290 100644 --- a/Session/Meta/Translations/zh-Hant.lproj/Localizable.strings +++ b/Session/Meta/Translations/zh-Hant.lproj/Localizable.strings @@ -26,6 +26,24 @@ "ATTACHMENT_DEFAULT_FILENAME" = "附件"; /* The title of the 'attachment error' alert. */ "ATTACHMENT_ERROR_ALERT_TITLE" = "寄送附件時發生錯誤"; +/* Attachment error message for image attachments which could not be converted to JPEG */ +"ATTACHMENT_ERROR_COULD_NOT_CONVERT_TO_JPEG" = "Unable to convert image."; +/* Attachment error message for video attachments which could not be converted to MP4 */ +"ATTACHMENT_ERROR_COULD_NOT_CONVERT_TO_MP4" = "Unable to process video."; +/* Attachment error message for image attachments which cannot be parsed */ +"ATTACHMENT_ERROR_COULD_NOT_PARSE_IMAGE" = "Unable to parse image."; +/* Attachment error message for image attachments in which metadata could not be removed */ +"ATTACHMENT_ERROR_COULD_NOT_REMOVE_METADATA" = "Unable to remove metadata from image."; +/* Attachment error message for image attachments which could not be resized */ +"ATTACHMENT_ERROR_COULD_NOT_RESIZE_IMAGE" = "Unable to resize image."; +/* Attachment error message for attachments whose data exceed file size limits */ +"ATTACHMENT_ERROR_FILE_SIZE_TOO_LARGE" = "Attachment is too large."; +/* Attachment error message for attachments with invalid data */ +"ATTACHMENT_ERROR_INVALID_DATA" = "Attachment includes invalid content."; +/* Attachment error message for attachments with an invalid file format */ +"ATTACHMENT_ERROR_INVALID_FILE_FORMAT" = "Attachment has an invalid file format."; +/* Attachment error message for attachments without any data */ +"ATTACHMENT_ERROR_MISSING_DATA" = "Attachment is empty."; /* Alert title when picking a document fails for an unknown reason */ "ATTACHMENT_PICKER_DOCUMENTS_FAILED_ALERT_TITLE" = "選取檔案時發生錯誤"; /* Alert body when picking a document fails because user picked a directory/bundle */ diff --git a/Session/Meta/Translations/zh_CN.lproj/Localizable.strings b/Session/Meta/Translations/zh_CN.lproj/Localizable.strings index 359a12684..9abccf9f3 100644 --- a/Session/Meta/Translations/zh_CN.lproj/Localizable.strings +++ b/Session/Meta/Translations/zh_CN.lproj/Localizable.strings @@ -26,6 +26,24 @@ "ATTACHMENT_DEFAULT_FILENAME" = "附件"; /* The title of the 'attachment error' alert. */ "ATTACHMENT_ERROR_ALERT_TITLE" = "附件发送错误"; +/* Attachment error message for image attachments which could not be converted to JPEG */ +"ATTACHMENT_ERROR_COULD_NOT_CONVERT_TO_JPEG" = "无法转换图片。"; +/* Attachment error message for video attachments which could not be converted to MP4 */ +"ATTACHMENT_ERROR_COULD_NOT_CONVERT_TO_MP4" = "无法处理该视频。"; +/* Attachment error message for image attachments which cannot be parsed */ +"ATTACHMENT_ERROR_COULD_NOT_PARSE_IMAGE" = "无法解析图片。"; +/* Attachment error message for image attachments in which metadata could not be removed */ +"ATTACHMENT_ERROR_COULD_NOT_REMOVE_METADATA" = "无法删除图片的元数据。"; +/* Attachment error message for image attachments which could not be resized */ +"ATTACHMENT_ERROR_COULD_NOT_RESIZE_IMAGE" = "无法调整图像大小。"; +/* Attachment error message for attachments whose data exceed file size limits */ +"ATTACHMENT_ERROR_FILE_SIZE_TOO_LARGE" = "附件过大。"; +/* Attachment error message for attachments with invalid data */ +"ATTACHMENT_ERROR_INVALID_DATA" = "附件中存在无效内容。"; +/* Attachment error message for attachments with an invalid file format */ +"ATTACHMENT_ERROR_INVALID_FILE_FORMAT" = "附件的文件格式无效。"; +/* Attachment error message for attachments without any data */ +"ATTACHMENT_ERROR_MISSING_DATA" = "附件为空。"; /* Alert title when picking a document fails for an unknown reason */ "ATTACHMENT_PICKER_DOCUMENTS_FAILED_ALERT_TITLE" = "文件选取失败。"; /* Alert body when picking a document fails because user picked a directory/bundle */ From 5ce7aa49ee1fb1c617504c4d17b9b45508bffa7c Mon Sep 17 00:00:00 2001 From: Ryan Zhao Date: Tue, 1 Feb 2022 15:12:48 +1100 Subject: [PATCH 25/26] mark messages with undownloaded attachments as read as well --- .../Messages/Signal/TSIncomingMessage.m | 29 +++---------------- 1 file changed, 4 insertions(+), 25 deletions(-) diff --git a/SessionMessagingKit/Messages/Signal/TSIncomingMessage.m b/SessionMessagingKit/Messages/Signal/TSIncomingMessage.m index 01769a1aa..e80bf0bdb 100644 --- a/SessionMessagingKit/Messages/Signal/TSIncomingMessage.m +++ b/SessionMessagingKit/Messages/Signal/TSIncomingMessage.m @@ -155,31 +155,10 @@ NS_ASSUME_NONNULL_BEGIN if (_read && readTimestamp >= self.expireStartedAt) { return; } - BOOL isTrusted = YES; - TSThread* thread = [self threadWithTransaction:transaction]; - if ([thread isKindOfClass:[TSContactThread class]]) { - TSContactThread* contactThread = (TSContactThread*)thread; - isTrusted = [[LKStorage shared] getContactWithSessionID:[contactThread contactSessionID] using:transaction].isTrusted; - } - - BOOL areAllAttachmentsDownloaded = YES; - if (isTrusted) { - for (NSString *attachmentId in self.attachmentIds) { - TSAttachment *attachment = [TSAttachment fetchObjectWithUniqueID:attachmentId transaction:transaction]; - // If the attachment download failed, we can mark this message as read. - // Otherwise, this message will never be marked as read. - if ([attachment isKindOfClass:[TSAttachmentPointer class]] - && ((TSAttachmentPointer *)attachment).state == TSAttachmentPointerStateFailed) { - continue; - } - areAllAttachmentsDownloaded = areAllAttachmentsDownloaded && attachment.isDownloaded; - if (!areAllAttachmentsDownloaded) break; - } - } - - if (!areAllAttachmentsDownloaded) { - return; - } + // We just ignore all attachments download state here and mark all messages as read + // This is a workaround for a situation that some large attachments won't be downloaded + // and just stuck in a downloading state. In that case, the corresponding message won't + // be able to be marked as read. _read = YES; [self saveWithTransaction:transaction]; From 69e464e8aefcb3be1be97227fdec90667261bfd3 Mon Sep 17 00:00:00 2001 From: Ryan Zhao Date: Thu, 3 Feb 2022 16:33:26 +1100 Subject: [PATCH 26/26] bump up version & build number --- Session.xcodeproj/project.pbxproj | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 0f978c0f5..c6ae0a2aa 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -1117,10 +1117,10 @@ 7B4C75CA26B37E0F0000AC89 /* UnsendRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnsendRequest.swift; sourceTree = ""; }; 7B4C75CC26BB92060000AC89 /* DeletedMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeletedMessageView.swift; sourceTree = ""; }; 7B7CB18A270591630079FF93 /* ShareLogsModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareLogsModal.swift; sourceTree = ""; }; + 7BA6F47DAD18D44D75B7110F /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionSnodeKit.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionSnodeKit.debug.xcconfig"; path = "Pods/Target Support Files/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionSnodeKit/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionSnodeKit.debug.xcconfig"; sourceTree = ""; }; 7BA7F4BA279F9F5800B3A466 /* EmptySearchResultCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptySearchResultCell.swift; sourceTree = ""; }; 7BA7F4BC27A216B600B3A466 /* Storage+RecentSearchResults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Storage+RecentSearchResults.swift"; sourceTree = ""; }; 7BA9057D27911C5800998B3C /* GlobalSearchViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlobalSearchViewController.swift; sourceTree = ""; }; - 7BA6F47DAD18D44D75B7110F /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionSnodeKit.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionSnodeKit.debug.xcconfig"; path = "Pods/Target Support Files/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionSnodeKit/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionSnodeKit.debug.xcconfig"; sourceTree = ""; }; 7BC01A3B241F40AB00BC7C55 /* SessionNotificationServiceExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = SessionNotificationServiceExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 7BC01A3D241F40AB00BC7C55 /* NotificationServiceExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationServiceExtension.swift; sourceTree = ""; }; 7BC01A3F241F40AB00BC7C55 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -5125,7 +5125,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 316; + CURRENT_PROJECT_VERSION = 319; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = SUQ8J2PCT7; FRAMEWORK_SEARCH_PATHS = "$(inherited)"; @@ -5150,7 +5150,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.11.20; + MARKETING_VERSION = 1.11.21; MTL_ENABLE_DEBUG_INFO = YES; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.ShareExtension"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -5198,7 +5198,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 316; + CURRENT_PROJECT_VERSION = 319; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = SUQ8J2PCT7; ENABLE_NS_ASSERTIONS = NO; @@ -5228,7 +5228,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.11.20; + MARKETING_VERSION = 1.11.21; MTL_ENABLE_DEBUG_INFO = NO; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.ShareExtension"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -5264,7 +5264,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 316; + CURRENT_PROJECT_VERSION = 319; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = SUQ8J2PCT7; FRAMEWORK_SEARCH_PATHS = "$(inherited)"; @@ -5287,7 +5287,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.11.20; + MARKETING_VERSION = 1.11.21; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.NotificationServiceExtension"; @@ -5338,7 +5338,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 316; + CURRENT_PROJECT_VERSION = 319; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = SUQ8J2PCT7; ENABLE_NS_ASSERTIONS = NO; @@ -5366,7 +5366,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.11.20; + MARKETING_VERSION = 1.11.21; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.NotificationServiceExtension"; @@ -6274,7 +6274,7 @@ CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 316; + CURRENT_PROJECT_VERSION = 319; DEVELOPMENT_TEAM = SUQ8J2PCT7; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -6313,7 +6313,7 @@ "$(SRCROOT)", ); LLVM_LTO = NO; - MARKETING_VERSION = 1.11.20; + MARKETING_VERSION = 1.11.21; OTHER_LDFLAGS = "$(inherited)"; OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\""; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger"; @@ -6345,7 +6345,7 @@ CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 316; + CURRENT_PROJECT_VERSION = 319; DEVELOPMENT_TEAM = SUQ8J2PCT7; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -6384,7 +6384,7 @@ "$(SRCROOT)", ); LLVM_LTO = NO; - MARKETING_VERSION = 1.11.20; + MARKETING_VERSION = 1.11.21; OTHER_LDFLAGS = "$(inherited)"; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger"; PRODUCT_NAME = Session;