Compare commits

..

9 Commits

Author SHA1 Message Date
Max Radermacher
c26023b8f7 Feature flags for .production. 2026-05-26 12:54:37 -05:00
Max Radermacher
f9259b2061 Fix release notes 2026-05-26 12:54:25 -05:00
Max Radermacher
a928cc67bf Update translations 2026-05-26 12:33:07 -05:00
kate-signal
4497fe6396 show verification code requested sheet in CVC 2026-05-26 09:38:29 -04:00
Max Radermacher
b1279de2b7 Fix isRetryable for network failure SignalErrors 2026-05-22 16:07:10 -05:00
Pete Walters
b5ab48b3a5 Migrate ChangeNumber prompt from HeroSheet -> ActionSheet 2026-05-21 10:10:30 -05:00
sashaweiss-signal
b0fbd722e6 Run sync-translations 2026-05-20 14:50:54 -07:00
Sasha Weiss
a77c2a4efe Update two KT sheet strings 2026-05-20 14:49:34 -07:00
Max Radermacher
b868afea7c Feature flags for .beta. 2026-05-20 14:49:58 -05:00
573 changed files with 13365 additions and 15674 deletions

View File

@ -18,7 +18,7 @@ concurrency:
env:
# Path format pulled from https://github.com/actions/runner-images/blob/main/images/macos/macos-26-arm64-Readme.md#xcode
DEVELOPER_DIR: /Applications/Xcode_26.5.app
DEVELOPER_DIR: /Applications/Xcode_26.4.app
jobs:
build_and_test:
@ -31,7 +31,7 @@ jobs:
strategy:
matrix:
# Add additional Xcode versions here if necessary.
xcode: ["Xcode_26.5"]
xcode: ["Xcode_26.4"]
steps:
- name: Set Xcode version

View File

@ -34,7 +34,7 @@ concurrency:
env:
# Path format pulled from https://github.com/actions/runner-images/blob/main/images/macos/macos-26-arm64-Readme.md#xcode
DEVELOPER_DIR: /Applications/Xcode_26.5.app
DEVELOPER_DIR: /Applications/Xcode_26.4.app
# v0.60.1
swiftformat-ref: c8e50ff2cfc2eab46246c072a9ae25ab656c6ec3

View File

@ -28,7 +28,7 @@ concurrency:
env:
# Path format pulled from https://github.com/actions/runner-images/blob/main/images/macos/macos-26-arm64-Readme.md#xcode
DEVELOPER_DIR: /Applications/Xcode_26.5.app
DEVELOPER_DIR: /Applications/Xcode_26.4.app
# v1.36.1
swift-protobuf-ref: a008af1a102ff3dd6cc3764bb69bf63226d0f5f6

View File

@ -7,7 +7,7 @@ on:
env:
# Path format pulled from https://github.com/actions/runner-images/blob/main/images/macos/macos-26-arm64-Readme.md#xcode
DEVELOPER_DIR: /Applications/Xcode_26.5.app
DEVELOPER_DIR: /Applications/Xcode_26.4.app
jobs:
check-strings:

View File

@ -17,7 +17,7 @@ concurrency:
env:
# Path format pulled from https://github.com/actions/runner-images/blob/main/images/macos/macos-26-arm64-Readme.md#xcode
DEVELOPER_DIR: /Applications/Xcode_26.5.app
DEVELOPER_DIR: /Applications/Xcode_26.4.app
jobs:
build:

View File

@ -17,7 +17,7 @@ concurrency:
env:
# Path format pulled from https://github.com/actions/runner-images/blob/main/images/macos/macos-26-arm64-Readme.md#xcode
DEVELOPER_DIR: /Applications/Xcode_26.5.app
DEVELOPER_DIR: /Applications/Xcode_26.4.app
jobs:
build:

3
.gitignore vendored
View File

