Compare commits

..

5 Commits

Author SHA1 Message Date
sashaweiss-signal
9a6ef4d128 Feature flags for .production. 2026-06-02 11:15:37 -07:00
sashaweiss-signal
35c3b676e3 Update translations 2026-06-02 11:15:10 -07:00
Pete Walters
c9fc97823f Add two separate modes to the Safety Tip screen
Co-authored-by: Max Radermacher <max@signal.org>
2026-06-02 13:01:00 -05:00
Max Radermacher
9c1ecb6910 Fix empty WAL file transfer during device transfer
Co-authored-by: Sasha Weiss <sasha@signal.org>
2026-05-28 11:30:08 -05:00
sashaweiss-signal
36184507a8 Feature flags for .beta. 2026-05-27 15:14:59 -07:00
438 changed files with 8277 additions and 10338 deletions

View File

@ -11,8 +11,8 @@ source 'https://cdn.cocoapods.org/'
pod 'blurhash', podspec: './ThirdParty/blurhash.podspec'
pod 'SwiftProtobuf', "1.36.1"
ENV['LIBSIGNAL_FFI_PREBUILD_CHECKSUM'] = '79f53932ff82f792b70e30bad3b38801da0b882137adaf65ad54d907a94f3d29'
pod 'LibSignalClient', git: 'https://github.com/signalapp/libsignal.git', tag: 'v0.95.0', testspecs: ["Tests"]
ENV['LIBSIGNAL_FFI_PREBUILD_CHECKSUM'] = 'e3b89de2afc950c9e317f2fff426ae8edc77a397520d2e0afbb717d738213fd5'
pod 'LibSignalClient', git: 'https://github.com/signalapp/libsignal.git', tag: 'v0.94.1', testspecs: ["Tests"]
# pod 'LibSignalClient', path: '../libsignal', testspecs: ["Tests"]
ENV['RINGRTC_PREBUILD_CHECKSUM'] = 'c19c813ab5255aa3cd7c2af36374100f7cc69c2fd794cae23baebd6ec9dae90c'

View File

@ -9,8 +9,8 @@ PODS:
- LibMobileCoin/CoreHTTP (6.0.2):
- SwiftProtobuf (~> 1.5)
- libPhoneNumber-iOS (1.2.0)
- LibSignalClient (0.95.0)
- LibSignalClient/Tests (0.95.0)
- LibSignalClient (0.94.1)
- LibSignalClient/Tests (0.94.1)
- libwebp (1.5.0):
- libwebp/demux (= 1.5.0)
- libwebp/mux (= 1.5.0)
@ -52,8 +52,8 @@ DEPENDENCIES:
- GRDB.swift/SQLCipher
- LibMobileCoin/CoreHTTP (from `https://github.com/signalapp/libmobilecoin-ios-artifacts`, tag `signal/6.0.2`)
- libPhoneNumber-iOS (from `https://github.com/signalapp/libPhoneNumber-iOS`, branch `signal-master`)
- LibSignalClient (from `https://github.com/signalapp/libsignal.git`, tag `v0.95.0`)
- LibSignalClient/Tests (from `https://github.com/signalapp/libsignal.git`, tag `v0.95.0`)
- LibSignalClient (from `https://github.com/signalapp/libsignal.git`, tag `v0.94.1`)
- LibSignalClient/Tests (from `https://github.com/signalapp/libsignal.git`, tag `v0.94.1`)
- libwebp
- lottie-ios
- MobileCoin/CoreHTTP (from `https://github.com/mobilecoinofficial/MobileCoin-Swift`, tag `v6.0.3`)
@ -89,7 +89,7 @@ EXTERNAL SOURCES:
:git: https://github.com/signalapp/libPhoneNumber-iOS
LibSignalClient:
:git: https://github.com/signalapp/libsignal.git
:tag: v0.95.0
:tag: v0.94.1
MobileCoin:
:git: https://github.com/mobilecoinofficial/MobileCoin-Swift
:tag: v6.0.3
@ -113,7 +113,7 @@ CHECKOUT OPTIONS:
:git: https://github.com/signalapp/libPhoneNumber-iOS
LibSignalClient:
:git: https://github.com/signalapp/libsignal.git
:tag: v0.95.0
:tag: v0.94.1
MobileCoin:
:git: https://github.com/mobilecoinofficial/MobileCoin-Swift
:tag: v6.0.3
@ -131,7 +131,7 @@ SPEC CHECKSUMS:
GRDB.swift: 1395cb3556df6b16ed69dfc74c3886abc75d2825
LibMobileCoin: 8503f567fa32184a5be7bc038fbd727747dd9991
libPhoneNumber-iOS: 1a34106b49dc6e12a7f37eb9aee7c64011509547
LibSignalClient: a98db1d538243e43ecac040005204bd274cbd8c7
LibSignalClient: cf53cea3c6cd2cac3e87d0f5f34c3a1c59fe1b8f
libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8
Logging: beeb016c9c80cf77042d62e83495816847ef108b
lottie-ios: fcb5e73e17ba4c983140b7d21095c834b3087418
@ -143,6 +143,6 @@ SPEC CHECKSUMS:
SQLCipher: ff2f045b20d675a73a70f7329395ddd4a2580063
SwiftProtobuf: 9e106a71456f4d3f6a3b0c8fd87ef0be085efc38
PODFILE CHECKSUM: ee98007764e1569e9dbe4f25053510725b19fc88
PODFILE CHECKSUM: cf592eb2b2ccbf3e467f82142ef4d4096e132343
COCOAPODS: 1.15.2

2
Pods

@ -1 +1 @@
Subproject commit 5e81462d833ad24e8091d7b6ab675c2cdc94af54
Subproject commit 2f7bce71b0b302c4961c940606b79b6f32bfb8d0

View File

@ -1,27 +1,76 @@
{
"#comment": "NOTE: This file is generated by /Scripts/sds_codegen/sds_generate.py. Do not manually edit it, instead run `sds_codegen.sh`.",
"#max": 80,
"BaseModel": 56,
"ExperienceUpgrade": 55,
"IncomingGroupsV2MessageJob": 63,
"InstalledSticker": 24,
"OWS100RemoveTSRecipientsMigration": 40,
"OWS101ExistingUsersBlockOnIdentityChange": 43,
"OWS102MoveLoggingPreferenceToUserDefaults": 47,
"OWS103EnableVideoCalling": 42,
"OWS104CreateRecipientIdentities": 45,
"OWS105AttachmentFilePaths": 44,
"OWS107LegacySounds": 50,
"OWS108CallLoggingPreference": 48,
"OWS109OutgoingMessageState": 51,
"OWSAddToContactsOfferMessage": 25,
"OWSAddToProfileWhitelistOfferMessage": 7,
"OWSBackupFragment": 32,
"OWSContactOffersInteraction": 22,
"OWSContactQuery": 57,
"OWSDatabaseMigration": 46,
"OWSDevice": 33,
"OWSDisappearingConfigurationUpdateInfoMessage": 28,
"OWSDisappearingMessagesConfiguration": 39,
"OWSGroupCallMessage": 65,
"OWSIncomingArchivedPaymentMessage": 78,
"OWSIncomingContactSyncJobRecord": 61,
"OWSIncomingGroupSyncJobRecord": 60,
"OWSIncomingPaymentMessage": 75,
"OWSLinkedDeviceReadReceipt": 36,
"OWSLocalUserLeaveGroupJobRecord": 74,
"OWSMessageContentJob": 15,
"OWSOutgoingArchivedPaymentMessage": 79,
"OWSOutgoingPaymentMessage": 68,
"OWSPaymentActivationRequestFinishedMessage": 77,
"OWSPaymentActivationRequestMessage": 76,
"OWSReaction": 62,
"OWSReceiptCredentialRedemptionJobRecord": 71,
"OWSRecipientIdentity": 38,
"OWSRecoverableDecryptionPlaceholder": 70,
"OWSResaveCollectionDBMigration": 49,
"OWSSendGiftBadgeJobRecord": 73,
"OWSSessionResetJobRecord": 52,
"OWSUnknownContactBlockOfferMessage": 5,
"OWSUnknownDBObject": 37,
"OWSUnknownProtocolVersionMessage": 54,
"OWSUserProfile": 41,
"OWSVerificationStateChangeMessage": 13,
"SSKJobRecord": 34,
"SSKMessageDecryptJobRecord": 53,
"SSKMessageSenderJobRecord": 35,
"SignalAccount": 30,
"SignalRecipient": 31,
"StickerPack": 14,
"TSCall": 20,
"TSContactThread": 27,
"TSErrorMessage": 9,
"TSGroupMember": 69,
"TSGroupThread": 26,
"TSIncomingMessage": 19,
"TSInfoMessage": 10,
"TSInteraction": 16,
"TSInvalidIdentityKeyErrorMessage": 17,
"TSInvalidIdentityKeyReceivingErrorMessage": 1,
"TSInvalidIdentityKeySendingErrorMessage": 23,
"TSMention": 64,
"TSMessage": 11,
"TSOutgoingMessage": 21,
"TSUnreadIndicatorInteraction": 4
"TSPaymentModel": 67,
"TSPaymentRequestModel": 66,
"TSPrivateStoryThread": 72,
"TSRecipientReadReceipt": 12,
"TSThread": 2,
"TSUnreadIndicatorInteraction": 4,
"TestModel": 59
}

View File

