diff --git a/.swiftlint.yml b/.swiftlint.yml index dd5b13d3ca..6807813b82 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -40,7 +40,8 @@ opt_in_rules: - empty_string - sorted_first_last attributes: - always_on_line_above: ["@objc", "@nonobjc"] + always_on_line_above: ["@available", "@objc", "@nonobjc"] + attributes_with_arguments_always_on_line_above: false inclusive_language: override_allowed_terms: ["master", "whitelist"] large_tuple: diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index 9c412e2a98..400f2004cf 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -7,6 +7,10 @@ objects = { /* Begin PBXBuildFile section */ + 05104D162C88EC3A00F8851F /* Colors.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 05104D142C88CDB300F8851F /* Colors.xcassets */; }; + 05104D182C8A151100F8851F /* AsyncViewTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 05104D172C8A151100F8851F /* AsyncViewTask.swift */; }; + 05104E3A2C8B541000F8851F /* AccessibleLayoutMetric.swift in Sources */ = {isa = PBXBuildFile; fileRef = 05104E392C8B540C00F8851F /* AccessibleLayoutMetric.swift */; }; + 0510F69E2C91EB3000FA3FDE /* ScrollBounceBehaviorIfAvailable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0510F69D2C91EB2800FA3FDE /* ScrollBounceBehaviorIfAvailable.swift */; }; 0512145B2C5BCECF0021EEC9 /* CollectionDifference+SSK.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0512145A2C5BCECF0021EEC9 /* CollectionDifference+SSK.swift */; }; 0517B9782BFCFF12002CDE7D /* TSThreadTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0517B9772BFCFF12002CDE7D /* TSThreadTests.swift */; }; 052647C12C6404DD0076E99D /* ChatListFilterStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 052647C02C6404D70076E99D /* ChatListFilterStore.swift */; }; @@ -523,10 +527,6 @@ 4C8A6DFC22E5499300469AE7 /* MediaZoomAnimationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C8A6DFB22E5499300469AE7 /* MediaZoomAnimationController.swift */; }; 4C8A6DFE22E54AFA00469AE7 /* MediaInteractiveDismiss.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C8A6DFD22E54AFA00469AE7 /* MediaInteractiveDismiss.swift */; }; 4C9D347B23679C25006A4307 /* ContactStreamTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9D347923679C13006A4307 /* ContactStreamTest.swift */; }; - 4C9D34972369F0FC006A4307 /* notificationPermission.json in Resources */ = {isa = PBXBuildFile; fileRef = 4C9D34962369F0FC006A4307 /* notificationPermission.json */; }; - 4C9D349B2369F11F006A4307 /* notificationPermission1.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C9D34982369F11E006A4307 /* notificationPermission1.png */; }; - 4C9D349C2369F11F006A4307 /* notificationPermission0.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C9D34992369F11E006A4307 /* notificationPermission0.png */; }; - 4C9D349D2369F11F006A4307 /* notificationPermission2.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C9D349A2369F11F006A4307 /* notificationPermission2.png */; }; 4CA46F4C219CCC630038ABDE /* MediaCaptionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CA46F4B219CCC630038ABDE /* MediaCaptionView.swift */; }; 4CA485BB2232339F004B9E7D /* PhotoCaptureViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CA485BA2232339F004B9E7D /* PhotoCaptureViewController.swift */; }; 4CB5F26720F6E1E2004D1B42 /* MessageActionsToolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CFF4C0920F55BBA005DA313 /* MessageActionsToolbar.swift */; }; @@ -3100,7 +3100,7 @@ F9D5BFCD2979A017001737E5 /* OWSRequestFactory+Spam.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9D5BFCC2979A017001737E5 /* OWSRequestFactory+Spam.swift */; }; F9D5BFCF2979AFF4001737E5 /* URLPathComponents.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9D5BFCE2979AFF4001737E5 /* URLPathComponents.swift */; }; F9D5BFD12979B027001737E5 /* URLPathComponentsTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9D5BFD02979B027001737E5 /* URLPathComponentsTest.swift */; }; - F9D5C39F2993F9FF004891FC /* RegistrationPermissionsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9D5C39E2993F9FF004891FC /* RegistrationPermissionsViewController.swift */; }; + F9D5C39F2993F9FF004891FC /* RegistrationPermissionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9D5C39E2993F9FF004891FC /* RegistrationPermissionsView.swift */; }; F9D83012282DBB1500399363 /* BadgeGiftingChooseBadgeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9D83011282DBB1500399363 /* BadgeGiftingChooseBadgeViewController.swift */; }; F9DD70B92811AF82000C5960 /* DonationViewsUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9DD70B82811AF82000C5960 /* DonationViewsUtil.swift */; }; F9E3006129A02D8800DCA219 /* RegistrationPinViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9E3006029A02D8800DCA219 /* RegistrationPinViewController.swift */; }; @@ -3246,6 +3246,10 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 05104D142C88CDB300F8851F /* Colors.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Colors.xcassets; sourceTree = ""; }; + 05104D172C8A151100F8851F /* AsyncViewTask.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncViewTask.swift; sourceTree = ""; }; + 05104E392C8B540C00F8851F /* AccessibleLayoutMetric.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessibleLayoutMetric.swift; sourceTree = ""; }; + 0510F69D2C91EB2800FA3FDE /* ScrollBounceBehaviorIfAvailable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollBounceBehaviorIfAvailable.swift; sourceTree = ""; }; 0512145A2C5BCECF0021EEC9 /* CollectionDifference+SSK.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CollectionDifference+SSK.swift"; sourceTree = ""; }; 0517B9772BFCFF12002CDE7D /* TSThreadTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TSThreadTests.swift; sourceTree = ""; }; 052647C02C6404D70076E99D /* ChatListFilterStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatListFilterStore.swift; sourceTree = ""; }; @@ -3846,10 +3850,6 @@ 4C9C50FF22F495F60054A33F /* TSAttachmentMultisendJob.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TSAttachmentMultisendJob.swift; sourceTree = ""; }; 4C9D347923679C13006A4307 /* ContactStreamTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactStreamTest.swift; sourceTree = ""; }; 4C9D347E23689E06006A4307 /* IncomingContactSyncJobQueue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IncomingContactSyncJobQueue.swift; sourceTree = ""; }; - 4C9D34962369F0FC006A4307 /* notificationPermission.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = notificationPermission.json; sourceTree = ""; }; - 4C9D34982369F11E006A4307 /* notificationPermission1.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = notificationPermission1.png; sourceTree = ""; }; - 4C9D34992369F11E006A4307 /* notificationPermission0.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = notificationPermission0.png; sourceTree = ""; }; - 4C9D349A2369F11F006A4307 /* notificationPermission2.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = notificationPermission2.png; sourceTree = ""; }; 4CA46F4B219CCC630038ABDE /* MediaCaptionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaCaptionView.swift; sourceTree = ""; }; 4CA485BA2232339F004B9E7D /* PhotoCaptureViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoCaptureViewController.swift; sourceTree = ""; }; 4CB5F26820F7D060004D1B42 /* MessageActions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageActions.swift; sourceTree = ""; }; @@ -6502,7 +6502,7 @@ F9D5BFCC2979A017001737E5 /* OWSRequestFactory+Spam.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OWSRequestFactory+Spam.swift"; sourceTree = ""; }; F9D5BFCE2979AFF4001737E5 /* URLPathComponents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLPathComponents.swift; sourceTree = ""; }; F9D5BFD02979B027001737E5 /* URLPathComponentsTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLPathComponentsTest.swift; sourceTree = ""; }; - F9D5C39E2993F9FF004891FC /* RegistrationPermissionsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegistrationPermissionsViewController.swift; sourceTree = ""; }; + F9D5C39E2993F9FF004891FC /* RegistrationPermissionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegistrationPermissionsView.swift; sourceTree = ""; }; F9D83011282DBB1500399363 /* BadgeGiftingChooseBadgeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BadgeGiftingChooseBadgeViewController.swift; sourceTree = ""; }; F9DD70B82811AF82000C5960 /* DonationViewsUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DonationViewsUtil.swift; sourceTree = ""; }; F9E3006029A02D8800DCA219 /* RegistrationPinViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegistrationPinViewController.swift; sourceTree = ""; }; @@ -7659,25 +7659,6 @@ path = MediaGallery; sourceTree = ""; }; - 4C9D34842369EA3E006A4307 /* NotificationPermission */ = { - isa = PBXGroup; - children = ( - 4C9D348B2369EA69006A4307 /* images */, - 4C9D34962369F0FC006A4307 /* notificationPermission.json */, - ); - path = NotificationPermission; - sourceTree = ""; - }; - 4C9D348B2369EA69006A4307 /* images */ = { - isa = PBXGroup; - children = ( - 4C9D34992369F11E006A4307 /* notificationPermission0.png */, - 4C9D34982369F11E006A4307 /* notificationPermission1.png */, - 4C9D349A2369F11F006A4307 /* notificationPermission2.png */, - ); - path = images; - sourceTree = ""; - }; 4CD675BF22E7BE47008010D2 /* Transitions */ = { isa = PBXGroup; children = ( @@ -8938,7 +8919,7 @@ 6659CCB029CD4650000C24C0 /* RegistrationConfirmModeSwitchViewController.swift */, F92E4C73299E9A0100C6E6C7 /* RegistrationLoadingViewController.swift */, F95A64F2299589CA007FDBDF /* RegistrationNavigationController.swift */, - F9D5C39E2993F9FF004891FC /* RegistrationPermissionsViewController.swift */, + F9D5C39E2993F9FF004891FC /* RegistrationPermissionsView.swift */, F905DFEA29A534F200BAD034 /* RegistrationPhoneNumberDiscoverabilityViewController.swift */, F9198484299AA7FC007FD5E4 /* RegistrationPhoneNumberInputView.swift */, F95A64F429959065007FDBDF /* RegistrationPhoneNumberViewController.swift */, @@ -9964,7 +9945,6 @@ isa = PBXGroup; children = ( 888017822741E5A500346E9A /* Boost */, - 4C9D34842369EA3E006A4307 /* NotificationPermission */, 34848D5B25D43ADD00E5034B /* about-mobilecoin.json */, 34848D5C25D43ADD00E5034B /* activate-payments.json */, 34848D5D25D43ADD00E5034B /* add-money.json */, @@ -10259,6 +10239,9 @@ B9D721742C87B8CB007EDA85 /* SwiftUIExtensions */ = { isa = PBXGroup; children = ( + 05104E392C8B540C00F8851F /* AccessibleLayoutMetric.swift */, + 05104D172C8A151100F8851F /* AsyncViewTask.swift */, + 0510F69D2C91EB2800FA3FDE /* ScrollBounceBehaviorIfAvailable.swift */, B9D721752C87B8EB007EDA85 /* SwiftUI+Animations.swift */, ); path = SwiftUIExtensions; @@ -10470,6 +10453,7 @@ 5033D46C29DCA8DE007FEADA /* URLs */, D99840BB297A04A300F7ED6D /* Usernames */, 052D17872C7E34D00023D56F /* AppIcon.xcassets */, + 05104D142C88CDB300F8851F /* Colors.xcassets */, B66DBF4919D5BBC8006EA940 /* Images.xcassets */, F0C124B626D4788A0031C96F /* NSE-Images.xcassets */, 881FF30623B5B1520023B620 /* Signal-AppStore.entitlements */, @@ -13363,7 +13347,7 @@ attributes = { BuildIndependentTargetsInParallel = YES; DefaultBuildSystemTypeForWorkspace = Original; - LastSwiftUpdateCheck = 1540; + LastSwiftUpdateCheck = 1600; LastTestingUpgradeCheck = 0600; LastUpgradeCheck = 1540; ORGANIZATIONNAME = "Open Whisper Systems"; @@ -13602,6 +13586,7 @@ 45B74A832044AAB600CD42F8 /* circles.aifc in Resources */, 4503F1BE20470A5B00CEE724 /* classic-quiet.aifc in Resources */, 4503F1BF20470A5B00CEE724 /* classic.aifc in Resources */, + 05104D162C88EC3A00F8851F /* Colors.xcassets in Resources */, 45B74A872044AAB600CD42F8 /* complete-quiet.aifc in Resources */, 45B74A7E2044AAB600CD42F8 /* complete.aifc in Resources */, 880FB3EE28CA53D400FA1C10 /* determinate_spinner_44.json in Resources */, @@ -13631,10 +13616,6 @@ 45B74A7F2044AAB600CD42F8 /* note-quiet.aifc in Resources */, 45B74A862044AAB600CD42F8 /* note.aifc in Resources */, B9B89EED2C064E760093A2FA /* notification_simple-01.caf in Resources */, - 4C9D34972369F0FC006A4307 /* notificationPermission.json in Resources */, - 4C9D349C2369F11F006A4307 /* notificationPermission0.png in Resources */, - 4C9D349B2369F11F006A4307 /* notificationPermission1.png in Resources */, - 4C9D349D2369F11F006A4307 /* notificationPermission2.png in Resources */, 3406D32E25DD80D600885B14 /* payments_spinner.json in Resources */, 3406D33225DD832800885B14 /* payments_spinner_dark.json in Resources */, 3406D32B25DD80D600885B14 /* payments_spinner_fail.json in Resources */, @@ -14341,11 +14322,13 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 05104E3A2C8B541000F8851F /* AccessibleLayoutMetric.swift in Sources */, 3402AA35271D9DCD0084CBAE /* ActionSheetController.swift in Sources */, 887F898228FF32A600D3B78E /* AllSignalConnectionsViewController.swift in Sources */, 342FFE62271DB2E7000AC89F /* AppContext+SignalUI.swift in Sources */, 3402AA4E271D9DCD0084CBAE /* ApprovalFooterView.swift in Sources */, 3402AA3F271D9DCD0084CBAE /* ApprovalRailCellView.swift in Sources */, + 05104D182C8A151100F8851F /* AsyncViewTask.swift in Sources */, 3402AA4B271D9DCD0084CBAE /* AttachmentApprovalToolbar.swift in Sources */, 763D7DDD27E25DC8002EA7E6 /* AttachmentApprovalTopBar.swift in Sources */, 3402AA4A271D9DCD0084CBAE /* AttachmentApprovalViewController.swift in Sources */, @@ -14518,6 +14501,7 @@ 88B987022880890800F8C74D /* SafetyNumberConfirmationSheet.swift in Sources */, 88B9870928808A8A00F8C74D /* ScanQRCodeViewController.swift in Sources */, 7677E41129F7A60500AC6A75 /* ScreenLockViewController.swift in Sources */, + 0510F69E2C91EB3000FA3FDE /* ScrollBounceBehaviorIfAvailable.swift in Sources */, 50597BBF2B97D629004681E1 /* SearchableNameFinder.swift in Sources */, 66FC638E29EDABAC00F00DAC /* SearchDisplayConfigurations.swift in Sources */, 66FBC4E328DA82AA00BD9E8B /* SelectMyStoryRecipientsViewController.swift in Sources */, @@ -15183,7 +15167,7 @@ F92E4C74299E9A0100C6E6C7 /* RegistrationLoadingViewController.swift in Sources */, 66533E3A29B9502100E8D928 /* RegistrationMode.swift in Sources */, F95A64F3299589CA007FDBDF /* RegistrationNavigationController.swift in Sources */, - F9D5C39F2993F9FF004891FC /* RegistrationPermissionsViewController.swift in Sources */, + F9D5C39F2993F9FF004891FC /* RegistrationPermissionsView.swift in Sources */, F905DFEB29A534F200BAD034 /* RegistrationPhoneNumberDiscoverabilityViewController.swift in Sources */, F9198485299AA7FC007FD5E4 /* RegistrationPhoneNumberInputView.swift in Sources */, F95A64F529959065007FDBDF /* RegistrationPhoneNumberViewController.swift in Sources */, @@ -18057,6 +18041,7 @@ buildSettings = { ASSETCATALOG_COMPILER_ALTERNATE_APPICON_NAMES = "AppIcon-color AppIcon-bubbles AppIcon-white AppIcon-dark AppIcon-dark-variant AppIcon-chat AppIcon-yellow AppIcon-news AppIcon-notes AppIcon-weather AppIcon-wave"; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ADDRESS_SANITIZER_CONTAINER_OVERFLOW = NO; CLANG_ANALYZER_SECURITY_FLOATLOOPCOUNTER = YES; CLANG_ANALYZER_SECURITY_INSECUREAPI_RAND = YES; @@ -18310,6 +18295,7 @@ buildSettings = { ASSETCATALOG_COMPILER_ALTERNATE_APPICON_NAMES = "AppIcon-color AppIcon-bubbles AppIcon-white AppIcon-dark AppIcon-dark-variant AppIcon-chat AppIcon-yellow AppIcon-news AppIcon-notes AppIcon-weather AppIcon-wave"; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ADDRESS_SANITIZER_CONTAINER_OVERFLOW = NO; CLANG_ANALYZER_SECURITY_FLOATLOOPCOUNTER = YES; CLANG_ANALYZER_SECURITY_INSECUREAPI_RAND = YES; @@ -18655,6 +18641,7 @@ buildSettings = { ASSETCATALOG_COMPILER_ALTERNATE_APPICON_NAMES = "AppIcon-color AppIcon-bubbles AppIcon-white AppIcon-dark AppIcon-dark-variant AppIcon-chat AppIcon-yellow AppIcon-news AppIcon-notes AppIcon-weather AppIcon-wave"; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ADDRESS_SANITIZER_CONTAINER_OVERFLOW = YES; CLANG_ANALYZER_SECURITY_FLOATLOOPCOUNTER = YES; CLANG_ANALYZER_SECURITY_INSECUREAPI_RAND = YES; @@ -18701,6 +18688,7 @@ buildSettings = { ASSETCATALOG_COMPILER_ALTERNATE_APPICON_NAMES = "AppIcon-color AppIcon-bubbles AppIcon-white AppIcon-dark AppIcon-dark-variant AppIcon-chat AppIcon-yellow AppIcon-news AppIcon-notes AppIcon-weather AppIcon-wave"; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ADDRESS_SANITIZER_CONTAINER_OVERFLOW = NO; CLANG_ANALYZER_SECURITY_FLOATLOOPCOUNTER = YES; CLANG_ANALYZER_SECURITY_INSECUREAPI_RAND = YES; diff --git a/Signal/Colors.xcassets/Contents.json b/Signal/Colors.xcassets/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/Signal/Colors.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Signal/Colors.xcassets/Signal/Background/Contents.json b/Signal/Colors.xcassets/Signal/Background/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/Signal/Colors.xcassets/Signal/Background/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Signal/Colors.xcassets/Signal/Background/background.colorset/Contents.json b/Signal/Colors.xcassets/Signal/Background/background.colorset/Contents.json new file mode 100644 index 0000000000..9c0e331e97 --- /dev/null +++ b/Signal/Colors.xcassets/Signal/Background/background.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xFF", + "green" : "0xFF", + "red" : "0xFF" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x00", + "green" : "0x00", + "red" : "0x00" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Signal/Colors.xcassets/Signal/Background/secondaryBackground.colorset/Contents.json b/Signal/Colors.xcassets/Signal/Background/secondaryBackground.colorset/Contents.json new file mode 100644 index 0000000000..5ae042d914 --- /dev/null +++ b/Signal/Colors.xcassets/Signal/Background/secondaryBackground.colorset/Contents.json @@ -0,0 +1,78 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xF0", + "green" : "0xEF", + "red" : "0xEF" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x1E", + "green" : "0x1C", + "red" : "0x1C" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "contrast", + "value" : "high" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xE7", + "green" : "0xE4", + "red" : "0xE4" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + }, + { + "appearance" : "contrast", + "value" : "high" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x38", + "green" : "0x34", + "red" : "0x34" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Signal/Colors.xcassets/Signal/Contents.json b/Signal/Colors.xcassets/Signal/Contents.json new file mode 100644 index 0000000000..6e965652df --- /dev/null +++ b/Signal/Colors.xcassets/Signal/Contents.json @@ -0,0 +1,9 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "provides-namespace" : true + } +} diff --git a/Signal/Colors.xcassets/Signal/Label/Contents.json b/Signal/Colors.xcassets/Signal/Label/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/Signal/Colors.xcassets/Signal/Label/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Signal/Colors.xcassets/Signal/Label/label.colorset/Contents.json b/Signal/Colors.xcassets/Signal/Label/label.colorset/Contents.json new file mode 100644 index 0000000000..0c600f92f1 --- /dev/null +++ b/Signal/Colors.xcassets/Signal/Label/label.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x00", + "green" : "0x00", + "red" : "0x00" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xFF", + "green" : "0xFF", + "red" : "0xFF" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Signal/Colors.xcassets/Signal/Label/quaternaryLabel.colorset/Contents.json b/Signal/Colors.xcassets/Signal/Label/quaternaryLabel.colorset/Contents.json new file mode 100644 index 0000000000..d2d91bdfed --- /dev/null +++ b/Signal/Colors.xcassets/Signal/Label/quaternaryLabel.colorset/Contents.json @@ -0,0 +1,78 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "0.180", + "blue" : "0x43", + "green" : "0x3C", + "red" : "0x3C" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "0.160", + "blue" : "0xF5", + "green" : "0xEB", + "red" : "0xEB" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "contrast", + "value" : "high" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "0.400", + "blue" : "0x43", + "green" : "0x3C", + "red" : "0x3C" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + }, + { + "appearance" : "contrast", + "value" : "high" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "0.260", + "blue" : "0xF5", + "green" : "0xEB", + "red" : "0xEB" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Signal/Colors.xcassets/Signal/Label/secondaryLabel.colorset/Contents.json b/Signal/Colors.xcassets/Signal/Label/secondaryLabel.colorset/Contents.json new file mode 100644 index 0000000000..37ad6e2fa1 --- /dev/null +++ b/Signal/Colors.xcassets/Signal/Label/secondaryLabel.colorset/Contents.json @@ -0,0 +1,78 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "0.720", + "blue" : "0x43", + "green" : "0x3C", + "red" : "0x3C" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "0.700", + "blue" : "0xF5", + "green" : "0xEB", + "red" : "0xEB" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "contrast", + "value" : "high" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "0.950", + "blue" : "0x43", + "green" : "0x3C", + "red" : "0x3C" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + }, + { + "appearance" : "contrast", + "value" : "high" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "0.800", + "blue" : "0xF5", + "green" : "0xEB", + "red" : "0xEB" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Signal/Colors.xcassets/Signal/Label/tertiaryLabel.colorset/Contents.json b/Signal/Colors.xcassets/Signal/Label/tertiaryLabel.colorset/Contents.json new file mode 100644 index 0000000000..dcbdd42d47 --- /dev/null +++ b/Signal/Colors.xcassets/Signal/Label/tertiaryLabel.colorset/Contents.json @@ -0,0 +1,78 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "0.300", + "blue" : "0x43", + "green" : "0x3C", + "red" : "0x3C" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "0.300", + "blue" : "0xF5", + "green" : "0xEB", + "red" : "0xEB" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "contrast", + "value" : "high" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "0.500", + "blue" : "0x43", + "green" : "0x3C", + "red" : "0x3C" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + }, + { + "appearance" : "contrast", + "value" : "high" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "0.400", + "blue" : "0xF5", + "green" : "0xEB", + "red" : "0xEB" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Signal/Images.xcassets/bell-ring.imageset/Contents.json b/Signal/Images.xcassets/bell-ring.imageset/Contents.json new file mode 100644 index 0000000000..bbce305b26 --- /dev/null +++ b/Signal/Images.xcassets/bell-ring.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "bell-ring.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Signal/Images.xcassets/bell-ring.imageset/bell-ring.pdf b/Signal/Images.xcassets/bell-ring.imageset/bell-ring.pdf new file mode 100644 index 0000000000..4203c47705 Binary files /dev/null and b/Signal/Images.xcassets/bell-ring.imageset/bell-ring.pdf differ diff --git a/Signal/Images.xcassets/person-circle-large.imageset/Contents.json b/Signal/Images.xcassets/person-circle-large.imageset/Contents.json new file mode 100644 index 0000000000..88fe2473c4 --- /dev/null +++ b/Signal/Images.xcassets/person-circle-large.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "person-circle.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Signal/Images.xcassets/person-circle-large.imageset/person-circle.pdf b/Signal/Images.xcassets/person-circle-large.imageset/person-circle.pdf new file mode 100644 index 0000000000..8a22f7b1fa Binary files /dev/null and b/Signal/Images.xcassets/person-circle-large.imageset/person-circle.pdf differ diff --git a/Signal/Lottie/NotificationPermission/images/notificationPermission0.png b/Signal/Lottie/NotificationPermission/images/notificationPermission0.png deleted file mode 100644 index 08a30c6cdc..0000000000 Binary files a/Signal/Lottie/NotificationPermission/images/notificationPermission0.png and /dev/null differ diff --git a/Signal/Lottie/NotificationPermission/images/notificationPermission1.png b/Signal/Lottie/NotificationPermission/images/notificationPermission1.png deleted file mode 100644 index cad76c826a..0000000000 Binary files a/Signal/Lottie/NotificationPermission/images/notificationPermission1.png and /dev/null differ diff --git a/Signal/Lottie/NotificationPermission/images/notificationPermission2.png b/Signal/Lottie/NotificationPermission/images/notificationPermission2.png deleted file mode 100644 index 7d3fe8a54d..0000000000 Binary files a/Signal/Lottie/NotificationPermission/images/notificationPermission2.png and /dev/null differ diff --git a/Signal/Lottie/NotificationPermission/notificationPermission.json b/Signal/Lottie/NotificationPermission/notificationPermission.json deleted file mode 100644 index 9d2fec853a..0000000000 --- a/Signal/Lottie/NotificationPermission/notificationPermission.json +++ /dev/null @@ -1 +0,0 @@ -{"v":"5.5.2","fr":60,"ip":0,"op":180,"w":1080,"h":600,"nm":"Comp 1","ddd":0,"assets":[{"id":"image_0","w":820,"h":232,"u":"images/","p":"notificationPermission0.png","e":0},{"id":"image_1","w":820,"h":232,"u":"images/","p":"notificationPermission1.png","e":0},{"id":"image_2","w":820,"h":232,"u":"images/","p":"notificationPermission2.png","e":0}],"layers":[{"ddd":0,"ind":1,"ty":2,"nm":"Signal Message/notifications.ai","cl":"ai","refId":"image_0","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":107,"s":[0]},{"t":116,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[540,110,0],"ix":2},"a":{"a":0,"k":[410,116,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.605,0.605,0.333],"y":[0.517,0.517,0]},"t":107,"s":[87,87,100]},{"i":{"x":[0.534,0.534,0.667],"y":[0.566,0.566,1]},"o":{"x":[0.351,0.351,0.333],"y":[2.108,2.108,0]},"t":116,"s":[101,101,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.448,0.448,0.333],"y":[-0.816,-0.816,0]},"t":125,"s":[99.5,99.5,100]},{"t":131,"s":[100,100,100]}],"ix":6}},"ao":0,"ip":0,"op":186,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":2,"nm":"Message 2/notifications.ai","cl":"ai","refId":"image_1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":64,"s":[540,110,0],"to":[0,0,0],"ti":[0,0,0]},{"t":72,"s":[540,304,0]}],"ix":2},"a":{"a":0,"k":[410,116,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"ip":0,"op":187,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":2,"nm":"Message 3/notifications.ai","cl":"ai","refId":"image_2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":64,"s":[540,308,0],"to":[0,0,0],"ti":[0,0,0]},{"t":72,"s":[540,502,0]}],"ix":2},"a":{"a":0,"k":[410,116,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"ip":0,"op":185,"st":0,"bm":0}],"markers":[]} diff --git a/Signal/Registration/RegistrationCoordinatorImpl.swift b/Signal/Registration/RegistrationCoordinatorImpl.swift index 0380dd1228..05abed8a22 100644 --- a/Signal/Registration/RegistrationCoordinatorImpl.swift +++ b/Signal/Registration/RegistrationCoordinatorImpl.swift @@ -1231,7 +1231,7 @@ public class RegistrationCoordinatorImpl: RegistrationCoordinator { if inMemoryState.needsSomePermissions { // This class is only used for primary device registration // which always needs contacts permissions. - return .value(.permissions(RegistrationPermissionsState(shouldRequestAccessToContacts: true))) + return .value(.permissions) } if inMemoryState.hasEnteredE164, let e164 = persistedState.e164 { return self.startSession(e164: e164) diff --git a/Signal/Registration/RegistrationStep.swift b/Signal/Registration/RegistrationStep.swift index ae77071025..c8db3e1007 100644 --- a/Signal/Registration/RegistrationStep.swift +++ b/Signal/Registration/RegistrationStep.swift @@ -10,7 +10,7 @@ public enum RegistrationStep: Equatable { // MARK: - Opening Steps case registrationSplash case changeNumberSplash - case permissions(RegistrationPermissionsState) + case permissions // MARK: - Actually registering diff --git a/Signal/Registration/UserInterface/RegistrationNavigationController.swift b/Signal/Registration/UserInterface/RegistrationNavigationController.swift index 726ee81f6a..a09c09c221 100644 --- a/Signal/Registration/UserInterface/RegistrationNavigationController.swift +++ b/Signal/Registration/UserInterface/RegistrationNavigationController.swift @@ -144,12 +144,10 @@ public class RegistrationNavigationController: OWSNavigationController { // No state to update. update: nil ) - case .permissions(let state): + case .permissions: return Controller( type: RegistrationPermissionsViewController.self, - make: { presenter in - return RegistrationPermissionsViewController(state: state, presenter: presenter) - }, + make: RegistrationPermissionsViewController.init(presenter:), // The state never changes here. In theory we would build // state update support in the permissions controller, // but its overkill so we have not. @@ -419,11 +417,10 @@ extension RegistrationNavigationController: RegistrationConfimModeSwitchPresente extension RegistrationNavigationController: RegistrationChangeNumberSplashPresenter {} extension RegistrationNavigationController: RegistrationPermissionsPresenter { - - func requestPermissions() -> Guarantee { + func requestPermissions() async { let guarantee = coordinator.requestPermissions() pushNextController(guarantee, loadingMode: nil) - return guarantee.asVoid() + await guarantee.asVoid().awaitable() } } diff --git a/Signal/Registration/UserInterface/RegistrationPermissionsView.swift b/Signal/Registration/UserInterface/RegistrationPermissionsView.swift new file mode 100644 index 0000000000..2c7fe09124 --- /dev/null +++ b/Signal/Registration/UserInterface/RegistrationPermissionsView.swift @@ -0,0 +1,197 @@ +// +// Copyright 2023 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only +// + +import SignalServiceKit +import SignalUI +import SwiftUI + +@MainActor +protocol RegistrationPermissionsPresenter { + func requestPermissions() async +} + +final class RegistrationPermissionsViewController: UIHostingController { + init(presenter: any RegistrationPermissionsPresenter) { + super.init(rootView: RegistrationPermissionsView(presenter: presenter)) + } + + @available(*, unavailable) + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +struct RegistrationPermissionsView: View { + var presenter: any RegistrationPermissionsPresenter + @State private var requestPermissions: RequestPermissionsTask? + + @Environment(\.horizontalSizeClass) private var horizontalSizeClass + @AccessibleLayoutMetric private var headerPadding = 16 + @AccessibleLayoutMetric private var headerSpacing = 12 + @AccessibleLayoutMetric(scale: 0.5) private var sectionSpacing = 64 + + var body: some View { + VStack { + VStack(spacing: headerSpacing) { + Text(OWSLocalizedString("ONBOARDING_PERMISSIONS_TITLE", comment: "Title of the 'onboarding permissions' view.")) + .font(.title.weight(.semibold)) + .lineLimit(1) + Text(OWSLocalizedString("ONBOARDING_PERMISSIONS_PREAMBLE", comment: "Preamble of the 'onboarding permissions' view.")) + .dynamicTypeSize(...DynamicTypeSize.accessibility1) + } + .multilineTextAlignment(.center) + .padding(.horizontal, headerPadding) + + ScrollView { + VStack { + Spacer(minLength: sectionSpacing) + .frame(maxHeight: $sectionSpacing.rawValue) + .layoutPriority(-1) + + VStack(alignment: .leading, spacing: 32) { + PermissionDescription { + Text(OWSLocalizedString("ONBOARDING_PERMISSIONS_NOTIFICATIONS_TITLE", comment: "Title introducing the 'Notifications' permission in the 'onboarding permissions' view.")) + } description: { + Text(OWSLocalizedString("ONBOARDING_PERMISSIONS_NOTIFICATIONS_DESCRIPTION", comment: "Description of the 'Notifications' permission in the 'onboarding permissions' view.")) + } icon: { + PermissionIcon(.bellRing) + } + + PermissionDescription { + Text(OWSLocalizedString("ONBOARDING_PERMISSIONS_CONTACTS_TITLE", comment: "Title introducing the 'Contacts' permission in the 'onboarding permissions' view.")) + } description: { + Text(OWSLocalizedString("ONBOARDING_PERMISSIONS_CONTACTS_DESCRIPTION", comment: "Description of the 'Contacts' permission in the 'onboarding permissions' view.")) + } icon: { + PermissionIcon(.personCircleLarge) + } + } + + Spacer(minLength: sectionSpacing) + .layoutPriority(-1) + + Button(CommonStrings.continueButton) { + requestPermissions = RequestPermissionsTask(presenter: presenter) + } + .buttonStyle(ContinueButtonStyle()) + .dynamicTypeSize(...DynamicTypeSize.accessibility2) + .frame(maxWidth: 400) + } + .padding(EdgeInsets(.layoutMarginsForRegistration(UIUserInterfaceSizeClass(horizontalSizeClass)))) + } + .scrollBounceBehaviorIfAvailable(.basedOnSize) + } + .foregroundStyle(Color.Signal.label, Color.Signal.secondaryLabel, Color.Signal.tertiaryLabel) + .dynamicTypeSize(...DynamicTypeSize.accessibility3) + .minimumScaleFactor(0.9) + .navigationBarBackButtonHidden() + .task($requestPermissions.animation()) + // FIXME: Forcing light mode for consistency with the rest of registration + .background(Color.Signal.background) + .environment(\.colorScheme, .light) + } +} + +private extension RegistrationPermissionsView { + struct ContinueButtonStyle: PrimitiveButtonStyle { + func makeBody(configuration: Configuration) -> some View { + Button(action: configuration.trigger) { + HStack { + Spacer() + configuration.label + .colorScheme(.dark) + .font(.headline) + Spacer() + } + .frame(minHeight: 32) + } + .buttonStyle(.borderedProminent) + } + } + + struct PermissionDescription: View { + var icon: Icon + var title: Title + var description: Description + + @Environment(\.dynamicTypeSize) private var dynamicTypeSize + + init(@ViewBuilder title: () -> Title, @ViewBuilder description: () -> Description, @ViewBuilder icon: () -> Icon) { + self.title = title() + self.description = description() + self.icon = icon() + } + + var body: some View { + if dynamicTypeSize.isAccessibilitySize { + VStack(alignment: .leading, spacing: 4) { + HStack(spacing: 16) { + icon + .frame(width: 36) + title + .font(.headline) + .lineLimit(1) + Spacer() + } + description + .font(.callout) + .foregroundStyle(.secondary) + } + } else { + HStack(alignment: .top, spacing: 16) { + icon + VStack(alignment: .leading, spacing: 4) { + title + .font(.headline) + .lineLimit(1) + description + .font(.callout) + .foregroundStyle(.secondary) + .layoutPriority(1) + } + Spacer() + } + } + } + } + + struct PermissionIcon: View { + var resource: ImageResource + + init(_ resource: ImageResource) { + self.resource = resource + } + + var body: some View { + Image(resource) + .resizable() + .aspectRatio(1, contentMode: .fit) + .frame(maxWidth: 48) + } + } + + struct RequestPermissionsTask: AsyncViewTask { + let id = UUID() + let presenter: any RegistrationPermissionsPresenter + + func perform() async { + await presenter.requestPermissions() + } + } +} + +#if DEBUG +private struct PreviewPermissionsPresenter: RegistrationPermissionsPresenter { + func requestPermissions() async { + try? await Task.sleep(nanoseconds: NSEC_PER_SEC) + } +} + +#Preview { + VStack { + Color.clear.frame(height: 44) + RegistrationPermissionsView(presenter: PreviewPermissionsPresenter()) + } +} +#endif diff --git a/Signal/Registration/UserInterface/RegistrationPermissionsViewController.swift b/Signal/Registration/UserInterface/RegistrationPermissionsViewController.swift deleted file mode 100644 index 15de63454d..0000000000 --- a/Signal/Registration/UserInterface/RegistrationPermissionsViewController.swift +++ /dev/null @@ -1,180 +0,0 @@ -// -// Copyright 2023 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only -// - -import Lottie -import SignalServiceKit -import SignalUI - -// MARK: - RegistrationPermissionsState - -public struct RegistrationPermissionsState: Equatable { - let shouldRequestAccessToContacts: Bool -} - -// MARK: - RegistrationPermissionsPresenter - -protocol RegistrationPermissionsPresenter: AnyObject { - func requestPermissions() -> Guarantee -} - -// MARK: - RegistrationPermissionsViewController - -class RegistrationPermissionsViewController: OWSViewController { - private let state: RegistrationPermissionsState - private weak var presenter: RegistrationPermissionsPresenter? - - public init( - state: RegistrationPermissionsState, - presenter: RegistrationPermissionsPresenter? - ) { - self.state = state - self.presenter = presenter - - super.init() - } - - @available(*, unavailable) - public override init() { - owsFail("This should not be called") - } - - // MARK: Rendering - - private lazy var animationView: LottieAnimationView = { - let animationView = LottieAnimationView(name: "notificationPermission") - animationView.loopMode = .playOnce - animationView.backgroundBehavior = .pauseAndRestore - animationView.contentMode = .scaleAspectFit - return animationView - }() - - private var giveAccessButton: OWSFlatButton? - - public override func viewDidLoad() { - super.viewDidLoad() - - navigationItem.setHidesBackButton(true, animated: false) - - view.backgroundColor = Theme.backgroundColor - - let scrollView = UIScrollView() - view.addSubview(scrollView) - scrollView.autoPinEdgesToSuperviewEdges() - - let stackView = UIStackView() - stackView.axis = .vertical - stackView.alignment = .fill - stackView.layoutMargins = UIEdgeInsets.layoutMarginsForRegistration( - traitCollection.horizontalSizeClass - ) - stackView.isLayoutMarginsRelativeArrangement = true - stackView.setContentHuggingHigh() - scrollView.addSubview(stackView) - stackView.autoPinEdgesToSuperviewEdges() - stackView.autoMatch( - .width, - to: .width, - of: view, - withOffset: -view.layoutMargins.totalWidth, - relation: .equal - ) - let heightConstraint = stackView.heightAnchor.constraint( - greaterThanOrEqualTo: view.layoutMarginsGuide.heightAnchor - ) - heightConstraint.isActive = true - - let titleText: String - let explanationText: String - let giveAccessText: String - if state.shouldRequestAccessToContacts { - titleText = OWSLocalizedString( - "ONBOARDING_PERMISSIONS_TITLE", - comment: "Title of the 'onboarding permissions' view." - ) - explanationText = OWSLocalizedString( - "ONBOARDING_PERMISSIONS_EXPLANATION", - comment: "Explanation in the 'onboarding permissions' view." - ) - giveAccessText = OWSLocalizedString( - "ONBOARDING_PERMISSIONS_ENABLE_PERMISSIONS_BUTTON", - comment: "Label for the 'give access' button in the 'onboarding permissions' view." - ) - } else { - titleText = OWSLocalizedString( - "LINKED_ONBOARDING_PERMISSIONS_TITLE", - comment: "Title of the 'onboarding permissions' view." - ) - explanationText = OWSLocalizedString( - "LINKED_ONBOARDING_PERMISSIONS_EXPLANATION", - comment: "Explanation in the 'onboarding permissions' view." - ) - giveAccessText = OWSLocalizedString( - "LINKED_ONBOARDING_PERMISSIONS_ENABLE_PERMISSIONS_BUTTON", - comment: "Label for the 'give access' button in the 'onboarding permissions' view." - ) - } - - let titleLabel = UILabel.titleLabelForRegistration(text: titleText) - titleLabel.accessibilityIdentifier = "registration.permissions.titleLabel" - titleLabel.setCompressionResistanceVerticalHigh() - stackView.addArrangedSubview(titleLabel) - stackView.setCustomSpacing(20, after: titleLabel) - - let explanationLabel = UILabel.explanationLabelForRegistration(text: explanationText) - explanationLabel.accessibilityIdentifier = "registration.permissions.explanationLabel" - explanationLabel.setCompressionResistanceVerticalHigh() - stackView.addArrangedSubview(explanationLabel) - stackView.setCustomSpacing(60, after: explanationLabel) - - stackView.addArrangedSubview(animationView) - animationView.setContentHuggingHigh() - let animationSize = animationView.intrinsicContentSize - animationView.autoPin(toAspectRatio: animationSize.width / animationSize.height) - - stackView.addArrangedSubview(UIView.vStretchingSpacer(minHeight: 60)) - - let giveAccessButton = OWSFlatButton.primaryButtonForRegistration( - title: giveAccessText, - target: self, - selector: #selector(giveAccessPressed) - ) - giveAccessButton.accessibilityIdentifier = "registration.permissions.giveAccessButton" - stackView.addArrangedSubview(giveAccessButton) - giveAccessButton.autoHCenterInSuperview() - NSLayoutConstraint.autoSetPriority(.defaultLow) { - giveAccessButton.autoPinEdge(toSuperviewEdge: .leading) - giveAccessButton.autoPinEdge(toSuperviewEdge: .trailing) - } - NSLayoutConstraint.autoSetPriority(.required) { - giveAccessButton.autoSetDimension(.width, toSize: 280, relation: .greaterThanOrEqual) - giveAccessButton.autoSetDimension(.height, toSize: 50, relation: .greaterThanOrEqual) - } - self.giveAccessButton = giveAccessButton - } - - public override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - - animationView.play() - } - - // MARK: Events - - @objc - private func giveAccessPressed() { - Logger.info("") - - requestPermissions() - } - - // MARK: Requesting permissions - - private func requestPermissions() { - giveAccessButton?.setEnabled(false) - presenter?.requestPermissions().observe(on: DispatchQueue.main) { [weak self] _ in - self?.giveAccessButton?.setEnabled(true) - } - } -} diff --git a/Signal/Registration/UserInterface/RegistrationPinAttemptsExhaustedAndMustCreateNewPinViewController.swift b/Signal/Registration/UserInterface/RegistrationPinAttemptsExhaustedAndMustCreateNewPinViewController.swift index 0e54187f18..50187265f1 100644 --- a/Signal/Registration/UserInterface/RegistrationPinAttemptsExhaustedAndMustCreateNewPinViewController.swift +++ b/Signal/Registration/UserInterface/RegistrationPinAttemptsExhaustedAndMustCreateNewPinViewController.swift @@ -120,9 +120,7 @@ class RegistrationPinAttemptsExhaustedAndMustCreateNewPinViewController: OWSView let stackView = UIStackView() stackView.axis = .vertical - stackView.layoutMargins = UIEdgeInsets.layoutMarginsForRegistration( - traitCollection.horizontalSizeClass - ) + stackView.directionalLayoutMargins = .layoutMarginsForRegistration(traitCollection.horizontalSizeClass) stackView.isLayoutMarginsRelativeArrangement = true view.addSubview(stackView) diff --git a/Signal/Registration/UserInterface/RegistrationReglockTimeoutViewController.swift b/Signal/Registration/UserInterface/RegistrationReglockTimeoutViewController.swift index f555c39788..71c7040c08 100644 --- a/Signal/Registration/UserInterface/RegistrationReglockTimeoutViewController.swift +++ b/Signal/Registration/UserInterface/RegistrationReglockTimeoutViewController.swift @@ -164,9 +164,7 @@ class RegistrationReglockTimeoutViewController: OWSViewController { let stackView = UIStackView() stackView.axis = .vertical - stackView.layoutMargins = UIEdgeInsets.layoutMarginsForRegistration( - traitCollection.horizontalSizeClass - ) + stackView.directionalLayoutMargins = .layoutMarginsForRegistration(traitCollection.horizontalSizeClass) stackView.isLayoutMarginsRelativeArrangement = true view.addSubview(stackView) diff --git a/Signal/Registration/UserInterface/RegistrationSplashViewController.swift b/Signal/Registration/UserInterface/RegistrationSplashViewController.swift index e3f869a54a..816598b214 100644 --- a/Signal/Registration/UserInterface/RegistrationSplashViewController.swift +++ b/Signal/Registration/UserInterface/RegistrationSplashViewController.swift @@ -38,9 +38,9 @@ public class RegistrationSplashViewController: OWSViewController { let stackView = UIStackView() stackView.axis = .vertical stackView.alignment = .fill - stackView.layoutMargins = { + stackView.directionalLayoutMargins = { let horizontalSizeClass = traitCollection.horizontalSizeClass - var result = UIEdgeInsets.layoutMarginsForRegistration(horizontalSizeClass) + var result = NSDirectionalEdgeInsets.layoutMarginsForRegistration(horizontalSizeClass) // We want the hero image a bit closer to the top. result.top = 16 return result diff --git a/Signal/Registration/UserInterface/RegistrationVerificationViewController.swift b/Signal/Registration/UserInterface/RegistrationVerificationViewController.swift index a7e58e9a81..36d5d3d571 100644 --- a/Signal/Registration/UserInterface/RegistrationVerificationViewController.swift +++ b/Signal/Registration/UserInterface/RegistrationVerificationViewController.swift @@ -261,9 +261,7 @@ class RegistrationVerificationViewController: OWSViewController { let stackView = UIStackView() stackView.axis = .vertical stackView.spacing = 12 - stackView.layoutMargins = UIEdgeInsets.layoutMarginsForRegistration( - traitCollection.horizontalSizeClass - ) + stackView.directionalLayoutMargins = .layoutMarginsForRegistration(traitCollection.horizontalSizeClass) stackView.isLayoutMarginsRelativeArrangement = true stackView.setContentHuggingHigh() scrollView.addSubview(stackView) diff --git a/Signal/Registration/UserInterface/RegistrationViewUtil.swift b/Signal/Registration/UserInterface/RegistrationViewUtil.swift index 1b252fa28b..8bd01eb94d 100644 --- a/Signal/Registration/UserInterface/RegistrationViewUtil.swift +++ b/Signal/Registration/UserInterface/RegistrationViewUtil.swift @@ -18,17 +18,17 @@ extension String { // MARK: - Layout margins -extension UIEdgeInsets { +extension NSDirectionalEdgeInsets { static func layoutMarginsForRegistration( _ horizontalSizeClass: UIUserInterfaceSizeClass - ) -> UIEdgeInsets { + ) -> NSDirectionalEdgeInsets { switch horizontalSizeClass { - case .unspecified, .compact: - return UIEdgeInsets(allButTop: 32) case .regular: - return UIEdgeInsets(allButTop: 112) + return NSDirectionalEdgeInsets(top: 0, leading: 112, bottom: 112, trailing: 112) + case .unspecified, .compact: + fallthrough @unknown default: - return UIEdgeInsets(allButTop: 32) + return NSDirectionalEdgeInsets(top: 0, leading: 32, bottom: 32, trailing: 32) } } diff --git a/Signal/test/Registration/RegistrationCoordinatorTest.swift b/Signal/test/Registration/RegistrationCoordinatorTest.swift index fba1e76c5c..c7c8d53208 100644 --- a/Signal/test/Registration/RegistrationCoordinatorTest.swift +++ b/Signal/test/Registration/RegistrationCoordinatorTest.swift @@ -231,15 +231,15 @@ public class RegistrationCoordinatorTest: XCTestCase { } // Now we should show the permissions. - XCTAssertEqual(nextStep.value, .permissions(Stubs.permissionsState())) + XCTAssertEqual(nextStep.value, .permissions) // Doesn't change even if we try and proceed. - XCTAssertEqual(coordinator.nextStep().value, .permissions(Stubs.permissionsState())) + XCTAssertEqual(coordinator.nextStep().value, .permissions) // Once the state is updated we can proceed. nextStep = coordinator.requestPermissions() XCTAssertNotNil(nextStep.value) XCTAssertNotEqual(nextStep.value, .registrationSplash) - XCTAssertNotEqual(nextStep.value, .permissions(Stubs.permissionsState())) + XCTAssertNotEqual(nextStep.value, .permissions) } } @@ -3693,7 +3693,7 @@ public class RegistrationCoordinatorTest: XCTestCase { // Now we should show the permissions. nextStep = coordinator.continueFromSplash() scheduler.runUntilIdle() - XCTAssertEqual(nextStep.value, .permissions(Stubs.permissionsState())) + XCTAssertEqual(nextStep.value, .permissions) // Once the state is updated we can proceed. nextStep = coordinator.requestPermissions() @@ -3907,10 +3907,6 @@ public class RegistrationCoordinatorTest: XCTestCase { // MARK: Step States - static func permissionsState() -> RegistrationPermissionsState { - return RegistrationPermissionsState(shouldRequestAccessToContacts: true) - } - static func pinEntryStateForRegRecoveryPath( mode: RegistrationMode, error: RegistrationPinValidationError? = nil, diff --git a/Signal/translations/en.lproj/Localizable.strings b/Signal/translations/en.lproj/Localizable.strings index ef4fe1fdef..12639a5f8c 100644 --- a/Signal/translations/en.lproj/Localizable.strings +++ b/Signal/translations/en.lproj/Localizable.strings @@ -4747,11 +4747,20 @@ /* warning to the user that linking a phone is not recommended */ "ONBOARDING_MODE_SWITCH_WARNING_REGISTERING" = "Linking your iPhone is not recommended and will limit core functionality."; -/* Label for the 'give access' button in the 'onboarding permissions' view. */ -"ONBOARDING_PERMISSIONS_ENABLE_PERMISSIONS_BUTTON" = "Allow Permissions"; +/* Description of the 'Contacts' permission in the 'onboarding permissions' view. */ +"ONBOARDING_PERMISSIONS_CONTACTS_DESCRIPTION" = "Find people you know. Your contacts are encrypted and not visible to the Signal service."; -/* Explanation in the 'onboarding permissions' view. */ -"ONBOARDING_PERMISSIONS_EXPLANATION" = "Allowing notifications and contacts lets you see when messages arrive and helps you find people you know. Contacts are encrypted so the Signal service can't see them."; +/* Title introducing the 'Contacts' permission in the 'onboarding permissions' view. */ +"ONBOARDING_PERMISSIONS_CONTACTS_TITLE" = "Contacts"; + +/* Description of the 'Notifications' permission in the 'onboarding permissions' view. */ +"ONBOARDING_PERMISSIONS_NOTIFICATIONS_DESCRIPTION" = "Get notified when new messages arrive."; + +/* Title introducing the 'Notifications' permission in the 'onboarding permissions' view. */ +"ONBOARDING_PERMISSIONS_NOTIFICATIONS_TITLE" = "Notifications"; + +/* Preamble of the 'onboarding permissions' view. */ +"ONBOARDING_PERMISSIONS_PREAMBLE" = "Signal would like to request the following permissions."; /* Title of the 'onboarding permissions' view. */ "ONBOARDING_PERMISSIONS_TITLE" = "Allow Permissions"; diff --git a/SignalUI/SwiftUIExtensions/AccessibleLayoutMetric.swift b/SignalUI/SwiftUIExtensions/AccessibleLayoutMetric.swift new file mode 100644 index 0000000000..e038a16bad --- /dev/null +++ b/SignalUI/SwiftUIExtensions/AccessibleLayoutMetric.swift @@ -0,0 +1,54 @@ +// +// Copyright 2024 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only +// + +import SwiftUI + +/// A value that is automatically scaled down at accessibility dynamic type sizes. +/// +/// This is similar to SwiftUI's `ScaledMetric`, which is designed to scale values +/// *up*, proportionally with dynamic type size. `AccessibleLayoutMetric` is instead +/// designed to make more space for content by tightening up spacing metrics at +/// large dynamic type sizes. +/// +/// ```swift +/// struct ContentView: View { +/// // Automatically scales down to 67% at accessibility dynamic type sizes. +/// @AccessibleLayoutMetric private var rowSpacing = 24 +/// +/// // The scale used at accessibility sizes can be customized. +/// @AccessibleLayoutMetric(scale: 0.5) private var viewPadding = 24 +/// +/// var body: some View { +/// VStack(spacing: spacing) { +/// Text("Moderately long text") +/// Text("Very long text…") +/// } +/// .padding(.horizontal, viewPadding) +/// } +/// } +/// ``` +@propertyWrapper +public struct AccessibleLayoutMetric: DynamicProperty { + @Environment(\.dynamicTypeSize) private var dynamicTypeSize + private let accessibilityScale: Value + + public let rawValue: Value + public private(set) var wrappedValue: Value + + public init(wrappedValue: Value, scale: Value = 0.67) { + self.accessibilityScale = scale + self.rawValue = wrappedValue + self.wrappedValue = wrappedValue + } + + public var projectedValue: Self { + self + } + + public mutating func update() { + let scale = dynamicTypeSize.isAccessibilitySize ? accessibilityScale : 1.0 + wrappedValue = rawValue * scale + } +} diff --git a/SignalUI/SwiftUIExtensions/AsyncViewTask.swift b/SignalUI/SwiftUIExtensions/AsyncViewTask.swift new file mode 100644 index 0000000000..065be76e3e --- /dev/null +++ b/SignalUI/SwiftUIExtensions/AsyncViewTask.swift @@ -0,0 +1,79 @@ +// +// Copyright 2024 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only +// + +import SwiftUI + +// MARK: - AsyncViewTask + +/// Represents an async operation that can be associated with the lifetime of +/// a view using the `task(_:)` view modifier. +public protocol AsyncViewTask: Identifiable { + /// Optionally provide a custom priority for the task. By default, the task + /// will be executed with the `.userInitied` priority. + /// See the `task(id:priority:_:)` view modifier for more information. + var priority: TaskPriority? { get } + + /// The asynchronous action performed by the task. + func perform() async +} + +extension AsyncViewTask { + public var priority: TaskPriority? { nil } +} + +// MARK: - AsyncViewTaskModifier + +extension View { + /// Associates a binding to an `AsyncViewTask` with the lifetime of a view. + /// + /// When the binding is `nil`, the task is not executing. To begin the task + /// set the value of the binding to an instance of the `AsyncTask` type. + /// + /// Buttons and other controls are automatically disabled while the task is + /// executing. + /// + /// To cancel the active task, set the value of the binding to `nil` or a + /// new task value. The previous task will automatically be cancelled before + /// a new task begins. + /// + /// ```swift + /// struct Nap: AsyncViewTask { + /// let id = UUID() + /// var duration: ContinousClock.Duration + /// + /// func perform() async { + /// try? await Task.sleep(for: duration) + /// } + /// } + /// + /// struct NappingButton: View { + /// @State private var nap: Nap? + /// + /// var body: some View { + /// Button("Take a Nap") { + /// nap = Nap(duration: .seconds(60)) + /// } + /// .task($nap.animation()) + /// } + /// } + /// ``` + public func task(_ task: Binding) -> some View { + modifier(AsyncViewTaskModifier(task: task)) + } +} + +public struct AsyncViewTaskModifier: ViewModifier { + @Binding var task: Task? + + public func body(content: Content) -> some View { + content + .disabled(task != nil) + .task(id: task?.id, priority: task?.priority ?? .userInitiated) { + guard let currentTask = task else { return } + defer { task = nil } + await currentTask.perform() + } + } +} diff --git a/SignalUI/SwiftUIExtensions/ScrollBounceBehaviorIfAvailable.swift b/SignalUI/SwiftUIExtensions/ScrollBounceBehaviorIfAvailable.swift new file mode 100644 index 0000000000..e1d5988b8b --- /dev/null +++ b/SignalUI/SwiftUIExtensions/ScrollBounceBehaviorIfAvailable.swift @@ -0,0 +1,39 @@ +// +// Copyright 2024 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only +// + +import SwiftUI + +public struct ScrollBounceBehaviorIfAvailableModifier: ViewModifier { + public enum Behavior { + case automatic + case always + case basedOnSize + + @available(iOS 16.4, *) + var asScrollBounceBehavior: ScrollBounceBehavior { + switch self { + case .automatic: .automatic + case .always: .always + case .basedOnSize: .basedOnSize + } + } + } + + var behavior: Behavior + + public func body(content: Content) -> some View { + if #available(iOS 16.4, *) { + content.scrollBounceBehavior(behavior.asScrollBounceBehavior) + } else { + content + } + } +} + +extension View { + public func scrollBounceBehaviorIfAvailable(_ behavior: ScrollBounceBehaviorIfAvailableModifier.Behavior) -> some View { + modifier(ScrollBounceBehaviorIfAvailableModifier(behavior: behavior)) + } +}