@ -36,9 +36,6 @@ Index/
*.sdsjson
Scripts/sds_codegen/sds-includes/*
# Logs
debuglogs/
/.idea
/.vscode

View File

@ -1 +1 @@
Xcode 26.5
Xcode 26.4.1

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

@ -7,16 +7,6 @@
objects = {
/* Begin PBXBuildFile section */
040506F82F7EEF3B0078B769 /* RemoteReleaseNotesFetchingManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 040506F72F7EEF290078B769 /* RemoteReleaseNotesFetchingManager.swift */; };
040506FA2F7EF4F50078B769 /* RemoteReleaseNotesFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 040506F92F7EF4ED0078B769 /* RemoteReleaseNotesFetcher.swift */; };
040506FC2F7FE3DB0078B769 /* RemoteAnnouncementModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 040506FB2F7FE3D50078B769 /* RemoteAnnouncementModel.swift */; };
040506FE2F7FE9170078B769 /* RemoteAnnouncementFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 040506FD2F7FE9130078B769 /* RemoteAnnouncementFetcher.swift */; };
040507012F804C240078B769 /* RemoteReleaseNotesService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 040506FF2F8049910078B769 /* RemoteReleaseNotesService.swift */; };
040507152F8063AB0078B769 /* RemoteReleaseNotesFetchingManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 040507142F8063A40078B769 /* RemoteReleaseNotesFetchingManagerTests.swift */; };
040507162F8063CE0078B769 /* RemoteReleaseNotesFetchingManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 040506F72F7EEF290078B769 /* RemoteReleaseNotesFetchingManager.swift */; };
040507172F8063E80078B769 /* RemoteMegaphoneFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = D98DD85D28EE53B00089333E /* RemoteMegaphoneFetcher.swift */; };
040507182F8064190078B769 /* RemoteReleaseNotesFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 040506F92F7EF4ED0078B769 /* RemoteReleaseNotesFetcher.swift */; };
040507192F8416090078B769 /* RemoteAnnouncementFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 040506FD2F7FE9130078B769 /* RemoteAnnouncementFetcher.swift */; };
04127D912F23B3B000B4E95B /* CVCapsuleLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04127D902F23B3B000B4E95B /* CVCapsuleLabel.swift */; };
041A5F072E05B3F900FAED05 /* BackupEnablementMegaphone.swift in Sources */ = {isa = PBXBuildFile; fileRef = 041A5F062E05B3F000FAED05 /* BackupEnablementMegaphone.swift */; };
041C24ED2DF782AF0065B685 /* OutgoingGroupUpdateMessageTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9BC9C6428B7C00A0077D442 /* OutgoingGroupUpdateMessageTest.swift */; };
@ -77,6 +67,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 +83,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 */; };
@ -533,6 +525,7 @@
45A1684D2A1C308800C2432D /* AudioPresentation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45A1684C2A1C308800C2432D /* AudioPresentation.swift */; };
45A2F005204473A3002E978A /* NewMessage.aifc in Resources */ = {isa = PBXBuildFile; fileRef = 45A2F004204473A3002E978A /* NewMessage.aifc */; };
45A3579827DAAC6A0051CE8B /* UserProfileTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45A3579727DAAC6A0051CE8B /* UserProfileTest.swift */; };
45B27B862037FFB400A539DF /* InternalFileBrowserViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45B27B852037FFB400A539DF /* InternalFileBrowserViewController.swift */; };
45B3680B2A1D75DF0067D05A /* AudioAllMediaPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45B3680A2A1D75DF0067D05A /* AudioAllMediaPresenter.swift */; };
45B74A742044AAB600CD42F8 /* aurora-quiet.aifc in Resources */ = {isa = PBXBuildFile; fileRef = 45B74A5B2044AAB300CD42F8 /* aurora-quiet.aifc */; };
45B74A752044AAB600CD42F8 /* synth-quiet.aifc in Resources */ = {isa = PBXBuildFile; fileRef = 45B74A5C2044AAB300CD42F8 /* synth-quiet.aifc */; };
@ -599,6 +592,7 @@
4CA485BB2232339F004B9E7D /* PhotoCaptureViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CA485BA2232339F004B9E7D /* PhotoCaptureViewController.swift */; };
4CB5F26720F6E1E2004D1B42 /* MessageActionsToolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CFF4C0920F55BBA005DA313 /* MessageActionsToolbar.swift */; };
4CB5F26920F7D060004D1B42 /* MessageActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB5F26820F7D060004D1B42 /* MessageActions.swift */; };
4CBBFE4A2306F5D300B37450 /* LogViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CBBFE492306F5D300B37450 /* LogViewController.swift */; };
4CC1ECF9211A47CE00CC13BE /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4CC1ECF8211A47CD00CC13BE /* StoreKit.framework */; settings = {ATTRIBUTES = (Weak, ); }; };
4CD675BE22E7BE35008010D2 /* MediaDismissAnimationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CD675BD22E7BE35008010D2 /* MediaDismissAnimationController.swift */; };
4CD675C522E7CF22008010D2 /* ConversationViewController+OWS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CD675C422E7CF22008010D2 /* ConversationViewController+OWS.swift */; };
@ -724,7 +718,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 +850,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 */; };
@ -1023,7 +1016,8 @@
6646573F2AC3B9190099DE1C /* MockRegistrationStateChangeManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6646573E2AC3B9190099DE1C /* MockRegistrationStateChangeManager.swift */; };
664657412AC4FB720099DE1C /* NotificationPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 664657402AC4FB720099DE1C /* NotificationPresenter.swift */; };
664657472ACB66630099DE1C /* TSAccountManagerObjcBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 664657462ACB66630099DE1C /* TSAccountManagerObjcBridge.swift */; };
66485EB32CD03F6400B8613F /* BackupArchiveErrorStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66485EB22CD03F5D00B8613F /* BackupArchiveErrorStore.swift */; };
66485EB02CCC515A00B8613F /* BackupArchiveInternalErrorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66485EAF2CCC50FA00B8613F /* BackupArchiveInternalErrorViewController.swift */; };
66485EB32CD03F6400B8613F /* BackupArchiveErrorPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66485EB22CD03F5D00B8613F /* BackupArchiveErrorPresenter.swift */; };
66485EB92CD17D6400B8613F /* DbRollbackTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66485EB82CD17D5D00B8613F /* DbRollbackTests.swift */; };
6649651C2BDC6EAD00E2DE98 /* AVAsset+Attachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6649651B2BDC6EAD00E2DE98 /* AVAsset+Attachment.swift */; };
6649651E2BDF169F00E2DE98 /* UIImage+Attachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6649651D2BDF169F00E2DE98 /* UIImage+Attachment.swift */; };
@ -1063,7 +1057,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 +1073,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 +1157,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 */; };
@ -1283,7 +1279,6 @@
66D31DAD2BC48E0100EAF735 /* OWSContactAddress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66D31DAC2BC48E0100EAF735 /* OWSContactAddress.swift */; };
66D31DAF2BC48E3A00EAF735 /* OWSContactName.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66D31DAE2BC48E3A00EAF735 /* OWSContactName.swift */; };
66D31F972E5E685300A1C82D /* InternalListMediaViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66D31F962E5E684E00A1C82D /* InternalListMediaViewController.swift */; };
66D31FA02E5E685300A1C82D /* InternalBackupSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66D31FA12E5E684E00A1C82D /* InternalBackupSettingsViewController.swift */; };
66D709E928E3999400B5013A /* StoryContextAssociatedData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66D709E828E3999400B5013A /* StoryContextAssociatedData.swift */; };
66D7B8FF2B9287F00005C98B /* AttachmentManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66D7B8FE2B9287F00005C98B /* AttachmentManager.swift */; };
66D7B9012B92889E0005C98B /* AttachmentManagerImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66D7B9002B92889E0005C98B /* AttachmentManagerImpl.swift */; };
@ -1451,6 +1446,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 */; };
@ -1621,6 +1618,7 @@
8864072C27F0DA38009916B6 /* StoryGroupReplyViewItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8864072B27F0DA37009916B6 /* StoryGroupReplyViewItem.swift */; };
8864072E27F0E8DF009916B6 /* StoryGroupReplyCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8864072D27F0E8DF009916B6 /* StoryGroupReplyCell.swift */; };
8864073127F21AD7009916B6 /* StoryReplyInputToolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8864073027F21AD7009916B6 /* StoryReplyInputToolbar.swift */; };
8868A089287F4514000E74A5 /* NewStorySheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8868A088287F4514000E74A5 /* NewStorySheet.swift */; };
8868A08A287F4551000E74A5 /* InteractiveSheetViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 880C2E01262A19DE006650B6 /* InteractiveSheetViewController.swift */; };
8868A08C287F4F81000E74A5 /* OWSTableSheetViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8868A08B287F4F81000E74A5 /* OWSTableSheetViewController.swift */; };
886BB3D225BA0C9D00079781 /* PreviewWallpaperViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88ABAB8E25B8BE3F0008C78A /* PreviewWallpaperViewController.swift */; };
@ -1673,7 +1671,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 +2699,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 +2739,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 +2878,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 +2894,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 */; };
@ -3177,7 +3172,6 @@
D9AE0AD929187F850063488B /* MessageSenderJobRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9AE0AD829187F850063488B /* MessageSenderJobRecord.swift */; };
D9AE0ADD2918B2960063488B /* JobRecord+Columns.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9AE0ADC2918B2960063488B /* JobRecord+Columns.swift */; };
D9B0AC7429EF42960070F31C /* TSInfoMessage+GroupUpdates+DisplayableGroupUpdateItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9B0AC7329EF42960070F31C /* TSInfoMessage+GroupUpdates+DisplayableGroupUpdateItem.swift */; };
D9B1A8BF2FB7B69200CE5FD3 /* FailIfThrowsRecordCursor.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9B1A8BE2FB7B68C00CE5FD3 /* FailIfThrowsRecordCursor.swift */; };
D9B2E1182E748E1900A823E4 /* OWSByteCountFormatStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9B2E1172E748DFB00A823E4 /* OWSByteCountFormatStyle.swift */; };
D9B8541229137C150058F97B /* JobRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9B8541129137C150058F97B /* JobRecord.swift */; };
D9B95A9629E6830B00D7CB95 /* JobRecordTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9B95A9429E682E900D7CB95 /* JobRecordTest.swift */; };
@ -3824,7 +3818,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 +3957,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 */; };
@ -4159,14 +4154,9 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
040506F72F7EEF290078B769 /* RemoteReleaseNotesFetchingManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteReleaseNotesFetchingManager.swift; sourceTree = "<group>"; };
040506F92F7EF4ED0078B769 /* RemoteReleaseNotesFetcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteReleaseNotesFetcher.swift; sourceTree = "<group>"; };
040506FB2F7FE3D50078B769 /* RemoteAnnouncementModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteAnnouncementModel.swift; sourceTree = "<group>"; };
040506FD2F7FE9130078B769 /* RemoteAnnouncementFetcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteAnnouncementFetcher.swift; sourceTree = "<group>"; };
040506FF2F8049910078B769 /* RemoteReleaseNotesService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteReleaseNotesService.swift; sourceTree = "<group>"; };
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 +4214,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>"; };
@ -4749,6 +4740,7 @@
45A3579727DAAC6A0051CE8B /* UserProfileTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfileTest.swift; sourceTree = "<group>"; };
45A663C41F92EC760027B59E /* GroupTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupTableViewCell.swift; sourceTree = "<group>"; };
45A6DAD51EBBF85500893231 /* ReminderView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReminderView.swift; sourceTree = "<group>"; };
45B27B852037FFB400A539DF /* InternalFileBrowserViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InternalFileBrowserViewController.swift; sourceTree = "<group>"; };
45B3680A2A1D75DF0067D05A /* AudioAllMediaPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioAllMediaPresenter.swift; sourceTree = "<group>"; };
45B74A5B2044AAB300CD42F8 /* aurora-quiet.aifc */ = {isa = PBXFileReference; lastKnownFileType = file; path = "aurora-quiet.aifc"; sourceTree = "<group>"; };
45B74A5C2044AAB300CD42F8 /* synth-quiet.aifc */ = {isa = PBXFileReference; lastKnownFileType = file; path = "synth-quiet.aifc"; sourceTree = "<group>"; };
@ -4831,6 +4823,7 @@
4CA485BA2232339F004B9E7D /* PhotoCaptureViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoCaptureViewController.swift; sourceTree = "<group>"; };
4CB5F26820F7D060004D1B42 /* MessageActions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageActions.swift; sourceTree = "<group>"; };
4CB93DC12180FF07004B9764 /* ProximityMonitoringManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProximityMonitoringManager.swift; sourceTree = "<group>"; };
4CBBFE492306F5D300B37450 /* LogViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogViewController.swift; sourceTree = "<group>"; };
4CC1ECF8211A47CD00CC13BE /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = System/Library/Frameworks/StoreKit.framework; sourceTree = SDKROOT; };
4CD675BD22E7BE35008010D2 /* MediaDismissAnimationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaDismissAnimationController.swift; sourceTree = "<group>"; };
4CD675C422E7CF22008010D2 /* ConversationViewController+OWS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ConversationViewController+OWS.swift"; sourceTree = "<group>"; };
@ -4990,7 +4983,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 +5115,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>"; };
@ -5294,7 +5286,8 @@
6646573E2AC3B9190099DE1C /* MockRegistrationStateChangeManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockRegistrationStateChangeManager.swift; sourceTree = "<group>"; };
664657402AC4FB720099DE1C /* NotificationPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationPresenter.swift; sourceTree = "<group>"; };
664657462ACB66630099DE1C /* TSAccountManagerObjcBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TSAccountManagerObjcBridge.swift; sourceTree = "<group>"; };
66485EB22CD03F5D00B8613F /* BackupArchiveErrorStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupArchiveErrorStore.swift; sourceTree = "<group>"; };
66485EAF2CCC50FA00B8613F /* BackupArchiveInternalErrorViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupArchiveInternalErrorViewController.swift; sourceTree = "<group>"; };
66485EB22CD03F5D00B8613F /* BackupArchiveErrorPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupArchiveErrorPresenter.swift; sourceTree = "<group>"; };
66485EB82CD17D5D00B8613F /* DbRollbackTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DbRollbackTests.swift; sourceTree = "<group>"; };
6649651B2BDC6EAD00E2DE98 /* AVAsset+Attachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AVAsset+Attachment.swift"; sourceTree = "<group>"; };
6649651D2BDF169F00E2DE98 /* UIImage+Attachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+Attachment.swift"; sourceTree = "<group>"; };
@ -5335,7 +5328,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 +5344,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 +5430,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>"; };
@ -5559,7 +5554,6 @@
66D31DAC2BC48E0100EAF735 /* OWSContactAddress.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OWSContactAddress.swift; sourceTree = "<group>"; };
66D31DAE2BC48E3A00EAF735 /* OWSContactName.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OWSContactName.swift; sourceTree = "<group>"; };
66D31F962E5E684E00A1C82D /* InternalListMediaViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InternalListMediaViewController.swift; sourceTree = "<group>"; };
66D31FA12E5E684E00A1C82D /* InternalBackupSettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InternalBackupSettingsViewController.swift; sourceTree = "<group>"; };
66D709E828E3999400B5013A /* StoryContextAssociatedData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoryContextAssociatedData.swift; sourceTree = "<group>"; };
66D7B8FE2B9287F00005C98B /* AttachmentManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentManager.swift; sourceTree = "<group>"; };
66D7B9002B92889E0005C98B /* AttachmentManagerImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentManagerImpl.swift; sourceTree = "<group>"; };
@ -5653,6 +5647,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>"; };
@ -5829,6 +5825,7 @@
8864072B27F0DA37009916B6 /* StoryGroupReplyViewItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoryGroupReplyViewItem.swift; sourceTree = "<group>"; };
8864072D27F0E8DF009916B6 /* StoryGroupReplyCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoryGroupReplyCell.swift; sourceTree = "<group>"; };
8864073027F21AD7009916B6 /* StoryReplyInputToolbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoryReplyInputToolbar.swift; sourceTree = "<group>"; };
8868A088287F4514000E74A5 /* NewStorySheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewStorySheet.swift; sourceTree = "<group>"; };
8868A08B287F4F81000E74A5 /* OWSTableSheetViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OWSTableSheetViewController.swift; sourceTree = "<group>"; };
886A58C8276A760600A1099B /* DonationSubscriptionManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DonationSubscriptionManager.swift; sourceTree = "<group>"; };
886A58C9276A760600A1099B /* DonationReceiptCredentialRedemptionJobQueue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DonationReceiptCredentialRedemptionJobQueue.swift; sourceTree = "<group>"; };
@ -5921,7 +5918,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 +6977,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 +7020,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 +7163,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 +7180,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>"; };
@ -7467,7 +7461,6 @@
D9AE0AD829187F850063488B /* MessageSenderJobRecord.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageSenderJobRecord.swift; sourceTree = "<group>"; };
D9AE0ADC2918B2960063488B /* JobRecord+Columns.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "JobRecord+Columns.swift"; sourceTree = "<group>"; };
D9B0AC7329EF42960070F31C /* TSInfoMessage+GroupUpdates+DisplayableGroupUpdateItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TSInfoMessage+GroupUpdates+DisplayableGroupUpdateItem.swift"; sourceTree = "<group>"; };
D9B1A8BE2FB7B68C00CE5FD3 /* FailIfThrowsRecordCursor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FailIfThrowsRecordCursor.swift; sourceTree = "<group>"; };
D9B2E1172E748DFB00A823E4 /* OWSByteCountFormatStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OWSByteCountFormatStyle.swift; sourceTree = "<group>"; };
D9B8541129137C150058F97B /* JobRecord.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JobRecord.swift; sourceTree = "<group>"; };
D9B91D8D2B17E2A600BCB11A /* GroupCallRecordRingUpdateDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupCallRecordRingUpdateDelegate.swift; sourceTree = "<group>"; };
@ -8140,7 +8133,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 +8273,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>"; };
@ -8464,14 +8458,6 @@
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
040507132F80639B0078B769 /* RemoteReleaseNotes */ = {
isa = PBXGroup;
children = (
040507142F8063A40078B769 /* RemoteReleaseNotesFetchingManagerTests.swift */,
);
path = RemoteReleaseNotes;
sourceTree = "<group>";
};
0436E4B12E5E2DC80011E125 /* Polls */ = {
isa = PBXGroup;
children = (
@ -9377,6 +9363,7 @@
340FC87A204DAC8C007AEB0F /* AppSettings */,
8809CE8822F93C0D00D38867 /* Attachment Keyboard */,
883A7FC1269F4BE700841DF9 /* Avatars */,
66485EB12CD03F3300B8613F /* BackupArchive */,
342FFE6C271EF580000AC89F /* Categories */,
F0B872B4269CF01E00D26481 /* ContextMenus */,
34D8C0221ED3673300188D7C /* DebugUI */,
@ -9900,22 +9887,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 = (
@ -10237,6 +10208,15 @@
path = DoubleTapToEdit;
sourceTree = "<group>";
};
6640DD612ACDD5CD00CE9A8C /* LocalStorage */ = {
isa = PBXGroup;
children = (
C14D49CD2D667F830033BA69 /* AccountKeyStore.swift */,
6640DD622ACDD5DE00CE9A8C /* SVRLocalStorage.swift */,
);
path = LocalStorage;
sourceTree = "<group>";
};
6645F30629BF8D1000B58EBD /* AccountAttributes */ = {
isa = PBXGroup;
children = (
@ -10270,6 +10250,14 @@
path = RegistrationStateChangeManager;
sourceTree = "<group>";
};
66485EB12CD03F3300B8613F /* BackupArchive */ = {
isa = PBXGroup;
children = (
66485EAF2CCC50FA00B8613F /* BackupArchiveInternalErrorViewController.swift */,
);
path = BackupArchive;
sourceTree = "<group>";
};
6649651A2BDC6E8D00E2DE98 /* Playback */ = {
isa = PBXGroup;
children = (
@ -10311,6 +10299,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 = (
@ -10353,20 +10355,15 @@
6673FF6A2978B5B900F96CFD /* SecureValueRecovery */ = {
isa = PBXGroup;
children = (
66C2B14F2A13F0CA008DDE72 /* MockSgxWebsocketConnectionFactory.swift */,
D9EDF2762E4D29F0001D4BEC /* AccountEntropyPool */,
6640DD612ACDD5CD00CE9A8C /* LocalStorage */,
66C2B13B2A0E9108008DDE72 /* SVR2 */,
D94AEB392D28837A00B03D7A /* MasterKey.swift */,
666654202AD0B03F00B23B32 /* MasterKeySyncManager.swift */,
66138FB5298326C7002E0CFE /* SecureValueRecovery.swift */,
662C440A2A156DF7001F83E2 /* SecureValueRecovery2Impl.swift */,
66C2B14C2A13E2C7008DDE72 /* SgxWebsocketConfigurator.swift */,
66C2B14A2A13E2AC008DDE72 /* SgxWebsocketConnection.swift */,
66C2B1482A13E2A0008DDE72 /* SgxWebsocketConnectionFactory.swift */,
66C2B13C2A0E9116008DDE72 /* SVR2AuthCredential.swift */,
50A26F192FB6991F000A2D8B /* SVR2PinHash.swift */,
669947B92A20129000E4DC0C /* SVR2Shims.swift */,
66C2B1552A1400E8008DDE72 /* SVR2WebsocketConfigurator.swift */,
66C2B1372A0DB6A9008DDE72 /* SVRAuthCredential.swift */,
6673FF6F2978C40300F96CFD /* SVRAuthCredentialStorage.swift */,
6673FF712979B33800F96CFD /* SVRAuthCredentialStorageImpl.swift */,
6640DD622ACDD5DE00CE9A8C /* SVRLocalStorage.swift */,
66C2B1352A0DB02E008DDE72 /* SVRUtil.swift */,
);
path = SecureValueRecovery;
@ -10709,6 +10706,18 @@
path = V2;
sourceTree = "<group>";
};
66C2B13B2A0E9108008DDE72 /* SVR2 */ = {
isa = PBXGroup;
children = (
662C440A2A156DF7001F83E2 /* SecureValueRecovery2Impl.swift */,
66C2B13C2A0E9116008DDE72 /* SVR2AuthCredential.swift */,
50A26F192FB6991F000A2D8B /* SVR2PinHash.swift */,
669947B92A20129000E4DC0C /* SVR2Shims.swift */,
66C2B1552A1400E8008DDE72 /* SVR2WebsocketConfigurator.swift */,
);
path = SVR2;
sourceTree = "<group>";
};
66C2B1422A12E043008DDE72 /* SecureValueRecovery */ = {
isa = PBXGroup;
children = (
@ -10718,6 +10727,25 @@
path = SecureValueRecovery;
sourceTree = "<group>";
};
66C2B1472A13E290008DDE72 /* SgxWebsocketConnection */ = {
isa = PBXGroup;
children = (
66C2B14E2A13F0BC008DDE72 /* Mocks */,
66C2B14C2A13E2C7008DDE72 /* SgxWebsocketConfigurator.swift */,
66C2B14A2A13E2AC008DDE72 /* SgxWebsocketConnection.swift */,
66C2B1482A13E2A0008DDE72 /* SgxWebsocketConnectionFactory.swift */,
);
path = SgxWebsocketConnection;
sourceTree = "<group>";
};
66C2B14E2A13F0BC008DDE72 /* Mocks */ = {
isa = PBXGroup;
children = (
66C2B14F2A13F0CA008DDE72 /* MockSgxWebsocketConnectionFactory.swift */,
);
path = Mocks;
sourceTree = "<group>";
};
66CD25572B0685CF00139E17 /* Archivers */ = {
isa = PBXGroup;
children = (
@ -11401,11 +11429,12 @@
isa = PBXGroup;
children = (
344A761024B366F4009D69A5 /* FlagsViewController.swift */,
66D31FA12E5E684E00A1C82D /* InternalBackupSettingsViewController.swift */,
665229882E218D53002C14A0 /* InternalDiskUsageViewController.swift */,
45B27B852037FFB400A539DF /* InternalFileBrowserViewController.swift */,
66D31F962E5E684E00A1C82D /* InternalListMediaViewController.swift */,
8862A55825F090C5005D65DB /* InternalSettingsViewController.swift */,
663883562D4C034F008EA898 /* InternalSQLClientViewController.swift */,
4CBBFE492306F5D300B37450 /* LogViewController.swift */,
344A761224B36C8C009D69A5 /* TestingViewController.swift */,
);
path = Internal;
@ -11439,10 +11468,7 @@
children = (
88A505FE23DBAE640005C012 /* UserInterface */,
88A505F323DA16E10005C012 /* ExperienceUpgradeManager.swift */,
040506FD2F7FE9130078B769 /* RemoteAnnouncementFetcher.swift */,
D98DD85D28EE53B00089333E /* RemoteMegaphoneFetcher.swift */,
040506F92F7EF4ED0078B769 /* RemoteReleaseNotesFetcher.swift */,
040506F72F7EEF290078B769 /* RemoteReleaseNotesFetchingManager.swift */,
);
path = Megaphones;
sourceTree = "<group>";
@ -11456,7 +11482,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 */,
@ -11613,7 +11639,6 @@
88E34F2522F269B600966CC2 /* StorageService */ = {
isa = PBXGroup;
children = (
F9C5CB12289453B200548EEE /* StorageService.swift */,
88E34F2622F269E900966CC2 /* StorageServiceManager.swift */,
88E34F2822F26CC100966CC2 /* StorageServiceProto+Sync.swift */,
D927372C2CD2DD0D00E15D95 /* StorageServiceRecordIkmMigrator.swift */,
@ -11640,7 +11665,6 @@
D99ABC712A3D0BAA0034CD3B /* QRCodes */,
50791B1B2D037A7800D747F8 /* RecipientPickers */,
661278052996BA6700A1D5A1 /* Registration */,
040507132F80639B0078B769 /* RemoteReleaseNotes */,
4C3EF8002109184A0007EBF7 /* SSKTests */,
D97046082E81D5B60034C05D /* Storage */,
E75DD3DC2810CD3500E32C36 /* subscriptions */,
@ -11790,6 +11814,7 @@
88F5D78B2880ABF900CE4D2D /* NewPrivateStoryConfirmViewController.swift */,
88F5D7892880A55E00CE4D2D /* NewPrivateStoryRecipientsViewController.swift */,
880FB3F228CC161800FA1C10 /* NewStoryHeaderView.swift */,
8868A088287F4514000E74A5 /* NewStorySheet.swift */,
66FBC4E228DA82AA00BD9E8B /* SelectMyStoryRecipientsViewController.swift */,
B99B155C2A71BA5200E26DAC /* StoryContextViewState.swift */,
88B6D67128076F37005D86EC /* StoryMessage+SignalUI.swift */,
@ -13343,11 +13368,9 @@
isa = PBXGroup;
children = (
D9C7CEB328EB8495001E87B6 /* ExperienceUpgrade.swift */,
F9C5CB5B289453B200548EEE /* ExperienceUpgradeFinder.swift */,
D9C7CECA28EBC09C001E87B6 /* ExperienceUpgradeManifest.swift */,
D97992AB2FC147C2002E79F3 /* ExperienceUpgradeStore.swift */,
040506FB2F7FE3D50078B769 /* RemoteAnnouncementModel.swift */,
D98DD85E28EE53B00089333E /* RemoteMegaphoneModel.swift */,
040506FF2F8049910078B769 /* RemoteReleaseNotesService.swift */,
);
path = Megaphones;
sourceTree = "<group>";
@ -13522,8 +13545,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 */,
@ -13668,7 +13689,7 @@
665C0D5F2ADF57D000539A37 /* BackupArchive+Shims.swift */,
661429182D35B9EA0043AA22 /* BackupArchive+Timestamp.swift */,
04BC94D12E061D7500446C52 /* BackupArchiveAttachmentByteCounter.swift */,
66485EB22CD03F5D00B8613F /* BackupArchiveErrorStore.swift */,
66485EB22CD03F5D00B8613F /* BackupArchiveErrorPresenter.swift */,
66232AE02CC0271F00AE6A76 /* BackupArchiveFullTextSearchIndexer.swift */,
665C0D5B2ADF538100539A37 /* BackupArchiveManager.swift */,
665C0D5D2ADF53E200539A37 /* BackupArchiveManagerImpl.swift */,
@ -13910,6 +13931,8 @@
children = (
D90AA32E2CC9616A00021CB0 /* Signal-Message-Backup-Tests */,
D90AA6182CC961ED00021CB0 /* BackupArchiveIntegrationTests.swift */,
041A5F082E05D3AC00FAED05 /* BackupEnablementReminderMegaphoneTests.swift */,
047A6DCF2E00B5640048EDF4 /* BackupKeyReminderMegaphoneTests.swift */,
04E66D432E00AB3A0059DBAC /* BackupSettingsStoreTests.swift */,
D9A36B922C7FEDA100CEC0E7 /* LineByLineStringDiff.swift */,
);
@ -14109,6 +14132,15 @@
path = DisappearingMessages;
sourceTree = "<group>";
};
D9EDF2762E4D29F0001D4BEC /* AccountEntropyPool */ = {
isa = PBXGroup;
children = (
C18D6FDF2D4D8FB40085E3B9 /* AccountEntropyPool.swift */,
D9EDF2772E4D2A1E001D4BEC /* AccountEntropyPoolManager.swift */,
);
path = AccountEntropyPool;
sourceTree = "<group>";
};
D9F6553029D6530B002A330A /* SDSCodableModel */ = {
isa = PBXGroup;
children = (
@ -14579,6 +14611,7 @@
F900F2DC27F25AB300431E09 /* DonationReceiptViewController.swift */,
D93EDC032AE9E3CD0004BDD9 /* DonationSettingsViewController+MySupport.swift */,
F9A8ACC6280A175E00AFC6A7 /* DonationSettingsViewController.swift */,
D96BE42D292EF04200E4FE1A /* PaypalButton.swift */,
);
path = Donations;
sourceTree = "<group>";
@ -14590,7 +14623,6 @@
D92EFDEB2F68EB7D0031D257 /* AttachmentBackfill */,
7255A4C32B98D5A800E95368 /* Attachments */,
720547F12B9C8F5E00E2CF2F /* Avatars */,
F9C5CA52289453B100548EEE /* Axolotl */,
665C0D5A2ADF537000539A37 /* Backups */,
F945FE482984795A00C835C7 /* Calls */,
D9C2D78529A80BE700D79715 /* ChangePhoneNumber */,
@ -14624,6 +14656,7 @@
046092252FBCD28300A8765F /* SafetyTips */,
50B791552E8B39230063E71E /* Search */,
6673FF6A2978B5B900F96CFD /* SecureValueRecovery */,
F9C5CB98289453B200548EEE /* Security */,
F9C5CAB4289453B200548EEE /* Spam */,
F9C5CA2F289453B100548EEE /* Storage */,
88E34F2522F269B600966CC2 /* StorageService */,
@ -14651,7 +14684,6 @@
F94261FF289B1B5400460798 /* Account */,
D92EFDED2F69B9D00031D257 /* AttachmentBackfill */,
50ED28002F0EDAFB00E57C54 /* Attachments */,
50DAF7E12FD87BFD00BE7430 /* Backups */,
F945FE4B298481D800C835C7 /* Calls */,
D985D86229B91C2B0087C90C /* ChangePhoneNumber */,
50E0198E2CC2491A0063EA48 /* Concurrency */,
@ -14740,7 +14772,6 @@
F9C5C950289453B100548EEE /* OWSAddToProfileWhitelistOfferMessage+SDS.swift */,
F9C5C997289453B100548EEE /* OWSAddToProfileWhitelistOfferMessage.h */,
F9C5C958289453B100548EEE /* OWSAddToProfileWhitelistOfferMessage.m */,
D944D2FC2FBCE9C00026FD21 /* OWSDecryptionPlaceholderExpirationJob.swift */,
F9C5C93B289453B100548EEE /* OWSIdentityManager.swift */,
F9C5C983289453B100548EEE /* OWSMessageDecrypter.swift */,
F9C5C973289453B100548EEE /* OWSMessageSend.swift */,
@ -15026,16 +15057,13 @@
isa = PBXGroup;
children = (
6646572F2AC369EB0099DE1C /* PhoneNumberDiscoverabilityManager */,
6659A0242A7C112700066AB7 /* PreKeys */,
661170BF2ABA458800A1B16D /* TSAccountManager */,
C18D6FDF2D4D8FB40085E3B9 /* AccountEntropyPool.swift */,
D9EDF2772E4D2A1E001D4BEC /* AccountEntropyPoolManager.swift */,
C14D49CD2D667F830033BA69 /* AccountKeyStore.swift */,
50F401CB2D483BF40094CA56 /* DeviceId.swift */,
50D6BDEE2ED6724600CC012E /* DeviceType.swift */,
D9F399AC2A95798A001599EC /* IdentityKeyChecker.swift */,
D9F399B12A96D65D001599EC /* IdentityKeyMismatchManager.swift */,
5033D46629D76BD0007FEADA /* LocalIdentifiers.swift */,
D94AEB392D28837A00B03D7A /* MasterKey.swift */,
72552EF32C9EF9E7008614AF /* OWSIdentity.swift */,
D9CAF74F2A0ACFF20049193A /* PniDistributionParameterBuilder.swift */,
C18E3C712A9FF65D003D1CF1 /* PniDistributionSyncMessage.swift */,
@ -15164,6 +15192,7 @@
F9C5CA2F289453B100548EEE /* Storage */ = {
isa = PBXGroup;
children = (
F9C5CA52289453B100548EEE /* AxolotlStore */,
F9C5CA31289453B100548EEE /* Database */,
667DEE562BC7148E00EFF32D /* MediaGallery */,
F9C5CA9B289453B100548EEE /* BaseModel.h */,
@ -15193,7 +15222,6 @@
F9B652C228D8E3DF006914CA /* DatabaseRecovery.swift */,
D9FF515B2F03A2A10011982F /* DBUInt64.swift */,
F9C5CA48289453B100548EEE /* DeepCopy.swift */,
D9B1A8BE2FB7B68C00CE5FD3 /* FailIfThrowsRecordCursor.swift */,
F9C5CA40289453B100548EEE /* GRDBDatabaseStorageAdapter.swift */,
F9C5CA47289453B100548EEE /* GRDBSchemaMigrator.swift */,
D9B95A9929E8918200D7CB95 /* InMemoryDB.swift */,
@ -15232,33 +15260,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 */ = {
@ -15305,14 +15332,13 @@
isa = PBXGroup;
children = (
F9C5CAD3289453B200548EEE /* API */,
66C2B1472A13E290008DDE72 /* SgxWebsocketConnection */,
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 +15415,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 +15436,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 */,
@ -15491,6 +15518,7 @@
7634F08C2A21963600BB93D5 /* Sounds.swift */,
F9613CDB2981F11400894B55 /* SqliteUtil.swift */,
F9C5CB47289453B200548EEE /* SSKPreferences.swift */,
F9C5CB12289453B200548EEE /* StorageService.swift */,
72DB95AD2C8C7C7B00FD2266 /* String+OWS.swift */,
F9C5CB09289453B200548EEE /* String+SSK.swift */,
668A010A2C2B602F007B8808 /* StringSanitizer.swift */,
@ -15538,6 +15566,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 = (
@ -17835,6 +17876,7 @@
88F5D78C2880ABF900CE4D2D /* NewPrivateStoryConfirmViewController.swift in Sources */,
88F5D78A2880A55E00CE4D2D /* NewPrivateStoryRecipientsViewController.swift in Sources */,
880FB3F328CC161800FA1C10 /* NewStoryHeaderView.swift in Sources */,
8868A089287F4514000E74A5 /* NewStorySheet.swift in Sources */,
3402AAAC271D9E180084CBAE /* NonContactTableViewCell.swift in Sources */,
507C07402F116E9200ECFEFA /* NormalizedImage.swift in Sources */,
3402AAAB271D9E180084CBAE /* OWSActionSheets.swift in Sources */,
@ -18051,6 +18093,7 @@
4C2F454F214C00E1004871FF /* AvatarTableViewCell.swift in Sources */,
32C584A825B81C6600256804 /* AvatarViewController.swift in Sources */,
B95A765C2B76C5BB00AA7E97 /* AvatarViewPresentationContextProvider.swift in Sources */,
66485EB02CCC515A00B8613F /* BackupArchiveInternalErrorViewController.swift in Sources */,
D932C0EB2E13AD3F00FEF9C3 /* BackupAttachmentDownloadTracker.swift in Sources */,
D93964B62E038C7B00094117 /* BackupAttachmentUploadTracker.swift in Sources */,
66A1F4E62E03641D0095DE4B /* BackupBGProcessingTaskRunner.swift in Sources */,
@ -18059,11 +18102,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 */,
@ -18449,12 +18490,12 @@
D9E43C072CC194140001536E /* IndividualCallViewController.swift in Sources */,
D97046062E81D4240034C05D /* InfoMessageGroupUpdateMigrator.swift in Sources */,
88BCCC8123837B7D00CE5FE6 /* InteractionReactionState.swift in Sources */,
66D31FA02E5E685300A1C82D /* InternalBackupSettingsViewController.swift in Sources */,
665229892E218D5F002C14A0 /* InternalDiskUsageViewController.swift in Sources */,
45B27B862037FFB400A539DF /* InternalFileBrowserViewController.swift in Sources */,
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 */,
@ -18474,6 +18515,7 @@
4C25768A23AD510800E0398D /* LoadMoreMessagesView.swift in Sources */,
D9E43C082CC194140001536E /* LocalVideoView.swift in Sources */,
88A9729422FB4D02004B4FBF /* LocationPicker.swift in Sources */,
4CBBFE4A2306F5D300B37450 /* LogViewController.swift in Sources */,
3496744F2076ACD000080B5F /* LongTextViewController.swift in Sources */,
88A941992409A391000E9700 /* LottieToggleButton.swift in Sources */,
5033D46929D7951F007FEADA /* MainAppContext.swift in Sources */,
@ -18569,6 +18611,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 */,
@ -18659,12 +18702,9 @@
F9E3006C299D76C3000323F8 /* RegistrationVerificationViewController.swift in Sources */,
F95D71A3299305C400ED3102 /* RegistrationViewUtil.swift in Sources */,
50EA40912E3A899F009CB839 /* RegistrationWebSocketManager.swift in Sources */,
040506FE2F7FE9170078B769 /* RemoteAnnouncementFetcher.swift in Sources */,
D997FA7628F8E3A2003C7B8B /* RemoteMegaphone.swift in Sources */,
509DC8DA2BCED88600375E86 /* RemoteMegaphoneFetcher.swift in Sources */,
55B753602D97304100CCC91C /* RemoteMuteToast.swift in Sources */,
040506FA2F7EF4F50078B769 /* RemoteReleaseNotesFetcher.swift in Sources */,
040506F82F7EEF3B0078B769 /* RemoteReleaseNotesFetchingManager.swift in Sources */,
D9E43C0D2CC194140001536E /* RemoteVideoView.swift in Sources */,
348433DF243CA94600C7F64A /* ReplaceAdminViewController.swift in Sources */,
F952C0A629C8DA5E00D93766 /* RequestAccountDataReportViewController.swift in Sources */,
@ -18844,11 +18884,6 @@
E16B440E2BBF242C00D2583E /* ReactionsModelTest.swift in Sources */,
661278082996BA8900A1D5A1 /* RegistrationCoordinatorTest.swift in Sources */,
6612780D2996BD0300A1D5A1 /* RegistrationCoordinatorTestShims.swift in Sources */,
040507192F8416090078B769 /* RemoteAnnouncementFetcher.swift in Sources */,
040507172F8063E80078B769 /* RemoteMegaphoneFetcher.swift in Sources */,
040507182F8064190078B769 /* RemoteReleaseNotesFetcher.swift in Sources */,
040507162F8063CE0078B769 /* RemoteReleaseNotesFetchingManager.swift in Sources */,
040507152F8063AB0078B769 /* RemoteReleaseNotesFetchingManagerTests.swift in Sources */,
F5C80FA22BE3F29F0028F76D /* RTCIceServerFetcherTest.swift in Sources */,
F963164B291AE06C00218FB7 /* ScrubbingLogFormatterTest.swift in Sources */,
505C2ED92997422D00C23FB2 /* SelfSignedIdentityTest.swift in Sources */,
@ -18984,7 +19019,7 @@
66F6D69E2C77E4C500EFAF75 /* BackupArchiveContactAttachmentArchiver.swift in Sources */,
66CD256E2B06E14F00139E17 /* BackupArchiveContactRecipientArchiver.swift in Sources */,
C1CA5F8E2BE2F21C00D733CA /* BackupArchiveDistributionListRecipientArchiver.swift in Sources */,
66485EB32CD03F6400B8613F /* BackupArchiveErrorStore.swift in Sources */,
66485EB32CD03F6400B8613F /* BackupArchiveErrorPresenter.swift in Sources */,
D91D9C8C2C3F06400009E4F7 /* BackupArchiveExpirationTimerChatUpdateArchiver.swift in Sources */,
66232AE12CC0272900AE6A76 /* BackupArchiveFullTextSearchIndexer.swift in Sources */,
D9A85DC22BE1719C003F7045 /* BackupArchiveGroupCallArchiver.swift in Sources */,
@ -19077,6 +19112,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,14 +19286,13 @@
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 */,
F9C5CC1D289453B300548EEE /* FailedMessagesJob.swift in Sources */,
7255A4C82B98DF3E00E95368 /* FailedStorySendDisplayController.swift in Sources */,
D9B1A8BF2FB7B69200CE5FD3 /* FailIfThrowsRecordCursor.swift in Sources */,
F9C5CE60289453B400548EEE /* FakeContactsManager.swift in Sources */,
F94BFA9528EBB0D800A5F34E /* FakeMessageSender.swift in Sources */,
F9C5CE54289453B400548EEE /* FakeStorageServiceManager.swift in Sources */,
@ -19381,6 +19416,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 +19581,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 +19703,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 */,
@ -19745,11 +19781,9 @@
6691E7F22996E9BC0032A68A /* RegistrationSessionManagerMock.swift in Sources */,
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 */,
5063B41E2C5432A30041CA51 /* ResolvableValue.swift in Sources */,
502C69742B06F0A400012867 /* Result.swift in Sources */,
50C0203E2CA4A7A500BDC4EF /* Retry.swift in Sources */,
@ -19824,6 +19858,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 +20129,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 +20227,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()
@ -148,6 +148,9 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
debugLogger.enableFileLogging(appContext: mainAppContext, canLaunchInBackground: true)
DebugLogger.configureSwiftLogging()
if DebugFlags.audibleErrorLogging {
debugLogger.enableErrorReporting()
}
Logger.warn("Launching…")
defer { Logger.info("Launched.") }
@ -369,14 +372,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()
}
@ -398,6 +400,7 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
let dataMigrationContinuation = globalsContinuation.initGlobals(
appContext: launchContext.appContext,
appReadiness: appReadiness,
backupArchiveErrorPresenterFactory: BackupArchiveErrorPresenterFactoryInternal(),
deviceBatteryLevelManager: DeviceBatteryLevelManagerImpl(),
deviceSleepManager: launchContext.deviceSleepManager,
paymentsEvents: PaymentsEventsMainApp(),
@ -643,16 +646,16 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
}
}
let remoteReleaseNotesFetchingManager = RemoteReleaseNotesFetchingManager(
db: DependenciesBridge.shared.db,
remoteReleaseNotesService: DependenciesBridge.shared.remoteReleaseNotesService,
let remoteMegaphoneFetcher = RemoteMegaphoneFetcher(
databaseStorage: SSKEnvironment.shared.databaseStorageRef,
signalService: SSKEnvironment.shared.signalServiceRef,
)
cron.schedulePeriodically(
uniqueKey: .fetchMegaphones,
approximateInterval: 3 * .day,
mustBeRegistered: false,
mustBeConnected: true,
operation: { try await remoteReleaseNotesFetchingManager.syncRemoteReleaseNotes() },
operation: { try await remoteMegaphoneFetcher.syncRemoteMegaphones() },
)
appReadiness.runNowOrWhenAppDidBecomeReadyAsync {
@ -717,7 +720,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()
@ -779,27 +781,6 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
operation: { try await blockingManager.syncBlockListIfNecessary(force: false) },
)
let svr = DependenciesBridge.shared.svr
// We must refresh our SVR2 credentials periodically. We typically do this
// when updating to a new version, but we want to refresh it after 14 days
// if we haven't upgraded.
cron.schedulePeriodically(
uniqueKey: .refreshSVRCredentials,
approximateInterval: 14 * .day,
mustBeRegistered: true,
mustBeDeviceType: .primary,
mustBeConnected: true,
operation: { try await svr.refreshCredentialsIfNecessary() },
)
cron.scheduleFrequently(
mustBeRegistered: true,
mustBeDeviceType: .primary,
mustBeConnected: true,
operation: { try await svr.refreshBackupIfNecessary() },
)
// Warm the "available emoji" cache, intentionally off the main thread.
Task.detached {
Emoji.warmAvailableCache()
@ -809,7 +790,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 {
@ -1252,20 +1233,14 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
switch action {
case .submitDebugLogsAndCrash:
addSubmitDebugLogsAction {
DebugLogs(dumper: logDumper).promptToSubmitLogs(
from: viewController,
supportTag: supportTag,
) {
DebugLogs.submitLogs(supportTag: supportTag, dumper: logDumper) {
owsFail("Exiting after submitting debug logs")
}
}
case .submitDebugLogsAndLaunchApp(let window, let launchContext):
addSubmitDebugLogsAction { [unowned window] in
DebugLogs(dumper: logDumper).promptToSubmitLogs(
from: viewController,
supportTag: supportTag,
) {
DebugLogs.submitLogs(supportTag: supportTag, dumper: logDumper) {
ignoreErrorAndLaunchApp(in: window, launchContext: launchContext)
}
}
@ -1392,7 +1367,7 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
}
}
refreshConnection(isAppActive: true)
refreshConnection(isAppActive: true, shouldRunCron: true)
// Every time we become active...
if registeredState != nil {
@ -1460,7 +1435,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 +1443,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 +1462,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 +1745,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 +1764,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 +1776,6 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
}
callService.initiateCall(to: callTarget, isVideo: isVideo)
}
return true
}
@ -1829,12 +1790,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 +1908,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 +1919,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
@ -287,7 +284,11 @@ public class AppEnvironment: NSObject {
// Things that should run on either the primary or linked devices.
if let registeredState, registeredState.isPrimary {
Task {
await avatarDefaultColorStorageServiceMigrator.performMigrationIfNecessary()
do {
try await avatarDefaultColorStorageServiceMigrator.performMigrationIfNecessary()
} catch {
Logger.warn("Couldn't perform avatar default color migration: \(error)")
}
}
Task {
@ -328,6 +329,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

@ -128,7 +128,6 @@ public class SignalApp {
owsFailDebug("Missing conversationSplitViewController.")
return
}
conversationSplitViewController.showAppSettingsWithMode(mode, completion: completion)
}

View File

@ -32,8 +32,8 @@ struct AvatarDefaultColorStorageServiceMigrator {
self.threadStore = threadStore
}
func performMigrationIfNecessary() async {
await db.awaitableWrite { tx in
func performMigrationIfNecessary() async throws {
try await db.awaitableWrite { tx in
if kvStore.hasValue(StoreKeys.hasEnqueuedMigrationKey, transaction: tx) {
return
}
@ -46,14 +46,15 @@ struct AvatarDefaultColorStorageServiceMigrator {
}
var groupV2MasterKeys = [GroupMasterKey]()
threadStore.enumerateGroupThreads(tx: tx) { groupThread in
if
try threadStore.enumerateGroupThreads(tx: tx) { groupThread in
guard
let groupModelV2 = groupThread.groupModel as? TSGroupModelV2,
let groupMasterKey = try? groupModelV2.masterKey()
{
groupV2MasterKeys.append(groupMasterKey)
else {
return true
}
groupV2MasterKeys.append(groupMasterKey)
return true
}

View File

@ -225,6 +225,7 @@ final class BackupDisablingManager {
accountEntropyPoolManager.setAccountEntropyPool(
newAccountEntropyPool: try! AccountEntropyPool(key: aepBeingRotatedString),
disablePIN: false,
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
@ -121,7 +120,7 @@ class BackupSettingsViewController:
self.onAppearAction = onAppearAction
switch onAppearAction {
case nil, .presentWelcomeToBackupsSheet, .disableOptimizeLocalStorage:
case .presentWelcomeToBackupsSheet, nil:
break
case .automaticallyStartBackup(let completion):
self.onBackupComplete = completion
@ -180,8 +179,6 @@ class BackupSettingsViewController:
presentWelcomeToBackupsSheet()
case .automaticallyStartBackup:
performManualBackup()
case .disableOptimizeLocalStorage:
setOptimizeLocalStorage(false)
}
}
@ -621,87 +618,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 +1018,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 +1085,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 +1141,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 +1158,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 +1361,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.",
@ -1468,6 +1390,7 @@ class BackupSettingsViewController:
accountEntropyPoolManager.setAccountEntropyPool(
newAccountEntropyPool: newCandidateAEP,
disablePIN: false,
tx: tx,
)
case .disabling, .free, .paid, .paidExpiringSoon, .paidAsTester:
@ -1710,21 +1633,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 +1915,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 BuildFlags.Backups.showOptimizeMedia {
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 BuildFlags.Backups.showOptimizeMedia {
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 {
@ -3279,8 +3209,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

@ -71,7 +71,6 @@ struct BackupOnboardingIntroView: View {
Image(.backupsLogo)
.frame(width: 80, height: 80)
.accessibilityHidden(true)
Spacer().frame(height: 16)
HStack {

View File

@ -192,16 +192,13 @@ class CallQualitySurveyManager {
return proto
}
func submit(
rating: CallQualitySurvey.Rating,
logsToSubmit logs: DebugLogs?,
) {
func submit(rating: CallQualitySurvey.Rating, shouldSubmitDebugLogs: Bool) {
var proto = buildProto(rating: rating)
Task {
if let logs {
if shouldSubmitDebugLogs {
do {
let debugLogURL = try await logs.uploadLogs()
let debugLogURL = try await DebugLogs.uploadLogs(dumper: .fromGlobals())
proto.debugLogURL = debugLogURL.absoluteString
} catch {
logger.error("Failed to submit debug logs: \(error)")

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

@ -2302,14 +2302,23 @@ private extension CallsListViewController {
}()
private func makeStartCallButton(viewModel: CallViewModel) -> UIButton {
var config = UIButton.Configuration.gray()
config.cornerStyle = .capsule
config.background.backgroundInsets = .init(margin: 2)
config.baseBackgroundColor = UIColor.Signal.tertiaryFill
config.baseForegroundColor = UIColor.Signal.label
let icon: ThemeIcon = switch viewModel.medium {
case .audio:
.buttonVoiceCall
case .video, .link:
.buttonVideoCall
}
config.image = Theme.iconImage(icon)
let button = UIButton(
configuration: .roundGray(image: Theme.iconImage(icon)),
configuration: config,
primaryAction: UIAction { [weak self] _ in
self?.detailsTapped(viewModel: viewModel)
},

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

@ -16,12 +16,10 @@ final class SurveyDebugLogViewController: CallQualitySurveySheetViewController {
private let tableViewController = OWSTableViewController2()
private var shouldSubmitDebugLogs = false
private var logs: DebugLogs
private let rating: CallQualitySurvey.Rating
init(rating: CallQualitySurvey.Rating) {
self.logs = DebugLogs(dumper: .fromGlobals())
self.rating = rating
super.init(nibName: nil, bundle: nil)
}
@ -131,8 +129,7 @@ final class SurveyDebugLogViewController: CallQualitySurveySheetViewController {
let container = UIView()
let textView = LinkingTextView { [weak self] in
guard let self else { return }
self.logs.showPreview(from: self)
self?.showDebugLogPreview()
}
textView.attributedText = .composed(of: [
OWSLocalizedString(
@ -196,6 +193,12 @@ final class SurveyDebugLogViewController: CallQualitySurveySheetViewController {
present(nav, animated: true)
}
private func showDebugLogPreview() {
let vc = DebugLogPreviewViewController()
let nav = OWSNavigationController(rootViewController: vc)
present(nav, animated: true)
}
override func customSheetHeight() -> CGFloat? {
let headerHeight = headerContainer.height
let collectionViewHeight = tableViewController.tableView.contentSize.height + tableViewController.tableView.contentInset.totalHeight
@ -206,7 +209,7 @@ final class SurveyDebugLogViewController: CallQualitySurveySheetViewController {
private func submit() {
sheetNav?.submit(
rating: self.rating,
logsToSubmit: shouldSubmitDebugLogs ? logs : nil,
shouldSubmitDebugLogs: self.shouldSubmitDebugLogs,
)
}
}

View File

@ -82,13 +82,10 @@ final class CallQualitySurveyNavigationController: UINavigationController {
pushViewController(vc, animated: false)
}
func submit(
rating: CallQualitySurvey.Rating,
logsToSubmit: DebugLogs?,
) {
func submit(rating: CallQualitySurvey.Rating, shouldSubmitDebugLogs: Bool) {
callQualitySurveyManager.submit(
rating: rating,
logsToSubmit: logsToSubmit,
shouldSubmitDebugLogs: shouldSubmitDebugLogs,
)
let host = presentingViewController
dismiss(animated: true) {

View File

@ -159,10 +159,11 @@ 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
BuildFlags.MemberLabel.display,
let groupThread = self?.groupThread,
let senderAci = address.aci,
let memberLabelString = groupThread.groupModel.groupMembership.memberLabel(for: senderAci)?.labelForRendering(),

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()
}
}
}
@ -192,19 +194,14 @@ class CVAttachmentProgressView: ManualLayoutView {
applyState(.tapToDownload, animated: animateStateChange)
case .enqueuedOrDownloading:
applyState(.unknownProgress, animated: animateStateChange)
NotificationCenter.default.addObserver(
self,
selector: #selector(processDownloadNotification(notification:)),
name: AttachmentDownloads.attachmentDownloadProgressNotification,
object: nil,
)
}
NotificationCenter.default.addObserver(
self,
selector: #selector(processDownloadNotification(notification:)),
name: AttachmentDownloads.attachmentDownloadProgressNotification,
object: nil,
)
NotificationCenter.default.addObserver(
self,
selector: #selector(processDownloadStoppedNotification(notification:)),
name: AttachmentDownloads.attachmentDownloadStoppedNotification,
object: nil,
)
}
}
@ -363,22 +360,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

@ -65,7 +65,7 @@ class CVComponentCollapseSet: CVComponentBase, CVRootComponent {
let hasWallpaper = conversationStyle.hasWallpaper
let wallpaperModeHasChanged = hasWallpaper != componentView.hasWallpaper
let isReusing = componentView.rootView.superview != nil
&& componentView.innerStack.superview != nil
&& componentView.label.superview != nil
&& !wallpaperModeHasChanged
if !isReusing {
@ -75,32 +75,19 @@ class CVComponentCollapseSet: CVComponentBase, CVRootComponent {
componentView.hasWallpaper = hasWallpaper
labelConfig.applyForRendering(label: componentView.label)
chevronConfig.applyForRendering(label: componentView.chevronLabel)
if isReusing {
componentView.innerStack.configureForReuse(
config: innerStackConfig,
cellMeasurement: cellMeasurement,
measurementKey: Self.measurementKey_innerStack,
)
componentView.outerStack.configureForReuse(
config: outerStackConfig,
cellMeasurement: cellMeasurement,
measurementKey: Self.measurementKey_outerStack,
)
} else {
componentView.innerStack.configure(
config: innerStackConfig,
cellMeasurement: cellMeasurement,
measurementKey: Self.measurementKey_innerStack,
subviews: [componentView.label, componentView.chevronContainer],
)
componentView.outerStack.configure(
config: outerStackConfig,
cellMeasurement: cellMeasurement,
measurementKey: Self.measurementKey_outerStack,
subviews: [componentView.innerStack],
subviews: [componentView.label],
)
let bubbleView: UIView
@ -123,20 +110,13 @@ class CVComponentCollapseSet: CVComponentBase, CVRootComponent {
}
componentView.outerStack.addSubview(bubbleView)
componentView.outerStack.sendSubviewToBack(bubbleView)
componentView.outerStack.addLayoutBlock { [innerStack = componentView.innerStack] _ in
bubbleView.frame = innerStack.frame.inset(by: Self.backgroundLayoutInsets)
}
componentView.innerStack.addLayoutBlock { [chevronContainer = componentView.chevronContainer, chevronLabel = componentView.chevronLabel] _ in
chevronLabel.bounds.size = chevronContainer.bounds.size
chevronLabel.center = CGPoint(x: chevronContainer.bounds.midX, y: chevronContainer.bounds.midY)
// This seemed easier than adding an entirely new ManualStackView
// just to constrain the label and background to
componentView.outerStack.addLayoutBlock { [label = componentView.label] _ in
bubbleView.frame = label.frame.inset(by: Self.backgroundLayoutInsets)
}
}
componentView.isShowingExpanded = collapseSet.isExpanded
componentView.chevronLabel.transform = collapseSet.isExpanded
? CGAffineTransform(rotationAngle: -.pi)
: .identity
if
hasWallpaper,
let wallpaperBlurView = componentView.wallpaperBlurView
@ -148,7 +128,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
@ -159,35 +147,6 @@ class CVComponentCollapseSet: CVComponentBase, CVRootComponent {
componentView: CVComponentView,
renderItem: CVRenderItem,
) -> Bool {
if let componentView = componentView as? CVComponentViewCollapseSet {
let wasExpanded = componentView.isShowingExpanded
let willBeExpanded = !wasExpanded
let expandedRotation: CGFloat = -.pi
let isRTL = componentView.chevronLabel.effectiveUserInterfaceLayoutDirection == .rightToLeft
let fromAngle: CGFloat
let toAngle: CGFloat
if willBeExpanded {
fromAngle = 0
toAngle = isRTL ? CGFloat.pi : -CGFloat.pi
} else {
fromAngle = expandedRotation
toAngle = isRTL ? -2 * CGFloat.pi : 0
}
componentView.isShowingExpanded = willBeExpanded
componentView.chevronLabel.transform = willBeExpanded
? CGAffineTransform(rotationAngle: expandedRotation)
: .identity
componentView.outerStack.accessibilityHint = accessibilityHint(isExpanded: willBeExpanded)
let animation = CABasicAnimation(keyPath: "transform.rotation.z")
animation.fromValue = fromAngle
animation.toValue = toAngle
animation.duration = 0.2
animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
componentView.chevronLabel.layer.add(animation, forKey: "chevronRotation")
}
componentDelegate.didTapCollapseSet(collapseSetId: interaction.uniqueId)
return true
}
@ -195,7 +154,6 @@ class CVComponentCollapseSet: CVComponentBase, CVRootComponent {
// MARK: - Measurement
fileprivate static let measurementKey_outerStack = "CVComponentCollapseSet.outerStack"
fileprivate static let measurementKey_innerStack = "CVComponentCollapseSet.innerStack"
func measure(maxWidth: CGFloat, measurementBuilder: CVCellMeasurement.Builder) -> CGSize {
owsAssertDebug(maxWidth > 0)
@ -203,38 +161,18 @@ class CVComponentCollapseSet: CVComponentBase, CVRootComponent {
0,
maxWidth - outerStackConfig.layoutMargins.totalWidth,
)
let chevronSize = CVText.measureLabel(config: chevronConfig, maxWidth: availableWidth)
let labelMaxWidth = max(0, availableWidth - chevronSize.width - innerStackConfig.spacing)
let labelSize = CVText.measureLabel(config: labelConfig, maxWidth: labelMaxWidth)
let innerMeasurement = ManualStackView.measure(
config: innerStackConfig,
measurementBuilder: measurementBuilder,
measurementKey: Self.measurementKey_innerStack,
subviewInfos: [
labelSize.asManualSubviewInfo(hasFixedWidth: true),
chevronSize.asManualSubviewInfo(hasFixedSize: true),
],
)
let labelSize = CVText.measureLabel(config: labelConfig, maxWidth: availableWidth)
let outerMeasurement = ManualStackView.measure(
config: outerStackConfig,
measurementBuilder: measurementBuilder,
measurementKey: Self.measurementKey_outerStack,
subviewInfos: [innerMeasurement.measuredSize.asManualSubviewInfo(hasFixedWidth: true)],
subviewInfos: [labelSize.asManualSubviewInfo(hasFixedWidth: true)],
)
return outerMeasurement.measuredSize
}
// MARK: - Layout
private var innerStackConfig: CVStackViewConfig {
CVStackViewConfig(
axis: .horizontal,
alignment: .center,
spacing: 4,
layoutMargins: .zero,
)
}
private var outerStackConfig: CVStackViewConfig {
CVStackViewConfig(
axis: .vertical,
@ -295,6 +233,7 @@ class CVComponentCollapseSet: CVComponentBase, CVRootComponent {
)
let nbsp = SignalSymbol.LeadingCharacter.nonBreakingSpace.rawValue
let chevron: SignalSymbol = collapseSet.isExpanded ? .chevronUp : .chevronDown
let result = NSMutableAttributedString()
result.append(leadingIcon.attributedString(
@ -303,33 +242,18 @@ class CVComponentCollapseSet: CVComponentBase, CVRootComponent {
attributes: [.foregroundColor: UIColor.Signal.label],
))
result.append(NSAttributedString(
string: "\(nbsp)\(labelText)",
string: "\(nbsp)\(labelText)\(nbsp)",
attributes: [
.font: labelFont,
.foregroundColor: UIColor.Signal.label,
],
))
return result
}
private var chevronConfig: CVLabelConfig {
CVLabelConfig(
text: .attributedText(chevronAttributedString),
displayConfig: .forUnstyledText(font: labelFont, textColor: .Signal.label),
font: labelFont,
textColor: .Signal.label,
numberOfLines: 1,
lineBreakMode: .byClipping,
textAlignment: .center,
)
}
private var chevronAttributedString: NSAttributedString {
SignalSymbol.chevronDown.attributedString(
result.append(chevron.attributedString(
for: .footnote,
clamped: false,
attributes: [.foregroundColor: UIColor.Signal.label],
)
))
return result
}
private var titleString: String {
@ -340,18 +264,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,
@ -410,14 +322,9 @@ class CVComponentCollapseSet: CVComponentBase, CVRootComponent {
class CVComponentViewCollapseSet: NSObject, CVComponentView {
fileprivate let outerStack = ManualStackView(name: "collapseSet.outerStack")
fileprivate let innerStack = ManualStackView(name: "collapseSet.innerStack")
fileprivate let label = CVLabel()
fileprivate let chevronContainer = UIView()
fileprivate let chevronLabel = CVLabel()
fileprivate let solidBackgroundView = UIView()
fileprivate var isShowingExpanded = false
fileprivate var wallpaperBlurView: CVWallpaperBlurView?
fileprivate func ensureWallpaperBlurView() -> CVWallpaperBlurView {
if let wallpaperBlurView = self.wallpaperBlurView {
@ -434,24 +341,14 @@ class CVComponentCollapseSet: CVComponentBase, CVRootComponent {
var rootView: UIView { outerStack }
override init() {
super.init()
chevronContainer.addSubview(chevronLabel)
}
func setIsCellVisible(_ isCellVisible: Bool) {}
func reset() {
label.reset()
chevronLabel.reset()
chevronLabel.transform = .identity
chevronLabel.layer.removeAnimation(forKey: "chevronRotation")
isShowingExpanded = false
solidBackgroundView.backgroundColor = nil
wallpaperBlurView?.removeFromSuperview()
wallpaperBlurView = nil
hasWallpaper = false
innerStack.reset()
outerStack.reset()
}
}

View File

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

View File

@ -493,9 +493,6 @@ public struct CVComponentState: Equatable {
let detailsText: NSAttributedString?
/// For mutual groups, lack thereof and note-to-self description.
let mutualGroupsText: NSAttributedString?
/// Populated if `mutualGroupsText` is not suitable for a11y, for
/// example if it embeds an image.
let mutualGroupsAccessibilityText: String?
let threadType: SafetyTipsType
let shouldShowSafetyTipsButton: Bool
let isOfficialChat: Bool
@ -528,6 +525,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 +1194,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()
@ -1363,33 +1361,7 @@ private extension CVComponentState.Builder {
case .failed:
mediaAlbumHasFailedAttachment = true
case .none:
// If optimize local storage is enabled, and the user has auto-downloads
// disabled, show the 'skipped attachment' download indicator. Otherwise
// render the attachment as normal, using the backup thumbnail for display.
let backupPlan = DependenciesBridge.shared.backupPlanManager.backupPlan(tx: transaction)
switch backupPlan {
case
.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
} else {
mediaAlbumHasSkippedAttachment = true
}
case
.free,
.disabled,
.disabling:
mediaAlbumHasSkippedAttachment = true
}
mediaAlbumHasSkippedAttachment = true
}
}
@ -1425,28 +1397,6 @@ private extension CVComponentState.Builder {
return result
}
private func canAutoDownloadAttachment(referencedAttachment: ReferencedAttachment) -> Bool {
let mediaBandwidthPreferenceStore = DependenciesBridge.shared.mediaBandwidthPreferenceStore
let autoDownloadableMediaTypes = mediaBandwidthPreferenceStore.autoDownloadableMediaTypes(tx: transaction)
let mimeType = referencedAttachment.attachment.mimeType
if MimeTypeUtil.isSupportedImageMimeType(mimeType) {
return autoDownloadableMediaTypes.contains(.photo)
}
if MimeTypeUtil.isSupportedVideoMimeType(mimeType) {
return autoDownloadableMediaTypes.contains(.video)
}
if MimeTypeUtil.isSupportedAudioMimeType(mimeType) {
if
autoDownloadableMediaTypes.contains(.audio),
referencedAttachment.reference.renderingFlag != .voiceMessage
{
return true
}
return false
}
return autoDownloadableMediaTypes.contains(.document)
}
mutating func buildThreadDetails() -> ThreadDetails {
owsAssertDebug(interaction is ThreadDetailsInteraction)
@ -1681,6 +1631,7 @@ private extension CVComponentState.Builder {
} else if let quotedMessage = message.quotedMessage {
var memberLabel: String?
if
BuildFlags.MemberLabel.display,
let groupThread = thread as? TSGroupThread,
!threadViewModel.hasPendingMessageRequest,
let originalMessageAuthor = quotedMessage.authorAddress.aci
@ -1763,10 +1714,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))
@ -255,30 +249,23 @@ public class CVComponentThreadDetails: CVComponentBase, CVRootComponent {
innerViews.append(UIView.spacer(withHeight: vSpacingSafetySectionDefault))
let mutualGroupsLabelConfig = mutualGroupsLabelConfig(attributedText: mutualGroupsText)
mutualGroupsLabelConfig.applyForRendering(label: mutualGroupsLabel)
mutualGroupsLabel.accessibilityLabel = safetySection.mutualGroupsAccessibilityText
innerViews.append(mutualGroupsLabel)
}
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 +448,7 @@ public class CVComponentThreadDetails: CVComponentBase, CVRootComponent {
)
}
private func safetyTipsButtonLabelConfig() -> CVLabelConfig {
private var safetyTipsButtonLabelConfig: CVLabelConfig {
CVLabelConfig.unstyledText(
OWSLocalizedString(
"SAFETY_TIPS_BUTTON_ACTION_TITLE",
@ -655,15 +642,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 +688,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 +737,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 +786,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 +803,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 +838,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,
@ -919,7 +906,6 @@ extension CVComponentThreadDetails {
shouldShowProfileNamesEducation: false,
detailsText: NSAttributedString(string: OWSLocalizedString("RELEASE_NOTES_DETAILS", comment: "Details text for the thread details view of the release notes channel")),
mutualGroupsText: nil,
mutualGroupsAccessibilityText: nil,
threadType: .contact,
shouldShowSafetyTipsButton: false,
isOfficialChat: true,
@ -1052,7 +1038,6 @@ extension CVComponentThreadDetails {
shouldShowProfileNamesEducation: shouldShowUnknownThreadWarning,
detailsText: membersAttributedText,
mutualGroupsText: nil,
mutualGroupsAccessibilityText: nil,
threadType: .group,
shouldShowSafetyTipsButton: shouldShowUnknownThreadWarning && groupThread.hasPendingMessageRequest(transaction: tx),
isOfficialChat: false,
@ -1085,7 +1070,6 @@ extension CVComponentThreadDetails {
with: .font(.dynamicTypeSubheadline),
.color(UIColor.Signal.label),
),
mutualGroupsAccessibilityText: nil,
threadType: .contact,
shouldShowSafetyTipsButton: false,
isOfficialChat: false,
@ -1190,7 +1174,6 @@ extension CVComponentThreadDetails {
" ",
formattedString,
]),
mutualGroupsAccessibilityText: formattedString,
threadType: .contact,
shouldShowSafetyTipsButton: isMessageRequest,
isOfficialChat: false,

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

@ -179,23 +179,6 @@ class ConversationHeaderView: UIView {
}
}
// MARK: Spinning Title
func updateTitleSpinning() {
let key = "spin"
if InMemorySettings.spinningConversationTitle {
guard layer.animation(forKey: key) == nil else { return }
let animation = CABasicAnimation(keyPath: "transform.rotation.z")
animation.toValue = NSNumber(value: Double.pi * 2)
animation.duration = 1
animation.isCumulative = true
animation.repeatCount = .greatestFiniteMagnitude
layer.add(animation, forKey: key)
} else {
layer.removeAnimation(forKey: key)
}
}
// MARK: Delegate Methods
@objc

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

@ -249,7 +249,6 @@ public class ConversationInputToolbar: UIView, QuotedReplyPreviewDelegate {
button.configuration?.baseForegroundColor = Style.buttonTintColor
button.accessibilityLabel = accessibilityLabel
button.accessibilityIdentifier = accessibilityIdentifier
button.isPointerInteractionEnabled = true
button.translatesAutoresizingMaskIntoConstraints = false
button.addConstraints([
button.widthAnchor.constraint(equalToConstant: LayoutMetrics.initialTextBoxHeight),
@ -346,7 +345,6 @@ public class ConversationInputToolbar: UIView, QuotedReplyPreviewDelegate {
button.configuration?.cornerStyle = .capsule
button.accessibilityLabel = MessageStrings.sendButton
button.accessibilityIdentifier = accessibilityIdentifier
button.isPointerInteractionEnabled = true
button.translatesAutoresizingMaskIntoConstraints = false
button.addConstraints([
button.widthAnchor.constraint(equalToConstant: buttonSize),
@ -380,7 +378,6 @@ public class ConversationInputToolbar: UIView, QuotedReplyPreviewDelegate {
comment: "Accessibility hint describing what you can do with the attachment button",
)
button.accessibilityIdentifier = accessibilityIdentifier
button.isPointerInteractionEnabled = true
button.translatesAutoresizingMaskIntoConstraints = false
button.addConstraints([
button.widthAnchor.constraint(equalToConstant: buttonSize),
@ -406,7 +403,6 @@ public class ConversationInputToolbar: UIView, QuotedReplyPreviewDelegate {
button.configuration?.baseForegroundColor = .white
button.configuration?.cornerStyle = .capsule
button.accessibilityIdentifier = accessibilityIdentifier
button.isPointerInteractionEnabled = true
button.translatesAutoresizingMaskIntoConstraints = false
button.addConstraints([
button.widthAnchor.constraint(equalToConstant: buttonSize),
@ -1219,7 +1215,7 @@ public class ConversationInputToolbar: UIView, QuotedReplyPreviewDelegate {
lazy var sendButton: UIButton = {
let button = UIButton(type: .system)
button.accessibilityLabel = MessageStrings.sendButton
button.isPointerInteractionEnabled = true
button.ows_adjustsImageWhenDisabled = true
button.accessibilityIdentifier = UIView.accessibilityIdentifier(in: self, name: "sendButton")
button.setImage(UIImage(imageLiteralResourceName: "send-blue-28"), for: .normal)
button.bounds.size = CGSize(width: 48, height: LayoutMetrics.initialToolbarHeight)
@ -1229,7 +1225,6 @@ public class ConversationInputToolbar: UIView, QuotedReplyPreviewDelegate {
lazy var cameraButton: UIButton = {
let button = UIButton(type: .system)
button.tintColor = Style.buttonTintColor
button.isPointerInteractionEnabled = true
button.accessibilityLabel = OWSLocalizedString(
"CAMERA_BUTTON_LABEL",
comment: "Accessibility label for camera button.",
@ -1247,7 +1242,6 @@ public class ConversationInputToolbar: UIView, QuotedReplyPreviewDelegate {
lazy var voiceMemoButton: UIButton = {
let button = UIButton(type: .system)
button.tintColor = Style.buttonTintColor
button.isPointerInteractionEnabled = true
button.accessibilityLabel = OWSLocalizedString(
"INPUT_TOOLBAR_VOICE_MEMO_BUTTON_ACCESSIBILITY_LABEL",
comment: "accessibility label for the button which records voice memos",
@ -1373,8 +1367,6 @@ public class ConversationInputToolbar: UIView, QuotedReplyPreviewDelegate {
override private init(frame: CGRect) {
super.init(frame: frame)
isPointerInteractionEnabled = true
addSubview(roundedCornersBackground)
roundedCornersBackground.autoCenterInSuperview()
roundedCornersBackground.autoSetDimensions(to: CGSize(square: 28))

View File

@ -545,14 +545,6 @@ extension ConversationViewController: CVLoadCoordinatorDelegate {
}
}
if
scrollAction.action == .none,
update.loadRequest.preferredScrollContinuityAnchorInteractionId != nil,
isScrolledToBottom
{
scrollAction = CVScrollAction(action: .bottomOfLoadWindow, isAnimated: false)
}
if .loadOlder == renderState.loadType {
scrollAction = .none
}

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,
)
}
@ -1455,7 +1409,6 @@ extension ConversationViewController: CVComponentDelegate {
public func didTapSafetyTips() {
let viewController = SafetyTipsViewController(
mode: .messageRequest,
primaryButton: SafetyTipsViewController.Button(
title: CommonStrings.viewMoreButton,
action: { [weak self] in

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

@ -31,8 +31,6 @@ extension ConversationViewController {
} else {
headerView.titleLabel.text = title
}
headerView.updateTitleSpinning()
}
public func createHeaderViews() {

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

@ -423,6 +423,7 @@ struct CVItemModelBuilder: CVItemBuilding {
var memberLabel: String?
if
BuildFlags.MemberLabel.display,
let groupThread = thread as? TSGroupThread,
!threadViewModel.hasPendingMessageRequest,
let senderAci = incomingSenderAddress.aci

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

@ -47,12 +47,11 @@ enum ContactSupportActionSheet {
let submitWithLogAction = ActionSheetAction(title: submitWithLogTitle, style: .default) { [weak fromViewController] _ in
guard let fromViewController else { return }
let logs = DebugLogs(dumper: logDumper)
let emailRequest = SupportEmailModel(
userDescription: nil,
emojiMood: nil,
supportFilter: emailFilter.asString,
debugLogPolicy: .requireUpload(logs),
debugLogPolicy: .requireUpload(logDumper),
hasRecentChallenge: logDumper.challengeReceivedRecently(),
)

View File

@ -8,7 +8,7 @@ import SignalServiceKit
import SignalUI
import zlib
struct DebugLogDumper {
public struct DebugLogDumper {
fileprivate var accountManager: (any TSAccountManager)?
fileprivate var appVersion: any AppVersion
fileprivate var db: (any DB)?
@ -25,7 +25,7 @@ struct DebugLogDumper {
)
}
func challengeReceivedRecently() -> Bool {
public func challengeReceivedRecently() -> Bool {
guard let db else {
return false
}
@ -57,134 +57,34 @@ struct DebugLogDumper {
}
}
final class DebugLogs {
private let dumper: DebugLogDumper
private var logsDirPath: String?
enum DebugLogs {
init(dumper: DebugLogDumper) {
self.dumper = dumper
self.logsDirPath = DebugLogs.collectAndFlushLogs(dumper: dumper)
}
deinit {
if let logsDirPath {
OWSFileSystem.deleteFile(logsDirPath)
}
}
func showPreview(
from viewController: UIViewController,
onSubmit: (() -> Void)? = nil,
onCancel: (() -> Void)? = nil,
) {
guard let logsDirPath else {
Logger.error("No logs path found for preview")
handleError(error: .noLogs, viewController: viewController)
onCancel?()
return
}
let logFilePaths = ((try? FileManager.default.contentsOfDirectory(atPath: logsDirPath)) ?? []).map {
URL(fileURLWithPath: logsDirPath).appendingPathComponent($0).path
}
let previewVC = DebugLogPreviewViewController(logFilePaths: logFilePaths, onSubmit: onSubmit, onCancel: onCancel)
let nav = OWSNavigationController(rootViewController: previewVC)
viewController.present(nav, animated: true)
}
/// Presents a log preview with an option to submit. Completion is only
/// called if the user submits, after the submission is completed.
@MainActor
func promptToSubmitLogs(
from viewController: UIViewController,
supportTag: String? = nil,
completion: (() -> Void)? = nil,
) {
showPreview(from: viewController, onSubmit: {
Task {
await viewController.awaitableDismiss(animated: true)
await self.submitLogs(supportTag: supportTag)
if let completion {
try? await Task.sleep(nanoseconds: 2 * NSEC_PER_SEC)
static func submitLogs(supportTag: String? = nil, dumper: DebugLogDumper, completion: (() -> Void)? = nil) {
let submitLogsCompletion = {
if let completion {
// Wait a moment. If the user opens a URL, it needs a moment to complete.
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
completion()
}
}
})
}
@MainActor
func promptToSubmitLogs(
from viewController: UIViewController,
supportTag: String? = nil,
) async {
let didSubmit = await withCheckedContinuation { continuation in
showPreview(
from: viewController,
onSubmit: {
continuation.resume(returning: true)
},
onCancel: {
continuation.resume(returning: false)
},
)
}
if didSubmit {
await viewController.awaitableDismiss(animated: true)
await submitLogs(supportTag: supportTag)
}
}
enum DebugLogsError: LocalizedError {
case noLogs
case couldNotPackageLogs
case uploadError(zipFilePath: String)
var errorDescription: String? { localizedErrorMessage }
var localizedErrorMessage: String {
switch self {
case .noLogs:
OWSLocalizedString(
"DEBUG_LOG_ALERT_NO_LOGS",
comment: "Error indicating that no debug logs could be found.",
)
case .couldNotPackageLogs:
OWSLocalizedString(
"DEBUG_LOG_ALERT_COULD_NOT_PACKAGE_LOGS",
comment: "Error indicating that the debug logs could not be packaged.",
)
case .uploadError:
OWSLocalizedString(
"DEBUG_LOG_ALERT_ERROR_UPLOADING_LOG",
comment: "Error indicating that a debug log could not be uploaded.",
)
}
}
}
@MainActor
private func submitLogs(supportTag: String?) async {
var supportFilter = "Signal - iOS Debug Log"
if let supportTag {
supportFilter += " - \(supportTag)"
}
guard let frontmostViewController = UIApplication.shared.frontmostViewControllerIgnoringAlerts else {
submitLogsCompletion()
return
}
uploadLogsUsingViewController(frontmostViewController, dumper: dumper) { url in
guard let presentingViewController = UIApplication.shared.frontmostViewControllerIgnoringAlerts else {
submitLogsCompletion()
return
}
let url: URL?
do {
url = try await uploadLogsWithUI(from: frontmostViewController)
} catch {
self.handleError(error: error, viewController: frontmostViewController)
return
}
guard let url else { return }
guard let presentingViewController = UIApplication.shared.frontmostViewControllerIgnoringAlerts else {
return
}
await withCheckedContinuation { (continuation: CheckedContinuation<Void, Never>) in
let alert = ActionSheetController(
title: NSLocalizedString("DEBUG_LOG_ALERT_TITLE", comment: "Title of the debug log alert."),
message: NSLocalizedString("DEBUG_LOG_ALERT_MESSAGE", comment: "Message of the debug log alert."),
@ -202,10 +102,10 @@ final class DebugLogs {
await ComposeSupportEmailOperation.sendEmailWithDefaultErrorHandling(
supportFilter: supportFilter,
logUrl: url,
hasRecentChallenge: self.dumper.challengeReceivedRecently(),
hasRecentChallenge: dumper.challengeReceivedRecently(),
)
}
continuation.resume()
submitLogsCompletion()
},
))
}
@ -218,7 +118,7 @@ final class DebugLogs {
handler: { _ in
UIPasteboard.general.string = url.absoluteString
presentingViewController.presentToast(text: CommonStrings.copiedToClipboardToast, image: .copy)
continuation.resume()
submitLogsCompletion()
},
))
alert.addAction(ActionSheetAction(
@ -231,39 +131,67 @@ final class DebugLogs {
AttachmentSharing.showShareUI(
for: url.absoluteString,
sender: nil,
completion: { continuation.resume() },
completion: submitLogsCompletion,
)
},
))
alert.addAction(ActionSheetAction(
title: CommonStrings.cancelButton,
style: .cancel,
handler: { _ in continuation.resume() },
handler: { _ in submitLogsCompletion() },
))
presentingViewController.presentActionSheet(alert)
}
}
@MainActor
private func uploadLogsWithUI(from viewController: UIViewController) async throws(DebugLogsError) -> URL? {
return try await ModalActivityIndicatorViewController.presentAndPropagateResult(
from: viewController,
private static func uploadLogsUsingViewController(_ viewController: UIViewController, dumper: DebugLogDumper, completion: @escaping (URL) -> Void) {
AssertIsOnMainThread()
ModalActivityIndicatorViewController.present(
fromViewController: viewController,
canCancel: true,
) { () throws(DebugLogsError) -> URL? in
do throws(DebugLogsError) {
return try await self.uploadLogs()
} catch {
if Task.isCancelled {
return nil
asyncBlock: { await _uploadLogs(dumper: dumper, modalActivityIndicator: $0, completion: completion) },
)
}
@MainActor
private static func _uploadLogs(dumper: DebugLogDumper, modalActivityIndicator: ModalActivityIndicatorViewController, completion: @escaping (URL) -> Void) async {
do {
let url = try await uploadLogs(dumper: dumper)
guard !modalActivityIndicator.wasCancelled else { return }
modalActivityIndicator.dismiss {
completion(url)
}
} catch {
guard !modalActivityIndicator.wasCancelled else {
if let logArchiveOrDirectoryPath = error.logArchiveOrDirectoryPath {
OWSFileSystem.deleteFile(logArchiveOrDirectoryPath)
}
throw error
return
}
modalActivityIndicator.dismiss {
DebugLogs.showFailureAlert(
with: error.localizedErrorMessage,
logArchiveOrDirectoryPath: error.logArchiveOrDirectoryPath,
)
}
}
}
// MARK: - Collecting & uploading
private static func collectLogs() -> String? {
private struct NoLogsError: Error {
var errorString: String {
OWSLocalizedString(
"DEBUG_LOG_ALERT_NO_LOGS",
comment: "Error indicating that no debug logs could be found.",
)
}
}
private static func collectLogs() -> Result<String, NoLogsError> {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy.MM.dd hh.mm.ss"
let dateString = dateFormatter.string(from: Date())
@ -275,7 +203,7 @@ final class DebugLogs {
let logFilePaths = DebugLogger.shared.allLogFilePaths
if logFilePaths.isEmpty {
return nil
return .failure(NoLogsError())
}
for logFilePath in logFilePaths {
@ -291,44 +219,50 @@ final class DebugLogs {
OWSFileSystem.protectFileOrFolder(atPath: copyFilePath)
}
return zipDirPath
return .success(zipDirPath)
}
func exportLogs(viewController: UIViewController) {
static func exportLogs() {
AssertIsOnMainThread()
guard let logsDirPath else {
return handleError(
error: .noLogs,
viewController: viewController,
)
}
AttachmentSharing.showShareUI(for: URL(fileURLWithPath: logsDirPath), sender: nil) {
OWSFileSystem.deleteFile(logsDirPath)
switch collectLogs() {
case let .success(logsDirPath):
AttachmentSharing.showShareUI(for: URL(fileURLWithPath: logsDirPath), sender: nil) {
OWSFileSystem.deleteFile(logsDirPath)
}
case let .failure(error):
Self.showFailureAlert(with: error.errorString, logArchiveOrDirectoryPath: nil)
return
}
}
private static func collectAndFlushLogs(
dumper: DebugLogDumper,
) -> String? {
// Dump any additional details that are relevant.
struct UploadDebugLogError: Error {
var localizedErrorMessage: String
var logArchiveOrDirectoryPath: String?
}
/// - Note: Various dependencies might not be initialized yet when this
/// method is called from the database recovery flow. Notably, the database
/// isn't available in that flow.
static func uploadLogs(dumper: DebugLogDumper) async throws(UploadDebugLogError) -> URL {
// Phase 1: Dump any additional details that are relevant.
dumper.dump()
Logger.info("About to zip debug logs")
// Flush pending logs to disk.
// Phase 2: Flush pending logs to disk.
Logger.flush()
// Make a local copy of all of the log files.
return collectLogs()
}
func uploadLogs() async throws(DebugLogsError) -> URL {
guard let logsDirPath else {
throw DebugLogsError.noLogs
// Phase 3: Make a local copy of all of the log files.
let zipDirPath: String
switch collectLogs() {
case let .success(logsDirPath):
zipDirPath = logsDirPath
case let .failure(error):
throw UploadDebugLogError(localizedErrorMessage: error.errorString)
}
// Zip up the log files.
let zipDirUrl = URL(fileURLWithPath: logsDirPath)
let zipFileUrl = URL(fileURLWithPath: (logsDirPath as NSString).appendingPathExtension("zip")!)
// Phase 4: Zip up the log files.
let zipDirUrl = URL(fileURLWithPath: zipDirPath)
let zipFileUrl = URL(fileURLWithPath: (zipDirPath as NSString).appendingPathExtension("zip")!)
let fileCoordinator = NSFileCoordinator()
var zipError: NSError?
fileCoordinator.coordinate(readingItemAt: zipDirUrl, options: [.forUploading], error: &zipError) { temporaryFileUrl in
@ -339,44 +273,38 @@ final class DebugLogs {
}
}
if zipError != nil || !OWSFileSystem.fileOrFolderExists(url: zipFileUrl) {
throw DebugLogsError.couldNotPackageLogs
let errorMessage = OWSLocalizedString(
"DEBUG_LOG_ALERT_COULD_NOT_PACKAGE_LOGS",
comment: "Error indicating that the debug logs could not be packaged.",
)
throw UploadDebugLogError(localizedErrorMessage: errorMessage, logArchiveOrDirectoryPath: zipDirPath)
}
OWSFileSystem.protectFileOrFolder(atPath: zipFileUrl.path)
OWSFileSystem.deleteFile(zipDirPath)
// Upload the log files.
// Phase 5: Upload the log files.
do {
let url = try await DebugLogUploader.uploadFile(fileUrl: zipFileUrl, mimeType: MimeType.applicationZip.rawValue)
try OWSFileSystem.deleteFile(url: zipFileUrl)
return url
} catch {
throw DebugLogsError.uploadError(zipFilePath: zipFileUrl.path)
let errorMessage = OWSLocalizedString(
"DEBUG_LOG_ALERT_ERROR_UPLOADING_LOG",
comment: "Error indicating that a debug log could not be uploaded.",
)
throw UploadDebugLogError(localizedErrorMessage: errorMessage, logArchiveOrDirectoryPath: zipFileUrl.path)
}
}
private func handleError(
error: DebugLogsError,
viewController: UIViewController,
) {
let logsPath: String?
let completion: (() -> Void)?
switch error {
case .noLogs:
logsPath = nil
completion = nil
case .couldNotPackageLogs:
logsPath = self.logsDirPath
completion = nil
case .uploadError(let zipFilePath):
logsPath = zipFilePath
completion = {
OWSFileSystem.deleteFile(zipFilePath)
}
private static func showFailureAlert(with message: String, logArchiveOrDirectoryPath: String?) {
let deleteArchive: (String) -> Void = { filePath in
OWSFileSystem.deleteFile(filePath)
}
let alert = ActionSheetController(message: error.localizedErrorMessage)
let alert = ActionSheetController(title: nil, message: message)
if let logsPath {
if let logArchiveOrDirectoryPath {
alert.addAction(.init(
title: OWSLocalizedString(
"DEBUG_LOG_ALERT_OPTION_EXPORT_LOG_ARCHIVE",
@ -384,18 +312,23 @@ final class DebugLogs {
),
) { _ in
AttachmentSharing.showShareUI(
for: URL(fileURLWithPath: logsPath),
for: URL(fileURLWithPath: logArchiveOrDirectoryPath),
sender: nil,
completion: completion,
completion: {
deleteArchive(logArchiveOrDirectoryPath)
},
)
})
}
alert.addAction(.init(title: CommonStrings.okButton) { _ in
completion?()
if let logArchiveOrDirectoryPath {
deleteArchive(logArchiveOrDirectoryPath)
}
})
viewController.presentActionSheet(alert)
let presentingViewController = UIApplication.shared.frontmostViewControllerIgnoringAlerts
presentingViewController?.presentActionSheet(alert)
}
}

View File

@ -32,6 +32,9 @@ extension DeviceTransferService {
let wal: DeviceTransferProtoFile = try {
let file = SSKEnvironment.shared.databaseStorageRef.grdbStorage.databaseWALFilePath
let size = try OWSFileSystem.fileSize(ofPath: file)
guard size > 0 else {
throw OWSAssertionError("database wal is empty")
}
estimatedTotalSize += size
let fileBuilder = DeviceTransferProtoFile.builder(
identifier: DeviceTransferService.databaseWALIdentifier,

View File

@ -5,7 +5,6 @@
import CryptoKit
import Foundation
import GRDB
import MultipeerConnectivity
import SignalServiceKit
@ -367,15 +366,7 @@ class DeviceTransferService: NSObject, DeviceTransferServiceProtocol {
taskGroup.addTask {
// Make a copy of the database files within a write transaction so we can be confident
// they aren't mutated during the copy. We then transfer these copies.
let dbCopy = try await SSKEnvironment.shared.databaseStorageRef.awaitableWrite { tx in
// The MultipeerConnectivity framework stalls if we try to send an empty
// file. The receiver requires a non-empty file. We can't send garbage
// (because that would corrupt the database), so mutate the database, force
// it to be written to the WAL file, and then send that result to our peer.
let store = NewKeyValueStore(collection: "DeviceTransferWAL")
store.writeValue(Randomness.generateRandomBytes(32), forKey: "MustBeNonEmpty", tx: tx)
store.removeValue(forKey: "MustBeNonEmpty", tx: tx)
sqlite3_db_cacheflush(tx.database.sqliteConnection!)
let dbCopy = try await SSKEnvironment.shared.databaseStorageRef.awaitableWrite { _ in
do {
let dbCopy = try Self.makeLocalCopy(databaseFile: database.database)
let walCopy = try Self.makeLocalCopy(databaseFile: database.wal)

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

@ -1,12 +0,0 @@
{
"images" : [
{
"filename" : "safetytip_48_pin.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -1,12 +0,0 @@
{
"images" : [
{
"filename" : "safetytip_48_lock.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

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

@ -1,52 +0,0 @@
//
// Copyright 2026 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import Foundation
public import SignalServiceKit
/// Handles fetching and parsing remote announcements.
public class RemoteAnnouncementFetcher: RemoteReleaseNotesFetcher<RemoteAnnouncementModel.Manifest, RemoteAnnouncementModel.Translation> {
override func updatePersistedData(
withFetchedData fetchedTranslations: [(RemoteAnnouncementModel.Manifest, RemoteAnnouncementModel.Translation)],
transaction: DBWriteTransaction,
) {
// TODO: [KC] implement!
}
override func fetchTranslationAndImage(
forManifest manifest: RemoteAnnouncementModel.Manifest,
withLocaleString localeString: String,
) async throws -> RemoteAnnouncementModel.Translation {
return try await Retry.performWithBackoff(
maxAttempts: 3,
isRetryable: { $0.isNetworkFailureOrTimeout || $0.is5xxServiceResponse },
block: {
guard
let translationUrlPath: String = .translationUrlPath(
forManifestId: manifest.id,
withLocaleString: localeString,
)
else {
throw OWSAssertionError("Failed to create translation URL path for manifest \(manifest.id)")
}
let translationParser = try await remoteReleaseNotesService.fetchTranslationParser(translationUrlPath: translationUrlPath)
let translation = try RemoteAnnouncementModel.Translation.parseFrom(parser: translationParser)
// TODO: [KC] May want to store whether we've downloaded media
let _ = try await self.downloadMediaIfNecessary(
mediaRemoteUrlPath: translation.mediaRemoteUrlPath,
mediaFileDirectory: RemoteAnnouncementModel.mediaDirectory,
translationId: translation.id,
)
if manifest.id != translation.id {
// We shouldn't fail here, but this scenario is
// unexpected so let's keep an eye out for it.
owsFailDebug("Have manifest ID \(manifest.id) that does not match fetched translation ID \(translation.id)")
}
return translation
},
)
}
}

View File

@ -4,40 +4,63 @@
//
import Foundation
public import SignalServiceKit
import SignalServiceKit
/// Handles fetching and parsing remote megaphones.
public class RemoteMegaphoneFetcher: RemoteReleaseNotesFetcher<RemoteMegaphoneModel.Manifest, RemoteMegaphoneModel.Translation> {
private let experienceUpgradeStore: ExperienceUpgradeStore
class RemoteMegaphoneFetcher {
private let databaseStorage: SDSDatabaseStorage
private let signalService: any OWSSignalServiceProtocol
override init(
db: DB,
remoteReleaseNotesService: any RemoteReleaseNotesServiceProtocol,
init(
databaseStorage: SDSDatabaseStorage,
signalService: any OWSSignalServiceProtocol,
) {
self.experienceUpgradeStore = ExperienceUpgradeStore()
super.init(
db: db,
remoteReleaseNotesService: remoteReleaseNotesService,
)
self.databaseStorage = databaseStorage
self.signalService = signalService
}
/// Fetch all remote megaphones currently on the service and persist them
/// locally. Removes any locally-persisted remote megaphones that are no
/// longer available remotely.
func syncRemoteMegaphones() async throws {
Logger.info("Beginning remote megaphone fetch.")
let megaphones: [RemoteMegaphoneModel]
do {
megaphones = try await fetchRemoteMegaphones()
} catch {
Logger.warn("\(error)")
throw error
}
Logger.info("Syncing \(megaphones.count) fetched remote megaphones with local state.")
await self.databaseStorage.awaitableWrite { transaction in
self.updatePersistedMegaphones(
withFetchedMegaphones: megaphones,
transaction: transaction,
)
}
}
}
// MARK: - Persisted megaphones
private extension RemoteMegaphoneFetcher {
/// 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,
func updatePersistedMegaphones(
withFetchedMegaphones serviceMegaphones: [RemoteMegaphoneModel],
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
@ -45,37 +68,107 @@ public class RemoteMegaphoneFetcher: RemoteReleaseNotesFetcher<RemoteMegaphoneMo
// if anything has changed about the megaphone we have the latest state.
// 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))
}
for serviceMegaphone in serviceMegaphones {
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)
}
}
}
// MARK: - Fetching
private extension RemoteMegaphoneFetcher {
func fetchRemoteMegaphones() async throws -> [RemoteMegaphoneModel] {
let manifests = try await fetchManifests()
return try await withThrowingTaskGroup(of: RemoteMegaphoneModel.self) { taskGroup in
for manifest in manifests {
taskGroup.addTask {
let translation = try await self.fetchTranslation(forMegaphoneManifest: manifest)
if manifest.id != translation.id {
// We shouldn't fail here, but this scenario is
// unexpected so let's keep an eye out for it.
owsFailDebug("Have manifest ID \(manifest.id) that does not match fetched translation ID \(translation.id)")
}
return RemoteMegaphoneModel(manifest: manifest, translation: translation)
}
}
return try await taskGroup.reduce(into: [], { $0.append($1) })
}
}
private func getUrlSession() -> OWSURLSessionProtocol {
signalService.urlSessionForUpdates2()
}
/// Fetch the manifests for the currently-active remote megaphones.
/// Manifests contain metadata about a megaphone, such as when it should be
/// shown and what actions it should expose. They do not contain any
/// user-visible content, such as strings.
private func fetchManifests() async throws -> [RemoteMegaphoneModel.Manifest] {
return try await Retry.performWithBackoff(
maxAttempts: 3,
isRetryable: { $0.isNetworkFailureOrTimeout || $0.is5xxServiceResponse },
block: {
Logger.info("Fetching remote megaphone manifests")
let response = try await getUrlSession().performRequest(
.manifestUrlPath,
method: .get,
)
guard let parser = response.responseBodyParamParser else {
throw OWSAssertionError("Missing or invalid body JSON for manifest!")
}
return try RemoteMegaphoneModel.Manifest.parseFrom(parser: parser)
},
)
}
/// Fetch user-displayable localized strings for the given manifest. Will
/// attempt to fetch a translation matching the user's current locale,
/// falling back to English otherwise.
private func fetchTranslation(
forMegaphoneManifest manifest: RemoteMegaphoneModel.Manifest,
) async throws -> RemoteMegaphoneModel.Translation {
let localeStrings: [String] = .possibleTranslationLocaleStrings
for (index, localeString) in localeStrings.enumerated() {
do {
var translation = try await fetchTranslation(forMegaphoneManifest: manifest, withLocaleString: localeString)
translation.setHasImage(try await self.downloadImageIfNecessary(forTranslation: translation))
return translation
} catch let error as OWSHTTPError where error.responseStatusCode == 404 && (index + 1) != localeStrings.endIndex {
// If this isn't the last locale & it's not found, try the next one.
continue
}
// If we hit a non-404 error, propagate it out immediately.
}
// We either return a value or throw an error in the loop as long as there
// is at least one locale.
throw OWSAssertionError("Unexpectedly found no locale strings!")
}
/// Fetch a translation for the given manifest, using the given locale
/// string. Retries automatically on network failure, if possible. May
/// fail with a 404, if no translation exists for the given locale string.
override func fetchTranslationAndImage(
forManifest manifest: RemoteMegaphoneModel.Manifest,
private func fetchTranslation(
forMegaphoneManifest manifest: RemoteMegaphoneModel.Manifest,
withLocaleString localeString: String,
) async throws -> RemoteMegaphoneModel.Translation {
return try await Retry.performWithBackoff(
@ -84,26 +177,236 @@ public class RemoteMegaphoneFetcher: RemoteReleaseNotesFetcher<RemoteMegaphoneMo
block: {
guard
let translationUrlPath: String = .translationUrlPath(
forManifestId: manifest.id,
forManifest: manifest,
withLocaleString: localeString,
)
else {
throw OWSAssertionError("Failed to create translation URL path for manifest \(manifest.id)")
}
let translationParser = try await remoteReleaseNotesService.fetchTranslationParser(translationUrlPath: translationUrlPath)
var translation = try RemoteMegaphoneModel.Translation.parseFrom(parser: translationParser)
translation.setHasImage(try await self.downloadMediaIfNecessary(
mediaRemoteUrlPath: translation.imageRemoteUrlPath,
mediaFileDirectory: RemoteMegaphoneModel.imagesDirectory,
translationId: translation.id,
))
if manifest.id != translation.id {
// We shouldn't fail here, but this scenario is
// unexpected so let's keep an eye out for it.
owsFailDebug("Have manifest ID \(manifest.id) that does not match fetched translation ID \(translation.id)")
Logger.info("Fetching remote megaphone translation")
let response = try await getUrlSession().performRequest(translationUrlPath, method: .get)
guard let parser = response.responseBodyParamParser else {
throw OWSAssertionError("Missing or invalid body JSON for translation!")
}
return try RemoteMegaphoneModel.Translation.parseFrom(parser: parser)
},
)
}
/// Downloads the image if necessary.
///
/// Doesn't perform any network requests if the image has already been
/// downloaded.
///
/// - Throws: If the image should be downloaded but can't be downloaded.
/// - Returns: Whether or not `translation` has an image.
private func downloadImageIfNecessary(
forTranslation translation: RemoteMegaphoneModel.Translation,
) async throws -> Bool {
guard let imageRemoteUrlPath = translation.imageRemoteUrlPath else {
return false
}
guard let imageFileUrl: URL = .imageFilePath(forFetchedTranslation: translation) else {
throw OWSAssertionError("Failed to get image file path for translation with ID \(translation.id)")
}
return try await Retry.performWithBackoff(
maxAttempts: 3,
isRetryable: { $0.isNetworkFailureOrTimeout || $0.is5xxServiceResponse },
block: {
do {
if !FileManager.default.fileExists(atPath: imageFileUrl.path) {
Logger.info("Fetching remote megaphone image")
let response = try await getUrlSession().performDownload(
imageRemoteUrlPath,
method: .get,
)
do {
try FileManager.default.moveItem(
at: response.downloadUrl,
to: imageFileUrl,
)
} catch let error {
throw OWSAssertionError("Failed to move downloaded image! \(error)")
}
}
return true
} catch where error.httpStatusCode == 404 {
owsFailDebug("Unexpectedly got 404 while fetching remote megaphone image for ID \(translation.id)!")
return false
} catch let error as OWSHTTPError {
owsFailDebug("Unexpectedly got error status code \(error.responseStatusCode) while fetching remote megaphone image for ID \(translation.id)!")
throw error
}
return translation
},
)
}
}
// MARK: URLs
private extension URL {
static func imageFilePath(forFetchedTranslation translation: RemoteMegaphoneModel.Translation) -> URL? {
let dirUrl = RemoteMegaphoneModel.imagesDirectory
guard OWSFileSystem.ensureDirectoryExists(dirUrl.path) else {
return nil
}
return dirUrl.appendingPathComponent(translation.imageLocalRelativePath)
}
}
private extension Array<String> {
/// A list of possible locale strings for which a translation may be
/// available, based on the user's current locale. Includes a fallback to
/// English.
static var possibleTranslationLocaleStrings: [String] {
var locales: [String] = []
if let langCode = Locale.current.languageCode {
locales.append(langCode)
if let regionCode = Locale.current.regionCode {
locales.append("\(langCode)_\(regionCode)")
}
}
// Always include English at the end, as a fallback. This translation
// should always exist.
return locales + ["en"]
}
}
private extension String {
/// The path at which remote megaphone manifests are listed.
static let manifestUrlPath = "dynamic/release-notes/release-notes-v2.json"
/// The path at which a translation may be found, for the given manifest
/// and locale string.
static func translationUrlPath(
forManifest manifest: RemoteMegaphoneModel.Manifest,
withLocaleString localeString: String,
) -> String? {
"static/release-notes/\(manifest.id)/\(localeString).json"
.percentEncodedAsUrlPath
}
}
// MARK: - Parsing manifests
private extension RemoteMegaphoneModel.Manifest {
private static let megaphonesKey = "megaphones"
private static let uuidKey = "uuid"
private static let priorityKey = "priority"
private static let iosMinVersionKey = "iosMinVersion"
private static let countriesKey = "countries"
private static let dontShowBeforeEpochSecondsKey = "dontShowBeforeEpochSeconds"
private static let dontShowAfterEpochSecondsKey = "dontShowAfterEpochSeconds"
private static let showForNumberOfDaysKey = "showForNumberOfDays"
private static let conditionalIdKey = "conditionalId"
private static let primaryCtaIdKey = "primaryCtaId"
private static let primaryCtaDataKey = "primaryCtaData"
private static let secondaryCtaIdKey = "secondaryCtaId"
private static let secondaryCtaDataKey = "secondaryCtaData"
static func parseFrom(parser megaphonesArrayParser: ParamParser) throws -> [Self] {
let individualMegaphones: [[String: Any]] = try megaphonesArrayParser.required(key: Self.megaphonesKey)
return try individualMegaphones.compactMap { megaphoneObject throws -> Self? in
let megaphoneParser = ParamParser(megaphoneObject)
guard let iosMinVersion: String = try megaphoneParser.optional(key: Self.iosMinVersionKey) else {
return nil
}
let uuid: String = try megaphoneParser.required(key: Self.uuidKey)
let priority: Int = try megaphoneParser.required(key: Self.priorityKey)
let countries: String = try megaphoneParser.required(key: Self.countriesKey)
let dontShowBeforeEpochSeconds: UInt64 = try megaphoneParser.required(key: Self.dontShowBeforeEpochSecondsKey)
let dontShowAfterEpochSeconds: UInt64 = try megaphoneParser.required(key: Self.dontShowAfterEpochSecondsKey)
let showForNumberOfDays: Int = try megaphoneParser.required(key: Self.showForNumberOfDaysKey)
let conditionalId: String? = try megaphoneParser.optional(key: Self.conditionalIdKey)
let primaryCtaId: String? = try megaphoneParser.optional(key: Self.primaryCtaIdKey)
let primaryCtaDataJson: [String: Any]? = try megaphoneParser.optional(key: Self.primaryCtaDataKey)
let secondaryCtaId: String? = try megaphoneParser.optional(key: Self.secondaryCtaIdKey)
let secondaryCtaDataJson: [String: Any]? = try megaphoneParser.optional(key: Self.secondaryCtaDataKey)
var conditionalCheck: ConditionalCheck?
if let conditionalId {
conditionalCheck = ConditionalCheck(fromConditionalId: conditionalId)
}
var primaryAction: Action?
if let primaryCtaId {
primaryAction = Action(fromActionId: primaryCtaId)
}
var primaryActionData: ActionData?
if let primaryCtaDataJson {
primaryActionData = try ActionData.parse(fromJson: primaryCtaDataJson)
}
var secondaryAction: Action?
if let secondaryCtaId {
secondaryAction = Action(fromActionId: secondaryCtaId)
}
var secondaryActionData: ActionData?
if let secondaryCtaDataJson {
secondaryActionData = try ActionData.parse(fromJson: secondaryCtaDataJson)
}
return RemoteMegaphoneModel.Manifest(
id: uuid,
priority: priority,
minAppVersion: iosMinVersion,
countries: countries,
dontShowBefore: dontShowBeforeEpochSeconds,
dontShowAfter: dontShowAfterEpochSeconds,
showForNumberOfDays: showForNumberOfDays,
conditionalCheck: conditionalCheck,
primaryAction: primaryAction,
primaryActionData: primaryActionData,
secondaryAction: secondaryAction,
secondaryActionData: secondaryActionData,
)
}
}
}
// MARK: - Parsing translations
private extension RemoteMegaphoneModel.Translation {
private static let uuidKey = "uuid"
private static let imageUrlKey = "image"
private static let titleKey = "title"
private static let bodyKey = "body"
private static let primaryCtaTextKey = "primaryCtaText"
private static let secondaryCtaTextKey = "secondaryCtaText"
static func parseFrom(parser: ParamParser) throws -> Self {
let uuid: String = try parser.required(key: Self.uuidKey)
let imageUrl: String? = try parser.optional(key: Self.imageUrlKey)
let title: String = try parser.required(key: Self.titleKey)
let body: String = try parser.required(key: Self.bodyKey)
let primaryCtaText: String? = try parser.optional(key: Self.primaryCtaTextKey)
let secondaryCtaText: String? = try parser.optional(key: Self.secondaryCtaTextKey)
guard uuid.isPermissibleAsFilename else {
throw OWSAssertionError("Translation had UUID that is illegal filename: \(uuid)")
}
return RemoteMegaphoneModel.Translation.makeWithoutLocalImage(
id: uuid,
title: title,
body: body,
imageRemoteUrlPath: imageUrl,
primaryActionText: primaryCtaText,
secondaryActionText: secondaryCtaText,
)
}
}

View File

@ -1,149 +0,0 @@
//
// Copyright 2026 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import Foundation
import SignalServiceKit
private extension Array<String> {
/// A list of possible locale strings for which a translation may be
/// available, based on the user's current locale. Includes a fallback to
/// English.
static var possibleTranslationLocaleStrings: [String] {
var locales: [String] = []
if let langCode = Locale.current.languageCode {
locales.append(langCode)
if let regionCode = Locale.current.regionCode {
locales.append("\(langCode)_\(regionCode)")
}
}
// Always include English at the end, as a fallback. This translation
// should always exist.
return locales + ["en"]
}
}
extension String {
/// The path at which a translation may be found, for the given manifest
/// and locale string.
static func translationUrlPath(
forManifestId manifestId: String,
withLocaleString localeString: String,
) -> String? {
"static/release-notes/\(manifestId)/\(localeString).json"
.percentEncodedAsUrlPath
}
}
// MARK: URLs
extension URL {
static func mediaFilePath(dirUrl: URL, mediaLocalRelativePath: String) -> URL? {
guard OWSFileSystem.ensureDirectoryExists(dirUrl.path) else {
return nil
}
return dirUrl.appendingPathComponent(mediaLocalRelativePath)
}
}
public class RemoteReleaseNotesFetcher<ManifestType, TranslationType> {
let db: DB
let remoteReleaseNotesService: any RemoteReleaseNotesServiceProtocol
var fetchedTranslations: [(ManifestType, TranslationType)] = []
init(
db: DB,
remoteReleaseNotesService: any RemoteReleaseNotesServiceProtocol,
) {
self.db = db
self.remoteReleaseNotesService = remoteReleaseNotesService
}
func run(manifests: [ManifestType]) async throws {
fetchedTranslations = try await withThrowingTaskGroup(of: (ManifestType, TranslationType).self) { taskGroup in
for manifest in manifests {
taskGroup.addTask {
let translation = try await self.fetchTranslation(forManifest: manifest)
return (manifest, translation)
}
}
return try await taskGroup.reduce(into: [], { $0.append($1) })
}
await db.awaitableWrite { tx in
updatePersistedData(withFetchedData: fetchedTranslations, transaction: tx)
}
}
/// Fetch user-displayable localized strings for the given manifest. Will
/// attempt to fetch a translation matching the user's current locale,
/// falling back to English otherwise.
private func fetchTranslation(
forManifest manifest: ManifestType,
) async throws -> TranslationType {
let localeStrings: [String] = .possibleTranslationLocaleStrings
for (index, localeString) in localeStrings.enumerated() {
do {
return try await fetchTranslationAndImage(forManifest: manifest, withLocaleString: localeString)
} catch let error as OWSHTTPError where error.responseStatusCode == 404 && (index + 1) != localeStrings.endIndex {
// If this isn't the last locale & it's not found, try the next one.
continue
}
// If we hit a non-404 error, propagate it out immediately.
}
// We either return a value or throw an error in the loop as long as there
// is at least one locale.
throw OWSAssertionError("Unexpectedly found no locale strings!")
}
/// Downloads the image if necessary.
///
/// Doesn't perform any network requests if the image has already been
/// downloaded.
///
/// - Throws: If the image should be downloaded but can't be downloaded.
/// - Returns: Whether or not `translation` has an image.
func downloadMediaIfNecessary(
mediaRemoteUrlPath: String?,
mediaFileDirectory: URL,
translationId: String,
) async throws -> Bool {
guard let mediaRemoteUrlPath else {
return false
}
guard let mediaFileUrl: URL = .mediaFilePath(dirUrl: mediaFileDirectory, mediaLocalRelativePath: translationId) else {
throw OWSAssertionError("Failed to get image file path for translation with ID \(translationId)")
}
return try await Retry.performWithBackoff(
maxAttempts: 3,
isRetryable: { $0.isNetworkFailureOrTimeout || $0.is5xxServiceResponse },
block: {
try await remoteReleaseNotesService.downloadMedia(
mediaRemoteUrlPath: mediaRemoteUrlPath,
mediaFileUrl: mediaFileUrl,
translationId: translationId,
)
},
)
}
func fetchTranslationAndImage(
forManifest manifest: ManifestType,
withLocaleString localeString: String,
) async throws -> TranslationType {
owsFail("Must override fetch")
}
func updatePersistedData(withFetchedData fetchedTranslations: [(ManifestType, TranslationType)], transaction: DBWriteTransaction) {
owsFail("Must override fetch")
}
}

View File

@ -1,71 +0,0 @@
//
// Copyright 2026 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import Foundation
import SignalServiceKit
/// Handles fetching and parsing remote megaphones and release notes.
public class RemoteReleaseNotesFetchingManager {
private let remoteReleaseNotesService: any RemoteReleaseNotesServiceProtocol
private let remoteMegaphoneFetcher: RemoteMegaphoneFetcher
private let remoteAnnouncementFetcher: RemoteAnnouncementFetcher
init(
db: DB,
remoteReleaseNotesService: any RemoteReleaseNotesServiceProtocol,
) {
self.remoteReleaseNotesService = remoteReleaseNotesService
self.remoteMegaphoneFetcher = RemoteMegaphoneFetcher(
db: db,
remoteReleaseNotesService: remoteReleaseNotesService,
)
self.remoteAnnouncementFetcher = RemoteAnnouncementFetcher(
db: db,
remoteReleaseNotesService: remoteReleaseNotesService,
)
}
/// Fetch all remote release notes currently on the service and persist them
/// locally. Removes any locally-persisted remote release notes that are no
/// longer available remotely.
func syncRemoteReleaseNotes() async throws {
Logger.info("Beginning remote release notes fetch.")
let (megaphoneManifests, announcementManifests) = try await fetchManifests()
let megaphoneResult = await Result {
try await remoteMegaphoneFetcher.run(manifests: megaphoneManifests)
}
if case .failure(let error) = megaphoneResult {
Logger.error("megaphone fetch failed: \(error)")
}
if BuildFlags.ReleaseNotesChannel.announcementFetch {
let announcementResult = await Result {
try await remoteAnnouncementFetcher.run(manifests: announcementManifests)
}
if case .failure(let error) = announcementResult {
Logger.error("announcement fetch failed: \(error)")
}
}
}
/// Fetch the manifests for the currently-active remote megaphones.
/// Manifests contain metadata about a megaphone, such as when it should be
/// shown and what actions it should expose. They do not contain any
/// user-visible content, such as strings.
private func fetchManifests() async throws -> ([RemoteMegaphoneModel.Manifest], [RemoteAnnouncementModel.Manifest]) {
return try await Retry.performWithBackoff(
maxAttempts: 3,
isRetryable: { $0.isNetworkFailureOrTimeout || $0.is5xxServiceResponse },
block: {
Logger.info("Fetching remote release notes manifests")
return try await remoteReleaseNotesService.fetchManifests()
},
)
}
}

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)
@ -368,13 +366,11 @@ public class NotificationActionHandler {
@MainActor
private class func submitDebugLogs(supportTag: String?) async {
guard let viewController = CurrentAppContext().frontmostViewController() else {
return
await withCheckedContinuation { continuation in
DebugLogs.submitLogs(supportTag: supportTag, dumper: .fromGlobals()) {
continuation.resume()
}
}
await DebugLogs(dumper: .fromGlobals()).promptToSubmitLogs(
from: viewController,
supportTag: supportTag,
)
}
@MainActor

View File

@ -25,7 +25,6 @@ class ProvisioningCoordinatorImpl: ProvisioningCoordinator {
private let signalService: OWSSignalServiceProtocol
private let storageServiceManager: StorageServiceManager
private let svr: SecureValueRecovery
private let svrLocalStorage: SVRLocalStorage
private let syncManager: SyncManagerProtocol
private let threadStore: ThreadStore
private let tsAccountManager: TSAccountManager
@ -48,7 +47,6 @@ class ProvisioningCoordinatorImpl: ProvisioningCoordinator {
signalService: OWSSignalServiceProtocol,
storageServiceManager: StorageServiceManager,
svr: SecureValueRecovery,
svrLocalStorage: SVRLocalStorage,
syncManager: SyncManagerProtocol,
threadStore: ThreadStore,
tsAccountManager: TSAccountManager,
@ -70,7 +68,6 @@ class ProvisioningCoordinatorImpl: ProvisioningCoordinator {
self.signalService = signalService
self.storageServiceManager = storageServiceManager
self.svr = svr
self.svrLocalStorage = svrLocalStorage
self.syncManager = syncManager
self.threadStore = threadStore
self.tsAccountManager = tsAccountManager
@ -362,11 +359,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,
@ -393,6 +402,7 @@ class ProvisioningCoordinatorImpl: ProvisioningCoordinator {
userProfileWriter: .linking,
transaction: tx,
)
self.svr.clearKeys(transaction: tx)
// reset to default (false)
self.receiptManager.setAreReadReceiptsEnabled(
@ -470,7 +480,7 @@ class ProvisioningCoordinatorImpl: ProvisioningCoordinator {
didLinkNSync: Bool,
) async throws(CompleteProvisioningError) {
let hasBackedUpMasterKey = self.db.read { tx in
self.svrLocalStorage.isMasterKeyBackedUp(tx: tx)
self.svr.hasBackedUpMasterKey(transaction: tx)
}
let capabilities = AccountAttributes.Capabilities(hasSVRBackups: hasBackedUpMasterKey)
do {
@ -693,7 +703,7 @@ class ProvisioningCoordinatorImpl: ProvisioningCoordinator {
let phoneNumberDiscoverability = tsAccountManager.phoneNumberDiscoverability(tx: tx)
let hasSVRBackups = svrLocalStorage.isMasterKeyBackedUp(tx: tx)
let hasSVRBackups = svr.hasBackedUpMasterKey(transaction: tx)
return AccountAttributes(
isManualMessageFetchEnabled: isManualMessageFetchEnabled,

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

@ -51,7 +51,6 @@ class ProvisioningController: NSObject {
signalService: SSKEnvironment.shared.signalServiceRef,
storageServiceManager: SSKEnvironment.shared.storageServiceManagerRef,
svr: DependenciesBridge.shared.svr,
svrLocalStorage: DependenciesBridge.shared.svrLocalStorage,
syncManager: SSKEnvironment.shared.syncManagerRef,
threadStore: ThreadStoreImpl(),
tsAccountManager: DependenciesBridge.shared.tsAccountManager,
@ -148,11 +147,7 @@ class ProvisioningController: NSObject {
@objc
@MainActor
private func submitLogs() {
guard let viewController = CurrentAppContext().frontmostViewController() else {
return
}
let logs = DebugLogs(dumper: .fromGlobals())
logs.promptToSubmitLogs(from: viewController, supportTag: "Onboarding")
DebugLogs.submitLogs(supportTag: "Onboarding", dumper: .fromGlobals())
}
// MARK: - Transitions

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

@ -41,7 +41,6 @@ public struct RegistrationCoordinatorDependencies {
public let signalService: OWSSignalServiceProtocol
public let storageServiceManager: RegistrationCoordinatorImpl.Shims.StorageServiceManager
public let svr: SecureValueRecovery
public let svrLocalStorage: SVRLocalStorage
public let svrAuthCredentialStore: SVRAuthCredentialStorage
public let timeoutProvider: RegistrationCoordinatorImpl.Shims.TimeoutProvider
public let tsAccountManager: TSAccountManager
@ -89,7 +88,6 @@ public struct RegistrationCoordinatorDependencies {
signalService: SSKEnvironment.shared.signalServiceRef,
storageServiceManager: RegistrationCoordinatorImpl.Wrappers.StorageServiceManager(SSKEnvironment.shared.storageServiceManagerRef),
svr: DependenciesBridge.shared.svr,
svrLocalStorage: DependenciesBridge.shared.svrLocalStorage,
svrAuthCredentialStore: DependenciesBridge.shared.svrCredentialStorage,
timeoutProvider: RegistrationCoordinatorImpl.Wrappers.TimeoutProvider(),
tsAccountManager: DependenciesBridge.shared.tsAccountManager,

View File

@ -424,6 +424,7 @@ public class RegistrationCoordinatorImpl: RegistrationCoordinator {
case .changingNumber:
break
case .registering, .reRegistering:
deps.svr.clearKeys(transaction: tx)
deps.ows2FAManager.clearLocalPinCode(tx)
}
}
@ -484,6 +485,9 @@ public class RegistrationCoordinatorImpl: RegistrationCoordinator {
case .changingNumber:
break
case .registering, .reRegistering:
// Whenever we do this, wipe the keys we've got.
// We don't want to have them and use then implicitly later.
deps.svr.clearKeys(transaction: tx)
deps.ows2FAManager.clearLocalPinCode(tx)
}
}
@ -520,6 +524,9 @@ public class RegistrationCoordinatorImpl: RegistrationCoordinator {
case .changingNumber:
break
case .registering, .reRegistering:
// Whenever we do this, wipe the keys we've got.
// We don't want to have them and use them implicitly later.
deps.svr.clearKeys(transaction: tx)
deps.ows2FAManager.clearLocalPinCode(tx)
}
}
@ -881,7 +888,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 +1276,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 +1438,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.
@ -2026,6 +2043,9 @@ public class RegistrationCoordinatorImpl: RegistrationCoordinator {
// Its possible we tried svr2 and kbs has the right info, or vice versa, but this is all
// best effort anyway; just fall back to session-based registration.
deps.svrAuthCredentialStore.removeSVR2CredentialsForCurrentUser(tx)
// Clear the SVR master key locally; we failed reglock so we know its wrong
// and useless anyway.
deps.svr.clearKeys(transaction: tx)
deps.ows2FAManager.clearLocalPinCode(tx)
self.updatePersistedState(tx) {
$0.e164WithKnownReglockEnabled = e164
@ -2165,7 +2185,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
}
}
@ -2303,7 +2323,7 @@ public class RegistrationCoordinatorImpl: RegistrationCoordinator {
// If we have a local master key, theres no need to restore after registration.
// (we will still back up though)
inMemoryState.shouldRestoreSVRMasterKeyAfterRegistration = localMasterKey == nil
inMemoryState.didHaveSVRBackupsPriorToReg = deps.svrLocalStorage.isMasterKeyBackedUp(tx: tx)
inMemoryState.didHaveSVRBackupsPriorToReg = deps.svr.hasBackedUpMasterKey(transaction: tx)
}
// MARK: - SVR Auth Credential Candidates Pathway
@ -2720,7 +2740,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.
@ -3524,13 +3544,13 @@ public class RegistrationCoordinatorImpl: RegistrationCoordinator {
let accountEntropyPool = getOrGenerateAccountEntropyPool()
if
let nextStep = await performSVRBackupStepsIfNeeded(
let backupStepGuarantee = await performSVRBackupStepsIfNeeded(
resetPINReminderInterval: false,
accountEntropyPool: accountEntropyPool,
accountIdentity: accountIdentity,
)
{
return nextStep
return backupStepGuarantee
}
return await exportAndWipeState(
@ -3898,7 +3918,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 {
@ -4017,10 +4037,9 @@ public class RegistrationCoordinatorImpl: RegistrationCoordinator {
let masterKey = accountEntropyPool.getMasterKey()
do {
try await deps.svr.backupMasterKey(
let backedUpMasterKey = try await deps.svr.backupMasterKey(
pin: pin,
masterKey: masterKey,
force: true,
authMethod: authMethod,
)
@ -4028,7 +4047,7 @@ public class RegistrationCoordinatorImpl: RegistrationCoordinator {
await db.awaitableWrite { tx in
logger.info("Setting pin code after SVR backup")
updateMasterKeyAndLocalState(
masterKey: masterKey,
masterKey: backedUpMasterKey,
tx: tx,
)
deps.ows2FAManager.markPinEnabled(
@ -4370,7 +4389,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)
@ -4761,7 +4780,10 @@ public class RegistrationCoordinatorImpl: RegistrationCoordinator {
private func reglockToken(for e164: E164) -> String? {
if
inMemoryState.wasReglockEnabledBeforeStarting || persistedState.e164WithKnownReglockEnabled == e164,
inMemoryState.wasReglockEnabledBeforeStarting
|| persistedState.e164WithKnownReglockEnabled == e164
,
let reglockToken = inMemoryState.reglockToken
{
return reglockToken

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

@ -32,9 +32,6 @@ public class RegistrationNavigationController: OWSNavigationController {
if #available(iOS 26.0, *) {
interactiveContentPopGestureRecognizer?.isEnabled = false
}
if #unavailable(iOS 26) {
navigationBar.tintColor = .Signal.accent
}
}
override public func viewWillAppear(_ animated: Bool) {
@ -65,7 +62,7 @@ public class RegistrationNavigationController: OWSNavigationController {
return
}
if let loadingMode, !step.isSealed {
if let loadingMode, step.isSealed.negated {
logger.info("Pushing loading controller")
isLoading = true
@ -500,8 +497,7 @@ public class RegistrationNavigationController: OWSNavigationController {
))
self.present(navVc, animated: true)
} else {
let logs = DebugLogs(dumper: .fromGlobals())
logs.promptToSubmitLogs(from: self, supportTag: "Registration")
DebugLogs.submitLogs(supportTag: "Registration", dumper: .fromGlobals())
}
}
}
@ -624,6 +620,17 @@ extension RegistrationNavigationController: RegistrationPinPresenter {
func submitWithCreateNewPinInstead() {
pushNextController(coordinator.skipAndCreateNewPINCode())
}
func enterRecoveryKey() {
pushNextController(
.value(.enterRecoveryKey(
RegistrationEnterAccountEntropyPoolState(
canShowBackButton: true,
canShowNoKeyHelpButton: false,
),
)),
)
}
}
extension RegistrationNavigationController: RegistrationPinAttemptsExhaustedAndMustCreateNewPinPresenter {

View File

@ -129,6 +129,16 @@ class RegistrationPhoneNumberViewController: OWSViewController {
// MARK: UI
private lazy var contextButton: ContextMenuButton = {
let result = ContextMenuButton(empty: ())
result.setImage(Theme.iconImage(.buttonMore), for: .normal)
if #unavailable(iOS 26) {
result.tintColor = .Signal.accent
}
result.autoSetDimensions(to: .square(40))
return result
}()
private lazy var titleLabel: UILabel = {
let result = UILabel.titleLabelForRegistration(text: OWSLocalizedString(
"REGISTRATION_PHONE_NUMBER_TITLE",
@ -168,13 +178,21 @@ class RegistrationPhoneNumberViewController: OWSViewController {
view.backgroundColor = .Signal.background
navigationItem.rightBarButtonItem = UIBarButtonItem(
title: CommonStrings.nextButton,
style: .done,
target: self,
action: #selector(didTapNext),
accessibilityIdentifier: "registration.phonenumber.nextButton",
navigationItem.leftBarButtonItem = UIBarButtonItem(
customView: contextButton,
accessibilityIdentifier: "registration.verificationCode.contextButton",
)
navigationItem.rightBarButtonItem = {
let barButtonItem = UIBarButtonItem(
title: CommonStrings.nextButton,
style: .done,
target: self,
action: #selector(didTapNext),
accessibilityIdentifier: "registration.phonenumber.nextButton",
)
barButtonItem.tintColor = .Signal.accent
return barButtonItem
}()
let stackView = addStaticContentStackView(
arrangedSubviews: [
@ -258,8 +276,7 @@ class RegistrationPhoneNumberViewController: OWSViewController {
},
))
}
navigationItem.leftBarButtonItem = .contextMenuButton(actions: actions)
contextButton.setActions(actions: actions)
let now = Date()

View File

@ -89,6 +89,8 @@ protocol RegistrationPinPresenter: AnyObject {
func submitWithCreateNewPinInstead()
func exitRegistration()
func enterRecoveryKey()
}
// MARK: - RegistrationPinViewController
@ -144,6 +146,21 @@ class RegistrationPinViewController: OWSViewController {
// MARK: Rendering
private lazy var moreButton: ContextMenuButton = {
let result = ContextMenuButton(empty: ())
result.setImage(Theme.iconImage(.buttonMore), for: .normal)
if #unavailable(iOS 26) {
result.tintColor = .Signal.accent
}
result.autoSetDimensions(to: .square(40))
return result
}()
private lazy var moreBarButton = UIBarButtonItem(
customView: moreButton,
accessibilityIdentifier: "registration.pin.disablePinButton",
)
private lazy var backButton: UIButton = {
let result = UIButton()
result.setTemplateImage(
@ -313,15 +330,19 @@ class RegistrationPinViewController: OWSViewController {
super.viewDidLoad()
view.backgroundColor = .Signal.background
navigationItem.rightBarButtonItem = UIBarButtonItem(
title: CommonStrings.nextButton,
style: .done,
target: self,
action: #selector(didTapNext),
accessibilityIdentifier: "registration.pin.nextButton",
)
navigationItem.rightBarButtonItem = {
let barButtonItem = UIBarButtonItem(
title: CommonStrings.nextButton,
style: .done,
target: self,
action: #selector(didTapNext),
accessibilityIdentifier: "registration.pin.nextButton",
)
barButtonItem.tintColor = .Signal.accent
return barButtonItem
}()
stackView = addStaticContentStackView(
self.stackView = addStaticContentStackView(
arrangedSubviews: [titleLabel, explanationView, pinTextField],
isScrollable: true,
shouldAvoidKeyboard: true,
@ -378,7 +399,9 @@ class RegistrationPinViewController: OWSViewController {
}
private func configureUIForCreatingNewPin() {
navigationItem.leftBarButtonItem = .contextMenuButton(actions: [
navigationItem.leftBarButtonItem = moreBarButton
moreButton.setActions(actions: [
UIAction(
title: OWSLocalizedString(
"PIN_CREATION_LEARN_MORE",
@ -439,7 +462,9 @@ class RegistrationPinViewController: OWSViewController {
skippability: RegistrationPinState.Skippability,
remainingAttempts: UInt?,
) {
var actions = [UIAction]()
navigationItem.leftBarButtonItem = moreBarButton
var actions = [UIMenuElement]()
if skippability.canSkip {
actions.append(UIAction(
title: OWSLocalizedString(
@ -452,11 +477,23 @@ 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)
}
navigationItem.leftBarButtonItem = .contextMenuButton(actions: actions)
moreButton.setActions(actions: actions)
showAttemptWarningIfNecessary(
remainingAttempts: remainingAttempts,
@ -668,6 +705,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(

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