@ -2440,23 +2440,31 @@ record_type_map = {}
# It's critical that our "record type" values are consistent, even if we add/remove/rename model classes.
# Therefore we persist the mapping of known classes in a JSON file that is under source control.
def update_record_type_map(record_type_swift_path, record_type_json_path):
old_record_types = {}
if os.path.exists(record_type_json_path):
with open(record_type_json_path, "r") as f:
old_record_types = json.load(f)
record_type_map_filepath = record_type_json_path
max_record_type = old_record_types.get("#max", 0)
if os.path.exists(record_type_map_filepath):
with open(record_type_map_filepath, "rt") as f:
json_string = f.read()
json_data = json.loads(json_string)
record_type_map.update(json_data)
max_record_type = 0
for class_name in record_type_map:
if class_name.startswith("#"):
continue
record_type = record_type_map[class_name]
max_record_type = max(max_record_type, record_type)
for clazz in global_class_map.values():
if not clazz.should_generate_extensions():
continue
if clazz.name in old_record_types:
record_type_map[clazz.name] = old_record_types[clazz.name]
else:
max_record_type += 1
record_type_map[clazz.name] = max_record_type
if clazz.name not in record_type_map:
if not clazz.should_generate_extensions():
continue
max_record_type = int(max_record_type) + 1
record_type = max_record_type
record_type_map[clazz.name] = record_type
record_type_map["#max"] = max_record_type
record_type_map["#comment"] = (
"NOTE: This file is generated by %s. Do not manually edit it, instead run `sds_codegen.sh`."
% (sds_common.pretty_module_path(__file__),)
@ -2464,7 +2472,7 @@ def update_record_type_map(record_type_swift_path, record_type_json_path):
json_string = json.dumps(record_type_map, sort_keys=True, indent=4)
sds_common.write_text_file_if_changed(record_type_json_path, json_string)
sds_common.write_text_file_if_changed(record_type_map_filepath, json_string)
# TODO: We'll need to import SignalServiceKit for non-SSK classes.

View File

@ -77,6 +77,7 @@
046092262FBCD2DA00A8765F /* SafetyTipsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 046092232FBCC7E700A8765F /* SafetyTipsManager.swift */; };
046926092E8EBAAE00B1FC74 /* TSInfoMessage+Polls.swift in Sources */ = {isa = PBXBuildFile; fileRef = 046926082E8EBAA800B1FC74 /* TSInfoMessage+Polls.swift */; };
0477BE322FA4FC41002F9B47 /* TSReleaseNotesThread.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0477BE312FA4FC38002F9B47 /* TSReleaseNotesThread.swift */; };
047A6DD02E00B5720048EDF4 /* BackupKeyReminderMegaphoneTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 047A6DCF2E00B5640048EDF4 /* BackupKeyReminderMegaphoneTests.swift */; };
0480F0002E57C51A006CBB29 /* BackupsEnabledNotificationMegaphone.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0480EFFF2E57C513006CBB29 /* BackupsEnabledNotificationMegaphone.swift */; };
0484CECE2F44B7BE009AB2CB /* AdminDeleteRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0484CECD2F44B7BB009AB2CB /* AdminDeleteRecord.swift */; };
0484CED02F44BD00009AB2CB /* AdminDeleteManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0484CECF2F44BCFB009AB2CB /* AdminDeleteManager.swift */; };
@ -92,6 +93,7 @@
04A573702E4D4BD50019651F /* OWSPoll.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04A5736F2E4D4BD30019651F /* OWSPoll.swift */; };
04A573722E53A3BF0019651F /* SupportKeyValueStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04A573712E53A3B40019651F /* SupportKeyValueStore.swift */; };
04A573762E75B00B0019651F /* DebugLogPreviewViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04A573752E75B00A0019651F /* DebugLogPreviewViewController.swift */; };
04AA85BC2E0EFC5C0051C0A7 /* BackupEnablementReminderMegaphoneTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 041A5F082E05D3AC00FAED05 /* BackupEnablementReminderMegaphoneTests.swift */; };
04AB61C62E5E37A800405699 /* PollRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04AB61C52E5E37A400405699 /* PollRecord.swift */; };
04AB61C82E5E399700405699 /* PollOptionRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04AB61C72E5E399400405699 /* PollOptionRecord.swift */; };
04AB61CA2E5E449100405699 /* PollManagerTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04AB61C92E5E448A00405699 /* PollManagerTest.swift */; };
@ -724,7 +726,7 @@
50552C2C2BAB8E8500815474 /* AuthCredentialStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50552C2B2BAB8E8500815474 /* AuthCredentialStore.swift */; };
5056B3BF2DEED72800F55320 /* MonotonicDateTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5056B3BE2DEED72800F55320 /* MonotonicDateTest.swift */; };
50589CDE2E8C44D5003EF42A /* PreKeyStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50589CDD2E8C44D5003EF42A /* PreKeyStore.swift */; };
50589CE02E8C4AD5003EF42A /* PreKeyRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50589CDF2E8C4AD5003EF42A /* PreKeyRecord.swift */; };
50589CE02E8C4AD5003EF42A /* PreKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50589CDF2E8C4AD5003EF42A /* PreKey.swift */; };
50597BBA2B97C38C004681E1 /* SignalAccountStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50597BB92B97C38C004681E1 /* SignalAccountStore.swift */; };
50597BBC2B97C449004681E1 /* UsernameLookupRecordStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50597BBB2B97C449004681E1 /* UsernameLookupRecordStore.swift */; };
50597BBF2B97D629004681E1 /* SearchableNameFinder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50597BBE2B97D629004681E1 /* SearchableNameFinder.swift */; };
@ -856,7 +858,6 @@
50D839512F916A3700EE009A /* MessageRequestDecliner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50D839502F916A3700EE009A /* MessageRequestDecliner.swift */; };
50D8796A2A16D2C20031345D /* MessageLoaderBatchTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50D879692A16D2C20031345D /* MessageLoaderBatchTest.swift */; };
50D9CD8D2C52D78000273D6C /* StoryRecipientManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50D9CD8C2C52D78000273D6C /* StoryRecipientManager.swift */; };
50DAF7E02FD87BEC00BE7430 /* OrphanedBackupAttachmentTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50DAF7DF2FD87BEC00BE7430 /* OrphanedBackupAttachmentTest.swift */; };
50DCCBFA2F1817280024D124 /* DisappearingMessagesConfigurationMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50DCCBF92F1817280024D124 /* DisappearingMessagesConfigurationMessage.swift */; };
50DCCBFC2F181A790024D124 /* ProfileKeyMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50DCCBFB2F181A790024D124 /* ProfileKeyMessage.swift */; };
50DCCBFE2F1820600024D124 /* OutgoingSenderKeyDistributionMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50DCCBFD2F1820600024D124 /* OutgoingSenderKeyDistributionMessage.swift */; };
@ -1063,7 +1064,7 @@
66586D3829005A1B00DDA9B9 /* story_viewer_onboarding_1.json in Resources */ = {isa = PBXBuildFile; fileRef = 66586D3529005A1B00DDA9B9 /* story_viewer_onboarding_1.json */; };
66586D3929005A1B00DDA9B9 /* story_viewer_onboarding_3.json in Resources */ = {isa = PBXBuildFile; fileRef = 66586D3629005A1B00DDA9B9 /* story_viewer_onboarding_3.json */; };
66586D4129009C0000DDA9B9 /* TextAttachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66586D4029009C0000DDA9B9 /* TextAttachment.swift */; };
6659A0262A7C11A800066AB7 /* PreKeyManagerImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6659A0252A7C11A800066AB7 /* PreKeyManagerImpl.swift */; };
6659A0262A7C11A800066AB7 /* PrekeyManagerImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6659A0252A7C11A800066AB7 /* PrekeyManagerImpl.swift */; };
6659A0282A7C11ED00066AB7 /* MockPreKeyManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6659A0272A7C11ED00066AB7 /* MockPreKeyManager.swift */; };
6659A0312A7C5B9700066AB7 /* PreKeyUploadBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6659A0302A7C5B9700066AB7 /* PreKeyUploadBundle.swift */; };
6659A0392A81933B00066AB7 /* ProvisioningPermissionsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6659A0382A81933B00066AB7 /* ProvisioningPermissionsViewController.swift */; };
@ -1079,6 +1080,7 @@
665FAE8C2A02C0D400FA298D /* SpoilerRevealState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 665FAE8B2A02C0D400FA298D /* SpoilerRevealState.swift */; };
6660725E2BAB36960084B3D2 /* AttachmentDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6660725D2BAB36960084B3D2 /* AttachmentDataSource.swift */; };
6664B9AB2A314EBD008EF74B /* SpoilerRevealStateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6664B9AA2A314EBD008EF74B /* SpoilerRevealStateTests.swift */; };
666654212AD0B03F00B23B32 /* MasterKeySyncManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 666654202AD0B03F00B23B32 /* MasterKeySyncManager.swift */; };
66681CDF2C58174F00E50136 /* BackupAttachmentDownloadStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66681CDE2C58174F00E50136 /* BackupAttachmentDownloadStoreTests.swift */; };
6671DC872CD44CA8002620EF /* LastVisibleInteractionStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6671DC862CD44C9B002620EF /* LastVisibleInteractionStore.swift */; };
66734F012CA1ED3F00558494 /* BackupAttachmentUploadScheduler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66734F002CA1ED3A00558494 /* BackupAttachmentUploadScheduler.swift */; };
@ -1162,6 +1164,7 @@
668B5BFC2C7E46D30018CF36 /* PaletteChatColor+Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 668B5BFB2C7E46D30018CF36 /* PaletteChatColor+Constants.swift */; };
668CAB3E289983520085A2C3 /* AudioMessagePlaybackRateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 668CAB3D289983520085A2C3 /* AudioMessagePlaybackRateView.swift */; };
668E403C2BE43752004B6730 /* SDAnimatedImage+Attachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 668E403B2BE43752004B6730 /* SDAnimatedImage+Attachment.swift */; };
668FE09B28B923A4008B9071 /* Bool+SSK.swift in Sources */ = {isa = PBXBuildFile; fileRef = 668FE09A28B923A4008B9071 /* Bool+SSK.swift */; };
668FE09F28B947ED008B9071 /* StoryContextMenuGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 668FE09E28B947ED008B9071 /* StoryContextMenuGenerator.swift */; };
6691E7EF2996E8FB0032A68A /* TSRequestOWSURLSessionMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6691E7EE2996E8FB0032A68A /* TSRequestOWSURLSessionMock.swift */; };
6691E7F22996E9BC0032A68A /* RegistrationSessionManagerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6691E7F12996E9BC0032A68A /* RegistrationSessionManagerMock.swift */; };
@ -1451,6 +1454,8 @@
729E0B0A2CA4AEB0002EC961 /* Threading.swift in Sources */ = {isa = PBXBuildFile; fileRef = 729E0B082CA4ADE2002EC961 /* Threading.swift */; };
72A132A52CA210C7000ACED6 /* DarwinNotificationCenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72A132A42CA210C2000ACED6 /* DarwinNotificationCenter.swift */; };
72A132A72CA25EF0000ACED6 /* SDSCrossProcess.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72A132A62CA25EE9000ACED6 /* SDSCrossProcess.swift */; };
72B0C2402C9EEA8200B57DAD /* PreKeyRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72B0C23F2C9EEA7700B57DAD /* PreKeyRecord.swift */; };
72B0C2422C9EED0E00B57DAD /* SignedPreKeyRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72B0C2412C9EED0800B57DAD /* SignedPreKeyRecord.swift */; };
72B4819D2BD60FDF008B8BA1 /* OWSMath.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72B4819C2BD60FDF008B8BA1 /* OWSMath.swift */; };
72B994DB2BE950DB000CBBFD /* TestAppContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72B994DA2BE950DB000CBBFD /* TestAppContext.swift */; };
72C905892B9A28BF00E586B8 /* Sounds.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7634F08C2A21963600BB93D5 /* Sounds.swift */; };
@ -1673,7 +1678,7 @@
88A357B923639384009D6B9A /* MemberActionSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88A357B823639384009D6B9A /* MemberActionSheet.swift */; };
88A4CC10246CE2760082211F /* TransferProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88A4CC0F246CE2760082211F /* TransferProgressView.swift */; };
88A505F423DA16E10005C012 /* ExperienceUpgradeManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88A505F323DA16E10005C012 /* ExperienceUpgradeManager.swift */; };
88A505FA23DBA1360005C012 /* IntroducingPINsMegaphone.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88A505F923DBA1360005C012 /* IntroducingPINsMegaphone.swift */; };
88A505FA23DBA1360005C012 /* IntroducingPINs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88A505F923DBA1360005C012 /* IntroducingPINs.swift */; };
88A941992409A391000E9700 /* LottieToggleButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88A941982409A391000E9700 /* LottieToggleButton.swift */; };
88A9729222FA5D4B004B4FBF /* AttachmentFormatPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88A9729122FA5D4B004B4FBF /* AttachmentFormatPickerView.swift */; };
88A9729422FB4D02004B4FBF /* LocationPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88A9729322FB4D02004B4FBF /* LocationPicker.swift */; };
@ -2701,7 +2706,6 @@
D92812E22FA95C1400667DCF /* DisplayableAccountEntropyPoolTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = D92812E12FA95C0D00667DCF /* DisplayableAccountEntropyPoolTest.swift */; };
D92A1CDA2E314BD400C91E21 /* DebugUIPrompts.swift in Sources */ = {isa = PBXBuildFile; fileRef = D92A1CD92E314BD000C91E21 /* DebugUIPrompts.swift */; };
D92AB7D829E3BEE30081CA7D /* OWSDeviceManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D92AB7D729E3BEE30081CA7D /* OWSDeviceManager.swift */; };
D92B55EF2FD0D9210083B070 /* BackupPlanOptionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D92B55EE2FD0D9210083B070 /* BackupPlanOptionView.swift */; };
D92C57552A2925AD00A03BB7 /* TSInfoMessage+DisplayableGroupUpdateItemTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = D92C57542A2925AD00A03BB7 /* TSInfoMessage+DisplayableGroupUpdateItemTest.swift */; };
D92CA9EF2F500EA500FDE32D /* LeaveGroupCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D92CA9EE2F500EA500FDE32D /* LeaveGroupCoordinator.swift */; };
D92CB5562F030F8300537EBE /* BackupSubscriptionAlreadyRedeemedSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = D92CB5552F030F7A00537EBE /* BackupSubscriptionAlreadyRedeemedSheet.swift */; };
@ -2742,7 +2746,6 @@
D943F3EF2892F89B008C0C8B /* NSELogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = D943F3EE2892F89B008C0C8B /* NSELogger.swift */; };
D94441312D55956B005B2A54 /* UUIDv7.swift in Sources */ = {isa = PBXBuildFile; fileRef = D94441302D559567005B2A54 /* UUIDv7.swift */; };
D94441332D559C6F005B2A54 /* UUIDv7Test.swift in Sources */ = {isa = PBXBuildFile; fileRef = D94441322D559C6B005B2A54 /* UUIDv7Test.swift */; };
D944D2FD2FBCE9C00026FD21 /* OWSDecryptionPlaceholderExpirationJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = D944D2FC2FBCE9C00026FD21 /* OWSDecryptionPlaceholderExpirationJob.swift */; };
D945319E2CE53CEB004DAB30 /* SubscriptionRedemptionNecessityChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = D945319D2CE53CC8004DAB30 /* SubscriptionRedemptionNecessityChecker.swift */; };
D94852272F6A224000B130B2 /* GroupCallVideoContextMenuConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D94852262F6A223500B130B2 /* GroupCallVideoContextMenuConfiguration.swift */; };
D9495A6D2C7683D100843BC1 /* TSOutgoingMessageRecipientState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9495A6C2C7683D100843BC1 /* TSOutgoingMessageRecipientState.swift */; };
@ -2882,8 +2885,8 @@
D96869452E1065F5005451E4 /* SeriallyAccessedState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D96869442E1065F1005451E4 /* SeriallyAccessedState.swift */; };
D968B4982C9E1AD1006B14E1 /* SmsLockIconView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D968B4972C9E1AC3006B14E1 /* SmsLockIconView.swift */; };
D968F71E2C34884B00AB318B /* BackupArchiveReleaseNotesRecipientArchiver.swift in Sources */ = {isa = PBXBuildFile; fileRef = D968F71D2C34884B00AB318B /* BackupArchiveReleaseNotesRecipientArchiver.swift */; };
D9697C162FD78FE400119F72 /* BackupNeverShareRecoveryKeySheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9697C152FD78FDD00119F72 /* BackupNeverShareRecoveryKeySheet.swift */; };
D96A94A72954E57F004EA434 /* DonateViewController+MonthlyPaypalDonation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D96A94A62954E57F004EA434 /* DonateViewController+MonthlyPaypalDonation.swift */; };
D96BE42E292EF04200E4FE1A /* PaypalButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D96BE42D292EF04200E4FE1A /* PaypalButton.swift */; };
D97046062E81D4240034C05D /* InfoMessageGroupUpdateMigrator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D97046052E81D41F0034C05D /* InfoMessageGroupUpdateMigrator.swift */; };
D970460A2E81D5C00034C05D /* InfoMessageGroupUpdateMigratorTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = D97046092E81D5BB0034C05D /* InfoMessageGroupUpdateMigratorTest.swift */; };
D970541A2CFE49E400AC7954 /* SubscriptionFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = D97054192CFE49E200AC7954 /* SubscriptionFetcher.swift */; };
@ -2898,7 +2901,6 @@
D9791BC42EAADF010016AA5A /* HTTPResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9791BC32EAADEFD0016AA5A /* HTTPResponse.swift */; };
D97992A12D9E55F20080A4F5 /* CurrencyFormatterTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = D97992A02D9E55E10080A4F5 /* CurrencyFormatterTest.swift */; };
D97992A32D9E55FB0080A4F5 /* CurrencyFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D97992A22D9E55F80080A4F5 /* CurrencyFormatter.swift */; };
D97992AC2FC147C5002E79F3 /* ExperienceUpgradeStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = D97992AB2FC147C2002E79F3 /* ExperienceUpgradeStore.swift */; };
D979CC262AD3933B006AAC49 /* IndividualCallRecordManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D979CC1F2AD3933B006AAC49 /* IndividualCallRecordManager.swift */; };
D979CC292AD3933B006AAC49 /* OutgoingCallEventSyncMessageManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D979CC222AD3933B006AAC49 /* OutgoingCallEventSyncMessageManager.swift */; };
D979CC2B2AD3933B006AAC49 /* InteractionStore+CallRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = D979CC242AD3933B006AAC49 /* InteractionStore+CallRecord.swift */; };
@ -3824,7 +3826,7 @@
F9C5CCA3289453B300548EEE /* StorageService.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9C5C9B8289453B100548EEE /* StorageService.pb.swift */; };
F9C5CCA4289453B300548EEE /* SSKProto+OWS.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9C5C9B9289453B100548EEE /* SSKProto+OWS.swift */; };
F9C5CCAC289453B300548EEE /* PreKeyManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9C5C9C2289453B100548EEE /* PreKeyManager.swift */; };
F9C5CCB0289453B300548EEE /* RemoteAttestationAuthFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9C5C9C7289453B100548EEE /* RemoteAttestationAuthFetcher.swift */; };
F9C5CCB0289453B300548EEE /* RemoteAttestation.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9C5C9C7289453B100548EEE /* RemoteAttestation.swift */; };
F9C5CCC0289453B300548EEE /* ContactDiscoveryTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9C5C9D9289453B100548EEE /* ContactDiscoveryTask.swift */; };
F9C5CCC3289453B300548EEE /* ContactDiscoveryError.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9C5C9DC289453B100548EEE /* ContactDiscoveryError.swift */; };
F9C5CCC5289453B300548EEE /* SignalAccount.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9C5C9DE289453B100548EEE /* SignalAccount.swift */; };
@ -3963,6 +3965,7 @@
F9C5CE29289453B400548EEE /* ModelReadCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9C5CB57289453B200548EEE /* ModelReadCache.swift */; };
F9C5CE2A289453B400548EEE /* Platform.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9C5CB58289453B200548EEE /* Platform.swift */; };
F9C5CE2B289453B400548EEE /* BuildFlags.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9C5CB59289453B200548EEE /* BuildFlags.swift */; };
F9C5CE2D289453B400548EEE /* ExperienceUpgradeFinder.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9C5CB5B289453B200548EEE /* ExperienceUpgradeFinder.swift */; };
F9C5CE2F289453B400548EEE /* SwiftSingletons.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9C5CB5D289453B200548EEE /* SwiftSingletons.swift */; };
F9C5CE33289453B400548EEE /* LocalDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9C5CB61289453B200548EEE /* LocalDevice.swift */; };
F9C5CE34289453B400548EEE /* AudioWaveformManagerImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9C5CB62289453B200548EEE /* AudioWaveformManagerImpl.swift */; };
@ -4167,6 +4170,7 @@
040507142F8063A40078B769 /* RemoteReleaseNotesFetchingManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteReleaseNotesFetchingManagerTests.swift; sourceTree = "<group>"; };
04127D902F23B3B000B4E95B /* CVCapsuleLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CVCapsuleLabel.swift; sourceTree = "<group>"; };
041A5F062E05B3F000FAED05 /* BackupEnablementMegaphone.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupEnablementMegaphone.swift; sourceTree = "<group>"; };
041A5F082E05D3AC00FAED05 /* BackupEnablementReminderMegaphoneTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupEnablementReminderMegaphoneTests.swift; sourceTree = "<group>"; };
042223B92EDF30B300158556 /* OutgoingUnpinMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OutgoingUnpinMessage.swift; sourceTree = "<group>"; };
0426758A2EC4D5BF00124C5F /* PinnedMessageIconView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PinnedMessageIconView.swift; sourceTree = "<group>"; };
0426758F2EC529F500124C5F /* TSInfoMessage+PinnedMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TSInfoMessage+PinnedMessage.swift"; sourceTree = "<group>"; };
@ -4224,6 +4228,7 @@
046092232FBCC7E700A8765F /* SafetyTipsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafetyTipsManager.swift; sourceTree = "<group>"; };
046926082E8EBAA800B1FC74 /* TSInfoMessage+Polls.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TSInfoMessage+Polls.swift"; sourceTree = "<group>"; };
0477BE312FA4FC38002F9B47 /* TSReleaseNotesThread.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TSReleaseNotesThread.swift; sourceTree = "<group>"; };
047A6DCF2E00B5640048EDF4 /* BackupKeyReminderMegaphoneTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupKeyReminderMegaphoneTests.swift; sourceTree = "<group>"; };
0480EFFF2E57C513006CBB29 /* BackupsEnabledNotificationMegaphone.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupsEnabledNotificationMegaphone.swift; sourceTree = "<group>"; };
0484CECD2F44B7BB009AB2CB /* AdminDeleteRecord.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdminDeleteRecord.swift; sourceTree = "<group>"; };
0484CECF2F44BCFB009AB2CB /* AdminDeleteManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdminDeleteManager.swift; sourceTree = "<group>"; };
@ -4990,7 +4995,7 @@
50552C302BAC079A00815474 /* CallLinkTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallLinkTest.swift; sourceTree = "<group>"; };
5056B3BE2DEED72800F55320 /* MonotonicDateTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MonotonicDateTest.swift; sourceTree = "<group>"; };
50589CDD2E8C44D5003EF42A /* PreKeyStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreKeyStore.swift; sourceTree = "<group>"; };
50589CDF2E8C4AD5003EF42A /* PreKeyRecord.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreKeyRecord.swift; sourceTree = "<group>"; };
50589CDF2E8C4AD5003EF42A /* PreKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreKey.swift; sourceTree = "<group>"; };
50597BB92B97C38C004681E1 /* SignalAccountStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignalAccountStore.swift; sourceTree = "<group>"; };
50597BBB2B97C449004681E1 /* UsernameLookupRecordStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UsernameLookupRecordStore.swift; sourceTree = "<group>"; };
50597BBE2B97D629004681E1 /* SearchableNameFinder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchableNameFinder.swift; sourceTree = "<group>"; };
@ -5122,7 +5127,6 @@
50D839502F916A3700EE009A /* MessageRequestDecliner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageRequestDecliner.swift; sourceTree = "<group>"; };
50D879692A16D2C20031345D /* MessageLoaderBatchTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageLoaderBatchTest.swift; sourceTree = "<group>"; };
50D9CD8C2C52D78000273D6C /* StoryRecipientManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoryRecipientManager.swift; sourceTree = "<group>"; };
50DAF7DF2FD87BEC00BE7430 /* OrphanedBackupAttachmentTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrphanedBackupAttachmentTest.swift; sourceTree = "<group>"; };
50DCCBF92F1817280024D124 /* DisappearingMessagesConfigurationMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisappearingMessagesConfigurationMessage.swift; sourceTree = "<group>"; };
50DCCBFB2F181A790024D124 /* ProfileKeyMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileKeyMessage.swift; sourceTree = "<group>"; };
50DCCBFD2F1820600024D124 /* OutgoingSenderKeyDistributionMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OutgoingSenderKeyDistributionMessage.swift; sourceTree = "<group>"; };
@ -5335,7 +5339,7 @@
66586D3529005A1B00DDA9B9 /* story_viewer_onboarding_1.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = story_viewer_onboarding_1.json; sourceTree = "<group>"; };
66586D3629005A1B00DDA9B9 /* story_viewer_onboarding_3.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = story_viewer_onboarding_3.json; sourceTree = "<group>"; };
66586D4029009C0000DDA9B9 /* TextAttachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextAttachment.swift; sourceTree = "<group>"; };
6659A0252A7C11A800066AB7 /* PreKeyManagerImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreKeyManagerImpl.swift; sourceTree = "<group>"; };
6659A0252A7C11A800066AB7 /* PrekeyManagerImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrekeyManagerImpl.swift; sourceTree = "<group>"; };
6659A0272A7C11ED00066AB7 /* MockPreKeyManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockPreKeyManager.swift; sourceTree = "<group>"; };
6659A0302A7C5B9700066AB7 /* PreKeyUploadBundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreKeyUploadBundle.swift; sourceTree = "<group>"; };
6659A0382A81933B00066AB7 /* ProvisioningPermissionsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProvisioningPermissionsViewController.swift; sourceTree = "<group>"; };
@ -5351,6 +5355,7 @@
665FAE8B2A02C0D400FA298D /* SpoilerRevealState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpoilerRevealState.swift; sourceTree = "<group>"; };
6660725D2BAB36960084B3D2 /* AttachmentDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentDataSource.swift; sourceTree = "<group>"; };
6664B9AA2A314EBD008EF74B /* SpoilerRevealStateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpoilerRevealStateTests.swift; sourceTree = "<group>"; };
666654202AD0B03F00B23B32 /* MasterKeySyncManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MasterKeySyncManager.swift; sourceTree = "<group>"; };
66681CDE2C58174F00E50136 /* BackupAttachmentDownloadStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupAttachmentDownloadStoreTests.swift; sourceTree = "<group>"; };
6671DC862CD44C9B002620EF /* LastVisibleInteractionStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LastVisibleInteractionStore.swift; sourceTree = "<group>"; };
66734F002CA1ED3A00558494 /* BackupAttachmentUploadScheduler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupAttachmentUploadScheduler.swift; sourceTree = "<group>"; };
@ -5436,6 +5441,7 @@
668B5BFB2C7E46D30018CF36 /* PaletteChatColor+Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PaletteChatColor+Constants.swift"; sourceTree = "<group>"; };
668CAB3D289983520085A2C3 /* AudioMessagePlaybackRateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioMessagePlaybackRateView.swift; sourceTree = "<group>"; };
668E403B2BE43752004B6730 /* SDAnimatedImage+Attachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SDAnimatedImage+Attachment.swift"; sourceTree = "<group>"; };
668FE09A28B923A4008B9071 /* Bool+SSK.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bool+SSK.swift"; sourceTree = "<group>"; };
668FE09E28B947ED008B9071 /* StoryContextMenuGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoryContextMenuGenerator.swift; sourceTree = "<group>"; };
6691E7EE2996E8FB0032A68A /* TSRequestOWSURLSessionMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TSRequestOWSURLSessionMock.swift; sourceTree = "<group>"; };
6691E7F12996E9BC0032A68A /* RegistrationSessionManagerMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegistrationSessionManagerMock.swift; sourceTree = "<group>"; };
@ -5653,6 +5659,8 @@
729E0B082CA4ADE2002EC961 /* Threading.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Threading.swift; sourceTree = "<group>"; };
72A132A42CA210C2000ACED6 /* DarwinNotificationCenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DarwinNotificationCenter.swift; sourceTree = "<group>"; };
72A132A62CA25EE9000ACED6 /* SDSCrossProcess.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SDSCrossProcess.swift; sourceTree = "<group>"; };
72B0C23F2C9EEA7700B57DAD /* PreKeyRecord.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreKeyRecord.swift; sourceTree = "<group>"; };
72B0C2412C9EED0800B57DAD /* SignedPreKeyRecord.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignedPreKeyRecord.swift; sourceTree = "<group>"; };
72B4819C2BD60FDF008B8BA1 /* OWSMath.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OWSMath.swift; sourceTree = "<group>"; };
72B994DA2BE950DB000CBBFD /* TestAppContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestAppContext.swift; sourceTree = "<group>"; };
72DB95AD2C8C7C7B00FD2266 /* String+OWS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+OWS.swift"; sourceTree = "<group>"; };
@ -5921,7 +5929,7 @@
88A4717228664DE3001A3065 /* BaseMemberViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseMemberViewController.swift; sourceTree = "<group>"; };
88A4CC0F246CE2760082211F /* TransferProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransferProgressView.swift; sourceTree = "<group>"; };
88A505F323DA16E10005C012 /* ExperienceUpgradeManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExperienceUpgradeManager.swift; sourceTree = "<group>"; };
88A505F923DBA1360005C012 /* IntroducingPINsMegaphone.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntroducingPINsMegaphone.swift; sourceTree = "<group>"; };
88A505F923DBA1360005C012 /* IntroducingPINs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntroducingPINs.swift; sourceTree = "<group>"; };
88A695BC232C18DF002F7B9B /* AudioWaveformProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioWaveformProgressView.swift; sourceTree = "<group>"; };
88A941982409A391000E9700 /* LottieToggleButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LottieToggleButton.swift; sourceTree = "<group>"; };
88A9729122FA5D4B004B4FBF /* AttachmentFormatPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentFormatPickerView.swift; sourceTree = "<group>"; };
@ -6980,7 +6988,6 @@
D92812E12FA95C0D00667DCF /* DisplayableAccountEntropyPoolTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisplayableAccountEntropyPoolTest.swift; sourceTree = "<group>"; };
D92A1CD92E314BD000C91E21 /* DebugUIPrompts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugUIPrompts.swift; sourceTree = "<group>"; };
D92AB7D729E3BEE30081CA7D /* OWSDeviceManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OWSDeviceManager.swift; sourceTree = "<group>"; };
D92B55EE2FD0D9210083B070 /* BackupPlanOptionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupPlanOptionView.swift; sourceTree = "<group>"; };
D92C57542A2925AD00A03BB7 /* TSInfoMessage+DisplayableGroupUpdateItemTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TSInfoMessage+DisplayableGroupUpdateItemTest.swift"; sourceTree = "<group>"; };
D92CA9EE2F500EA500FDE32D /* LeaveGroupCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LeaveGroupCoordinator.swift; sourceTree = "<group>"; };
D92CB5552F030F7A00537EBE /* BackupSubscriptionAlreadyRedeemedSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupSubscriptionAlreadyRedeemedSheet.swift; sourceTree = "<group>"; };
@ -7024,7 +7031,6 @@
D943F3EE2892F89B008C0C8B /* NSELogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSELogger.swift; sourceTree = "<group>"; };
D94441302D559567005B2A54 /* UUIDv7.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UUIDv7.swift; sourceTree = "<group>"; };
D94441322D559C6B005B2A54 /* UUIDv7Test.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UUIDv7Test.swift; sourceTree = "<group>"; };
D944D2FC2FBCE9C00026FD21 /* OWSDecryptionPlaceholderExpirationJob.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OWSDecryptionPlaceholderExpirationJob.swift; sourceTree = "<group>"; };
D945319D2CE53CC8004DAB30 /* SubscriptionRedemptionNecessityChecker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionRedemptionNecessityChecker.swift; sourceTree = "<group>"; };
D94852262F6A223500B130B2 /* GroupCallVideoContextMenuConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupCallVideoContextMenuConfiguration.swift; sourceTree = "<group>"; };
D9495A6C2C7683D100843BC1 /* TSOutgoingMessageRecipientState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TSOutgoingMessageRecipientState.swift; sourceTree = "<group>"; };
@ -7168,9 +7174,9 @@
D96869442E1065F1005451E4 /* SeriallyAccessedState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeriallyAccessedState.swift; sourceTree = "<group>"; };
D968B4972C9E1AC3006B14E1 /* SmsLockIconView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SmsLockIconView.swift; sourceTree = "<group>"; };
D968F71D2C34884B00AB318B /* BackupArchiveReleaseNotesRecipientArchiver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupArchiveReleaseNotesRecipientArchiver.swift; sourceTree = "<group>"; };
D9697C152FD78FDD00119F72 /* BackupNeverShareRecoveryKeySheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupNeverShareRecoveryKeySheet.swift; sourceTree = "<group>"; };
D96A94A62954E57F004EA434 /* DonateViewController+MonthlyPaypalDonation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DonateViewController+MonthlyPaypalDonation.swift"; sourceTree = "<group>"; };
D96A94A82955270D004EA434 /* Stripe+Subscriptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Stripe+Subscriptions.swift"; sourceTree = "<group>"; };
D96BE42D292EF04200E4FE1A /* PaypalButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PaypalButton.swift; sourceTree = "<group>"; };
D97046052E81D41F0034C05D /* InfoMessageGroupUpdateMigrator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoMessageGroupUpdateMigrator.swift; sourceTree = "<group>"; };
D97046092E81D5BB0034C05D /* InfoMessageGroupUpdateMigratorTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoMessageGroupUpdateMigratorTest.swift; sourceTree = "<group>"; };
D97054192CFE49E200AC7954 /* SubscriptionFetcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionFetcher.swift; sourceTree = "<group>"; };
@ -7185,7 +7191,6 @@
D9791BC32EAADEFD0016AA5A /* HTTPResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPResponse.swift; sourceTree = "<group>"; };
D97992A02D9E55E10080A4F5 /* CurrencyFormatterTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrencyFormatterTest.swift; sourceTree = "<group>"; };
D97992A22D9E55F80080A4F5 /* CurrencyFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrencyFormatter.swift; sourceTree = "<group>"; };
D97992AB2FC147C2002E79F3 /* ExperienceUpgradeStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExperienceUpgradeStore.swift; sourceTree = "<group>"; };
D979CC1F2AD3933B006AAC49 /* IndividualCallRecordManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IndividualCallRecordManager.swift; sourceTree = "<group>"; };
D979CC202AD3933B006AAC49 /* IncomingCallEventSyncMessageManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IncomingCallEventSyncMessageManager.swift; sourceTree = "<group>"; };
D979CC222AD3933B006AAC49 /* OutgoingCallEventSyncMessageManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OutgoingCallEventSyncMessageManager.swift; sourceTree = "<group>"; };
@ -8140,7 +8145,7 @@
F9C5C9B8289453B100548EEE /* StorageService.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StorageService.pb.swift; sourceTree = "<group>"; };
F9C5C9B9289453B100548EEE /* SSKProto+OWS.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "SSKProto+OWS.swift"; sourceTree = "<group>"; };
F9C5C9C2289453B100548EEE /* PreKeyManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreKeyManager.swift; sourceTree = "<group>"; };
F9C5C9C7289453B100548EEE /* RemoteAttestationAuthFetcher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RemoteAttestationAuthFetcher.swift; sourceTree = "<group>"; };
F9C5C9C7289453B100548EEE /* RemoteAttestation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RemoteAttestation.swift; sourceTree = "<group>"; };
F9C5C9D9289453B100548EEE /* ContactDiscoveryTask.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactDiscoveryTask.swift; sourceTree = "<group>"; };
F9C5C9DC289453B100548EEE /* ContactDiscoveryError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactDiscoveryError.swift; sourceTree = "<group>"; };
F9C5C9DE289453B100548EEE /* SignalAccount.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SignalAccount.swift; sourceTree = "<group>"; };
@ -8280,6 +8285,7 @@
F9C5CB57289453B200548EEE /* ModelReadCache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ModelReadCache.swift; sourceTree = "<group>"; };
F9C5CB58289453B200548EEE /* Platform.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Platform.swift; sourceTree = "<group>"; };
F9C5CB59289453B200548EEE /* BuildFlags.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BuildFlags.swift; sourceTree = "<group>"; };
F9C5CB5B289453B200548EEE /* ExperienceUpgradeFinder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExperienceUpgradeFinder.swift; sourceTree = "<group>"; };
F9C5CB5D289453B200548EEE /* SwiftSingletons.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwiftSingletons.swift; sourceTree = "<group>"; };
F9C5CB61289453B200548EEE /* LocalDevice.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LocalDevice.swift; sourceTree = "<group>"; };
F9C5CB62289453B200548EEE /* AudioWaveformManagerImpl.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AudioWaveformManagerImpl.swift; sourceTree = "<group>"; };
@ -9900,22 +9906,6 @@
path = Debugging;
sourceTree = "<group>";
};
50DAF7E12FD87BFD00BE7430 /* Backups */ = {
isa = PBXGroup;
children = (
50DAF7E22FD87C7000BE7430 /* Attachments */,
);
path = Backups;
sourceTree = "<group>";
};
50DAF7E22FD87C7000BE7430 /* Attachments */ = {
isa = PBXGroup;
children = (
50DAF7DF2FD87BEC00BE7430 /* OrphanedBackupAttachmentTest.swift */,
);
path = Attachments;
sourceTree = "<group>";
};
50E0198E2CC2491A0063EA48 /* Concurrency */ = {
isa = PBXGroup;
children = (
@ -10311,6 +10301,20 @@
path = WhoAmI;
sourceTree = "<group>";
};
6659A0242A7C112700066AB7 /* PreKeys */ = {
isa = PBXGroup;
children = (
6659A0272A7C11ED00066AB7 /* MockPreKeyManager.swift */,
F9C5C9C2289453B100548EEE /* PreKeyManager.swift */,
6659A0252A7C11A800066AB7 /* PrekeyManagerImpl.swift */,
C17345BA2A5E000300C6426D /* PreKeyTarget.swift */,
D94AEB3B2D28940500B03D7A /* PreKeyTaskAPIClient.swift */,
C1ED5CA02A72E3D5009AD3FC /* PreKeyTaskManager.swift */,
6659A0302A7C5B9700066AB7 /* PreKeyUploadBundle.swift */,
);
path = PreKeys;
sourceTree = "<group>";
};
6659A02D2A7C171900066AB7 /* PreKeys */ = {
isa = PBXGroup;
children = (
@ -11456,7 +11460,7 @@
D9C2D77F299EC11400D79715 /* CreateUsernameMegaphone.swift */,
D9C0AE682BD82DBC00FCB05E /* InactiveLinkedDeviceReminderMegaphone.swift */,
04BBBE8F2E259A5F00E914B1 /* InactivePrimaryDeviceReminderMegaphone.swift */,
88A505F923DBA1360005C012 /* IntroducingPINsMegaphone.swift */,
88A505F923DBA1360005C012 /* IntroducingPINs.swift */,
8837F74023DA0B0F00772A32 /* MegaphoneView.swift */,
B9B7BC642D41C61500C26E42 /* NewLinkedDeviceNotificationMegaphone.swift */,
8806EF18248DBD7200E764C7 /* NotificationPermissionReminderMegaphone.swift */,
@ -13343,8 +13347,8 @@
isa = PBXGroup;
children = (
D9C7CEB328EB8495001E87B6 /* ExperienceUpgrade.swift */,
F9C5CB5B289453B200548EEE /* ExperienceUpgradeFinder.swift */,
D9C7CECA28EBC09C001E87B6 /* ExperienceUpgradeManifest.swift */,
D97992AB2FC147C2002E79F3 /* ExperienceUpgradeStore.swift */,
040506FB2F7FE3D50078B769 /* RemoteAnnouncementModel.swift */,
D98DD85E28EE53B00089333E /* RemoteMegaphoneModel.swift */,
040506FF2F8049910078B769 /* RemoteReleaseNotesService.swift */,
@ -13522,8 +13526,6 @@
D97C9FF12DD3FB7200191CE2 /* BackupDisablingManager.swift */,
D93FA5BE2DE77E440013879E /* BackupEnablingManager.swift */,
D93BDD932E43063B00779BD8 /* BackupKeepKeySafeSheet.swift */,
D9697C152FD78FDD00119F72 /* BackupNeverShareRecoveryKeySheet.swift */,
D92B55EE2FD0D9210083B070 /* BackupPlanOptionView.swift */,
D98CA2B22DF2450E0060370E /* BackupRecordKeyViewController.swift */,
04E66D412DFF3A3E0059DBAC /* BackupRecoveryKeyReminderCoordinator.swift */,
04B975452E43A4AA00E20364 /* BackupRefreshManager.swift */,
@ -13910,6 +13912,8 @@
children = (
D90AA32E2CC9616A00021CB0 /* Signal-Message-Backup-Tests */,
D90AA6182CC961ED00021CB0 /* BackupArchiveIntegrationTests.swift */,
041A5F082E05D3AC00FAED05 /* BackupEnablementReminderMegaphoneTests.swift */,
047A6DCF2E00B5640048EDF4 /* BackupKeyReminderMegaphoneTests.swift */,
04E66D432E00AB3A0059DBAC /* BackupSettingsStoreTests.swift */,
D9A36B922C7FEDA100CEC0E7 /* LineByLineStringDiff.swift */,
);
@ -14579,6 +14583,7 @@
F900F2DC27F25AB300431E09 /* DonationReceiptViewController.swift */,
D93EDC032AE9E3CD0004BDD9 /* DonationSettingsViewController+MySupport.swift */,
F9A8ACC6280A175E00AFC6A7 /* DonationSettingsViewController.swift */,
D96BE42D292EF04200E4FE1A /* PaypalButton.swift */,
);
path = Donations;
sourceTree = "<group>";
@ -14590,7 +14595,6 @@
D92EFDEB2F68EB7D0031D257 /* AttachmentBackfill */,
7255A4C32B98D5A800E95368 /* Attachments */,
720547F12B9C8F5E00E2CF2F /* Avatars */,
F9C5CA52289453B100548EEE /* Axolotl */,
665C0D5A2ADF537000539A37 /* Backups */,
F945FE482984795A00C835C7 /* Calls */,
D9C2D78529A80BE700D79715 /* ChangePhoneNumber */,
@ -14624,6 +14628,7 @@
046092252FBCD28300A8765F /* SafetyTips */,
50B791552E8B39230063E71E /* Search */,
6673FF6A2978B5B900F96CFD /* SecureValueRecovery */,
F9C5CB98289453B200548EEE /* Security */,
F9C5CAB4289453B200548EEE /* Spam */,
F9C5CA2F289453B100548EEE /* Storage */,
88E34F2522F269B600966CC2 /* StorageService */,
@ -14651,7 +14656,6 @@
F94261FF289B1B5400460798 /* Account */,
D92EFDED2F69B9D00031D257 /* AttachmentBackfill */,
50ED28002F0EDAFB00E57C54 /* Attachments */,
50DAF7E12FD87BFD00BE7430 /* Backups */,
F945FE4B298481D800C835C7 /* Calls */,
D985D86229B91C2B0087C90C /* ChangePhoneNumber */,
50E0198E2CC2491A0063EA48 /* Concurrency */,
@ -14740,7 +14744,6 @@
F9C5C950289453B100548EEE /* OWSAddToProfileWhitelistOfferMessage+SDS.swift */,
F9C5C997289453B100548EEE /* OWSAddToProfileWhitelistOfferMessage.h */,
F9C5C958289453B100548EEE /* OWSAddToProfileWhitelistOfferMessage.m */,
D944D2FC2FBCE9C00026FD21 /* OWSDecryptionPlaceholderExpirationJob.swift */,
F9C5C93B289453B100548EEE /* OWSIdentityManager.swift */,
F9C5C983289453B100548EEE /* OWSMessageDecrypter.swift */,
F9C5C973289453B100548EEE /* OWSMessageSend.swift */,
@ -15026,6 +15029,7 @@
isa = PBXGroup;
children = (
6646572F2AC369EB0099DE1C /* PhoneNumberDiscoverabilityManager */,
6659A0242A7C112700066AB7 /* PreKeys */,
661170BF2ABA458800A1B16D /* TSAccountManager */,
C18D6FDF2D4D8FB40085E3B9 /* AccountEntropyPool.swift */,
D9EDF2772E4D2A1E001D4BEC /* AccountEntropyPoolManager.swift */,
@ -15036,6 +15040,7 @@
D9F399B12A96D65D001599EC /* IdentityKeyMismatchManager.swift */,
5033D46629D76BD0007FEADA /* LocalIdentifiers.swift */,
D94AEB392D28837A00B03D7A /* MasterKey.swift */,
666654202AD0B03F00B23B32 /* MasterKeySyncManager.swift */,
72552EF32C9EF9E7008614AF /* OWSIdentity.swift */,
D9CAF74F2A0ACFF20049193A /* PniDistributionParameterBuilder.swift */,
C18E3C712A9FF65D003D1CF1 /* PniDistributionSyncMessage.swift */,
@ -15164,6 +15169,7 @@
F9C5CA2F289453B100548EEE /* Storage */ = {
isa = PBXGroup;
children = (
F9C5CA52289453B100548EEE /* AxolotlStore */,
F9C5CA31289453B100548EEE /* Database */,
667DEE562BC7148E00EFF32D /* MediaGallery */,
F9C5CA9B289453B100548EEE /* BaseModel.h */,
@ -15232,33 +15238,32 @@
path = Snapshots;
sourceTree = "<group>";
};
F9C5CA52289453B100548EEE /* Axolotl */ = {
F9C5CA52289453B100548EEE /* AxolotlStore */ = {
isa = PBXGroup;
children = (
667664352A43BBCD00716B84 /* CombinedFingerprints.swift */,
50A156C62FA11AA8008FE086 /* Fingerprint.swift */,
F9C5CA5F289453B100548EEE /* Model */,
C198FDD52A37C905000BCAC9 /* KyberPreKeyStoreImpl.swift */,
504F98B02EAFFAC600DF465B /* KyberPreKeyUseRecord.swift */,
6659A0272A7C11ED00066AB7 /* MockPreKeyManager.swift */,
F9C5CA59289453B100548EEE /* OldSenderKeyStore.swift */,
F9C5CB9F289453B200548EEE /* OWSRecipientIdentity.swift */,
F9C5CB99289453B200548EEE /* OWSVerificationState.h */,
5010B6B32C6BD41E00314CD4 /* PreKeyBundle.swift */,
50589CDF2E8C4AD5003EF42A /* PreKey.swift */,
5050A8782B76E2E100E9BFA4 /* PreKeyId.swift */,
F9C5C9C2289453B100548EEE /* PreKeyManager.swift */,
6659A0252A7C11A800066AB7 /* PreKeyManagerImpl.swift */,
50589CDF2E8C4AD5003EF42A /* PreKeyRecord.swift */,
50589CDD2E8C44D5003EF42A /* PreKeyStore.swift */,
F9C5CA75289453B100548EEE /* PreKeyStoreImpl.swift */,
C17345BA2A5E000300C6426D /* PreKeyTarget.swift */,
D94AEB3B2D28940500B03D7A /* PreKeyTaskAPIClient.swift */,
C1ED5CA02A72E3D5009AD3FC /* PreKeyTaskManager.swift */,
6659A0302A7C5B9700066AB7 /* PreKeyUploadBundle.swift */,
501050BA2EB959A4005161CA /* SessionStore.swift */,
F9C5CA56289453B100548EEE /* SignalProtocolStore.swift */,
F9C5CA55289453B100548EEE /* SignedPreKeyStoreImpl.swift */,
);
path = Axolotl;
path = AxolotlStore;
sourceTree = "<group>";
};
F9C5CA5F289453B100548EEE /* Model */ = {
isa = PBXGroup;
children = (
5010B6B32C6BD41E00314CD4 /* PreKeyBundle.swift */,
72B0C23F2C9EEA7700B57DAD /* PreKeyRecord.swift */,
72B0C2412C9EED0800B57DAD /* SignedPreKeyRecord.swift */,
);
path = Model;
sourceTree = "<group>";
};
F9C5CA85289453B100548EEE /* JobRecords */ = {
@ -15307,12 +15312,10 @@
F9C5CAD3289453B200548EEE /* API */,
88DF819328E112F600F8BA80 /* SignalProxy */,
669E8FE528B4149200043D28 /* BaseOWSURLSessionMock.swift */,
727328062CA6CF530080E2C7 /* Certificates.swift */,
F9C5CAC4289453B200548EEE /* ChatConnectionManager.swift */,
509A8DC12E25817E0024BF14 /* ConnectionLock.swift */,
F9C5CAF7289453B200548EEE /* ContentProxy.swift */,
F9C5CAF2289453B200548EEE /* HttpHeaders.swift */,
727328042CA6619A0080E2C7 /* HttpSecurityPolicy.swift */,
F9C5CAF1289453B200548EEE /* NetworkInterfaceSet.swift */,
F9C5CAC8289453B200548EEE /* OutageDetection.swift */,
72328C8A2C6C7322000EA728 /* OWSCensorshipConfiguration.swift */,
@ -15389,7 +15392,7 @@
F9D5BFCC2979A017001737E5 /* OWSRequestFactory+Spam.swift */,
D95C39E7296DEBFB00A9DA23 /* OWSRequestFactory+Usernames.swift */,
F9C5CAE2289453B200548EEE /* OWSRequestFactory.swift */,
F9C5C9C7289453B100548EEE /* RemoteAttestationAuthFetcher.swift */,
F9C5C9C7289453B100548EEE /* RemoteAttestation.swift */,
66C2B1302A05D28A008DDE72 /* TSRequest.swift */,
);
path = Requests;
@ -15410,6 +15413,7 @@
058B49922C66804B00307D38 /* AVAssetExportSession+Async.swift */,
F9C5CB64289453B200548EEE /* Batching.swift */,
F9C5CB40289453B200548EEE /* Bench.swift */,
668FE09A28B923A4008B9071 /* Bool+SSK.swift */,
E7D7C93E28B580AC003F043B /* Bundle+OWS.swift */,
88D7BA9D266809F50088D1C2 /* CallMessageRelay.swift */,
76387BEF28F4ED73002C7BA5 /* CaseIterable.swift */,
@ -15538,6 +15542,19 @@
path = TestUtils;
sourceTree = "<group>";
};
F9C5CB98289453B200548EEE /* Security */ = {
isa = PBXGroup;
children = (
727328062CA6CF530080E2C7 /* Certificates.swift */,
667664352A43BBCD00716B84 /* CombinedFingerprints.swift */,
50A156C62FA11AA8008FE086 /* Fingerprint.swift */,
727328042CA6619A0080E2C7 /* HttpSecurityPolicy.swift */,
F9C5CB9F289453B200548EEE /* OWSRecipientIdentity.swift */,
F9C5CB99289453B200548EEE /* OWSVerificationState.h */,
);
path = Security;
sourceTree = "<group>";
};
F9C5CBA3289453B200548EEE /* Groups */ = {
isa = PBXGroup;
children = (
@ -18059,11 +18076,9 @@
041A5F072E05B3F900FAED05 /* BackupEnablementMegaphone.swift in Sources */,
D9DF21EC2E21BD6600A962B2 /* BackupEnablingManager.swift in Sources */,
D93BDD942E43064500779BD8 /* BackupKeepKeySafeSheet.swift in Sources */,
D9697C162FD78FE400119F72 /* BackupNeverShareRecoveryKeySheet.swift in Sources */,
D999345A2DE97BBC002C9196 /* BackupOnboardingCoordinator.swift in Sources */,
D9DE34FD2DEE7765005099D7 /* BackupOnboardingIntroViewController.swift in Sources */,
D98CA2AD2DF14A890060370E /* BackupOnboardingKeyIntroViewController.swift in Sources */,
D92B55EF2FD0D9210083B070 /* BackupPlanOptionView.swift in Sources */,
D98CA2B32DF245140060370E /* BackupRecordKeyViewController.swift in Sources */,
04E66D422DFF3A4B0059DBAC /* BackupRecoveryKeyReminderCoordinator.swift in Sources */,
50438A8E2ECBBDF600FCB28F /* BackupRefreshManager.swift in Sources */,
@ -18454,7 +18469,7 @@
66D31F972E5E685300A1C82D /* InternalListMediaViewController.swift in Sources */,
8862A55925F090C5005D65DB /* InternalSettingsViewController.swift in Sources */,
663883572D4C0360008EA898 /* InternalSQLClientViewController.swift in Sources */,
88A505FA23DBA1360005C012 /* IntroducingPINsMegaphone.swift in Sources */,
88A505FA23DBA1360005C012 /* IntroducingPINs.swift in Sources */,
32AC5CE7255B51E900829BD8 /* JoinGroupCallPill.swift in Sources */,
45C845AD291466C0005F6EA5 /* JournalingOrderedDictionary.swift in Sources */,
5045F44229E0DB7100058E5F /* LaunchJobs.swift in Sources */,
@ -18569,6 +18584,7 @@
3495FF0525F9091400959D6E /* PaymentsViewPassphraseGridViewController.swift in Sources */,
3495FF0F25F9538900959D6E /* PaymentsViewPassphraseSplashViewController.swift in Sources */,
34FB6A5325D2D10400E599B1 /* PaymentsViewUtils.swift in Sources */,
D96BE42E292EF04200E4FE1A /* PaypalButton.swift in Sources */,
667AF9DE2B4C5824008AEE5D /* PersistableGroupUpdateItem+CVComponentSystemMessageAction.swift in Sources */,
C176B48A299DA25500B1900D /* PhoneNumberPrivacySettingsViewController.swift in Sources */,
4C5250D221E7BD7D00CE3D95 /* PhoneNumberValidator.swift in Sources */,
@ -19077,6 +19093,7 @@
50F039C42C6D239500162B99 /* BlockedRecipientStore.swift in Sources */,
F9C5CC31289453B300548EEE /* BlockingManager.swift in Sources */,
F9C5CC74289453B300548EEE /* BlurHash.swift in Sources */,
668FE09B28B923A4008B9071 /* Bool+SSK.swift in Sources */,
505F76332BC45C0700B1B51C /* BuildFlags+Generated.swift in Sources */,
F9C5CE2B289453B400548EEE /* BuildFlags.swift in Sources */,
D9F9A63B2BFFFCC400EF13EC /* BulkDeleteInteractionJobQueue.swift in Sources */,
@ -19250,8 +19267,8 @@
F9C5CE44289453B400548EEE /* Error+IsRetryable.swift in Sources */,
F9C5CE23289453B400548EEE /* Error+SSK.swift in Sources */,
D9C7CEB428EB8495001E87B6 /* ExperienceUpgrade.swift in Sources */,
F9C5CE2D289453B400548EEE /* ExperienceUpgradeFinder.swift in Sources */,
D9C7CECB28EBC09C001E87B6 /* ExperienceUpgradeManifest.swift in Sources */,
D97992AC2FC147C5002E79F3 /* ExperienceUpgradeStore.swift in Sources */,
D98BC5332EE387A30052A81F /* ExpirationJob.swift in Sources */,
C1FB9B752B16498C00D51A3B /* ExternalPendingDonationStore.swift in Sources */,
F9C5CE57289453B400548EEE /* Factories.swift in Sources */,
@ -19381,6 +19398,7 @@
F9C5CDF6289453B400548EEE /* LRUCache.swift in Sources */,
F9C5CDE3289453B400548EEE /* MailtoLink.swift in Sources */,
D94AEB3A2D28837F00B03D7A /* MasterKey.swift in Sources */,
666654212AD0B03F00B23B32 /* MasterKeySyncManager.swift in Sources */,
F9C5CE08289453B400548EEE /* Math+OWS.swift in Sources */,
66BED7E32B9B8FDF00236BAD /* MediaBandwidthPreferenceStore.swift in Sources */,
66BED7E62B9B929600236BAD /* MediaBandwidthPreferenceStoreImpl.swift in Sources */,
@ -19545,7 +19563,6 @@
66D31DA92BC48D7900EAF735 /* OWSContactPhoneNumber.swift in Sources */,
725465192BA00F7500EABFD2 /* OWSContactsManager.swift in Sources */,
72328C892C6C6733000EA728 /* OWSCountryMetadata.swift in Sources */,
D944D2FD2FBCE9C00026FD21 /* OWSDecryptionPlaceholderExpirationJob.swift in Sources */,
F9C5CCFD289453B300548EEE /* OWSDevice.swift in Sources */,
D92AB7D829E3BEE30081CA7D /* OWSDeviceManager.swift in Sources */,
F9C5CE0C289453B400548EEE /* OWSDeviceNames.swift in Sources */,
@ -19668,11 +19685,12 @@
500876142BF7B32A00D6F615 /* Preconditions.swift in Sources */,
7255A4D42B98E36900E95368 /* Preferences.swift in Sources */,
D95C39EC296E1BC600A9DA23 /* PrefixedLogger.swift in Sources */,
50589CE02E8C4AD5003EF42A /* PreKey.swift in Sources */,
5010B6B42C6BD41E00314CD4 /* PreKeyBundle.swift in Sources */,
5050A8792B76E2E100E9BFA4 /* PreKeyId.swift in Sources */,
F9C5CCAC289453B300548EEE /* PreKeyManager.swift in Sources */,
6659A0262A7C11A800066AB7 /* PreKeyManagerImpl.swift in Sources */,
50589CE02E8C4AD5003EF42A /* PreKeyRecord.swift in Sources */,
6659A0262A7C11A800066AB7 /* PrekeyManagerImpl.swift in Sources */,
72B0C2402C9EEA8200B57DAD /* PreKeyRecord.swift in Sources */,
50589CDE2E8C44D5003EF42A /* PreKeyStore.swift in Sources */,
F9C5CD52289453B300548EEE /* PreKeyStoreImpl.swift in Sources */,
C17345BB2A5E000300C6426D /* PreKeyTarget.swift in Sources */,
@ -19746,7 +19764,7 @@
6646573B2AC388C70099DE1C /* RegistrationStateChangeManager.swift in Sources */,
6646573D2AC3894D0099DE1C /* RegistrationStateChangeManagerImpl.swift in Sources */,
040506FC2F7FE3DB0078B769 /* RemoteAnnouncementModel.swift in Sources */,
F9C5CCB0289453B300548EEE /* RemoteAttestationAuthFetcher.swift in Sources */,
F9C5CCB0289453B300548EEE /* RemoteAttestation.swift in Sources */,
F9C5CE17289453B400548EEE /* RemoteConfigManager.swift in Sources */,
D98DD86028EE53B00089333E /* RemoteMegaphoneModel.swift in Sources */,
040507012F804C240078B769 /* RemoteReleaseNotesService.swift in Sources */,
@ -19824,6 +19842,7 @@
F9C5CC9A289453B300548EEE /* SignalService.pb.swift in Sources */,
F9C5CCE2289453B300548EEE /* SignalServiceAddress.swift in Sources */,
F9C5CDBB289453B400548EEE /* SignalServiceProfile.swift in Sources */,
72B0C2422C9EED0E00B57DAD /* SignedPreKeyRecord.swift in Sources */,
F9C5CD33289453B300548EEE /* SignedPreKeyStoreImpl.swift in Sources */,
F9C5CC51289453B300548EEE /* SMKError.swift in Sources */,
F9C5CC53289453B300548EEE /* SMKSecretSessionCipher.swift in Sources */,
@ -20094,6 +20113,8 @@
D90AA6192CC961ED00021CB0 /* BackupArchiveIntegrationTests.swift in Sources */,
66681CDF2C58174F00E50136 /* BackupAttachmentDownloadStoreTests.swift in Sources */,
66C795302C9B83A200C13937 /* BackupAttachmentUploadStoreTests.swift in Sources */,
04AA85BC2E0EFC5C0051C0A7 /* BackupEnablementReminderMegaphoneTests.swift in Sources */,
047A6DD02E00B5720048EDF4 /* BackupKeyReminderMegaphoneTests.swift in Sources */,
66A1F4EB2E07CEA50095DE4B /* BackupListMediaManagerTests.swift in Sources */,
04E66D452E00AB6A0059DBAC /* BackupSettingsStoreTests.swift in Sources */,
F9426283289B1B5600460798 /* BlockingManagerTests.swift in Sources */,
@ -20190,7 +20211,6 @@
D979CC3A2AD3964E006AAC49 /* Numbers+Random.swift in Sources */,
D95E149D2E3D22FD00B5B70B /* ObjectRetainerTest.swift in Sources */,
663D02DF2C069AB600350632 /* OrphanedAttachmentCleanerTest.swift in Sources */,
50DAF7E02FD87BEC00BE7430 /* OrphanedBackupAttachmentTest.swift in Sources */,
D9AA37A02A86E0910088EFFB /* OutgoingCallEventSyncMessageTest.swift in Sources */,
D925C7BB2B7BEC0F00AC73B0 /* OutgoingCallLogEventSyncMessageTest.swift in Sources */,
D9D3216A2A8AC9B0004FC110 /* OutgoingGroupCallUpdateMessageTest.swift in Sources */,

View File

@ -87,7 +87,7 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
}
appReadiness.runNowOrWhenAppDidBecomeReadySync {
self.refreshConnection(isAppActive: false)
self.refreshConnection(isAppActive: false, shouldRunCron: false)
}
clearAppropriateNotificationsAndRestoreBadgeCount()
@ -369,14 +369,13 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
}
}
private func configureGlobalUI(in window: UIWindow) {
let screenLockUI = AppEnvironment.shared.screenLockUI
let windowManager = AppEnvironment.shared.windowManagerRef
private lazy var screenLockUI = ScreenLockUI(appReadiness: appReadiness)
private func configureGlobalUI(in window: UIWindow) {
Theme.setupSignalAppearance()
screenLockUI.setupWithRootWindow(window)
windowManager.setupWithRootWindow(window, screenBlockingWindow: screenLockUI.screenBlockingWindow)
AppEnvironment.shared.windowManagerRef.setupWithRootWindow(window, screenBlockingWindow: screenLockUI.screenBlockingWindow)
screenLockUI.startObserving()
}
@ -717,7 +716,6 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
// element" should call .restart() on the appropriate job.
dependenciesBridge.deletedCallRecordExpirationJob.start()
dependenciesBridge.disappearingMessagesExpirationJob.start()
dependenciesBridge.decryptionPlaceholderExpirationJob.start()
dependenciesBridge.storyMessageExpirationJob.start()
dependenciesBridge.pinnedMessageExpirationJob.start()
@ -809,7 +807,7 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
// launching from the background, without this, we end up waiting some extra
// seconds before receiving an actionable push notification.
if !appContext.isMainAppAndActive {
self.refreshConnection(isAppActive: false)
self.refreshConnection(isAppActive: false, shouldRunCron: false)
}
if registeredState != nil {
@ -1392,7 +1390,7 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
}
}
refreshConnection(isAppActive: true)
refreshConnection(isAppActive: true, shouldRunCron: true)
// Every time we become active...
if registeredState != nil {
@ -1460,7 +1458,7 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
/// is in the background.
private var backgroundFetchHandle: BackgroundTaskHandle?
private func refreshConnection(isAppActive: Bool) {
private func refreshConnection(isAppActive: Bool, shouldRunCron: Bool) {
let chatConnectionManager = DependenciesBridge.shared.chatConnectionManager
let oldActiveConnectionTokens = self.activeConnectionTokens
@ -1468,10 +1466,9 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
// If we're active, open a connection.
self.activeConnectionTokens = chatConnectionManager.requestConnections()
oldActiveConnectionTokens.forEach { $0.releaseConnection() }
// Start a new Cron task on activate.
self.startCronTask()
if shouldRunCron {
self.startCronTask()
}
// We're back in the foreground. We've passed off connection management to
// the foreground logic, so just tear it down without waiting for anything.
self.backgroundFetchHandle?.interrupt()
@ -1488,14 +1485,17 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
do {
await backgroundFetcher.start()
oldActiveConnectionTokens.forEach { $0.releaseConnection() }
// If there's a Cron task running that was started in the foreground, wait
// for it to finish.
await withTaskCancellationHandler(
operation: { await cronTask?.value },
onCancel: { cronTask?.cancel() },
)
// If there's a fresh request to run Cron when entering the background,
// start a new Cron instance.
if shouldRunCron {
await self.runCron()
}
// This will usually be limited to 30 seconds rather than 3 minutes.
let waitDeadline = startDate.adding(180)
if isPastRegistration {
@ -1768,24 +1768,8 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
return false
}
let isVideo = isVideoCall(intent)
Task { @MainActor [appReadiness] in
do {
try await appReadiness.waitForAppReady()
} catch {
return
}
let callService = AppEnvironment.shared.callService!
let screenLockUI = AppEnvironment.shared.screenLockUI
appReadiness.runNowOrWhenAppDidBecomeReadySync {
let tsAccountManager = DependenciesBridge.shared.tsAccountManager
do {
try await screenLockUI.waitForScreenUnlockThrowingPrevious()
} catch {
return
}
guard tsAccountManager.registrationStateWithMaybeSneakyTransaction.isRegistered else {
Logger.warn("Ignoring user activity; not registered.")
return
@ -1803,6 +1787,7 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
// * It can be received if the user taps the "video" button for a contact
// in the contacts app. If so, the correct response is to try to initiate a
// new call to that user - unless there is another call in progress.
let callService = AppEnvironment.shared.callService!
if let currentCall = callService.callServiceState.currentCall {
if isVideo, case .individual = currentCall.mode, currentCall.mode.matches(callTarget) {
Logger.info("Upgrading existing call to video")
@ -1814,7 +1799,6 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
}
callService.initiateCall(to: callTarget, isVideo: isVideo)
}
return true
}
@ -1829,12 +1813,17 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
scheduleBgAppRefresh()
let attachmentDownloadmanager = DependenciesBridge.shared.attachmentDownloadManager
let db = DependenciesBridge.shared.db
let tsAccountManager = DependenciesBridge.shared.tsAccountManager
let registeredState = try? tsAccountManager.registeredStateWithMaybeSneakyTransaction()
if let registeredState {
Logger.info("localAci: \(registeredState.localIdentifiers.aci)")
db.write { transaction in
ExperienceUpgradeFinder.markAllCompleteForNewUser(transaction: transaction)
}
attachmentDownloadmanager.beginDownloadingIfNecessary()
// Schedule a Cron run if we're in the foreground.
@ -1942,15 +1931,9 @@ extension AppDelegate: UNUserNotificationCenterDelegate {
Task { @MainActor [appReadiness] () -> Void in
defer { completionHandler() }
do {
try await self.appReadiness.waitForAppReady()
} catch {
return
}
try await self.appReadiness.waitForAppReady()
let screenLockUI = AppEnvironment.shared.screenLockUI
let backgroundMessageFetcherFactory = DependenciesBridge.shared.backgroundMessageFetcherFactory
let backgroundMessageFetcher = backgroundMessageFetcherFactory.buildFetcher()
// So that we open up a connection for replies.
await backgroundMessageFetcher.start()
@ -1959,11 +1942,7 @@ extension AppDelegate: UNUserNotificationCenterDelegate {
let elapsedDuration = (MonotonicDate() - startDate).seconds
try await withCooperativeTimeout(seconds: 27 - elapsedDuration) {
// Do the actual thing we care about.
try await NotificationActionHandler.handleNotificationResponse(
response,
appReadiness: appReadiness,
screenLockUI: screenLockUI,
)
try await NotificationActionHandler.handleNotificationResponse(response, appReadiness: appReadiness)
// Then wait for any enqueued messages (e.g., read receipts) to be sent.
try await backgroundMessageFetcher.waitForFetchingProcessingAndSideEffects()

View File

@ -22,12 +22,12 @@ public class AppEnvironment: NSObject {
@MainActor
var ownedObjects = [AnyObject]()
let cvAudioPlayerRef: CVAudioPlayer
let deviceTransferServiceRef: DeviceTransferService
let pushRegistrationManagerRef: PushRegistrationManager
let screenLockUI: ScreenLockUI
let speechManagerRef: SpeechManager
let windowManagerRef: WindowManager
let cvAudioPlayerRef = CVAudioPlayer()
let speechManagerRef = SpeechManager()
let windowManagerRef = WindowManager()
private(set) var appIconBadgeUpdater: AppIconBadgeUpdater!
private(set) var avatarHistoryManager: AvatarHistoryManager!
@ -44,12 +44,8 @@ public class AppEnvironment: NSObject {
private var registrationIdMismatchManager: RegistrationIdMismatchManager!
init(appReadiness: AppReadiness, deviceTransferService: DeviceTransferService) {
self.cvAudioPlayerRef = CVAudioPlayer()
self.deviceTransferServiceRef = deviceTransferService
self.screenLockUI = ScreenLockUI(appReadiness: appReadiness)
self.pushRegistrationManagerRef = PushRegistrationManager(appReadiness: appReadiness)
self.speechManagerRef = SpeechManager()
self.windowManagerRef = WindowManager()
super.init()
@ -257,6 +253,7 @@ public class AppEnvironment: NSObject {
let db = DependenciesBridge.shared.db
let groupCallPeekClient = SSKEnvironment.shared.groupCallManagerRef.groupCallPeekClient
let interactionStore = DependenciesBridge.shared.interactionStore
let masterKeySyncManager = DependenciesBridge.shared.masterKeySyncManager
let notificationPresenter = SSKEnvironment.shared.notificationPresenterRef
let recipientDatabaseTable = DependenciesBridge.shared.recipientDatabaseTable
let storageServiceManager = SSKEnvironment.shared.storageServiceManagerRef
@ -328,6 +325,12 @@ public class AppEnvironment: NSObject {
} else {
}
Task {
await db.awaitableWrite { tx in
masterKeySyncManager.runStartupJobs(tx: tx)
}
}
Task {
await db.awaitableWrite { tx in
groupCallRecordRingingCleanupManager.cleanupRingingCalls(tx: tx)

View File

@ -1,45 +0,0 @@
//
// Copyright 2026 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import SignalServiceKit
import SignalUI
final class BackupNeverShareRecoveryKeySheet: HeroSheetViewController {
init(
primaryButton: HeroSheetViewController.Button,
secondaryButton: HeroSheetViewController.Button?,
) {
let bodyText: NSAttributedString = NSAttributedString.composed(of: [
OWSLocalizedString(
"BACKUP_NEVER_SHARE_RECOVERY_KEY_SHEET_BODY",
comment: "Body for a warning sheet shown to discourage the user from sharing their 'Recovery Key', warning them not to share it with anyone.",
).styled(
with: .xmlRules([.style("bold", StringStyle(.font(.dynamicTypeSubheadline.bold())))]),
),
" ",
CommonStrings.learnMore.styled(
with: .link(.Support.phishingPrevention),
),
])
super.init(
hero: .circleIcon(
icon: .errorTriangle,
iconSize: 40,
tintColor: .Signal.red,
backgroundColor: UIColor(rgbHex: 0xF8E0D9),
),
title: OWSLocalizedString(
"BACKUP_NEVER_SHARE_RECOVERY_KEY_SHEET_TITLE",
comment: "Title for a warning sheet shown to discourage the user from sharing their 'Recovery Key'.",
),
body: HeroSheetViewController.Body(
textContent: .attributed(bodyText),
),
primary: .button(primaryButton),
secondary: secondaryButton.map { .button($0) },
)
}
}

View File

@ -1,105 +0,0 @@
//
// Copyright 2026 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import SignalServiceKit
import SwiftUI
struct BackupPlanOptionView: View {
struct BulletPoint {
let icon: UIImage
let text: String
}
let title: String
let subtitle: String
let bullets: [BulletPoint]
let isCurrentPlan: Bool
let isSelected: Bool
let onTap: () -> Void
var body: some View {
Button(action: onTap) {
HStack(alignment: .top) {
VStack(alignment: .leading) {
if isCurrentPlan {
Label(
OWSLocalizedString(
"CHOOSE_BACKUP_PLAN_CURRENT_PLAN_LABEL",
comment: "A label indicating that a given Backup plan option is what the user has already enabled.",
),
systemImage: "checkmark",
)
.font(.footnote)
.foregroundStyle(Color.Signal.secondaryLabel)
.padding(.vertical, 4)
.padding(.horizontal, 12)
.background {
Capsule().fill(Color.Signal.secondaryFill)
}
}
Text(title)
.font(.headline)
.multilineTextAlignment(.leading)
Text(subtitle).foregroundStyle(Color.Signal.secondaryLabel)
ForEach(bullets, id: \.text) { bullet in
Label {
Text(bullet.text).font(.subheadline)
} icon: {
Image(uiImage: bullet.icon)
.foregroundStyle(
isSelected
? Color.Signal.ultramarine
: Color.Signal.label,
)
}
.padding(.leading, 20)
.padding(.vertical, 2)
}
}
Spacer()
Group {
if isSelected {
Circle()
.fill(Color.Signal.ultramarine)
.overlay {
Image(systemName: "checkmark")
.resizable()
.foregroundColor(.white)
.padding(6)
}
} else {
Circle()
.stroke(Color.Signal.secondaryLabel, lineWidth: 2)
.opacity(0.3)
}
}
.frame(width: 24, height: 24)
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.vertical, 20)
.padding(.leading, 20)
.padding(.trailing, 16)
.background(Color.Signal.secondaryGroupedBackground)
.cornerRadius(16)
.overlay {
RoundedRectangle(cornerRadius: 16)
.stroke(
Color.Signal.ultramarine,
lineWidth: isSelected ? 3 : 0,
)
}
.shadow(
color: isSelected ? .black.opacity(0.12) : .clear,
radius: 8,
y: 2,
)
}
.buttonStyle(.plain)
}
}

View File

@ -77,9 +77,6 @@ class BackupRecordKeyViewController: OWSViewController, OWSNavigationChildContro
override func viewDidLoad() {
super.viewDidLoad()
let screenLockUI = AppEnvironment.shared.screenLockUI
screenLockUI.sensitiveContentDidLoad(inViewController: self)
view.backgroundColor = .Signal.groupedBackground
if let onBackPressedBlock {
@ -118,7 +115,7 @@ class BackupRecordKeyViewController: OWSViewController, OWSNavigationChildContro
comment: "Title for a button allowing users to copy their 'Recovery Key' to the clipboard.",
)),
primaryAction: UIAction { [weak self] _ in
self?.copyToClipboardWithConfirmation()
self?.copyToClipboard()
},
),
]
@ -178,26 +175,6 @@ class BackupRecordKeyViewController: OWSViewController, OWSNavigationChildContro
stackView.setCustomSpacing(32, after: aepTextView)
}
private func copyToClipboardWithConfirmation() {
let warningSheet = BackupNeverShareRecoveryKeySheet(
primaryButton: HeroSheetViewController.Button(
title: OWSLocalizedString(
"BACKUP_RECORD_KEY_COPY_WARNING_SHEET_PRIMARY_BUTTON_TITLE",
comment: "Title for the primary button in a warning sheet shown before copying the user's 'Recovery Key' to the clipboard, which acknowledges the warning and proceeds with the copy.",
),
action: { sheet in
sheet.dismiss(animated: true) { [weak self] in
guard let self else { return }
copyToClipboard()
}
},
),
secondaryButton: nil,
)
present(warningSheet, animated: true)
}
private func copyToClipboard() {
UIPasteboard.general.setItems(
[[UIPasteboard.typeAutomatic: displayableAEP.displayString]],

View File

@ -16,7 +16,6 @@ class BackupSettingsViewController:
enum OnAppearAction {
case presentWelcomeToBackupsSheet
case automaticallyStartBackup(completion: ((UIViewController) -> Void)?)
case disableOptimizeLocalStorage
}
private let accountEntropyPoolManager: AccountEntropyPoolManager
@ -69,6 +68,7 @@ class BackupSettingsViewController:
backupSubscriptionManager: DependenciesBridge.shared.backupSubscriptionManager,
db: DependenciesBridge.shared.db,
deviceSleepManager: deviceSleepManager,
remoteConfig: SSKEnvironment.shared.remoteConfigManagerRef,
subscriptionConfigManager: DependenciesBridge.shared.subscriptionConfigManager,
tsAccountManager: DependenciesBridge.shared.tsAccountManager,
)
@ -92,6 +92,7 @@ class BackupSettingsViewController:
backupSubscriptionManager: BackupSubscriptionManager,
db: DB,
deviceSleepManager: DeviceSleepManager,
remoteConfig: RemoteConfigProvider,
subscriptionConfigManager: SubscriptionConfigManager,
tsAccountManager: TSAccountManager,
) {
@ -121,7 +122,7 @@ class BackupSettingsViewController:
self.onAppearAction = onAppearAction
switch onAppearAction {
case nil, .presentWelcomeToBackupsSheet, .disableOptimizeLocalStorage:
case .presentWelcomeToBackupsSheet, nil:
break
case .automaticallyStartBackup(let completion):
self.onBackupComplete = completion
@ -146,6 +147,7 @@ class BackupSettingsViewController:
),
hasBackupFailed: backupFailureStateManager.hasFailedBackup(tx: tx),
isBackgroundAppRefreshDisabled: Self.isBackgroundAppRefreshDisabled(),
isOptimizeStorageEnabled: remoteConfig.currentConfig().isOptimizeStorageEnabled,
)
return viewModel
@ -180,8 +182,6 @@ class BackupSettingsViewController:
presentWelcomeToBackupsSheet()
case .automaticallyStartBackup:
performManualBackup()
case .disableOptimizeLocalStorage:
setOptimizeLocalStorage(false)
}
}
@ -621,87 +621,28 @@ class BackupSettingsViewController:
final class WelcomeToBackupsSheet: HeroSheetViewController {
override var canBeDismissed: Bool { false }
init(
optimizeLocalStorage: (isOn: Bool, onValueChanged: (Bool) -> Void)?,
onConfirm: @escaping (HeroSheetViewController) -> Void,
) {
let toggle: HeroSheetViewController.Body.Toggle?
if let (isOn, onValueChanged) = optimizeLocalStorage {
toggle = HeroSheetViewController.Body.Toggle(
title: OWSLocalizedString(
"BACKUP_SETTINGS_WELCOME_TO_BACKUPS_SHEET_OPTIMIZE_MEDIA_TOGGLE_TITLE",
comment: "Title for a toggle shown after the user enables backups, letting them enable the Optimize Storage feature.",
),
footer: OWSLocalizedString(
"BACKUP_SETTINGS_WELCOME_TO_BACKUPS_SHEET_OPTIMIZE_MEDIA_TOGGLE_FOOTER",
comment: "Footer for a toggle shown after the user enables backups, letting them enable the Optimize Storage feature.",
),
isOn: isOn,
onValueChanged: onValueChanged,
)
} else {
toggle = nil
}
init(onConfirm: @escaping () -> Void) {
super.init(
hero: .image(.backupsSubscribed),
title: OWSLocalizedString(
"BACKUP_SETTINGS_WELCOME_TO_BACKUPS_SHEET_TITLE",
comment: "Title for a sheet shown after the user enables backups.",
),
body: HeroSheetViewController.Body(
textContent: .plain(OWSLocalizedString(
"BACKUP_SETTINGS_WELCOME_TO_BACKUPS_SHEET_MESSAGE",
comment: "Message for a sheet shown after the user enables backups.",
)),
toggle: toggle,
body: OWSLocalizedString(
"BACKUP_SETTINGS_WELCOME_TO_BACKUPS_SHEET_MESSAGE",
comment: "Message for a sheet shown after the user enables backups.",
),
primaryButton: HeroSheetViewController.Button(
title: CommonStrings.okButton,
action: { _ in onConfirm() },
),
primary: .button(HeroSheetViewController.Button(
title: OWSLocalizedString(
"BACKUP_SETTINGS_WELCOME_TO_BACKUPS_SHEET_BUTTON_TITLE",
comment: "Title for a button in a sheet shown after the user enables backups.",
),
action: { onConfirm($0) },
)),
secondary: nil,
)
}
}
let backupPlan = db.read { tx in
backupPlanManager.backupPlan(tx: tx)
}
let welcomeToBackupsSheet: WelcomeToBackupsSheet
switch backupPlan {
case .disabled,
.disabling,
.free:
welcomeToBackupsSheet = WelcomeToBackupsSheet(
optimizeLocalStorage: nil,
onConfirm: { sheet in
sheet.dismiss(animated: true) { [self] in
viewModel.performManualBackup()
}
},
)
case .paid,
.paidAsTester,
.paidExpiringSoon:
var isOptimizeStorageEnabled = false
welcomeToBackupsSheet = WelcomeToBackupsSheet(
optimizeLocalStorage: (
isOn: isOptimizeStorageEnabled,
onValueChanged: { isOptimizeStorageEnabled = $0 },
),
onConfirm: { sheet in
sheet.dismiss(animated: true) { [self] in
setOptimizeLocalStorage(isOptimizeStorageEnabled)
viewModel.performManualBackup()
}
},
)
let welcomeToBackupsSheet = WelcomeToBackupsSheet { [self] in
viewModel.performManualBackup()
dismiss(animated: true)
}
present(welcomeToBackupsSheet, animated: true)
@ -1080,38 +1021,35 @@ class BackupSettingsViewController:
// MARK: -
fileprivate func setOptimizeLocalStorage(_ newOptimizeLocalStorage: Bool) {
let hasMadeAtLeastOneBackup: Bool? = db.write { tx in
let isPaidPlanTester: Bool = db.write { tx in
let currentBackupPlan = backupPlanManager.backupPlan(tx: tx)
let lastBackupDetails = backupSettingsStore.lastBackupDetails(tx: tx)
let newBackupPlan: BackupPlan
let isPaidPlanTester: Bool
switch currentBackupPlan {
case .disabled,
.disabling,
.free,
.paid(optimizeLocalStorage: newOptimizeLocalStorage),
.paidExpiringSoon(optimizeLocalStorage: newOptimizeLocalStorage),
.paidAsTester(optimizeLocalStorage: newOptimizeLocalStorage):
return nil
case .disabled, .disabling, .free:
owsFailDebug("Shouldn't be setting Optimize Local Storage: \(currentBackupPlan)")
return false
case .paid:
newBackupPlan = .paid(optimizeLocalStorage: newOptimizeLocalStorage)
isPaidPlanTester = false
case .paidExpiringSoon:
newBackupPlan = .paidExpiringSoon(optimizeLocalStorage: newOptimizeLocalStorage)
isPaidPlanTester = false
case .paidAsTester:
newBackupPlan = .paidAsTester(optimizeLocalStorage: newOptimizeLocalStorage)
isPaidPlanTester = true
}
backupPlanManager.setBackupPlan(newBackupPlan, tx: tx)
return lastBackupDetails != nil
return isPaidPlanTester
}
if
hasMadeAtLeastOneBackup == true,
!newOptimizeLocalStorage
{
// If disabling Optimize Local Storage with media potentially
// offloaded, offer to start downloads now.
// If disabling Optimize Local Storage, offer to start downloads now.
if !newOptimizeLocalStorage {
showDownloadOffloadedMediaSheet()
} else if isPaidPlanTester {
showOffloadedMediaForTestersWarningSheet(onAcknowledge: {})
}
}
@ -1150,41 +1088,54 @@ class BackupSettingsViewController:
presentActionSheet(actionSheet)
}
private func showOffloadedMediaForTestersWarningSheet(
onAcknowledge: @escaping () -> Void,
) {
let actionSheet = ActionSheetController(
title: OWSLocalizedString(
"BACKUP_SETTINGS_OPTIMIZE_LOCAL_STORAGE_TESTER_WARNING_SHEET_TITLE",
comment: "Title for an action sheet warning users who are testers about the Optimize Local Storage feature.",
),
message: OWSLocalizedString(
"BACKUP_SETTINGS_OPTIMIZE_LOCAL_STORAGE_TESTER_WARNING_SHEET_MESSAGE",
comment: "Message for an action sheet warning users who are testers about the Optimize Local Storage feature.",
),
)
actionSheet.addAction(ActionSheetAction(
title: CommonStrings.okButton,
handler: { _ in
onAcknowledge()
},
))
presentActionSheet(actionSheet)
}
// MARK: -
fileprivate func setIsBackupDownloadQueueSuspended(_ isSuspended: Bool, backupPlan: BackupPlan) {
if isSuspended {
let warningTitle: String?
let warningMessage: String?
switch backupPlan {
case .disabled, .disabling:
warningTitle = OWSLocalizedString(
"BACKUP_SETTINGS_SKIP_DOWNLOADS_DISABLING_WARNING_SHEET_TITLE",
comment: "Title for a sheet warning the user about skipping downloads while disabling Backups.",
)
warningMessage = OWSLocalizedString(
"BACKUP_SETTINGS_SKIP_DOWNLOADS_DISABLING_WARNING_SHEET_MESSAGE",
comment: "Message for a sheet warning the user about skipping downloads while disabling Backups.",
)
case .free, .paidExpiringSoon:
warningTitle = OWSLocalizedString(
"BACKUP_SETTINGS_SKIP_DOWNLOADS_EXPIRING_WARNING_SHEET_TITLE",
comment: "Title for a sheet warning the user about skipping downloads that will expire.",
)
warningMessage = OWSLocalizedString(
"BACKUP_SETTINGS_SKIP_DOWNLOADS_EXPIRING_WARNING_SHEET_MESSAGE",
comment: "Message for a sheet warning the user about skipping downloads that will expire.",
)
case .paid, .paidAsTester:
warningTitle = nil
warningMessage = nil
}
if let warningTitle, let warningMessage {
case .disabled, .disabling, .free, .paid:
db.write { tx in
backupSettingsStore.setIsBackupDownloadQueueSuspended(true, tx: tx)
}
case .paidAsTester:
showOffloadedMediaForTestersWarningSheet(onAcknowledge: { [self] in
db.write { tx in
backupSettingsStore.setIsBackupDownloadQueueSuspended(true, tx: tx)
}
})
case .paidExpiringSoon:
let warningSheet = ActionSheetController(
title: warningTitle,
message: warningMessage,
title: OWSLocalizedString(
"BACKUP_SETTINGS_SKIP_DOWNLOADS_WARNING_SHEET_TITLE",
comment: "Title for a sheet warning the user about skipping downloads.",
),
message: OWSLocalizedString(
"BACKUP_SETTINGS_SKIP_DOWNLOADS_WARNING_SHEET_MESSAGE",
comment: "Message for a sheet warning the user about skipping downloads.",
),
)
warningSheet.addAction(ActionSheetAction(
title: OWSLocalizedString(
@ -1193,31 +1144,9 @@ class BackupSettingsViewController:
),
style: .destructive,
handler: { [self] _ in
let secondWarningSheet = ActionSheetController(
title: OWSLocalizedString(
"BACKUP_SETTINGS_SKIP_DOWNLOADS_SECOND_WARNING_SHEET_TITLE",
comment: "Title for a double-confirmation sheet warning the user about skipping downloads.",
),
message: OWSLocalizedString(
"BACKUP_SETTINGS_SKIP_DOWNLOADS_SECOND_WARNING_SHEET_MESSAGE",
comment: "Message for a double-confirmation sheet warning the user about skipping downloads.",
),
)
secondWarningSheet.addAction(ActionSheetAction(
title: OWSLocalizedString(
"BACKUP_SETTINGS_SKIP_DOWNLOADS_SECOND_WARNING_SHEET_ACTION_SKIP",
comment: "Title for an action in a double-confirmation sheet warning the user about skipping downloads.",
),
style: .destructive,
handler: { [self] _ in
db.write { tx in
backupSettingsStore.setIsBackupDownloadQueueSuspended(true, tx: tx)
}
},
))
secondWarningSheet.addAction(.cancel)
presentActionSheet(secondWarningSheet)
db.write { tx in
backupSettingsStore.setIsBackupDownloadQueueSuspended(true, tx: tx)
}
},
))
warningSheet.addAction(ActionSheetAction(
@ -1232,10 +1161,6 @@ class BackupSettingsViewController:
warningSheet.addAction(.cancel)
presentActionSheet(warningSheet)
} else {
db.write { tx in
backupSettingsStore.setIsBackupDownloadQueueSuspended(true, tx: tx)
}
}
} else {
db.write { tx in
@ -1439,10 +1364,10 @@ class BackupSettingsViewController:
onConfirmed: { [weak self] _ in
guard let self else { return }
self.finalizeNewRecoveryKey(newCandidateAEP: newCandidateAEP)
// Pop all the way back to Backup Settings.
navigationController?.popToViewController(self, animated: true) {
self.finalizeNewRecoveryKey(newCandidateAEP: newCandidateAEP)
self.presentToast(text: OWSLocalizedString(
"BACKUP_SETTINGS_CREATE_NEW_KEY_SUCCESS_TOAST",
comment: "Toast shown when a new Recovery Key has been created successfully.",
@ -1617,6 +1542,8 @@ private class BackupSettingsViewModel: ObservableObject {
/// from running.)
@Published var isBackgroundAppRefreshDisabled: Bool
@Published var isOptimizeStorageEnabled: Bool
weak var actionsDelegate: ActionsDelegate?
init(
@ -1633,6 +1560,7 @@ private class BackupSettingsViewModel: ObservableObject {
mediaTierCapacityOverflow: UInt64?,
hasBackupFailed: Bool,
isBackgroundAppRefreshDisabled: Bool,
isOptimizeStorageEnabled: Bool,
) {
self.backupSubscriptionConfiguration = backupSubscriptionConfiguration
@ -1652,6 +1580,8 @@ private class BackupSettingsViewModel: ObservableObject {
self.mediaTierCapacityOverflow = mediaTierCapacityOverflow
self.hasBackupFailed = hasBackupFailed
self.isBackgroundAppRefreshDisabled = isBackgroundAppRefreshDisabled
self.isOptimizeStorageEnabled = isOptimizeStorageEnabled
}
// MARK: -
@ -1710,21 +1640,16 @@ private class BackupSettingsViewModel: ObservableObject {
// MARK: -
/// Whether the "Optimze Storage" feature is available, per the current
/// `BackupPlan`.
var isOptimizeLocalStorageAvailable: Bool {
var optimizeLocalStorageAvailable: Bool {
switch backupPlan {
case .disabled, .disabling, .free:
false
case .paid, .paidAsTester:
case .paid, .paidExpiringSoon, .paidAsTester:
true
case .paidExpiringSoon(let optimizeLocalStorage):
// Only allow disabling Optimize Storage if expiring soon, not enabling.
optimizeLocalStorage
}
}
var isOptimizeLocalStorageEnabled: Bool {
var optimizeLocalStorage: Bool {
switch backupPlan {
case .disabled, .disabling, .free:
false
@ -1997,32 +1922,44 @@ struct BackupSettingsView: View {
viewModel: viewModel,
)
Toggle(
OWSLocalizedString(
"BACKUP_SETTINGS_OPTIMIZE_LOCAL_STORAGE_TOGGLE_TITLE",
comment: "Title for a toggle allowing users to change the Optimize Local Storage setting.",
),
isOn: Binding(
get: { viewModel.isOptimizeLocalStorageEnabled },
set: { viewModel.setOptimizeLocalStorage($0) },
),
).disabled(!viewModel.isOptimizeLocalStorageAvailable)
} footer: {
let footerText: String = if viewModel.isOptimizeLocalStorageAvailable {
OWSLocalizedString(
"BACKUP_SETTINGS_OPTIMIZE_LOCAL_STORAGE_TOGGLE_FOOTER_AVAILABLE",
comment: "Footer for a toggle allowing users to change the Optimize Local Storage setting, if the toggle is available.",
)
} else {
OWSLocalizedString(
"BACKUP_SETTINGS_OPTIMIZE_LOCAL_STORAGE_TOGGLE_FOOTER_UNAVAILABLE",
comment: "Footer for a toggle allowing users to change the Optimize Local Storage setting, if the toggle is unavailable.",
)
if viewModel.isOptimizeStorageEnabled {
Toggle(
OWSLocalizedString(
"BACKUP_SETTINGS_OPTIMIZE_LOCAL_STORAGE_TOGGLE_TITLE",
comment: "Title for a toggle allowing users to change the Optimize Local Storage setting.",
),
isOn: Binding(
get: { viewModel.optimizeLocalStorage },
set: { viewModel.setOptimizeLocalStorage($0) },
),
).disabled(!viewModel.optimizeLocalStorageAvailable)
}
} footer: {
if viewModel.isOptimizeStorageEnabled {
let footerText: String = if
viewModel.optimizeLocalStorageAvailable,
viewModel.isPaidPlanTester
{
OWSLocalizedString(
"BACKUP_SETTINGS_OPTIMIZE_LOCAL_STORAGE_TOGGLE_FOOTER_AVAILABLE_FOR_TESTERS",
comment: "Footer for a toggle allowing users to change the Optimize Local Storage setting, if the toggle is available and they are a tester.",
)
} else if viewModel.optimizeLocalStorageAvailable {
OWSLocalizedString(
"BACKUP_SETTINGS_OPTIMIZE_LOCAL_STORAGE_TOGGLE_FOOTER_AVAILABLE",
comment: "Footer for a toggle allowing users to change the Optimize Local Storage setting, if the toggle is available.",
)
} else {
OWSLocalizedString(
"BACKUP_SETTINGS_OPTIMIZE_LOCAL_STORAGE_TOGGLE_FOOTER_UNAVAILABLE",
comment: "Footer for a toggle allowing users to change the Optimize Local Storage setting, if the toggle is unavailable.",
)
}
Text(footerText)
.foregroundStyle(Color.Signal.secondaryLabel)
.font(.caption)
Text(footerText)
.foregroundStyle(Color.Signal.secondaryLabel)
.font(.caption)
}
}
SignalSection {
@ -3241,6 +3178,7 @@ private extension BackupSettingsViewModel {
mediaTierCapacityOverflow: mediaTierCapacityOverflow,
hasBackupFailed: hasBackupFailed,
isBackgroundAppRefreshDisabled: isBackgroundAppRefreshDisabled,
isOptimizeStorageEnabled: false,
)
let actionsDelegate = PreviewActionsDelegate()
viewModel.actionsDelegate = actionsDelegate
@ -3279,8 +3217,7 @@ private extension BackupSettingsViewModel {
backupSubscriptionLoadingState: .loaded(.paidButExpiring(
expirationDate: Date().addingTimeInterval(.week),
)),
backupPlan: .paidExpiringSoon(optimizeLocalStorage: true),
latestBackupAttachmentDownloadUpdateState: .suspended,
backupPlan: .paidExpiringSoon(optimizeLocalStorage: false),
))
}

View File

@ -8,10 +8,7 @@ import SignalUI
import StoreKit
import SwiftUI
class ChooseBackupPlanViewController:
HostingController<ChooseBackupPlanView>,
ChooseBackupPlanViewModel.ActionsDelegate
{
class ChooseBackupPlanViewController: HostingController<ChooseBackupPlanView> {
typealias OnConfirmPlanSelectionBlock = (ChooseBackupPlanViewController, PlanSelection) -> Void
enum StoreKitAvailability {
@ -121,9 +118,11 @@ class ChooseBackupPlanViewController:
onConfirmPlanSelectionBlock: onConfirmPlanSelectionBlock,
)
}
}
// MARK: - ChooseBackupPlanViewModel.ActionsDelegate
// MARK: - ChooseBackupPlanViewModel.ActionsDelegate
extension ChooseBackupPlanViewController: ChooseBackupPlanViewModel.ActionsDelegate {
fileprivate func confirmSelection(_ planSelection: PlanSelection) {
switch (initialPlanSelection, planSelection) {
case (.free, .free), (.paid, .paid):
@ -234,7 +233,7 @@ struct ChooseBackupPlanView: View {
Spacer().frame(height: 20)
BackupPlanOptionView(
PlanOptionView(
title: OWSLocalizedString(
"CHOOSE_BACKUP_PLAN_FREE_PLAN_TITLE",
comment: "Title for the free plan option, when choosing a Backup plan.",
@ -248,11 +247,11 @@ struct ChooseBackupPlanView: View {
viewModel.freeMediaTierDays,
),
bullets: [
BackupPlanOptionView.BulletPoint(icon: .thread, text: OWSLocalizedString(
PlanOptionView.BulletPoint(iconKey: "thread", text: OWSLocalizedString(
"CHOOSE_BACKUP_PLAN_BULLET_FULL_TEXT_BACKUP",
comment: "Text for a bullet point in a list of Backup features, describing that all text messages are included.",
)),
BackupPlanOptionView.BulletPoint(icon: .albumTilt, text: String.localizedStringWithFormat(
PlanOptionView.BulletPoint(iconKey: "album-tilt", text: String.localizedStringWithFormat(
OWSLocalizedString(
"CHOOSE_BACKUP_PLAN_BULLET_RECENT_MEDIA_BACKUP_%d",
tableName: "PluralAware",
@ -270,7 +269,7 @@ struct ChooseBackupPlanView: View {
Spacer().frame(height: 16)
BackupPlanOptionView(
PlanOptionView(
title: {
switch viewModel.storeKitAvailability {
case .available(let paidPlanDisplayPrice):
@ -293,15 +292,15 @@ struct ChooseBackupPlanView: View {
comment: "Subtitle for the paid plan option, when choosing a Backup plan.",
),
bullets: [
BackupPlanOptionView.BulletPoint(icon: .thread, text: OWSLocalizedString(
PlanOptionView.BulletPoint(iconKey: "thread", text: OWSLocalizedString(
"CHOOSE_BACKUP_PLAN_BULLET_FULL_TEXT_BACKUP",
comment: "Text for a bullet point in a list of Backup features, describing that all text messages are included.",
)),
BackupPlanOptionView.BulletPoint(icon: .albumTilt, text: OWSLocalizedString(
PlanOptionView.BulletPoint(iconKey: "album-tilt", text: OWSLocalizedString(
"CHOOSE_BACKUP_PLAN_BULLET_FULL_MEDIA_BACKUP",
comment: "Text for a bullet point in a list of Backup features, describing that all media is included.",
)),
BackupPlanOptionView.BulletPoint(icon: .data, text: String.nonPluralLocalizedStringWithFormat(
PlanOptionView.BulletPoint(iconKey: "data", text: String.nonPluralLocalizedStringWithFormat(
OWSLocalizedString(
"CHOOSE_BACKUP_PLAN_BULLET_STORAGE_AMOUNT",
comment: "Text for a bullet point in a list of Backup features, describing the amount of included storage. Embeds {{ the amount of storage preformatted as a localized byte count, e.g. '100 GB' }}.",
@ -384,6 +383,106 @@ struct ChooseBackupPlanView: View {
// MARK: -
private struct PlanOptionView: View {
struct BulletPoint {
let iconKey: String
let text: String
}
let title: String
let subtitle: String
let bullets: [BulletPoint]
let isCurrentPlan: Bool
let isSelected: Bool
let onTap: () -> Void
var body: some View {
Button(action: onTap) {
HStack(alignment: .top) {
VStack(alignment: .leading) {
if isCurrentPlan {
Label(
OWSLocalizedString(
"CHOOSE_BACKUP_PLAN_CURRENT_PLAN_LABEL",
comment: "A label indicating that a given Backup plan option is what the user has already enabled.",
),
systemImage: "checkmark",
)
.font(.footnote)
.foregroundStyle(Color.Signal.secondaryLabel)
.padding(.vertical, 4)
.padding(.horizontal, 12)
.background {
Capsule().fill(Color.Signal.secondaryFill)
}
}
Text(title)
.font(.headline)
.multilineTextAlignment(.leading)
Text(subtitle).foregroundStyle(Color.Signal.secondaryLabel)
ForEach(bullets, id: \.iconKey) { bullet in
Label {
Text(bullet.text).font(.subheadline)
} icon: {
Image(bullet.iconKey)
.foregroundStyle(
isSelected
? Color.Signal.ultramarine
: Color.Signal.label,
)
}
.padding(.leading, 20)
.padding(.vertical, 2)
}
}
Spacer()
Group {
if isSelected {
Circle()
.fill(Color.Signal.ultramarine)
.overlay {
Image(systemName: "checkmark")
.resizable()
.foregroundColor(.white)
.padding(6)
}
} else {
Circle()
.stroke(Color.Signal.secondaryLabel, lineWidth: 2)
.opacity(0.3)
}
}
.frame(width: 24, height: 24)
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.vertical, 20)
.padding(.leading, 20)
.padding(.trailing, 16)
.background(Color.Signal.secondaryGroupedBackground)
.cornerRadius(16)
.overlay {
RoundedRectangle(cornerRadius: 16)
.stroke(
Color.Signal.ultramarine,
lineWidth: isSelected ? 3 : 0,
)
}
.shadow(
color: isSelected ? .black.opacity(0.12) : .clear,
radius: 8,
y: 2,
)
}
.buttonStyle(.plain)
}
}
// MARK: -
#if DEBUG
private extension ChooseBackupPlanViewModel {

View File

@ -31,8 +31,9 @@ struct DisplayableAccountEntropyPool {
.uppercased()
.map { char in
switch char {
case "0": "="
case "O", "o": "#"
// TODO: Reenable this once support is available for all platforms
// case "0": "="
// case "O", "o": "#"
default: char
}
},

View File

@ -51,9 +51,6 @@ class EnterAccountEntropyPoolViewController: OWSViewController {
override func viewDidLoad() {
super.viewDidLoad()
let screenLockUI = AppEnvironment.shared.screenLockUI
screenLockUI.sensitiveContentDidLoad(inViewController: self)
view.backgroundColor = colorConfig.background
navigationItem.rightBarButtonItem = UIBarButtonItem(
title: CommonStrings.nextButton,

View File

@ -240,7 +240,7 @@ extension CallControlsOverflowView: MessageReactionPickerDelegate {
self.react(with: reaction)
}
func didSelectShowFullEmojiPicker() {
func didSelectAnyEmoji() {
let sheet = EmojiPickerSheet(
message: nil,
reactionPickerConfigurationListener: self,

View File

@ -290,7 +290,7 @@ class IndividualCallSheetDataSource: CallDrawerSheetDataSource {
isLocalUser: false,
isUnknown: false,
isAudioMuted: self.individualCall.isRemoteAudioMuted,
isVideoMuted: !self.individualCall.isRemoteVideoEnabled,
isVideoMuted: self.individualCall.isRemoteVideoEnabled.negated,
isPresenting: self.individualCall.isRemoteSharingScreen,
))
}

View File

@ -125,7 +125,6 @@ extension NewCallViewController: RecipientContextMenuHelperDelegate {
// MARK: - RecipientPickerDelegate
extension NewCallViewController: RecipientPickerDelegate, UsernameLinkScanDelegate {
func recipientPicker(
_ recipientPickerViewController: RecipientPickerViewController,
selectionStyleForRecipient recipient: PickedRecipient,
@ -134,10 +133,7 @@ extension NewCallViewController: RecipientPickerDelegate, UsernameLinkScanDelega
return .default
}
func recipientPicker(
_ recipientPickerViewController: RecipientPickerViewController,
didSelectRecipient recipient: PickedRecipient,
) {
func recipientPicker(_ recipientPickerViewController: RecipientPickerViewController, didSelectRecipient recipient: PickedRecipient) {
switch recipient.identifier {
case let .address(address):
let thread = TSContactThread.getOrCreateThread(contactAddress: address)
@ -147,12 +143,7 @@ extension NewCallViewController: RecipientPickerDelegate, UsernameLinkScanDelega
}
}
func recipientPicker(
_ recipientPickerViewController: RecipientPickerViewController,
contactCellAccessoryForRecipient recipient: PickedRecipient,
transaction: DBReadTransaction,
) -> ContactCellView.Accessory? {
func recipientPicker(_ recipientPickerViewController: RecipientPickerViewController, accessoryViewForRecipient recipient: PickedRecipient, transaction: DBReadTransaction) -> ContactCellAccessoryView? {
let stackView = UIStackView()
stackView.axis = .horizontal
stackView.spacing = 20

View File

@ -159,7 +159,7 @@ class MessageUserSubsetSheet: OWSTableSheetViewController {
cell.selectionStyle = .none
var configuration = ContactCellView.Configuration(address: address, localUserDisplayMode: .asLocalUser)
let configuration = ContactCellConfiguration(address: address, localUserDisplayMode: .asLocalUser)
configuration.forceDarkAppearance = self?.forceDarkMode ?? false
if

View File

@ -114,7 +114,9 @@ class CVAttachmentProgressView: ManualLayoutView {
addLayoutBlock { view in
guard let view = view as? CVAttachmentProgressView else { return }
view.loadInitialStateIfNeeded()
DispatchQueue.main.async {
view.loadInitialStateIfNeeded()
}
}
}
@ -199,12 +201,6 @@ class CVAttachmentProgressView: ManualLayoutView {
name: AttachmentDownloads.attachmentDownloadProgressNotification,
object: nil,
)
NotificationCenter.default.addObserver(
self,
selector: #selector(processDownloadStoppedNotification(notification:)),
name: AttachmentDownloads.attachmentDownloadStoppedNotification,
object: nil,
)
}
}
@ -363,22 +359,6 @@ class CVAttachmentProgressView: ManualLayoutView {
applyState(.progress(progress: progress), animated: window != nil)
}
@objc
private func processDownloadStoppedNotification(notification: Notification) {
AssertIsOnMainThread()
guard
let attachmentId = notification.userInfo?[AttachmentDownloads.attachmentDownloadAttachmentIDKey] as? Attachment.IDType
else {
owsFailDebug("Missing notificationAttachmentId.")
return
}
guard attachmentId == self.attachmentId else {
return
}
applyState(.tapToDownload, animated: window != nil)
}
@objc
private func processUploadNotification(notification: Notification) {
AssertIsOnMainThread()

View File

@ -148,7 +148,15 @@ class CVComponentCollapseSet: CVComponentBase, CVRootComponent {
componentView.outerStack.isAccessibilityElement = true
componentView.outerStack.accessibilityLabel = titleString
componentView.outerStack.accessibilityTraits = .button
componentView.outerStack.accessibilityHint = accessibilityHint(isExpanded: collapseSet.isExpanded)
componentView.outerStack.accessibilityHint = collapseSet.isExpanded
? OWSLocalizedString(
"COLLAPSE_SET_ACCESSIBILITY_HINT_COLLAPSE",
comment: "VoiceOver hint for an expanded collapse set button.",
)
: OWSLocalizedString(
"COLLAPSE_SET_ACCESSIBILITY_HINT_EXPAND",
comment: "VoiceOver hint for a collapsed collapse set button.",
)
}
// MARK: - Events
@ -179,7 +187,6 @@ class CVComponentCollapseSet: CVComponentBase, CVRootComponent {
componentView.chevronLabel.transform = willBeExpanded
? CGAffineTransform(rotationAngle: expandedRotation)
: .identity
componentView.outerStack.accessibilityHint = accessibilityHint(isExpanded: willBeExpanded)
let animation = CABasicAnimation(keyPath: "transform.rotation.z")
animation.fromValue = fromAngle
@ -340,18 +347,6 @@ class CVComponentCollapseSet: CVComponentBase, CVRootComponent {
)
}
private func accessibilityHint(isExpanded: Bool) -> String {
isExpanded
? OWSLocalizedString(
"COLLAPSE_SET_ACCESSIBILITY_HINT_COLLAPSE",
comment: "VoiceOver hint for an expanded collapse set button.",
)
: OWSLocalizedString(
"COLLAPSE_SET_ACCESSIBILITY_HINT_EXPAND",
comment: "VoiceOver hint for a collapsed collapse set button.",
)
}
private func summaryLabel(
count: Int,
type: CollapseSetInteraction.MessagesType,

View File

@ -150,7 +150,6 @@ private class CVQuotedMessageViewAdapter: CVQuotedMessageViewDelegate {
DependenciesBridge.shared.attachmentDownloadManager.enqueueDownloadOfAttachmentsForMessage(
message,
priority: .userInitiated,
useThumbnails: false,
tx: tx,
)
}

View File

@ -528,6 +528,7 @@ public struct CVComponentState: Equatable {
static func ==(lhs: CollapseSet, rhs: CollapseSet) -> Bool {
return lhs.collapsedInteractions.map(\.uniqueId) == rhs.collapsedInteractions.map(\.uniqueId)
&& lhs.collapseSetType == rhs.collapseSetType
&& lhs.isExpanded == rhs.isExpanded
&& lhs.finalTimerDescription == rhs.finalTimerDescription
}
}
@ -1196,7 +1197,7 @@ private extension CVComponentState.Builder {
self.collapseSet = CVComponentState.CollapseSet(
collapsedInteractions: collapseSetInteraction.collapsedInteractions,
collapseSetType: collapseSetInteraction.collapseSetType,
isExpanded: viewStateSnapshot.expandedCollapseSetIds.contains(collapseSetInteraction.uniqueId),
isExpanded: collapseSetInteraction.isExpanded,
finalTimerDescription: collapseSetInteraction.finalTimerDescription,
)
return build()
@ -1372,15 +1373,8 @@ private extension CVComponentState.Builder {
.paid(let optimizeLocalStorage),
.paidAsTester(let optimizeLocalStorage),
.paidExpiringSoon(let optimizeLocalStorage):
if
optimizeLocalStorage,
canAutoDownloadAttachment(referencedAttachment: attachment),
attachment.attachment.localRelativeFilePathThumbnail != nil
{
// If optimize storage is enabled, auto-downloads are enabled,
// and the backup thumbnail is present, show the backup thumbnail
// as a true attachment (don't show the download icon overlay).
mediaAlbumHasSkippedAttachment = false
if optimizeLocalStorage {
mediaAlbumHasSkippedAttachment = !canAutoDownloadAttachment(referencedAttachment: attachment)
} else {
mediaAlbumHasSkippedAttachment = true
}
@ -1763,10 +1757,10 @@ private extension CVComponentState.Builder {
let caption = referencedAttachment.reference.legacyMessageCaption
let hasCaption = caption.map {
return !CVComponentState.displayableCaption(
return CVComponentState.displayableCaption(
text: $0,
transaction: transaction,
).fullTextValue.isEmpty
).fullTextValue.isEmpty.negated
} ?? false
switch cvAttachment {

View File

@ -195,22 +195,16 @@ public class CVComponentThreadDetails: CVComponentBase, CVRootComponent {
if safetySection.shouldShowProfileNamesEducation {
innerViews.append(UIView.spacer(withHeight: vSpacingNotVerifiedLabel))
var buttonConfiguration = headerButtonConfigurationBase()
buttonConfiguration.baseBackgroundColor = .Signal.warningLabel.withAlphaComponent(0.2)
buttonConfiguration.contentInsets = notVerifierButtonContentInsets
let nameNotVerifiedButton = componentView.profileNamesEducationButton
let nameNotVerifiedButtonLabelConfig = nameNotVerifiedConfig()
nameNotVerifiedButtonLabelConfig.applyForRendering(buttonConfiguration: &buttonConfiguration)
let nameNotVerifiedButton = UIButton(
configuration: buttonConfiguration,
primaryAction: UIAction { _ in
componentDelegate.didTapNameEducation(type: safetySection.threadType)
},
)
nameNotVerifiedButtonLabelConfig.applyForRendering(button: nameNotVerifiedButton)
nameNotVerifiedButton.backgroundColor = UIColor.Signal.warningLabel.withAlphaComponent(0.2)
nameNotVerifiedButton.ows_contentEdgeInsets = .init(hMargin: hPaddingNotVerifiedButton, vMargin: vPaddingNotVerifiedButton)
nameNotVerifiedButton.dimsWhenHighlighted = true
nameNotVerifiedButton.block = {
componentDelegate.didTapNameEducation(type: safetySection.threadType)
}
innerViews.append(nameNotVerifiedButton)
componentView.profileNamesEducationButton = nameNotVerifiedButton
} else if safetySection.isOfficialChat {
innerViews.append(UIView.spacer(withHeight: vSpacingNotVerifiedLabel))
@ -260,25 +254,19 @@ public class CVComponentThreadDetails: CVComponentBase, CVRootComponent {
}
if safetySection.shouldShowSafetyTipsButton {
var buttonConfiguration = headerButtonConfigurationBase()
buttonConfiguration.contentInsets = safetyButtonContentInsets
buttonConfiguration.baseBackgroundColor =
let showTipsButton = componentView.showTipsButton
safetyTipsButtonLabelConfig.applyForRendering(buttonConfiguration: &showTipsButton.configuration!)
showTipsButton.configuration?.baseBackgroundColor =
conversationStyle.hasWallpaper ? .Signal.MaterialBase.button : .Signal.secondaryFill
let safetyTipsButtonLabelConfig = safetyTipsButtonLabelConfig()
safetyTipsButtonLabelConfig.applyForRendering(buttonConfiguration: &buttonConfiguration)
let showTipsButton = UIButton(
configuration: buttonConfiguration,
primaryAction: UIAction { _ in
showTipsButton.addAction(
UIAction { _ in
componentDelegate.didTapSafetyTips()
},
for: .primaryActionTriggered,
)
innerViews.append(UIView.spacer(withHeight: vSpacingSafetyButton))
innerViews.append(showTipsButton)
componentView.showTipsButton = showTipsButton
}
}
@ -461,7 +449,7 @@ public class CVComponentThreadDetails: CVComponentBase, CVRootComponent {
)
}
private func safetyTipsButtonLabelConfig() -> CVLabelConfig {
private var safetyTipsButtonLabelConfig: CVLabelConfig {
CVLabelConfig.unstyledText(
OWSLocalizedString(
"SAFETY_TIPS_BUTTON_ACTION_TITLE",
@ -655,15 +643,11 @@ public class CVComponentThreadDetails: CVComponentBase, CVRootComponent {
private let vSpacingSafetySectionDefault: CGFloat = 8
private let safetyButtonContentInsets = NSDirectionalEdgeInsets(hMargin: 12, vMargin: 5)
private let notVerifierButtonContentInsets = NSDirectionalEdgeInsets(hMargin: 12, vMargin: 2)
private func headerButtonConfigurationBase() -> UIButton.Configuration {
var configuration = UIButton.Configuration.filled()
configuration.cornerStyle = .capsule
return configuration
}
private let hPaddingGroupDetails: CGFloat = 25
private let vPaddingNotVerifiedButton: CGFloat = 2
private let hPaddingNotVerifiedButton: CGFloat = 12
private let vOffsetThreadDetailsOutline: CGFloat = 16
private let minBottomPadding: CGFloat = 4
@ -705,18 +689,20 @@ public class CVComponentThreadDetails: CVComponentBase, CVRootComponent {
if let safetySection = threadDetails.safetySection {
if safetySection.shouldShowProfileNamesEducation {
innerSubviewInfos.append(CGSize(square: vSpacingNotVerifiedLabel).asManualSubviewInfo)
let buttonSize = CVText.measureLabel(
let notVerifiedSize = CVText.measureLabel(
config: nameNotVerifiedConfig(),
maxWidth: maxContentWidth,
) + notVerifierButtonContentInsets.asSize
innerSubviewInfos.append(buttonSize.asManualSubviewInfo)
)
let notVerifiedSizeWithPadding = CGSize(width: notVerifiedSize.width + hPaddingNotVerifiedButton * 2, height: notVerifiedSize.height + vPaddingNotVerifiedButton * 2)
innerSubviewInfos.append(notVerifiedSizeWithPadding.asManualSubviewInfo)
} else if safetySection.isOfficialChat {
innerSubviewInfos.append(CGSize(square: vSpacingNotVerifiedLabel).asManualSubviewInfo)
let buttonSize = CVText.measureLabel(
let officialLabelSize = CVText.measureLabel(
config: officialLabelConfig(),
maxWidth: maxContentWidth,
) + notVerifierButtonContentInsets.asSize
innerSubviewInfos.append(buttonSize.asManualSubviewInfo)
)
let officialLabelSizeWithPadding = CGSize(width: officialLabelSize.width + hPaddingNotVerifiedButton * 2, height: officialLabelSize.height + vPaddingNotVerifiedButton * 2)
innerSubviewInfos.append(officialLabelSizeWithPadding.asManualSubviewInfo)
}
}
@ -752,7 +738,7 @@ public class CVComponentThreadDetails: CVComponentBase, CVRootComponent {
if safetySection.shouldShowSafetyTipsButton {
innerSubviewInfos.append(CGSize(square: vSpacingSafetyButton).asManualSubviewInfo)
let buttonSize = CVText.measureLabel(
config: safetyTipsButtonLabelConfig(),
config: safetyTipsButtonLabelConfig,
maxWidth: maxContentWidth,
) + safetyButtonContentInsets.asSize
innerSubviewInfos.append(buttonSize.asManualSubviewInfo)
@ -801,8 +787,7 @@ public class CVComponentThreadDetails: CVComponentBase, CVRootComponent {
if let safetySection = threadDetails.safetySection {
if
safetySection.shouldShowSafetyTipsButton,
let showTipsButton = componentView.showTipsButton,
showTipsButton.bounds.contains(sender.location(in: componentView.showTipsButton))
componentView.showTipsButton.bounds.contains(sender.location(in: componentView.showTipsButton))
{
componentDelegate.didTapSafetyTips()
return true
@ -819,8 +804,7 @@ public class CVComponentThreadDetails: CVComponentBase, CVRootComponent {
if
safetySection.shouldShowProfileNamesEducation,
let profileNamesEducationButton = componentView.profileNamesEducationButton,
profileNamesEducationButton.bounds.contains(sender.location(in: componentView.profileNamesEducationButton))
componentView.profileNamesEducationButton.bounds.contains(sender.location(in: componentView.profileNamesEducationButton))
{
componentDelegate.didTapNameEducation(type: safetySection.threadType)
return true
@ -855,13 +839,17 @@ public class CVComponentThreadDetails: CVComponentBase, CVRootComponent {
fileprivate let titleButton = CVButton()
fileprivate let bioLabel = CVLabel()
fileprivate var profileNamesEducationButton: UIButton?
fileprivate let profileNamesEducationButton = OWSRoundedButton()
fileprivate let officialLabel = CVLabel()
fileprivate let reviewCarefullyLabel = CVLabel()
fileprivate let detailsButton = CVButton()
fileprivate let mutualGroupsLabel = CVLabel()
fileprivate var showTipsButton: UIButton?
fileprivate let showTipsButton: UIButton = {
let button = UIButton(configuration: .gray())
button.configuration?.contentInsets = NSDirectionalEdgeInsets(hMargin: 10, vMargin: 5)
return button
}()
fileprivate let groupDescriptionPreviewView = GroupDescriptionPreviewView(
shouldDeactivateConstraints: true,

View File

@ -59,7 +59,7 @@ extension TSInfoMessage.PersistableGroupUpdateItem {
)
{
owsAssertDebug(
!isTail,
isTail.negated,
"Collapsed item with a following request shouldn't be a tail!",
)
return nextItemAction

View File

@ -9,7 +9,6 @@ public import UIKit
public protocol ConversationInputTextViewDelegate: AnyObject {
func didAttemptAttachmentPaste()
func didAttemptAccountEntropyPoolPaste(completePaste: @escaping () -> Void)
func inputTextViewSendMessagePressed()
func textViewDidChange(_ textView: UITextView)
}
@ -200,50 +199,9 @@ class ConversationInputTextView: BodyRangesTextView {
return
}
if handleAttemptedAccountEntropyPoolPaste() {
return
}
super.paste(sender)
}
private func handleAttemptedAccountEntropyPoolPaste() -> Bool {
let accountKeyStore = DependenciesBridge.shared.accountKeyStore
let db = DependenciesBridge.shared.db
guard let pasteboardString = UIPasteboard.general.strings?.first else {
return false
}
let filteredPasteboardString = pasteboardString.filter { !$0.isWhitespace }
guard
let pastedAEP = try? DisplayableAccountEntropyPool(displayString: filteredPasteboardString),
let localAEP = db.read(block: { accountKeyStore.getAccountEntropyPool(tx: $0) }),
pastedAEP.rawValue == localAEP
else {
return false
}
inputTextViewDelegate?.didAttemptAccountEntropyPoolPaste(
completePaste: { [weak self] in
guard let self else { return }
let pasteRange: UITextRange
if let selectedTextRange {
pasteRange = selectedTextRange
} else if let endRange = textRange(from: endOfDocument, to: endOfDocument) {
pasteRange = endRange
} else {
return
}
replace(pasteRange, withText: filteredPasteboardString)
},
)
return true
}
// MARK: - UITextViewDelegate
override func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {

View File

@ -1219,6 +1219,7 @@ public class ConversationInputToolbar: UIView, QuotedReplyPreviewDelegate {
lazy var sendButton: UIButton = {
let button = UIButton(type: .system)
button.accessibilityLabel = MessageStrings.sendButton
button.ows_adjustsImageWhenDisabled = true
button.isPointerInteractionEnabled = true
button.accessibilityIdentifier = UIView.accessibilityIdentifier(in: self, name: "sendButton")
button.setImage(UIImage(imageLiteralResourceName: "send-blue-28"), for: .normal)

View File

@ -33,8 +33,6 @@ extension ConversationViewController: CVComponentDelegate {
viewState.expandedCollapseSets.insert(collapseSetId)
}
loadCoordinator.enqueueReload(
updatedInteractionIds: [collapseSetId],
deletedInteractionIds: [],
preferredScrollContinuityAnchorInteractionId: collapseSetId,
)
}
@ -183,7 +181,7 @@ extension ConversationViewController: CVComponentDelegate {
// If any of the failed or pending downloads were enqueued by a Backup
// restore, immediately attempt to download those attachments.
Task.detached {
Task {
let attachmentDownloadManager = DependenciesBridge.shared.attachmentDownloadManager
let attachmentStore = DependenciesBridge.shared.attachmentStore
let backupAttachmentDownloadStore = DependenciesBridge.shared.backupAttachmentDownloadStore
@ -194,22 +192,17 @@ extension ConversationViewController: CVComponentDelegate {
return
}
enum DownloadTypeToEnqueue {
case thumbnail
case fullsize
}
let messageTypeToDownload: DownloadTypeToEnqueue? = db.read { tx in
let messageHasAnyEnqueuedBackupDownloads = db.read { tx in
let referencedAttachments = attachmentStore.fetchReferencedAttachmentsOwnedByMessage(
messageRowId: messageRowId,
tx: tx,
)
let downloadTypes: [DownloadTypeToEnqueue] = referencedAttachments.compactMap { referencedAttachment in
return referencedAttachments.contains { referencedAttachment in
// We only auto-download on appear if we've got a cdn number to try.
// The user can still manual download if there isn't one (using fallback cdn).
guard referencedAttachment.attachment.mediaTierInfo?.cdnNumber != nil else {
return nil
return false
}
// Otherwise use presence in the backup download queue to indicate
// downloadability; this just functionally bumps the priority so the
@ -220,60 +213,22 @@ extension ConversationViewController: CVComponentDelegate {
tx: tx,
)
switch enqueuedDownload?.state {
case .ineligible:
if referencedAttachment.attachment.localRelativeFilePathThumbnail != nil {
return nil
}
let enqueuedThumbnail = backupAttachmentDownloadStore.getEnqueuedDownload(
attachmentRowId: referencedAttachment.attachment.id,
thumbnail: true,
tx: tx,
)
switch enqueuedThumbnail?.state {
case .ready:
return .thumbnail
case .done, .ineligible, nil:
// There is already a thumbnail, or never will be a thumbnail to display here.
// Either way, no need to re-enqueue the thumbnail
return nil
}
case nil, .done:
return nil
case nil, .done, .ineligible:
return false
case .ready:
return .fullsize
return true
}
}
if downloadTypes.contains(.fullsize) {
return .fullsize
} else if downloadTypes.contains(.thumbnail) {
return .thumbnail
} else {
return nil
}
}
switch messageTypeToDownload {
case .fullsize:
if messageHasAnyEnqueuedBackupDownloads {
await db.awaitableWrite { tx in
attachmentDownloadManager.enqueueDownloadOfAttachmentsForMessage(
message,
priority: .default,
useThumbnails: false,
tx: tx,
)
}
case .thumbnail:
await db.awaitableWrite { tx in
attachmentDownloadManager.enqueueDownloadOfAttachmentsForMessage(
message,
priority: .default,
useThumbnails: true,
tx: tx,
)
}
case .none:
break
}
}
}
@ -287,7 +242,6 @@ extension ConversationViewController: CVComponentDelegate {
attachmentDownloadManager.enqueueDownloadOfAttachmentsForMessage(
message,
priority: .userInitiated,
useThumbnails: false,
tx: tx,
)
}

View File

@ -295,58 +295,6 @@ extension ConversationViewController: ConversationInputTextViewDelegate {
}
}
public func didAttemptAccountEntropyPoolPaste(completePaste: @escaping () -> Void) {
let warningSheet = BackupNeverShareRecoveryKeySheet(
primaryButton: .dismissing(
title: OWSLocalizedString(
"CONVERSATION_INPUT_PASTE_RECOVERY_KEY_WARNING_SHEET_DO_NOT_SHARE_BUTTON_TITLE",
comment: "Title for the button in a warning sheet shown before the user pastes their 'Recovery Key' into the chat input text field that dismisses the sheet without pasting the key.",
),
),
secondaryButton: HeroSheetViewController.Button(
title: OWSLocalizedString(
"CONVERSATION_INPUT_PASTE_RECOVERY_KEY_WARNING_SHEET_SHARE_BUTTON_TITLE",
comment: "Title for the button in a warning sheet shown before the user pastes their 'Recovery Key' into the chat input text field that acknowledges the warning and proceeds with the paste.",
),
style: .secondaryDestructive,
action: .custom({ [weak self] sheet in
sheet.dismiss(animated: true) {
let doubleWarningSheet = ActionSheetController(
title: OWSLocalizedString(
"CONVERSATION_INPUT_PASTE_RECOVERY_KEY_CONFIRM_SHEET_TITLE",
comment: "Title for a second confirmation sheet shown after the user opts to paste their 'Recovery Key' into the chat input text field anyway.",
),
message: OWSLocalizedString(
"CONVERSATION_INPUT_PASTE_RECOVERY_KEY_CONFIRM_SHEET_MESSAGE",
comment: "Message body for a second confirmation sheet shown after the user opts to paste their 'Recovery Key' into the chat input text field anyway, warning them not to share it.",
),
)
doubleWarningSheet.addAction(ActionSheetAction(
title: OWSLocalizedString(
"CONVERSATION_INPUT_PASTE_RECOVERY_KEY_CONFIRM_SHEET_PASTE_BUTTON_TITLE",
comment: "Title for the destructive button in a second confirmation sheet shown after the user opts to paste their 'Recovery Key' into the chat input text field anyway, that proceeds with the paste.",
),
style: .destructive,
handler: { _ in
completePaste()
},
))
doubleWarningSheet.addAction(ActionSheetAction(
title: OWSLocalizedString(
"CONVERSATION_INPUT_PASTE_RECOVERY_KEY_CONFIRM_SHEET_DO_NOT_SHARE_BUTTON_TITLE",
comment: "Title for the button in a second confirmation sheet shown after the user opts to paste their 'Recovery Key' into the chat input text field anyway, that dismisses the sheet without pasting the key.",
),
))
self?.present(doubleWarningSheet, animated: true)
}
}),
),
)
present(warningSheet, animated: true)
}
public func inputTextViewSendMessagePressed() {
AssertIsOnMainThread()

View File

@ -372,8 +372,8 @@ extension ConversationViewController {
timer.invalidate()
return
}
// If the view isn't visible, return
guard self.view.window != nil else {
owsFailDebug("Read timer fired when ConversationViewController is not in a view hierarchy")
timer.invalidate()
return
}

View File

@ -5,7 +5,7 @@
import SignalServiceKit
final class CollapseSetInteraction: TSInteraction {
class CollapseSetInteraction: TSInteraction {
enum MessagesType: Equatable {
case groupUpdates
@ -18,6 +18,8 @@ final class CollapseSetInteraction: TSInteraction {
let collapseSetType: MessagesType
let isExpanded: Bool
let finalTimerDescription: String?
override var isDynamicInteraction: Bool { true }
@ -30,10 +32,12 @@ final class CollapseSetInteraction: TSInteraction {
thread: TSThread,
collapsedInteractions: [TSInteraction],
collapseSetType: MessagesType,
isExpanded: Bool = false,
) {
owsPrecondition(!collapsedInteractions.isEmpty)
self.collapsedInteractions = collapsedInteractions
self.collapseSetType = collapseSetType
self.isExpanded = isExpanded
self.finalTimerDescription = Self.disappearingTimerDescription(
for: collapsedInteractions,
type: collapseSetType,
@ -41,17 +45,13 @@ final class CollapseSetInteraction: TSInteraction {
let firstInteraction = collapsedInteractions[0]
super.init(
customUniqueId: Self.id(firstInteraction: firstInteraction),
customUniqueId: "CollapseSet_\(firstInteraction.timestamp)",
timestamp: firstInteraction.timestamp,
receivedAtTimestamp: firstInteraction.receivedAtTimestamp,
thread: thread,
)
}
static func id(firstInteraction: TSInteraction) -> String {
"CollapseSet_\(firstInteraction.timestamp)"
}
private static func disappearingTimerDescription(
for interactions: [TSInteraction],
type: MessagesType,

View File

@ -427,23 +427,6 @@ public class CVLoadCoordinator: NSObject {
loadIfNecessary()
}
public func enqueueReload(
updatedInteractionIds: Set<String>,
deletedInteractionIds: Set<String>,
preferredScrollContinuityAnchorInteractionId: String,
) {
AssertIsOnMainThread()
loadRequestBuilder.reload(
updatedInteractionIds: updatedInteractionIds,
deletedInteractionIds: deletedInteractionIds,
)
loadRequestBuilder.reload(
preferredScrollContinuityAnchorInteractionId: preferredScrollContinuityAnchorInteractionId,
)
loadIfNecessary()
}
public func enqueueReloadWithoutCaches() {
AssertIsOnMainThread()

View File

@ -80,10 +80,6 @@ public class CVLoader: NSObject {
localAci: localAci,
transaction: transaction,
)
let preprocessingContext = MessageLoaderPreprocessingContext(
thread: loadContext.thread,
oldestUnreadSortId: viewStateSnapshot.oldestUnreadMessageSortId,
)
// Don't cache in the reset() case.
let canReuseInteractions = loadRequest.canReuseInteractionModels && !loadRequest.didReset
@ -136,35 +132,30 @@ public class CVLoader: NSObject {
focusMessageId: focusMessageIdOnOpen,
reusableInteractions: [:],
deletedInteractionIds: [],
preprocessingContext: preprocessingContext,
tx: transaction,
)
case .loadSameLocation:
try messageLoader.loadSameLocation(
reusableInteractions: reusableInteractions,
deletedInteractionIds: deletedInteractionIds,
preprocessingContext: preprocessingContext,
tx: transaction,
)
case .loadOlder:
try messageLoader.loadOlderMessagePage(
reusableInteractions: reusableInteractions,
deletedInteractionIds: deletedInteractionIds,
preprocessingContext: preprocessingContext,
tx: transaction,
)
case .loadNewer:
try messageLoader.loadNewerMessagePage(
reusableInteractions: reusableInteractions,
deletedInteractionIds: deletedInteractionIds,
preprocessingContext: preprocessingContext,
tx: transaction,
)
case .loadNewest:
try messageLoader.loadNewestMessagePage(
reusableInteractions: reusableInteractions,
deletedInteractionIds: deletedInteractionIds,
preprocessingContext: preprocessingContext,
tx: transaction,
)
case .loadPageAroundInteraction(let interactionId, _):
@ -172,7 +163,6 @@ public class CVLoader: NSObject {
aroundInteractionId: interactionId,
reusableInteractions: reusableInteractions,
deletedInteractionIds: deletedInteractionIds,
preprocessingContext: preprocessingContext,
tx: transaction,
)
}
@ -181,18 +171,36 @@ public class CVLoader: NSObject {
throw error
}
let expandedInteractions = messageLoader.loadedDisplayableInteractions.flatMap { interaction in
if
let collapseSet = interaction as? CollapseSetInteraction,
viewStateSnapshot.expandedCollapseSetIds.contains(collapseSet.uniqueId)
let initialLoadCount = messageLoader.loadedInteractions.count
var processedInteractions = Self.preprocessInteractions(
messageLoader.loadedInteractions,
loadContext: loadContext,
)
if case .loadInitialMapping = loadRequest.loadType {
let maxExtraLoads = 5
var extraLoads = 0
while
processedInteractions.count < initialLoadCount,
messageLoader.canLoadOlder,
extraLoads < maxExtraLoads
{
return [collapseSet] + collapseSet.collapsedInteractions
try messageLoader.loadOlderMessagePage(
reusableInteractions: reusableInteractions,
deletedInteractionIds: deletedInteractionIds,
tx: transaction,
)
processedInteractions = Self.preprocessInteractions(
messageLoader.loadedInteractions,
loadContext: loadContext,
)
extraLoads += 1
}
return [interaction]
}
let itemModels = self.buildItemModels(
interactions: expandedInteractions,
interactions: processedInteractions,
loadContext: loadContext,
updatedInteractionIds: updatedInteractionIds,
localAci: localAci,
@ -264,6 +272,214 @@ public class CVLoader: NSObject {
return itemModelBuilder.buildItems(localAci: localAci, interactions: interactions)
}
// MARK: - Interaction Preprocessing
private static let maxCollapseSetSize = 50
/// Takes a list of interactions and applies preprocessing before the expensive task of creating `CVItemModel`s via `CVItemModelBuilder.buildItems`.
///
/// 1. Inserts date headers
/// 2. Inserts unread indicator
/// 3. Collapses chat events
private static func preprocessInteractions(
_ interactions: [TSInteraction],
loadContext: CVLoadContext,
) -> [TSInteraction] {
let thread = loadContext.thread
let isGroupThread = thread.isGroupThread
let expandedCollapseSets = loadContext.viewStateSnapshot.expandedCollapseSets
let oldestUnreadSortId = loadContext.viewStateSnapshot.oldestUnreadMessageSortId
let todayDate = Date()
var result = [TSInteraction]()
var currentRun = [TSInteraction]()
var currentRunType: CollapseSetInteraction.MessagesType?
var pastUnreadIndicator = false
var shouldShowDateOnNextViewItem = true
var previousDaysBeforeToday: Int?
func finalizeSet() {
defer {
currentRun.removeAll()
currentRunType = nil
}
guard currentRun.count >= 2, let runType = currentRunType else {
result.append(contentsOf: currentRun)
return
}
let collapseId = "CollapseSet_\(currentRun[0].timestamp)"
let isExpanded = expandedCollapseSets.contains(collapseId)
let collapseSetInteraction = CollapseSetInteraction(
thread: thread,
collapsedInteractions: currentRun,
collapseSetType: runType,
isExpanded: isExpanded,
)
result.append(collapseSetInteraction)
if isExpanded {
result.append(contentsOf: currentRun)
}
}
for interaction in interactions {
let timestamp = interaction.timestamp
let daysBeforeToday = DateUtil.daysFrom(
firstDate: Date(millisecondsSince1970: timestamp),
toSecondDate: todayDate,
)
if let previousDaysBeforeToday {
if daysBeforeToday != previousDaysBeforeToday {
shouldShowDateOnNextViewItem = true
}
} else {
// Only show for the first item if the date is not today
shouldShowDateOnNextViewItem = daysBeforeToday != 0
}
if
shouldShowDateOnNextViewItem,
canShowDateHeader(before: interaction)
{
// Collapse sets shouldn't cross date boundaries
finalizeSet()
result.append(DateHeaderInteraction(thread: thread, timestamp: timestamp))
shouldShowDateOnNextViewItem = false
}
previousDaysBeforeToday = daysBeforeToday
// Only insert one unread indicator and don't collapse unread events
if pastUnreadIndicator {
result.append(interaction)
continue
}
if let oldestUnreadSortId, oldestUnreadSortId <= interaction.sortId {
finalizeSet()
let unreadIndicatorInteraction = UnreadIndicatorInteraction(
thread: thread,
timestamp: timestamp,
receivedAtTimestamp: interaction.receivedAtTimestamp,
)
result.append(unreadIndicatorInteraction)
pastUnreadIndicator = true
result.append(interaction)
continue
}
guard BuildFlags.collapsingChatEvents else {
result.append(interaction)
continue
}
let collapseType = collapseSetType(for: interaction, isGroupThread: isGroupThread)
if let collapseType {
let isDifferentSetThanCurrentRun = currentRunType != nil && currentRunType != collapseType
let exceededCurrentRunLimit = currentRun.count >= maxCollapseSetSize
if isDifferentSetThanCurrentRun || exceededCurrentRunLimit {
finalizeSet()
}
currentRun.append(interaction)
currentRunType = collapseType
} else {
finalizeSet()
result.append(interaction)
}
}
finalizeSet()
return result
}
private static func canShowDateHeader(before interaction: TSInteraction) -> Bool {
switch interaction.interactionType {
case .unknown, .typingIndicator, .threadDetails, .dateHeader, .unknownThreadWarning, .defaultDisappearingMessageTimer, .collapseSet:
return false
case .info:
guard let infoMessage = interaction as? TSInfoMessage else {
owsFailDebug("Invalid interaction.")
return false
}
// Only show the date for non-synced thread messages;
return infoMessage.messageType != .syncedThread
case .unreadIndicator, .incomingMessage, .outgoingMessage, .error, .call:
return true
}
}
private static func collapseSetType(
for interaction: TSInteraction,
isGroupThread: Bool,
) -> CollapseSetInteraction.MessagesType? {
switch interaction.interactionType {
case .info:
guard let infoMessage = interaction as? TSInfoMessage else {
owsFailDebug("info interaction is not TSInfoMessage")
return nil
}
switch infoMessage.messageType {
case .typeDisappearingMessagesUpdate:
return .timerChanges
case .typeGroupUpdate:
if
let wrapper = infoMessage.infoMessageUserInfo?[.groupUpdateItems]
as? TSInfoMessage.PersistableGroupUpdateItemsWrapper
{
for event in wrapper.updateItems {
switch event {
case
.groupTerminatedByLocalUser,
.groupTerminatedByOtherUser,
.groupTerminatedByUnknownUser:
return nil
case
.disappearingMessagesEnabledByLocalUser,
.disappearingMessagesEnabledByOtherUser,
.disappearingMessagesEnabledByUnknownUser,
.disappearingMessagesDisabledByLocalUser,
.disappearingMessagesDisabledByOtherUser,
.disappearingMessagesDisabledByUnknownUser:
return .timerChanges
default:
break
}
}
}
return isGroupThread ? .groupUpdates : .chatUpdates
case .verificationStateChange,
.profileUpdate,
.phoneNumberChange,
.typeEndPoll,
.typePinnedMessage:
return isGroupThread ? .groupUpdates : .chatUpdates
default:
return nil
}
case .error:
guard let errorMessage = interaction as? TSErrorMessage else {
owsFailDebug("error interaction is not TSErrorMessage")
return nil
}
if errorMessage.errorType == .nonBlockingIdentityChange {
return isGroupThread ? .groupUpdates : .chatUpdates
}
return nil
case .call:
// Don't collapse an active group call.
if
let groupCallMessage = interaction as? OWSGroupCallMessage,
!groupCallMessage.hasEnded
{
return nil
}
return .callEvents
default:
return nil
}
}
// MARK: -
#if USE_DEBUG_UI
public static func debugui_buildStandaloneRenderItem(

View File

@ -42,7 +42,7 @@ struct CVViewStateSnapshot {
let hasActiveCall: Bool
let currentGroupThreadCallGroupId: GroupIdentifier?
let expandedCollapseSetIds: Set<String>
let expandedCollapseSets: Set<String>
private static var currentCallProvider: any CurrentCallProvider { DependenciesBridge.shared.currentCallProvider }
@ -64,7 +64,7 @@ struct CVViewStateSnapshot {
oldestUnreadMessageSortId: oldestUnreadMessageSortId,
hasActiveCall: currentCallProvider.hasCurrentCall,
currentGroupThreadCallGroupId: currentCallProvider.currentGroupThreadCallGroupId,
expandedCollapseSetIds: viewState.expandedCollapseSets,
expandedCollapseSets: viewState.expandedCollapseSets,
)
}
@ -84,7 +84,7 @@ struct CVViewStateSnapshot {
oldestUnreadMessageSortId: nil,
hasActiveCall: false,
currentGroupThreadCallGroupId: nil,
expandedCollapseSetIds: [],
expandedCollapseSets: [],
)
}
}

View File

@ -7,13 +7,11 @@ import Foundation
import SignalServiceKit
private enum Constants {
/// The maximum number of top-level interactions to keep in memory. We start
/// dropping interactions (in an LRU fashion) once we've exceeded this value.
/// The maximum number of interactions to keep in memory. We start dropping
/// interactions (in an LRU fashion) once we've exceeded this value.
///
/// TODO: Should we reduce this value?
static let maxDisplayableInteractionCount = 500
static let maxCollapseSetSize = 50
static let maxInteractionCount = 500
}
protocol MessageLoaderBatchFetcher {
@ -30,19 +28,11 @@ protocol MessageLoaderInteractionFetcher {
// MARK: -
struct MessageLoaderPreprocessingContext {
let thread: TSThread
let oldestUnreadSortId: UInt64?
}
// MARK: -
class MessageLoader {
private let batchFetcher: MessageLoaderBatchFetcher
private let interactionFetchers: [MessageLoaderInteractionFetcher]
private(set) var loadedInteractions: [TSInteraction] = []
private(set) var loadedDisplayableInteractions: [TSInteraction] = []
/// If true, there might be older messages that could be loaded. If false,
/// we believe we've reached the beginning of the chat.
@ -100,61 +90,10 @@ class MessageLoader {
case sameLocation
}
/// A single display unit: one standalone interaction or a collapse set.
private struct LoadedSegment {
/// Either a single item to be displayed or multiple updates to be
/// grouped in a collapse set.
var rawInteractions: [TSInteraction]
/// Zero or more generated elements (date header or unread indicator)
/// followed by the elements to be displayed. The single raw item
/// itself, or a collapse set which would be followed by
/// `rawInteractions` if expanded.
var displayableInteractions: [TSInteraction]
}
/// Groups raw interactions with the displayable interactions they produce
/// during preprocessing, so trimming can drop complete display units.
private struct LoadedPage {
let segments: [LoadedSegment]
var rawInteractions: [TSInteraction] {
segments.flatMap(\.rawInteractions)
}
var displayableInteractions: [TSInteraction] {
segments.flatMap(\.displayableInteractions)
}
var rawInteractionCount: Int {
segments.lazy.map(\.rawInteractions.count).reduce(0, +)
}
func trimmingDisplayableInteractions(
trimOlder: Bool,
) -> LoadedPage {
let segments = trimOlder ? self.segments.reversed() : self.segments
var trimmedSegments: [LoadedSegment] = []
var displayableCount = 0
for segment in segments {
let segmentDisplayableCount = segment.displayableInteractions.count
displayableCount += segmentDisplayableCount
guard displayableCount <= Constants.maxDisplayableInteractionCount else {
break
}
trimmedSegments.append(segment)
}
if trimOlder {
trimmedSegments.reverse()
}
return LoadedPage(segments: trimmedSegments)
}
}
func loadMessagePage(
aroundInteractionId interactionUniqueId: String,
reusableInteractions: [String: TSInteraction],
deletedInteractionIds: Set<String>?,
preprocessingContext: MessageLoaderPreprocessingContext? = nil,
tx: DBReadTransaction,
) throws {
try ensureLoaded(
@ -162,7 +101,6 @@ class MessageLoader {
count: initialLoadCount,
reusableInteractions: reusableInteractions,
deletedInteractionIds: deletedInteractionIds,
preprocessingContext: preprocessingContext,
tx: tx,
)
}
@ -170,7 +108,6 @@ class MessageLoader {
func loadNewerMessagePage(
reusableInteractions: [String: TSInteraction],
deletedInteractionIds: Set<String>?,
preprocessingContext: MessageLoaderPreprocessingContext? = nil,
tx: DBReadTransaction,
) throws {
try ensureLoaded(
@ -178,7 +115,6 @@ class MessageLoader {
count: initialLoadCount * 2,
reusableInteractions: reusableInteractions,
deletedInteractionIds: deletedInteractionIds,
preprocessingContext: preprocessingContext,
tx: tx,
)
}
@ -186,7 +122,6 @@ class MessageLoader {
func loadOlderMessagePage(
reusableInteractions: [String: TSInteraction],
deletedInteractionIds: Set<String>?,
preprocessingContext: MessageLoaderPreprocessingContext? = nil,
tx: DBReadTransaction,
) throws {
try ensureLoaded(
@ -194,7 +129,6 @@ class MessageLoader {
count: initialLoadCount * 2,
reusableInteractions: reusableInteractions,
deletedInteractionIds: deletedInteractionIds,
preprocessingContext: preprocessingContext,
tx: tx,
)
}
@ -202,7 +136,6 @@ class MessageLoader {
func loadNewestMessagePage(
reusableInteractions: [String: TSInteraction],
deletedInteractionIds: Set<String>?,
preprocessingContext: MessageLoaderPreprocessingContext? = nil,
tx: DBReadTransaction,
) throws {
try ensureLoaded(
@ -210,7 +143,6 @@ class MessageLoader {
count: initialLoadCount,
reusableInteractions: reusableInteractions,
deletedInteractionIds: deletedInteractionIds,
preprocessingContext: preprocessingContext,
tx: tx,
)
}
@ -219,7 +151,6 @@ class MessageLoader {
focusMessageId: String?,
reusableInteractions: [String: TSInteraction],
deletedInteractionIds: Set<String>?,
preprocessingContext: MessageLoaderPreprocessingContext? = nil,
tx: DBReadTransaction,
) throws {
if let focusMessageId {
@ -228,14 +159,12 @@ class MessageLoader {
count: initialLoadCount,
reusableInteractions: reusableInteractions,
deletedInteractionIds: deletedInteractionIds,
preprocessingContext: preprocessingContext,
tx: tx,
)
} else {
try loadNewestMessagePage(
reusableInteractions: reusableInteractions,
deletedInteractionIds: deletedInteractionIds,
preprocessingContext: preprocessingContext,
tx: tx,
)
}
@ -244,15 +173,13 @@ class MessageLoader {
func loadSameLocation(
reusableInteractions: [String: TSInteraction],
deletedInteractionIds: Set<String>?,
preprocessingContext: MessageLoaderPreprocessingContext? = nil,
tx: DBReadTransaction,
) throws {
try ensureLoaded(
.sameLocation,
count: max(initialLoadCount, loadedDisplayableInteractions.count),
count: max(initialLoadCount, loadedInteractions.count),
reusableInteractions: reusableInteractions,
deletedInteractionIds: deletedInteractionIds,
preprocessingContext: preprocessingContext,
tx: tx,
)
}
@ -268,122 +195,21 @@ class MessageLoader {
count: Int,
reusableInteractions: [String: TSInteraction],
deletedInteractionIds: Set<String>?,
preprocessingContext: MessageLoaderPreprocessingContext?,
tx: DBReadTransaction,
) throws {
owsAssertDebug(count > 0)
let maxRawInteractionFetchCount = Constants.maxDisplayableInteractionCount * Constants.maxCollapseSetSize
let count = count.clamp(1, maxRawInteractionFetchCount)
let loadedDisplayableCount = loadedDisplayableInteractions.count
let desiredDisplayableInteractionCount: Int = switch direction {
case .older, .newer:
loadedDisplayableCount + count
case .sameLocation:
max(initialLoadCount, loadedDisplayableCount)
case .around, .newest:
count
}
var loadBatch = try buildLoadBatch(
let count = count.clamp(1, Constants.maxInteractionCount)
let loadBatch = try buildLoadBatch(
direction,
count: count,
deletedInteractionIds: deletedInteractionIds,
tx: tx,
)
var loadedPage = buildLoadedPage(
for: loadBatch,
loadedInteractions = fetchInteractions(
uniqueIds: loadBatch.uniqueIds,
reusableInteractions: reusableInteractions,
preprocessingContext: preprocessingContext,
tx: tx,
)
func loadMoreIfNeeded(context: MessageLoaderPreprocessingContext) throws -> Bool {
let loadedDisplayableInteractionCount = loadedPage.displayableInteractions.count
guard loadedDisplayableInteractionCount < desiredDisplayableInteractionCount else {
return false
}
// Heuristically adjust fetch size based on the proportion of
// messages so far that are collapsed.
let remainingCount = desiredDisplayableInteractionCount - loadedDisplayableInteractionCount
let estimatedRawInteractionsPerDisplayableInteraction = min(
Constants.maxCollapseSetSize,
max(
1,
Int(ceil(Double(loadedPage.rawInteractionCount) / Double(max(loadedDisplayableInteractionCount, 1)))),
),
)
let fetchCount = min(
maxRawInteractionFetchCount,
max(count, remainingCount * estimatedRawInteractionsPerDisplayableInteraction),
)
guard fetchCount > 0 else {
return false
}
func fetchOlder() throws -> Bool {
guard
loadBatch.canLoadOlder,
let firstInteraction = loadedPage.segments.first?.rawInteractions.first,
let rowId = firstInteraction.sqliteRowId
else {
return false
}
return try self.fetchOlder(before: rowId, count: fetchCount, batch: &loadBatch, tx: tx) > 0
}
func fetchNewer() throws -> Bool {
guard
loadBatch.canLoadNewer,
let lastInteraction = loadedPage.segments.last?.rawInteractions.last,
let rowId = lastInteraction.sqliteRowId
else {
return false
}
return try self.fetchNewer(after: rowId, count: fetchCount, batch: &loadBatch, tx: tx) > 0
}
let didLoadMore: Bool
switch direction {
case .older, .newest:
didLoadMore = try fetchOlder()
case .newer:
didLoadMore = try fetchNewer()
case .sameLocation, .around:
if try fetchOlder() {
didLoadMore = true
} else {
didLoadMore = try fetchNewer()
}
}
guard didLoadMore else {
return false
}
loadedPage = buildLoadedPage(
for: loadBatch,
reusableInteractions: reusableInteractions,
preprocessingContext: context,
tx: tx,
)
return true
}
if let preprocessingContext {
while try loadMoreIfNeeded(context: preprocessingContext) {
// Loading more messages...
}
}
trimLoadedPageIfNeeded(
&loadBatch,
loadedPage: &loadedPage,
loadDirection: direction,
)
loadedInteractions = loadedPage.rawInteractions
loadedDisplayableInteractions = loadedPage.displayableInteractions
canLoadNewer = loadBatch.canLoadNewer
canLoadOlder = loadBatch.canLoadOlder
}
@ -402,6 +228,24 @@ class MessageLoader {
)
}
/// Expands `batch` with `count` messages preceding `rowId`.
@discardableResult
func fetchOlder(before rowId: Int64, count: Int, batch: inout MessageLoaderBatch) throws -> Int {
let uniqueIds: [String] = try fetch(filter: .before(rowId), limit: count)
batch.insertOlder(uniqueIds: uniqueIds, didReachOldest: uniqueIds.count < count)
batch.trimNewer()
return uniqueIds.count
}
/// Expands `batch` with `count` messages succeeding `rowId`.
@discardableResult
func fetchNewer(after rowId: Int64, count: Int, batch: inout MessageLoaderBatch) throws -> Int {
let uniqueIds: [String] = try fetch(filter: .after(rowId), limit: count)
batch.insertNewer(uniqueIds: uniqueIds, didReachNewest: uniqueIds.count < count)
batch.trimOlder()
return uniqueIds.count
}
/// Fetches uniqueIds in the range of provided rowIds.
func fetchRange(_ rowIds: ClosedRange<Int64>) throws -> [String] {
return try fetch(filter: .range(rowIds), limit: rowIds.count)
@ -421,8 +265,8 @@ class MessageLoader {
return try loadNewest()
}
var batch = MessageLoaderBatch(canLoadNewer: true, canLoadOlder: true, uniqueIds: [uniqueId])
let olderCount = try fetchOlder(before: rowId, count: count / 2, batch: &batch, tx: tx)
try fetchNewer(after: rowId, count: count - olderCount, batch: &batch, tx: tx)
let olderCount = try fetchOlder(before: rowId, count: count / 2, batch: &batch)
try fetchNewer(after: rowId, count: count - olderCount, batch: &batch)
return batch
}
@ -467,7 +311,7 @@ class MessageLoader {
return batch
case .older:
var batch = priorLoad.batch
try fetchOlder(before: priorLoad.range.lowerBound, count: count, batch: &batch, tx: tx)
try fetchOlder(before: priorLoad.range.lowerBound, count: count, batch: &batch)
return batch
case .sameLocation where !priorLoad.batch.canLoadNewer:
// If we're loading at the same location and are already at the end of the
@ -475,13 +319,13 @@ class MessageLoader {
fallthrough
case .newer:
var batch = priorLoad.batch
try fetchNewer(after: priorLoad.range.upperBound, count: count, batch: &batch, tx: tx)
try fetchNewer(after: priorLoad.range.upperBound, count: count, batch: &batch)
return batch
case .sameLocation:
var batch = priorLoad.batch
if batch.uniqueIds.count < initialLoadCount {
try fetchOlder(before: priorLoad.range.lowerBound, count: initialLoadCount, batch: &batch, tx: tx)
try fetchNewer(after: priorLoad.range.upperBound, count: initialLoadCount, batch: &batch, tx: tx)
try fetchOlder(before: priorLoad.range.lowerBound, count: initialLoadCount, batch: &batch)
try fetchNewer(after: priorLoad.range.upperBound, count: initialLoadCount, batch: &batch)
}
return batch
case .around(interactionUniqueId: let uniqueId):
@ -499,32 +343,6 @@ class MessageLoader {
}
}
/// Expands `batch` with `count` messages preceding `rowId`.
@discardableResult
private func fetchOlder(
before rowId: Int64,
count: Int,
batch: inout MessageLoaderBatch,
tx: DBReadTransaction,
) throws -> Int {
let uniqueIds = try batchFetcher.fetchUniqueIds(filter: .before(rowId), limit: count, tx: tx)
batch.insertOlder(uniqueIds: uniqueIds, didReachOldest: uniqueIds.count < count)
return uniqueIds.count
}
/// Expands `batch` with `count` messages succeeding `rowId`.
@discardableResult
private func fetchNewer(
after rowId: Int64,
count: Int,
batch: inout MessageLoaderBatch,
tx: DBReadTransaction,
) throws -> Int {
let uniqueIds = try batchFetcher.fetchUniqueIds(filter: .after(rowId), limit: count, tx: tx)
batch.insertNewer(uniqueIds: uniqueIds, didReachNewest: uniqueIds.count < count)
return uniqueIds.count
}
private func fetchInteractions(
uniqueIds interactionIds: [String],
reusableInteractions: [String: TSInteraction] = [:],
@ -542,268 +360,6 @@ class MessageLoader {
}
return refinery.values.compacted()
}
private func buildLoadedPage(
for batch: MessageLoaderBatch,
reusableInteractions: [String: TSInteraction],
preprocessingContext: MessageLoaderPreprocessingContext?,
tx: DBReadTransaction,
) -> LoadedPage {
let rawInteractions = fetchInteractions(
uniqueIds: batch.uniqueIds,
reusableInteractions: reusableInteractions,
tx: tx,
)
return LoadedPage(
segments: Self.preprocessInteractions(
rawInteractions,
preprocessingContext: preprocessingContext,
),
)
}
private func trimLoadedPageIfNeeded(
_ loadBatch: inout MessageLoaderBatch,
loadedPage: inout LoadedPage,
loadDirection: LoadWindowDirection,
) {
guard loadedPage.displayableInteractions.count > Constants.maxDisplayableInteractionCount else {
return
}
let trimOlder: Bool = switch loadDirection {
case .newer, .around, .newest, .sameLocation:
true
case .older:
false
}
loadedPage = loadedPage.trimmingDisplayableInteractions(trimOlder: trimOlder)
loadBatch.uniqueIds = loadedPage.rawInteractions.map(\.uniqueId)
if trimOlder {
loadBatch.canLoadOlder = true
} else {
loadBatch.canLoadNewer = true
}
}
/// Converts interactions into page segments. When a preprocessing context
/// is provided, this also inserts dynamic items (date headers and unread
/// indicators) and collapse sets.
private static func preprocessInteractions(
_ interactions: [TSInteraction],
preprocessingContext: MessageLoaderPreprocessingContext?,
) -> [LoadedSegment] {
guard let preprocessingContext else {
return interactions.map { interaction in
LoadedSegment(rawInteractions: [interaction], displayableInteractions: [interaction])
}
}
let thread = preprocessingContext.thread
let isGroupThread = thread.isGroupThread
let oldestUnreadSortId = preprocessingContext.oldestUnreadSortId
let todayDate = Date()
var result = [LoadedSegment]()
var pendingDisplayableInteractions = [TSInteraction]()
var currentRun = [TSInteraction]()
var currentRunType: CollapseSetInteraction.MessagesType?
var pastUnreadIndicator = false
var shouldShowDateOnNextViewItem = true
var previousDaysBeforeToday: Int?
func appendItem(_ interaction: TSInteraction) {
result.append(LoadedSegment(
rawInteractions: [interaction],
displayableInteractions: pendingDisplayableInteractions + [interaction],
))
pendingDisplayableInteractions.removeAll()
}
func finalizeSet() {
defer {
currentRun.removeAll()
currentRunType = nil
}
guard !currentRun.isEmpty else {
return
}
guard currentRun.count >= 2, let runType = currentRunType else {
for interaction in currentRun {
appendItem(interaction)
}
return
}
let collapseSetInteraction = CollapseSetInteraction(
thread: thread,
collapsedInteractions: currentRun,
collapseSetType: runType,
)
result.append(LoadedSegment(
rawInteractions: currentRun,
displayableInteractions: pendingDisplayableInteractions + [collapseSetInteraction],
))
pendingDisplayableInteractions.removeAll()
}
for interaction in interactions {
let timestamp = interaction.timestamp
let daysBeforeToday = DateUtil.daysFrom(
firstDate: Date(millisecondsSince1970: timestamp),
toSecondDate: todayDate,
)
if let previousDaysBeforeToday {
if daysBeforeToday != previousDaysBeforeToday {
shouldShowDateOnNextViewItem = true
}
} else {
// Only show for the first item if the date is not today
shouldShowDateOnNextViewItem = daysBeforeToday != 0
}
if
shouldShowDateOnNextViewItem,
canShowDateHeader(before: interaction)
{
// Collapse sets shouldn't cross date boundaries
finalizeSet()
pendingDisplayableInteractions.append(DateHeaderInteraction(thread: thread, timestamp: timestamp))
shouldShowDateOnNextViewItem = false
}
previousDaysBeforeToday = daysBeforeToday
// Only insert one unread indicator and don't collapse unread events
if pastUnreadIndicator {
appendItem(interaction)
continue
}
if let oldestUnreadSortId, oldestUnreadSortId <= interaction.sortId {
finalizeSet()
let unreadIndicatorInteraction = UnreadIndicatorInteraction(
thread: thread,
timestamp: timestamp,
receivedAtTimestamp: interaction.receivedAtTimestamp,
)
pendingDisplayableInteractions.append(unreadIndicatorInteraction)
pastUnreadIndicator = true
appendItem(interaction)
continue
}
guard BuildFlags.collapsingChatEvents else {
appendItem(interaction)
continue
}
let collapseType = collapseSetType(for: interaction, isGroupThread: isGroupThread)
if let collapseType {
let isDifferentSetThanCurrentRun = currentRunType != nil && currentRunType != collapseType
let exceededCurrentRunLimit = currentRun.count >= Constants.maxCollapseSetSize
if isDifferentSetThanCurrentRun || exceededCurrentRunLimit {
finalizeSet()
}
currentRun.append(interaction)
currentRunType = collapseType
} else {
finalizeSet()
appendItem(interaction)
}
}
finalizeSet()
return result
}
private static func canShowDateHeader(before interaction: TSInteraction) -> Bool {
switch interaction.interactionType {
case .unknown, .typingIndicator, .threadDetails, .dateHeader, .unknownThreadWarning, .defaultDisappearingMessageTimer, .collapseSet:
return false
case .info:
guard let infoMessage = interaction as? TSInfoMessage else {
owsFailDebug("Invalid interaction.")
return false
}
// Only show the date for non-synced thread messages;
return infoMessage.messageType != .syncedThread
case .unreadIndicator, .incomingMessage, .outgoingMessage, .error, .call:
return true
}
}
private static func collapseSetType(
for interaction: TSInteraction,
isGroupThread: Bool,
) -> CollapseSetInteraction.MessagesType? {
switch interaction.interactionType {
case .info:
guard let infoMessage = interaction as? TSInfoMessage else {
owsFailDebug("info interaction is not TSInfoMessage")
return nil
}
switch infoMessage.messageType {
case .typeDisappearingMessagesUpdate:
return .timerChanges
case .typeGroupUpdate:
if
let wrapper = infoMessage.infoMessageUserInfo?[.groupUpdateItems]
as? TSInfoMessage.PersistableGroupUpdateItemsWrapper
{
for event in wrapper.updateItems {
switch event {
case
.groupTerminatedByLocalUser,
.groupTerminatedByOtherUser,
.groupTerminatedByUnknownUser:
return nil
case
.disappearingMessagesEnabledByLocalUser,
.disappearingMessagesEnabledByOtherUser,
.disappearingMessagesEnabledByUnknownUser,
.disappearingMessagesDisabledByLocalUser,
.disappearingMessagesDisabledByOtherUser,
.disappearingMessagesDisabledByUnknownUser:
return .timerChanges
default:
break
}
}
}
return isGroupThread ? .groupUpdates : .chatUpdates
case .verificationStateChange,
.profileUpdate,
.phoneNumberChange,
.typeEndPoll,
.typePinnedMessage:
return isGroupThread ? .groupUpdates : .chatUpdates
default:
return nil
}
case .error:
guard let errorMessage = interaction as? TSErrorMessage else {
owsFailDebug("error interaction is not TSErrorMessage")
return nil
}
if errorMessage.errorType == .nonBlockingIdentityChange {
return isGroupThread ? .groupUpdates : .chatUpdates
}
return nil
case .call:
// Don't collapse an active group call.
if
let groupCallMessage = interaction as? OWSGroupCallMessage,
!groupCallMessage.hasEnded
{
return nil
}
return .callEvents
default:
return nil
}
}
}
// MARK: -
@ -891,6 +447,8 @@ struct MessageLoaderBatch {
}
uniqueIds = otherUniqueIds.dropLast(overlappingCount) + uniqueIds
mergeCanLoad(otherLoadBatch)
// Make sure we keep all of `self`, so trim entries we just added if needed.
trimOlder()
case (let firstIndex?, nil):
let overlappingCount = uniqueIds.endIndex - firstIndex
guard uniqueIds.suffix(overlappingCount) == otherUniqueIds.prefix(overlappingCount) else {
@ -900,6 +458,8 @@ struct MessageLoaderBatch {
}
uniqueIds += otherUniqueIds.dropFirst(overlappingCount)
mergeCanLoad(otherLoadBatch)
// Make sure we keep all of `self`, so trim entries we just added if needed.
trimNewer()
case (let firstIndex?, let lastIndex?):
guard uniqueIds[firstIndex...lastIndex] == otherUniqueIds[...] else {
// If this breaks, it probably means `deletedInteractionIds` is broken (or
@ -934,4 +494,24 @@ struct MessageLoaderBatch {
canLoadNewer = false
}
}
mutating func trimOlder() {
guard uniqueIds.count > Constants.maxInteractionCount else {
return
}
uniqueIds = Array(uniqueIds.suffix(Constants.maxInteractionCount))
// We trimmed from the beginning. If the oldest had been marked as loaded,
// it's no longer loaded.
canLoadOlder = true
}
mutating func trimNewer() {
guard uniqueIds.count > Constants.maxInteractionCount else {
return
}
uniqueIds = Array(uniqueIds.prefix(Constants.maxInteractionCount))
// We trimmed from the end. If the newest had already been marked as
// loaded, it's no longer loaded.
canLoadNewer = true
}
}

View File

@ -113,7 +113,7 @@ extension EmojiReactionPickerConfigViewController: MessageReactionPickerDelegate
present(picker, animated: true)
}
func didSelectShowFullEmojiPicker() {
func didSelectAnyEmoji() {
// No-op for configuration
}
}

View File

@ -1,7 +1,7 @@
{
"images" : [
{
"filename" : "official-wallpaper.pdf",
"filename" : "official_wallpaper_reduced.pdf",
"idiom" : "universal"
}
],

View File

@ -3,666 +3,369 @@
// SPDX-License-Identifier: AGPL-3.0-only
//
import Contacts
import SignalServiceKit
import SignalUI
class ExperienceUpgradeManager {
private enum StoreKeys {
static let lastMegaphoneDismissDate = "lastExperienceUpgradeDismissDate"
}
private weak static var lastPresented: ExperienceUpgradeView?
private static var lastPresentedMegaphone: Megaphone?
private static var lastPresentedMegaphoneView: MegaphoneView?
static func presentNext(fromViewController: UIViewController) -> Bool {
let db = DependenciesBridge.shared.db
let tsAccountManager = DependenciesBridge.shared.tsAccountManager
private static var accountKeyStore: AccountKeyStore { DependenciesBridge.shared.accountKeyStore }
private static let backupSettingsStore = BackupSettingsStore()
private static let dateProvider: DateProvider = { Date() }
private static var db: DB { DependenciesBridge.shared.db }
private static var deviceStore: OWSDeviceStore { DependenciesBridge.shared.deviceStore }
private static var donationReceiptCredentialResultStore: DonationReceiptCredentialResultStore { DependenciesBridge.shared.donationReceiptCredentialResultStore }
private static let experienceUpgradeStore = ExperienceUpgradeStore()
private static var inactiveLinkedDeviceFinder: InactiveLinkedDeviceFinder { DependenciesBridge.shared.inactiveLinkedDeviceFinder }
private static var inactivePrimaryDeviceStore: InactivePrimaryDeviceStore { DependenciesBridge.shared.inactivePrimaryDeviceStore }
private static let keyValueStore = NewKeyValueStore(collection: "ExperienceUpgradeManager")
private static var localUsernameManager: LocalUsernameManager { DependenciesBridge.shared.localUsernameManager }
private static var networkManager: NetworkManager { SSKEnvironment.shared.networkManagerRef }
private static var ows2FAManager: OWS2FAManager { SSKEnvironment.shared.ows2FAManagerRef }
private static var profileManager: ProfileManager { SSKEnvironment.shared.profileManagerRef }
private static var reachabilityManager: SSKReachabilityManager { SSKEnvironment.shared.reachabilityManagerRef }
private static var remoteConfigManager: RemoteConfigManager { SSKEnvironment.shared.remoteConfigManagerRef }
private static var storageServiceManager: StorageServiceManager { SSKEnvironment.shared.storageServiceManagerRef }
private static var usernameEducationManager: UsernameEducationManager { DependenciesBridge.shared.usernameEducationManager }
private static var tsAccountManager: TSAccountManager { DependenciesBridge.shared.tsAccountManager }
private static var usernameSelectionCoordinator: UsernameSelectionCoordinator {
UsernameSelectionCoordinator(
currentUsername: nil,
context: UsernameSelectionCoordinator.Context(
databaseStorage: db,
networkManager: networkManager,
storageServiceManager: storageServiceManager,
usernameEducationManager: usernameEducationManager,
localUsernameManager: localUsernameManager,
),
)
}
static func reconcilePresentedExperienceUpgrade(fromViewController: UIViewController) {
let now = Date()
var shouldClearNewDeviceNotification = false
var shouldClearBackupsEnabledDetails = false
let lastMegaphoneDismissDate: Date
let nextMegaphone: Megaphone?
(
lastMegaphoneDismissDate,
nextMegaphone,
) = db.read { tx in
let optionalNext = db.read { transaction -> ExperienceUpgrade? in
let tx = transaction
guard
let registeredState = try? tsAccountManager.registeredState(tx: tx),
let registrationDate = tsAccountManager.registrationDate(tx: tx)
else {
return (.distantPast, nil)
return nil
}
let lastMegaphoneDismissDate = keyValueStore.fetchValue(
Date.self,
forKey: StoreKeys.lastMegaphoneDismissDate,
tx: tx,
) ?? .distantPast
let now = Date()
let timeIntervalSinceRegistration = now.timeIntervalSince(registrationDate)
var nextMegaphone: Megaphone?
for upgrade in allKnownExperienceUpgrades(tx: tx) {
if nextMegaphone != nil {
break
}
guard
!upgrade.isComplete,
!upgrade.isSnoozed(now: now),
!upgrade.hasPassedNumberOfDaysToShow(now: now),
now.timeIntervalSince(registrationDate) > upgrade.manifest.delayAfterRegistration,
now < upgrade.manifest.expirationDate,
(registeredState.isPrimary || upgrade.manifest.showOnLinkedDevices)
else {
continue
}
switch upgrade.manifest {
case .introducingPins:
if checkPreconditionsForIntroducingPins(tx: tx) {
nextMegaphone = IntroducingPinsMegaphone(
experienceUpgrade: upgrade,
fromViewController: fromViewController,
)
return ExperienceUpgradeFinder.allKnownExperienceUpgrades(transaction: tx)
.first { upgrade in
guard
!upgrade.isComplete,
!upgrade.isSnoozed(now: now),
!upgrade.hasPassedNumberOfDaysToShow(now: now),
timeIntervalSinceRegistration > upgrade.manifest.delayAfterRegistration,
now < upgrade.manifest.expirationDate,
(registeredState.isPrimary || upgrade.manifest.showOnLinkedDevices)
else {
return false
}
case .notificationPermissionReminder:
if checkPreconditionsForNotificationsPermissionsReminder() {
nextMegaphone = NotificationPermissionReminderMegaphone(
experienceUpgrade: upgrade,
fromViewController: fromViewController,
)
}
case .newLinkedDeviceNotification:
switch checkPreconditionsForNewLinkedDeviceNotification(tx: tx) {
case .display(let mostRecentlyLinkedDeviceDetails):
nextMegaphone = NewLinkedDeviceNotificationMegaphone(
db: db,
deviceStore: deviceStore,
experienceUpgrade: upgrade,
mostRecentlyLinkedDeviceDetails: mostRecentlyLinkedDeviceDetails,
)
case .skip:
switch upgrade.manifest {
case .introducingPins:
return ExperienceUpgradeManifest
.checkPreconditionsForIntroducingPins(transaction: transaction)
case .notificationPermissionReminder:
return ExperienceUpgradeManifest
.checkPreconditionsForNotificationsPermissionsReminder()
case .newLinkedDeviceNotification:
let result = ExperienceUpgradeManifest
.checkPreconditionsForNewLinkedDeviceNotification(tx: transaction)
switch result {
case .display:
return true
case .skip:
return false
case .clearNotification:
shouldClearNewDeviceNotification = true
return false
}
case .createUsernameReminder:
return ExperienceUpgradeManifest
.checkPreconditionsForCreateUsernameReminder(transaction: transaction)
case .remoteMegaphone(let megaphone):
return ExperienceUpgradeManifest
.checkPreconditionsForRemoteMegaphone(megaphone, tx: transaction)
case .inactiveLinkedDeviceReminder:
return ExperienceUpgradeManifest
.checkPreconditionsForInactiveLinkedDeviceReminder(tx: transaction)
case .inactivePrimaryDeviceReminder:
return ExperienceUpgradeManifest
.checkPreconditionsForInactivePrimaryDeviceReminder(tx: transaction)
case .pinReminder:
return ExperienceUpgradeManifest
.checkPreconditionsForPinReminder(transaction: transaction)
case .contactPermissionReminder:
return ExperienceUpgradeManifest
.checkPreconditionsForContactsPermissionReminder()
case .backupKeyReminder:
return ExperienceUpgradeManifest
.checkPreconditionsForRecoveryKeyReminder(
backupSettingsStore: BackupSettingsStore(),
tsAccountManager: DependenciesBridge.shared.tsAccountManager,
transaction: transaction,
)
case .enableBackupsReminder:
return ExperienceUpgradeManifest
.checkPreconditionsForBackupEnablementReminder(
backupSettingsStore: BackupSettingsStore(),
remoteConfigProvider: SSKEnvironment.shared.remoteConfigManagerRef,
tsAccountManager: DependenciesBridge.shared.tsAccountManager,
transaction: transaction,
)
case .haveEnabledBackupsNotification:
let result = ExperienceUpgradeManifest
.checkPreconditionsForEnabledBackupsNotification(tx: tx)
switch result {
case .display:
return true
case .skip:
return false
case .clearStoredDetails:
shouldClearBackupsEnabledDetails = true
}
case .unrecognized:
break
case .clearNotification:
shouldClearNewDeviceNotification = true
}
case .createUsernameReminder:
if checkPreconditionsForCreateUsernameReminder(tx: tx) {
nextMegaphone = CreateUsernameMegaphone(
usernameSelectionCoordinator: usernameSelectionCoordinator,
experienceUpgrade: upgrade,
fromViewController: fromViewController,
)
}
case .remoteMegaphone(let remoteMegaphoneModel):
if
checkPreconditionsForRemoteMegaphone(
remoteMegaphoneModel: remoteMegaphoneModel,
now: now,
tx: tx,
)
{
nextMegaphone = RemoteMegaphone(
experienceUpgrade: upgrade,
remoteMegaphoneModel: remoteMegaphoneModel,
fromViewController: fromViewController,
)
}
case .inactiveLinkedDeviceReminder:
if let inactiveLinkedDevice = checkPreconditionsForInactiveLinkedDeviceReminder(tx: tx) {
nextMegaphone = InactiveLinkedDeviceReminderMegaphone(
inactiveLinkedDevice: inactiveLinkedDevice,
fromViewController: fromViewController,
experienceUpgrade: upgrade,
)
}
case .inactivePrimaryDeviceReminder:
if checkPreconditionsForInactivePrimaryDeviceReminder(tx: tx) {
nextMegaphone = InactivePrimaryDeviceReminderMegaphone(
fromViewController: fromViewController,
experienceUpgrade: upgrade,
)
}
case .pinReminder:
if checkPreconditionsForPinReminder(tx: tx) {
nextMegaphone = PinReminderMegaphone(
experienceUpgrade: upgrade,
fromViewController: fromViewController,
)
}
case .contactPermissionReminder:
if checkPreconditionsForContactsPermissionReminder() {
nextMegaphone = ContactPermissionReminderMegaphone(
experienceUpgrade: upgrade,
fromViewController: fromViewController,
)
}
case .backupKeyReminder:
if checkPreconditionsForRecoveryKeyReminder(tx: tx) {
nextMegaphone = RecoveryKeyReminderMegaphone(
experienceUpgrade: upgrade,
fromViewController: fromViewController,
)
}
case .enableBackupsReminder:
if checkPreconditionsForBackupEnablementReminder(tx: tx) {
nextMegaphone = BackupEnablementMegaphone(
experienceUpgrade: upgrade,
fromViewController: fromViewController,
)
}
case .haveEnabledBackupsNotification:
switch checkPreconditionsForEnabledBackupsNotification(
now: now,
tx: tx,
) {
case .display(let lastBackupEnabledDetails):
nextMegaphone = BackupsEnabledNotificationMegaphone(
experienceUpgrade: upgrade,
fromViewController: fromViewController,
backupsEnabledTime: lastBackupEnabledDetails.enabledTime,
db: db,
backupSettingsStore: backupSettingsStore,
)
case .skip:
break
case .clearStoredDetails:
shouldClearBackupsEnabledDetails = true
}
case .unrecognized:
break
}
}
return (
lastMegaphoneDismissDate,
nextMegaphone,
)
return false
}
}
if shouldClearNewDeviceNotification {
db.write { tx in
deviceStore.clearMostRecentlyLinkedDeviceDetails(tx: tx)
DependenciesBridge.shared.db.write { tx in
DependenciesBridge.shared.deviceStore.clearMostRecentlyLinkedDeviceDetails(tx: tx)
}
}
if shouldClearBackupsEnabledDetails {
db.write { tx in
backupSettingsStore.clearLastBackupEnabledDetails(tx: tx)
DependenciesBridge.shared.db.write { tx in
BackupSettingsStore().clearLastBackupEnabledDetails(tx: tx)
}
}
guard let nextMegaphone else {
_ = dismissLastPresented(now: now)
return
}
if
let lastPresentedMegaphone,
lastPresentedMegaphone.experienceUpgrade.manifest == nextMegaphone.experienceUpgrade.manifest
{
return
}
// If we're dismissing a megaphone, don't immediately present another.
if dismissLastPresented(now: now) {
return
} else if
now.timeIntervalSince(lastMegaphoneDismissDate) > .day
{
let megaphoneView = nextMegaphone.buildView()
megaphoneView.present(fromViewController: fromViewController)
lastPresentedMegaphone = nextMegaphone
lastPresentedMegaphoneView = megaphoneView
db.write { tx in
experienceUpgradeStore.markAsViewed(
experienceUpgrade: nextMegaphone.experienceUpgrade,
tx: tx,
)
}
}
}
/// Returns an array of all recognized ``ExperienceUpgrade``s. Contains the
/// persisted record if one exists and is applicable, and an in-memory
/// model otherwise.
private static func allKnownExperienceUpgrades(
tx: DBReadTransaction,
) -> [ExperienceUpgrade] {
var experienceUpgrades = [ExperienceUpgrade]()
var localManifestsWithoutRecords = ExperienceUpgradeManifest.wellKnownLocalUpgradeManifests
// Load any experience upgrades with persisted records...
experienceUpgradeStore.enumerateExperienceUpgrades(tx: tx) { experienceUpgrade in
if case .unrecognized = experienceUpgrade.manifest {
// Ignore any no-longer-recognized records.
return
}
guard experienceUpgrade.manifest.shouldSave else {
// Ignore saved records that we no longer persist.
return
}
experienceUpgrades.append(experienceUpgrade)
localManifestsWithoutRecords.remove(experienceUpgrade.manifest)
}
// ...and instantiate new (in-memory) models for any local manifests
// without persisted records.
for localManifest in localManifestsWithoutRecords {
experienceUpgrades.append(ExperienceUpgrade.makeNew(withManifest: localManifest))
}
return ExperienceUpgradeManifest.sortedByImportance(experienceUpgrades)
}
/// - Returns
/// Whether or not we dismissed a megaphone.
private static func dismissLastPresented(now: Date) -> Bool {
guard lastPresentedMegaphone != nil, let lastPresentedMegaphoneView else {
return false
}
db.write { tx in
keyValueStore.writeValue(
now,
forKey: StoreKeys.lastMegaphoneDismissDate,
tx: tx,
)
}
lastPresentedMegaphoneView.dismiss()
self.lastPresentedMegaphone = nil
self.lastPresentedMegaphoneView = nil
return true
}
// MARK: - Megaphone Preconditions
private static func checkPreconditionsForIntroducingPins(
tx: DBReadTransaction,
) -> Bool {
// The PIN setup flow requires an internet connection and you to not already have a PIN
if
reachabilityManager.isReachable,
tsAccountManager.registrationState(tx: tx).isRegisteredPrimaryDevice,
accountKeyStore.getMasterKey(tx: tx) == nil
{
return true
}
return false
}
private static func checkPreconditionsForNotificationsPermissionsReminder() -> Bool {
let (promise, future) = Promise<Bool>.pending()
DispatchQueue.global(qos: .userInitiated).async {
UNUserNotificationCenter.current().getNotificationSettings { settings in
future.resolve(settings.authorizationStatus == .authorized)
}
}
DispatchQueue.global().asyncAfter(deadline: .now() + 0.05) {
guard promise.result == nil else { return }
future.reject(OWSGenericError("timeout fetching notification permissions"))
}
do {
return !(try promise.wait())
} catch {
Logger.warn("failed to query notification permission")
return false
}
}
private enum NewLinkedDeviceNotificationResult {
case display(MostRecentlyLinkedDeviceDetails)
case skip
case clearNotification
}
private static func checkPreconditionsForNewLinkedDeviceNotification(
tx: DBReadTransaction,
) -> NewLinkedDeviceNotificationResult {
// If we already have presented this experience upgrade, do nothing.
guard
let mostRecentlyLinkedDeviceDetails = deviceStore.mostRecentlyLinkedDeviceDetails(tx: tx)
let next = optionalNext,
lastPresented?.experienceUpgrade.manifest != next.manifest
else {
return .skip
}
// No need to show a megaphone if notifications are on, which we happen
// to already check for the notification permission megaphone.
return if !checkPreconditionsForNotificationsPermissionsReminder() {
.clearNotification
} else if Date() > mostRecentlyLinkedDeviceDetails.shouldRemindUserAfter {
.display(mostRecentlyLinkedDeviceDetails)
} else {
.skip
}
}
private enum BackupsEnabledNotificationResult {
case display(BackupSettingsStore.LastBackupEnabledDetails)
case skip
case clearStoredDetails
}
private static func checkPreconditionsForEnabledBackupsNotification(
now: Date,
tx: DBReadTransaction,
) -> BackupsEnabledNotificationResult {
guard let lastBackupEnabledDetails = backupSettingsStore.lastBackupEnabledDetails(tx: tx) else {
return .skip
}
// Don't show the megaphone if notifications are enabled, we'll send
// a notification instead. Clear the stored details so we don't show
// a stale megaphone in the future.
guard checkPreconditionsForNotificationsPermissionsReminder() else {
return .clearStoredDetails
}
if now > lastBackupEnabledDetails.shouldRemindUserAfter {
return .display(lastBackupEnabledDetails)
} else {
return .skip
}
}
private static func checkPreconditionsForCreateUsernameReminder(
tx: DBReadTransaction,
) -> Bool {
guard
localUsernameManager.usernameState(
tx: tx,
).isExplicitlyUnset
else {
// If we have a username, do not show the reminder.
return false
}
if tsAccountManager.phoneNumberDiscoverability(tx: tx).orDefault.isDiscoverable {
// If phone number discovery is enabled, do not prompt to create a
// username.
return false
}
/// The elapsed interval since the user disabled phone number
/// discovery. Note that we need to invert the sign as this date will
/// be in the past.
let timeIntervalSinceDisabledDiscovery = tsAccountManager
.lastSetIsDiscoverableByPhoneNumber(tx: tx)
.timeIntervalSinceNow * -1
let requiredDelayAfterDisablingDiscovery: TimeInterval = 3 * .day
return timeIntervalSinceDisabledDiscovery > requiredDelayAfterDisablingDiscovery
}
private static func checkPreconditionsForInactiveLinkedDeviceReminder(
tx: DBReadTransaction,
) -> InactiveLinkedDevice? {
return inactiveLinkedDeviceFinder.findLeastActiveLinkedDevice(tx: tx)
}
private static func checkPreconditionsForInactivePrimaryDeviceReminder(
tx: DBReadTransaction,
) -> Bool {
return inactivePrimaryDeviceStore.valueForInactivePrimaryDeviceAlert(transaction: tx)
}
private static func checkPreconditionsForPinReminder(
tx: DBReadTransaction,
) -> Bool {
return ows2FAManager.isDueForV2Reminder(transaction: tx)
}
private static func checkPreconditionsForContactsPermissionReminder() -> Bool {
switch CNContactStore.authorizationStatus(for: .contacts) {
case .authorized, .limited:
return false
case .restricted:
// If this isn't allowed by device policy, don't nag.
return false
case .denied, .notDetermined:
return true
@unknown default:
return false
}
}
private static func checkPreconditionsForRecoveryKeyReminder(
tx: DBReadTransaction,
) -> Bool {
guard tsAccountManager.registrationState(tx: tx).isRegisteredPrimaryDevice else {
return false
}
switch backupSettingsStore.backupPlan(tx: tx) {
case .disabled, .disabling:
return false
case .free, .paid, .paidExpiringSoon, .paidAsTester:
break
}
guard let firstBackupDate = backupSettingsStore.lastBackupDetails(tx: tx)?.firstBackupDate else {
return false
}
let lastReminderDate = backupSettingsStore.lastRecoveryKeyReminderDate(tx: tx)
let fourteenDaysAgo = Date().addingTimeInterval(-14 * .day)
guard let lastReminderDate else {
// Return true if the first backup happened over 2 weeks ago
// and we haven't shown a reminder yet.
return firstBackupDate < fourteenDaysAgo
}
// Return true if there's been no reminder within 6 months.
return lastReminderDate < Date().addingTimeInterval(-6 * .month)
}
private static func checkPreconditionsForBackupEnablementReminder(
tx: DBReadTransaction,
) -> Bool {
guard
remoteConfigManager.currentConfig().backupsMegaphone,
tsAccountManager.registrationState(tx: tx).isRegisteredPrimaryDevice
else {
return false
}
guard !backupSettingsStore.haveBackupsEverBeenEnabled(tx: tx) else {
return false
}
return InteractionFinder.outgoingAndIncomingMessageCount(transaction: tx, limit: 1) >= 1
}
// MARK: Remote megaphone
private static func checkPreconditionsForRemoteMegaphone(
remoteMegaphoneModel: RemoteMegaphoneModel,
now: Date,
tx: DBReadTransaction,
) -> Bool {
let manifest = remoteMegaphoneModel.manifest
let translation = remoteMegaphoneModel.translation
let minimumVersion = AppVersionNumber(manifest.minAppVersion)
let currentVersion = AppVersionNumber(AppVersionImpl.shared.currentAppVersion)
guard currentVersion >= minimumVersion else {
return false
}
guard now.timeIntervalSince1970 > TimeInterval(manifest.dontShowBefore) else {
return false
}
guard let localIdentifiers = tsAccountManager.localIdentifiers(tx: tx) else {
return false
}
guard
RemoteConfig.isCountryCodeBucketEnabled(
csvString: manifest.countries,
key: manifest.id,
localIdentifiers: localIdentifiers,
)
else {
return false
}
guard
validateRemoteMegaphone(
conditionalCheck: manifest.conditionalCheck,
tx: tx,
)
else {
return false
}
guard
validateRemoteMegaphone(
action: manifest.primaryAction,
withText: translation.primaryActionText,
)
else {
return false
}
guard
validateRemoteMegaphone(
action: manifest.secondaryAction,
withText: translation.secondaryActionText,
)
else {
return false
}
return true
}
private static func validateRemoteMegaphone(
conditionalCheck: RemoteMegaphoneModel.Manifest.ConditionalCheck?,
tx: DBReadTransaction,
) -> Bool {
guard let conditionalCheck else {
// Having no conditional check is valid.
return true
}
switch conditionalCheck {
case .standardDonate:
if profileManager.localUserProfile(tx: tx)?.hasBadge == true {
// Fail the check if we currently have a badge.
if optionalNext == nil {
dismissLastPresented()
return false
} else if
donationReceiptCredentialResultStore
.hasAnyPaymentsStillProcessing(tx: tx)
{
// Fail the check if we have any in-progress payments.
return false
}
return true
case .internalUser:
// Show this megaphone to all internal users, even if they already
// have a badge.
return DebugFlags.internalMegaphoneEligible
case .unrecognized(let conditionalId):
Logger.warn("Found unrecognized conditional check with ID \(conditionalId), bailing.")
return false
}
}
private static func validateRemoteMegaphone(
action: RemoteMegaphoneModel.Manifest.Action?,
withText text: String?,
) -> Bool {
guard let action else {
// Having no action is valid...
return true
}
guard action.isRecognized else {
// ...but we need to recognize it...
Logger.warn("Found unrecognized action with ID \(action.actionId), bailing.")
return false
}
guard text != nil else {
// ...and have text for it.
Logger.warn("Missing action text for action \(action.actionId)")
return false
}
return true
}
}
// MARK: -
private extension RemoteMegaphoneModel.Manifest.Action {
var isRecognized: Bool {
if case .unrecognized = self {
return false
}
return true
}
}
// MARK: -
private extension DonationReceiptCredentialResultStore {
/// Do we have any payments that have been initiated, but are still
/// in-progress?
func hasAnyPaymentsStillProcessing(tx: DBReadTransaction) -> Bool {
for requestErrorMode in Mode.allCases {
if
let requestError = getRequestError(errorMode: requestErrorMode, tx: tx),
case .paymentStillProcessing = requestError.errorCode
{
} else {
return true
}
}
return false
// Otherwise, dismiss any currently present experience upgrade. It's
// no longer next and may have been completed.
dismissLastPresented()
let didPresentView: Bool
if
let megaphone = self.megaphone(
forExperienceUpgrade: next,
fromViewController: fromViewController,
)
{
megaphone.present(fromViewController: fromViewController)
lastPresented = megaphone
didPresentView = true
} else {
didPresentView = false
}
db.write { tx in
ExperienceUpgradeFinder.markAsViewed(experienceUpgrade: next, transaction: tx)
}
return didPresentView
}
// MARK: - Experience Specific Helpers
static func dismissPINReminderIfNecessary() {
dismissLastPresented(ifMatching: .pinReminder)
}
/// Marks the given upgrade as complete, and dismisses it if currently presented.
static func clearExperienceUpgrade(_ manifest: ExperienceUpgradeManifest, transaction: DBWriteTransaction) {
ExperienceUpgradeFinder.markAsComplete(experienceUpgradeManifest: manifest, transaction: transaction)
transaction.addSyncCompletion {
Task { @MainActor in
dismissLastPresented(ifMatching: manifest)
}
}
}
private static func dismissLastPresented(ifMatching manifest: ExperienceUpgradeManifest? = nil) {
guard let lastPresented else {
return
}
if
let manifest,
lastPresented.experienceUpgrade.manifest != manifest
{
return
}
lastPresented.dismiss(animated: false, completion: nil)
self.lastPresented = nil
}
// MARK: - Megaphone
private static func hasMegaphone(forExperienceUpgrade experienceUpgrade: ExperienceUpgrade) -> Bool {
switch experienceUpgrade.manifest {
case
.introducingPins,
.pinReminder,
.notificationPermissionReminder,
.newLinkedDeviceNotification,
.createUsernameReminder,
.inactiveLinkedDeviceReminder,
.inactivePrimaryDeviceReminder,
.contactPermissionReminder,
.backupKeyReminder,
.enableBackupsReminder,
.haveEnabledBackupsNotification:
return true
case .remoteMegaphone:
// Remote megaphones are always presentable. We filter out any with
// unpresentable fields (e.g., unrecognized actions) before we get
// out of the `ExperienceUpgradeFinder`.
return true
case .unrecognized:
return false
}
}
private static func megaphone(forExperienceUpgrade experienceUpgrade: ExperienceUpgrade, fromViewController: UIViewController) -> MegaphoneView? {
let db = DependenciesBridge.shared.db
let deviceStore = DependenciesBridge.shared.deviceStore
let localUsernameManager = DependenciesBridge.shared.localUsernameManager
let inactiveLinkedDeviceFinder = DependenciesBridge.shared.inactiveLinkedDeviceFinder
switch experienceUpgrade.manifest {
case .introducingPins:
return IntroducingPinsMegaphone(experienceUpgrade: experienceUpgrade, fromViewController: fromViewController)
case .pinReminder:
return PinReminderMegaphone(experienceUpgrade: experienceUpgrade, fromViewController: fromViewController)
case .notificationPermissionReminder:
return NotificationPermissionReminderMegaphone(experienceUpgrade: experienceUpgrade, fromViewController: fromViewController)
case .newLinkedDeviceNotification:
let mostRecentlyLinkedDeviceDetails = db.read { tx in
deviceStore.mostRecentlyLinkedDeviceDetails(tx: tx)
}
guard let mostRecentlyLinkedDeviceDetails else {
owsFailDebug("Missing mostRecentlyLinkedDeviceDetails")
return nil
}
return NewLinkedDeviceNotificationMegaphone(
db: DependenciesBridge.shared.db,
deviceStore: DependenciesBridge.shared.deviceStore,
experienceUpgrade: experienceUpgrade,
mostRecentlyLinkedDeviceDetails: mostRecentlyLinkedDeviceDetails,
)
case .createUsernameReminder:
let usernameIsUnset: Bool = db.read { tx in
return localUsernameManager.usernameState(tx: tx).isExplicitlyUnset
}
guard usernameIsUnset else {
owsFailDebug("Should never try and show this megaphone if a username is set!")
return nil
}
return CreateUsernameMegaphone(
usernameSelectionCoordinator: .init(
currentUsername: nil,
context: .init(
databaseStorage: SSKEnvironment.shared.databaseStorageRef,
networkManager: SSKEnvironment.shared.networkManagerRef,
storageServiceManager: SSKEnvironment.shared.storageServiceManagerRef,
usernameEducationManager: DependenciesBridge.shared.usernameEducationManager,
localUsernameManager: DependenciesBridge.shared.localUsernameManager,
),
),
experienceUpgrade: experienceUpgrade,
fromViewController: fromViewController,
)
case .inactiveLinkedDeviceReminder:
let inactiveLinkedDevice: InactiveLinkedDevice? = db.read { tx in
return inactiveLinkedDeviceFinder.findLeastActiveLinkedDevice(tx: tx)
}
guard let inactiveLinkedDevice else {
owsFailDebug("Trying to show inactive linked device megaphone, but have no device!")
return nil
}
return InactiveLinkedDeviceReminderMegaphone(
inactiveLinkedDevice: inactiveLinkedDevice,
fromViewController: fromViewController,
experienceUpgrade: experienceUpgrade,
)
case .inactivePrimaryDeviceReminder:
let isPrimaryDevice = db.read { tx in
// If isPrimaryDevice is nil, it means we aren't registered yet, and shouldn't show the megaphone.
return DependenciesBridge.shared.tsAccountManager.registrationState(tx: tx).isPrimaryDevice ?? true
}
guard !isPrimaryDevice else {
owsFailDebug("Trying to show inactive primary device megaphone, but this is the primary device or an unregistered device")
return nil
}
return InactivePrimaryDeviceReminderMegaphone(fromViewController: fromViewController, experienceUpgrade: experienceUpgrade)
case .contactPermissionReminder:
return ContactPermissionReminderMegaphone(experienceUpgrade: experienceUpgrade, fromViewController: fromViewController)
case .remoteMegaphone(let megaphone):
return RemoteMegaphone(
experienceUpgrade: experienceUpgrade,
remoteMegaphoneModel: megaphone,
fromViewController: fromViewController,
)
case .backupKeyReminder:
return RecoveryKeyReminderMegaphone(experienceUpgrade: experienceUpgrade, fromViewController: fromViewController)
case .enableBackupsReminder:
return BackupEnablementMegaphone(
experienceUpgrade: experienceUpgrade,
fromViewController: fromViewController,
)
case .haveEnabledBackupsNotification:
let lastBackupsEnabledDetails = db.read { tx in
BackupSettingsStore().lastBackupEnabledDetails(tx: tx)
}
guard let lastBackupsEnabledDetails else {
owsFailDebug("Missing lastBackupsEnabledDetails")
return nil
}
return BackupsEnabledNotificationMegaphone(
experienceUpgrade: experienceUpgrade,
fromViewController: fromViewController,
backupsEnabledTime: lastBackupsEnabledDetails.enabledTime,
db: db,
)
case .unrecognized:
return nil
}
}
}
// MARK: - ExperienceUpgradeView
protocol ExperienceUpgradeView: AnyObject {
var experienceUpgrade: ExperienceUpgrade { get }
var isPresented: Bool { get }
func dismiss(animated: Bool, completion: (() -> Void)?)
}
extension ExperienceUpgradeView {
func markAsSnoozedWithSneakyTransaction() {
SSKEnvironment.shared.databaseStorageRef.write { transaction in
ExperienceUpgradeFinder.markAsSnoozed(
experienceUpgrade: self.experienceUpgrade,
transaction: transaction,
)
}
}
func markAsCompleteWithSneakyTransaction() {
SSKEnvironment.shared.databaseStorageRef.write { transaction in
ExperienceUpgradeFinder.markAsComplete(
experienceUpgrade: self.experienceUpgrade,
transaction: transaction,
)
}
}
}

View File

@ -8,36 +8,20 @@ public import SignalServiceKit
/// Handles fetching and parsing remote megaphones.
public class RemoteMegaphoneFetcher: RemoteReleaseNotesFetcher<RemoteMegaphoneModel.Manifest, RemoteMegaphoneModel.Translation> {
private let experienceUpgradeStore: ExperienceUpgradeStore
override init(
db: DB,
remoteReleaseNotesService: any RemoteReleaseNotesServiceProtocol,
) {
self.experienceUpgradeStore = ExperienceUpgradeStore()
super.init(
db: db,
remoteReleaseNotesService: remoteReleaseNotesService,
)
}
/// Update our local persisted megaphone state with freshly-fetched
/// megaphones from the service. Updates existing megaphones if present,
/// and creates new ones if necessary. Removes any locally-persisted
/// megaphones that no longer exist on the service.
override func updatePersistedData(
withFetchedData fetchedTranslations: [(RemoteMegaphoneModel.Manifest, RemoteMegaphoneModel.Translation)],
transaction tx: DBWriteTransaction,
transaction: DBWriteTransaction,
) {
// Get any persisted ExperienceUpgrades for the remote megaphones.
var experienceUpgradesByMegaphoneId: [String: ExperienceUpgrade] = [:]
experienceUpgradeStore.enumerateExperienceUpgrades(tx: tx) { experienceUpgrade in
guard case .remoteMegaphone(let model) = experienceUpgrade.manifest else {
return
// Get the current remote megaphones.
var localRemoteMegaphones: [String: ExperienceUpgrade] = [:]
ExperienceUpgrade.anyEnumerate(transaction: transaction) { upgrade, _ in
if case .remoteMegaphone = upgrade.manifest {
localRemoteMegaphones[upgrade.uniqueId] = upgrade
}
experienceUpgradesByMegaphoneId[model.manifest.id] = experienceUpgrade
}
// Insert all megaphones we got from the service. If we already have a
@ -46,28 +30,23 @@ public class RemoteMegaphoneFetcher: RemoteReleaseNotesFetcher<RemoteMegaphoneMo
// For example, if the user's locale has changed we may have updated
// translations.
for (manifest, translation) in fetchedTranslations {
let remoteMegaphoneModel = RemoteMegaphoneModel(manifest: manifest, translation: translation)
let experienceUpgrade: ExperienceUpgrade
if let persisted = experienceUpgradesByMegaphoneId.removeValue(forKey: manifest.id) {
experienceUpgrade = persisted
} else {
experienceUpgrade = .makeNew(withManifest: .remoteMegaphone(megaphone: remoteMegaphoneModel))
}
let serviceMegaphone = RemoteMegaphoneModel(manifest: manifest, translation: translation)
if let existingLocalMegaphone = localRemoteMegaphones[serviceMegaphone.id] {
existingLocalMegaphone.updateManifestRemoteMegaphone(withRefetchedMegaphone: serviceMegaphone)
existingLocalMegaphone.anyUpsert(transaction: transaction)
experienceUpgradeStore.upsertRemoteMegaphone(
experienceUpgrade: experienceUpgrade,
newRemoteMegaphoneModel: remoteMegaphoneModel,
tx: tx,
)
localRemoteMegaphones.removeValue(forKey: serviceMegaphone.id)
} else {
ExperienceUpgrade
.makeNew(withManifest: .remoteMegaphone(megaphone: serviceMegaphone))
.anyInsert(transaction: transaction)
}
}
// Remove records for any remaining local megaphones, which are no
// longer on the service.
for (_, experienceUpgradeToRemove) in experienceUpgradesByMegaphoneId {
experienceUpgradeStore.remove(
experienceUpgrade: experienceUpgradeToRemove,
tx: tx,
)
for (_, experienceUpgradeToRemove) in localRemoteMegaphones {
experienceUpgradeToRemove.anyRemove(transaction: transaction)
}
}

View File

@ -7,7 +7,7 @@ import Foundation
import SignalServiceKit
import UIKit
class BackupEnablementMegaphone: Megaphone {
class BackupEnablementMegaphone: MegaphoneView {
init(
experienceUpgrade: ExperienceUpgrade,
fromViewController: UIViewController,
@ -22,7 +22,7 @@ class BackupEnablementMegaphone: Megaphone {
"BACKUP_ENABLEMENT_REMINDER_MEGAPHONE_BODY",
comment: "Body for Backup enablement reminder megaphone",
)
image = .backupsLogo
imageName = "backups-logo"
let primaryButtonTitle = OWSLocalizedString(
"BACKUP_ENABLEMENT_REMINDER_MEGAPHONE_ACTION",
@ -33,9 +33,10 @@ class BackupEnablementMegaphone: Megaphone {
comment: "Snooze text for Backup enablement reminder megaphone",
)
let primaryButton = Button(title: primaryButtonTitle) { [weak self] in
let primaryButton = MegaphoneView.Button(title: primaryButtonTitle) { [weak self] in
SignalApp.shared.showAppSettings(mode: .backups())
self?.markAsSnoozedWithSneakyTransaction()
self?.dismiss(animated: true)
}
let secondaryButton = snoozeButton(
@ -43,7 +44,7 @@ class BackupEnablementMegaphone: Megaphone {
snoozeTitle: secondaryButtonTitle,
)
buttons = [primaryButton, secondaryButton]
setButtons(primary: primaryButton, secondary: secondaryButton)
}
required init(coder: NSCoder) {

View File

@ -7,7 +7,7 @@ import Foundation
import SignalServiceKit
import UIKit
class BackupsEnabledNotificationMegaphone: Megaphone {
class BackupsEnabledNotificationMegaphone: MegaphoneView {
private let db: DB
private let backupSettingsStore: BackupSettingsStore
init(
@ -34,33 +34,33 @@ class BackupsEnabledNotificationMegaphone: Megaphone {
),
backupsEnabledTime.formatted(date: .omitted, time: .shortened),
)
image = .backupsLogo
imageName = "backups-logo"
let primaryButtonTitle = OWSLocalizedString(
"BACKUPS_VIEW_SETTINGS_BUTTON",
comment: "Action text for backups enabled megaphone taking user to backup settings",
)
let primaryButton = Button(title: primaryButtonTitle) { [weak self] in
let primaryButton = MegaphoneView.Button(title: primaryButtonTitle) { [weak self] in
SignalApp.shared.showAppSettings(mode: .backups())
self?.stopShowing()
self?.markAsViewed()
self?.dismiss(animated: true)
}
let secondaryButton = Button(title: CommonStrings.okButton) { [weak self] in
self?.stopShowing()
let secondaryButton = MegaphoneView.Button(title: CommonStrings.okButton) { [weak self] in
self?.markAsViewed()
self?.dismiss(animated: true)
}
buttons = [primaryButton, secondaryButton]
setButtons(primary: primaryButton, secondary: secondaryButton)
}
required init(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func stopShowing() {
private func markAsViewed() {
db.write { tx in
backupSettingsStore.clearLastBackupEnabledDetails(tx: tx)
}
NotificationCenter.default.post(name: .megaphoneStateDidChange, object: nil)
}
}

View File

@ -6,7 +6,9 @@
import SignalServiceKit
import SignalUI
class ContactPermissionReminderMegaphone: Megaphone {
class ContactPermissionReminderMegaphone: MegaphoneView {
weak var actionSheetController: ActionSheetController?
init(experienceUpgrade: ExperienceUpgrade, fromViewController: UIViewController) {
super.init(experienceUpgrade: experienceUpgrade)
@ -18,16 +20,15 @@ class ContactPermissionReminderMegaphone: Megaphone {
"CONTACT_PERMISSION_REMINDER_MEGAPHONE_BODY",
comment: "Body for contact permission reminder megaphone",
)
image = .contacts
imageName = "contacts"
let primaryButtonTitle = OWSLocalizedString(
"CONTACT_PERMISSION_REMINDER_MEGAPHONE_ACTION",
comment: "Action text for contact permission reminder megaphone",
)
let primaryButton = Button(title: primaryButtonTitle) {
let actionSheetController = ActionSheetController()
actionSheetController.isCancelable = true
let primaryButton = MegaphoneView.Button(title: primaryButtonTitle) { [weak self] in
guard let self else { return }
let turnOnView: TurnOnPermissionView
if #available(iOS 18, *) {
@ -36,7 +37,6 @@ class ContactPermissionReminderMegaphone: Megaphone {
.withRenderingMode(.alwaysTemplate)
turnOnView = TurnOnPermissionView(
fromActionSheetController: actionSheetController,
title: OWSLocalizedString(
"CONTACT_PERMISSION_ACTION_SHEET_2_TITLE",
comment: "Title for contact permission action sheet",
@ -71,7 +71,6 @@ class ContactPermissionReminderMegaphone: Megaphone {
)
} else {
turnOnView = TurnOnPermissionView(
fromActionSheetController: actionSheetController,
title: OWSLocalizedString(
"CONTACT_PERMISSION_ACTION_SHEET_TITLE",
comment: "Title for contact permission action sheet",
@ -99,8 +98,11 @@ class ContactPermissionReminderMegaphone: Megaphone {
)
}
let actionSheetController = ActionSheetController()
actionSheetController.customHeader = turnOnView
actionSheetController.isCancelable = true
fromViewController.presentActionSheet(actionSheetController)
self.actionSheetController = actionSheetController
}
let secondaryButton = snoozeButton(
@ -110,11 +112,15 @@ class ContactPermissionReminderMegaphone: Megaphone {
comment: "Snooze action text for contact permission reminder megaphone",
),
)
buttons = [primaryButton, secondaryButton]
setButtons(primary: primaryButton, secondary: secondaryButton)
}
required init(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func dismiss(animated: Bool = true, completion: (() -> Void)? = nil) {
super.dismiss(animated: animated, completion: completion)
actionSheetController?.dismiss(animated: animated)
}
}

View File

@ -7,7 +7,7 @@ import Foundation
import SignalServiceKit
import UIKit
class CreateUsernameMegaphone: Megaphone {
class CreateUsernameMegaphone: MegaphoneView {
private let usernameSelectionCoordinator: UsernameSelectionCoordinator
init(
@ -29,7 +29,7 @@ class CreateUsernameMegaphone: Megaphone {
comment: "Body text for an interactive in-app prompt to set up a Signal username.",
)
image = .usernames48
imageName = "usernames-48-color"
imageContentMode = .center
let setUpButton = Button(title: CommonStrings.learnMore) { [weak self, weak fromViewController] in
@ -46,7 +46,7 @@ class CreateUsernameMegaphone: Megaphone {
self.onNotNowTapped()
}
buttons = [setUpButton, notNowButton]
setButtons(primary: setUpButton, secondary: notNowButton)
}
@available(*, unavailable, message: "Use other constructor!")
@ -56,10 +56,15 @@ class CreateUsernameMegaphone: Megaphone {
private func onSetUpTapped(fromViewController: UIViewController) {
markAsSnoozedWithSneakyTransaction()
usernameSelectionCoordinator.present(fromViewController: fromViewController)
dismiss(animated: true) {
self.usernameSelectionCoordinator.present(fromViewController: fromViewController)
}
}
private func onNotNowTapped() {
markAsSnoozedWithSneakyTransaction()
dismiss(animated: true)
}
}

View File

@ -6,7 +6,11 @@
import SignalServiceKit
import UIKit
final class InactiveLinkedDeviceReminderMegaphone: Megaphone {
final class InactiveLinkedDeviceReminderMegaphone: MegaphoneView {
private var inactiveLinkedDeviceFinder: InactiveLinkedDeviceFinder {
DependenciesBridge.shared.inactiveLinkedDeviceFinder
}
private let inactiveLinkedDevice: InactiveLinkedDevice
/// The number of days until the linked device represented by this megaphone
@ -46,21 +50,22 @@ final class InactiveLinkedDeviceReminderMegaphone: Megaphone {
inactiveLinkedDevice.displayName,
)
image = .inactiveLinkedDeviceReminderMegaphone
imageName = "inactive-linked-device-reminder-megaphone"
imageContentMode = .center
let dontRemindMeButton = Button(title: OWSLocalizedString(
"INACTIVE_LINKED_DEVICE_REMINDER_MEGAPHONE_DONT_REMIND_ME_BUTTON",
comment: "Title for a button in an in-app megaphone about a user's inactive linked device, indicating the user doesn't want to be reminded.",
)) {
let db = DependenciesBridge.shared.db
let inactiveLinkedDeviceFinder = DependenciesBridge.shared.inactiveLinkedDeviceFinder
db.write { tx in
inactiveLinkedDeviceFinder.permanentlyDisableFinders(tx: tx)
}
NotificationCenter.default.post(name: .megaphoneStateDidChange, object: nil)
DependenciesBridge.shared.db.asyncWrite(
block: { tx in
self.inactiveLinkedDeviceFinder.permanentlyDisableFinders(tx: tx)
},
completionQueue: .main,
completion: { [weak self] in
self?.dismiss()
},
)
}
let gotItButton = snoozeButton(
fromViewController: fromViewController,
@ -69,8 +74,7 @@ final class InactiveLinkedDeviceReminderMegaphone: Megaphone {
comment: "Title for a button in an in-app megaphone about a user's inactive linked device, temporarily dismissing the megaphone.",
),
)
buttons = [gotItButton, dontRemindMeButton]
setButtons(primary: gotItButton, secondary: dontRemindMeButton)
}
@available(*, unavailable, message: "Use other constructor!")

View File

@ -6,7 +6,7 @@
import SafariServices
import SignalServiceKit
final class InactivePrimaryDeviceReminderMegaphone: Megaphone {
final class InactivePrimaryDeviceReminderMegaphone: MegaphoneView {
init(
fromViewController: UIViewController,
experienceUpgrade: ExperienceUpgrade,
@ -23,7 +23,7 @@ final class InactivePrimaryDeviceReminderMegaphone: Megaphone {
comment: "Body for an in-app megaphone about a user's inactive primary device.",
)
image = .phoneWarning
imageName = "phone-warning"
imageContentMode = .center
let viewControllerRef = fromViewController
@ -41,8 +41,7 @@ final class InactivePrimaryDeviceReminderMegaphone: Megaphone {
comment: "Title for a button in an in-app megaphone about a user's inactive primary device, temporarily dismissing the megaphone.",
),
)
buttons = [gotItButton, learnMoreButton]
setButtons(primary: gotItButton, secondary: learnMoreButton)
}
@available(*, unavailable, message: "Use other constructor!")

View File

@ -0,0 +1,52 @@
//
// Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import SafariServices
import SignalServiceKit
import SignalUI
class IntroducingPinsMegaphone: MegaphoneView {
init(experienceUpgrade: ExperienceUpgrade, fromViewController: UIViewController) {
super.init(experienceUpgrade: experienceUpgrade)
titleText = OWSLocalizedString("PINS_MEGAPHONE_TITLE", comment: "Title for PIN megaphone when user doesn't have a PIN")
bodyText = OWSLocalizedString("PINS_MEGAPHONE_BODY", comment: "Body for PIN megaphone when user doesn't have a PIN")
imageName = "PIN_megaphone"
let primaryButtonTitle = OWSLocalizedString("PINS_MEGAPHONE_ACTION", comment: "Action text for PIN megaphone when user doesn't have a PIN")
let primaryButton = MegaphoneView.Button(title: primaryButtonTitle) { [weak self] in
let viewController = PinSetupViewController(
mode: .creating,
showCancelButton: true,
completionHandler: { [weak self, weak fromViewController] _, error in
guard let self, let fromViewController else { return }
if let error {
Logger.error("failed to create pin: \(error)")
} else {
// success
self.markAsCompleteWithSneakyTransaction()
}
self.dismiss(animated: false)
fromViewController.dismiss(animated: true) {
self.presentToast(
text: OWSLocalizedString("PINS_MEGAPHONE_TOAST", comment: "Toast indicating that a PIN has been created."),
fromViewController: fromViewController,
)
}
},
)
fromViewController.present(OWSNavigationController(rootViewController: viewController), animated: true)
}
let secondaryButton = snoozeButton(fromViewController: fromViewController)
setButtons(primary: primaryButton, secondary: secondaryButton)
}
required init(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

View File

@ -1,51 +0,0 @@
//
// Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import SafariServices
import SignalServiceKit
import SignalUI
class IntroducingPinsMegaphone: Megaphone {
init(experienceUpgrade: ExperienceUpgrade, fromViewController: UIViewController) {
super.init(experienceUpgrade: experienceUpgrade)
titleText = OWSLocalizedString("PINS_MEGAPHONE_TITLE", comment: "Title for PIN megaphone when user doesn't have a PIN")
bodyText = OWSLocalizedString("PINS_MEGAPHONE_BODY", comment: "Body for PIN megaphone when user doesn't have a PIN")
image = .pinMegaphone
let primaryButtonTitle = OWSLocalizedString("PINS_MEGAPHONE_ACTION", comment: "Action text for PIN megaphone when user doesn't have a PIN")
let primaryButton = Button(title: primaryButtonTitle) { [weak self] in
let viewController = PinSetupViewController(
mode: .creating,
showCancelButton: true,
onSuccess: { pinSetupViewController in
pinSetupViewController.dismiss(animated: true) { [weak self, weak fromViewController] in
guard let self, let fromViewController else { return }
markAsCompleteWithSneakyTransaction()
fromViewController.presentToast(text: OWSLocalizedString(
"PINS_MEGAPHONE_TOAST",
comment: "Toast indicating that a PIN has been created.",
))
}
},
)
fromViewController.present(OWSNavigationController(rootViewController: viewController), animated: true)
}
let secondaryButton = snoozeButton(
fromViewController: fromViewController,
snoozeTitle: MegaphoneStrings.remindMeLater,
)
buttons = [primaryButton, secondaryButton]
}
required init(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

View File

@ -7,107 +7,91 @@ import Lottie
import SignalServiceKit
import SignalUI
class Megaphone {
class MegaphoneView: UIView, ExperienceUpgradeView {
let experienceUpgrade: ExperienceUpgrade
var imageName: String? {
didSet {
if imageName != nil { image = nil }
}
}
var image: UIImage? {
didSet {
if image != nil { imageName = nil }
}
}
var imageContentMode: UIView.ContentMode = .scaleAspectFit
var animation: Animation?
struct Animation {
let name: String
let backgroundImageName: String?
let backgroundImageInset: CGFloat
let speed: CGFloat
let loopMode: LottieLoopMode
let backgroundBehavior: LottieBackgroundBehavior
let contentMode: UIView.ContentMode
init(
name: String,
backgroundImageName: String? = nil,
backgroundImageInset: CGFloat = 0,
speed: CGFloat = 1,
loopMode: LottieLoopMode = .playOnce,
backgroundBehavior: LottieBackgroundBehavior = .forceFinish,
contentMode: UIView.ContentMode = .scaleAspectFit,
) {
self.name = name
self.speed = speed
self.loopMode = loopMode
self.backgroundBehavior = backgroundBehavior
self.contentMode = contentMode
self.backgroundImageName = backgroundImageName
self.backgroundImageInset = backgroundImageInset
}
}
enum ButtonOrientation {
case horizontal
case vertical
}
var buttonOrientation: ButtonOrientation = .horizontal {
willSet { assert(!hasPresented) }
}
var titleText: String? {
willSet { assert(!hasPresented) }
}
var bodyText: String? {
willSet { assert(!hasPresented) }
}
struct Button {
let title: String
let action: () -> Void
}
let experienceUpgrade: ExperienceUpgrade
var image: UIImage?
var imageContentMode: UIView.ContentMode = .scaleAspectFit
var titleText: String?
var bodyText: String?
var buttons: [Button] = []
private var buttons: [Button] = []
func setButtons(primary: Button, secondary: Button? = nil) {
assert(!hasPresented)
init(experienceUpgrade: ExperienceUpgrade) {
self.experienceUpgrade = experienceUpgrade
}
func buildView() -> MegaphoneView {
guard let titleText, let bodyText else {
owsFail("Megaphone missing title or body text!")
}
guard (1...2).contains(buttons.count) else {
owsFail("Megaphone must have 1 or 2 buttons!")
}
return MegaphoneView(
image: image,
imageContentMode: imageContentMode,
titleText: titleText,
bodyText: bodyText,
buttons: buttons,
)
}
func snoozeButton(
fromViewController: UIViewController,
snoozeTitle: String,
) -> Button {
return Button(title: snoozeTitle) { [weak self, weak fromViewController] in
guard let self, let fromViewController else { return }
markAsSnoozedWithSneakyTransaction()
fromViewController.presentToast(text: MegaphoneStrings.weWillRemindYouLater)
if let secondary {
buttons = [primary, secondary]
} else {
buttons = [primary]
}
}
// MARK: -
func markAsSnoozedWithSneakyTransaction() {
let db = DependenciesBridge.shared.db
let experienceUpgradeStore = ExperienceUpgradeStore()
db.write { tx in
experienceUpgradeStore.markAsSnoozed(
experienceUpgrade: experienceUpgrade,
tx: tx,
)
}
NotificationCenter.default.post(name: .megaphoneStateDidChange, object: nil)
}
func markAsCompleteWithSneakyTransaction() {
let db = DependenciesBridge.shared.db
let experienceUpgradeStore = ExperienceUpgradeStore()
db.write { tx in
experienceUpgradeStore.markAsComplete(
experienceUpgrade: experienceUpgrade,
tx: tx,
)
}
NotificationCenter.default.post(name: .megaphoneStateDidChange, object: nil)
}
}
// MARK: -
class MegaphoneView: UIView {
private let image: UIImage?
private let imageContentMode: UIView.ContentMode
private let titleText: String
private let bodyText: String
private let buttons: [Megaphone.Button]
var isPresented: Bool { superview != nil }
private let darkThemeBackgroundOverlay = UIView()
private let stackView = UIStackView()
init(
image: UIImage?,
imageContentMode: UIView.ContentMode,
titleText: String,
bodyText: String,
buttons: [Megaphone.Button],
) {
self.image = image
self.imageContentMode = imageContentMode
self.titleText = titleText
self.bodyText = bodyText
self.buttons = buttons
init(experienceUpgrade: ExperienceUpgrade) {
self.experienceUpgrade = experienceUpgrade
super.init(frame: .zero)
@ -134,16 +118,23 @@ class MegaphoneView: UIView {
fatalError("init(coder:) has not been implemented")
}
// MARK: -
private var hasPresented = false
func present(fromViewController: UIViewController) {
AssertIsOnMainThread()
guard !hasPresented else { return owsFailDebug("can only present once") }
guard titleText != nil, bodyText != nil else {
return owsFailDebug("megaphone is not prepared for presentation")
}
// Top section
let labelStack = createLabelStack()
let topStackSubviews: [UIView]
if let image {
topStackSubviews = [createImageContainer(image: image), labelStack]
if imageName != nil || image != nil || animation != nil {
topStackSubviews = [createImageContainer(), labelStack]
} else {
topStackSubviews = [labelStack]
}
@ -155,31 +146,51 @@ class MegaphoneView: UIView {
topStackView.layoutMargins = UIEdgeInsets(top: 12, leading: 12, bottom: 12, trailing: 12)
stackView.addArrangedSubview(topStackView)
stackView.addArrangedSubview(createButtonsStack())
// Buttons
if buttons.count > 0 {
stackView.addArrangedSubview(createButtonsStack())
} else {
assert(buttons.isEmpty)
addDismissButton()
}
fromViewController.view.addSubview(self)
autoPinEdge(toSuperviewSafeArea: .leading, withInset: 8)
autoPinEdge(toSuperviewSafeArea: .trailing, withInset: 8)
autoPinEdge(toSuperviewSafeArea: .bottom, withInset: 8)
animationView?.play()
alpha = 0
UIView.animate(withDuration: 0.2) {
self.alpha = 1
}
}
func dismiss() {
removeFromSuperview()
hasPresented = true
}
// MARK: -
@objc
private func applyTheme() {
darkThemeBackgroundOverlay.isHidden = !Theme.isDarkThemeEnabled
}
private func createLabelStack() -> UIStackView {
@objc
private func tappedDismiss() {
dismiss()
}
func dismiss(animated: Bool = true, completion: (() -> Void)? = nil) {
UIView.animate(withDuration: animated ? 0.2 : 0, animations: {
self.alpha = 0
}) { _ in
self.removeFromSuperview()
completion?()
}
}
func createLabelStack() -> UIStackView {
let titleLabel = UILabel()
titleLabel.numberOfLines = 0
titleLabel.lineBreakMode = .byWordWrapping
@ -205,15 +216,47 @@ class MegaphoneView: UIView {
return labelStack
}
private func createImageContainer(image: UIImage) -> UIView {
let container = UIView()
let imageView = UIImageView()
imageView.image = image
imageView.contentMode = self.imageContentMode
container.addSubview(imageView)
imageView.autoPinWidthToSuperview()
imageView.autoPinToSquareAspectRatio()
imageView.autoVCenterInSuperview()
private var animationView: LottieAnimationView?
func createImageContainer() -> UIView {
let container: UIView
if let image = { () -> UIImage? in
if let imageName { return UIImage(named: imageName) }
return image
}() {
container = UIView()
let imageView = UIImageView()
imageView.image = image
imageView.contentMode = self.imageContentMode
container.addSubview(imageView)
imageView.autoPinWidthToSuperview()
imageView.autoPinToSquareAspectRatio()
imageView.autoVCenterInSuperview()
} else if let animation {
container = UIView()
if let backgroundImageName = animation.backgroundImageName {
let backgroundImageView = UIImageView()
backgroundImageView.image = UIImage(named: backgroundImageName)
backgroundImageView.contentMode = .scaleAspectFill
container.addSubview(backgroundImageView)
backgroundImageView.autoPinWidthToSuperview(withMargin: animation.backgroundImageInset)
backgroundImageView.autoVCenterInSuperview()
}
let animationView = LottieAnimationView(name: animation.name)
self.animationView = animationView
animationView.contentMode = animation.contentMode
animationView.animationSpeed = animation.speed
animationView.loopMode = animation.loopMode
animationView.backgroundBehavior = animation.backgroundBehavior
container.addSubview(animationView)
animationView.autoPinEdgesToSuperviewEdges()
} else {
owsFailDebug("unexpectedly missing animation and image")
container = UIView()
}
container.autoSetDimension(.width, toSize: 64)
container.autoSetDimension(.height, toSize: 64, relation: .greaterThanOrEqual)
@ -221,10 +264,7 @@ class MegaphoneView: UIView {
return container
}
private func createButtonView(
_ button: Megaphone.Button,
font: UIFont = .regularFont(ofSize: 15),
) -> OWSFlatButton {
func createButtonView(_ button: Button, font: UIFont = .regularFont(ofSize: 15)) -> OWSFlatButton {
let buttonView = OWSFlatButton()
buttonView.setTitle(title: button.title, font: font, titleColor: Theme.darkThemePrimaryColor)
@ -235,26 +275,30 @@ class MegaphoneView: UIView {
return buttonView
}
private func createButtonsStack() -> UIStackView {
func createButtonsStack() -> UIStackView {
let buttonsStack = UIStackView()
buttonsStack.addBackgroundView(withBackgroundColor: .ows_blackAlpha20)
switch buttons.count {
case 1:
buttonsStack.addArrangedSubview(createButtonView(
buttons[0],
font: .regularFont(ofSize: 15),
))
buttonsStack.addArrangedSubview(createButtonView(buttons[0]))
case 2:
var previousButton: UIView?
for button in buttons {
let buttonView = createButtonView(
button,
font: previousButton == nil ? .semiboldFont(ofSize: 15) : .regularFont(ofSize: 15),
font: previousButton == nil ? UIFont.semiboldFont(ofSize: 15) : .regularFont(ofSize: 15),
)
buttonsStack.insertArrangedSubview(buttonView, at: 0)
switch buttonOrientation {
case .vertical:
buttonsStack.addArrangedSubview(buttonView)
case .horizontal:
buttonsStack.insertArrangedSubview(buttonView, at: 0)
}
previousButton?.autoMatch(.width, to: .width, of: buttonView)
previousButton = buttonView
}
@ -263,14 +307,44 @@ class MegaphoneView: UIView {
divider.backgroundColor = .ows_whiteAlpha20
dividerContainer.addSubview(divider)
buttonsStack.insertArrangedSubview(dividerContainer, at: 1)
buttonsStack.axis = .horizontal
divider.autoSetDimension(.width, toSize: 1)
divider.autoPinWidthToSuperview()
divider.autoPinHeightToSuperview(withMargin: 8)
switch buttonOrientation {
case .vertical:
buttonsStack.axis = .vertical
divider.autoSetDimension(.height, toSize: 1)
divider.autoPinHeightToSuperview()
divider.autoPinWidthToSuperview(withMargin: 12)
case .horizontal:
buttonsStack.axis = .horizontal
divider.autoSetDimension(.width, toSize: 1)
divider.autoPinWidthToSuperview()
divider.autoPinHeightToSuperview(withMargin: 8)
}
default:
owsFail("Megaphones must have one or two buttons!")
owsFailDebug("only supports 1 or 2 buttons")
}
return buttonsStack
}
func addDismissButton() {
let dismissButton = UIButton()
dismissButton.setTemplateImage(Theme.iconImage(.buttonX), tintColor: Theme.darkThemePrimaryColor)
dismissButton.addTarget(self, action: #selector(tappedDismiss), for: .touchUpInside)
addSubview(dismissButton)
dismissButton.autoSetDimensions(to: CGSize(square: 40))
dismissButton.autoPinEdge(toSuperviewEdge: .trailing)
dismissButton.autoPinEdge(toSuperviewEdge: .top)
}
func snoozeButton(fromViewController: UIViewController, snoozeTitle: String = MegaphoneStrings.remindMeLater) -> Button {
return Button(title: snoozeTitle) { [weak self] in
self?.markAsSnoozedWithSneakyTransaction()
self?.dismiss {
self?.presentToast(text: MegaphoneStrings.weWillRemindYouLater, fromViewController: fromViewController)
}
}
}
}

View File

@ -5,7 +5,7 @@
import SignalServiceKit
final class NewLinkedDeviceNotificationMegaphone: Megaphone {
final class NewLinkedDeviceNotificationMegaphone: MegaphoneView {
private let db: DB
private let deviceStore: OWSDeviceStore
@ -19,7 +19,7 @@ final class NewLinkedDeviceNotificationMegaphone: Megaphone {
self.deviceStore = deviceStore
super.init(experienceUpgrade: experienceUpgrade)
image = .inactiveLinkedDeviceReminderMegaphone
imageName = "inactive-linked-device-reminder-megaphone"
imageContentMode = .center
titleText = OWSLocalizedString(
"LINKED_DEVICE_NOTIFICATION_TITLE",
@ -45,16 +45,18 @@ final class NewLinkedDeviceNotificationMegaphone: Megaphone {
),
) { [weak self] in
SignalApp.shared.showAppSettings(mode: .linkedDevices)
self?.stopShowing()
self?.markAsViewed()
self?.dismiss()
}
let acknowledgeButton = Button(
title: CommonStrings.acknowledgeButton,
) { [weak self] in
self?.stopShowing()
self?.markAsViewed()
self?.dismiss()
}
buttons = [acknowledgeButton, viewDeviceButton]
setButtons(primary: acknowledgeButton, secondary: viewDeviceButton)
}
@MainActor
@ -62,11 +64,9 @@ final class NewLinkedDeviceNotificationMegaphone: Megaphone {
fatalError("init(coder:) has not been implemented")
}
private func stopShowing() {
private func markAsViewed() {
db.write { tx in
deviceStore.clearMostRecentlyLinkedDeviceDetails(tx: tx)
}
NotificationCenter.default.post(name: .megaphoneStateDidChange, object: nil)
}
}

View File

@ -6,7 +6,9 @@
import SignalServiceKit
import SignalUI
class NotificationPermissionReminderMegaphone: Megaphone {
class NotificationPermissionReminderMegaphone: MegaphoneView {
weak var actionSheetController: ActionSheetController?
init(experienceUpgrade: ExperienceUpgrade, fromViewController: UIViewController) {
super.init(experienceUpgrade: experienceUpgrade)
@ -18,19 +20,17 @@ class NotificationPermissionReminderMegaphone: Megaphone {
"NOTIFICATION_PERMISSION_REMINDER_MEGAPHONE_BODY",
comment: "Body for notification permission reminder megaphone",
)
image = .notificationMegaphone
imageName = "notificationMegaphone"
let primaryButtonTitle = OWSLocalizedString(
"NOTIFICATION_PERMISSION_REMINDER_MEGAPHONE_ACTION",
comment: "Action text for notification permission reminder megaphone",
)
let primaryButton = Button(title: primaryButtonTitle) {
let actionSheetController = ActionSheetController()
actionSheetController.isCancelable = true
let primaryButton = MegaphoneView.Button(title: primaryButtonTitle) { [weak self] in
guard let self else { return }
let turnOnView = TurnOnPermissionView(
fromActionSheetController: actionSheetController,
title: OWSLocalizedString(
"NOTIFICATION_PERMISSION_ACTION_SHEET_TITLE",
comment: "Title for notification permission action sheet",
@ -64,8 +64,11 @@ class NotificationPermissionReminderMegaphone: Megaphone {
],
)
let actionSheetController = ActionSheetController()
actionSheetController.customHeader = turnOnView
actionSheetController.isCancelable = true
fromViewController.presentActionSheet(actionSheetController)
self.actionSheetController = actionSheetController
}
let secondaryButton = snoozeButton(
@ -75,16 +78,18 @@ class NotificationPermissionReminderMegaphone: Megaphone {
comment: "Snooze action text for contact permission reminder megaphone",
),
)
buttons = [primaryButton, secondaryButton]
setButtons(primary: primaryButton, secondary: secondaryButton)
}
required init(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
// MARK: -
override func dismiss(animated: Bool = true, completion: (() -> Void)? = nil) {
super.dismiss(animated: animated, completion: completion)
actionSheetController?.dismiss(animated: animated)
}
}
class TurnOnPermissionView: UIStackView {
struct Step {
@ -92,12 +97,7 @@ class TurnOnPermissionView: UIStackView {
let text: String
}
init(
fromActionSheetController: ActionSheetController,
title: String,
message: String,
steps: [Step],
) {
init(title: String, message: String, steps: [Step], button: UIButton? = nil) {
super.init(frame: .zero)
axis = .vertical
@ -121,14 +121,10 @@ class TurnOnPermissionView: UIStackView {
}
// Button
let primaryButton = UIButton(
let primaryButton = button ?? UIButton(
configuration: .largePrimary(title: CommonStrings.goToSettingsButton),
primaryAction: UIAction { [weak self, weak fromActionSheetController] _ in
guard let self, let fromActionSheetController else { return }
fromActionSheetController.dismiss(animated: true) {
self.goToSettings()
}
primaryAction: UIAction { [weak self] _ in
self?.goToSettings()
},
)
let buttonContainer = UIView.container()

View File

@ -7,47 +7,46 @@ import Foundation
import SignalServiceKit
import UIKit
class PinReminderMegaphone: Megaphone {
class PinReminderMegaphone: MegaphoneView {
init(experienceUpgrade: ExperienceUpgrade, fromViewController: UIViewController) {
super.init(experienceUpgrade: experienceUpgrade)
titleText = OWSLocalizedString("PIN_REMINDER_MEGAPHONE_TITLE", comment: "Title for PIN reminder megaphone")
bodyText = OWSLocalizedString("PIN_REMINDER_MEGAPHONE_BODY", comment: "Body for PIN reminder megaphone")
image = .pinMegaphone
imageName = "PIN_megaphone"
let primaryButtonTitle = OWSLocalizedString("PIN_REMINDER_MEGAPHONE_ACTION", comment: "Action text for PIN reminder megaphone")
let primaryButton = Button(title: primaryButtonTitle) { [weak fromViewController] in
guard let fromViewController else { return }
let vc = PinReminderViewController { [weak self] pinReminderViewController, result in
let primaryButton = MegaphoneView.Button(title: primaryButtonTitle) { [weak self] in
let vc = PinReminderViewController { result in
// Always dismiss the PIN reminder view (we dismiss the *megaphone* later).
pinReminderViewController.dismiss(animated: true)
fromViewController.dismiss(animated: true)
guard let self else { return }
switch result {
case .succeeded:
presentToastForNewRepetitionInterval(
self.dismiss(animated: false)
self.presentToastForNewRepetitionInterval(
wasSuccessful: true,
fromViewController: fromViewController,
)
case .canceled(didGuessWrong: true):
presentToastForNewRepetitionInterval(
self.dismiss(animated: false)
self.presentToastForNewRepetitionInterval(
wasSuccessful: false,
fromViewController: fromViewController,
)
case .changedPin, .canceled(didGuessWrong: false):
case .changedPin:
self.dismiss(animated: false)
case .canceled(didGuessWrong: false):
break
}
NotificationCenter.default.post(name: .megaphoneStateDidChange, object: nil)
}
fromViewController.present(vc, animated: true)
}
buttons = [primaryButton]
setButtons(primary: primaryButton)
}
required init(coder: NSCoder) {
@ -104,6 +103,6 @@ class PinReminderMegaphone: Megaphone {
toastText = MegaphoneStrings.weWillRemindYouLater
}
fromViewController.presentToast(text: toastText)
presentToast(text: toastText, fromViewController: fromViewController)
}
}

View File

@ -7,7 +7,7 @@ import Foundation
import SignalServiceKit
import UIKit
class RecoveryKeyReminderMegaphone: Megaphone {
class RecoveryKeyReminderMegaphone: MegaphoneView {
init(
experienceUpgrade: ExperienceUpgrade,
fromViewController: UIViewController,
@ -22,7 +22,7 @@ class RecoveryKeyReminderMegaphone: Megaphone {
"BACKUP_KEY_REMINDER_MEGAPHONE_BODY",
comment: "Body for Recovery Key reminder megaphone",
)
image = .backupsKey
imageName = "backups-key"
let primaryButtonTitle = OWSLocalizedString(
"BACKUP_KEY_REMINDER_MEGAPHONE_ACTION",
@ -33,7 +33,7 @@ class RecoveryKeyReminderMegaphone: Megaphone {
comment: "Snooze text for Recovery Key reminder megaphone",
)
let primaryButton = Button(title: primaryButtonTitle) {
let primaryButton = MegaphoneView.Button(title: primaryButtonTitle) {
let accountKeyStore = DependenciesBridge.shared.accountKeyStore
let backupSettingsStore = BackupSettingsStore()
let db = DependenciesBridge.shared.db
@ -46,17 +46,11 @@ class RecoveryKeyReminderMegaphone: Megaphone {
aep: aep,
fromViewController: fromViewController,
onSuccess: {
self.dismiss()
self.presentToastForNewRepetitionInterval(fromViewController: fromViewController)
db.write { tx in
backupSettingsStore.setLastRecoveryKeyReminderDate(Date(), tx: tx)
}
let toastText = OWSLocalizedString(
"BACKUP_KEY_REMINDER_SUCCESSFUL_TOAST",
comment: "Toast indicating that the Recovery Key was correct.",
)
fromViewController.presentToast(text: toastText)
NotificationCenter.default.post(name: .megaphoneStateDidChange, object: nil)
},
).presentVerifyFlow()
}
@ -66,10 +60,19 @@ class RecoveryKeyReminderMegaphone: Megaphone {
snoozeTitle: secondaryButtonTitle,
)
buttons = [primaryButton, secondaryButton]
setButtons(primary: primaryButton, secondary: secondaryButton)
}
required init(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func presentToastForNewRepetitionInterval(fromViewController: UIViewController) {
let toastText = OWSLocalizedString(
"BACKUP_KEY_REMINDER_SUCCESSFUL_TOAST",
comment: "Toast indicating that the Recovery Key was correct.",
)
presentToast(text: toastText, fromViewController: fromViewController)
}
}

View File

@ -6,7 +6,7 @@
import SignalServiceKit
import SignalUI
class RemoteMegaphone: Megaphone {
class RemoteMegaphone: MegaphoneView {
private let megaphoneModel: RemoteMegaphoneModel
init(
@ -31,7 +31,7 @@ class RemoteMegaphone: Megaphone {
}
if let primary = megaphoneModel.presentablePrimaryAction {
let primaryButton = Button(title: primary.presentableText) { [weak self, weak fromViewController] in
let primaryButton = MegaphoneView.Button(title: primary.presentableText) { [weak self, weak fromViewController] in
guard
let self,
let fromViewController
@ -45,7 +45,7 @@ class RemoteMegaphone: Megaphone {
}
if let secondary = megaphoneModel.presentableSecondaryAction {
let secondaryButton = Button(title: secondary.presentableText) { [weak self, weak fromViewController] in
let secondaryButton = MegaphoneView.Button(title: secondary.presentableText) { [weak self, weak fromViewController] in
guard
let self,
let fromViewController
@ -58,9 +58,9 @@ class RemoteMegaphone: Megaphone {
)
}
buttons = [primaryButton, secondaryButton]
setButtons(primary: primaryButton, secondary: secondaryButton)
} else {
buttons = [primaryButton]
setButtons(primary: primaryButton)
}
}
}
@ -80,13 +80,16 @@ class RemoteMegaphone: Megaphone {
switch action {
case .snooze:
markAsSnoozedWithSneakyTransaction()
dismiss()
case .finish:
markAsCompleteWithSneakyTransaction()
dismiss()
case .donate:
let done = { [weak self] in
guard let self else { return }
// Snooze regardless of outcome.
self.markAsSnoozedWithSneakyTransaction()
self.dismiss(animated: false)
}
guard
@ -131,6 +134,7 @@ class RemoteMegaphone: Megaphone {
guard let self else { return }
// Snooze regardless of outcome.
self.markAsSnoozedWithSneakyTransaction()
self.dismiss(animated: false)
}
guard
@ -149,6 +153,7 @@ class RemoteMegaphone: Megaphone {
fromViewController.present(navController, animated: true, completion: done)
case .unrecognized(let actionId):
owsFailDebug("Unrecognized action with ID \(actionId) should never have made it into \(buttonDescriptor) button!")
dismiss()
}
}
}

View File

@ -15,7 +15,6 @@ public class NotificationActionHandler {
class func handleNotificationResponse(
_ response: UNNotificationResponse,
appReadiness: AppReadinessSetter,
screenLockUI: ScreenLockUI,
) async throws {
owsAssertDebug(appReadiness.isAppReady)
@ -64,7 +63,6 @@ public class NotificationActionHandler {
}
switch responseAction {
case .callBack:
try await screenLockUI.waitForScreenUnlockThrowingPrevious()
try await self.callBack(userInfo: userInfo)
case .markAsRead:
try await markAsRead(userInfo: userInfo)

View File

@ -362,11 +362,23 @@ class ProvisioningCoordinatorImpl: ProvisioningCoordinator {
self.tsAccountManager.setRegistrationId(aciRegistrationId, for: .aci, tx: tx)
self.tsAccountManager.setRegistrationId(pniRegistrationId, for: .pni, tx: tx)
self.svr.storeKeys(
fromProvisioningMessage: provisionMessage,
authedDevice: .explicit(authedDevice),
tx: tx,
)
do {
try svr.storeKeys(
fromProvisioningMessage: provisionMessage,
authedDevice: .explicit(authedDevice),
tx: tx,
)
} catch {
switch error {
case SVR.KeysError.missingMasterKey:
owsFailDebug("Failed to store master key from provisioning message")
return .obsoleteLinkedDeviceError
case SVR.KeysError.missingOrInvalidMRBK:
return .obsoleteLinkedDeviceError
default:
owsFailDebug("Unexpected Error")
}
}
self.receiptManager.setAreReadReceiptsEnabled(
provisionMessage.areReadReceiptsEnabled,

View File

@ -50,7 +50,7 @@ public class ProvisioningManager {
var aciIdentityKeyPair: ECKeyPair
var pniIdentityKeyPair: ECKeyPair
var areReadReceiptsEnabled: Bool
var aep: SignalServiceKit.AccountEntropyPool
var rootKey: LinkingProvisioningMessage.RootKey
var mediaRootBackupKey: MediaRootBackupKey
var profileKey: Aes256Key
}
@ -64,11 +64,13 @@ public class ProvisioningManager {
owsFail("Can't provision without a pni identity.")
}
let areReadReceiptsEnabled = receiptManager.areReadReceiptsEnabled(tx: tx)
let rootKey: LinkingProvisioningMessage.RootKey
guard let accountEntropyPool = accountKeyStore.getAccountEntropyPool(tx: tx) else {
// This should be impossible; the only times you don't have
// an AEP are during registration.
owsFail("Can't provision without account entropy pool.")
}
rootKey = .accountEntropyPool(accountEntropyPool)
let mrbk = accountKeyStore.getOrGenerateMediaRootBackupKey(tx: tx)
guard let profileKey = profileManager.localUserProfile(tx: tx)?.profileKey else {
owsFail("Can't provision without a profile key.")
@ -78,7 +80,7 @@ public class ProvisioningManager {
aciIdentityKeyPair: aciIdentityKeyPair,
pniIdentityKeyPair: pniIdentityKeyPair,
areReadReceiptsEnabled: areReadReceiptsEnabled,
aep: accountEntropyPool,
rootKey: rootKey,
mediaRootBackupKey: mrbk,
profileKey: profileKey,
)
@ -103,7 +105,7 @@ public class ProvisioningManager {
let provisioningCode = try await deviceProvisioningService.requestDeviceProvisioningCode()
let provisioningMessage = LinkingProvisioningMessage(
aep: provisioningState.aep,
rootKey: provisioningState.rootKey,
aci: myAci,
phoneNumber: myPhoneNumber,
pni: myPni,

View File

@ -52,11 +52,10 @@ class LinkAndSyncSecondaryProgressViewModel: ObservableObject {
guard !didTapCancel else { return }
self.isIndeterminate = !(
progress
.progress(for: .waitingForBackup)?
.isFinished ?? false
)
self.isIndeterminate = progress
.progress(for: .waitingForBackup)?
.isFinished.negated
?? true
if
let downloadSource = progress.progressForChild(

View File

@ -99,6 +99,8 @@ public class _RegistrationCoordinator_CNContactsStoreWrapper: _RegistrationCoord
public protocol _RegistrationCoordinator_ExperienceManagerShim {
func clearIntroducingPinsExperience(_ tx: DBWriteTransaction)
func enableAllGetStartedCards(_ tx: DBWriteTransaction)
}
@ -106,6 +108,10 @@ public class _RegistrationCoordinator_ExperienceManagerWrapper: _RegistrationCoo
public init() {}
public func clearIntroducingPinsExperience(_ tx: DBWriteTransaction) {
ExperienceUpgradeManager.clearExperienceUpgrade(.introducingPins, transaction: tx)
}
public func enableAllGetStartedCards(_ tx: DBWriteTransaction) {
GetStartedBannerViewController.enableAllCards(writeTx: tx)
}

View File

@ -14,6 +14,7 @@ public enum RegistrationBackupRestoreError {
case incorrectRecoveryKey
case recoveryKeyRegistrationFailed
case versionMismatch
case retryableSVRBError
case unretryableSVRBError
case networkError
case rateLimited
@ -80,10 +81,14 @@ public class RegistrationCoordinatorBackupErrorPresenterImpl:
return .versionMismatch
case let error as SVRBError:
switch error {
case .retryableAutomatically, .retryableByUser:
return .retryableSVRBError
case .unrecoverable:
return .unretryableSVRBError
case .incorrectRecoveryKey:
return .incorrectRecoveryKey
case .cancellationError:
return .cancellation
}
default:
return .generic
@ -266,7 +271,7 @@ public class RegistrationCoordinatorBackupErrorPresenterImpl:
)
}
})
case .cancellation:
case .retryableSVRBError, .cancellation:
title = OWSLocalizedString(
"REGISTRATION_BACKUP_RESTORE_ERROR_RETRYABLE_SERVER_ERROR_TITLE",
comment: "Title for a sheet telling users to try restoring a backup again after a server error.",

View File

@ -881,7 +881,7 @@ public class RegistrationCoordinatorImpl: RegistrationCoordinator {
var hasBackedUpToSVR = false
var didSkipSVRBackup = false
var shouldBackUpToSVR: Bool {
return !hasBackedUpToSVR && !didSkipSVRBackup
return hasBackedUpToSVR.negated && didSkipSVRBackup.negated
}
var backupMetadataHeader: BackupNonce.MetadataHeader?
@ -1269,7 +1269,7 @@ public class RegistrationCoordinatorImpl: RegistrationCoordinator {
// but these values aren't persisted to their final destination until the very end of
// registration, so persiting the these values once at the start is the easiest way to
// avoid problems.
// Note: We should generate new registration ids if we are reregistering
// Note: We should not reuse existing registration ids if we are reregistering
updatePersistedState(tx) {
if $0.aciRegistrationId == nil {
$0.aciRegistrationId = RegistrationIdGenerator.generate()
@ -1431,6 +1431,16 @@ public class RegistrationCoordinatorImpl: RegistrationCoordinator {
deps.backupArchiveManager.scheduleRestoreFromSVRBBeforeNextExport(tx: tx)
}
if
inMemoryState.hasBackedUpToSVR
|| inMemoryState.didHaveSVRBackupsPriorToReg
|| inMemoryState.backupRestoreState == .finalized
{
// No need to show the experience if we made the pin
// and backed up.
deps.experienceManager.clearIntroducingPinsExperience(tx)
}
// Persist the AEP. RegCoordinator manages all necessary side
// effects, like updating Account Attributes and rotating the
// Storage Service manifest.
@ -2165,7 +2175,7 @@ public class RegistrationCoordinatorImpl: RegistrationCoordinator {
private func loadSVRAuthCredentialCandidates(_ tx: DBReadTransaction) {
let svr2AuthCredentialCandidates: [SVR2AuthCredential] = deps.svrAuthCredentialStore.getAuthCredentials(tx)
if !svr2AuthCredentialCandidates.isEmpty {
if svr2AuthCredentialCandidates.isEmpty.negated {
inMemoryState.svr2AuthCredentialCandidates = svr2AuthCredentialCandidates
}
}
@ -2720,7 +2730,7 @@ public class RegistrationCoordinatorImpl: RegistrationCoordinator {
guard
(
inMemoryState.accountEntropyPool != nil ||
!persistedState.hasGivenUpTryingToRestoreWithSVR
persistedState.hasGivenUpTryingToRestoreWithSVR.negated
)
else {
// If we haven't set an AEP, and have already exhausted our SVR backup attempts, we are stuck.
@ -3898,7 +3908,7 @@ public class RegistrationCoordinatorImpl: RegistrationCoordinator {
}
if let reglockToken = self.reglockToken(for: accountIdentity.e164) {
if !inMemoryState.hasSetReglock {
if inMemoryState.hasSetReglock.negated {
return await self.enableReglock(accountIdentity: accountIdentity, reglockToken: reglockToken)
}
} else {
@ -4370,7 +4380,7 @@ public class RegistrationCoordinatorImpl: RegistrationCoordinator {
switch mode {
case .reRegistering(let state):
if !persistedState.hasResetForReRegistration {
if persistedState.hasResetForReRegistration.negated {
db.write { tx in
let isPrimaryDevice = deps.tsAccountManager.registrationState(tx: tx).isPrimaryDevice ?? true
let discoverability = deps.phoneNumberDiscoverabilityManager.phoneNumberDiscoverability(tx: tx)

View File

@ -261,7 +261,9 @@ extension RegistrationCoordinatorLoaderImpl.Mode.ChangeNumberState.PendingPniSta
private enum CodingKeys: String, CodingKey {
case newE164
case pniIdentityKeyPair
case localDevicePniSignedPreKeyRecord // deprecated
case localDevicePniSignedPreKeyRecordData
case localDevicePniPqLastResortPreKeyRecord // deprecated
case localDevicePniPqLastResortPreKeyRecordData
case localDevicePniRegistrationId
}
@ -274,6 +276,11 @@ extension RegistrationCoordinatorLoaderImpl.Mode.ChangeNumberState.PendingPniSta
if let modernValue = try container.decodeIfPresent(Data.self, forKey: .localDevicePniPqLastResortPreKeyRecordData) {
self.localDevicePniPqLastResortPreKeyRecord = .success(try LibSignalClient.KyberPreKeyRecord(bytes: modernValue))
} else if
BuildFlags.decodeDeprecatedPreKeys,
let deprecatedValue = try container.decodeIfPresent(KyberRecordKeyData.self, forKey: .localDevicePniPqLastResortPreKeyRecord)
{
self.localDevicePniPqLastResortPreKeyRecord = .success(try LibSignalClient.KyberPreKeyRecord(bytes: deprecatedValue.keyData))
} else {
// We don't want to fail the ENTIRE registration operation when this is
// missing -- we can recover in this case, but we need to communicate the
@ -287,6 +294,19 @@ extension RegistrationCoordinatorLoaderImpl.Mode.ChangeNumberState.PendingPniSta
if let modernValue = try container.decodeIfPresent(Data.self, forKey: .localDevicePniSignedPreKeyRecordData) {
self.localDevicePniSignedPreKeyRecord = .success(try LibSignalClient.SignedPreKeyRecord(bytes: modernValue))
} else if
BuildFlags.decodeDeprecatedPreKeys,
let deprecatedValue = try container.decodeIfPresent(Data.self, forKey: .localDevicePniSignedPreKeyRecord)
{
guard let signedPreKeyRecord = try NSKeyedUnarchiver.unarchivedObject(ofClass: SignalServiceKit.SignedPreKeyRecord.self, from: deprecatedValue) else {
throw DecodingError.dataCorruptedError(forKey: .localDevicePniSignedPreKeyRecord, in: container, debugDescription: "")
}
self.localDevicePniSignedPreKeyRecord = .success(try LibSignalClient.SignedPreKeyRecord(
id: UInt32(bitPattern: signedPreKeyRecord.id),
timestamp: signedPreKeyRecord.generatedAt.ows_millisecondsSince1970,
privateKey: signedPreKeyRecord.keyPair.keyPair.privateKey,
signature: signedPreKeyRecord.signature,
))
} else {
// We don't want to fail the ENTIRE registration operation when this is
// missing -- we can recover in this case, but we need to communicate the
@ -325,6 +345,12 @@ extension RegistrationCoordinatorLoaderImpl.Mode.ChangeNumberState.PendingPniSta
)
}
/// A shim of the former KyberPreKeyRecord that contains what's necessary to
/// maintain continuity with historically-encoded values.
private struct KyberRecordKeyData: Codable {
var keyData: Data
}
// MARK: NSKeyed[Un]Archiver
private static func decodeKeyedArchive<T: NSObject & NSSecureCoding>(

View File

@ -75,7 +75,7 @@ class RegistrationLoadingViewController: OWSViewController, OWSNavigationChildCo
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
if !spinnerView.isAnimating {
if spinnerView.isAnimating.negated {
spinnerView.startAnimating()
}
}

View File

@ -65,7 +65,7 @@ public class RegistrationNavigationController: OWSNavigationController {
return
}
if let loadingMode, !step.isSealed {
if let loadingMode, step.isSealed.negated {
logger.info("Pushing loading controller")
isLoading = true
@ -624,6 +624,17 @@ extension RegistrationNavigationController: RegistrationPinPresenter {
func submitWithCreateNewPinInstead() {
pushNextController(coordinator.skipAndCreateNewPINCode())
}
func enterRecoveryKey() {
pushNextController(
.value(.enterRecoveryKey(
RegistrationEnterAccountEntropyPoolState(
canShowBackButton: true,
canShowNoKeyHelpButton: false,
),
)),
)
}
}
extension RegistrationNavigationController: RegistrationPinAttemptsExhaustedAndMustCreateNewPinPresenter {

View File

@ -89,6 +89,8 @@ protocol RegistrationPinPresenter: AnyObject {
func submitWithCreateNewPinInstead()
func exitRegistration()
func enterRecoveryKey()
}
// MARK: - RegistrationPinViewController
@ -452,6 +454,18 @@ class RegistrationPinViewController: OWSViewController {
))
}
actions.append(
UIAction(
title: OWSLocalizedString(
"PIN_ENTER_EXISTING_USE_RECOVERY_KEY",
comment: "If the user is re-registering, they need to enter their PIN to restore all their data. If they don't remember their PIN, they may remember their Recovery Key which can be used instead of a PIN.",
),
handler: { [weak self] _ in
self?.presenter?.enterRecoveryKey()
},
),
)
if let exitAction = exitAction() {
actions.append(exitAction)
}
@ -668,6 +682,15 @@ class RegistrationPinViewController: OWSViewController {
}
}
actionSheet.addAction(.init(
title: OWSLocalizedString(
"ONBOARDING_2FA_SKIP_AND_USE_RECOVERY_KEY",
comment: "Label for action to use Recovery Key instead of PIN for registration.",
),
) { [weak self] _ in
self?.presenter?.enterRecoveryKey()
})
actionSheet.addAction(.init(title: CommonStrings.contactSupport) { [weak self] _ in
guard let self else { return }
ContactSupportActionSheet.present(

View File

@ -333,8 +333,8 @@ class RegistrationVerificationViewController: OWSViewController {
}
explanationLabel.text = explanationLabelText()
wrongNumberButton.isHidden = !state.canChangeE164
helpButton.isHidden = !state.showHelpText
wrongNumberButton.isHidden = state.canChangeE164.negated
helpButton.isHidden = state.showHelpText.negated
verificationCodeView.updateColors()
}

View File

@ -1306,42 +1306,6 @@ limitations under the License.
<key>Type</key>
<string>PSGroupSpecifier</string>
</dict>
<dict>
<key>FooterText</key>
<string>Copyright (c) 2014 Alex Crichton
Copyright (c) 2020 Ivan Nikulin &lt;ifaaan@gmail.com&gt;
Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the
Software without restriction, including without
limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software
is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice
shall be included in all copies or substantial portions
of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
</string>
<key>License</key>
<string>MIT License</string>
<key>Title</key>
<string>boring-sys 5.0.2</string>
<key>Type</key>
<string>PSGroupSpecifier</string>
</dict>
<dict>
<key>FooterText</key>
<string>
@ -1590,6 +1554,42 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
<key>Type</key>
<string>PSGroupSpecifier</string>
</dict>
<dict>
<key>FooterText</key>
<string>Copyright (c) 2014 Alex Crichton
Copyright (c) 2020 Ivan Nikulin &lt;ifaaan@gmail.com&gt;
Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the
Software without restriction, including without
limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software
is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice
shall be included in all copies or substantial portions
of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
</string>
<key>License</key>
<string>MIT License</string>
<key>Title</key>
<string>boring-sys 5.0.2</string>
<key>Type</key>
<string>PSGroupSpecifier</string>
</dict>
<dict>
<key>FooterText</key>
<string>
@ -1932,7 +1932,7 @@ USE OR OTHER DEALINGS IN THE SOFTWARE.
<key>License</key>
<string>MIT License</string>
<key>Title</key>
<string>cesu8 1.1.0, jni-sys-macros 0.4.1, neon 1.1.1, objc2-core-foundation 0.3.2, objc2-io-kit 0.3.2, protobuf-parse 3.7.2, tonic-prost-build 0.14.5, windows 0.61.3, windows 0.62.2, windows-collections 0.2.0, windows-collections 0.3.2, windows-core 0.61.2, windows-core 0.62.2, windows-future 0.2.1, windows-future 0.3.2, windows-implement 0.60.2, windows-interface 0.59.3, windows-link 0.1.3, windows-link 0.2.1, windows-numerics 0.2.0, windows-numerics 0.3.1, windows-result 0.3.4, windows-result 0.4.1, windows-strings 0.4.2, windows-strings 0.5.1, windows-threading 0.1.0, windows-threading 0.2.1</string>
<string>cesu8 1.1.0, jni-sys-macros 0.4.1, neon 1.1.1, objc2-core-foundation 0.3.2, objc2-io-kit 0.3.2, pbjson 0.9.0, pbjson-build 0.9.0, pbjson-types 0.9.0, protobuf-parse 3.7.2, tonic-prost-build 0.14.5, windows 0.61.3, windows 0.62.2, windows-collections 0.2.0, windows-collections 0.3.2, windows-core 0.61.2, windows-core 0.62.2, windows-future 0.2.1, windows-future 0.3.2, windows-implement 0.60.2, windows-interface 0.59.3, windows-link 0.1.3, windows-link 0.2.1, windows-numerics 0.2.0, windows-numerics 0.3.1, windows-result 0.3.4, windows-result 0.4.1, windows-strings 0.4.2, windows-strings 0.5.1, windows-threading 0.1.0, windows-threading 0.2.1</string>
<key>Type</key>
<string>PSGroupSpecifier</string>
</dict>
@ -2628,6 +2628,42 @@ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
<key>Type</key>
<string>PSGroupSpecifier</string>
</dict>
<dict>
<key>FooterText</key>
<string>Copyright (c) 2012 The Go Authors. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Google Inc. nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.</string>
<key>License</key>
<string>BSD 3-Clause "New" or "Revised" License</string>
<key>Title</key>
<string>curve25519-dalek 4.1.3</string>
<key>Type</key>
<string>PSGroupSpecifier</string>
</dict>
<dict>
<key>FooterText</key>
<string>Copyright (c) 2016-2021 isis agora lovecruft. All rights reserved.
@ -2658,42 +2694,6 @@ TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.</string>
<key>License</key>
<string>BSD 3-Clause "New" or "Revised" License</string>
<key>Title</key>
<string>curve25519-dalek 4.1.3</string>
<key>Type</key>
<string>PSGroupSpecifier</string>
</dict>
<dict>
<key>FooterText</key>
<string>Copyright (c) 2012 The Go Authors. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Google Inc. nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.</string>
<key>License</key>
<string>BSD 3-Clause "New" or "Revised" License</string>
@ -9835,41 +9835,6 @@ DEALINGS IN THE SOFTWARE.
<key>Type</key>
<string>PSGroupSpecifier</string>
</dict>
<dict>
<key>FooterText</key>
<string>Copyright (c) 2020 InfluxData
Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the
Software without restriction, including without
limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software
is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice
shall be included in all copies or substantial portions
of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
</string>
<key>License</key>
<string>MIT License</string>
<key>Title</key>
<string>pbjson 0.9.0, pbjson-build 0.9.0, pbjson-types 0.9.0</string>
<key>Type</key>
<string>PSGroupSpecifier</string>
</dict>
<dict>
<key>FooterText</key>
<string>Copyright (c) 2013-2025 The rust-url developers

View File

@ -30,7 +30,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>8.16</string>
<string>8.14</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>

View File

@ -1,16 +0,0 @@
{
"images" : [
{
"filename" : "error-triangle.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true,
"template-rendering-intent" : "template"
}
}

View File

@ -1,107 +0,0 @@
%PDF-1.7
1 0 obj
<< >>
endobj
2 0 obj
<< /Length 3 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 0.980103 15.426270 cm
0.000000 0.000000 0.000000 scn
11.019876 2.073738 m
10.324054 2.073738 9.775503 1.481371 9.828871 0.787598 c
10.252174 -4.715347 l
10.283031 -5.116498 10.617537 -5.426263 11.019876 -5.426263 c
11.422214 -5.426263 11.756720 -5.116498 11.787577 -4.715347 c
12.210880 0.787598 l
12.264248 1.481371 11.715697 2.073738 11.019876 2.073738 c
h
f*
n
Q
q
1.000000 0.000000 -0.000000 1.000000 0.980103 20.426270 cm
0.000000 0.000000 0.000000 scn
9.769861 -12.926263 m
9.769861 -12.235908 10.329505 -11.676263 11.019861 -11.676263 c
11.710217 -11.676263 12.269861 -12.235908 12.269861 -12.926263 c
12.269861 -13.616619 11.710217 -14.176262 11.019861 -14.176262 c
10.329505 -14.176262 9.769861 -13.616619 9.769861 -12.926263 c
h
f*
n
Q
q
1.000000 0.000000 -0.000000 1.000000 0.980103 2.568329 cm
0.000000 0.000000 0.000000 scn
8.107189 18.687922 m
9.410621 20.914618 12.629106 20.914612 13.932535 18.687920 c
21.572298 5.636655 l
22.889338 3.386715 21.266691 0.556679 18.659622 0.556679 c
3.380099 0.556679 l
0.773029 0.556679 -0.849612 3.386719 0.467425 5.636659 c
8.107189 18.687922 l
h
12.422260 17.803856 m
11.794682 18.875969 10.245042 18.875969 9.617465 17.803858 c
1.977700 4.752595 l
1.343571 3.669292 2.124843 2.306679 3.380099 2.306679 c
18.659622 2.306679 l
19.914881 2.306679 20.696152 3.669289 20.062023 4.752591 c
12.422260 17.803856 l
h
f*
n
Q
endstream
endobj
3 0 obj
1436
endobj
4 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 24.000000 24.000000 ]
/Resources 1 0 R
/Contents 2 0 R
/Parent 5 0 R
>>
endobj
5 0 obj
<< /Kids [ 4 0 R ]
/Count 1
/Type /Pages
>>
endobj
6 0 obj
<< /Pages 5 0 R
/Type /Catalog
>>
endobj
xref
0 7
0000000000 65535 f
0000000010 00000 n
0000000034 00000 n
0000001526 00000 n
0000001549 00000 n
0000001722 00000 n
0000001796 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
1855
%%EOF

View File

@ -390,21 +390,33 @@ class UsernameLinkPresentQRCodeViewController: OWSTableViewController2 {
}
private func buildResetButtonView() -> UIView {
let button = UIButton(
configuration: .smallSecondary(title: resetButtonString),
primaryAction: UIAction { [weak self] _ in
self?.didTapResetButton()
},
)
let button = OWSRoundedButton { [weak self] in
self?.tappedResetButton()
}
if case .resetting = usernameLinkState {
button.setTitle(resetButtonString, for: .normal)
button.ows_contentEdgeInsets = UIEdgeInsets(hMargin: 16, vMargin: 6)
button.backgroundColor = Theme.isDarkThemeEnabled ? .ows_gray80 : .ows_whiteAlpha70
button.titleLabel!.font = .dynamicTypeSubheadline.bold()
button.setTitleColor(Theme.primaryTextColor, for: .normal)
button.configureForMultilineTitle()
button.dimsWhenHighlighted = true
button.dimsWhenDisabled = true
switch usernameLinkState {
case .resetting:
button.isEnabled = false
case .available, .corrupted:
button.isEnabled = true
}
return CenteringStackView(centeredSubviews: [button])
}
private func didTapResetButton() {
private func tappedResetButton() {
let actionSheet = ActionSheetController(message: OWSLocalizedString(
"USERNAME_LINK_QR_CODE_VIEW_RESET_SHEET_MESSAGE",
comment: "A message explaining what will happen if the user resets their QR code.",

View File

@ -8,7 +8,7 @@ import SignalUI
class UsernameSelectionCoordinator {
struct Context {
let databaseStorage: DB
let databaseStorage: SDSDatabaseStorage
let networkManager: NetworkManager
let storageServiceManager: StorageServiceManager
let usernameEducationManager: UsernameEducationManager

View File

@ -23,7 +23,7 @@ class UsernameSelectionViewController: OWSViewController, OWSNavigationChildCont
/// A wrapper for injected dependencies.
struct Context {
let networkManager: NetworkManager
let databaseStorage: DB
let databaseStorage: SDSDatabaseStorage
let localUsernameManager: LocalUsernameManager
let storageServiceManager: StorageServiceManager
}

View File

@ -355,7 +355,7 @@ class AccountSettingsViewController: OWSTableViewController2 {
SSKEnvironment.shared.ows2FAManagerRef.setAreRemindersEnabled(false, transaction: transaction)
}
NotificationCenter.default.post(name: .megaphoneStateDidChange, object: nil)
ExperienceUpgradeManager.dismissPINReminderIfNecessary()
} else {
self.updateTableContents()
}
@ -498,7 +498,7 @@ class AccountSettingsViewController: OWSTableViewController2 {
// MARK: -
private func showChangePin() {
let vc = PinSetupViewController(mode: .changing) { [weak self] _ in
let vc = PinSetupViewController(mode: .changing) { [weak self] _, _ in
guard let self else { return }
self.navigationController?.popToViewController(self, animated: true)
}
@ -508,7 +508,7 @@ class AccountSettingsViewController: OWSTableViewController2 {
private func showCreatePin() {
let vc = PinSetupViewController(
mode: .creating,
) { [weak self] _ in
) { [weak self] _, _ in
guard let self else { return }
self.navigationController?.popToViewController(self, animated: true)
}

View File

@ -113,7 +113,7 @@ class AdvancedPinSettingsTableViewController: OWSTableViewController2 {
private func showCreatePin() {
let viewController = PinSetupViewController(
mode: .creating,
onSuccess: { [weak self] _ in
completionHandler: { [weak self] _, _ in
guard let self else { return }
self.navigationController?.popToViewController(self, animated: true)
self.updateTableContents()

View File

@ -411,7 +411,6 @@ class AppSettingsViewController: OWSTableViewController2 {
infoStack.autoPinTrailingToSuperviewMargin()
if let usernameLinkButton = profileCellUsernameLinkButton() {
usernameLinkButton.sizeToFit() // this is required
cell.accessoryView = usernameLinkButton
} else {
cell.accessoryType = .disclosureIndicator
@ -510,7 +509,11 @@ class AppSettingsViewController: OWSTableViewController2 {
return profileInfoStack
}
/// If we have a username, produces a button that takes the user to their username link QR code.
/// If we have a username, produces a button that takes the user to their
/// username link QR code.
///
/// Note that this button does not use autolayout, so as to play nice with
/// ``UITableViewCell``'s accessory view.
private func profileCellUsernameLinkButton() -> UIButton? {
let localUsername: String
let localUsernameLink: Usernames.UsernameLink
@ -523,26 +526,34 @@ class AppSettingsViewController: OWSTableViewController2 {
localUsernameLink = usernameLink
}
var buttonConfiguration = UIButton.Configuration.roundGray(image: .qrCode)
buttonConfiguration.contentInsets = .init(margin: 8) // makes 40 dp button
return UIButton(
configuration: buttonConfiguration,
primaryAction: UIAction { [weak self] _ in
guard let self else { return }
let usernameLinkButton = OWSRoundedButton { [weak self] in
guard let self else { return }
let usernameLinkController = UsernameLinkQRCodeContentController(
db: DependenciesBridge.shared.db,
localUsernameManager: DependenciesBridge.shared.localUsernameManager,
username: localUsername,
usernameLink: localUsernameLink,
changeDelegate: self,
scanDelegate: self,
)
let usernameLinkController = UsernameLinkQRCodeContentController(
db: DependenciesBridge.shared.db,
localUsernameManager: DependenciesBridge.shared.localUsernameManager,
username: localUsername,
usernameLink: localUsernameLink,
changeDelegate: self,
scanDelegate: self,
)
let navController = OWSNavigationController(rootViewController: usernameLinkController)
self.present(navController, animated: true)
},
)
let navController = OWSNavigationController(rootViewController: usernameLinkController)
self.present(navController, animated: true)
}
if Theme.isDarkThemeEnabled {
usernameLinkButton.backgroundColor = .ows_gray65
usernameLinkButton.setTemplateImage(Theme.iconImage(.qrCode), tintColor: .ows_gray15)
} else {
usernameLinkButton.backgroundColor = .ows_gray05
usernameLinkButton.setImage(Theme.iconImage(.qrCode), for: .normal)
}
usernameLinkButton.bounds = CGRect(origin: .zero, size: .square(36))
usernameLinkButton.imageView?.autoSetDimensions(to: .square(20))
return usernameLinkButton
}
private func didTapDonate() {

View File

@ -0,0 +1,48 @@
//
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import SignalServiceKit
import SignalUI
import UIKit
class PaypalButton: UIButton {
private let actionBlock: () -> Void
init(actionBlock: @escaping () -> Void) {
self.actionBlock = actionBlock
super.init(frame: .zero)
addTarget(self, action: #selector(didTouchUpInside), for: .touchUpInside)
configureStyling()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) is not implemented.")
}
// MARK: Styling
private func configureStyling() {
setImage(UIImage(named: "paypal-logo"), for: .normal)
ows_adjustsImageWhenDisabled = false
ows_adjustsImageWhenHighlighted = false
if #available(iOS 26.0, *) {
configuration = .prominentGlass()
tintColor = UIColor(rgbHex: 0xF6C757)
} else {
layer.cornerRadius = 12
backgroundColor = UIColor(rgbHex: 0xF6C757)
}
}
// MARK: Actions
@objc
private func didTouchUpInside() {
actionBlock()
}
}

View File

@ -64,25 +64,16 @@ class InternalBackupSettingsViewController: OWSTableViewController2 {
let vc = InternalListMediaViewController()
self?.navigationController?.pushViewController(vc, animated: true)
})
section.add(.switch(
withText: "Regenerate backup thumbnails",
subtitle: "Regenerate backup thumbnails on next offloading run",
isOn: { db.read(block: backupSettingsStore.shouldGenerateThumbnailsOnNextOffloading(tx:)) },
actionBlock: { _ in
db.write { tx in
let currentValue = backupSettingsStore.shouldGenerateThumbnailsOnNextOffloading(tx: tx)
backupSettingsStore.setShouldGenerateThumbnailsOnNextOffloading(!currentValue, tx: tx)
}
},
))
section.add(.switch(
withText: "Aggressive optimize media",
subtitle: "Don't keep recent attachments when optimize media is enabled",
isOn: { Attachment.offloadingThresholdOverride },
actionBlock: { _ in
Attachment.offloadingThresholdOverride = !Attachment.offloadingThresholdOverride
},
))
if RemoteConfig.current.isOptimizeStorageEnabled {
section.add(.switch(
withText: "Aggressive optimize media",
subtitle: "Don't keep recent attachments when optimize media is enabled",
isOn: { Attachment.offloadingThresholdOverride },
actionBlock: { _ in
Attachment.offloadingThresholdOverride = !Attachment.offloadingThresholdOverride
},
))
}
contents.add(section)

View File

@ -42,6 +42,9 @@ class InternalDiskUsageViewController: OWSTableViewController2 {
let voiceMessageCacheSize = folderSizeRecursive(of: VoiceMessageInterruptedDraftStore.draftVoiceMessageDirectory)
let librarySize = folderSizeRecursive(ofPath: OWSFileSystem.appLibraryDirectoryPath()) ?? 0
let libraryCachesSize = folderSizeRecursive(ofPath: OWSFileSystem.cachesDirectoryPath()) ?? 0
let documentsSize = folderSizeRecursive(ofPath: OWSFileSystem.appDocumentDirectoryPath())
let sharedDataSize = folderSizeRecursive(ofPath: OWSFileSystem.appSharedDataDirectoryPath())
let bundleSize = folderSizeRecursive(ofPath: Bundle.main.bundlePath)
let tmpSize = folderSizeRecursive(ofPath: NSTemporaryDirectory())
}
@ -52,8 +55,10 @@ class InternalDiskUsageViewController: OWSTableViewController2 {
nonisolated static func build() async -> InternalDiskUsageViewController {
await Task.yield()
try! await DependenciesBridge.shared.orphanedAttachmentCleaner.runUntilFinished()
async let orphanedAttachmentByteCount = Self.orphanAttachmentByteCount()
let diskUsage = DiskUsage()
let diskUsageTask = Task { DiskUsage() }
let orphanedAttachmentByteCountTask = Task { await Self.orphanAttachmentByteCount() }
let diskUsage = await diskUsageTask.value
let orphanedAttachmentByteCount = await orphanedAttachmentByteCountTask.value
return await InternalDiskUsageViewController(
diskUsage: diskUsage,
orphanedAttachmentByteCount: orphanedAttachmentByteCount,
@ -82,6 +87,29 @@ class InternalDiskUsageViewController: OWSTableViewController2 {
let byteCountFormatter = ByteCountFormatter()
let knownFilesSize: UInt64 = [UInt64?](
arrayLiteral:
diskUsage.dbSize,
diskUsage.dbWalSize,
diskUsage.dbShmSize,
diskUsage.attachmentSize,
diskUsage.emojiCacheSize,
diskUsage.stickerCacheSize,
diskUsage.avatarCacheSize,
diskUsage.voiceMessageCacheSize,
diskUsage.librarySize,
)
.compacted()
.reduce(0, +)
var totalFilesystemSize: UInt64 = [UInt64?](
arrayLiteral:
diskUsage.librarySize,
diskUsage.documentsSize,
diskUsage.sharedDataSize,
)
.compacted()
.reduce(0, +)
let diskUsageSection = OWSTableSection(title: "Disk Usage")
diskUsageSection.add(.copyableItem(label: "DB Size", value: byteCountFormatter.string(for: diskUsage.dbSize)))
diskUsageSection.add(.copyableItem(label: "DB WAL Size", value: byteCountFormatter.string(for: diskUsage.dbWalSize)))
@ -92,12 +120,36 @@ class InternalDiskUsageViewController: OWSTableViewController2 {
diskUsageSection.add(.copyableItem(label: "Sticker cache size", value: byteCountFormatter.string(for: diskUsage.stickerCacheSize)))
diskUsageSection.add(.copyableItem(label: "Avatar cache size", value: byteCountFormatter.string(for: diskUsage.avatarCacheSize)))
diskUsageSection.add(.copyableItem(label: "Voice message drafts size", value: byteCountFormatter.string(for: diskUsage.voiceMessageCacheSize)))
diskUsageSection.add(.copyableItem(label: "Library folder size (includes Caches)", value: byteCountFormatter.string(for: diskUsage.librarySize)))
diskUsageSection.add(.copyableItem(label: "Library (minus caches) folder size", value: byteCountFormatter.string(for: diskUsage.librarySize - diskUsage.libraryCachesSize)))
diskUsageSection.add(.copyableItem(label: "Caches folder size", value: byteCountFormatter.string(for: diskUsage.libraryCachesSize)))
diskUsageSection.add(.copyableItem(label: "Tmp size", value: byteCountFormatter.string(for: diskUsage.tmpSize)))
diskUsageSection.add(.copyableItem(label: "Bundle size", value: byteCountFormatter.string(for: diskUsage.bundleSize)))
diskUsageSection.add(.copyableItem(label: "Ancillary files size", value: byteCountFormatter.string(for: totalFilesystemSize - knownFilesSize)))
if TSConstants.isUsingProductionService {
let stagingSharedDataSize = folderSizeRecursive(
ofPath: FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: TSConstantsStaging().applicationGroup)!.path,
)
diskUsageSection.add(.copyableItem(label: "Staging app group size", value: byteCountFormatter.string(for: stagingSharedDataSize)))
totalFilesystemSize += stagingSharedDataSize ?? 0
} else {
let prodSharedDataSize = folderSizeRecursive(
ofPath: FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: TSConstantsProduction().applicationGroup)!.path,
)
diskUsageSection.add(.copyableItem(label: "Prod app group size", value: byteCountFormatter.string(for: prodSharedDataSize)))
totalFilesystemSize += prodSharedDataSize ?? 0
}
diskUsageSection.add(.copyableItem(
label: "Total filesystem size",
subtitle: "Should match \"Documents & Data\" in Settings>General>Storage>Signal",
value: byteCountFormatter.string(for: totalFilesystemSize),
))
contents.add(diskUsageSection)
let otherDiskUsageSection = OWSTableSection(title: "Other Disk Usage")
otherDiskUsageSection.add(.copyableItem(label: "Tmp size", value: byteCountFormatter.string(for: diskUsage.tmpSize)))
otherDiskUsageSection.add(.copyableItem(label: "Bundle size", value: byteCountFormatter.string(for: diskUsage.bundleSize)))
contents.add(otherDiskUsageSection)
self.contents = contents
}

View File

@ -353,9 +353,11 @@ extension LinkedDevicesViewModel: LinkDeviceViewControllerDelegate {
if let details, Date() > details.shouldRemindUserAfter {
db.write { tx in
deviceStore.clearMostRecentlyLinkedDeviceDetails(tx: tx)
ExperienceUpgradeManager.clearExperienceUpgrade(
.newLinkedDeviceNotification,
transaction: tx,
)
}
NotificationCenter.default.post(name: .megaphoneStateDidChange, object: nil)
}
SSKEnvironment.shared.notificationPresenterRef.clearDeliveredNewLinkedDevicesNotifications()

View File

@ -25,9 +25,6 @@ class PaymentsViewPassphraseGridViewController: OWSViewController {
override func viewDidLoad() {
super.viewDidLoad()
let screenLockUI = AppEnvironment.shared.screenLockUI
screenLockUI.sensitiveContentDidLoad(inViewController: self)
title = OWSLocalizedString(
"SETTINGS_PAYMENTS_VIEW_PASSPHRASE_TITLE",
comment: "Title for the 'view payments passphrase' view of the app settings.",

View File

@ -95,7 +95,10 @@ class BlockListViewController: OWSTableViewController2 {
OWSTableItem(
dequeueCellBlock: { [weak self] tableView in
let cell = tableView.dequeueReusableCell(withIdentifier: ContactTableViewCell.reuseIdentifier) as! ContactTableViewCell
let config = ContactCellView.Configuration(address: address, localUserDisplayMode: .asUser)
let config = ContactCellConfiguration(
address: address,
localUserDisplayMode: .asUser,
)
if self != nil {
SSKEnvironment.shared.databaseStorageRef.read { transaction in
cell.configure(configuration: config, transaction: transaction)

View File

@ -204,8 +204,8 @@ class AttachmentFormatPickerView: UIView {
private static func cases(except: [AttachmentType]) -> [AttachmentType] {
let showGifSearch = RemoteConfig.current.enableGifSearch
return allCases.filter { (value: AttachmentType) in
if value == .gif, !showGifSearch { return false }
return !except.contains(value)
if value == .gif, showGifSearch.negated { return false }
return except.contains(value).negated
}
}

View File

@ -93,7 +93,7 @@ public class ContextMenuReactionBarAccessory: ContextMenuTargetedPreviewAccessor
if let focusedEmoji = reactionPicker.focusedEmoji {
switch focusedEmoji {
case .more:
didSelectShowFullEmojiPicker()
didSelectAnyEmoji()
case .emoji(let emoji):
let isRemoving = emoji == self.itemViewModel?.reactionState?.localUserEmoji
if let index = reactionPicker.currentEmojiSet().firstIndex(of: emoji) {
@ -125,7 +125,7 @@ public class ContextMenuReactionBarAccessory: ContextMenuTargetedPreviewAccessor
}
}
func didSelectShowFullEmojiPicker() {
func didSelectAnyEmoji() {
guard let message = itemViewModel?.interaction as? TSMessage else {
owsFailDebug("Not sending reaction for unexpected interaction type")
return

View File

@ -45,62 +45,6 @@ class DebugUIMisc: DebugUIPage {
}
}),
]
if let groupThread = thread as? TSGroupThread {
items += [
OWSTableItem(title: "Insert 50 group update messages", actionBlock: {
let updateItems: [TSInfoMessage.PersistableGroupUpdateItem] = [
.genericUpdateByLocalUser,
.genericUpdateByUnknownUser,
.nameRemovedByLocalUser,
.nameRemovedByUnknownUser,
.avatarChangedByLocalUser,
.avatarChangedByUnknownUser,
.avatarRemovedByLocalUser,
.avatarRemovedByUnknownUser,
.localUserLeft,
.localUserRemovedByUnknownUser,
.localUserWasInvitedByLocalUser,
.localUserWasInvitedByUnknownUser,
.localUserAcceptedInviteFromUnknownUser,
.localUserJoined,
.localUserAddedByLocalUser,
.localUserAddedByUnknownUser,
.localUserDeclinedInviteFromUnknownUser,
.localUserInviteRevokedByUnknownUser,
.localUserRequestedToJoin,
.localUserRequestApprovedByUnknownUser,
.localUserRequestCanceledByLocalUser,
.localUserRequestRejectedByUnknownUser,
.inviteLinkResetByLocalUser,
.inviteLinkResetByUnknownUser,
.inviteLinkEnabledWithoutApprovalByLocalUser,
.inviteLinkEnabledWithApprovalByLocalUser,
.inviteLinkDisabledByLocalUser,
.inviteLinkApprovalDisabledByLocalUser,
.inviteLinkApprovalEnabledByLocalUser,
.localUserJoinedViaInviteLink,
.wasMigrated,
.localUserInvitedAfterMigration,
.createdByLocalUser,
.createdByUnknownUser,
.inviteFriendsToNewlyCreatedGroup,
].shuffled()
SSKEnvironment.shared.databaseStorageRef.write { tx in
for i in 0..<50 {
let item = updateItems[i % updateItems.count]
let infoMessage = TSInfoMessage(
thread: groupThread,
messageType: .typeGroupUpdate,
infoMessageUserInfo: [.groupUpdateItems: TSInfoMessage.PersistableGroupUpdateItemsWrapper([item])],
)
infoMessage.anyInsert(transaction: tx)
}
}
}),
]
}
return OWSTableSection(title: name, items: items)
}

View File

@ -211,24 +211,11 @@ class DonateChoosePaymentMethodSheet: StackSheetViewController {
}
}
private func createPaypalButton() -> UIButton {
var configuration: UIButton.Configuration = if #available(iOS 26, *) { .prominentGlass() } else { .borderedProminent() }
configuration.image = UIImage(resource: .paypalLogo)
configuration.baseBackgroundColor = UIColor(rgbHex: 0xF6C757)
if #available(iOS 26, *) {
configuration.cornerStyle = .capsule
} else {
configuration.cornerStyle = .fixed
configuration.background.cornerRadius = 12
private func createPaypalButton() -> PaypalButton {
PaypalButton { [weak self] in
guard let self else { return }
self.didChoosePaymentMethod(self, .paypal)
}
return UIButton(
configuration: configuration,
primaryAction: UIAction { [weak self] _ in
guard let self else { return }
self.didChoosePaymentMethod(self, .paypal)
},
)
}
private func createCreditOrDebitCardButton() -> UIButton {

View File

@ -79,16 +79,16 @@ class CLVViewState {
}
}
enum BackupSubscriptionAlreadyRedeemedAlertType: CaseIterable {
enum BackupSubscriptionFailedToRedeemAlertType: CaseIterable {
case avatarBadge
case menuItem
}
var backupSubscriptionAlreadyRedeemedAlerts: Set<BackupSubscriptionAlreadyRedeemedAlertType> = [] {
var backupSubscriptionFailedToRedeemAlerts: Set<BackupSubscriptionFailedToRedeemAlertType> = [] {
didSet {
settingsButtonCreator.updateState(
showBackupsSubscriptionAlreadyRedeemedAvatarBadge: backupSubscriptionAlreadyRedeemedAlerts.contains(.avatarBadge),
showBackupsSubscriptionAlreadyRedeemedMenuItem: backupSubscriptionAlreadyRedeemedAlerts.contains(.menuItem),
showBackupsSubscriptionAlreadyRedeemedAvatarBadge: backupSubscriptionFailedToRedeemAlerts.contains(.avatarBadge),
showBackupsSubscriptionAlreadyRedeemedMenuItem: backupSubscriptionFailedToRedeemAlerts.contains(.menuItem),
)
}
}

View File

@ -27,10 +27,6 @@ class ChatListFYISheetCoordinator {
let probablyHasCurrentSubscription: Bool
}
struct BackupSubscriptionExpiringSoonWithPendingDownloads {
let warning: BackupSubscriptionIssueStore.IAPSubscriptionExpiringSoonWarning
}
struct BackupSubscriptionExpired {
enum SubscriptionType {
case iap
@ -53,7 +49,6 @@ class ChatListFYISheetCoordinator {
case badgeThanks(BadgeThanks)
case badgeIssue(BadgeIssue)
case badgeExpiration(BadgeExpiration)
case backupSubscriptionExpiringSoonWithPendingDownloads(BackupSubscriptionExpiringSoonWithPendingDownloads)
case backupSubscriptionExpired(BackupSubscriptionExpired)
case backupSubscriptionFailedToRenew(BackupSubscriptionFailedToRenew)
case keyTransparencySelfCheckFailed(KeyTransparencySelfCheckFailed)
@ -62,13 +57,11 @@ class ChatListFYISheetCoordinator {
}
private let backupArchiveErrorStore: BackupArchiveErrorStore
private let backupAttachmentDownloadStore: BackupAttachmentDownloadStore
private let backupExportJobRunner: BackupExportJobRunner
private let backupSubscriptionIssueStore: BackupSubscriptionIssueStore
private let dateProvider: DateProvider
private let db: DB
private let donationReceiptCredentialResultStore: DonationReceiptCredentialResultStore
private let donationSubscriptionManager: DonationSubscriptionManager
private let db: DB
private let keyTransparencyStore: KeyTransparencyStore
private let networkManager: NetworkManager
private let profileManager: ProfileManager
@ -76,25 +69,21 @@ class ChatListFYISheetCoordinator {
init(
backupArchiveErrorStore: BackupArchiveErrorStore,
backupAttachmentDownloadStore: BackupAttachmentDownloadStore,
backupExportJobRunner: BackupExportJobRunner,
backupSubscriptionIssueStore: BackupSubscriptionIssueStore,
dateProvider: @escaping DateProvider,
db: DB,
donationReceiptCredentialResultStore: DonationReceiptCredentialResultStore,
donationSubscriptionManager: DonationSubscriptionManager,
db: DB,
keyTransparencyStore: KeyTransparencyStore,
networkManager: NetworkManager,
profileManager: ProfileManager,
) {
self.backupArchiveErrorStore = backupArchiveErrorStore
self.backupAttachmentDownloadStore = backupAttachmentDownloadStore
self.backupExportJobRunner = backupExportJobRunner
self.backupSubscriptionIssueStore = backupSubscriptionIssueStore
self.dateProvider = dateProvider
self.db = db
self.donationReceiptCredentialResultStore = donationReceiptCredentialResultStore
self.donationSubscriptionManager = donationSubscriptionManager
self.db = db
self.keyTransparencyStore = keyTransparencyStore
self.networkManager = networkManager
self.profileManager = profileManager
@ -117,8 +106,6 @@ class ChatListFYISheetCoordinator {
// MARK: -
private func nextSheetToPresent(tx: DBReadTransaction) -> FYISheet? {
let now = dateProvider()
if let sheet = shouldShowSMSVerificationCodeSentSheet(tx: tx) {
return sheet
} else if let sheet = shouldShowBadgeThanksSheet(successMode: .oneTimeBoost, tx: tx) {
@ -141,15 +128,6 @@ class ChatListFYISheetCoordinator {
mostRecentSubscriptionPaymentMethod: donationSubscriptionManager.getMostRecentSubscriptionPaymentMethod(tx: tx),
probablyHasCurrentSubscription: donationSubscriptionManager.probablyHasCurrentSubscription(tx: tx),
))
} else if
let warning = backupSubscriptionIssueStore.shouldWarnIAPSubscriptionExpiringSoon(tx: tx),
warning.date < now,
// Only show the warning if there are downloads we still need to do.
backupAttachmentDownloadStore.hasAnyIncompleteDownloads(isThumbnail: false, tx: tx)
{
return .backupSubscriptionExpiringSoonWithPendingDownloads(FYISheet.BackupSubscriptionExpiringSoonWithPendingDownloads(
warning: warning,
))
} else if backupSubscriptionIssueStore.shouldWarnIAPSubscriptionExpired(tx: tx) {
return .backupSubscriptionExpired(FYISheet.BackupSubscriptionExpired(subscriptionType: .iap))
} else if backupSubscriptionIssueStore.shouldWarnTestFlightSubscriptionExpired(tx: tx) {
@ -282,8 +260,6 @@ class ChatListFYISheetCoordinator {
await _present(badgeIssue: badgeIssue, from: chatListViewController)
case .badgeExpiration(let badgeExpiration):
await _present(badgeExpiration: badgeExpiration, from: chatListViewController)
case .backupSubscriptionExpiringSoonWithPendingDownloads(let backupSubscriptionExpiringSoonWithPendingDownloads):
await _present(backupSubscriptionExpiringSoonWithPendingDownloads: backupSubscriptionExpiringSoonWithPendingDownloads, from: chatListViewController)
case .backupSubscriptionExpired(let backupSubscriptionExpired):
await _present(backupSubscriptionExpired: backupSubscriptionExpired, from: chatListViewController)
case .backupSubscriptionFailedToRenew(let backupSubscriptionFailedToRenew):
@ -456,31 +432,6 @@ class ChatListFYISheetCoordinator {
}
}
private func _present(
backupSubscriptionExpiringSoonWithPendingDownloads: FYISheet.BackupSubscriptionExpiringSoonWithPendingDownloads,
from chatListViewController: ChatListViewController,
) async {
let logger = PrefixedLogger(prefix: "[Backups]")
logger.info("Showing BackupSubscriptionExpiringSoonWithPendingDownloads FYI sheet.")
let warning = backupSubscriptionExpiringSoonWithPendingDownloads.warning
let sheet = BackupSubscriptionExpiringSoonWithPendingDownloadsHeroSheet(
iapSubscriptionExpiringSoonWarning: warning,
onDownloadBackupNow: {
chatListViewController.showAppSettings(mode: .backups(onAppearAction: .disableOptimizeLocalStorage))
},
)
chatListViewController.present(sheet, animated: true) { [self] in
db.write { tx in
backupSubscriptionIssueStore.setDidWarnIAPSubscriptionExpiringSoon(
warning: warning,
tx: tx,
)
}
}
}
private func _present(
backupSubscriptionExpired: FYISheet.BackupSubscriptionExpired,
from chatListViewController: ChatListViewController,
@ -594,63 +545,6 @@ extension ChatListViewController: BadgeIssueSheetDelegate {
// MARK: -
private class BackupSubscriptionExpiringSoonWithPendingDownloadsHeroSheet: HeroSheetViewController {
override var canBeDismissed: Bool { false }
init(
iapSubscriptionExpiringSoonWarning: BackupSubscriptionIssueStore.IAPSubscriptionExpiringSoonWarning,
onDownloadBackupNow: @escaping () -> Void,
) {
let title = switch iapSubscriptionExpiringSoonWarning {
case .firstWarning:
OWSLocalizedString(
"BACKUP_SUBSCRIPTION_EXPIRING_SOON_PENDING_DOWNLOADS_HERO_SHEET_FIRST_WARNING_TITLE",
comment: "Title for a sheet warning users that their Backup subscription is expiring soon, and they have pending downloads.",
)
case .secondWarning:
OWSLocalizedString(
"BACKUP_SUBSCRIPTION_EXPIRING_SOON_PENDING_DOWNLOADS_HERO_SHEET_SECOND_WARNING_TITLE",
comment: "Title for a sheet warning users that their Backup subscription is expiring soon, and they have pending downloads.",
)
}
super.init(
hero: .circleIcon(
icon: .backupErrorBold,
iconSize: 40,
tintColor: .Signal.red,
backgroundColor: UIColor(rgbHex: 0xFFDDDB),
),
title: title,
body: OWSLocalizedString(
"BACKUP_SUBSCRIPTION_EXPIRING_SOON_PENDING_DOWNLOADS_HERO_SHEET_BODY",
comment: "Body for a sheet warning users that their Backup subscription is expiring soon, and they have pending downloads.",
),
primaryButton: HeroSheetViewController.Button(
title: OWSLocalizedString(
"BACKUP_SUBSCRIPTION_EXPIRING_SOON_PENDING_DOWNLOADS_HERO_SHEET_PRIMARY_BUTTON",
comment: "Primary button for a sheet warning users that their Backup subscription is expiring soon, and they have pending downloads.",
),
action: { sheet in
sheet.dismiss(animated: true) {
onDownloadBackupNow()
}
},
),
secondaryButton: HeroSheetViewController.Button(
title: OWSLocalizedString(
"BACKUP_SUBSCRIPTION_EXPIRING_SOON_PENDING_DOWNLOADS_HERO_SHEET_SECONDARY_BUTTON",
comment: "Secondary button for a sheet warning users that their Backup subscription is expiring soon, and they have pending downloads.",
),
style: .secondaryDestructive,
action: .dismiss,
),
)
}
}
// MARK: -
private class BackupSubscriptionExpiredHeroSheet: HeroSheetViewController {
init(
subscriptionType: ChatListFYISheetCoordinator.FYISheet.BackupSubscriptionExpired.SubscriptionType,

View File

@ -111,8 +111,8 @@ extension ChatListViewController {
)
NotificationCenter.default.addObserver(
self,
selector: #selector(reconcileExperienceUpgrades),
name: .megaphoneStateDidChange,
selector: #selector(reloadExperienceUpgrades),
name: .inactivePrimaryDeviceChanged,
object: nil,
)
NotificationCenter.default.addObserver(
@ -262,8 +262,11 @@ extension ChatListViewController {
private func applicationDidBecomeActive(_ notification: NSNotification) {
AssertIsOnMainThread()
reconcileExperienceUpgrades()
updateShouldBeUpdatingView()
if !ExperienceUpgradeManager.presentNext(fromViewController: self) {
presentGetStartedBannerIfNecessary()
}
}
@objc
@ -341,6 +344,13 @@ extension ChatListViewController {
updateUsernameReminderView()
loadCoordinator.loadIfNecessary()
}
@objc
private func reloadExperienceUpgrades() {
AssertIsOnMainThread()
_ = ExperienceUpgradeManager.presentNext(fromViewController: self)
}
}
// MARK: - Notifications

View File

@ -273,17 +273,17 @@ extension ChatListViewController {
}
public func updateBackupSubscriptionFailedToRedeemAlertsWithSneakyTx() {
typealias BackupSubscriptionAlreadyRedeemedAlertType = CLVViewState.BackupSubscriptionAlreadyRedeemedAlertType
typealias BackupSubscriptionFailedToRedeemAlertType = CLVViewState.BackupSubscriptionFailedToRedeemAlertType
let db = DependenciesBridge.shared.db
let backupSubscriptionIssueStore = BackupSubscriptionIssueStore()
viewState.backupSubscriptionAlreadyRedeemedAlerts = db.read { tx in
var alerts = Set<BackupSubscriptionAlreadyRedeemedAlertType>()
if backupSubscriptionIssueStore.shouldShowIAPSubscriptionAlreadyRedeemedChatListBadge(tx: tx) {
viewState.backupSubscriptionFailedToRedeemAlerts = db.read { tx in
var alerts = Set<BackupSubscriptionFailedToRedeemAlertType>()
if backupSubscriptionIssueStore.shouldShowIAPSubscriptionFailedToRenewChatListBadge(tx: tx) {
alerts.insert(.avatarBadge)
}
if backupSubscriptionIssueStore.shouldShowIAPSubscriptionAlreadyRedeemedChatListMenuItem(tx: tx) {
if backupSubscriptionIssueStore.shouldShowIAPSubscriptionFailedToRenewChatListMenuItem(tx: tx) {
alerts.insert(.menuItem)
}
return alerts

View File

@ -233,16 +233,16 @@ public class ChatListViewController: OWSViewController, HomeTabViewController {
override public func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
defer {
hasEverAppeared = true
}
appReadiness.setUIIsReady()
presentGetStartedBannerIfNecessary()
reconcileExperienceUpgrades()
if getStartedBanner == nil, !hasEverPresentedExperienceUpgrade, ExperienceUpgradeManager.presentNext(fromViewController: self) {
hasEverPresentedExperienceUpgrade = true
} else if !hasEverAppeared {
presentGetStartedBannerIfNecessary()
}
requestReviewIfAppropriate()
showFYISheetIfNecessary()
viewState.searchResultsController.viewDidAppear(animated)
@ -253,8 +253,10 @@ public class ChatListViewController: OWSViewController, HomeTabViewController {
}
}
showFYISheetIfNecessary()
Task { try await self.checkForFailedServiceExtensionLaunches() }
hasEverAppeared = true
if viewState.multiSelectState.isActive {
showToolbar()
} else {
@ -359,26 +361,17 @@ public class ChatListViewController: OWSViewController, HomeTabViewController {
}
}
// MARK: - Experience Upgrades
@objc
func reconcileExperienceUpgrades() {
ExperienceUpgradeManager.reconcilePresentedExperienceUpgrade(fromViewController: self)
}
// MARK: - FYI sheets
@objc
func showFYISheetIfNecessary() {
let fyiSheetCoordinator = ChatListFYISheetCoordinator(
backupArchiveErrorStore: BackupArchiveErrorStore(),
backupAttachmentDownloadStore: BackupAttachmentDownloadStore(),
backupExportJobRunner: DependenciesBridge.shared.backupExportJobRunner,
backupSubscriptionIssueStore: BackupSubscriptionIssueStore(),
dateProvider: { Date() },
db: DependenciesBridge.shared.db,
donationReceiptCredentialResultStore: DependenciesBridge.shared.donationReceiptCredentialResultStore,
donationSubscriptionManager: DependenciesBridge.shared.donationSubscriptionManager,
db: DependenciesBridge.shared.db,
keyTransparencyStore: KeyTransparencyStore(),
networkManager: SSKEnvironment.shared.networkManagerRef,
profileManager: SSKEnvironment.shared.profileManagerRef,
@ -389,7 +382,7 @@ public class ChatListViewController: OWSViewController, HomeTabViewController {
}
}
// MARK: UI Components -
// MARK: UI Components
private lazy var emptyChatListView: UIView = {
let titleLabel = UILabel.explanationTextLabel(text: NSLocalizedString(
@ -912,7 +905,6 @@ public class ChatListViewController: OWSViewController, HomeTabViewController {
}
// In Production this will pop up at most 3 times per 365 days.
Logger.info("requesting review")
SKStoreReviewController.requestReview(in: windowScene)
Self.didRequestReview = true
}

View File

@ -167,7 +167,7 @@ class StoryPageViewController: UIPageViewController {
// and an ongoing paging drag transition but the scrollview isn't dragging) and resolve it
// by closing the transition out ourselves.
if
!pendingTransitionViewControllers.isEmpty,
pendingTransitionViewControllers.isEmpty.negated,
isTransitioningByScroll,
!isUserDraggingScrollView
{

View File

@ -125,7 +125,7 @@ extension StoryReplySheet {
tryToSendReaction(reaction)
}
func didSelectShowFullEmojiPicker() {
func didSelectAnyEmoji() {
// nil is intentional, the message is for showing other reactions already
// on the message, which we don't wanna do for stories.
let sheet = EmojiPickerSheet(message: nil) { [weak self] selectedEmoji in

View File

@ -11,14 +11,17 @@ class GroupStorySettingsViewController: OWSTableViewController2 {
init(thread: TSGroupThread) {
self.thread = thread
super.init()
title = thread.groupNameOrDefault
}
override func viewDidLoad() {
super.viewDidLoad()
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
updateBarButtons()
updateTableContents()
}
private func updateBarButtons() {
title = thread.groupNameOrDefault
navigationItem.rightBarButtonItem = .contextMenuButton(actions: [
UIAction(
@ -39,20 +42,17 @@ class GroupStorySettingsViewController: OWSTableViewController2 {
},
),
])
}
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(ContactTableViewCell.self, forCellReuseIdentifier: ContactTableViewCell.reuseIdentifier)
updateTableContents()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
updateTableContents()
}
private var isShowingAllViewers = false
private func updateTableContents(shouldReload: Bool = true) {
let contents = OWSTableContents()
defer { self.setContents(contents, shouldReload: shouldReload) }
@ -89,7 +89,10 @@ class GroupStorySettingsViewController: OWSTableViewController2 {
return UITableViewCell()
}
cell.configureWithSneakyTransaction(address: viewerAddress, localUserDisplayMode: .asLocalUser)
SSKEnvironment.shared.databaseStorageRef.read { transaction in
let configuration = ContactCellConfiguration(address: viewerAddress, localUserDisplayMode: .asLocalUser)
cell.configure(configuration: configuration, transaction: transaction)
}
return cell
}, actionBlock: { [weak self] in
@ -117,8 +120,8 @@ class GroupStorySettingsViewController: OWSTableViewController2 {
let rowLabel = UILabel()
rowLabel.text = CommonStrings.seeAllButton
rowLabel.textColor = .Signal.label
rowLabel.font = .dynamicTypeBodyClamped
rowLabel.textColor = Theme.primaryTextColor
rowLabel.font = OWSTableItem.primaryLabelFont
rowLabel.lineBreakMode = .byTruncatingTail
let contentRow = UIStackView(arrangedSubviews: [iconView, rowLabel])
@ -138,17 +141,13 @@ class GroupStorySettingsViewController: OWSTableViewController2 {
let deleteSection = OWSTableSection()
contents.add(deleteSection)
deleteSection.add(OWSTableItem(
customCellBlock: {
return OWSTableItem.buildCell(
itemName: OWSLocalizedString(
"GROUP_STORY_SETTINGS_DELETE_BUTTON",
comment: "Button to delete the story on the 'group story settings' view",
),
textColor: .Signal.red,
accessoryType: .none,
)
},
deleteSection.add(.actionItem(
withText: OWSLocalizedString(
"GROUP_STORY_SETTINGS_DELETE_BUTTON",
comment: "Button to delete the story on the 'group story settings' view",
),
textColor: .ows_accentRed,
accessibilityIdentifier: nil,
actionBlock: { [weak self] in
self?.deleteStoryWithConfirmation()
},

View File

@ -121,7 +121,10 @@ final class PrivateStorySettingsViewController: OWSTableViewController2 {
return UITableViewCell()
}
cell.configureWithSneakyTransaction(address: viewerAddress, localUserDisplayMode: .asLocalUser)
SSKEnvironment.shared.databaseStorageRef.read { transaction in
let configuration = ContactCellConfiguration(address: viewerAddress, localUserDisplayMode: .asLocalUser)
cell.configure(configuration: configuration, transaction: transaction)
}
return cell
}, actionBlock: { [weak self] in

View File

@ -253,7 +253,7 @@ private class StoryThreadCell: ContactTableViewCell {
// MARK: - ContactTableViewCell
func configure(conversationItem: StoryConversationItem, transaction: DBReadTransaction) {
var configuration: ContactCellView.Configuration
let configuration: ContactCellConfiguration
switch conversationItem.messageRecipient {
case .contact:
owsFailDebug("Unexpected recipient for story")
@ -268,21 +268,21 @@ private class StoryThreadCell: ContactTableViewCell {
owsFailDebug("Failed to find group thread")
return
}
configuration = ContactCellView.Configuration(groupThread: groupThread, localUserDisplayMode: .noteToSelf)
configuration = ContactCellConfiguration(groupThread: groupThread, localUserDisplayMode: .noteToSelf)
case .privateStory(_, let isMyStory):
if isMyStory {
guard let localAddress = DependenciesBridge.shared.tsAccountManager.localIdentifiersWithMaybeSneakyTransaction?.aciAddress else {
owsFailDebug("Unexpectedly missing local address")
return
}
configuration = ContactCellView.Configuration(address: localAddress, localUserDisplayMode: .asUser)
configuration = ContactCellConfiguration(address: localAddress, localUserDisplayMode: .asUser)
configuration.customName = conversationItem.title(transaction: transaction)
} else {
guard let image = conversationItem.image else {
owsFailDebug("Unexpectedly missing image for private story")
return
}
configuration = ContactCellView.Configuration(name: conversationItem.title(transaction: transaction), avatar: image)
configuration = ContactCellConfiguration(name: conversationItem.title(transaction: transaction), avatar: image)
}
}

View File

@ -301,9 +301,9 @@ class StoryInfoSheet: OWSTableViewController2, DatabaseChangeDelegate, UIAdaptiv
}
SSKEnvironment.shared.databaseStorageRef.read { transaction in
var configuration = ContactCellView.Configuration(address: address, localUserDisplayMode: .asUser)
let configuration = ContactCellConfiguration(address: address, localUserDisplayMode: .asUser)
configuration.forceDarkAppearance = true
configuration.accessory = self.buildContactCellAccessory(
configuration.accessoryView = self.buildAccessoryView(
text: accessoryText,
transaction: transaction,
)
@ -325,10 +325,10 @@ class StoryInfoSheet: OWSTableViewController2, DatabaseChangeDelegate, UIAdaptiv
})
}
private func buildContactCellAccessory(
private func buildAccessoryView(
text: String,
transaction: DBReadTransaction,
) -> ContactCellView.Accessory {
) -> ContactCellAccessoryView {
let label = CVLabel()
let labelConfig = CVLabelConfig.unstyledText(
text,
@ -338,7 +338,7 @@ class StoryInfoSheet: OWSTableViewController2, DatabaseChangeDelegate, UIAdaptiv
labelConfig.applyForRendering(label: label)
let labelSize = CVText.measureLabel(config: labelConfig, maxWidth: .greatestFiniteMagnitude)
return .init(accessoryView: label, size: labelSize)
return ContactCellAccessoryView(accessoryView: label, size: labelSize)
}
// MARK: - DatabaseChangeDelegate

View File

@ -57,7 +57,7 @@ class StoryListDataSource: NSObject {
}
var visibleStories: [StoryViewModel] {
return syncingModels.exposedModel.stories.filter { !$0.isHidden }
return syncingModels.exposedModel.stories.filter(\.isHidden.negated)
}
var hiddenStories: [StoryViewModel] {
@ -490,9 +490,9 @@ class StoryListDataSource: NSObject {
// * Any system story context with all its stories unviewed is always sorted at the top.
// * We then show viewed stories, sorted by when they were viewed, with the most recently viewed at the top
private static func sortStoryModels(lhs: StoryViewModel, rhs: StoryViewModel) -> Bool {
if lhs.isSystemStory, lhs.messages.allSatisfy({ !$0.isViewed }) {
if lhs.isSystemStory, lhs.messages.allSatisfy(\.isViewed.negated) {
return true
} else if rhs.isSystemStory, rhs.messages.allSatisfy({ !$0.isViewed }) {
} else if rhs.isSystemStory, rhs.messages.allSatisfy(\.isViewed.negated) {
return false
} else if
let lhsViewedTimestamp = lhs.latestMessageViewedTimestamp,
@ -706,11 +706,7 @@ class StoryListDataSource: NSObject {
extension StoryListDataSource: DatabaseChangeDelegate {
func databaseChangesDidUpdate(databaseChanges: DatabaseChanges) {
if databaseChanges.didUpdate(tableName: NicknameRecord.databaseTableName) {
reloadStories()
} else {
updateStories(forRowIds: databaseChanges.storyMessageRowIds)
}
updateStories(forRowIds: databaseChanges.storyMessageRowIds)
}
func databaseChangesDidUpdateExternally() {
@ -775,7 +771,7 @@ private class SyncingStoryListViewModel {
trueModel.wrappedValue = changes.newModel
DispatchQueue.main.async {
self._threadSafeStoryContexts.set(changes.newModel.stories.map(\.context))
self._threadSafeVisibleStoryContexts.set(changes.newModel.stories.lazy.filter { !$0.isHidden }.map(\.context))
self._threadSafeVisibleStoryContexts.set(changes.newModel.stories.lazy.filter(\.isHidden.negated).map(\.context))
self._threadSafeHiddenStoryContexts.set(changes.newModel.stories.lazy.filter(\.isHidden).map(\.context))
self.exposedModel = changes.newModel
sync(changes)
@ -843,7 +839,7 @@ private struct StoryListViewModel {
}
var visibleStories: [StoryViewModel] {
return stories.lazy.filter { !$0.isHidden }
return stories.lazy.filter(\.isHidden.negated)
}
// MARK: Hidden stories

View File

@ -241,7 +241,7 @@ class LongTextViewController: OWSViewController {
textView.textAlignment = displayableText.fullTextNaturalAlignment
linkItems = items
if !items.isEmpty {
if items.isEmpty.negated {
textView.addGestureRecognizer(UITapGestureRecognizer(
target: self,
action: #selector(didTapMessageTextView),

Some files were not shown because too many files have changed in this diff Show More